using System; using System.Linq; using Robust.Shared.Maths; using Robust.Shared.Utility; namespace Robust.Shared.Timing { /// /// This holds main loop timing information and helper functions. /// [Virtual] public class GameTiming : IGameTiming { // number of sample frames to store for profiling private const int NumFrames = 60; private readonly IStopwatch _realTimer = new Stopwatch(); private readonly long[] _realFrameTimes = new long[NumFrames]; private int _frameIdx; private TimeSpan _lastRealTime; /// /// Default constructor. /// public GameTiming() { // does nothing if timer is already running _realTimer.Start(); Paused = true; TickRate = NumFrames; } /// /// Is program execution inside of the simulation update, or rendering? /// public bool InSimulation { get; set; } /// /// Is the simulation currently paused? /// public bool Paused { get; set; } // Cached values for making CurTime not jump whenever the tickrate // changes. This holds the last calculated value of CurTime, and the // tick that the value was calculated for. The next time CurTime is // calculated, it should only try to monotonically increase both of // these values // // Notice that it starts from GameTick 1 - the "first tick" has no impact // on timing public (TimeSpan, GameTick) TimeBase { get; set; } = (TimeSpan.Zero, GameTick.First); /// /// The current synchronized uptime of the simulation. Use this for in-game timing. This can be rewound for /// prediction, and is affected by Paused and TimeScale. /// public TimeSpan CurTime { get { // last tickrate change epoch var (time, lastTimeTick) = TimeBase; // add our current time to it. // the server never rewinds time, and the client never rewinds time outside of prediction. // the only way this assert should fail is if the TickRate is changed inside prediction, which should never happen. //DebugTools.Assert(CurTick >= lastTimeTick); //TODO: turns out prediction leaves CurTick at the last predicted tick, and not at the last processed server tick //so time gets rewound when processing events like TickRate. time += TickPeriod.Mul(CurTick.Value - lastTimeTick.Value); if (!InSimulation) // rendering can draw frames between ticks { DebugTools.Assert(0 <= (time + TickRemainder).TotalSeconds); return time + TickRemainder; } DebugTools.Assert(0 <= time.TotalSeconds); return time; } } /// /// The current real uptime of the simulation. Use this for UI and out of game timing. /// public TimeSpan RealTime => _realTimer.Elapsed; public TimeSpan FrameStartTime => _lastRealTime; public virtual TimeSpan ServerTime => TimeSpan.Zero; /// /// The simulated time it took to render the last frame. /// public TimeSpan FrameTime => CalcFrameTime(); /// /// The real time it took to render the last frame. /// public TimeSpan RealFrameTime { get; private set; } /// /// Average real frame time over the last 50 frames. /// public TimeSpan RealFrameTimeAvg => TimeSpan.FromTicks((long)_realFrameTimes.Average()); /// /// Standard Deviation of the real frame time over the last 50 frames. /// public TimeSpan RealFrameTimeStdDev => CalcRftStdDev(); /// /// Average real FPS over the last 50 frames. /// public double FramesPerSecondAvg => CalcFpsAvg(); /// /// The current simulation tick being processed. /// public GameTick CurTick { get; set; } = new(1); // Time always starts on the first tick /// /// Timespan for the last tick. /// public TimeSpan LastTick { get; set; } private ushort _tickRate; private TimeSpan _tickRemainder; /// /// The target ticks/second of the simulation. /// public ushort TickRate { get => _tickRate; set => SetTickRateAt(value, CurTick); } public float TimeScale { get; set; } = 1; /// /// The length of a tick at the current TickRate. 1/TickRate. /// public TimeSpan TickPeriod => TimeSpan.FromTicks((long)(1.0 / TickRate * TimeSpan.TicksPerSecond)); /// /// The remaining time left over after the last tick was ran. /// public TimeSpan TickRemainder { get => _tickRemainder; set { // Generally the upper limit is Tickrate*2, but changing the tickrate mid-round can make this really large until timing can stabilize DebugTools.Assert(TimeSpan.Zero <= TickRemainder); _tickRemainder = value; } } public TimeSpan TickRemainderRealtime => TickRemainder * TimeScale; public TimeSpan CalcAdjustedTickPeriod() { // ranges from -1 to 1, with 0 being 'default' var ratio = MathHelper.Clamp(TickTimingAdjustment, -0.99f, 0.99f); // Final period ranges from near 0 (runs very fast to catch up) or 2 * tick period (runs at half speed). return TickPeriod * (1-ratio) * TimeScale; } /// /// Current graphics frame since init OpenGL which is taken as frame 1, from swapbuffer to swapbuffer. Useful to set a /// conditional breakpoint on specific frames, and synchronize with OGL debugging tools that capture frames. /// Depending on the tools used, this frame number will vary between 1 frame more or less due to how that tool is counting frames, /// i.e. starting from 0 or 1, having a separate counter, etc. Available in timing debug panel. /// public uint CurFrame { get; set; } = 1; /// public float TickTimingAdjustment { get; set; } = 0; /// /// Ends the 'lap' of the timer, updating frame time info. /// public void StartFrame() { // calculate real timing info var curRealTime = RealTime; RealFrameTime = curRealTime - _lastRealTime; _lastRealTime = curRealTime; // update profiling _frameIdx = (1 + _frameIdx) % _realFrameTimes.Length; _realFrameTimes[_frameIdx] = RealFrameTime.Ticks; } private TimeSpan CalcFrameTime() { // calculate simulation FrameTime if (InSimulation) { return TimeSpan.FromTicks(TickPeriod.Ticks); } else { return Paused ? TimeSpan.Zero : RealFrameTime; } } // Calculate and store the current time value, based on the current tick rate. // Call this whenever you change the TickRate. private void CacheCurTime(GameTick atTick) { var (cachedTime, lastTimeTick) = TimeBase; var newTime = cachedTime + TickPeriod.Mul(atTick.Value - lastTimeTick.Value); DebugTools.Assert(TimeSpan.Zero <= newTime); TimeBase = (newTime, atTick); } /// /// Resets the simulation time. /// public void ResetSimTime() { ResetSimTime((TimeSpan.Zero, GameTick.First)); } public void ResetSimTime((TimeSpan, GameTick) timeBase) { TimeBase = timeBase; CurTick = GameTick.First; TickRemainder = TimeSpan.Zero; Paused = true; } public void SetTickRateAt(ushort tickRate, GameTick atTick) { // Check this, because TickRate is a divisor in the cache calculation // The first time TickRate is set, no time will have passed anyways if (_tickRate != 0) // Cache BEFORE updating the tick rate, because ticks up until // now have been on a different rate, so they count for a // different amount of time CacheCurTime(atTick); _tickRate = tickRate; } public virtual TimeSpan RealLocalToServer(TimeSpan local) { return TimeSpan.Zero; } public virtual TimeSpan RealServerToLocal(TimeSpan server) { return TimeSpan.Zero; } protected virtual TimeSpan? GetServerOffset() { return null; } public bool IsFirstTimePredicted { get; protected set; } = true; /// public virtual bool InPrediction => false; /// public bool ApplyingState {get; protected set; } /// /// Calculates the average FPS of the last 50 real frame times. /// /// private double CalcFpsAvg() { return 1 / (_realFrameTimes.Average() / TimeSpan.TicksPerSecond); } /// /// Calculates the standard deviation of the last 50 real frame times. /// /// private TimeSpan CalcRftStdDev() { var sum = _realFrameTimes.Sum(); var count = _realFrameTimes.Length; var avg = sum / (double)count; double devSquared = 0.0f; for (var i = 0; i < count; ++i) { if (_realFrameTimes[i] == 0) continue; var ft = _realFrameTimes[i]; var dt = ft - avg; devSquared += dt * dt; } var variance = devSquared / (count - 1); return TimeSpan.FromTicks((long)Math.Sqrt(variance)); } internal static bool IsTimescaleValid(float scale) { return scale > 0 && float.IsNormal(scale) && float.IsFinite(scale); } } }