A scouting pass over V4c stopped before writing code and reported two structural blockers. Both verified against source. The pinned V0 contract was missing a blend mode. WbDrawDispatcher.ApplyRetailBlend selects three blend functions from each DAT surface's TranslucencyKind, and InvAlpha - OneMinusSrcAlpha over SrcAlpha - had no representation. Blend is baked into the pipeline and is not dynamic, so it could not be handled at the encoder, and folding it onto StraightAlpha would have silently changed how every inverse-alpha surface composites. ParticleRenderer needs it too. The contract grows here, in one reviewed commit, rather than a slice inventing a workaround for it. Retiring V2's interim handle table turns out to be its own slice. The renderers only intern bindless handles; the raw ulong is produced by the texture caches, baked into ObjectRenderBatch, and carried by GroupKey - the bucketing key V4c is forbidden to change - and by CachedBatch, where it gates cache validity. That is now V4t, with its own pixel gate. Until it lands, the world renderers bind their existing interim tables through the encoder as ordinary storage buffers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
181 lines
8.3 KiB
C#
181 lines
8.3 KiB
C#
using System.Numerics;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Runtime.InteropServices;
|
|
using AcDream.App.Rendering.Gpu;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Gpu;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public sealed class GpuContractTests
|
|
{
|
|
[Fact]
|
|
public void PushConstantBlockMatchesThePinnedLayout()
|
|
{
|
|
Assert.Equal(GpuBindingModel.PushConstantBytes, Unsafe.SizeOf<GpuPushConstants>());
|
|
Assert.True(GpuBindingModel.PushConstantBytes <= GpuBindingModel.MaxPushConstantBytes);
|
|
|
|
Assert.Equal(0, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.ViewProjection)));
|
|
Assert.Equal(64, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.DrawIdOffset)));
|
|
Assert.Equal(68, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.LightingMode)));
|
|
Assert.Equal(72, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.RenderPass)));
|
|
Assert.Equal(76, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.LightDebug)));
|
|
Assert.Equal(80, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.TextureIndexA)));
|
|
Assert.Equal(84, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.TextureIndexB)));
|
|
Assert.Equal(88, (int)Marshal.OffsetOf<GpuPushConstants>(nameof(GpuPushConstants.ParamA)));
|
|
Assert.Equal(92, (int)Marshal.OffsetOf<GpuPushConstants>(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<GpuBlendMode>().Length);
|
|
Assert.Contains(GpuBlendMode.None, Enum.GetValues<GpuBlendMode>());
|
|
Assert.Contains(GpuBlendMode.StraightAlpha, Enum.GetValues<GpuBlendMode>());
|
|
Assert.Contains(GpuBlendMode.Additive, Enum.GetValues<GpuBlendMode>());
|
|
Assert.Contains(GpuBlendMode.InverseAlpha, Enum.GetValues<GpuBlendMode>());
|
|
}
|
|
|
|
[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,
|
|
};
|
|
}
|