diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index 26fce2f4..925a50ce 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -93,6 +93,13 @@ public sealed class RetailPViewRenderer { } + // FW3 visual-gate fix: the interior root's dynamics phase, invoked by + // the driver's clearInteriorDepth closure at the walk's pre-clear + // boundary (retail draws outside objects inside LScape::draw, before + // the clear+seals). Assigned per frame around the Replay, always + // cleared in finally. + private Action? _walkPreClearDynamics; + // Campaign FW3.2b-2: the walk's production world-data registries // (published/retired by LandblockRenderPublisher) plus the per-frame // driver state. Null until the composition passes them; the static @@ -300,6 +307,16 @@ public sealed class RetailPViewRenderer Action clearInteriorDepth = () => { + // FW3 visual-gate fix (owner report: doors/candles invisible + // looking out; the crossing vanish): retail draws the + // OUTSIDE world's objects INSIDE LScape::draw — strictly + // BEFORE the depth clear + seals (the #118 house-exit + // clip+vanish lesson: anything drawn after the seals z-fails + // against their true-depth stamp the moment it stands beyond + // the door plane). The surviving dynamic routes + outdoor + // particles + weather therefore run HERE, at the walk's + // pre-clear boundary, for an interior root. + _walkPreClearDynamics?.Invoke(); // Retail PView::DrawCells 0x005A4872 drains the landscape // alpha list immediately before the gated full depth clear — // mirrors DrawLandscapeThroughOutsideView's own pre-clear @@ -480,16 +497,51 @@ public sealed class RetailPViewRenderer // feed the surviving dynamic routes only (plan §FW3 item 1's // dual-compute split). Campaign FW3.4a: the walk itself // already ran (Collect, above) — this is Replay only. - DrawWalkDrivenStatics(ctx, walkExecutor!, walkDriver!); - passes.UseIndoorMembershipOnlyRouting(); - DrawLandscapeDynamicsPhase( - ctx, - passes, - clipAssembly, - partition, - viewcone, - frameEntityPasses, - in frameView); + // + // FW3 visual-gate fix: for an INTERIOR root the dynamics + // phase (outside dynamics + look-in dynamics + outdoor + // particles + weather) must draw at the walk's PRE-CLEAR + // boundary — retail draws them inside LScape::draw, before + // the clear+seals — so the driver's clearInteriorDepth + // closure invokes it via _walkPreClearDynamics. For an + // OUTDOOR root there is no clear (retail has none) and the + // phase runs after the driver, matching the old order. + RenderFrameView capturedView = frameView; + IRenderFrameEntityPassExecutor? capturedPasses = frameEntityPasses; + InteriorEntityPartition.Result? capturedPartition = partition; + ViewconeCuller capturedViewcone = viewcone; + _walkPreClearDynamics = () => + { + passes.UseIndoorMembershipOnlyRouting(); + DrawLandscapeDynamicsPhase( + ctx, + passes, + clipAssembly, + capturedPartition, + capturedViewcone, + capturedPasses, + in capturedView); + }; + try + { + DrawWalkDrivenStatics(ctx, walkExecutor!, walkDriver!); + passes.UseIndoorMembershipOnlyRouting(); + if (ctx.RootCell.IsOutdoorNode) + { + DrawLandscapeDynamicsPhase( + ctx, + passes, + clipAssembly, + partition, + viewcone, + frameEntityPasses, + in frameView); + } + } + finally + { + _walkPreClearDynamics = null; + } } else {