From 296bfd8d48ab44e9b1ea07380996202c02d501e6 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 3 Sep 2026 07:28:10 +0200 Subject: [PATCH] docs(render): S3 chunk 3 contract -- the landscape interleave Verified against the two cathedral transcripts and the decomp: DrawBlock 0x005a17c0 draws LandCell then SortCell per land cell (alwaysDrawObjects=1, flush=0.75 makes the per-cell alpha flush inert); the ported ring order, closest-cell table and ring-to-LOD mapping reproduce every captured LC/SC sequence at both poses with zero mismatches once LOD cell ids are read as x*8+y+1 in LOD coordinates; one land cell's terrain is arithmetic on the existing cell-major 384-index landblock mesh. The interleave is expected to remove the Holtburg terrain-punch leak by draw order alone (S4 hypothesis); LScape::draw draws the sky twice (before and after the blocks). Co-Authored-By: Claude Fable 5.1 --- .../s3-walk-ownership-map.md | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md b/docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md index fbbe43cf..96af78fc 100644 --- a/docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md +++ b/docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md @@ -344,3 +344,144 @@ 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.** `LScape::draw` @0x00506330 calls + `GameSky::Draw(sky, 0)` @0x0050633c BEFORE `draw_check_blocks`, walks the + blocks (`DrawBlock` @0x00506374), and then calls `GameSky::Draw(sky, 1)` + @0x00506396 AFTER every block. acdream emits one `Sky()` event at the + landscape turn's start. The implementer reads `GameSky::Draw` (grep it) + and reports what the second pass (`arg2 == 1`) draws; if it is a real + post-terrain layer (horizon band, celestial sprites, the far fog dome), + chunk 3 adds the second sky event after the last land cell; if it is + empty at retail defaults, the contract records why with the address. Do + not guess — read it. + +### 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).