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
// ---------------------------------------------------------------------------