diff --git a/osu.Desktop/LowLatency/NVAPIDirect3D11LowLatencyProvider.cs b/osu.Desktop/LowLatency/NVAPIDirect3D11LowLatencyProvider.cs new file mode 100644 index 000000000000..c533def7750f --- /dev/null +++ b/osu.Desktop/LowLatency/NVAPIDirect3D11LowLatencyProvider.cs @@ -0,0 +1,89 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#pragma warning disable IDE1006 // Naming rule violation + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Versioning; +using osu.Framework.Graphics.Rendering.LowLatency; + +namespace osu.Desktop.LowLatency +{ + /// + /// Provider for NVIDIA's NVAPI (Reflex) low latency features. + /// + [SuppressMessage("ReSharper", "InconsistentNaming")] + [SupportedOSPlatform("windows")] + internal sealed class NVAPIDirect3D11LowLatencyProvider : IDirect3D11LowLatencyProvider + { + public bool IsAvailable { get; private set; } + + private IntPtr _deviceHandle; + + /// + /// Initialize the NVAPI low latency provider with a native device handle. Ensure NVAPI is available before calling this method. + /// + /// An to the handle of the D3D11 device. + /// Throws an exception if NVAPI is unavailable, or the device handle provided was invalid. + public void Initialize(IntPtr nativeDeviceHandle) + { + _deviceHandle = nativeDeviceHandle; + IsAvailable = NVAPI.Available && _deviceHandle != IntPtr.Zero; + + if (!IsAvailable) + throw new InvalidOperationException("NVAPI is not available or the provided device handle is invalid."); + } + + /// + /// Set the low latency mode. + /// + /// The to use. + /// Throws an exception if an attempt to set the low latency mode was unsuccessful. + public void SetMode(LatencyMode mode) + { + if (!IsAvailable || _deviceHandle == IntPtr.Zero) + return; + + bool enable = mode != LatencyMode.Off; + bool boost = mode == LatencyMode.Boost; + var status = NVAPI.SetSleepModeHelper(_deviceHandle, enable, boost, boost, 0); + + if (status != NvStatus.OK) + throw new InvalidOperationException($"Failed to set NVAPI low latency (Sleep) mode: {status}"); + } + + /// + /// Set a latency marker for the current frame. + /// + /// WARNING: Do not log any errors that come from this method, they should be ignored as this method runs in a realtime environment. + /// The to set. + /// The frame number this marker is for. + /// Throws an exception if the attempt to set the marker was unsuccessful. Please ensure this exception is ignored. + public void SetMarker(LatencyMarker marker, ulong frameId) + { + if (!IsAvailable || _deviceHandle == IntPtr.Zero) + return; + + var status = NVAPI.SetLatencyMarkerHelper(_deviceHandle, (uint)marker, frameId); + + if (status != NvStatus.OK) + throw new InvalidOperationException($"Failed to set NVAPI latency marker: {status}"); + } + + /// + /// Ensure this is called once per frame, at the start of the Update phase, to allow NVAPI to manage frame sleep timing. + /// + /// Throws an exception if the Sleep attempt was unsuccessful. + public void FrameSleep() + { + if (!IsAvailable || _deviceHandle == IntPtr.Zero) + return; + + var status = NVAPI.FrameSleepHelper(_deviceHandle); + + if (status != NvStatus.OK) + throw new InvalidOperationException($"Failed to perform NVAPI frame sleep: {status}"); + } + } +} diff --git a/osu.Desktop/NVAPI.cs b/osu.Desktop/NVAPI.cs index fd372cddc582..79ffefc21e5d 100644 --- a/osu.Desktop/NVAPI.cs +++ b/osu.Desktop/NVAPI.cs @@ -104,6 +104,21 @@ internal static class NVAPI private static readonly SaveSettingsDelegate SaveSettings; + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate NvStatus SetSleepModeDelegate(IntPtr pDevice, ref NvSetSleepModeParams pParams); + + private static readonly SetSleepModeDelegate SetSleepMode; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate NvStatus SetLatencyMarkerDelegate(IntPtr pDevice, ref NvFrameMarkerParams pParams); + + private static readonly SetLatencyMarkerDelegate SetLatencyMarker; + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate NvStatus SleepDelegate(IntPtr pDevice); + + private static readonly SleepDelegate Sleep; + public static NvStatus Status { get; private set; } = NvStatus.OK; public static bool Available { get; private set; } @@ -338,6 +353,36 @@ private static bool setSetting(NvSettingID settingId, uint settingValue) return !checkError(SaveSettings(sessionHandle), nameof(SaveSettings)); } + internal static NvStatus SetSleepModeHelper(IntPtr devicePtr, bool enable, bool boost, bool useMarkersOptimize, uint minimumIntervalUs) + { + if (!Available) + return NvStatus.NO_IMPLEMENTATION; + + NvSetSleepModeParams sleepParams = new NvSetSleepModeParams + { + version = NvSetSleepModeParams.Stride, + bLowLatencyMode = enable ? (byte)1 : (byte)0, + bLowLatencyBoost = boost ? (byte)1 : (byte)0, + minimumIntervalUs = 0, + bUseMarkersToOptimize = useMarkersOptimize ? (byte)1 : (byte)0, + }; + + return SetSleepMode(devicePtr, ref sleepParams); + } + + internal static NvStatus SetLatencyMarkerHelper(IntPtr devicePtr, uint marker, ulong frameId) + { + var frameMarkerParams = new NvFrameMarkerParams + { + version = NvFrameMarkerParams.Stride, + frameID = frameId, + markerType = marker + }; + return SetLatencyMarker(devicePtr, ref frameMarkerParams); + } + + internal static NvStatus FrameSleepHelper(IntPtr devicePtr) => Sleep(devicePtr); + /// /// Creates a session to access the driver configuration. /// @@ -400,6 +445,9 @@ static NVAPI() getDelegate(0x7FA2173A, out EnumApplications); getDelegate(0x4347A9DE, out CreateApplication); getDelegate(0xFCBC7E14, out SaveSettings); + getDelegate(0xAC1CA9E0, out SetSleepMode); + getDelegate(0xD9984C05, out SetLatencyMarker); + getDelegate(0x852CD1D2, out Sleep); } if (createSession()) @@ -426,6 +474,40 @@ private static void getDelegate(uint id, out T newDelegate) where T : class private delegate NvStatus InitializeDelegate(); } + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct NvSetSleepModeParams + { + public uint version; + public byte bLowLatencyMode; + public byte bLowLatencyBoost; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)] + private byte[] _pad0; // explicit padding to align next uint + + public uint minimumIntervalUs; // NvU32 + + public byte bUseMarkersToOptimize; // NvBool (1 byte) + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 31)] + private byte[] rsvd; // reserved (must be zeroed) + + public static uint Stride => (uint)Marshal.SizeOf(typeof(NvSetSleepModeParams)) | (1u << 16); + } + + [StructLayout(LayoutKind.Sequential)] + internal struct NvFrameMarkerParams + { + public uint version; + public ulong frameID; + public uint markerType; + public ulong rsvd0; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 56)] + public byte[] rsvd; + + public static uint Stride => (uint)Marshal.SizeOf(typeof(NvFrameMarkerParams)) | (1u << 16); + } + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] internal struct NvSetting { diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 612edb24706d..e45c27b2d3f0 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -5,6 +5,7 @@ using System.IO; using System.Runtime.Versioning; using osu.Desktop.LegacyIpc; +using osu.Desktop.LowLatency; using osu.Desktop.Windows; using osu.Framework; using osu.Framework.Development; @@ -138,6 +139,10 @@ public static void Main(string[] args) host.Run(new TournamentGame()); else { + // Attempt to use the NVAPI Low Latency Provider. This should only succeed on systems with NVIDIA GPUs and the proper drivers installed. + if (NVAPI.Available) + host.SetLowLatencyProvider(new NVAPIDirect3D11LowLatencyProvider()); + host.Run(new OsuGameDesktop(args) { IsFirstRun = isFirstRun diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs index a8b127d5229a..dd700a02e928 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs @@ -6,6 +6,7 @@ using osu.Framework.Configuration; using osu.Framework.Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Rendering.LowLatency; using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Game.Configuration; @@ -21,12 +22,17 @@ public partial class RendererSettings : SettingsSubsection private bool automaticRendererInUse; + private SettingsEnumDropdown? reflexSetting; + [BackgroundDependencyLoader] private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, IDialogOverlay? dialogOverlay, OsuGame? game, GameHost host) { var renderer = config.GetBindable(FrameworkSetting.Renderer); automaticRendererInUse = renderer.Value == RendererType.Automatic; + var reflexMode = config.GetBindable(FrameworkSetting.LatencyMode); + var frameSyncMode = config.GetBindable(FrameworkSetting.FrameSync); + Children = new Drawable[] { new RendererSettingsDropdown @@ -51,6 +57,13 @@ private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, IDi LabelText = GraphicsSettingsStrings.ThreadingMode, Current = config.GetBindable(FrameworkSetting.ExecutionMode) }, + reflexSetting = new SettingsEnumDropdown + { + LabelText = "NVIDIA Reflex", + Current = reflexMode, + Keywords = new[] { @"nvidia", @"latency", @"reflex" }, + TooltipText = "Reduces latency by leveraging the NVIDIA Reflex API on NVIDIA GPUs.\nRecommended to have On, turn Off only if experiencing issues." + }, new SettingsCheckbox { LabelText = GraphicsSettingsStrings.ShowFPS, @@ -58,6 +71,24 @@ private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, IDi }, }; + // Ensure NVIDIA reflex is turned off and hidden if the resolved renderer isn't Direct3D 11 + if (host.ResolvedRenderer is not (RendererType.Deferred_Direct3D11 or RendererType.Direct3D11)) + { + reflexMode.Value = LatencyMode.Off; + reflexSetting.Hide(); + } + + // Disable frame limiter if reflex is enabled and add notice when reflex boost is enabled + reflexMode.BindValueChanged(r => + { + frameSyncMode.Disabled = r.NewValue != LatencyMode.Off; + + reflexSetting.ClearNoticeText(); + + if (r.NewValue == LatencyMode.Boost) + setReflexBoostNotice(); + }, true); + renderer.BindValueChanged(r => { if (r.NewValue == host.ResolvedRenderer) @@ -81,6 +112,8 @@ private void load(FrameworkConfigManager config, OsuConfigManager osuConfig, IDi }); } + private void setReflexBoostNotice() => reflexSetting?.SetNoticeText("Boost increases GPU power consumption and may increase latency in some cases. Disable Boost if experiencing issues.", true); + private partial class RendererSettingsDropdown : SettingsEnumDropdown { protected override OsuDropdown CreateDropdown() => new RendererDropdown();