#125: gpu_us query ring reads never-begun query objects - root cause of the WB_DIAG GL error cascade; fixed + live-verified

Root cause (by read, verified live): a glGenQueries name does not become
a QUERY OBJECT until its first glBeginQuery - GetQueryObject on a
never-begun name is GL_INVALID_OPERATION. The N.6 gpu_us ring assumed
ONE dispatcher Draw per frame with both passes always non-empty; the
pview pipeline issues MANY small Draws per frame (landscape slices,
per-cell static buckets, dynamics), where zero-draw passes routinely
skip BeginQuery. Under ACDREAM_WB_DIAG=1 the slot read queued an
InvalidOperation EVERY frame - silently, until WB's diligent
texture-path glGetError checks ate the stale errors and treated their
own successful uploads as failures ([wb-error] + the sticky drop) and
ProcessDirtyUpdates' check threw (process death, tower-wbdiag3.log).
The GL-error-attribution trap, textbook form.

Fix: begun-flags per ring slot per target; the read path only queries
slots that were actually begun (a skipped pass contributes 0 ns).
Live verification (tower-wbdiag4.log, in-tower spawn): zero [wb-error]
(was 7), no crash, gpu_us reads real values (9-11 us) for the first
time under the pview pipeline, meshMissing=0 / entSeen==entDrawn.

Consequences: (1) the #119 missing-stairs mechanism theory via sticky
GL upload failures is RETIRED for normal runs (WB_DIAG off = no query
calls = no errors; clean runs confirmed zero wb-error) - and the
in-tower screenshot on the current build shows the spiral staircase
RENDERING, so the stairs were most plausibly a #120 flood-corruption
casualty (the tower threshold cells portal back to 0x0107 exactly in
the ping-pong window); user verdict pending. (2) The sticky-drop
defect (upload failure never retried) stays filed under #125 as
defense-in-depth debt - the trigger is gone but the design flaw isn't.

Suites: App 236, Core 1419+2skip, UI 420, Net 294.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-11 18:19:55 +02:00
parent 63d14c3d6b
commit fcade06c46

View file

