docs: the cathedral seven-report synthesis + adjudicated FW4 plan
Files the independent review corpus (fable/opus/external + audit, DAT-format, F418 DAT-geometry, retail decomp reconstruction) and the synthesis that adjudicates their disagreements: retail roots INTERIOR at the posed terrace defect pose with a ONE-CELL flood and ov=1 (the free captures rooted outdoor - both were real); the falls are outdoor landblock emitters 0xCF418000-13; the "thin seam bands" claim is DAT-refuted (full-size exits; the seam is a coincident double exit-to-outside plane at y=48); clear+seals live inside retail''s ov>0 block with a one-frame seal latch. Carries the instrument-first plan (falls-owner trace, depth checkpoints, pose conformance) and the decomp-pinned repair sequence toward FW4''s single-owner goal. Also sweeps in the probe scaffolding slice 5''s path-limited add had missed (RenderingDiagnostics field + the launch-options row wording). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
7f500b97a3
commit
6d990d38b3
10 changed files with 1940 additions and 1 deletions
157
docs/research/2026-08-30-cathedral-review-audit.md
Normal file
157
docs/research/2026-08-30-cathedral-review-audit.md
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# Cathedral review — code + probe-log audit (companion report)
|
||||
|
||||
Delivered 2026-08-30 by the code/log audit agent (separate from the
|
||||
fable/opus mechanism reviews; this one audited the six commits, the exact
|
||||
HEAD frame flow, and every `fw3-*`/`fw4-*` probe log of the day). The full
|
||||
text below is verbatim from the agent's report.
|
||||
|
||||
## Executive synthesis
|
||||
|
||||
1. All six FW4 slices behave as written on frames where the walk yields
|
||||
exit views — the post-slice-6 log shows the falls emitting pre-clear
|
||||
and draining over full-screen terrain depth on those frames.
|
||||
2. Two defect mechanisms visible in the FINAL log are untouched by all
|
||||
six commits:
|
||||
- **Anomaly 1 — `ov==0` frames:** 66 of 26,601 interior frames at the
|
||||
terrace produced ZERO walk exit views → `RetailFrameWalk.DrawInside`
|
||||
skips `DrawLandscape` entirely → no terrain color, no terrain depth,
|
||||
no sky that frame. Everything drawn that frame (pre-clear alpha
|
||||
included — 17 frames had the falls straddler set drain against an
|
||||
EMPTY depth buffer) composites over stale color. These are the seam
|
||||
frames. Slices 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.
|
||||
93
docs/research/2026-08-30-cathedral-review-datformat.md
Normal file
93
docs/research/2026-08-30-cathedral-review-datformat.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# Cathedral review — DAT-format facts (companion report)
|
||||
|
||||
Delivered 2026-08-30 by the DAT-format verification agent (decompiled the
|
||||
actual `Chorizite.DatReaderWriter 2.1.7` NuGet binary with ilspycmd and
|
||||
cross-checked against `docs/research/named-retail/acclient.h`). Key facts
|
||||
for the cathedral synthesis; flagged caveats preserved.
|
||||
|
||||
## Sourcing caveats
|
||||
|
||||
- Our DAT parser is the CONSUMED NuGet `Chorizite.DatReaderWriter 2.1.7`
|
||||
(no vendored source; facts below come from decompiling the exact linked
|
||||
DLL).
|
||||
- `references/WorldBuilder` is an uninitialized submodule in THIS worktree
|
||||
(read from the main worktree at a different SHA — flagged).
|
||||
- `references/ACViewer` does not exist in either worktree (claims about
|
||||
ACViewer: UNVERIFIED).
|
||||
|
||||
## EnvCell record (read order, decompiled Unpack)
|
||||
|
||||
Flags (int) → self-Id (uint, duplicate) → NumSurfaces (byte) → NumPortals
|
||||
(byte) → NumVisibleCells (ushort) → Surfaces[](ushort) → EnvironmentId
|
||||
(ushort) → CellStructure (ushort) → Position (Frame) → CellPortals[] →
|
||||
VisibleCells[] (ushort) → [HasStaticObjs 0x2] StaticObjects (count +
|
||||
Stab{Id,Frame}[]) → [HasRestrictionObj 0x8] RestrictionObj.
|
||||
|
||||
`EnvCellFlags`: SeenOutside=0x1, HasStaticObjs=0x2, HasRestrictionObj=0x8.
|
||||
**0x4 is unclaimed.**
|
||||
|
||||
Retail cross-check: `CEnvCell : CObjCell` (acclient.h:32072-32091) maps
|
||||
1:1; retail's `stab_list` lives on the BASE `CObjCell`
|
||||
(acclient.h:30914-30932) and is the **PVS list of CELL IDS** — NOT placed
|
||||
objects. The DatReaderWriter `Stab{Id,Frame}` placed-object struct is a
|
||||
coincidental homonym (zero matches for `Stab` in the named-retail header).
|
||||
|
||||
## CellPortal / BuildingPortal
|
||||
|
||||
`CellPortal { PortalFlags Flags; ushort PolygonId; ushort OtherCellId;
|
||||
ushort OtherPortalId; }` — `PortalFlags: ExactMatch=1, PortalSide=2`.
|
||||
**No 0x4 member anywhere** (package, our code, WorldBuilder, and retail's
|
||||
own `CCellPortal` decode exactly two semantic fields: portal_side,
|
||||
exact_match). NOTE: this contradicts the earlier campaign note
|
||||
"CCellPortal DAT flag 0x4 = leads outside" — the outside link is the
|
||||
`OtherCellId == 0xFFFF` sentinel, not a flag bit.
|
||||
|
||||
- ExactMatch consumers: `PortalVisibilityBuilder.cs:1168-1183` (skips
|
||||
reciprocal clip — decomp:433689), Walk factories
|
||||
(`WalkBuildingFactory.cs:76`, `WalkCellFactory.cs:94`).
|
||||
- PortalSide convention: `(Flags & 0x2) == 0 → normal points INTO the
|
||||
owning cell` (`CellPortal.cs:23-24`, `PortalInfo.cs:22-44`);
|
||||
`WalkCellFactory.cs:93`: `PortalSide = (Flags & 0x2) != 0 ? 0 : 1`.
|
||||
- `OtherCellId == 0xFFFF` = the outdoor-exit sentinel (18+ consumers);
|
||||
the walk world remaps it to `0xFFFFFFFFu`.
|
||||
|
||||
`BuildingPortal` (BuildingInfo.Portals) is a DISTINCT dat type — carries
|
||||
its own **packed StabList of cell ids** (retail `CBldPortal` with
|
||||
num_stabs/stab_list + `sidedness`). The walk's `WalkBuildingFactory.cs:81`
|
||||
reads it directly; the older `Wb.BuildingLoader` path DISCARDS it and
|
||||
recomputes by BFS.
|
||||
|
||||
## LandBlockInfo
|
||||
|
||||
`NumCells → Objects[Stab] → Buildings[BuildingInfo{ModelId, Frame,
|
||||
NumLeaves, Portals[BuildingPortal]}] → [flag] RestrictionTable`.
|
||||
Confirmed field-for-field; consumer `LandblockLoader.cs:69-103`.
|
||||
|
||||
## Water / waterfall (part d)
|
||||
|
||||
- `LandDefs::WaterType` is COMPUTED client-side from terrain paint (types
|
||||
0x10-0x14 WaterRunning..WaterDeepSea); AcDream already computes it for
|
||||
physics (`TerrainSurface.cs:92-128`). It is NOT packed in the records.
|
||||
- `SurfaceTexture.Textures` is a multi-frame list (the structural shape of
|
||||
a flip-book animation) but EVERY consumer in both repos freezes one
|
||||
fixed index (`Textures[0]`, WB `[^1]`) — **no water/texture animation is
|
||||
implemented anywhere**; whether retail animates via this list is
|
||||
UNVERIFIED.
|
||||
- Which object IS "the waterfall" near the cathedral: UNVERIFIED from
|
||||
source — two old bug comments treat it as OUTDOOR landscape-alpha
|
||||
content (`PortalVisibilityBuilder.cs:730-736`,
|
||||
`RetailPViewRenderer.cs:714-726`), consistent with a translucent stab —
|
||||
but note the live probes identified the terrace falls PARTICLE owners as
|
||||
EnvCell StaticObjects of cell 0xF4180106 (ids 4f418012-15), which is
|
||||
the `HasStaticObjs` list above; both can be true (mesh stab outdoors +
|
||||
emitter stabs in cells).
|
||||
|
||||
## Notable corrections this report makes to campaign lore
|
||||
|
||||
1. "DAT flag 0x4 = leads outside" (PV campaign memory) — REFUTED: no 0x4
|
||||
exists; outside = OtherCellId 0xFFFF.
|
||||
2. "stab list" ambiguity — retail stab_list = PVS cell ids on CObjCell;
|
||||
placed objects are a different structure; docs/comments conflating the
|
||||
two should be read carefully.
|
||||
3. Building portals carry authored per-portal cell StabLists in the dat —
|
||||
the old Wb path recomputes what the dat already provides.
|
||||
111
docs/research/2026-08-30-cathedral-review-datgeometry.md
Normal file
111
docs/research/2026-08-30-cathedral-review-datgeometry.md
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
# Cathedral review — F418 authored DAT geometry (companion report)
|
||||
|
||||
Delivered 2026-08-30 by the DAT-geometry agent: fresh byte-exact parsers
|
||||
over the installed `client_cell_1.dat`/`client_portal.dat` (every record
|
||||
parsed with 0 bytes left over). Ground truth for the synthesis. Condensed;
|
||||
citations and caveats preserved from the agent's full text.
|
||||
|
||||
## The landblock (LandblockInfo 0xF418FFFE)
|
||||
|
||||
24 EnvCells (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.
|
||||
141
docs/research/2026-08-30-cathedral-review-decomp.md
Normal file
141
docs/research/2026-08-30-cathedral-review-decomp.md
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# Cathedral review — retail interior-root frame reconstruction (companion report)
|
||||
|
||||
Delivered 2026-08-30 by the decomp-reconstruction agent (BN pseudo-C +
|
||||
live Ghidra arbitration on 8081 + acclient.h + symbols.json). THE retail
|
||||
mechanism reference for the synthesis. Condensed; all pc:/address
|
||||
citations preserved.
|
||||
|
||||
## Frame skeleton (interior root)
|
||||
|
||||
1. `SceneTool::BeginScene` @0x0043dad0: **full Clear(7) — COLOR+depth+
|
||||
stencil — every frame** (pc:68673-68686). `Flip` bumps m_nFrameStamp
|
||||
at frame end (pc:428642).
|
||||
2. `RenderNormalMode` @0x00453aa0: interior camera → optional
|
||||
LScape::update_viewpoint (only when viewer_cell->seen_outside,
|
||||
pc:92667-71) → DrawInside(viewer_cell) → **unconditional final drain
|
||||
FlushAlphaList(0f)** (pc:92687).
|
||||
3. TWO PView singletons (pc:427800/427813): `indoor_pview` (draw_landscape
|
||||
=1) for the root; `outdoor_pview` (0) for building look-ins — look-in
|
||||
floods can never re-trigger landscape/clear/seals (their
|
||||
outside_view.view_count is always 0; ClipPortals never raises it when
|
||||
draw_landscape=0, pc:433666-84).
|
||||
4. `PView::DrawInside` @0x005a5860: push root view (full viewport) →
|
||||
`ConstructView(root, 0xffff)` — PURE CPU flood, draws nothing; exit
|
||||
portals merge clipped apertures into outside_view (pc:433662-76,
|
||||
cliplandscape=1 @0x00820f4c).
|
||||
|
||||
## PView::DrawCells @0x005a4840 — ALL of this inside `ov > 0` (pc:432715):
|
||||
|
||||
1. `Render::PortalList = &outside_view`; `LScape::draw` (pc:432718-19).
|
||||
2. **Full outdoor alpha drain** FlushAlphaList(0f) (pc:432720).
|
||||
3. `m_nFrameStamp += 1` (pc:432722) — re-arms part dedupe so a part can
|
||||
draw in BOTH the outdoor and interior passes of one frame (straddlers
|
||||
draw twice; "emit once" is anti-retail).
|
||||
4. **Gated z-only clear** (pc:432725-32, Ghidra-verified):
|
||||
`forceClear(const 0) || portalsDrawnCount != 0` — the counter is
|
||||
incremented ONLY by own-depth portal stamps (seals, arg2==0,
|
||||
pc:424569-70) → **a ONE-FRAME LATCH: last frame's seals arm this
|
||||
frame's clear.** Steady-state vista frames clear every frame; the
|
||||
FIRST such frame does not.
|
||||
5. **Exit seals** (pc:432737-807): per flooded cell (far→near), per view,
|
||||
EVERY portal with other_cell 0xffffffff:
|
||||
`DrawPortalPolyInternal(poly, 0)` — REAL projected depth,
|
||||
DEPTHTEST_ALWAYS + z-write ON (pc:424584), invisible (vertex alpha 0,
|
||||
pc:424686), CULL_NONE, CPU-clipped to the current view (pc:424577).
|
||||
(Sibling mode arg2==1 = the far-Z PUNCH z≈0.999999, look-ins only;
|
||||
does not increment the counter.)
|
||||
|
||||
Then UNCONDITIONALLY (even when ov==0):
|
||||
6. Lighting restore (pc:432811-12).
|
||||
7. **Cell shells** far→near: DrawEnvCell dedupes per frame stamp — the
|
||||
shell draws ONCE, WHOLE; the per-view loop is vestigial for it; no
|
||||
view planes applied; correctness = pure z-test (pc:427885-88).
|
||||
8. **Cell objects** far→near: PortalList = the cell's views →
|
||||
DrawObjCellForDummies → shadow parts sorted by CYpt (viewer distance
|
||||
to scaled sort_center, insertion_sort @pc:701156-75) →
|
||||
CPhysicsPart::Draw — statics, dynamics, AND particle-emitter parts
|
||||
through ONE machinery.
|
||||
9. Final drain (pc:92687) vs interior depth + seal-stamped apertures.
|
||||
|
||||
## Clip vs cull (the definitive table)
|
||||
|
||||
- CPU polygon-clipped: ONLY portal-chain polys during the flood
|
||||
(GetClip @0x005a4320) and seal/punch fans at draw (their own
|
||||
polyClipFinish vs the current view, pc:424577, + a ±12 local-XY trivial
|
||||
reject pc:424506-50).
|
||||
- Sphere-vs-view CULLED, whole-drawn: all meshes (statics, dynamics,
|
||||
particles' parts) — DrawMesh per view: viewconeCheck(sphere)
|
||||
(pc:429295-96); PARTIAL draws WHOLE under that view; a mesh in N views
|
||||
submits N times. Building PORTAL pass is cone-exempt (punches always
|
||||
attempted; GetClip <3 points is the real stop, pc:429302-10).
|
||||
- Cached-flag culled: terrain cells via in_view stamped by
|
||||
draw_check_blocks @0x00505f80 iterating EVERY view of PortalList
|
||||
(blocks wholly outside every exit view skip at pc:267936). Terrain
|
||||
polys then draw whole + immediate (landPolysDraw @0x006b7040 — no view
|
||||
clip, no alpha list).
|
||||
- No test at all: EnvCell shell geometry; seal z-behavior (ALWAYS).
|
||||
- `alwaysDrawObjects = 1` (@0x00820ed4): DrawSortCell runs for EVERY cell
|
||||
of a drawn block — the per-part per-view cone check is the real object
|
||||
gate (no pixel leak, just CPU).
|
||||
|
||||
## The alpha lists (retail)
|
||||
|
||||
TWO static FIFO lists, 3000 entries each (Clip @0x00871b10, Alpha
|
||||
@0x008af370). NO depth key stored, NO sort at flush — order = submission
|
||||
order; far-to-near correctness comes from the cell walk + per-cell CYpt
|
||||
insertion sort. Overflow DROPS the subset (pc:424983). Entry routing:
|
||||
s_AlphaDelayMask (default 0xE) vs the surface mask; bit3 → Clip list.
|
||||
|
||||
Flush sites (grep-complete):
|
||||
| # | Site | Arg | Depth in place |
|
||||
|---|------|-----|----------------|
|
||||
| 1 | DrawBuilding @0x0059f30b | 0f FULL | outdoor-so-far (pre-punch) |
|
||||
| 2 | DrawBlock @0x005a1a07 | 0.75 valve, fires per outdoor cell (BN's `flush>1` guard is a MISREAD — Ghidra: fires when flush<=1.0) | mid-landscape |
|
||||
| 3 | DrawCells @0x005a4872 | 0f FULL | complete outdoor depth (pre-clear) |
|
||||
| 4 | RenderNormalMode @0x00453b8b | 0f FULL (final) | interior + seals |
|
||||
| 5 | CreatureMode::Render (chargen only) | 0f | its own viewport |
|
||||
|
||||
## Depth at final drain (interior root)
|
||||
|
||||
- Aperture/vista pixels: the SEAL's portal-plane depth (outdoor per-pixel
|
||||
depth NO LONGER EXISTS — cleared; outdoor COLOR survives under the
|
||||
stamp). Interior alpha fragments behind the portal plane FAIL there.
|
||||
- Outdoor particles (falls): drawn in the landscape pass, drained at
|
||||
site #3 vs outdoor depth. Cone-PARTIAL spill outside the aperture is
|
||||
buried because EVERY interior pixel paints after every outdoor drain
|
||||
point — protection is pure ORDERING, not depth.
|
||||
|
||||
## ShouldDrawParticles @0x0050fe60 — an UPDATE-time gate
|
||||
|
||||
Called per emitter from ParticleEmitter::UpdateParticles (physics-side),
|
||||
NOT during rendering: `ExaminationObject → 1; CYpt > degrade_distance →
|
||||
0; cell NULL → 0; cell->IsInView()==0 → 0`. On false: SetNoDraw(1),
|
||||
degraded_out=1 (particles stop updating AND drawing).
|
||||
- CLandCell::IsInView @0x00532cb0 = the PREVIOUS frame's stamped in_view
|
||||
(one-frame-stale gate).
|
||||
- **CEnvCell does NOT override it** — the slot resolves to the ICF-folded
|
||||
constant @0x005269f0 `return 1` (PARTIALLY_INSIDE, PDB-verified): for
|
||||
interior emitters the cell test is VACUOUS; distance is the only
|
||||
update-time gate; their draw is flood-gated instead.
|
||||
|
||||
## Can content outside the flood/exit views draw? NO.
|
||||
|
||||
Interior: pure flood membership. Outdoor blocks/cells: in_view under ≥1
|
||||
exit view. Outdoor parts: per-exit-view sphere-cone in DrawMesh. Building
|
||||
portal pass cone-exempt but clip-stopped. No distance bypass exists.
|
||||
|
||||
## Corrections to campaign docs
|
||||
|
||||
1. DrawBlock per-cell flush polarity RESOLVED (fires; 0.75 pressure
|
||||
valve) — BN misread.
|
||||
2. The m_nFrameStamp bump separates the landscape and interior passes of
|
||||
ONE interior-root frame; look-ins NEVER bump (outdoor_pview ov==0).
|
||||
3. NEW: CEnvCell::IsInView = constant PARTIALLY_INSIDE (ICF fold,
|
||||
symbols.json 19-name cluster) — not in any FW doc.
|
||||
|
||||
## UNVERIFIED (the agent's own list)
|
||||
|
||||
The 0xF418 falls emitters' owner (land cell vs EnvCell static — dat
|
||||
question; note the DAT-geometry companion resolves this: landblock
|
||||
object-list stabs); z-write during alpha drains; insertion_sort
|
||||
direction byte-proof; the ±12 reject polarity; GameSky depth behavior.
|
||||
436
docs/research/2026-08-30-cathedral-review-fable.md
Normal file
436
docs/research/2026-08-30-cathedral-review-fable.md
Normal file
|
|
@ -0,0 +1,436 @@
|
|||
# Cathedral rendering review — FABLE (2026-08-30)
|
||||
|
||||
Independent report-only review of the cathedral-terrace defect (falls
|
||||
particles + marker stalks over terrain; landblock `0xF418`, camera in
|
||||
`0xF4180104`/`0xF4180106`), baseline `37febd1f`, HEAD `bb9212c7`, branch
|
||||
`claude/campaign-w-retail-frame-walk`.
|
||||
|
||||
**Evidence basis and honesty note.** Everything below marked with a citation
|
||||
was read directly by this reviewer (oracle trace files, decomp pseudocode
|
||||
docs + their byte-verification notes, commit messages/diffs, git topology,
|
||||
ISSUES/register/memory docs, id-allocator source). Three deeper evidence
|
||||
threads (fresh decomp re-reads of the alpha/particle path, a raw DAT parse
|
||||
of `0xF418`, and the HEAD code-flow + probe-log audit) were dispatched but
|
||||
had not returned when this report was due; every claim that depended on
|
||||
them is explicitly marked **unverified**. Per the prompt's rule, unverified
|
||||
is stated as unverified — nothing below is a plausible guess dressed as
|
||||
fact.
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive verdict (≤10 lines)
|
||||
|
||||
The six fixes failed because they accepted a false premise about WHERE this
|
||||
scene's content sits in retail's frame. The posed oracle traces show that at
|
||||
both defect poses retail's interior flood is EXACTLY ONE cell (`DC ov=1 n=1:
|
||||
f4180104` / `f4180106`) — every other cathedral cell, including the falls'
|
||||
stab-owner cells, draws as a *building look-in inside the landscape scope*,
|
||||
pre-depth-clear, its alpha drained against full landscape depth
|
||||
(`posed/terrace-center.log` F5, `posed/cathedral-arrival.log` F1-2;
|
||||
`PView::DrawCells` @0x005a4840 model §5). Our pipeline instead treats the
|
||||
whole complex as interior-flood content drawing post-clear, then tried to
|
||||
rescue it with seals (slice 2) and a non-retail exit-plane-straddle
|
||||
classifier (slices 4/5) that under-covers exactly the fully-outside falls
|
||||
emitters. Fix = route every object's particles to its owner cell's actual
|
||||
walk turn (look-in ⇒ landscape scope ⇒ pre-clear drain), delete the straddle
|
||||
classifier, and re-land the quarantined marker-degrade port for the stalks.
|
||||
|
||||
---
|
||||
|
||||
## 2. Retail frame reconstruction (for an interior root with exit portals)
|
||||
|
||||
### 2.1 The frame skeleton — verified from the FW0 model + live traces
|
||||
|
||||
Source of truth used: `docs/research/2026-08-30-fw-walk-pseudocode.md`
|
||||
(§1, §2, §3, §5, §9 — its seal loop, deferred z-clear, DEPTHTEST_ALWAYS,
|
||||
punch-Z constants are recorded there as byte-verified against the raw
|
||||
pseudo-C at `@0059a49af`, `@005a48a9`, `@0059be02`, `@0059bf4b`), the
|
||||
extraction appendix `2026-08-30-fw-walk-pseudocode-appendix.md` (notably
|
||||
the DrawBuilding/DrawMeshInternal full-sequence note at appendix line 811),
|
||||
and the live cdb traces in `docs/research/2026-08-30-fw-walk-oracle/posed/`.
|
||||
|
||||
Order of one interior-rooted frame (`SmartBox::RenderNormalMode`
|
||||
@0x00453aa0 → `PView::DrawInside` @0x005a5860 → `ConstructView` →
|
||||
`PView::DrawCells` @0x005a4840):
|
||||
|
||||
1. **Flood** (`ConstructView(CEnvCell)` @0x005a57b0): view-driven — a
|
||||
neighbor cell enters `cell_draw_list` only if its portal polygon
|
||||
survives `PView::GetClip` @0x005a4320 against the current view (the ONLY
|
||||
CPU polygon clip in the frame). Exit portals
|
||||
(`other_cell_id == 0xFFFFFFFF`) raise `outside_view`.
|
||||
2. **If `outside_view.view_count > 0`** (the traces' `ov`):
|
||||
a. `Render::PortalList = &outside_view`; **`LScape::draw`** @0x00506330
|
||||
— the whole landscape walk THROUGH the exit views: sky, terrain
|
||||
blocks far-to-near (view-CULLED per cell/block by
|
||||
`draw_check_blocks`' in_view union — **never view-clipped, never
|
||||
scissored**; `set_view` installs NO GPU state), buildings far-to-near,
|
||||
and — per building — the **look-in machinery**:
|
||||
`DrawBuilding` @0x0059f2a0 → BSP portal pass 1 punches apertures
|
||||
(`DrawPortalPolyInternal` flag=1: far-Z 0.999998987, DEPTHTEST_ALWAYS,
|
||||
z-write ON, alpha 0) → pass 2 recurses `ConstructView(CEnvCell)` +
|
||||
`DrawCells` (**`ov=0` — no landscape, no clear, no seals**) drawing
|
||||
look-in EnvCells + their objects into the punched aperture
|
||||
(appendix line 811). `DrawBuilding` itself calls
|
||||
**`FlushAlphaList(0f)`** before each building; `DrawBlock`
|
||||
@0x005a17c0 has the mid-walk pressure valve
|
||||
`FlushAlphaList(flush=0.75)` (appendix lines 700, 1027).
|
||||
b. **`FlushAlphaList(0f)`** — the pre-clear boundary drain: EVERYTHING
|
||||
queued during the landscape scope (terrain alpha, building alpha,
|
||||
look-in cell content alpha — and the particle polys of objects drawn
|
||||
in that scope) drains against **full landscape depth**. This is the
|
||||
drain that occludes the falls with terrain/water/trees.
|
||||
c. **Gated depth clear** (`if (forceClear || portalsDrawnCount != 0)
|
||||
Clear(z)`, model §5; `portalsDrawnCount` counts ONLY flag-0 seals —
|
||||
appendix line 811). Color is preserved; only depth clears.
|
||||
d. **Exit seals**: for the FLOODED cells' exit portals only,
|
||||
`DrawPortalPolyInternal(poly, 0)` — the portal's own projected depth,
|
||||
DEPTHTEST_ALWAYS, z-write ON, invisible (model §2, §5; byte-verified
|
||||
per model §9).
|
||||
3. **Interior passes** (post-clear): `DrawEnvCell` far-to-near (BSP shells),
|
||||
then `DrawObjCellForDummies` (the flooded cells' objects) — depth
|
||||
accumulates fresh.
|
||||
4. **Final drain**: `FlushAlphaList(0f)` back in `RenderNormalMode` — the
|
||||
flooded cells' alpha/particles drain against interior depth + the seals.
|
||||
|
||||
### 2.2 The four prompt questions
|
||||
|
||||
**(a) clipped vs culled vs whole.** CPU-clipped: ONLY the portal chain's own
|
||||
polygon (`GetClip` @0x005a4320 — the model calls it "the only CPU polygon
|
||||
clip in the frame"). View-culled (coarse sphere-vs-cone,
|
||||
`Render::viewconeCheck` @0x0054c250, PARTIAL draws whole): objects.
|
||||
Cell/block-granular culled (in_view interval grids): terrain blocks/cells.
|
||||
Drawn whole with no per-view test: everything that passes those gates —
|
||||
shells, meshes, particles. No GPU clip planes/scissor exist anywhere in this
|
||||
frame (`set_view` = CPU globals only, model §6).
|
||||
|
||||
**(b) order relative to clear and seals.** PRE-clear: sky, terrain, water
|
||||
(terrain-hosted — see §3, unverified detail), building shells, building
|
||||
punches, **look-in EnvCell shells + look-in cell objects + their
|
||||
emitters**, and all their alpha via the boundary drain. POST-clear: seals
|
||||
first, then ONLY the root flood's EnvCell shells + objects + their
|
||||
emitters, drained at the final flush. Seals draw exactly the flooded
|
||||
cells' exit-portal polygons at own depth, color-write effectively off
|
||||
(alpha 0), depth ALWAYS + write ON.
|
||||
|
||||
**(c) alpha list.** ONE list. Flush sites established from the docs:
|
||||
per-building `FlushAlphaList(0f)` in `DrawBuilding` (appendix 811); the
|
||||
`DrawBlock` pressure valve at `flush=0.75` (~2250 meshes, appendix 1032);
|
||||
the `DrawCells` pre-clear boundary `FlushAlphaList(0f)` (model §5); the
|
||||
`RenderNormalMode` end-of-frame `FlushAlphaList(0f)` (model §1). Depth in
|
||||
place: landscape depth at the first three; interior + seal depth at the
|
||||
final one. **Unverified:** an exhaustive caller enumeration of
|
||||
`D3DPolyRender::FlushAlphaList` @0x0059d2e0 from the decomp itself, and
|
||||
the exact particle-poly insertion path (the dispatched decomp read had not
|
||||
returned; the MP2-era digest additionally records
|
||||
`CPhysicsObj::ShouldDrawParticles` @0x0050FE60 = authored degrade range +
|
||||
`cell->IsInView()`, from `claude-memory/project_render_pipeline_digest.md`).
|
||||
|
||||
**(d) depth at final-drain time.** Aperture pixels: the seal's own depth
|
||||
(near the camera) — anything beyond the exits z-fails, so interior
|
||||
particles cannot repaint the vista. Vista pixels visible through the
|
||||
aperture ARE aperture pixels (the opening's polys); pixels covered by
|
||||
interior geometry hold that geometry's depth. The falls are never in this
|
||||
drain in retail at these poses — they drained pre-clear with the landscape.
|
||||
**Unverified residual:** whether any visible vista pixel can lie OUTSIDE
|
||||
the union of authored exit-portal polygons at the live chase camera (slice
|
||||
6's observation implies yes for the WALK's clipped views; whether that
|
||||
reflects portal-poly authoring or our clip is open — §8 Q1).
|
||||
|
||||
### 2.3 The live traces at the defect poses (decisive, read directly)
|
||||
|
||||
`posed/terrace-center.log` F5 (camera cell `f4180104` — the owner's
|
||||
teleloc cell):
|
||||
|
||||
```
|
||||
DI f4180104
|
||||
DC pv=009d4e38 ov=1 n=1: f4180104 ← the interior flood is ONE cell
|
||||
LS
|
||||
BLD f518002e … BLD f4180014 BLD f418000a BLD f418000c BLD f418000b (14 buildings)
|
||||
```
|
||||
|
||||
`posed/cathedral-arrival.log` F1-2 (camera cell `f4180106`):
|
||||
|
||||
```
|
||||
DI f4180106
|
||||
DC pv=009d4e38 ov=1 n=1: f4180106 ← flood = root cell only
|
||||
LS
|
||||
BLD f3180020 … BLD f418000c
|
||||
DC pv=009d4398 ov=0 n=2: f4180101 f4180100 ← look-in punch during LS
|
||||
… BLD f418000b
|
||||
DC pv=009d4398 ov=0 n=1: f4180104 ← 0xF4180104 IS A LOOK-IN
|
||||
BLD f418000a
|
||||
```
|
||||
|
||||
`posed/terrace-edge.log` F5 (outdoor root, camera cell `f418000b`): look-in
|
||||
punches `n=1: f4180106`, `n=1: f4180101`, `n=1: f4180104` interleaved with
|
||||
the building walk — no clear, no seals, everything drains against full
|
||||
depth (why the owner sees a correct scene outdoors).
|
||||
|
||||
**Conclusion:** at the defect poses, `0xF4180104` and `0xF4180106` are in
|
||||
DIFFERENT buildings' interiors (`f418000b` vs the `f418000a/c` group —
|
||||
consistent with the "seam" being a building boundary), retail's interior
|
||||
flood never crosses it, and ALL non-root cathedral cells + their stabs
|
||||
compose in the landscape scope, pre-clear. The "toggles with which side of
|
||||
the seam the camera is on" symptom is the root flip between these two
|
||||
one-cell floods (and outdoor).
|
||||
|
||||
---
|
||||
|
||||
## 3. DAT geometry findings
|
||||
|
||||
**Verified:**
|
||||
|
||||
- The falls' observed particle owners `0x4F418012`–`0x4F418015` decode per
|
||||
`src/AcDream.Core/World/InteriorEntityIdAllocator.cs:48`
|
||||
(`0x40000000 | X<<20 | Y<<12 | counter`) to **interior EnvCell-stab
|
||||
statics of landblock F418**, counters 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.*
|
||||
673
docs/research/2026-08-30-cathedral-review-opus.md
Normal file
673
docs/research/2026-08-30-cathedral-review-opus.md
Normal file
|
|
@ -0,0 +1,673 @@
|
|||
# Cathedral rendering review — independent report (Opus)
|
||||
|
||||
**Reviewer:** Opus 5, independent. **Mode:** report-only (no edits, no builds,
|
||||
no client launches, no commits). **Reviewed:** `37febd1f` (baseline) →
|
||||
`bb9212c7` (HEAD), branch `claude/campaign-w-retail-frame-walk`, worktree
|
||||
`peaceful-blackburn-5333f0`.
|
||||
**Date:** 2026-08-30.
|
||||
|
||||
Evidence classes used, in order of authority: (1) the named retail decomp
|
||||
(`docs/research/named-retail/acclient_2013_pseudo_c.txt` + Ghidra MCP at
|
||||
`127.0.0.1:8081`); (2) the installed DATs, read with the already-built
|
||||
`tools/A8CellAudit/bin/Release/net10.0/A8CellAudit.exe`; (3) the live probe
|
||||
logs in `%LOCALAPPDATA%\Temp\fw3-*.log` / `fw4-*.log`; (4) our source at both
|
||||
SHAs. Everything I could not verify is labelled **UNVERIFIED** inline.
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive verdict
|
||||
|
||||
1. **The ordering fix the last three slices were chasing already landed.** On
|
||||
HEAD the falls' four emitters submit *and drain* pre-clear
|
||||
(`fw4-slice6-gate.log:12786-12801`). Phase is no longer the binding
|
||||
constraint, so slices 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 <cell>`
|
||||
(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
|
||||
```
|
||||
165
docs/research/2026-08-30-cathedral-review-prompt.md
Normal file
165
docs/research/2026-08-30-cathedral-review-prompt.md
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
# Cathedral rendering review — independent troubleshooting prompt
|
||||
|
||||
You are an independent reviewer investigating a persistent rendering defect
|
||||
in **acdream** (a modern C# .NET 10 Asheron's Call client; the code is
|
||||
modern, the behavior must be retail). This is a **report-only**
|
||||
investigation: read anything, run read-only commands (git log/diff/show,
|
||||
grep, dat inspection), but do NOT edit code, do NOT build, do NOT launch
|
||||
clients, and do NOT commit. Your single deliverable is a written report
|
||||
(the path is given at the end).
|
||||
|
||||
## Where the code is
|
||||
|
||||
- Git worktree (the active campaign branch, work here):
|
||||
`C:\Users\erikn\source\repos\acdream\.claude\worktrees\peaceful-blackburn-5333f0`
|
||||
- Branch: `claude/campaign-w-retail-frame-walk`
|
||||
- HEAD at review time: `bb9212c7` (FW4 slice 6)
|
||||
- **Baseline before ANY cathedral-specific fix: `37febd1f`** — the owner
|
||||
first reported this defect on a build of that commit. Six fix commits
|
||||
followed (see below). The owner's verdict: *nothing has fixed anything*.
|
||||
- Main repository (for main-branch history): `C:\Users\erikn\source\repos\acdream`
|
||||
|
||||
## The defect (owner-observed, on a live ACE server at 127.0.0.1:9000)
|
||||
|
||||
At the cathedral terrace, landblock `0xF418`, standing in interior EnvCells
|
||||
`0xF4180104` / `0xF4180106` (owner teleloc: `0xF4180104 [31.9 57.3 169.8]`):
|
||||
|
||||
1. **Waterfall particles** (the blue falls over the lake outside) render on
|
||||
top of terrain, water, and other content that should occlude them.
|
||||
Intermittent — toggles with which side of "the seam" (the terrace's
|
||||
interior/outdoor boundary) the CAMERA is on. Standing outdoors near the
|
||||
falls (`0xF418000B`, `0xF4180015`) they look correct.
|
||||
2. **"Stalks"** (dark monster-marker shapes near the lake-edge trees,
|
||||
server-spawned weenies, e.g. entities parented at `0xF418010D`) leak
|
||||
over trees and scenery. This predates the current campaign.
|
||||
3. NEW (after the sixth fix): the owner reports **particles WITHIN the
|
||||
cathedral are also wrong** (uncharacterized — treat as a regression
|
||||
candidate of the fix stack).
|
||||
4. A previous session's analysis (owner-supplied) adds: the visible
|
||||
cathedral shell is assembled from multiple authored GfxObjs
|
||||
(`0x01001FB2`, `0x01001FB0`, `0x01001FB3`) + EnvCells + a portal graph;
|
||||
cell `0xF4180104` has exterior portals 0/1/2/4 and interior portal 3 →
|
||||
`0xF4180105`; "if an incorrect depth or portal route prevents one
|
||||
wall's colour pass from drawing, the wall appears transparent even
|
||||
though its geometry and opaque texture are present."
|
||||
|
||||
**The oracle fact that anchors everything: the RETAIL client connected to
|
||||
the SAME ACE server renders this scene perfectly.** So a correct
|
||||
client-side mechanism exists; the task is to find what retail actually
|
||||
does for this scene and where our port diverges.
|
||||
|
||||
## What you must do
|
||||
|
||||
1. **Reconstruct retail's actual frame for this scene** from the named
|
||||
decomp: `docs/research/named-retail/acclient_2013_pseudo_c.txt`
|
||||
(18,366 PDB-named functions; grep by `class::method`) and
|
||||
`docs/research/named-retail/acclient.h`. Key functions:
|
||||
`SmartBox::RenderNormalMode` @0x00453aa0, `PView::DrawInside` @0x005a5860,
|
||||
`PView::DrawCells` @0x005a4840 (landscape draw pc:432719, gated depth
|
||||
clear pc:432731-2, exit seals pc:432785-6), `LScape::draw` @0x00506330,
|
||||
`RenderDeviceD3D::DrawBlock` @0x005a17c0 (per-shadow-cell object draw
|
||||
pc:430056-430064), `RenderDeviceD3D::DrawSortCell` @0x0059f140,
|
||||
`RenderDeviceD3D::DrawBuilding` @0x0059f2a0,
|
||||
`CPhysicsObj::ShouldDrawParticles` @0x0050FE60,
|
||||
`D3DPolyRender::FlushAlphaList` @0x0059d2e0, `PView::GetClip` @0x005a4320,
|
||||
`PView::InitCell` @0x005a4b70. A Ghidra MCP HTTP server may be live at
|
||||
`http://127.0.0.1:8081` (`/decompile_function?address=0x...`) serving the
|
||||
same 2013 build with PDB names — use it when grep is too noisy.
|
||||
Answer specifically: for an interior root whose cell has exit portals,
|
||||
(a) what exactly gets view-clipped vs view-culled vs drawn whole;
|
||||
(b) when does each content class (terrain, sky, building shells,
|
||||
EnvCell shells, cell-static objects + their emitters, dynamic objects +
|
||||
their emitters, unattached emitters, water) draw relative to the gated
|
||||
depth clear and the exit seals; (c) how does the ONE alpha list drain
|
||||
(which flush sites, what depth is in place at each); (d) what depth
|
||||
does the frame hold at the aperture and at the vista at final-drain time.
|
||||
|
||||
2. **Read the cathedral's actual geometry from the installed DATs**
|
||||
(`%USERPROFILE%\Documents\Asheron's Call\`, DatCollection is the
|
||||
in-tree reader; `references/ACViewer` is a rendering oracle):
|
||||
landblock `0xF418` — the LandblockInfo object list, the EnvCells
|
||||
`0x0100`-`0x0115`-ish (esp. `0x0104`/`0x0105`/`0x0106`), their portal
|
||||
polygons (which are exits, flag 0x4 → other_cell 0xFFFF), their
|
||||
stab lists (the falls objects: local static ids `0x4F418012-15` were
|
||||
observed as the falls' particle owners; setup/gfx ids of waterfall
|
||||
objects), and where the water surface geometry lives. Establish:
|
||||
are the falls objects EnvCell stabs, landblock objects, or both?
|
||||
What shape/extent are the exit portal polygons vs the real visible
|
||||
opening (the prior campaign claims they are thin "seam bands")?
|
||||
|
||||
3. **Audit our implementation against that reconstruction**, at BOTH
|
||||
`37febd1f` (baseline) and `bb9212c7` (HEAD). The relevant code:
|
||||
`src/AcDream.App/Rendering/Walk/` (RetailFrameWalk, WalkPView,
|
||||
WalkFrameDriver, WalkViews, WalkScreenClip, WalkProductionWorldData),
|
||||
`src/AcDream.App/Rendering/RetailPViewRenderer.cs`,
|
||||
`RetailPViewPassExecutor*.cs`, `ClipFrameAssembler.cs`,
|
||||
`ClipPlaneSet.cs`, `ParticleRenderer.cs`, `RetailAlphaQueue.cs`,
|
||||
`PortalVisibilityBuilder.cs` (the legacy visibility layer FW4 is
|
||||
deleting), `Scene/RenderScenePViewFrameProduct.cs`.
|
||||
|
||||
4. **Judge the six fix commits individually** — each claims a retail
|
||||
citation; decide keep / revert / wrong-mechanism for each:
|
||||
- `37febd1f` FW4 slice 1 — interior outside-view slices from the walk
|
||||
(was: old PortalVisibilityBuilder slices; also fixed punch-fan index
|
||||
aliasing).
|
||||
- `c40aecfc` slice 2 — exit seals stamp the walk's own flood.
|
||||
- `5f7ccdea` slice 3 — outside-stage predicate reads the walk flood.
|
||||
- `f3a03efc` slice 4 — exit-plane straddler DYNAMICS' particles emit
|
||||
pre-clear once (was: deliberately last-pass).
|
||||
- `0d6cd5c0` slice 5 — straddling CELL-STATIC particles emit pre-clear
|
||||
(retail shadow-cell rule claim).
|
||||
- `bb9212c7` slice 6 — interior roots draw terrain+sky UNCLIPPED
|
||||
(view-culled only), one terrain turn (was: per-exit-view
|
||||
scissor+clip slices).
|
||||
Evidence from the live probes is in
|
||||
`%LOCALAPPDATA%\Temp\fw3-*.log` and `%LOCALAPPDATA%\Temp\fw4-*.log`
|
||||
(`[walk-root]`, `[walk-part]`, `[walk-alpha]`, `[walk-dyn]`,
|
||||
`[walk-portal]`, `[walk-emit]` lines; the probe is
|
||||
`ACDREAM_PROBE_WALK_ROOT`, documented in `docs/launch-options.md`).
|
||||
|
||||
5. **Explain why the symptom survived all six fixes**, and produce a
|
||||
concrete, mechanism-first fix plan (what to change, where, what retail
|
||||
evidence pins it, and what a falsifiable test/probe for it looks like
|
||||
BEFORE any code is written).
|
||||
|
||||
## Required background reading (in this order)
|
||||
|
||||
1. `claude-memory/project_pview_visibility_campaign.md` (the #456
|
||||
campaign memory — the proven mechanism model, the do-not-retry list;
|
||||
note: `claude-memory/` is a junction in the MAIN repo root at
|
||||
`C:\Users\erikn\source\repos\acdream\claude-memory\`; the same files
|
||||
are at `C:\Users\erikn\.claude\projects\C--Users-erikn-source-repos-acdream\memory\`)
|
||||
2. `docs/plans/2026-08-30-campaign-fw-frame-walk.md` (the active
|
||||
campaign plan — stages FW0-FW6, the FW1 conformance status, the FW4
|
||||
slice log)
|
||||
3. `claude-memory/project_render_pipeline_digest.md` (render SSOT +
|
||||
DO-NOT-RETRY table)
|
||||
4. `docs/research/2026-08-30-fw-walk-oracle/` (the ten retail cdb traces
|
||||
the walk is conformance-tested against)
|
||||
5. `docs/plans/2026-08-30-pview-visibility-campaign.md` (the abandoned
|
||||
PView campaign — what was tried and refuted)
|
||||
|
||||
## Hard rules
|
||||
|
||||
- The decomp is ground truth; ACE/ACViewer/WorldBuilder are interpretation
|
||||
aids. Where sources disagree, the decomp wins.
|
||||
- Do not propose workarounds (suppression flags, grace periods,
|
||||
symptom-site early-outs). Root mechanisms only.
|
||||
- Respect the do-not-retry lists in the memory docs (e.g., depth stamps as
|
||||
"retail's mechanism" is decomp-refuted; the panels are portal polys, not
|
||||
inert decoration).
|
||||
- Every claim in your report needs a citation: a decomp address, a DAT
|
||||
field, a file:line in our tree, a probe-log line, or a commit SHA.
|
||||
- If you cannot verify a claim, say so explicitly — an honest "unverified"
|
||||
beats a plausible guess. This investigation has already burned six fix
|
||||
rounds on plausible guesses.
|
||||
|
||||
## Deliverable
|
||||
|
||||
Write your full report to: **`docs/research/2026-08-30-cathedral-review-<YOURNAME>.md`**
|
||||
(replace `<YOURNAME>` with your model name, e.g. `fable`, `opus`,
|
||||
`external`). Structure: Executive verdict (≤10 lines) → Retail frame
|
||||
reconstruction → DAT geometry findings → Divergence table (ranked) →
|
||||
Per-commit verdicts → Why the symptom survived → Recommended fix plan →
|
||||
Open questions. Length: as long as it needs to be; completeness beats
|
||||
brevity.
|
||||
155
docs/research/2026-08-30-cathedral-synthesis.md
Normal file
155
docs/research/2026-08-30-cathedral-synthesis.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
# Cathedral rendering — the seven-report synthesis and adjudicated plan
|
||||
|
||||
2026-08-30. Synthesizes: the fable review, the opus review, the external
|
||||
model review (owner-supplied), the code+probe-log audit, the DAT-format
|
||||
report, the F418 DAT-geometry report, and the retail decomp
|
||||
reconstruction (`2026-08-30-cathedral-review-*.md`). Where reports
|
||||
disagreed, the adjudication below names the winner and the evidence.
|
||||
|
||||
## Adjudications (the disagreements, settled)
|
||||
|
||||
1. **Root kind at the terrace.** The fable review said "retail never
|
||||
roots interior at the terrace" (top-level free traces: zero DI); the
|
||||
external review cited `DI f4180104, ov=1, n=1`. BOTH are right about
|
||||
different captures: `posed/terrace-center.log` (the defect pose) shows
|
||||
39× `DI f4180104` + `DC ov=1 n=1`; the top-level free captures rooted
|
||||
outdoor. VERDICT: retail roots interior at our defect pose, with a
|
||||
ONE-CELL flood and healthy exit views. The divergence to chase is the
|
||||
FLOOD SHAPE and the exit-view survival, not the root kind.
|
||||
2. **The falls' identity.** Opus said cell-0x106 stabs; the DAT-geometry
|
||||
report proves the falls emitters are OUTDOOR LANDBLOCK OBJECTS
|
||||
(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).
|
||||
Loading…
Add table
Add a link
Reference in a new issue