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
|
|
@ -274,7 +274,8 @@ internal sealed class RetailWorldRenderCompositionFactory
|
|||
new(
|
||||
gl,
|
||||
Path.Combine(shadersDirectory, "mesh_modern.vert"),
|
||||
Path.Combine(shadersDirectory, "mesh_modern.frag"));
|
||||
Path.Combine(shadersDirectory, "mesh_modern.frag"),
|
||||
includeCommonPreamble: true);
|
||||
|
||||
public WbMeshAdapter CreateMeshAdapter(
|
||||
GL gl,
|
||||
|
|
|
|||
62
src/AcDream.App/Rendering/GlBindlessHandleTable.cs
Normal file
62
src/AcDream.App/Rendering/GlBindlessHandleTable.cs
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V2 (docs/plans/2026-07-27-vulkan-campaign.md §3.4, §5.2):
|
||||
/// pure bookkeeping that assigns a stable small-integer "slot" to each distinct
|
||||
/// GL_ARB_bindless_texture handle a renderer submits. A batch/pass no longer
|
||||
/// carries the 64-bit handle directly into its GPU-visible struct; it carries
|
||||
/// this slot index instead, and the shader looks the handle back up from a
|
||||
/// binding=9 storage buffer (<c>GpuBindingModel.StorageTextureTable</c>) — the
|
||||
/// GL-only emulation of Vulkan's global sampled-texture descriptor array
|
||||
/// (<c>GpuBindingModel.TextureTableSet</c>). That indirection is what makes
|
||||
/// the CPU-side batch data backend-neutral.
|
||||
///
|
||||
/// Owns no GL resource. The owning renderer uploads <see cref="Handles"/> to
|
||||
/// its own storage buffer whenever <see cref="Dirty"/> is set — mirroring how
|
||||
/// it already uploads its other per-frame/per-registration SSBOs — and clears
|
||||
/// the flag with <see cref="MarkFlushed"/>. Never releases a slot: entries
|
||||
/// accumulate for the renderer's lifetime, exactly like the world atlas and
|
||||
/// composite-texture caches it draws handles from.
|
||||
///
|
||||
/// This class, plus the per-renderer SSBO it backs, is deleted at
|
||||
/// V4c/V4d/V4e when each renderer moves onto <c>IGpuDevice</c>'s own
|
||||
/// retirement-gated table — see the campaign doc's §5.2 for why V2 cannot
|
||||
/// reach that table yet.
|
||||
///
|
||||
/// Each renderer that needs the indirection (WbDrawDispatcher,
|
||||
/// EnvCellRenderer, TerrainModernRenderer, ParticleRenderer) owns its own
|
||||
/// instance. Nothing requires index agreement between renderers: each rebinds
|
||||
/// its own buffer to binding=9 immediately before its own draw call, so two
|
||||
/// renderers may legitimately assign different slots to the same handle.
|
||||
/// </summary>
|
||||
internal sealed class GlBindlessHandleTable
|
||||
{
|
||||
private readonly Dictionary<ulong, uint> _slotByHandle = new();
|
||||
private ulong[] _handles = new ulong[64];
|
||||
private int _count;
|
||||
|
||||
/// <summary>True after a <see cref="GetOrAdd"/> call registers a handle not seen before.</summary>
|
||||
public bool Dirty { get; private set; }
|
||||
|
||||
/// <summary>Live prefix of assigned handles, in slot order (array index == slot).</summary>
|
||||
public ReadOnlySpan<ulong> Handles => _handles.AsSpan(0, _count);
|
||||
|
||||
/// <summary>Returns the stable slot for a handle, registering it on first use.</summary>
|
||||
public uint GetOrAdd(ulong bindlessHandle)
|
||||
{
|
||||
if (_slotByHandle.TryGetValue(bindlessHandle, out uint slot))
|
||||
return slot;
|
||||
|
||||
if (_count == _handles.Length)
|
||||
Array.Resize(ref _handles, _handles.Length * 2);
|
||||
slot = (uint)_count;
|
||||
_handles[_count] = bindlessHandle;
|
||||
_count++;
|
||||
_slotByHandle.Add(bindlessHandle, slot);
|
||||
Dirty = true;
|
||||
return slot;
|
||||
}
|
||||
|
||||
/// <summary>Call once the owning renderer has uploaded <see cref="Handles"/> to its SSBO.</summary>
|
||||
public void MarkFlushed() => Dirty = false;
|
||||
}
|
||||
|
|
@ -10,16 +10,72 @@ public sealed class Shader : IDisposable
|
|||
public uint Program { get; private set; }
|
||||
|
||||
public Shader(GL gl, string vertexPath, string fragmentPath)
|
||||
: this(gl, vertexPath, fragmentPath, includeCommonPreamble: false)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V2 (docs/plans/2026-07-27-vulkan-campaign.md §3.4): when
|
||||
/// <paramref name="includeCommonPreamble"/> is true, the text of
|
||||
/// <c>Shaders/common.glsl</c> — sitting alongside <paramref name="vertexPath"/>
|
||||
/// — is spliced into both sources right after their leading
|
||||
/// <c>#version</c>/<c>#extension</c> block. GL has no <c>#include</c>, so this
|
||||
/// is plain string concatenation at load time rather than a GLSL-level
|
||||
/// mechanism. Every existing two-argument-path caller is unaffected: the
|
||||
/// convenience constructor above always passes false.
|
||||
/// </summary>
|
||||
public Shader(GL gl, string vertexPath, string fragmentPath, bool includeCommonPreamble)
|
||||
{
|
||||
_gl = gl;
|
||||
string vertexSource = File.ReadAllText(vertexPath);
|
||||
string fragmentSource = File.ReadAllText(fragmentPath);
|
||||
if (includeCommonPreamble)
|
||||
{
|
||||
string? directory = Path.GetDirectoryName(vertexPath);
|
||||
string commonPath = directory is null
|
||||
? "common.glsl"
|
||||
: Path.Combine(directory, "common.glsl");
|
||||
string commonSource = File.ReadAllText(commonPath);
|
||||
vertexSource = InjectPreamble(vertexSource, commonSource);
|
||||
fragmentSource = InjectPreamble(fragmentSource, commonSource);
|
||||
}
|
||||
Program = ShaderProgramConstruction.Build(
|
||||
new GlShaderProgramBuildApi(gl),
|
||||
vertexSource,
|
||||
fragmentSource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inserts <paramref name="preamble"/> right after the shader's leading
|
||||
/// <c>#version</c>/<c>#extension</c>/blank-line block. GLSL requires
|
||||
/// <c>#version</c> to be the very first statement in the source, so the
|
||||
/// preamble cannot simply be prepended — it has to land after that block,
|
||||
/// before the first real declaration.
|
||||
/// </summary>
|
||||
private static string InjectPreamble(string source, string preamble)
|
||||
{
|
||||
int insertAt = 0;
|
||||
int lineStart = 0;
|
||||
while (lineStart < source.Length)
|
||||
{
|
||||
int lineEnd = source.IndexOf('\n', lineStart);
|
||||
if (lineEnd < 0)
|
||||
lineEnd = source.Length;
|
||||
string trimmed = source[lineStart..lineEnd].TrimStart();
|
||||
bool isLeadingLine =
|
||||
trimmed.Length == 0
|
||||
|| trimmed.StartsWith("#version", StringComparison.Ordinal)
|
||||
|| trimmed.StartsWith("#extension", StringComparison.Ordinal);
|
||||
if (!isLeadingLine)
|
||||
break;
|
||||
|
||||
insertAt = lineEnd < source.Length ? lineEnd + 1 : lineEnd;
|
||||
lineStart = lineEnd + 1;
|
||||
}
|
||||
|
||||
return string.Concat(source.AsSpan(0, insertAt), preamble, "\n", source.AsSpan(insertAt));
|
||||
}
|
||||
|
||||
public void Use() => _gl.UseProgram(Program);
|
||||
|
||||
public unsafe void SetMatrix4(string name, Matrix4x4 m)
|
||||
|
|
|
|||
41
src/AcDream.App/Rendering/Shaders/common.glsl
Normal file
41
src/AcDream.App/Rendering/Shaders/common.glsl
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Campaign V slice V2 shared preamble (docs/plans/2026-07-27-vulkan-campaign.md
|
||||
// §3.4, §5.2). GL has no #include, so AcDream.App.Rendering.Shader
|
||||
// concatenates this file's text into every shader source that opts in
|
||||
// (Shader(gl, vertPath, fragPath, includeCommonPreamble: true)), inserted
|
||||
// right after the leading #version / #extension block so it can declare new
|
||||
// layout bindings and macros before the rest of the shader body runs.
|
||||
//
|
||||
// --- set 1 (uniform buffers) ------------------------------------------------
|
||||
// GL keeps the SSBO and UBO binding-number namespaces separate, so today's
|
||||
// SceneLighting UBO (binding=1) never collides with BatchBuffer's SSBO
|
||||
// (also binding=1). Vulkan has ONE binding namespace per set, so the Vulkan
|
||||
// backend (from V6 on) moves every uniform buffer to its own set (1) to keep
|
||||
// both binding numbers. ACDREAM_UBO_SET is a no-op under GL today and is
|
||||
// redefined to `set = 1,` when the same source is compiled for Vulkan, so
|
||||
// applying it to every UBO layout now costs nothing and needs no source edit
|
||||
// at the call sites later.
|
||||
#define ACDREAM_UBO_SET
|
||||
|
||||
// --- set 0 binding 9 / set 2 (the global texture table) --------------------
|
||||
// GL emulates Vulkan's set 2 variable-count sampled-texture descriptor array
|
||||
// (GpuBindingModel.TextureTableSet) with a plain storage buffer of packed
|
||||
// GL_ARB_bindless_texture handles at set 0 binding 9
|
||||
// (GpuBindingModel.StorageTextureTable). A batch/pass no longer carries a
|
||||
// 64-bit bindless handle directly into its GPU-visible struct; it carries a
|
||||
// small integer slot index into this table instead, which is what makes the
|
||||
// CPU-side data model backend-neutral (Campaign V slice V2). The table itself
|
||||
// — and every per-renderer GL-side handle-slot allocator that fills it
|
||||
// (WbDrawDispatcher, EnvCellRenderer, TerrainModernRenderer, ParticleRenderer)
|
||||
// — is deleted once each renderer moves onto IGpuDevice's own retirement-gated
|
||||
// table at V4c/V4d/V4e; see the campaign doc's §5.2 for why V2 cannot reach
|
||||
// that table yet.
|
||||
layout(std430, binding = 9) readonly buffer TextureTableBuf {
|
||||
uvec2 gTextureTable[];
|
||||
};
|
||||
|
||||
// Looks up the packed bindless handle for table slot `idx`. Callers still
|
||||
// wrap the result in `sampler2DArray(...)` themselves at the use site (kept
|
||||
// explicit rather than folded into one sampler-returning macro) because every
|
||||
// existing call site already follows that exact pattern and a function cannot
|
||||
// return an opaque sampler type built from a runtime value in GLSL.
|
||||
#define ACDREAM_TEXTURE_HANDLE(idx) gTextureTable[idx]
|
||||
|
|
@ -27,7 +27,7 @@ struct Light {
|
|||
vec4 colorAndIntensity;
|
||||
vec4 coneAngleEtc;
|
||||
};
|
||||
layout(std140, binding = 1) uniform SceneLighting {
|
||||
layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
|
||||
Light uLights[8];
|
||||
vec4 uCellAmbient;
|
||||
vec4 uFogParams;
|
||||
|
|
|
|||
|
|
@ -9,8 +9,18 @@ struct InstanceData {
|
|||
mat4 transform;
|
||||
};
|
||||
|
||||
// Campaign V slice V2 (2026-07-27): textureHandle (uvec2, a 64-bit
|
||||
// GL_ARB_bindless_texture handle) became textureIndex (uint) plus an explicit
|
||||
// pad word. textureIndex is a slot into the binding=9 handle table
|
||||
// (ACDREAM_TEXTURE_HANDLE, common.glsl) that main() below looks up once per
|
||||
// vertex to reconstruct the exact same uvec2 handle main() used to receive
|
||||
// directly — one indirection, identical value. The pad word keeps
|
||||
// textureLayer/flags at their original std430 offsets (8/12), so the struct
|
||||
// is still 16 bytes and every existing CPU writer's layout is unchanged
|
||||
// (GpuBindingModel.GpuBatchDataStrideBytes).
|
||||
struct BatchData {
|
||||
uvec2 textureHandle; // bindless handle for sampler2DArray
|
||||
uint textureIndex; // slot into the binding=9 handle table
|
||||
uint _pad; // keeps textureLayer/flags at offsets 8/12
|
||||
uint textureLayer; // layer in the shared WB or pooled composite array
|
||||
uint flags; // reserved — N.5 dispatcher owns all blend state
|
||||
// (glBlendFunc per pass). If a future phase wants
|
||||
|
|
@ -168,7 +178,7 @@ struct Light {
|
|||
vec4 colorAndIntensity;
|
||||
vec4 coneAngleEtc;
|
||||
};
|
||||
layout(std140, binding = 1) uniform SceneLighting {
|
||||
layout(std140, ACDREAM_UBO_SET binding = 1) uniform SceneLighting {
|
||||
Light uLights[8];
|
||||
vec4 uCellAmbient;
|
||||
vec4 uFogParams;
|
||||
|
|
@ -313,6 +323,9 @@ void main() {
|
|||
vTexCoord = aTexCoord;
|
||||
|
||||
BatchData b = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||
vTextureHandle = b.textureHandle;
|
||||
// Campaign V slice V2: reconstruct the SAME uvec2 handle the shader used
|
||||
// to receive directly from BatchData, now via one binding=9 table lookup.
|
||||
// vTextureHandle's type and every downstream frag-shader use are unchanged.
|
||||
vTextureHandle = ACDREAM_TEXTURE_HANDLE(b.textureIndex);
|
||||
vTextureLayer = b.textureLayer;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,18 @@ using Chorizite.Core.Render;
|
|||
namespace AcDream.App.Rendering.Wb {
|
||||
/// <summary>
|
||||
/// Per-batch (draw call) data for modern rendering.
|
||||
/// Consists of a bindless texture handle to a texture array and the layer index.
|
||||
/// Indexed by gl_DrawIDARB in the vertex shader.
|
||||
/// Consists of a texture-table slot (Campaign V slice V2; was a raw 64-bit
|
||||
/// bindless handle) and the layer index within the shared/pooled array.
|
||||
/// Indexed by gl_DrawIDARB in the vertex shader. Same 16-byte std430 shape
|
||||
/// as mesh_modern.vert's BatchData: TextureTableIndex/Reserved/TextureIndex/
|
||||
/// Flags at offsets 0/4/8/12 — see ModernBatchDataLayoutTests.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 8)]
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
public struct ModernBatchData {
|
||||
public ulong TextureHandle; // 8 bytes
|
||||
public uint TextureIndex; // 4 bytes
|
||||
public uint Padding; // 4 bytes
|
||||
public uint TextureTableIndex; // 4 bytes — slot into the binding=9 handle table
|
||||
public uint Reserved; // 4 bytes — pad, keeps TextureIndex/Flags at offsets 8/12
|
||||
public uint TextureIndex; // 4 bytes — layer within the texture array
|
||||
public uint Flags; // 4 bytes — reserved, matches mesh_modern.vert's BatchData.flags
|
||||
}
|
||||
|
||||
public struct LandblockMdiCommand {
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
105
tests/AcDream.App.Tests/Rendering/GlBindlessHandleTableTests.cs
Normal file
105
tests/AcDream.App.Tests/Rendering/GlBindlessHandleTableTests.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
using AcDream.App.Rendering;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V2 (2026-07-27): pure-CPU proof of
|
||||
/// <see cref="GlBindlessHandleTable"/>'s bookkeeping — the handle→slot
|
||||
/// allocator each Campaign V-touched renderer (WbDrawDispatcher,
|
||||
/// EnvCellRenderer, TerrainModernRenderer, ParticleRenderer) owns to back its
|
||||
/// own binding=9 GL texture table.
|
||||
/// </summary>
|
||||
public class GlBindlessHandleTableTests
|
||||
{
|
||||
[Fact]
|
||||
public void GetOrAdd_FirstHandle_AssignsSlotZero_AndMarksDirty()
|
||||
{
|
||||
var table = new GlBindlessHandleTable();
|
||||
|
||||
uint slot = table.GetOrAdd(0xDEADBEEFu);
|
||||
|
||||
Assert.Equal(0u, slot);
|
||||
Assert.True(table.Dirty);
|
||||
Assert.Equal(new ulong[] { 0xDEADBEEFu }, table.Handles.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrAdd_SameHandleTwice_ReturnsSameSlot_AndDoesNotDuplicate()
|
||||
{
|
||||
var table = new GlBindlessHandleTable();
|
||||
|
||||
uint first = table.GetOrAdd(111ul);
|
||||
table.MarkFlushed();
|
||||
uint second = table.GetOrAdd(111ul);
|
||||
|
||||
Assert.Equal(first, second);
|
||||
Assert.False(table.Dirty); // no NEW handle was registered
|
||||
Assert.Single(table.Handles.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrAdd_DistinctHandles_AssignStableIncreasingSlots()
|
||||
{
|
||||
var table = new GlBindlessHandleTable();
|
||||
|
||||
uint a = table.GetOrAdd(1ul);
|
||||
uint b = table.GetOrAdd(2ul);
|
||||
uint c = table.GetOrAdd(3ul);
|
||||
// Re-querying an already-registered handle must not shift anyone else's slot.
|
||||
uint aAgain = table.GetOrAdd(1ul);
|
||||
|
||||
Assert.Equal(0u, a);
|
||||
Assert.Equal(1u, b);
|
||||
Assert.Equal(2u, c);
|
||||
Assert.Equal(a, aAgain);
|
||||
Assert.Equal(new ulong[] { 1ul, 2ul, 3ul }, table.Handles.ToArray());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ZeroHandle_IsRegisteredLikeAnyOther_NotSpecialCased()
|
||||
{
|
||||
// The pre-V2 behaviour let a batch/pass carry a literal zero bindless
|
||||
// handle through to the shader unchanged (an existing "no texture"
|
||||
// edge case some batches hit). V2 must reproduce that bit-for-bit: a
|
||||
// zero handle gets a real slot whose table entry is uvec2(0,0) — the
|
||||
// same value the shader would have received directly before V2.
|
||||
var table = new GlBindlessHandleTable();
|
||||
|
||||
uint slot = table.GetOrAdd(0ul);
|
||||
|
||||
Assert.Equal(0u, table.Handles[(int)slot]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MarkFlushed_ClearsDirty_UntilNextNewHandle()
|
||||
{
|
||||
var table = new GlBindlessHandleTable();
|
||||
table.GetOrAdd(42ul);
|
||||
Assert.True(table.Dirty);
|
||||
|
||||
table.MarkFlushed();
|
||||
Assert.False(table.Dirty);
|
||||
|
||||
table.GetOrAdd(42ul); // already known — must NOT re-dirty
|
||||
Assert.False(table.Dirty);
|
||||
|
||||
table.GetOrAdd(43ul); // genuinely new — must re-dirty
|
||||
Assert.True(table.Dirty);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrAdd_GrowsPastInitialCapacity_WithoutLosingEarlierSlots()
|
||||
{
|
||||
var table = new GlBindlessHandleTable();
|
||||
const int count = 200; // exceeds the 64-entry initial backing array
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
uint slot = table.GetOrAdd((ulong)i + 1000ul);
|
||||
Assert.Equal((uint)i, slot);
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
Assert.Equal((ulong)i + 1000ul, table.Handles[i]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign V slice V2 (2026-07-27): CPU-side proof that
|
||||
/// <see cref="ModernBatchData"/> — EnvCellRenderer's per-batch GPU struct,
|
||||
/// sharing mesh_modern.vert's BatchData shader-side layout with
|
||||
/// WbDrawDispatcher — packs at the exact std430 offsets the shader expects
|
||||
/// after TextureHandle (a raw 64-bit ARB_bindless_texture handle) became
|
||||
/// TextureTableIndex (a slot into the binding=9 handle table). Mirrors
|
||||
/// ClipFrameLayoutTests' role for ClipFrame and
|
||||
/// WbDrawDispatcherIndirectBuilderTests.BatchDataPublic_LayoutMatchesPrivateBatchData
|
||||
/// for WbDrawDispatcher's own BatchData — a silent layout drift here would
|
||||
/// desync EnvCellRenderer's uploaded bytes from what mesh_modern.vert reads,
|
||||
/// with no build error.
|
||||
///
|
||||
/// Layout under test (std430, 16 bytes total):
|
||||
/// offset 0 : uint TextureTableIndex
|
||||
/// offset 4 : uint Reserved (pad)
|
||||
/// offset 8 : uint TextureIndex (layer within the texture array)
|
||||
/// offset 12 : uint Flags
|
||||
/// </summary>
|
||||
public class ModernBatchDataLayoutTests
|
||||
{
|
||||
[Fact]
|
||||
public void Size_Is16Bytes_MatchingGpuBatchDataStride()
|
||||
{
|
||||
Assert.Equal(16, Unsafe.SizeOf<ModernBatchData>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FieldOffsets_MatchStd430Layout()
|
||||
{
|
||||
Assert.Equal(0, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.TextureTableIndex)));
|
||||
Assert.Equal(4, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.Reserved)));
|
||||
Assert.Equal(8, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.TextureIndex)));
|
||||
Assert.Equal(12, (int)Marshal.OffsetOf<ModernBatchData>(nameof(ModernBatchData.Flags)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FieldValues_RoundTripThroughTheStruct()
|
||||
{
|
||||
var data = new ModernBatchData
|
||||
{
|
||||
TextureTableIndex = 7u,
|
||||
TextureIndex = 3u,
|
||||
};
|
||||
|
||||
Assert.Equal(7u, data.TextureTableIndex);
|
||||
Assert.Equal(0u, data.Reserved);
|
||||
Assert.Equal(3u, data.TextureIndex);
|
||||
Assert.Equal(0u, data.Flags);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,9 +20,9 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
|
|||
// Arrange — three groups: 2 opaque (12+1 instances) + 1 transparent (12 instances)
|
||||
var groups = new List<WbDrawDispatcher.IndirectGroupInput>
|
||||
{
|
||||
new(IndexCount: 100, FirstIndex: 0, BaseVertex: 0, InstanceCount: 12, FirstInstance: 0, TextureHandle: 0xAA, TextureLayer: 0, Translucency: TranslucencyKind.Opaque),
|
||||
new(IndexCount: 200, FirstIndex: 100, BaseVertex: 0, InstanceCount: 12, FirstInstance: 12, TextureHandle: 0xBB, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend),
|
||||
new(IndexCount: 50, FirstIndex: 300, BaseVertex: 100, InstanceCount: 1, FirstInstance: 24, TextureHandle: 0xCC, TextureLayer: 0, Translucency: TranslucencyKind.Opaque),
|
||||
new(IndexCount: 100, FirstIndex: 0, BaseVertex: 0, InstanceCount: 12, FirstInstance: 0, TextureIndex: 0xAA, TextureLayer: 0, Translucency: TranslucencyKind.Opaque),
|
||||
new(IndexCount: 200, FirstIndex: 100, BaseVertex: 0, InstanceCount: 12, FirstInstance: 12, TextureIndex: 0xBB, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend),
|
||||
new(IndexCount: 50, FirstIndex: 300, BaseVertex: 100, InstanceCount: 1, FirstInstance: 24, TextureIndex: 0xCC, TextureLayer: 0, Translucency: TranslucencyKind.Opaque),
|
||||
};
|
||||
|
||||
var indirect = new DrawElementsIndirectCommand[16];
|
||||
|
|
@ -57,9 +57,9 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
|
|||
Assert.Equal(12u, indirect[2].BaseInstance);
|
||||
|
||||
// BatchData parallel — same indices as indirect
|
||||
Assert.Equal(0xAAul, batch[0].TextureHandle);
|
||||
Assert.Equal(0xCCul, batch[1].TextureHandle);
|
||||
Assert.Equal(0xBBul, batch[2].TextureHandle);
|
||||
Assert.Equal(0xAAu, batch[0].TextureIndex);
|
||||
Assert.Equal(0xCCu, batch[1].TextureIndex);
|
||||
Assert.Equal(0xBBu, batch[2].TextureIndex);
|
||||
Assert.Equal(CullMode.CounterClockwise, cull[0]);
|
||||
Assert.Equal(CullMode.CounterClockwise, cull[1]);
|
||||
Assert.Equal(CullMode.CounterClockwise, cull[2]);
|
||||
|
|
@ -71,13 +71,13 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
|
|||
var groups = new List<WbDrawDispatcher.IndirectGroupInput>
|
||||
{
|
||||
new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0,
|
||||
TextureHandle: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.Opaque,
|
||||
TextureIndex: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.Opaque,
|
||||
CullMode: CullMode.Clockwise),
|
||||
new(IndexCount: 20, FirstIndex: 10, BaseVertex: 0, InstanceCount: 1, FirstInstance: 1,
|
||||
TextureHandle: 0x2, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend,
|
||||
TextureIndex: 0x2, TextureLayer: 0, Translucency: TranslucencyKind.AlphaBlend,
|
||||
CullMode: CullMode.None),
|
||||
new(IndexCount: 30, FirstIndex: 30, BaseVertex: 0, InstanceCount: 1, FirstInstance: 2,
|
||||
TextureHandle: 0x3, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap,
|
||||
TextureIndex: 0x3, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap,
|
||||
CullMode: CullMode.Landblock),
|
||||
};
|
||||
var indirect = new DrawElementsIndirectCommand[4];
|
||||
|
|
@ -113,7 +113,7 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
|
|||
// because the discard handles transparency, not blending.
|
||||
var groups = new List<WbDrawDispatcher.IndirectGroupInput>
|
||||
{
|
||||
new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0, TextureHandle: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap),
|
||||
new(IndexCount: 10, FirstIndex: 0, BaseVertex: 0, InstanceCount: 1, FirstInstance: 0, TextureIndex: 0x1, TextureLayer: 0, Translucency: TranslucencyKind.ClipMap),
|
||||
};
|
||||
var indirect = new DrawElementsIndirectCommand[4];
|
||||
var batch = new WbDrawDispatcher.BatchDataPublic[4];
|
||||
|
|
@ -130,9 +130,18 @@ public sealed class WbDrawDispatcherIndirectBuilderTests
|
|||
// Task 10 will use MemoryMarshal.Cast<BatchData, BatchDataPublic> to
|
||||
// expose the dispatcher's per-frame BatchData[] scratch to BuildIndirectArrays
|
||||
// without copying. The cast is only safe if the structs have identical
|
||||
// layout (size, field offsets). Both use [StructLayout(Sequential, Pack=8)].
|
||||
// layout (size, field offsets).
|
||||
//
|
||||
// Campaign V slice V2 (2026-07-27): TextureHandle (ulong, an
|
||||
// ARB_bindless_texture handle) became TextureIndex (uint) plus an
|
||||
// explicit Reserved pad word — a slot into the binding=9 handle table
|
||||
// (GpuBindingModel.StorageTextureTable) instead of the raw handle.
|
||||
// The struct stays 16 bytes and TextureLayer/Flags keep their offsets
|
||||
// (8/12), matching GpuBindingModel.GpuBatchDataStrideBytes and every
|
||||
// existing CPU writer, so both structs only need 4-byte packing now.
|
||||
Assert.Equal(16, System.Runtime.CompilerServices.Unsafe.SizeOf<WbDrawDispatcher.BatchDataPublic>());
|
||||
Assert.Equal(0, (int)System.Runtime.InteropServices.Marshal.OffsetOf<WbDrawDispatcher.BatchDataPublic>(nameof(WbDrawDispatcher.BatchDataPublic.TextureHandle)));
|
||||
Assert.Equal(0, (int)System.Runtime.InteropServices.Marshal.OffsetOf<WbDrawDispatcher.BatchDataPublic>(nameof(WbDrawDispatcher.BatchDataPublic.TextureIndex)));
|
||||
Assert.Equal(4, (int)System.Runtime.InteropServices.Marshal.OffsetOf<WbDrawDispatcher.BatchDataPublic>(nameof(WbDrawDispatcher.BatchDataPublic.Reserved)));
|
||||
Assert.Equal(8, (int)System.Runtime.InteropServices.Marshal.OffsetOf<WbDrawDispatcher.BatchDataPublic>(nameof(WbDrawDispatcher.BatchDataPublic.TextureLayer)));
|
||||
Assert.Equal(12, (int)System.Runtime.InteropServices.Marshal.OffsetOf<WbDrawDispatcher.BatchDataPublic>(nameof(WbDrawDispatcher.BatchDataPublic.Flags)));
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue