diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index c95178d..b9c481b 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -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.