using System.Collections.Generic;
using System.Linq;
using AcDream.App.Rendering.Gpu.Vk;
using Silk.NET.Vulkan;
namespace AcDream.App.Tests.Rendering.Gpu.Vk;
///
/// Campaign V slice V5 โ physical-device ranking and queue-family selection.
///
/// The development machine has exactly one GPU, so the ordering that actually
/// matters on a user's machine is never exercised by running the client. These
/// tests are the only coverage the policy gets, which is precisely why the
/// ranking is pure data rather than something the enumeration loop decides
/// inline.
///
public sealed class VulkanDeviceSelectionTests
{
private const ulong Gib = 1024ul * 1024ul * 1024ul;
private static VulkanPhysicalDeviceCandidate Device(
int index,
string name,
PhysicalDeviceType type,
ulong heapBytes) => new(
index,
name,
type,
ApiVersion: VulkanApiVersion.Make(1, 3, 280),
DriverVersion: 1,
VendorId: 0x1002,
DeviceId: 0x7550,
DeviceLocalHeapBytes: heapBytes);
[Fact]
public void DiscreteBeatsIntegratedBeatsVirtualBeatsCpu()
{
// Vulkan's own enum ordering is Other < Integrated < Discrete < Virtual
// < Cpu, which is neither our order nor monotone. Pin the mapping.
Assert.True(
VulkanPhysicalDeviceSelection.PreferenceRank(PhysicalDeviceType.DiscreteGpu)
< VulkanPhysicalDeviceSelection.PreferenceRank(PhysicalDeviceType.IntegratedGpu));
Assert.True(
VulkanPhysicalDeviceSelection.PreferenceRank(PhysicalDeviceType.IntegratedGpu)
< VulkanPhysicalDeviceSelection.PreferenceRank(PhysicalDeviceType.VirtualGpu));
Assert.True(
VulkanPhysicalDeviceSelection.PreferenceRank(PhysicalDeviceType.VirtualGpu)
< VulkanPhysicalDeviceSelection.PreferenceRank(PhysicalDeviceType.Cpu));
Assert.True(
VulkanPhysicalDeviceSelection.PreferenceRank(PhysicalDeviceType.Cpu)
< VulkanPhysicalDeviceSelection.PreferenceRank(PhysicalDeviceType.Other));
}
[Fact]
public void TheDiscreteGpuWinsOverALargerIntegratedHeap()
{
// An integrated GPU can report the whole of system RAM as device-local.
// Type preference must dominate the heap tie-break, not the other way up.
List candidates =
[
Device(0, "Intel Iris Xe", PhysicalDeviceType.IntegratedGpu, 32 * Gib),
Device(1, "AMD Radeon RX 9070 XT", PhysicalDeviceType.DiscreteGpu, 16 * Gib),
];
VulkanPhysicalDeviceChoice? choice =
VulkanPhysicalDeviceSelection.Choose(candidates, deviceOverride: null);
Assert.NotNull(choice);
Assert.Equal("AMD Radeon RX 9070 XT", choice.Device.DeviceName);
Assert.Contains("automatic", choice.Reason, System.StringComparison.Ordinal);
}
[Fact]
public void TwoDiscreteGpusAreBrokenByTheLargerDeviceLocalHeap()
{
List candidates =
[
Device(0, "Small Discrete", PhysicalDeviceType.DiscreteGpu, 8 * Gib),
Device(1, "Big Discrete", PhysicalDeviceType.DiscreteGpu, 24 * Gib),
];
VulkanPhysicalDeviceChoice? choice =
VulkanPhysicalDeviceSelection.Choose(candidates, deviceOverride: null);
Assert.Equal("Big Discrete", choice!.Device.DeviceName);
}
///
/// Two identical GPUs must produce the same choice on every launch, or a
/// capability report from one run does not describe the next.
///
[Fact]
public void IdenticalDevicesAreBrokenDeterministicallyByEnumerationIndex()
{
List candidates =
[
Device(0, "Twin", PhysicalDeviceType.DiscreteGpu, 16 * Gib),
Device(1, "Twin", PhysicalDeviceType.DiscreteGpu, 16 * Gib),
];
Assert.Equal(
0,
VulkanPhysicalDeviceSelection.Choose(candidates, null)!.Device.Index);
Assert.Equal(
0,
VulkanPhysicalDeviceSelection.Choose([.. candidates.AsEnumerable().Reverse()], null)!
.Device.Index);
}
[Fact]
public void CpuAndSoftwareDevicesAreLastButStillSelectable()
{
// lavapipe is the CI second implementation (plan ยง5, slice V9), so a
// CPU device must still be chosen when it is the only one present.
List candidates =
[
Device(0, "llvmpipe", PhysicalDeviceType.Cpu, 0),
];
VulkanPhysicalDeviceChoice? choice =
VulkanPhysicalDeviceSelection.Choose(candidates, deviceOverride: null);
Assert.Equal("llvmpipe", choice!.Device.DeviceName);
}
[Fact]
public void NoEnumeratedDeviceYieldsNoChoice()
{
Assert.Null(VulkanPhysicalDeviceSelection.Choose([], deviceOverride: null));
}
[Fact]
public void TheOverrideMatchesADecimalEnumerationIndex()
{
List candidates =
[
Device(0, "AMD Radeon RX 9070 XT", PhysicalDeviceType.DiscreteGpu, 16 * Gib),
Device(1, "Intel Iris Xe", PhysicalDeviceType.IntegratedGpu, 32 * Gib),
];
VulkanPhysicalDeviceChoice? choice =
VulkanPhysicalDeviceSelection.Choose(candidates, "1");
Assert.Equal("Intel Iris Xe", choice!.Device.DeviceName);
Assert.Contains("ACDREAM_VULKAN_DEVICE=1", choice.Reason, System.StringComparison.Ordinal);
}
[Theory]
[InlineData("iris")]
[InlineData("IRIS")]
[InlineData("Intel Iris Xe")]
public void TheOverrideMatchesACaseInsensitiveNameSubstring(string value)
{
List candidates =
[
Device(0, "AMD Radeon RX 9070 XT", PhysicalDeviceType.DiscreteGpu, 16 * Gib),
Device(1, "Intel Iris Xe", PhysicalDeviceType.IntegratedGpu, 32 * Gib),
];
VulkanPhysicalDeviceChoice? choice =
VulkanPhysicalDeviceSelection.Choose(candidates, value);
Assert.Equal("Intel Iris Xe", choice!.Device.DeviceName);
Assert.Contains("matched device name", choice.Reason, System.StringComparison.Ordinal);
}
///
/// A stale environment variable must not stop the client starting; it must
/// say so in the report and start on the right GPU anyway.
///
[Fact]
public void AnOverrideThatMatchesNothingFallsBackAndSaysSo()
{
List candidates =
[
Device(0, "AMD Radeon RX 9070 XT", PhysicalDeviceType.DiscreteGpu, 16 * Gib),
];
VulkanPhysicalDeviceChoice? choice =
VulkanPhysicalDeviceSelection.Choose(candidates, "GeForce");
Assert.Equal("AMD Radeon RX 9070 XT", choice!.Device.DeviceName);
Assert.Contains("matched no enumerated device", choice.Reason, System.StringComparison.Ordinal);
}
///
/// A bare digit is a substring of most real device names โ "7" occurs in
/// "AMD Radeon RX 9070 XT" โ so an all-digits override must be an index and
/// nothing else. Falling through to substring matching would make an
/// out-of-range index quietly select a device by coincidence.
///
[Fact]
public void AnOutOfRangeIndexOverrideFallsBackRatherThanMatchingADigitInADeviceName()
{
List candidates =
[
Device(0, "AMD Radeon RX 9070 XT", PhysicalDeviceType.DiscreteGpu, 16 * Gib),
];
VulkanPhysicalDeviceChoice? choice =
VulkanPhysicalDeviceSelection.Choose(candidates, "7");
Assert.Equal(0, choice!.Device.Index);
Assert.Contains("matched no enumerated device", choice.Reason, System.StringComparison.Ordinal);
}
///
/// The exact index/substring split must not break names that legitimately
/// contain digits: "RX 7900" is not digits alone, so it is a name.
///
[Fact]
public void ANameContainingDigitsStillMatchesAsASubstring()
{
List candidates =
[
Device(0, "AMD Radeon RX 9070 XT", PhysicalDeviceType.DiscreteGpu, 24 * Gib),
Device(1, "AMD Radeon RX 7900 XTX", PhysicalDeviceType.DiscreteGpu, 16 * Gib),
];
VulkanPhysicalDeviceChoice? choice =
VulkanPhysicalDeviceSelection.Choose(candidates, "RX 7900");
Assert.Equal("AMD Radeon RX 7900 XTX", choice!.Device.DeviceName);
}
[Theory]
[InlineData("0", true)]
[InlineData("12", true)]
[InlineData("", false)]
[InlineData("-1", false)]
[InlineData("7900 XTX", false)]
[InlineData("Radeon", false)]
public void OnlyAnAllDigitsOverrideIsTreatedAsAnIndex(string value, bool expected)
{
Assert.Equal(expected, VulkanPhysicalDeviceSelection.IsDecimalIndex(value));
}
[Fact]
public void OneFamilyThatDoesBothIsPreferredOverASplitPair()
{
List families =
[
new(0, SupportsGraphics: true, SupportsPresent: false),
new(1, SupportsGraphics: false, SupportsPresent: true),
new(2, SupportsGraphics: true, SupportsPresent: true),
];
VulkanQueueFamilyChoice? choice = VulkanQueueFamilySelection.Choose(families);
Assert.NotNull(choice);
Assert.True(choice.IsUnified);
Assert.Equal(2u, choice.GraphicsFamily);
Assert.Equal(2u, choice.PresentFamily);
}
[Fact]
public void ASplitDeviceStillProducesAUsablePair()
{
List families =
[
new(0, SupportsGraphics: true, SupportsPresent: false),
new(1, SupportsGraphics: false, SupportsPresent: true),
];
VulkanQueueFamilyChoice? choice = VulkanQueueFamilySelection.Choose(families);
Assert.NotNull(choice);
Assert.False(choice.IsUnified);
Assert.Equal(0u, choice.GraphicsFamily);
Assert.Equal(1u, choice.PresentFamily);
}
[Fact]
public void ADeviceWithNoPresentCapableFamilyYieldsNoChoice()
{
List families =
[
new(0, SupportsGraphics: true, SupportsPresent: false),
];
Assert.Null(VulkanQueueFamilySelection.Choose(families));
// The headless probe has no surface and so no present requirement.
Assert.Equal(0u, VulkanQueueFamilySelection.ChooseGraphicsOnly(families));
}
[Fact]
public void AComputeOnlyDeviceHasNoGraphicsFamily()
{
List families =
[
new(0, SupportsGraphics: false, SupportsPresent: false),
];
Assert.Null(VulkanQueueFamilySelection.ChooseGraphicsOnly(families));
}
[Fact]
public void RequiredExtensionsMustBeAdvertisedButOptionalOnesMayBeAbsent()
{
VulkanExtensionPlan plan = VulkanExtensionSelection.Resolve(
available: ["VK_KHR_surface", "VK_KHR_win32_surface", "VK_EXT_debug_utils"],
required: ["VK_KHR_surface", "VK_KHR_win32_surface"],
optional:
[
VulkanExtensionSelection.DebugUtilsExtension,
VulkanExtensionSelection.MemoryBudgetExtension,
]);
Assert.True(plan.IsSatisfied);
Assert.Contains("VK_KHR_surface", plan.Enabled);
Assert.Contains(VulkanExtensionSelection.DebugUtilsExtension, plan.Enabled);
Assert.Contains(VulkanExtensionSelection.MemoryBudgetExtension, plan.UnavailableOptional);
Assert.Empty(plan.MissingRequired);
}
[Fact]
public void AMissingRequiredExtensionIsReportedByName()
{
VulkanExtensionPlan plan = VulkanExtensionSelection.Resolve(
available: ["VK_KHR_surface"],
required: ["VK_KHR_surface", VulkanExtensionSelection.SwapchainExtension],
optional: []);
Assert.False(plan.IsSatisfied);
Assert.Equal(
[VulkanExtensionSelection.SwapchainExtension],
plan.MissingRequired);
}
[Fact]
public void AnExtensionNamedBothRequiredAndOptionalIsEnabledOnce()
{
VulkanExtensionPlan plan = VulkanExtensionSelection.Resolve(
available: [VulkanExtensionSelection.DebugUtilsExtension],
required: [VulkanExtensionSelection.DebugUtilsExtension],
optional: [VulkanExtensionSelection.DebugUtilsExtension]);
Assert.Single(plan.Enabled);
}
}