95 lines
4.6 KiB
C#
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.");
|
|
}
|
|
}
|