diff --git a/src/Ryujinx.Common/GraphicsDriver/VkDiagProbe.cs b/src/Ryujinx.Common/GraphicsDriver/VkDiagProbe.cs new file mode 100644 index 000000000..474745909 --- /dev/null +++ b/src/Ryujinx.Common/GraphicsDriver/VkDiagProbe.cs @@ -0,0 +1,150 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Common.GraphicsDriver +{ + /// + /// Forces the NVIDIA driver to initialize its CUDA interop state, by creating and destroying + /// a throwaway Vulkan device that requests VK_NVX_binary_import. + /// + /// This must run before the guest address space is reserved (guest process creation): once + /// that reservation exists, cuInit fails with CUDA_ERROR_OUT_OF_MEMORY and the driver + /// refuses the CUDA-backed device extensions (VK_NVX_binary_import, VK_NV_optical_flow) for + /// the rest of the process lifetime, which kills DLSS. Initialized early, the CUDA state + /// stays valid for the whole session. + /// + /// Raw P/Invoke on purpose: it runs before any Silk.NET instance exists. + /// + public static class VkDiagProbe + { + [StructLayout(LayoutKind.Sequential)] + private struct InstanceCreateInfo + { + public int SType; public IntPtr PNext; public uint Flags; public IntPtr PApplicationInfo; + public uint EnabledLayerCount; public IntPtr PpEnabledLayerNames; + public uint EnabledExtensionCount; public IntPtr PpEnabledExtensionNames; + } + + [StructLayout(LayoutKind.Sequential)] + private struct DeviceQueueCreateInfo + { + public int SType; public IntPtr PNext; public uint Flags; + public uint QueueFamilyIndex; public uint QueueCount; public IntPtr PQueuePriorities; + } + + [StructLayout(LayoutKind.Sequential)] + private struct DeviceCreateInfo + { + public int SType; public IntPtr PNext; public uint Flags; + public uint QueueCreateInfoCount; public IntPtr PQueueCreateInfos; + public uint EnabledLayerCount; public IntPtr PpEnabledLayerNames; + public uint EnabledExtensionCount; public IntPtr PpEnabledExtensionNames; + public IntPtr PEnabledFeatures; + } + + [DllImport("vulkan-1.dll")] private static extern int vkCreateInstance(ref InstanceCreateInfo ci, IntPtr alloc, out IntPtr instance); + [DllImport("vulkan-1.dll")] private static extern void vkDestroyInstance(IntPtr instance, IntPtr alloc); + [DllImport("vulkan-1.dll")] private static extern int vkEnumeratePhysicalDevices(IntPtr instance, ref uint count, IntPtr[] devices); + [DllImport("vulkan-1.dll")] private static extern void vkGetPhysicalDeviceProperties(IntPtr dev, IntPtr props); + [DllImport("vulkan-1.dll")] private static extern int vkCreateDevice(IntPtr physDev, ref DeviceCreateInfo ci, IntPtr alloc, out IntPtr device); + [DllImport("vulkan-1.dll")] private static extern void vkDestroyDevice(IntPtr device, IntPtr alloc); + + /// + /// Runs the warm-up and returns a result string for the caller to log. + /// Never throws; a machine without an NVIDIA device reports that instead. + /// + public static string WarmUpCudaInterop() + { + try + { + return Probe(); + } + catch (Exception e) + { + return $"threw {e.GetType().Name}: {e.Message}"; + } + } + + private static string Probe() + { + var ici = new InstanceCreateInfo { SType = 1 }; + int r = vkCreateInstance(ref ici, IntPtr.Zero, out IntPtr instance); + if (r != 0) + { + return $"vkCreateInstance failed: {r}"; + } + + try + { + uint count = 0; + vkEnumeratePhysicalDevices(instance, ref count, null); + IntPtr[] devs = new IntPtr[count]; + vkEnumeratePhysicalDevices(instance, ref count, devs); + + IntPtr nvidia = IntPtr.Zero; + IntPtr props = Marshal.AllocHGlobal(4096); + + try + { + foreach (IntPtr d in devs) + { + vkGetPhysicalDeviceProperties(d, props); + if ((uint)Marshal.ReadInt32(props, 8) == 0x10DE) + { + nvidia = d; + } + } + } + finally + { + Marshal.FreeHGlobal(props); + } + + if (nvidia == IntPtr.Zero) + { + return "no NVIDIA device"; + } + + return ProbeDevice(nvidia); + } + finally + { + vkDestroyInstance(instance, IntPtr.Zero); + } + } + + private static string ProbeDevice(IntPtr nvidia) + { + IntPtr priority = Marshal.AllocHGlobal(4); + Marshal.Copy(new float[] { 1.0f }, 0, priority, 1); + + var qci = new DeviceQueueCreateInfo { SType = 2, QueueFamilyIndex = 0, QueueCount = 1, PQueuePriorities = priority }; + IntPtr pQci = Marshal.AllocHGlobal(Marshal.SizeOf()); + Marshal.StructureToPtr(qci, pQci, false); + + IntPtr extName = Marshal.StringToHGlobalAnsi("VK_NVX_binary_import"); + IntPtr extArr = Marshal.AllocHGlobal(IntPtr.Size); + Marshal.WriteIntPtr(extArr, 0, extName); + + var dci = new DeviceCreateInfo + { + SType = 3, QueueCreateInfoCount = 1, PQueueCreateInfos = pQci, + EnabledExtensionCount = 1, PpEnabledExtensionNames = extArr, + }; + + int dr = vkCreateDevice(nvidia, ref dci, IntPtr.Zero, out IntPtr device); + + if (dr == 0) + { + vkDestroyDevice(device, IntPtr.Zero); + } + + Marshal.FreeHGlobal(extName); + Marshal.FreeHGlobal(extArr); + Marshal.FreeHGlobal(pQci); + Marshal.FreeHGlobal(priority); + + return $"VK_NVX_binary_import alone -> {(dr == 0 ? "OK" : dr == -3 ? "ErrorInitializationFailed" : dr.ToString())}"; + } + } +} diff --git a/src/Ryujinx.Graphics.Vulkan/Dlss/Streamline.cs b/src/Ryujinx.Graphics.Vulkan/Dlss/Streamline.cs index 7f31f6054..b994da24d 100644 --- a/src/Ryujinx.Graphics.Vulkan/Dlss/Streamline.cs +++ b/src/Ryujinx.Graphics.Vulkan/Dlss/Streamline.cs @@ -161,6 +161,8 @@ namespace Ryujinx.Graphics.Vulkan.Dlss private static IntPtr _interposerHandle; private static IntPtr _logPathPtr; private static IntPtr _featuresPtr; + private static IntPtr _projectIdPtr; + private static IntPtr _engineVersionPtr; private static bool _initialized; private static bool _everInitialized; @@ -208,6 +210,17 @@ namespace Ryujinx.Graphics.Vulkan.Dlss pref.RenderApi = RenderApiVulkan; pref.Flags = PrefFlagDisableCLStateTracking; + // NGX refuses to initialize without a valid application identity ("Please provide + // correct application id when calling slInit" in sl.log), which disables DLSS + // entirely. 100721531 is the placeholder id from NVIDIA's Streamline sample; with + // that id, Streamline requires a projectId GUID and engine version for the + // NGX-with-project-id init path instead. + pref.ApplicationId = 100721531; + _projectIdPtr = Marshal.StringToHGlobalAnsi("2a90c1c2-7fbf-4626-8b57-0d9a2072b1cd"); + pref.ProjectId = _projectIdPtr; + _engineVersionPtr = Marshal.StringToHGlobalAnsi("1.0.0"); + pref.EngineVersion = _engineVersionPtr; + if (loadFrameGeneration) { pref.Flags |= PrefFlagUseManualHooking; @@ -410,6 +423,18 @@ namespace Ryujinx.Graphics.Vulkan.Dlss _featuresPtr = IntPtr.Zero; } + if (_projectIdPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_projectIdPtr); + _projectIdPtr = IntPtr.Zero; + } + + if (_engineVersionPtr != IntPtr.Zero) + { + Marshal.FreeHGlobal(_engineVersionPtr); + _engineVersionPtr = IntPtr.Zero; + } + if (_interposerHandle != IntPtr.Zero) { NativeLibrary.Free(_interposerHandle); diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs index 9b3fbb6ad..faeeeadfc 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs @@ -15,9 +15,17 @@ namespace Ryujinx.Graphics.Vulkan public unsafe static class VulkanInitialization { private const uint InvalidIndex = uint.MaxValue; + private const int InvalidDeviceIndex = -1; private static readonly uint _minimalVulkanVersion = Vk.Version11.Value; private static readonly uint _minimalInstanceVulkanVersion = Vk.Version12.Value; - private static readonly uint _maximumVulkanVersion = Vk.Version12.Value; + // VK_MAKE_API_VERSION(0, 1, 3, 0). Opt-in through RYUJINX_VK_API13, because some NVIDIA + // extensions are refused when the instance only requests Vulkan 1.2. + private const uint VulkanVersion13 = (1u << 22) | (3u << 12); + + private static readonly uint _maximumVulkanVersion = + Environment.GetEnvironmentVariable("RYUJINX_VK_API13") is "1" or "true" or "on" + ? VulkanVersion13 + : Vk.Version12.Value; private const string AppName = "Ryujinx.Graphics.Vulkan"; private const int QueuesCount = 2; @@ -136,6 +144,10 @@ namespace Ryujinx.Graphics.Vulkan result.ThrowOnError(); + Logger.Info?.Print(LogClass.Gpu, + $"Vulkan instance: requested API {(_maximumVulkanVersion >> 22) & 0x7F}.{(_maximumVulkanVersion >> 12) & 0x3FF}, " + + $"loader reports {(instance.InstanceVersion.Value >> 22) & 0x7F}.{(instance.InstanceVersion.Value >> 12) & 0x3FF}."); + return instance; } @@ -148,22 +160,93 @@ namespace Ryujinx.Graphics.Vulkan { if (IsPreferredAndSuitableDevice(api, physicalDevices[i], surface, preferredGpuId)) { + Logger.Info?.Print(LogClass.Gpu, + $"GPU selection: using preferred device \"{physicalDevices[i].DeviceName}\" ({physicalDevices[i].Id})."); + return physicalDevices[i]; } } - // If we fail to do that, just use the first compatible GPU. + if (!string.IsNullOrEmpty(preferredGpuId)) + { + LogPreferredDeviceSkipped(physicalDevices, preferredGpuId); + } + + // If we fail to do that, prefer a discrete GPU over an integrated one, so that a + // machine with both does not silently fall back to the integrated device. + int firstSuitable = InvalidDeviceIndex; + for (int i = 0; i < physicalDevices.Length; i++) { - if (IsSuitableDevice(api, physicalDevices[i], surface)) + if (!IsSuitableDevice(api, physicalDevices[i], surface)) { + continue; + } + + if (physicalDevices[i].PhysicalDeviceProperties.DeviceType == PhysicalDeviceType.DiscreteGpu) + { + Logger.Info?.Print(LogClass.Gpu, + $"GPU selection: falling back to discrete device \"{physicalDevices[i].DeviceName}\" ({physicalDevices[i].Id})."); + return physicalDevices[i]; } + + if (firstSuitable == InvalidDeviceIndex) + { + firstSuitable = i; + } + } + + // No discrete GPU passed the checks, so use the first compatible GPU instead. + if (firstSuitable != InvalidDeviceIndex) + { + Logger.Warning?.Print(LogClass.Gpu, + $"GPU selection: no suitable discrete GPU found, falling back to \"{physicalDevices[firstSuitable].DeviceName}\" " + + $"({physicalDevices[firstSuitable].Id}, {physicalDevices[firstSuitable].PhysicalDeviceProperties.DeviceType})."); + + return physicalDevices[firstSuitable]; } throw new VulkanException("Initialization failed, none of the available GPUs meets the minimum requirements."); } + private static void LogPreferredDeviceSkipped(VulkanPhysicalDevice[] physicalDevices, string preferredGpuId) + { + for (int i = 0; i < physicalDevices.Length; i++) + { + if (physicalDevices[i].Id != preferredGpuId) + { + continue; + } + + string missingExtension = null; + + foreach (string requiredExtension in _requiredExtensions) + { + if (!physicalDevices[i].IsDeviceExtensionPresent(requiredExtension)) + { + missingExtension = requiredExtension; + + break; + } + } + + string why = missingExtension != null + ? $"it does not support the required extension {missingExtension}" + : "no queue family supports graphics, compute and presentation on this window's surface"; + + Logger.Warning?.Print(LogClass.Gpu, + $"GPU selection: preferred device \"{physicalDevices[i].DeviceName}\" ({preferredGpuId}) was found but rejected because {why}."); + + return; + } + + string enumerated = string.Join(", ", physicalDevices.Select(x => $"\"{x.DeviceName}\" ({x.Id})")); + + Logger.Warning?.Print(LogClass.Gpu, + $"GPU selection: preferred device {preferredGpuId} was not found among the {physicalDevices.Length} enumerated device(s): {enumerated}."); + } + internal static DeviceInfo[] GetSuitablePhysicalDevices(Vk api) { nint appName = Marshal.StringToHGlobalAnsi(AppName); @@ -719,6 +802,10 @@ namespace Ryujinx.Graphics.Vulkan PhysicalDeviceSynchronization2Features featuresSync2; PhysicalDeviceOpticalFlowFeaturesNV featuresOpticalFlow; + // Remember the chain head before the DLSS-only feature structs are linked in, so that + // the staged fallback in CreateDevice can retry without them. + void* pFeaturesBaseline = pExtendedFeatures; + if (sync2Supported) { featuresSync2 = new() @@ -731,6 +818,10 @@ namespace Ryujinx.Graphics.Vulkan pExtendedFeatures = &featuresSync2; } + // Remember the chain head before the optical flow feature is linked in, so that the + // fallback in CreateDevice can drop that feature without rebuilding the whole chain. + void* pFeaturesWithoutOpticalFlow = pExtendedFeatures; + if (opticalFlowSupported) { featuresOpticalFlow = new() @@ -771,40 +862,145 @@ namespace Ryujinx.Graphics.Vulkan $"featureQuery: sync2Supported={sync2Supported}, opticalFlowSupported={opticalFlowSupported}."); } - nint* ppEnabledExtensions = stackalloc nint[enabledExtensions.Length]; + Logger.Info?.Print(LogClass.Gpu, + $"Device creation: requesting {enabledExtensions.Length} extension(s): {string.Join(", ", enabledExtensions)}."); - for (int i = 0; i < enabledExtensions.Length; i++) + Result TryCreateDevice( + string[] extensions, + void* pNextChain, + uint queueInfoCount, + DeviceQueueCreateInfo* queues, + PhysicalDeviceFeatures* enabledFeatures, + out Device created) { - ppEnabledExtensions[i] = Marshal.StringToHGlobalAnsi(enabledExtensions[i]); + nint* ppExtensions = stackalloc nint[extensions.Length]; + + for (int i = 0; i < extensions.Length; i++) + { + ppExtensions[i] = Marshal.StringToHGlobalAnsi(extensions[i]); + } + + DeviceCreateInfo createInfo = new() + { + SType = StructureType.DeviceCreateInfo, + PNext = pNextChain, + QueueCreateInfoCount = queueInfoCount, + PQueueCreateInfos = queues, + PpEnabledExtensionNames = (byte**)ppExtensions, + EnabledExtensionCount = (uint)extensions.Length, + PEnabledFeatures = enabledFeatures, + }; + + Result attempt = api.CreateDevice(physicalDevice.PhysicalDevice, in createInfo, null, out created); + + for (int i = 0; i < extensions.Length; i++) + { + Marshal.FreeHGlobal(ppExtensions[i]); + } + + return attempt; } - DeviceCreateInfo deviceCreateInfo = new() - { - SType = StructureType.DeviceCreateInfo, - PNext = pExtendedFeatures, - QueueCreateInfoCount = useOpticalFlowQueue ? 2u : 1u, - PQueueCreateInfos = queueCreateInfos, - PpEnabledExtensionNames = (byte**)ppEnabledExtensions, - EnabledExtensionCount = (uint)enabledExtensions.Length, - PEnabledFeatures = &features, - }; + Result result = TryCreateDevice( + enabledExtensions, + pExtendedFeatures, + useOpticalFlowQueue ? 2u : 1u, + queueCreateInfos, + &features, + out Device device); - Result result = api.CreateDevice(physicalDevice.PhysicalDevice, in deviceCreateInfo, null, out Device device); - - if (result != Result.Success && useOpticalFlowQueue) + // Stage 2: drop the dedicated optical-flow queue, the opticalFlow feature and the + // VK_NV_optical_flow extension together. + if (result != Result.Success && (useOpticalFlowQueue || opticalFlowSupported)) { Logger.Warning?.Print(LogClass.Gpu, - $"NVOFA: device creation with the dedicated optical-flow queue failed ({result}); retrying without it. " + + $"Device creation failed ({result}) with optical flow enabled; retrying with the dedicated queue, " + + "the opticalFlow feature and the VK_NV_optical_flow extension all removed. " + "DLSS and Frame Generation stay on; hardware optical flow (NvOpticalFlow) is disabled."); opticalFlowQueueFamilyIndex = uint.MaxValue; - deviceCreateInfo.QueueCreateInfoCount = 1; - result = api.CreateDevice(physicalDevice.PhysicalDevice, in deviceCreateInfo, null, out device); + + result = TryCreateDevice( + enabledExtensions.Where(x => x != "VK_NV_optical_flow").ToArray(), + pFeaturesWithoutOpticalFlow, + 1, + queueCreateInfos, + &features, + out device); + + if (result == Result.Success) + { + Logger.Info?.Print(LogClass.Gpu, + "Device creation: succeeded once optical flow was removed, so the driver rejected the dedicated " + + "optical-flow queue, the opticalFlow feature or the VK_NV_optical_flow extension."); + } } - for (int i = 0; i < enabledExtensions.Length; i++) + // Stage 3: probe each extension DLSS adds on its own, then keep every one the driver + // accepts. This preserves as much DLSS capability as the driver allows, instead of + // dropping the whole set because one member of it is refused. + if (result != Result.Success && Dlss.DlssIntegration.IsEnabled) { - Marshal.FreeHGlobal(ppEnabledExtensions[i]); + string[] baselineExtensions = enabledExtensions.Except(Dlss.DlssIntegration.DeviceExtensions).ToArray(); + string[] dlssExtensions = enabledExtensions.Except(baselineExtensions).ToArray(); + + Logger.Warning?.Print(LogClass.Gpu, + $"Device creation still failed ({result}); probing each of the {dlssExtensions.Length} DLSS device " + + "extensions on its own to find which one the driver refuses."); + + List accepted = []; + + foreach (string candidate in dlssExtensions) + { + Result probe = TryCreateDevice( + baselineExtensions.Append(candidate).ToArray(), + pFeaturesBaseline, + 1, + queueCreateInfos, + &features, + out Device probeDevice); + + Logger.Info?.Print(LogClass.Gpu, + $"Device creation probe: baseline + {candidate} -> {(probe == Result.Success ? "OK" : probe.ToString())}."); + + if (probe == Result.Success) + { + accepted.Add(candidate); + api.DestroyDevice(probeDevice, null); + } + } + + string[] rejected = dlssExtensions.Except(accepted).ToArray(); + + Logger.Warning?.Print(LogClass.Gpu, + $"Device creation: the driver refuses {rejected.Length} extension(s) [{string.Join(", ", rejected)}]; " + + $"retrying with the {accepted.Count} accepted one(s) kept [{string.Join(", ", accepted)}]."); + + opticalFlowQueueFamilyIndex = uint.MaxValue; + + // Keep the synchronization2 feature only when its extension survived the probe. + bool keepSync2 = sync2Supported && accepted.Contains("VK_KHR_synchronization2"); + + result = TryCreateDevice( + baselineExtensions.Concat(accepted).ToArray(), + keepSync2 ? pFeaturesWithoutOpticalFlow : pFeaturesBaseline, + 1, + queueCreateInfos, + &features, + out device); + + if (result == Result.Success) + { + Logger.Warning?.Print(LogClass.Gpu, + $"Device creation: succeeded without [{string.Join(", ", rejected)}]. " + + "DLSS features that depend on those extensions will not be available in this session."); + } + else + { + Logger.Error?.Print(LogClass.Gpu, + $"Device creation: still failed ({result}) even with only the accepted extensions, so the cause " + + "lies outside the extensions DLSS adds."); + } } result.ThrowOnError(); diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs index f385db21b..a9ac95893 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs @@ -136,6 +136,17 @@ namespace Ryujinx.Graphics.Vulkan if (IsMoltenVk = OperatingSystem.IsMacOS()) MVKInitialization.Initialize(); + // The renderer is created before the guest address space is reserved. The NVIDIA + // driver's CUDA interop must initialize before that reservation exists, or it fails + // with CUDA_ERROR_OUT_OF_MEMORY and vkCreateDevice then refuses VK_NVX_binary_import + // and VK_NV_optical_flow (both CUDA-backed) for the rest of the session, which kills + // DLSS. Warmed up here, the state stays valid after the reservation. + if (Dlss.DlssIntegration.IsEnabled && OperatingSystem.IsWindows()) + { + string warmUp = Ryujinx.Common.GraphicsDriver.VkDiagProbe.WarmUpCudaInterop(); + Logger.Info?.Print(LogClass.Gpu, $"DLSS: CUDA interop warm-up: {warmUp}."); + } + SupportsMTL31 = OperatingSystem.IsMacOSVersionAtLeast(14); }