From 45d51ccf347bafd57cebe8e1d491ddfa7a73c575 Mon Sep 17 00:00:00 2001 From: sajittarius Date: Wed, 26 Aug 2026 20:30:04 -0400 Subject: [PATCH 1/2] Fix DLSS: CUDA interop warm-up and NGX application identity DLSS never worked on Windows: vkCreateDevice refused VK_NVX_binary_import and VK_NV_optical_flow with ErrorInitializationFailed, and NGX refused to initialize even when the extensions were present. Root cause 1: once guest process creation (Device.LoadNsp) reserves the guest address space, cuInit fails with CUDA_ERROR_OUT_OF_MEMORY for the rest of the process lifetime, so the driver refuses the two CUDA/OFA- backed device extensions. CUDA state initialized before the reservation stays valid all session. Fix: the VulkanRenderer constructor (which runs before game load) creates and destroys a throwaway device requesting VK_NVX_binary_import when DLSS is enabled, forcing CUDA init first. Root cause 2: Streamline.Initialize passed no application identity, and NGX disables all NGX-based features without one ("Please provide correct application id when calling slInit" in sl.log). Fix: pass the Streamline sample ApplicationId plus a ProjectId GUID and EngineVersion, selecting the NGX project-id init path. Also in this change: - Staged device-creation fallback: retry without optical flow, then probe each DLSS device extension individually and keep the accepted ones, instead of crashing on ErrorInitializationFailed. - GPU selection: prefer a discrete GPU in the fallback path and log why a preferred device was skipped. - VkDiagProbe: raw P/Invoke probe used for the warm-up, plus lifecycle diagnostic checkpoints gated on RYUJINX_VK_DIAG (off by default), appending to vkdiag.txt next to the exe. Verified end to end on an RTX 4090 (driver 616.56): all 30 device extensions accepted, dedicated optical-flow queue acquired, DLSS reports available, and DLSS4 transformer upscaling runs in-game. Co-Authored-By: Claude Fable 5 --- .../GraphicsDriver/VkDiagProbe.cs | 216 +++++++++++++ .../Dlss/Streamline.cs | 25 ++ .../VulkanInitialization.cs | 289 ++++++++++++++++-- src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 11 + src/Ryujinx/Program.cs | 4 + src/Ryujinx/Systems/AppHost.cs | 6 + 6 files changed, 527 insertions(+), 24 deletions(-) create mode 100644 src/Ryujinx.Common/GraphicsDriver/VkDiagProbe.cs diff --git a/src/Ryujinx.Common/GraphicsDriver/VkDiagProbe.cs b/src/Ryujinx.Common/GraphicsDriver/VkDiagProbe.cs new file mode 100644 index 000000000..a6bb7813c --- /dev/null +++ b/src/Ryujinx.Common/GraphicsDriver/VkDiagProbe.cs @@ -0,0 +1,216 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; + +namespace Ryujinx.Common.GraphicsDriver +{ + /// + /// Temporary diagnostic: creates a throwaway Vulkan device requesting VK_NVX_binary_import + /// at chosen points in the process lifetime, to find when the NVIDIA driver starts refusing + /// it (it refuses at game boot, but accepts in a fresh standalone process). + /// Results are appended to vkdiag.txt next to the executable. + /// Gated on RYUJINX_DLSS so normal runs are unaffected. Remove once the cause is found. + /// + 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("nvcuda.dll")] private static extern int cuInit(uint flags); + [DllImport("nvcuda.dll")] private static extern int cuDeviceGetCount(out int count); + + [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); + + public static void Run(string checkpoint) + { + RunCore(checkpoint, () => Probe()); + } + + /// + /// Forces the NVIDIA driver to initialize its CUDA interop state now, by creating and + /// destroying a throwaway device that requests VK_NVX_binary_import. Must run before the + /// guest address space is reserved: 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. + /// Initialized early, the CUDA state stays valid for the whole session. + /// Always runs (no env gate); returns a result string for the caller to log. + /// + public static string WarmUpCudaInterop() + { + try + { + return Probe(); + } + catch (Exception e) + { + return $"threw {e.GetType().Name}: {e.Message}"; + } + } + + /// + /// Probes an existing physical-device handle (e.g. Ryujinx's own) instead of creating a + /// fresh instance, so an instance-specific refusal can be told apart from a global one. + /// + public static void RunOnDevice(string checkpoint, IntPtr physicalDevice) + { + RunCore(checkpoint, () => ProbeDevice(physicalDevice)); + } + + private static void RunCore(string checkpoint, Func probe) + { + // "1" enables every checkpoint; a point key (e.g. "P2") enables just that one. + string gate = Environment.GetEnvironmentVariable("RYUJINX_VK_DIAG"); + bool enabled = gate is "1" or "true" or "on" + || (gate is not null && checkpoint.StartsWith(gate + " ", StringComparison.Ordinal)); + + if (!enabled) + { + return; + } + + string line; + + try + { + string cuda; + + try + { + int ci = cuInit(0); + int cc = cuDeviceGetCount(out int n); + cuda = $"cuInit={ci} cuDeviceGetCount={cc} devices={n}"; + } + catch (Exception e) + { + cuda = $"cuda threw {e.GetType().Name}"; + } + + line = $"{DateTime.Now:HH:mm:ss.fff} [{checkpoint}] {probe()}; {cuda}"; + } + catch (Exception e) + { + line = $"{DateTime.Now:HH:mm:ss.fff} [{checkpoint}] threw {e.GetType().Name}: {e.Message}"; + } + + try + { + File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "vkdiag.txt"), line + Environment.NewLine); + } + catch + { + // Diagnostics must never break startup. + } + } + + 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..116170881 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; @@ -57,6 +65,8 @@ namespace Ryujinx.Graphics.Vulkan internal static VulkanInstance CreateInstance(Vk api, GraphicsDebugLevel logLevel, string[] requiredExtensions) { + Ryujinx.Common.GraphicsDriver.VkDiagProbe.Run("P2 CreateInstance (render thread)"); + List enabledLayers = []; IReadOnlySet instanceExtensions = VulkanInstance.GetInstanceExtensions(api); @@ -136,6 +146,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 +162,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 +804,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 +820,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 +864,188 @@ 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(); + + // Diagnostic: the same request succeeds on a fresh raw instance in this very + // process, so compare a fresh instance against Ryujinx's own instance handles + // at the moment of failure. + Ryujinx.Common.GraphicsDriver.VkDiagProbe.Run("P3 fresh raw instance at failure time"); + Ryujinx.Common.GraphicsDriver.VkDiagProbe.RunOnDevice( + "P3b Ryujinx physicalDevice handle, raw vkCreateDevice", + physicalDevice.PhysicalDevice.Handle); + + // Diagnostic: a refused extension passes in a standalone process with no features + // enabled, so re-probe each one with progressively fewer features to find whether + // a feature (base struct or pNext chain) is what makes the driver refuse it here. + foreach (string candidate in rejected) + { + Result probeAlone = TryCreateDevice( + [candidate], null, 1, queueCreateInfos, null, out Device diagDevice); + if (probeAlone == Result.Success) + { + api.DestroyDevice(diagDevice, null); + } + + Result probeNoFeatures = TryCreateDevice( + baselineExtensions.Append(candidate).ToArray(), + null, 1, queueCreateInfos, null, out diagDevice); + if (probeNoFeatures == Result.Success) + { + api.DestroyDevice(diagDevice, null); + } + + Result probeBaseFeatures = TryCreateDevice( + baselineExtensions.Append(candidate).ToArray(), + null, 1, queueCreateInfos, &features, out diagDevice); + if (probeBaseFeatures == Result.Success) + { + api.DestroyDevice(diagDevice, null); + } + + Logger.Warning?.Print(LogClass.Gpu, + $"Device creation diag for {candidate}: alone, no features -> {probeAlone}; " + + $"baseline exts, no features -> {probeNoFeatures}; " + + $"baseline exts + base feature struct, no pNext chain -> {probeBaseFeatures}; " + + $"baseline exts + full baseline feature chain -> ErrorInitializationFailed (probed above)."); + } + + 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); } diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 1790da3e3..3faf9cdcc 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -58,6 +58,8 @@ namespace Ryujinx.Ava Version = ReleaseInformation.Version; + VkDiagProbe.Run("P0 Main start"); + if (OperatingSystem.IsWindows()) { if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041)) @@ -193,6 +195,8 @@ namespace Ryujinx.Ava Initialize(args); + VkDiagProbe.Run("P1 after Initialize"); + DlssUiSettings.ApplyAtStartup(); LoggerAdapter.Register(); diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 917f022a3..274c74aa9 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -962,6 +962,8 @@ namespace Ryujinx.Ava.Systems throw new OperationCanceledException(cts.Token); } + Ryujinx.Common.GraphicsDriver.VkDiagProbe.Run("P1.9 after game loaded"); + ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata => appMetadata.UpdatePreGame() ); @@ -1009,6 +1011,8 @@ namespace Ryujinx.Ava.Systems private void InitEmulatedSwitch() { + Ryujinx.Common.GraphicsDriver.VkDiagProbe.Run("P1.7 InitEmulatedSwitch start"); + // Initialize KeySet. VirtualFileSystem.ReloadKeySet(); @@ -1037,6 +1041,8 @@ namespace Ryujinx.Ava.Systems _viewModel.UiHandler ) ); + + Ryujinx.Common.GraphicsDriver.VkDiagProbe.Run("P1.8 after Switch created"); } private static IHardwareDeviceDriver InitializeAudio() -- 2.39.5 From b882b46e53d07c1e776e64a77e5e832b0a32ce42 Mon Sep 17 00:00:00 2001 From: sajittarius Date: Wed, 26 Aug 2026 20:33:35 -0400 Subject: [PATCH 2/2] Remove diagnostic checkpoints left from the DLSS investigation Drops the RYUJINX_VK_DIAG lifecycle probes (Program.cs, AppHost.cs, VulkanInitialization.cs) and the failure-time feature-bisect loop in CreateDevice; they existed only to locate the CUDA init failure. Strips VkDiagProbe down to the WarmUpCudaInterop path that the fix uses. The staged device-creation fallback and per-extension probe stay: they are the runtime safety net when a driver refuses an extension. Verified after cleanup: warm-up OK, optical-flow queue acquired, DLSS available on a plain launch with no env vars. Co-Authored-By: Claude Fable 5 --- .../GraphicsDriver/VkDiagProbe.cs | 142 +++++------------- .../VulkanInitialization.cs | 45 ------ src/Ryujinx/Program.cs | 4 - src/Ryujinx/Systems/AppHost.cs | 6 - 4 files changed, 38 insertions(+), 159 deletions(-) diff --git a/src/Ryujinx.Common/GraphicsDriver/VkDiagProbe.cs b/src/Ryujinx.Common/GraphicsDriver/VkDiagProbe.cs index a6bb7813c..474745909 100644 --- a/src/Ryujinx.Common/GraphicsDriver/VkDiagProbe.cs +++ b/src/Ryujinx.Common/GraphicsDriver/VkDiagProbe.cs @@ -1,15 +1,19 @@ using System; -using System.IO; using System.Runtime.InteropServices; namespace Ryujinx.Common.GraphicsDriver { /// - /// Temporary diagnostic: creates a throwaway Vulkan device requesting VK_NVX_binary_import - /// at chosen points in the process lifetime, to find when the NVIDIA driver starts refusing - /// it (it refuses at game boot, but accepts in a fresh standalone process). - /// Results are appended to vkdiag.txt next to the executable. - /// Gated on RYUJINX_DLSS so normal runs are unaffected. Remove once the cause is found. + /// 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 { @@ -38,9 +42,6 @@ namespace Ryujinx.Common.GraphicsDriver public IntPtr PEnabledFeatures; } - [DllImport("nvcuda.dll")] private static extern int cuInit(uint flags); - [DllImport("nvcuda.dll")] private static extern int cuDeviceGetCount(out int count); - [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); @@ -48,19 +49,9 @@ namespace Ryujinx.Common.GraphicsDriver [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); - public static void Run(string checkpoint) - { - RunCore(checkpoint, () => Probe()); - } - /// - /// Forces the NVIDIA driver to initialize its CUDA interop state now, by creating and - /// destroying a throwaway device that requests VK_NVX_binary_import. Must run before the - /// guest address space is reserved: 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. - /// Initialized early, the CUDA state stays valid for the whole session. - /// Always runs (no env gate); returns a result string for the caller to log. + /// 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() { @@ -74,61 +65,6 @@ namespace Ryujinx.Common.GraphicsDriver } } - /// - /// Probes an existing physical-device handle (e.g. Ryujinx's own) instead of creating a - /// fresh instance, so an instance-specific refusal can be told apart from a global one. - /// - public static void RunOnDevice(string checkpoint, IntPtr physicalDevice) - { - RunCore(checkpoint, () => ProbeDevice(physicalDevice)); - } - - private static void RunCore(string checkpoint, Func probe) - { - // "1" enables every checkpoint; a point key (e.g. "P2") enables just that one. - string gate = Environment.GetEnvironmentVariable("RYUJINX_VK_DIAG"); - bool enabled = gate is "1" or "true" or "on" - || (gate is not null && checkpoint.StartsWith(gate + " ", StringComparison.Ordinal)); - - if (!enabled) - { - return; - } - - string line; - - try - { - string cuda; - - try - { - int ci = cuInit(0); - int cc = cuDeviceGetCount(out int n); - cuda = $"cuInit={ci} cuDeviceGetCount={cc} devices={n}"; - } - catch (Exception e) - { - cuda = $"cuda threw {e.GetType().Name}"; - } - - line = $"{DateTime.Now:HH:mm:ss.fff} [{checkpoint}] {probe()}; {cuda}"; - } - catch (Exception e) - { - line = $"{DateTime.Now:HH:mm:ss.fff} [{checkpoint}] threw {e.GetType().Name}: {e.Message}"; - } - - try - { - File.AppendAllText(Path.Combine(AppContext.BaseDirectory, "vkdiag.txt"), line + Environment.NewLine); - } - catch - { - // Diagnostics must never break startup. - } - } - private static string Probe() { var ici = new InstanceCreateInfo { SType = 1 }; @@ -179,38 +115,36 @@ namespace Ryujinx.Common.GraphicsDriver 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 { - IntPtr priority = Marshal.AllocHGlobal(4); - Marshal.Copy(new float[] { 1.0f }, 0, priority, 1); + SType = 3, QueueCreateInfoCount = 1, PQueueCreateInfos = pQci, + EnabledExtensionCount = 1, PpEnabledExtensionNames = extArr, + }; - var qci = new DeviceQueueCreateInfo { SType = 2, QueueFamilyIndex = 0, QueueCount = 1, PQueuePriorities = priority }; - IntPtr pQci = Marshal.AllocHGlobal(Marshal.SizeOf()); - Marshal.StructureToPtr(qci, pQci, false); + int dr = vkCreateDevice(nvidia, ref dci, IntPtr.Zero, out IntPtr device); - 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())}"; + 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/VulkanInitialization.cs b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs index 116170881..faeeeadfc 100644 --- a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs +++ b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs @@ -65,8 +65,6 @@ namespace Ryujinx.Graphics.Vulkan internal static VulkanInstance CreateInstance(Vk api, GraphicsDebugLevel logLevel, string[] requiredExtensions) { - Ryujinx.Common.GraphicsDriver.VkDiagProbe.Run("P2 CreateInstance (render thread)"); - List enabledLayers = []; IReadOnlySet instanceExtensions = VulkanInstance.GetInstanceExtensions(api); @@ -974,49 +972,6 @@ namespace Ryujinx.Graphics.Vulkan string[] rejected = dlssExtensions.Except(accepted).ToArray(); - // Diagnostic: the same request succeeds on a fresh raw instance in this very - // process, so compare a fresh instance against Ryujinx's own instance handles - // at the moment of failure. - Ryujinx.Common.GraphicsDriver.VkDiagProbe.Run("P3 fresh raw instance at failure time"); - Ryujinx.Common.GraphicsDriver.VkDiagProbe.RunOnDevice( - "P3b Ryujinx physicalDevice handle, raw vkCreateDevice", - physicalDevice.PhysicalDevice.Handle); - - // Diagnostic: a refused extension passes in a standalone process with no features - // enabled, so re-probe each one with progressively fewer features to find whether - // a feature (base struct or pNext chain) is what makes the driver refuse it here. - foreach (string candidate in rejected) - { - Result probeAlone = TryCreateDevice( - [candidate], null, 1, queueCreateInfos, null, out Device diagDevice); - if (probeAlone == Result.Success) - { - api.DestroyDevice(diagDevice, null); - } - - Result probeNoFeatures = TryCreateDevice( - baselineExtensions.Append(candidate).ToArray(), - null, 1, queueCreateInfos, null, out diagDevice); - if (probeNoFeatures == Result.Success) - { - api.DestroyDevice(diagDevice, null); - } - - Result probeBaseFeatures = TryCreateDevice( - baselineExtensions.Append(candidate).ToArray(), - null, 1, queueCreateInfos, &features, out diagDevice); - if (probeBaseFeatures == Result.Success) - { - api.DestroyDevice(diagDevice, null); - } - - Logger.Warning?.Print(LogClass.Gpu, - $"Device creation diag for {candidate}: alone, no features -> {probeAlone}; " + - $"baseline exts, no features -> {probeNoFeatures}; " + - $"baseline exts + base feature struct, no pNext chain -> {probeBaseFeatures}; " + - $"baseline exts + full baseline feature chain -> ErrorInitializationFailed (probed above)."); - } - 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)}]."); diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 3faf9cdcc..1790da3e3 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -58,8 +58,6 @@ namespace Ryujinx.Ava Version = ReleaseInformation.Version; - VkDiagProbe.Run("P0 Main start"); - if (OperatingSystem.IsWindows()) { if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041)) @@ -195,8 +193,6 @@ namespace Ryujinx.Ava Initialize(args); - VkDiagProbe.Run("P1 after Initialize"); - DlssUiSettings.ApplyAtStartup(); LoggerAdapter.Register(); diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 274c74aa9..917f022a3 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -962,8 +962,6 @@ namespace Ryujinx.Ava.Systems throw new OperationCanceledException(cts.Token); } - Ryujinx.Common.GraphicsDriver.VkDiagProbe.Run("P1.9 after game loaded"); - ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText, appMetadata => appMetadata.UpdatePreGame() ); @@ -1011,8 +1009,6 @@ namespace Ryujinx.Ava.Systems private void InitEmulatedSwitch() { - Ryujinx.Common.GraphicsDriver.VkDiagProbe.Run("P1.7 InitEmulatedSwitch start"); - // Initialize KeySet. VirtualFileSystem.ReloadKeySet(); @@ -1041,8 +1037,6 @@ namespace Ryujinx.Ava.Systems _viewModel.UiHandler ) ); - - Ryujinx.Common.GraphicsDriver.VkDiagProbe.Run("P1.8 after Switch created"); } private static IHardwareDeviceDriver InitializeAudio() -- 2.39.5