Pixel-grounded investigation concluded the indoor 'world from below' is a cell-MEMBERSHIP disagreement between render-side CellVisibility and physics-side ResolveCellId, not any single draw gate (terrain has one gated draw path; it leaks only on render null-root frames). Decision with user: full migration onto one retail CObjCell graph across physics+collision+render+streaming, staged in 5 verify-each cycles. This lands the evidence model + the Stage 1 (ObjCell scaffold) design. No code yet. - docs/research/2026-06-02-render-cell-membership-evidence.md (the why, from pixels) - docs/superpowers/specs/2026-06-02-unified-cell-graph-stage1-design.md (Stage 1) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.4 KiB
Render Cell-Membership — evidence model (2026-06-02)
Canonical "why" for the Unified Cell Graph (CObjCell) pivot. This is the pixel-grounded investigation that ended the week-long indoor-render saga. Companion design spec:
docs/superpowers/specs/2026-06-02-unified-cell-graph-stage1-design.md. Predecessors:docs/research/2026-05-31-render-architecture-reset-handoff.md,docs/research/2026-06-01-render-reset-session2-handoff.md.
0. One-line conclusion
The render pipeline maintains its own indoor-cell system (CellVisibility +
PortalVisibilityBuilder + 3 gates) that is separate from, and out of sync with,
physics cell tracking. When the render side's answer is "outdoor/null" — which
happens on spawn-in, on within-building flicker, and on building entry — terrain
draws ungated and you see the world from below. Point-fixing any one gate cannot
win because the visibility input is unreliable and disagrees with physics. The fix
is to unify onto one cell membership all systems share (the retail CObjCell
model), executed as a staged migration (see spec).
1. Symptom
Standing inside the Holtburg cottage (room or cellar) the interior does not seal: the full outdoor world (terrain hills, water, scenery) renders and the player appears to float in it ("world from below"). The exterior renders correctly.
2. Method
Live client against local ACE, four launches, character +Acdream. Existing
baseline probes only — no code added (investigation was report-only):
ACDREAM_PROBE_SHELL ([shell]), ACDREAM_PROBE_VIS ([vis]),
ACDREAM_PROBE_FLAP ([flap]/[flap-cam]), ACDREAM_PROBE_ENVCELL,
ACDREAM_PROBE_CELL ([cell-transit]). Screenshots via PowerShell
CopyFromScreen on the AcDream.App window → PNG → read.
Gotcha (recorded so it isn't re-hit): Tee-Object writes the log as UTF-16LE.
GNU grep (Bash) silently matches nothing on it; the ripgrep-based Grep tool and
PowerShell Get-Content decode it correctly. Early "0 probes" Bash reads were false
negatives — always read these logs with ripgrep/PowerShell.
3. Findings (per waypoint)
- Exterior — correct. Opaque textured cottage walls, building sitting on dirt, no holes. No indoor probes fire (outdoor root). The outdoor case is sound and the migration must preserve it.
- Room, entered from outside (run 1) — indoor path NEVER engaged.
[vis]=[shell]=[envcells]=0. The "interior" we saw was the building-shell GfxObj + interior statics drawn by the outdoor entity pass; dark void where nothing covered. ⇒ outdoor→indoor building entry did not transit the player into the cell. - Cellar, walked in (run 1) — indoor path engaged but leaks.
[vis] root=0xA9B40174 cells=3 ids=[0174,0175,0171] outside(polys=0,planes=0)— visibility computed correctly (windowless ⇒ empty OutsideView).[shell]shows all three shells render fine (gfx=1 tr=0 zh=0).[flap-cam] terrain=Skip outVisible=False— terrain decision is correct. Yet the screenshot is the full outdoor world. - First-person test (user-confirmed) — terrain shows even with the eye inside the cell. ⇒ the leak is not the 3rd-person camera; it's the gate/root.
- Spawn directly into the cellar (run 3) — the cleanest proof. Only one
[cell-transit] 0x0 → 0xA9B40174 reason=teleport. Physics knew the indoor cell instantly. Render fired zero[vis]/[shell]while standing still ⇒ world from below. ⇒ render and physics disagree: physics indoor, render outdoor. - Move from the cellar (run 3) — render re-engages on motion.
[cell-transit]flickers0174↔0175↔0171(reason=resolver);[vis]/[shell]start firing.[vis] root=0xA9B40174 cells=1vsroot=0xA9B40175 cells=3— the portal walk from0174reaches only itself (incomplete traversal); the root flickers, so the sealed set flickers 1↔3 and gaps show. - Room from the cellar side (run 3). The cottage interior is one connected
EnvCell group:
0171(room/Z94) ↔0175(stairs/Z93) ↔0174(cellar/Z90); the cell flickers among the three while standing still. Render engaged (came up through the graph) yet still showed terrain at room level.
4. Causal model
- Terrain has exactly one draw path (
_terrain.Draw,GameWindow.cs:7415/7420), both call sites behind theTerrainClipModegate; no second terrain/water/skybox source exists (code-confirmed). So terrain can only appear whenterrainClipMode != Skip, which happens when the render root resolves outdoor (clipRoot==null→ theelsebranch atGameWindow.cs:7352→ defaultPlanes→ ungated terrain). - Two independent cell resolvers. Render root =
CellVisibility.ComputeVisibility (visRootPos)→FindCameraCell(AABBPointInCell, render-side registry). Physics =PhysicsEngine.ResolveCellId→PlayerMovementController.CellId(BSP-based, separate registry). They disagree. - Render registration lags/misses. Render cells enter
CellVisibilityvia_pendingCells→ render-threadAddCell(GameWindow.cs:5749), a different lifecycle from physics' worker-threadCacheCellStruct. On spawn-in the render side has nothing registered yet ⇒clipRoot==null⇒ terrain. - The
[flap-cam]probe can't see the failure frames — it only logs insideif (clipRoot is not null). Thenull-root frames (where terrain draws) are invisible to it, which is why prior log-archaeology missed this. - Two failure variants, one disease: building entry (outdoor→indoor) leaves you outdoor (finding 2); within-building the membership flickers and the portal walk is incomplete (findings 6–7). Both are "cell membership recomputed from geometry, unreliable, and disagreeing across systems."
5. Verdict → the pivot
Unify onto one cell membership every system shares — the retail CObjCell model
(one curr_cell, GetVisible magnitude dispatch, portal graph). Scope decided with
the user (2026-06-02): full migration across physics + collision + render +
streaming, freeze lifted as needed, executed as a 5-stage chain that keeps the client
runnable + visually verified at each step. Design: the companion Stage 1 spec.
6. Disproven / do-not-repeat (this session + predecessors)
- Camera/eye as the root cause (FP test: terrain shows with eye inside the cell).
- A second terrain renderer (code-confirmed there is none).
- "Render-gate consolidation only" as sufficient (the broken input is the membership, upstream of the gate).
- Earlier disproven: H1 PVS grounding, H2 PortalSide, cull mode, shell geometry/texture missing, zoom-confound, the screen-space stencil-mask rule (breaks outdoors), flag-based per-entity gate routing (= patchwork).
7. Grounding research (full reports archived in session transcript)
Two research agents produced the retail structure + acdream inventory that ground the
design; their key facts are folded into the spec's "Retail grounding" and "acdream
current state" sections with citations. Decomp anchors:
CObjCell (acclient.h:30915), CEnvCell (:32072), CLandCell (:31886),
CSortCell (:31880), CCellPortal (:32300), CBldPortal (:32094),
CellStruct (:32275); GetVisible (pseudo_c:308209), change_cell (:281192),
find_cell_list (:308742), ConstructView (:433750/:433827),
InitCell (:432896), find_visible_child_cell (:311397).