From 30cafea218d7b28c23d2a8bd8d4d0b713a47a8b3 Mon Sep 17 00:00:00 2001 From: Acruid Date: Fri, 19 Jul 2019 12:49:14 -0700 Subject: [PATCH] Adds a debug net graph to the game state system. --- .../GameStates/ClientGameStateManager.cs | 23 ++- .../GameStates/GameStateProcessor.cs | 10 +- .../GameStates/IGameStateProcessor.cs | 3 +- Robust.Client/GameStates/NetGraphOverlay.cs | 136 +++++++++++++++++- Robust.Client/Graphics/Overlays/Overlay.cs | 2 +- .../GameStates/IClientGameStateManager.cs | 15 +- Robust.Shared/GameStates/GameState.cs | 6 + Robust.Shared/Network/Messages/MsgState.cs | 11 +- Robust.Shared/Network/NetManager.cs | 3 + .../GameStates/GameStateProcessor_Tests.cs | 24 ++-- 10 files changed, 202 insertions(+), 31 deletions(-) diff --git a/Robust.Client/GameStates/ClientGameStateManager.cs b/Robust.Client/GameStates/ClientGameStateManager.cs index 54329933b..3ceec2f8b 100644 --- a/Robust.Client/GameStates/ClientGameStateManager.cs +++ b/Robust.Client/GameStates/ClientGameStateManager.cs @@ -1,4 +1,5 @@ -using Robust.Client.Interfaces; +using System; +using Robust.Client.Interfaces; using Robust.Client.Interfaces.GameObjects; using Robust.Client.Interfaces.GameStates; using Robust.Shared.GameStates; @@ -35,6 +36,12 @@ namespace Robust.Client.GameStates /// public int TargetBufferSize => _processor.TargetBufferSize; + /// + public int CurrentBufferSize => _processor.CurrentBufferSize; + + /// + public event Action GameStateApplied; + /// public void Initialize() { @@ -77,7 +84,7 @@ namespace Robust.Client.GameStates { var state = message.State; - _processor.AddNewState(state, message.MsgSize); + _processor.AddNewState(state); // we always ack everything we receive, even if it is late AckGameState(state.ToSequence); @@ -105,6 +112,18 @@ namespace Robust.Client.GameStates _entities.ApplyEntityStates(curState.EntityStates, curState.EntityDeletions, nextState?.EntityStates); _players.ApplyPlayerStates(curState.PlayerStates); _mapManager.ApplyGameStatePost(curState.MapData); + + GameStateApplied?.Invoke(new GameStateAppliedArgs(curState)); + } + } + + public class GameStateAppliedArgs : EventArgs + { + public GameState AppliedState { get; } + + public GameStateAppliedArgs(GameState appliedState) + { + AppliedState = appliedState; } } } diff --git a/Robust.Client/GameStates/GameStateProcessor.cs b/Robust.Client/GameStates/GameStateProcessor.cs index 30e98abeb..14770be47 100644 --- a/Robust.Client/GameStates/GameStateProcessor.cs +++ b/Robust.Client/GameStates/GameStateProcessor.cs @@ -53,7 +53,7 @@ namespace Robust.Client.GameStates } /// - public void AddNewState(GameState state, int stateSize) + public void AddNewState(GameState state) { // any state from tick 0 is a full state, and needs to be handled different if (state.FromSequence == GameTick.Zero) @@ -64,7 +64,7 @@ namespace Robust.Client.GameStates _lastFullState = state; if (Logging) - Logger.InfoS("net", $"Received Full GameState: to={state.ToSequence}, sz={stateSize}"); + Logger.InfoS("net", $"Received Full GameState: to={state.ToSequence}, sz={state.PayloadSize}"); return; } @@ -76,7 +76,7 @@ namespace Robust.Client.GameStates if (state.ToSequence <= lastTick && !_waitingForFull) // CurTick isn't set properly when WaitingForFull { if (Logging) - Logger.DebugS("net.state", $"Received Old GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={stateSize}, buf={_stateBuffer.Count}"); + Logger.DebugS("net.state", $"Received Old GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}"); return; } @@ -90,7 +90,7 @@ namespace Robust.Client.GameStates continue; if (Logging) - Logger.DebugS("net.state", $"Received Dupe GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={stateSize}, buf={_stateBuffer.Count}"); + Logger.DebugS("net.state", $"Received Dupe GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}"); return; } @@ -99,7 +99,7 @@ namespace Robust.Client.GameStates _stateBuffer.Add(state); if (Logging) - Logger.DebugS("net.state", $"Received New GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={stateSize}, buf={_stateBuffer.Count}"); + Logger.DebugS("net.state", $"Received New GameState: cTick={_timing.CurTick}, fSeq={state.FromSequence}, tSeq={state.ToSequence}, sz={state.PayloadSize}, buf={_stateBuffer.Count}"); } /// diff --git a/Robust.Client/GameStates/IGameStateProcessor.cs b/Robust.Client/GameStates/IGameStateProcessor.cs index 30275d496..f608e80fc 100644 --- a/Robust.Client/GameStates/IGameStateProcessor.cs +++ b/Robust.Client/GameStates/IGameStateProcessor.cs @@ -56,8 +56,7 @@ namespace Robust.Client.GameStates /// Adds a new state into the processor. These are usually from networking or replays. /// /// Newly received state. - /// Optionally provide the size in bytes of this new state. This is strictly for debug logging. - void AddNewState(GameState state, int stateSize); + void AddNewState(GameState state); /// /// Calculates the current and next state to apply for a given game tick. diff --git a/Robust.Client/GameStates/NetGraphOverlay.cs b/Robust.Client/GameStates/NetGraphOverlay.cs index 1609c3f33..fc083ad6e 100644 --- a/Robust.Client/GameStates/NetGraphOverlay.cs +++ b/Robust.Client/GameStates/NetGraphOverlay.cs @@ -1,33 +1,159 @@ -using Robust.Client.Graphics; +using System.Collections.Generic; +using Robust.Client.Graphics; using Robust.Client.Graphics.Drawing; using Robust.Client.Graphics.Overlays; using Robust.Client.Interfaces.Console; +using Robust.Client.Interfaces.GameStates; using Robust.Client.Interfaces.Graphics.Overlays; using Robust.Client.Interfaces.ResourceManagement; using Robust.Client.ResourceManagement; +using Robust.Shared.Interfaces.Network; +using Robust.Shared.Interfaces.Timing; using Robust.Shared.IoC; using Robust.Shared.Maths; +using Robust.Shared.Timing; namespace Robust.Client.GameStates { + /// + /// Visual debug overlay for the network diagnostic graph. + /// internal class NetGraphOverlay : Overlay { + [Dependency] private readonly IGameTiming _gameTiming; + [Dependency] private readonly IClientNetManager _netManager; + [Dependency] private readonly IClientGameStateManager _gameStateManager; + + private const int HistorySize = 60 * 3; // number of ticks to keep in history. + private const int TargetPayloadBps = 56000 / 8; // Target Payload size in Bytes per second. A mind-numbing fifty-six thousand bits per second, who would ever need more? + private const int MidrangePayloadBps = 33600 / 8; // mid-range line + private const int BytesPerPixel = 2; // If you are running the game on a DSL connection, you can scale the graph to fit your absurd bandwidth. + private const int LowerGraphOffset = 100; // Offset on the Y axis in pixels of the lower lag/interp graph. + private const int MsPerPixel = 4; // Latency Milliseconds per pixel, for scaling the graph. + + /// public override OverlaySpace Space => OverlaySpace.ScreenSpace; - private Font _font; + private readonly Font _font; + private GameTick _lastTick; + private int _warningPayloadSize; + private int _midrangePayloadSize; + + private List<(GameTick Tick, int Payload, int lag, int interp)> _history = new List<(GameTick Tick, int Payload, int lag, int interp)>(HistorySize+10); public NetGraphOverlay() : base(nameof(NetGraphOverlay)) { IoCManager.InjectDependencies(this); var cache = IoCManager.Resolve(); _font = new VectorFont(cache.GetResource("/Nano/NotoSans/NotoSans-Regular.ttf"), 10); + + _gameStateManager.GameStateApplied += HandleGameStateApplied; + } + + private void HandleGameStateApplied(GameStateAppliedArgs args) + { + var toSeq = args.AppliedState.ToSequence; + var sz = args.AppliedState.PayloadSize; + + // calc payload size + _warningPayloadSize = TargetPayloadBps / _gameTiming.TickRate; + _midrangePayloadSize = MidrangePayloadBps / _gameTiming.TickRate; + + // calc lag + var lag = _netManager.ServerChannel.Ping; + + // calc interp info + var interpBuff = _gameStateManager.CurrentBufferSize - _gameStateManager.MinBufferSize; + + _history.Add((toSeq, sz, lag, interpBuff)); + } + + /// + internal override void FrameUpdate(RenderFrameEventArgs args) + { + base.FrameUpdate(args); + + var over = _history.Count - HistorySize; + if (over > 0) + { + _history.RemoveRange(0, over); + } } protected override void Draw(DrawingHandleBase handle) { - //TODO: Make me actually work! - handle.DrawLine(new Vector2(50,50), new Vector2(100,100), Color.Green); - DrawString((DrawingHandleScreen)handle, _font, new Vector2(60, 50), "Hello World!"); + // remember, 0,0 is top left of ui with +X right and +Y down + + var leftMargin = 300; + var width = HistorySize; + var height = 500; + + // bottom payload line + handle.DrawLine(new Vector2(leftMargin, height), new Vector2(leftMargin + width, height), Color.DarkGray.WithAlpha(0.8f)); + + // bottom lag line + handle.DrawLine(new Vector2(leftMargin, height + LowerGraphOffset), new Vector2(leftMargin + width, height + LowerGraphOffset), Color.DarkGray.WithAlpha(0.8f)); + + int lastLagY = -1; + int lastLagMs = -1; + // data points + for (var i = 0; i < _history.Count; i++) + { + var state = _history[i]; + + // draw the payload size + var xOff = leftMargin + i; + var yoff = height - state.Payload / BytesPerPixel; + handle.DrawLine(new Vector2(xOff, height), new Vector2(xOff, yoff), Color.LightGreen.WithAlpha(0.8f)); + + // second tick marks + if (state.Tick.Value % _gameTiming.TickRate == 0) + { + handle.DrawLine(new Vector2(xOff, height), new Vector2(xOff, height+2), Color.LightGray); + } + + // lag data + var lagYoff = height + LowerGraphOffset - state.lag / MsPerPixel; + lastLagY = lagYoff - 1; + lastLagMs = state.lag; + handle.DrawLine(new Vector2(xOff, lagYoff - 2), new Vector2(xOff, lagYoff - 1), Color.Blue.WithAlpha(0.8f)); + + // interp data + Color interpColor; + if(state.interp < 0) + interpColor = Color.Red; + else if(state.interp < _gameStateManager.TargetBufferSize - _gameStateManager.MinBufferSize) + interpColor = Color.Yellow; + else + interpColor = Color.Green; + + handle.DrawLine(new Vector2(xOff, height + LowerGraphOffset), new Vector2(xOff, height + LowerGraphOffset + state.interp * 6), interpColor.WithAlpha(0.8f)); + } + + // top payload warning line + var warnYoff = height - _warningPayloadSize / BytesPerPixel; + handle.DrawLine(new Vector2(leftMargin, warnYoff), new Vector2(leftMargin + width, warnYoff), Color.DarkGray.WithAlpha(0.8f)); + + // mid payload line + var midYoff = height - _midrangePayloadSize / BytesPerPixel; + handle.DrawLine(new Vector2(leftMargin, midYoff), new Vector2(leftMargin + width, midYoff), Color.DarkGray.WithAlpha(0.8f)); + + // payload text + DrawString((DrawingHandleScreen)handle, _font, new Vector2(leftMargin + width, warnYoff), "56K"); + DrawString((DrawingHandleScreen)handle, _font, new Vector2(leftMargin + width, midYoff), "33.6K"); + + // interp text info + if(lastLagY != -1) + DrawString((DrawingHandleScreen)handle, _font, new Vector2(leftMargin + width, lastLagY), $"{lastLagMs.ToString()}ms"); + + DrawString((DrawingHandleScreen)handle, _font, new Vector2(leftMargin, height + LowerGraphOffset), $"{_gameStateManager.CurrentBufferSize.ToString()} states"); + } + + protected override void Dispose(bool disposing) + { + _gameStateManager.GameStateApplied -= HandleGameStateApplied; + + base.Dispose(disposing); } private void DrawString(DrawingHandleScreen handle, Font font, Vector2 pos, string str) diff --git a/Robust.Client/Graphics/Overlays/Overlay.cs b/Robust.Client/Graphics/Overlays/Overlay.cs index 45188cdd4..eb61513fd 100644 --- a/Robust.Client/Graphics/Overlays/Overlay.cs +++ b/Robust.Client/Graphics/Overlays/Overlay.cs @@ -75,7 +75,7 @@ namespace Robust.Client.Graphics.Overlays _isDirty = true; } - internal void FrameUpdate(RenderFrameEventArgs args) + internal virtual void FrameUpdate(RenderFrameEventArgs args) { } diff --git a/Robust.Client/Interfaces/GameStates/IClientGameStateManager.cs b/Robust.Client/Interfaces/GameStates/IClientGameStateManager.cs index 11d73bb74..dcfbff0bd 100644 --- a/Robust.Client/Interfaces/GameStates/IClientGameStateManager.cs +++ b/Robust.Client/Interfaces/GameStates/IClientGameStateManager.cs @@ -1,4 +1,7 @@ -namespace Robust.Client.Interfaces.GameStates +using System; +using Robust.Client.GameStates; + +namespace Robust.Client.Interfaces.GameStates { /// /// Engine service that provides processing and management of game states. @@ -20,6 +23,16 @@ /// int TargetBufferSize { get; } + /// + /// Number of game states currently in the state buffer. + /// + int CurrentBufferSize { get; } + + /// + /// This is called after the game state has been applied for the current tick. + /// + event Action GameStateApplied; + /// /// One time initialization of the service. /// diff --git a/Robust.Shared/GameStates/GameState.cs b/Robust.Shared/GameStates/GameState.cs index 052644173..d97666c31 100644 --- a/Robust.Shared/GameStates/GameState.cs +++ b/Robust.Shared/GameStates/GameState.cs @@ -16,6 +16,12 @@ namespace Robust.Shared.GameStates [field:NonSerialized] public bool Extrapolated { get; set; } + /// + /// The serialized size in bytes of this game state. + /// + [field:NonSerialized] + public int PayloadSize { get; set; } + /// /// Constructor! /// diff --git a/Robust.Shared/Network/Messages/MsgState.cs b/Robust.Shared/Network/Messages/MsgState.cs index 2605cc421..fe62a84bc 100644 --- a/Robust.Shared/Network/Messages/MsgState.cs +++ b/Robust.Shared/Network/Messages/MsgState.cs @@ -1,9 +1,11 @@ -using Lidgren.Network; +using System; +using Lidgren.Network; using Robust.Shared.GameStates; using Robust.Shared.Interfaces.Network; using Robust.Shared.Interfaces.Serialization; using Robust.Shared.IoC; using System.IO; +using Robust.Shared.Utility; namespace Robust.Shared.Network.Messages { @@ -20,13 +22,15 @@ namespace Robust.Shared.Network.Messages public override void ReadFromBuffer(NetIncomingMessage buffer) { MsgSize = buffer.LengthBytes; - var length = buffer.ReadInt32(); + var length = buffer.ReadVariableInt32(); var stateData = buffer.ReadBytes(length); using (var stateStream = new MemoryStream(stateData)) { var serializer = IoCManager.Resolve(); State = serializer.Deserialize(stateStream); } + + State.PayloadSize = length; } public override void WriteToBuffer(NetOutgoingMessage buffer) @@ -34,8 +38,9 @@ namespace Robust.Shared.Network.Messages var serializer = IoCManager.Resolve(); using (var stateStream = new MemoryStream()) { + DebugTools.Assert(stateStream.Length <= Int32.MaxValue); serializer.Serialize(stateStream, State); - buffer.Write((int)stateStream.Length); + buffer.WriteVariableInt32((int)stateStream.Length); buffer.Write(stateStream.ToArray()); } MsgSize = buffer.LengthBytes; diff --git a/Robust.Shared/Network/NetManager.cs b/Robust.Shared/Network/NetManager.cs index 557d3c496..47310b61a 100644 --- a/Robust.Shared/Network/NetManager.cs +++ b/Robust.Shared/Network/NetManager.cs @@ -307,6 +307,9 @@ namespace Robust.Shared.Network { var netConfig = new NetPeerConfiguration("SS14_NetTag"); + // ping the client 4 times every second. + netConfig.PingInterval = 0.25f; + #if DEBUG //Simulate Latency netConfig.SimulatedLoss = _config.GetCVar("net.fakeloss"); diff --git a/Robust.UnitTesting/Client/GameStates/GameStateProcessor_Tests.cs b/Robust.UnitTesting/Client/GameStates/GameStateProcessor_Tests.cs index ba7324864..e10c09f93 100644 --- a/Robust.UnitTesting/Client/GameStates/GameStateProcessor_Tests.cs +++ b/Robust.UnitTesting/Client/GameStates/GameStateProcessor_Tests.cs @@ -19,8 +19,8 @@ namespace Robust.UnitTesting.Client.GameStates var timing = timingMock.Object; var processor = new GameStateProcessor(timing); - processor.AddNewState(GameStateFactory(0, 1), 0); - processor.AddNewState(GameStateFactory(1, 2), 0); // buffer is at 2/3, so processing should be blocked + processor.AddNewState(GameStateFactory(0, 1)); + processor.AddNewState(GameStateFactory(1, 2)); // buffer is at 2/3, so processing should be blocked // calculate states for first tick timing.CurTick = new GameTick(3); @@ -39,9 +39,9 @@ namespace Robust.UnitTesting.Client.GameStates var timing = timingMock.Object; var processor = new GameStateProcessor(timing); - processor.AddNewState(GameStateFactory(0, 1), 0); - processor.AddNewState(GameStateFactory(1, 2), 0); - processor.AddNewState(GameStateFactory(2, 3), 0); // buffer is now full, otherwise cannot calculate states. + processor.AddNewState(GameStateFactory(0, 1)); + processor.AddNewState(GameStateFactory(1, 2)); + processor.AddNewState(GameStateFactory(2, 3)); // buffer is now full, otherwise cannot calculate states. // calculate states for first tick timing.CurTick = new GameTick(1); @@ -67,9 +67,9 @@ namespace Robust.UnitTesting.Client.GameStates var timing = timingMock.Object; var processor = new GameStateProcessor(timing); - processor.AddNewState(GameStateFactory(0, 1), 0); - processor.AddNewState(GameStateFactory(1, 2), 0); - processor.AddNewState(GameStateFactory(2, 3), 0); // buffer is now full, otherwise cannot calculate states. + processor.AddNewState(GameStateFactory(0, 1)); + processor.AddNewState(GameStateFactory(1, 2)); + processor.AddNewState(GameStateFactory(2, 3)); // buffer is now full, otherwise cannot calculate states. // calculate states for first tick timing.CurTick = new GameTick(3); @@ -87,7 +87,7 @@ namespace Robust.UnitTesting.Client.GameStates // a few moments later... timing.CurTick = new GameTick(5); // current clock is ahead of server - processor.AddNewState(GameStateFactory(3, 4), 0); // received a late state + processor.AddNewState(GameStateFactory(3, 4)); // received a late state var result = processor.ProcessTickStates(timing.CurTick, out _, out _); Assert.That(result, Is.False); @@ -174,9 +174,9 @@ namespace Robust.UnitTesting.Client.GameStates var timing = timingMock.Object; var processor = new GameStateProcessor(timing); - processor.AddNewState(GameStateFactory(0, 1), 0); - processor.AddNewState(GameStateFactory(1, 2), 0); - processor.AddNewState(GameStateFactory(2, 3), 0); // buffer is now full, otherwise cannot calculate states. + processor.AddNewState(GameStateFactory(0, 1)); + processor.AddNewState(GameStateFactory(1, 2)); + processor.AddNewState(GameStateFactory(2, 3)); // buffer is now full, otherwise cannot calculate states. // calculate states for first tick timing.CurTick = new GameTick(1);