acdream/docs/research/2026-08-30-cathedral-review-opus.md
Erik 6d990d38b3 docs: the cathedral seven-report synthesis + adjudicated FW4 plan
Files the independent review corpus (fable/opus/external + audit,
DAT-format, F418 DAT-geometry, retail decomp reconstruction) and the
synthesis that adjudicates their disagreements: retail roots INTERIOR
at the posed terrace defect pose with a ONE-CELL flood and ov=1 (the
free captures rooted outdoor - both were real); the falls are outdoor
landblock emitters 0xCF418000-13; the "thin seam bands" claim is
DAT-refuted (full-size exits; the seam is a coincident double
exit-to-outside plane at y=48); clear+seals live inside retail''s ov>0
block with a one-frame seal latch. Carries the instrument-first plan
(falls-owner trace, depth checkpoints, pose conformance) and the
decomp-pinned repair sequence toward FW4''s single-owner goal.

Also sweeps in the probe scaffolding slice 5''s path-limited add had
missed (RenderingDiagnostics field + the launch-options row wording).

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

673 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Cathedral rendering review — independent report (Opus)
**Reviewer:** Opus 5, independent. **Mode:** report-only (no edits, no builds,
no client launches, no commits). **Reviewed:** `37febd1f` (baseline) →
`bb9212c7` (HEAD), branch `claude/campaign-w-retail-frame-walk`, worktree
`peaceful-blackburn-5333f0`.
**Date:** 2026-08-30.
Evidence classes used, in order of authority: (1) the named retail decomp
(`docs/research/named-retail/acclient_2013_pseudo_c.txt` + Ghidra MCP at
`127.0.0.1:8081`); (2) the installed DATs, read with the already-built
`tools/A8CellAudit/bin/Release/net10.0/A8CellAudit.exe`; (3) the live probe
logs in `%LOCALAPPDATA%\Temp\fw3-*.log` / `fw4-*.log`; (4) our source at both
SHAs. Everything I could not verify is labelled **UNVERIFIED** inline.
---
## 1. Executive verdict
1. **The ordering fix the last three slices were chasing already landed.** On
HEAD the falls' four emitters submit *and drain* pre-clear
(`fw4-slice6-gate.log:12786-12801`). Phase is no longer the binding
constraint, so slices 46 were repairing an already-repaired axis.
2. **The falls are `{4f418012..4f418015}` — four STAB STATICS of EnvCell
`0xF4180106`**, reached on the failing frames as a **building look-in**,
not as a root-flood cell. Slices 3 and 4 named the wrong objects
(`f4243`/`f4285` are ~9.8 m-radius *dynamics*).
3. **Slice 6's factual premise is refuted by the DAT.** `0xF4180104`'s exit
portals are the cell's **full side faces** (4-vert rectangles at ±12.00 m,
7-vert arches at ≈±5.03 m) — not "thin bands".
4. **The only depth-destroying operation in the pre-clear scope is the
portal PUNCH** (far-Z, `ALWAYS`+write). Retail bounds it with an
unconditional **full** `FlushAlphaList(0f)` at *every* building
(@0x0059f30b); ours is a distance-filtered *partial* flush. That is the
highest-ranked unexamined suspect — but I could not prove it offline.
5. **Four other decomp-provable divergences** are live at HEAD: unconditional
clear+seals (retail gates both on `ov>0`), a clipped punch (retail punches
the *unclipped* polygon), terrain drawn with no per-landcell `in_view`
cull, and a sorted alpha drain (retail's has **no sort**).
6. **The "stalks" have a completed, decomp-cited fix that is simply not on
this branch**: `fce250ec` (marker-class degrade admission) is **not an
ancestor of `bb9212c7`**. Nothing on this branch suppresses them.
7. **Why six fixes failed:** three different object identities, zero depth
measurements, two false retail citations, six commits in 105 minutes with
no gate between them. The failure is a *method* failure, not a
*mechanism-knowledge* failure.
8. **Recommendation:** stop shipping fixes. Instrument the depth buffer and
the punch census first (§7 Stage 0). Then land R1R7, each gated.
9. **Commit verdicts:** keep 1/2/3 (with corrections), revert 4, revert 5,
keep 6's *shape* but fix its cull.
10. **I could not determine the final residual pixel mechanism offline** and
say so rather than adding a seventh guess.
---
## 2. Retail frame reconstruction
Everything in this section is read from the decomp at the cited address.
### 2.1 The four (and only four) alpha drains
`grep "FlushAlphaList"` over the whole pseudo-C returns exactly five hits —
the definition plus **four call sites**:
| Address | Site | Argument | Meaning |
|---|---|---|---|
| `0x00452bf0` | a `SmartBox` sibling (not the normal-mode path) | `0f` | drain all |
| `0x00453b8b` | `SmartBox::RenderNormalMode` epilogue | `0f` | **frame-end drain** |
| `0x0059f30b` | `RenderDeviceD3D::DrawBuilding`, **before** the portal pass | `0f` | **pre-punch drain** |
| `0x005a1a07` | `RenderDeviceD3D::DrawBlock`, after `DrawSortCell` | global `flush` | pressure valve |
| `0x005a4872` | `PView::DrawCells`, after `LScape::draw` | `0f` | **pre-clear drain** |
`D3DPolyRender::FlushAlphaList` @0x0059d2e0 (Ghidra decompile, full body read):
```c
if (((float)alphaedMeshCountClip < param_1 * 3000.0f) &&
((float)alphaedMeshCountAlpha < param_1 * 3000.0f)) return false; // 0x453B8000 = 3000.0f
... walk alphaedMeshListClip [0..count) -> RenderMeshSubset ... ; alphaedMeshCountClip = 0;
... walk alphaedMeshListAlpha [0..count) -> RenderMeshSubset ... ; alphaedMeshCountAlpha = 0;
```
Three load-bearing facts:
- **There are TWO lists**, `alphaedMeshListClip` and `alphaedMeshListAlpha`
(both capacity `0xbb8` = 3000), drained clip-first.
- **There is NO SORT.** The drain is a straight forward walk of the arrays.
`D3DPolyRender::AddMeshToAlphaList` @0x0059c230 stores only
`{MeshBuffer*, subsetIndex, CSurface*, flags, worldMatrix}` — **no depth
key exists**. Retail's alpha order *is* the walk's submission order.
- `flush(0f)` always drains (threshold 0 ⇒ `count < 0` false). `flush(0.75)`
drains only above 2250 queued.
The `DrawBlock` valve at `0x005a19ee-005a1a07` compares the **global `flush`
against `1.0f`**, not the queue size, and only calls through when the compare
fails. At any `flush ≤ 1.0` it never fires, and at `flush > 1.0` the
threshold `flush*3000` exceeds the 3000-entry cap, so it can never drain
either. **The mid-walk valve is effectively inert.** (The `test ah,0x41`
polarity is BN-unlifted; I read the idiom, not the raw bytes — **polarity
UNVERIFIED**, but both readings give "inert", so it is not load-bearing.)
What defers, from `D3DPolyRender::DrawMesh` @0x0059d4a0: the decision is
**per mesh SUBSET**, keyed on `s_AlphaDelayMask & mesh->isStippledOrAlphaedMask[subset]`,
with an early bail when `render_device+0x7e0 != 0`. It is a *surface
property*, never a distance.
### 2.2 `PView::DrawCells` @0x005a4840 — byte-order confirmed
```c
if (this->outside_view.view_count > 0) { // 005a4852
Render::useSunlightSet(1); // 005a485a
Render::PortalList = this; // 005a485f
LScape::draw(this->lscape); // 005a486b
D3DPolyRender::FlushAlphaList(0f); // 005a4872 <-- PRE-CLEAR DRAIN
render_device->m_nFrameStamp += 1; // 005a4886 <-- dedupe RE-ARM
if (forceClear == 0) { cond = (portalsDrawnCount == 0); portalsDrawnCount = 0; } // 005a4893-9e
if (forceClear != 0 || !cond)
render_device->Clear(4, black, 1.0f); // 005a48a9 <-- Z CLEAR
for (cell = cell_draw_list[n-1] .. [0]) // 005a48c9 far->near
if (cell->structure->drawing_bsp != 0) {
positionPush; for each view: setup_view;
for each portal: if (other_cell_id == 0xffffffff)
DrawPortalPolyInternal(portal.poly, 0); // 005a49b7 <-- SEALS (own depth)
}
} // 005a4852 block ENDS here
Render::useSunlightSet(0); Render::restore_all_lighting(); // 005a49f3
for (cell far->near) if (drawing_bsp) { per view: setup_view; DrawEnvCell(cell); } // 005a4a05
for (cell far->near) { PortalList = cell top view; DrawObjCellForDummies(cell); } // 005a4ae3
```
**The depth clear AND the exit seals are both strictly inside
`outside_view.view_count > 0`.** There is no drain after `DrawEnvCell` and
none after `DrawObjCellForDummies` — the interior scope's alpha survives to
`RenderNormalMode`'s frame-end drain @0x00453b8b (verified: that epilogue
runs immediately after the root branch, before the target/rendering
callbacks).
### 2.3 Buildings, punches, and the answer to (a) "clipped vs culled vs whole"
`RenderDeviceD3D::DrawBuilding` @0x0059f2a0, read verbatim:
```c
outdoor_pview->outdoor_portal_list = b->portals;
CPhysicsPart::UpdateViewerDistance(part0);
if (part0->gfxobj[part0->deg_level] != 0) { // 0059f2d3 — degrade gate
... set detail surface state ...
D3DPolyRender::FlushAlphaList(0f); // 0059f30b — FULL drain, unconditional
CPhysicsPart::Draw(part0, 1); // 0059f31d — PORTAL pass
ObjBuildingOrBuildingPart = 1;
CPhysicsPart::Draw(part0, 0); // 0059f336 — SHELL pass
ObjBuildingOrBuildingPart = 0;
}
```
`PView::ConstructView(CBldPortal)` @0x005a59a0 — the punch itself:
```c
side = sidedness(viewer, poly.plane, F_EPSILON=0.000199999995f); // 005a59c6
if (portal_side ? side==NEGATIVE : side==POSITIVE) {
PView::GetClip(this, side, poly, &clip_view, &npts, pass); // 005a5a23
if (npts != 0 && (dest = CEnvCell::GetVisible(other_cell_id)) != 0
&& Render::copy_view(dest->portal_view[dest->num_view-1], &clip_view, npts) != 0) {
if (arg5 != 2) DrawPortalPolyInternal(esi /* the RAW polygon */, arg5 == 1); // 005a5a7b
framePop();
if (arg5 != 1) PView::ConstructView(this, dest, other_portal_id); // 005a5a9c
return 1;
}
}
return 0;
```
Two things this settles:
- **Pass 1 punches (flag 1 = far-Z), pass 2 recurses.** Pass 2 draws no
polygon at all.
- **The punched polygon is `esi` — the ORIGINAL, UNCLIPPED authored
polygon.** `clip_view`/`npts` are used only to build the destination cell's
view. Retail never hardware-clips or scissors the punch.
Answering the prompt's question (a) for an interior root with exit portals:
| Content | Treatment |
|---|---|
| terrain (`DrawLandCell`) | **view-CULLED** per block and per 24 m landcell by `LScape::draw_check_blocks` @0x00505f80 / `landcell_check` @0x005050a0 against every active view; the surviving cells draw **whole**. Never clipped. |
| sky (`GameSky::Draw`) | drawn once at the head of `LScape::draw`, **whole**. |
| building shells | **whole**, per building, after that building's punch pass; gated only by `gfxobj[deg_level] != 0`. |
| EnvCell shells (`DrawEnvCell`) | **whole**, per cell per installed view. |
| objects / cell statics / dynamics | `Render::viewconeCheck` @0x0054c250 sphere-vs-view; PARTIAL draws **whole**. |
| **portal-chain polygons only** | the *only* CPU-clipped geometry, via `PView::GetClip` @0x005a4320`ACRender::polyClipFinish`. And the clip result is used for the **view**, not for the drawn punch. |
### 2.4 Where particles live (this was the missing piece)
`ParticleEmitter::EmitParticle` @0x0051d126:
```c
Particle::Init(...);
CPhysicsObj::AddPartToShadowCells(this->physobj); // 0051d126
```
**Every emitted particle is registered as a `CPhysicsPart` in its owner
object's SHADOW CELLS.** It therefore draws at those cells' turns through
`DrawBlock` (pass 1 updates in-view cells' shadow objects and
insertion-sorts shadow parts by depth; pass 2 draws them via `DrawSortCell`
`DrawObjCell`) and through `DrawObjCellForDummies` for interior cells —
i.e. through the same `CPhysicsPart::Draw``DrawMesh` path as any mesh,
and so through the same per-subset alpha-delay branch.
`CPhysicsObj::ShouldDrawParticles` @0x0050fe60 is **not** a draw-order gate.
Its single caller is `ParticleEmitter::UpdateParticles` @0x0051d1a6 — the
**simulation** side. Body:
```c
if (m_bExaminationObject == 0) {
if (!(CYpt < arg2 /*degrade_distance*/)) return 0;
if (cell == 0) return 0;
if (cell->vtable->IsInView() == 0) return 0;
}
return 1;
```
### 2.5 Answers to (b), (c), (d)
**(b) Draw order relative to the clear and the seals**, interior root, `ov>0`:
```
sky ─┐
terrain (culled cells) │
per building: │ all inside LScape::draw → ALPHA QUEUED
FlushAlphaList(0f) │ (and drained at every building)
punch (far-Z, raw) │
look-in DrawCells │
shell │
outdoor objects+particles┘
────────── FlushAlphaList(0f) @005a4872 ────────── drain vs TRUE landscape depth
────────── m_nFrameStamp += 1 ────────── per-part dedupe RE-ARMED
────────── Clear(z) @005a48a9 ────────── gated on portalsDrawnCount
────────── exit seals @005a49b7 ────────── own-depth panels at apertures
DrawEnvCell per flood cell ─┐
DrawObjCellForDummies per flood cell (objects+their │ ALPHA QUEUED
particles) ─┘
────────── FlushAlphaList(0f) @00453b8b ────────── drain vs interior depth + seals
```
**(c) How the list drains.** Two arrays, no sort, insertion order, drained at
the four sites above; each drain zeroes its counts. The depth in place at
each: *pre-punch* = everything drawn so far this landscape walk; *pre-clear*
= complete landscape depth including punched apertures and whatever look-in
content refilled them; *frame-end* = cleared depth seals interior shells
interior objects.
**(d) Depth at final drain.** At the **aperture** the seals own it (portal
surface's own projected depth, written `DEPTHTEST_ALWAYS` + z-write after the
clear). At the **vista** (everything the landscape painted) depth is `1.0`
the clear erased it — and only **colour** survives. That is precisely why
retail *must* drain the landscape alpha before the clear: any landscape-scope
alpha that survived into the interior scope would z-pass across the whole
vista. Retail's guarantee is structural: after `FlushAlphaList(0f)` at
`005a4872` **the list is empty**, so nothing can.
---
## 3. DAT geometry findings (installed DATs, read live)
Run: `tools/A8CellAudit/bin/Release/net10.0/A8CellAudit.exe portals <cell>`
(already built; no compilation performed).
### 3.1 Landblock `0xF418` inventory
- **88 outdoor objects** `0xCF418000``0xCF418057`. **Ordinals 17 are model
`0x0200085A`** — the spawn-marker Setup that retail never draws (see §6).
- **9 buildings** `0xCF418058``0xCF418060`. Four carry portals:
`0x01001FB0`→cells 0100-0102, **`0x01001FB2`→0103,0104**,
**`0x01001FB3`→0106-010B**, `0x01001FB7`→0112-0115.
- **24 EnvCells** `0xF4180100``0xF4180117`.
### 3.2 The cells that matter
**`0xF4180104`** (the owner's teleloc), `localAABB min=(-5.70,-12.00,9.80)
max=(5.02,12.00,20.00)`:
| portal | other | flags | polyId | verts | plane N | centroid·N |
|---|---|---|---|---|---|---|
| 0 | `0xFFFF` EXIT | 5 | 36 | **4** | (0, 1, 0) | **12.000** |
| 1 | `0xFFFF` EXIT | 5 | 39 | **7** | (1, 0, 0.06) | 5.028 |
| 2 | `0xFFFF` EXIT | 5 | 37 | **4** | (0, 1, 0) | **12.000** |
| 3 | `0x0105` | ExactMatch, PortalSide | 40 | 8 | (0, 0, 1) | 5.100 |
| 4 | `0xFFFF` EXIT | 5 | 38 | **7** | (1, 0, 0.06) | 5.035 |
**This refutes slice 6's stated premise.** Portals 0 and 2 are 4-vertex
rectangles sitting exactly on the cell's ±Y faces at ±12.00 m — i.e. **the
entire 10.7 m × 10.2 m end faces**. Portals 1 and 4 are 7-vertex arches on
the ±X faces at ≈±5.03 m. This is an open-sided gallery, not a set of thin
seam bands. The "seam band" the PV campaign observed is a **screen-space**
artifact of how our clip slices are built, not an authored polygon shape.
**`0xF4180106`** — the falls' owner cell, `localAABB min=(-5.49,-12.00,9.80)
max=(5.23,-6.00,20.00)`: 3 portals — interior →`0x0110` (poly 78, 9 verts),
interior →`0x010F` (poly 77, 9 verts), and **one exit** (poly 76, **4 verts**,
N=(0,1,0), centroid·N = 3.000, flags 7). One 4-vertex outdoor aperture.
**`0xF418010F`**: 2 portals, both interior (→`0x0107`, →`0x0106`), **no
exits**.
### 3.3 The exit-portal flag
Across all 24 cells of `0xF418`: every `other == 0xFFFF` portal has flags
**5 or 7** (bit `0x4` always set); every interior portal has flags **1 or 3**
(bit `0x4` never set). Bit `0x4` ≡ "leads outside" — exactly as
`claude-memory/project_pview_visibility_campaign.md` records. Our tree
detects exits purely by the `0xFFFF` sentinel and never reads bit `0x4`
(`src/AcDream.App/Rendering/Walk/WalkCellFactory.cs:93`); the two are
redundant here, so this is correct-by-accident but not a defect.
### 3.4 The falls' identity — PINNED
`%LOCALAPPDATA%\Temp\fw3-falls-probe.log`, aggregated:
```
10303 [walk-part] cell-owners n=6 ids=[4f418012,4f418013,4f418014,4f418015,f4180106,f418010f]
192 [walk-part] cell-owners n=5 ids=[4f418012,4f418013,4f418014,4f418015,f4180106]
```
`{4f418012, 4f418013, 4f418014, 4f418015}` **never appear without cell
`f4180106`**. `InteriorEntityIdAllocator.Base` mints
`0x40000000 | (lbX<<20) | (lbY<<12)` = `0x4F418000` for landblock `0xF418`
(`src/AcDream.Core/World/InteriorEntityIdAllocator.cs:47-48`), and the
`0xC…` sibling namespace covers landblock outdoor statics. So the falls are
**four EnvCell static objects of `0xF4180106`** — confirmed interior-cell
statics, not landblock objects and not unattached emitters.
Corroborating: the `[walk-emit]` interior-unattached dump added at slice 5
**never printed a single line** in any of the three gate logs
(`grep -c walk-emit` = 0 in `fw4-emit-probe.log`, `fw4-slice5-gate.log`,
`fw4-slice6-gate.log`) — the loop body is over an empty list. The
unattached-emitter theory is dead.
**Cross-check against retail:** `terrace-edge.log` (retail's own walk at the
#456 pose) punches `DC pv=009d4b08 ov=0 n=2: f4180106 f418010f` every frame,
and `cathedral-arrival.log` roots at `DI f4180106` on all 39 frames — so
retail reaches this exact cell through the same two routes.
**UNVERIFIED:** the Setup/GfxObj ids of the four falls objects. The audit
tool has no stab-dump verb and I did not build one. Also UNVERIFIED: where
the lake **water surface** geometry lives (landcell terrain vs a separate
surface). Both are cheap to establish and both matter to the fix plan.
---
## 4. Divergence table (ranked)
Rank = (how load-bearing for this defect) × (confidence it is real).
| # | Divergence | Retail | acdream @ `bb9212c7` | Evidence |
|---|---|---|---|---|
| **D1** | **Per-building alpha barrier is partial, not full** | `FlushAlphaList(0f)` — drains the *entire* list before every punch pass | `AlphaBarrier``FlushLandscapeAlphaFartherThan(d)` — drains only entries with `ViewerDistance >= d` | decomp `0x0059f30b`; `WalkFrameDriver.cs:655`; `RetailPViewPassExecutor.WalkLeaf.cs:246-247`; `RetailPViewPassExecutor.cs:649-650`; `RetailAlphaQueue.cs:261-278` |
| **D2** | **Clear + seals fire unconditionally** | both strictly inside `if (outside_view.view_count > 0)` | emitted unconditionally at `OnInteriorFloodDrawTurn`, doc-commented as deliberate | decomp `0x005a4852``0x005a49b7`; `WalkFrameDriver.cs:690-704` ("both unconditional … whether or not a landscape turn just ran") |
| **D3** | **The punch is clipped, with a silent fall-open** | `DrawPortalPolyInternal(esi, 1)` on the **raw** polygon; no view clip, no scissor | clipped to `slices[activeViewIndex].Planes`; when the index is out of range it silently falls back to *no* planes | decomp `0x005a5a7b`; `RetailPViewPassExecutor.WalkLeaf.cs:145-158` |
| **D4** | **Interior-root terrain has no `in_view` cull** | `draw_check_blocks`/`landcell_check` mark per-block and per-24 m-cell visibility against the exit views; only marked cells draw | `_terrain.Draw(camera, frustum, clipPlanes: default, ndcClipAabb: full)` — every frustum-visible landblock slot draws whole | decomp `0x00505f80` / `0x005050a0`; `RetailPViewPassExecutor.WalkLeaf.cs:99-110`; `TerrainModernRenderer.cs:202-242` |
| **D5** | **The alpha drain sorts** | no sort — insertion order is the walk order; `AddMeshToAlphaList` stores no depth key | `SortRetailOrder()` radix-sorts by viewer distance at every flush | decomp `0x0059d2e0`, `0x0059c230`; `RetailAlphaQueue.cs:162, 269` |
| **D6** | **Marker degrade admission absent** | `gfxobj[deg_level] == 0` ⇒ part not drawn; spawn markers author a nothing-ladder and are never drawn at any distance | no per-entity degrade selection on this branch | `git merge-base --is-ancestor fce250ec bb9212c7`**NO**; walk degrade applies only to `WalkBuilding` (`WalkBuildingFactory.cs:87-120`) |
| **D7** | **"Emit exactly once" invariant** | `m_nFrameStamp += 1` *after* the pre-clear drain deliberately re-arms the per-part dedupe so an object drawn outdoors draws **again** in the interior pass | slices 4/5 subtract pre-clear owners from both last-pass arms so each owner emits exactly once per frame | decomp `0x005a4886`; `RetailPViewRenderer.cs:2184-2185, 2322-2324` |
| **D8** | **Straddle heuristics instead of shadow-cell membership** | an object's parts live in every cell its sphere overlaps (`AddPartToShadowCells`), and draw at each of those cells' turns | `SphereStraddlesExitPlane(cell, c, r)` geometric test over flood cells | decomp `0x0051d126`; `RetailPViewRenderer.cs:597-626`; `WalkPView.cs` (slice 5) |
| **D9** | **Particle visibility is one frame stale and now over-wide** | `cell->IsInView()` is the *current* frame's flag | `TerrainModernRenderer.VisibleCellIds` + `InViewCells` are marked at frame end and consumed next frame; slice 6 widened the terrain set to the whole frustum | `WorldSceneRenderer.cs:263-264, 441-447`; `TerrainModernRenderer.cs:225-234` |
| D10 | Exit flag `0x4` never read | flag `0x4` ≡ leads-outside | detected only via the `0xFFFF` sentinel | `WalkCellFactory.cs:93`; A8CellAudit flags 5/7 vs 1/3 — **redundant here, benign** |
D1D3 are the ones that can plausibly produce "alpha paints across pixels
whose true occluder is in front of it". D4 and D9 are the ones that can
plausibly produce the **new** post-slice-6 report (#3, "particles within the
cathedral are also wrong"). D6 is the whole of the "stalks" half.
---
## 5. Per-commit verdicts
### `37febd1f` — FW4 slice 1, interior outside-view slices from the walk
**KEEP.** The diagnosis is sound and the mechanism is retail's: retail has
exactly one visibility structure, and having the walk decide *whether*
terrain draws while `PortalVisibilityBuilder` decided *where* is a state
that retail cannot produce. `ClipFrameAssembler.ReassembleOutsideViewFromWalk`
is the right seam. The punch-fan index aliasing repair is real.
**Correction owed:** it left the *silent* fall-open at
`RetailPViewPassExecutor.WalkLeaf.cs:146-148` — an out-of-range
`activeViewIndex` produces an unclipped punch with no log. That violates the
campaign's own binding rule 3 ("fail loud, never silently drop") and is the
same bug class the commit was fixing.
### `c40aecfc` — FW4 slice 2, exit seals stamp the walk's own flood
**KEEP the mechanism; the stated diagnosis was wrong.** Sealing from
`cell_draw_list` is exactly `0x005a49b7`, and taking that list from the walk
rather than the old apparatus is correct. But the commit message's causal
claim — "the falls … submit POST-depth-clear on interior roots … the drain
z-passed across the unsealed opening" — is falsified by its own successor
evidence: after slices 45 the falls submit and drain *pre*-clear and the
symptom is unchanged.
**Corrections owed:** (i) seals must be gated on `ov>0` (D2); (ii) retail's
seal is the authored polygon under the cell's `setup_view`, not a hardware
slice clip — the "old assembly's per-cell slice clips where present" path is
an invented hybrid; (iii) the commit says it seals "unclipped where the old
apparatus had no entry (benign)" — that inconsistency means two different
seal geometries exist in one frame depending on an unrelated data structure's
contents.
### `5f7ccdea` — FW4 slice 3, outside-stage predicate reads the walk flood
**KEEP; wrong target.** Feeding `DynamicDrawsInOutsideStage` the walk's
`VisitedCells` instead of the old flood is right for the same one-visibility-
structure reason. But the commit identifies "the same two emitter entities
(f4243/f4285)" as the falls. The probes show `f4243`/`f4285` are **dynamic
entities of radius ≈9.8-10 m** parented at `f4180104`/`f4180106`
(`fw4-slice3-gate.log`, `fw4-slice6-gate.log`: `[walk-dyn] … id=f4243
parent=f4180104 r=9.8 outside=1 cone=1`), while the falls are the four
`4f41801[2-5]` cell statics. Slice 3 fixed a real desync for the wrong
objects, and its success criterion was therefore never testable.
### `f3a03efc` — FW4 slice 4, straddler dynamics emit pre-clear, once
**REVERT.** Three independent problems:
1. **Wrong objects** — same misidentification as slice 3.
2. **Invented rule.** Retail has no "exit-plane straddler" concept. An
object's parts are in every shadow cell its sphere overlaps
(`AddPartToShadowCells` @0x0051d126); membership, not a plane test,
decides where it draws.
3. **Contradicts the decomp.** The commit's core invariant is "every owner
emits exactly once". Retail deliberately bumps `m_nFrameStamp` at
`0x005a4886`*after* the pre-clear drain — precisely so that a part
drawn during `LScape::draw` **draws again** in the interior pass. The
`ExceptWith(_preClearParticleOwnerScratch)` subtractions at
`RetailPViewRenderer.cs:2184` and `:2322` implement the opposite of
retail's rule.
### `0d6cd5c0` — FW4 slice 5, straddling cell statics' particles emit pre-clear
**REVERT, but keep the insight.** The retail citation is the right one —
`DrawBlock` pc:430056-430064 / the shadow-cell draw *is* how an object
overlapping outdoor landcells reaches the landscape scope. The implementation
is not that rule: it is `SphereStraddlesExitPlane` over the walk's **flood**
cells (`RetailPViewRenderer.cs:604-626`), gated with
`if ((cellId & 0xFFFF) < 0x100 || _lookInCellIds.Contains(cellId)) continue;`
— it **explicitly skips look-in cells**. On the failing frames
`0xF4180106` is a *look-in* cell (`fw4-slice6-gate.log:12786` shows it in
`walkCells` while the root flood at `:10352` does not contain it), so this
slice does not touch the falls at all. Its own probe evidence shows the falls
arriving on the `cell-owners` route, not the `static-owners n=12
ids=[4f418007…]` straddler set it created.
### `bb9212c7` — FW4 slice 6, interior roots draw landscape unclipped
**KEEP THE SHAPE, FIX THE CULL, RETRACT THE PREMISE.**
- **Right:** collapsing slice 1's per-view terrain fan to **one** terrain
turn is a genuine correction — `LScape::draw` runs its block walk once, and
the active views feed only `draw_check_blocks`. Removing the hardware clip
and scissor is also retail-correct ("retail never clips ordinary meshes per
view").
- **Wrong premise:** "at the #456 cathedral seam those authored portal polys
are thin BANDS far narrower than the real opening" is **refuted by the
DAT** (§3.2): `0xF4180104`'s exits are full ±12.00 m faces and 7-vertex
arches; `0xF4180106`'s exit is a 4-vertex face. The narrow bands are a
property of our clip-slice construction, not of the authored geometry —
which means the *slice builder* is the thing to look at, and slice 6
papered over that by removing the consumer instead.
- **Regression risk:** it replaced "clipped too tight" with "**not culled at
all**". Retail restricts the landscape to `in_view` landcells; the walk
already computes exactly that set (`RetailFrameWalk.DrawLandscape` calls
`landscape.CheckBlocks(ctx.CyPlane, activeViews)` at
`RetailFrameWalk.cs:152` and emits per-cell turns), and the leaf renderer
**ignores it**. Two consequences: terrain colour is painted over the whole
frustum before the clear (so any hole in the interior repaint shows world
where retail would show only the aperture), and
`TerrainModernRenderer.VisibleCellIds` — which feeds next frame's
`ShouldDrawParticles` cell-in-view gate — is widened to the whole frustum.
Both are live candidates for the owner's new report #3.
---
## 6. The "stalks" — a separate, already-solved defect
The lake-edge marker "stalks" are not part of the falls story and do not
need a new mechanism. `fce250ec` ("port retail marker-class degrade
admission") implements the decomp rule — `get_degrade` @0x0051E4B0 selects a
per-part LOD off the CYpt viewer distance, and both `DrawBuilding`
@0x0059f2a0 (`gfxobj[deg_level] != 0`, verified above at `0059f2d3`) and
`CPhysicsPart::Draw` @0x0050D7A0 skip the draw when the selected id is 0;
spawn markers author `{self at MaxDist=0, id 0 to infinity}` and are
therefore **never** drawn by retail at any distance.
`git merge-base --is-ancestor fce250ec bb9212c7` returns **NO**. That commit
lives only on the quarantined `claude/cathedral-rendering-debug-47a417`.
Nothing on the FW branch replaces it: the FW1 degrade work
(`e23a589a`, `c1a029ed`, `b3ac5872`, all ancestors) selects a building's
drawing-BSP inside `WalkBuilding` only (`WalkBuildingFactory.cs:87-120`,
`WalkBuilding.cs:101-114`); no degrade gate is applied to cell statics or
dynamics anywhere in `src/` (`grep -n "Degrade" Walk/*.cs` and
`WalkStaticStreamPopulator.cs` — zero hits in the populator).
The DAT confirms the class is present here: ordinals 17 of landblock
`0xF418`'s outdoor object list are model `0x0200085A`, the known marker
Setup.
**Caveat, honestly flagged:** the prompt describes the observed stalks as
"server-spawned weenies, e.g. entities parented at `0xF418010D`". Weenies
carry their own Setup, so whether restoring `fce250ec` covers *these*
particular stalks depends on the Setup ACE gives them.
`claude-memory/project_pview_visibility_campaign.md` already flags this:
"If stalk-like objects still show at a gate, they're server weenies with a
different model: identify by guid." **UNVERIFIED** until someone logs the
guid → Setup → DIDDegrade chain for one visible stalk.
---
## 7. Why the symptom survived all six fixes
This is a method failure. Five specific causes, each with evidence:
1. **The object was never identified before the fixes started.** Slice 2
called the falls "cell-owners route" emitters, slice 3 named
`f4243`/`f4285` (dynamics), slice 4 called them "interior-parented
exit-plane straddler DYNAMICS", slice 5 called them "EnvCell STAB-LIST
statics". Only slice 5 was close, and even it used the wrong sense of
"stab" — in the DAT, `numStabs`/`stab_list` is the **visible-cell** list
(`references/ACE/Source/ACE.DatLoader/FileTypes/EnvCell.cs:36-68`;
our `WalkCell.StabList` is cell ids, `WalkCellFactory.cs:105`), while the
placed objects are `StaticObjects`. The four falls were identifiable from
`fw3-falls-probe.log` — which existed **before slice 2** — in one grep.
2. **The depth buffer was never measured.** Every slice reasoned about depth
from code reading. Slice 6's own message asserts "the depth they test
against is EMPTY" as a conclusion, not a measurement. The probe apparatus
answers *when* (`phase=pre/post`) and *who* (`ids=[…]`) but has no verb
for *what depth is at these pixels*. The single question that would
discriminate between D1, D3, D4 and "the particle's own depth is wrong"
has never been asked of the machine.
3. **Once `phase=pre` was achieved, the hypothesis space collapsed.** By
slice 5 the falls demonstrably drained pre-clear
(`fw4-slice6-gate.log:12793-12801`). At that point the ordering axis was
closed and every further ordering change was necessarily inert. Slice 6
correctly inferred "then the depth must be empty" but then guessed at
*why* instead of measuring, and guessed a DAT fact that is false.
4. **Two retail citations were mis-scoped, and nothing caught it.** Slice 4's
"emit exactly once" is the opposite of `m_nFrameStamp += 1` @0x005a4886.
Slice 6's "thin bands" is contradicted by four lines of A8CellAudit
output from an already-built tool. The campaign's binding rule 2 ("grep
named first … pseudocode notes before C#") was followed for the *shape*
of the mechanism and skipped for the *specific numbers*.
5. **Six commits in 105 minutes with no gate between them.** `37febd1f`
18:21 → `bb9212c7` 20:06. Each was validated by hermetic tests only
(6,762/0), which cannot see a depth-composition defect. Each slice's
premise was built on the previous slice's *unvalidated* conclusion, so the
errors compounded: slice 4 built on slice 3's misidentification, slice 5
built on slice 4's invented rule, slice 6 built on slice 5's non-effect.
The campaign's own rule 4 ("the transition checklist is a standing
self-run gate … before the owner sees it") was not exercised between
slices.
---
## 8. Recommended fix plan
**Ground rule: no code change until Stage 0 produces a number.** The
investigation has spent six rounds on plausible mechanisms; the cheapest
remaining move is to make the frame observable.
### Stage 0 — instrument (no behaviour change)
**P1 — depth checkpoint readback.** Under a new
`ACDREAM_PROBE_WALK_DEPTH=1` (documented in `docs/launch-options.md` in the
same commit, per the binding rule), capture the depth attachment at three
points in an interior-rooted frame: (i) after the terrain turn, (ii) after
the last building turn, (iii) immediately before `passes.FlushLandscapeAlpha()`
in the `clearInteriorDepth` closure (`RetailPViewRenderer.cs:356`). Emit a
downsampled 64×36 depth grid per checkpoint plus a per-pixel min/max.
*Falsifiable prediction (D1/D3):* at checkpoint (iii), the screen cells where
the falls bleed hold ≈`0.999999` (punched far-Z), while checkpoint (i) holds
real terrain depth at the same cells. If (iii) already holds real terrain
depth, **D1 and D3 are dead** and the residual is the particles' own depth —
which the same dump measures by also logging each drained falls submission's
projected NDC z.
**P2 — punch census.** Per punch fan, log building id, portal index,
`activeViewIndex`, `slices.Length`, whether `planes` was empty (the silent
fall-open), and the fan's screen-space AABB.
*Falsifiable prediction:* on failing frames at least one punch of building
`0x01001FB3` (the owner of `0xF4180106`) is emitted with `planes` empty and a
screen AABB covering the falls' region.
**P3 — one grep, zero code:** dump `EnvCell 0xF4180106`'s `StaticObjects`
(a `stabs` verb on `A8CellAudit`, ~20 lines) to get the four falls objects'
Setup/GfxObj/DIDDegrade ids, and do the same for one visible stalk's weenie
Setup. This closes the two UNVERIFIED items in §3.4 and §6 and tells you
whether D6 alone fixes the stalks.
### Stage 1 — decomp-provable repairs (each its own commit, each gated)
| id | change | retail pin |
|---|---|---|
| **R1** | `AlphaBarrier` becomes a **full** `FlushLandscapeAlpha()`. Retire `FlushLandscapeAlphaFartherThan` from production. | `0x0059f30b``FlushAlphaList(0f)`, unconditional, inside the degrade gate |
| **R2** | Gate `ClearInteriorDepth` **and** `ExitSeals` on `outsideView.ViewCount > 0`; delete the "unconditional" doc claim at `WalkFrameDriver.cs:695-699`. | `0x005a4852` opens the block; `0x005a48a9` and `0x005a49b7` are both inside it |
| **R3** | Punch the **unclipped** authored polygon; delete the `slices[activeViewIndex]` clip. Make an out-of-range `activeViewIndex` throw, not silently fall open. | `0x005a5a7b` draws `esi` (the raw `CPolygon*`); the clip result feeds only `copy_view` |
| **R4** | Terrain draws the walk's own `in_view` landcell set (already computed by `WalkLandscape.CheckBlocks`), whole and unclipped. Retire the frustum-only interior path. | `0x00505f80` `draw_check_blocks`, `0x005050a0` `landcell_check` |
| **R5** | Drop `SortRetailOrder` once the walk owns submission order end to end; drain in insertion order. | `0x0059d2e0` has no sort; `0x0059c230` stores no depth key |
| **R6** | Cherry-pick `fce250ec`'s `RetailDegradePolicy` + `WbMeshAdapter.IsNeverDrawnByDegrade` onto this branch and apply it in the walk's static populator and the dynamic classifier. | `get_degrade` @0x0051E4B0; `gfxobj[deg_level] != 0` at `0x0059f2d3` and `CPhysicsPart::Draw` @0x0050D7A0 |
| **R7** | Replace `SphereStraddlesExitPlane` + the "emit exactly once" subtractions with retail's rule: an object submits at **every cell it is a shadow member of**, with a frame-stamp dedupe that is **re-armed after the pre-clear drain**. | `AddPartToShadowCells` @0x0051d126; `m_nFrameStamp += 1` @0x005a4886; `GetDrawnThisFrame`/`SetDrawnThisFrame` @0x0059f388 |
**Ordering:** R2 and R3 are cheap and decomp-unambiguous — land them first;
they may change the symptom on their own and they make P1/P2's readings
easier to interpret. R1 next (it is the mechanism D1 names). R4 next (it also
closes the D9 widening). R7 replaces slices 4 and 5 wholesale, so revert
`f3a03efc` and `0d6cd5c0` as part of it rather than layering. R5 and R6 are
independent and can land any time.
**Gate for each:** the two-checkpoint depth dump from P1 at the cathedral
pose, plus the campaign's own transition checklist (portal-in, Holtburg house
exit, teleport stopwatch, steady-state fps) — rule 4, which was skipped for
all six slices. Do **not** stack two of these before an owner look.
### What NOT to do
- Do not add a seventh ordering fix. The ordering axis is closed
(`fw4-slice6-gate.log:12793-12801`).
- Do not suppress the falls emitters, add a depth bias, force particle depth
writes, or scissor the drain. All are symptom-site workarounds and all are
already on the render digest's DO-NOT-RETRY list in spirit.
- Do not reintroduce per-view terrain clipping. Slice 6 is right that retail
culls rather than clips; the fix is to add the cull, not to restore the
clip.
---
## 9. Open questions
1. **What depth is actually at the failing pixels at the pre-clear drain?**
Unanswered. This is the whole investigation in one question and Stage 0/P1
answers it in one run.
2. **Setup/GfxObj/DIDDegrade of `0xF4180106`'s four static objects.**
UNVERIFIED — needs a `stabs` verb on `A8CellAudit` (no build of production
code required).
3. **Where does the lake water surface live** — landcell terrain polys, or a
separate surface with its own pass? If it is a separate translucent pass,
it shares the alpha list with the falls and their relative order is decided
by D5 (our sort) rather than by the walk. UNVERIFIED.
4. **The `DrawBlock` alpha-valve polarity** (`test ah,0x41` @0x005a19fc).
I read the idiom, not the raw bytes. Both readings make the valve inert at
default `flush`, so it is not load-bearing here, but the FW pseudocode doc
still lists it as owed and it should be closed from the binary.
5. **Are the observed stalks the marker class?** Depends on the weenie Setup
ACE assigns. If not, `fce250ec` will not cover them and the guid → Setup
chain must be logged live.
6. **Why are our clip slices "thin bands" when the authored polygons are full
faces?** Slice 6 removed the *consumer* of those slices for terrain, but
the slices still drive the punch fans and the dynamics apertures. If the
slice builder is producing bands from full-face polygons, that is a defect
in `ClipFrameAssembler`/`ClipPlaneSet`/`WalkScreenClip` that outlives this
bug. Worth one focused offline check against `0xF4180104` poly 36 (a
4-vertex ±12.00 m face) — project it and compare against the produced
slice.
7. **Does the one-frame lag in particle visibility matter here?**
`WorldSceneRenderer.cs:441-447` marks visible cells at frame end for the
next frame, while retail asks `cell->IsInView()` on the current frame.
Combined with slice 6's frustum-wide `VisibleCellIds`, this is the most
likely source of the owner's new "particles within the cathedral" report,
but I have no capture of that symptom to test against. UNVERIFIED.
---
### Appendix — commands used (all read-only)
```
git show --stat 37febd1f c40aecfc 5f7ccdea f3a03efc 0d6cd5c0 bb9212c7
git merge-base --is-ancestor fce250ec bb9212c7 # -> NO
tools/A8CellAudit/bin/Release/net10.0/A8CellAudit.exe portals F4180104|F4180106|F418010F
curl -s "http://127.0.0.1:8081/decompile_function?address=0x0059d2e0"
grep -n "FlushAlphaList|AddMeshToAlphaList|DrawPortalPolyInternal|ShouldDrawParticles" \
docs/research/named-retail/acclient_2013_pseudo_c.txt
grep/awk over %LOCALAPPDATA%\Temp\fw3-falls-probe.log, fw4-slice3-gate.log,
fw4-slice5-gate.log, fw4-slice6-gate.log, fw4-emit-probe.log
```