Commit 2 deleted the GL rendering backend's implementations; this step removes the package references and shader vocabulary they leave behind, so nothing in the App project still spells Silk.NET.OpenGL. Silk.NET.OpenGL and Silk.NET.OpenGL.Extensions.ARB are dropped from AcDream.App.csproj. Chorizite.Core stays — the audit is NOT clean: its Render.Enums (TextureFormat, BufferUsage) and Lib.BoundingBox types are used directly and extensively across the Wb texture/mesh pipeline, independent of the deleted GL IUniformBuffer implementers the package comment used to cite. The stale comment is corrected in place. IMeshPipelineDevice.Gl is removed along with the GL? gl parameter threaded through WbMeshAdapter's four constructors, WorldRenderComposition's CreateMeshAdapter, and VulkanMeshPipelineDevice's Gl => null implementation — nothing read any of them once the legacy per-mesh upload bodies were gone (confirmed by grep: the sole non-doc-comment hit was a test assertion). While in WbMeshAdapter.Dispose(), found and fixed a real bug along the way: its teardown still pattern-matched the deleted GL GpuFrameFlightController to decide whether to wait for submitted work, which VulkanFrameFlightController replaced at slice V6a without this site being updated — so the wait had been silently dead on every Vulkan run since then. Retargeted to VulkanFrameFlightController, which carries the same WaitForSubmittedWork(). The GL pixel-format vocabulary (Silk.NET.OpenGL.PixelFormat/PixelType) that WorldTextureArray/TextureFormatExtensions/TextureAtlasManager used for upload validation is replaced by AcDream.Content's existing Silk.NET-free UploadPixelFormat/UploadPixelType enums (added at MP1a to keep the bake tool GL-free); two new members (Rgb, Red, Float) extend that enum with their GL ABI constants to cover the full vocabulary WorldTextureArray needs, since MP1a's original set only covered what the extractor itself emits. ObjectMeshManager's App-boundary cast `(Silk.NET.OpenGL.PixelFormat?)batch.UploadPixelFormat` becomes a direct pass-through now that both sides share the type. GpuBindingModel.StorageTextureTable (the GL-only binding=9 emulation of the Vulkan texture table) is deleted and StorageBindingCount drops from 10 to 9; the descriptor-set-layout code that builds from that count (VulkanPipelineLayouts, VulkanFrameBindings) is untouched and just allocates one fewer always-dummy-seeded, always-unused binding. Several fully dead GL-only classes came along for the ride, confirmed by zero construction sites: SilkFramebufferViewportTarget (NullFramebufferViewportTarget is the sole production IFramebufferViewportTarget), SilkRenderGlStateReader (NullRenderGlStateReader.Instance is the sole IRenderGlStateReader), RuntimeRenderFrameClearPhase (VulkanRenderFrameClearPhase is the sole IRenderFrameClearPhase, expressing the same atmosphere-clear logic as a pass load-op instead), and GpuFrameTimer plus FrameProfiler's GL-owning FrameBoundary(GL) overload and BeginGpuFrame/EndGpuFrame bracket (RecordGpuSample is the only GPU-timing path any backend uses now — the ACDREAM_WB_DIAG nested-query exclusion these existed for no longer applies, since WbDrawDispatcher's own diagnostic GPU sampling already moved to the device's Vulkan timer pool). GpuFrameFlightController itself stays (never constructed with a real fence API in production, but its retirement-ledger/serial-ring logic is backend-neutral and still covered by its own unit tests) — only its GL-specific parts (the public GL constructor overload, SilkGpuFenceApi) are deleted, since removing the whole class would mean restructuring the frozen Slice-8 composition shape's GpuFrameFlightController? threading, which is out of this commit's scope. TextureParameters.cs and BufferUsageExtensions.cs (zero callers each) are deleted outright. common.glsl is deleted: nothing in the actual Vulkan .spv build reads it. tools/ShaderCompiler/Program.cs compiles each .vert/.frag pair directly and tools/ShaderCompiler/VulkanGlslPreamble.cs injects its own complete self-contained preamble per file; common.glsl's textual concatenation was exclusively Shader.cs's GL-only mechanism, deleted at Commit 2. The five shader files that named it in comments (mesh_modern.vert, particle.vert, particle.frag, sky.frag, terrain_modern.frag) are corrected to point at VulkanGlslPreamble.cs instead. mesh.vert/mesh.frag — the pre-N.5 legacy shader pair the mandatory modern path already made unreachable, with zero C# consumers and no compiled .spv — are deleted too. Regenerated via tools/compile-shaders.ps1: 9/9 remaining shader pairs compile (previously 9/10, with mesh the sole failure — the VulkanShaderManifestTests doc comment's "nine of ten are not Vulkan-expressible" was already stale before this commit). Test fallout: dead-subject test methods/files are deleted rather than patched (TextRendererFailureSafetyTests.cs, ClipFrameUploadTests.cs, GpuResourceRetirementTransactionTests.cs's GL queue tests, one WorldRenderDiagnosticsTests source-order test, one RenderFrameResourceControllerTests clear-phase-order test); tests whose subject moved or was renamed are updated in place rather than deleted (GpuContractTests, VulkanCapabilityGateTests, MeshPipelineDeviceSeamTests' pinned seven-member surface now reads six, ParticleBindlessInstanceTests' cross-dialect check now covers the one surviving dialect, WbMeshAdapterTests' misleadingly-named null-gl test — gpuDevice was always the parameter that actually threw). Build: `dotnet build AcDream.slnx -c Release` — 0 warnings, 0 errors, with the Silk.NET.OpenGL/.Extensions.ARB package references physically removed from the csproj (not just unreferenced in code). Tests: full-solution `dotnet test` green across every project. Zero remaining `using Silk.NET.OpenGL` anywhere in src/ or tests/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
602 lines
24 KiB
C#
602 lines
24 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using AcDream.App.Platform;
|
|
using AcDream.App.Rendering.Gpu;
|
|
using AcDream.App.Rendering.Gpu.Vk;
|
|
using Silk.NET.Vulkan;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Gpu.Vk;
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V5 — the Vulkan capability gate's accept/reject matrix.
|
|
///
|
|
/// These run with no driver, no device and no window: the gate is a pure
|
|
/// function from a captured record to a list of operator-facing sentences, and
|
|
/// that separation is the whole reason the interop layer stays thin. Each
|
|
/// mandatory feature gets its own "absent means rejected" case, because a gate
|
|
/// that silently stops checking one requirement is exactly the failure the GL
|
|
/// probe was written to prevent.
|
|
/// </summary>
|
|
public sealed class VulkanCapabilityGateTests
|
|
{
|
|
private static VulkanCapabilityRecord SupportedRecord(
|
|
VulkanDeviceFeatureSupport? features = null,
|
|
VulkanDeviceLimitSupport? limits = null,
|
|
VulkanFormatSupport? formats = null,
|
|
VulkanSurfaceSupport? surface = null,
|
|
VulkanFunctionProbeResult? probe = null,
|
|
uint? apiVersion = null)
|
|
{
|
|
var record = new VulkanCapabilityRecord(
|
|
DateTimeOffset.UnixEpoch,
|
|
"win-x64",
|
|
GraphicalHostOperatingSystem.Windows,
|
|
GraphicalDisplayProtocol.Windows,
|
|
GraphicalDisplayProtocol.Windows,
|
|
"Vulkan 1.3.0",
|
|
"Vulkan 1.3.280",
|
|
apiVersion ?? VulkanApiVersion.Make(1, 3, 280),
|
|
"AMD Radeon RX 9070 XT",
|
|
"vendor 0x1002, device 0x7550, driver 2.0.0 (raw 0x00800000)",
|
|
PhysicalDeviceType.DiscreteGpu,
|
|
0,
|
|
"automatic",
|
|
RequestedDeviceOverride: null,
|
|
ForcedUnsupportedFeature: null,
|
|
AvailableDevices: [],
|
|
InstanceExtensions: ["VK_KHR_surface", "VK_KHR_win32_surface"],
|
|
DeviceExtensions: ["VK_KHR_swapchain"],
|
|
GraphicsQueueFamily: 0,
|
|
PresentQueueFamily: 0,
|
|
features ?? VulkanDeviceFeatureSupport.Complete,
|
|
limits ?? VulkanDeviceLimitSupport.Complete,
|
|
formats ?? VulkanFormatSupport.Complete,
|
|
surface ?? SupportedSurface(),
|
|
probe ?? PassingProbe(),
|
|
SupportFailures: []);
|
|
return VulkanCapabilityRequirements.Reevaluate(record);
|
|
}
|
|
|
|
private static VulkanSurfaceSupport SupportedSurface() => new(
|
|
PresentSupported: true,
|
|
SelectedFormat: Format.B8G8R8A8Unorm,
|
|
SelectedColorSpace: ColorSpaceKHR.SpaceSrgbNonlinearKhr,
|
|
SelectedPresentMode: PresentModeKHR.FifoKhr,
|
|
SelectedImageCount: 3,
|
|
SelectedWidth: 1280,
|
|
SelectedHeight: 720,
|
|
SupportsTransferSource: true,
|
|
AvailableFormats: [Format.B8G8R8A8Unorm],
|
|
AvailablePresentModes: [PresentModeKHR.FifoKhr, PresentModeKHR.ImmediateKhr]);
|
|
|
|
private static VulkanFunctionProbeResult PassingProbe() => new(
|
|
DeviceCreation: true,
|
|
DescriptorIndexingLayout: true,
|
|
PushConstantLayout: true,
|
|
DynamicRenderingClear: true,
|
|
TimelineSemaphoreWait: true,
|
|
HostQueryReset: true,
|
|
OffscreenReadback: true,
|
|
Failures: []);
|
|
|
|
[Fact]
|
|
public void ACompleteDeviceIsAccepted()
|
|
{
|
|
VulkanCapabilityRecord record = SupportedRecord();
|
|
|
|
Assert.Empty(record.SupportFailures);
|
|
Assert.True(record.IsSupported);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Every field on <see cref="VulkanDeviceFeatureSupport"/> is mandatory, so
|
|
/// clearing any one of them must produce exactly one new failure. Driving
|
|
/// this by reflection rather than by hand means a feature added to the record
|
|
/// without a matching Evaluate clause fails here instead of shipping
|
|
/// unchecked.
|
|
/// </summary>
|
|
[Fact]
|
|
public void EveryRequiredFeatureIsIndividuallyEnforced()
|
|
{
|
|
IEnumerable<string> featureNames = typeof(VulkanDeviceFeatureSupport)
|
|
.GetProperties()
|
|
.Where(property => property.PropertyType == typeof(bool))
|
|
.Select(property => property.Name);
|
|
|
|
foreach (string name in featureNames)
|
|
{
|
|
VulkanDeviceFeatureSupport? reduced =
|
|
VulkanDeviceFeatureSupport.Complete.Without(name);
|
|
Assert.NotNull(reduced);
|
|
|
|
VulkanCapabilityRecord record = SupportedRecord(features: reduced);
|
|
Assert.False(
|
|
record.IsSupported,
|
|
$"clearing {name} must reject the device.");
|
|
Assert.Single(record.SupportFailures);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void AnUnknownFeatureNameIsNotSilentlyIgnored()
|
|
{
|
|
Assert.Null(VulkanDeviceFeatureSupport.Complete.Without("NotAVulkanFeature"));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1, 2)]
|
|
[InlineData(1, 0)]
|
|
[InlineData(0, 9)]
|
|
public void ADeviceBelowVulkan13IsRejected(uint major, uint minor)
|
|
{
|
|
VulkanCapabilityRecord record = SupportedRecord(
|
|
apiVersion: VulkanApiVersion.Make(major, minor, 0));
|
|
|
|
Assert.Contains(
|
|
record.SupportFailures,
|
|
failure => failure.Contains("Vulkan 1.3 is required", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void Vulkan14IsAccepted()
|
|
{
|
|
VulkanCapabilityRecord record = SupportedRecord(
|
|
apiVersion: VulkanApiVersion.Make(1, 4, 0));
|
|
|
|
Assert.True(record.IsSupported);
|
|
}
|
|
|
|
[Fact]
|
|
public void PushConstantsBelowThePinnedBlockAreRejected()
|
|
{
|
|
VulkanCapabilityRecord record = SupportedRecord(
|
|
limits: VulkanDeviceLimitSupport.Complete with
|
|
{
|
|
MaxPushConstantsSize = GpuBindingModel.PushConstantBytes - 1,
|
|
});
|
|
|
|
Assert.Contains(
|
|
record.SupportFailures,
|
|
failure => failure.Contains("push-constant bytes are required", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void FewerThanEightClipDistancesAreRejected()
|
|
{
|
|
VulkanCapabilityRecord record = SupportedRecord(
|
|
limits: VulkanDeviceLimitSupport.Complete with { MaxClipDistances = 6 });
|
|
|
|
Assert.Contains(
|
|
record.SupportFailures,
|
|
failure => failure.Contains("clip distances are required", StringComparison.Ordinal));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets 0 (storage), 1 (uniform) and 2 (texture table) are bound at once, so
|
|
/// two bound sets is not enough. This is the limit the §3.4 binding model
|
|
/// silently assumes.
|
|
/// </summary>
|
|
[Fact]
|
|
public void FewerThanThreeBoundDescriptorSetsAreRejected()
|
|
{
|
|
Assert.Equal(3u, VulkanCapabilityRequirements.VulkanDescriptorSetCount);
|
|
|
|
VulkanCapabilityRecord record = SupportedRecord(
|
|
limits: VulkanDeviceLimitSupport.Complete with { MaxBoundDescriptorSets = 2 });
|
|
|
|
Assert.Contains(
|
|
record.SupportFailures,
|
|
failure => failure.Contains(
|
|
"simultaneously bound descriptor sets are required",
|
|
StringComparison.Ordinal));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign V slice V6g, plan §5.5.7 defect 1. The V6b layout declared all
|
|
/// ten of set 0's bindings <c>STORAGE_BUFFER_DYNAMIC</c> and was rejected by
|
|
/// the RX 9070 XT's limit of 8 — silently, and only visible under the
|
|
/// validation layer. The split has to stay inside Vulkan's GUARANTEED
|
|
/// minimum, not merely inside one device's: at four, no conformant
|
|
/// implementation can fail the layout, which is what slice V9's lavapipe row
|
|
/// and the deferred physical Linux row depend on.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TheDynamicStorageBindingSplitFitsVulkansGuaranteedMinimum()
|
|
{
|
|
const uint VulkanGuaranteedMinimum = 4;
|
|
Assert.True(
|
|
VulkanPipelineLayouts.DynamicStorageBindingCount <= VulkanGuaranteedMinimum,
|
|
$"set 0 declares {VulkanPipelineLayouts.DynamicStorageBindingCount} dynamic storage " +
|
|
$"bindings; Vulkan only guarantees {VulkanGuaranteedMinimum}.");
|
|
|
|
// The bindings that stay dynamic are the ones a renderer feeds from the
|
|
// per-frame ring; the rest point at long-lived buffers bound once per
|
|
// pass and buy nothing from a dynamic offset.
|
|
Assert.True(VulkanPipelineLayouts.IsDynamicStorageBinding(GpuBindingModel.StorageInstances));
|
|
Assert.True(VulkanPipelineLayouts.IsDynamicStorageBinding(GpuBindingModel.StorageBatches));
|
|
Assert.False(VulkanPipelineLayouts.IsDynamicStorageBinding(GpuBindingModel.StorageGlobalLights));
|
|
Assert.False(VulkanPipelineLayouts.IsDynamicStorageBinding(GpuBindingModel.StorageClipRegions));
|
|
|
|
// Binding 9 (the GL-only uvec2 handle-table emulation, StorageTextureTable)
|
|
// is deleted as of Campaign V slice V11 — the Vulkan backend always bound
|
|
// set 2 instead and never touched it, so there is no longer a ninth
|
|
// binding to assert never spends a scarce dynamic descriptor.
|
|
}
|
|
|
|
[Fact]
|
|
public void ADeviceWithTooFewDynamicBufferDescriptorsIsRejected()
|
|
{
|
|
VulkanCapabilityRecord storage = SupportedRecord(
|
|
limits: VulkanDeviceLimitSupport.Complete with
|
|
{
|
|
MaxDescriptorSetStorageBuffersDynamic =
|
|
VulkanPipelineLayouts.DynamicStorageBindingCount - 1,
|
|
});
|
|
VulkanCapabilityRecord uniform = SupportedRecord(
|
|
limits: VulkanDeviceLimitSupport.Complete with
|
|
{
|
|
MaxDescriptorSetUniformBuffersDynamic =
|
|
VulkanFrameBindings.DynamicUniformBindingCount - 1,
|
|
});
|
|
|
|
Assert.Contains(
|
|
storage.SupportFailures,
|
|
failure => failure.Contains("dynamic storage bindings", StringComparison.Ordinal));
|
|
Assert.Contains(
|
|
uniform.SupportFailures,
|
|
failure => failure.Contains("dynamic uniform bindings", StringComparison.Ordinal));
|
|
Assert.Empty(SupportedRecord().SupportFailures);
|
|
}
|
|
|
|
[Fact]
|
|
public void ATextureTableSmallerThanTheCapacityIsRejectedPerSetAndPerStage()
|
|
{
|
|
VulkanCapabilityRecord perSet = SupportedRecord(
|
|
limits: VulkanDeviceLimitSupport.Complete with
|
|
{
|
|
MaxDescriptorSetUpdateAfterBindSampledImages =
|
|
GpuBindingModel.TextureTableCapacity - 1,
|
|
});
|
|
VulkanCapabilityRecord perStage = SupportedRecord(
|
|
limits: VulkanDeviceLimitSupport.Complete with
|
|
{
|
|
MaxPerStageDescriptorUpdateAfterBindSampledImages =
|
|
GpuBindingModel.TextureTableCapacity - 1,
|
|
});
|
|
|
|
Assert.Contains(
|
|
perSet.SupportFailures,
|
|
failure => failure.Contains("update-after-bind sampled images", StringComparison.Ordinal));
|
|
Assert.Contains(
|
|
perStage.SupportFailures,
|
|
failure => failure.Contains("fragment stage", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void MissingGraphicsTimestampsAreRejected()
|
|
{
|
|
VulkanCapabilityRecord record = SupportedRecord(
|
|
limits: VulkanDeviceLimitSupport.Complete with { TimestampComputeAndGraphics = false });
|
|
|
|
Assert.Contains(
|
|
record.SupportFailures,
|
|
failure => failure.Contains("timestamps are required", StringComparison.Ordinal));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The single highest-severity finding of the V3 audit: the swapchain is
|
|
/// UNORM, not sRGB. A surface that cannot offer UNORM must be a startup
|
|
/// failure, because silently taking an _SRGB format would brighten every
|
|
/// frame and pass every automated gate until V7.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ASurfaceWithoutTheUnormFormatIsRejected()
|
|
{
|
|
VulkanCapabilityRecord record = SupportedRecord(
|
|
formats: VulkanFormatSupport.Complete with { SwapchainUnormFormat = false });
|
|
|
|
Assert.Contains(
|
|
record.SupportFailures,
|
|
failure => failure.Contains("B8G8R8A8_UNORM", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void AMissingDepthStencilFormatIsRejected()
|
|
{
|
|
VulkanCapabilityRecord record = SupportedRecord(
|
|
formats: VulkanFormatSupport.Complete with { DepthStencilFormat = Format.Undefined });
|
|
|
|
Assert.Contains(
|
|
record.SupportFailures,
|
|
failure => failure.Contains("depth+stencil format", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false, true, true)]
|
|
[InlineData(true, false, true)]
|
|
[InlineData(true, true, false)]
|
|
public void MissingAnyBcBlockIsRejected(bool bc1, bool bc2, bool bc3)
|
|
{
|
|
VulkanCapabilityRecord record = SupportedRecord(
|
|
formats: VulkanFormatSupport.Complete with
|
|
{
|
|
Bc1Sampled = bc1,
|
|
Bc2Sampled = bc2,
|
|
Bc3Sampled = bc3,
|
|
});
|
|
|
|
Assert.Contains(
|
|
record.SupportFailures,
|
|
failure => failure.Contains("BC1, BC2 and BC3", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void ASurfaceWithoutTransferSourceUsageIsRejected()
|
|
{
|
|
VulkanCapabilityRecord record = SupportedRecord(
|
|
surface: SupportedSurface() with { SupportsTransferSource = false });
|
|
|
|
Assert.Contains(
|
|
record.SupportFailures,
|
|
failure => failure.Contains("TRANSFER_SRC", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void ADeviceThatCannotPresentIsRejected()
|
|
{
|
|
VulkanCapabilityRecord record = SupportedRecord(
|
|
surface: SupportedSurface() with { PresentSupported = false });
|
|
|
|
Assert.Contains(
|
|
record.SupportFailures,
|
|
failure => failure.Contains("cannot present", StringComparison.Ordinal));
|
|
}
|
|
|
|
/// <summary>
|
|
/// A headless capture (no window) is still evaluable — that is what lets the
|
|
/// probe be offscreen — and simply carries no surface requirements.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AHeadlessCaptureWithNoSurfaceIsAccepted()
|
|
{
|
|
VulkanCapabilityRecord record = SupportedRecord(surface: null);
|
|
|
|
Assert.True(record.IsSupported);
|
|
}
|
|
|
|
[Fact]
|
|
public void ProbeFailuresArePrefixedAndReplaceTheIndividualChecks()
|
|
{
|
|
VulkanCapabilityRecord record = SupportedRecord(
|
|
probe: VulkanFunctionProbeResult.NotRun);
|
|
|
|
Assert.Contains(
|
|
record.SupportFailures,
|
|
failure => failure.StartsWith("Vulkan device probe:", StringComparison.Ordinal));
|
|
// The per-step sentences are suppressed when the probe reported its own
|
|
// failure, so the operator gets the cause rather than seven symptoms.
|
|
Assert.DoesNotContain(
|
|
record.SupportFailures,
|
|
failure => failure.Contains("probe did not pass", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("DescriptorIndexingLayout", "descriptor-indexing layout probe")]
|
|
[InlineData("PushConstantLayout", "push-constant pipeline-layout probe")]
|
|
[InlineData("DynamicRenderingClear", "dynamic-rendering clear probe")]
|
|
[InlineData("TimelineSemaphoreWait", "timeline-semaphore wait probe")]
|
|
[InlineData("HostQueryReset", "host query-reset probe")]
|
|
[InlineData("OffscreenReadback", "offscreen readback probe")]
|
|
public void EachActiveProbeStepIsIndividuallyEnforced(string step, string expected)
|
|
{
|
|
VulkanFunctionProbeResult probe = step switch
|
|
{
|
|
"DescriptorIndexingLayout" => PassingProbe() with { DescriptorIndexingLayout = false },
|
|
"PushConstantLayout" => PassingProbe() with { PushConstantLayout = false },
|
|
"DynamicRenderingClear" => PassingProbe() with { DynamicRenderingClear = false },
|
|
"TimelineSemaphoreWait" => PassingProbe() with { TimelineSemaphoreWait = false },
|
|
"HostQueryReset" => PassingProbe() with { HostQueryReset = false },
|
|
_ => PassingProbe() with { OffscreenReadback = false },
|
|
};
|
|
|
|
VulkanCapabilityRecord record = SupportedRecord(probe: probe);
|
|
|
|
Assert.Contains(
|
|
record.SupportFailures,
|
|
failure => failure.Contains(expected, StringComparison.Ordinal));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The V3 audit noted the GL gate requires sRGB-framebuffer support the
|
|
/// renderer never uses, and that the Vulkan gate "must not carry the stale
|
|
/// requirement forward". This is that tripwire.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TheGateDoesNotRequireSrgbAnything()
|
|
{
|
|
VulkanCapabilityRecord record = SupportedRecord(
|
|
features: VulkanDeviceFeatureSupport.Complete.Without("MultiDrawIndirect")!);
|
|
|
|
Assert.DoesNotContain(
|
|
record.SupportFailures,
|
|
failure => failure.Contains("sRGB", StringComparison.OrdinalIgnoreCase));
|
|
}
|
|
|
|
[Fact]
|
|
public void TheForcedUnsupportedKnobRejectsTheNamedFeature()
|
|
{
|
|
VulkanCapabilityRecord forced = VulkanCapabilityRequirements.ApplyForcedUnsupported(
|
|
SupportedRecord(),
|
|
"timelineSemaphore");
|
|
|
|
Assert.False(forced.IsSupported);
|
|
Assert.Equal("timelineSemaphore", forced.ForcedUnsupportedFeature);
|
|
Assert.Contains(
|
|
forced.SupportFailures,
|
|
failure => failure.Contains("timelineSemaphore is required", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void TheForcedUnsupportedKnobIgnoresAnUnsetValue()
|
|
{
|
|
VulkanCapabilityRecord unchanged = VulkanCapabilityRequirements.ApplyForcedUnsupported(
|
|
SupportedRecord(),
|
|
featureName: null);
|
|
|
|
Assert.True(unchanged.IsSupported);
|
|
Assert.Null(unchanged.ForcedUnsupportedFeature);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A knob that names a nonexistent feature must fail loudly. A silent no-op
|
|
/// would report a pass the operator never actually exercised.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TheForcedUnsupportedKnobFailsOnAnUnknownName()
|
|
{
|
|
VulkanCapabilityRecord forced = VulkanCapabilityRequirements.ApplyForcedUnsupported(
|
|
SupportedRecord(),
|
|
"TeapotShading");
|
|
|
|
Assert.False(forced.IsSupported);
|
|
Assert.Contains(
|
|
forced.SupportFailures,
|
|
failure => failure.Contains(
|
|
"which is not a required Vulkan feature",
|
|
StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void TheUnsupportedMessageNamesThePlatformDeviceFailuresAndReport()
|
|
{
|
|
VulkanCapabilityRecord record = VulkanCapabilityRequirements.ApplyForcedUnsupported(
|
|
SupportedRecord(),
|
|
"DynamicRendering");
|
|
|
|
string message = VulkanCapabilityGuard.FormatUnsupportedMessage(
|
|
record,
|
|
"artifacts/graphical-capabilities-vulkan.json");
|
|
|
|
Assert.Contains("win-x64", message, StringComparison.Ordinal);
|
|
Assert.Contains("AMD Radeon RX 9070 XT", message, StringComparison.Ordinal);
|
|
Assert.Contains("dynamicRendering is required", message, StringComparison.Ordinal);
|
|
Assert.Contains("graphical-capabilities-vulkan.json", message, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void ThrowIfUnsupportedRaisesNotSupportedExceptionForTheExitFourContract()
|
|
{
|
|
VulkanCapabilityRecord rejected = VulkanCapabilityRequirements.ApplyForcedUnsupported(
|
|
SupportedRecord(),
|
|
"Synchronization2");
|
|
|
|
// Program.cs maps NotSupportedException out of window.Run() to exit 4.
|
|
Assert.Throws<NotSupportedException>(
|
|
() => VulkanCapabilityGuard.ThrowIfUnsupported(rejected, "report.json"));
|
|
VulkanCapabilityGuard.ThrowIfUnsupported(SupportedRecord(), "report.json");
|
|
}
|
|
|
|
[Fact]
|
|
public void TheReportFileNameSitsBesideTheGlOne()
|
|
{
|
|
Assert.Equal(
|
|
"graphical-capabilities-vulkan.json",
|
|
VulkanCapabilityGuard.ReportFileName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The JSON report is the artifact an operator sends with a bug report, so
|
|
/// the fields that identify the machine and explain the refusal must be
|
|
/// present and readable — enums as names, not integers.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TheJsonReportCarriesTheIdentifyingFieldsAsReadableNames()
|
|
{
|
|
VulkanCapabilityRecord record = VulkanCapabilityRequirements.ApplyForcedUnsupported(
|
|
SupportedRecord(),
|
|
"Maintenance4");
|
|
|
|
string json = VulkanCapabilityReportWriter.Serialize(record);
|
|
using JsonDocument document = JsonDocument.Parse(json);
|
|
JsonElement root = document.RootElement;
|
|
|
|
Assert.Equal("win-x64", root.GetProperty("RuntimeIdentifier").GetString());
|
|
Assert.Equal("AMD Radeon RX 9070 XT", root.GetProperty("DeviceName").GetString());
|
|
Assert.Equal("DiscreteGpu", root.GetProperty("DeviceType").GetString());
|
|
Assert.Equal("Windows", root.GetProperty("OperatingSystem").GetString());
|
|
Assert.Equal("Maintenance4", root.GetProperty("ForcedUnsupportedFeature").GetString());
|
|
Assert.False(root.GetProperty("Features").GetProperty("Maintenance4").GetBoolean());
|
|
Assert.Equal(
|
|
"B8G8R8A8Unorm",
|
|
root.GetProperty("Surface").GetProperty("SelectedFormat").GetString());
|
|
Assert.NotEmpty(root.GetProperty("SupportFailures").EnumerateArray().ToArray());
|
|
}
|
|
|
|
/// <summary>
|
|
/// The record projects onto the backend-neutral contract the renderers
|
|
/// consult. The alignment fields are load-bearing rather than informational —
|
|
/// a ring allocation that violates one is a driver error on Vulkan.
|
|
/// </summary>
|
|
[Fact]
|
|
public void TheRecordProjectsOntoTheBackendNeutralContract()
|
|
{
|
|
VulkanCapabilityRecord record = SupportedRecord(
|
|
limits: VulkanDeviceLimitSupport.Complete with
|
|
{
|
|
MinStorageBufferOffsetAlignment = 16,
|
|
MinUniformBufferOffsetAlignment = 64,
|
|
MaxColorSampleCount = 4,
|
|
MaxDescriptorSetUpdateAfterBindSampledImages = 500_000,
|
|
MaxPerStageDescriptorUpdateAfterBindSampledImages = 16_384,
|
|
});
|
|
|
|
GpuCapabilityRecord projected = record.ToGpuCapabilityRecord();
|
|
|
|
Assert.Equal(GpuBackendKind.Vulkan, projected.Backend);
|
|
Assert.Equal("AMD Radeon RX 9070 XT", projected.DeviceName);
|
|
Assert.Equal("Vulkan 1.3.280", projected.ApiVersion);
|
|
Assert.Equal(16u, projected.MinStorageBufferOffsetAlignment);
|
|
Assert.Equal(64u, projected.MinUniformBufferOffsetAlignment);
|
|
Assert.Equal(4u, projected.MaxSampleCount);
|
|
// The table is limited by whichever of the two counts is smaller.
|
|
Assert.Equal(16_384u, projected.MaxTextureTableSlots);
|
|
Assert.Equal(GpuBindingModel.StorageBindingCount, projected.MaxStorageBufferBindings);
|
|
Assert.True(projected.SupportsMultiDrawIndirect);
|
|
Assert.True(projected.SupportsDrawParameters);
|
|
Assert.True(projected.SupportsTextureCompressionBc);
|
|
Assert.True(projected.SupportsTimestampQueries);
|
|
// The whole point of the campaign's CPU target: per-frame data written
|
|
// straight into mapped memory rather than copied through BufferSubData.
|
|
Assert.True(projected.SupportsPersistentlyMappedRings);
|
|
Assert.Empty(projected.SupportFailures);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A device the Vulkan gate accepted must also satisfy the backend-neutral
|
|
/// contract's own <c>SupportFailures</c>. If the two ever disagree, one of
|
|
/// them is checking something the other is not.
|
|
/// </summary>
|
|
[Fact]
|
|
public void AnAcceptedDeviceAlsoSatisfiesTheNeutralContract()
|
|
{
|
|
GpuCapabilityRecord projected = SupportedRecord().ToGpuCapabilityRecord();
|
|
|
|
Assert.True(projected.IsSupported);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(1u, 3u, 280u)]
|
|
[InlineData(1u, 4u, 0u)]
|
|
[InlineData(0u, 0u, 1u)]
|
|
public void ApiVersionPackingRoundTrips(uint major, uint minor, uint patch)
|
|
{
|
|
uint packed = VulkanApiVersion.Make(major, minor, patch);
|
|
|
|
Assert.Equal(major, VulkanApiVersion.Major(packed));
|
|
Assert.Equal(minor, VulkanApiVersion.Minor(packed));
|
|
Assert.Equal(patch, VulkanApiVersion.Patch(packed));
|
|
Assert.Equal($"Vulkan {major}.{minor}.{patch}", VulkanApiVersion.Describe(packed));
|
|
}
|
|
}
|