From 0940d7961a19484c567e98d53fea851a2ce0a28c Mon Sep 17 00:00:00 2001 From: Erik Date: Wed, 27 May 2026 20:12:20 +0200 Subject: [PATCH] =?UTF-8?q?fix(render):=20Phase=20A8=20=E2=80=94=20cell-me?= =?UTF-8?q?sh=20Landblock=20CullMode=20=E2=86=92=20None=20+=20cull=20state?= =?UTF-8?q?=20restore?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cull A/B diagnostic (prior commit's ACDREAM_A8_DISABLE_CULL=1) in visual-gate-#3 confirmed: cell-mesh polys are being culled by back-face culling, which is why floors disappear when looking down from inside a room. Per-cell audit data showed every cell-mesh batch has CullMode.Landblock — assigned because AC's CellStruct polys carry SidesType=Landblock in the dat. Our SetCullMode maps Landblock to glCullFace(Back), matching WB. Root cause: WB sets `glFrontFace(GLEnum.CW)` globally at GameScene.cs:843. Our WbDrawDispatcher.cs:1056 sets `glFrontFace(CCW)` — the GL default, opposite of WB. With our flipped-from-natural fan triangulation in BuildCellStructPolygonIndices (which emits (i, i-1, 0) for each fan triangle, reversing the input vertex order), the resulting effective winding from the camera's perspective is OPPOSITE WB's. Cull-back then removes the OPPOSITE face from what WB does — hiding the floor side that should be visible from inside the room. Within a single cell-mesh batch, the polys face every direction (walls outward, floor up, ceiling down) but all share CullMode.Landblock. No single cull setting can be correct for all three orientations simultaneously — the retail-faithful approach is to render cell polys double-sided (cull off). Two changes scoped to EnvCellRenderer.RenderModernMDIInternal so other renderers aren't affected: 1. Remap CullMode.Landblock → None when iterating per-cull-mode batch groups. Cell polys render with cull disabled, all faces visible. CullMode.Landblock is only assigned by PrepareCellStructMeshData (cell polys) in this codebase — terrain uses a different render path. Scope is exactly right. 2. Explicitly Enable(CullFace) + CullFace(Back) at Render exit so the dispatcher's subsequent IndoorPass + LiveDynamic Draws don't inherit the cull-disabled state. The see-through-head symptom in visual-gate-#3 was caused by exactly this state leak from the ACDREAM_A8_DISABLE_CULL=1 diagnostic; the proper fix needs the explicit restore. Also updates the static `_currentCullMode` cache so the next Render call's first SetCullMode comparison is correct. Removed the ACDREAM_A8_DISABLE_CULL diagnostic env var — its role as A/B test is complete. 14/14 EnvCellRenderer tests pass. Build green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Rendering/Wb/EnvCellRenderer.cs | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs index 085c371..ad3e4e1 100644 --- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs +++ b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.cs @@ -81,16 +81,6 @@ public sealed unsafe class EnvCellRenderer : IDisposable private static uint _currentVao; private static CullMode? _currentCullMode; - // Phase A8 A/B diagnostic (2026-05-28 visual-gate-#2 follow-up): - // ACDREAM_A8_DISABLE_CULL=1 forces every batch's effective CullMode to - // None (no face culling). Used to isolate whether the missing-floor - // symptom is caused by polygon-winding+CullMode interaction or by - // something else (lighting, depth, alpha). If the floor appears with - // this set, cull/winding is the bug. If not, look elsewhere. Static - // because it's read once at startup; no need to re-query per draw. - private static readonly bool _forceCullModeNone = - string.Equals(Environment.GetEnvironmentVariable("ACDREAM_A8_DISABLE_CULL"), "1", StringComparison.Ordinal); - public bool NeedsPrepare { get; private set; } = true; public bool IsDisposed { get; private set; } @@ -825,6 +815,22 @@ public sealed unsafe class EnvCellRenderer : IDisposable _gl.BindVertexArray(0); _currentVao = 0; + // Phase A8 fix (2026-05-28 visual-gate-#3 follow-up): explicitly + // restore cull-back at exit. The Landblock→None override above + // can leave cull DISABLED if the last batch's CullMode was + // Landblock — which would leak into the subsequent dispatcher + // draws (IndoorPass building shells, then LiveDynamic chars + + // NPCs + doors), making them all render see-through (no + // back-face cull). The see-through-head symptom in the + // ACDREAM_A8_DISABLE_CULL=1 A/B test was caused exactly by + // this state leak. Re-enabling cull here restores the + // dispatcher's expected default and updates our static cache + // so the next Render call's first SetCullMode comparison is + // correct. + _gl.Enable(EnableCap.CullFace); + _gl.CullFace(TriangleFace.Back); + _currentCullMode = CullMode.CounterClockwise; + // Update frame stats for probe emission at the call site. _lastFrameStats.CellsRendered = filter?.Count ?? snapshot.BatchedByCell.Count; _lastFrameStats.TrianglesDrawn = 0; @@ -997,10 +1003,24 @@ public sealed unsafe class EnvCellRenderer : IDisposable foreach (var group in batchesByCullMode) { var cullMode = (CullMode)(group.Key % 4); - // A/B diagnostic: ACDREAM_A8_DISABLE_CULL=1 overrides every batch - // to no-culling. Reveals whether floor-missing is a cull/winding - // bug or something else. - if (_forceCullModeNone) cullMode = CullMode.None; + // Phase A8 fix (2026-05-28 visual-gate-#3 evidence): override + // CullMode.Landblock to None for cell-mesh batches. WB sets + // glFrontFace(CW) globally (GameScene.cs:843) so its CullMode + // mapping (Landblock→Back) culls the correct side; we set + // glFrontFace(CCW) in WbDrawDispatcher (line 1056) so the + // mapping would cull the OPPOSITE side, hiding cell floors. + // Cell-mesh polys with CullMode.Landblock represent the floor + + // walls + ceiling of a single room — they face different + // directions but share one CullMode value, so a single cull + // setting can't be correct for all of them. The retail-faithful + // approach is double-sided rendering for cell polys (cull off), + // matching what the cull-disable A/B diagnostic empirically + // confirmed (floor visible with cull off in visual-gate-#3). + // CullMode.Landblock is only ever assigned in this codebase by + // PrepareCellStructMeshData (cell polys) — terrain has its own + // renderer that doesn't go through this code path — so this + // override is scoped exactly right. + if (cullMode == CullMode.Landblock) cullMode = CullMode.None; if (_currentCullMode != cullMode) { SetCullMode(cullMode);