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}."), }; }