Skip to content

Commit

Permalink
Precise, time period-independent timing for Windows game loop.
Browse files Browse the repository at this point in the history
  • Loading branch information
PJB3005 committed Jul 15, 2023
1 parent d7ee2bc commit c392d4f
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 10 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ END TEMPLATE-->
### Other

* BQL `with` now includes paused entities.
* The game loop now times more accurately and avoids sleeping more than necessary.

### Internal

Expand Down
7 changes: 6 additions & 1 deletion Robust.Client/GameController/GameController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,12 @@ internal bool StartupContinue(DisplayMode displayMode)
// Setup main loop
if (_mainLoop == null)
{
_mainLoop = new GameLoop(_gameTiming, _runtimeLog, _prof, _logManager.GetSawmill("eng"))
_mainLoop = new GameLoop(
_gameTiming,
_runtimeLog,
_prof,
_logManager.GetSawmill("eng"),
GameLoopOptions.FromCVars(_configurationManager))
{
SleepMode = displayMode == DisplayMode.Headless ? SleepMode.Delay : SleepMode.None
};
Expand Down
7 changes: 6 additions & 1 deletion Robust.Server/BaseServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,12 @@ internal void SetupMainLoop()
{
if (_mainLoop == null)
{
_mainLoop = new GameLoop(_time, _runtimeLog, _prof, _log.GetSawmill("eng"))
_mainLoop = new GameLoop(
_time,
_runtimeLog,
_prof,
_log.GetSawmill("eng"),
GameLoopOptions.FromCVars(_config))
{
SleepMode = SleepMode.Delay,
DetectSoftLock = true,
Expand Down
6 changes: 6 additions & 0 deletions Robust.Shared/CVars.cs
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,12 @@ protected CVars()
public static readonly CVarDef<bool> SysGCCollectStart =
CVarDef.Create("sys.gc_collect_start", true);

/// <summary>
/// Use precise sleeping methods in the game loop.
/// </summary>
public static readonly CVarDef<bool> SysPreciseSleep =
CVarDef.Create("sys.precise_sleep", true);

/*
* METRICS
*/
Expand Down
49 changes: 43 additions & 6 deletions Robust.Shared/Timing/GameLoop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
using System.Threading;
using Robust.Shared.Log;
using Robust.Shared.Exceptions;
using Robust.Shared.Maths;
using Prometheus;
using Robust.Shared.Configuration;
using Robust.Shared.Profiling;

namespace Robust.Shared.Timing
{
public interface IGameLoop
internal interface IGameLoop
{
event EventHandler<FrameEventArgs> Input;
event EventHandler<FrameEventArgs> Tick;
Expand Down Expand Up @@ -46,8 +46,10 @@ public interface IGameLoop
/// <summary>
/// Manages the main game loop for a GameContainer.
/// </summary>
public sealed class GameLoop : IGameLoop
internal sealed class GameLoop : IGameLoop
{
private static readonly TimeSpan DelayTime = TimeSpan.FromMilliseconds(1);

public const string ProfTextStartFrame = "Start Frame";

private static readonly Histogram _frameTimeHistogram = Metrics.CreateHistogram(
Expand Down Expand Up @@ -100,18 +102,27 @@ public sealed class GameLoop : IGameLoop
private readonly ProfManager _prof;
private readonly ISawmill _sawmill;

private readonly PrecisionSleep _precisionSleep;

#if EXCEPTION_TOLERANCE
private int _tickExceptions;

private const int MaxSoftLockExceptions = 10;
#endif

public GameLoop(IGameTiming timing, IRuntimeLog runtimeLog, ProfManager prof, ISawmill sawmill)
public GameLoop(
IGameTiming timing,
IRuntimeLog runtimeLog,
ProfManager prof,
ISawmill sawmill,
GameLoopOptions options)
{
_timing = timing;
_runtimeLog = runtimeLog;
_prof = prof;
_sawmill = sawmill;

_precisionSleep = options.Precise ? new PrecisionSleepUniversal() : PrecisionSleep.Create();
}

/// <summary>
Expand Down Expand Up @@ -208,6 +219,8 @@ public void Run()
using var tickGroup = _prof.Group("Tick");
_prof.WriteValue("Tick", ProfData.Int64(_timing.CurTick.Value));

// System.Console.WriteLine($"Tick started at: {_timing.RealTime - _timing.LastTick}");

if (EnableMetrics)
{
using (_frameTimeHistogram.NewTimer())
Expand Down Expand Up @@ -304,12 +317,36 @@ public void Run()
// Set sleep to 1 if you want to be nice and give the rest of the timeslice up to the os scheduler.
// Set sleep to 0 if you want to use 100% cpu, but still cooperate with the scheduler.
// do not call sleep if you want to be 'that thread' and hog 100% cpu.
if (SleepMode != SleepMode.None)
Thread.Sleep((int)SleepMode);
switch (SleepMode)
{
case SleepMode.Yield:
Thread.Sleep(0);
break;

case SleepMode.Delay:
// We try to sleep exactly until the next tick.
// But no longer than 1ms so input can keep processing.
var timeToSleep = (_timing.LastTick + _timing.TickPeriod) - _timing.RealTime;
if (timeToSleep > DelayTime)
timeToSleep = DelayTime;

if (timeToSleep.Ticks > 0)
_precisionSleep.Sleep(timeToSleep);

break;
}
}
}
}

internal sealed record GameLoopOptions(bool Precise)
{
public static GameLoopOptions FromCVars(IConfigurationManager cfg)
{
return new GameLoopOptions(cfg.GetCVar(CVars.SysPreciseSleep));
}
}

/// <summary>
/// Methods the GameLoop can use to limit the Update rate.
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion Robust.Shared/Timing/IGameTiming.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ public interface IGameTiming
GameTick CurTick { get; set; }

/// <summary>
/// Timespan for the last tick.
/// Time, relative to <see cref="RealTime"/>, the last tick started at.
/// If we're currently in simulation, that's THIS tick.
/// </summary>
TimeSpan LastTick { get; set; }

Expand Down
110 changes: 110 additions & 0 deletions Robust.Shared/Timing/PrecisionSleep.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using TerraFX.Interop.Windows;

namespace Robust.Shared.Timing;

/// <summary>
/// Helper for more precise sleeping functionality than <see cref="Thread.Sleep(int)"/>.
/// </summary>
internal abstract class PrecisionSleep : IDisposable
{
/// <summary>
/// Sleep for the specified amount of time.
/// </summary>
public abstract void Sleep(TimeSpan time);

/// <summary>
/// Create the most optimal optimization for the current platform.
/// </summary>
public static PrecisionSleep Create()
{
// Check Windows 10 1803
if (OperatingSystem.IsWindows() && Environment.OSVersion.Version.Build >= 17134)
return new PrecisionSleepWindowsHighResolution();

return new PrecisionSleepUniversal();
}

public virtual void Dispose()
{
}
}

/// <summary>
/// Universal cross-platform implementation of <see cref="PrecisionSleep"/>. Not very precise!
/// </summary>
internal sealed class PrecisionSleepUniversal : PrecisionSleep
{
public override void Sleep(TimeSpan time)
{
Thread.Sleep(time);
}
}

/// <summary>
/// High-precision implementation of <see cref="PrecisionSleep"/> that is available since Windows 10 1803.
/// </summary>
internal sealed unsafe class PrecisionSleepWindowsHighResolution : PrecisionSleep
{
private HANDLE _timerHandle;

public PrecisionSleepWindowsHighResolution()
{
// CREATE_WAITABLE_TIMER_HIGH_RESOLUTION is only supported since Windows 10 1803
_timerHandle = Windows.CreateWaitableTimerExW(
null,
null,
CREATE.CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
Windows.TIMER_ALL_ACCESS);

if (_timerHandle == HANDLE.NULL)
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
}

public override void Sleep(TimeSpan time)
{
LARGE_INTEGER due;
Windows.GetSystemTimeAsFileTime((FILETIME*)(&due));

due.QuadPart += time.Ticks;

var success = Windows.SetWaitableTimer(
_timerHandle,
&due,
0,
null,
null,
BOOL.FALSE
);

if (!success)
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());

var waitResult = Windows.WaitForSingleObject(_timerHandle, Windows.INFINITE);
if (waitResult == WAIT.WAIT_FAILED)
Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());

GC.KeepAlive(this);
}

private void DisposeCore()
{
Windows.CloseHandle(_timerHandle);

_timerHandle = default;
}

public override void Dispose()
{
DisposeCore();

GC.SuppressFinalize(this);
}

~PrecisionSleepWindowsHighResolution()
{
DisposeCore();
}
}
7 changes: 6 additions & 1 deletion Robust.UnitTesting/Shared/Timing/GameLoop_Test.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ public void SingleStepTest()
newStopwatch.SetupGet(p => p.Elapsed).Returns(elapsedVal);
var gameTiming = GameTimingFactory(newStopwatch.Object);
gameTiming.Paused = false;
var loop = new GameLoop(gameTiming, new RuntimeLog(), new ProfManager(), new LogManager().RootSawmill);
var loop = new GameLoop(
gameTiming,
new RuntimeLog(),
new ProfManager(),
new LogManager().RootSawmill,
new GameLoopOptions(false));

var callCount = 0;
loop.Tick += (sender, args) => callCount++;
Expand Down

0 comments on commit c392d4f

Please sign in to comment.