feat(pipeline): MP0 - FrameProfEnabled flag + GPU TimeElapsed ring
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>
This commit is contained in:
parent
7d74c68c60
commit
0a5f7d4924
2 changed files with 104 additions and 0 deletions
87
src/AcDream.App/Diagnostics/GpuFrameTimer.cs
Normal file
87
src/AcDream.App/Diagnostics/GpuFrameTimer.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -567,4 +567,21 @@ public static class RenderingDiagnostics
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool ShouldRenderIndoor(uint playerCellId, bool renderRootResolved)
|
public static bool ShouldRenderIndoor(uint playerCellId, bool renderRootResolved)
|
||||||
=> renderRootResolved && IsEnvCellId(playerCellId);
|
=> renderRootResolved && IsEnvCellId(playerCellId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MP0 (2026-07-05) — master toggle for the permanent frame profiler
|
||||||
|
/// (<c>AcDream.App.Diagnostics.FrameProfiler</c>): CPU frame time
|
||||||
|
/// (swap-to-swap), whole-frame GPU time, per-stage CPU attribution,
|
||||||
|
/// per-frame allocation counters, reported as one <c>[frame-prof]</c>
|
||||||
|
/// line every ~5 s. Permanent apparatus (every MP-track gate reads it) —
|
||||||
|
/// do NOT strip with session probes. The whole-frame GPU query is
|
||||||
|
/// self-disabled while <c>ACDREAM_WB_DIAG=1</c> (GL forbids nested
|
||||||
|
/// TimeElapsed queries; WbDrawDispatcher owns per-pass queries under
|
||||||
|
/// that flag — the 2026-06-23 "separate flags" measurement lesson).
|
||||||
|
/// Initial state from <c>ACDREAM_FRAME_PROF=1</c>; runtime-toggleable
|
||||||
|
/// via the DebugPanel mirror (<c>DebugVM.FrameProf</c>).
|
||||||
|
/// Spec: docs/superpowers/specs/2026-07-05-modern-pipeline-design.md §5.
|
||||||
|
/// </summary>
|
||||||
|
public static bool FrameProfEnabled { get; set; } =
|
||||||
|
Environment.GetEnvironmentVariable("ACDREAM_FRAME_PROF") == "1";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue