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,135 @@
# Retail PView Indoor Render Pseudocode (2013 EoR)
This note pins the indoor render port to the named retail decomp. The goal is
behavioral fidelity: modern GL renderers may supply the draw calls, but the
frame ownership, visibility graph, and draw order follow these functions.
## SmartBox::RenderNormalMode @ 0x00453aa0
```text
if render device has open scene:
outside = SmartBox::is_player_outside(player position)
seenOutside = outside || viewer_cell.seen_outside
set FOV/view distance
if !outside:
if seenOutside:
LScape::update_viewpoint(lscape, Position::get_outside_cell_id(viewer))
Render::update_viewpoint(viewer)
RenderDeviceD3D::DrawInside(viewer_cell)
else:
LScape::update_viewpoint(lscape, viewer.objcell_id)
Render::update_viewpoint(viewer)
Render::set_default_view()
Render::useSunlightSet(1)
LScape::draw(lscape)
FlushAlphaList()
run targeting/render callbacks
```
Important split: the top-level branch follows `is_player_outside`, while indoor
render calls `DrawInside(viewer_cell)`.
## RenderDeviceD3D::DrawInside @ 0x0059f0d0
```text
PView::DrawInside(RenderDeviceD3D::indoor_pview, viewer_cell)
```
This is a thin forwarder. The PView owns the indoor frame.
## PView::DrawInside @ 0x005a5860
```text
reset object scale
CEnvCell::curr_view_push(root_cell)
PView::add_views(root_cell.num_stabs, root_cell.stab_list)
Frame::cache()
Render::positionPush(root identity frame)
Render::copy_view(root_cell.portal_view[last], null, 4) # full-screen root view
forceClear = PView::ConstructView(root_cell, 0xffff)
PView::DrawCells(forceClear)
Render::framePop()
PView::remove_views(root_cell.num_stabs, root_cell.stab_list)
root_cell.num_view--
```
## PView::ConstructView(CEnvCell*) @ 0x005a57b0
```text
clear outside_view and cell draw/todo state
insert root cell into distance-priority todo list
while todo is not empty:
cell = pop nearest
append cell to cell_draw_list
InitCell(cell, otherPortalId)
project/clip each portal against the current cell view
exit portals append clipped polygons to outside_view
interior portals append clipped polygons to neighbor portal_view
newly discovered neighbors enter the todo list once
return forceClear flag
```
`cell_draw_list` is the only indoor membership source. Later growth can add view
polygons to a discovered cell, but does not create a second draw-list entry.
## PView::DrawCells @ 0x005a4840
```text
if outside_view.view_count > 0:
Render::useSunlightSet(1)
Render::PortalList = this
LScape::draw(lscape) # landscape clipped by outside_view
D3DPolyRender::FlushAlphaList(0)
render_device.frameStamp++
if forceClear || portalsDrawnCount != 0:
render_device.Clear(DepthOnly)
# Loop 1: exit portal masks, reverse cell_draw_list
for cell in reverse(cell_draw_list):
if cell.structure.drawing_bsp:
push cell frame and surfaces
for each current portal_view slice:
CEnvCell::setup_view(cell, slice)
for each exit portal:
DrawPortalPolyInternal(portal polygon)
pop frame
Render::useSunlightSet(0)
Render::restore_all_lighting()
# Loop 2: closed cell shells, reverse cell_draw_list
for cell in reverse(cell_draw_list):
if cell.structure.drawing_bsp:
push cell frame and surfaces
for each current portal_view slice:
CEnvCell::setup_view(cell, slice)
DrawEnvCell(cell)
pop frame
# Loop 3: cell object lists, reverse cell_draw_list
for cell in reverse(cell_draw_list):
Render::PortalList = cell.portal_view[last]
DrawObjCellForDummies(cell)
restore object scale
Render::useSunlightSet(1)
```
There is no global indoor object, terrain, sky, weather, or particle pass. Every
visible indoor object comes from the cell draw list, and the landscape appears
only through `outside_view`.
## RenderDeviceD3D::DrawObjCellForDummies @ 0x005a0760
```text
for object in cell.object_list:
draw object under Render::PortalList
attached effects/particles follow the owning object visibility
```
acdream maps this to per-cell `WorldEntity.ParentCellId` buckets. Parentless
live objects must not bypass the indoor PView graph.

View file

