fix(render): §4 outdoor full-world flap — empty Transparent pass leaked DepthMask(false), no-oping the frame depth clear

ROOT CAUSE (pinned by the [clip-route] probe run + [gl-state] tripwire, one
capture): EnvCellRenderer.RenderModernMDIInternal established the Transparent
pass state (Enable(Blend) + DepthMask(false)) BEFORE the batch pass-filter, so
a cell whose batches are ALL opaque (a plain cottage interior) hit the
totalDraws==0 early-out and returned without ever reaching the end-of-pass
restore. The frame ended with dmask=0 + blend=1; the NEXT frame's
glClear(GL_DEPTH_BUFFER_BIT) silently no-oped (depth clears honor glDepthMask),
and every world fragment — terrain, entities, player, sky — failed GL_LESS
against its own previous-frame depth ghost. Screen = the fog-tinted clear
color. Onset locks to the building-flood merge because that is the first frame
the flooded building shell draws; holds while merged (the leak re-arms every
frame); camera rotation recovers because the cell drops from the flood and the
restore-skipping path stops running.

Capture evidence (flap-cliproute-capture.log): all three draw-input suspects
exonerated — landscape scissor full-screen all run, terrain-UBO/region-SSBO
planes full-screen on both sides of every merge, all 41,373 instances on the
correct repacked slot with cullEnt=0 — while [gl-state] showed frames entering
with dmask=0 blend=1 for exactly the merged stretches (145,238 consecutive
frames in the held window, flipping with each merge boundary at the end-of-run
strobe cycles).

Fix (all paths root-cause, no suppression):
- EnvCellRenderer: move the pass-state establish BELOW the totalDraws==0
  early-out so state is only set on a path that always reaches the restore;
  hoist the globalVao==0 check (the second leak-shaped early-out) above the
  state set.
- GameWindow frame clear: assert DepthMask(true) before glClear — the clear
  DEPENDS on the depth write mask, so it sets the state it depends on
  (feedback_render_self_contained_gl_state; this is the 4th instance of the
  class, in the same function as the 1st).

Very likely the same family as the "parts of the screen flash while running
past cottages" and cottage enter/exit artifacts (every brief merge = a
1-frame no-op depth clear). Visual gate pending.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-10 09:13:14 +02:00
parent 682cba36f1
commit c4df241690
2 changed files with 47 additions and 23 deletions

View file

@ -7093,6 +7093,15 @@ public sealed class GameWindow : IDisposable
System.Math.Clamp(fogColor.Z, 0f, 1f),
1f);
// §4 outdoor full-world flap (2026-06-10): the depth clear DEPENDS on the depth
// write mask — glClear(GL_DEPTH_BUFFER_BIT) is silently gated by glDepthMask.
// A pass that leaked DepthMask(false) at frame end (EnvCellRenderer's empty
// Transparent pass, fixed same-day) turned this clear into a no-op and the whole
// world failed GL_LESS against its own previous-frame depth ghost. Per
// feedback_render_self_contained_gl_state: the clear site asserts the state it
// depends on rather than inheriting it. The [gl-state] tripwire still detects
// any future leak.
_gl.DepthMask(true);
_gl!.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit);
// WB GameScene.cs:830-843 establishes CW as the frame-global

View file

@ -1016,31 +1016,17 @@ public sealed unsafe class EnvCellRenderer : IDisposable
int passIdx = (int)renderPass;
if (passIdx < 0 || passIdx > 2) return;
// §4 outdoor full-world flap (2026-06-10): hoisted from below the SSBO uploads.
// Without the global VAO nothing can draw, and returning AFTER the pass state
// was established leaked it (same early-out shape as the totalDraws==0 leak —
// see the comment on the state-establish block below).
var globalVao = _meshManager.GlobalBuffer?.VAO ?? 0u;
if (globalVao == 0) return;
// WB BaseObjectRenderManager.cs:715-716:
shader.Use();
shader.SetInt("uFilterByCell", 0);
// Phase U.4 ROOT-CAUSE FIX (cell-shell "transparent walls / only bluish
// background, flickering when moving"): establish this pass's BLEND + DepthMask
// state OURSELVES rather than inheriting it. Render(Opaque) runs right after the
// terrain draw (which sets neither) and after particles / last frame's transparent
// pass — so whatever left GL_BLEND enabled made the OPAQUE shells composite their
// (often sub-1.0 alpha) wall textures against the bluish clear color (terrain is
// Skip'd indoors), toggling with per-frame ordering → flicker. Mirror the working
// WbDrawDispatcher passes (Disable(Blend)+DepthMask(true) opaque;
// Enable(Blend)+DepthMask(false) transparent). Restored to opaque defaults at the
// end of the draw loop so a Transparent pass can't leak into later draws.
if (renderPass == WbRenderPass.Transparent)
{
_gl.Enable(EnableCap.Blend);
_gl.DepthMask(false);
}
else
{
_gl.Disable(EnableCap.Blend);
_gl.DepthMask(true);
}
// WB BaseObjectRenderManager.cs:718-740: group batches by CullMode + additive flag.
var batchesByCullMode = new Dictionary<int, List<(ObjectRenderBatch batch, int instanceCount, int instanceOffset)>>();
int totalDraws = 0;
@ -1078,6 +1064,35 @@ public sealed unsafe class EnvCellRenderer : IDisposable
// WB BaseObjectRenderManager.cs:743:
if (totalDraws == 0) return;
// Phase U.4 ROOT-CAUSE FIX (cell-shell "transparent walls / only bluish
// background, flickering when moving"): establish this pass's BLEND + DepthMask
// state OURSELVES rather than inheriting it. Mirror the working WbDrawDispatcher
// passes (Disable(Blend)+DepthMask(true) opaque; Enable(Blend)+DepthMask(false)
// transparent). Restored to opaque defaults at the end of the draw loop so a
// Transparent pass can't leak into later draws.
//
// §4 outdoor full-world flap fix (2026-06-10): this block MOVED below the
// totalDraws==0 early-out above. It used to run before the batch grouping, so a
// Transparent pass over a cell whose batches are ALL opaque (a plain cottage
// interior) set Blend-on/DepthMask-off and then returned at the count check
// WITHOUT reaching the restore. The frame ended with dmask=0; the NEXT frame's
// glClear(DEPTH) silently no-oped (depth clears honor glDepthMask), every world
// fragment failed GL_LESS against its own previous-frame depth ghost, and the
// whole screen dropped to the fog-tinted clear color — onset-locked to the
// building-flood merge (the first frame a flooded building shell draws), holding
// until camera rotation dropped the cell from the flood. From here down every
// path reaches the end-of-pass restore.
if (renderPass == WbRenderPass.Transparent)
{
_gl.Enable(EnableCap.Blend);
_gl.DepthMask(false);
}
else
{
_gl.Disable(EnableCap.Blend);
_gl.DepthMask(true);
}
// WB BaseObjectRenderManager.cs:745-759: resize buffers if needed.
if (totalDraws > _mdiCommandCapacity)
{
@ -1199,8 +1214,8 @@ public sealed unsafe class EnvCellRenderer : IDisposable
}
// WB BaseObjectRenderManager.cs:807-818: bind VAO + SSBOs + barrier.
var globalVao = _meshManager.GlobalBuffer?.VAO ?? 0u;
if (globalVao == 0) return;
// (globalVao validated at the top of the method — a return here would leak the
// pass state established above.)
if (_currentVao != globalVao)
{
_gl.BindVertexArray(globalVao);