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