fix(render): Phase A8 R3.5 v2 — gate depth-clear on cameraReallyInside too

R3.5 v1 only gated the stencil branch on `cameraReallyInside`; the
depth-clear-if-inside at line ~7129 stayed on `cameraInsideCell`. During
grace frames after exit:

  cameraInsideCell   = true  (grace, holds previous cell for 3 frames)
  cameraReallyInside = false (PointInCell on camera pos returns false)

So depth-clear FIRED (writing depth = 1.0 globally) but the OUTDOOR branch
ran (single Draw(All) on every entity). With depth cleared, terrain's
depth = 1.0 — every entity below terrain (cellar geometry, basement
GfxObjs, anything at world Z < terrain Z) won the depth test and rendered
THROUGH the ground. User reported: "stand outside or pass outside → flicker
where objects are visible through ground and walls of other buildings are
missing."

v2 fix: unify depth-related gates on `cameraReallyInside`. During grace
frames depth-clear is now ALSO skipped; terrain depth survives; the
outdoor pass renders normally with proper terrain occlusion. Sky /
lighting / particles continue to use `cameraInsideCell` for smooth
grace-aware transitions.

The two-flag split is now explicit:
  cameraInsideCell    → sky, lighting (smooth, grace-aware)
  cameraReallyInside  → depth-clear, stencil branch (strict, no grace)

Closes the persistent transition flicker observed in R4 visual
verification after v1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-26 12:40:35 +02:00
parent 38d537491f
commit 2bfeafd358

View file

@ -7122,17 +7122,42 @@ public sealed class GameWindow : IDisposable
_terrainCpuSampleCursor = (_terrainCpuSampleCursor + 1) % _terrainCpuSamples.Length; _terrainCpuSampleCursor = (_terrainCpuSampleCursor + 1) % _terrainCpuSamples.Length;
MaybeFlushTerrainDiag(); MaybeFlushTerrainDiag();
// Conditional depth clear: when camera is inside a building, clear // Phase A8 R3.5 — transition-flicker fix. `cameraInsideCell`
// depth (not color) so interior geometry writes fresh Z values on top // stays true for ~3 grace frames after the camera physically
// of the terrain color buffer. Exit portals show outdoor terrain color // exits a cell (see CellVisibility._cellSwitchGraceFrames).
// because we kept the color buffer. Matching ACME GameScene.cs pattern. // The grace mechanism prevents sky/lighting flicker at the
if (cameraInsideCell) // doorway threshold, but the render-frame mechanisms that
// touch depth (depth-clear AND the stencil pipeline) MUST be
// gated on the stricter PointInCell containment so they don't
// fire during grace frames when the camera is actually outside.
//
// cameraInsideCell — lenient, grace-aware → sky, lighting
// cameraReallyInside — PointInCell, no grace → depth-clear,
// stencil pipeline branch
//
// R3.5 v1 only gated the stencil branch on `cameraReallyInside`;
// depth-clear stayed on `cameraInsideCell`. Result: during grace
// frames the depth-clear ran but the outdoor branch ran (because
// !cameraReallyInside), so terrain depth was destroyed AND
// everything below terrain (cellars, basement geometry) won the
// depth test in the outdoor pass → "objects visible through
// ground." R3.5 v2 unifies the depth-related gates on
// `cameraReallyInside` so terrain depth is preserved during
// grace, eliminating the through-ground artifact.
bool cameraReallyInside = visibility?.CameraCell is not null
&& CellVisibility.PointInCell(camPos, visibility.CameraCell);
// Conditional depth clear: when camera is ACTUALLY inside a cell
// volume (not just in the grace window), clear depth (not color)
// so interior geometry writes fresh Z values on top of the
// terrain color buffer. Matches ACME GameScene.cs pattern.
if (cameraReallyInside)
_gl!.Clear(ClearBufferMask.DepthBufferBit); _gl!.Clear(ClearBufferMask.DepthBufferBit);
// L-fix1 (2026-04-28): animated-entity id set. Required by both // L-fix1 (2026-04-28): animated-entity id set. Required by both
// the cameraInsideCell branch (to route them to LiveDynamic pass) // the cameraReallyInside branch (to route them to LiveDynamic
// and the outdoor path (where it preserves visibility across // pass) and the outdoor path (where it preserves visibility
// landblock frustum culling). // across landblock frustum culling).
HashSet<uint>? animatedIds = null; HashSet<uint>? animatedIds = null;
if (_animatedEntities.Count > 0) if (_animatedEntities.Count > 0)
{ {
@ -7141,23 +7166,6 @@ public sealed class GameWindow : IDisposable
animatedIds.Add(k); animatedIds.Add(k);
} }
// Phase A8 R3.5 — transition-flicker fix. `cameraInsideCell`
// stays true for ~3 grace frames after the camera physically
// exits a cell (see CellVisibility._cellSwitchGraceFrames).
// The grace mechanism prevents lighting/sky flicker at the
// doorway threshold, but if the stencil pipeline runs during
// grace frames it marks/punches at the OLD cell's portal
// silhouettes — which are now behind/beside the camera — and
// the subsequent IndoorPass + stencil-gated outdoor produce
// a brief frame of "walls disappear + buildings under ground"
// garbage. Gate the stencil branch on the stricter
// `PointInCell` containment check so the pipeline only runs
// when the camera is ACTUALLY inside its cell volume; sky /
// lighting / depth-clear continue to use `cameraInsideCell`
// for their smoother grace-aware behavior.
bool cameraReallyInside = visibility?.CameraCell is not null
&& CellVisibility.PointInCell(camPos, visibility.CameraCell);
// The `visibility?.CameraCell is not null` repeat is for the // The `visibility?.CameraCell is not null` repeat is for the
// compiler's null-flow analysis: `cameraReallyInside` already // compiler's null-flow analysis: `cameraReallyInside` already
// implies it, but flow doesn't propagate through a separate // implies it, but flow doesn't propagate through a separate