acdream/docs/research/2026-07-08-186-connector-grey-flap-handoff.md
Erik 4dea66b633 docs(#186): close — render InsideSide from dat PortalSide bit (fix 8257b9ba); retail trace overturned PICK + FLOOD-epsilon hypotheses
The live retail cdb trace decided it: retail roots at the connector 0118 at the
grey pose (NOT the PICK fork) AND still draws the player room 0116 from that root,
because retail's InitCell side test reads the dat PortalSide bit where acdream's
render path reconstructed the interior side from the cell AABB centroid (mis-sides
a thin connector). ISSUES #186 -> CLOSED; handoff gets a RESOLVED banner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 15:07:46 +02:00

162 lines
12 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.

# Handoff: #186 — indoor GREY flap at a top-floor connecting room (new house type)
> **✅ RESOLVED 2026-07-08 · fix `8257b9ba` · live gate PASSED.** The retail cdb trace (§3, run via
> `tools/cdb/issue186-connector-decider.cdb`) OVERTURNED both hypotheses below. Retail roots at the
> connector `0118` at the grey pose (same as acdream — so NOT the PICK fork) AND still DRAWS the player
> room `0116` from that root — because retail's `PView::InitCell` side test reads the dat **`PortalSide`
> bit**, while acdream's render path reconstructed the portal's interior side from the cell **AABB
> centroid**, which mis-sides a THIN connector cell (`0118`, 5 render polys → centroid on the wrong side
> of the `0118→0116` doorway → back-portal cull → grey). Fix = derive `InsideSide` from the dat bit in
> `GameWindow.BuildLoadedCell`, matching retail + acdream's own physics (`CellTransit.cs:190`). Surgical
> (dat diagnostic: agrees with the old centroid on every portal but the one #186 broke). **The §2/§3
> "acdream drops the room" read was right; the "acdream over-switches the root / flood-epsilon" mechanism
> was WRONG — it was the InsideSide *source*, not the pick or the epsilon.** Everything below is the
> pre-fix investigation, kept for the trail.
**Date:** 2026-07-08 · **Status:** ~~ROOT CAUSE NARROWED (report-only) — fix NOT started.~~ RESOLVED (see banner). It lives in
the FROZEN portal-flood / viewer-cell render code (the doorway-FLAP family that cost weeks), so it needs
a **retail cdb trace** to pin the last fork BEFORE any code, then a careful change. Milestone: M1.5.
**Read these first, in order:**
1. This handoff.
2. `claude-memory/project_render_pipeline_digest.md` — the render/doorway-FLAP SSOT + DO-NOT-RETRY table
("root at VIEWER cell"; the deleted EyeInsidePortalOpening rescue; PortalSideEpsilon root-lag).
3. `docs/architecture/retail-divergence-register.md` rows **AD-17** (≤8 clip-plane / union-AABB scissor),
**AD-20** (camera-sweep fallback seeds eye cell from PLAYER cell), **AD-21** (null-clipRoot outdoor fallback).
4. `docs/ISSUES.md` #186.
---
## 0. Symptom (user, live)
Top floor of a **house type not tried before**; a connecting opening/room between two parts of the house.
Passing through → a brief **GREY** flap (the framebuffer clear color). Stopping at the precise spot → the
WHOLE screen is grey and stays; **turning the camera clears it, turning back → grey again** (camera-DIRECTION
dependent, player stationary). A jump is NOT involved (that's #185). **Retail is SEAMLESS at the same spot
(user-confirmed) → this is a real acdream bug, not inherent.**
## 1. The cells + the grey pose (ACDREAM_PROBE_FLAP capture)
Landblock `0xF682`. Three cells, all envId `0x0365`, cell-struct origin `(132,84,62)`:
| cell | render polys | physics polys | portals | role |
|---|---|---|---|---|
| `0xF6820116` | **15** | 12 | →`0117,0118,0113` | player's room (closed) |
| `0xF6820117` | **10** | 6 | →`0118,0116,0115,0119` | next room (closed) |
| `0xF6820118` | **5** | 3 | →`0117,0116` | **thin connector** (root when grey) |
Grey-pose facts (from `[flap]`/`[flap-cam]`/`[flap-sweep]`, 88,249 frames; the grey frames are `vis=1`
`root=0xF6820118`, ~6,725 frames):
- `[flap-cam]`: `root=viewerCell=0xF6820118 playerCell=0xF6820116 eyeInRoot=Y playerInRoot=n
terrain=Skip outVisible=False eye=(134.81,83.59,69.07) player=(137.30,83.32,66.82)`.
- `[flap]` grey line: `root=0118 | p0->0x0117 D=0.14 TRV proj=4 clip=0 | p1->0x0116 D=0.18 CULL proj=5
clip=4 || outPolys=0 vis=1`. (Dominant D to 0117 ≈ 0.14; ALWAYS positive 0.000.50 → eye is genuinely
INSIDE 0118, never across the plane.)
- `[flap-sweep]`: `ok=True resolved=Y pulledIn=-0.00` (the chase boom reaches FULL distance, NO collision
pull-in, seating the eye in the connector).
**Read of the geometry:** the eye sits in the sparse connector `0118`, in the doorway threshold, looking
BACK toward the player's room `0116`. `p1->0116` is ON-screen (proj=5 clip=4) but side-CULLED (eye ~0.180.31
on `0116`'s side of the p1 plane — a back-portal). `p0->0117` is genuinely OFF-screen (`clip=0` with proj≥3
= off-screen per the builder's own probe doc, `PortalVisibilityBuilder.cs:818-821`). So no neighbour floods,
only the 5-poly connector draws, and its shell doesn't cover the forward view → the framebuffer clear color
(fog, `GameWindow.cs:8968`) shows through = GREY. Turning re-aims off the back-portal → a room floods → picture.
## 2. What is CONFIRMED / RULED OUT (workflow `wf_362bbeda-933`, 4 tracers + synthesis, + my own re-checks)
- ❌ **NOT null viewer-root / AD-21** — the render root is a valid indoor cell on 88,248/88,249 frames
(one lone `[outdoor-node] root=OUT`). (This refutes THE FIRST hypothesis in this investigation.)
- ❌ **NOT viewer-cell root-lag / AD-20** — `eyeInRoot=Y` at every grey frame: the eye is geometrically
INSIDE `0118`'s BSP. The root is the fresh eye-derived swept `curr_cell` (`PhysicsCameraCollisionProbe.cs:92`),
NOT a stale player-seeded fallback. (Refutes the workflow synthesis's own leading "31 cm gap" theory —
the eye is NOT across the boundary; I checked the `eyeInRoot` flag directly.)
- ❌ **NOT the flood-admission being unfaithful** — the empty-clip cull (`PortalVisibilityBuilder.cs:320-324`)
and the back-portal side-CULL (`:292-298`, `CameraOnInteriorSide :857-863`) are faithful ports:
retail `PView::ClipPortals` gates neighbour processing on `if (ecx_8 != 0)` (pc 005a5660) and
`AddViewToPortals` on `view_count != 0` (pc 005a5357). #177 already proved acdream's flood is retail-identical.
- ❌ **NOT the color clear (the tempting "3-line fix" — DEAD END, do not revisit).** Retail's DrawCells
(`0x005a4840`, decomp line 432709) has its only `Clear` INSIDE `if (outside_view.view_count > 0)`
(line 432715), right AFTER `LScape::draw` (the landscape draw, 432719). The Clear is
`Clear(4, &RGBAColor_Black, 1.0f)` (`0x820fc0 = RGBAColor_Black`) — but clearing COLOR *after* drawing the
landscape would erase it, so that `4` flag is **depth/stencil, not color**. So retail's color clear is NOT
gated here, and matching "don't color-clear a sealed interior" would be WRONG. (This is why I did the cdb
prep — it killed a wrong fix before shipping.)
- ✅ **It IS a real acdream bug** (retail seamless) in the **doorway-FLAP flood/pick family**: acdream drops
the room the camera looks back at (`0116`) from the connector root; **retail keeps it drawn.**
## 3. THE ONE OPEN FORK → the retail cdb trace (do this FIRST in the new session)
Retail is seamless, so retail either (A) **roots at the player's room `0116`, not the connector** (a
viewer-cell PICK divergence — acdream over-switches to `0118`), or (B) **roots at `0118` but its flood still
admits `0116`** (a FLOOD divergence — retail keeps the back-portal / uses an on-screen sliver acdream drops).
Static decomp cannot decide; observe retail.
**Toolchain (CLAUDE.md "Retail debugger toolchain"; binary CONFIRMED pairs with `refs/acclient.pdb` via
`py tools/pdb-extract/check_exe_pdb.py "C:/Turbine/Asheron's Call/acclient.exe"` → MATCH).**
- User: launch retail, connect to local ACE, walk to the SAME connecting room, and **HOLD the grey pose**
(so most captured frames are it).
- `bp acclient!SmartBox::update_viewer` (`0x00453ce0`) → dump `viewer_cell` (a `CObjCell*`, `acclient.h:35194`)
and its cell id. AND/OR `bp acclient!PView::DrawCells` (`0x005a4840`) → dump `cell_draw_num` +
`cell_draw_list.data[]` ids (`acclient.h:45939-45940`) — the drawn cell set.
- Decide: retail `viewer_cell == 0116` → **PICK fix** (acdream's viewer-cell pick/hysteresis over-retains
the connector — `CellTransit.cs:862-916 BuildCellSetAndPickContaining`, or the camera boom coasting the eye
into the connector at `pulledIn=-0.00` where retail's PathClipped `viewer_sphere` sweep hard-stops in
`0116`). retail `viewer_cell == 0118` but `cell_draw_list` ⊇ `0116` → **FLOOD fix** (retail admits the
back-portal from the connector; acdream's side-CULL / the deleted EyeInsidePortalOpening rescue is the site).
- cdb watchouts (CLAUDE.md): `qd` ONLY at a top-level threshold, never inside a conditional bp action (strands
cdb → kills retail); `dt acclient!Class @ecx field` for reads (`@@c++` is broken here); `.sympath <local>`
(no `srv*`, the `;` splits); spaced `.if ()` lets `qd` detach.
## 4. Apparatus (re-create in the new session — temp files won't persist)
- **Flap capture:** launch acdream with `ACDREAM_PROBE_FLAP=1` (canonical live cmd, CLAUDE.md). Emits per
frame: `[flap]` (root + per-portal proj/clip/side, `PortalVisibilityBuilder.cs:790`), `[flap-cam]`
(`root/viewerCell/playerCell/eyeInRoot/playerInRoot/terrain/outVisible`, `GameWindow.cs:11142-11152`),
`[flap-sweep]` (`ok/resolved/pulledIn/viewerCell`, `PhysicsCameraCollisionProbe.cs:74-87`), `[outdoor-node]`
(`root=IN/OUT`). ⚠️ heavy (per-frame); output goes to the raw background-task `.output`, not the Tee'd log.
Have the user HOLD the grey pose + turn to clear + turn back a couple times.
- **Offline cell geometry dump:** `tests/AcDream.Core.Tests/Physics/Issue186ConnectorCellGeometryInspectionTests.cs`
(`Dump_ConnectorCells_ShellAndCollision`) loads `0116/0117/0118` from the dat and prints render/physics poly
counts + normal-axis buckets. No live launch needed. (That's how the §1 table was produced.)
- **Decomp workflow:** `wf_362bbeda-933` (this session) — the 4-tracer + synthesis analysis; results in its
journal under the session's `subagents/workflows/` dir.
## 5. DO-NOT-RETRY (each already refuted or a known trap)
- Do NOT re-open the **null-root / AD-20 / AD-21** angle — root is valid, `eyeInRoot=Y`.
- Do NOT implement the **color-clear gating** — retail's gated `Clear` is depth/stencil (post-`LScape::draw`);
matching it would add non-retail stale-content.
- Do NOT re-add the deleted **`EyeInsidePortalOpening` rescue** (`PortalVisibilityBuilder.cs:314-319`) blindly
— it caused the `0171↔0173` flood-cycle churn (the ORIGINAL doorway flap).
- Do NOT widen **`PortalSideEpsilon`** (`:38-56`) — a documented root-lag band, NOT retail (F_EPSILON=0.0002);
BR-4 refuted tightening/widening without eye-exact tracking.
- Do NOT "fix" the flood by propagating an empty/edge-on portal — the empty-clip cull is retail-faithful.
- The sparse connector geometry (`0118` = 5 render / 3 physics polys) is FAITHFUL to the dat (retail draws the
same). It is context, not the bug — the bug is that `0116` isn't drawn, not that `0118` is thin.
## 6. Key code sites
- Frame-top clear (unconditional color+depth+stencil to fog): `GameWindow.cs:8968-8983`. **NOT the fix.**
- Viewer-cell resolution: `GameWindow.cs:9137-9148` (`viewerCellId = _retailChaseCamera.ViewerCellId`).
- Camera boom sweep: `PhysicsCameraCollisionProbe.cs` (`SweepEye`, PathClipped, `pulledIn`, `r.CellId=curr_cell`).
- Viewer-cell PICK: `CellTransit.cs:862-916` (`BuildCellSetAndPickContaining`, center-based `point_in_cell`,
current-cell-first hysteresis, port of `find_cell_list` pseudo_c:308788-308825).
- Portal FLOOD: `PortalVisibilityBuilder.cs` (`Build`; side-test `CameraOnInteriorSide :857-863`; empty-clip
cull `:320-324`; `PortalSideEpsilon :38-56`; the off-screen-vs-sliver note `:818-821`).
## 7. Retail decomp anchors
- `PView::DrawCells` `0x005a4840` (decomp line 432709): Clear gated on `outside_view.view_count>0` (432715),
Clear at 432732 (depth/stencil, post-`LScape::draw`); cells drawn unconditionally after (432815+).
- `SmartBox::update_viewer` `0x00453ce0`: `viewer_cell = sphere_path.curr_cell` (the render root = swept cell).
- `PView::ClipPortals` empty-clip gate `if(ecx_8!=0)` pc 005a5660; `AddViewToPortals` `view_count!=0` pc 005a5357.
- `PView::InitCell` side test decomp:432962. `RGBAColor_Black` @ `0x820fc0`.
## 8. Acceptance
- No grey flap passing through OR stopping in the connecting room at any camera angle; matches retail's seamless.
- Regression: the outside→inside doorway + other multi-room houses + the #177/#181 render-flap suites unchanged.
## 9. Context: this session also FIXED #185 (unrelated, DONE + gated)
The outdoor-stairs "invisible wall" (`07c5b832`) — a shadow part-id uint32 overflow dropping stair collision.
Unrelated to #186 (that was collision/registration; #186 is render/visibility). See
`docs/superpowers/specs/2026-07-08-185-outdoor-stairs-fix-design.md`.