From 298ef01809301072706e6fac21e0fa8f1866320a Mon Sep 17 00:00:00 2001 From: Spooks <62370103+Spooks4576@users.noreply.github.com> Date: Mon, 13 Jul 2026 06:05:59 -0600 Subject: [PATCH] [VideoOut] Prefer NVIDIA/discrete GPUs over integrated (#97) SelectPhysicalDevice took the first device exposing a graphics+present queue. On hybrid-graphics laptops that is the integrated GPU, so the discrete card went unused - and AMD's integrated driver access-violates inside vkCreateGraphicsPipelines while compiling some translated guest shaders, killing the process. The CLR surfaced that native AV as "Invalid Program: attempted to call a UnmanagedCallersOnly method from managed code", which made it look like a CPU/threading fault. Score the candidates instead: NVIDIA parts win, other discrete GPUs come next, and an integrated GPU is only chosen when nothing else can present. SHARPEMU_VK_DEVICE= pins a specific adapter, and the selected device is logged. --- .../VideoOut/VulkanVideoPresenter.cs | 63 +++++++++++++++++-- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs index 9a6f279..5707818 100644 --- a/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs +++ b/src/SharpEmu.Libs/VideoOut/VulkanVideoPresenter.cs @@ -178,6 +178,7 @@ internal static unsafe class VulkanVideoPresenter private static uint _windowHeight; private static bool _closed; private const string DebugUtilsExtensionName = "VK_EXT_debug_utils"; + private const uint NvidiaVendorId = 0x10DE; private static bool _splashHidden; private static long _enqueuedGuestWorkSequence; private static long _completedGuestWorkSequence; @@ -1502,6 +1503,13 @@ internal static unsafe class VulkanVideoPresenter Check(_vk.EnumeratePhysicalDevices(_instance, &deviceCount, devicePointer), "vkEnumeratePhysicalDevices"); } + // Hybrid laptops enumerate the iGPU first, and AMD's integrated driver + // segfaults compiling some translated shaders, so rank rather than take + // the first hit. SHARPEMU_VK_DEVICE= pins an adapter by name. + var deviceOverride = Environment.GetEnvironmentVariable("SHARPEMU_VK_DEVICE"); + var bestScore = int.MinValue; + var found = false; + foreach (var device in devices) { uint queueCount = 0; @@ -1521,13 +1529,60 @@ internal static unsafe class VulkanVideoPresenter continue; } - _physicalDevice = device; - _queueFamilyIndex = index; - return; + _vk.GetPhysicalDeviceProperties(device, out var properties); + var name = SilkMarshal.PtrToString((nint)properties.DeviceName) ?? string.Empty; + var score = ScorePhysicalDevice(properties, name, deviceOverride); + Console.Error.WriteLine( + $"[LOADER][INFO] Vulkan candidate: {name} ({properties.DeviceType}) score={score}"); + + if (score > bestScore) + { + bestScore = score; + _physicalDevice = device; + _queueFamilyIndex = index; + found = true; + } + + break; } } - throw new InvalidOperationException("No Vulkan graphics/present queue was found."); + if (!found) + { + throw new InvalidOperationException("No Vulkan graphics/present queue was found."); + } + + _vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected); + Console.Error.WriteLine( + $"[LOADER][INFO] Vulkan device: {SilkMarshal.PtrToString((nint)selected.DeviceName)} ({selected.DeviceType})"); + } + + 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, + // Last resort: only picked when nothing else can present. + PhysicalDeviceType.IntegratedGpu => -100, + _ => 10, + }; + + if (properties.VendorID == NvidiaVendorId) + { + score += 500; + } + + return score; } private void CreateDevice()