179 lines
7.1 KiB
C#
179 lines
7.1 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V6l: every RHI vertex layout's declared stride must be the
|
|
/// footprint of the record the CPU actually uploads.
|
|
///
|
|
/// <para>V6k found the sky's arm declaring 32 bytes against a 36-byte
|
|
/// <see cref="Vertex"/> — the record carries a <c>TerrainLayer</c> 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: <b>every <c>.Rhi.cs</c> arm restates a CPU record's footprint from memory,
|
|
/// and only one of them had a test.</b> This is that test for the rest.</para>
|
|
///
|
|
/// <para>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.</para>
|
|
/// </summary>
|
|
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<VertexPositionNormalTexture>(),
|
|
GpuVertexLayout.WorldMesh.StrideBytes);
|
|
Assert.Equal(
|
|
(uint)VertexPositionNormalTexture.Size,
|
|
GpuVertexLayout.WorldMesh.StrideBytes);
|
|
}
|
|
|
|
[Fact]
|
|
public void TerrainStrideMatchesTheUploadedTerrainVertex()
|
|
{
|
|
Assert.Equal(
|
|
(uint)Unsafe.SizeOf<TerrainVertex>(),
|
|
TerrainModernRenderer.TerrainVertexLayout.StrideBytes);
|
|
}
|
|
|
|
[Fact]
|
|
public void SkyStrideMatchesTheUploadedRecord()
|
|
{
|
|
// The defect that produced this whole family of assertions.
|
|
Assert.Equal(
|
|
(uint)Unsafe.SizeOf<Vertex>(),
|
|
SkyRenderer.SkyVertexLayout.StrideBytes);
|
|
}
|
|
|
|
[Fact]
|
|
public void RetainedUiSpriteStrideMatchesTheFloatsThePrducerAppends()
|
|
{
|
|
// The retained UI's producer is a List<float>, 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<ParticleRenderer.BillboardGpuInstance>(),
|
|
layout.StrideOf(1));
|
|
Assert.Equal(GpuVertexInputRate.Instance, layout.InputRateOf(1));
|
|
}
|
|
|
|
[Fact]
|
|
public void ParticleMeshStridesMatchTheirUploadedRecords()
|
|
{
|
|
GpuVertexLayout layout = ParticleRenderer.MeshVertexLayout;
|
|
|
|
Assert.Equal(
|
|
(uint)Unsafe.SizeOf<VertexPositionNormalTexture>(),
|
|
layout.StrideOf(0));
|
|
Assert.Equal(GpuVertexInputRate.Vertex, layout.InputRateOf(0));
|
|
|
|
// Binding 1 is the exact mesh-particle record: model, RGBA, clip slot.
|
|
Assert.Equal(
|
|
(uint)Unsafe.SizeOf<ParticleRenderer.MeshParticleGpuInstance>(),
|
|
layout.StrideOf(1));
|
|
Assert.Equal(GpuVertexInputRate.Instance, layout.InputRateOf(1));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
[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}.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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}."),
|
|
};
|
|
}
|