docs(render): S3 chunk 2 contract -- exact PView state

Decomp-verified during the capture session: PView::DrawCells 0x005a4840
keeps the landscape flush, stamp advance, gated Z clear and exit seals
inside outside_view.view_count > 0; the clear is gated by the persistent
D3DPolyRender::portalsDrawnCount (0x008719b4), which only exit seals feed
in the building path (pass 1 punches pass TRUE; forceClear is never
written). acdream's unconditional clear/seals and the _skyDrawnThisFrame
proxy are the divergence chunk 2 retires. The captures' second PView is
outdoor_pview's look-in flood (RenderDeviceD3D::Init 0x0059efb0).

Also: the cathedral floating-stairs scene row now expects retail's
exterior-slab leak (retail axiom), the retail screenshot is saved at
logs/oh-capture/cathedral-leak.retail.png, and the S3 ledger row moves to
chunk 2 in progress.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-03 07:14:39 +02:00
parent c1b24ec0c0
commit 5c64679b11
2 changed files with 159 additions and 5 deletions

File diff suppressed because one or more lines are too long

View file

@ -145,8 +145,8 @@ Lead verifies every finding against the source before an edit (S2 precedent:
`ov=4`, EC/OC stamps, 531 `LC`/`SC` land-cell draws per frame in far-to-near `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` order over landblocks `f3``fe` × `00``18`), `cathedral-leak.parts.log`
(3 frames, 5,933 PD / 965 DM), `cathedral-leak.alphadepth.log` (5 frames, (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, to be saved as 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`): standing on the floating `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 stairs in `f4180108`, a large slab of the EXTERIOR wall (arched exterior
stone texture) cuts diagonally through the stair chamber from the upper stone texture) cuts diagonally through the stair chamber from the upper
left, drawn over the interior landings and ramps that remain visible around left, drawn over the interior landings and ramps that remain visible around
@ -157,7 +157,8 @@ Lead verifies every finding against the source before an edit (S2 precedent:
`logs/oh-capture/cathedral-arrival.walk.log` (5 frames, 5,604 lines: main `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 `009d4f48` DI `f4180108`, DC `ov=3` n=2 `f4180108 f4180107`; a SECOND
PView `009d4530` with `ov=0` draws cells `f4180113/f4180112/f4180114` each PView `009d4530` with `ov=0` draws cells `f4180113/f4180112/f4180114` each
frame with no DI of its own — its identity is a chunk-2 audit item; 454 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` `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, (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 1,735 AM / 3,570 FL / 35 PM / 19 PC). NOTE: the kit README expected
@ -174,3 +175,156 @@ Lead verifies every finding against the source before an edit (S2 precedent:
`DrawSortCell` 0x0059f140, one line per call with the cell's `m_DID`). `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 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. 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 @0x005a48930x005a48a9, and the exit-seal
loop @0x005a48c00x005a49eb (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 (@0x005a489c0x005a489e). `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, 602610, 642, 693703) 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.