acdream/src/AcDream.App/Rendering/Gpu/GpuBindingModel.cs
Erik 621b16364b 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>
2026-07-27 14:26:26 +02:00

129 lines
6.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

namespace AcDream.App.Rendering.Gpu;
/// <summary>
/// Campaign V (V0): the PINNED binding model shared by the CPU renderers, the
/// GLSL sources, and both RHI backends. Every number here appears in a shader
/// file; changing one is a contract change that must land in the same commit as
/// the matching shader edit.
///
/// The model is deliberately Vulkan-shaped and dual-legal:
///
/// set 0 — storage buffers, bindings 0..9. GLSL omits the set qualifier, and
/// GL_KHR_vulkan_glsl defines an omitted set as set 0, so one source
/// file compiles for both backends.
/// set 1 — uniform buffers. Vulkan has ONE binding namespace per set, while
/// GL keeps GL_SHADER_STORAGE_BUFFER and GL_UNIFORM_BUFFER tables
/// separate. Today mesh_modern.vert exploits that: the BatchBuffer
/// SSBO and the SceneLighting UBO both sit at binding=1. Moving UBOs
/// to their own set preserves both numbers and removes the collision.
/// set 2 — the global sampled-texture table that replaces ARB_bindless_texture.
/// Vulkan binds it as one variable-count, partially-bound,
/// update-after-bind descriptor array. GL emulates it with a storage
/// buffer of uvec2 handles at set 0 binding 9 (<see cref="StorageTextureTable"/>),
/// which is why both a set index and a storage binding exist here.
///
/// A batch no longer carries a 64-bit bindless handle; it carries a
/// <see cref="GpuTextureSlot"/> index into the table. That single change is what
/// makes the CPU-side data model backend-neutral (Campaign V slice V2), and it
/// lands on GL — pixel-gated — long before any Vulkan code exists.
/// </summary>
internal static class GpuBindingModel
{
// ---- set 0: storage buffers (identical numbering to today's SSBO bindings) ----
/// <summary>Per-instance transforms. std430 <c>InstanceData { mat4 transform; }</c>.</summary>
public const uint StorageInstances = 0;
/// <summary>Per-draw batch metadata. std430 <c>BatchData</c> (see <see cref="GpuBatchDataStrideBytes"/>).</summary>
public const uint StorageBatches = 1;
/// <summary>Phase U.3 shared per-frame clip regions (<c>CellClip</c>, 144 B/slot). Slot 0 = no-clip.</summary>
public const uint StorageClipRegions = 2;
/// <summary>Phase U.3 per-instance clip-slot index, parallel to <see cref="StorageInstances"/>.</summary>
public const uint StorageClipSlots = 3;
/// <summary>A7 Fix B global point/spot light array.</summary>
public const uint StorageGlobalLights = 4;
/// <summary>A7 Fix B per-instance light set: 8 indices into the global light array, -1 = unused.</summary>
public const uint StorageInstanceLightSets = 5;
/// <summary>#142 per-instance indoor flag (1 = parented to an EnvCell, skip the sun).</summary>
public const uint StorageInstanceIndoor = 6;
/// <summary>#188 per-instance opacity multiplier for TransparentPartHook fades.</summary>
public const uint StorageInstanceAlpha = 7;
/// <summary>Retail SmartBox selection lighting: one vec2 (luminosity, diffuse) per instance.</summary>
public const uint StorageInstanceSelectionLighting = 8;
/// <summary>
/// GL-only emulation of the Vulkan texture table: a storage buffer of uvec2
/// bindless handles indexed by <see cref="GpuTextureSlot.Index"/>. The Vulkan
/// backend binds <see cref="TextureTableSet"/> instead and never uses this
/// binding; it is deleted with the GL backend at slice V11.
/// </summary>
public const uint StorageTextureTable = 9;
/// <summary>One past the highest storage binding — the count both backends must support.</summary>
public const uint StorageBindingCount = 10;
// ---- set 1: uniform buffers ----
/// <summary>
/// SceneLighting std140 block. Keeps binding=1 so the existing shader source
/// and <c>SceneLightingUboBinding</c> layout are untouched; the set index is
/// what disambiguates it from <see cref="StorageBatches"/> under Vulkan.
/// </summary>
public const uint UniformSceneLighting = 1;
/// <summary>Set index carrying every uniform buffer.</summary>
public const uint UniformSet = 1;
// ---- set 2: the global texture table ----
/// <summary>Set index of the sampled-texture descriptor array (Vulkan) / logical table (GL).</summary>
public const uint TextureTableSet = 2;
/// <summary>Binding of the descriptor array within <see cref="TextureTableSet"/>.</summary>
public const uint TextureTableBinding = 0;
/// <summary>
/// Upper bound on simultaneously registered textures. Vulkan requires
/// <c>maxDescriptorSetUpdateAfterBindSampledImages</c> to reach this; the
/// capability probe asserts it rather than discovering it at draw time.
/// </summary>
public const uint TextureTableCapacity = 16384;
// ---- push constants ----
/// <summary>
/// Bytes actually written by <see cref="GpuPushConstants"/>. Vulkan guarantees
/// at least <see cref="MaxPushConstantBytes"/>, so 32 bytes of headroom remain
/// for later slices; any growth updates this constant and the shader block in
/// the same commit.
/// </summary>
public const int PushConstantBytes = 96;
/// <summary>The Vulkan-guaranteed minimum push-constant budget. A hard ceiling for us.</summary>
public const int MaxPushConstantBytes = 128;
// ---- shared layout facts the CPU writers and the shaders must agree on ----
/// <summary>
/// std430 stride of <c>BatchData</c>. The bindless <c>uvec2 textureHandle</c>
/// becomes <c>uint textureIndex</c> plus one pad word at slice V2, so the
/// stride is unchanged and every existing CPU writer keeps its offsets.
/// </summary>
public const int GpuBatchDataStrideBytes = 16;
/// <summary>Clip planes per <c>CellClip</c> slot; also the required <c>gl_ClipDistance</c> size.</summary>
public const int ClipPlanesPerSlot = 8;
/// <summary>std430 stride of one <c>CellClip</c> slot: 16 B header + 8 × vec4.</summary>
public const int ClipRegionStrideBytes = 16 + (ClipPlanesPerSlot * 16);
/// <summary>Lights selected per object by retail's <c>minimize_object_lighting</c>.</summary>
public const int MaxLightsPerObject = 8;
}