@ -0,0 +1,163 @@
# Indoor render HANG — root cause: `PortalVisibilityBuilder.Build` non-termination — 2026-06-06
> Report-only investigation (user chose "investigate more first"). **No code changed.**
> Worktree `thirsty-goldberg-51bb9b`. This blocks the verbatim-DrawCells port's Task 2
> visual gate: every indoor frame can freeze here.
## Symptom
Three launches of the client all **froze** (`AppHangB1`, Windows Event Log) within
seconds-to-minutes of the camera being indoors at the Holtburg cottage. Not a crash —
no access violation, no managed exception. The captured managed stack of the frozen
render thread (`hang-stack.txt`, via `dotnet-stack`) shows it **CPU-spinning**:
```
CPU_TIME
CellView.Add(ViewPolygon)
PortalVisibilityBuilder.AddRegion(CellView, List<ViewPolygon>)
PortalVisibilityBuilder.Build(...)
RetailPViewRenderer.DrawInside(...)
GameWindow.OnRender(...)
```
App.Tests 207/207 and Core 1331/4/1 are green; the bug is invisible to the suite (see §Evidence).
## Verdict
**It is NOT Task 2 (the verbatim-DrawCells / grey fix).** `Build(...)` runs at the very
top of `DrawInside` ([RetailPViewRenderer.cs:43](../../src/AcDream.App/Rendering/RetailPViewRenderer.cs)),
**before** any line Task 2 touched, and the call is byte-identical pre/post-change. Task 2's
draw logic was independently confirmed correct in the run-1 log: `[render-sig] draw=[…]`
equalled `ids=[…]` with `miss=[]`, and `[shell]` showed every visible cell drawing textured
(`zh=0`). The grey fix works.
**Root cause:** `PortalVisibilityBuilder.Build`'s portal BFS does not terminate for real
cottage geometry. It **re-enqueues a popped cell every time that cell's `CellView` grows**:
`queued.Remove(cell.CellId)` on pop ([:122](../../src/AcDream.App/Rendering/PortalVisibilityBuilder.cs))
+ `if (grew && queued.Add(neighbourId))` on grow ([:289](../../src/AcDream.App/Rendering/PortalVisibilityBuilder.cs)).
Termination therefore depends entirely on growth stopping. Growth is gated only by
`CellView.Add`'s **exact-match dedup** (`SamePolygon`, eps `1e-4`,
[PortalView.cs:79](../../src/AcDream.App/Rendering/PortalView.cs)). The **near-side portal clip**
(`ClipPortalAgainstView``PortalProjection.ProjectToClip``ClipToRegion`,
[:474/:485](../../src/AcDream.App/Rendering/PortalVisibilityBuilder.cs)) produces a polygon
that is a hair different on each `A↔B` reciprocal round (float drift through the homogeneous
project→clip round-trip with a non-identity cell transform). The dedup never matches the
drifted near-duplicate → the region grows without bound → the cell re-enqueues forever →
`CellView.Polygons` grows to N, and `CellView.Add`'s O(N) dedup scan makes the whole thing
O(N²) → frozen.
## Evidence
1. **Captured stack** pins the spin to `CellView.Add ← AddRegion ← Build`, pure managed
`CPU_TIME` (not a GL call, not blocked, not a fault).
2. **The code already documents this exact failure** at
[:694-697](../../src/AcDream.App/Rendering/PortalVisibilityBuilder.cs): the *reciprocal*
clip deliberately stays on the float-stable `ProjectToNdc` path *because*
"per-round float drift defeated the CellView SamePolygon dedup, inflating a tight A<->B
reciprocal view to ~4x its area." The **near-side** clip ([:474](../../src/AcDream.App/Rendering/PortalVisibilityBuilder.cs))
did not get the same treatment — it uses `ProjectToClip`.
3. **The only bound was removed this session.** [:74](../../src/AcDream.App/Rendering/PortalVisibilityBuilder.cs):
"Fixpoint termination replacing the old `MaxReprocessPerCell` hard cap." The fixpoint never
converges under drift; with the cap gone there is no other bound (no iteration cap, no
max-polygon cap, no time bound).
4. **It's the dirty-tree rewire the handoff said to KEEP.** `git diff --stat`:
`PortalVisibilityBuilder.cs +426/45` and `PortalProjection.cs +111` are **uncommitted**.
`ProjectToClip` is part of the new `PortalProjection` lines. The handoff
(`2026-06-06-verbatim-drawcells-port-pickup-handoff.md`) lists this rewire as the faithful
foundation to preserve and says "the clip math is already faithful — do not harden the
w-clip." The clip is faithful in the *picture* it computes; it is the *non-termination*
that is broken.
5. **Why the suite is green:** `PortalVisibilityBuilderTests` build cells with
`WorldTransform = Matrix4x4.Identity` and axis-aligned quads in 2-cell **chains**
(`cam → ground → exit`). No `A↔B` cycle, no transform-induced drift → the project→clip
round-trip is exact → the dedup collapses duplicates → the BFS converges. The real cottage
is a **cyclic** cell cluster (`0x016F0x0175`, mutual portals) with **non-identity**
transforms → drift + cycle → non-termination. The suite cannot reach the failing case.
6. **Why run 1 survived 113 frames then froze:** `Build` converges at most camera poses; only
specific poses create the non-converging drift cycle. The freeze coincided with the
metastable doorway flip (`[render-sig] stable` went 39→0, visible-cell count 5→4) one frame
before the log ended.
## Hypotheses (ranked)
1. **(confirmed)** Non-terminating BFS: re-enqueue-on-grow + `ProjectToClip` drift defeats the
`SamePolygon` dedup → unbounded `CellView` growth. Falsify: a re-process cap, a
drift-tolerant dedup, or `ProjectToNdc` on the near-side clip all make `Build` terminate.
2. *(ruled out)* GPU/driver hang from a malformed draw — the stack is pure managed `CPU_TIME`
in `CellView.Add`, never a GL call; no fault.
3. *(ruled out)* Probe-output stdout saturation — disproven: the probe-free run also hung.
4. *(ruled out)* Task 2 — `Build` is upstream of every Task 2 line and unchanged by it.
## Fix options (all additive — none reverts the dirty tree)
| | Fix | Touches | Pro | Con |
|---|---|---|---|---|
| **A** *(rec.)* | **Drift-tolerant dedup**: round clipped polygon vertices to a small grid (≈`1e-3`) before `AddRegion`, or widen/snaps `SamePolygon`'s match, so near-duplicates collapse → growth converges. | `CellView`/`AddRegion` | Fixes the actual root cause ("drift defeats dedup"); keeps the faithful `ProjectToClip`; preserves growth-propagation. ~10 lines. | Tolerance is a tuning constant (pick conservatively; over-merge = minor over-tighten). |
| **B** | **Restore a re-process bound** (`MaxReprocessPerCell`-style cap on the BFS). | `Build` loop | Smallest; guarantees termination; doesn't touch clip. | A guard, not a root fix; may under-include a late-growing view. The user's "no workarounds" rule applies — this is the band-aid. |
| **C** | **Near-side clip on `ProjectToNdc`** (what the reciprocal clip already uses). | `ClipPortalAgainstView` | Removes the drift source directly; consistent with `:694`. | Steps on this session's homogeneous near-eye clip work; the handoff's "don't harden the w-clip" is closest to here. |
**Recommended next step:** approve **A** (drift-tolerant dedup) — it closes the precise
mechanism the code half-acknowledges at `:694`, terminates structurally, and leaves the
faithful clip path intact. Implement in a follow-up (not report-only) session, then re-run the
Task 2 visual gate (probe-free) at the cottage + cellar.
## What this is NOT
- **NOT** Task 2 / the grey fix — that is verified working (`draw==ids`, `miss=[]`, textured shells).
- **NOT** a wrong-pixels / unfaithful-projection bug — it's a **termination** bug. The handoff's
"the clip math is faithful, don't harden the w-clip" is about projection *correctness*; this is
BFS *convergence*. Don't chase the w-clip.
- **NOT** a GPU/shader/driver hang and **NOT** the probe firehose (both ruled out by the stack
and the probe-free repro).
---
## Reassessment — is the dirty-tree builder rewire sound? (post Option A)
Option A (drift-tolerant `CellView.Add` dedup, `CellViewDedupTests` green) was implemented and the
client relaunched. Result: the hang **moved out of `CellView.Add`** (A worked for its target) but
**relocated to `ScreenPolygonClip.ClipByEdge`** via `ApplyReciprocalClip` (second captured stack,
`hang-stack2.txt`). `ScreenPolygonClip.Intersect`/`ClipByEdge` are **both bounded `for` loops**
they cannot spin on one call — so the spin is the **outer `Build` BFS** still not terminating and
calling them a runaway number of times. **Option A is necessary but not sufficient.**
### Git evidence (what the dirty rewire changed re: termination)
- **HEAD (committed)** near-side portal clip = `PortalProjection.ProjectToNdc` (float-stable;
`git show HEAD:` line 146). **The dirty rewire switched it to `ProjectToClip`** (`ClipPortalAgainstView`,
dirty line 474) — the homogeneous near-eye clip, introduced to fix the near/grazing-doorway flap/void.
- The `MaxReprocessPerCell` **hard cap was removed earlier** (committed Phase U.2a `d880775`), replaced
by "fixpoint termination." **Neither HEAD nor the dirty tree has a hard iteration bound.**
- The dirty rewire's own comment (`PortalVisibilityBuilder.cs:519-522`) documents that
`ProjectToClip` "produced per-round float drift that defeated the CellView SamePolygon dedup" — and
applied that lesson **only to the reciprocal clip** (kept on `ProjectToNdc`), leaving the **near-side**
clip on the drift-prone `ProjectToClip`.
### Soundness verdict
The builder's termination model is **unsound by construction.** It relies on the clipped regions
reaching a geometric fixpoint — re-clipping a cell's view reproduces *exactly-equal* polygons that the
dedup recognises — with **no hard iteration bound.** That only holds if the clip is float-stable.
`ProjectToClip` (needed for faithful near-doorway projection) injects per-round drift, so re-clipping
never reproduces an exactly-equal polygon, the dedup never catches it, and the re-enqueue-on-grow flood
never converges → infinite loop. **You cannot have BOTH faithful near-doorway projection (`ProjectToClip`)
AND convergence-via-exact-dedup-without-a-bound.** HEAD got away with it because `ProjectToNdc` was
stable enough to converge (and it sealed — user-verified); the dirty switch tipped it into non-termination.
The rewire fixed the *projection* and, apparently never having been launched, shipped a hang.
A's drift-tolerant dedup *narrows* the gap but cannot *close* it: for some geometry the per-round drift
exceeds any fixed snap grid, so growth still produces new keys forever. Only a **hard bound** guarantees
termination.
### Paths (for the user to choose)
| | Path | Termination | Projection fidelity | Risk |
|---|---|---|---|---|
| **1** *(rec.)* | Keep `ProjectToClip` + add **enqueue-once** bound (D) — the builder's own comment already calls enqueue-once "the hard termination guarantee"; the re-enqueue-on-grow is the bug. Keep A. | Guaranteed (≤N pops) | Full (faithful doorway clip kept) | Minor under-inclusion of late growth → visual-verify; widen to a cap if needed |
| **2** | Keep `ProjectToClip` + add a **re-process cap** (B, restore `MaxReprocessPerCell`). Keep A. | Guaranteed (≤N×K) | Full | Less faithful than enqueue-once; a tuning constant |
| **3** | **Revert** the near-side `ProjectToClip → ProjectToNdc` (back to HEAD). | Restored (HEAD converged) | **Loses** the rewire's near-doorway fix → reintroduces the flap/void (separate bug) | Throws away this session's projection work; contradicts the keep-the-dirty-tree directive |
A bound (paths 1/2) is the sound fix: it makes termination independent of clip drift, so the faithful
`ProjectToClip` projection AND guaranteed termination coexist. **Recommendation: path 1** (enqueue-once +
keep A), visual-verify for under-inclusion. Reverting (path 3) only trades the hang back for the
flap/void.

