Skip to content
3 changes: 3 additions & 0 deletions osu.Framework/Configuration/FrameworkConfigManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -101,6 +103,7 @@ public enum FrameworkSetting

Renderer,
WindowMode,
LatencyMode,
ConfineMouseMode,
FrameSync,
ExecutionMode,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move this to IDirect3D11LowLatencyProvider, to avoid the problem of passing the wrong device handle.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 5c02d43 (#6666)


void SetMode(LatencyMode mode);

void SetMarker(LatencyMarker marker, ulong frameId);

void FrameSleep();
}
}
17 changes: 17 additions & 0 deletions osu.Framework/Graphics/Rendering/LowLatency/LatencyMarker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
}
}
12 changes: 12 additions & 0 deletions osu.Framework/Graphics/Rendering/LowLatency/LatencyMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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() { }
}
}
138 changes: 130 additions & 8 deletions osu.Framework/Platform/GameHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -458,14 +459,21 @@ protected virtual void OnExited()
Exited?.Invoke();
}

private readonly TripleBuffer<DrawNode> drawRoots = new TripleBuffer<DrawNode>();
private readonly TripleBuffer<DrawBufferData> drawRoots = new TripleBuffer<DrawBufferData>();

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++;
Expand All @@ -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;
Expand All @@ -507,7 +523,7 @@ protected virtual void DrawFrame()

Renderer.AllowTearing = windowMode.Value == WindowMode.Fullscreen;

TripleBuffer<DrawNode>.Buffer buffer;
TripleBuffer<DrawBufferData>.Buffer buffer;

using (drawMonitor.BeginCollecting(PerformanceCollectionType.Sleep))
{
Expand All @@ -518,16 +534,22 @@ protected virtual void DrawFrame()
Renderer.WaitUntilNextFrameReady();

didRenderFrame = false;
buffer = drawRoots.GetForRead(IsActive.Value ? TripleBuffer<DrawNode>.DEFAULT_READ_TIMEOUT : 0);
buffer = drawRoots.GetForRead(IsActive.Value ? TripleBuffer<DrawBufferData>.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));

Expand All @@ -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);
Expand 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;
Expand Down Expand Up @@ -1214,6 +1242,8 @@ private void bootstrapSceneGraph(Game game)

private Bindable<FrameSync> frameSyncMode;

private Bindable<LatencyMode> latencyMode;

private IBindable<DisplayMode> currentDisplayMode;

private Bindable<string> ignoredInputHandlers;
Expand Down Expand Up @@ -1252,6 +1282,9 @@ protected virtual void SetupConfig(IDictionary<FrameworkSetting, object> default
frameSyncMode = Config.GetBindable<FrameSync>(FrameworkSetting.FrameSync);
frameSyncMode.ValueChanged += _ => updateFrameSyncMode();

latencyMode = Config.GetBindable<LatencyMode>(FrameworkSetting.LatencyMode);
latencyMode.BindValueChanged(mode => setLowLatencyMode(mode.NewValue), true);

#pragma warning disable 618
// pragma region can be removed 20210911
ignoredInputHandlers = Config.GetBindable<string>(FrameworkSetting.IgnoredInputHandlers);
Expand Down Expand Up @@ -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);
Expand All @@ -1390,6 +1431,87 @@ private void setVSyncMode()
DrawThread.Scheduler.Add(() => Renderer.VerticalSync = frameSyncMode.Value == FrameSync.VSync);
}

private IDirect3D11LowLatencyProvider direct3D11LowLatency = NoOpDirect3D11LowLatencyProvider.INSTANCE;
private bool lowLatencyInitialized;

/// <summary>
/// Set the low latency provider to be used by this host.
/// </summary>
/// <param name="provider">The <see cref="IDirect3D11LowLatencyProvider"/> to use.</param>
public void SetLowLatencyProvider(IDirect3D11LowLatencyProvider provider)
{
direct3D11LowLatency = provider ?? NoOpDirect3D11LowLatencyProvider.INSTANCE;
Logger.Log("Low latency provider set to: " + direct3D11LowLatency.GetType().ReadableName());
TryInitializeLowLatencyProvider();
}

/// <summary>
/// Attempts to initialize the low latency provider if it has not already been initialized.
/// </summary>
/// <remarks>
/// This is called automatically after setting a new low latency provider via <see cref="SetLowLatencyProvider(IDirect3D11LowLatencyProvider)"/>.
/// It should only be called manually if the provider was set before the renderer was initialized, or if the renderer has changed.
/// </remarks>
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");
}
}

/// <summary>
/// Construct all input handlers for this host. The order here decides the priority given to handlers, with the earliest occurring having higher priority.
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions osu.Framework/Threading/DrawThread.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}

Expand Down
Loading