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>
120 lines
7.4 KiB
Markdown
120 lines
7.4 KiB
Markdown
# 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)
|
||
|
||
1. **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.
|
||
2. **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.**
|
||
3. **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.
|
||
4. **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.
|
||
5. **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.**
|
||
6. **Move from the cellar (run 3) — render re-engages on motion.** `[cell-transit]`
|
||
flickers `0174↔0175↔0171` (`reason=resolver`); `[vis]`/`[shell]` start firing.
|
||
`[vis] root=0xA9B40174 cells=1` vs `root=0xA9B40175 cells=3` — the portal walk from
|
||
`0174` reaches **only itself** (incomplete traversal); the root flickers, so the
|
||
sealed set flickers 1↔3 and gaps show.
|
||
7. **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 the `TerrainClipMode` gate; **no second terrain/water/skybox
|
||
source exists** (code-confirmed). So terrain can only appear when `terrainClipMode
|
||
!= Skip`, which happens when the render **root resolves outdoor** (`clipRoot==null`
|
||
→ the `else` branch at `GameWindow.cs:7352` → default `Planes` → ungated terrain).
|
||
- **Two independent cell resolvers.** Render root = `CellVisibility.ComputeVisibility
|
||
(visRootPos)` → `FindCameraCell` (AABB `PointInCell`, render-side registry).
|
||
Physics = `PhysicsEngine.ResolveCellId` → `PlayerMovementController.CellId`
|
||
(BSP-based, separate registry). They disagree.
|
||
- **Render registration lags/misses.** Render cells enter `CellVisibility` via
|
||
`_pendingCells` → render-thread `AddCell` (`GameWindow.cs:5749`), a *different
|
||
lifecycle* from physics' worker-thread `CacheCellStruct`. 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 inside
|
||
`if (clipRoot is not null)`. The `null`-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`).
|