diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index c910eabd..67137574 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -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 diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs index 064cb264..2fe1a37a 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs @@ -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 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);