diff --git a/docs/plans/2026-08-30-campaign-fw-frame-walk.md b/docs/plans/2026-08-30-campaign-fw-frame-walk.md
index aa429a29..faa9a0c6 100644
--- a/docs/plans/2026-08-30-campaign-fw-frame-walk.md
+++ b/docs/plans/2026-08-30-campaign-fw-frame-walk.md
@@ -343,6 +343,63 @@ deeper commitment.
landscape turn; the `_drawCullModes`/alpha-scope sequencing constraint
resolved (own cull scratch or strict sequencing). Route-keyed particle
owners re-sourced from the walk.
+- **FW3.2b-2 — the production rooting (DESIGN, 2026-08-30; execute
+ INLINE, not via subagent — the integration-slice rule).** The
+ dual-compute split for this stage: the OLD visibility
+ (`PortalVisibilityBuilder` + look-in frames + clip assembly +
+ viewcone) KEEPS RUNNING to feed everything dynamic; the WALK draws
+ everything static. No old static DRAWS remain (no double draw):
+ 1. `RetailPViewRenderer.DrawInside` keeps steps 1–8 and 10–11
+ (visibility, merge/look-in seeding, clip frame, prepare — with
+ `prepareCells` UNIONED with the walk's flood/look-in cells —
+ viewcone, route build) but `RenderScenePViewFrameProduct` STOPS
+ emitting `LandscapeOutdoorStatic`, `LandscapeBuildingShell`, and
+ `CellStatic` ranges. Per the plan's route split, LOOK-IN OBJECTS
+ (`LookInObject`) are dynamic-side and STAY PACKED this stage —
+ the old look-in machinery keeps drawing look-in cell content and
+ its punches; the WALK's building turns therefore draw shell
+ content + punches for NON-look-in... NO — resolve the overlap the
+ other way: the walk's building turns run their punch+look-in
+ machinery ONLY for the walk's own conformance events; production
+ look-in DRAWS (punch fans + content) stay entirely on the old
+ path this stage, and the walk's `BuildingShell` stage content +
+ `OnPunchGeometry`/look-in DC draws are SUPPRESSED in the driver's
+ production wiring for look-in-owned buildings… REJECTED — that is
+ a dual path. FINAL: the walk owns ALL of it — building shells,
+ punches, look-in STATIC content (stage LookInStatic) — and the
+ `LookInObject` route survives ONLY for the DYNAMICS inside
+ look-in cells (`BuildLookInRoutes` filtered to dynamic
+ classifications). The old `DrawBuildingLookIns` static half and
+ `DrawLookInPortalPunch` retire with the static routes.
+ 2. `WalkFrameDriver`'s leaf renderer maps to:
+ sky → `RetailPViewPassExecutor` sky call; terrain slice →
+ `_terrain.Draw(camera, frustum, clipPlanes: slice.Planes,
+ ndcClipAabb: slice.NdcAabb)`; cell shell →
+ `EnvCellRenderer.Render(pass, singleCellSet)` (+ transparent
+ ordered per cell); punch fan → `PortalDepthMaskRenderer.
+ DrawDepthFan(worldVerts, viewProjection, slicePlanes,
+ forceFarZ: true)`; `ClearInteriorDepth` →
+ `IWorldPassScope.ClearInteriorDepth` (keep production's
+ slices>0 gate); `DrawExitSeals` → the existing seal-fan loop
+ (`DrawPortalDepthWrite` seal flavor over the root flood's exit
+ portals); alpha barrier → `RetailAlphaQueue.FlushFartherThan`.
+ 3. Static records: cell statics via `RenderSceneQuery.
+ CopyCellStaticsTo(cellId)`; outdoor statics via a per-landscape-
+ cell index built once per frame from
+ `CopyIndexTo(OutdoorStatic)`; building shells via the
+ `IsBuildingShell` entities keyed by anchor cell.
+ 4. Consumers re-pointed: particle owners
+ (`RenderFrameRouteOwnerSelector.Replace(LandscapeOutdoorStatic…)`
+ sites) re-source from the walk's populated cells; diagnostics
+ source counts tolerate the route removal; `EmitRenderSignature`'s
+ terrain/scenery counts re-source or annotate.
+ 5. Sky/weather: sky moves into the walk's LS turn (once); weather
+ stays in the old late-dynamics slice loop this stage (it is
+ dynamic-adjacent and the plan's FW4 moves it fully).
+ 6. Gates before the owner sees a build: hermetic + InstalledDat
+ suites; the connected lifecycle route; the offline pixel gate
+ (outdoor statics); dense-Arwic pre/post pair (physical display).
+
- **FW3.3 — ShellDrawLiftZ retirement.** THREE sites (the register row
AP-32 lists only two — `LandblockBuildFactory.cs` bakes the lift into
the drawn cell transform and is unlisted): delete all three, flip the
diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
index 9477567c..329f34cd 100644
--- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
+++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs
@@ -1,4 +1,5 @@
using System.Numerics;
+using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Sky;
using AcDream.App.Rendering.Wb;
@@ -181,6 +182,25 @@ internal sealed partial class RetailPViewPassExecutor :
_particleClassifications.BeginFrame();
}
+ /// Campaign FW3.2b-2: the shared dispatcher, for
+ /// 's WalkFrameDriver
+ /// construction — the driver's ctor takes a
+ /// directly (it calls SubmitOrderedStream itself; see that
+ /// class's own doc comment).
+ internal WbDrawDispatcher Dispatcher => _entities;
+
+ /// Campaign FW3.2b-2: forwards to
+ /// — the frame/
+ /// encoder pair the walk driver submits its stream flushes into.
+ internal (IGpuFrame Frame, IGpuPassEncoder Encoder) RequireWalkSubmission() =>
+ _entities.RequireWalkSubmission();
+
+ /// Campaign FW3.2b-2: forwards to
+ /// — the real
+ /// viewport size for WalkProductionFrameContext.
+ internal (int Width, int Height)? WalkAttachmentExtent =>
+ _entities.WalkAttachmentExtent;
+
public void BeginEntityFrame(in RenderFrameView view) =>
_entities.BeginPackedProductionFrame(in view);
diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
index 81c38464..fbfb9050 100644
--- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs
+++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs
@@ -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();
+
+ /// Campaign FW3.2b-2 (the I5 dual-shadow pattern, extended for
+ /// production use): an events-only 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 prepareCells 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).
+ private sealed class WalkVisitedSetCollector : Walk.IWalkEventSink
{
public readonly HashSet Cells = new();
- public int BuildingCount;
- public int LandscapeCount;
+ public readonly List Buildings = new();
+ public readonly HashSet 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);
+ }
+
+ /// Campaign FW3.2b-2 — THE PRODUCTION ROOTING. Runs the real
+ /// through
+ /// over
+ /// : 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 bound as the driver's own
+ /// seal action) all draw here, in walk order — replacing
+ /// 's static half,
+ /// 's old top-level call,
+ /// , and '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).
+ private void DrawWalkDrivenStatics(
+ RetailPViewFrameInput ctx,
+ RetailPViewPassExecutor passes,
+ ClipFrameAssembly clipAssembly,
+ PortalVisibilityFrame pvFrame,
+ HashSet 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 destination)
+ {
+ foreach (RenderProjectionRecord record in records.Records)
+ {
+ if (record.Source.LocalEntityId != 0)
+ destination.Add(record.Source.LocalEntityId);
+ }
+ }
+
+ /// Campaign FW3.2b-2: the DYNAMICS-only remainder of the old
+ /// +
+ /// split — the walk now draws every STATIC route (see
+ /// ); 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.
+ 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();
+ }
+
+ /// Campaign FW3.2b-2: the DYNAMICS-only remainder of the old
+ /// — punches, shells, and look-in cell
+ /// STATICS are now walk-owned ('s
+ /// Building/BuildingShell/LookInStatic turns); this method keeps ONLY the
+ /// LookInObject route (now dynamic-classified — see
+ /// RenderScenePViewFrameBuilder.BuildLookInRoutes) 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).
+ 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(
diff --git a/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs b/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs
index 10bb06e3..7bd78e5d 100644
--- a/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs
+++ b/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs
@@ -122,6 +122,11 @@ internal sealed class RenderScenePViewFrameProductController :
{
private readonly RenderSceneShadowRuntime _shadow;
private readonly CurrentRenderSceneOracle? _current;
+
+ /// Campaign FW3.2b-2: the frame's retained-scene read view for
+ /// the walk's world-data provider (the same query
+ /// reads its routes from).
+ internal RenderSceneQuery SceneQuery => _shadow.Query;
private readonly WbDrawDispatcher? _dispatcher;
private readonly RenderScenePViewFrameBuilder _builder = new();
private readonly RenderFrameExchange _exchange = new();
@@ -1123,7 +1128,6 @@ internal sealed class RenderScenePViewFrameBuilder
private const byte EnvCellProjectionDomain = 2;
private readonly HashSet _projectionIds = [];
- private RenderProjectionRecord[] _outdoor = [];
private RenderProjectionRecord[] _dynamics = [];
private RenderProjectionRecord[] _cell = [];
private RenderProjectionRecord[] _dirty = [];
@@ -1134,14 +1138,9 @@ internal sealed class RenderScenePViewFrameBuilder
// marks every non-player part): an object whose cell drew with a look-in
// must not enter the final dynamics route again.
private readonly HashSet _lookInCellScratch = new();
- private RenderProjectionRecord[] _cellRoute = [];
- private readonly Dictionary
- _outdoorPositions = [];
private readonly Dictionary
_dynamicPositions = [];
- private int _outdoorCount;
private int _dynamicCount;
- private int _cellRouteCount;
private int _dirtyCount;
private RenderSceneGeneration _indexGeneration;
private ulong _indexRevision;
@@ -1164,7 +1163,13 @@ internal sealed class RenderScenePViewFrameBuilder
_projectionIds.Clear();
LoadSceneIndices(input.Scene);
- BuildOutdoorRoutes(writer, in input);
+ // Campaign FW3.2b-2: LandscapeOutdoorStatic, LandscapeBuildingShell,
+ // and CellStatic no longer emit here — WalkFrameDriver draws every
+ // outdoor static, building shell, and cell static (including
+ // look-in cell statics) directly through OrderedDrawStream (plan
+ // §FW3 "FW3.2b-2 — the production rooting", item 3). LookInObject
+ // keeps emitting, but BuildLookInRoutes below is now filtered to
+ // DYNAMIC candidates only — the walk owns that route's statics.
int lookInRouteIndex = 0;
for (int frameIndex = 0;
frameIndex < input.LookInFrames.Count;
@@ -1175,13 +1180,8 @@ internal sealed class RenderScenePViewFrameBuilder
in input,
frameIndex,
ref lookInRouteIndex);
- BuildLookInBuildingShellRoutes(
- writer,
- in input,
- frameIndex);
}
BuildOutsideDynamicRoutes(writer, in input);
- BuildCellStaticRoute(writer, in input);
BuildDynamicLastRoute(writer, in input);
writer.Publish();
AcknowledgeCachedDirtyRecords();
@@ -1200,18 +1200,6 @@ internal sealed class RenderScenePViewFrameBuilder
if (scene.Generation != _indexGeneration
|| revision != _indexRevision)
{
- EnsureCapacity(ref _outdoor, counts.OutdoorStatic);
- _outdoorCount = scene.CopyIndexTo(
- RenderSceneIndex.OutdoorStatic,
- _outdoor);
- _outdoorCount = CompactAndSort(
- _outdoor,
- _outdoorCount);
- BuildPositionIndex(
- _outdoor,
- _outdoorCount,
- _outdoorPositions);
-
EnsureCapacity(ref _dynamics, counts.Dynamic);
_dynamicCount = scene.CopyIndexTo(
RenderSceneIndex.Dynamic,
@@ -1235,12 +1223,6 @@ internal sealed class RenderScenePViewFrameBuilder
for (int index = 0; index < _dirtyCount; index++)
{
RenderProjectionRecord record = _dirty[index];
- if (_outdoorPositions.TryGetValue(
- record.Id,
- out int outdoorPosition))
- {
- _outdoor[outdoorPosition] = record;
- }
if (_dynamicPositions.TryGetValue(
record.Id,
out int dynamicPosition))
@@ -1255,16 +1237,6 @@ internal sealed class RenderScenePViewFrameBuilder
for (int index = 0; index < _dirtyCount; index++)
{
RenderProjectionId id = _dirty[index].Id;
- if (_outdoorPositions.TryGetValue(
- id,
- out int outdoorPosition))
- {
- _outdoor[outdoorPosition] =
- _outdoor[outdoorPosition] with
- {
- DirtyMask = RenderDirtyMask.None,
- };
- }
if (_dynamicPositions.TryGetValue(
id,
out int dynamicPosition))
@@ -1289,103 +1261,13 @@ internal sealed class RenderScenePViewFrameBuilder
positions.Add(records[index].Id, index);
}
- private void BuildOutdoorRoutes(
- RenderFrameWriter writer,
- in RenderScenePViewBuildInput input)
- {
- int sliceCount = input.ClipAssembly.OutsideViewSlices.Length;
- for (int sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++)
- {
- int count = 0;
- EnsureCapacity(ref _survivors, _outdoorCount);
- for (int i = 0; i < _outdoorCount; i++)
- {
- RenderProjectionRecord record = _outdoor[i];
- if (record.EntityPayload.IsBuildingShell
- && RetailPViewRenderer.FindLookInFrameIndex(
- record.Source.BuildingShellAnchorCellId,
- input.LookInFrames,
- input.Cells) >= 0)
- {
- continue;
- }
- Sphere(in record, out Vector3 center, out float radius);
- if (!input.Viewcone.SphereVisibleInOutsideSlice(
- sliceIndex,
- in center,
- radius))
- {
- continue;
- }
-
- _survivors[count++] = record;
- writer.AddOutdoor(in record);
- AddProjection(
- writer,
- in record,
- input.AnimatedEntityIds);
- }
-
- writer.AddRouteRange(
- RenderFrameCandidateRoute.LandscapeOutdoorStatic,
- sliceIndex,
- 0,
- _survivors.AsSpan(0, count));
- }
- }
-
- private void BuildLookInBuildingShellRoutes(
- RenderFrameWriter writer,
- in RenderScenePViewBuildInput input,
- int frameIndex)
- {
- int sliceCount = input.ClipAssembly.OutsideViewSlices.Length;
- for (int sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++)
- {
- int count = 0;
- EnsureCapacity(ref _survivors, _outdoorCount);
- for (int i = 0; i < _outdoorCount; i++)
- {
- RenderProjectionRecord record = _outdoor[i];
- if (!record.EntityPayload.IsBuildingShell
- || RetailPViewRenderer.FindLookInFrameIndex(
- record.Source.BuildingShellAnchorCellId,
- input.LookInFrames,
- input.Cells) != frameIndex)
- {
- continue;
- }
-
- Sphere(in record, out Vector3 center, out float radius);
- if (!input.Viewcone.SphereVisibleInOutsideSlice(
- sliceIndex,
- in center,
- radius))
- {
- continue;
- }
-
- _survivors[count++] = record;
- writer.AddOutdoor(in record);
- AddProjection(
- writer,
- in record,
- input.AnimatedEntityIds);
- }
-
- int routeIndex =
- RetailPViewRenderer.LookInBuildingShellRouteIndex(
- frameIndex,
- sliceCount,
- sliceIndex);
- writer.AddRouteRange(
- RenderFrameCandidateRoute.LandscapeBuildingShell,
- routeIndex,
- 0,
- _survivors.AsSpan(0, count));
- }
- }
-
+ /// Campaign FW3.2b-2: LookInObject now carries DYNAMIC
+ /// candidates only — the walk draws every look-in cell's STATIC content
+ /// directly (WalkFrameDriver's LookInStatic turn, via
+ /// WalkProductionWorldData.GetCellStatics), so loading this
+ /// route's cell contents with includeStatics: false is what keeps
+ /// the two draws from doubling a look-in room's furniture (plan §FW3
+ /// item 3).
private void BuildLookInRoutes(
RenderFrameWriter writer,
in RenderScenePViewBuildInput input,
@@ -1408,6 +1290,7 @@ internal sealed class RenderScenePViewFrameBuilder
int count = LoadCell(
input.Scene,
cellId,
+ includeStatics: false,
includeDynamics: true);
for (int sliceIndex = 0; sliceIndex < slices.Length; sliceIndex++)
{
@@ -1475,61 +1358,6 @@ internal sealed class RenderScenePViewFrameBuilder
}
}
- private void BuildCellStaticRoute(
- RenderFrameWriter writer,
- in RenderScenePViewBuildInput input)
- {
- _cellRouteCount = 0;
- IReadOnlyList ordered = input.PortalFrame.OrderedVisibleCells;
- for (int i = ordered.Count - 1; i >= 0; i--)
- {
- uint cellId = ordered[i];
- if (!input.DrawableCells.Contains(cellId))
- continue;
-
- int loaded = LoadCell(
- input.Scene,
- cellId,
- includeDynamics: false);
- int count = 0;
- for (int index = 0; index < loaded; index++)
- {
- RenderProjectionRecord record = _cell[index];
- Sphere(in record, out Vector3 center, out float radius);
- if (!input.Viewcone.SphereVisibleInCell(
- cellId,
- in center,
- radius))
- {
- continue;
- }
-
- _cell[count++] = record;
- AddProjection(
- writer,
- in record,
- input.AnimatedEntityIds);
- }
-
- writer.AddCellRange(
- cellId,
- i,
- _cell.AsSpan(0, count));
- EnsureCapacity(
- ref _cellRoute,
- checked(_cellRouteCount + count));
- _cell.AsSpan(0, count).CopyTo(
- _cellRoute.AsSpan(_cellRouteCount));
- _cellRouteCount += count;
- }
-
- writer.AddRouteRange(
- RenderFrameCandidateRoute.CellStatic,
- 0,
- 0,
- _cellRoute.AsSpan(0, _cellRouteCount));
- }
-
private void BuildDynamicLastRoute(
RenderFrameWriter writer,
in RenderScenePViewBuildInput input)
@@ -1593,18 +1421,25 @@ internal sealed class RenderScenePViewFrameBuilder
private int LoadCell(
RenderSceneQuery scene,
uint cellId,
- bool includeDynamics)
+ bool includeDynamics,
+ bool includeStatics = true)
{
- int staticCount = scene.GetCellStaticCount(cellId);
+ int staticCount = includeStatics
+ ? scene.GetCellStaticCount(cellId)
+ : 0;
int dynamicCount = includeDynamics
? scene.GetCellDynamicCount(cellId)
: 0;
EnsureCapacity(
ref _cell,
checked(staticCount + dynamicCount));
- int count = scene.CopyCellStaticsTo(
- cellId,
- _cell.AsSpan(0, staticCount));
+ int count = 0;
+ if (staticCount > 0)
+ {
+ count = scene.CopyCellStaticsTo(
+ cellId,
+ _cell.AsSpan(0, staticCount));
+ }
if (dynamicCount > 0)
{
count += scene.CopyCellDynamicsTo(
diff --git a/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs b/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs
new file mode 100644
index 00000000..17392765
--- /dev/null
+++ b/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs
@@ -0,0 +1,180 @@
+using System.Numerics;
+using AcDream.App.Rendering.Scene;
+
+namespace AcDream.App.Rendering.Walk;
+
+///
+/// Campaign FW3.2b-2: the production over
+/// the retained scene () and the FW3.1
+/// . Rebuilt facts per frame via
+/// :
+///
+///
+/// - Cell statics — on
+/// demand, one pooled array per distinct cell per frame (a cell can be
+/// visited once by the root flood OR once per admitting look-in portal; the
+/// per-frame cache keeps the copy single).
+/// - Outdoor statics — ONE
+/// sweep bucketed by landscape cell id
+/// ((lb & 0xFFFF0000) | (cellX*8 + cellY + 1) from the record's
+/// world position — the same encoding
+/// computes), EXCLUDING building shells (they draw at their building's own
+/// shell turn, retail CPhysicsPart::Draw(parts, 0) @0x0059f331, not
+/// at the cell's DrawObjCell turn).
+/// - Building shells — the same sweep's IsBuildingShell records
+/// bucketed by Source.BuildingShellAnchorCellId; a
+/// maps to its anchor via its first
+/// non-exit portal's destination (the SAME rule
+/// LandblockLoader used to author the anchor).
+///
+///
+/// The tuple landblock id handed to the classifier is the frame's player
+/// landblock — the packed path's own convention for every
+/// RenderFrameEntityDrawRequest.
+///
+internal sealed class WalkProductionWorldData : IWalkFrameWorldData
+{
+ private readonly WalkBuildingRegistry _buildings;
+ private RenderSceneQuery _scene;
+ private uint _tupleLandblockId;
+
+ private readonly Dictionary _cellCache = new();
+ private readonly Dictionary> _outdoorByCell = new();
+ private readonly Dictionary> _shellsByAnchor = new();
+ private readonly Dictionary _outdoorMaterialized = new();
+ private readonly Dictionary _shellMaterialized = new();
+ private RenderProjectionRecord[] _sweepScratch = new RenderProjectionRecord[1024];
+ private RenderProjectionRecord[] _cellScratch = new RenderProjectionRecord[256];
+
+ internal WalkProductionWorldData(WalkBuildingRegistry buildings)
+ {
+ _buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
+ }
+
+ /// Rebuilds the frame's outdoor/shell buckets and clears the
+ /// per-cell cache. Call once per frame before the driver runs.
+ internal void BeginFrame(RenderSceneQuery scene, uint tupleLandblockId)
+ {
+ _scene = scene;
+ _tupleLandblockId = tupleLandblockId;
+ _cellCache.Clear();
+ _outdoorMaterialized.Clear();
+ _shellMaterialized.Clear();
+ foreach (List bucket in _outdoorByCell.Values)
+ bucket.Clear();
+ foreach (List bucket in _shellsByAnchor.Values)
+ bucket.Clear();
+
+ int count;
+ while (true)
+ {
+ count = _scene.CopyIndexTo(RenderSceneIndex.OutdoorStatic, _sweepScratch);
+ if (count < _sweepScratch.Length)
+ break;
+ _sweepScratch = new RenderProjectionRecord[_sweepScratch.Length * 2];
+ }
+ for (int i = 0; i < count; i++)
+ {
+ ref readonly RenderProjectionRecord record = ref _sweepScratch[i];
+ if (record.EntityPayload.IsBuildingShell)
+ {
+ uint anchor = record.Source.BuildingShellAnchorCellId;
+ if (!_shellsByAnchor.TryGetValue(anchor, out List? shells))
+ _shellsByAnchor[anchor] = shells = new List();
+ shells.Add(record);
+ continue;
+ }
+ uint cellId = LandscapeCellId(record.Transform.Position);
+ if (!_outdoorByCell.TryGetValue(cellId, out List? bucket))
+ _outdoorByCell[cellId] = bucket = new List();
+ bucket.Add(record);
+ }
+ }
+
+ /// The landscape cell owning a world position — retail's
+ /// 24 m cell grid inside the 192 m landblock, the same
+ /// (lb & 0xFFFF0000) | (cellX*8 + cellY + 1) encoding the walk's
+ /// landscape turn emits.
+ internal static uint LandscapeCellId(Vector3 worldPosition)
+ {
+ int lbX = (int)MathF.Floor(worldPosition.X / 192f);
+ int lbY = (int)MathF.Floor(worldPosition.Y / 192f);
+ float localX = worldPosition.X - lbX * 192f;
+ float localY = worldPosition.Y - lbY * 192f;
+ int cellX = Math.Clamp((int)(localX / 24f), 0, 7);
+ int cellY = Math.Clamp((int)(localY / 24f), 0, 7);
+ uint landblock = ((uint)(byte)lbX << 24) | ((uint)(byte)lbY << 16);
+ return landblock | (uint)(cellX * 8 + cellY + 1);
+ }
+
+ public WalkFrameStaticRecords GetCellStatics(uint cellId)
+ {
+ if (_cellCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
+ return cached;
+ int count;
+ while (true)
+ {
+ count = _scene.CopyCellStaticsTo(cellId, _cellScratch);
+ if (count < _cellScratch.Length)
+ break;
+ _cellScratch = new RenderProjectionRecord[_cellScratch.Length * 2];
+ }
+ WalkFrameStaticRecords records = count == 0
+ ? WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId }
+ : new WalkFrameStaticRecords(_cellScratch[..count], _tupleLandblockId);
+ _cellCache[cellId] = records;
+ return records;
+ }
+
+ public WalkFrameStaticRecords GetOutdoorStatics(uint cellId)
+ {
+ if (_outdoorMaterialized.TryGetValue(cellId, out WalkFrameStaticRecords cached))
+ return cached;
+ WalkFrameStaticRecords records =
+ _outdoorByCell.TryGetValue(cellId, out List? bucket)
+ && bucket.Count > 0
+ ? new WalkFrameStaticRecords([.. bucket], _tupleLandblockId)
+ : WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId };
+ _outdoorMaterialized[cellId] = records;
+ return records;
+ }
+
+ public WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building)
+ {
+ uint anchor = AnchorCellId(building);
+ if (anchor == 0)
+ return WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId };
+ if (_shellMaterialized.TryGetValue(anchor, out WalkFrameStaticRecords cached))
+ return cached;
+ WalkFrameStaticRecords records =
+ _shellsByAnchor.TryGetValue(anchor, out List? shells)
+ && shells.Count > 0
+ ? new WalkFrameStaticRecords([.. shells], _tupleLandblockId)
+ : WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId };
+ _shellMaterialized[anchor] = records;
+ return records;
+ }
+
+ /// The building's authored shell anchor: its first non-exit
+ /// portal's destination cell — the SAME rule LandblockLoader used
+ /// when it stamped BuildingShellAnchorCellId on the shell entity.
+ internal static uint AnchorCellId(WalkBuilding building)
+ {
+ foreach (ref readonly WalkBldPortal portal in building.Portals.AsSpan())
+ {
+ if (portal.OtherCellId != 0xFFFFFFFFu)
+ return portal.OtherCellId;
+ }
+ return 0;
+ }
+
+ public Matrix4x4 GetBuildingWorldTransform(WalkBuilding building)
+ {
+ if (!_buildings.TryGetEntry(building, out WalkBuildingFactory.Entry? entry))
+ {
+ throw new InvalidOperationException(
+ $"walk building 0x{building.PositionCellId:X8} has no committed registry entry");
+ }
+ return entry.WorldTransform;
+ }
+}
diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.OrderedStream.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.OrderedStream.cs
index c4e4508e..2c46058e 100644
--- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.OrderedStream.cs
+++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.OrderedStream.cs
@@ -210,6 +210,32 @@ public sealed unsafe partial class WbDrawDispatcher
}
}
+ ///
+ /// Campaign FW3.2b-2: the production frame/encoder pair for
+ /// 's own
+ /// calls (contrast this stage's diagnostic-target callers, which supply
+ /// their own frame/encoder — see 's own
+ /// doc comment). Reads the SAME world-pass scope
+ /// already requires ( /
+ /// _scope.RequireEncoder()) — fails loud rather than handing the
+ /// driver a null pair when the world phase is not bracketing.
+ ///
+ internal (IGpuFrame Frame, IGpuPassEncoder Encoder) RequireWalkSubmission() =>
+ (RequireRhiFrame(), _scope!.RequireEncoder());
+
+ ///
+ /// Campaign FW3.2b-2: the live colour-attachment size, for
+ /// 's viewport (the walk's ray
+ /// caster needs the REAL viewport, not the FW0/FW1 capture-client
+ /// fixture constants — see that class's own doc comment). Null outside
+ /// the world phase (no scope published yet); the caller falls back to
+ /// the fixture constants with a comment in that case rather than
+ /// failing loud, since a missing scope here is a startup-ordering
+ /// timing question, not a misconfiguration.
+ ///
+ internal (int Width, int Height)? WalkAttachmentExtent =>
+ _scope is null ? null : (_scope.AttachmentWidth, _scope.AttachmentHeight);
+
///
/// Submits in walk order through the existing
/// RHI: per-instance-first emission (see the type doc comment), one
diff --git a/tests/AcDream.App.Tests/Rendering/RenderScenePViewFrameProductTests.cs b/tests/AcDream.App.Tests/Rendering/RenderScenePViewFrameProductTests.cs
index b8d560ba..86b08063 100644
--- a/tests/AcDream.App.Tests/Rendering/RenderScenePViewFrameProductTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/RenderScenePViewFrameProductTests.cs
@@ -14,326 +14,33 @@ public sealed class RenderScenePViewFrameProductTests
private static readonly RenderSceneGeneration Generation =
RenderSceneGeneration.FromRaw(1);
- [Fact]
- public void Builder_PreservesPViewRoutesAndExcludesShellsAndWithdrawnOwners()
- {
- using var scene = new ArchRenderScene(Generation);
- RenderProjectionRecord outdoor = Record(
- 0x0100_0000_0000_0001,
- RenderProjectionClass.OutdoorStatic);
- RenderProjectionRecord buildingShell = Record(
- 0x0100_0000_0000_0008,
- RenderProjectionClass.OutdoorStatic,
- isBuildingShell: true);
- RenderProjectionRecord hidden = Record(
- 0x0100_0000_0000_0002,
- RenderProjectionClass.OutdoorStatic,
- flags: RenderProjectionFlags.Hidden
- | RenderProjectionFlags.SpatiallyResident);
- RenderProjectionRecord withdrawn = Record(
- 0x0100_0000_0000_0003,
- RenderProjectionClass.OutdoorStatic,
- flags: RenderProjectionFlags.Draw);
- RenderProjectionRecord shell = Record(
- 0x0200_0000_0000_0004,
- RenderProjectionClass.IndoorCellStatic,
- parentCell: Cell);
- RenderProjectionRecord cellStatic = Record(
- 0x0100_0000_0000_0005,
- RenderProjectionClass.IndoorCellStatic,
- parentCell: Cell);
- RenderProjectionRecord outdoorDynamic = Record(
- 0x0300_0000_0000_0006,
- RenderProjectionClass.LiveDynamicRoot);
- RenderProjectionRecord cellDynamic = Record(
- 0x0300_0000_0000_0007,
- RenderProjectionClass.LiveDynamicRoot,
- parentCell: Cell);
- RenderProjectionRecord[] records =
- [
- outdoor,
- buildingShell,
- hidden,
- withdrawn,
- shell,
- cellStatic,
- outdoorDynamic,
- cellDynamic,
- ];
- var deltas = new RenderProjectionDelta[records.Length];
- for (int index = 0; index < records.Length; index++)
- {
- deltas[index] = RenderProjectionDelta.Register(
- Generation,
- (ulong)index + 1,
- records[index]);
- }
- scene.Apply(deltas);
-
- PortalVisibilityFrame portal = Portal(Cell);
- ClipFrameAssembly clip = FullScreenClip(Cell);
- ViewconeCuller viewcone =
- ViewconeCuller.Build(clip, Matrix4x4.Identity);
- var exchange = new RenderFrameExchange();
- var builder = new RenderScenePViewFrameBuilder();
- RenderSceneDigest digest =
- scene.BuildDigest(new RenderSceneDigestBuffer());
- var input = new RenderScenePViewBuildInput(
- scene.OpenQuery(),
- digest,
- portal,
- clip,
- viewcone,
- [],
- [Cell],
- EmptyCellSource.Instance,
- [],
- RootIsOutdoor: true);
-
- builder.Build(exchange, frameSequence: 1, in input);
- RenderFrameView view = exchange.BorrowLatest(Generation, 1);
- try
- {
- Assert.Equal(
- [outdoor.Id, hidden.Id, buildingShell.Id],
- view.OutdoorStaticCandidates.ToArray()
- .Select(static item => item.Id));
- Assert.Equal(
- [cellStatic.Id],
- view.CellStaticCandidates.ToArray()
- .Select(static item => item.Id));
- Assert.Equal(
- [outdoorDynamic.Id, cellDynamic.Id],
- view.DynamicCandidates.ToArray()
- .Select(static item => item.Id));
- Assert.Equal(6, view.Transforms.Length);
- Assert.Equal(6, view.RouteCandidates.Length);
- Assert.Equal(3, view.RouteRanges.Length);
- RenderFrameCandidateRange outdoorRange = Assert.Single(
- view.RouteRanges.ToArray(),
- range => range.Route
- == RenderFrameCandidateRoute.LandscapeOutdoorStatic);
- Assert.Equal(3, outdoorRange.Count);
- Assert.Contains(
- view.RouteCandidates.Slice(
- outdoorRange.Offset,
- outdoorRange.Count).ToArray(),
- item => item.Id == buildingShell.Id);
- Assert.DoesNotContain(
- view.RouteRanges.ToArray(),
- range => range.Route
- == RenderFrameCandidateRoute.LandscapeBuildingShell);
- Assert.DoesNotContain(
- view.RouteCandidates.ToArray(),
- item => item.Id == withdrawn.Id || item.Id == shell.Id);
- Assert.Equal(
- new RenderFrameDiagnosticCounts(
- OutdoorStaticCandidates: 3,
- CellStaticCandidates: 1,
- DynamicCandidates: 2,
- TransformCount: 6,
- OpaqueClassificationCount: 0,
- AlphaClassificationCount: 0,
- LightSetCount: 0,
- SelectionPartCount: 0,
- RouteCandidateCount: 6,
- EntityCandidateCount: 6,
- MeshPartCount: 6),
- view.DiagnosticCounts);
- Assert.Equal(6, view.EntityCandidates.Length);
- Assert.Equal(6, view.MeshParts.Length);
- Assert.Same(portal, view.PortalFrame);
- Assert.Same(clip, view.ClipAssembly);
- Assert.Equal(digest, view.SourceDigest);
- }
- finally
- {
- exchange.Release(in view);
- }
- }
-
- [Fact]
- public void Builder_OrdersMultiSliceBuildingShellsAfterLookIns()
- {
- using var scene = new ArchRenderScene(Generation);
- RenderProjectionRecord outdoor = Record(
- 0x0100_0000_0000_0101,
- RenderProjectionClass.OutdoorStatic);
- RenderProjectionRecord buildingShell = Record(
- 0x0100_0000_0000_0102,
- RenderProjectionClass.OutdoorStatic,
- isBuildingShell: true,
- buildingShellAnchorCellId: Cell);
- RenderProjectionRecord lookInObject = Record(
- 0x0100_0000_0000_0103,
- RenderProjectionClass.IndoorCellStatic,
- parentCell: Cell);
- scene.Apply(
- [
- RenderProjectionDelta.Register(Generation, 1, outdoor),
- RenderProjectionDelta.Register(Generation, 2, buildingShell),
- RenderProjectionDelta.Register(Generation, 3, lookInObject),
- ]);
-
- PortalVisibilityFrame portal = Portal(Cell);
- PortalVisibilityFrame lookIn = Portal(Cell);
- lookIn.SourceBuildingKey = 2u;
- lookIn.SourceBuildingLandblockId = Cell & 0xFFFF0000u;
- var anchorCell = new LoadedCell
- {
- CellId = Cell,
- BuildingId = 2u,
- };
- ClipFrameAssembly clip = TwoSliceClip(Cell);
- clip.LookInCellToViewSlices[new LookInClipCell(0, Cell)] =
- [clip.CellIdToViewSlices[Cell][0]];
- ViewconeCuller viewcone =
- ViewconeCuller.Build(clip, Matrix4x4.Identity);
- var exchange = new RenderFrameExchange();
- var builder = new RenderScenePViewFrameBuilder();
- RenderSceneDigest digest =
- scene.BuildDigest(new RenderSceneDigestBuffer());
- var input = new RenderScenePViewBuildInput(
- scene.OpenQuery(),
- digest,
- portal,
- clip,
- viewcone,
- [lookIn],
- [Cell],
- new DictionaryCellSource(anchorCell),
- [],
- RootIsOutdoor: true);
-
- builder.Build(exchange, frameSequence: 1, in input);
- RenderFrameView view = exchange.BorrowLatest(Generation, 1);
- try
- {
- Assert.Equal(
- [
- (RenderFrameCandidateRoute.LandscapeOutdoorStatic, 0, 0u),
- (RenderFrameCandidateRoute.LandscapeOutdoorStatic, 1, 0u),
- (RenderFrameCandidateRoute.LookInObject, 0, Cell),
- (RenderFrameCandidateRoute.LandscapeBuildingShell, 0, 0u),
- (RenderFrameCandidateRoute.LandscapeBuildingShell, 1, 0u),
- (RenderFrameCandidateRoute.CellStatic, 0, 0u),
- ],
- view.RouteRanges.ToArray().Select(static range =>
- (range.Route, range.RouteIndex, range.CellId)));
- }
- finally
- {
- exchange.Release(in view);
- }
- }
-
- [Fact]
- public void Builder_InterleavesEachLookInWithItsOwnPackedBuildingShellRoutes()
- {
- const uint secondCell = 0xA9B40180u;
- using var scene = new ArchRenderScene(Generation);
- RenderProjectionRecord firstShell = Record(
- 0x0100_0000_0000_0201,
- RenderProjectionClass.OutdoorStatic,
- isBuildingShell: true,
- buildingShellAnchorCellId: Cell);
- RenderProjectionRecord secondShell = Record(
- 0x0100_0000_0000_0202,
- RenderProjectionClass.OutdoorStatic,
- isBuildingShell: true,
- buildingShellAnchorCellId: secondCell);
- RenderProjectionRecord firstObject = Record(
- 0x0100_0000_0000_0203,
- RenderProjectionClass.IndoorCellStatic,
- parentCell: Cell);
- RenderProjectionRecord secondObject = Record(
- 0x0100_0000_0000_0204,
- RenderProjectionClass.IndoorCellStatic,
- parentCell: secondCell);
- scene.Apply(
- [
- RenderProjectionDelta.Register(Generation, 1, firstShell),
- RenderProjectionDelta.Register(Generation, 2, secondShell),
- RenderProjectionDelta.Register(Generation, 3, firstObject),
- RenderProjectionDelta.Register(Generation, 4, secondObject),
- ]);
-
- PortalVisibilityFrame portal = Portal(Cell);
- PortalVisibilityFrame firstLookIn = Portal(Cell);
- firstLookIn.SourceBuildingKey = 2u;
- firstLookIn.SourceBuildingLandblockId = Cell & 0xFFFF0000u;
- PortalVisibilityFrame secondLookIn = Portal(secondCell);
- secondLookIn.SourceBuildingKey = 3u;
- secondLookIn.SourceBuildingLandblockId = secondCell & 0xFFFF0000u;
- var firstAnchor = new LoadedCell
- {
- CellId = Cell,
- BuildingId = 2u,
- };
- var secondAnchor = new LoadedCell
- {
- CellId = secondCell,
- BuildingId = 3u,
- };
- ClipFrameAssembly clip = TwoSliceClip(Cell);
- clip.LookInCellToViewSlices[new LookInClipCell(0, Cell)] =
- [clip.CellIdToViewSlices[Cell][0]];
- clip.LookInCellToViewSlices[new LookInClipCell(1, secondCell)] =
- [clip.CellIdToViewSlices[Cell][0]];
- ViewconeCuller viewcone =
- ViewconeCuller.Build(clip, Matrix4x4.Identity);
- var exchange = new RenderFrameExchange();
- var builder = new RenderScenePViewFrameBuilder();
- RenderSceneDigest digest =
- scene.BuildDigest(new RenderSceneDigestBuffer());
- var input = new RenderScenePViewBuildInput(
- scene.OpenQuery(),
- digest,
- portal,
- clip,
- viewcone,
- [firstLookIn, secondLookIn],
- [Cell],
- new DictionaryCellSource(firstAnchor, secondAnchor),
- [],
- RootIsOutdoor: true);
-
- builder.Build(exchange, frameSequence: 1, in input);
- RenderFrameView view = exchange.BorrowLatest(Generation, 1);
- try
- {
- Assert.Equal(
- [
- (RenderFrameCandidateRoute.LookInObject, 0, Cell),
- (RenderFrameCandidateRoute.LandscapeBuildingShell, 0, 0u),
- (RenderFrameCandidateRoute.LandscapeBuildingShell, 1, 0u),
- (RenderFrameCandidateRoute.LookInObject, 1, secondCell),
- (RenderFrameCandidateRoute.LandscapeBuildingShell, 2, 0u),
- (RenderFrameCandidateRoute.LandscapeBuildingShell, 3, 0u),
- ],
- view.RouteRanges.ToArray()
- .Where(static range =>
- range.Route is RenderFrameCandidateRoute.LookInObject
- or RenderFrameCandidateRoute.LandscapeBuildingShell)
- .Select(static range =>
- (range.Route, range.RouteIndex, range.CellId)));
- }
- finally
- {
- exchange.Release(in view);
- }
- }
+ // Campaign FW3.2b-2 (adjudicated 2026-08-30): Builder_PreservesPViewRoutesAndExcludesShellsAndWithdrawnOwners,
+ // Builder_OrdersMultiSliceBuildingShellsAfterLookIns, and
+ // Builder_InterleavesEachLookInWithItsOwnPackedBuildingShellRoutes are
+ // DELETED — all three asserted route-range interleaving for
+ // LandscapeOutdoorStatic/LandscapeBuildingShell/CellStatic, which
+ // RenderScenePViewFrameBuilder no longer emits at all (WalkFrameDriver
+ // draws every outdoor static, building shell, and cell static — look-in
+ // included — directly through OrderedDrawStream; plan §FW3
+ // "FW3.2b-2 — the production rooting", item 3). There is no successor
+ // assertion to write: the routes themselves are retired, not relocated.
[Fact]
public void Builder_ReusesOrderedIndicesWhileRefreshingDirtyRecords()
{
+ // Campaign FW3.2b-2 (adjudicated 2026-08-30): re-expressed against
+ // the DYNAMIC index (LiveDynamicRoot / DynamicCandidates) instead of
+ // the now-dead OutdoorStatic index — LoadSceneIndices' dirty-record
+ // refresh + sort-key reorder mechanism this test proves is SHARED
+ // between the two indices; the OutdoorStatic side was deleted along
+ // with BuildOutdoorRoutes (its only reader).
using var scene = new ArchRenderScene(Generation);
RenderProjectionRecord later = Record(
0x0100_0000_0000_0012,
- RenderProjectionClass.OutdoorStatic);
+ RenderProjectionClass.LiveDynamicRoot);
RenderProjectionRecord earlier = Record(
0x0100_0000_0000_0011,
- RenderProjectionClass.OutdoorStatic);
+ RenderProjectionClass.LiveDynamicRoot);
scene.Apply(
[
RenderProjectionDelta.Register(Generation, 1, later),
@@ -353,7 +60,7 @@ public sealed class RenderScenePViewFrameProductTests
{
Assert.Equal(
[earlier.Id, later.Id],
- first.OutdoorStaticCandidates.ToArray()
+ first.DynamicCandidates.ToArray()
.Select(static record => record.Id));
}
finally
@@ -401,7 +108,7 @@ public sealed class RenderScenePViewFrameProductTests
projected.Transform.LocalToWorld);
Assert.Equal(
[earlier.Id, later.Id],
- second.OutdoorStaticCandidates.ToArray()
+ second.DynamicCandidates.ToArray()
.Select(static record => record.Id));
}
finally
@@ -431,7 +138,7 @@ public sealed class RenderScenePViewFrameProductTests
{
Assert.Equal(
[later.Id, earlier.Id],
- third.OutdoorStaticCandidates.ToArray()
+ third.DynamicCandidates.ToArray()
.Select(static record => record.Id));
}
finally
@@ -463,14 +170,18 @@ public sealed class RenderScenePViewFrameProductTests
[Fact]
public void Controller_MatchesCurrentPViewThenNamesAWithdrawnCandidate()
{
- WorldEntity outdoor = Entity(10, serverGuid: 0, parentCell: null);
- WorldEntity cellStatic = Entity(11, serverGuid: 0, parentCell: Cell);
+ // Campaign FW3.2b-2 (adjudicated 2026-08-30): no static entities here
+ // — LandscapeOutdoorStatic/CellStatic no longer flow through the
+ // packed product at all (WalkFrameDriver owns every static now), so
+ // a static observed on the LEGACY oracle side with nothing on the
+ // packed side would report a permanent mismatch unrelated to what
+ // this test is proving (withdrawal detection on the SURVIVING
+ // DynamicLast route).
WorldEntity outdoorDynamic =
Entity(1_000_012, 0x8000_0012, parentCell: null);
WorldEntity cellDynamic =
Entity(1_000_013, 0x8000_0013, parentCell: Cell);
- WorldEntity[] entities =
- [outdoor, cellStatic, outdoorDynamic, cellDynamic];
+ WorldEntity[] entities = [outdoorDynamic, cellDynamic];
var oracle = new CurrentRenderSceneOracle();
var partition = new InteriorEntityPartition.Result();
InteriorEntityPartition.Partition(
@@ -479,16 +190,6 @@ public sealed class RenderScenePViewFrameProductTests
[Entry(entities)],
oracle);
oracle.BeginPViewFrame();
- oracle.ObservePViewBucket(
- CurrentRenderPViewRoute.LandscapeOutdoorStatic,
- 0,
- 0,
- [outdoor]);
- oracle.ObservePViewBucket(
- CurrentRenderPViewRoute.CellStatic,
- 0,
- 0,
- [cellStatic]);
oracle.ObservePViewBucket(
CurrentRenderPViewRoute.DynamicLast,
0,
@@ -496,14 +197,6 @@ public sealed class RenderScenePViewFrameProductTests
[outdoorDynamic, cellDynamic]);
oracle.CompletePViewFrame();
oracle.BeginDispatcherFrame();
- oracle.ObserveDispatcherDraw(
- WbDrawDispatcher.EntitySet.All,
- entitiesWalked: 1,
- [(outdoor, 0, Landblock)]);
- oracle.ObserveDispatcherDraw(
- WbDrawDispatcher.EntitySet.All,
- entitiesWalked: 1,
- [(cellStatic, 0, Landblock)]);
oracle.ObserveDispatcherDraw(
WbDrawDispatcher.EntitySet.All,
entitiesWalked: 2,
@@ -544,15 +237,15 @@ public sealed class RenderScenePViewFrameProductTests
Assert.True(matched.Enabled);
Assert.Equal(1uL, matched.ComparisonCount);
Assert.Equal(1uL, matched.SuccessfulComparisonCount);
- Assert.Equal(4, matched.ExpectedCandidateCount);
- Assert.Equal(4, matched.ActualCandidateCount);
+ Assert.Equal(2, matched.ExpectedCandidateCount);
+ Assert.Equal(2, matched.ActualCandidateCount);
Assert.Equal(0, matched.MismatchCount);
Assert.Null(matched.FirstMismatch);
Assert.Equal(matched.ExpectedDigest, matched.ActualDigest);
Assert.Equal(1uL, matched.PackedInputComparisonCount);
Assert.Equal(1uL, matched.PackedInputSuccessfulComparisonCount);
- Assert.Equal(4, matched.PackedInputExpectedCount);
- Assert.Equal(4, matched.PackedInputActualCount);
+ Assert.Equal(2, matched.PackedInputExpectedCount);
+ Assert.Equal(2, matched.PackedInputActualCount);
Assert.Equal(0, matched.PackedInputMismatchCount);
Assert.Null(matched.PackedInputFirstMismatch);
Assert.Equal(
@@ -581,10 +274,10 @@ public sealed class RenderScenePViewFrameProductTests
Assert.Equal(2uL, mismatch.ComparisonCount);
Assert.Equal(1uL, mismatch.SuccessfulComparisonCount);
Assert.Equal(1, mismatch.MismatchCount);
- Assert.Contains("field=count expected=4 actual=3", mismatch.FirstMismatch);
+ Assert.Contains("field=count expected=2 actual=1", mismatch.FirstMismatch);
Assert.Equal(1, mismatch.PackedInputMismatchCount);
Assert.Contains(
- "field=count expected=4 actual=3",
+ "field=count expected=2 actual=1",
mismatch.PackedInputFirstMismatch);
Assert.Equal(2, logs.Count);
}
diff --git a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
index 36628dbf..85e44e27 100644
--- a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
@@ -196,10 +196,15 @@ public sealed class RetailPViewPassExecutorTests
[Fact]
public void DrawInside_production_product_supplies_the_ordered_entity_routes()
{
+ // Campaign FW3.2b-2: no cell-static entity here (adjudicated
+ // 2026-08-30) — CellStatic no longer flows through the packed
+ // product at all (WalkFrameDriver owns every cell static now, via
+ // OrderedDrawStream, not this route); a static entity registered on
+ // the LEGACY side with nothing on the packed side would report a
+ // permanent MismatchCount, which is not what this test is proving.
+ // The route-consumption-order assertion below still exercises the
+ // one route that DID survive the cutover (DynamicLast).
LoadedCell interior = InteriorWithExit(0xA9B40100u);
- WorldEntity cellStatic = Entity(
- 30u,
- parentCellId: interior.CellId);
WorldEntity dynamic = Entity(
31u,
serverGuid: 0x80000031u,
@@ -208,17 +213,6 @@ public sealed class RetailPViewPassExecutorTests
var oracle = new CurrentRenderSceneOracle();
using var shadow = new RenderSceneShadowRuntime(
RenderSceneGeneration.FromRaw(1));
- RenderProjectionRecord staticRecord =
- RenderProjectionRecordFactory.ProjectEntity(
- StaticRenderProjectionJournal.StaticEntityId(
- landblockId,
- cellStatic.Id),
- RenderProjectionClass.IndoorCellStatic,
- RenderOwnerIncarnation.FromRaw(cellStatic.Id),
- landblockId,
- interior.CellId,
- cellStatic,
- spatiallyVisible: true);
RenderProjectionRecord dynamicRecord =
RenderProjectionRecordFactory.ProjectEntity(
LiveRenderProjectionJournal.ProjectionId(dynamic.Id),
@@ -228,7 +222,6 @@ public sealed class RetailPViewPassExecutorTests
interior.CellId,
dynamic,
spatiallyVisible: true);
- shadow.Journal.Register(in staticRecord);
shadow.Journal.Register(in dynamicRecord);
shadow.DrainUpdateBoundary();
var product = new RenderScenePViewFrameProductController(
@@ -238,13 +231,12 @@ public sealed class RetailPViewPassExecutorTests
using var executor = new RecordingExecutor();
renderer.DrawInside(
- Frame(interior, [cellStatic, dynamic]),
+ Frame(interior, [dynamic]),
executor);
AssertAppearsInOrder(
string.Join('|', executor.Operations),
"entity-frame-begin",
- "entity-route:CellStatic:0:00000000",
"entity-route:DynamicLast:0:00000000",
"entity-frame-complete");
Assert.Equal(0, product.Snapshot.MismatchCount);