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