using System; using System.Text.Json; using AcDream.App.Platform; using AcDream.App.Rendering.Gpu.Vk; using Silk.NET.Vulkan; namespace AcDream.App.Tests.Rendering.Gpu.Vk; /// /// Campaign V slice V9: the capability report is a CI contract, not just a /// diagnostic. /// /// The linux-vulkan job in /// .github/workflows/headless-portability.yml asserts the lavapipe run's /// verdict by reading graphical-capabilities-vulkan.json with jq. /// Those jq paths and the enum spellings they compare against are /// invisible to the compiler: renaming a record property or swapping an enum /// converter would leave every existing test green and turn CI red on a /// different branch, days later, with a failure that reads like a driver /// problem. These tests pin the exact strings the job depends on, so the rename /// fails here first and says why. /// /// The record below is shaped like lavapipe deliberately — a CPU device /// on X11 reporting Vulkan 1.4 — because that is the device the job runs on. /// /// public sealed class VulkanCapabilityReportContractTests { private static VulkanCapabilityRecord LavapipeShapedRecord( string? forcedUnsupportedFeature = null) { var record = new VulkanCapabilityRecord( DateTimeOffset.UnixEpoch, "linux-x64", GraphicalHostOperatingSystem.Linux, GraphicalDisplayProtocol.X11, GraphicalDisplayProtocol.X11, "Vulkan 1.4.0", "Vulkan 1.4.305", VulkanApiVersion.Make(1, 4, 305), "llvmpipe (LLVM 19.1.7, 256 bits)", "vendor 0x10005, device 0x0, driver 0.0.1", PhysicalDeviceType.Cpu, 0, "automatic", RequestedDeviceOverride: null, ForcedUnsupportedFeature: null, AvailableDevices: [], InstanceExtensions: ["VK_KHR_surface", "VK_KHR_xlib_surface"], DeviceExtensions: ["VK_KHR_swapchain"], GraphicsQueueFamily: 0, PresentQueueFamily: 0, VulkanDeviceFeatureSupport.Complete, VulkanDeviceLimitSupport.Complete, VulkanFormatSupport.Complete, new VulkanSurfaceSupport( 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]), new VulkanFunctionProbeResult( DeviceCreation: true, DescriptorIndexingLayout: true, PushConstantLayout: true, DynamicRenderingClear: true, TimelineSemaphoreWait: true, HostQueryReset: true, OffscreenReadback: true, Failures: []), SupportFailures: []); record = VulkanCapabilityRequirements.Reevaluate(record); return VulkanCapabilityRequirements.ApplyForcedUnsupported( record, forcedUnsupportedFeature); } private static JsonElement Report(string? forcedUnsupportedFeature = null) => JsonDocument .Parse( VulkanCapabilityReportWriter.Serialize( LavapipeShapedRecord(forcedUnsupportedFeature))) .RootElement; /// /// The passing run's assertions, one for one with the "Probe the Vulkan /// capability gate on lavapipe" step. /// [Fact] public void ThePassingRunCarriesEveryFieldTheCiJobReads() { JsonElement report = Report(); Assert.Equal(0, report.GetProperty("SupportFailures").GetArrayLength()); Assert.Equal("X11", report.GetProperty("ActiveDisplayProtocol").GetString()); // The exact spelling the job greps for. Silk.NET's enum member is Cpu; // a converter change that produced "CPU" or "4" would pass every other // test in this project and fail only in CI. Assert.Equal("Cpu", report.GetProperty("DeviceType").GetString()); JsonElement probe = report.GetProperty("FunctionProbe"); Assert.Equal(0, probe.GetProperty("Failures").GetArrayLength()); Assert.True(probe.GetProperty("DeviceCreation").GetBoolean()); Assert.True(probe.GetProperty("OffscreenReadback").GetBoolean()); // Present and human-readable: the job prints these to the log so a // failure elsewhere still records which device ran. Assert.False(string.IsNullOrWhiteSpace(report.GetProperty("DeviceName").GetString())); Assert.False(string.IsNullOrWhiteSpace(report.GetProperty("DeviceApiVersion").GetString())); Assert.False(string.IsNullOrWhiteSpace(report.GetProperty("DriverInfo").GetString())); } /// /// The job unpacks the API version out of DeviceApiVersionPacked with /// jq arithmetic — dividing by 2^22 for the major and by 2^12 for the minor /// — rather than regexing the display string, which would break at 1.10. /// This asserts that arithmetic against the same packing the client uses. /// [Fact] public void ThePackedApiVersionUnpacksTheWayTheCiJobUnpacksIt() { uint packed = Report().GetProperty("DeviceApiVersionPacked").GetUInt32(); uint major = packed / 4194304; uint minor = packed % 4194304 / 4096; Assert.Equal(VulkanApiVersion.Major(packed), major); Assert.Equal(VulkanApiVersion.Minor(packed), minor); Assert.True(major > 1 || (major == 1 && minor >= 3)); } /// /// The forced-unsupported run's assertions, one for one with the "Verify the /// forced-unsupported gate exits 4" step. The feature name is the literal /// the workflow passes. /// [Fact] public void TheForcedUnsupportedRunCarriesEveryFieldTheCiJobReads() { JsonElement report = Report("timelineSemaphore"); Assert.Equal( "timelineSemaphore", report.GetProperty("ForcedUnsupportedFeature").GetString()); Assert.False( report.GetProperty("Features").GetProperty("TimelineSemaphore").GetBoolean()); JsonElement failures = report.GetProperty("SupportFailures"); Assert.True(failures.GetArrayLength() > 0); // The job matches on the feature name inside the failure sentence. Assert.Contains( failures.EnumerateArray(), failure => failure.GetString()?.Contains( "timelineSemaphore", StringComparison.Ordinal) == true); } /// /// The operator-facing refusal names the report path. The job greps the log /// for the file name, because a gate that refuses without saying where the /// evidence is has failed at the only job it has on a machine nobody owns. /// [Fact] public void TheRefusalMessageNamesTheReportTheJobUploads() { string message = VulkanCapabilityGuard.FormatUnsupportedMessage( LavapipeShapedRecord("timelineSemaphore"), "/tmp/diagnostics/graphical-capabilities-vulkan.json"); Assert.Contains("graphical-capabilities-vulkan.json", message, StringComparison.Ordinal); Assert.Contains("timelineSemaphore", message, StringComparison.Ordinal); } /// /// The report file name is what the workflow's VULKAN_REPORT path ends in. /// [Fact] public void TheReportFileNameIsTheOneTheWorkflowPathNames() { Assert.Equal( "graphical-capabilities-vulkan.json", VulkanCapabilityGuard.ReportFileName); } }