acdream/docs/superpowers/specs/2026-06-08-portal-flood-enqueue-once-port-design.md
Erik 6c3a96b26e diag(render): flap re-diagnosed as portal-flood re-clip DRIFT; physics + camera REFUTED
The 2026-06-08 AM "physics rest micro-jitter" diagnosis is refuted with primary
evidence (door-recheck 216K standstill records: 0 position re-snaps; player
byte-stable during the flap). Two adversarial verification sub-agents confirmed:

- Retail roots the render at the camera viewer_cell (swept from the player via
  SmartBox::update_viewer 0x453ce0; DrawInside(viewer_cell) 0x453aa0) and toggles
  DrawInside / LScape::draw -- so acdream's eye-cell rooting + inside/outside
  toggle are RETAIL-FAITHFUL. The locked-design "root at player cell" is wrong.
- The flap is render membership instability, eye-motion-driven: the visible-cell
  set oscillates (8<->3) as the eye sweeps monotonically. Root = the
  re-enqueue-on-growth DRIFT (PortalVisibilityBuilder.cs:322, MaxReprocessPerCell
  =16) re-clipping each grown cell every round -> sub-cm eye jitter flips membership.

Fix (spec, not yet implemented): verbatim port of retail's enqueue-once flood
(ConstructView + AddViewToPortals): enqueue once on first discovery, clip each
cell's portals once, union late growth in place (AddToCell) + draw-reorder
(FixCellList), never re-enqueue. Kills the drift; rooting/camera/seal untouched.

This commit lands VERIFIED GROUNDWORK + design only:
- spec: docs/superpowers/specs/2026-06-08-portal-flood-enqueue-once-port-design.md
- findings: docs/research/2026-06-08-flap-physics-diagnosis-REFUTED-its-render-membership.md
- [pv-input] probe gains rawPlayer + yaw (disambiguates the varying input)
- 4 GREEN physics rest-stability tests (prove rest is bit-stable -> flap not physics)
- apparatus: launch-flap-capture.ps1, analyze_flap_live.py, find_burst.py
- captured fixtures: tests/.../Fixtures/flap-doorway/0xA9B4017{0..5}.json

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 11:21:46 +02:00

219 lines
15 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.

