Adds a debug net graph to the game state system.

This commit is contained in:
Acruid
2019-07-19 12:49:14 -07:00
parent d9297fa1c9
commit 30cafea218
10 changed files with 202 additions and 31 deletions

View File

@@ -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
/// <inheritdoc />
public int TargetBufferSize => _processor.TargetBufferSize;
/// <inheritdoc />
public int CurrentBufferSize => _processor.CurrentBufferSize;
/// <inheritdoc />
public event Action<GameStateAppliedArgs> GameStateApplied;
/// <inheritdoc />
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;
}
}
}

View File

@@ -53,7 +53,7 @@ namespace Robust.Client.GameStates
}
/// <inheritdoc />
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}");
}
/// <inheritdoc />

View File

@@ -56,8 +56,7 @@ namespace Robust.Client.GameStates
/// Adds a new state into the processor. These are usually from networking or replays.
/// </summary>
/// <param name="state">Newly received state.</param>
/// <param name="stateSize">Optionally provide the size in bytes of this new state. This is strictly for debug logging.</param>
void AddNewState(GameState state, int stateSize);
void AddNewState(GameState state);
/// <summary>
/// Calculates the current and next state to apply for a given game tick.

View File

@@ -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
{
/// <summary>
/// Visual debug overlay for the network diagnostic graph.
/// </summary>
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.
/// <inheritdoc />
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<IResourceCache>();
_font = new VectorFont(cache.GetResource<FontResource>("/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));
}
/// <inheritdoc />
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)

View File

@@ -75,7 +75,7 @@ namespace Robust.Client.Graphics.Overlays
_isDirty = true;
}
internal void FrameUpdate(RenderFrameEventArgs args)
internal virtual void FrameUpdate(RenderFrameEventArgs args)
{
}

View File

@@ -1,4 +1,7 @@
namespace Robust.Client.Interfaces.GameStates
using System;
using Robust.Client.GameStates;
namespace Robust.Client.Interfaces.GameStates
{
/// <summary>
/// Engine service that provides processing and management of game states.
@@ -20,6 +23,16 @@
/// </summary>
int TargetBufferSize { get; }
/// <summary>
/// Number of game states currently in the state buffer.
/// </summary>
int CurrentBufferSize { get; }
/// <summary>
/// This is called after the game state has been applied for the current tick.
/// </summary>
event Action<GameStateAppliedArgs> GameStateApplied;
/// <summary>
/// One time initialization of the service.
/// </summary>

View File

@@ -16,6 +16,12 @@ namespace Robust.Shared.GameStates
[field:NonSerialized]
public bool Extrapolated { get; set; }
/// <summary>
/// The serialized size in bytes of this game state.
/// </summary>
[field:NonSerialized]
public int PayloadSize { get; set; }
/// <summary>
/// Constructor!
/// </summary>

View File

@@ -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<IRobustSerializer>();
State = serializer.Deserialize<GameState>(stateStream);
}
State.PayloadSize = length;
}
public override void WriteToBuffer(NetOutgoingMessage buffer)
@@ -34,8 +38,9 @@ namespace Robust.Shared.Network.Messages
var serializer = IoCManager.Resolve<IRobustSerializer>();
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;

View File

@@ -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<float>("net.fakeloss");

View File

@@ -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);