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);
}