Skip to content

Commit

Permalink
Account for windows time period latency in Lidgren.
Browse files Browse the repository at this point in the history
1. Set timeBeginPeriod(3) on the server to reduce scheduler latency in the lidgren thread.
2. Add 16ms of guaranteed lag bias to client prediction calculations to account for scheduler latency.

Both of these changes are to account for how the windows scheduler seems to handle time periods in related to socket polls. See this Discord conversation for why, details down below as well: https://discord.com/channels/310555209753690112/770682801607278632/798309250291204107

Basically Windows has this thing called time periods which determines the precision of sleep operations and such. By default it's like 16ms so a sleep will only be accurate to within 16ms.

Problem: Lidgren polls the socket with a timeout of 1ms.

The way Windows seems to handle this is that:
1. if a message comes into the socket, the poll immediately ends and Lidgren can handle it.
2. If nothing comes in, it takes the whole 16ms time period to actually process stuff.

Oh yeah, and Lidgren's thread needs to keep pumping at a steady rate or else it *won't flush its send queue*. On Windows it seems to normally pump at 65/125 Hz. On Linux it goes like 950 Hz as intended.

Now, the worst part is that (1) causes Lidgren's latency calculation to always read 0 (over localhost) instead of the 30~ms it SHOULD BE (assuming client and server localhost).

That 30ms of unaccounted delay worst caseis enough to cause prediction undershoot and have messages arrive too late. Yikes.

So, to fix this...

On the server we just decrease the tick period and call it a day. Screw your battery life players don't have local servers running anyways.

On the client we bias the prediction calculations to account for this "unmeasurable" lag.

Of course, all this can be configured via CVars.
  • Loading branch information
PJB3005 committed Jan 12, 2021
1 parent 3954163 commit 2898f53
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 8 deletions.
13 changes: 8 additions & 5 deletions Robust.Client/GameStates/ClientGameStateManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public class ClientGameStateManager : IClientGameStateManager

public bool Predicting { get; private set; }

public int PredictSize { get; private set; }
public int PredictTickBias { get; private set; }
public float PredictLagBias { get; private set; }

public int StateBufferMergeThreshold { get; private set; }

Expand All @@ -82,14 +83,16 @@ public void Initialize()
_config.OnValueChanged(CVars.NetInterpRatio, i => _processor.InterpRatio = i, true);
_config.OnValueChanged(CVars.NetLogging, b => _processor.Logging = b, true);
_config.OnValueChanged(CVars.NetPredict, b => Predicting = b, true);
_config.OnValueChanged(CVars.NetPredictSize, i => PredictSize = i, true);
_config.OnValueChanged(CVars.NetPredictTickBias, i => PredictTickBias = i, true);
_config.OnValueChanged(CVars.NetPredictLagBias, i => PredictLagBias = i, true);
_config.OnValueChanged(CVars.NetStateBufMergeThreshold, i => StateBufferMergeThreshold = i, true);

_processor.Interpolation = _config.GetCVar(CVars.NetInterp);
_processor.InterpRatio = _config.GetCVar(CVars.NetInterpRatio);
_processor.Logging = _config.GetCVar(CVars.NetLogging);
Predicting = _config.GetCVar(CVars.NetPredict);
PredictSize = _config.GetCVar(CVars.NetPredictSize);
PredictTickBias = _config.GetCVar(CVars.NetPredictTickBias);
PredictLagBias = _config.GetCVar(CVars.NetPredictLagBias);
}

/// <inheritdoc />
Expand Down Expand Up @@ -256,9 +259,9 @@ public void ApplyGameState()
var hasPendingInput = pendingInputEnumerator.MoveNext();
var hasPendingMessage = pendingMessagesEnumerator.MoveNext();

var ping = _network.ServerChannel!.Ping / 1000f; // seconds.
var ping = _network.ServerChannel!.Ping / 1000f + PredictLagBias; // seconds.
var targetTick = _timing.CurTick.Value + _processor.TargetBufferSize +
(int) Math.Ceiling(_timing.TickRate * ping) + PredictSize;
(int) Math.Ceiling(_timing.TickRate * ping) + PredictTickBias;

// Logger.DebugS("net.predict", $"Predicting from {_lastProcessedTick} to {targetTick}");

Expand Down
12 changes: 12 additions & 0 deletions Robust.Server/BaseServer.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using Prometheus;
using Robust.Server.Console;
Expand Down Expand Up @@ -37,6 +38,7 @@
using Robust.Shared.Network.Messages;
using Robust.Server.DataMetrics;
using Robust.Server.Log;
using Robust.Server.Utility;
using Robust.Shared.Localization;
using Robust.Shared.Serialization;
using Serilog.Debugging;
Expand Down Expand Up @@ -320,6 +322,11 @@ public bool Start(Func<ILogHandler>? logHandlerFactory = null)

