A8CellAudit portals now dumps each cell's local AABB. Real flap cells: 0171 local y in [-7.65, 1.15], 0170 in [-8.61, -7.65]; the 0171->0170 portal plane is at y=-7.65 (0171's MIN boundary), no overlap. So an eye genuinely inside 0171 always has side-test D<=0 -> always traverses 0171->0170; the side test cannot cull 0170 while the eye is in 0171. The flap therefore requires the eye OUTSIDE 0171 while root is still 0171 (cache/grace/3rd-person camera) -> a camera-cell-resolution issue, not the side test (H2, disproven) and not the per-frame PVS set (H1, in doubt). Mechanism still unconfirmed -> needs a live eye-pos capture. Stale H2 conclusion in the characterization note corrected with a banner. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
7.3 KiB
U.4c flap characterization — real-dat evidence (Task U.4c-1)
⚠️ 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:
- PortalSide swap is a no-op (
docs/research/2026-05-31-u4c-initcell-pseudocode.md): the datPortalSidesense, under the(Flags & 2)==0boolean convention + our normal winding, is byte-identical to our centroidInsideSideat every pose. The "anti-correlation" this note saw is in the raw 0/1 values, which cancel. Swapping changes nothing.- The eye cannot cross the
0171→0170plane while inside0171(real AABBs):0171local y ∈ [-7.65, 1.15]; the portal plane is at local y = -7.65 (0171's min boundary). The eye in0171always has side-testD ≤ 0→ always traverses. So the side test can only cull0170when the eye is OUTSIDE0171while root is still0171(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,PortalSideflags) 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
-
Direct portal, not multi-hop.
0170is0171's portal[0]. The [vis] flap (cells=4 …0170vscells=3without it, at stableroot=0xA9B40171) is the0171→0170side test flipping as the camera moves within0171. Multi-hop PVS grounding (H1) would not fix a direct-portal flip. -
PortalSideis an authored, direction-asymmetric dat flag. The same opening carriesPortalSideUNSET on0171→0170and SET on the reverse0170→0171. It is inportal.Flags(DatReaderWriter.Enums.PortalFlags.PortalSide), already surfaced intoLoadedCell.Portals[i].Flagsas(ushort)portal.Flags(GameWindow:5614-5618). Our builder never reads it — it usesClipPlanes[i].InsideSide, whichBuildLoadedCellderives from the cell centroid (centroidDot >= 0 ? 0 : 1, ~5648). -
Our
InsideSideis anti-correlated with the datPortalSide. For0171→0170:InsideSide=1,PortalSide=0. For0170→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). -
Camera cell
0171hasseenOutside=Y. Retail keeps the landscape engaged while the camera is in0171(RenderNormalMode92649;grab_visible_cells311878). Terrain should not vanish from0171. (Retail still gates the per-frame draw onoutside_viewnon-empty inDrawCells432715 — so the real fix is keeping0170's exit in the per-frame view, i.e. fixing the side test, not floating terrain offoutside_view.) -
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.