View file

@ -0,0 +1,589 @@
# Handoff - M1.5 Indoor Render / Retail PView Replacement Attempts - 2026-06-06
This is a **stop-and-handoff** note for the next agent. It records what was tried, what changed on disk, what the user still sees, and what evidence should drive the next step.
The user explicitly stopped this thread after repeated visual regressions. Do **not** continue the same patching loop. Treat all current uncommitted render work as suspect until re-audited against named retail.
## Worktree And Rules
- Worktree: `C:\Users\erikn\source\repos\acdream\.claude\worktrees\thirsty-goldberg-51bb9b`
- Branch: `claude/thirsty-goldberg-51bb9b`
- Starting HEAD called out by the user: `8116d10`
- Do **not** branch or create a new worktree.
- Do **not** push without asking.
- Never run `git stash` or `git gc`.
- PowerShell on Windows.
- Launch logs are UTF-16.
- Build before launching.
- Use `apply_patch` for manual edits.
- Do not revert dirty changes unless the user explicitly asks.
Current child handoff thread created before this file:
- Child thread id: `019e9d5c-bb34-7fe3-85cc-6b9065b4e882`
- It was forked same-directory, not a new worktree.
- A follow-up prompt was sent there with the immediate evidence and constraints.
## User-Visible State At Stop
The latest user report, after the most recent relaunch:
- Transition flaps still happen between outdoor/indoor, room/room, and cellar.
- Ground floor became transparent instead of sealed.
- Cellar remains broken.
- Prior screenshots showed grey or black background filling cell openings.
- Prior screenshots showed indoor walls losing texture or drawing as clear/background color.
- Prior screenshots showed character cut in half on the cellar stairs.
- User explicitly says we are back to old bugs and nothing feels solid.
Important: **do not claim any current code is fixed**. Build/tests passed for some pieces, but visual acceptance failed.
## Current Dirty State
`git status --short --branch` showed these tracked files modified:
- `src/AcDream.App/Rendering/ClipFrameAssembler.cs`
- `src/AcDream.App/Rendering/ClipPlaneSet.cs`
- `src/AcDream.App/Rendering/GameWindow.cs`
- `src/AcDream.App/Rendering/InteriorEntityPartition.cs`
- `src/AcDream.App/Rendering/InteriorRenderer.cs`
- `src/AcDream.App/Rendering/ParticleRenderer.cs`
- `src/AcDream.App/Rendering/PortalView.cs`
- `src/AcDream.App/Rendering/PortalVisibilityBuilder.cs`
- `src/AcDream.App/Rendering/Wb/ObjectMeshManager.cs`
- `src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs`
- `src/AcDream.Core/Rendering/RenderingDiagnostics.cs`
- `src/AcDream.Core/World/WorldEntity.cs`
- `tests/AcDream.App.Tests/Rendering/ClipFrameAssemblerTests.cs`
- `tests/AcDream.App.Tests/Rendering/ClipPlaneSetTests.cs`
- `tests/AcDream.App.Tests/Rendering/InteriorEntityPartitionTests.cs`
- `tests/AcDream.App.Tests/Rendering/PortalVisibilityBuilderTests.cs`
- `tests/AcDream.App.Tests/Rendering/Wb/WbDrawDispatcherClipSlotTests.cs`
- `tests/AcDream.Core.Tests/Rendering/RenderingDiagnosticsTests.cs`
- `tools/TextureDump/Program.cs`
Important untracked files include:
- `src/AcDream.App/Rendering/RetailPViewRenderer.cs`
- `docs/research/2026-06-05-retail-pview-indoor-render-pseudocode.md`
- many probe logs and local scripts/images, including `launch-flap-shell-capture-relaunch.log`, `launch-pview-watermark-probe.log`, `a8-current-room-cellar-audit.txt`, `texture-current-room-surfaces.txt`, `analyze_*.py`, `retail-*-trace.log`, and several screenshots.
Diff size before this handoff file:
- 19 tracked files changed.
- About 1593 insertions and 773 deletions.
## Validation That Passed But Did Not Prove Visual Correctness
After the last attempted PortalVisibilityBuilder patch:
```powershell
dotnet test tests\AcDream.App.Tests\AcDream.App.Tests.csproj -c Debug --filter "FullyQualifiedName~PortalVisibilityBuilderTests|FullyQualifiedName~PortalProjectionTests"
```
Passed: 29/29.
```powershell
dotnet build -c Debug --no-restore
```
Succeeded, with 9 known warnings.
```powershell
dotnet test tests\AcDream.App.Tests\AcDream.App.Tests.csproj -c Debug --no-build
```
Passed: 196/196.
These results only prove the pure/tested slices compile and pass. They did **not** solve the live render.
## Retail PView Reference Already Written
New pseudocode note exists:
- `docs/research/2026-06-05-retail-pview-indoor-render-pseudocode.md`
It summarizes:
- `SmartBox::RenderNormalMode @ 0x00453aa0`
- `RenderDeviceD3D::DrawInside @ 0x0059f0d0`
- `PView::DrawInside @ 0x005a5860`
- `PView::ConstructView @ 0x005a57b0`
- `PView::DrawCells @ 0x005a4840`
- `RenderDeviceD3D::DrawObjCellForDummies @ 0x005a0760`
Core retail model from that note:
- Outdoor: `LScape::draw`, then portal/interior peering through PView portal paths.
- Indoor: `DrawInside(viewer_cell)`.
- `PView::ConstructView` builds `cell_draw_list`, per-cell `portal_view`, and `outside_view`.
- `PView::DrawCells` draws outside landscape through `outside_view`, then reverse `cell_draw_list` exit masks, reverse shells, reverse object lists.
- No global indoor terrain/entity/particle pass should bypass PView membership.
## Retail Functions That Still Matter
Re-read named retail before more code:
- `PView::AddViewToPortals @ 0x005a52d0`
- `PView::ConstructView @ 0x005a57b0`
- `PView::ClipPortals @ 0x005a5520`
- `PView::FixCellList @ 0x005a5250`
- `PView::AdjustCellView @ 0x005a5770`
- `PView::OtherPortalClip @ 0x005a5400`
- `PView::GetClip` around `0x005a4320`
- `SmartBox::RenderNormalMode @ 0x00453aa0`
- `SmartBox::update_viewer @ 0x00453ce0`
- `RenderDeviceD3D::DrawObjCellForDummies @ 0x005a0760`
The critical retail detail not faithfully settled yet:
- Retail tracks `view_count` and `update_count`.
- When a cell view grows after the cell was already processed, retail calls `FixCellList` / `AdjustCellView`.
- Current acdream code only approximates this. It may not match draw-list ordering or downstream propagation.
## What We Tried
### 1. Treated symptoms as separate render leaks
The session started with symptoms that looked separate:
- dynamic objects and particles visible through ground when looking out from inside;
- outside ground texture covering cellar entrance when looking in;
- grey flaps when crossing cell boundaries;
- missing cellar floor / grey cellar;
- transparent or textureless interior walls.
The user correctly pushed back that these are probably one render-pipeline failure: indoor/outdoor, cells, shells, terrain, objects, particles, and doors must all agree on one visible-cell graph.
### 2. Gated dynamic objects and particles by ownership
Attempt:
- `WorldEntity.ParentCellId` was populated for player/spawns/teleports/motion updates.
- `InteriorEntityPartition` was changed so live dynamic entities with an indoor `ParentCellId` go into their cell bucket instead of a global live-dynamic overlay.
- `WbDrawDispatcher.ResolveEntitySlot` was changed so `ServerGuid != 0` no longer always means "draw unclipped indoors".
- Particles were moved toward PView-scoped / owner-scoped behavior instead of a global indoor scene pass.
Effect:
- User reported this stopped much of the obvious dynamic-object/particle bleeding when looking out.
- It did **not** fix grey/background transition flaps.
- It did **not** fix cellar/floor/walls.
Current risk:
- This direction is probably correct, but the exact routing must be audited. A later attempt also cleared clip routing to avoid character/shell cutting, so "PView membership" and "GPU clip slot routing" are currently mixed/confused.
### 3. Added/used a `RetailPViewRenderer`
Attempt:
- Added `src/AcDream.App/Rendering/RetailPViewRenderer.cs`.
- Moved part of indoor draw orchestration into `RetailPViewRenderer.DrawInside`.
- Added `DrawPortal` for outdoor-looking-in through `PortalVisibilityBuilder.BuildFromExterior`.
- The renderer currently does:
- `PortalVisibilityBuilder.Build`
- `ClipFrameAssembler.Assemble`
- `_envCells.PrepareRenderBatches(filter: drawableCells)`
- `InteriorEntityPartition.Partition`
- landscape through outside slices
- exit masks
- EnvCell shells
- object buckets
Effect:
- This is not a full retail replacement yet.
- User repeatedly saw unchanged or worse symptoms.
- FPS was reported drastically down after one iteration.
- Subsequent attempts produced missing textures / white or grey wall panels.
Current risk:
- `RetailPViewRenderer` is not truly verbatim retail. It keeps modern infrastructure and approximates PView with GPU clip slots and callbacks.
- The user asked "have you ported retail verbatim?" and the honest answer remains no.
- `GameWindow` still has a lot of orchestration, diagnostics, and render routing around this. It is not a small caller yet.
### 4. Reworked `ClipFrameAssembler` from one clip per cell to per-slice clip slots
Attempt:
- `ClipFrameAssembler` was rewritten toward per-polygon/slice output:
- `CellIdToViewSlices`
- `OutsideViewSlices`
- per-slice `ClipViewSlice`
- `TerrainClipMode` for outside-view landscape
- The goal was to represent retail `portal_view` slices more closely.
Effect:
- The code is plausible as a draw assist, but it is not retail membership.
- User saw regressions including black covers during transitions.
Current risk:
- The next agent must ensure `ClipFrameAssembler` never decides PView membership.
- It should be draw-assist only.
- Several failures looked like GPU clip slots cutting shells or characters at door/stair boundaries.
### 5. Disabled clip routing for shells/entities to stop character/stair cutting
Attempt:
- `RetailPViewRenderer.UseIndoorMembershipOnlyRouting` clears `_envCells.SetClipRouting(null)` and `_entities.ClearClipRouting()`.
- Comment says retail portal views decide eligibility, but feeding those 2D views into GL clip distances slices characters and shells at stair/door boundaries.
Effect:
- This was a reaction to user screenshots where the character was cut in half on stairs.
- It may reduce character slicing.
- It may also mean shells/objects are currently only membership-gated, not portal-view clipped.
Current risk:
- This is not a settled retail copy. It is an emergency compromise.
- Retail does use per-view setup (`CEnvCell::setup_view`) around shell/object drawing. We need to know whether our GL clip-plane model is simply the wrong mechanism for that setup.
### 6. Tried EnvCell / DAT polygon side handling changes
Attempt:
- `ObjectMeshManager` changed CellStruct polygon side handling:
- DAT `CullMode` interpreted as retail `CPolygon::sides_type`.
- `0 = pos`
- `1 = pos twice with reversed winding`
- `2 = pos + neg surface`
- `NoPos` / `NoNeg` still suppress faces.
- Added explicit normal inversion / winding reversal logic.
Effect:
- User saw missing textures/white/grey interior panels after some launches.
- The attempt did not fix the cellar or transition flaps.
Current risk:
- This may be correct retail interpretation or may be partially wrong.
- Audit with DAT dumps and retail/ACME references before keeping.
- `a8-current-room-cellar-audit.txt` and `texture-current-room-surfaces.txt` may contain useful surface/cell evidence.
### 7. Tried outside-looking-in via `BuildFromExterior`
Attempt:
- `PortalVisibilityBuilder.BuildFromExterior` seeds interior cell views through outside-facing exit portals.
- `RetailPViewRenderer.DrawPortal` calls it from outdoor branch.
- Tests were added:
- seeds interior cell through outside portal;
- does not seed when camera is on interior side;
- traverses deeper interior portals;
- max seed distance skips distant exit portal.
Effect:
- User initially reported walls became visible looking in from outside, but ground/cellar entrance composition stayed wrong.
- Later launches regressed to transparent/grey panels and missing textures.
Current risk:
- This is probably needed, but the exterior portal path is not proven retail-faithful.
- `BuildFromExterior` may now have duplicated-looking test diff context; inspect file carefully.
### 8. Tried broad "no hybrid" render routing in `GameWindow`
Attempt:
- `GameWindow` was changed so indoor path should call `RetailPViewRenderer.DrawInside`.
- Outdoor path should draw world and call `DrawPortal`.
- Global indoor terrain/entity/particle passes were reduced or bypassed.
- New render signature diagnostics log:
- `branch`
- `root`
- `viewerRoot`
- `playerRoot`
- `viewerCell`
- `playerCell`
- `gate`
- `terrain`
- `skyGate`
- `zclear`
- `sceneParticles`
- `outSlices`
- `outPolys`
- `ids`
- `draw`
- object partition counts
Effect:
- User explicitly asked whether the hybrid was totally gone.
- It is not safe to answer "yes" without auditing `GameWindow`.
- Symptoms persisted, so either the routing is still hybrid or the PView graph/draw setup is wrong enough that "no hybrid" alone does not solve it.
Current risk:
- `GameWindow.cs` has a very large diff, around 1000 lines touched.
- Next agent should not blindly keep it.
- Audit all remaining global passes while `clipRoot != null`.
### 9. Tried PView `update_count`-style reprocessing
Attempt in the last aborted step:
- `PortalView.CellView.Add` now returns `bool` and deduplicates near-identical polygons.
- `PortalVisibilityBuilder.Build` replaced `seen` with:
- `queued`
- `drawListed`
- `processedViewCounts`
- A cell can be requeued when its view grows.
- Each processing pass clips portals against only newly added view polygons.
- Similar logic was added to `BuildFromExterior`.
- Added tests:
- `Build_CollapsedInteriorPortalNearEyeBeyondHalfMeter_FloodsNeighbour`
- `Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit`
Effect:
- Focused tests passed.
- Live probe after patch still showed `outPolys` toggling near root `0xA9B40172`.
- User then reported transition flaps still there and now ground floor transparent.
Current risk:
- This patch is **unaccepted** and may be wrong.
- It approximates retail `update_count`, but does not necessarily implement `FixCellList`, `AdjustCellPlace`, or retail draw-list ordering correctly.
### 10. Widened eye-standing-in-portal fallback
Attempt:
- `EyeStandingPerpDist` widened from `0.5f` to `1.75f`.
- Motivation: live cellar capture had `0174 -> 0175` traversable with `D=-1.41` but `ProjectToNdc` returned zero vertices.
- The fallback still requires the perpendicular projection to land inside the portal opening.
Effect:
- It made one unit test pass for the cellar-style collapsed portal.
- It did not solve live transitions.
Current risk:
- This may be a bandaid, not retail.
- It should be validated against `OtherPortalClip` / `GetClip` in named retail before keeping.
### 11. Tried reciprocal clip fallback for eye-in-opening
Attempt:
- Before `ApplyReciprocalClip`, code clones `clippedRegion` when `eyeInsideOpening`.
- If reciprocal clipping empties the region, it restores the pre-reciprocal region.
Effect:
- Tests passed.
- Live visual did not.
Current risk:
- This may over-include.
- It is not proven retail-faithful.
## Critical Evidence From Logs
### Cellar startup: root 0174 only sees itself
From `launch-flap-shell-capture-relaunch.log`:
```text
[flap] root=0xA9B40174 eye=(154.50,4.99,92.25) localEye=(7.43,2.51,-1.77) |
p0->0x0175 D=-1.41 TRV proj=0 clip=-1 || outPolys=0 vis=1
[flap-cam] root=0xA9B40174 viewerCell=0xA9B40174 playerCell=0xA9B40174
... terrain=Skip outVisible=False
[render-sig] frame=49 branch=RetailPViewInside root=0xA9B40174
... terrain=Skip/skip sky=n zclear=n sceneParticles=none
outSlices=0 outPolys=0 ids=[0xA9B40174] draw=[0xA9B40174]
```
Meaning:
- The player/viewer/root are in cellar cell `0174`.
- The only visible portal to stair connector `0175` is traversable.
- Projection produces zero vertices.
- The PView flood stops at the cellar.
- Only the cellar draws; stair/main-floor cells are not in the visible set.
This is a direct candidate cause for missing floor/grey composition.
### Root 0172: outside view toggles on/off
From `launch-pview-watermark-probe.log`:
```text
frame=3625 root=0xA9B40172
p0->0x0173 D=... TRV proj=4 clip=4
p1->0x016F D=5.28 TRV proj=0 clip=-1
outPolys=1 vis=6
ids include 0xA9B40170
terrain=Skip/draw sky=Y zclear=Y
frame=3626 root=0xA9B40172
p0->0x0173 D=... TRV proj=5 clip=5
p1->0x016F D=5.39 TRV proj=0 clip=-1
outPolys=0 vis=5
ids missing 0xA9B40170
terrain=Skip/skip sky=n zclear=n
frame=3647 root=0xA9B40172
outPolys=1 ids include 0xA9B40170 terrain draw
frame=3648/3649 root=0xA9B40172
outPolys=0 ids missing 0xA9B40170 terrain skip
```
Meaning:
- The same root cell can alternate between seeing outside and not seeing outside.
- `0x016F` is involved in the root flap line but projects to zero.
- Sometimes `0x0170` becomes reachable and outside terrain/sky/depth clear run; sometimes it disappears.
- The visible-cell list and outside-view list are not stable.
Open question:
- Is `0x016F` an outdoor/land cell, an env cell lookup miss, or a portal that retail handles differently?
- Is the toggling caused by projection/clip degeneracy, wrong portal reciprocal handling, update-count propagation, or camera/viewer-cell root?
### Earlier known evidence: root 0171 vs player 0174 contradiction
From the older `2026-06-05-shell-sealing-cellar-floor-handoff.md`:
```text
[flap-cam] root=0xA9B40171 viewerCell=0xA9B40171 playerCell=0xA9B40174
...
[flap] root=0xA9B40171 ... p1->0173 proj=0 ...
```
Meaning:
- Earlier, camera/root was the room while player was cellar.
- The flood did not seal the player's cell.
- Later, after branch/viewer changes, there are also frames where root/player/viewer are all `0174` but the flood still fails on `0174 -> 0175`.
This means the problem is probably not only "wrong root"; it also includes projection/portal traversal/flood propagation or mesh-shell handling.
## What Not To Retry Blindly
Do not simply:
- switch the root to player cell as a workaround;
- widen `EyeStandingPerpDist` further;
- globally draw all indoor shells;
- globally draw terrain/entities/particles while inside;
- turn off all clipping and hope depth sorts it;
- keep adding `if cellar` or Holtburg-cottage-specific handling;
- claim "no hybrid" without auditing all `GameWindow` indoor/outdoor passes;
- equate unit-test pass with visual correctness.
The user has explicitly asked for retail smoothness, not a new patch stack.
## Likely Root Problem Space
The next fix probably lives in one of these, but evidence must decide:
1. **PView graph construction is not retail-faithful.**
- Missing or wrong `update_count` / `FixCellList` / `AdjustCellView`.
- Wrong draw-list ordering when a processed cell receives new views.
- Downstream portal propagation incomplete.
2. **Portal projection/clip behavior differs from retail.**
- `0174 -> 0175` traversable but `proj=0`.
- `0172 -> 016F` traversable but `proj=0`.
- `OtherPortalClip` / `GetClip` may not match retail.
3. **Outdoor/exit portal classification is wrong.**
- `OtherCellId=0xFFFF` is treated as exit/outside, but `0x016F` may be another kind of outside/land portal or missing env cell.
- OutsideView may be created through a downstream path that acdream sometimes drops.
4. **Renderer draw setup is still hybrid or ordered wrong.**
- `GameWindow` may still draw or skip global passes inconsistently.
- Sky/terrain/depth clear decisions are visibly flapping with `outside_view`.
5. **EnvCell shell mesh/surface handling is wrong.**
- Missing/transparent/white walls and floors may be mesh/surface/cull-side regressions.
- Audit `ObjectMeshManager` side handling against retail and DAT dumps.
6. **GPU clip slots are being used as membership or hard clipping when retail uses view setup differently.**
- Character cut in half on stairs strongly suggests hard clip-plane use on avatars/shells is wrong or applied at wrong pass.
## Suggested Next Procedure
1. Stop patching. Inspect the dirty diff first.
2. Read `docs/research/2026-06-05-retail-pview-indoor-render-pseudocode.md`.
3. Re-read named retail functions listed above.
4. Parse `launch-flap-shell-capture-relaunch.log` and `launch-pview-watermark-probe.log` around the cited frames.
5. Add better probes if needed:
- cell id;
- portal index;
- other cell id;
- portal flags;
- other portal id;
- traversable decision;
- standing distance;
- projection vertex count;
- clip vertex count;
- reciprocal clip result;
- outside-view add/skip reason;
- cell view count / processed count / update count;
- queue/requeue reason;
- draw-list insertion/reorder.
6. Decide from evidence whether `0174 -> 0175` and `0172 -> 016F` fail because of projection, reciprocal clip, cell lookup/classification, or update propagation.
7. Patch only the retail mismatch.
8. Build/test before launch.
9. Launch with probes once.
10. Then launch clean for FPS/visual feel.
11. Do not call it done until the user visually confirms retail smoothness.
## Launch Command
Use PowerShell:
```powershell
Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 3
$env:ACDREAM_DAT_DIR = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE = "1"
$env:ACDREAM_TEST_HOST = "127.0.0.1"
$env:ACDREAM_TEST_PORT = "9000"
$env:ACDREAM_TEST_USER = "testaccount"
$env:ACDREAM_TEST_PASS = "testpassword"
$env:ACDREAM_PROBE_FLAP = "1"
$env:ACDREAM_PROBE_SHELL = "1"
$env:ACDREAM_PROBE_VIS = "1"
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
Tee-Object -FilePath "launch-next-pview.log"
```
For clean visual/FPS run, remove the probe env vars.
## Minimal Prompt For Next Agent
```text
Continue acdream M1.5 indoor render in SAME worktree:
C:\Users\erikn\source\repos\acdream\.claude\worktrees\thirsty-goldberg-51bb9b
branch claude/thirsty-goldberg-51bb9b. Do NOT branch/worktree. Do NOT push. NEVER stash/gc.
The current dirty render code is not visually accepted. The user stopped the prior agent after repeated regressions.
Read docs/research/2026-06-06-retail-pview-renderer-replacement-attempts-handoff.md first.
Current symptoms: transition flaps still happen indoor/outdoor, room/room, cellar; ground floor is now transparent; cellar broken; prior runs showed grey/black clear color, missing wall textures, and character cut on stairs.
Do not patch first. Audit dirty diff, read named retail PView, parse launch-flap-shell-capture-relaunch.log and launch-pview-watermark-probe.log. Determine exactly why:
1) cellar root 0174 fails to traverse 0174 -> 0175 when proj=0;
2) root 0172 toggles outside_view/0170 reachability while 0172 -> 016F has proj=0;
3) shell/object/terrain/depth-clear decisions disagree.
Patch only the retail mismatch. Build/test before relaunch. Do not claim success before user visual confirmation.
```

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.
```