_stringSerializer.LockStrings();

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _config.GetCVar(CVars.SysWinTickPeriod) >= 0)
{
WindowsTickPeriod.TimeBeginPeriod((uint) _config.GetCVar(CVars.SysWinTickPeriod));
}

return false;
}

Expand Down Expand Up @@ -500,6 +507,11 @@ private void Cleanup()
AppDomain.CurrentDomain.ProcessExit -= ProcessExiting;

//TODO: This should prob shutdown all managers in a loop.

if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _config.GetCVar(CVars.SysWinTickPeriod) >= 0)
{
WindowsTickPeriod.TimeEndPeriod((uint) _config.GetCVar(CVars.SysWinTickPeriod));
}
}

private string UpdateBps()
Expand Down
38 changes: 38 additions & 0 deletions Robust.Server/Utility/WindowsTickPeriod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using System;
using System.Runtime.InteropServices;

namespace Robust.Server.Utility
{
internal static class WindowsTickPeriod
{
private const uint TIMERR_NOERROR = 0;
// This is an actual error code my god.
private const uint TIMERR_NOCANDO = 97;

public static void TimeBeginPeriod(uint period)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new InvalidOperationException();

var ret = timeBeginPeriod(period);
if (ret != TIMERR_NOERROR)
throw new InvalidOperationException($"timeBeginPeriod returned error: {ret}");
}

public static void TimeEndPeriod(uint period)
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
throw new InvalidOperationException();

var ret = timeEndPeriod(period);
if (ret != TIMERR_NOERROR)
throw new InvalidOperationException($"timeEndPeriod returned error: {ret}");
}

[DllImport("Winmm.dll")]
private static extern uint timeBeginPeriod(uint uPeriod);

[DllImport("Winmm.dll")]
private static extern uint timeEndPeriod(uint uPeriod);
}
}
18 changes: 16 additions & 2 deletions Robust.Shared/CVars.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Runtime.InteropServices;
using Robust.Shared.Configuration;
using Robust.Shared.Log;

Expand Down Expand Up @@ -59,8 +60,19 @@ protected CVars()
public static readonly CVarDef<bool> NetPredict =
CVarDef.Create("net.predict", true, CVar.ARCHIVE);

public static readonly CVarDef<int> NetPredictSize =
CVarDef.Create("net.predict_size", 1, CVar.ARCHIVE);
public static readonly CVarDef<int> NetPredictTickBias =
CVarDef.Create("net.predict_tick_bias", 1, CVar.ARCHIVE);

// On Windows we default this to 16ms lag bias, to account for time period lag in the Lidgren thread.
// Basically due to how time periods work on Windows, messages are (at worst) time period-delayed when sending.
// BUT! Lidgren's latency calculation *never* measures this due to how it works.
// This broke some prediction calculations quite badly so we bias them to mask it.
// This is not necessary on Linux because Linux, for better or worse,
// just has the Lidgren thread go absolute brr polling.
public static readonly CVarDef<float> NetPredictLagBias = CVarDef.Create(
"net.predict_lag_bias",
RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 0.016f : 0,
CVar.ARCHIVE);

public static readonly CVarDef<int> NetStateBufMergeThreshold =
CVarDef.Create("net.state_buf_merge_threshold", 5, CVar.ARCHIVE);
Expand All @@ -77,6 +89,8 @@ protected CVars()
public static readonly CVarDef<int> NetTickrate =
CVarDef.Create("net.tickrate", 60, CVar.ARCHIVE | CVar.REPLICATED | CVar.SERVER);

public static readonly CVarDef<int> SysWinTickPeriod =
CVarDef.Create("sys.win_tick_period", 3, CVar.SERVERONLY);

#if DEBUG
public static readonly CVarDef<float> NetFakeLoss = CVarDef.Create("net.fakeloss", 0f, CVar.CHEAT);
Expand Down
3 changes: 2 additions & 1 deletion Robust.UnitTesting/RobustIntegrationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Robust.Server.Interfaces;
using Robust.Server.Interfaces.Console;
using Robust.Server.Interfaces.ServerStatus;
using Robust.Shared;
using Robust.Shared.ContentPack;
using Robust.Shared.Interfaces.Configuration;
using Robust.Shared.Interfaces.Network;
Expand Down Expand Up @@ -357,7 +358,7 @@ private void _serverMain()
}
}

cfg.OverrideConVars(new []{("log.runtimelog", "false")});
cfg.OverrideConVars(new []{("log.runtimelog", "false"), (CVars.SysWinTickPeriod.Name, "-1")});

if (server.Start(() => new TestLogHandler("SERVER")))
{
Expand Down

0 comments on commit 2898f53

Please sign in to comment.