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>
This commit is contained in:
parent
d6aa526dd3
commit
6c3a96b26e
14 changed files with 8231 additions and 1 deletions
|
|
@ -0,0 +1,127 @@
|
|||
# Handoff/Findings — the "physics rest µm-jitter" flap diagnosis is REFUTED; the flap is a RENDER membership instability at the grazing doorway portal — 2026-06-08 (PM)
|
||||
|
||||
> **Supersedes** `docs/research/2026-06-08-flap-rootcause-physics-rest-handoff.md` (the AM handoff). That
|
||||
> handoff asked to be treated as a suspect statement and verified — this session verified it with primary
|
||||
> evidence and it does **not** hold. The flap is **not physics** and **not camera drift**. It is the
|
||||
> portal-flood **membership flickering non-monotonically at the grazing doorway portal as the camera eye
|
||||
> sweeps** (i.e. while you turn the camera at the doorway). This is the SPEC's §2.2 diagnosis
|
||||
> (`2026-06-08-portal-flood-membership-stability-design.md`), NOT its refuted §4 enqueue-once fix.
|
||||
|
||||
---
|
||||
|
||||
## 1. What was claimed (AM handoff) vs what the evidence shows
|
||||
|
||||
**AM claim:** the flap's varying input is a **physics resting-position µm jitter** — `_body.Position`
|
||||
blips ~1 ULP between ticks at rest → `RenderPosition` (Lerp of physics) jitters → eye jitters → flood
|
||||
flips. Fix = physics rest-stability (broaden `kill_velocity`, hold contact plane).
|
||||
|
||||
**Refuted by three independent pieces of primary evidence:**
|
||||
|
||||
### A. The physics body is bit-stable at standstill — it does NOT blip
|
||||
`door-recheck-capture.jsonl` (515 MB, 238,342 `ResolveWithTransition` records, captured standing at the
|
||||
doorway, cells `0xA9B40170/0171/0174/0175/0031`):
|
||||
- **216,300 true-standstill records** (zero velocity, `currentPos==targetPos`).
|
||||
- **0** resolve re-snaps (`result.position != input` never happens at standstill).
|
||||
- **0** cross-tick `currentPos` drift (the body position is carried forward byte-identically).
|
||||
- The `grounded-but-cp=none` contradictory state DOES occur (3.5% of frames) but produces **no** position
|
||||
blip.
|
||||
|
||||
Confirmed independently with **4 new deterministic tests** (all GREEN — they PROVE rest is bit-stable):
|
||||
- `PlayerMovementControllerTests.Update_AtRestNoInput_RenderPositionBitStableAcrossManyFrames` (flat terrain)
|
||||
- `PlayerMovementControllerTests.Update_WalkThenStop_SettlesToBitStableRest` (flat terrain, post-motion)
|
||||
- `CellarUpTrajectoryReplayTests.IndoorCellarFloor_AtRestZeroOffset_BodyPositionBitStable` (indoor cell, resolver loop)
|
||||
- `CellarUpTrajectoryReplayTests.IndoorCell_FullController_AtRestNoInput_RenderPositionBitStable` (indoor cell, full controller loop)
|
||||
|
||||
### B. On the actual flap frames, the PLAYER position is byte-identical
|
||||
`pvinput.log` window 7748–7758 (a clean `flood 6↔2` flap), player `RenderPosition` for **11 consecutive
|
||||
frames**: `(155.632858, 13.527222, 94.000000)` — **byte-identical**. The physics output the camera reads
|
||||
does not move at all during the flap. (The 1-ULP *player* blip the AM handoff cited is at the **outdoor**
|
||||
`flood=1` records — a red herring, not the indoor flap.)
|
||||
|
||||
### C. The EYE moves while the player is still — and the camera DOES settle when idle
|
||||
Same window: the eye orbits smoothly ~1 mm/frame (X ↓, Y ↑, Z constant) — a slow **camera rotation**
|
||||
around the stationary player. And:
|
||||
- Of **888** flood flips in the capture, only **1** had a byte-identical eye (and that one is the
|
||||
outdoor→indoor root switch). Every other flip had a moved eye → **the flood is deterministic in the
|
||||
eye; it changes only when the eye moves** (matches `Build_IsDeterministic_*`).
|
||||
- Longest **indoor byte-identical-eye runs: 203 / 181 / 178 frames (~3.4 s)** — within each, the flood is
|
||||
a **single constant value** (no flicker). **61%** of indoor frames have a byte-stable eye.
|
||||
- ⇒ The camera **settles** at rest (no boom drift, no spring oscillation). When the eye is still, the
|
||||
flood is stable. The flap fires **only while the eye is moving**.
|
||||
|
||||
## 2. The actual mechanism
|
||||
|
||||
When the camera eye **sweeps** through the grazing doorway portal (you turn the camera at the threshold),
|
||||
the deep cell cluster `{0172,0173,0174,0175}` flickers in/out — flood `6,6,6,2,2,6,6,6,2,6,2` — i.e.
|
||||
**non-monotonic membership across a monotonic eye sweep**. A correct visibility flood would transition
|
||||
the deep cluster in/out **once** as the grazing portal closes; instead its clip flips empty↔non-empty as
|
||||
the eye crosses and re-crosses the knife-edge. This is the SPEC's §2.2 diagnosis (the grazing portal's
|
||||
clip / re-clip drift makes `clippedRegion.Count` flip `0↔N`, dropping the deep cluster on empty-clip
|
||||
frames).
|
||||
|
||||
It is **NOT** physics (A, B). It is **NOT** camera drift/oscillation (C: eye byte-stable ~3.4 s when
|
||||
idle). It is a **render-side portal-flood membership instability at grazing angles**, surfaced by camera
|
||||
rotation.
|
||||
|
||||
## 3. Status of prior fixes / diagnoses
|
||||
|
||||
- **AM physics-rest fix** — would not have fixed the flap (physics rest is already bit-stable). Do not pursue.
|
||||
- **SPEC §2.2 diagnosis (grazing-portal membership instability)** — CONFIRMED by this evidence.
|
||||
- **SPEC §4 enqueue-once fix** — already refuted in the AM handoff (retail propagates late slices via
|
||||
`AddToCell`, decomp :433494; broke `Build_ViewGrowthAfterDoneCell_PropagatesNewSlicesToExit`). So the
|
||||
correct fix is a render-side membership stabilization that is monotonic under a sweeping eye **without**
|
||||
breaking late-slice propagation — design TBD (brainstorm).
|
||||
|
||||
## 4. Apparatus (this session)
|
||||
- 4 GREEN rest-stability tests (above) — keep as regression guards + evidence that physics rest is bit-stable.
|
||||
- Analysis scripts (ad-hoc, `/tmp`): door-recheck standstill survey; pvinput flood-flip vs eye/player
|
||||
delta buckets; indoor byte-stable-eye-run scan. Re-derivable from `pvinput.log` + `door-recheck-capture.jsonl`.
|
||||
- Existing probes `[pv-input]` (`ACDREAM_PROBE_PVINPUT`) and `[render-sig]` remain the live gate.
|
||||
|
||||
## 5. Next step (proposed)
|
||||
Brainstorm + design a render-side fix that makes the deep-cluster membership **monotonic/stable as the eye
|
||||
sweeps the grazing portal** (candidates: more robust grazing-portal clip, a retail-faithful single-process
|
||||
traversal that doesn't re-clip-drift, or matching retail's exact `GetClip`/`polyClipFinish` epsilon). Then
|
||||
TDD a builder test that sweeps the eye across the grazing angle and asserts monotonic membership, and
|
||||
visual-gate by turning the camera at the cottage doorway.
|
||||
|
||||
## 6. LIVE-CONFIRMED (2026-06-08 PM, targeted doorway capture, user-driven)
|
||||
|
||||
A fresh instrumented capture (`launch-flap-capture.ps1`; `[pv-input]` enhanced with `rawPlayer`=raw
|
||||
physics body pos + `yaw`; cells `0170-0175` dumped to `tests/.../Fixtures/flap-doorway/`; 84K frames)
|
||||
confirms the diagnosis across every state and decomposes the flap into THREE render sub-issues. **In
|
||||
every case the player render-pos AND raw physics-pos are byte-identical (0 µm) — physics is conclusively
|
||||
exonerated; the flap is 100% camera-eye-driven.**
|
||||
|
||||
| State (user-driven) | player moves | eye moves | flood |
|
||||
|---|---|---|---|
|
||||
| Idle, hands fully off | no (0 µm) | **no (0 µm)** | **stable** (no flap) |
|
||||
| Turn / walk | no (0 µm) | yes (mm, yaw) | oscillates |
|
||||
| Camera smoothing-glide after a turn (yaw byte-constant, eye glides monotonically, decelerating) | no | yes (mm) | **oscillates 8↔3** ← this is the "flickers while idle" the user perceived |
|
||||
|
||||
Key burst (row 11167): **yaw byte-constant**, eye X glides monotonically 155.109→155.435 (18→5 mm/frame,
|
||||
decelerating), flood `8→3→8…3→8`. Monotonic eye ⇒ non-monotonic membership ⇒ **render** instability (not
|
||||
camera hunting).
|
||||
|
||||
**Three sub-issues (all eye-driven, physics out):**
|
||||
- **A — Membership oscillation:** flood non-monotonic as the eye sweeps within a *stable* root.
|
||||
outside-looking-in `8↔3`; outdoor-root `17↔33` (21 flips/2500 frames); indoor-root `2↔6`.
|
||||
- **B — Root toggle (the big one):** at the threshold, `outRoot` flips outdoor↔indoor as the eye crosses
|
||||
the door plane → wholesale visible-set swap **≈18-33 cells ↔ 2 cells** (4 toggles in 2500 frames). This
|
||||
is the "two-branch" outdoor-node-vs-indoor-cell root switch the unification was meant to remove — still
|
||||
present.
|
||||
- **C — Indoor-root under-inclusion:** eye just inside ⇒ `outRoot=n` flood **= 2, stable** for 2438
|
||||
frames → outdoors + other rooms missing (the indoor flood does not reach back out the exit portal / to
|
||||
adjacent cells). C is B's partner: the swap *to* indoor loses the scene → "textures missing."
|
||||
|
||||
**Fix scope:** core render pipeline (root resolution + flood + grazing-clip), NOT physics, NOT camera.
|
||||
Spec §2.2 (membership instability) is right for A; B+C are the threshold root-resolution/flood issues.
|
||||
Spec §4 enqueue-once stays refuted. Design needs brainstorming (saga has reverted speculative render
|
||||
fixes — see `feedback_render_one_gate`, `feedback_verify_render_seal_before_layering`).
|
||||
|
||||
Apparatus added: `launch-flap-capture.ps1`, `analyze_flap_live.py`, `find_burst.py`, fixtures
|
||||
`tests/AcDream.Core.Tests/Fixtures/flap-doorway/0xA9B4017{0..5}.json`, `flap-doorway-resolve.jsonl`.
|
||||
|
||||
**Memory to correct:** `project_indoor_flap_rootcause` (root is render: A membership instability + B
|
||||
root-toggle + C indoor under-inclusion, all under a moving camera eye — NOT physics rest, NOT camera
|
||||
drift; the "two-branch split" B is still live).
|
||||
|
|
@ -0,0 +1,219 @@
|
|||
# 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).
|
||||
Loading…
Add table
Add a link
Reference in a new issue