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();
}
}
}
}