using System; using Silk.NET.OpenGL; namespace AcDream.App.Diagnostics; /// /// MP0 (2026-07-05) — whole-frame GPU time via a ring of /// 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. /// /// 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. /// 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(); } /// /// 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. /// 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; } /// End any active query without beginning a new one (used when the profiler is toggled off mid-session). 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]); } }