acdream/docs/plans/2026-08-30-campaign-fw-frame-walk.md
Erik 5d51b37ad8 docs: Campaign FW - FW3.2b-2 static-cutover status in the plan
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 15:45:17 +02:00

29 KiB
Raw Blame History

Campaign FW — the retail frame walk (implementation plan)

Status: ACTIVE (opened 2026-08-30; spec approved by owner 2026-08-30). Spec (approved): docs/superpowers/specs/2026-08-30-campaign-w-retail-frame-walk-design.md — note: the campaign letter is FW (frame walk); the spec predates the rename (the roadmap already owns "Phase W" for the historical Unified Cell Graph program). Branch: claude/campaign-w-retail-frame-walk off main e102fb36. Predecessor: Campaign PV, halted 2026-08-30 (docs/plans/2026-08-30-pview-visibility-campaign.md); its back half is quarantined on claude/cathedral-rendering-debug-47a417 and nothing merges from it.

One-sentence goal: retail owns every frame decision (rooting, portal views, cone gating, landscape walk, buildings, stage order, depth semantics, degrade, particles, camera); modern code owns only pixel-neutral GPU mechanics; the ~4.7K-line accumulated visibility layer and its patch apparatus are deleted at their stages.


Binding rules (all stages, binding on subagents)

  1. The ownership line is the campaign invariant. If a modern component makes a decision retail did not make, that is a campaign bug regardless of pixels. Every ported function cites its named retail symbol.
  2. Grep named first. docs/research/named-retail/acclient_2013_pseudo_c.txt by class::method before any fresh decompilation; pseudocode notes in docs/research/ before C# (the mandatory workflow).
  3. Fail loud, never silently drop. The PV3 post-mortem rule: any admission/seeding/ordering filter that rejects work must log the rejection under a diagnostic flag or fall open — silent drops produced this week's hollow-world regressions.
  4. The transition checklist is a standing self-run gate. Every candidate build gets, before the owner sees it: portal-in arrival frames, a Holtburg house exit, a teleport-to-Holtburg stopwatch, and a steady-state fps read (drive.ps1 + screenshots). No terrace-only verification, ever again.
  5. No long-lived dual path. Campaign V precedent: a stage that cuts over deletes the code it replaces in the same stage. Short-lived scaffolding inside one stage is fine; a toggle that survives a stage boundary is not.
  6. Register discipline. Deviations add their row in the same commit; ported mechanisms delete rows in the same commit (docs/architecture/retail-divergence-register.md).
  7. Suites green at every stage close (hermetic filter Lane!=InstalledDat&Lane!=Linux&Lane!=Manual&Lane!=Timing&Lane!=Live&Lane!=PreparedPackage), plus the stage's own gates. Sonnet implements; Opus reviews stage boundaries (architectural + retail-faithfulness lenses).

Verified seam map (recon 2026-08-30; cite these, do not re-derive)

Frame chain today: GameWindow.OnRender (GameWindow.cs:1785/1809) → GameFrameGraphSlot.RenderRenderFrameOrchestrator.Render (RenderFrameOrchestrator.cs:206/226) → VulkanWorldScenePhase (VulkanCompositionFramePhases.cs:113, owns the backbuffer pass) → WorldSceneRenderer.Render (WorldSceneRenderer.cs:128) → RetailPViewRenderer.DrawInside (RetailPViewRenderer.cs:112), which builds PortalVisibilityFrame internally (line 122) and drives RetailPViewPassExecutor through the packed route contract (RenderFrameCandidateRoute: LandscapeOutdoorStatic → LandscapeBuildingShell → LookInObject → LandscapeOutsideDynamic → CellStatic → DynamicLast; WbDrawDispatcher.PackedOracle.cs:108/171 enforces in-order consumption).

What the walk-order submitter REPLACES: GroupKey material bucketing (ClassifyBatches WbDrawDispatcher.cs:3292, ClassifyPackedBatches PackedOracle.cs:610, _groups/_packedGroups), PartitionInstanceGroups (:2232), and the two distance sorts (CompareOpaqueSubmissionOrder :2865, CompareTransparentSubmissionOrder :2871). Note: the deferred-alpha path (DeferTransparentGroups :2619 → RetailAlphaQueue) already reconstructs draw-local order per instance — the transparent side is half-way to walk-order today and is the SSBO-layout pattern to follow.

