fix(render): Phase A8 — remove cull-restore at EnvCell exit (lets IndoorPass inherit cull-off)

Visual-gate-#4 evidence revealed the prior commit's cull-restore-at-exit
addition was wrong. The Landblock→None CullMode override worked correctly
for cell-mesh polys, but the cull-back state I restored at Render exit
propagated to the subsequent `dispatcher.Draw(IndoorPass)` call. The
dispatcher's IndoorPass renders AC's cottage shell — landblock-baked
GfxObj parts (wooden floor planks, wall slabs) whose pos-side winding +
our FrontFace=CCW + cull-back = floor poly is back-facing and culled.
User saw light blue sky through the floor in gate-#4.

Reverting the cull-restore lets cull-disabled propagate from
EnvCellRenderer.Render through IndoorPass. Cottage shell renders
double-sided so the floor + wall slabs are visible from any angle.
Step 4's gl.Enable(EnableCap.CullFace) at the terrain pass (line
~10768) + the cleanup block's enable (line ~10870) re-establish
cull-back BEFORE the LiveDynamic dispatcher.Draw fires — so chars,
NPCs, doors still render solid (no see-through-head regression from
gate-#3's ACDREAM_A8_DISABLE_CULL=1 diagnostic).

The retail-faithful long-term fix is matching WB's `glFrontFace(GLEnum.CW)`
globally (per GameScene.cs:843) so cull-back selects the correct side
for AC's natural polygon winding without needing double-sided rendering.
That requires a wider audit of every consumer's FrontFace assumption
(translucent crystal renderer + others) and is deferred.

14/14 EnvCellRenderer tests pass. Build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-27 20:24:59 +02:00
parent 0940d7961a
commit d5deeb3314

View file

@ -815,21 +815,32 @@ 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;
// Phase A8 (2026-05-28 visual-gate-#4 follow-up): NO cull-restore
// at exit. The Landblock→None override can leave cull DISABLED
// if the last batch was Landblock — and that's intentional: the
// subsequent `dispatcher.Draw(IndoorPass)` call in
// RenderInsideOutAcdream's Step 3 wants cull-off too, because
// AC's cottage-shell GfxObj parts (the wooden floor planks +
// wall slabs that the player walks on / through) have winding
// that gets back-face-culled by the dispatcher's default
// FrontFace=CCW. Letting cull stay off through IndoorPass
// renders both shell and cell mesh double-sided, so floors are
// visible from above (and inverted-front-facing wall slabs are
// visible from inside the room). Step 4's
// `gl.Enable(EnableCap.CullFace)` (line ~10768) + the cleanup
// block's enable (line ~10870) re-establish cull-back before
// LiveDynamic chars / NPCs / doors render — so those still
// look solid (no see-through head). The static `_currentVao`
// is reset because the next Render call's batch loop needs to
// re-issue BindVertexArray regardless; `_currentCullMode` is
// intentionally left at None so the cache matches actual GL
// state until the next Render call's per-batch SetCullMode
// either confirms or re-sets it.
//
// The retail-faithful long-term move is matching WB's
// glFrontFace(CW) globally (GameScene.cs:843) so cull-back
// selects the correct side for AC's polygon winding without
// double-sided rendering — deferred until a wider audit.
// Update frame stats for probe emission at the call site.
_lastFrameStats.CellsRendered = filter?.Count ?? snapshot.BatchedByCell.Count;