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
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue