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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-03 07:28:10 +02:00
parent fa217bc6c7
commit 296bfd8d48

View file

@ -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 01 → side 8, ring 2 → 4, ring 34 → 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 ~530700 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).