docs(render): R-A2b plan — back-portal side-cull (Option B), verify-first B1/B2 pin

Reading retail InitCell (:432896) side test during writing-plans showed retail's flood is acyclic (the back portal fails the side test, so 0171<->0173 can't cycle). Our flood traverses the back portal -> the cycle -> the churn. Option B (user-chosen): cull the back portal like retail, keep the forward-portal void rescue, remove the dead cap. Phase 1 pins WHY the back portal is traversed (B1 eyeInsideOpening bypass vs B2 CameraOnInteriorSide convention) before the fix; spec REVISION updated A->B.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-09 10:25:28 +02:00
parent 3fd71a123c
commit 7b8a490da9
2 changed files with 321 additions and 6 deletions

View file

@ -14,6 +14,34 @@
---
> ## ⚠️ REVISION (2026-06-09, writing-plans decomp pass): approach changed A → B (back-portal side-cull)
> Reading retail `PView::InitCell` (`:432896`; side test at `:432962`) + `AddToCell` (`:433050`) during
> plan-writing showed WHY retail never churns: its per-portal **side test culls the "back" portal** (the
> doorway just flooded through — the viewpoint is on its exit side), so retail's flood is an acyclic tree
> and the `0171↔0173` mutual cycle **cannot form**. Retail has **no** eye-in-opening bypass of that cull.
>
> Our flood forms the cycle because the back portal `0173→0171` **is traversed** where retail culls it
> (`[pv-trace]`: `pop 0173 p0->0171 grew=True`). The re-enqueue churn (what §4 Option A targeted) is a
> *consequence* of that non-retail cycle. The user chose the more-faithful **Option B**: cull the back
> portal like retail (kill the cycle at its source), **keep** the forward-portal clip-empty void rescue,
> and remove the now-dead `MaxReprocessPerCell` cap. **§4 (Option A coverage test) is superseded by §4-B
> below.**
>
> **Open — WHY is the back portal traversed (this pins the exact fix; plan Phase 1 verifies before code):**
> - **(B1) the bypass:** `EyeInsidePortalOpening` switches off the side-cull (`Build` lines ~208-216:
> `!CameraOnInteriorSide(...) && !eyeInsideOpening`) when the eye is within 1.75 m of a doorway → fix:
> drop `&& !eyeInsideOpening` from the side-cull (back portals cull; the *separate* clip-empty rescue at
> `Build` ~241-250 still rescues FORWARD portals, so the 2026-06-05 void fix is preserved).
> - **(B2) the side test itself:** `CameraOnInteriorSide` (`PortalVisibilityBuilder.cs:717-724`) returns
> true for the back portal where retail's `InitCell` test (`eax_9 == portal_side`, `:432962`) culls it →
> fix: align our side test to retail's convention.
> - **Discriminator:** the back portal's signed distance `D` to the doorway plane at the churn frames —
> `> 1.75 m ⇒ B2` (bypass is off; the side test passed on its own); `≤ 1.75 m ⇒ B1` (bypass in play).
> At `root=0171`, `p1->0173` was measured at `D=-2.73 m` (bypass off) — *indicating B2* — but the churn
> cluster was at a different eye pose with no captured `D`, so Phase 1 confirms before the fix.
---
## 1. Summary
The indoor **flap** (grey/background flashing through doorways while *moving*) is a portal-flood
@ -25,12 +53,17 @@ the neighbour re-enqueues. It ping-pongs to the `MaxReprocessPerCell=16` cap, wh
**arbitrary depth**. Because the cut depth depends on the exact eye position, sub-cm eye creep makes the
visible cell set swing (2↔4 cells) frame-to-frame → the grey flap.
**The fix (Option A — approved):** port retail's *bounded* propagation. A candidate contribution that is
**already covered by the neighbour's accumulated view does not count as growth** (no re-enqueue); only the
**uncovered remainder** propagates. This mirrors retail, where a redundant contribution **clips to empty
before `copy_view` appends it**, so the flood terminates structurally. Remove the `MaxReprocessPerCell` +
`popCounts` band-aid (termination is now by construction). Keep re-processing of genuinely-new slices.
Scope: `PortalVisibilityBuilder` only — no camera, rooting, clip-math, or seal change.
**The fix — see the REVISION banner above: Option B (back-portal side-cull), not the Option A coverage
test described in this paragraph.** Retail's flood is acyclic because its per-portal side test culls the
back portal; our flood cycles because the back portal is traversed (sub-mechanism B1/B2 pinned by plan
Phase 1). Fix: cull the back portal like retail (kill the cycle), keep the forward-portal clip-empty void
rescue, remove the now-dead `MaxReprocessPerCell` + `popCounts` cap. Scope: `PortalVisibilityBuilder` only
— no camera, rooting, clip-math, or seal change.
>
> _(Original Option A text, superseded — kept for the record:)_ port retail's *bounded* propagation: a
> candidate contribution already covered by the neighbour's accumulated view does not count as growth; only
> the uncovered remainder propagates. Mirrors retail's "redundant → empty before `copy_view`". This is a
> non-retail mechanism bounding a cycle retail never forms — Option B removes the cycle instead.
---