Live ACDREAM_PROBE_FLAP moving capture: flap frames are uniformly res=Grace eyeInRoot=n terrain=Skip; good frames eyeInRoot=Y terrain=Planes. The 3rd-person camera EYE drifts out of the player's cell -> FindCameraCell returns the stale cell for 3 grace frames -> from that stale root the doorway portal is behind the eye (D=+1.26 CULL) -> exit cell drops -> terrain+shells Skip. Clip math is fine (clip=5 when eye inside). Fix: (1) root visibility at the PLAYER's cell (retail CellManager::ChangePosition tracks curr_cell by player; acdream already does this for lighting at GameWindow:7152); (2) keep a player-reachable cell + exit when the threshold eye-projection degenerates. Supersedes H2 and the earlier idle-frame 'stale root refuted' note. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
128 lines
9.1 KiB
Markdown
128 lines
9.1 KiB
Markdown
# U.4c flap characterization — real-dat evidence (Task U.4c-1)
|
|
|
|
> **✅ CONVERGED ROOT CAUSE (2026-05-31, live ACDREAM_PROBE_FLAP moving capture) — read this first.**
|
|
> The flap is the **visibility root being driven by the 3rd-person camera EYE instead of the
|
|
> player.** Live data (`u4c-flap2.log`): every flap frame is `res=Grace eyeInRoot=n terrain=Skip`;
|
|
> every good frame is `eyeInRoot=Y terrain=Planes`. When the chase eye drifts out of the player's
|
|
> cell (through a wall into an adjacent cell / an AABB gap), `CellVisibility.FindCameraCell(camPos)`
|
|
> finds no cell and returns the **stale previous cell for 3 grace frames**; from that stale root the
|
|
> doorway portal is behind the eye (`p0->0x0170 D=+1.26 CULL`) → the exit cell `0170` drops →
|
|
> `outPolys=0` → terrain + shells Skip. Pose where it works: eye genuinely inside (`D=-0.97 TRV
|
|
> proj=4 clip=5 → outPolys=1`). The clip math is NOT broken (clip=5 when the eye is inside); the
|
|
> ROOT is wrong. At the exact threshold the eye-projection of the doorway also degenerates
|
|
> (`proj=0`) even when the player would reach it — so the fix has two parts:
|
|
> **(1) root visibility at the PLAYER's cell, not the eye** (retail `CellManager::ChangePosition`
|
|
> tracks `curr_cell` by the player; acdream already does this for lighting at GameWindow.cs:7152);
|
|
> **(2) a player-reachable cell + its exit render even when the per-frame eye-projection
|
|
> degenerates** (the spec's "set stable, clip refines WHERE not WHICH" principle, correctly rooted).
|
|
> This supersedes BOTH the H2 (side-test) conclusion below AND the earlier "stale root refuted"
|
|
> note (which was judged from a single idle frame; the moving capture proves the eye does leave the
|
|
> cell). Apparatus: `RenderingDiagnostics.ProbeFlapEnabled` ([flap]/[flap-cam] lines), commits
|
|
> `b5f2bf2`/`8941d1e` + the probe commits.
|
|
|
|
> **⚠️ CORRECTION (2026-05-31, later same day) — the H2 conclusion below is WRONG.**
|
|
> Two follow-on evidence steps overturned the "port the side test (H2)" conclusion this note reaches:
|
|
> 1. **PortalSide swap is a no-op** (`docs/research/2026-05-31-u4c-initcell-pseudocode.md`): the
|
|
> dat `PortalSide` sense, under the `(Flags & 2)==0` boolean convention + our normal winding, is
|
|
> byte-identical to our centroid `InsideSide` at every pose. The "anti-correlation" this note saw
|
|
> is in the raw 0/1 values, which cancel. Swapping changes nothing.
|
|
> 2. **The eye cannot cross the `0171→0170` plane while inside `0171`** (real AABBs): `0171` local
|
|
> y ∈ [-7.65, 1.15]; the portal plane is at local y = -7.65 (`0171`'s min boundary). The eye in
|
|
> `0171` always has side-test `D ≤ 0` → always traverses. So the side test can only cull `0170`
|
|
> when the eye is OUTSIDE `0171` while root is still `0171` (cache/grace/3rd-person pull-back) — a
|
|
> camera-cell-resolution issue, not the side test and not the per-frame PVS set. Mechanism is NOT
|
|
> yet confirmed (needs live eye-position + root-cell + per-portal drop-reason at the flap frames).
|
|
> The raw dat data in this note (topology, planes, `seenOutside=Y`, `PortalSide` flags) is correct
|
|
> and still useful; only the **H2 fix conclusion** is retracted. See the pseudocode note for the
|
|
> disproof and the handoff at the session end for the convergence plan.
|
|
|
|
|
|
|
|
**Date:** 2026-05-31. **Tool:** `tools/A8CellAudit/Program.cs` `portals` subcommand (extended this
|
|
session to dump per-portal plane + centroid-`InsideSide` + dat `PortalSide`). DAT-only, no live session.
|
|
|
|
## TL;DR
|
|
|
|
The doorway flap is a **direct `0xA9B40171 → 0xA9B40170` portal** side-test flip — **not** a multi-hop
|
|
PVS-reachability problem. The window/exit cell `0xA9B40170` is a *direct portal neighbour* of the
|
|
camera cell `0xA9B40171`. The root cause is **H2 (confirmed): our per-frame side test
|
|
(`CameraOnInteriorSide`) derives the sidedness *sense* from the cell centroid
|
|
(`GameWindow.BuildLoadedCell` ~5648), independently of — and anti-correlated with — the dat's
|
|
authored per-portal `PortalSide` flag, which retail's `PView::InitCell` (decomp:432896) uses.**
|
|
This supersedes the spec's leading hypothesis (H1, stab_list set-grounding): the stab_list/PVS
|
|
grounding (Layer 1 data, already plumbed) remains a correct retail-faithful improvement and the
|
|
`seen_outside` anchor is real, but the **flap fix is the side-test port**, not set-grounding.
|
|
|
|
## Real cell topology (Holtburg cottage, landblock 0xA9B40000)
|
|
|
|
`A8CellAudit portals 0xA9B40171 0xA9B40170 0xA9B40174 0xA9B40175`:
|
|
|
|
```
|
|
0xA9B40171 (camera/root, seenOutside=Y) — 3 portals, all flags=ExactMatch, all BUILDER_SEES=OK:
|
|
portal[0] -> 0xA9B40170 polyId=54 N=(0,-1,0) d=-7.65 centroidDot=-4.400 ourInsideSide=1 datPortalSide=0
|
|
portal[1] -> 0xA9B40173 polyId=55 N=(-1,0,0) d= 4.10 centroidDot=-2.450 ourInsideSide=1 datPortalSide=0
|
|
portal[2] -> 0xA9B40175 polyId=56 N=(0,0,-1) d=-0.00 centroidDot=-4.600 ourInsideSide=1 datPortalSide=0
|
|
|
|
0xA9B40170 (window cell, seenOutside=Y) — 2 portals, BUILDER_SEES=OK:
|
|
portal[0] -> 0xFFFF (EXIT/outdoor) flags=5 polyId=4 N=(0,-0.99,0.10) d=-8.56 centroidDot=-0.346 ourInsideSide=1 datPortalSide=0
|
|
portal[1] -> 0xA9B40171 flags=ExactMatch,PortalSide polyId=5 N=(0,-1,0) d=-7.65 centroidDot=+0.480 ourInsideSide=0 datPortalSide=1
|
|
|
|
0xA9B40174 (seenOutside=Y) -> 0xA9B40175
|
|
0xA9B40175 (seenOutside=Y) -> 0xA9B40171, -> 0xA9B40174
|
|
```
|
|
|
|
## Findings
|
|
|
|
1. **Direct portal, not multi-hop.** `0170` is `0171`'s portal[0]. The [vis] flap (`cells=4 …0170`
|
|
vs `cells=3` without it, at stable `root=0xA9B40171`) is the `0171→0170` side test flipping as the
|
|
camera moves within `0171`. Multi-hop PVS grounding (H1) would not fix a direct-portal flip.
|
|
|
|
2. **`PortalSide` is an authored, direction-asymmetric dat flag.** The same opening carries
|
|
`PortalSide` UNSET on `0171→0170` and SET on the reverse `0170→0171`. It is in
|
|
`portal.Flags` (`DatReaderWriter.Enums.PortalFlags.PortalSide`), already surfaced into
|
|
`LoadedCell.Portals[i].Flags` as `(ushort)portal.Flags` (GameWindow:5614-5618). **Our builder
|
|
never reads it** — it uses `ClipPlanes[i].InsideSide`, which `BuildLoadedCell` derives from the
|
|
cell centroid (`centroidDot >= 0 ? 0 : 1`, ~5648).
|
|
|
|
3. **Our `InsideSide` is anti-correlated with the dat `PortalSide`.** For `0171→0170`:
|
|
`InsideSide=1`, `PortalSide=0`. For `0170→0171`: `InsideSide=0`, `PortalSide=1`. We synthesize a
|
|
sidedness sense (with our own cross-product normal winding) that is opposite to the authoritative
|
|
dat value. A centroid guess is also structurally fragile for non-convex / oddly-placed portals
|
|
and for the 3rd-person camera **eye** (which can sit on the far side of an interior portal plane
|
|
while still rooted in the cell).
|
|
|
|
4. **Camera cell `0171` has `seenOutside=Y`.** Retail keeps the landscape engaged while the camera
|
|
is in `0171` (`RenderNormalMode` 92649; `grab_visible_cells` 311878). Terrain should not vanish
|
|
from `0171`. (Retail still gates the *per-frame draw* on `outside_view` non-empty in `DrawCells`
|
|
432715 — so the real fix is keeping `0170`'s exit in the per-frame view, i.e. fixing the side
|
|
test, not floating terrain off `outside_view`.)
|
|
|
|
5. **All portal polygons resolve (`BUILDER_SEES=OK`).** Not a degenerate-polygon bug.
|
|
|
|
## The fix (evidence-selected: H2)
|
|
|
|
Port retail `PView::InitCell` (decomp:432896, sidedness at 432928-432968) sidedness into the
|
|
builder's portal-traversal test: use the dat's authored `PortalSide` (from `portal.Flags`) for the
|
|
traversal *sense*, replacing the centroid-derived `InsideSide`. The plane (Normal, D) still comes
|
|
from the portal polygon geometry; only the *sense* source changes (centroid guess → authored bit).
|
|
|
|
**Open detail — the exact sign-correct mapping (do NOT guess; port + validate).** InitCell's
|
|
condition is convoluted in the BN decomp (a "clearly behind ⇒ cull" branch, an on-plane branch, and
|
|
`if (computed_sidedness == portal_side) cull else traverse`), and it interacts with the portal
|
|
polygon's normal orientation/winding. The `A8CellAudit` `retailTraversesAtCentroid` column was a
|
|
first-cut formula and is UNRELIABLE (it reported "no-traverse" for all of `0171`'s portals, which is
|
|
absurd — proof the naive sense was wrong). Resolve the exact sense by reading InitCell carefully,
|
|
porting it, and validating that (a) all of `0171`'s neighbours stay reachable from interior poses
|
|
and (b) `0170` (and its exit) stays reachable across a camera sweep near the `0171→0170` plane —
|
|
then the live visual gate. Keep `Builder_BackFacingPortal_NotTraversed` / sealed-cellar /
|
|
window-narrowing tests faithful (a fix that just "always traverses" is a band-aid and must fail
|
|
them).
|
|
|
|
## Apparatus
|
|
|
|
`A8CellAudit portals <cellIds...>` now prints, per portal: plane `N`/`d`, `centroidDot`, our
|
|
`InsideSide`, the dat `PortalSide`. Use it on any flap cell to see the centroid-vs-authored
|
|
divergence. The committed Task-1 synthetic flap test (`Build_NearBoundaryIntermediatePortal…`) models
|
|
the *mechanism* (a side-test flip drops a downstream exit cell) but via a multi-hop `InsideSide`
|
|
cull; U.4c-3 should re-author it (or add a sibling) to faithfully model the **direct-portal
|
|
`PortalSide`-vs-centroid** divergence, so the test is a clean gate for the actual fix.
|