# Portal-Flood Enqueue-Once Port — the indoor "flap" fix (verified design)
**Date:** 2026-06-08
**Branch:** `claude/thirsty-goldberg-51bb9b`
**Status:** Design approved (brainstorm). Pending implementation plan.
> **Supersedes** the enqueue logic in `docs/superpowers/specs/2026-06-08-portal-flood-membership-stability-design.md`
> (whose §4 enqueue-once was an *incomplete* attempt — it dropped the `AddToCell` growth half) and the
> physics-rest direction in `docs/research/2026-06-08-flap-rootcause-physics-rest-handoff.md` (refuted).
> **Diagnosis evidence:** `docs/research/2026-06-08-flap-physics-diagnosis-REFUTED-its-render-membership.md`
> + this session's two adversarial verification agents (retail decomp + acdream code/data).
---
## 1. Summary
The indoor render **flap** (interior textures battling / popping in and out at a building doorway) is a
**render-side portal-flood membership instability**: as the camera **eye** moves (turning the camera, or
the camera's smoothing-glide after a turn), the set of cells the flood deems visible **oscillates**
(e.g. `8↔3`) even though the eye sweeps **monotonically**. The root is acdream's **re-enqueue-on-growth
"drift"** in `PortalVisibilityBuilder.Build` (`cs:322`, `MaxReprocessPerCell = 16`): a cell whose view
grows is re-enqueued and its portals **re-clipped from the grown (drifted) view** each round; under
sub-cm eye motion each frame re-clips slightly differently → the visible set flips.
The fix is a **verbatim port of retail's enqueue-once portal traversal** (`PView::ConstructView` +
`AddViewToPortals`): a cell is enqueued **only on first discovery**; its portals are clipped **exactly
once** (at pop); later growth into an already-discovered cell is unioned **incrementally in place**
(`AddToCell`) and its draw-list slot re-ordered (`FixCellList`) — **never re-enqueued, never re-clipped
from scratch**. This makes the visible set a deterministic function of the **root + geometry**, so it no
longer drifts with eye jitter. Localized to `PortalVisibilityBuilder`. No camera, physics, rooting, clip-
math, or seal change.
---
## 2. Root cause — verified this session
### 2.1 What the flap is NOT (refuted with primary evidence)
- **Not physics.** `door-recheck-capture.jsonl`: **216,300 standstill physics records, 0 position
re-snaps** — the body is byte-stable at rest. Deterministic tests (flat terrain + indoor cell, resolver
+ full controller) confirm: a resting body holds a byte-identical position. The 2026-06-08 AM
"physics rest µm-jitter" diagnosis is refuted.
- **Not the camera rooting or the inside/outside toggle.** Verified against retail (agent 1):
`SmartBox::RenderNormalMode` (0x453aa0) calls **`DrawInside(viewer_cell)`** (decomp 92675), and
`SmartBox::update_viewer` (0x453ce0) sets `viewer_cell` from a **swept `CTransition`** seeded at the
**player's cell** (`init_path(cell_1, …)` 92866 → `viewer_cell = sphere_path.curr_cell` 92871). So
rooting at the camera's `viewer_cell` and toggling `DrawInside`/`LScape::draw` are **retail-faithful**.
The locked-design claim "root at the player cell" (`2026-06-02 …redesign-design.md` §1.5) is **wrong**;
acdream's current `clipRoot = viewerRoot ?? _outdoorNode` (eye-cell rooting) is correct and stays.
- **Not camera drift at rest.** When the eye is byte-stable (hands-off idle), the flood is rock-stable
(203/181/178-frame byte-identical-eye runs hold a single flood value). The camera settles; the flap
fires **only while the eye moves**.
### 2.2 What the flap IS (verified — agent 2 + live capture)
- The flood oscillates **only when the eye moves**: across ~7,800 flood flips, **3** had a byte-identical
eye (all startup/streaming); **~87 %** of eye-motion flips have a **byte-identical player** position.
A clean burst (yaw byte-constant, eye gliding monotonically 18→5 mm/frame as the camera settles) shows
flood `8→3→8…`**non-monotonic membership under a monotonic eye sweep**.
- The mechanism is the **re-enqueue/re-clip drift**: `PortalVisibilityBuilder.cs:322`
`if (grew && popCounts.GetValueOrDefault(neighbourId) < MaxReprocessPerCell && queued.Add(neighbourId))
todo.Insert(neighbour, dist);` re-enqueues a grown neighbour up to 16×; each re-process re-clips the
cell's portals from its grown view, so sub-cm eye jitter walks `ClipToRegion`'s surviving-vertex count
across the empty/non-empty boundary → the deep cluster `{0172-0175}` drops/returns → the flap.
- **Sub-issue "C" (indoor flood=2 / "missing textures") is mostly a *symptom* of this drift**, not a
missing seal: the landscape-through-the-door seal **is** present in the indoor path
(`RetailPViewRenderer.DrawInside``DrawLandscapeThroughOutsideView`). When the flood drops `8→3`,
the `OutsideView`/terrain/cell clip shrinks → things vanish. Fixing the drift removes the symptom.
---
## 3. Retail grounding (the traversal being ported)
All from `docs/research/named-retail/acclient_2013_pseudo_c.txt`:
- **`PView::ConstructView`** (0x5a57b0, :433750): `InitCell(root)` + `InsCellTodoList(root)`, then a loop
that **pops one cell at a time** from the todo list, **appends it to the draw list** (← that is
membership), sets `cell_view_done = 1` (:433784), runs `ClipPortals` once, then `AddViewToPortals`.
- **`PView::AddViewToPortals`** (0x5a52d0, :433446): for each visible portal to a neighbour, three cases
keyed on the neighbour's stamps (`processed_stamp` = `*(view+0x44)`, `view_stamp` = `*(view+0x38)`):
- **First discovery** (`processed_stamp == 0`, :433478): `InitCell(neighbour)` + `InsCellTodoList`
(**enqueue once**).
- **Growth** (`processed_stamp != view_stamp`, :433492): `AddToCell(neighbour)` + if already drawn
`FixCellList`; then `processed_stamp = view_stamp`. **No re-enqueue. No re-clip from scratch.**
- **Already current** (`processed_stamp == view_stamp`): **nothing**.
- **`PView::AddToCell`** (0x5a4d90, :433050): clips the cell's portals against **only the newly-added
view slices** (`for i = esi[0x11]; i < esi[0xe]`) — an **incremental** union, not a full re-clip; it
does **not** re-contribute to `OutsideView`.
- **`PView::FixCellList`** (0x5a5250, :433407) → `AdjustDrawList` (:433107): **re-orders** the grown cell
in the draw list to preserve draw order. No re-flood.
- **`PView::InitCell`** (0x5a4b70, :432896): seeds the cell's view, clips its portals against the full
incoming view, stamps with `master_timestamp`; returns whether the cell is non-empty (→ enqueue).
So retail clips each cell's portals **exactly once** (at pop). Late growth refines a cell's own view +
draw order, never its downstream flood. This is the `cell_view_done` "process each cell once" guarantee.
---
## 4. The fix (design)
**Scope: `PortalVisibilityBuilder.Build` only.** Replace the re-enqueue-on-growth fixpoint with retail's
enqueue-once traversal. Concretely:
**Change A — enqueue-once (`Build` ~308-328).** Today:
var nview = GetOrCreate(frame.CellViews, neighbourId);
bool grew = AddRegion(nview, clippedRegion); // union in place (= retail AddToCell)
if (grew && popCounts.GetValueOrDefault(neighbourId) < MaxReprocessPerCell && queued.Add(neighbourId))
todo.Insert(neighbour, dist); // RE-ENQUEUE on growth the drift
New: enqueue a neighbour into `todo` **only on first discovery** i.e. when it has **no `CellViews`
entry yet** (retail `processed_stamp == 0` `InitCell` + `InsCellTodoList`). On growth into an
already-discovered neighbour, **keep `AddRegion`** (incremental union = `AddToCell`) and re-order it in
the draw list if already present (`FixCellList`, §Change C), but **do not** re-insert into `todo`.
**Change B — remove the re-enqueue machinery.** Delete `MaxReprocessPerCell`, `popCounts`, and the
per-pop re-enqueue / `queued`-reset logic in the pop loop. Termination is now by construction (each cell
enqueued 1, popped 1; N cells total), matching retail `cell_view_done`. The `MaxReprocessPerCell` cap
existed **only** as a termination band-aid for the re-enqueue with enqueue-once it is dead.
**Change C — draw-list re-order on growth (`FixCellList`).** When growth unions into an
already-discovered cell that is **already in `OrderedVisibleCells`**, re-position it to preserve
closest-first draw order (retail `AdjustDrawList` :433107). If acdream's `OrderedVisibleCells` is already
distance-sorted at assembly time and order is not load-bearing for correctness, this degrades to a no-op
confirm during implementation; do **not** add ordering machinery the renderer doesn't consume.
**Unchanged (explicitly):** the per-portal clip (`ProjectToClip`/`ClipToRegion`), the
`EyeInsidePortalOpening` degenerate-portal guard (`Build:235-244`), the reciprocal `OtherPortalClip`, the
`OutsideView` exit contribution, the rooting (`clipRoot = viewerRoot ?? _outdoorNode`), the camera, and
the landscape-through-door seal. No new predicate, no robustness heuristic, no hysteresis.
**Why this is the flap fix, not a band-aid:** with each cell's portals clipped once, the visible set is a
deterministic function of `(root, geometry)` independent of the per-round re-clip path. Sub-cm eye
jitter changes the *projection* (and thus what's drawn within each clipped cell, correctly) but no longer
changes *which cells are members*. The membership stops oscillating; the textures stop battling.
---
## 5. The `Build_ViewGrowthAfterDoneCell` question (open item, resolve during implementation)
The re-enqueue was added 2026-06-07 "to propagate late-discovered slices to exit portals," and
`PortalVisibilityBuilderTests.Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit` encodes that. But
the decomp shows retail's `AddToCell` (:433050) only clips the cell's **own** portals against new slices
+ re-orders draw position it does **not** re-contribute to `OutsideView` (the exit slice is emitted by
`ClipPortals` at pop, once). So "late growth reaches the exit/OutsideView" appears to be **non-retail**.
**Action:** read `PView::ClipPortals` (the OutsideView contribution site) during implementation to
confirm. If confirmed, this test encodes the non-faithful re-enqueue behavior and is **corrected to
match retail** (late growth refines the cell's view + draw order, not the OutsideView). It will **not**
be satisfied by reinstating the re-enqueue. If the OutsideView tests
(`Builder_Cellar_WindowClippedToStairwell`, look-in tests) shrink, that is the retail behavior, handled
retail's way not by re-adding the drift.
---
## 6. Testing (TDD)
The flap manifests only under live µm/mm eye motion at a specific grazing geometry, so the **visual gate
is acceptance**; the unit layer pins determinism + guards regressions.
1. **Deterministic eye-sweep stability (new, the RED→GREEN driver).** In `AcDream.App.Tests`
(alongside `PortalVisibilityBuilderTests`, since `PortalVisibilityBuilder` is an App-layer type), build
the flood at a sequence of eye positions stepping across the grazing door angle (sub-cm steps
reproducing the live sweep). **Assert each cell's membership across the sweep is a single contiguous
run** no `present→absent→present` (or `absent→present→absent`) flicker for any cell. That is the
precise anti-flap property (the live capture showed `8→3→8→3`, multiple transitions per cell). RED
under the re-enqueue drift; GREEN after enqueue-once. *Fixture note:* the captured dumps live at
`tests/AcDream.Core.Tests/Fixtures/flap-doorway/0xA9B4017{0..5}.json`; the test must reach them
(shared path or copied into `AcDream.App.Tests/Fixtures`) and the cells must carry the portal graph +
clip planes `Build` consumes. If the cell-dump format omits portals/clip-planes, the impl plan either
extends the dump or synthesizes a minimal doorway portal topology reproducing the grazing geometry
surface this as the first implementation step, do not silently weaken the test.
2. **Enqueue-once termination + dedup (new).** Diamond (a cell reachable from two parents) + cycle
fixtures: assert the flood terminates with `MaxReprocessPerCell` removed, `OrderedVisibleCells` is
deduped, each reachable cell present exactly once, and (if a per-cell pop counter is cheap to surface)
each cell popped 1.
3. **No membership regression.** `Build_IsDeterministic_*`, `Build_EyeStandingInInteriorPortal_FloodsNeighbour`,
`Build_CollapsedInteriorPortalNearEyeBeyondHalfMeter_FloodsNeighbour`,
`Build_DegeneratePortalToTheSide_NotFlooded_NoOverInclusion` (#95 guard), and the cellar/window/look-in
tests stay **green**. `Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit` is handled per §5.
4. **Visual gate (user) — acceptance.** At the cottage doorway: turn the camera back and forth and walk
through the interior rooms render steadily, no battling/popping; the `[pv-input]` flood is stable
for a given eye pose. Re-run with `launch-flap-capture.ps1`.
`dotnet build` + `dotnet test` green before the visual gate.
---
## 7. Scope / non-goals
- **In scope:** `PortalVisibilityBuilder.Build` enqueue logic (enqueue-once; remove
`MaxReprocessPerCell`/`popCounts`/re-enqueue; incremental union on growth; draw-order re-position) + the
new/updated tests; reading `ClipPortals` to settle §5.
- **Non-goals (deferred / untouched):**
- **No rooting change** eye-cell rooting (`clipRoot = viewerRoot ?? _outdoorNode`) is retail-faithful
2.1). The locked design's "root at player cell" is refuted, not implemented.
- **No clip-math change** (`ProjectToClip`/`ClipToRegion`), no `EyeInsidePortalOpening` change, no
overlap predicate, no hysteresis/robustness heuristic.
- **No camera, physics, or seal change.** The landscape-through-door seal already exists; C is a symptom
of the drift and resolves with it.
- The 4 GREEN physics rest-stability tests added this session stay as regression guards (they document
that physics rest is bit-stable the flap is not physics).
---
## 8. Apparatus + references
- **Diagnosis + verification:** `docs/research/2026-06-08-flap-physics-diagnosis-REFUTED-its-render-membership.md`;
this session's two adversarial verification agents (retail decomp CONFIRMED rooting/seal; acdream
code/data CONFIRMED physics-out + eye-driven + the `cs:322` drift).
- **Captured fixtures:** `tests/AcDream.Core.Tests/Fixtures/flap-doorway/0xA9B4017{0..5}.json`;
`flap-doorway-resolve.jsonl`. Apparatus: `launch-flap-capture.ps1`, `analyze_flap_live.py`,
`find_burst.py`, the `[pv-input]` probe (`ACDREAM_PROBE_PVINPUT`, now logs eye/player/rawPlayer/yaw).
- **Retail decomp anchors:** `ConstructView` :433750, `AddViewToPortals` :433446, `InitCell` :432896,
`AddToCell` :433050, `FixCellList` :433407 / `AdjustDrawList` :433107, `InsCellTodoList` :433183,
`SmartBox::update_viewer` :92761, `SmartBox::RenderNormalMode` :92635.
- **Superseded:** `2026-06-08-portal-flood-membership-stability-design.md` §4 (incomplete enqueue-once);
`2026-06-08-flap-rootcause-physics-rest-handoff.md` (physics direction, refuted).
- **Memory to correct after ship:** `project_indoor_flap_rootcause` (root = the `PortalVisibilityBuilder`
re-enqueue/re-clip **drift** under a moving eye; rooting/toggle is retail-faithful; physics + camera
exonerated).