Skip to content

Commit

Permalink
Implement new slowdown and catchup algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
adammitchelldev committed Jan 17, 2021
1 parent 5c4b40f commit 3cf277b
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 90 deletions.
57 changes: 27 additions & 30 deletions OpenRA.Game/Game.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ public static class Game
{
public const int DefaultNetTickScale = 3; // 120ms net tick for 40ms timestep
public const int NewNetcodeNetTickScale = 1; // Net tick every world frame
public const int MaxNetworkSimLag = 50;
public const int MaxSimLagBeforeFrameDrops = 50;
public const int Timestep = 40;
public const int TimestepJankThreshold = 250; // Don't catch up for delays larger than 250ms
public const double NetCatchupFactor = 0.1;
public const double NetCatchupFactor = 0.2;
public static InstalledMods Mods { get; private set; }
public static ExternalMods ExternalMods { get; private set; }

Expand Down Expand Up @@ -595,30 +597,30 @@ static void InnerLogicTick(OrderManager orderManager)
Cursor.Tick();
}

int worldTimestep;
if (world == null)
worldTimestep = Timestep;
else if (world.IsLoadingGameSave)
worldTimestep = 1;
else if (orderManager.IsStalling)
worldTimestep = 1;
else if (orderManager.CatchUpFrames > 0)
worldTimestep = (int)Math.Floor(world.Timestep / (1.0 + NetCatchupFactor * orderManager.CatchUpFrames)); // Smooth catchup
else
worldTimestep = world.Timestep;
var worldTimestep = orderManager.SuggestedTimestep;

var worldTickDelta = tick - orderManager.LastTickTime;
if (worldTimestep == 0 || worldTickDelta < worldTimestep)
return;

