feat(render): Campaign V slice V2c - particle texture-index migration
Completes Campaign V slice V2 by moving both particle render paths - billboard (particle.vert/.frag) and mesh-emitter (particle_mesh.vert/.frag) - from raw 64-bit ARB_bindless_texture handles to binding=9 handle-table indices, matching V2a's mesh path and V2b's terrain path. Particles differ from both prior sub-slices in HOW the handle reaches the shader: - Billboard particles carry it as a per-INSTANCE vertex attribute (not an SSBO batch or a per-draw uniform), because each particle can use a different texture within one instanced draw. aTextureHandle (location 6, uvec2) became aTextureIndex (uint); particle.vert looks it up via ACDREAM_TEXTURE_HANDLE and reconstructs the SAME uvec2 into vTextureHandle exactly as before, so particle.frag - including its zero-handle check for the procedural circle fallback - needed no change at all. The per-instance ABI struct BillboardGpuInstance shrank by 4 bytes (one uint slot instead of two uint handle halves); ParticleBindlessInstanceTests updated for the new 68-byte layout and the vertex attribute declaration text. - Mesh-emitter particles carry it as a per-draw uniform (uTextureHandle, uvec2) exactly like terrain's pattern from V2b: one texture per draw call, set right before it. Became uTextureIndex (uint) + the same ACDREAM_TEXTURE_HANDLE lookup. ParticleRenderer owns its own GlBindlessHandleTable and binding=9 SSBO, independent of the other three renderers' tables, created eagerly in the constructor alongside the other GL resources it already creates there. Unlike the other three renderers, flushing/binding the table happens immediately before EVERY individual draw call (four call sites: immediate billboard, immediate mesh, and both halves of the deferred/prepared RetailAlphaQueue path) rather than once per pipeline-state switch - a run of consecutive mesh-particle sub-batches can register a new handle partway through (each sub-batch has its own texture), and the table must be current for each one, not just the first. TextureCache's particle-texture cache (AcquireParticleTexture, StandaloneBindlessTextureCache) needed no change: it only ever hands back a raw ulong handle, and both ParticleGfxInfo.TextureHandle and ParticleInstance.TextureHandle keep carrying that raw value - the table lookup is added exactly where each path already converts its handle into GPU-visible state (WriteBillboardGpuInstance and the two ProgramUniform call sites). Coverage caveat (flagged per the campaign doc's slice table): the offline pixel gate's fixed outdoor view has no particles in frame, so it does not exercise this slice - it only confirms nothing else regressed. This change is correspondingly kept strictly mechanical (indirection only, no logic change), but it still needs a user visual check with live particle emitters before being trusted as pixel-identical. Gate: dotnet build -c Release green, dotnet test tests/AcDream.App.Tests -c Release green (3843 passed / 3 skipped on a clean run - one unrelated pre-existing flaky allocation test, UiDatFontTests, failed once and passed on immediate re-run in isolation and in the full suite, confirmed unrelated to this change), and tools/run-offline-pixel-gate.ps1 passed against the V2b commit's build with a 4.62e-05 differing-pixel fraction (a tripwire only, per the coverage caveat above). 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
1f1f6c088b
commit
a85743f70d
4 changed files with 130 additions and 32 deletions
|
|
@ -8,15 +8,16 @@ public sealed class ParticleBindlessInstanceTests
|
|||
[Fact]
|
||||
public void BillboardGpuInstance_MatchesVertexAttributeAbi()
|
||||
{
|
||||
Assert.Equal(72, Marshal.SizeOf<ParticleRenderer.BillboardGpuInstance>());
|
||||
// Campaign V slice V2c (2026-07-27): TextureHandleLow/High (the split
|
||||
// halves of a raw 64-bit ARB_bindless_texture handle, 8 bytes) became
|
||||
// one TextureIndex (a binding=9 handle-table slot, 4 bytes), so the
|
||||
// struct shrank by 4 bytes; TextureIndex keeps TextureHandleLow's
|
||||
// former offset (64 — right after the four vec4 fields).
|
||||
Assert.Equal(68, Marshal.SizeOf<ParticleRenderer.BillboardGpuInstance>());
|
||||
Assert.Equal(
|
||||
new IntPtr(64),
|
||||
Marshal.OffsetOf<ParticleRenderer.BillboardGpuInstance>(
|
||||
nameof(ParticleRenderer.BillboardGpuInstance.TextureHandleLow)));
|
||||
Assert.Equal(
|
||||
new IntPtr(68),
|
||||
Marshal.OffsetOf<ParticleRenderer.BillboardGpuInstance>(
|
||||
nameof(ParticleRenderer.BillboardGpuInstance.TextureHandleHigh)));
|
||||
nameof(ParticleRenderer.BillboardGpuInstance.TextureIndex)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -29,7 +30,12 @@ public sealed class ParticleBindlessInstanceTests
|
|||
string vertex = File.ReadAllText(Path.Combine(shadersDirectory, "particle.vert"));
|
||||
string fragment = File.ReadAllText(Path.Combine(shadersDirectory, "particle.frag"));
|
||||
|
||||
Assert.Contains("layout(location = 6) in uvec2 aTextureHandle;", vertex);
|
||||
// Campaign V slice V2c: the per-instance attribute now carries a
|
||||
// binding=9 table slot, not the raw handle; particle.vert
|
||||
// reconstructs the SAME uvec2 handle via ACDREAM_TEXTURE_HANDLE
|
||||
// before handing it to particle.frag, so the fragment shader's half
|
||||
// (extension, reconstruction, varying type/name) is untouched.
|
||||
Assert.Contains("layout(location = 6) in uint aTextureIndex;", vertex);
|
||||
Assert.Contains("flat out uvec2 vTextureHandle;", vertex);
|
||||
Assert.Contains("#extension GL_ARB_bindless_texture : require", fragment);
|
||||
Assert.Contains("sampler2DArray(vTextureHandle)", fragment);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue