# 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 4–6 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 R1–R7, 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 ` (already built; no compilation performed). ### 3.1 Landblock `0xF418` inventory - **88 outdoor objects** `0xCF418000`–`0xCF418057`. **Ordinals 1–7 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** | D1–D3 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 4–5 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 1–7 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 ```