diff --git a/osu.Framework/Configuration/FrameworkConfigManager.cs b/osu.Framework/Configuration/FrameworkConfigManager.cs index 8a1f261ab0..e7beb0a8b3 100644 --- a/osu.Framework/Configuration/FrameworkConfigManager.cs +++ b/osu.Framework/Configuration/FrameworkConfigManager.cs @@ -8,6 +8,7 @@ using System.Drawing; using osu.Framework.Configuration.Tracking; using osu.Framework.Extensions; +using osu.Framework.Graphics.Rendering.LowLatency; using osu.Framework.Graphics.Video; using osu.Framework.Input; using osu.Framework.Platform; @@ -42,6 +43,7 @@ protected override void InitialiseDefaults() SetDefault(FrameworkSetting.FrameSync, FrameSync.Limit2x); SetDefault(FrameworkSetting.WindowMode, WindowMode.Windowed); SetDefault(FrameworkSetting.Renderer, RendererType.Automatic); + SetDefault(FrameworkSetting.LatencyMode, LatencyMode.Off); SetDefault(FrameworkSetting.ShowUnicode, false); SetDefault(FrameworkSetting.Locale, string.Empty); @@ -101,6 +103,7 @@ public enum FrameworkSetting Renderer, WindowMode, + LatencyMode, ConfineMouseMode, FrameSync, ExecutionMode, diff --git a/osu.Framework/Graphics/Rendering/LowLatency/IDirect3D11LowLatencyProvider.cs b/osu.Framework/Graphics/Rendering/LowLatency/IDirect3D11LowLatencyProvider.cs new file mode 100644 index 0000000000..6c5bc739c9 --- /dev/null +++ b/osu.Framework/Graphics/Rendering/LowLatency/IDirect3D11LowLatencyProvider.cs @@ -0,0 +1,20 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Framework.Graphics.Rendering.LowLatency +{ + public interface IDirect3D11LowLatencyProvider + { + bool IsAvailable { get; } + + void Initialize(IntPtr nativeDeviceHandle); + + void SetMode(LatencyMode mode); + + void SetMarker(LatencyMarker marker, ulong frameId); + + void FrameSleep(); + } +} diff --git a/osu.Framework/Graphics/Rendering/LowLatency/LatencyMarker.cs b/osu.Framework/Graphics/Rendering/LowLatency/LatencyMarker.cs new file mode 100644 index 0000000000..65b9e2a2ce --- /dev/null +++ b/osu.Framework/Graphics/Rendering/LowLatency/LatencyMarker.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Framework.Graphics.Rendering.LowLatency +{ + public enum LatencyMarker + { + SimulationStart, + SimulationEnd, + RenderSubmitStart, + RenderSubmitEnd, + PresentStart, + PresentEnd, + InputSample, + TriggerFlash + } +} diff --git a/osu.Framework/Graphics/Rendering/LowLatency/LatencyMode.cs b/osu.Framework/Graphics/Rendering/LowLatency/LatencyMode.cs new file mode 100644 index 0000000000..0cf129e20a --- /dev/null +++ b/osu.Framework/Graphics/Rendering/LowLatency/LatencyMode.cs @@ -0,0 +1,12 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Framework.Graphics.Rendering.LowLatency +{ + public enum LatencyMode + { + Off = 0, + On = 1, + Boost = 2 + } +} diff --git a/osu.Framework/Graphics/Rendering/LowLatency/NoOpDirect3D11LowLatencyProvider.cs b/osu.Framework/Graphics/Rendering/LowLatency/NoOpDirect3D11LowLatencyProvider.cs new file mode 100644 index 0000000000..f5274751ee --- /dev/null +++ b/osu.Framework/Graphics/Rendering/LowLatency/NoOpDirect3D11LowLatencyProvider.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Framework.Graphics.Rendering.LowLatency +{ + internal sealed class NoOpDirect3D11LowLatencyProvider : IDirect3D11LowLatencyProvider + { + public static readonly NoOpDirect3D11LowLatencyProvider INSTANCE = new NoOpDirect3D11LowLatencyProvider(); + + public bool IsAvailable => false; + + public void Initialize(IntPtr deviceHandle) { } + + public void SetMode(LatencyMode mode) { } + + public void SetMarker(LatencyMarker marker, ulong frameId) { } + + public void FrameSleep() { } + } +} diff --git a/osu.Framework/Platform/GameHost.cs b/osu.Framework/Platform/GameHost.cs index a29c197f2d..b6c2921859 100644 --- a/osu.Framework/Platform/GameHost.cs +++ b/osu.Framework/Platform/GameHost.cs @@ -30,6 +30,7 @@ using osu.Framework.Graphics.OpenGL; using osu.Framework.Graphics.Rendering; using osu.Framework.Graphics.Rendering.Deferred; +using osu.Framework.Graphics.Rendering.LowLatency; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Handlers; @@ -458,14 +459,21 @@ protected virtual void OnExited() Exited?.Invoke(); } - private readonly TripleBuffer drawRoots = new TripleBuffer(); + private readonly TripleBuffer drawRoots = new TripleBuffer(); internal Container Root { get; private set; } private ulong frameCount; + private class DrawBufferData + { + public DrawNode Node; + public ulong FrameCount; + } + protected virtual void UpdateFrame() { + FrameSleep(); if (Root == null) return; frameCount++; @@ -483,11 +491,19 @@ protected virtual void UpdateFrame() TypePerformanceMonitor.NewFrame(); + LatencyMark(LatencyMarker.SimulationStart, frameCount); + Root.UpdateSubTree(); Root.UpdateSubTreeMasking(); using (var buffer = drawRoots.GetForWrite()) - buffer.Object = Root.GenerateDrawNodeSubtree(frameCount, buffer.Index, false); + { + buffer.Object ??= new DrawBufferData(); + buffer.Object.Node = Root.GenerateDrawNodeSubtree(frameCount, buffer.Index, false); + buffer.Object.FrameCount = frameCount; + } + + LatencyMark(LatencyMarker.SimulationEnd, frameCount); } private bool didRenderFrame; @@ -507,7 +523,7 @@ protected virtual void DrawFrame() Renderer.AllowTearing = windowMode.Value == WindowMode.Fullscreen; - TripleBuffer.Buffer buffer; + TripleBuffer.Buffer buffer; using (drawMonitor.BeginCollecting(PerformanceCollectionType.Sleep)) { @@ -518,16 +534,22 @@ protected virtual void DrawFrame() Renderer.WaitUntilNextFrameReady(); didRenderFrame = false; - buffer = drawRoots.GetForRead(IsActive.Value ? TripleBuffer.DEFAULT_READ_TIMEOUT : 0); + buffer = drawRoots.GetForRead(IsActive.Value ? TripleBuffer.DEFAULT_READ_TIMEOUT : 0); } if (buffer == null) return; - Debug.Assert(buffer.Object != null); - try { + if (buffer.Object is not DrawBufferData data) + return; + + DrawNode rootNode = data.Node; + ulong bufferFrameCount = data.FrameCount; + + LatencyMark(LatencyMarker.RenderSubmitStart, bufferFrameCount); + using (drawMonitor.BeginCollecting(PerformanceCollectionType.DrawReset)) Renderer.BeginFrame(new Vector2(Window.ClientSize.Width, Window.ClientSize.Height)); @@ -539,7 +561,7 @@ protected virtual void DrawFrame() Renderer.PushDepthInfo(DepthInfo.Default); // Front pass - DrawNode.DrawOtherOpaqueInterior(buffer.Object, Renderer); + DrawNode.DrawOtherOpaqueInterior(rootNode, Renderer); Renderer.PopDepthInfo(); Renderer.SetBlendMask(BlendingMask.All); @@ -554,14 +576,20 @@ protected virtual void DrawFrame() } // Back pass - DrawNode.DrawOther(buffer.Object, Renderer); + DrawNode.DrawOther(rootNode, Renderer); Renderer.PopDepthInfo(); Renderer.FinishFrame(); + LatencyMark(LatencyMarker.RenderSubmitEnd, bufferFrameCount); + using (drawMonitor.BeginCollecting(PerformanceCollectionType.SwapBuffer)) + { + LatencyMark(LatencyMarker.PresentStart, bufferFrameCount); Swap(); + LatencyMark(LatencyMarker.PresentEnd, bufferFrameCount); + } Window.OnDraw(); didRenderFrame = true; @@ -1214,6 +1242,8 @@ private void bootstrapSceneGraph(Game game) private Bindable frameSyncMode; + private Bindable latencyMode; + private IBindable currentDisplayMode; private Bindable ignoredInputHandlers; @@ -1252,6 +1282,9 @@ protected virtual void SetupConfig(IDictionary default frameSyncMode = Config.GetBindable(FrameworkSetting.FrameSync); frameSyncMode.ValueChanged += _ => updateFrameSyncMode(); + latencyMode = Config.GetBindable(FrameworkSetting.LatencyMode); + latencyMode.BindValueChanged(mode => setLowLatencyMode(mode.NewValue), true); + #pragma warning disable 618 // pragma region can be removed 20210911 ignoredInputHandlers = Config.GetBindable(FrameworkSetting.IgnoredInputHandlers); @@ -1373,6 +1406,14 @@ private void updateFrameSyncMode() break; } + // If low latency is enabled, we want to limit the draw thread to refresh rate as anything above is unnecessary. + // Keep Update thread at 1000hz for input & audio responsiveness. + if (lowLatencyInitialized && latencyMode.Value != LatencyMode.Off) + { + drawLimiter = refreshRate; + updateLimiter = int.MaxValue; + } + if (!AllowBenchmarkUnlimitedFrames) { drawLimiter = Math.Min(maximum_sane_fps, drawLimiter); @@ -1390,6 +1431,87 @@ private void setVSyncMode() DrawThread.Scheduler.Add(() => Renderer.VerticalSync = frameSyncMode.Value == FrameSync.VSync); } + private IDirect3D11LowLatencyProvider direct3D11LowLatency = NoOpDirect3D11LowLatencyProvider.INSTANCE; + private bool lowLatencyInitialized; + + /// + /// Set the low latency provider to be used by this host. + /// + /// The to use. + public void SetLowLatencyProvider(IDirect3D11LowLatencyProvider provider) + { + direct3D11LowLatency = provider ?? NoOpDirect3D11LowLatencyProvider.INSTANCE; + Logger.Log("Low latency provider set to: " + direct3D11LowLatency.GetType().ReadableName()); + TryInitializeLowLatencyProvider(); + } + + /// + /// Attempts to initialize the low latency provider if it has not already been initialized. + /// + /// + /// This is called automatically after setting a new low latency provider via . + /// It should only be called manually if the provider was set before the renderer was initialized, or if the renderer has changed. + /// + internal void TryInitializeLowLatencyProvider() + { + if (lowLatencyInitialized || direct3D11LowLatency is NoOpDirect3D11LowLatencyProvider) return; + if (Renderer is not IVeldridRenderer veldridRenderer || !Renderer.IsInitialised) return; + + try + { + Logger.Log("Attempting to initialize low latency provider..."); + + IntPtr deviceHandle = veldridRenderer.Device.GetD3D11Info().Device; + if (deviceHandle == IntPtr.Zero) return; + + direct3D11LowLatency.Initialize(deviceHandle); + setLowLatencyMode(latencyMode.Value); + lowLatencyInitialized = true; + } + catch (Exception e) + { + // Intentionally not logged as an error, as failure is expected (this method can be run before renderer initialization). + Logger.Log("Failed to initialize low latency provider: " + e, level: LogLevel.Important); + } + } + + private void setLowLatencyMode(LatencyMode mode) + { + try + { + direct3D11LowLatency.SetMode(mode); + } + catch (Exception e) + { + logException(e, "unobserved"); + } + } + + internal void LatencyMark(LatencyMarker marker, ulong frameId) + { + try + { + direct3D11LowLatency.SetMarker(marker, frameId); + } + catch (Exception) + { + // WARNING: Do not log anything here or otherwise catch the error. + // This method is called extremely frequently (multiple times per frame) and doing so could cause massive performance degradation. + } + } + + internal void FrameSleep() + { + try + { + direct3D11LowLatency.FrameSleep(); + } + catch (Exception e) + { + logException(e, "unobserved"); + } + } + /// /// Construct all input handlers for this host. The order here decides the priority given to handlers, with the earliest occurring having higher priority. /// diff --git a/osu.Framework/Threading/DrawThread.cs b/osu.Framework/Threading/DrawThread.cs index 08079fce55..884b59545f 100644 --- a/osu.Framework/Threading/DrawThread.cs +++ b/osu.Framework/Threading/DrawThread.cs @@ -30,6 +30,8 @@ protected sealed override void OnInitialize() { host.Renderer.BeginFrame(new Vector2(window.ClientSize.Width, window.ClientSize.Height)); host.Renderer.FinishFrame(); + + host.TryInitializeLowLatencyProvider(); } }