feat(render): Phase A8 — full GL state probe + pool diagnostics (apparatus)
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) <noreply@anthropic.com>
This commit is contained in:
parent
9559726960
commit
375f9a7b9b
2 changed files with 74 additions and 5 deletions
|
|
@ -10892,25 +10892,79 @@ public sealed class GameWindow : IDisposable
|
||||||
|
|
||||||
private int _phaseA8DrawOrderFrame = 0;
|
private int _phaseA8DrawOrderFrame = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase A8 apparatus (2026-05-28): per-step GL state assertion probe.
|
||||||
|
/// Logs the full relevant GL state at each step boundary of
|
||||||
|
/// <see cref="RenderInsideOutAcdream"/> so an operator can read the log
|
||||||
|
/// offline and compare against WB's expected state (lifted line-by-line
|
||||||
|
/// from <c>VisibilityManager.cs:73-239</c>).
|
||||||
|
/// <para>Heavy under <c>ACDREAM_PROBE_VIS=1</c> (~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.</para>
|
||||||
|
/// </summary>
|
||||||
private void EmitDrawOrderProbe(int step, char sub)
|
private void EmitDrawOrderProbe(int step, char sub)
|
||||||
{
|
{
|
||||||
if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeVisibilityEnabled) return;
|
if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeVisibilityEnabled) return;
|
||||||
_gl!.GetInteger(Silk.NET.OpenGL.GLEnum.StencilTest, out int stOn);
|
var gl = _gl!;
|
||||||
_gl!.GetInteger(Silk.NET.OpenGL.GLEnum.DepthFunc, out int depthFn);
|
|
||||||
_gl!.GetBoolean(Silk.NET.OpenGL.GLEnum.DepthWritemask, out var depthMask);
|
// 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<bool> 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<bool> cwmask = stackalloc bool[4];
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (bool* p = cwmask)
|
||||||
|
gl.GetBoolean(Silk.NET.OpenGL.GLEnum.ColorWritemask, p);
|
||||||
|
}
|
||||||
|
|
||||||
string subStr = sub == ' ' ? "" : sub.ToString();
|
string subStr = sub == ' ' ? "" : sub.ToString();
|
||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"[draworder] frame={_phaseA8DrawOrderFrame} step={step}{subStr} " +
|
$"[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)
|
private void EmitEnvCellProbe(int ourBldgs, int otherBldgs, int filterCnt)
|
||||||
{
|
{
|
||||||
if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeEnvCellEnabled) return;
|
if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeEnvCellEnabled) return;
|
||||||
var stats = _envCellRenderer?.Stats ?? default;
|
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(
|
Console.WriteLine(
|
||||||
$"[envcells] cells={stats.CellsRendered} tris={stats.TrianglesDrawn} " +
|
$"[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)
|
private void EmitStencilProbe(string op)
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,21 @@ public sealed unsafe class EnvCellRenderer : IDisposable
|
||||||
public struct LastFrameStats { public int CellsRendered; public int TrianglesDrawn; }
|
public struct LastFrameStats { public int CellsRendered; public int TrianglesDrawn; }
|
||||||
private LastFrameStats _lastFrameStats;
|
private LastFrameStats _lastFrameStats;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
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
|
// Constructor + Initialize
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue