feat(render): indoor render WORKS — terminating portal flood + every-cell seal + look-in FPS

Checkpoint of the unified retail-faithful indoor render. The two-week HANG/grey is fixed and the
interior seals (live-verified by the user). Commits the session render-rewrite foundation together
with the fixes that made it functional.

- HANG fix: PortalVisibilityBuilder.Build portal flood did not terminate (the faithful ProjectToClip
  near-side clip drifts per round, defeating the CellView dedup; the BFS had no bound after U.2a removed
  MaxReprocessPerCell). Fix = drift-tolerant snapped/canonical CellView.Add dedup (PortalView.cs) plus
  restored MaxReprocessPerCell=16 bounded re-enqueue (PortalVisibilityBuilder.cs). Re-enqueue is kept
  (load-bearing for late-slice propagation, Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit);
  only its count is capped. CellViewDedupTests added.
- Seal (DrawCells Task 2): RetailPViewRenderer.DrawEnvCellShells draws EVERY visible cell via
  IndoorDrawPlan.ShellPass (was gated on the ClipFrameAssembler slot filter, leaving slot-less cells grey).
- Look-in FPS: GameWindow exterior look-in candidates limited to the player landblock +-1 (was all ~81
  loaded LBs iterated every outdoor frame). No behaviour change (far cells were >48m, already culled).

Remaining dominant issue = the FLAP at transitions: viewer-cell metastability (render roots at the
camera-eye cell, which oscillates outdoor-indoor as the 3rd-person boom drifts across the doorway,
confirmed in render-sig). SEPARATE fix, NOT the DrawCells port. Full handoff + flap fix plan + tracked
follow-ups (#78 terrain, look-in-from-inside, look-in FPS, L-spotlight):
docs/research/2026-06-07-indoor-render-session-handoff.md.

Baselines: build 0 err; App.Tests 210/210; Core.Tests 1331 pass / 4 fail (pre-existing) / 1 skip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-07 10:14:43 +02:00
parent bff1955066
commit 1405dd8e90
27 changed files with 3635 additions and 814 deletions

View file

@ -0,0 +1,160 @@
# Indoor Render — Session Handoff: HANG fixed + interior SEALS; the FLAP is next — 2026-06-07
> Worktree `thirsty-goldberg-51bb9b`, branch `claude/thirsty-goldberg-51bb9b`. PowerShell on Windows;
> launch logs UTF-16; build before launch; acceptance is the user's eyes. Live ACE `127.0.0.1:9000`,
> `testaccount`/`testpassword`, char `+Acdream` (spawns near the Holtburg / "Arcanum" cottage —
> landblock `0xA9B4`, cottage cells `0xA9B4016F0175`). Do NOT branch/worktree, push, or `git stash`/`gc`.
## TL;DR
The two-week indoor-render **HANG is FIXED** and the interior **SEALS** (walls/floor/ceiling draw,
textured) — both committed this session and live-verified by the user ("Ok now it runs!"). A
structured live test pinned the remaining dominant visible issue, the **FLAP at transitions**, as
**viewer-cell metastability**: the render roots at the camera-eye cell, which oscillates
outdoor↔indoor as the 3rd-person boom drifts across the doorway plane. **The flap is a SEPARATE,
already-designed fix — it is NOT the verbatim DrawCells port; finishing the port will not fix it.**
Next session: **fix the flap** (camera-boom stability + viewer-cell dead-zone). Tracked follow-ups:
#78 terrain gating, look-in-from-inside sealing, look-in FPS, L-spotlight.
## What shipped this session (committed — see `git log` on this branch)
### 1. The HANG fix (the blocker)
Indoor frames froze (`AppHangB1`; not a crash — captured the spinning managed stack via a
`dotnet-stack` hang-watcher). Root cause: `PortalVisibilityBuilder.Build`'s portal-visibility flood
**did not terminate** for real cottage geometry. Two layers, two fixes (both kept):
- **A — drift-tolerant `CellView.Add` dedup** (`src/AcDream.App/Rendering/PortalView.cs`). The flood
re-queues a cell every time its view GROWS; growth only stops when the dedup recognises a re-clipped
region as a duplicate. The faithful `ProjectToClip` near-side clip drifts per round, so the old
exact index-by-index match (eps 1e-4) never caught the near-duplicate → unbounded growth → O(n²)
CPU-spin in `CellView.Add`. Fix: key each polygon by its vertices **snapped to a 1e-3 NDC grid**,
consecutive-dedup'd, **canonically rotated** to a lex-min start → finite key space → convergence.
Tests: `tests/AcDream.App.Tests/Rendering/CellViewDedupTests.cs` (3).
- **B — bounded re-enqueue** (`src/AcDream.App/Rendering/PortalVisibilityBuilder.cs`). A alone did not
fully converge (the spin relocated to `ScreenPolygonClip.ClipByEdge` — bounded loops — inside the
still-non-terminating BFS). Restored the **`MaxReprocessPerCell = 16`** hard cap that Phase U.2a
deleted ("fixpoint termination" left the loop with NO bound). **Kept the re-enqueue** — it is
load-bearing for late-slice propagation (`Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit`).
Pure enqueue-once was tried and **broke that test**, so re-enqueue is kept and merely bounded.
- Deep diagnosis + the reassessment that led to B: `docs/research/2026-06-06-indoor-render-hang-rootcause.md`.
- **Verified:** clean exit (255→0); runs indoors with no freeze; the indoor flood converges in ~1
round/cell at normal positions (measured 35 pops/frame, 1 view-poly/cell). The cap only bites at
the metastable doorway.
### 2. The SEAL (verbatim DrawCells port — Task 2)
`RetailPViewRenderer.DrawEnvCellShells` now iterates `IndoorDrawPlan.ShellPass(pvFrame)` — **every**
visible cell's shell draws (was gated on `ClipFrameAssembler`'s slot filter → cells without a slot
were silently dropped → grey clear-color void). Verified: interior seals + textured. (Task 1
`IndoorDrawPlan` + its test committed earlier as `bff1955`.)
### 3. Look-in FPS
`GameWindow` exterior-look-in candidate cells limited to the player's landblock **±1** (was **all
~81 loaded landblocks** iterated every outdoor frame just to discard them via the 48 m seed cutoff).
Provably no behavior change (excluded cells are >48 m, already culled). Outdoor FPS improved but
still **~110 fps / ~9 ms (was ~200)** — `DrawPortal` still draws ~12 building interiors/frame (see
follow-up).
## Baselines (must hold at next session start)
- `dotnet build -c Debug` **0 errors**.
- App.Tests **210/210** (205 baseline + IndoorDrawPlanTests 2 + CellViewDedupTests 3).
- Core.Tests **1331 pass / 4 fail / 1 skip** — the 4 are pre-existing Physics door/step-up, unrelated.
## Structured live test — findings (Holtburg/Arcanum cottage, 2026-06-07)
User walked a 6-step protocol (inside-still → camera-pan → doorway-threshold → just-outside →
looking-at-cottage → cellar) and reported 8 behaviours; `ACDREAM_PROBE_FLAP` `[render-sig]`
correlated each.
| # | Observed | Cause | Bucket |
|---|---|---|---|
| 2,3,6,8 | walls briefly transparent / window+entrance "covered by the world background" / abrupt "teleport" through the doorway — all **at transitions (camera crossing a threshold)** | **THE FLAP** | viewer-cell stability (NEXT) |
| 1 | outdoor grass covers the cellar-entrance hole (steady, looking in from outside) | outdoor terrain not gated over the indoor floor opening | **#78** terrain gating |
| 7 | from inside, a building seen through the doorway has transparent walls (world-bg shows); pops back when you step outside | look-out shows other buildings unsealed | look-in/look-out completeness |
| 5 | spotlight blobs on textures from the ceiling lamp (always been there) | point-light artifact | **L-spotlight** (separate) |
| FPS | inside very high; outside **110 fps / ~9 ms** (was ~200) | `DrawPortal` draws ~12 interiors/frame | look-in cost |
| 4 | cellar transitions **stable** ✓ | vertical transition doesn't cross the outdoor boundary | — |
### The FLAP — pinned (render-sig evidence)
`[render-sig]` over the doorway shows the render branch + the cell it roots at flip-flopping while the
**player cell stays inside**:
```
50× branch=OutdoorRoot viewer=0xA9B40031 (outdoor) player=0xA9B40171 (indoor) gate=in
16× branch=RetailPViewInside viewer=0xA9B40170 (indoor) player=0xA9B40171 gate=in
113× branch=RetailPViewInside viewer=0xA9B40171 (indoor) player=0xA9B40171 gate=in
... oscillates 0x0031 ↔ 0x0170 ↔ 0x0171 frame-to-frame ...
```
**Mechanism:** the render roots at the **viewer (camera-eye) cell** (`clipRoot = viewerRoot`, Phase W
"one viewpoint"). The 3rd-person boom drifts the eye across the doorway plane; acdream re-resolves the
viewer cell fresh each frame with **no hysteresis** → it flips between outdoor `0x0031` and indoor
`0x0170/0x0171` → the render flips `OutdoorRoot``RetailPViewInside` → the indoor seal drops (walls
transparent, outdoor world/grass shows) then re-seals → **flapping**. This is exactly the 2026-06-05
viewer-cell-flicker diagnosis, now confirmed against the live render branch.
## RECOMMENDED NEXT WORK — fix the FLAP (separate, already-designed)
Per `docs/research/2026-06-05-viewer-cell-flicker-rootcause-and-fix-plan-handoff.md`, 3 retail-faithful parts:
1. **Viewer-cell dead-zone (do this first)** — ±0.2 mm cell hysteresis so a sub-mm eye drift can't flip
the cell (`PhysicsCameraCollisionProbe.SweepEye`; retail `point_inside_cell_bsp` 0x53c1f0). Highest
leverage — likely kills most of the flap on its own.
2. **Camera-boom stability** — stop the boom drifting at rest (`RetailChaseCamera.UpdateCamera`; retail
`UpdateCamera` 0x456660).
3. **w-space (w=0) portal clip** — close-portal projection degeneracy (`PortalProjection` /
`PortalVisibilityBuilder`; retail `GetClip` 0x5a4320 / `polyClipFinish` 0x6b6d00). Lower priority.
Apparatus ready: `ACDREAM_PROBE_FLAP` emits `[render-sig]` (branch/viewer/player/gate per frame),
`[flap]`, `[flap-cam]`, `[flap-sweep]` — light enough to launch with (the heavy `ACDREAM_PROBE_SHELL`
firehose is what previously caused an I/O stall; avoid it).
## Tracked follow-ups (logged; not yet fixed)
- **#78 terrain gating** — outdoor terrain (grass) draws over the indoor cellar-entrance hole (and likely
other indoor floors). Decomp anchor `CEnvCell::find_visible_child_cell` (`acclient_2013_pseudo_c.txt:311397`).
- **Look-in-from-inside** — buildings seen through your door/window from inside render unsealed
(transparent walls); the look-out pass doesn't draw other buildings' shells. DrawCells port Task 5/7
territory (or R2 "outside-looking-in").
- **Look-in FPS**`DrawPortal` draws ~12 building interiors every outdoor frame (~110 vs ~200 fps).
Optimize: only look into buildings whose exit portals are frustum-visible; skip when no door is in view.
- **L-spotlight** — ceiling-lamp point light makes spotlight blobs on textures. Pre-existing, separate.
## verbatim DrawCells port — remaining tasks (deferred)
Plan: `docs/superpowers/plans/2026-06-06-verbatim-retail-indoor-render-port.md`. Task 1 + Task 2 done.
**Task 3** (objects no-clip) is effectively already satisfied (objects draw membership-gated with no
clip; no half-characters observed). **Tasks 48** (per-slice trim, look-out, delete `ClipFrameAssembler`,
look-in, final) are **cleanup with no current visible payoff** — the seal works and there is **no visible
bleed** (the "glitches between cells" were the FLAP, not bleed). **Task 4 (trim) is intricate** (its
per-slice `_clipFrame.Reset()` is coupled with the landscape/particle passes that still read
`clipAssembly` slots) and **risks re-slicing the working seal** — do it carefully, fresh, and only when
clean architecture is the priority.
## DO NOT re-litigate
- The HANG fix (A drift-dedup + B bounded re-enqueue) is correct + verified. **Do NOT try pure
enqueue-once** — it breaks `Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit` (late-slice
propagation needs the re-enqueue; the cap, not removal, is the termination guarantee).
- The grey was the `drawableCells` / `ClipFrameAssembler` slot filter; Task 2 fixed it. The clip math is
faithful — do not "harden the w-clip".
- **The FLAP is NOT the DrawCells port.** It is viewer-cell metastability (camera/membership). Tasks 48
will NOT fix it.
- The render roots at the VIEWER (camera-eye) cell intentionally (Phase W "one viewpoint"). The flap fix
is to STABILISE the viewer cell (dead-zone + boom), NOT to re-root at the player cell (superseded).
## Copy-paste pickup prompt (next session)
```
Pick up the indoor-render work in worktree thirsty-goldberg-51bb9b (branch
claude/thirsty-goldberg-51bb9b). PowerShell; launch logs UTF-16; build before launch; acceptance is
the user's eyes. Do NOT branch/worktree, push, git stash/gc, or revert the dirty tree.
Read first: docs/research/2026-06-07-indoor-render-session-handoff.md (state, what shipped, the FLAP
diagnosis, do-not-relitigate). Then docs/research/2026-06-05-viewer-cell-flicker-rootcause-and-fix-plan-handoff.md
(the flap fix plan).
Confirm baselines: build 0 errors; App.Tests 210/210; Core.Tests 1331 pass / 4 fail (pre-existing) / 1 skip.
The indoor HANG is fixed and the interior SEALS (shipped + committed last session). The remaining
dominant visible issue is the FLAP at transitions — viewer-cell metastability: the render roots at the
camera-eye cell, which oscillates outdoor↔indoor as the 3rd-person boom drifts across the doorway (no
hysteresis), confirmed in [render-sig]. FIX THE FLAP, starting with the viewer-cell dead-zone
(PhysicsCameraCollisionProbe.SweepEye; retail point_inside_cell_bsp 0x53c1f0), then camera-boom
stability (RetailChaseCamera.UpdateCamera; retail UpdateCamera 0x456660). Launch with ACDREAM_PROBE_FLAP
only (NOT ACDREAM_PROBE_SHELL — it stalls on I/O). Gate on the user's eyes at the cottage doorway.
Do NOT: retry pure enqueue-once (breaks late-slice propagation); re-root render at the player cell
(viewer-cell rooting is intentional); finish DrawCells port Tasks 4-8 expecting it to fix the flap (it
won't). Tracked follow-ups (not the flap): #78 terrain gating (grass over cellar hole), look-in-from-
inside sealing, look-in FPS (DrawPortal ~12 interiors/frame), L-spotlight.
```