using System.Runtime.CompilerServices;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Sky;
using AcDream.Content;
using AcDream.Core.Terrain;
using Xunit;
namespace AcDream.App.Tests.Rendering;
///
/// Campaign V slice V6l: every RHI vertex layout's declared stride must be the
/// footprint of the record the CPU actually uploads.
///
/// V6k found the sky's arm declaring 32 bytes against a 36-byte
/// — the record carries a TerrainLayer member no sky
/// attribute names — and drew the dome as a field of noise while nothing else in
/// the frame looked wrong, no validation rule was violated, and the offline pixel
/// gate masked the band. Its report generalised the lesson rather than banking
/// it: every .Rhi.cs arm restates a CPU record's footprint from memory,
/// and only one of them had a test. This is that test for the rest.
///
/// Each assertion names the REQUIREMENT — "the stride is the uploaded
/// record's footprint" — rather than today's number, so adding or removing a
/// member of any of these records keeps the gate honest instead of pinning an
/// answer that has drifted.
///
public class RhiVertexLayoutStrideTests
{
[Fact]
public void WorldMeshStrideMatchesTheVertexRecordTheMeshArenaPacks()
{
// ObjectMeshManager writes VertexPositionNormalTexture into
// GlobalMeshBuffer; WbDrawDispatcher, EnvCellRenderer and the
// mesh-particle pipeline all read it back through this layout.
Assert.Equal(
(uint)Unsafe.SizeOf(),
GpuVertexLayout.WorldMesh.StrideBytes);
Assert.Equal(
(uint)VertexPositionNormalTexture.Size,
GpuVertexLayout.WorldMesh.StrideBytes);
}
[Fact]
public void TerrainStrideMatchesTheUploadedTerrainVertex()
{
Assert.Equal(
(uint)Unsafe.SizeOf(),
TerrainModernRenderer.TerrainVertexLayout.StrideBytes);
}
[Fact]
public void SkyStrideMatchesTheUploadedRecord()
{
// The defect that produced this whole family of assertions.
Assert.Equal(
(uint)Unsafe.SizeOf(),
SkyRenderer.SkyVertexLayout.StrideBytes);
}
[Fact]
public void RetainedUiSpriteStrideMatchesTheFloatsThePrducerAppends()
{
// The retained UI's producer is a List, so its "record" is the
// float count AppendQuad writes per vertex. Binding the layout to that
// constant is what makes adding a per-vertex value fail here rather than
// shear every glyph in the frame.
Assert.Equal(
(uint)(TextRenderer.FloatsPerVertex * sizeof(float)),
TextRenderer.SpriteVertexLayout.StrideBytes);
}
[Fact]
public void DebugLineStrideMatchesTheFloatsTheProducerAppends()
{
Assert.Equal(
(uint)(DebugLineRenderer.FloatsPerVertex * sizeof(float)),
DebugLineRenderer.VertexLayout.StrideBytes);
}
[Fact]
public void ParticleBillboardStridesMatchTheirUploadedRecords()
{
GpuVertexLayout layout = ParticleRenderer.BillboardVertexLayout;
// Binding 0 is the shared unit quad: four floats (XY position, UV).
Assert.Equal(4u * sizeof(float), layout.StrideOf(0));
Assert.Equal(GpuVertexInputRate.Vertex, layout.InputRateOf(0));
// Binding 1 is one BillboardGpuInstance per particle. Getting this
// stride wrong is the sky defect wearing a per-instance face.
Assert.Equal(
(uint)Unsafe.SizeOf(),
layout.StrideOf(1));
Assert.Equal(GpuVertexInputRate.Instance, layout.InputRateOf(1));
}
[Fact]
public void ParticleMeshStridesMatchTheirUploadedRecords()
{
GpuVertexLayout layout = ParticleRenderer.MeshVertexLayout;
Assert.Equal(
(uint)Unsafe.SizeOf(),
layout.StrideOf(0));
Assert.Equal(GpuVertexInputRate.Vertex, layout.InputRateOf(0));
// Binding 1 is a mat4 model plus an RGBA colour, written as loose floats
// by WriteMeshGpuInstance.
Assert.Equal(
(uint)(ParticleRenderer.MeshInstanceFloats * sizeof(float)),
layout.StrideOf(1));
Assert.Equal(GpuVertexInputRate.Instance, layout.InputRateOf(1));
}
///
/// The same defect wearing its other face: an attribute that reaches past
/// the stride it is read with. Checked over every layout at once, because
/// the point of this file is that no arm should be the one without a test.
///
[Fact]
public void EveryAttributeFitsInsideItsBindingStride()
{
foreach ((string name, GpuVertexLayout layout) in EveryRhiVertexLayout())
{
foreach (GpuVertexAttribute attribute in layout.Attributes)
{
uint stride = layout.StrideOf(attribute.Binding);
uint size = SizeOf(attribute.Format);
Assert.True(
attribute.OffsetBytes + size <= stride,
$"{name}: attribute at location {attribute.Location} reaches past "
+ $"binding {attribute.Binding}'s {stride}-byte stride.");
}
}
}
[Fact]
public void EveryAttributeNamesADeclaredBinding()
{
foreach ((string name, GpuVertexLayout layout) in EveryRhiVertexLayout())
{
foreach (GpuVertexAttribute attribute in layout.Attributes)
{
Assert.True(
layout.Bindings.Any(binding => binding.Binding == attribute.Binding),
$"{name}: attribute at location {attribute.Location} names undeclared "
+ $"binding {attribute.Binding}.");
}
}
}
///
/// Every vertex layout any production RHI pipeline is built with. A new arm
/// that does not appear here is the gap V6k found; adding the row is the
/// whole cost of not repeating it.
///
internal static IEnumerable<(string Name, GpuVertexLayout Layout)> EveryRhiVertexLayout()
{
yield return ("world mesh", GpuVertexLayout.WorldMesh);
yield return ("terrain", TerrainModernRenderer.TerrainVertexLayout);
yield return ("sky", SkyRenderer.SkyVertexLayout);
yield return ("retained UI sprite", TextRenderer.SpriteVertexLayout);
yield return ("debug line", DebugLineRenderer.VertexLayout);
yield return ("particle billboard", ParticleRenderer.BillboardVertexLayout);
yield return ("particle mesh", ParticleRenderer.MeshVertexLayout);
}
private static uint SizeOf(GpuVertexFormat format) => format switch
{
GpuVertexFormat.Float1 => 4u,
GpuVertexFormat.Float2 => 8u,
GpuVertexFormat.Float3 => 12u,
GpuVertexFormat.Float4 => 16u,
GpuVertexFormat.UByte4Normalized => 4u,
GpuVertexFormat.UByte4UInt => 4u,
GpuVertexFormat.UInt1 => 4u,
_ => throw new NotSupportedException($"No size known for {format}."),
};
}