acdream/docs/plans/2026-08-30-campaign-fw-frame-walk.md
Erik e65644cb33 feat(render) Campaign FW2: OrderedDrawStream + walk-order submitter
The walk-order submission layer over the existing RHI (plan section FW2):

- OrderedDrawStream: append-only walk-ordered draw commands
  (GroupKey + transform + per-instance data + WalkDrawStage + cell
  provenance), struct-of-arrays with one lockstep Reset (#193 shape).
  The PortalPunch stage exists but has no FW2 submission path - the
  submitter throws on it; punch emission lands with FW3 wiring.
- WbDrawDispatcher.OrderedStream partial: per-instance-first emission
  (the deferred-alpha shape - command i owns instance i, walk order
  survives into the indirect array), each SSBO section written once,
  then one DrawIndirectRangeRhi call per maximal merge run. Runs are
  built by pure-CPU BuildOrderedMergeRuns and may never span a stage,
  pipeline-bucket, or cull boundary; ValidateMergeRun re-checks every
  emitted run and throws (the campaign fail-loud rule). Nothing is
  sorted, reordered, or dropped: N commands in, N indirect commands
  out, covered exactly once.
- WorldDepthContract: retail world depth verified verbatim from the
  decomp - Render::zfuncVal @0x00820e1c = 0x2, SetDepthBufferMode
  @0x005a2d10 writes the enum directly as D3DRS_ZFUNC so the value IS
  D3DCMP_LESS, applied by the surface-state applier @0x0059c80a with
  Z-write toggled by blend; the LESSEQUAL sites are GameSky::Draw-local.
  Seven world pipeline sites now cite the named constant (no value
  changes).
- Plan updated: FW1 status block + gate amendment (the ten pose-stamped
  retail traces supersede re-expressing the old-builder replay
  fixtures; those retire with the old builder at FW4 and their
  scenario classes re-verify at the FW3/FW4 connected gates).

Known FW2 scope notes recorded in the code: the building-detail
overlay replay is production wiring (FW3); the _drawCullModes scratch
may not interleave with a mid-flight RetailAlphaQueue scope (FW3
sequencing constraint). The pixel A/B equivalence proof rides FW3's
cutover toggle where a walk-driven scene first exists.

Suites: full Release build 0 warnings; Walk lane 154/1 skip;
hermetic 6,714/0 (+27 new).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 12:31:31 +02:00

20 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.

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.

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).