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 BlendModesCoverEveryRetailTranslucencyKind() { // WbDrawDispatcher.ApplyRetailBlend selects a blend function from each DAT // surface's TranslucencyKind. Retail has three, and the V0 contract shipped // with only two — slice V4c found the gap. Mapping InvAlpha onto // StraightAlpha would silently change how every inverse-alpha surface // composites, so the contract has to carry all three. Assert.Equal(4, Enum.GetValues().Length); Assert.Contains(GpuBlendMode.None, Enum.GetValues()); Assert.Contains(GpuBlendMode.StraightAlpha, Enum.GetValues()); Assert.Contains(GpuBlendMode.Additive, Enum.GetValues()); Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues()); } [Fact] public void IntegerVertexAttributesAreRepresentableDistinctlyFromNormalizedOnes() { // terrain_modern.vert declares locations 2-5 as uvec4 and the CPU feeds // them with glVertexAttribIPointer. GL leaves an integer shader input // undefined if it arrives through the float path, and Vulkan needs the // format named as R8G8B8A8_UINT rather than _UNORM — so the two cannot be // the same contract value. Those packed bytes carry terrain-type, road and // split-direction codes, so normalising them would produce garbage, not an // approximation. Assert.NotEqual(GpuVertexFormat.UByte4Normalized, GpuVertexFormat.UByte4UInt); Assert.Contains(GpuVertexFormat.UByte4UInt, Enum.GetValues()); } [Fact] public void UniformBindingsDoNotCollide() { uint[] uniformBindings = [ GpuBindingModel.UniformSceneLighting, GpuBindingModel.UniformTerrainTiling, ]; Assert.Equal(uniformBindings.Length, uniformBindings.Distinct().Count()); // Binding 2 is the terrain clip block; the tiling array must not take it. Assert.NotEqual(2u, GpuBindingModel.UniformTerrainTiling); } [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, }; }