What is REUSED unchanged: GlobalMeshBuffer arena (GlobalMeshBuffer.cs:84), WbMeshAdapter/ObjectMeshManager mesh prep + self-heal (WbMeshAdapter.cs:328/435), TextureCache GpuTextureSlot resolution (WbDrawDispatcher.cs:3421), the ring-section SSBO writers + shared WorldTransformFrameArena (Rhi.cs:913/945), MeshPipelineSet (Rhi.cs:49), and the terminal MDI recorder DrawIndirectRangeRhi (Rhi.cs:798).

Caveats that shaped the stages: three parallel classifiers exist (classic, packed, directional-shadow — WbDrawDispatcher.DirectionalShadows.cs:1021) and must agree until each is cut over; EntityClassificationCache and PackedProjectionClassificationCache assume group-bucketed submission and retire with their routes; StageImmediateGroup lays per-instance SSBOs by group-order cursor, so the walk submitter re-derives layout from stream order (start per-instance like deferred alpha; merge adjacent runs after correctness).

Visibility consumers to re-point at the walk output (the complete production list — radar, audio, and picking do NOT consume visibility):

  • Particles: WorldSceneRenderer.cs:263ParticleVisibilityControllerParticleSystem.ApplyRetailView (Core/Vfx/ParticleSystem.cs:313), fed outdoors by TerrainModernRenderer.VisibleCellIds.
  • Lights: WorldSceneRenderer.cs:264RuntimeWorldFrameEnvironmentPreparation (WorldRenderFrameBuilder.cs:543) → LightManager.BuildPointLightSnapshot (Core/Lighting/LightManager.cs:284).
  • EnvCell prepare filter: RetailPViewPassExecutor.cs:276EnvCellRenderer.PrepareRenderBatches (EnvCellRenderer.cs:456).
  • Entity routes: RenderScenePViewFrameProduct.cs:1330-1587.
  • Gated diagnostics only beyond that.

Streaming atomicity (what already holds vs what FW adds): per-cell publication is ALREADY atomic — one dictionary swap of a complete immutable EnvCellLandblock (EnvCellRenderer.CommitPublication:347); outdoor terrain commits at LandblockRenderPublisher.AdvanceBeginOne:267. The real partial states are (a) landblock-stage terrain-before-interiors across yielded publication stages (LandblockPresentationPipeline.cs:711/757 — deliberate walking pop-in, hidden by the reveal hold during portal/login), and (b) committed cells drawing before every contained static mesh is resident (skip-if-missing, EnvCellRenderer.cs:720). FW keeps both as EXPLICIT, register-rowed adaptations; the walk consumes only the committed registry (CellVisibility.TryGetCell — production role is the registry; its BFS is already dead code).

