RenderingDiagnostics.FrameProfEnabled is the master toggle for the upcoming permanent frame profiler (ACDREAM_FRAME_PROF=1, runtime mirror via DebugVM.FrameProf). GpuFrameTimer wraps a depth-4 ring of TimeElapsed queries for whole-frame GPU time, mirroring WbDrawDispatcher's query idiom (including the #125 never-begun-slot guard). Caller must not run this while ACDREAM_WB_DIAG=1 owns TimeElapsed queries — GL forbids nested TimeElapsed queries. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
87 lines
2.9 KiB
C#
87 lines
2.9 KiB
C#
using System;
|
|
using Silk.NET.OpenGL;
|
|
|
|
namespace AcDream.App.Diagnostics;
|
|
|
|
/// <summary>
|
|
/// MP0 (2026-07-05) — whole-frame GPU time via a ring of
|
|
/// <see cref="QueryTarget.TimeElapsed"/> queries (depth 4, so results are
|
|
/// read ~3 frames late and never stall). Mirrors WbDrawDispatcher's query
|
|
/// idiom including the #125 lesson: a glGenQueries name is not a query
|
|
/// OBJECT until first glBeginQuery, so never-begun slots are skipped via
|
|
/// the Begun flags.
|
|
///
|
|
/// <para>MUST NOT be active while ACDREAM_WB_DIAG=1: GL forbids two
|
|
/// simultaneously active TimeElapsed queries and WbDrawDispatcher brackets
|
|
/// its passes with them under that flag. The caller (FrameProfiler)
|
|
/// enforces the exclusion; this class just does the ring.</para>
|
|
/// </summary>
|
|
internal sealed class GpuFrameTimer : IDisposable
|
|
{
|
|
private const int RingDepth = 4;
|
|
|
|
private readonly GL _gl;
|
|
private readonly uint[] _queries = new uint[RingDepth];
|
|
private readonly bool[] _begun = new bool[RingDepth];
|
|
private int _frameIndex;
|
|
private bool _queryActive;
|
|
|
|
public GpuFrameTimer(GL gl)
|
|
{
|
|
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
|
for (int i = 0; i < RingDepth; i++)
|
|
_queries[i] = _gl.GenQuery();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call once per frame at the frame boundary. Ends the previous
|
|
/// frame's query, polls the oldest slot non-blocking, begins this
|
|
/// frame's query. Returns the completed GPU time in microseconds for
|
|
/// a ~RingDepth-frames-old frame, or null when no result is ready.
|
|
/// </summary>
|
|
public long? FrameBoundary()
|
|
{
|
|
if (_queryActive)
|
|
{
|
|
_gl.EndQuery(QueryTarget.TimeElapsed);
|
|
_queryActive = false;
|
|
}
|
|
|
|
long? completedUs = null;
|
|
int readSlot = _frameIndex % RingDepth; // about to be reused — oldest
|
|
if (_begun[readSlot])
|
|
{
|
|
_gl.GetQueryObject(_queries[readSlot], QueryObjectParameterName.ResultAvailable, out int avail);
|
|
if (avail != 0)
|
|
{
|
|
_gl.GetQueryObject(_queries[readSlot], QueryObjectParameterName.Result, out ulong ns);
|
|
completedUs = (long)(ns / 1000UL);
|
|
}
|
|
// Not available ⇒ sample silently dropped (same policy as
|
|
// WbDrawDispatcher) — percentiles tolerate missing samples.
|
|
}
|
|
|
|
_gl.BeginQuery(QueryTarget.TimeElapsed, _queries[readSlot]);
|
|
_begun[readSlot] = true;
|
|
_queryActive = true;
|
|
_frameIndex++;
|
|
return completedUs;
|
|
}
|
|
|
|
/// <summary>End any active query without beginning a new one (used when the profiler is toggled off mid-session).</summary>
|
|
public void Stop()
|
|
{
|
|
if (_queryActive)
|
|
{
|
|
_gl.EndQuery(QueryTarget.TimeElapsed);
|
|
_queryActive = false;
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Stop();
|
|
for (int i = 0; i < RingDepth; i++)
|
|
_gl.DeleteQuery(_queries[i]);
|
|
}
|
|
}
|