using System.Globalization;
using Silk.NET.Vulkan;
namespace AcDream.App.Rendering.Gpu.Vk;
/// The chosen device plus the sentence explaining why, which the report records verbatim.
internal sealed record VulkanPhysicalDeviceChoice(
VulkanPhysicalDeviceCandidate Device,
string Reason);
///
/// Campaign V slice V5, plan §4.11: "discrete > integrated > virtual >
/// CPU, tie-broken by largest device-local heap, with an
/// ACDREAM_VULKAN_DEVICE override recorded in the report."
///
/// Pure ranking over values so the
/// policy is unit-tested without enumerating a real instance — which matters
/// because this machine has exactly one GPU and the ordering it exercises is
/// therefore never the interesting case.
///
internal static class VulkanPhysicalDeviceSelection
{
///
/// Preference rank; lower is better. Vulkan's
/// numbering is Other(0) < Integrated(1) < Discrete(2) < Virtual(3)
/// < Cpu(4), which is neither our order nor a monotone one, so it is
/// mapped explicitly rather than compared numerically.
///
internal static int PreferenceRank(PhysicalDeviceType type) => type switch
{
PhysicalDeviceType.DiscreteGpu => 0,
PhysicalDeviceType.IntegratedGpu => 1,
PhysicalDeviceType.VirtualGpu => 2,
PhysicalDeviceType.Cpu => 3,
_ => 4,
};
///
/// Choose a device.
///
/// is an enumeration index when it is
/// entirely decimal digits, and a case-insensitive device-name substring
/// otherwise. The split is exact rather than "try index, then fall back to
/// substring" because a bare digit is a substring of most real device names —
/// 7 occurs in "AMD Radeon RX 9070 XT" — so a fall-through would make
/// an out-of-range index quietly select a device by coincidence. A name that
/// genuinely contains digits ("RX 7900 XTX") still matches, because it is not
/// digits alone.
///
/// An override that matches nothing falls back to the automatic choice
/// and says so in the reason: refusing to start over a stale environment
/// variable is worse than starting on the right GPU anyway.
///
internal static VulkanPhysicalDeviceChoice? Choose(
IReadOnlyList candidates,
string? deviceOverride)
{
ArgumentNullException.ThrowIfNull(candidates);
if (candidates.Count == 0)
return null;
if (!string.IsNullOrWhiteSpace(deviceOverride))
{
string trimmed = deviceOverride.Trim();
if (IsDecimalIndex(trimmed))
{
int index = int.Parse(trimmed, NumberStyles.None, CultureInfo.InvariantCulture);
VulkanPhysicalDeviceCandidate? byIndex =
candidates.FirstOrDefault(candidate => candidate.Index == index);
if (byIndex is not null)
{
return new VulkanPhysicalDeviceChoice(
byIndex,
$"ACDREAM_VULKAN_DEVICE={trimmed} selected device index {index}.");
}
}
else
{
VulkanPhysicalDeviceCandidate? byName = candidates.FirstOrDefault(
candidate => candidate.DeviceName.Contains(
trimmed,
StringComparison.OrdinalIgnoreCase));
if (byName is not null)
{
return new VulkanPhysicalDeviceChoice(
byName,
$"ACDREAM_VULKAN_DEVICE={trimmed} matched device name '{byName.DeviceName}'.");
}
}
VulkanPhysicalDeviceCandidate automatic = Rank(candidates);
return new VulkanPhysicalDeviceChoice(
automatic,
$"ACDREAM_VULKAN_DEVICE={trimmed} matched no enumerated device; " +
$"fell back to the automatic choice '{automatic.DeviceName}' " +
$"({automatic.DeviceType}, {Gib(automatic.DeviceLocalHeapBytes)} device-local).");
}
VulkanPhysicalDeviceCandidate chosen = Rank(candidates);
return new VulkanPhysicalDeviceChoice(
chosen,
$"automatic: '{chosen.DeviceName}' ({chosen.DeviceType}, " +
$"{Gib(chosen.DeviceLocalHeapBytes)} device-local) ranked first of " +
$"{candidates.Count} enumerated device(s).");
}
///
/// Deterministic ordering: type preference, then largest device-local heap,
/// then enumeration index. The final index tie-break exists so two identical
/// GPUs always produce the same choice across launches.
///
internal static VulkanPhysicalDeviceCandidate Rank(
IReadOnlyList candidates)
{
ArgumentNullException.ThrowIfNull(candidates);
if (candidates.Count == 0)
throw new ArgumentException("At least one candidate is required.", nameof(candidates));
VulkanPhysicalDeviceCandidate best = candidates[0];
for (int i = 1; i < candidates.Count; i++)
{
if (Compare(candidates[i], best) < 0)
best = candidates[i];
}
return best;
}
/// Negative when is the better device.
internal static int Compare(
VulkanPhysicalDeviceCandidate left,
VulkanPhysicalDeviceCandidate right)
{
ArgumentNullException.ThrowIfNull(left);
ArgumentNullException.ThrowIfNull(right);
int byType = PreferenceRank(left.DeviceType).CompareTo(PreferenceRank(right.DeviceType));
if (byType != 0)
return byType;
int byHeap = right.DeviceLocalHeapBytes.CompareTo(left.DeviceLocalHeapBytes);
return byHeap != 0 ? byHeap : left.Index.CompareTo(right.Index);
}
/// An index override is decimal digits and nothing else.
internal static bool IsDecimalIndex(string value)
{
if (string.IsNullOrEmpty(value))
return false;
foreach (char character in value)
{
if (character is < '0' or > '9')
return false;
}
return true;
}
private static string Gib(ulong bytes)
=> (bytes / (1024d * 1024d * 1024d)).ToString("0.##", CultureInfo.InvariantCulture) + " GiB";
}
///
/// Which queue family carries graphics and which carries present. Slice V5 uses
/// one graphics+present queue with transfers riding it (plan §4.8); a device
/// whose present-capable family is separate is still supported, and the
/// swapchain then declares concurrent sharing.
///
internal sealed record VulkanQueueFamilyChoice(
uint GraphicsFamily,
uint PresentFamily)
{
/// True when one queue serves both, which is the case on every GPU we target.
internal bool IsUnified => GraphicsFamily == PresentFamily;
}
/// One enumerated queue family, reduced to the two facts the selector needs.
internal readonly record struct VulkanQueueFamilyCandidate(
uint Index,
bool SupportsGraphics,
bool SupportsPresent);
///
/// Pure queue-family selection. Prefers a single family that does both, because
/// that removes the concurrent-sharing declaration and the ownership transfers
/// that would otherwise be needed on every swapchain image.
///
internal static class VulkanQueueFamilySelection
{
internal static VulkanQueueFamilyChoice? Choose(
IReadOnlyList families)
{
ArgumentNullException.ThrowIfNull(families);
foreach (VulkanQueueFamilyCandidate family in families)
{
if (family.SupportsGraphics && family.SupportsPresent)
return new VulkanQueueFamilyChoice(family.Index, family.Index);
}
uint? graphics = null;
uint? present = null;
foreach (VulkanQueueFamilyCandidate family in families)
{
if (graphics is null && family.SupportsGraphics)
graphics = family.Index;
if (present is null && family.SupportsPresent)
present = family.Index;
}
return graphics is { } g && present is { } p
? new VulkanQueueFamilyChoice(g, p)
: null;
}
///
/// The headless variant used by the offscreen capability probe, which has no
/// surface and therefore no present requirement.
///
internal static uint? ChooseGraphicsOnly(
IReadOnlyList families)
{
ArgumentNullException.ThrowIfNull(families);
foreach (VulkanQueueFamilyCandidate family in families)
{
if (family.SupportsGraphics)
return family.Index;
}
return null;
}
}