fix(render) Campaign FW3 visual gate: interior dynamics draw PRE-clear

Owner gate report (Holtburg, looking out from a house): no doors on
buildings, no candle particles, the character vanishing for a frame
when crossing out. One family: the cutover moved the interior depth
clear + exit seals into the walk turn, but the dynamics phase
(outside dynamics, look-in dynamics, outdoor particles, weather)
still ran AFTER the whole walk - everything beyond the door plane
z-failed against the seals stamp, the exact #118 house-exit
clip+vanish class the old code documented.

Retail draws the outside world's objects INSIDE LScape::draw,
strictly before the clear+seals. The driver's clearInteriorDepth
closure now invokes the dynamics phase at that pre-clear boundary for
interior roots (_walkPreClearDynamics, assigned around the Replay,
cleared in finally); outdoor roots keep the post-driver order (no
clear exists there). Packed route consumption order is unchanged
(LookInObject -> LandscapeOutsideDynamic -> DynamicLast).

Suites: full Release build 0 warnings; hermetic 6,758/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 17:27:11 +02:00
parent 806f87ab79
commit f8fc21d467

View file

@ -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
{