using System.Text.Json;
using System.Text.Json.Serialization;
using AcDream.App.Platform;
using Silk.NET.Vulkan;
namespace AcDream.App.Rendering.Gpu.Vk;
///
/// Every Vulkan device feature the acdream renderer requires, with the reason
/// each one is mandatory recorded next to it (Campaign V plan §4.1).
///
/// This is a plain data record with no Silk handles in it, which is the point:
/// the interop layer fills it from vkGetPhysicalDeviceFeatures2, the
/// forced-unsupported gate knob clears one field of it, and
/// turns it into operator-facing
/// sentences — all three testable with no driver, no device, and no window.
///
internal sealed record VulkanDeviceFeatureSupport
{
// ---- core 1.0 ----
/// The three MDI dispatch sites are the entire draw architecture.
public required bool MultiDrawIndirect { get; init; }
/// Indirect commands carry a non-zero firstInstance as the per-group instance base.
public required bool DrawIndirectFirstInstance { get; init; }
/// Phase U.3's per-cell screen-space clip gate writes gl_ClipDistance[8].
public required bool ShaderClipDistance { get; init; }
/// DXT1/3/5 DAT surfaces upload as BC1/2/3 with no transcode.
public required bool TextureCompressionBc { get; init; }
/// Sampler-quality parity with the GL path.
public required bool SamplerAnisotropy { get; init; }
// ---- 1.1 ----
/// gl_DrawID. Resets per indirect dispatch exactly as GL's does.
public required bool ShaderDrawParameters { get; init; }
// ---- 1.2 ----
/// One monotonic serial replaces the GL fence array; the retirement ledger keeps its keys.
public required bool TimelineSemaphore { get; init; }
/// Reset timestamp pools from the CPU instead of burning command-buffer calls.
public required bool HostQueryReset { get; init; }
/// The global texture table is a runtime-sized descriptor array.
public required bool RuntimeDescriptorArray { get; init; }
/// Unregistered table slots are legitimately absent rather than an error.
public required bool DescriptorBindingPartiallyBound { get; init; }
/// Texture registration appends a descriptor write without rebuilding the set.
public required bool DescriptorBindingSampledImageUpdateAfterBind { get; init; }
/// A slot may be rewritten while a command buffer that does not read it is pending.
public required bool DescriptorBindingUpdateUnusedWhilePending { get; init; }
/// The table's 16384-slot capacity is a variable descriptor count.
public required bool DescriptorBindingVariableDescriptorCount { get; init; }
///
/// nonuniformEXT in the fragment shaders. Required, not optional:
/// within one MDI dispatch different draws read different Batches[]
/// entries, and "dynamically uniform" is defined over the whole dispatch on
/// some implementations (plan §4.6).
///
public required bool ShaderSampledImageArrayNonUniformIndexing { get; init; }
// ---- 1.3 ----
/// No render-pass or framebuffer objects anywhere in the frame.
public required bool DynamicRendering { get; init; }
/// Every barrier in the frame skeleton is a vkCmdPipelineBarrier2.
public required bool Synchronization2 { get; init; }
/// Relaxed shader interface rules for the dual-legal GLSL sources.
public required bool Maintenance4 { get; init; }
///
/// Every feature present. The starting point for the forced-unsupported gate
/// knob and for tests that assert one specific absence at a time.
///
internal static VulkanDeviceFeatureSupport Complete { get; } = new()
{
MultiDrawIndirect = true,
DrawIndirectFirstInstance = true,
ShaderClipDistance = true,
TextureCompressionBc = true,
SamplerAnisotropy = true,
ShaderDrawParameters = true,
TimelineSemaphore = true,
HostQueryReset = true,
RuntimeDescriptorArray = true,
DescriptorBindingPartiallyBound = true,
DescriptorBindingSampledImageUpdateAfterBind = true,
DescriptorBindingUpdateUnusedWhilePending = true,
DescriptorBindingVariableDescriptorCount = true,
ShaderSampledImageArrayNonUniformIndexing = true,
DynamicRendering = true,
Synchronization2 = true,
Maintenance4 = true,
};
///
/// Returns this record with the named feature cleared, or null when
/// the name matches no required feature. Drives
/// ACDREAM_VULKAN_FORCE_UNSUPPORTED; matching is case-insensitive on
/// the property name.
///
internal VulkanDeviceFeatureSupport? Without(string featureName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(featureName);
return featureName.Trim() switch
{
var n when Is(n, nameof(MultiDrawIndirect)) => this with { MultiDrawIndirect = false },
var n when Is(n, nameof(DrawIndirectFirstInstance)) => this with { DrawIndirectFirstInstance = false },
var n when Is(n, nameof(ShaderClipDistance)) => this with { ShaderClipDistance = false },
var n when Is(n, nameof(TextureCompressionBc)) => this with { TextureCompressionBc = false },
var n when Is(n, nameof(SamplerAnisotropy)) => this with { SamplerAnisotropy = false },
var n when Is(n, nameof(ShaderDrawParameters)) => this with { ShaderDrawParameters = false },
var n when Is(n, nameof(TimelineSemaphore)) => this with { TimelineSemaphore = false },
var n when Is(n, nameof(HostQueryReset)) => this with { HostQueryReset = false },
var n when Is(n, nameof(RuntimeDescriptorArray)) => this with { RuntimeDescriptorArray = false },
var n when Is(n, nameof(DescriptorBindingPartiallyBound)) => this with { DescriptorBindingPartiallyBound = false },
var n when Is(n, nameof(DescriptorBindingSampledImageUpdateAfterBind)) => this with { DescriptorBindingSampledImageUpdateAfterBind = false },
var n when Is(n, nameof(DescriptorBindingUpdateUnusedWhilePending)) => this with { DescriptorBindingUpdateUnusedWhilePending = false },
var n when Is(n, nameof(DescriptorBindingVariableDescriptorCount)) => this with { DescriptorBindingVariableDescriptorCount = false },
var n when Is(n, nameof(ShaderSampledImageArrayNonUniformIndexing)) => this with { ShaderSampledImageArrayNonUniformIndexing = false },
var n when Is(n, nameof(DynamicRendering)) => this with { DynamicRendering = false },
var n when Is(n, nameof(Synchronization2)) => this with { Synchronization2 = false },
var n when Is(n, nameof(Maintenance4)) => this with { Maintenance4 = false },
_ => null,
};
static bool Is(string candidate, string name)
=> string.Equals(candidate, name, StringComparison.OrdinalIgnoreCase);
}
}
///
/// The device limits the plan asserts up front rather than discovering at draw
/// time (plan §4.1, §3.4).
///
internal sealed record VulkanDeviceLimitSupport
{
/// Must reach ; Vulkan guarantees 128.
public required uint MaxPushConstantsSize { get; init; }
/// Must reach .
public required uint MaxClipDistances { get; init; }
/// Sets 0, 1 and 2 are all bound simultaneously, so at least 3.
public required uint MaxBoundDescriptorSets { get; init; }
///
/// Must reach .
///
/// Added at slice V6g. The V6b layout declared all ten of set 0's
/// bindings dynamic and was rejected by this limit on the RX 9070 XT, which
/// allows 8 — silently, at vkCreatePipelineLayout, and only visible
/// with the validation layer loaded (plan §5.5.7 defect 1). Reading the limit
/// into the record means the gate rejects such a device at startup, in the
/// report, under the same exit-code-4 contract as every other requirement,
/// rather than the layout failing at the first pipeline.
///
public required uint MaxDescriptorSetStorageBuffersDynamic { get; init; }
/// Must reach the number of dynamic uniform bindings set 1 declares.
public required uint MaxDescriptorSetUniformBuffersDynamic { get; init; }
/// Must reach .
public required uint MaxDescriptorSetUpdateAfterBindSampledImages { get; init; }
/// Must reach for the fragment stage.
public required uint MaxPerStageDescriptorUpdateAfterBindSampledImages { get; init; }
/// The frame profiler's GPU timings need graphics-queue timestamps.
public required bool TimestampComputeAndGraphics { get; init; }
/// Ring allocations must satisfy this; getting it wrong is a driver error on Vulkan.
public required uint MinStorageBufferOffsetAlignment { get; init; }
/// As above, for the SceneLighting uniform block.
public required uint MinUniformBufferOffsetAlignment { get; init; }
/// Largest 2D image edge; the terrain atlas and composite arrays are sized against it.
public required uint MaxImageDimension2D { get; init; }
/// Highest colour sample count the framebuffer supports, as a plain count (1/2/4/8...).
public required uint MaxColorSampleCount { get; init; }
///
/// A profile that satisfies every requirement, used as the base for tests
/// and for the forced-unsupported knob. The numbers are the Vulkan 1.3
/// guaranteed minimums where a guarantee exists, and the acdream requirement
/// where it does not.
///
internal static VulkanDeviceLimitSupport Complete { get; } = new()
{
MaxPushConstantsSize = GpuBindingModel.MaxPushConstantBytes,
MaxClipDistances = GpuBindingModel.ClipPlanesPerSlot,
MaxBoundDescriptorSets = 4,
// Vulkan's guaranteed minimums. That the layout fits inside them is the
// point of slice V6g's split — see VulkanPipelineLayouts.
MaxDescriptorSetStorageBuffersDynamic = 4,
MaxDescriptorSetUniformBuffersDynamic = 8,
MaxDescriptorSetUpdateAfterBindSampledImages = GpuBindingModel.TextureTableCapacity,
MaxPerStageDescriptorUpdateAfterBindSampledImages = GpuBindingModel.TextureTableCapacity,
TimestampComputeAndGraphics = true,
MinStorageBufferOffsetAlignment = 256,
MinUniformBufferOffsetAlignment = 256,
MaxImageDimension2D = 16384,
MaxColorSampleCount = 8,
};
}
///
/// Format-level support that features alone do not prove: the swapchain colour
/// format, a depth+stencil format for #117's portal punch, and the three BC
/// blocks the DAT surfaces upload as.
///
internal sealed record VulkanFormatSupport
{
///
/// B8G8R8A8_UNORM is offered by the surface. Corrected at slice V3:
/// the renderer is plain UNORM end to end and an _SRGB swapchain
/// would apply an unwanted encode to already-display-space values.
///
public required bool SwapchainUnormFormat { get; init; }
/// The chosen depth+stencil format, or when none is usable.
public required Format DepthStencilFormat { get; init; }
/// BC1 (DXT1) sampled-image support with optimal tiling.
public required bool Bc1Sampled { get; init; }
/// BC2 (DXT3) sampled-image support with optimal tiling.
public required bool Bc2Sampled { get; init; }
/// BC3 (DXT5) sampled-image support with optimal tiling.
public required bool Bc3Sampled { get; init; }
internal static VulkanFormatSupport Complete { get; } = new()
{
SwapchainUnormFormat = true,
DepthStencilFormat = Format.D32SfloatS8Uint,
Bc1Sampled = true,
Bc2Sampled = true,
Bc3Sampled = true,
};
}
///
/// What the presentation surface offers, and what slice V5 selected from it.
/// Null on the record when the device was probed headlessly (no window), which
/// is how the capability logic stays testable without opening one.
///
internal sealed record VulkanSurfaceSupport(
bool PresentSupported,
Format SelectedFormat,
ColorSpaceKHR SelectedColorSpace,
PresentModeKHR SelectedPresentMode,
uint SelectedImageCount,
uint SelectedWidth,
uint SelectedHeight,
bool SupportsTransferSource,
IReadOnlyList AvailableFormats,
IReadOnlyList AvailablePresentModes);
///
/// One physical device as the selector sees it. Ordering and tie-breaking are
/// computed from these fields alone (plan §4.11), so the ranking is unit-tested
/// without enumerating a real instance.
///
internal sealed record VulkanPhysicalDeviceCandidate(
int Index,
string DeviceName,
PhysicalDeviceType DeviceType,
uint ApiVersion,
uint DriverVersion,
uint VendorId,
uint DeviceId,
ulong DeviceLocalHeapBytes);
///
/// Result of the active Vulkan probe. Mirrors
/// GraphicalFunctionProbeResult: advertisement is not evidence, so the
/// probe really creates the device, the descriptor layouts, an offscreen target,
/// and a submitted command buffer, then reads pixels back.
///
internal sealed record VulkanFunctionProbeResult(
bool DeviceCreation,
bool DescriptorIndexingLayout,
bool PushConstantLayout,
bool DynamicRenderingClear,
bool TimelineSemaphoreWait,
bool HostQueryReset,
bool OffscreenReadback,
IReadOnlyList Failures)
{
internal static VulkanFunctionProbeResult NotRun { get; } = new(
false,
false,
false,
false,
false,
false,
false,
["active Vulkan device probe did not run"]);
}
///
/// Campaign V slice V5 — the Vulkan sibling of
/// GraphicalCapabilityRecord. Passive capture plus the active probe
/// result plus the derived failure list, written atomically to
/// graphical-capabilities-vulkan.json and turned into the same
/// → exit-code-4 contract.
///
internal sealed record VulkanCapabilityRecord(
DateTimeOffset CapturedAtUtc,
string RuntimeIdentifier,
GraphicalHostOperatingSystem OperatingSystem,
GraphicalDisplayProtocol RequestedDisplayProtocol,
GraphicalDisplayProtocol ActiveDisplayProtocol,
string InstanceApiVersion,
string DeviceApiVersion,
uint DeviceApiVersionPacked,
string DeviceName,
string DriverInfo,
PhysicalDeviceType DeviceType,
int SelectedDeviceIndex,
string DeviceSelectionReason,
string? RequestedDeviceOverride,
string? ForcedUnsupportedFeature,
IReadOnlyList AvailableDevices,
IReadOnlyList InstanceExtensions,
IReadOnlyList DeviceExtensions,
uint GraphicsQueueFamily,
uint PresentQueueFamily,
VulkanDeviceFeatureSupport Features,
VulkanDeviceLimitSupport Limits,
VulkanFormatSupport Formats,
VulkanSurfaceSupport? Surface,
VulkanFunctionProbeResult FunctionProbe,
IReadOnlyList SupportFailures)
{
internal bool IsSupported => SupportFailures.Count == 0;
///
/// Project onto the backend-neutral contract the renderers consult. The
/// alignment fields carry across verbatim because ring allocations are
/// validated against them, and SupportsPersistentlyMappedRings is
/// unconditionally true: writing per-frame data straight into mapped memory
/// is the mechanism behind Campaign V's CPU-cost target (plan §4.3).
///
internal GpuCapabilityRecord ToGpuCapabilityRecord() => new()
{
Backend = GpuBackendKind.Vulkan,
DeviceName = DeviceName,
DriverInfo = DriverInfo,
ApiVersion = DeviceApiVersion,
MaxTextureTableSlots =
Math.Min(
Limits.MaxDescriptorSetUpdateAfterBindSampledImages,
Limits.MaxPerStageDescriptorUpdateAfterBindSampledImages),
// Sets 0..2 give each binding its own namespace, so the storage
// bindings the model declares (nine, since Campaign V slice V11
// deleted the GL-only StorageTextureTable binding) are always all
// available once the set count requirement passes. There is no
// per-set binding-count limit in Vulkan below
// maxPerStageDescriptorStorageBuffers, which is far higher.
MaxStorageBufferBindings = GpuBindingModel.StorageBindingCount,
MaxPushConstantBytes = Limits.MaxPushConstantsSize,
MinStorageBufferOffsetAlignment = Limits.MinStorageBufferOffsetAlignment,
MinUniformBufferOffsetAlignment = Limits.MinUniformBufferOffsetAlignment,
MaxClipDistances = Limits.MaxClipDistances,
MaxSampleCount = Limits.MaxColorSampleCount,
SupportsMultiDrawIndirect = Features.MultiDrawIndirect,
SupportsDrawParameters = Features.ShaderDrawParameters,
SupportsTextureCompressionBc = Features.TextureCompressionBc,
SupportsTimestampQueries = Limits.TimestampComputeAndGraphics,
SupportsPersistentlyMappedRings = true,
};
}
///
/// Turns a captured into operator-facing
/// failure sentences. Deliberately the exact shape of
/// GraphicalCapabilityRequirements.Evaluate, and deliberately NOT
/// carrying its stale sRGB-framebuffer requirement forward: slice V3 established
/// that the renderer never enables sRGB encoding anywhere (plan §4.10).
///
internal static class VulkanCapabilityRequirements
{
/// Vulkan 1.3 is the floor; nothing below it is considered.
internal const uint RequiredApiMajor = 1;
/// Vulkan 1.3 is the floor; nothing below it is considered.
internal const uint RequiredApiMinor = 3;
internal static IReadOnlyList Evaluate(VulkanCapabilityRecord capabilities)
{
ArgumentNullException.ThrowIfNull(capabilities);
var failures = new List();
uint major = VulkanApiVersion.Major(capabilities.DeviceApiVersionPacked);
uint minor = VulkanApiVersion.Minor(capabilities.DeviceApiVersionPacked);
if (major < RequiredApiMajor || (major == RequiredApiMajor && minor < RequiredApiMinor))
{
failures.Add(
$"Vulkan {RequiredApiMajor}.{RequiredApiMinor} is required; " +
$"the selected device reports {major}.{minor}.");
}
VulkanDeviceFeatureSupport features = capabilities.Features;
if (!features.MultiDrawIndirect)
failures.Add("multiDrawIndirect is required to submit world geometry.");
if (!features.DrawIndirectFirstInstance)
failures.Add("drawIndirectFirstInstance is required; indirect commands carry a per-group instance base.");
if (!features.ShaderDrawParameters)
failures.Add("shaderDrawParameters (gl_DrawID) is required to select per-draw batch data.");
if (!features.ShaderClipDistance)
failures.Add("shaderClipDistance is required by the per-cell clip gate.");
if (!features.TextureCompressionBc)
failures.Add("textureCompressionBC is required to upload DAT surfaces without transcoding.");
if (!features.SamplerAnisotropy)
failures.Add("samplerAnisotropy is required for sampler-quality parity.");
if (!features.TimelineSemaphore)
failures.Add("timelineSemaphore is required; the frame serial is the semaphore value.");
if (!features.HostQueryReset)
failures.Add("hostQueryReset is required to reset timestamp pools from the CPU.");
if (!features.RuntimeDescriptorArray)
failures.Add("runtimeDescriptorArray is required by the global texture table.");
if (!features.DescriptorBindingPartiallyBound)
failures.Add("descriptorBindingPartiallyBound is required; unregistered texture slots are legitimately absent.");
if (!features.DescriptorBindingSampledImageUpdateAfterBind)
failures.Add("descriptorBindingSampledImageUpdateAfterBind is required to register textures without rebuilding the set.");
if (!features.DescriptorBindingUpdateUnusedWhilePending)
failures.Add("descriptorBindingUpdateUnusedWhilePending is required to recycle texture slots while frames are in flight.");
if (!features.DescriptorBindingVariableDescriptorCount)
failures.Add("descriptorBindingVariableDescriptorCount is required to size the texture table.");
if (!features.ShaderSampledImageArrayNonUniformIndexing)
failures.Add("shaderSampledImageArrayNonUniformIndexing is required; one indirect dispatch reads different texture slots per draw.");
if (!features.DynamicRendering)
failures.Add("dynamicRendering is required; the frame uses no render-pass or framebuffer objects.");
if (!features.Synchronization2)
failures.Add("synchronization2 is required; every barrier in the frame is a barrier2.");
if (!features.Maintenance4)
failures.Add("maintenance4 is required for the relaxed shader interface rules the shared GLSL relies on.");
VulkanDeviceLimitSupport limits = capabilities.Limits;
if (limits.MaxPushConstantsSize < GpuBindingModel.PushConstantBytes)
{
failures.Add(
$"{GpuBindingModel.PushConstantBytes} push-constant bytes are required; " +
$"this device provides {limits.MaxPushConstantsSize}.");
}
if (limits.MaxClipDistances < GpuBindingModel.ClipPlanesPerSlot)
{
failures.Add(
$"{GpuBindingModel.ClipPlanesPerSlot} clip distances are required by the per-cell clip gate; " +
$"this device provides {limits.MaxClipDistances}.");
}
if (limits.MaxBoundDescriptorSets < VulkanDescriptorSetCount)
{
failures.Add(
$"{VulkanDescriptorSetCount} simultaneously bound descriptor sets are required " +
$"(storage, uniform, texture table); this device provides {limits.MaxBoundDescriptorSets}.");
}
if (limits.MaxDescriptorSetStorageBuffersDynamic < VulkanPipelineLayouts.DynamicStorageBindingCount)
{
failures.Add(
$"set 0 declares {VulkanPipelineLayouts.DynamicStorageBindingCount} dynamic storage bindings " +
$"(Vulkan guarantees 4); this device provides {limits.MaxDescriptorSetStorageBuffersDynamic}.");
}
if (limits.MaxDescriptorSetUniformBuffersDynamic < VulkanFrameBindings.DynamicUniformBindingCount)
{
failures.Add(
$"set 1 declares {VulkanFrameBindings.DynamicUniformBindingCount} dynamic uniform bindings " +
$"(Vulkan guarantees 8); this device provides {limits.MaxDescriptorSetUniformBuffersDynamic}.");
}
if (limits.MaxDescriptorSetUpdateAfterBindSampledImages < GpuBindingModel.TextureTableCapacity)
{
failures.Add(
$"the texture table needs {GpuBindingModel.TextureTableCapacity} update-after-bind sampled images; " +
$"this device provides {limits.MaxDescriptorSetUpdateAfterBindSampledImages} per set.");
}
if (limits.MaxPerStageDescriptorUpdateAfterBindSampledImages < GpuBindingModel.TextureTableCapacity)
{
failures.Add(
$"the texture table needs {GpuBindingModel.TextureTableCapacity} update-after-bind sampled images " +
$"in the fragment stage; this device provides {limits.MaxPerStageDescriptorUpdateAfterBindSampledImages}.");
}
if (!limits.TimestampComputeAndGraphics)
failures.Add("graphics-queue timestamps are required by the frame profiler.");
VulkanFormatSupport formats = capabilities.Formats;
if (!formats.SwapchainUnormFormat)
{
failures.Add(
"the presentation surface must offer B8G8R8A8_UNORM; the renderer is " +
"plain UNORM end to end and an sRGB swapchain would re-encode every frame.");
}
if (formats.DepthStencilFormat == Format.Undefined)
failures.Add("a combined depth+stencil format is required by the portal aperture punch.");
if (!formats.Bc1Sampled || !formats.Bc2Sampled || !formats.Bc3Sampled)
failures.Add("BC1, BC2 and BC3 sampled-image support is required to upload DAT surfaces.");
if (capabilities.Surface is { } surface)
{
if (!surface.PresentSupported)
failures.Add("the selected device cannot present to the window surface.");
if (!surface.SupportsTransferSource)
failures.Add("the swapchain must support TRANSFER_SRC usage for screenshot capture.");
}
if (capabilities.FunctionProbe.Failures.Count != 0)
{
failures.AddRange(
capabilities.FunctionProbe.Failures.Select(
failure => $"Vulkan device probe: {failure}"));
}
else
{
if (!capabilities.FunctionProbe.DeviceCreation)
failures.Add("the Vulkan device-creation probe did not pass.");
if (!capabilities.FunctionProbe.DescriptorIndexingLayout)
failures.Add("the descriptor-indexing layout probe did not pass.");
if (!capabilities.FunctionProbe.PushConstantLayout)
failures.Add("the push-constant pipeline-layout probe did not pass.");
if (!capabilities.FunctionProbe.DynamicRenderingClear)
failures.Add("the dynamic-rendering clear probe did not pass.");
if (!capabilities.FunctionProbe.TimelineSemaphoreWait)
failures.Add("the timeline-semaphore wait probe did not pass.");
if (!capabilities.FunctionProbe.HostQueryReset)
failures.Add("the host query-reset probe did not pass.");
if (!capabilities.FunctionProbe.OffscreenReadback)
failures.Add("the offscreen readback probe did not return the expected pixels.");
}
return failures;
}
///
/// Sets 0 (storage), 1 (uniform) and 2 (texture table) are bound at once,
/// so maxBoundDescriptorSets must reach 3. Vulkan guarantees 4.
///
internal const uint VulkanDescriptorSetCount =
GpuBindingModel.TextureTableSet + 1;
///
/// Re-derive after any
/// mutation. Mirrors GraphicalCapabilityProbe.WithFunctionProbe: the
/// record is never trusted to carry a stale failure list.
///
internal static VulkanCapabilityRecord Reevaluate(VulkanCapabilityRecord capabilities)
{
ArgumentNullException.ThrowIfNull(capabilities);
VulkanCapabilityRecord cleared = capabilities with { SupportFailures = [] };
return cleared with { SupportFailures = Evaluate(cleared) };
}
///
/// Apply ACDREAM_VULKAN_FORCE_UNSUPPORTED. An unrecognised name is a
/// hard failure rather than a silent no-op: a gate knob that quietly does
/// nothing would report a pass the operator did not actually get.
///
internal static VulkanCapabilityRecord ApplyForcedUnsupported(
VulkanCapabilityRecord capabilities,
string? featureName)
{
ArgumentNullException.ThrowIfNull(capabilities);
if (string.IsNullOrWhiteSpace(featureName))
return capabilities;
VulkanDeviceFeatureSupport? forced = capabilities.Features.Without(featureName);
if (forced is null)
{
return Reevaluate(
capabilities with
{
ForcedUnsupportedFeature = featureName,
FunctionProbe = capabilities.FunctionProbe with
{
Failures =
[
.. capabilities.FunctionProbe.Failures,
$"ACDREAM_VULKAN_FORCE_UNSUPPORTED named '{featureName}', " +
"which is not a required Vulkan feature.",
],
},
});
}
return Reevaluate(
capabilities with
{
Features = forced,
ForcedUnsupportedFeature = featureName,
});
}
}
/// Packed VK_MAKE_API_VERSION arithmetic, kept out of the interop layer so it is testable.
internal static class VulkanApiVersion
{
internal static uint Major(uint packed) => (packed >> 22) & 0x7Fu;
internal static uint Minor(uint packed) => (packed >> 12) & 0x3FFu;
internal static uint Patch(uint packed) => packed & 0xFFFu;
internal static uint Make(uint major, uint minor, uint patch)
=> (major << 22) | (minor << 12) | patch;
internal static string Describe(uint packed)
=> $"Vulkan {Major(packed)}.{Minor(packed)}.{Patch(packed)}";
}
///
/// The same throw/format/report contract the GL gate publishes: an unsupported
/// device raises , which Program.cs
/// turns into exit code 4 next to the written report.
///
internal static class VulkanCapabilityGuard
{
/// File name of the Vulkan report, beside the GL one in the diagnostics directory.
internal const string ReportFileName = "graphical-capabilities-vulkan.json";
internal static void ThrowIfUnsupported(
VulkanCapabilityRecord capabilities,
string reportPath)
{
ArgumentNullException.ThrowIfNull(capabilities);
if (!capabilities.IsSupported)
throw new NotSupportedException(FormatUnsupportedMessage(capabilities, reportPath));
}
internal static string FormatUnsupportedMessage(
VulkanCapabilityRecord capabilities,
string reportPath)
{
ArgumentNullException.ThrowIfNull(capabilities);
ArgumentException.ThrowIfNullOrWhiteSpace(reportPath);
return
"acdream's Vulkan renderer is unsupported by the selected device.\n" +
$"Platform: {capabilities.RuntimeIdentifier}, " +
$"{capabilities.ActiveDisplayProtocol}, " +
$"{capabilities.DeviceName} ({capabilities.DeviceType}), " +
$"{capabilities.DeviceApiVersion}, {capabilities.DriverInfo}\n" +
string.Join(
"\n",
capabilities.SupportFailures.Select(failure => $" - {failure}")) +
$"\nFull capability report: {Path.GetFullPath(reportPath)}";
}
}
///
/// Atomic JSON writer, byte-for-byte the same temp-then-move contract as
/// GraphicalCapabilityReportWriter so a crashed launch never leaves a
/// half-written report behind.
///
internal static class VulkanCapabilityReportWriter
{
private static readonly JsonSerializerOptions Options = new()
{
WriteIndented = true,
Converters =
{
new JsonStringEnumConverter(),
},
};
internal static void Write(string path, VulkanCapabilityRecord capabilities)
{
ArgumentException.ThrowIfNullOrWhiteSpace(path);
ArgumentNullException.ThrowIfNull(capabilities);
string fullPath = Path.GetFullPath(path);
string? directory = Path.GetDirectoryName(fullPath);
if (!string.IsNullOrEmpty(directory))
Directory.CreateDirectory(directory);
string temporaryPath = fullPath + ".tmp";
File.WriteAllText(temporaryPath, Serialize(capabilities));
File.Move(temporaryPath, fullPath, overwrite: true);
}
/// Exposed so the report's shape can be asserted without touching the file system.
internal static string Serialize(VulkanCapabilityRecord capabilities)
{
ArgumentNullException.ThrowIfNull(capabilities);
return JsonSerializer.Serialize(capabilities, Options);
}
}