feat(render): Campaign V slice V2a - mesh path texture-index migration
Moves the mesh/EnvCell draw path's per-batch texture representation from a 64-bit ARB_bindless_texture handle to a small integer table index, entirely on the still-shipping GL backend, with zero pixel change. This is the CPU-side half of the eventual Vulkan descriptor-array indexing model: a table index is the backend-neutral form (Vulkan indexes a descriptor array with it directly), while a raw bindless handle is GL-only. Landing the data-model change now, on GL, under a strict self-differential pixel gate, keeps it separate from V4c's much larger RHI-plumbing change (see docs/plans/2026-07-27-vulkan-campaign.md section 5.2 for why the table cannot be device-owned yet). Mechanism: mesh_modern.vert's BatchData struct carries `textureIndex` (a slot) instead of `textureHandle` (uvec2); the vertex shader looks the slot up in a new binding=9 storage buffer (GpuBindingModel.StorageTextureTable) and passes the reconstructed uvec2 handle to the fragment shader exactly as before, so mesh_modern.frag needed no change at all beyond the UBO-set macro below. The 16-byte std430 stride is unchanged (GpuBindingModel.GpuBatchDataStrideBytes); textureLayer/flags keep their offsets, so every existing CPU writer's layout is untouched. The handle->slot table (GlBindlessHandleTable, new, pure C#) is owned separately by WbDrawDispatcher and EnvCellRenderer rather than shared through a single TextureCache-owned instance: EnvCellRenderer never had a TextureCache dependency, and nothing requires index agreement between renderers since each rebinds its own binding=9 buffer immediately before its own draw call. This avoided threading a new constructor parameter through EnvCellRenderer (and its six test call sites) for no behavioral benefit. TextureCache and CompositeTextureArrayCache turned out to need no changes at all: they only ever produce raw ulong handles, and that production path is unaffected - the new indirection is entirely a WbDrawDispatcher/EnvCellRenderer-side concern, added exactly where each already assembles its per-batch GPU struct (ToInput, the copy-back loop, PrepareDeferredAlphaDraws for the RetailAlphaQueue path, and EnvCellRenderer's ModernBatchData construction). The table itself is a single non-ring buffer (unlike the per-frame triple-buffered SSBOs) because a genuinely new handle is rare - new dat surfaces/composite overrides, not every frame - so it flushes only when GlBindlessHandleTable.Dirty is set, mirroring how the existing texture caches already upload infrequently. Shader-side, introduced Rendering/Shaders/common.glsl as the shared preamble GL has no #include for: Shader.cs gained an `includeCommonPreamble` overload that splices the file's text in after the leading #version/#extension block (GLSL requires #version first). It declares the binding=9 table plus the ACDREAM_TEXTURE_HANDLE(idx) lookup macro, and a scaffolding ACDREAM_UBO_SET macro (a no-op under GL today, redefined to `set = 1,` when the Vulkan toolchain compiles this same source at V6+, per the campaign doc's set-1 UBO note) applied to both SceneLighting UBO declarations now so no later slice needs to touch them again. Tests: WbDrawDispatcherIndirectBuilderTests updated for the renamed IndirectGroupInput/BatchDataPublic fields; new ModernBatchDataLayoutTests (mirrors ClipFrameLayoutTests' role, but for EnvCellRenderer's GPU struct) and GlBindlessHandleTableTests (pure-CPU allocator behavior, including the zero-handle case, which is registered like any other handle rather than special-cased, since that's what reproduces the pre-V2 sampling result bit-for-bit). Gate: dotnet build -c Release green, dotnet test tests/AcDream.App.Tests -c Release green (3843 passed / 3 skipped, +9 over the 3834/3 baseline), and tools/run-offline-pixel-gate.ps1 passed with a 2.84e-05 differing-pixel fraction against the parent commit - within the documented ~33x same-commit noise margin. No divergence-register row: this introduces no retail behavior deviation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8b57ba7167
commit
d365476ebb
12 changed files with 550 additions and 49 deletions
|
|
@ -153,6 +153,19 @@ public sealed unsafe class EnvCellRenderer :
|
|||
private uint _sharedClipRegionSsbo;
|
||||
private uint _fallbackClipRegionSsbo;
|
||||
|
||||
// Campaign V slice V2 (2026-07-27): GL-only emulation of the eventual
|
||||
// Vulkan global texture descriptor array (binding=9,
|
||||
// GpuBindingModel.StorageTextureTable). Owns its own table rather than
|
||||
// sharing WbDrawDispatcher's — EnvCellRenderer never had a TextureCache
|
||||
// dependency and nothing requires index agreement between renderers (each
|
||||
// rebinds its own buffer to binding=9 immediately before its own draw
|
||||
// call). See GlBindlessHandleTable's doc comment and the campaign doc's
|
||||
// §5.2. Lazily created; grown/uploaded only when a genuinely new handle
|
||||
// appears (rare — see FlushAndBindTextureTable).
|
||||
private readonly GlBindlessHandleTable _textureTable = new();
|
||||
private uint _textureTableSsbo;
|
||||
private int _textureTableSsboCapacityBytes;
|
||||
|
||||
// Reusable scratch arrays — avoid per-frame allocation.
|
||||
// WB BaseObjectRenderManager.cs:58-59: private DrawElementsIndirectCommand[] _commands = Array.Empty<...>()
|
||||
private DrawElementsIndirectCommand[] _commands = Array.Empty<DrawElementsIndirectCommand>();
|
||||
|
|
@ -1611,8 +1624,11 @@ public sealed unsafe class EnvCellRenderer :
|
|||
{
|
||||
_modernBatches[cmdIndex] = new ModernBatchData
|
||||
{
|
||||
TextureHandle = item.batch.BindlessTextureHandle,
|
||||
TextureIndex = (uint)item.batch.TextureIndex,
|
||||
// Campaign V slice V2: table slot, not the raw handle.
|
||||
// See _textureTable's doc comment for why EnvCellRenderer
|
||||
// owns its own table rather than sharing WbDrawDispatcher's.
|
||||
TextureTableIndex = _textureTable.GetOrAdd(item.batch.BindlessTextureHandle),
|
||||
TextureIndex = (uint)item.batch.TextureIndex,
|
||||
};
|
||||
|
||||
_commands[cmdIndex] = new DrawElementsIndirectCommand
|
||||
|
|
@ -1766,6 +1782,7 @@ public sealed unsafe class EnvCellRenderer :
|
|||
BindClipRegionBinding2();
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 4, _globalLightsSsbo); // A7 Fix D (D-2)
|
||||
_gl.BindBufferBase(GLEnum.ShaderStorageBuffer, 5, _instLightSetSsbo); // A7 Fix D (D-2)
|
||||
FlushAndBindTextureTable(); // Campaign V slice V2 (binding=9)
|
||||
_gl.BindBuffer(GLEnum.DrawIndirectBuffer, _mdiCommandBuffer);
|
||||
|
||||
_gl.MemoryBarrier(MemoryBarrierMask.ShaderStorageBarrierBit | MemoryBarrierMask.CommandBarrierBit);
|
||||
|
|
@ -1970,6 +1987,53 @@ public sealed unsafe class EnvCellRenderer :
|
|||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// FlushAndBindTextureTable (Campaign V slice V2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Uploads <see cref="_textureTable"/>'s handles to <see cref="_textureTableSsbo"/>
|
||||
/// when a new one was registered since the last flush, then (re)binds it at
|
||||
/// <see cref="AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable"/>.
|
||||
/// A genuinely new handle is rare — new dat surfaces/atlases, not every
|
||||
/// frame — so this is not part of the ring-buffered per-frame SSBO set;
|
||||
/// see GlBindlessHandleTable's doc comment.
|
||||
/// </summary>
|
||||
private void FlushAndBindTextureTable()
|
||||
{
|
||||
if (_textureTableSsbo == 0)
|
||||
_textureTableSsbo = TrackedGlResource.CreateBuffer(_gl, "creating EnvCell texture-table SSBO");
|
||||
|
||||
if (_textureTable.Dirty)
|
||||
{
|
||||
ReadOnlySpan<ulong> handles = _textureTable.Handles;
|
||||
int byteCount = handles.Length * sizeof(ulong);
|
||||
fixed (ulong* p = handles)
|
||||
{
|
||||
if (_textureTableSsboCapacityBytes < byteCount)
|
||||
{
|
||||
int grown = DynamicBufferCapacity.Grow(_textureTableSsboCapacityBytes, byteCount);
|
||||
TrackedGlResource.AllocateBufferStorage(
|
||||
_gl,
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
_textureTableSsbo,
|
||||
_textureTableSsboCapacityBytes,
|
||||
grown,
|
||||
GLEnum.DynamicDraw,
|
||||
"growing EnvCell texture-table SSBO");
|
||||
_textureTableSsboCapacityBytes = grown;
|
||||
}
|
||||
_gl.BindBuffer(GLEnum.ShaderStorageBuffer, _textureTableSsbo);
|
||||
_gl.BufferSubData(GLEnum.ShaderStorageBuffer, 0, (nuint)byteCount, p);
|
||||
}
|
||||
_textureTable.MarkFlushed();
|
||||
}
|
||||
_gl.BindBufferBase(
|
||||
GLEnum.ShaderStorageBuffer,
|
||||
AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
|
||||
_textureTableSsbo);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// BindClipRegionBinding2 (Phase U.3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -2138,6 +2202,12 @@ public sealed unsafe class EnvCellRenderer :
|
|||
AcDream.App.Rendering.ClipFrame.CellClipStrideBytes,
|
||||
"fallback-clip-region",
|
||||
"deleting EnvCell fallback clip SSBO");
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
_textureTableSsbo,
|
||||
_textureTableSsboCapacityBytes,
|
||||
"texture-table",
|
||||
"deleting EnvCell texture-table SSBO");
|
||||
_disposeResources = new RetryableResourceReleaseLedger(releases);
|
||||
}
|
||||
|
||||
|
|
@ -2159,6 +2229,8 @@ public sealed unsafe class EnvCellRenderer :
|
|||
_globalLightsSsbo = 0;
|
||||
_instLightSetSsbo = 0;
|
||||
_fallbackClipRegionSsbo = 0;
|
||||
_textureTableSsbo = 0;
|
||||
_textureTableSsboCapacityBytes = 0;
|
||||
_disposeResources = null;
|
||||
IsDisposed = true;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue