From fcade06c46c8c1006183b78f38e15cdbbd917496 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 11 Jun 2026 18:19:55 +0200 Subject: [PATCH] #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 --- .../Rendering/Wb/WbDrawDispatcher.cs | 61 ++++++++++++++++--- 1 file changed, 52 insertions(+), 9 deletions(-) diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index f7214e9e..34c70af0 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -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);