fix(render): Phase A8 R3.5 — gate stencil branch on PointInCell containment

Closes the transition-flicker symptom observed during R4 visual verification:
brief 1-3 frames after exiting a building where outdoor scenery rendered
with wrong stencil mask, "walls disappear and buildings show under ground"
shimmer, and sky stayed suppressed.

Root cause: CellVisibility.FindCameraCell holds the previous CameraCell
for ~3 grace frames after the camera physically exits the cell volume
(see _cellSwitchGraceFrames). The grace mechanism prevents flicker at
the doorway threshold for sky/lighting/depth-clear, but the new R3
stencil branch was using `cameraInsideCell` directly — so during grace
frames it ran MarkAndPunch with the previous cell's portals (now behind/
beside the camera) and the IndoorPass + stencil-gated outdoor produced
the garbage frame.

Fix: compute `cameraReallyInside` via the stricter
CellVisibility.PointInCell containment check and use it (instead of
`cameraInsideCell`) as the gate for the stencil branch. Sky, depth-clear,
lighting, and particles continue to use `cameraInsideCell` so their
smooth grace-aware behavior is unchanged.

Handoff item #10 (docs/research/2026-05-26-a8-revert-handoff.md) flagged
this exact concern: "Likely the CellSwitchGraceFrameCount = 3 interacting
with stencil setup timing." Confirmed and closed.

Visual-verification of the fix is part of R4 (re-run).

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

View file

@ -7141,7 +7141,29 @@ public sealed class GameWindow : IDisposable
animatedIds.Add(k);
}
if (cameraInsideCell && _indoorStencilPipeline is not null
// 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
// compiler's null-flow analysis: `cameraReallyInside` already
// implies it, but flow doesn't propagate through a separate
// local. Restating it inside the if condition lets the branch
// body use the un-`?`d form without null-forgiving.
if (cameraReallyInside && _indoorStencilPipeline is not null
&& visibility?.CameraCell is not null)
{
// Phase A8 R3 — WB RenderInsideOut order.