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);
}
}