using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Gpu;
namespace AcDream.App.Tests.Rendering.Gpu;
///
/// Campaign V slice V0 — the pinned RHI contract.
///
/// These are not behaviour tests; they are the tripwires that stop the contract
/// drifting out from under the shaders and the two backends. Every constant
/// asserted here also appears in a GLSL source file or in a backend's binding
/// setup, so a change that lands in only one place fails here rather than as a
/// corrupted frame.
///
public sealed class GpuContractTests
{
[Fact]
public void PushConstantBlockMatchesThePinnedLayout()
{
Assert.Equal(GpuBindingModel.PushConstantBytes, Unsafe.SizeOf());
Assert.True(GpuBindingModel.PushConstantBytes <= GpuBindingModel.MaxPushConstantBytes);
Assert.Equal(0, (int)Marshal.OffsetOf(nameof(GpuPushConstants.ViewProjection)));
Assert.Equal(64, (int)Marshal.OffsetOf(nameof(GpuPushConstants.DrawIdOffset)));
Assert.Equal(68, (int)Marshal.OffsetOf(nameof(GpuPushConstants.LightingMode)));
Assert.Equal(72, (int)Marshal.OffsetOf(nameof(GpuPushConstants.RenderPass)));
Assert.Equal(76, (int)Marshal.OffsetOf(nameof(GpuPushConstants.LightDebug)));
Assert.Equal(80, (int)Marshal.OffsetOf(nameof(GpuPushConstants.TextureIndexA)));
Assert.Equal(84, (int)Marshal.OffsetOf(nameof(GpuPushConstants.TextureIndexB)));
Assert.Equal(88, (int)Marshal.OffsetOf(nameof(GpuPushConstants.ParamA)));
Assert.Equal(92, (int)Marshal.OffsetOf(nameof(GpuPushConstants.ParamB)));
}
[Fact]
public void StorageBindingsMatchTheShaderSources()
{
// mesh_modern.vert declares std430 bindings 0..8 in exactly this order;
// binding 9 is the GL-only texture handle table added by slice V2.
Assert.Equal(0u, GpuBindingModel.StorageInstances);
Assert.Equal(1u, GpuBindingModel.StorageBatches);
Assert.Equal(2u, GpuBindingModel.StorageClipRegions);
Assert.Equal(3u, GpuBindingModel.StorageClipSlots);
Assert.Equal(4u, GpuBindingModel.StorageGlobalLights);
Assert.Equal(5u, GpuBindingModel.StorageInstanceLightSets);
Assert.Equal(6u, GpuBindingModel.StorageInstanceIndoor);
Assert.Equal(7u, GpuBindingModel.StorageInstanceAlpha);
Assert.Equal(8u, GpuBindingModel.StorageInstanceSelectionLighting);
Assert.Equal(9u, GpuBindingModel.StorageTextureTable);
Assert.Equal(10u, GpuBindingModel.StorageBindingCount);
}
[Fact]
public void UniformAndTextureTableLiveInSeparateSets()
{
// The SceneLighting UBO keeps binding=1 even though the BatchBuffer SSBO
// also uses binding=1. GL tolerates that because its SSBO and UBO binding
// tables are separate; Vulkan does not, so the set index disambiguates.
Assert.Equal(GpuBindingModel.StorageBatches, GpuBindingModel.UniformSceneLighting);
Assert.NotEqual(0u, GpuBindingModel.UniformSet);
Assert.NotEqual(GpuBindingModel.UniformSet, GpuBindingModel.TextureTableSet);
}
[Fact]
public void ClipRegionStrideMatchesTheUploadedLayout()
{
// ClipFrame lays these bytes out on the CPU; ClipFrameLayoutTests pins the
// producer side, this pins the contract side. 16 B header + 8 x vec4.
Assert.Equal(8, GpuBindingModel.ClipPlanesPerSlot);
Assert.Equal(144, GpuBindingModel.ClipRegionStrideBytes);
}
[Fact]
public void UnassignedTextureSlotIsNeverAValidIndex()
{
Assert.False(GpuTextureSlot.Unassigned.IsAssigned);
Assert.True(new GpuTextureSlot(0).IsAssigned);
Assert.Equal("slot#unassigned", GpuTextureSlot.Unassigned.ToString());
Assert.Equal("slot#7", new GpuTextureSlot(7).ToString());
}
[Fact]
public void WorldMeshVertexLayoutMatchesTheMeshShaderInputs()
{
GpuVertexLayout layout = GpuVertexLayout.WorldMesh;
Assert.Equal(32u, layout.StrideBytes);
Assert.Equal(3, layout.Attributes.Length);
Assert.Equal(new GpuVertexAttribute(0, GpuVertexFormat.Float3, 0), layout.Attributes[0]);
Assert.Equal(new GpuVertexAttribute(1, GpuVertexFormat.Float3, 12), layout.Attributes[1]);
Assert.Equal(new GpuVertexAttribute(2, GpuVertexFormat.Float2, 24), layout.Attributes[2]);
}
[Fact]
public void MultisampledBackbufferPassResolvesWhileDepthIsDiscarded()
{
GpuPassDescription multisampled = GpuPassDescription.BackbufferClear("world", Vector4.Zero, sampleCount: 4);
Assert.Equal(GpuStoreOp.Resolve, multisampled.Color.Store);
Assert.Null(multisampled.Color.Target);
Assert.Equal(GpuStoreOp.DontCare, multisampled.Depth!.Value.Store);
Assert.Equal(1f, multisampled.Depth!.Value.ClearDepth);
GpuPassDescription single = GpuPassDescription.BackbufferClear("world", Vector4.Zero, sampleCount: 1);
Assert.Equal(GpuStoreOp.Store, single.Color.Store);
}
[Fact]
public void CapabilityRecordAcceptsADeviceThatMeetsEveryRequirement()
{
GpuCapabilityRecord record = SupportedRecord();
Assert.Empty(record.SupportFailures);
Assert.True(record.IsSupported);
}
[Fact]
public void CapabilityRecordNamesEveryMissingRequirement()
{
GpuCapabilityRecord record = SupportedRecord() with
{
SupportsMultiDrawIndirect = false,
SupportsDrawParameters = false,
SupportsTextureCompressionBc = false,
MaxTextureTableSlots = 16,
MaxStorageBufferBindings = 4,
MaxPushConstantBytes = 32,
MaxClipDistances = 0,
};
Assert.False(record.IsSupported);
Assert.Equal(7, record.SupportFailures.Count);
Assert.Contains(record.SupportFailures, failure => failure.Contains("Multi-draw-indirect", StringComparison.Ordinal));
Assert.Contains(record.SupportFailures, failure => failure.Contains("gl_DrawID", StringComparison.Ordinal));
Assert.Contains(record.SupportFailures, failure => failure.Contains("BC (DXT)", StringComparison.Ordinal));
Assert.Contains(record.SupportFailures, failure => failure.Contains("clip distances", StringComparison.Ordinal));
}
[Fact]
public void TimestampSupportIsOptional()
{
// Losing GPU timing degrades profiling; it must never refuse to start.
GpuCapabilityRecord record = SupportedRecord() with { SupportsTimestampQueries = false };
Assert.True(record.IsSupported);
}
private static GpuCapabilityRecord SupportedRecord() => new()
{
Backend = GpuBackendKind.Vulkan,
DeviceName = "test-adapter",
DriverInfo = "test-driver",
ApiVersion = "Vulkan 1.3.0",
MaxTextureTableSlots = GpuBindingModel.TextureTableCapacity,
MaxStorageBufferBindings = GpuBindingModel.StorageBindingCount,
MaxPushConstantBytes = GpuBindingModel.MaxPushConstantBytes,
MinStorageBufferOffsetAlignment = 64,
MinUniformBufferOffsetAlignment = 256,
MaxClipDistances = GpuBindingModel.ClipPlanesPerSlot,
MaxSampleCount = 8,
SupportsMultiDrawIndirect = true,
SupportsDrawParameters = true,
SupportsTextureCompressionBc = true,
SupportsTimestampQueries = true,
SupportsPersistentlyMappedRings = true,
};
}