Retail CPU-clips only portal polygons, punch fans and exit seals
(polyClipFinish); ordinary meshes, shells, terrain, particles and the
weather are never view-clipped. Inventory at edb32783: keep the seal and
punch clips (with equivalence pins), delete the per-slice weather loop,
the sky-post-scene particle clip slot, the dead cell-shell clip routing,
the slot-list shape of the part classifier, and the seven
ProbeCathedral* discriminators with their launch-option rows.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
564 lines
38 KiB
Markdown
564 lines
38 KiB
Markdown
# S3 — Walk: exact PView views, leaf admission, and interleave — ownership map and chunk plan
|
||
|
||
**Status:** DRAFT packet written 2026-09-03 at the S2 closeout, BEFORE the
|
||
owner's §7 capture session. Every retail claim below is decomp-verified
|
||
(named pseudo-C + Ghidra bridge, cited by address); the capture session adds
|
||
the per-frame TRANSCRIPT the automated gate needs and may amend §3 (marked
|
||
"capture-owned"). Nothing in this packet is implemented yet.
|
||
|
||
**Plan:** `docs/plans/2026-09-01-campaign-overhaul-world-solidity.md` §9 S3.
|
||
**Binding inputs:** `oh1-retail-world-contract.md` §4–§5 (view/frame order),
|
||
`oh1-built-mesh-view-contract.md` (caller order, stamps, direct answers,
|
||
binding constraints), `2026-08-30-fw-flood-pseudocode-appendix.md`, the FW
|
||
plan's verified seam map (`docs/plans/2026-08-30-campaign-fw-frame-walk.md`
|
||
§"Verified seam map"), and S2's ownership truth
|
||
(`s2-membership-ownership-map.md` §9: the registry is the only membership
|
||
source; the walk reads `GetRetailPartEntriesInCell`).
|
||
|
||
## 1. What S3 changes (behavior)
|
||
|
||
`WalkPView` owns retail's exact per-frame state — the portal-view lists, the
|
||
view/update counts, the todo order, the draw list, and the stamps — and each
|
||
content category follows its retail leaf rule. Terrain is emitted as ordered
|
||
landcell events interleaved with buildings and object lists in retail
|
||
far-to-near order. `DrawBuilding` is one semantic sequence. The "all terrain
|
||
first" simplification and every generic GPU clip rule that retail does not
|
||
have are deleted. S3 ships no evidence infrastructure beyond the one
|
||
print-only transcript emitter its own gate consumes (rule §5).
|
||
|
||
## 2. Retail truth (decomp-verified; addresses are the Sept 2013 build)
|
||
|
||
| Retail function | Address | What it decides | Verified in |
|
||
|---|---|---|---|
|
||
| `PView::InitCell` / `InsCellTodoList` | (walk root; see flood appendix) | seeds the flood from the viewer cell, orders the todo list | flood appendix |
|
||
| `PView::ClipPortals` / `AddViewToPortals` / `ConstructView` | `PView::GetClip → polyClipFinish @0x006B6D00` | builds CHILD VIEWS by polygon-clipping PORTAL polygons — the only polygon clip in the walk | built-mesh contract §"What polygon-clips?" |
|
||
| `Render::copy_view` | `0x0054dfc0` | copies the view polygon, derives edge planes (0x48 semantic fields, prune, cap 31) | built-mesh contract ledger |
|
||
| `Render::set_view` | `0x0054d0e0` | installs the view in render globals; NO scissor, NO general mesh clip | idem |
|
||
| `Render::obj_view_set` | `0x0054b9b0` | transforms the active planes/sphere through `FrameCurrent`, publishes `local_object_center/radius` | idem (side effects unrepresented — Medium row) |
|
||
| `Render::viewconeCheck` | `0x0054c250` | the ordinary sphere admission: strict-outside / inclusive-partial | `WalkVisibilityMath` pins |
|
||
| `CPhysicsPart::Draw` | `0x0050d7a0` | outer part-stamp skip (`+0xdc` vs device `+0xb0`); force bypasses; local player bypasses the inner stamp | idem |
|
||
| `RenderDeviceD3D::DrawMeshInternal` | `0x0059f360` | first admitted view stamps the non-player part and submits the WHOLE eligible mesh once; later views return | idem |
|
||
| `RenderDeviceD3D::DrawEnvCell` | `0x0059f170` | no sphere admission; cell stamp (`+0x130`) gate; whole shell once per stamp | idem |
|
||
| `RenderDeviceD3D::DrawBuilding` | `0x0059f2a0` | detail state → `FlushAlphaList(0)` → `CPhysicsPart::Draw(part,1)` portal-only BSP pass → flag → `Draw(part,0)` ordinary shell | idem |
|
||
| `PView::DrawCells` | `0x005a4840` | outside-view branch (sunlight, `LScape::draw`, alpha flush, device-stamp advance @`0x005a4886`, counter consume/clear, exit seals) then reverse shells (`CEnvCell::setup_view @0x0052c430` per live view + `DrawEnvCell`) then reverse object lists (`DrawObjCellForDummies`) | retail-world contract §5.2 |
|
||
| `LScape::draw_check_blocks` / `landcell_check` / `grab_visible_cells` / `draw` / `calc_draw_order` | (landscape walk) | admitted land cells in retail FAR-TO-NEAR order; per admitted cell `DrawLandCell` THEN `DrawSortCell` (building pass / look-in / shell / object turn) | retail-world contract §5.2; **capture-owned:** the exact `calc_draw_order` sequence at the four poses |
|
||
| `DrawObjCellForDummies` | (object-list path) | installs the cell's top `portal_view_type` as `PortalList`, walks the cell's shadow part list (`CShadowPart::insertion_sort` by `CYpt`) and draws each part through the view loop | built-mesh contract §"object-list path" |
|
||
|
||
Facts already PROVED and binding (do not re-derive, do not weaken):
|
||
- the built cell shell is stamped and submitted whole once after Boolean admission;
|
||
- portal polygons, not built meshes, are polygon-clipped;
|
||
- `DrawCells` draws all shells then all object lists, both in reverse
|
||
draw-list order, views ascending;
|
||
- device, cell-shell and part stamps persist with the local-player bypass;
|
||
- the "first view clips the shell" hypothesis is RETIRED.
|
||
|
||
## 3. Current acdream owners (functions, not line numbers — re-locate at chunk time)
|
||
|
||
| Retail mechanism | Current owner | Assessment (from the built-mesh comparison at `eaea8776`, re-checked against `c94a1a40` file list) | S3 action |
|
||
|---|---|---|---|
|
||
| view polygon / planes | `WalkViews` | strong match | keep; pin |
|
||
| viewcone sense | `WalkVisibilityMath` | exact | keep |
|
||
| flood + todo order | `WalkFrameDriver` (flood turns, `VisitedCells`, `OnLandscape`) | order proven for shells/objects; `_skyDrawnThisFrame` is a PROXY for retail's `outside_view.view_count != 0` | chunk 2 replaces the proxy with the real outside-view count |
|
||
| whole-mesh admission at slot 0 | `WbDrawDispatcher.WalkClassify` (`ResolveWalkLightSet`, part stamps, `RetailWholeMeshSlot`) | correct | keep; chunk 4 deletes the still-translated clip slots per category |
|
||
| part stamp identity | `(RenderProjectionId, PartIndex)` | retail identity is the `CPhysicsPart*`; S2 made per-cell membership registry-borrowed, so one logical part now reaches the classifier from EVERY cell it is in with the SAME projection id — the identity audit the contract asked for is now structurally answered; pin it | chunk 2 pin |
|
||
| cell shell once per stamp + mid-frame re-arm | `WalkFrameDriver` shell loop, `RetailPViewPassExecutor.WalkLeaf` | correct | keep; re-arm gate moves to the real outside-view count |
|
||
| building sequence | `WalkFrameDriver` building turn, `RetailFrameWalk` | matches alpha barrier → portal pass → shell | keep; pin as ONE sequence |
|
||
| terrain emission | `RetailPViewPassExecutor` landscape stage (whole-stage terrain event before building/object turns) | DIVERGES: retail is `DrawLandCell` then `DrawSortCell` PER admitted land cell, far-to-near | chunk 3 |
|
||
| landscape leaf's use of the installed outside-view list | `TerrainModernRenderer` visible cells + `WalkFrameDriver.VisitedLandscapeCellIds` | top-level ownership proven; per-leaf pixel use is an S3 audit item | chunk 3 audit (bounded: one pose) |
|
||
| generic GPU clip rules for categories retail never clips | Vulkan clip slots on look-in / straddling / building categories (`RetailPViewPassExecutor`, `UseIndoorMembershipOnlyRouting`) | retail clips only portal polygons | chunk 4 delete, with a CPU/GPU equivalence pin for any category that must keep a translated clip |
|
||
| `obj_view_set` side effects | not represented (world-space sphere vs captured planes) | mathematically aligned for affine transforms | out of S3 unless the capture shows a consumer |
|
||
| missing `SelectionSphere` fallback | "any view → whole mesh" | not a retail branch | keep as a corruption fallback, outside parity claims (register row if it survives S5) |
|
||
|
||
## 4. Chunks (bounded; one Sonnet agent each; stop on a third round)
|
||
|
||
**Chunk 1 — transcript emitter + parser + fixture import (needs §7 captures).**
|
||
One print-only flag (documented in `docs/launch-options.md` in the same
|
||
commit) emits the OH line kinds the capture templates produce
|
||
(`tools/walk-oracle/oh/*.template`: walk / parts / alpha-depth) from the
|
||
production walk; the existing FW0-style test-side parser is extended for the
|
||
new kinds; the four captured pose logs are imported as fixtures. Gate: the
|
||
emitter's lines parse with the same parser as the retail logs; NO validator,
|
||
NO canonical JSONL (rule §5, [[feedback-evidence-infrastructure-sink]]).
|
||
BLOCKED until the owner runs the capture session.
|
||
|
||
**Chunk 2 — exact PView state.** `WalkPView` (new, App/Rendering/Walk) owns:
|
||
the live view list per cell (retail `portal_view_type` lists), the
|
||
outside-view count, the view/update counts, the todo order, the draw list and
|
||
the two stamps. `_skyDrawnThisFrame` and every other proxy for
|
||
`outside_view.view_count != 0` is replaced by the real count. Per-category
|
||
leaf contract pinned: whole-once (built shell), Boolean sphere (ordinary
|
||
part), actual polygon clip (portal polygons only), repeated submission (local
|
||
player). Part identity pin: the same logical part reached from two cells
|
||
classifies once per stamp. Can start before the capture (decomp oracle);
|
||
the capture confirms.
|
||
|
||
**Chunk 3 — interleave.** Terrain becomes ordered landcell events: for each
|
||
admitted land cell in retail far-to-near order, `DrawLandCell` (that cell's
|
||
terrain range) then `DrawSortCell` (building pass 1 / look-in work / building
|
||
shell / land-cell object turn). Per-landcell terrain ranges come from the
|
||
existing terrain mesh layout (index ranges per landcell — verify
|
||
`TerrainModernRenderer` exposes them; if not, the chunk adds the range table
|
||
at bake/upload time, not a per-frame rebuild). The whole-stage terrain event
|
||
is deleted in chunk 4, not here (two-step so the pixel gate can bisect).
|
||
Gate: terrain pixels/mesh counts unchanged when no interleave is present;
|
||
dense-outdoor p50/p99 recorded before/after.
|
||
|
||
**Chunk 4 — deletions.** The whole-stage terrain event; the generic clip
|
||
rules on categories retail never clips (Vulkan clip slots removed from those
|
||
categories); any residual "first view clips the shell" code path. Gate: no
|
||
duplicate draw of a stamped part across a pan/zoom replay; membership/draw
|
||
count changes only at a retail view boundary.
|
||
|
||
## 5. Review (one Opus retail lens + one shader/perf lens at the S3 boundary)
|
||
|
||
- retail lens on every stamp/view loop branch and on the call order against
|
||
the capture transcript and the decomp;
|
||
- shader review of clip-space sign, y inversion, W plane, plane count and
|
||
scissor for the categories that KEEP a translated clip;
|
||
- performance review: the command-count increase from interleaving is
|
||
measured (dense outdoor p50/p99), not hidden by reordering.
|
||
Lead verifies every finding against the source before an edit (S2 precedent:
|
||
19 findings, 4 blocking, all real; see `s2-membership-ownership-map.md` §9).
|
||
|
||
## 6. Automated gate (S3 has no standalone owner gate; folds into G3)
|
||
|
||
- transcript equality with the §7 captures at the captured depth for all four
|
||
poses plus the FW0 still fixtures;
|
||
- tests distinguish whole-shell-once, Boolean part admission and actual
|
||
polygon clipping;
|
||
- pan/zoom replay changes membership/draw count only at a retail view boundary;
|
||
- no duplicate draw of a stamped part;
|
||
- terrain pixels/mesh counts unchanged when no interleave is present;
|
||
- self-gate route (`tools/overhaul-selfgate/`) at the three owner poses +
|
||
the four capture poses, pixel-diffed against the G2 candidate run
|
||
`logs/selfgate-20260903-010022-s2-review-fixes-3` — the per-region diff,
|
||
not the eye, is the verdict ([[feedback-deleted-fallback-carried-population]]).
|
||
|
||
## 6b. Capture session progress (2026-09-03)
|
||
|
||
- Recon PASSED against the live 2013 client (`logs/oh-capture/oh-recon.log`):
|
||
every template offset and breakpoint address matches; globals sane.
|
||
- **Pose `cathedral-leak` (extra, owner-chosen):** standing on the cathedral
|
||
stairs where the outside structure leaks into the interior — RETAIL SHOWS
|
||
THE ARTIFACT (2013 and EoR clients both). Transcript:
|
||
`logs/oh-capture/cathedral-leak.walk.log` (5 frames: DI `f4180108`, DC with
|
||
`ov=4`, EC/OC stamps, 531 `LC`/`SC` land-cell draws per frame in far-to-near
|
||
order over landblocks `f3`–`fe` × `00`–`18`), `cathedral-leak.parts.log`
|
||
(3 frames, 5,933 PD / 965 DM), `cathedral-leak.alphadepth.log` (5 frames,
|
||
1,407 AM / 2,856 FL / 28 PM / 16 PC). Owner's retail screenshot (2026-09-03, 2013 client, SAVED at
|
||
`logs/oh-capture/cathedral-leak.retail.png`, lead-verified): standing on the floating
|
||
stairs in `f4180108`, a large slab of the EXTERIOR wall (arched exterior
|
||
stone texture) cuts diagonally through the stair chamber from the upper
|
||
left, drawn over the interior landings and ramps that remain visible around
|
||
it — the same leak acdream shows at the same spot. This pose is S3/S4's
|
||
primary oracle: the target is to reproduce retail's frame at it, leak
|
||
included.
|
||
- **Pose `cathedral-arrival`** (owner "ready" 2026-09-03 06:58): transcript
|
||
`logs/oh-capture/cathedral-arrival.walk.log` (5 frames, 5,604 lines: main
|
||
PView `009d4f48` DI `f4180108`, DC `ov=3` n=2 `f4180108 f4180107`; a SECOND
|
||
PView `009d4530` with `ov=0` draws cells `f4180113/f4180112/f4180114` each
|
||
frame with no DI of its own — RESOLVED (§8 R1): it is `RenderDeviceD3D::outdoor_pview`'s
|
||
look-in `DrawCells`, driven by `PView::DrawPortal` from the building pass; 454
|
||
`LC` + 619 `SC` per frame, 40 EC / 40 OC total), `cathedral-arrival.parts.log`
|
||
(3 frames, 7,822 PD / 1,240 DM), `cathedral-arrival.alphadepth.log` (5 frames,
|
||
1,735 AM / 3,570 FL / 35 PM / 19 PC). NOTE: the kit README expected
|
||
`DI f4180106` (FW0's arrival still); the live client reported `f4180108`,
|
||
the stair-chamber cell — the owner's actual standing cell, recorded as-is.
|
||
- **Chunk-3 oracle read off the two cathedral transcripts (frame 2 of each,
|
||
viewer in `f4180108`):** after the root `DI`/`DC` and ONE `LS`, retail emits
|
||
`LC <cell>` then `SC <cell>` for every admitted land cell in STRICT alternation
|
||
(`LSLSLS…`, never two `LC` in a row) — `DrawLandCell` then `DrawSortCell` per
|
||
cell, exactly the per-cell interleave chunk 3 ports. Block order is far-to-near
|
||
from the far corner: rows `y=00…0d` each walked `x=fe→f6`, then from the
|
||
Chebyshev-10 ring inward the column and row interleave diagonally
|
||
(`fe0e fd0e fe0f fc0e fe10 fb0e fe11 …`), ending at `… f417 f318 f418`; the
|
||
exact rule is `LScape::calc_draw_order`'s and the transcript pins it. Far
|
||
blocks draw ONE cell (`xx000001`, side count 1); near the viewer `SC` fires for
|
||
all 64 cells of the block while `LC` fires only for the in-view ones (`f418`:
|
||
16 LC / 64 SC; `f417`: 56 / 64; `f318`: 3 / 64) — `DrawSortCell` runs under
|
||
`alwaysDrawObjects` for every cell of an in-view block, `DrawLandCell` only for
|
||
in-view cells (`RenderDeviceD3D::DrawBlock` @0x005a19d9 — chunk 3 re-reads the
|
||
exact gate). Per frame: leak pose 531 LC / 757 SC / 16 BLD; arrival pose
|
||
533 LC / 698 SC / 14 BLD.
|
||
- Remaining poses per the kit README: `holtburg-doorway-still`,
|
||
`terrace-edge`, `foundry-deep`.
|
||
|
||
## 7. Open questions for the capture session (ask for exactly these two)
|
||
|
||
1. `LScape::calc_draw_order` sequence at each of the four poses (far-to-near
|
||
land cell order) — chunk 3's oracle. Captured by the walk template's `LC`/`SC`
|
||
lines (added 2026-09-03: `RenderDeviceD3D::DrawLandCell` 0x0059f120 /
|
||
`DrawSortCell` 0x0059f140, one line per call with the cell's `m_DID`).
|
||
2. Per-frame `outside_view.view_count` and the device-stamp advance count at
|
||
each pose — chunk 2's oracle for the re-arm gate.
|
||
|
||
## 8. Chunk 2 contract — exact PView state (lead, 2026-09-03, written during the capture session)
|
||
|
||
Every claim below was re-read this session in the named pseudo-C and, where
|
||
BN's FPU rendering was ambiguous, in Ghidra (port 8081, `patchmem.gpr`, the
|
||
paired 2013 build). Addresses are the Sept 2013 build.
|
||
|
||
### 8.1 Retail facts (verified)
|
||
|
||
- **R1 — two PViews.** `RenderDeviceD3D::Init` @0x0059efb0 constructs
|
||
`indoor_pview = PView(…, 1)` and `outdoor_pview = PView(…, 0)`; the ctor
|
||
argument is `draw_landscape` (@0x005a52c1). `RenderDeviceD3D::DrawInside`
|
||
@0x0059f0d0 tail-calls `PView::DrawInside(indoor_pview, cell)`;
|
||
`RenderDeviceD3D::DrawPortal` @0x0059f0e0 pins `building_view = -1` around
|
||
`PView::DrawPortal(outdoor_pview, …)`. The captures' second `pv` with
|
||
`ov=0` (`009d50e0` at cathedral-leak, `009d4530` at cathedral-arrival,
|
||
cells `f4180112/13/14`, four `DrawCells` per frame, no `DI`) is
|
||
outdoor_pview's look-in flood through the cathedral's exterior portals.
|
||
acdream's `RetailFrameWalk._outdoorPView { DrawLandscape = false }` and
|
||
`WalkBuildingPortals.DrawPortal` already model this. PIN ONLY.
|
||
- **R2 — flood reset.** `PView::ConstructView(CEnvCell)` @0x005a57b0 sets
|
||
`outside_view.view_count = 0`, `master_timestamp++`, `cell_todo_num = 0`,
|
||
`cell_draw_num = 0` before `InitCell`. `WalkPView.ConstructView` matches.
|
||
PIN ONLY.
|
||
- **R3 — the `outside_view.view_count > 0` branch encloses the whole
|
||
landscape turn.** `PView::DrawCells` @0x005a4840: from 0x005a4852 to
|
||
0x005a49eb, ALL of `useSunlightSet(1)`, `PortalList = this`,
|
||
`LScape::draw`, `FlushAlphaList(0f)` @0x005a4872, `m_nFrameStamp += 1`
|
||
@0x005a4886, the gated Z clear @0x005a4893–0x005a48a9, and the exit-seal
|
||
loop @0x005a48c0–0x005a49eb (per draw-list cell far-to-near, per live view
|
||
`CEnvCell::setup_view` @0x005a4983, then `DrawPortalPolyInternal(portal_poly,
|
||
0)` @0x005a49b7 for every portal whose `other_cell_id == 0xffffffff`) sit
|
||
INSIDE `if (outside_view.view_count > 0)`. Only `useSunlightSet(0)`,
|
||
`restore_all_lighting`, the reverse shell loop @0x005a4a00 and the reverse
|
||
object-list loop @0x005a4ade are unconditional.
|
||
**DIVERGENCE TODAY:** `WalkFrameDriver.OnInteriorFloodDrawTurn` emits
|
||
`ClearInteriorDepth` and `ExitSeals` unconditionally and gates the stamp
|
||
advance on the `_skyDrawnThisFrame` proxy; its comment and the driver's
|
||
type doc claim "both unconditional for an interior root's own flood" — that
|
||
claim is wrong against the decomp, and the `WalkFrameDriverTests` pin that
|
||
reads "ov==0 … CLEAR/SEALS still fire unconditionally" pins the wrong
|
||
behavior. (The plan's S5 cleanup inventory already lists "unconditional
|
||
clear code"; chunk 2 retires it now.)
|
||
- **R4 — the Z clear is gated by a persistent seal counter.** The clear is
|
||
`if (forceClear != 0 || portalsDrawnCount != 0) Clear(Z)`, with
|
||
`D3DPolyRender::portalsDrawnCount` (uint16 @0x008719b4) read-then-zeroed
|
||
at that point (@0x005a489c–0x005a489e). `forceClear` (@0x008ed824) is a
|
||
`.data` int with no write anywhere in the pseudo-C (its only references
|
||
are the two reads and its definition) → the clear fires iff
|
||
`portalsDrawnCount != 0`. The counter increments ONLY in
|
||
`DrawPortalPolyInternal` @0x0059bc90 at 0x0059bd74, and only when its
|
||
second argument is FALSE, after a degenerate-polygon guard (Ghidra: skip
|
||
when every vertex has `x == +12`, or every `x == −12`, or every `y ==
|
||
+12`, or every `y == −12` — an equality sentinel, NOT a ±12 clip box as
|
||
BN's FPU rendering suggests) and BEFORE `polyClipFinish` — it counts
|
||
calls, not surviving fans. False-argument callers: the exit seals
|
||
(@0x005a49b7, literal 0); pass-3 `ConstructView(CBldPortal)` punches
|
||
(@0x005a5a7b, `arg5 == 1` → false for pass 3); `PView::DrawPortal`'s
|
||
`arg4 == 3` failure branch (@0x005a5b7c). But `DrawMeshInternal` runs
|
||
buildings through passes 1 and 2 only (@0x0059f3cc / @0x0059f3d9), and
|
||
pass 1's punch passes TRUE (`maxZ1`, no increment) — so in the building
|
||
path the counter is fed by EXIT SEALS ALONE. Consequence, a retail quirk
|
||
to port as-is: the first `ov>0` interior frame after a period without
|
||
seals draws NO Z clear; every later `ov>0` frame clears because the
|
||
previous frame's seals armed the counter. Outdoor frames and look-in
|
||
`DrawCells` (`ov=0`) neither arm nor consume it.
|
||
- **R5 — punch/seal depth state.** `SetDepthBufferMode(DEPTHTEST_ALWAYS,
|
||
(maxZ >> 2) & 1)` with `maxZ1 = 7` (argument true: building pass 1) and
|
||
`maxZ2 = 6` (argument false: seals, pass 3); both write depth. S4's
|
||
terrain-punch question is untouched by chunk 2.
|
||
- **R6 — part identity.** Retail stamps the `CPhysicsPart` (+0xdc against the
|
||
device's +0xb0). After S2, one logical part reaches the classifier from
|
||
every cell it is in with the SAME projection id, so `(RenderProjectionId,
|
||
PartIndex)` is the identity. PIN ONLY.
|
||
|
||
### 8.2 Production behavior change
|
||
|
||
- **B1 — the real count gates the whole turn.** `RetailFrameWalk.DrawInside`
|
||
passes `_interiorPView.OutsideView.ViewCount` to
|
||
`IWalkEventSink.OnInteriorFloodDrawTurn(cells, outsideViewCount)`. The
|
||
driver then executes retail's branch exactly: `outsideViewCount > 0` →
|
||
the landscape flush leaf (retail `FlushAlphaList(0f)` @0x005a4872 plus the
|
||
pre-clear dynamics hook that `RetailPViewRenderer.ClearWalkInteriorDepth`
|
||
bundles today), the stamp advance (`AdvanceWalkPartPassStamp` + the shell
|
||
and particle-turn re-arms), then `ClearInteriorDepth` ONLY IF
|
||
`PortalsDrawnCount != 0` (read-then-zero, R4), then `ExitSeals`;
|
||
`outsideViewCount == 0` → none of them. `_skyDrawnThisFrame` is deleted;
|
||
its "second landscape turn" fail-loud guard is kept on a frame-scoped
|
||
count field, its "landscape ran" use is replaced by the count the sink
|
||
receives.
|
||
- **B2 — the counter.** The driver owns `PortalsDrawnCount` (retail
|
||
`D3DPolyRender::portalsDrawnCount`), retained ACROSS frames (the driver is
|
||
retained by `RetailPViewRenderer`; never cleared by `BeginFrame`/
|
||
`AbortFrame`/`EndFrame`), incremented at the exit-seal turn by the number of
|
||
seal polygons actually submitted: for each flood cell (far-to-near) × each
|
||
live view slice × each portal with `OtherCellId == 0xFFFF` and ≥3
|
||
vertices — the same enumeration `RetailPViewPassExecutor.DrawPortalDepthWrite`
|
||
performs. Implement it as a return value from the seal leaf
|
||
(`DrawExitSeals` returns the submitted count) so the count and the draws
|
||
come from ONE enumeration; the driver adds it. Never incremented by punch
|
||
fans. Not touched by look-in `DrawCells`.
|
||
- **B3 — leaf split.** `RetailPViewRenderer.ClearWalkInteriorDepth` splits into
|
||
`FlushWalkLandscape` (pre-clear dynamics + `FlushLandscapeAlpha`) and
|
||
`ClearWalkInteriorDepth` (the Z clear only); `IWalkFrameLeafRenderer` gains
|
||
the flush leaf and `DrawExitSeals` returns `int`; `WalkFrameEventKind`
|
||
gains `LandscapeFlush`. The fake leaf in `WalkFrameDriverTests` logs it as
|
||
`"LFLUSH"`.
|
||
- **B4 — no new owner class.** The packet's "`WalkPView` (new)" is the
|
||
EXISTING `WalkPView`; chunk 2 adds no class. The per-category leaf
|
||
contract (whole-once built shell; Boolean sphere for ordinary parts;
|
||
polygon clip for portal polygons only; repeated submission for the local
|
||
player) is pinned by tests — the implementer first LISTS which of these
|
||
four pins already exist (`WalkFrameDriverTests`, `WbDrawDispatcher`
|
||
WalkClassify tests, `WalkVisibilityMath` pins) and adds ONLY the missing
|
||
ones, naming the retail address in each test's comment.
|
||
|
||
### 8.3 Tests (flip, add, keep)
|
||
|
||
- T1 (flip): the `ov==0` pin expects NO `LFLUSH`/`CLEAR`/`SEALS` and no stamp
|
||
advance before the flood — the opposite of today's expectation.
|
||
- T2 (add): `ov>0` first frame → `LFLUSH`, NO `CLEAR`, `SEALS`, flood; the next
|
||
frame of the same driver → `LFLUSH`, `CLEAR`, `SEALS`, flood (armed by frame
|
||
1's seals); a driver whose flood cells have no exit portal never clears.
|
||
- T3 (add): a look-in `DrawCells` (ov=0, `LookInStatic` stage) neither arms
|
||
nor consumes the counter.
|
||
- T4 (add if absent): R1 (two pviews, the outdoor one never raises an
|
||
outside view) and R2 (the reset) pins.
|
||
- T5 (update): the existing direct `OnInteriorFloodDrawTurn` callers
|
||
(`WalkFrameDriverTests` ≈ lines 349, 602–610, 642, 693–703) pass the count
|
||
they model; the two "look-in shell repaints after the clear" tests model
|
||
`ov>0`: one asserts the first-frame no-clear quirk explicitly, the other
|
||
pre-arms the counter (a prior seal turn) and asserts the clear — both
|
||
behaviors stay pinned.
|
||
- Keep: every other driver/walk pin unchanged; `LaunchOptionsDocumentationTests`
|
||
is unaffected (no flag added or removed).
|
||
|
||
### 8.4 Out of scope for chunk 2
|
||
|
||
Terrain interleave (chunk 3); clip-slot deletions and the
|
||
`ProbeCathedral*` discriminators (chunk 4 / the S5 inventory); S4's punch
|
||
z-func; the pre-clear dynamics hook's retail home (`LScape::draw`'s per-cell
|
||
object turn — chunk 3 places it).
|
||
|
||
### 8.5 Lead verification and gate
|
||
|
||
Every R-fact re-read at its address before the commit; the App hermetic
|
||
lane plus the InstalledDat lane green; the three-pose self-gate plus the two
|
||
captured cathedral poses pixel-diffed against
|
||
`logs/selfgate-20260903-062615-s2-chunk6` — the expected delta is zero at
|
||
steady state (the only retail-different frame is the first `ov>0` frame after
|
||
a no-seal period, which no still captures); the walk transcript at
|
||
cathedral-arrival must still show the root `DC ov=3` and four look-in
|
||
`DC ov=0` turns per frame.
|
||
|
||
## 9. Chunk 3 contract — the landscape interleave (lead, 2026-09-03)
|
||
|
||
Written during the capture session from the two cathedral transcripts and
|
||
the decomp; every fact below is verified at the cited address or against
|
||
the captured sequence itself. Chunk 3 implements AFTER chunk 2 lands (it
|
||
edits the same driver).
|
||
|
||
### 9.1 Retail facts (verified)
|
||
|
||
- **R1 — the per-cell interleave.** `RenderDeviceD3D::DrawBlock` @0x005a17c0
|
||
runs two loops over the block's `draw_array` (far-to-near): loop 1
|
||
(@0x005a1876) `IsInView && num_shadow_objects` → `UpdateObjCell` +
|
||
`CShadowPart::insertion_sort`; loop 2 (@0x005a197d) `if (IsInView)
|
||
DrawLandCell(cell)` @0x005a19c0, then `if (alwaysDrawObjects != 0 ||
|
||
IsInView) DrawSortCell(cell)` @0x005a19e6 followed by
|
||
`FlushAlphaList(flush)` only when `flush > 1`. Retail defaults:
|
||
`alwaysDrawObjects = 1` (@0x00820ed4), `flush = 0.75f` (@0x00820ed0) —
|
||
so `DrawSortCell` runs for EVERY cell of an in-view block, `DrawLandCell`
|
||
only for in-view cells, and the per-cell alpha flush is inert. Transcript
|
||
(frame 2 of both captures): one `LS`, then `LC`/`SC` in strict
|
||
alternation, never two `LC` in a row; 531 LC / 757 SC (leak), 533 LC / 698
|
||
SC (arrival); `SC` = 64 per near block, `LC` = only the in-view cells.
|
||
- **R2 — the order port is oracle-exact.** `LandWalkOrder.GetBlockOrder`
|
||
(`LScape::get_block_order` @0x00504c50, walked backwards) and
|
||
`FillCellOrderFarToNear` + `ClosestCell` (`CLandBlock::calc_sq_draw_order`
|
||
@0x0052f4a0; `CLandBlock::calc_draw_order` @0x00530300 is a tail call to
|
||
it) reproduce the captured `LC` AND `SC` sequences for all 260 (arrival) /
|
||
256 (leak) blocks with ZERO mismatches, given two facts the check needed:
|
||
(i) the captured blocks span Chebyshev ring 24, so the grid radius is ≥ 24
|
||
— `WalkLandscapeAssembler.MidRadius = 25` is consistent; (ii) LOD cell ids
|
||
are numbered `x*8 + y + 1` in LOD coordinates (a side-2 block's cells are
|
||
`1, 2, 9, 10`), i.e. retail's `lcell[x*side+y].m_DID`. LOD by ring from the
|
||
captures: ring 0–1 → side 8, ring 2 → 4, ring 3–4 → 2, ring ≥ 5 → 1 —
|
||
exactly `WalkLandscapeAssembler.SideCellCountForRing`. The FW-era
|
||
`IWalkEventSink.OnLandscapeCellTurn(landblockId, side, cellIndex)` already
|
||
expands a coarse cell into its covered 8×8 owner buckets.
|
||
- **R3 — one land cell's terrain is arithmetic on the existing mesh.**
|
||
`DrawLandCell` @0x0059f120 draws one land cell at the block's LOD.
|
||
acdream's terrain mesh is 8×8 cells per landblock, cell-major
|
||
(`LandblockMesh.Build`: `cy` outer, `cx` inner, `indices[i] = i`, 6
|
||
indices per cell), uploaded per slot with `FirstIndex = slot * 384`
|
||
(`TerrainModernRenderer.IndicesPerLandblock == 384`). acdream cell
|
||
`(cx, cy)` is the contiguous run `[(cy*8 + cx)*6, +6)` inside the slot; a
|
||
retail LOD cell (side `n`, LOD coords `(X, Y)`) covers `cx ∈ [X*8/n,
|
||
(X+1)*8/n)`, `cy ∈ [Y*8/n, (Y+1)*8/n)` → `8/n` runs of `(8/n)*6`
|
||
contiguous indices (side 8: one run of 6; side 4: two runs of 12; side 2:
|
||
four runs of 24; side 1: one run of 384). No range table is baked; no
|
||
per-frame rebuild.
|
||
- **R4 — why the interleave is a behavior change (the S4 hypothesis).** The
|
||
far-Z punch (`DrawPortalPolyInternal`, `DEPTHTEST_ALWAYS` + write) runs at
|
||
the BUILDING's cell turn (`DrawSortCell` → `DrawBuilding`). In retail
|
||
every NEARER land cell's terrain is drawn AFTER that punch and overwrites
|
||
the punched depth, so a doorway behind a hill cannot show its interior
|
||
through the hill. acdream draws ALL terrain first (`TerrainSlice(0)` at the
|
||
landscape turn) and every building afterwards, so a doorway behind a hill
|
||
punches through it — the doorway-sized house fragments through the
|
||
Holtburg hill in the owner's G2 screenshot. Chunk 3 is expected to remove
|
||
that artifact by ORDER alone; the Holtburg self-gate pose is the verdict.
|
||
If it does, S4's "exact z-func" question closes with no z-func change
|
||
(ledger + S4 row note). This supersedes the packet's §4 "two-step
|
||
bisect" wording: the whole-stage terrain event is replaced in THIS chunk
|
||
(drawing terrain twice would already hide the punch and make the gate
|
||
meaningless), and chunk 4 loses that deletion.
|
||
- **R5 — the sky draws TWICE, and the second pass is the WEATHER.**
|
||
`LScape::draw` @0x00506330 calls `GameSky::Draw(sky, 0)` @0x0050633c BEFORE
|
||
`draw_check_blocks`, walks the blocks (`DrawBlock` @0x00506374), then calls
|
||
`GameSky::Draw(sky, 1)` @0x00506396 AFTER every block. `GameSky::Draw`
|
||
@0x00506ff0's `arg2 == 1` is the weather pass, gated on
|
||
`SmartBox::is_player_outside` @0x00451e80 (already Ghidra-arbitrated and
|
||
modeled by `RetailPViewPassExecutor.DrawLandscapeSliceLate`'s `RenderWeather`
|
||
call — see that method's comment). Chunk 3 keeps the sky at the landscape
|
||
turn's start and places the weather pass AFTER the last land cell of the
|
||
turn (retail order); the implementer confirms where the late landscape
|
||
slice runs relative to the new per-cell events and moves the weather call
|
||
only if it currently runs before them.
|
||
|
||
### 9.2 Production behavior change
|
||
|
||
- **B1 — per-cell land-cell events.** `RetailFrameWalk.DrawLandscape` emits,
|
||
per `DrawArray` cell in retail order: `sink.OnLandCellTurn(landblockId,
|
||
side, cellIndex)` when `CellInView[cellIndex] != Outside` (R1's
|
||
`DrawLandCell` gate), THEN the existing `DrawBuilding` + `OnLandscapeCellTurn`
|
||
(the `DrawSortCell` half, gated by `AlwaysDrawObjects || in view` as today).
|
||
The driver records a `LandCell` frame event (slot landblock id, LOD side,
|
||
LOD cell index, plus the retail LOD cell id `x*8+y+1` for the chunk-1
|
||
transcript) at that point in the stream, and DELETES the whole-stage
|
||
`TerrainSlice(0)` emission and the `DrawTerrainSlice` leaf. The leaf
|
||
`DrawLandCell` submits the R3 index runs through a new
|
||
`TerrainModernRenderer.DrawCellRuns(landblockId, side, cellIndex)` (or a
|
||
batched form, B2) with the same pipeline/state the whole-block draw uses,
|
||
UNCLIPPED for interior roots exactly as the FW4-slice-6 comment demands
|
||
(retail never view-clips terrain; the exit seals and the interior repaint
|
||
own aperture exactness).
|
||
- **B2 — order-preserving batching.** Consecutive `LandCell` events of the
|
||
SAME slot with no intervening non-terrain event merge into ONE indirect
|
||
draw whose command list is the concatenated runs in event order; any
|
||
intervening event (building alpha barrier, punch, shell, statics mark)
|
||
splits the batch. Pixel-identical by construction; this is the only
|
||
concession to the ~530–700 land cells per frame.
|
||
- **B3 — `TerrainModernRenderer.Draw(...)`** keeps serving non-walk callers
|
||
(directional-shadow receivers etc.); the walk path stops calling it. The
|
||
`_walkTerrainInViewLandcells` plumbing that only fed the walk draw is
|
||
deleted if nothing else reads it (grep; if a shadow pass reads it, leave it
|
||
and say so).
|
||
- **B4 — no new class beyond the leaf/renderer entry points, no flag, no
|
||
probe.** The existing `OnLandscapeCellTurn` LOD expansion is unchanged.
|
||
|
||
### 9.3 Tests
|
||
|
||
- T1 (order pin, R2): `LandWalkOrder` + `WalkLandscape.CalcDrawOrder` +
|
||
`SideCellCountForRing` reproduce frame 2's `LC` and `SC` sequences of BOTH
|
||
captures. Fixture = the parsed cell-id sequences (reuse the existing
|
||
test-side reader `tests/AcDream.App.Tests/Rendering/Walk/WalkOracleTrace.cs`
|
||
for the `LC`/`SC` line kinds; no new evidence infrastructure). Viewer:
|
||
block `f4/18`, sq cell `(1, 1)` (from the `P` line: x 36.6, y 24.0).
|
||
- T2 (interleave, R1/B1): a two-block synthetic landscape with one building
|
||
and one out-of-view cell: events are `SKY, LC(far)…, [LC, BLD…, statics]…`
|
||
with each cell's `LC` before its own building/statics, cells far-to-near;
|
||
the out-of-view cell gets NO `LC` but keeps its sort turn under
|
||
`AlwaysDrawObjects`; no `TerrainSlice` event exists any more.
|
||
- T3 (runs, R3): for side ∈ {1, 2, 4, 8} and every LOD cell, the runs cover
|
||
exactly the covered 8×8 cells' index ranges, disjoint, total `(8/n)²·6`.
|
||
- T4 (batching, B2): two consecutive same-slot `LC` merge into one draw;
|
||
an intervening building barrier splits; order inside the batch is the
|
||
event order.
|
||
- T5: existing `WalkFrameDriverTests` / renderer pins on `TERRAIN:0` updated
|
||
to the per-cell events; every other pin unchanged.
|
||
|
||
### 9.4 Out of scope
|
||
|
||
Clip-slot deletions (chunk 4); any depth-compare change to the punch (S4 —
|
||
but the Holtburg verdict is recorded and S4 re-scopes if the punch leak is
|
||
gone); the pre-clear dynamics hook's home (chunk 4).
|
||
|
||
### 9.5 Lead verification and gate
|
||
|
||
R-facts re-read at their addresses; the App hermetic + InstalledDat lanes
|
||
green; the three-pose self-gate plus the Holtburg terrain-leak pose (the
|
||
owner's G2 screenshot spot) plus the two cathedral poses, pixel-diffed
|
||
against the chunk-2 run: the ONLY expected delta is the punch region at
|
||
Holtburg (fragments gone); dense-outdoor p50/p99 recorded before/after with
|
||
the existing frame-time diagnostics (no new counters).
|
||
|
||
## 10. Chunk 4 contract — deletions (lead draft, 2026-09-03; re-locate every owner at chunk time, after chunks 2 and 3 land)
|
||
|
||
### 10.1 What retail clips, and what it does not (verified)
|
||
|
||
- Retail polygon-clips exactly three things on the CPU with
|
||
`ACRender::polyClipFinish`: portal polygons when building child views
|
||
(`PView::GetClip` @0x005a4320 → `polyClipFinish`), the punch fans
|
||
(`DrawPortalPolyInternal` @0x0059bc90 clips the fan against the installed
|
||
view — `building_view` @0x0059f3bf during the building pass), and the
|
||
exit seals (the same function under `CEnvCell::setup_view` per live view,
|
||
@0x005a4983–0x005a49b7). Ordinary meshes, cell shells, terrain, particles
|
||
and the weather are NEVER view-clipped: `DrawMeshInternal` @0x0059f360
|
||
submits the whole mesh after the sphere test, `DrawEnvCell` @0x0059f170
|
||
draws the whole shell, `LScape::draw` draws whole blocks, and
|
||
`GameSky::Draw(sky, 1)` @0x00506396 runs ONCE after the blocks with only
|
||
the `is_player_outside` gate (§9 R5).
|
||
|
||
### 10.2 acdream inventory (at `edb32783`; names, not lines)
|
||
|
||
KEEP (translated clips with a retail CPU counterpart — each gets a
|
||
CPU/GPU equivalence pin if it lacks one):
|
||
- exit seals: `RetailPViewRenderer.DrawWalkExitPortalMasks` →
|
||
`DrawExitPortalMask(cell, InteriorFloodViewClipPlanesAt(...))` per (flood
|
||
cell, live view) — retail's `setup_view` + `polyClipFinish` per view;
|
||
- punch fans: `DrawPunchFan(worldPolygon, activeViewIndex)` clipped by the
|
||
pinned view's slice planes — retail's `building_view` clip;
|
||
- the walk's `CaptureViews`/`AppendClipSlot` slot table ONLY as far as the
|
||
two consumers above need it.
|
||
|
||
DELETE (no retail counterpart):
|
||
- the whole-stage terrain event and `DrawTerrainSlice` leaf — already
|
||
replaced by chunk 3 (§9 R4); chunk 4 verifies nothing re-grew;
|
||
- the per-outside-view-slice weather/late landscape loop
|
||
(`RetailPViewRenderer` "GameSky's weather pass still runs through each
|
||
active landscape view": `SetTerrainClip` + `ClearClipRouting` +
|
||
`DrawLandscapeSliceLate` per slice with `BeginDoorwayScissor` +
|
||
`BindTerrainClip`) — retail draws the weather ONCE, unclipped; one
|
||
weather draw after the last land cell (chunk 3 places it; chunk 4 deletes
|
||
the loop, the scissor, and the terrain-clip binding if no consumer
|
||
remains);
|
||
- the sky-post-scene particle submission's per-slice `clipSlot:
|
||
slice.Slot` — retail inserts emitters into one unclipped alpha list;
|
||
- `RetailPViewPassExecutor.SetCellShellClipRouting` (no caller in src or
|
||
tests) and every `SetClipRouting`/`ClearClipRouting`/
|
||
`UseIndoorMembershipOnlyRouting` call that becomes a no-op once no shell
|
||
or entity clip routing exists — the "first view clips the shell" residue;
|
||
- `WbDrawDispatcher.WalkClassify.ResolvePartClipSlots`'s slot LIST return
|
||
shape: it already answers "whole mesh" (`RetailWholeMeshSlot`) whenever
|
||
any view admits the sphere; reduce it to the Boolean retail asks
|
||
(`SphereVisibleInLookInTurn`) and delete `VisibleClipSlotsInLookInTurn`
|
||
if the seals/punches do not use it (they read planes, not slots — verify);
|
||
- the `ProbeCathedralSkip*` discriminators (`ProbeCathedralSkipFloatingStairSeals`,
|
||
`…StairBuildingPunch`, `…FloatingStairCellShells`, `…SouthLookInCellShells`,
|
||
`…LookInShellCellId`, `…SouthBuildingShell`, `ProbeCathedralShellOrderEnabled`)
|
||
with their `docs/launch-options.md` rows and `RenderingDiagnostics`
|
||
members — the investigation they served is closed by the retail axiom
|
||
(the cathedral leak is retail behavior); the S5 inventory already lists
|
||
them, chunk 4 takes them because it touches every one of their call
|
||
sites anyway.
|
||
|
||
### 10.3 Gate
|
||
|
||
- no duplicate draw of a stamped part across a pan/zoom replay (the
|
||
existing replay pins); membership/draw counts change only at a retail view
|
||
boundary;
|
||
- the three-pose self-gate + Holtburg + the two cathedral poses pixel-diffed
|
||
against the chunk-3 run: expected delta ZERO except where a deleted
|
||
per-slice scissor previously under-painted the weather at an exit-view
|
||
edge (record the region if it appears; it is the deletion's intended
|
||
effect, not a regression);
|
||
- `LaunchOptionsDocumentationTests` green with the seven probe rows and
|
||
their read sites removed together (the test enforces both directions);
|
||
- the register: no row is added (every deletion removes an acdream-only
|
||
rule); any KEEP item still lacking a CPU/GPU equivalence pin gets one.
|