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>
This commit is contained in:
Erik 2026-08-30 20:52:14 +02:00
parent 7f500b97a3
commit 6d990d38b3
10 changed files with 1940 additions and 1 deletions

View file

@ -315,7 +315,7 @@ issue is closed, the strip was missed; delete both.
| `ACDREAM_PROBE_CHILD_CELL` | c4 route 7 | `=1` | gates one `[child-cell]` line per Runtime committed-child canonical-cell write in `RuntimeLiveEntitySessionController`, `RuntimeEntityObjectLifetime`, `RuntimeEntityDirectory` (parent/child guid, old/new cell, cause tag) | print-only | `PhysicsDiagnostics.ProbeChildCellEnabled` | | `ACDREAM_PROBE_CHILD_CELL` | c4 route 7 | `=1` | gates one `[child-cell]` line per Runtime committed-child canonical-cell write in `RuntimeLiveEntitySessionController`, `RuntimeEntityObjectLifetime`, `RuntimeEntityDirectory` (parent/child guid, old/new cell, cause tag) | print-only | `PhysicsDiagnostics.ProbeChildCellEnabled` |
| `ACDREAM_PROBE_CLIPROUTE` | "throwaway apparatus — strip once §4 ships" | `=1` | print-on-change `[clip-route]` / `[clip-route-disp]` / `[clip-route-scis]` lines: outside-slice clip routing, region-SSBO bytes, terrain-UBO head, actual GL/RHI scissor state | print-only | `RenderingDiagnostics.ProbeClipRouteEnabled` | | `ACDREAM_PROBE_CLIPROUTE` | "throwaway apparatus — strip once §4 ships" | `=1` | print-on-change `[clip-route]` / `[clip-route-disp]` / `[clip-route-scis]` lines: outside-slice clip routing, region-SSBO bytes, terrain-UBO head, actual GL/RHI scissor state | print-only | `RenderingDiagnostics.ProbeClipRouteEnabled` |
| `ACDREAM_PROBE_WALK_SHADOW` | Campaign FW3.2b-2 flip apparatus — dies with the flip commit | `=1` | runs the PRODUCTION retail frame walk per frame in SHADOW (no draws change) and prints one `[walk-shadow]` line per frame whose visited cell/building sets diverge from the old visibility path (the I5 dual-shadow pattern applied to the FW3 static cutover) | print-only; adds the per-frame CPU cost of the shadow walk while set | `RenderingDiagnostics.ProbeWalkShadowEnabled` | | `ACDREAM_PROBE_WALK_SHADOW` | Campaign FW3.2b-2 flip apparatus — dies with the flip commit | `=1` | runs the PRODUCTION retail frame walk per frame in SHADOW (no draws change) and prints one `[walk-shadow]` line per frame whose visited cell/building sets diverge from the old visibility path (the I5 dual-shadow pattern applied to the FW3 static cutover) | print-only; adds the per-frame CPU cost of the shadow walk while set | `RenderingDiagnostics.ProbeWalkShadowEnabled` |
| `ACDREAM_PROBE_WALK_ROOT` | FW3 visual-gate apparatus (stairwell-transition + cathedral-falls defects) — dies with those fixes | `=1` | after every interior/outdoor root flip, prints 8 `[walk-root]` lines (one per frame): viewer cell, root cell, camera-cell resolution, slice count, eye position, and the walk's visited cell list; also prints `[walk-part]` lines at every particle submission route (route name, owner ids, pre/post-depth-clear phase) and `[walk-alpha]` lines at every retail alpha-queue drain — together they pin which crossing frames root wrong, flood short, or submit particles after the interior depth clear | print-only; chatty near particle-heavy scenes while set | `RenderingDiagnostics.ProbeWalkRootEnabled` | | `ACDREAM_PROBE_WALK_ROOT` | FW3 visual-gate apparatus (stairwell-transition + cathedral-falls defects) — dies with those fixes | `=1` | after every interior/outdoor root flip, prints 8 `[walk-root]` lines (one per frame): viewer cell, root cell, camera-cell resolution, slice count, eye position, and the walk's visited cell list; also prints `[walk-part]` lines at every particle submission route (route name, owner ids, pre/post-depth-clear phase), `[walk-alpha]` lines at every retail alpha-queue drain, `[walk-dyn]` lines with each dynamic's outside-stage/viewcone verdicts (~every 120th product frame), and `[walk-portal]` lines with the walk PView's per-portal side/clip decisions (~every 90th frame) — together they pin which crossing frames root wrong, flood short, submit particles after the interior depth clear, or collapse an exit view at the portal walk | print-only; chatty near particle-heavy scenes while set | `RenderingDiagnostics.ProbeWalkRootEnabled` |
| `ACDREAM_PROBE_CONTACT_PLANE` | spike-only, 2026-05-20 | `=1` | gates one `[cp-write]` line per write to `CollisionInfo.ContactPlane*`/`LastKnownContactPlane*` fields (field, old→new, caller method via stack walk, source line); only logs on actual value changes | print-only, but performs a stack walk to identify the caller method when firing — real CPU cost per write while on (not just a string format); suppresses no-op writes to bound volume | `PhysicsDiagnostics.ProbeContactPlaneEnabled` | | `ACDREAM_PROBE_CONTACT_PLANE` | spike-only, 2026-05-20 | `=1` | gates one `[cp-write]` line per write to `CollisionInfo.ContactPlane*`/`LastKnownContactPlane*` fields (field, old→new, caller method via stack walk, source line); only logs on actual value changes | print-only, but performs a stack walk to identify the caller method when firing — real CPU cost per write while on (not just a string format); suppresses no-op writes to bound volume | `PhysicsDiagnostics.ProbeContactPlaneEnabled` |
| `ACDREAM_PROBE_ENT` | #138 | `="1"` | Traces the persistent player entity across teleport streaming churn: presence in the render draw-set flat view vs. survival of the dynamics cull, to distinguish "missing from draw set" vs "present but culled" | print-only, "Observation-only — emits no behavior change" (doc comment). `LogPlayerDynOnChange` dedupes by transition to avoid per-frame spam. Marked STRIP-once-root-caused (like the dense-town FPS apparatus). | `EntityVanishProbe.Enabled` (`EntityVanishProbe.cs:23-24`), issue #138-B | | `ACDREAM_PROBE_ENT` | #138 | `="1"` | Traces the persistent player entity across teleport streaming churn: presence in the render draw-set flat view vs. survival of the dynamics cull, to distinguish "missing from draw set" vs "present but culled" | print-only, "Observation-only — emits no behavior change" (doc comment). `LogPlayerDynOnChange` dedupes by transition to avoid per-frame spam. Marked STRIP-once-root-caused (like the dense-town FPS apparatus). | `EntityVanishProbe.Enabled` (`EntityVanishProbe.cs:23-24`), issue #138-B |
| `ACDREAM_PROBE_FLAP` | "throwaway apparatus — strip once the flap mechanism is confirmed" | `=1` | EVERY FRAME (unthrottled, not change-gated) while the camera root is indoor: `[flap]` from `PortalVisibilityBuilder.Build` (portal side-test/traverse/cull/projection) + paired `[flap-cam]` from `PhysicsCameraCollisionProbe`/`[flap-sweep]` (FindCameraCell resolution, eye positions) | print-only, but unthrottled per-frame `StringBuilder` allocation + `Console.WriteLine` on multiple call sites while indoor — heavy log volume/allocation under sustained indoor play; does not alter rendered output | `RenderingDiagnostics.ProbeFlapEnabled` | | `ACDREAM_PROBE_FLAP` | "throwaway apparatus — strip once the flap mechanism is confirmed" | `=1` | EVERY FRAME (unthrottled, not change-gated) while the camera root is indoor: `[flap]` from `PortalVisibilityBuilder.Build` (portal side-test/traverse/cull/projection) + paired `[flap-cam]` from `PhysicsCameraCollisionProbe`/`[flap-sweep]` (FindCameraCell resolution, eye positions) | print-only, but unthrottled per-frame `StringBuilder` allocation + `Console.WriteLine` on multiple call sites while indoor — heavy log volume/allocation under sustained indoor play; does not alter rendered output | `RenderingDiagnostics.ProbeFlapEnabled` |

View file

@ -0,0 +1,157 @@
# Cathedral review — code + probe-log audit (companion report)
Delivered 2026-08-30 by the code/log audit agent (separate from the
fable/opus mechanism reviews; this one audited the six commits, the exact
HEAD frame flow, and every `fw3-*`/`fw4-*` probe log of the day). The full
text below is verbatim from the agent's report.
## Executive synthesis
1. All six FW4 slices behave as written on frames where the walk yields
exit views — the post-slice-6 log shows the falls emitting pre-clear
and draining over full-screen terrain depth on those frames.
2. Two defect mechanisms visible in the FINAL log are untouched by all
six commits:
- **Anomaly 1 — `ov==0` frames:** 66 of 26,601 interior frames at the
terrace produced ZERO walk exit views → `RetailFrameWalk.DrawInside`
skips `DrawLandscape` entirely → no terrain color, no terrain depth,
no sky that frame. Everything drawn that frame (pre-clear alpha
included — 17 frames had the falls straddler set drain against an
EMPTY depth buffer) composites over stale color. These are the seam
frames. Slices 15 fixed staging; slice 6 fixed clipping WHEN the
landscape turn runs; nothing addressed the no-views case.
- **Anomaly 2 — cell `0xF418010D` never floods:** the connecting
portal from `f418010c` classifies `inview=1` (a FACING SURFACE, not
an armed opening) at the terrace viewpoint (`[walk-portal] init
cell=f418010c i=1 dest=f418010d d=-23..-50 pside=0 inview=1`), so
the walk never traverses into it: never sealed, always fails the
outside-stage flood gate (`outside=0` in every sampled `[walk-dyn]`
line since the probe existed), and its r≈10 m weenies (the STALKS:
f428a/f428b etc.) ride DynamicLast — post-clear meshes that z-pass
over every vista pixel no seal covers, on EVERY interior frame.
3. Combined with the fable review's oracle-trace finding (retail has ZERO
interior-rooted frames at the terrace — `terrace-center.log` and
`terrace-edge.log` contain no `DI` lines at all; the whole cathedral
arrives as `ov=0 n=1` building look-ins under an OUTDOOR root), the
remaining user-visible defects reduce to:
- ROOT DIVERGENCE A: our camera-cell resolution roots INTERIOR at the
terrace where retail roots OUTDOOR. An outdoor root has no clear, no
seals, no ov==0 case — the entire problem class does not exist in
retail's frame shape at these poses.
- ROOT DIVERGENCE B: the walk's portal side classification at
`f418010c → f418010d` (facing-surface vs opening — `PortalSide` vs
the sign of `d`, `WalkPView.cs:103-120`) — the same family as the
FW1 foundry F6779 residue.
4. UNVERIFIED (flagged, not papered over): whether the old apparatus's
flood contains f418010d (needed for the stalk meshes' DynamicLast
viewcone admission); the exact dat portal/panel geometry behind the 66
`ov==0` frames; the color-attachment load behavior behind slice 6's
"stale color" claim.
## Full audit text
### PART 1 — The six same-day commits (times: 18:21 / 19:22 / 19:28 / 19:37 / 19:58 / 20:06)
- `37febd1f` slice 1: interior outside-view slices from the walk
(`ClipFrameAssembler.ReassembleOutsideViewFromWalk`; PrepareClipFrame
moved below the walk block; Landscape event carries ViewCount; the
per-view TerrainSlice fan; `activeTerrainSliceCount` deleted;
`ACDREAM_PROBE_WALK_ROOT` added). 4 new materializer tests.
- `c40aecfc` slice 2: exit seals stamp the walk's own flood
(`WalkFrameDriver.InteriorFloodCells`; `DrawWalkExitPortalMasks` — the
walk decides WHICH cells seal, the OLD assembly still decides the
per-cell CLIP, unclipped fallback). Phase-tag probe apparatus added.
No test changes.
- `5f7ccdea` slice 3: `outsideStageFlood = walkDriver.VisitedCells`
passed to BOTH `DynamicDrawsInOutsideStage` call sites (renderer +
frame product). `[walk-dyn]` probe. No test changes.
- `f3a03efc` slice 4: interior roots keep outside-stage owners
(straddlers included) in the pre-clear union, record them in
`_preClearParticleOwnerScratch`, both DrawDynamicsLast arms subtract.
Outdoor keeps ExceptRoute(DynamicLast). No test changes.
- `0d6cd5c0` slice 5: the falls re-identified as EnvCell stab statics;
`SphereStraddlesExitPlane` extracted; straddling flood-cell statics'
owners submit pre-clear, post-replay cell union subtracts.
`[walk-emit]`/`[walk-portal]` probes. No test changes.
- `bb9212c7` slice 6: one TerrainSlice(0) fan; interior roots draw
terrain+sky UNCLIPPED (view-culled only); outdoor keeps the sliced
path. Latent: the bounds check at `WalkLeaf.cs:81-86` still throws if
the reassembly yields 0 slices while a Landscape turn fired. No test
changes.
Test posture: slice 1 shipped tests; slices 26 shipped zero test
changes (probe + production only).
### PART 2 — HEAD frame flow for an interior-rooted production frame
Setup: BeginFrame (phase:=pre) → OLD PortalVisibilityBuilder.Build →
BuildInteriorRootLookIns (old apparatus) → AssembleClipFrame +
AppendLookInClipFrames (old per-cell slices) → walk Collect (no GPU;
flood → optional Landscape turn iff `OutsideView.ViewCount > 0`
building turns → OnInteriorFloodDrawTurn: Clear/Seals events +
InteriorFloodCells + flood cell turns) → ReassembleOutsideViewFromWalk →
the ONE PrepareClipFrame → prepareCells = old look-in walk →
ViewconeCuller.Build (outside planes = walk-derived; per-cell planes =
OLD) → BuildAndBorrow with outsideStageFlood = walk VisitedCells →
routes: LookInObject (old-assembly slices), LandscapeOutsideDynamic
(walk slices + cone), DynamicLast (indoor dynamics; viewcone from OLD
per-cell planes — a cell with NO plane set returns false).
Replay (pre-clear only when a Landscape turn recorded): Sky (unclipped,
interior) → TerrainSlice(0) (unclipped, depth write ON) → interleaved
static/building/look-in mesh ranges + AlphaBarrier partial drains +
punch fans (stencil far-Z, clipped by the walk view pinned at emission)
→ ClearInteriorDepth event → closure: [static-owner particles +
slice-5 straddler statics + DrawLandscapeDynamicsPhase (EARLY-OUTS
ENTIRELY if OutsideViewSlices.Length == 0) ] → FlushLandscapeAlpha (the
pre-clear drain, full far→near over true landscape depth) →
ClearInteriorDepth (depth aspect only, color preserved; phase:=post) →
ExitSeals (walk InteriorFloodCells × old-assembly clips; TRUE depth,
ALWAYS+write, no color; cells not in the walk flood never seal) →
flood CellShell + statics (the interior repaint).
Post-replay: cell-owner particles (walk flood minus look-ins minus
straddlers; post, deferred) → DrawDynamicsLast (post-clear meshes,
old-plane viewcone; particles minus pre-set) → unattached-interior →
MarkVisibleCells(old look-in walk) → final alpha EndFrame drain
(far→near, depth LESS/no write vs clear+seal+repaint depth) →
ParticleVisibilityController admission = the union of both apparatuses.
Structural observations: the walk decides the seal CELL SET, the OLD
assembly still decides seal clips, look-in slices, and the DynamicLast
per-cell viewcone; the walk's thin exit views still gate all dynamics
staging; if ViewCount == 0 no Landscape turn fires at all while clear +
seals + repaint still run.
### PART 3 — Probe-log findings
Log-era map: `fw3-walk-root-probe.log` 18:09 = pre-slice-1 AND NOT the
cathedral (all roots 0xA9B4… — the stairwell investigation);
`fw3-falls-probe.log` 19:23 ≈ slice-2; `fw4-seal-gate` 19:27;
`fw4-slice3-gate` 19:32; `fw4-slice4-gate` 19:44; `fw4-portal-probe`
19:50 (slice-4 + uncommitted slice-5 probes); `fw4-slice5-gate` ≈19:59;
`fw4-slice6-gate` 20:23 = the ONLY post-slice-6 run.
- The falls emitters are `4f418012..15` + `f4180106` (InteriorEntityId-
Allocator statics of cell 0xF4180106), riding a per-look-in-cell
`DrawCellParticles` union; `viewer=0xF4180106` appears in ZERO
[walk-root] lines (the 8-frame post-flip sampler never caught it).
- Post-slice-6 frames WITH exit views: falls pre-clear, no
cell-owners/dyn-owners post — the staging repairs work as written.
- ANOMALY 1: 66/26,601 interior frames with NO landscape turn (no
SkyPreScene, no outdoor static-owners, no unattached-outdoor) — the
ov==0 shape; all 66 carry `dyn-owners n=2 ids=[f4243,f4287]
phase=post`, and 17 carry a pre-clear falls drain against EMPTY depth.
- ANOMALY 2: cell f418010d in ZERO walkCells lists across all 7
cathedral logs; `init cell=f418010c i=1 dest=f418010d d=-23..-50
pside=0 inview=1`; its dynamics f428a/f428b always `outside=0`; their
particles appear in NO dyn-owners set — the visible stalks are the
MESHES, drawn post-clear.
- ANOMALY 3: 17 seam FLIPs at nearly identical eye positions
(interior 0xF4180104 eye=(31.09,61.34,172.05) vs outdoor 0xF418000B
eye=(30.90,63.04,172.05)).
- ANOMALY 4: `[walk-emit]` empty in ALL logs — the interior-unattached
route carried no emitters (the falls are attached statics).
- ANOMALY 5: the surviving exit views are thin (n=4 clips on portals
j=1/2/4; j=0 often n=0) — consistent with the authored polys being
narrow bands.

View file

@ -0,0 +1,93 @@
# Cathedral review — DAT-format facts (companion report)
Delivered 2026-08-30 by the DAT-format verification agent (decompiled the
actual `Chorizite.DatReaderWriter 2.1.7` NuGet binary with ilspycmd and
cross-checked against `docs/research/named-retail/acclient.h`). Key facts
for the cathedral synthesis; flagged caveats preserved.
## Sourcing caveats
- Our DAT parser is the CONSUMED NuGet `Chorizite.DatReaderWriter 2.1.7`
(no vendored source; facts below come from decompiling the exact linked
DLL).
- `references/WorldBuilder` is an uninitialized submodule in THIS worktree
(read from the main worktree at a different SHA — flagged).
- `references/ACViewer` does not exist in either worktree (claims about
ACViewer: UNVERIFIED).
## EnvCell record (read order, decompiled Unpack)
Flags (int) → self-Id (uint, duplicate) → NumSurfaces (byte) → NumPortals
(byte) → NumVisibleCells (ushort) → Surfaces[]ushort) → EnvironmentId
(ushort) → CellStructure (ushort) → Position (Frame) → CellPortals[] →
VisibleCells[] (ushort) → [HasStaticObjs 0x2] StaticObjects (count +
Stab{Id,Frame}[]) → [HasRestrictionObj 0x8] RestrictionObj.
`EnvCellFlags`: SeenOutside=0x1, HasStaticObjs=0x2, HasRestrictionObj=0x8.
**0x4 is unclaimed.**
Retail cross-check: `CEnvCell : CObjCell` (acclient.h:32072-32091) maps
1:1; retail's `stab_list` lives on the BASE `CObjCell`
(acclient.h:30914-30932) and is the **PVS list of CELL IDS** — NOT placed
objects. The DatReaderWriter `Stab{Id,Frame}` placed-object struct is a
coincidental homonym (zero matches for `Stab` in the named-retail header).
## CellPortal / BuildingPortal
`CellPortal { PortalFlags Flags; ushort PolygonId; ushort OtherCellId;
ushort OtherPortalId; }` — `PortalFlags: ExactMatch=1, PortalSide=2`.
**No 0x4 member anywhere** (package, our code, WorldBuilder, and retail's
own `CCellPortal` decode exactly two semantic fields: portal_side,
exact_match). NOTE: this contradicts the earlier campaign note
"CCellPortal DAT flag 0x4 = leads outside" — the outside link is the
`OtherCellId == 0xFFFF` sentinel, not a flag bit.
- ExactMatch consumers: `PortalVisibilityBuilder.cs:1168-1183` (skips
reciprocal clip — decomp:433689), Walk factories
(`WalkBuildingFactory.cs:76`, `WalkCellFactory.cs:94`).
- PortalSide convention: `(Flags & 0x2) == 0 → normal points INTO the
owning cell` (`CellPortal.cs:23-24`, `PortalInfo.cs:22-44`);
`WalkCellFactory.cs:93`: `PortalSide = (Flags & 0x2) != 0 ? 0 : 1`.
- `OtherCellId == 0xFFFF` = the outdoor-exit sentinel (18+ consumers);
the walk world remaps it to `0xFFFFFFFFu`.
`BuildingPortal` (BuildingInfo.Portals) is a DISTINCT dat type — carries
its own **packed StabList of cell ids** (retail `CBldPortal` with
num_stabs/stab_list + `sidedness`). The walk's `WalkBuildingFactory.cs:81`
reads it directly; the older `Wb.BuildingLoader` path DISCARDS it and
recomputes by BFS.
## LandBlockInfo
`NumCells → Objects[Stab] → Buildings[BuildingInfo{ModelId, Frame,
NumLeaves, Portals[BuildingPortal]}] → [flag] RestrictionTable`.
Confirmed field-for-field; consumer `LandblockLoader.cs:69-103`.
## Water / waterfall (part d)
- `LandDefs::WaterType` is COMPUTED client-side from terrain paint (types
0x10-0x14 WaterRunning..WaterDeepSea); AcDream already computes it for
physics (`TerrainSurface.cs:92-128`). It is NOT packed in the records.
- `SurfaceTexture.Textures` is a multi-frame list (the structural shape of
a flip-book animation) but EVERY consumer in both repos freezes one
fixed index (`Textures[0]`, WB `[^1]`) — **no water/texture animation is
implemented anywhere**; whether retail animates via this list is
UNVERIFIED.
- Which object IS "the waterfall" near the cathedral: UNVERIFIED from
source — two old bug comments treat it as OUTDOOR landscape-alpha
content (`PortalVisibilityBuilder.cs:730-736`,
`RetailPViewRenderer.cs:714-726`), consistent with a translucent stab —
but note the live probes identified the terrace falls PARTICLE owners as
EnvCell StaticObjects of cell 0xF4180106 (ids 4f418012-15), which is
the `HasStaticObjs` list above; both can be true (mesh stab outdoors +
emitter stabs in cells).
## Notable corrections this report makes to campaign lore
1. "DAT flag 0x4 = leads outside" (PV campaign memory) — REFUTED: no 0x4
exists; outside = OtherCellId 0xFFFF.
2. "stab list" ambiguity — retail stab_list = PVS cell ids on CObjCell;
placed objects are a different structure; docs/comments conflating the
two should be read carefully.
3. Building portals carry authored per-portal cell StabLists in the dat —
the old Wb path recomputes what the dat already provides.

View file

@ -0,0 +1,111 @@
# Cathedral review — F418 authored DAT geometry (companion report)
Delivered 2026-08-30 by the DAT-geometry agent: fresh byte-exact parsers
over the installed `client_cell_1.dat`/`client_portal.dat` (every record
parsed with 0 bytes left over). Ground truth for the synthesis. Condensed;
citations and caveats preserved from the agent's full text.
## The landblock (LandblockInfo 0xF418FFFE)
24 EnvCells (0xF41801000x0117), 88 objects, 9 buildings. The cathedral
complex is authored in the BUILDINGS list: 0x01001FB0 (north hall, cells
0x1000x102), 0x01001FB2 (the 0x1030x105 colonnade block), 0x01001FB3
(the main cathedral, cells 0x1060x111, 13 building portals), 0x01001FB7
(south hall 0x1120x115), 0x01001FD3 (sealed annex 0x1160x117), plus
four portal-less shells.
## THE FALLS — identity corrected (slices 4/5 chased the wrong owners)
The waterfall emitters are **LANDBLOCK OBJECT-LIST STABS** (outdoor), not
EnvCell statics and not weenies:
- Objects[0..6]: Setup **0x0200085A** ×7 — EAST falls jets (x≈169170,
z=158.6), script 0x330008A6 → emitter 0x320004A4 (v=22.5 m/s jets).
- Objects[7..13]: Setup **0x02000859** ×7 — NORTH falls jets (y≈120.8),
script 0x330008A5 → emitters 0x320004A3/A2 (arc + plunge, accel 40).
- Objects[14..19]: Setup **0x020003C5** ×6 — lake-base mist at z=99.9
(incl. an authored exact-duplicate pair).
Their runtime ids live in the **0xC namespace**
(`LandblockStaticEntityIdAllocator`): east 0xCF41800006, north
0xCF4180070D, mist 0xCF41800E13.
**The probe-observed `0x4F41801215` owners are NOT the falls** — the 0x4
namespace is `InteriorEntityIdAllocator`; simulating the hydration filter
maps them to cell 0xF4180106's OWN stabs: **two 0x020005C1 TORCHES**
(flame emitters 0x320003AB/0x320003A5), one bare prop (0x01000DF0), one
plant (0x020007A3). Slice 5's "the falls are EnvCell stab statics" was a
misidentification; the `cell-owners n=5` submissions were the torches.
The falls billboards' degrade tables keep them visible to **392 m**
they are authored to be seen across the whole vista.
## THE "STALKS" — premise corrected
Setup 0x0200085A (the PV campaign's "monster stalks") is **not a visible
stalk**: its sole part GfxObj 0x010001EC is retail's runtime-hidden
marker (GfxObj flags 0xA; DIDDegrade 0x1100002E = never drawn at any
distance), shared as the invisible anchor by 13 of this landblock's
setups (falls jets, torch/sound anchors). The visible dark shapes at the
east cliff are these anchors DRAWN — i.e., a never-drawn-degrade skip
missing on the path that renders them (the marker-degrade port fce250ec
is stranded on the quarantined PV branch per the audit report). Cell
0xF418010D (the audit's stalk suspect) is actually a fully-interior
undercroft/crypt with 81 candle-prop stabs and NO exit portals — its
non-flooding is CORRECT behavior, not the stalk mechanism.
## EXIT-PORTAL GEOMETRY — the "thin seam bands" claim REFUTED
All exit portals at the defect cells are FULL-SIZE authored openings with
sills at the cell floor (z=169.80):
- 0xF4180104: four exits — two 10.72 × 10.20 m facade trapezoids (y=48
south, y=72 north) and two 24.0 × 9.2 m side ARCHES (x≈31 west, x≈41
east; the 0.57 m X-thickness in an AABB is just the arch bow, the polys
stand in the Y-Z plane). One interior ceiling portal → 0x105.
- 0xF4180106: one 10.72 × 10.20 m exit (y=48 north) + two 1.67 × 5.89 m
door arches (→0x10F, →0x110).
Slice 6's "authored polys are thin bands far narrower than the real
opening" is therefore WRONG about the authoring — if the walk's exit
views come out thin or empty (the ov==0 frames), the loss happens in OUR
clipping/traversal, not in the dat.
**Two real seam facts to carry instead:**
1. **0x104's south exit poly and 0x106's north exit poly are COINCIDENT**
— the same 10.72 × 10.20 m rectangle in the same y=48 plane (buildings
01001FB2 and 01001FB3 abut), EACH marked exit-to-outside (0xFFFF)
rather than cell-to-cell. This shared double-exit plane at y=48 is
"the seam".
2. The openings' sills sit ~9.8 m above the outdoor terrace (floor 169.8
vs terrain 160): the lake (z≈100) only becomes visible within ~23 m
of an opening — the downward vista is sill-clipped by geometry, not by
portal truncation.
## Portal flags (from all 24 cells)
Bit 0x1=ExactMatch, 0x2=PortalSide, and **0x4 co-occurs always and only
with other_cell==0xFFFF** (observed flags 0x5/0x7 on exits) — so the old
"flag 0x4 = leads outside" lore is observationally right as a CORRELATE
in this landblock even though the DatReaderWriter enum doesn't name it
(the datformat report found no 0x4 member; semantic asserted from perfect
correlation, not decomp).
## Water
The lake/falls SURFACES are outdoor TERRAIN paint: WaterShallowSea
(type 18) in the north basin (z 96108) and terrace water court (z=160),
FauxWaterRunning (type 22) on the cliff faces. No water objects besides
the six mist emitters.
## Cell 0xF4180104 / 0x0106 contents
- 0x104: 16 stabs — plants + FOUR looping ambient-SOUND anchors
(0x0200093D/3E/3F/09AF — how the falls' roar is heard indoors; no
particles) + prop.
- 0x106: 5 stabs — the two torches, sound marker, prop, plant.
- 0x105: a ceiling-linked upper cell (horizontal portals only).
## Caveats (the agent's own)
The 0x4F41801215 mapping mirrors `LandblockBuildFactory`'s hydration
filter in Python (high confidence, not executed C#); flag-0x4 semantics
asserted from correlation; terrain-name indices aligned against ACE's
LandDefs enum.

View file

@ -0,0 +1,141 @@
# Cathedral review — retail interior-root frame reconstruction (companion report)
Delivered 2026-08-30 by the decomp-reconstruction agent (BN pseudo-C +
live Ghidra arbitration on 8081 + acclient.h + symbols.json). THE retail
mechanism reference for the synthesis. Condensed; all pc:/address
citations preserved.
## Frame skeleton (interior root)
1. `SceneTool::BeginScene` @0x0043dad0: **full Clear(7) — COLOR+depth+
stencil — every frame** (pc:68673-68686). `Flip` bumps m_nFrameStamp
at frame end (pc:428642).
2. `RenderNormalMode` @0x00453aa0: interior camera → optional
LScape::update_viewpoint (only when viewer_cell->seen_outside,
pc:92667-71) → DrawInside(viewer_cell) → **unconditional final drain
FlushAlphaList(0f)** (pc:92687).
3. TWO PView singletons (pc:427800/427813): `indoor_pview` (draw_landscape
=1) for the root; `outdoor_pview` (0) for building look-ins — look-in
floods can never re-trigger landscape/clear/seals (their
outside_view.view_count is always 0; ClipPortals never raises it when
draw_landscape=0, pc:433666-84).
4. `PView::DrawInside` @0x005a5860: push root view (full viewport) →
`ConstructView(root, 0xffff)` — PURE CPU flood, draws nothing; exit
portals merge clipped apertures into outside_view (pc:433662-76,
cliplandscape=1 @0x00820f4c).
## PView::DrawCells @0x005a4840 — ALL of this inside `ov > 0` (pc:432715):
1. `Render::PortalList = &outside_view`; `LScape::draw` (pc:432718-19).
2. **Full outdoor alpha drain** FlushAlphaList(0f) (pc:432720).
3. `m_nFrameStamp += 1` (pc:432722) — re-arms part dedupe so a part can
draw in BOTH the outdoor and interior passes of one frame (straddlers
draw twice; "emit once" is anti-retail).
4. **Gated z-only clear** (pc:432725-32, Ghidra-verified):
`forceClear(const 0) || portalsDrawnCount != 0` — the counter is
incremented ONLY by own-depth portal stamps (seals, arg2==0,
pc:424569-70) → **a ONE-FRAME LATCH: last frame's seals arm this
frame's clear.** Steady-state vista frames clear every frame; the
FIRST such frame does not.
5. **Exit seals** (pc:432737-807): per flooded cell (far→near), per view,
EVERY portal with other_cell 0xffffffff:
`DrawPortalPolyInternal(poly, 0)` — REAL projected depth,
DEPTHTEST_ALWAYS + z-write ON (pc:424584), invisible (vertex alpha 0,
pc:424686), CULL_NONE, CPU-clipped to the current view (pc:424577).
(Sibling mode arg2==1 = the far-Z PUNCH z≈0.999999, look-ins only;
does not increment the counter.)
Then UNCONDITIONALLY (even when ov==0):
6. Lighting restore (pc:432811-12).
7. **Cell shells** far→near: DrawEnvCell dedupes per frame stamp — the
shell draws ONCE, WHOLE; the per-view loop is vestigial for it; no
view planes applied; correctness = pure z-test (pc:427885-88).
8. **Cell objects** far→near: PortalList = the cell's views →
DrawObjCellForDummies → shadow parts sorted by CYpt (viewer distance
to scaled sort_center, insertion_sort @pc:701156-75) →
CPhysicsPart::Draw — statics, dynamics, AND particle-emitter parts
through ONE machinery.
9. Final drain (pc:92687) vs interior depth + seal-stamped apertures.
## Clip vs cull (the definitive table)
- CPU polygon-clipped: ONLY portal-chain polys during the flood
(GetClip @0x005a4320) and seal/punch fans at draw (their own
polyClipFinish vs the current view, pc:424577, + a ±12 local-XY trivial
reject pc:424506-50).
- Sphere-vs-view CULLED, whole-drawn: all meshes (statics, dynamics,
particles' parts) — DrawMesh per view: viewconeCheck(sphere)
(pc:429295-96); PARTIAL draws WHOLE under that view; a mesh in N views
submits N times. Building PORTAL pass is cone-exempt (punches always
attempted; GetClip <3 points is the real stop, pc:429302-10).
- Cached-flag culled: terrain cells via in_view stamped by
draw_check_blocks @0x00505f80 iterating EVERY view of PortalList
(blocks wholly outside every exit view skip at pc:267936). Terrain
polys then draw whole + immediate (landPolysDraw @0x006b7040 — no view
clip, no alpha list).
- No test at all: EnvCell shell geometry; seal z-behavior (ALWAYS).
- `alwaysDrawObjects = 1` (@0x00820ed4): DrawSortCell runs for EVERY cell
of a drawn block — the per-part per-view cone check is the real object
gate (no pixel leak, just CPU).
## The alpha lists (retail)
TWO static FIFO lists, 3000 entries each (Clip @0x00871b10, Alpha
@0x008af370). NO depth key stored, NO sort at flush — order = submission
order; far-to-near correctness comes from the cell walk + per-cell CYpt
insertion sort. Overflow DROPS the subset (pc:424983). Entry routing:
s_AlphaDelayMask (default 0xE) vs the surface mask; bit3 → Clip list.
Flush sites (grep-complete):
| # | Site | Arg | Depth in place |
|---|------|-----|----------------|
| 1 | DrawBuilding @0x0059f30b | 0f FULL | outdoor-so-far (pre-punch) |
| 2 | DrawBlock @0x005a1a07 | 0.75 valve, fires per outdoor cell (BN's `flush>1` guard is a MISREAD — Ghidra: fires when flush<=1.0) | mid-landscape |
| 3 | DrawCells @0x005a4872 | 0f FULL | complete outdoor depth (pre-clear) |
| 4 | RenderNormalMode @0x00453b8b | 0f FULL (final) | interior + seals |
| 5 | CreatureMode::Render (chargen only) | 0f | its own viewport |
## Depth at final drain (interior root)
- Aperture/vista pixels: the SEAL's portal-plane depth (outdoor per-pixel
depth NO LONGER EXISTS — cleared; outdoor COLOR survives under the
stamp). Interior alpha fragments behind the portal plane FAIL there.
- Outdoor particles (falls): drawn in the landscape pass, drained at
site #3 vs outdoor depth. Cone-PARTIAL spill outside the aperture is
buried because EVERY interior pixel paints after every outdoor drain
point — protection is pure ORDERING, not depth.
## ShouldDrawParticles @0x0050fe60 — an UPDATE-time gate
Called per emitter from ParticleEmitter::UpdateParticles (physics-side),
NOT during rendering: `ExaminationObject → 1; CYpt > degrade_distance →
0; cell NULL → 0; cell->IsInView()==0 → 0`. On false: SetNoDraw(1),
degraded_out=1 (particles stop updating AND drawing).
- CLandCell::IsInView @0x00532cb0 = the PREVIOUS frame's stamped in_view
(one-frame-stale gate).
- **CEnvCell does NOT override it** — the slot resolves to the ICF-folded
constant @0x005269f0 `return 1` (PARTIALLY_INSIDE, PDB-verified): for
interior emitters the cell test is VACUOUS; distance is the only
update-time gate; their draw is flood-gated instead.
## Can content outside the flood/exit views draw? NO.
Interior: pure flood membership. Outdoor blocks/cells: in_view under ≥1
exit view. Outdoor parts: per-exit-view sphere-cone in DrawMesh. Building
portal pass cone-exempt but clip-stopped. No distance bypass exists.
## Corrections to campaign docs
1. DrawBlock per-cell flush polarity RESOLVED (fires; 0.75 pressure
valve) — BN misread.
2. The m_nFrameStamp bump separates the landscape and interior passes of
ONE interior-root frame; look-ins NEVER bump (outdoor_pview ov==0).
3. NEW: CEnvCell::IsInView = constant PARTIALLY_INSIDE (ICF fold,
symbols.json 19-name cluster) — not in any FW doc.
## UNVERIFIED (the agent's own list)
The 0xF418 falls emitters' owner (land cell vs EnvCell static — dat
question; note the DAT-geometry companion resolves this: landblock
object-list stabs); z-write during alpha drains; insertion_sort
direction byte-proof; the ±12 reject polarity; GameSky depth behavior.

View file

@ -0,0 +1,436 @@
# Cathedral rendering review — FABLE (2026-08-30)
Independent report-only review of the cathedral-terrace defect (falls
particles + marker stalks over terrain; landblock `0xF418`, camera in
`0xF4180104`/`0xF4180106`), baseline `37febd1f`, HEAD `bb9212c7`, branch
`claude/campaign-w-retail-frame-walk`.
**Evidence basis and honesty note.** Everything below marked with a citation
was read directly by this reviewer (oracle trace files, decomp pseudocode
docs + their byte-verification notes, commit messages/diffs, git topology,
ISSUES/register/memory docs, id-allocator source). Three deeper evidence
threads (fresh decomp re-reads of the alpha/particle path, a raw DAT parse
of `0xF418`, and the HEAD code-flow + probe-log audit) were dispatched but
had not returned when this report was due; every claim that depended on
them is explicitly marked **unverified**. Per the prompt's rule, unverified
is stated as unverified — nothing below is a plausible guess dressed as
fact.
---
## 1. Executive verdict (≤10 lines)
The six fixes failed because they accepted a false premise about WHERE this
scene's content sits in retail's frame. The posed oracle traces show that at
both defect poses retail's interior flood is EXACTLY ONE cell (`DC ov=1 n=1:
f4180104` / `f4180106`) — every other cathedral cell, including the falls'
stab-owner cells, draws as a *building look-in inside the landscape scope*,
pre-depth-clear, its alpha drained against full landscape depth
(`posed/terrace-center.log` F5, `posed/cathedral-arrival.log` F1-2;
`PView::DrawCells` @0x005a4840 model §5). Our pipeline instead treats the
whole complex as interior-flood content drawing post-clear, then tried to
rescue it with seals (slice 2) and a non-retail exit-plane-straddle
classifier (slices 4/5) that under-covers exactly the fully-outside falls
emitters. Fix = route every object's particles to its owner cell's actual
walk turn (look-in ⇒ landscape scope ⇒ pre-clear drain), delete the straddle
classifier, and re-land the quarantined marker-degrade port for the stalks.
---
## 2. Retail frame reconstruction (for an interior root with exit portals)
### 2.1 The frame skeleton — verified from the FW0 model + live traces
Source of truth used: `docs/research/2026-08-30-fw-walk-pseudocode.md`
(§1, §2, §3, §5, §9 — its seal loop, deferred z-clear, DEPTHTEST_ALWAYS,
punch-Z constants are recorded there as byte-verified against the raw
pseudo-C at `@0059a49af`, `@005a48a9`, `@0059be02`, `@0059bf4b`), the
extraction appendix `2026-08-30-fw-walk-pseudocode-appendix.md` (notably
the DrawBuilding/DrawMeshInternal full-sequence note at appendix line 811),
and the live cdb traces in `docs/research/2026-08-30-fw-walk-oracle/posed/`.
Order of one interior-rooted frame (`SmartBox::RenderNormalMode`
@0x00453aa0`PView::DrawInside` @0x005a5860`ConstructView`
`PView::DrawCells` @0x005a4840):
1. **Flood** (`ConstructView(CEnvCell)` @0x005a57b0): view-driven — a
neighbor cell enters `cell_draw_list` only if its portal polygon
survives `PView::GetClip` @0x005a4320 against the current view (the ONLY
CPU polygon clip in the frame). Exit portals
(`other_cell_id == 0xFFFFFFFF`) raise `outside_view`.
2. **If `outside_view.view_count > 0`** (the traces' `ov`):
a. `Render::PortalList = &outside_view`; **`LScape::draw`** @0x00506330
— the whole landscape walk THROUGH the exit views: sky, terrain
blocks far-to-near (view-CULLED per cell/block by
`draw_check_blocks`' in_view union — **never view-clipped, never
scissored**; `set_view` installs NO GPU state), buildings far-to-near,
and — per building — the **look-in machinery**:
`DrawBuilding` @0x0059f2a0 → BSP portal pass 1 punches apertures
(`DrawPortalPolyInternal` flag=1: far-Z 0.999998987, DEPTHTEST_ALWAYS,
z-write ON, alpha 0) → pass 2 recurses `ConstructView(CEnvCell)` +
`DrawCells` (**`ov=0` — no landscape, no clear, no seals**) drawing
look-in EnvCells + their objects into the punched aperture
(appendix line 811). `DrawBuilding` itself calls
**`FlushAlphaList(0f)`** before each building; `DrawBlock`
@0x005a17c0 has the mid-walk pressure valve
`FlushAlphaList(flush=0.75)` (appendix lines 700, 1027).
b. **`FlushAlphaList(0f)`** — the pre-clear boundary drain: EVERYTHING
queued during the landscape scope (terrain alpha, building alpha,
look-in cell content alpha — and the particle polys of objects drawn
in that scope) drains against **full landscape depth**. This is the
drain that occludes the falls with terrain/water/trees.
c. **Gated depth clear** (`if (forceClear || portalsDrawnCount != 0)
Clear(z)`, model §5; `portalsDrawnCount` counts ONLY flag-0 seals —
appendix line 811). Color is preserved; only depth clears.
d. **Exit seals**: for the FLOODED cells' exit portals only,
`DrawPortalPolyInternal(poly, 0)` — the portal's own projected depth,
DEPTHTEST_ALWAYS, z-write ON, invisible (model §2, §5; byte-verified
per model §9).
3. **Interior passes** (post-clear): `DrawEnvCell` far-to-near (BSP shells),
then `DrawObjCellForDummies` (the flooded cells' objects) — depth
accumulates fresh.
4. **Final drain**: `FlushAlphaList(0f)` back in `RenderNormalMode` — the
flooded cells' alpha/particles drain against interior depth + the seals.
### 2.2 The four prompt questions
**(a) clipped vs culled vs whole.** CPU-clipped: ONLY the portal chain's own
polygon (`GetClip` @0x005a4320 — the model calls it "the only CPU polygon
clip in the frame"). View-culled (coarse sphere-vs-cone,
`Render::viewconeCheck` @0x0054c250, PARTIAL draws whole): objects.
Cell/block-granular culled (in_view interval grids): terrain blocks/cells.
Drawn whole with no per-view test: everything that passes those gates —
shells, meshes, particles. No GPU clip planes/scissor exist anywhere in this
frame (`set_view` = CPU globals only, model §6).
**(b) order relative to clear and seals.** PRE-clear: sky, terrain, water
(terrain-hosted — see §3, unverified detail), building shells, building
punches, **look-in EnvCell shells + look-in cell objects + their
emitters**, and all their alpha via the boundary drain. POST-clear: seals
first, then ONLY the root flood's EnvCell shells + objects + their
emitters, drained at the final flush. Seals draw exactly the flooded
cells' exit-portal polygons at own depth, color-write effectively off
(alpha 0), depth ALWAYS + write ON.
**(c) alpha list.** ONE list. Flush sites established from the docs:
per-building `FlushAlphaList(0f)` in `DrawBuilding` (appendix 811); the
`DrawBlock` pressure valve at `flush=0.75` (~2250 meshes, appendix 1032);
the `DrawCells` pre-clear boundary `FlushAlphaList(0f)` (model §5); the
`RenderNormalMode` end-of-frame `FlushAlphaList(0f)` (model §1). Depth in
place: landscape depth at the first three; interior + seal depth at the
final one. **Unverified:** an exhaustive caller enumeration of
`D3DPolyRender::FlushAlphaList` @0x0059d2e0 from the decomp itself, and
the exact particle-poly insertion path (the dispatched decomp read had not
returned; the MP2-era digest additionally records
`CPhysicsObj::ShouldDrawParticles` @0x0050FE60 = authored degrade range +
`cell->IsInView()`, from `claude-memory/project_render_pipeline_digest.md`).
**(d) depth at final-drain time.** Aperture pixels: the seal's own depth
(near the camera) — anything beyond the exits z-fails, so interior
particles cannot repaint the vista. Vista pixels visible through the
aperture ARE aperture pixels (the opening's polys); pixels covered by
interior geometry hold that geometry's depth. The falls are never in this
drain in retail at these poses — they drained pre-clear with the landscape.
**Unverified residual:** whether any visible vista pixel can lie OUTSIDE
the union of authored exit-portal polygons at the live chase camera (slice
6's observation implies yes for the WALK's clipped views; whether that
reflects portal-poly authoring or our clip is open — §8 Q1).
### 2.3 The live traces at the defect poses (decisive, read directly)
`posed/terrace-center.log` F5 (camera cell `f4180104` — the owner's
teleloc cell):
```
DI f4180104
DC pv=009d4e38 ov=1 n=1: f4180104 ← the interior flood is ONE cell
LS
BLD f518002e … BLD f4180014 BLD f418000a BLD f418000c BLD f418000b (14 buildings)
```
`posed/cathedral-arrival.log` F1-2 (camera cell `f4180106`):
```
DI f4180106
DC pv=009d4e38 ov=1 n=1: f4180106 ← flood = root cell only
LS
BLD f3180020 … BLD f418000c
DC pv=009d4398 ov=0 n=2: f4180101 f4180100 ← look-in punch during LS
… BLD f418000b
DC pv=009d4398 ov=0 n=1: f4180104 ← 0xF4180104 IS A LOOK-IN
BLD f418000a
```
`posed/terrace-edge.log` F5 (outdoor root, camera cell `f418000b`): look-in
punches `n=1: f4180106`, `n=1: f4180101`, `n=1: f4180104` interleaved with
the building walk — no clear, no seals, everything drains against full
depth (why the owner sees a correct scene outdoors).
**Conclusion:** at the defect poses, `0xF4180104` and `0xF4180106` are in
DIFFERENT buildings' interiors (`f418000b` vs the `f418000a/c` group —
consistent with the "seam" being a building boundary), retail's interior
flood never crosses it, and ALL non-root cathedral cells + their stabs
compose in the landscape scope, pre-clear. The "toggles with which side of
the seam the camera is on" symptom is the root flip between these two
one-cell floods (and outdoor).
---
## 3. DAT geometry findings
**Verified:**
- The falls' observed particle owners `0x4F418012``0x4F418015` decode per
`src/AcDream.Core/World/InteriorEntityIdAllocator.cs:48`
(`0x40000000 | X<<20 | Y<<12 | counter`) to **interior EnvCell-stab
statics of landblock F418**, counters 0x120x15 — NOT landblock objects
(0xC0000000+ namespace) and NOT scenery (0x80000000+). The slice-5 commit
message (`0d6cd5c0`) reached the same identification ("EnvCell STAB-LIST
statics … ids minted by InteriorEntityIdAllocator").
- Retail cell topology at the poses (from the traces, §2.3): `0xF4180104`
belongs to building `f418000b`'s look-in set; `0xF4180100/0101` to
`f418000c`'s; `0xF4180106` is punched separately (arrival trace);
interior floods do not cross between them at the captured camera states.
- The prior campaign's offline sightline probe (recorded in
`claude-memory/project_pview_visibility_campaign.md` and the PV plan)
established: the exit portal of the terrace cell and the building's
never-drawn panel are the SAME surface, and the "#456 seam band" IS the
terrace cell's portal[4] polygon — i.e. at least one exit aperture is a
thin band, corroborating slice 6's live finding that scissoring terrain
to the walk's exit views starves most of the vista of depth/color.
**Unverified (the DAT parse thread had not returned):** the per-cell portal
tables (flags/0x4 exits/other_cell ids) for `0x0104/0x0105/0x0106/0x010D/
0x010F`; WHICH EnvCell's stab list carries counters 0x120x15 (the counter
is allocated in ascending cell order in `LandblockBuildFactory`
`src/AcDream.App/Streaming/LandblockBuildFactory.cs:494-503`, so the DAT
stab tables resolve it mechanically); the exit-portal polygon vertex
extents vs the visible opening; whether the lake water is terrain-hosted;
the cathedral shell GfxObj roster (`0x01001FB2/B0/B3` — asserted by the
owner's prior analysis and consistent with the PV panel model
(`0x01001FB0/B2/B3/B7` panels, PV plan §"established model" item 3), but
not independently re-read here).
---
## 4. Divergence table (ranked by causal weight for THIS defect)
| # | Divergence | Ours | Retail | Evidence |
|---|---|---|---|---|
| D1 | **Particle emission site for cell statics/dynamics** | flood-membership + `SphereStraddlesExitPlane` classifier decides pre- vs post-clear emission (slices 4/5) | the owner object's actual walk turn decides: drawn during LS (shadow-cell overlap or look-in) ⇒ landscape scope ⇒ pre-clear drain; drawn in the root flood ⇒ final drain | traces §2.3; model §5; appendix 811; commit msgs `f3a03efc`/`0d6cd5c0` |
| D2 | **Interior "flood" membership for the cell-owners route** | the production cell set treats the connected cathedral complex as interior content (slice-2/3 messages: "the cell-owners route", "the OLD apparatus flood"); walk conformance proves the WALK floods n=1, but consumers were still being cut over one at a time | flood = what survives GetClip from the root; at these poses exactly ONE cell; everything else is landscape-scope | traces §2.3; commit msgs `c40aecfc`/`5f7ccdea`; plan FW3.2b-2 "prepareCells UNIONED" |
| D3 | **Dynamics-last** (register row AD-19, `docs/architecture/retail-divergence-register.md:147`): ALL dynamics in one z-buffered final pass | per-landcell painter-ordered draws inside the landscape pass; interior per cell turn | register row (self-declared); `DrawBlock` 0x005a17c0 / DrawSortCell pc:430124 |
| D4 | **No degrade/LOD admission on this branch** — the marker stalks (Setup `0x0200085A` → GfxObj `0x010001EC`, ladder `0x1100002E` = {self at MaxDist 0, id 0 to ∞}) are DRAWN | retail selects `gfxobj[deg_level]` via `get_degrade` @0x0051E4B0 and **never draws** this model at any distance (gates in `DrawBuilding` @0x0059F2A0 and `CPhysicsPart::Draw` @0x0050D7A0) | `fce250ec` exists ONLY on `claude/cathedral-rendering-debug-47a417` (`git branch -a --contains`; `git merge-base --is-ancestor fce250ec HEAD` = false; `git grep IsNeverDrawnByDegrade -- src` = empty at HEAD) |
| D5 | **Seal/vista coverage geometry**: post-clear alpha can z-pass wherever the visible vista is not covered by a stamped seal; slice 6 proved the walk's exit views at this seam are "thin BANDS far narrower than the real opening" | retail is not exposed to this because the leaking content class isn't post-clear in the first place (D1/D2) | commit msg `bb9212c7`; PV sightline probe (campaign memory) |
| D6 | (regression candidate) straddle-classified emitters now emit pre-clear on interior roots — an interior emitter drained at the landscape flush is subsequently over-painted by the post-clear interior repaint | retail: root-cell emitters drain at the final flush, after interior geometry | mechanism prediction from `f3a03efc`/`0d6cd5c0` + model §5; matches the owner's new "particles WITHIN the cathedral are wrong" report — **unverified until probed** |
The pre-existing #451 fix (owner-accepted 2026-08-27, `docs/ISSUES.md:249-289`)
had already implemented the D1-correct contract for exterior particles
("submits attached and ownerless exterior particles inside `LScape::draw`
for every PView root"). The FW2/FW3 window (`git diff --stat
e102fb36..37febd1f -- src/AcDream.App/Rendering` = ~7,081 insertions:
the whole `Walk/` tree, `WalkFrameDriver`, the static cutover) is the
regression window that displaced it.
---
## 5. Per-commit verdicts
**`37febd1f` (slice 1 — interior outside-view slices from the walk): KEEP.**
Correct single-source-of-truth move (retail has ONE visibility structure);
fixed a real dual-path desync (the #119-family grass flash) with a real
mechanism (old-apparatus fat/degenerate exit views + walk-indexed punch
fans). Not causal for the cathedral defect either way. Its per-view terrain
slice FAN was still non-retail — superseded by slice 6, which it should be
read together with.
**`c40aecfc` (slice 2 — exit seals stamp the walk's flood): KEEP the code,
REJECT the diagnosis (wrong-mechanism).** Stamping the walk's own flood's
exit portals is exactly retail (model §5 seal loop, pc:432785-6, and the
walk flood is conformance-proven against the traces). But the commit's
premise — falls emitters submitting POST-clear is "retail's own timing" —
is refuted by the traces: retail composes those cells pre-clear as
look-ins. Seals could never fix an over-draw whose correct rendering is
"drawn pre-clear with landscape depth"; at best they would z-reject the
falls and make them wrongly INVISIBLE through the aperture. The plan's
slice-2 paragraph (`docs/plans/2026-08-30-campaign-fw-frame-walk.md`
FW4 slice 2) carries the same false premise and should be corrected.
**`5f7ccdea` (slice 3 — outside-stage predicate reads the walk flood):
KEEP.** Feeding predicates from the walk's VisitedCells (root flood +
look-in floods — "all of whose objects retail draws inside LScape::draw,
pre-clear") is the correct retail-shaped direction and removed one more
old-apparatus consumer. Insufficient alone: it gates the DYNAMIC split,
and the falls turned out to be cell statics (slice 5's own finding).
**`f3a03efc` (slice 4 — straddler dynamics' particles pre-clear, once):
WRONG-MECHANISM — revert once D1 lands.** Retail has no exit-plane-straddle
classifier for emission timing; the cited rule ("retail submits an
emitter's polys at its object's FIRST draw — the landscape stage for
anything overlapping outdoor shadow cells") is right, but the
implementation substitutes a plane-straddle test for draw-site membership.
It under-covers emitters wholly OUTSIDE the exit plane (they never
"straddle" ⇒ stay post-clear ⇒ still leak) and over-covers interior
straddlers into the pre-clear drain where the interior repaint erases them
(divergence D6 — the likely source of the new in-cathedral particle
wrongness). Drawn-once bookkeeping (each owner emits exactly once) is worth
preserving in whatever replaces it.
**`0d6cd5c0` (slice 5 — straddling cell-statics' particles pre-clear):
WRONG-MECHANISM — revert once D1 lands.** Its identification work is the
best of the six (falls = EnvCell stab statics; the shadow-cell rule
correctly cited at `DrawBlock` pc:430056-430064), but it ports the rule as
the same geometric straddle test instead of the actual membership question
("is this object drawn during the landscape scope this frame?" — for the
falls cells at these poses: yes, as building look-ins, regardless of any
plane test). Same under/over-coverage failure modes as slice 4.
**`bb9212c7` (slice 6 — interior roots draw landscape unclipped): KEEP.**
Matches retail exactly: landscape content is view-CULLED, never
view-CLIPPED (model §3: the views feed only `draw_check_blocks`' in_view
union; `set_view` installs no GPU state; no scissor exists). Its probe
finding — falls submitted pre-clear with depth LESS still passed because
the scissored terrain had left the vista without depth — is the
load-bearing discovery of the round and independently confirms the thin-band
aperture geometry. It cannot, by itself, fix emitters that still submit
post-clear on other frames/poses, and it does not address D6.
---
## 6. Why the symptom survived all six fixes
1. **A wrong premise was locked in at slice 2 and never re-examined against
the traces the campaign itself captured.** "The falls submit post-clear —
retail's own timing" is contradicted by `posed/terrace-center.log` /
`posed/cathedral-arrival.log`: retail's interior flood at these poses is
ONE cell and the falls' owner cells are landscape-scope look-ins. Every
subsequent fix (seals, predicates, straddle emission) tried to make a
post-clear world safe instead of putting the content in the pre-clear
world where retail has it.
2. **The corrective classifier is not retail's rule.** The straddle test
(slices 4/5) approximates "overlaps outdoor shadow cells" with "sphere
crosses the exit plane" — a strictly narrower predicate that misses
fully-outside emitters (the falls hang OVER the lake) and wrongly
captures interior straddlers.
3. **Mixed visibility sources during the staged cutover.** Slices 2 and 3
each discovered another consumer still reading the OLD apparatus's flood
(seal set, outside-stage predicate). The FW3.2b-2 design explicitly runs
old visibility for dynamics beside the walk for statics
(`docs/plans/2026-08-30-campaign-fw-frame-walk.md`); every such seam is
a place where the walk's correct n=1 flood is not what production
consumes. AD-19 (dynamics-last) keeps the weenie stalks post-clear
regardless.
4. **Two of the leaking content classes cannot be fixed by ANY
ordering/seal work:** the marker stalks should never be drawn at all
(D4 — the degrade admission port is quarantined off-branch), and
pre-slice-6 the vista had no depth to occlude against (D5) — so even
correct pre-clear emission "passed" its z-test into stale color.
5. **Each fix was validated against the previous fix's failure, not against
a falsifiable retail prediction.** The probes pinned real facts
(phase=pre/post, straddler classification) but the acceptance question
was "did the leak stop" rather than "does the frame now match §2.1's
shape at this pose" — so five plausible partial mechanisms shipped in
one day without any landing the frame-shape divergence (D1/D2).
---
## 7. Recommended fix plan (mechanism-first)
**Step 0 — probes before code (falsifiable predictions):**
- **P1:** With `ACDREAM_PROBE_WALK_ROOT` at the terrace pose on HEAD, the
falls owners `0x4F418012-15` will log submission OUTSIDE the landscape
scope (post-clear route or final drain) on defect frames, while the
walk's own event stream visits their owner cells at BUILDING LOOK-IN
turns (not in the interior flood, which stays n=1). If instead they
already log landscape-scope submission + pre-clear drain on defect
frames, D1 is falsified — investigate the drain's depth state
(attachment, compare op) instead.
- **P2:** The DAT stab tables will place stabs #0x12-0x15 in cell(s) OTHER
than the frame's root cell at the defect poses (look-in-reached cells).
If they are stabs of the root cell itself, the look-in theory is
falsified.
- **P3:** Forcing the falls emitters to submit at their owner cell's
look-in turn (landscape scope, drained at the pre-clear boundary flush)
removes the over-draw at EVERY camera on interior roots, while the falls
stay visible through the opening and terrain-occluded exactly as on
outdoor roots.
- **P4:** Reverting slices 4/5's straddle emission (in favor of the
walk-turn rule) makes the new "particles within the cathedral are wrong"
report disappear (root-cell emitters return to the final drain).
- **P5 (optional, owner-assisted):** a cdb capture at the terrace with a
breakpoint on the particle-poly alpha-list insertion will show falls
polys entering the list BEFORE `DrawCells`' depth clear on
interior-rooted retail frames.
**Step 1 — the emission-site port (fixes the falls).** An object's
particles submit when and where the object itself draws in the walk:
look-in cell content and outdoor shadow-cell content submit during the
landscape turn and drain at the pre-clear boundary flush; root-flood cell
content submits at its cell turn and drains at the final flush. Retail
pins: appendix 811 (look-in `DrawCells` inside the landscape scope),
`DrawBlock` pc:430056-430064 (shadow-cell object draw), model §5 (the two
drain sites). Delete `SphereStraddlesExitPlane`-based emission (slices
4/5) in the same commit; keep the emit-once ledger.
**Step 2 — finish the flood-consumer cutover (fixes the class).** Every
remaining consumer of the old apparatus's cell set (cell-statics particle
owners, dynamics routes, prepare unions) reads the walk's per-turn visited
sets. AD-19's interior-root half retires when dynamics draw at their
cell's walk turn; if that is deferred, dynamics must at minimum gate on
the walk's visited set so a non-visited cell's weenies cannot draw at all.
**Step 3 — re-land the marker-degrade admission (fixes the stalks being
visible at all).** `fce250ec`'s mechanism (never-drawn degrade ladders:
`get_degrade` @0x0051E4B0; `gfxobj[deg_level] != 0` gates) is a retail
port stranded on the quarantined branch. Re-land it cleanly on this branch
(fresh commit, not a cross-merge from quarantine), covering static AND
dynamic classifiers. If stalk-like shapes remain afterwards, identify by
guid before theorizing (they'd be weenies with a different model —
campaign-memory rule).
**Step 4 — verification gates.** The ten oracle fixtures stay green (the
walk itself is already conformant); add a fixture-level assertion that the
DRIVER's submission stream places look-in-cell content in the landscape
scope (the current conformance compares walk events, not submission
scope — that gap is how D1 shipped); then the owner matrix: terrace both
cells, seam crossing, outdoor falls, interior candles, doorway flap,
Holtburg exit.
---
## 8. Open questions
- **Q1:** Why are the walk's exit views at this seam thin bands — is the
full visible opening authored as multiple portals (0/1/2/4) of which
only portal 4 survives GetClip at these camera states, or is our
clip/winding at fault, or does the chase camera sit outside the cell
volume? (Slice 6 measured the symptom; the cause is unestablished. The
PV probe's "panel solid covers every sightline" was measured at ≤6 m
camera rises only.)
- **Q2:** Which EnvCell(s) own falls stabs #0x12-0x15, and do the falls
additionally exist as dynamics (slice 3 probed entities "f4243/f4285"
as [walk-dyn] — the relationship between those ids and the stab owners
is unestablished). **DAT parse owed.**
- **Q3:** The exact HEAD drain/depth state of `ParticleRenderer` under
Vulkan (depth attachment, compare op, write mask) at each drain site —
required to close P1's alternative branch. **Code audit owed.**
- **Q4:** Whether `ShouldDrawParticles` (@0x0050FE60) is evaluated
per-frame or per-view in our port, and against which cell set at HEAD.
- **Q5:** The gated clear's `portalsDrawnCount` semantics across frames
(seals increment it AFTER the clear check — does the first interior
frame after an outdoor frame skip the clear in retail?), and whether our
`slices>0` production gate is equivalent.
- **Q6:** Whether the seal fans at HEAD still apply the old assembly's
per-cell slice clips ("where present", slice 2) and whether that can
shrink a seal below its authored polygon.
- **Q7:** The uncharacterized in-cathedral particle wrongness (owner
report #3) — D6 is a prediction, not a diagnosis; one probe frame with
`[walk-part]`/`[walk-alpha]` at a candle would settle it.
---
*Reviewer: Fable. All decomp addresses cite the named 2013 build
(`docs/research/named-retail/`); trace lines cite files under
`docs/research/2026-08-30-fw-walk-oracle/posed/`; commit claims cite the
SHAs in §5; everything not directly read is marked unverified above.*

View file

@ -0,0 +1,673 @@
# 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
```

View file

@ -0,0 +1,165 @@
# Cathedral rendering review — independent troubleshooting prompt
You are an independent reviewer investigating a persistent rendering defect
in **acdream** (a modern C# .NET 10 Asheron's Call client; the code is
modern, the behavior must be retail). This is a **report-only**
investigation: read anything, run read-only commands (git log/diff/show,
grep, dat inspection), but do NOT edit code, do NOT build, do NOT launch
clients, and do NOT commit. Your single deliverable is a written report
(the path is given at the end).
## Where the code is
- Git worktree (the active campaign branch, work here):
`C:\Users\erikn\source\repos\acdream\.claude\worktrees\peaceful-blackburn-5333f0`
- Branch: `claude/campaign-w-retail-frame-walk`
- HEAD at review time: `bb9212c7` (FW4 slice 6)
- **Baseline before ANY cathedral-specific fix: `37febd1f`** — the owner
first reported this defect on a build of that commit. Six fix commits
followed (see below). The owner's verdict: *nothing has fixed anything*.
- Main repository (for main-branch history): `C:\Users\erikn\source\repos\acdream`
## The defect (owner-observed, on a live ACE server at 127.0.0.1:9000)
At the cathedral terrace, landblock `0xF418`, standing in interior EnvCells
`0xF4180104` / `0xF4180106` (owner teleloc: `0xF4180104 [31.9 57.3 169.8]`):
1. **Waterfall particles** (the blue falls over the lake outside) render on
top of terrain, water, and other content that should occlude them.
Intermittent — toggles with which side of "the seam" (the terrace's
interior/outdoor boundary) the CAMERA is on. Standing outdoors near the
falls (`0xF418000B`, `0xF4180015`) they look correct.
2. **"Stalks"** (dark monster-marker shapes near the lake-edge trees,
server-spawned weenies, e.g. entities parented at `0xF418010D`) leak
over trees and scenery. This predates the current campaign.
3. NEW (after the sixth fix): the owner reports **particles WITHIN the
cathedral are also wrong** (uncharacterized — treat as a regression
candidate of the fix stack).
4. A previous session's analysis (owner-supplied) adds: the visible
cathedral shell is assembled from multiple authored GfxObjs
(`0x01001FB2`, `0x01001FB0`, `0x01001FB3`) + EnvCells + a portal graph;
cell `0xF4180104` has exterior portals 0/1/2/4 and interior portal 3 →
`0xF4180105`; "if an incorrect depth or portal route prevents one
wall's colour pass from drawing, the wall appears transparent even
though its geometry and opaque texture are present."
**The oracle fact that anchors everything: the RETAIL client connected to
the SAME ACE server renders this scene perfectly.** So a correct
client-side mechanism exists; the task is to find what retail actually
does for this scene and where our port diverges.
## What you must do
1. **Reconstruct retail's actual frame for this scene** from the named
decomp: `docs/research/named-retail/acclient_2013_pseudo_c.txt`
(18,366 PDB-named functions; grep by `class::method`) and
`docs/research/named-retail/acclient.h`. Key functions:
`SmartBox::RenderNormalMode` @0x00453aa0, `PView::DrawInside` @0x005a5860,
`PView::DrawCells` @0x005a4840 (landscape draw pc:432719, gated depth
clear pc:432731-2, exit seals pc:432785-6), `LScape::draw` @0x00506330,
`RenderDeviceD3D::DrawBlock` @0x005a17c0 (per-shadow-cell object draw
pc:430056-430064), `RenderDeviceD3D::DrawSortCell` @0x0059f140,
`RenderDeviceD3D::DrawBuilding` @0x0059f2a0,
`CPhysicsObj::ShouldDrawParticles` @0x0050FE60,
`D3DPolyRender::FlushAlphaList` @0x0059d2e0, `PView::GetClip` @0x005a4320,
`PView::InitCell` @0x005a4b70. A Ghidra MCP HTTP server may be live at
`http://127.0.0.1:8081` (`/decompile_function?address=0x...`) serving the
same 2013 build with PDB names — use it when grep is too noisy.
Answer specifically: for an interior root whose cell has exit portals,
(a) what exactly gets view-clipped vs view-culled vs drawn whole;
(b) when does each content class (terrain, sky, building shells,
EnvCell shells, cell-static objects + their emitters, dynamic objects +
their emitters, unattached emitters, water) draw relative to the gated
depth clear and the exit seals; (c) how does the ONE alpha list drain
(which flush sites, what depth is in place at each); (d) what depth
does the frame hold at the aperture and at the vista at final-drain time.
2. **Read the cathedral's actual geometry from the installed DATs**
(`%USERPROFILE%\Documents\Asheron's Call\`, DatCollection is the
in-tree reader; `references/ACViewer` is a rendering oracle):
landblock `0xF418` — the LandblockInfo object list, the EnvCells
`0x0100`-`0x0115`-ish (esp. `0x0104`/`0x0105`/`0x0106`), their portal
polygons (which are exits, flag 0x4 → other_cell 0xFFFF), their
stab lists (the falls objects: local static ids `0x4F418012-15` were
observed as the falls' particle owners; setup/gfx ids of waterfall
objects), and where the water surface geometry lives. Establish:
are the falls objects EnvCell stabs, landblock objects, or both?
What shape/extent are the exit portal polygons vs the real visible
opening (the prior campaign claims they are thin "seam bands")?
3. **Audit our implementation against that reconstruction**, at BOTH
`37febd1f` (baseline) and `bb9212c7` (HEAD). The relevant code:
`src/AcDream.App/Rendering/Walk/` (RetailFrameWalk, WalkPView,
WalkFrameDriver, WalkViews, WalkScreenClip, WalkProductionWorldData),
`src/AcDream.App/Rendering/RetailPViewRenderer.cs`,
`RetailPViewPassExecutor*.cs`, `ClipFrameAssembler.cs`,
`ClipPlaneSet.cs`, `ParticleRenderer.cs`, `RetailAlphaQueue.cs`,
`PortalVisibilityBuilder.cs` (the legacy visibility layer FW4 is
deleting), `Scene/RenderScenePViewFrameProduct.cs`.
4. **Judge the six fix commits individually** — each claims a retail
citation; decide keep / revert / wrong-mechanism for each:
- `37febd1f` FW4 slice 1 — interior outside-view slices from the walk
(was: old PortalVisibilityBuilder slices; also fixed punch-fan index
aliasing).
- `c40aecfc` slice 2 — exit seals stamp the walk's own flood.
- `5f7ccdea` slice 3 — outside-stage predicate reads the walk flood.
- `f3a03efc` slice 4 — exit-plane straddler DYNAMICS' particles emit
pre-clear once (was: deliberately last-pass).
- `0d6cd5c0` slice 5 — straddling CELL-STATIC particles emit pre-clear
(retail shadow-cell rule claim).
- `bb9212c7` slice 6 — interior roots draw terrain+sky UNCLIPPED
(view-culled only), one terrain turn (was: per-exit-view
scissor+clip slices).
Evidence from the live probes is in
`%LOCALAPPDATA%\Temp\fw3-*.log` and `%LOCALAPPDATA%\Temp\fw4-*.log`
(`[walk-root]`, `[walk-part]`, `[walk-alpha]`, `[walk-dyn]`,
`[walk-portal]`, `[walk-emit]` lines; the probe is
`ACDREAM_PROBE_WALK_ROOT`, documented in `docs/launch-options.md`).
5. **Explain why the symptom survived all six fixes**, and produce a
concrete, mechanism-first fix plan (what to change, where, what retail
evidence pins it, and what a falsifiable test/probe for it looks like
BEFORE any code is written).
## Required background reading (in this order)
1. `claude-memory/project_pview_visibility_campaign.md` (the #456
campaign memory — the proven mechanism model, the do-not-retry list;
note: `claude-memory/` is a junction in the MAIN repo root at
`C:\Users\erikn\source\repos\acdream\claude-memory\`; the same files
are at `C:\Users\erikn\.claude\projects\C--Users-erikn-source-repos-acdream\memory\`)
2. `docs/plans/2026-08-30-campaign-fw-frame-walk.md` (the active
campaign plan — stages FW0-FW6, the FW1 conformance status, the FW4
slice log)
3. `claude-memory/project_render_pipeline_digest.md` (render SSOT +
DO-NOT-RETRY table)
4. `docs/research/2026-08-30-fw-walk-oracle/` (the ten retail cdb traces
the walk is conformance-tested against)
5. `docs/plans/2026-08-30-pview-visibility-campaign.md` (the abandoned
PView campaign — what was tried and refuted)
## Hard rules
- The decomp is ground truth; ACE/ACViewer/WorldBuilder are interpretation
aids. Where sources disagree, the decomp wins.
- Do not propose workarounds (suppression flags, grace periods,
symptom-site early-outs). Root mechanisms only.
- Respect the do-not-retry lists in the memory docs (e.g., depth stamps as
"retail's mechanism" is decomp-refuted; the panels are portal polys, not
inert decoration).
- Every claim in your report needs a citation: a decomp address, a DAT
field, a file:line in our tree, a probe-log line, or a commit SHA.
- If you cannot verify a claim, say so explicitly — an honest "unverified"
beats a plausible guess. This investigation has already burned six fix
rounds on plausible guesses.
## Deliverable
Write your full report to: **`docs/research/2026-08-30-cathedral-review-<YOURNAME>.md`**
(replace `<YOURNAME>` with your model name, e.g. `fable`, `opus`,
`external`). Structure: Executive verdict (≤10 lines) → Retail frame
reconstruction → DAT geometry findings → Divergence table (ranked) →
Per-commit verdicts → Why the symptom survived → Recommended fix plan →
Open questions. Length: as long as it needs to be; completeness beats
brevity.

View file

@ -0,0 +1,155 @@
# Cathedral rendering — the seven-report synthesis and adjudicated plan
2026-08-30. Synthesizes: the fable review, the opus review, the external
model review (owner-supplied), the code+probe-log audit, the DAT-format
report, the F418 DAT-geometry report, and the retail decomp
reconstruction (`2026-08-30-cathedral-review-*.md`). Where reports
disagreed, the adjudication below names the winner and the evidence.
## Adjudications (the disagreements, settled)
1. **Root kind at the terrace.** The fable review said "retail never
roots interior at the terrace" (top-level free traces: zero DI); the
external review cited `DI f4180104, ov=1, n=1`. BOTH are right about
different captures: `posed/terrace-center.log` (the defect pose) shows
39× `DI f4180104` + `DC ov=1 n=1`; the top-level free captures rooted
outdoor. VERDICT: retail roots interior at our defect pose, with a
ONE-CELL flood and healthy exit views. The divergence to chase is the
FLOOD SHAPE and the exit-view survival, not the root kind.
2. **The falls' identity.** Opus said cell-0x106 stabs; the DAT-geometry
report proves the falls emitters are OUTDOOR LANDBLOCK OBJECTS
(0xCF41800013: 7 east jets Setup 0x0200085A, 7 north jets 0x02000859,
6 lake mist 0x020003C5), and 0x4F41801215 are cell 0x106's TORCHES +
prop + plant. VERDICT: DAT-geometry wins (byte-exact parse); slice 5
moved torches — the in-cathedral particle regression. Confirmed by
the external review. Residual: trace the 0xC owners' live route
(never directly observed in any probe — instrumentation step 1).
3. **"Thin authored seam bands."** REFUTED by the DAT: all exits at
0x104/0x106 are full-size (10.72×10.20 m facades, 24×9.2 m arches).
Thin/empty exit views (the 66 ov==0 frames) are OUR
projection/clip/traversal loss. The REAL seam: 0x104's south exit and
0x106's north exit are the SAME coincident rectangle at y=48, EACH
authored exit-to-outside — two abutting buildings; retail cannot
flood between these cells (they meet only through 0xFFFF portals).
4. **"Retail gates clear+seals on ov>0."** Refined by the decomp
reconstruction: the WHOLE landscape→drain→stamp→clear→seals block is
inside `ov > 0` (pc:432715), and the depth clear is additionally a
ONE-FRAME LATCH armed by the previous frame's seals
(`portalsDrawnCount`, pc:432725-32). Ours emits clear+seals
unconditionally — divergence confirmed, mechanism refined.
5. **"Stale color."** Retail full-clears COLOR+depth every frame at
BeginScene (Clear(7), pc:68673-86). Whether our ov==0 frames show
stale color depends on our world-pass color-load behavior —
UNVERIFIED; the instrumentation phase settles it. Either way the
ov==0 frames are wrong (retail has ov=1 at those poses).
## The frame truth (retail, interior root — the decomp reconstruction)
See `2026-08-30-cathedral-review-decomp.md` for the full pc-cited version.
Load-bearing facts:
- TWO PViews: the root's (draw_landscape=1) and the look-ins'
(draw_landscape=0, ov always 0) — look-in floods are ISOLATED from the
root flood and never trigger landscape/clear/seals.
- Landscape (sky, terrain in_view-culled + drawn whole, buildings with
punches + look-in interiors, outdoor objects INCLUDING the falls
emitters' parts) all draws PRE-clear; the full alpha drain at
pc:432720 resolves outdoor translucency vs outdoor depth.
- m_nFrameStamp re-arms mid-frame (pc:432722): straddling parts draw in
BOTH scopes. "Emit once" is anti-retail.
- z-only clear (latch-gated) → exit seals: EVERY 0xFFFF portal of EVERY
flooded cell, TRUE depth, DEPTHTEST_ALWAYS + write, invisible fan,
clipped to the installed view.
- Interior shells (whole, once, z-test only) → interior objects/emitters
(per-view sphere-cull, whole) → final drain vs interior+seal depth.
- Alpha lists: two FIFOs, NO sort at flush; order = the walk +
per-cell CYpt insertion sort. Full-flush sites: DrawBuilding (0f!),
DrawCells pre-clear, RenderNormalMode final; DrawBlock's per-cell
0.75 pressure valve (BN misread resolved — it DOES fire).
- ShouldDrawParticles = UPDATE-time NoDraw (distance; cell test VACUOUS
for interior emitters — CEnvCell inherits constant PARTIALLY_INSIDE,
PDB-verified ICF fold @0x005269f0).
## Why six fixes failed (unanimous)
Slices 13+6 repaired real seams of the hybrid renderer; slices 45
acted on a misidentified target (torches ≠ falls) with an invented
mechanism (sphere-straddle) — and no round measured depth at the
artifact pixels or verified the target's identity against the DAT before
editing. The two live defect mechanisms (the ov==0 frames; the missing
degrade admission for marker anchors) were never in any slice's scope.
## Per-commit ledger (final)
- 37febd1f slice 1 — KEEP (transitional; the walk owns outside views;
fixed real punch-fan index aliasing).
- c40aecfc slice 2 — KEEP the rule (seal the walk's flood); REWORK: use
the walk cell's installed views for the clip, not legacy per-cell
slices/no-clip fallback (plan step 4).
- 5f7ccdea slice 3 — KEEP mechanically for now; REWORK: VisitedCells is
a union of root flood + look-ins — retail stages those differently
(plan step 2 splits the sets and re-sources the predicate).
- f3a03efc slice 4 — REVERTED (7f500b97).
- 0d6cd5c0 slice 5 — REVERTED (7f500b97).
- bb9212c7 slice 6 — KEEP the one-turn/no-hard-clip principle; FIX: the
interior terrain currently uses the full camera frustum — restore
retail's in_view landcell culling under the active exit views
(draw_check_blocks @0x00505f80) (plan step 8).
## The plan (instrument-first; each step gated)
PHASE I — instrumentation (NO rendering edits):
1. **Falls-owner trace**: watch owners 0xCF41800013 live — route
(outdoor-static vs other), walk turn, submission phase, drain point,
projected bounds/depth. Never infer identity from aggregate sets
again.
2. **Depth checkpoints**: read back depth at one known-bad pixel after
terrain / around punches / before the pre-clear drain / after clear /
after seals / before the final drain. Distinguishes missing terrain
depth vs punch overwrite vs clear/seal mis-gating vs particle depth
state.
3. **Pose conformance**: drive OUR walk at posed/terrace-center's exact
camera. Retail truth: DI f4180104, root flood n=1, ov=1, and separate
look-ins. Compare our root flood size, exit-view survival, and the
VisitedCells composition (root vs look-in members). If we produce
ov=0 or a multi-cell ROOT flood there, fix WalkPView
projection/side/clip math FIRST — the F6779 foundry residue is the
same family, and the coincident y=48 double-exit plane is the prime
suspect surface.
PHASE II — decomp-pinned repairs (one at a time, each with a falsifiable
prediction and a connected check):
4. Split the stage sets: InteriorFloodCells (root flood, post-clear) /
look-in cells (pre-clear, landscape scope) / VisitedLandscapeCellIds —
delete every union-based stage decision (incl. slice 3's predicate
source and the cell-owners particle union).
5. Gate clear+seals as retail does (the ov>0 block; note the seal-latch
nuance for the clear).
6. Seals from the walk cell's installed views (drop the legacy clip
borrow + unclipped fallback).
7. Retail particle staging: emitters draw at their owner cell's actual
walk turn; adopt the frame-stamp boundary (mid-frame re-arm) so
straddling parts draw in both scopes; delete SphereStraddlesExitPlane.
8. Alpha parity: DrawBuilding barrier = FULL flush; the queue preserves
insertion order (remove the global distance sort); keep the 0.75
per-cell valve semantics.
9. Terrain: whole + unclipped (slice 6) but culled by the walk's
in_view landcell set (draw_check_blocks port).
10. Re-land the runtime-hidden degrade admission for outdoor objects
(the stalks = drawn never-drawn marker anchors; fce250ec is NOT an
ancestor of this branch). If a stalk survives, log its live
GUID→Setup→GfxObj chain.
PHASE III — FW4 completion: delete the legacy PortalVisibilityBuilder
production role, legacy clip frames/viewcone feeds, packed dynamic
routes; the walk becomes the sole frame-decision owner; architecture doc
updated (it currently overstates single ownership).
## Open questions carried
- Depth at the leaking pixel pre-drain (Phase I.2 answers).
- The 0xC falls owners' actual live route (Phase I.1 answers).
- Why full-size authored portals yield thin/zero views at some poses
(Phase I.3 localizes; the fix lands in WalkPView).
- Our world-pass color-load behavior (stale color vs black on ov==0).
- Seal/punch clip equivalence vs retail setup_view+polyClipFinish.
- The ~1.5 MB/frame walk alloc tail (FW6).

View file

@ -189,6 +189,14 @@ public static class RenderingDiagnostics
/// Written only while the probe is on.</summary> /// Written only while the probe is on.</summary>
public static string WalkRootPhase = ""; public static string WalkRootPhase = "";
/// <summary>Companion scratch for <see cref="ProbeWalkRootEnabled"/>
/// (throwaway, same investigation): true on the frames (~every 90th)
/// where the walk's PView logs its per-portal seen/in-view/clip
/// decisions as <c>[walk-portal]</c> lines — pins WHY an exit view
/// collapses at a given camera pose. Set by the renderer before
/// Collect; read by WalkPView.</summary>
public static bool WalkPortalProbeThisFrame;
/// <summary> /// <summary>
/// Bounded-propagation port apparatus (2026-06-08). When true, PortalVisibilityBuilder.Build emits /// Bounded-propagation port apparatus (2026-06-08). When true, PortalVisibilityBuilder.Build emits
/// one [portal-churn] summary line per call: per-cell pop count (re-pops = churn), total re-enqueues, /// one [portal-churn] summary line per call: per-cell pop count (re-pops = churn), total re-enqueues,