using Blacklight.Core; using System; using System.Text; namespace Glass.Testbed { /// /// What each pass of a frame costs, averaged over a window, and how many frames missed a vblank. /// public class FrameStats { private const double ReadoutIntervalInSeconds = 0.5; // 1.5 rather than 2 so a frame that misses a vblank by a hair still counts, without ordinary jitter in a // frame that made it tripping the counter. private const double DroppedFrameRatio = 1.5; private TimeSpan updateSinceReadout; private TimeSpan drawSinceReadout; private TimeSpan submitSinceReadout; private TimeSpan gpuWaitSinceReadout; private TimeSpan vsyncWaitSinceReadout; private TimeSpan intervalSinceReadout; private int framesSinceReadout; private readonly VerticalSyncMode verticalSync; private readonly bool measuresGpuWait; // Kept rather than rebuilt, since the readout is rewritten for as long as the process runs. private readonly StringBuilder readout = new StringBuilder(); private TimeSpan? previousSubmitAt; // Present cannot return faster than the display refreshes, so the shortest interval seen is the refresh // period. Self-calibrating, where asking the adapter for it would mean a new WindowHandle member. private TimeSpan shortestInterval = TimeSpan.MaxValue; public int DroppedFrames { get; private set; } public string Text { get; private set; } = "frame: measuring..."; public FrameStats(VerticalSyncMode verticalSync, bool measuresGpuWait) { this.verticalSync = verticalSync; this.measuresGpuWait = measuresGpuWait; } /// /// Whether the gap since the previous frame reached the screen is long enough to be a missed vblank rather /// than jitter. /// private bool IsDropped(TimeSpan interval) { return interval.TotalMilliseconds > shortestInterval.TotalMilliseconds * DroppedFrameRatio; } /// /// Divides the field about to be written from whatever has already been written, if anything has. /// private void AppendFieldSeparator() { if (readout.Length > 0) { readout.Append(" | "); } } private void AppendMilliseconds(string label, TimeSpan sinceReadout, double frames) { AppendFieldSeparator(); readout.Append(label).Append(' ') .AppendFormat("{0:F2}", sinceReadout.TotalMilliseconds / frames).Append(" ms"); } private string Format() { double frames = framesSinceReadout; double intervalInMilliseconds = intervalSinceReadout.TotalMilliseconds / frames; readout.Clear(); AppendMilliseconds("update", updateSinceReadout, frames); AppendMilliseconds("draw", drawSinceReadout, frames); AppendMilliseconds("submit", submitSinceReadout, frames); if (measuresGpuWait) { AppendMilliseconds("gpu", gpuWaitSinceReadout, frames); } // Nothing is waiting on the display when vsync is off, so what's left is the bare Present call. AppendMilliseconds( verticalSync == VerticalSyncMode.Off ? "present" : "vsync", vsyncWaitSinceReadout, frames); AppendFieldSeparator(); readout.Append("frame ").AppendFormat("{0:F1}", intervalInMilliseconds) .Append(" ms (").AppendFormat("{0:F0}", 1000 / intervalInMilliseconds).Append(" fps)"); // A dropped frame is one that missed a vblank, which needs there to have been a vblank to miss. if (verticalSync != VerticalSyncMode.Off) { AppendFieldSeparator(); readout.Append("dropped ").Append(DroppedFrames); } return readout.ToString(); } private void StartNewReadout() { updateSinceReadout = TimeSpan.Zero; drawSinceReadout = TimeSpan.Zero; submitSinceReadout = TimeSpan.Zero; gpuWaitSinceReadout = TimeSpan.Zero; vsyncWaitSinceReadout = TimeSpan.Zero; intervalSinceReadout = TimeSpan.Zero; framesSinceReadout = 0; } /// /// Takes one frame's costs. /// public void RecordFrame(TimeSpan update, TimeSpan draw, FrameCost frameCost, TimeSpan submittedAt) { // The first frame has nothing to measure an interval against, and costs a good deal more than a // steady-state one anyway. if (!previousSubmitAt.HasValue) { previousSubmitAt = submittedAt; return; } TimeSpan interval = submittedAt - previousSubmitAt.Value; previousSubmitAt = submittedAt; if (interval < shortestInterval) { shortestInterval = interval; } else if (IsDropped(interval)) { ++DroppedFrames; } updateSinceReadout += update; drawSinceReadout += draw; submitSinceReadout += frameCost.Submit; gpuWaitSinceReadout += frameCost.GpuWait; vsyncWaitSinceReadout += frameCost.VSyncWait; intervalSinceReadout += interval; ++framesSinceReadout; // Averaged over a window rather than shown per frame, which jitters too much to read. if (intervalSinceReadout.TotalSeconds >= ReadoutIntervalInSeconds) { Text = Format(); StartNewReadout(); } } } }