feat(render) Campaign FW3.2b-2: THE STATIC CUTOVER - the walk drives production statics

The retail frame walk now drives every production static draw. In
RetailPViewRenderer.DrawInside, when the concrete executor + the
packed product + the FW3.1 walk registries are all wired (all
production compositions - anything less throws):

- A pre-walk events-only collection pass (the shadow sink generalized
  to WalkVisitedSetCollector) gathers the frame's visited cells,
  buildings, and landscape-cell turns; the visited cells union into
  prepareCells so EnvCellRenderer prepares every shell the driver
  draws.
- DrawWalkDrivenStatics runs the WalkFrameDriver over the production
  world data (WalkProductionWorldData over RenderSceneQuery + the
  building registry): sky, terrain slices, outdoor statics at their
  landscape-cell turns, buildings (alpha barrier -> punch/look-in
  passes -> shell) in retail order, interior clear+seals as leaf
  closures (the old tail block's drain reasoning moves with them),
  flood cells shell-then-contents. Landscape/cell-stage particle
  owners re-source from the walk's visited sets - retail gates
  particles per cell turn (ShouldDrawParticles @0x0050FE60), which
  this is; the old sphere filter was the approximation.
- DrawLandscapeDynamicsPhase + DrawBuildingLookInDynamics carry the
  dynamics-only remainder (LookInObject now dynamic-classified,
  late outside-dynamics + weather, particle unions); DrawDynamicsLast
  and the outdoor flush are unchanged.
- The product builder stops emitting LandscapeOutdoorStatic /
  LandscapeBuildingShell / CellStatic (methods deleted, dead index
  tracking removed); LookInObject loads cells with
  includeStatics: false.

The old static path survives ONLY behind !walkActive for the
standalone/diagnostic executor-fake path that keeps 15 retail-ordering
regression tests exercising the barrier/punch/seal machinery; no
production composition can reach it. Its deletion is FW4 scope (the
plan's "deleting the patch apparatus") - recorded in the plan.

Transitional risks recorded in code/report: the two-pass walk cost
(FW3.4 measures), the interior slice-count reconciliation between the
old clip assembly and the walk's own exit-view survival, and the
outdoor merged-flood punch coverage now riding the walk's own
building-BSP punches (retail-faithful per FW1; the owner visual gate
verifies).

Suites (lead-verified): full Release build 0 warnings; hermetic
6,750/0 (baseline minus the three deleted route tests); Walk lane
201/1; InstalledDat Walk conformance 40/1 untouched. The two
IL-branch tests the implementation round reported failing pass in
every lead run - the recurring parallel-load flake pair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 15:43:22 +02:00
parent 878533597d
commit 4918677b45
8 changed files with 919 additions and 612 deletions

View file

@ -101,6 +101,12 @@ public sealed class RetailPViewRenderer
private readonly Walk.WalkLandscapeAssembler? _walkLandscape;
private readonly CellVisibility? _walkCellRegistry;
// Campaign FW3.2b-2: the production IWalkFrameWorldData over the retained
// scene — owned here (not just inside the driver) because DrawInside also
// reads it directly to re-source particle owners for the routes the walk
// now draws (plan §FW3 item 4). Non-null exactly when _walkBuildings is.
private readonly Walk.WalkProductionWorldData? _walkWorldData;
internal RetailPViewRenderer(
InteriorEntityPartition.IObserver? partitionObserver,
RenderScenePViewFrameProductController? sceneFrameProduct = null,
@ -111,6 +117,9 @@ public sealed class RetailPViewRenderer
_walkBuildings = walkBuildings;
_walkLandscape = walkLandscape;
_walkCellRegistry = walkCellRegistry;
_walkWorldData = walkBuildings is not null
? new Walk.WalkProductionWorldData(walkBuildings)
: null;
_partitionObserver = partitionObserver;
_candidateObserver = partitionObserver as ICurrentRenderPViewObserver;
_sceneFrameProduct = sceneFrameProduct;
@ -187,6 +196,90 @@ public sealed class RetailPViewRenderer
var drawableCells = _drawableCellsScratch;
passes.UseIndoorMembershipOnlyRouting();
// Campaign FW3.2b-2: the production rooting. A concrete executor is
// required — the walk submits through WbDrawDispatcher.SubmitOrderedStream
// and needs a real GPU frame/encoder (RequireWalkSubmission), which no
// test IRetailPViewPassExecutor fake can supply. Whenever a concrete
// executor IS present, the packed entity-route product must ALSO be
// wired with all three walk registries — a scene product without the
// walk data (or vice versa) is a production miswiring, not a
// legacy/diagnostic shape, so it fails loud rather than silently
// falling back to the retired static routes (plan §FW3 item 6).
RetailPViewPassExecutor? walkExecutor = passes as RetailPViewPassExecutor;
bool walkRegistriesReady =
_walkBuildings is not null
&& _walkLandscape is not null
&& _walkCellRegistry is not null
&& _walkWorldData is not null;
if (walkExecutor is not null
&& _sceneFrameProduct is not null
&& !walkRegistriesReady)
{
throw new InvalidOperationException(
"RetailPViewRenderer has a concrete pass executor and a "
+ "RenderScenePViewFrameProductController but the walk registries "
+ "(WalkBuildingRegistry/WalkLandscapeAssembler/CellVisibility) are "
+ "not all wired — the Campaign FW3.2b-2 static cutover requires "
+ "every piece together; see FrameRootComposition's "
+ "RetailPViewRenderer construction.");
}
bool walkActive =
walkExecutor is not null
&& _sceneFrameProduct is not null
&& walkRegistriesReady;
// Campaign FW3.2b-2 pre-walk collection pass: run the production walk
// EVENTS-ONLY (no leaf draws) to learn the flood cell set it will draw
// this frame, BEFORE prepareCells is finalized below — so EnvCellRenderer
// prepares batches for every shell the driver will draw later in this
// same DrawInside call. The same WalkProductionFrameContext + camera
// cell resolved here are reused for the real driven run further down
// (plan §FW3 "FW3.2b-2 — the production rooting", item 1).
Walk.WalkProductionFrameContext? walkContext = null;
Walk.WalkLandscape? walkLandscape = null;
Walk.WalkCell? walkCameraCell = null;
if (walkActive)
{
Matrix4x4 view = ctx.CameraView;
var forward = Vector3.Normalize(new Vector3(-view.M13, -view.M23, -view.M33));
(int Width, int Height)? attachment = walkExecutor!.WalkAttachmentExtent;
// Fallback matches the FW3.2b-2 shadow probe's own comment: every
// screen projection shares the same constants, so this is only
// reached before the world pass has published its scope.
float viewportWidth = attachment?.Width ?? 1024f;
float viewportHeight = attachment?.Height ?? 720f;
walkContext = new Walk.WalkProductionFrameContext(
_walkCellRegistry!,
_walkBuildings!,
ctx.ViewerEyePos,
forward,
ctx.ViewProjection,
viewportWidth,
viewportHeight);
_walkLandscape!.SetViewer(ctx.ViewerCellId, ctx.ViewerEyePos);
walkLandscape = _walkLandscape.Landscape;
if ((ctx.ViewerCellId & 0xFFFFu) >= 0x100)
{
walkCameraCell = _walkCellRegistry!.TryGetCell(ctx.ViewerCellId, out LoadedCell? loaded)
? loaded?.Walk
: null;
if (walkCameraCell is null)
{
throw new InvalidOperationException(
$"walk root=0x{ctx.ViewerCellId:X8}: the interior camera cell has "
+ "no committed walk data — the Campaign FW3.2b-2 static cutover "
+ "requires the walk registry to already hold the viewer's own cell "
+ "(fail-loud rule; a silently skipped root would leave the frame "
+ "with no static draws at all).");
}
}
_walkVisitedScratch.Reset();
_frameWalk.WalkFrame(
ctx.ViewerCellId, walkCameraCell, walkLandscape, walkContext, _walkVisitedScratch);
}
// #124: look-in cells need prepared shell batches + their statics routed
// into partition.ByCell (consumed ONLY by DrawBuildingLookIns — the main
// cell-object pass iterates pvFrame.OrderedVisibleCells, which never
@ -194,7 +287,7 @@ public sealed class RetailPViewRenderer
// seals, the outside-stage predicate, and the frame result.
var prepareCells = drawableCells;
_lookInCellIds.Clear();
if (_lookInFrames.Count > 0)
if (_lookInFrames.Count > 0 || walkActive)
{
_lookInPrepareScratch.Clear();
_lookInPrepareScratch.UnionWith(drawableCells);
@ -206,6 +299,13 @@ public sealed class RetailPViewRenderer
_lookInCellIds.Add(c);
}
}
if (walkActive)
{
// The walk's own flood/look-in cell set — unioned in (never
// aliased with drawableCells, which the outside-stage and seal
// predicates below still need scoped to the OLD flood only).
_lookInPrepareScratch.UnionWith(_walkVisitedScratch.Cells);
}
prepareCells = _lookInPrepareScratch;
}
@ -330,52 +430,85 @@ public sealed class RetailPViewRenderer
}
}
DrawLandscapeThroughOutsideView(
ctx,
passes,
clipAssembly,
partition,
viewcone,
frameEntityPasses,
in frameView);
passes.UseIndoorMembershipOnlyRouting();
// Retail DrawBuilding @0x0059F2A0 runs FlushAlphaList(0f) BEFORE
// its portal-only far-Z pass. In retail's strict far→near walk
// everything queued at that instant is FARTHER than the structure
// being punched, so no already-drained poly can meet a punched
// aperture's falsified depth, and everything drained later is
// NEARER than the punched structure and legitimately composites in
// front of it. The batched outdoor frame reproduces that invariant
// here: drain the far prefix — every entry at or beyond the
// nearest cell whose exit-portal mask is about to punch far-Z —
// against still-true landscape depth. Without this, an exterior
// waterfall beyond the cathedral drains after the punches and
// z-passes across every aperture pixel whose true depth the punch
// erased (#132 regression found at the 2026-08-29 cathedral gate).
// Interior roots keep their pre-clear stage-boundary drain.
if (ctx.RootCell.IsOutdoorNode)
if (walkActive)
{
passes.FlushLandscapeAlphaFartherThan(
ExitPortalMaskBarrierDistance(
pvFrame,
drawableCells,
ctx.Cells,
ctx.CameraWorldPosition));
// Campaign FW3.2b-2: the walk owns every static draw —
// terrain/sky, building shells + punches + look-in cell
// statics, and the interior root's own flood shells +
// statics (with the depth clear + exit seals at their real
// retail turn). The OLD visibility (pvFrame/clipAssembly/
// viewcone, already built above) keeps running unchanged to
// feed the surviving dynamic routes only (plan §FW3 item 1's
// dual-compute split).
DrawWalkDrivenStatics(
ctx,
walkExecutor!,
clipAssembly,
pvFrame,
drawableCells,
walkContext!,
walkCameraCell,
walkLandscape!);
passes.UseIndoorMembershipOnlyRouting();
DrawLandscapeDynamicsPhase(
ctx,
passes,
clipAssembly,
partition,
viewcone,
frameEntityPasses,
in frameView);
}
else
{
DrawLandscapeThroughOutsideView(
ctx,
passes,
clipAssembly,
partition,
viewcone,
frameEntityPasses,
in frameView);
passes.UseIndoorMembershipOnlyRouting();
// Retail DrawBuilding @0x0059F2A0 runs FlushAlphaList(0f) BEFORE
// its portal-only far-Z pass. In retail's strict far→near walk
// everything queued at that instant is FARTHER than the structure
// being punched, so no already-drained poly can meet a punched
// aperture's falsified depth, and everything drained later is
// NEARER than the punched structure and legitimately composites in
// front of it. The batched outdoor frame reproduces that invariant
// here: drain the far prefix — every entry at or beyond the
// nearest cell whose exit-portal mask is about to punch far-Z —
// against still-true landscape depth. Without this, an exterior
// waterfall beyond the cathedral drains after the punches and
// z-passes across every aperture pixel whose true depth the punch
// erased (#132 regression found at the 2026-08-29 cathedral gate).
// Interior roots keep their pre-clear stage-boundary drain.
if (ctx.RootCell.IsOutdoorNode)
{
passes.FlushLandscapeAlphaFartherThan(
ExitPortalMaskBarrierDistance(
pvFrame,
drawableCells,
ctx.Cells,
ctx.CameraWorldPosition));
}
DrawExitPortalMasks(ctx, passes, pvFrame, clipAssembly, drawableCells);
DrawEnvCellShells(passes, pvFrame);
DrawCellObjectLists(
ctx,
passes,
pvFrame,
clipAssembly,
drawableCells,
partition,
viewcone,
frameEntityPasses,
in frameView);
}
DrawExitPortalMasks(ctx, passes, pvFrame, clipAssembly, drawableCells);
DrawEnvCellShells(passes, pvFrame);
DrawCellObjectLists(
ctx,
passes,
pvFrame,
clipAssembly,
drawableCells,
partition,
viewcone,
frameEntityPasses,
in frameView);
DrawDynamicsLast(
ctx,
passes,
@ -930,8 +1063,8 @@ public sealed class RetailPViewRenderer
return;
}
}
var sink = new WalkShadowSink();
_walkShadowFrameWalk.WalkFrame(
var sink = new WalkVisitedSetCollector();
_frameWalk.WalkFrame(
ctx.ViewerCellId, cameraCell, landscape, context, sink);
int onlyWalk = 0;
@ -956,13 +1089,40 @@ public sealed class RetailPViewRenderer
}
}
private readonly Walk.RetailFrameWalk _walkShadowFrameWalk = new();
// Campaign FW3.2b-2: the one RetailFrameWalk instance shared by the
// diagnostic shadow probe, the production pre-walk collection pass, and
// the real driven run — WalkFrame calls are never concurrent/re-entrant
// within a single-threaded render loop, so one shared instance is safe
// and avoids re-allocating the walk's own PView scratch per call site.
private readonly Walk.RetailFrameWalk _frameWalk = new();
private sealed class WalkShadowSink : Walk.IWalkEventSink
// Campaign FW3.2b-2: the pre-walk collection pass's reusable sink (see
// DrawInside's walkActive block) — reset and re-driven once per frame.
private readonly WalkVisitedSetCollector _walkVisitedScratch = new();
/// <summary>Campaign FW3.2b-2 (the I5 dual-shadow pattern, extended for
/// production use): an events-only <see cref="Walk.IWalkEventSink"/> that
/// collects the SETS a driven run would touch, without doing any leaf
/// drawing itself — the shadow probe's original role, now ALSO the
/// pre-walk collection pass's role (plan §FW3 item 1: the walk's flood
/// cell set for the <c>prepareCells</c> union, the visited building list
/// and landscape-cell turn ids for re-sourcing particle owners once the
/// walk owns the static routes those owners used to ride).</summary>
private sealed class WalkVisitedSetCollector : Walk.IWalkEventSink
{
public readonly HashSet<uint> Cells = new();
public int BuildingCount;
public int LandscapeCount;
public readonly List<Walk.WalkBuilding> Buildings = new();
public readonly HashSet<uint> LandscapeCellIds = new();
public int BuildingCount => Buildings.Count;
public int LandscapeCount => LandscapeCellIds.Count;
public void Reset()
{
Cells.Clear();
Buildings.Clear();
LandscapeCellIds.Clear();
}
public void Emit(in Walk.WalkEvent walkEvent)
{
@ -975,14 +1135,358 @@ public sealed class RetailPViewRenderer
foreach (uint id in walkEvent.Cells)
Cells.Add(id);
break;
case Walk.WalkEventKind.Building:
BuildingCount++;
break;
case Walk.WalkEventKind.Landscape:
LandscapeCount++;
break;
}
}
public void OnLandscapeCellTurn(uint cellId) => LandscapeCellIds.Add(cellId);
public void OnBuildingTurn(Walk.WalkBuilding building) => Buildings.Add(building);
}
/// <summary>Campaign FW3.2b-2 — THE PRODUCTION ROOTING. Runs the real
/// <see cref="Walk.RetailFrameWalk"/> through
/// <see cref="Walk.WalkFrameDriver"/> over
/// <see cref="WbDrawDispatcher.SubmitOrderedStream"/>: terrain/sky, every
/// building's shell + punch + look-in cell statics, and the interior
/// root's own flood shells + statics (with retail's depth-clear/exit-seal
/// turn, via <see cref="DrawExitPortalMasks"/> bound as the driver's own
/// seal action) all draw here, in walk order — replacing
/// <see cref="DrawLandscapeThroughOutsideView"/>'s static half,
/// <see cref="DrawExitPortalMasks"/>'s old top-level call,
/// <see cref="DrawEnvCellShells"/>, and <see cref="DrawCellObjectLists"/>'s
/// static half for this frame (plan §FW3 "FW3.2b-2 — the production
/// rooting", item 2). Also re-sources the particle owners the routes it
/// just replaced used to ride (item 4).</summary>
private void DrawWalkDrivenStatics(
RetailPViewFrameInput ctx,
RetailPViewPassExecutor passes,
ClipFrameAssembly clipAssembly,
PortalVisibilityFrame pvFrame,
HashSet<uint> drawableCells,
Walk.WalkProductionFrameContext walkContext,
Walk.WalkCell? cameraCell,
Walk.WalkLandscape landscape)
{
_walkWorldData!.BeginFrame(_sceneFrameProduct!.SceneQuery, ctx.PlayerLandblockId ?? 0u);
Action clearInteriorDepth = () =>
{
// Retail PView::DrawCells 0x005A4872 drains the landscape alpha
// list immediately before the gated full depth clear — mirrors
// DrawLandscapeThroughOutsideView's own pre-clear drain (this
// action only ever fires for an INTERIOR root; see
// IWalkFrameLeafRenderer.ClearInteriorDepth's own doc comment for
// why the driver never invokes it outdoors).
passes.FlushLandscapeAlpha();
passes.ClearInteriorDepth();
};
Action drawExitSeals = () =>
DrawExitPortalMasks(ctx, passes, pvFrame, clipAssembly, drawableCells);
var leafRenderer = new WalkProductionLeafRenderer(
passes, ctx, clipAssembly, clearInteriorDepth, drawExitSeals);
var driver = new Walk.WalkFrameDriver(passes.Dispatcher, leafRenderer, _walkWorldData);
var (frame, encoder) = passes.RequireWalkSubmission();
int activeTerrainSliceCount = clipAssembly.OutsideViewSlices.Length;
if (ctx.RootCell.IsOutdoorNode && activeTerrainSliceCount != 1)
{
throw new InvalidOperationException(
"walk static cutover: an outdoor root's clip assembly produced "
+ $"{activeTerrainSliceCount} outside-view slices, not the expected 1 — "
+ "WalkFrameDriver's landscape turn assumes the outdoor root's default "
+ "full-screen view (plan §FW3 item 2c's pinned assumption; assert "
+ "rather than silently coercing to 1).");
}
driver.RunFrame(
_frameWalk,
ctx.ViewerCellId,
cameraCell,
landscape,
walkContext,
frame,
encoder,
ctx.ViewProjection,
ctx.CameraWorldPosition,
activeTerrainSliceCount);
// 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 _walkVisitedScratch.LandscapeCellIds)
UnionRecordOwners(_walkWorldData.GetOutdoorStatics(cellId), _staticParticleUnionScratch);
foreach (Walk.WalkBuilding building in _walkVisitedScratch.Buildings)
UnionRecordOwners(_walkWorldData.GetBuildingShellStatics(building), _staticParticleUnionScratch);
if (_staticParticleUnionScratch.Count > 0)
{
passes.DrawLandscapeStaticParticles(
ctx,
new RetailPViewLandscapeStaticParticleContext(_staticParticleUnionScratch));
_staticParticleUnionScratch.Clear();
}
// 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
// DrawBuildingLookInDynamics so a static owner is never submitted
// twice.
_cellParticleOwnerScratch.Clear();
foreach (uint cellId in _walkVisitedScratch.Cells)
{
if (_lookInCellIds.Contains(cellId))
continue;
UnionRecordOwners(_walkWorldData.GetCellStatics(cellId), _cellParticleOwnerScratch);
}
if (_cellParticleOwnerScratch.Count > 0)
{
passes.DrawCellParticles(
ctx,
new RetailPViewCellSliceContext(0u, NoClipSlice, _cellParticleOwnerScratch));
}
}
private static void UnionRecordOwners(
Walk.WalkFrameStaticRecords records, HashSet<uint> destination)
{
foreach (RenderProjectionRecord record in records.Records)
{
if (record.Source.LocalEntityId != 0)
destination.Add(record.Source.LocalEntityId);
}
}
/// <summary>Campaign FW3.2b-2: the DYNAMICS-only remainder of the old
/// <see cref="DrawLandscapeThroughOutsideView"/> + <see cref="DrawBuildingLookIns"/>
/// split — the walk now draws every STATIC route (see
/// <see cref="DrawWalkDrivenStatics"/>); this method keeps ONLY what
/// stays on the OLD visibility pipeline per the plan's dual-compute
/// split: outdoor-cell unattached particles, LookInObject dynamics + their
/// per-cell particles, the late per-slice outside-dynamics/weather loop,
/// and the late particle union submission.</summary>
private void DrawLandscapeDynamicsPhase(
RetailPViewFrameInput ctx,
IRetailPViewPassExecutor passes,
ClipFrameAssembly clipAssembly,
InteriorEntityPartition.Result? partition,
ViewconeCuller viewcone,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView)
{
if (clipAssembly.OutsideViewSlices.Length == 0)
return;
// Ownerless OUTDOOR-cell emitters — now unconditional: the old
// hasBuildingLookIns gate only existed to sequence this submission
// around the OLD static barrier drains, which no longer run here
// (the walk owns its own alpha barriers — WalkFrameDriver.OnBuildingTurn).
passes.DrawUnattachedSceneParticles(ctx, outdoorCells: true);
DrawBuildingLookInDynamics(
ctx, passes, clipAssembly, partition, frameEntityPasses, in frameView);
// LATE phase (per slice): outside-stage dynamics' meshes + weather —
// unchanged from DrawLandscapeThroughOutsideView's own late loop.
_staticParticleUnionScratch.Clear();
int probeSliceIndex = 0;
foreach (var slice in clipAssembly.OutsideViewSlices)
{
passes.SetTerrainClip(slice.Planes);
passes.ClearClipRouting();
_outdoorStaticScratch.Clear();
_lateParticleOwnerScratch.Clear();
foreach (var e in _outsideStageDynamics)
{
EntitySphere(e, out var c, out float r);
if (viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r))
{
_outdoorStaticScratch.Add(e);
if (!InteriorEntityPartition.IsIndoorCellId(e.ParentCellId))
_lateParticleOwnerScratch.Add(e.Id);
}
}
if (frameEntityPasses is not null)
{
RenderFrameRouteOwnerSelector.Union(
_lateParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
probeSliceIndex,
0);
}
_candidateObserver?.ObservePViewBucket(
CurrentRenderPViewRoute.LandscapeOutsideDynamic,
probeSliceIndex,
0,
_outdoorStaticScratch);
RenderFrameEntityDrawRequest? entityDraw =
frameEntityPasses is null
? null
: new RenderFrameEntityDrawRequest(
frameView,
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
probeSliceIndex,
0,
ctx.PlayerLandblockId ?? 0);
probeSliceIndex++;
_staticParticleUnionScratch.UnionWith(_lateParticleOwnerScratch);
passes.DrawLandscapeSliceLate(
ctx,
new RetailPViewLandscapeLateSliceContext(slice, _outdoorStaticScratch)
{
EntityDraw = entityDraw,
});
}
// Late-particle union submission — DynamicLast owners excluded, same
// as DrawLandscapeThroughOutsideView's own final submission.
if (frameEntityPasses is not null)
{
RenderFrameRouteOwnerSelector.ExceptRoute(
_staticParticleUnionScratch, in frameView, RenderFrameCandidateRoute.DynamicLast);
}
if (_staticParticleUnionScratch.Count > 0)
{
passes.DrawLandscapeStaticParticles(
ctx,
new RetailPViewLandscapeStaticParticleContext(_staticParticleUnionScratch));
_staticParticleUnionScratch.Clear();
}
passes.UseIndoorMembershipOnlyRouting();
}
/// <summary>Campaign FW3.2b-2: the DYNAMICS-only remainder of the old
/// <see cref="DrawBuildingLookIns"/> — punches, shells, and look-in cell
/// STATICS are now walk-owned (<see cref="Walk.WalkFrameDriver"/>'s
/// Building/BuildingShell/LookInStatic turns); this method keeps ONLY the
/// LookInObject route (now dynamic-classified — see
/// <c>RenderScenePViewFrameBuilder.BuildLookInRoutes</c>) and the per-cell
/// particle union that route's owners feed, unioned with the walk's
/// static owners for that SAME cell (plan §FW3 item 4 — GetCellStatics
/// fills the gap the retired CellStatic-route particle submission left
/// for look-in cells specifically).</summary>
private void DrawBuildingLookInDynamics(
RetailPViewFrameInput ctx,
IRetailPViewPassExecutor passes,
ClipFrameAssembly clipAssembly,
InteriorEntityPartition.Result? partition,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView)
{
if (_lookInFrames.Count == 0)
return;
int lookInRouteIndex = 0;
for (int frameIndex = 0; frameIndex < _lookInFrames.Count; frameIndex++)
{
PortalVisibilityFrame frame = _lookInFrames[frameIndex];
for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--)
{
uint cellId = frame.OrderedVisibleCells[i];
var clipKey = new LookInClipCell(frameIndex, cellId);
if (!clipAssembly.LookInCellToViewSlices.TryGetValue(
clipKey,
out ClipViewSlice[]? cellSlices)
|| cellSlices.Length == 0)
{
continue;
}
_cellStaticScratch.Clear();
if (partition is not null)
{
foreach (var e in partition.Dynamics)
if (e.ParentCellId == cellId)
_cellStaticScratch.Add(e);
}
bool cellDrewObjects = false;
_cellParticleUnionScratch.Clear();
foreach (ClipViewSlice slice in cellSlices)
{
int routeIndex = lookInRouteIndex++;
passes.UseCellPortalViewRouting(cellId, slice);
if (frameEntityPasses is not null)
{
RenderFrameRouteOwnerSelector.Replace(
_cellParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.LookInObject,
routeIndex,
cellId);
}
else
{
ReplaceOwnerIds(
_cellParticleOwnerScratch,
_cellStaticScratch);
}
if (frameEntityPasses is not null
|| _cellStaticScratch.Count > 0)
{
_candidateObserver?.ObservePViewBucket(
CurrentRenderPViewRoute.LookInObject,
routeIndex,
cellId,
_cellStaticScratch);
_oneCell.Clear();
_oneCell.Add(cellId);
DrawEntityRouteOrLegacy(
ctx,
passes,
frameEntityPasses,
in frameView,
RenderFrameCandidateRoute.LookInObject,
routeIndex,
cellId,
_cellStaticScratch,
_oneCell);
cellDrewObjects = true;
_cellParticleUnionScratch.UnionWith(
_cellParticleOwnerScratch);
}
}
// The walk already drew this cell's STATIC content
// (WalkFrameDriver's LookInStatic turn) but never submits
// particles for it — GetCellStatics fills that gap, unioned
// with the dynamic route's own owners so ONE
// DrawCellParticles call covers both.
if (_walkWorldData is not null)
{
Walk.WalkFrameStaticRecords statics =
_walkWorldData.GetCellStatics(cellId);
foreach (RenderProjectionRecord record in statics.Records)
{
if (record.Source.LocalEntityId != 0)
{
_cellParticleUnionScratch.Add(record.Source.LocalEntityId);
cellDrewObjects = true;
}
}
}
if (cellDrewObjects)
{
passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext(
cellId, NoClipSlice, _cellParticleUnionScratch));
}
}
passes.UseIndoorMembershipOnlyRouting();
}
}
private void DrawLandscapeThroughOutsideView(