using (new PerfSample("tick_time"))
{
// Tick the world to advance the world time to match real time:
// If dt < TickJankThreshold then we should try and catch up by repeatedly ticking
// If dt >= TickJankThreshold then we should accept the jank and progress at the normal rate
// dt is rounded down to an integer tick count in order to preserve fractional tick components.
var integralTickTimestep = (worldTickDelta / worldTimestep) * worldTimestep;
orderManager.LastTickTime += integralTickTimestep >= TimestepJankThreshold ? integralTickTimestep : worldTimestep;
if (world == null || !orderManager.ShouldUseCatchUp || !orderManager.LobbyInfo.GlobalSettings.UseNewNetcode)
{
// Tick the world to advance the world time to match real time:
// If dt < TickJankThreshold then we should try and catch up by repeatedly ticking
// If dt >= TickJankThreshold then we should accept the jank and progress at the normal rate
// dt is rounded down to an integer tick count in order to preserve fractional tick components.
var integralTickTimestep = (worldTickDelta / worldTimestep) * worldTimestep;
orderManager.LastTickTime += integralTickTimestep >= TimestepJankThreshold ? integralTickTimestep : worldTimestep;
}
// else
// {
// // Console.WriteLine("stalled {0}", worldTimestep);
// orderManager.LastTickTime = tick;
// }

orderManager.RealTickTime = tick;

Sound.Tick();

Expand Down Expand Up @@ -799,23 +801,16 @@ static void Loop()
{
// Ideal time between logic updates. Timestep = 0 means the game is paused
// but we still call LogicTick() because it handles pausing internally.
var logicInterval = worldRenderer != null && worldRenderer.World.Timestep != 0 ? worldRenderer.World.Timestep : Timestep;
// var logicInterval = worldRenderer != null && worldRenderer.World.Timestep != 0 ? worldRenderer.World.Timestep : OrderManager.SuggestedTimestep;
var logicInterval = 1;

// Ideal time between screen updates
var maxFramerate = Settings.Graphics.CapFramerate ? Settings.Graphics.MaxFramerate.Clamp(1, 1000) : 1000;
var renderInterval = 1000 / maxFramerate;

if (OrderManager.IsStalling)
logicInterval = 1;

// TODO: limit rendering if we are taking too long to catch up
else if (OrderManager.CatchUpFrames > 0)
logicInterval = (int)Math.Floor(logicInterval / (1.0 + NetCatchupFactor * OrderManager.CatchUpFrames));

// Tick as fast as possible while restoring game saves, capping rendering at 5 FPS
// Whilst restoring game saves, cap rendering at 5 FPS
if (OrderManager.World != null && OrderManager.World.IsLoadingGameSave)
{
logicInterval = 1;
renderInterval = 200;
}

Expand All @@ -827,18 +822,20 @@ static void Loop()

// When's the next update (logic or render)
var nextUpdate = Math.Min(nextLogic, nextRender);

if (now >= nextUpdate)
{
var forceRender = renderBeforeNextTick || now >= forcedNextRender;

if (now >= nextLogic && !renderBeforeNextTick)
{
nextLogic += logicInterval;
// nextLogic += logicInterval;
nextLogic = now + logicInterval;

LogicTick();

// Force at least one render per tick during regular gameplay
if (!OrderManager.IsStalling && !(OrderManager.CatchUpFrames > 2) && OrderManager.World != null && !OrderManager.World.IsLoadingGameSave && !OrderManager.World.IsReplay)
if (!OrderManager.IsStalling && OrderManager.SimLag < MaxSimLagBeforeFrameDrops && OrderManager.World != null && !OrderManager.World.IsLoadingGameSave && !OrderManager.World.IsReplay)
renderBeforeNextTick = true;
}

Expand Down
39 changes: 12 additions & 27 deletions OpenRA.Game/Network/Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public interface IConnection : IDisposable
void Send(int frame, IEnumerable<byte[]> orders);
void SendImmediate(IEnumerable<byte[]> orders);
void SendSync(int frame, byte[] syncData);
void Receive(Action<int, byte[]> packetFn);
void Receive(Action<int, byte[], int> packetFn);
}

public class ConnectionTarget
Expand Down Expand Up @@ -96,6 +96,7 @@ protected struct ReceivedPacket
{
public int FromClient;
public byte[] Data;
public int Timestep;
}

readonly ConcurrentBag<ReceivedPacket> receivedPackets = new ConcurrentBag<ReceivedPacket>();
Expand Down Expand Up @@ -162,7 +163,7 @@ protected void AddPacket(ReceivedPacket packet)
receivedPackets.Add(packet);
}

public virtual void Receive(Action<int, byte[]> packetFn)
public virtual void Receive(Action<int, byte[], int> packetFn)
{
var packets = new List<ReceivedPacket>(receivedPackets.Count);

Expand All @@ -173,7 +174,7 @@ public virtual void Receive(Action<int, byte[]> packetFn)

foreach (var p in packets)
{
packetFn(p.FromClient, p.Data);
packetFn(p.FromClient, p.Data, p.Timestep);
Recorder?.Receive(p.FromClient, p.Data);
}
}
Expand Down Expand Up @@ -313,7 +314,7 @@ void NetworkConnectionReceive()
var client = reader.ReadInt32();
var buf = reader.ReadBytes(len);

if (UseNewNetcode && client == LocalClientId && len == 7 && buf[4] == (byte)OrderType.Ack)
if (UseNewNetcode && client == LocalClientId && len == 9 && buf[4] == (byte)OrderType.Ack)
{
Ack(buf);
}
Expand All @@ -336,42 +337,26 @@ void NetworkConnectionReceive()

void Ack(byte[] buf)
{
int frameReceived;
short framesToAck;
using (var reader = new BinaryReader(new MemoryStream(buf)))
{
frameReceived = reader.ReadInt32();
reader.ReadByte();
framesToAck = reader.ReadInt16();
}
var reader = new BinaryReader(new MemoryStream(buf));
var frameReceived = reader.ReadInt32();
reader.ReadByte();
var framesToAck = reader.ReadInt16();
var timestep = reader.ReadInt16();

var ms = new MemoryStream(4 + awaitingAckPackets.Take(framesToAck).Sum(i => i.Length));
ms.WriteArray(BitConverter.GetBytes(frameReceived));

for (var i = 0; i < framesToAck; i++)
{
byte[] queuedPacket = default;
if (awaitingAckPackets.Count > 0 && !awaitingAckPackets.TryDequeue(out queuedPacket))
{
// The dequeuing failed because of concurrency, so we retry
for (var c = 0; c < 5; c++)
{
if (awaitingAckPackets.TryDequeue(out queuedPacket))
{
break;
}
}
}

if (queuedPacket == default)
if (!awaitingAckPackets.TryDequeue(out var queuedPacket))
{
throw new InvalidOperationException("Received acks for unknown frames");
}

ms.WriteArray(queuedPacket);
}

AddPacket(new ReceivedPacket { FromClient = LocalClientId, Data = ms.GetBuffer() });
AddPacket(new ReceivedPacket { FromClient = LocalClientId, Data = ms.GetBuffer(), Timestep = timestep });
}

public override int LocalClientId { get { return clientId; } }
Expand Down
21 changes: 20 additions & 1 deletion OpenRA.Game/Network/FrameData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public override string ToString()

readonly HashSet<int> quitClients = new HashSet<int>();
readonly Dictionary<int, Queue<byte[]>> framePackets = new Dictionary<int, Queue<byte[]>>();
readonly Queue<int> timestepData = new Queue<int>();

public IEnumerable<int> ClientsPlayingInFrame()
{
Expand All @@ -46,7 +47,7 @@ public void ClientQuit(int clientId)
quitClients.Add(clientId);
}

public void AddFrameOrders(int clientId, byte[] orders)
public void AddFrameOrders(int clientId, byte[] orders, int timestep)
{
// HACK: Due to design we can actually receive client orders before the game start order
// has been acted on, since immediate orders are buffered, so not all clients will have
Expand All @@ -56,6 +57,9 @@ public void AddFrameOrders(int clientId, byte[] orders)

var frameData = framePackets[clientId];
frameData.Enqueue(orders);

if (timestep != 0)
timestepData.Enqueue(timestep);
}

public bool IsReadyForFrame()
Expand All @@ -80,5 +84,20 @@ public int BufferSizeForClient(int client)
{
return framePackets[client].Count;
}

public bool TryPeekTimestep(out int timestep)
{
return timestepData.TryPeek(out timestep);
}

public void AdvanceFrame()
{
timestepData.TryDequeue(out _);
}

public int BufferTimeRemaining()
{
return timestepData.Sum();
}
}
}
Loading

0 comments on commit 3cf277b

Please sign in to comment.