fix(render): FW4 - static-owner particles submit at their walk turns

Ports the user-verified #132 invariant (e102fb36 on the old pipeline)
walk-natively, on the owner''s direction: retail''s falls containment
is POSITIONAL - an outdoor emitter''s polys join the ONE alpha list
during its owner cell''s DrawObjCell in the far-to-near landscape walk
(DrawSortCell @0x005a17c0), so every nearer building''s pre-punch
barrier (DrawBuilding @0x0059f2a0''s FlushAlphaList @0x0059f30b)
drains the already-queued FARTHER content against still-true depth
BEFORE its punch stamps far-Z into the aperture. The walk path had
kept the per-building AlphaBarrier events but batched ALL landscape
static-owner submission into one lump at the pre-clear closure - after
every punch had run: the barriers fired over an empty queue and the
falls drained against punched-far aperture pixels (phase=pre in every
probe line, which is why six rounds of phase-staging repairs could
never see it - the phase was right, the position within the phase was
wrong).

New WalkFrameEventKind.StaticParticles: each landscape cell''s and
each building shell''s emitter owners now submit AT THAT TURN during
Replay (marked so the owner''s meshes flush first - retail''s
per-object order); the batched SubmitWalkLandscapeStaticParticles and
its closure/post-replay call sites are deleted for both root kinds.
Two driver sequence pins adjudicated to the new turn order.

Hermetic 6,762/0; InstalledDat walk conformance 40/1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 21:26:40 +02:00
parent f4522297cc
commit 280c054943
4 changed files with 123 additions and 47 deletions

View file

@ -622,11 +622,10 @@ public sealed class RetailPViewRenderer
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);
// Static-owner emitters now submit AT THEIR OWN WALK
// TURNS during Replay (WalkFrameEventKind.StaticParticles
// — the #132 positional invariant), so the pre-clear
// closure carries only the dynamics phase.
passes.UseIndoorMembershipOnlyRouting();
DrawLandscapeDynamicsPhase(
ctx,
@ -1358,33 +1357,6 @@ 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>
/// <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)
{
_staticParticleUnionScratch.Clear();
foreach (uint cellId in driver.VisitedLandscapeCellIds)
UnionRecordOwners(_walkWorldData!.GetOutdoorStatics(cellId), _staticParticleUnionScratch);
foreach (Walk.WalkBuilding building in driver.VisitedBuildings)
UnionRecordOwners(_walkWorldData!.GetBuildingShellStatics(building), _staticParticleUnionScratch);
if (_staticParticleUnionScratch.Count > 0)
{
passes.DrawLandscapeStaticParticles(
ctx,
new RetailPViewLandscapeStaticParticleContext(_staticParticleUnionScratch));
_staticParticleUnionScratch.Clear();
}
}
private void DrawWalkDrivenStatics(
RetailPViewFrameInput ctx,
RetailPViewPassExecutor passes,
@ -1393,18 +1365,14 @@ public sealed class RetailPViewRenderer
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);
// Landscape-stage static-owner particles (candles, the cathedral
// falls) submit AT THEIR OWN WALK TURNS inside Replay
// (WalkFrameEventKind.StaticParticles) for BOTH root kinds — the
// #132 positional invariant (e102fb36's user-verified rule ported
// walk-natively): each owner cell's emitters enter the one alpha
// queue at that cell's far-to-near position, so every nearer
// building's pre-punch AlphaBarrier drains the farther content
// against still-true depth before its punch stamps far-Z.
// Cell-stage particle owners for the interior root's OWN flood cells
// (non-look-in — the walk draws their statics too, via