acdream/docs/research/2026-06-02-render-cell-membership-evidence.md
Erik e8c7164ad9 docs(render): Unified Cell Graph pivot — evidence model + Stage 1 spec
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>
2026-06-02 08:27:00 +02:00

120 lines
7.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 67). 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`).