acdream/tests/AcDream.App.Tests/Rendering/ParticleBindlessInstanceTests.cs
Erik 1d2f2f738f
All checks were successful
CI / linux-portable (push) Successful in 3m32s
CI / windows-gate (push) Successful in 6m55s
CI / release (push) Successful in 2m12s
fix #451: stabilize portal seam rendering
2026-08-27 14:30:21 +02:00

95 lines
4.6 KiB
C#

using System.Reflection;
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(72, Marshal.SizeOf<ParticleRenderer.BillboardGpuInstance>());
Assert.Equal(
new IntPtr(64),
Marshal.OffsetOf<ParticleRenderer.BillboardGpuInstance>(
nameof(ParticleRenderer.BillboardGpuInstance.TextureIndex)));
Assert.Equal(
new IntPtr(68),
Marshal.OffsetOf<ParticleRenderer.BillboardGpuInstance>(
nameof(ParticleRenderer.BillboardGpuInstance.ClipSlot)));
}
[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 carries a binding=9
// table slot, not the raw handle.
//
// Campaign V slice V6e: the SLOT is what crosses the stage boundary now,
// and the fragment stage does the lookup — a varying cannot carry a
// Vulkan descriptor. The untextured particle is spelled by the reserved
// index rather than by a null handle, because Vulkan's descriptor array
// cannot be asked whether an element was ever written.
Assert.Contains("layout(location = 6) in uint aTextureIndex;", vertex);
Assert.Contains("layout(location = 7) in uint aClipSlot;", vertex);
Assert.Contains("clipRegions[aClipSlot]", vertex);
Assert.Contains("flat out uint vTextureIndex;", vertex);
Assert.Contains("vTextureIndex = aTextureIndex;", vertex);
Assert.Contains("#extension GL_ARB_bindless_texture : require", fragment);
Assert.Contains("flat in uint vTextureIndex;", fragment);
Assert.Contains("ACDREAM_SAMPLE_ARRAY(vTextureIndex, vec3(vTex, 0.0))", fragment);
Assert.Contains("vTextureIndex != ACDREAM_TEXTURE_NONE", fragment);
Assert.DoesNotContain("uniform sampler2D uParticleTexture", fragment);
}
/// <summary>
/// Campaign V slice V6e: "this particle has no texture" is a reserved index
/// rather than a null handle, and that value is written in two places — the
/// CPU that produces it and the Vulkan preamble that tests it in the shader.
/// Before Campaign V slice V11 deleted the GL arm there was a third copy in
/// common.glsl, which this test also cross-checked; two copies of a magic
/// number is still a drift waiting to happen, and its failure mode is
/// silent: a particle would sample slot 0xFFFFFFFF instead of drawing the
/// procedural blob.
/// </summary>
[Fact]
public void TheReservedNoTextureSlotAgreesBetweenCpuAndTheVulkanPreamble()
{
const string literal = "0xFFFFFFFF";
object? cpuValue = typeof(ParticleRenderer)
.GetField("NoTextureSlot", BindingFlags.NonPublic | BindingFlags.Static)
?.GetRawConstantValue();
Assert.Equal(0xFFFFFFFFu, Assert.IsType<uint>(cpuValue));
// The Vulkan half is injected by the offline compiler at compile time,
// not carried in a shared GLSL source, so it is a separate declaration
// that has to say the same thing.
string preamble = File.ReadAllText(Path.Combine(
RepositoryRoot(), "tools", "ShaderCompiler", "VulkanGlslPreamble.cs"));
Assert.Contains($"#define ACDREAM_TEXTURE_NONE {literal}u", preamble);
}
private static string RepositoryRoot()
{
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
directory = directory.Parent;
return directory?.FullName
?? throw new InvalidOperationException(
"Could not locate the repository root from the test binary.");
}
}