From 375f9a7b9b40f3c5c9b25c2c9f5c80679eac5f4b Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 27 May 2026 19:11:00 +0200 Subject: [PATCH] =?UTF-8?q?feat(render):=20Phase=20A8=20=E2=80=94=20full?= =?UTF-8?q?=20GL=20state=20probe=20+=20pool=20diagnostics=20(apparatus)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Defense-in-depth apparatus per the 2026-05-27 handoff's option-1 recommendation. The audit-found pool aliasing bug (prior commit) is the primary fix; this probe is the safety net for any unidentified residual issue when the visual gate runs. EmitDrawOrderProbe now logs the full GL state at each step boundary of RenderInsideOutAcdream — stencil test/func/ref/mask/op, depth func/mask, cull face/mode, blend src/dst, color writemask, current VAO, current program. An operator can read the log offline and compare line-by-line against WB's expected state at VisibilityManager.cs:73-239. Any divergence pinpoints the bug's GL-state shape; matching state confirms the issue is elsewhere (instance data, mesh upload, etc.). EmitEnvCellProbe now logs pool diagnostics — total pool size + snapshot's PostPreparePoolIndex high-water mark. A spike in poolTotal across stationary- camera frames, or a divergence between poolHwm and cell-count, signals pool-management regression. The fix-the-bug-first principle keeps this probe dormant by default; enable via ACDREAM_PROBE_VIS=1 only when investigating. Heavy (~10 GL queries per step × 5-10 steps per frame), but gated. 86/86 App tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 64 +++++++++++++++++-- .../Rendering/Wb/EnvCellRenderer.cs | 15 +++++ 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 6495f1d..cfb6a83 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -10892,25 +10892,79 @@ public sealed class GameWindow : IDisposable private int _phaseA8DrawOrderFrame = 0; + /// + /// Phase A8 apparatus (2026-05-28): per-step GL state assertion probe. + /// Logs the full relevant GL state at each step boundary of + /// so an operator can read the log + /// offline and compare against WB's expected state (lifted line-by-line + /// from VisibilityManager.cs:73-239). + /// Heavy under ACDREAM_PROBE_VIS=1 (~10 GL queries per call, + /// 5-10 calls per indoor frame), but only enabled when the operator + /// explicitly opts in. The fix-the-bug-first principle keeps this probe + /// dormant by default; turn it on only when the visual is broken and + /// you need evidence. + /// private void EmitDrawOrderProbe(int step, char sub) { if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeVisibilityEnabled) return; - _gl!.GetInteger(Silk.NET.OpenGL.GLEnum.StencilTest, out int stOn); - _gl!.GetInteger(Silk.NET.OpenGL.GLEnum.DepthFunc, out int depthFn); - _gl!.GetBoolean(Silk.NET.OpenGL.GLEnum.DepthWritemask, out var depthMask); + var gl = _gl!; + + // Single-int queries. + gl.GetInteger(Silk.NET.OpenGL.GLEnum.StencilTest, out int stOn); + gl.GetInteger(Silk.NET.OpenGL.GLEnum.DepthFunc, out int depthFn); + gl.GetBoolean(Silk.NET.OpenGL.GLEnum.DepthWritemask, out var depthMask); + gl.GetInteger(Silk.NET.OpenGL.GLEnum.CullFace, out int cullEnabled); + gl.GetInteger(Silk.NET.OpenGL.GLEnum.CullFaceMode, out int cullMode); + gl.GetInteger(Silk.NET.OpenGL.GLEnum.BlendSrc, out int blendSrc); + gl.GetInteger(Silk.NET.OpenGL.GLEnum.BlendDst, out int blendDst); + gl.GetInteger(Silk.NET.OpenGL.GLEnum.StencilFunc, out int sFunc); + gl.GetInteger(Silk.NET.OpenGL.GLEnum.StencilRef, out int sRef); + gl.GetInteger(Silk.NET.OpenGL.GLEnum.StencilValueMask, out int sValMask); + gl.GetInteger(Silk.NET.OpenGL.GLEnum.StencilWritemask, out int sWriteMask); + gl.GetInteger(Silk.NET.OpenGL.GLEnum.StencilFail, out int sFail); + gl.GetInteger(Silk.NET.OpenGL.GLEnum.StencilPassDepthFail, out int sPdFail); + gl.GetInteger(Silk.NET.OpenGL.GLEnum.StencilPassDepthPass, out int sPdPass); + gl.GetInteger(Silk.NET.OpenGL.GLEnum.VertexArrayBinding, out int vao); + gl.GetInteger(Silk.NET.OpenGL.GLEnum.CurrentProgram, out int prog); + + // ColorWritemask is a bool[4]. Silk.NET exposes it via the + // GetBoolean overload with a Span destination — but a tighter + // capture (just whether all four bits are on) is sufficient for the + // diagnostic. Query each component separately via the indexed form. + // GL spec: GL_COLOR_WRITEMASK returns 4 booleans in order R,G,B,A. + Span cwmask = stackalloc bool[4]; + unsafe + { + fixed (bool* p = cwmask) + gl.GetBoolean(Silk.NET.OpenGL.GLEnum.ColorWritemask, p); + } + string subStr = sub == ' ' ? "" : sub.ToString(); Console.WriteLine( $"[draworder] frame={_phaseA8DrawOrderFrame} step={step}{subStr} " + - $"stencil={(stOn != 0 ? "on" : "off")} depthFn=0x{depthFn:X} depthMask={depthMask}"); + $"stencil={(stOn != 0 ? "on" : "off")} depthFn=0x{depthFn:X} depthMask={depthMask} " + + $"cull={(cullEnabled != 0 ? "on" : "off")}({(cullMode == (int)Silk.NET.OpenGL.GLEnum.Front ? "front" : cullMode == (int)Silk.NET.OpenGL.GLEnum.Back ? "back" : "f+b")}) " + + $"blend=0x{blendSrc:X}/0x{blendDst:X} " + + $"sFunc=0x{sFunc:X}:{sRef}:0x{sValMask:X} " + + $"sOp=0x{sFail:X}/0x{sPdFail:X}/0x{sPdPass:X} sMask=0x{sWriteMask:X} " + + $"cMask=({(cwmask[0] ? 'R' : '-')}{(cwmask[1] ? 'G' : '-')}{(cwmask[2] ? 'B' : '-')}{(cwmask[3] ? 'A' : '-')}) " + + $"vao={vao} prog={prog}"); } private void EmitEnvCellProbe(int ourBldgs, int otherBldgs, int filterCnt) { if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeEnvCellEnabled) return; var stats = _envCellRenderer?.Stats ?? default; + // Phase A8 apparatus (2026-05-28): also log pool stats. A spike in + // poolTotal or a divergence between hwm and ourBldgs cell counts + // signals pool-management regression — the bug class the audit + // caught. Hwm should track number-of-(cell,gfxObj)-pairs per visible + // landblock and stay roughly stable across stationary-camera frames. + var pool = _envCellRenderer?.GetPoolDiagnostics() ?? (0, 0); Console.WriteLine( $"[envcells] cells={stats.CellsRendered} tris={stats.TrianglesDrawn} " + - $"ourBldgs={ourBldgs} otherBldgs={otherBldgs} filterCnt={filterCnt}"); + $"ourBldgs={ourBldgs} otherBldgs={otherBldgs} filterCnt={filterCnt} " + + $"poolTotal={pool.PoolTotal} poolHwm={pool.SnapshotPoolHwm}"); } private void EmitStencilProbe(string op) diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs index c42bd3c..48927ef 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs @@ -88,6 +88,21 @@ public sealed unsafe class EnvCellRenderer : IDisposable public struct LastFrameStats { public int CellsRendered; public int TrianglesDrawn; } private LastFrameStats _lastFrameStats; + /// + /// Diagnostic accessor for the [envcells] probe (Phase A8 apparatus 2026-05-28). + /// Returns (pool-list count total, snapshot's PostPreparePoolIndex high-water). + /// A divergence between expected and actual values would indicate a pool- + /// management regression — exactly the bug class the 2026-05-28 audit caught. + /// + public (int PoolTotal, int SnapshotPoolHwm) GetPoolDiagnostics() + { + int poolTotal; + lock (_listPool) poolTotal = _listPool.Count; + int hwm; + lock (_renderLock) hwm = _activeSnapshot.PostPreparePoolIndex; + return (poolTotal, hwm); + } + // --------------------------------------------------------------------------- // Constructor + Initialize // ---------------------------------------------------------------------------