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
20 changes: 20 additions & 0 deletions osu.Framework/Graphics/Rendering/LowLatency/ILowLatencyProvider.cs
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 ILowLatencyProvider
{
bool IsAvailable { get; }

void Initialize(IntPtr nativeDeviceHandle);

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 NoOpLowLatencyProvider : ILowLatencyProvider
{
public static readonly NoOpLowLatencyProvider INSTANCE = new NoOpLowLatencyProvider();

public bool IsAvailable => false;

public void Initialize(IntPtr deviceHandle) { }

public void SetMode(LatencyMode mode) { }

public void SetMarker(LatencyMarker marker, ulong frameId) { }

public void FrameSleep() { }
}
}
83 changes: 83 additions & 0 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 @@ -466,6 +467,7 @@ protected virtual void OnExited()

protected virtual void UpdateFrame()
{
FrameSleep();
if (Root == null) return;

frameCount++;
Expand All @@ -483,11 +485,15 @@ 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);

LatencyMark(LatencyMarker.SimulationEnd, frameCount);
}

private bool didRenderFrame;
Expand Down Expand Up @@ -528,6 +534,8 @@ protected virtual void DrawFrame()

try
{
LatencyMark(LatencyMarker.RenderSubmitStart, frameCount);

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.

The frameCount is set from the update thread and read here from the draw thread. This can cause a multitude of problems when running in multi-threaded:

  • calls to LatencyMark() in the same draw frame having different frame counts
  • frame count not being the one used when generating the buffer (as the update thread runs twice as fast)

To make this correct, the frameCount should be stored in the buffer.

@smallketchup82 smallketchup82 Nov 13, 2025

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.

Fixed in 6163e21 (#6666) and 225a4e5 (#6666). Let me know if you had a different solution in mind.

I wonder if this might fix the frametime spikes I observed predominantly in the Reflex On and Boost modes.
image
(green is off, orange is on, blue is with boost)


using (drawMonitor.BeginCollecting(PerformanceCollectionType.DrawReset))
Renderer.BeginFrame(new Vector2(Window.ClientSize.Width, Window.ClientSize.Height));

Expand Down Expand Up @@ -560,6 +568,8 @@ protected virtual void DrawFrame()

Renderer.FinishFrame();

LatencyMark(LatencyMarker.RenderSubmitEnd, frameCount);

using (drawMonitor.BeginCollecting(PerformanceCollectionType.SwapBuffer))
Swap();

Expand All @@ -577,12 +587,16 @@ protected virtual void DrawFrame()
/// </summary>
protected virtual void Swap()
{
LatencyMark(LatencyMarker.PresentStart, frameCount);

Renderer.SwapBuffers();

if (Window.GraphicsSurface.Type == GraphicsSurfaceType.OpenGL && Renderer.VerticalSync)
// without waiting (i.e. glFinish), vsync is basically unplayable due to the extra latency introduced.
// we will likely want to give the user control over this in the future as an advanced setting.
Renderer.WaitUntilIdle();

LatencyMark(LatencyMarker.PresentEnd, frameCount);
}

/// <summary>
Expand Down Expand Up @@ -1390,6 +1404,75 @@ private void setVSyncMode()
DrawThread.Scheduler.Add(() => Renderer.VerticalSync = frameSyncMode.Value == FrameSync.VSync);
}

private ILowLatencyProvider lowLatency = NoOpLowLatencyProvider.INSTANCE;
private bool lowLatencyInitialized;

public void SetLowLatencyProvider(ILowLatencyProvider provider, IntPtr deviceHandle = 0)
{
lowLatency = provider ?? NoOpLowLatencyProvider.INSTANCE;
TryInitializeLowLatencyProvider();
}

internal void TryInitializeLowLatencyProvider()
{
if (lowLatencyInitialized || lowLatency is NoOpLowLatencyProvider) return;

if (Renderer is not IVeldridRenderer veldridRenderer) return;

try
{
IntPtr deviceHandle = veldridRenderer.Device.GetD3D11Info().Device;

if (deviceHandle == IntPtr.Zero) return;

lowLatency.Initialize(deviceHandle);
var currentLatencyMode = Config.GetBindable<LatencyMode>(FrameworkSetting.LatencyMode);
SetLowLatencyMode(currentLatencyMode.Value);
lowLatencyInitialized = true;
}
catch (Exception)
{
// ignored
}
}

public void SetLowLatencyMode(LatencyMode mode)

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.

This doesn't need to be public, FrameworkSetting.LatencyMode can and should be set through the FrameworkConfigManager.

@smallketchup82 smallketchup82 Nov 20, 2025

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.

Fixed in f250d43 (#6666)

{
try
{
Config.GetBindable<LatencyMode>(FrameworkSetting.LatencyMode).Value = mode;
lowLatency.SetMode(mode);
}
catch (Exception)
{
// ignored
}
}

internal void LatencyMark(LatencyMarker marker, ulong frameId)
{
try
{
lowLatency.SetMarker(marker, frameId);
}
catch (Exception)
{
// ignored
}
}

internal void FrameSleep()
{
try
{
lowLatency.FrameSleep();
}
catch (Exception)
{
// ignored
}
}

/// <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