feat(render): Campaign V slice V0 — pin the Vulkan-shaped RHI contract

Campaign V migrates the renderer from OpenGL 4.3+extensions to a single
Vulkan 1.3 backend on Windows x64 and Linux x64, then deletes the GL path.
Motivation is compatibility and efficiency, not rescue: mandatory
GL_ARB_bindless_texture is the exact floor that parked Slice L (Mesa
D3D12/llvmpipe lack it) while Vulkan descriptor indexing is core, and
per-frame data can be written straight into mapped memory rather than
copied through BufferSubData.

V0 pins the contract every later slice codes against. Nothing consumes it
yet, so this commit changes no runtime behavior.

The seam is a minimal Vulkan-shaped RHI implemented FIRST on GL. That
ordering is the point: the twelve renderers then port one at a time under a
strict pixel gate on the still-shipping backend, so a divergence is
attributed to one slice instead of surfacing at a big-bang integration.
Duplicating renderers per backend was rejected because WbDrawDispatcher is
4,449 lines holding only ~62 GL call sites — the API surface is small and
the retail-fidelity CPU logic is large, and forking the latter is how subtle
regressions enter.

Contract highlights:
  - GpuBindingModel pins set/binding numbers dual-legal for GL and Vulkan
    GLSL. Storage bindings 0-8 keep today's shader numbering; UBOs move to
    their own set, which resolves the binding=1 collision GL only tolerates
    because it keeps SSBO and UBO tables separate.
  - GpuRingAllocation is a ref struct replacing every per-frame
    BufferSubData; the compiler forbids outliving the owning frame.
  - GpuTextureSlot replaces bindless handles. Unassigned is a loud
    uint.MaxValue sentinel rather than a silent resolve to slot 0 — the
    failure mode behind the magenta 1x1 UI placeholder bug. Renderers
    needing a fallback take the device's really-registered default slot.
  - Renderers always speak GL winding/viewport conventions; the Vulkan
    backend compensates with a negative viewport height in exactly one
    mapping function.

Verified while writing the plan: acdream's cameras already build
[0,1]-NDC projections (PortalProjection.cs:12-13), which is Vulkan's
convention. No projection rework is needed and depth precision improves,
at the cost of shifted z-fight patterns — the one pre-approved divergence
class, registered per instance at V7.

Gate: Release build green; App suite 3,785 passed / 3 skipped (3,763
baseline plus 22 new contract tests). Note for later slices, recorded in
the plan: run the suite in Release. LandblockBuildOriginTests'
far-strip test asserts behavior that LandblockStreamer.cs:505 deliberately
turns into a loud Debug.Assert in Debug builds, so a Debug run shows one
pre-existing failure that is not a regression.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-27 14:26:26 +02:00
parent f6275f4501
commit 621b16364b
17 changed files with 2577 additions and 0 deletions

View file

@ -0,0 +1,79 @@
namespace AcDream.App.Rendering.Gpu;
/// <summary>
/// Records draw work inside one <see cref="GpuPassDescription"/>. Disposing the
/// encoder closes the pass.
///
/// The surface is deliberately small: it is exactly what acdream's twelve
/// renderers do, expressed the way Vulkan wants it. Everything that Vulkan bakes
/// into a pipeline (blend, depth compare, alpha-to-coverage, topology) is absent
/// here by design — those live in <see cref="GpuPipelineDescription"/>. Only the
/// state core Vulkan 1.3 makes dynamic is settable per draw.
/// </summary>
internal interface IGpuPassEncoder : IDisposable
{
/// <summary>The pass this encoder is recording into.</summary>
GpuPassDescription Pass { get; }
/// <summary>Binds the shader program and all baked fixed state.</summary>
void BindPipeline(IGpuPipeline pipeline);
/// <summary>
/// Binds a storage buffer range to a <see cref="GpuBindingModel"/> storage
/// binding. Ranges come straight from <see cref="GpuRingAllocation"/> for
/// per-frame data, or from a long-lived buffer for persistent data.
/// </summary>
void BindStorageBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes);
/// <summary>Binds a uniform buffer range — currently only the SceneLighting block.</summary>
void BindUniformBuffer(uint binding, IGpuBuffer buffer, uint offsetBytes, uint sizeBytes);
/// <summary>Binds the interleaved vertex source matching the pipeline's vertex layout.</summary>
void BindVertexBuffer(IGpuBuffer buffer, uint offsetBytes);
/// <summary>Binds the index source.</summary>
void BindIndexBuffer(IGpuBuffer buffer, uint offsetBytes, GpuIndexType indexType);
/// <summary>Writes the shared push-constant block. Survives pipeline changes within a pass.</summary>
void SetPushConstants(in GpuPushConstants constants);
/// <summary>
/// Sets the drawable rectangle. Callers always pass GL-convention coordinates
/// (origin bottom-left); the Vulkan backend converts by emitting a negative
/// viewport height, so no renderer performs a Y flip itself.
/// </summary>
void SetViewport(int x, int y, int width, int height);
/// <summary>Sets the scissor rectangle in the same convention as <see cref="SetViewport"/>.</summary>
void SetScissor(int x, int y, int width, int height);
/// <summary>Dynamic cull override — how the world dispatcher draws double-sided geometry.</summary>
void SetCullMode(GpuCullMode cullMode);
/// <summary>Dynamic winding override, in GL convention. The Vulkan backend applies its own inversion.</summary>
void SetFrontFace(GpuFrontFace frontFace);
/// <summary>Dynamic depth-write override — how the translucent pass stops occluding later draws.</summary>
void SetDepthWrite(bool enabled);
/// <summary>Draws indexed geometry directly, without an indirect buffer.</summary>
void DrawIndexed(uint indexCount, uint instanceCount, uint firstIndex, int vertexOffset, uint firstInstance);
/// <summary>Draws non-indexed geometry — the retained UI's batched sprite/glyph quads.</summary>
void Draw(uint vertexCount, uint instanceCount, uint firstVertex, uint firstInstance);
/// <summary>
/// The production draw call: one submission covering <paramref name="drawCount"/>
/// commands read from <paramref name="commands"/>. Each command's draw index is
/// visible to the shader as <c>gl_DrawID</c>, offset by
/// <see cref="GpuPushConstants.DrawIdOffset"/>.
/// </summary>
void MultiDrawIndexedIndirect(IGpuBuffer commands, uint offsetBytes, uint drawCount, uint strideBytes);
/// <summary>
/// Opens a GPU timing scope whose result becomes readable through
/// <see cref="IGpuTimerPool.TryResolve"/> once this frame retires. Returns a
/// no-op disposable when the backend cannot measure GPU time.
/// </summary>
IDisposable BeginTimerScope(string scopeName);
}