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>
39 KiB
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
- 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. - The falls are
{4f418012..4f418015}— four STAB STATICS of EnvCell0xF4180106, 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/f4285are ~9.8 m-radius dynamics). - 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". - The only depth-destroying operation in the pre-clear scope is the
portal PUNCH (far-Z,
ALWAYS+write). Retail bounds it with an unconditional fullFlushAlphaList(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. - 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-landcellin_viewcull, and a sorted alpha drain (retail's has no sort). - The "stalks" have a completed, decomp-cited fix that is simply not on
this branch:
fce250ec(marker-class degrade admission) is not an ancestor ofbb9212c7. Nothing on this branch suppresses them. - 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.
- Recommendation: stop shipping fixes. Instrument the depth buffer and the punch census first (§7 Stage 0). Then land R1–R7, each gated.
- Commit verdicts: keep 1/2/3 (with corrections), revert 4, revert 5, keep 6's shape but fix its cull.
- 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):
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,
alphaedMeshListClipandalphaedMeshListAlpha(both capacity0xbb8= 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 < 0false).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
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:
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:
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/nptsare 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:
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:
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 model0x0200085A— 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:
- Wrong objects — same misidentification as slice 3.
- 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. - Contradicts the decomp. The commit's core invariant is "every owner
emits exactly once". Retail deliberately bumps
m_nFrameStampat0x005a4886— after the pre-clear drain — precisely so that a part drawn duringLScape::drawdraws again in the interior pass. TheExceptWith(_preClearParticleOwnerScratch)subtractions atRetailPViewRenderer.cs:2184and:2322implement 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::drawruns its block walk once, and the active views feed onlydraw_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_viewlandcells; the walk already computes exactly that set (RetailFrameWalk.DrawLandscapecallslandscape.CheckBlocks(ctx.CyPlane, activeViews)atRetailFrameWalk.cs:152and 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), andTerrainModernRenderer.VisibleCellIds— which feeds next frame'sShouldDrawParticlescell-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:
-
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_listis the visible-cell list (references/ACE/Source/ACE.DatLoader/FileTypes/EnvCell.cs:36-68; ourWalkCell.StabListis cell ids,WalkCellFactory.cs:105), while the placed objects areStaticObjects. The four falls were identifiable fromfw3-falls-probe.log— which existed before slice 2 — in one grep. -
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. -
Once
phase=prewas 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. -
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. -
Six commits in 105 minutes with no gate between them.
37febd1f18:21 →bb9212c720: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
- 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.
- Setup/GfxObj/DIDDegrade of
0xF4180106's four static objects. UNVERIFIED — needs astabsverb onA8CellAudit(no build of production code required). - 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.
- The
DrawBlockalpha-valve polarity (test ah,0x41@0x005a19fc). I read the idiom, not the raw bytes. Both readings make the valve inert at defaultflush, 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. - Are the observed stalks the marker class? Depends on the weenie Setup
ACE assigns. If not,
fce250ecwill not cover them and the guid → Setup chain must be logged live. - 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/WalkScreenClipthat outlives this bug. Worth one focused offline check against0xF4180104poly 36 (a 4-vertex ±12.00 m face) — project it and compare against the produced slice. - Does the one-frame lag in particle visibility matter here?
WorldSceneRenderer.cs:441-447marks visible cells at frame end for the next frame, while retail askscell->IsInView()on the current frame. Combined with slice 6's frustum-wideVisibleCellIds, 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