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
|
|
@ -493,6 +493,17 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
private uint _instSelectionLightingSsbo;
|
||||
private int _instSelectionLightingSsboCapacityBytes;
|
||||
|
||||
// Campaign V slice V2 (2026-07-27): GL-only emulation of the eventual
|
||||
// Vulkan global texture descriptor array (binding=9,
|
||||
// GpuBindingModel.StorageTextureTable). A genuinely new handle is rare —
|
||||
// new dat surfaces/atlases, not every frame — so this single buffer is
|
||||
// NOT part of the ring-buffered DynamicBufferSet below; see
|
||||
// GlBindlessHandleTable's doc comment and the campaign doc's §5.2 for why
|
||||
// this table is owned here rather than by GlGpuDevice. Lazily created.
|
||||
private readonly GlBindlessHandleTable _textureTable = new();
|
||||
private uint _textureTableSsbo;
|
||||
private int _textureTableSsboCapacityBytes;
|
||||
|
||||
private sealed class DynamicBufferSet
|
||||
{
|
||||
public uint InstanceSsbo;
|
||||
|
|
@ -587,18 +598,22 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
private int _transparentDrawCount;
|
||||
private int _transparentByteOffset;
|
||||
|
||||
// std430 layout: ulong TextureHandle (uvec2) at offset 0, uint TextureLayer
|
||||
// at offset 8, uint Flags at offset 12. Total 16 bytes.
|
||||
// Pack=8 (not 4) because std430's uvec2 requires 8-byte alignment — Pack=4
|
||||
// works today by accident (TextureHandle is the first field, so offset 0 is
|
||||
// always 8-byte aligned), but adding a 4-byte field before TextureHandle
|
||||
// without bumping Pack would silently misalign the GPU struct.
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 8)]
|
||||
// Campaign V slice V2 (2026-07-27): std430 layout: uint TextureIndex at
|
||||
// offset 0, uint Reserved (pad) at offset 4, uint TextureLayer at offset 8,
|
||||
// uint Flags at offset 12. Total 16 bytes — unchanged from before V2, so
|
||||
// every existing CPU writer's offsets are unchanged (see
|
||||
// GpuBindingModel.GpuBatchDataStrideBytes). TextureIndex used to be a
|
||||
// 64-bit ulong TextureHandle (an ARB_bindless_texture handle, uvec2 in
|
||||
// GLSL); it is now a slot into the binding=9 handle table
|
||||
// (mesh_modern.vert's BatchData.textureIndex / ACDREAM_TEXTURE_HANDLE),
|
||||
// which is why the struct only needs 4-byte (not 8-byte) packing now.
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
private struct BatchData
|
||||
{
|
||||
public ulong TextureHandle; // bindless handle (uvec2 in GLSL)
|
||||
public uint TextureLayer;
|
||||
public uint Flags;
|
||||
public uint TextureIndex; // slot into the binding=9 handle table
|
||||
public uint Reserved; // pad — keeps TextureLayer/Flags at offsets 8/12
|
||||
public uint TextureLayer;
|
||||
public uint Flags;
|
||||
}
|
||||
|
||||
private readonly record struct DeferredAlphaInstance(
|
||||
|
|
@ -2221,9 +2236,9 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
{
|
||||
_batchData[i] = new BatchData
|
||||
{
|
||||
TextureHandle = _batchPublicScratch[i].TextureHandle,
|
||||
TextureLayer = _batchPublicScratch[i].TextureLayer,
|
||||
Flags = _batchPublicScratch[i].Flags,
|
||||
TextureIndex = _batchPublicScratch[i].TextureIndex,
|
||||
TextureLayer = _batchPublicScratch[i].TextureLayer,
|
||||
Flags = _batchPublicScratch[i].Flags,
|
||||
};
|
||||
}
|
||||
_opaqueDrawCount = layout.OpaqueCount;
|
||||
|
|
@ -2298,6 +2313,10 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
UploadSsbo(_instLightSetSsbo, 5, ref _instLightSetSsboCapacityBytes,
|
||||
lp, immediateInstances * LightManager.MaxLightsPerObject * sizeof(int));
|
||||
|
||||
// Campaign V slice V2 (binding=9): uploads only when ToInput registered
|
||||
// a genuinely new handle this frame; otherwise just rebinds.
|
||||
FlushAndBindTextureTable();
|
||||
|
||||
fixed (DrawElementsIndirectCommand* cp = _indirectCommands)
|
||||
{
|
||||
UploadDynamicBuffer(
|
||||
|
|
@ -2502,13 +2521,15 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
private static IndirectGroupInput ToInput(InstanceGroup g) => new(
|
||||
// Campaign V slice V2: instance method (not static) because it converts
|
||||
// the group's raw bindless handle to a _textureTable slot.
|
||||
private IndirectGroupInput ToInput(InstanceGroup g) => new(
|
||||
IndexCount: g.IndexCount,
|
||||
FirstIndex: g.FirstIndex,
|
||||
BaseVertex: g.BaseVertex,
|
||||
InstanceCount: g.InstanceCount,
|
||||
FirstInstance: g.FirstInstance,
|
||||
TextureHandle: g.BindlessTextureHandle,
|
||||
TextureIndex: _textureTable.GetOrAdd(g.BindlessTextureHandle),
|
||||
TextureLayer: g.TextureLayer,
|
||||
Translucency: g.Translucency,
|
||||
CullMode: g.CullMode);
|
||||
|
|
@ -2961,7 +2982,8 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
GroupKey key = entry.Key;
|
||||
_batchData[i] = new BatchData
|
||||
{
|
||||
TextureHandle = key.BindlessTextureHandle,
|
||||
// Campaign V slice V2: table slot, not the raw handle.
|
||||
TextureIndex = _textureTable.GetOrAdd(key.BindlessTextureHandle),
|
||||
TextureLayer = key.TextureLayer,
|
||||
Flags = 0,
|
||||
};
|
||||
|
|
@ -3010,6 +3032,12 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
_gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 6, _instIndoorSsbo);
|
||||
_gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 7, _instAlphaSsbo);
|
||||
_gl.BindBufferBase(BufferTargetARB.ShaderStorageBuffer, 8, _instSelectionLightingSsbo);
|
||||
// Campaign V slice V2: already flushed/uploaded in UploadDeferredAlphaBuffers
|
||||
// (this is the same non-ring buffer as the main draw path); just rebind.
|
||||
_gl.BindBufferBase(
|
||||
BufferTargetARB.ShaderStorageBuffer,
|
||||
AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
|
||||
_textureTableSsbo);
|
||||
BindClipRegionBinding2();
|
||||
_gl.BindVertexArray(global.VAO);
|
||||
_gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, _indirectBuffer);
|
||||
|
|
@ -3145,6 +3173,9 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
UploadSsbo(_instSelectionLightingSsbo, 8, ref _instSelectionLightingSsboCapacityBytes,
|
||||
p, count * sizeof(float) * 2);
|
||||
UploadGlobalLights();
|
||||
// Campaign V slice V2 (binding=9): PrepareDeferredAlphaDraws registers
|
||||
// handles into _textureTable above; flush/rebind before DrawPreparedAlphaBatch.
|
||||
FlushAndBindTextureTable();
|
||||
|
||||
fixed (DrawElementsIndirectCommand* p = _indirectCommands)
|
||||
{
|
||||
|
|
@ -3425,6 +3456,42 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
count * GlobalLightPacker.FloatsPerLight * sizeof(float));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V2: uploads <see cref="_textureTable"/>'s handles to
|
||||
/// <see cref="_textureTableSsbo"/> when a new one was registered since the
|
||||
/// last flush (by <see cref="ToInput"/> or <see cref="PrepareDeferredAlphaDraws"/>),
|
||||
/// then (re)binds it at <see cref="AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable"/>.
|
||||
/// A genuinely new handle is rare — new dat surfaces/composite overrides,
|
||||
/// not every frame — so unlike the SSBOs above this is not part of the
|
||||
/// ring-buffered <see cref="DynamicBufferSet"/>; see
|
||||
/// <see cref="GlBindlessHandleTable"/>'s doc comment.
|
||||
/// </summary>
|
||||
private unsafe void FlushAndBindTextureTable()
|
||||
{
|
||||
if (_textureTableSsbo == 0)
|
||||
_textureTableSsbo = TrackedGlResource.CreateBuffer(_gl, "creating WB texture-table SSBO");
|
||||
|
||||
if (_textureTable.Dirty)
|
||||
{
|
||||
ReadOnlySpan<ulong> handles = _textureTable.Handles;
|
||||
int byteCount = handles.Length * sizeof(ulong);
|
||||
fixed (ulong* p = handles)
|
||||
{
|
||||
UploadDynamicBuffer(
|
||||
BufferTargetARB.ShaderStorageBuffer,
|
||||
_textureTableSsbo,
|
||||
ref _textureTableSsboCapacityBytes,
|
||||
p,
|
||||
byteCount);
|
||||
}
|
||||
_textureTable.MarkFlushed();
|
||||
}
|
||||
_gl.BindBufferBase(
|
||||
BufferTargetARB.ShaderStorageBuffer,
|
||||
AcDream.App.Rendering.Gpu.GpuBindingModel.StorageTextureTable,
|
||||
_textureTableSsbo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase U.3: bind the per-cell clip-region SSBO to binding=2. Prefers the
|
||||
/// shared <see cref="ClipFrame"/> buffer (set via <see cref="SetClipRegionSsbo"/>);
|
||||
|
|
@ -4089,6 +4156,13 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
"deleting entity fallback clip SSBO",
|
||||
_gl.DeleteBuffer);
|
||||
|
||||
AddTrackedBufferRelease(
|
||||
releases,
|
||||
_textureTableSsbo,
|
||||
_textureTableSsboCapacityBytes,
|
||||
"texture-table",
|
||||
"deleting entity texture-table SSBO");
|
||||
|
||||
if (!_gpuQueriesInitialized)
|
||||
return;
|
||||
for (int i = 0; i < GpuQueryRingDepth; i++)
|
||||
|
|
@ -4191,6 +4265,8 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
_instAlphaSsbo = 0;
|
||||
_instSelectionLightingSsbo = 0;
|
||||
_fallbackClipRegionSsbo = 0;
|
||||
_textureTableSsbo = 0;
|
||||
_textureTableSsboCapacityBytes = 0;
|
||||
Array.Clear(_gpuQueryOpaque);
|
||||
Array.Clear(_gpuQueryTransparent);
|
||||
_gpuQueriesInitialized = false;
|
||||
|
|
@ -4210,6 +4286,8 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
|
||||
/// <summary>
|
||||
/// Public view of the per-group inputs to <see cref="BuildIndirectArrays"/> — used in tests.
|
||||
/// Campaign V slice V2: <c>TextureIndex</c> is a slot into the binding=9
|
||||
/// handle table (was a raw 64-bit bindless <c>TextureHandle</c>).
|
||||
/// </summary>
|
||||
public readonly record struct IndirectGroupInput(
|
||||
int IndexCount,
|
||||
|
|
@ -4217,7 +4295,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
int BaseVertex,
|
||||
int InstanceCount,
|
||||
int FirstInstance,
|
||||
ulong TextureHandle,
|
||||
uint TextureIndex,
|
||||
uint TextureLayer,
|
||||
TranslucencyKind Translucency,
|
||||
CullMode CullMode = CullMode.CounterClockwise);
|
||||
|
|
@ -4226,12 +4304,13 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
/// Public mirror of the per-group <see cref="BatchData"/> uploaded to the SSBO.
|
||||
/// Tests verify the layout. Same field shape as the private BatchData.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 8)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
public struct BatchDataPublic
|
||||
{
|
||||
public ulong TextureHandle;
|
||||
public uint TextureLayer;
|
||||
public uint Flags;
|
||||
public uint TextureIndex;
|
||||
public uint Reserved;
|
||||
public uint TextureLayer;
|
||||
public uint Flags;
|
||||
}
|
||||
|
||||
/// <summary>Result of <see cref="BuildIndirectArrays"/>.</summary>
|
||||
|
|
@ -4279,9 +4358,10 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
};
|
||||
var bd = new BatchDataPublic
|
||||
{
|
||||
TextureHandle = g.TextureHandle,
|
||||
TextureLayer = g.TextureLayer,
|
||||
Flags = 0,
|
||||
TextureIndex = g.TextureIndex,
|
||||
Reserved = 0,
|
||||
TextureLayer = g.TextureLayer,
|
||||
Flags = 0,
|
||||
};
|
||||
|
||||
if (IsOpaque(g.Translucency))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue