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>
44 lines
2 KiB
C#
44 lines
2 KiB
C#
using System.Runtime.InteropServices;
|
|
using AcDream.App.Rendering;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public sealed class ParticleBindlessInstanceTests
|
|
{
|
|
[Fact]
|
|
public void BillboardGpuInstance_MatchesVertexAttributeAbi()
|
|
{
|
|
// 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.TextureIndex)));
|
|
}
|
|
|
|
[Fact]
|
|
public void BillboardShaders_ConsumeOneBindlessTextureHandlePerInstance()
|
|
{
|
|
string shadersDirectory = Path.Combine(
|
|
AppContext.BaseDirectory,
|
|
"Rendering",
|
|
"Shaders");
|
|
string vertex = File.ReadAllText(Path.Combine(shadersDirectory, "particle.vert"));
|
|
string fragment = File.ReadAllText(Path.Combine(shadersDirectory, "particle.frag"));
|
|
|
|
// 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);
|
|
Assert.DoesNotContain("uniform sampler2D uParticleTexture", fragment);
|
|
}
|
|
}
|