The patch apparatus to delete (exact sites):

  • ShellDrawLiftZ +0.02 lift, three consumers: PortalVisibilityBuilder.cs:353 (const + Build's drawLiftZ), RetailPViewPassExecutor.cs:697 (depth fans), LandblockBuildFactory.cs:523 (baked into draw cellTransform).
  • Punch stencils (#117 adaptation): PortalDepthMaskRenderer.Rhi.cs:59-125.
  • Look-in seeding heuristics: PortalVisibilityBuilder.BuildFromExterior:713-923
    • callers MergeNearbyBuildingFloods (RetailPViewRenderer.cs:411), BuildInteriorRootLookIns (:471), DrawBuildingLookIns (:600-860), and the barrier math (:515-559).
  • The InViewCells side-channel (RetailPViewRenderer.cs:2187) once consumers read the walk output.

Camera facts: RetailChaseCamera.csDistance default 2.61 m, clamps DistanceMin=2/DistanceMax=40 (:110-113); collision probe + slope align + chase already default-on retail behaviors (Core/Rendering/CameraDiagnostics.cs:28-50). Retail envelope (from the d71f5ef1 diff, which lives ONLY on the quarantined branch — this branch has no AD-116 row): CameraSet::Farther @0x00458890 scales the viewer offset multiplicatively and REFUSES the write once |offset.y| ≥ 10 (gates |x|<10, |y|<10, z<450, z>-1.8); the standard arc saturates near offset (0, 10, +8.33) ≈ a 13.0 m boom. CameraSet::Raise @0x00457980 rotates at preserved length. TS-56 records the non-retail adjustment scalars (CameraAdjustmentSpeed=40/s etc.).

Retail anchors already established: SmartBox::RenderNormalMode @0x00453aa0 (frame rooting), PView::DrawCells @0x005A4840, LScape::draw @0x00506330, LScape::grab_visible_cells @0x00504EC0, DrawBuilding pre-punch flush @0x0059F2A0, ShouldDrawParticles @0x0050FE60, the untextured skip in D3DPolyRender::DrawMesh @0x0059d4a0, ACRender::polyClipFinish (two call sites: PView::GetClip + debug), Render::viewconeCheck.


Stages

FW0 — the walk oracle (tooling + fixtures)

STATUS: COMPLETE 2026-08-30. Ten trace fixtures + README (docs/research/2026-08-30-fw-walk-oracle/, commit 71b11817), the replay helper + 19 fixture goldens (9f4c0f95), and the port-ready pseudocode model + full decomp appendix (docs/research/2026-08-30-fw-walk-pseudocode.md, a6885aa2). Findings that reshaped the campaign: the frame roots at the CAMERA's cell; outdoor frames draw the #456 far building every frame and bury it by depth (no hiding mechanism exists to port); retail DOES draw depth-only invisible portal-polygon panels (DrawPortalPolyInternal — punch far-Z / seal own-depth) — the AD-117 stamps re-invented a real mechanism at the wrong site. Known gap: the first capture round did not record the camera pose; exact FW1 replay needs pose-stamped traces. A prepared template (tools/walk-oracle/fw0-capture-pose.cdb.template) makes the re-capture a short owner-assisted session; scheduled for when FW1's walk is ready to gate (structural conformance proceeds meanwhile on the existing traces).

Goal: retail's actual per-frame draw walk, captured and replayable, BEFORE any port is judged.

  • Build the cdb capture harness (tools/walk-oracle/): breakpoint script logging, per frame, the sequence of cell/building draw entries with stage boundaries — hook PView::DrawCells, CEnvCell draw, CLandBlock/LScape::draw, DrawBuilding; dump cell ids (this offsets verified with one-shot dt first). cdb safety rules from the PV campaign are binding: never inline -c attach to a client that matters, every j branch ends in gc, qd threshold auto-detach, watch hit-rate lag.
  • Capture positions (owner assists — retail sessions): Sanctuary terrace center + terrace EDGE (normal camera — the #456 acceptance pose), cathedral portal-in arrival, a Holtburg doorway (the flap scene), a Holtburg street + house exit, one deep dungeon corridor. Record camera pose per capture.
  • Also capture the same positions in acdream (world state: cell registry contents) so W1 replays are apples-to-apples.
  • Pin the port function list as a research note with pseudocode stubs: docs/research/2026-08-30-fw-walk-pseudocode.md (grow it through FW1).
  • Deliverable/gate: trace fixtures checked into docs/research/2026-08-30-fw-walk-oracle/, replayable by a test helper; harness scripts committed; no production code touched.

FW1 — RetailFrameWalk + PViewSet (pure CPU, conformant)

Goal: the ported walk as a standalone module that reproduces the oracle traces exactly.

  • New src/AcDream.App/Rendering/Walk/: RetailFrameWalk (rooting per SmartBox::RenderNormalMode; EnvCell recursion per PView::DrawCells; landscape walk per LScape::draw/grab_visible_cells; building look-ins per DrawBuilding + ConstructView(CBldPortal)), PViewSet (ConstructView/set_view/PView::GetClip — the chain polygon is the only CPU-clipped geometry; one view per chain, never merged), IWalkWorldView (the read seam: committed cells via the registry, buildings via BuildingRegistry, camera pose).
  • Output: an ordered walk event stream (cell id, view, stage) matching the oracle trace format, plus the frame's visible-cell set.
  • Pseudocode first (workflow step 3), C# port line-by-line (step 4), named symbols in comments.
  • Gates: oracle-trace replay tests pass position-for-position; synthetic-world unit tests for rooting/recursion/cone edge cases; the existing replay fixture suite (Issue95/113/120/124/127/130/131/176-177/ 181, SanctuaryPortalSeam, HouseExitWalkReplay, TowerAscentReplay, CornerFloodReplay) re-expressed against the walk — behavior differences vs the old builder are adjudicated against the ORACLE, not against the old builder. No production wiring; hermetic suites green.

FW1 STATUS (2026-08-30, @77f5342b): NINE of the ten pose-stamped fixtures reproduce retail frame-exactly (foundry-deep all 39 frames, doorway-still, street-outdoor, terrace-center, terrace-edge — the #456 acceptance pose — cathedral-arrival, holtburg-walkout, -transitions, -walkabout); foundry-entry is exact through F66 with the F67F79 standing segment parked on ONE live number (building 0036's root-plane viewpoint — probe tools/walk-oracle/fw1-f67-viewpoint-probe.cdb, goal-sanctioned retail-session stop filed with the user). Load-bearing adjudications, all decomp-cited: the two-arm get_degrade threshold rule (ideal→max at the live deg_mul≈+0.99), deg_mul's DYNAMIC swing under capture load (doorway-still pins mul=0 — an environment pin like the viewport), znear=0.1 confirmed, and the Ghidra-arbitrated portal walker truth table (BN FPU pseudo-C mis-renders branch sense — three separate misreads this stage; Ghidra first, always). Gate amendment: the old-replay-fixture re-expression is retired as an FW1 gate — the ten traces are direct retail evidence and strictly supersede fixtures that encode the OLD builder's behavior; the old suite's scenario classes (doorway flap, dungeon seams, tower ascent, corner flood) are covered by the traces and re-verified live at the FW3/FW4 connected gates, where the old fixtures retire with the old builder. Production classes: RetailFrameWalk, WalkPView (the PViewSet role), WalkBuildingPortals, WalkLandscape, WalkVisibilityMath, WalkScreenClip, WalkCopyView under src/AcDream.App/Rendering/Walk/.

FW2 — OrderedDrawStream + OrderPreservingSubmitter

Goal: walk-order submission through the existing RHI, proven pixel-equivalent on static content.

  • OrderedDrawStream: append-only (mesh ref, transform, material state, stage, cell) commands in walk order. OrderPreservingSubmitter: binds GlobalMeshBuffer once, reuses ring-section writers + MeshPipelineSet
    • DrawIndirectRangeRhi; emits per-instance first (the deferred-alpha SSBO pattern), then merges ADJACENT same-state commands into MDI runs — a merge across a state or stage boundary is forbidden by construction (assert it).
  • Depth semantics: port retail's depth compare/write configuration for the world passes verbatim in this stage (current set uses Less — verify against retail's raster state and document the finding either way).
  • Diagnostic harness: a dev scene (offscreen target) drawn twice — old path vs walk path — framebuffer-compared (the Slice-7 comparison pattern, with the pixel-diff repeat floor from feedback_pixel_diff_needs_repeat_floor).
  • Gates: submitter unit tests (order preservation, merge legality); framebuffer equivalence on static-content scenes; hermetic suites green.

FW2 STATUS (2026-08-30, @e65644cb): OrderedDrawStream + WbDrawDispatcher.OrderedStream (the submitter partial — per-instance- first emission, pure-CPU merge-run builder, fail-loud stage/pipeline/ cull boundary validation) and WorldDepthContract are landed with 27 RecordingGpuDevice gates; hermetic 6,714/0. Depth verified verbatim from the decomp (zfuncVal 0x2 = D3DCMP_LESS via SetDepthBufferMode @0x005a2d10; LESSEQUAL is GameSky-local). Gate adjudication: the offscreen pixel A/B rides FW3's cutover toggle — a walk-driven scene first exists there, IGpuRenderTarget has no readback verb today, and opaque content under Less is order-independent, so the RecordingGpuDevice order/merge referees are FW2's automated gate and the pixel proof joins FW3's (offline pixel gate + owner visual gate). Deferred to FW3 and recorded in the code: the building-detail overlay replay, portal-punch emission, and the _drawCullModes/alpha-scope sequencing constraint.

FW3 — static world cutover + THE perf checkpoint

Goal: terrain, EnvCell shells, and buildings draw via the walk in production; the statics gather path is deleted; perf is measured before deeper commitment.

  • Root the production frame at the walk: WorldSceneRenderer drives RetailFrameWalk + submitter for the static routes (LandscapeOutdoorStatic, LandscapeBuildingShell, CellStatic); dynamic routes (LookInObject, LandscapeOutsideDynamic, DynamicLast) stay on the packed path within this stage only (the route-contract split makes the seam clean).
  • Retire the three ShellDrawLiftZ sites in this stage — walk order + retail depth semantics replace the tie-break; prove it at the #130 doorway-strip scene before deleting, and delete the register row in the same commit.
  • Terrain/sky/weather draw at their walk turns (the stage interleave from RetailPViewPassExecutor's ported knowledge moves inward).
  • Perf checkpoint (the spec's stop rule): dense Arwic uncapped, Release, exact binary: CPU/GPU p50/p99 + fps vs the current production profile (519.7 fps / 1.869 / 1.096 ms baseline). Within 20% → proceed; worse → STOP, owner decision with numbers on the table.
  • Gates: transition checklist self-run; hermetic suites; owner visual gate (statics look right indoors + outdoors, no seam strip at the doorway, no z-fighting at shells); perf numbers recorded in this plan.

FW3 SLICE PLAN (2026-08-30, from the cutover-surface survey):

  • FW3.1 — production walk world data. Materialize the walk's world model at landblock commit time, registry-owned, through the LEGAL IDatReaderWriter seam (never raw DatCollectionRuntimeDatAccessArchitectureTests): (a) WalkCell fields onto/beside LoadedCell (portal side/exact from the RAW Flags already preserved on CellPortalInfo; plane from the existing ClipPlanes parallel list; the 0xFFFF→0xFFFFFFFF exit-sentinel widening); (b) a production WalkBuilding build in LandblockBuildFactory/BuildingLoader — the BIGGEST gap: today's Building drops ModelId/Frame/portal flags/stab lists at load; (c) the walk landscape assembly (retail z-slabs from the height table, the SideCellCount LOD pyramid, CellBuildings[], viewer-centred grid) fed from landblock residency; (d) the production frame context: camera/rays/CyPlane/ObjectToClip derived from the LIVE WorldCameraFrame + real viewport (the capture client's 1024×720 constants are fixture pins, not production values). Conformance: the ten oracle fixtures re-run against the PRODUCTION builders (same DATs → identical walk output as the test adapter).
  • FW3.2 — walk-driven static routes. WorldSceneRenderer drives RetailFrameWalkOrderedDrawStream (Terrain/BuildingShell/CellStatic/LookInStatic/PortalPunch stages) → SubmitOrderedStream for static content; the old builder STOPS EMITTING LandscapeOutdoorStatic/LandscapeBuildingShell/CellStatic ranges (the CompletePackedProductionFrame count assertion forbids emit-but-skip); dynamic routes (LookInObject, LandscapeOutsideDynamic, DynamicLast) stay packed — which requires ClipFrameAssembler + ViewconeCuller to SURVIVE this stage (their slices key the dynamic routes). Walk-path SELECTION publication (static picking today rides publishSelection: true in the packed classifier — an unlisted consumer; the walk submitter must feed RetailSelectionScene or statics go un-clickable). Portal-punch emission via PortalDepthMaskRenderer at walk turns; building-detail overlay replay; sky/weather move from per-slice to the walk's 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 18 and 1011 (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); ClearInteriorDepthIWorldPassScope.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.2b-2 STATUS (2026-08-30, @4918677b): THE STATIC CUTOVER IS COMMITTED. The walk drives every production static draw (DrawWalkDrivenStatics + the dynamics-remainder phases); the three static routes are deleted from the product builder; particle owners re-source from the walk's per-cell-turn visited sets (retail's own gate — ShouldDrawParticles @0x0050FE60). Landed evidence: the walk shadow probe ran 11k+ live frames faultless with walkCells ⊂ oldCells every frame. The old static path survives ONLY behind !walkActive for the executor-fake test path (15 retail-ordering regression tests; unreachable by production) — its deletion is FW4 scope. Transitional risks recorded in code: two-pass walk cost (FW3.4 measures), interior slice-count reconciliation between the old clip assembly and the walk's exit-view survival, outdoor merged-flood punch coverage now riding the walk's building-BSP punches (owner visual gate verifies). Suites: hermetic 6,750/0; Walk 201/1; InstalledDat conformance 40/1 untouched.

  • 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 #130 proof (UnliftedGate_LeavesTheStripAtTheDrawnTopEdge is explicitly written to fail-by-gap until the lift is gone), delete AP-32 in the same commit.
  • FW3.4 — perf checkpoint + gates. BASELINE CORRECTION: the 519.7 fps / 1.869 / 1.096 ms numbers are the Aerlinthe ordinary-production profile, NOT dense Arwic. Before the cutover lands, capture a fresh dense-Arwic pre-cutover baseline with the same harness (tools/run-connected-r6-soak.ps1 -Uncapped -DenseTown), and apply the ±20% stop rule to THAT pair. Transition checklist: the lifecycle gate + dense-town soak exist; a Holtburg house-exit stop and a teleport stopwatch metric must be ADDED to the lifecycle route (drive.ps1 from the spec does not exist — the checklist runs on the existing gate scripts).

FW4 — entities, particles, dynamics into walk turns; delete the apparatus

Goal: everything draws at its walk turn; the patch apparatus and the old visibility layer are gone.

  • Entities gate per view via Render::viewconeCheck at their cell's walk turn; dynamics-last and alpha interleave per the walk's stages (RetailAlphaQueue becomes the stream's alpha stage or is absorbed — decide at implementation with the retail stage list as the arbiter); particles draw at their owner cell's turn (ShouldDrawParticles), unclipped.
  • Re-point the consumers at the walk's visible-cell output: particles (ParticleVisibilityController), lights (BuildPointLightSnapshot filter), EnvCell prepare filter. Directional-shadow caster selection consumes the same set.
  • DELETE in this stage (registry rows updated in the same commits): look-in seeding + callers + barrier math; punch stencils (PortalDepthMaskRenderer #117); the InViewCells side-channel; PortalVisibilityBuilder's production role; RetailPViewRenderer / RetailPViewPassExecutor shells (their stage knowledge now lives in the walk); the packed route machinery + PackedOracle + both classification caches; CellVisibility's dead BFS.
  • Gates: the full re-expressed replay suite; transition checklist; hermetic suites; the owner visual matrix — cathedral #456 acceptance (far building, monsters, stalks hidden at the terrace edge at NORMAL camera; no seam strip; no z-fighting; candle/falls/lake/portal-flood intact), doorway flap, dungeon wall/floor seams, portal-in, house exit, teleport timing.

FW5 — camera completion

Goal: the camera is retail's, end to end.

  • Port CameraSet::Farther/CameraSet::Raise verbatim into RetailChaseCamera: the multiplicative offset scaling with the write-refusal gates (|x|<10, |y|<10, z<450, z>1.8 — NOT a simple distance clamp), the preserved-length pitch. The 40 m dev zoom goes away with it (owner-approved in the spec round).
  • Port retail's zoom/pitch adjustment rates if recoverable from decomp (retires TS-56); if not recoverable, TS-56 stays with a note.
  • Gates: camera unit tests against ported constants; owner side-by-side feel gate vs retail (chase, zoom saturation ≈13 m boom, collision, slope align).

FW6 — closeout

  • Divergence-register reconciliation (added: landblock-stage streaming exposure rows if not already rowed, order-preserving MDI merging as intentional architecture; retired: lift, #117 stencils, look-in-era rows, TS-56 if ported).
  • Docs: architecture doc render section rewritten around the walk; worldbuilder-inventory.md updated (WB = asset layer only); launch-options.md rows for any probes added/removed (probes die with their investigations).
  • Memory: project_render_pipeline_digest.md rewritten current-truth-on-top around the walk; campaign memory closed out.
  • Roadmap + milestones updated; the quarantined PV branch gets a disposition note (kept as archive; nothing merges).
  • Final gates: full hermetic suites, both standard connected routes, the complete owner visual matrix, merge to main.

Rollbacks

Each stage lands as its own commit series; record git revert anchors here as stages close (the Modern Runtime plan's convention). FW3 and FW4 are the cutover stages — their revert anchors are mandatory entries.

Risks (tracked, from the spec)

  • Perf — decided by FW3's checkpoint, numbers over hope.
  • Streaming scope creep — only the two register-rowed exposure adaptations may be touched; anything more is out of scope.
  • Oracle fidelity — traces need owner-assisted retail sessions (FW0); captured once, checked in.
  • Classifier drift during staging — FW3's split runs walk statics beside packed dynamics for one stage; the route contract seam is the guard, and FW4 deletes the duality.
  • Camera ergonomics — the dev zoom disappears at FW5; a separate dev flycam is deliberately deferred (the Fly camera in CameraController already exists for development framing).