205 lines
7.7 KiB
C#
205 lines
7.7 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Gpu;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
/// <summary>
|
|
/// Phase U.3: CPU-side proof that <see cref="ClipFrame"/> packs the shared clip
|
|
/// data in the EXACT std430 (mesh SSBO) / std140 (terrain UBO) byte layout the
|
|
/// shaders read. A silent layout drift here would mis-clip at U.4 with no build
|
|
/// error — these tests are the gate that catches it.
|
|
///
|
|
/// Layout under test (mesh CellClip, std430):
|
|
/// offset 0 : uint count
|
|
/// offset 4 : uint _p0 (pad)
|
|
/// offset 8 : uint _p1 (pad)
|
|
/// offset 12 : uint _p2 (pad)
|
|
/// offset 16 : vec4 planes[0] (16-byte vec4 stride)
|
|
/// ...
|
|
/// offset 16 + i*16 : vec4 planes[i]
|
|
/// stride 144 bytes per slot.
|
|
/// Terrain UBO (std140): int count at 0 (padded to 16), vec4 planes[8] at 16.
|
|
/// </summary>
|
|
public class ClipFrameLayoutTests
|
|
{
|
|
private static float ReadFloat(System.ReadOnlySpan<byte> b, int offset)
|
|
=> System.BitConverter.ToSingle(b.Slice(offset, 4));
|
|
|
|
private static uint ReadUInt(System.ReadOnlySpan<byte> b, int offset)
|
|
=> System.BitConverter.ToUInt32(b.Slice(offset, 4));
|
|
|
|
private static int ReadInt(System.ReadOnlySpan<byte> b, int offset)
|
|
=> System.BitConverter.ToInt32(b.Slice(offset, 4));
|
|
|
|
[Fact]
|
|
public void LayoutConstants_MatchShaderStruct()
|
|
{
|
|
// CellClip: 16 (count + 3 pad uints) + 8*16 (vec4 planes) = 144.
|
|
Assert.Equal(144, ClipFrame.CellClipStrideBytes);
|
|
Assert.Equal(16, ClipFrame.CellClipPlanesOffset);
|
|
Assert.Equal(8, ClipFrame.MaxPlanes);
|
|
Assert.Equal(144, ClipFrame.TerrainUboBytes);
|
|
// Binding contract: mesh clip regions on SSBO binding=2, terrain on UBO binding=2.
|
|
// The mesh side's binding index moved off ClipFrame at Campaign V slice
|
|
// V11 — the RHI arm addresses it through GpuBindingModel.StorageClipRegions
|
|
// instead of a raw GL binding constant (see ClipFrame's BeginFrame doc
|
|
// comment); the terrain UBO binding is still genuinely shared, so it stays.
|
|
Assert.Equal(2u, GpuBindingModel.StorageClipRegions);
|
|
Assert.Equal(2u, ClipFrame.TerrainClipUboBinding);
|
|
}
|
|
|
|
[Fact]
|
|
public void NoClip_HasExactlyOneSlot_AllZeros_Count0()
|
|
{
|
|
var frame = ClipFrame.NoClip();
|
|
Assert.Equal(1, frame.SlotCount);
|
|
|
|
var bytes = frame.RegionBytesForTest;
|
|
Assert.Equal(ClipFrame.CellClipStrideBytes, bytes.Length); // 144 — exactly one slot
|
|
|
|
// count == 0 ⇒ shader passes every plane (no-clip).
|
|
Assert.Equal(0u, ReadUInt(bytes, 0));
|
|
// Every byte of the reserved no-clip slot is zero.
|
|
foreach (var b in bytes)
|
|
Assert.Equal(0, b);
|
|
}
|
|
|
|
[Fact]
|
|
public void NoClip_TerrainBytes_Count0_AllZeros()
|
|
{
|
|
var frame = ClipFrame.NoClip();
|
|
var t = frame.TerrainBytesForTest;
|
|
Assert.Equal(ClipFrame.TerrainUboBytes, t.Length);
|
|
Assert.Equal(0, ReadInt(t, 0)); // count 0 ⇒ terrain ungated
|
|
foreach (var b in t)
|
|
Assert.Equal(0, b);
|
|
}
|
|
|
|
[Fact]
|
|
public void AppendSlot_WritesCountAndPlanes_AtStd430Offsets()
|
|
{
|
|
var frame = ClipFrame.NoClip();
|
|
|
|
// Three distinct planes so each lands at a verifiable offset.
|
|
var p0 = new Vector4(1f, 0f, 0f, 0.5f);
|
|
var p1 = new Vector4(0f, 1f, 0f, 0.25f);
|
|
var p2 = new Vector4(-1f, 0f, 0f, -0.75f);
|
|
|
|
int slot = frame.AppendSlot(new[] { p0, p1, p2 });
|
|
Assert.Equal(1, slot); // slot 0 is the reserved no-clip; this is slot 1
|
|
Assert.Equal(2, frame.SlotCount);
|
|
|
|
var bytes = frame.RegionBytesForTest;
|
|
Assert.Equal(2 * ClipFrame.CellClipStrideBytes, bytes.Length); // two slots now
|
|
|
|
int baseOff = slot * ClipFrame.CellClipStrideBytes; // 144
|
|
|
|
// count == 3 at offset 0 of the slot; the 3 pad uints stay zero.
|
|
Assert.Equal(3u, ReadUInt(bytes, baseOff + 0));
|
|
Assert.Equal(0u, ReadUInt(bytes, baseOff + 4));
|
|
Assert.Equal(0u, ReadUInt(bytes, baseOff + 8));
|
|
Assert.Equal(0u, ReadUInt(bytes, baseOff + 12));
|
|
|
|
// planes[0..2] at offset 16, 32, 48 (vec4 stride 16).
|
|
AssertPlaneAt(bytes, baseOff + 16, p0);
|
|
AssertPlaneAt(bytes, baseOff + 32, p1);
|
|
AssertPlaneAt(bytes, baseOff + 48, p2);
|
|
|
|
// Slot 0 (the reserved no-clip) is untouched: still count 0.
|
|
Assert.Equal(0u, ReadUInt(bytes, 0));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetSlotPlanes_BorrowsTheExactPackedClipRegion()
|
|
{
|
|
using ClipFrame frame = ClipFrame.NoClip();
|
|
Vector4[] expected =
|
|
[
|
|
new(1f, 2f, 3f, 4f),
|
|
new(-5f, 6f, -7f, 8f),
|
|
];
|
|
|
|
int slot = frame.AppendSlot(expected);
|
|
|
|
ReadOnlySpan<Vector4> actual = frame.GetSlotPlanes(checked((uint)slot));
|
|
Assert.Equal(expected.Length, actual.Length);
|
|
Assert.Equal(expected[0], actual[0]);
|
|
Assert.Equal(expected[1], actual[1]);
|
|
Assert.Equal(0, frame.GetSlotPlanes(0).Length);
|
|
}
|
|
|
|
[Fact]
|
|
public void AppendSlot_EmptyPlaneList_PacksNoClipSlot_Count0()
|
|
{
|
|
var frame = ClipFrame.NoClip();
|
|
int slot = frame.AppendSlot(System.ReadOnlySpan<Vector4>.Empty);
|
|
Assert.Equal(1, slot);
|
|
|
|
var bytes = frame.RegionBytesForTest;
|
|
Assert.Equal(0u, ReadUInt(bytes, slot * ClipFrame.CellClipStrideBytes)); // count 0
|
|
}
|
|
|
|
[Fact]
|
|
public void AppendSlot_ClampsToEightPlanes()
|
|
{
|
|
var frame = ClipFrame.NoClip();
|
|
var planes = new Vector4[12];
|
|
for (int i = 0; i < planes.Length; i++)
|
|
planes[i] = new Vector4(i, 0f, 0f, 0f);
|
|
|
|
int slot = frame.AppendSlot(planes);
|
|
var bytes = frame.RegionBytesForTest;
|
|
// Only MaxPlanes (8) are recorded in the count.
|
|
Assert.Equal((uint)ClipFrame.MaxPlanes, ReadUInt(bytes, slot * ClipFrame.CellClipStrideBytes));
|
|
}
|
|
|
|
[Fact]
|
|
public void AppendSlot_FromClipPlaneSet_AxisAlignedSquare_PacksFourPlanes()
|
|
{
|
|
// A unit square in NDC → ClipPlaneSet with 4 convex planes.
|
|
var cv = new CellView();
|
|
cv.Add(new ViewPolygon(new[]
|
|
{
|
|
new Vector2(-0.5f, -0.5f), new Vector2(0.5f, -0.5f),
|
|
new Vector2(0.5f, 0.5f), new Vector2(-0.5f, 0.5f),
|
|
}));
|
|
var cps = ClipPlaneSet.From(cv);
|
|
Assert.Equal(4, cps.Count);
|
|
|
|
var frame = ClipFrame.NoClip();
|
|
int slot = frame.AppendSlot(cps);
|
|
|
|
var bytes = frame.RegionBytesForTest;
|
|
int baseOff = slot * ClipFrame.CellClipStrideBytes;
|
|
Assert.Equal(4u, ReadUInt(bytes, baseOff + 0));
|
|
|
|
// Each packed plane must match the ClipPlaneSet's plane bit-for-bit.
|
|
for (int i = 0; i < 4; i++)
|
|
AssertPlaneAt(bytes, baseOff + ClipFrame.CellClipPlanesOffset + i * 16, cps.Planes[i]);
|
|
}
|
|
|
|
[Fact]
|
|
public void SetTerrainClip_WritesCountAndPlanes_AtStd140Offsets()
|
|
{
|
|
var frame = ClipFrame.NoClip();
|
|
var p0 = new Vector4(0.3f, -0.4f, 0f, 0.1f);
|
|
var p1 = new Vector4(-0.6f, 0.8f, 0f, -0.2f);
|
|
|
|
frame.SetTerrainClip(new[] { p0, p1 });
|
|
|
|
var t = frame.TerrainBytesForTest;
|
|
Assert.Equal(2, ReadInt(t, 0)); // int count at offset 0
|
|
AssertPlaneAt(t, ClipFrame.CellClipPlanesOffset + 0, p0); // planes start at 16 under std140 too
|
|
AssertPlaneAt(t, ClipFrame.CellClipPlanesOffset + 16, p1);
|
|
}
|
|
|
|
private static void AssertPlaneAt(System.ReadOnlySpan<byte> bytes, int offset, Vector4 expected)
|
|
{
|
|
Assert.Equal(expected.X, ReadFloat(bytes, offset + 0), 6);
|
|
Assert.Equal(expected.Y, ReadFloat(bytes, offset + 4), 6);
|
|
Assert.Equal(expected.Z, ReadFloat(bytes, offset + 8), 6);
|
|
Assert.Equal(expected.W, ReadFloat(bytes, offset + 12), 6);
|
|
}
|
|
}
|