Fix DLSS on Windows: CUDA interop warm-up and NGX application identity #6

Open
sajittarius wants to merge 2 commits from sajittarius/the-beast-roofer-edition:dlss-cuda-warmup-and-ngx-identity into master
4 changed files with 406 additions and 24 deletions
@@ -0,0 +1,150 @@
using System;
using System.Runtime.InteropServices;
namespace Ryujinx.Common.GraphicsDriver
{
/// <summary>
/// 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.
/// </summary>
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);
/// <summary>
/// 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.
/// </summary>
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<DeviceQueueCreateInfo>());
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())}";
}
}
}
@@ -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);
@@ -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 deviceCreateInfo = new()
DeviceCreateInfo createInfo = new()
{
SType = StructureType.DeviceCreateInfo,
PNext = pExtendedFeatures,
QueueCreateInfoCount = useOpticalFlowQueue ? 2u : 1u,
PQueueCreateInfos = queueCreateInfos,
PpEnabledExtensionNames = (byte**)ppEnabledExtensions,
EnabledExtensionCount = (uint)enabledExtensions.Length,
PEnabledFeatures = &features,
PNext = pNextChain,
QueueCreateInfoCount = queueInfoCount,
PQueueCreateInfos = queues,
PpEnabledExtensionNames = (byte**)ppExtensions,
EnabledExtensionCount = (uint)extensions.Length,
PEnabledFeatures = enabledFeatures,
};
Result result = api.CreateDevice(physicalDevice.PhysicalDevice, in deviceCreateInfo, null, out Device device);
Result attempt = api.CreateDevice(physicalDevice.PhysicalDevice, in createInfo, null, out created);
if (result != Result.Success && useOpticalFlowQueue)
for (int i = 0; i < extensions.Length; i++)
{
Marshal.FreeHGlobal(ppExtensions[i]);
}
return attempt;
}
Result result = TryCreateDevice(
enabledExtensions,
pExtendedFeatures,
useOpticalFlowQueue ? 2u : 1u,
queueCreateInfos,
&features,
out Device device);
// 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<string> 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();
@@ -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);
}