fix(render) Campaign FW3 visual gate: candle flames pre-clear too

Second owner report at the same gate: candle-flame particles still
invisible looking out. Flame emitters attach to STATIC owners (lamp
and candle statics), and the walk path submitted the landscape-stage
static-owner particle union AFTER the driver replay - post-clear, so
the pre-clear drain never carried them and a later flush z-failed
them at the sealed aperture. For an interior root the submission now
runs at the walk pre-clear boundary (retail inserts each emitter
during its cell landscape walk turn, before the clear+seals);
outdoor roots keep the post-replay frame-end placement (#132).
Interior cells own emitters stay in the final world scope where the
walls already own depth.

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:34:14 +02:00
parent f8fc21d467
commit 77fd40771b

View file

@ -510,8 +510,14 @@ public sealed class RetailPViewRenderer
IRenderFrameEntityPassExecutor? capturedPasses = frameEntityPasses;
InteriorEntityPartition.Result? capturedPartition = partition;
ViewconeCuller capturedViewcone = viewcone;
Walk.WalkFrameDriver capturedDriver = walkDriver!;
_walkPreClearDynamics = () =>
{
// Retail inserts static-owner emitters (candle flames)
// during their cells' landscape walk turns — submit
// BEFORE the pre-clear drain so they composite against
// still-true landscape depth, never the seals.
SubmitWalkLandscapeStaticParticles(ctx, walkExecutor!, capturedDriver);
passes.UseIndoorMembershipOnlyRouting();
DrawLandscapeDynamicsPhase(
ctx,
@ -1243,20 +1249,19 @@ public sealed class RetailPViewRenderer
/// routes it replaced used to ride, from the driver's own visited
/// sets (plan §FW3 "FW3.2b-2 — the production rooting", items 2 and
/// 4).</summary>
private void DrawWalkDrivenStatics(
/// <summary>The landscape-stage static-owner particle submission: the
/// union of every outdoor-static record from a landscape cell the walk
/// visited this frame, plus every shell record from a building the walk
/// visited (plan §FW3 item 4 — retail gates particles per cell turn,
/// <c>ShouldDrawParticles</c> @0x0050FE60, so this is MORE
/// retail-faithful than the old per-slice sphere filter it replaced).
/// Interior roots invoke this at the walk's pre-clear boundary; outdoor
/// roots post-replay.</summary>
private void SubmitWalkLandscapeStaticParticles(
RetailPViewFrameInput ctx,
RetailPViewPassExecutor passes,
Walk.WalkFrameDriver driver)
{
var (frame, encoder) = passes.RequireWalkSubmission();
driver.Replay(frame, encoder);
// Landscape-stage particle owners: the union of every outdoor-static
// record from a landscape cell the walk visited this frame, plus
// every shell record from a building the walk visited (plan §FW3
// item 4 — retail gates particles per cell turn, ShouldDrawParticles
// @0x0050FE60, so this is MORE retail-faithful than the old per-
// slice sphere filter it replaces).
_staticParticleUnionScratch.Clear();
foreach (uint cellId in driver.VisitedLandscapeCellIds)
UnionRecordOwners(_walkWorldData!.GetOutdoorStatics(cellId), _staticParticleUnionScratch);
@ -1269,12 +1274,36 @@ public sealed class RetailPViewRenderer
new RetailPViewLandscapeStaticParticleContext(_staticParticleUnionScratch));
_staticParticleUnionScratch.Clear();
}
}
private void DrawWalkDrivenStatics(
RetailPViewFrameInput ctx,
RetailPViewPassExecutor passes,
Walk.WalkFrameDriver driver)
{
var (frame, encoder) = passes.RequireWalkSubmission();
driver.Replay(frame, encoder);
// Landscape-stage static-owner particles (candle flames on lamp and
// candle statics): for an OUTDOOR root they submit here, post-replay
// (drained at the frame-end flush over finished depth — the #132
// placement). For an INTERIOR root they must submit at the walk's
// PRE-CLEAR boundary instead — retail inserts each one during its
// cell's landscape walk turn, before the clear+seals; a post-seal
// drain z-fails them at the aperture (the visual-gate "no candle
// flames looking out" report) — so SubmitWalkLandscapeStaticParticles
// is invoked from _walkPreClearDynamics in that case, and skipped
// here.
if (ctx.RootCell.IsOutdoorNode)
SubmitWalkLandscapeStaticParticles(ctx, passes, driver);
// Cell-stage particle owners for the interior root's OWN flood cells
// (non-look-in — the walk draws their statics too, via
// OnInteriorFloodDrawTurn/EmitCellTurn, so the retired CellStatic
// route's cell-particle submission needs the same re-sourcing).
// Look-in cells get their OWN per-cell union in
// These stay POST-replay: interior emitters draw in the final world
// scope where the cell walls already own depth (retail's cell-walk
// insertion). Look-in cells get their OWN per-cell union in
// DrawBuildingLookInDynamics so a static owner is never submitted
// twice.
_cellParticleOwnerScratch.Clear();