@ -257,6 +257,19 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
private const int GpuQueryRingDepth = 3;
private readonly uint[] _gpuQueryOpaque = new uint[GpuQueryRingDepth];
private readonly uint[] _gpuQueryTransparent = new uint[GpuQueryRingDepth];
// #125: a glGenQueries name does not become a QUERY OBJECT until its first
// glBeginQuery — GetQueryObject on a never-begun name is GL_INVALID_OPERATION.
// The N.6 ring assumed ONE Draw per frame with both passes always non-empty;
// the pview pipeline issues MANY small Draws per frame (landscape slices,
// per-cell buckets, dynamics), where zero-draw passes routinely skip
// BeginQuery. Under ACDREAM_WB_DIAG=1 the slot read then queued an
// InvalidOperation EVERY frame — silently, until WB's diligent texture-path
// glGetError checks ate the stale errors and treated their own successful
// uploads as failures ([wb-error] + sticky drop) and ProcessDirtyUpdates'
// check threw (process death; tower-wbdiag3.log). Track which slots were
// actually begun and only read those.
private readonly bool[] _gpuQueryOpaqueBegun = new bool[GpuQueryRingDepth];
private readonly bool[] _gpuQueryTransparentBegun = new bool[GpuQueryRingDepth];
private int _gpuQueryFrameIndex;
private readonly long[] _gpuSamples = new long[256]; // microseconds
private int _gpuSampleCursor;
@ -1303,18 +1316,40 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
// re-reading the same slot, producing duplicate stale samples.
if (diag && _gpuQueriesInitialized && _gpuQueryFrameIndex >= GpuQueryRingDepth)
{
_gl.GetQueryObject(_gpuQueryOpaque[gpuQuerySlot], QueryObjectParameterName.ResultAvailable, out int avail);
if (avail != 0)
// #125: only read slots whose query objects were actually BEGUN (a
// zero-draw pass skips BeginQuery; reading a never-begun name is
// GL_INVALID_OPERATION). A pass that never ran contributes 0 ns.
ulong opaqueNs = 0, transNs = 0;
bool anyRead = false, allAvailable = true;
if (_gpuQueryOpaqueBegun[gpuQuerySlot])
{
_gl.GetQueryObject(_gpuQueryOpaque[gpuQuerySlot], QueryObjectParameterName.ResultAvailable, out int availO);
if (availO != 0)
{
_gl.GetQueryObject(_gpuQueryOpaque[gpuQuerySlot], QueryObjectParameterName.Result, out opaqueNs);
anyRead = true;
}
else allAvailable = false;
}
if (_gpuQueryTransparentBegun[gpuQuerySlot])
{
_gl.GetQueryObject(_gpuQueryTransparent[gpuQuerySlot], QueryObjectParameterName.ResultAvailable, out int availT);
if (availT != 0)
{
_gl.GetQueryObject(_gpuQueryTransparent[gpuQuerySlot], QueryObjectParameterName.Result, out transNs);
anyRead = true;
}
else allAvailable = false;
}
// If a begun query isn't available yet the sample is dropped
// silently. MedianMicros computes over the non-zero subset, so
// dropped samples don't poison the median.
if (anyRead && allAvailable)
{
_gl.GetQueryObject(_gpuQueryOpaque[gpuQuerySlot], QueryObjectParameterName.Result, out ulong opaqueNs);
_gl.GetQueryObject(_gpuQueryTransparent[gpuQuerySlot], QueryObjectParameterName.Result, out ulong transNs);
long gpuUs = (long)((opaqueNs + transNs) / 1000UL);
_gpuSamples[_gpuSampleCursor] = gpuUs;
_gpuSampleCursor = (_gpuSampleCursor + 1) % _gpuSamples.Length;
}
// If avail==0 the sample is dropped silently. MedianMicros
// computes over the non-zero subset, so dropped samples don't
// poison the median.
}
// ── Phase 7: opaque pass ─────────────────────────────────────────────
@ -1334,7 +1369,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
// mesh_modern.vert for why this is needed.
_shader.SetInt("uDrawIDOffset", 0);
_gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, _indirectBuffer);
if (diag && _gpuQueriesInitialized) _gl.BeginQuery(QueryTarget.TimeElapsed, _gpuQueryOpaque[gpuQuerySlot]);
if (diag && _gpuQueriesInitialized)
{
_gl.BeginQuery(QueryTarget.TimeElapsed, _gpuQueryOpaque[gpuQuerySlot]);
_gpuQueryOpaqueBegun[gpuQuerySlot] = true; // #125
}
DrawIndirectRange(0, _opaqueDrawCount);
if (diag && _gpuQueriesInitialized) _gl.EndQuery(QueryTarget.TimeElapsed);
if (AlphaToCoverage) _gl.Disable(EnableCap.SampleAlphaToCoverage);
@ -1358,7 +1397,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
// section. BuildIndirectArrays preserves CullMode in _drawCullModes.
_gl.FrontFace(FrontFaceDirection.CW);
_shader.SetInt("uRenderPass", 1);
if (diag && _gpuQueriesInitialized) _gl.BeginQuery(QueryTarget.TimeElapsed, _gpuQueryTransparent[gpuQuerySlot]);
if (diag && _gpuQueriesInitialized)
{
_gl.BeginQuery(QueryTarget.TimeElapsed, _gpuQueryTransparent[gpuQuerySlot]);
_gpuQueryTransparentBegun[gpuQuerySlot] = true; // #125
}
DrawIndirectRange(_opaqueDrawCount, _transparentDrawCount);
if (diag && _gpuQueriesInitialized) _gl.EndQuery(QueryTarget.TimeElapsed);
_gl.DepthMask(true);