diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index b9853c72..be3dbbf1 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -271,6 +271,10 @@ internal static unsafe class VulkanVideoPresenter private static bool _presenterCloseRequested; private const string DebugUtilsExtensionName = "VK_EXT_debug_utils"; private const uint NvidiaVendorId = 0x10DE; + private const uint AmdVendorId = 0x1002; + // Other GPU PCI vendor IDs, for reference when adding future rules: + // Intel 0x8086, Apple 0x106B, Qualcomm 0x5143 (Windows-on-ARM), Microsoft software 0x1414. + private const int LastResortPenalty = 1000; private const string PortabilityEnumerationExtensionName = "VK_KHR_portability_enumeration"; private const string PortabilitySubsetExtensionName = "VK_KHR_portability_subset"; private const int GlfwPlatformHint = 0x00050003; @@ -279,6 +283,50 @@ internal static unsafe class VulkanVideoPresenter private const int GlfwPlatformWayland = 0x00060003; private const int GlfwPlatformX11 = 0x00060004; private const int GlfwPlatformNull = 0x00060005; + + internal static int ScorePhysicalDevice( + PhysicalDeviceProperties properties, + string name, + string? deviceOverride) + { + if (!string.IsNullOrWhiteSpace(deviceOverride)) + { + return name.Contains(deviceOverride, StringComparison.OrdinalIgnoreCase) ? 1000 : -1000; + } + + var score = properties.DeviceType switch + { + PhysicalDeviceType.DiscreteGpu => 300, + PhysicalDeviceType.VirtualGpu => 100, + PhysicalDeviceType.IntegratedGpu => 50, + PhysicalDeviceType.Cpu => 20, + _ => 10, + }; + + if (properties.VendorID == NvidiaVendorId) + { + score += 500; + } + + score -= ComputeDevicePenalty(properties, OperatingSystem.IsWindows()); + + return score; + } + + internal static int ComputeDevicePenalty(PhysicalDeviceProperties properties, bool isWindows) + { + var penalty = 0; + + if (isWindows && + properties.DeviceType == PhysicalDeviceType.IntegratedGpu && + properties.VendorID == AmdVendorId) + { + penalty += LastResortPenalty; + } + + return penalty; + } + private static bool _splashHidden; private static long _enqueuedGuestWorkSequence; // Largest contiguous completed sequence, retained for compact diagnostics. @@ -3200,33 +3248,6 @@ internal static unsafe class VulkanVideoPresenter _window.Title = VideoOutExports.GetWindowTitle(); } - private static int ScorePhysicalDevice( - PhysicalDeviceProperties properties, - string name, - string? deviceOverride) - { - if (!string.IsNullOrWhiteSpace(deviceOverride)) - { - return name.Contains(deviceOverride, StringComparison.OrdinalIgnoreCase) ? 1000 : -1000; - } - - var score = properties.DeviceType switch - { - PhysicalDeviceType.DiscreteGpu => 300, - PhysicalDeviceType.VirtualGpu => 100, - PhysicalDeviceType.Cpu => 50, - PhysicalDeviceType.IntegratedGpu => -100, - _ => 10, - }; - - if (properties.VendorID == NvidiaVendorId) - { - score += 500; - } - - return score; - } - private void LoadComputeDeviceLimits() { _vk.GetPhysicalDeviceProperties(_physicalDevice, out var properties); diff --git a/tests/SharpEmu.Libs.Tests/VideoOut/VulkanPhysicalDeviceScoringTests.cs b/tests/SharpEmu.Libs.Tests/VideoOut/VulkanPhysicalDeviceScoringTests.cs new file mode 100644 index 00000000..d515ff76 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/VideoOut/VulkanPhysicalDeviceScoringTests.cs @@ -0,0 +1,94 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using SharpEmu.Libs.VideoOut; +using Silk.NET.Vulkan; +using Xunit; + +namespace SharpEmu.Libs.Tests.VideoOut; + +public sealed class VulkanPhysicalDeviceScoringTests +{ + private const uint NvidiaVendorId = 0x10DE; + private const uint AmdVendorId = 0x1002; + private const uint IntelVendorId = 0x8086; + private const uint QualcommVendorId = 0x5143; + + private static PhysicalDeviceProperties Device(PhysicalDeviceType type, uint vendorId = 0) + => new() { DeviceType = type, VendorID = vendorId }; + + private static int Score(PhysicalDeviceType type, uint vendorId = 0) + => VulkanVideoPresenter.ScorePhysicalDevice(Device(type, vendorId), name: string.Empty, deviceOverride: null); + + [Fact] + public void RealIntegratedGpuOutranksSoftwareRasterizer() + { + Assert.True(Score(PhysicalDeviceType.IntegratedGpu) > Score(PhysicalDeviceType.Cpu)); + } + + [Theory] + [InlineData(IntelVendorId)] + [InlineData(QualcommVendorId)] + [InlineData(0u)] + public void NonAmdIntegratedGpuBeatsSoftware(uint vendorId) + { + Assert.True(Score(PhysicalDeviceType.IntegratedGpu, vendorId) > Score(PhysicalDeviceType.Cpu)); + } + + [Fact] + public void DiscreteGpuOutranksIntegratedGpu() + { + Assert.True(Score(PhysicalDeviceType.DiscreteGpu) > Score(PhysicalDeviceType.IntegratedGpu)); + } + + [Fact] + public void NvidiaDiscreteGpuScoresHighest() + { + var nvidia = Score(PhysicalDeviceType.DiscreteGpu, NvidiaVendorId); + Assert.True(nvidia > Score(PhysicalDeviceType.DiscreteGpu)); + Assert.True(nvidia > Score(PhysicalDeviceType.IntegratedGpu)); + } + + [Fact] + public void DeviceOverridePinsMatchingAdapterAndExcludesOthers() + { + var properties = Device(PhysicalDeviceType.Cpu); + Assert.Equal(1000, VulkanVideoPresenter.ScorePhysicalDevice(properties, "Radeon Graphics", "radeon")); + Assert.Equal(-1000, VulkanVideoPresenter.ScorePhysicalDevice(properties, "Radeon Graphics", "nvidia")); + } + + [Fact] + public void AmdIntegratedGpuIsLastResortOnWindows() + { + var penalty = VulkanVideoPresenter.ComputeDevicePenalty( + Device(PhysicalDeviceType.IntegratedGpu, AmdVendorId), isWindows: true); + Assert.True(penalty > 0); + Assert.True(50 - penalty < 10); + } + + [Fact] + public void AmdApuIsNotPenalizedOffWindows() + { + var penalty = VulkanVideoPresenter.ComputeDevicePenalty( + Device(PhysicalDeviceType.IntegratedGpu, AmdVendorId), isWindows: false); + Assert.Equal(0, penalty); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void AmdDiscreteGpuIsNeverPenalized(bool isWindows) + { + Assert.Equal(0, VulkanVideoPresenter.ComputeDevicePenalty( + Device(PhysicalDeviceType.DiscreteGpu, AmdVendorId), isWindows)); + } + + [Theory] + [InlineData(IntelVendorId)] + [InlineData(QualcommVendorId)] + public void NonAmdIntegratedGpuIsNeverPenalized(uint vendorId) + { + Assert.Equal(0, VulkanVideoPresenter.ComputeDevicePenalty( + Device(PhysicalDeviceType.IntegratedGpu, vendorId), isWindows: true)); + } +}