InitCell decode (PortalFlags.PortalSide=0x2) + a swept-pose A8CellAudit comparison (O=centroid, A=winding-corrected PortalSide, B=opposite) over the real flap cells. A is IDENTICAL to O at every pose/every portal — the (Flags&2)==0 boolean convention makes the dat PortalSide sense equal to our centroid sense, so swapping is a no-op and cannot fix the flap. B culls true-interior poses (wrong polarity). Conclusion: the flap is NOT the side-test sense — it's the 3rd-person camera eye crossing an interior portal plane while FindCameraCell still roots in the cell; ANY plane-side test culls there. No production code changed (no no-op shipped). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.4 KiB
U.4c — PView::InitCell portal-side pseudocode (Task U.4c-3 oracle note)
Decomp anchor: docs/research/named-retail/acclient_2013_pseudo_c.txt:432896
(PView::InitCell), sidedness classification at 432928-432975; the consumer gate
PView::ClipPortals at 433589. Struct layout from acclient.h:
CCellPortal (~32300) { other_cell_id; other_cell_ptr; portal /*CPolygon* */; portal_side; other_portal_id; exact_match }
(stride 0x18, matches InitCell's edx_1 += 0x18); portal_info (~32458) { int seen; int inflag }.
PortalFlags.PortalSide = 0x2, ExactMatch = 0x1 — confirmed by reflection
against Chorizite.DatReaderWriter 2.1.7 (the version the audit tool + app use):
PortalFlags.ExactMatch = 1 (0x1)
PortalFlags.PortalSide = 2 (0x2)
Our physics-side PortalInfo.PortalSide already decodes this as a BOOLEAN with
INVERTED polarity: PortalSide(bool) = (Flags & 2) == 0 (TRUE when the bit is
clear). This matches ACE EnvCell.find_transit_cells and the existing pseudocode
doc acclient_indoor_transitions_pseudocode.md:291.
What InitCell actually computes (per portal)
Render::FrameCurrent[0..2] is the viewer position in the cell's pushed
coordinate frame (positionPush(3, &ebp->pos) at 432906 → viewer expressed in
cell-local space). ecx_4 = portal->portal is the CPolygon*; ecx_4[8..0xb]
are the portal-plane coefficients (a, b, c, d).
D = FrameCurrent.z*c + FrameCurrent.y*b + FrameCurrent.x*a + d
= dot(viewerLocal, planeNormal) + planeD // signed distance, viewer→plane
eps = 0.000199999995f // ~2e-4
// 432938-432969: a 3-way classification of D vs ±eps, then a compare to portal_side.
// (Decoded from the x87 status-word idiom: byte[1] bit0=C0 (D<y), bit2=C2 (unordered),
// bit6=C3 (D==y). &0x41 = C0|C3 = D<=y ; &0x05 = C0|C2 = D< y (ordered).)
if (D <= +eps) { // 432941: not clearly in front
if (D < -eps) { // 432948 (!p_1 is false): clearly BEHIND the plane
portal.inflag = 0; var_4 = 1; // 432954 — handled, no side compare
// (seen was cleared to 0 at 432933 for non-entry portals)
} else { // -eps <= D <= +eps : ON the plane
eax_9 = 1; goto compare; // 432950
}
} else { // D > +eps : clearly IN FRONT
eax_9 = 0; // 432959
compare:
if (eax_9 == portal.portal_side) // 432962 (raw int field 0/1)
portal.inflag = 0; var_4 = 1; // 432964
else
portal.inflag = 1; // 432968
}
ClipPortals (433589) traverses a portal iff seen != 0 && inflag != 1.
NOTE: seen is set elsewhere (entry portal at 432973-432974; AddViewToPortals/
AddToCell/SetOtherSeen on neighbour discovery) — InitCell here primarily writes
inflag. The BN field-write polarity for InitCell's local seen/inflag toggling
is genuinely ambiguous in the pseudo-C (the *(uint32_t*)esi base aliases
portal.data, and the entry-portal vs other-portal cases write both fields), and
mis-decoding it is exactly the #98 trap. We therefore DO NOT hinge the port on the
exact BN write polarity.
What we port (robust, cross-checked, polarity-resolved by real data)
Three things are unambiguous and cross-checked against THREE independent sources
(InitCell structure; ACE find_transit_cells as ported in our own
CellTransit.cs:161; acclient_indoor_transitions_pseudocode.md:305-314):
- Signed distance
D = dot(viewerLocal, planeN) + planeD(we already compute this inCameraOnInteriorSide; the plane geometry is fine). - A near-plane band is always traversable (the on-plane
eax_9=1branch + the±epstolerance). This is what kills the flap: a camera a few cm either side of the opening is in/near the band and never spuriously culled. - The traverse/cull SENSE is keyed on the authored
PortalSidebit, NOT a centroid guess.
The sense (resolved against real dat — winding-corrected)
Boolean PortalSide = (Flags & 2) == 0 (our existing convention, matching ACE /
PortalInfo.cs:44). The doc-§314 intuition says "PortalSide=true ⇒ interior is the
POSITIVE half-space (dist>0 is inside the cell)", which would give
PortalSide ? (D > -eps) : (D < +eps). But that intuition assumes ACE/retail's
stored plane-normal orientation; our hydration normal is Cross(p1-p0, p2-p0)
(GameWindow:5641), whose winding is FLIPPED relative to ACE's for these cells. The
real-dat evidence proves it: for 0171→0170 (N=(0,-1,0), d=-7.65) the cell's own
centroid is at centroidDot = -4.400 (NEGATIVE side) yet datPortalSide=0
(PortalSide-bool=true). So for OUR winding, PortalSide=true ⇒ interior is the
NEGATIVE half. The sign-correct traverse test is the INVERSE of the doc intuition:
traverse(portal, D) =
PortalSide ? (D < +eps) // OUR winding: interior = negative half → cull only when clearly on positive (far) side
: (D > -eps) // interior = positive half → cull only when clearly on negative (far) side
eps = 0.01f // PortalSideEpsilon, the existing builder tolerance (≈ retail's near-plane band, scaled to our units)
Genuinely back-facing is still culled: a camera clearly on the FAR side (|D| >> eps, wrong sign) fails the inequality → CULL. The ±eps band keeps a camera sitting a few cm either side of the opening traversable → no flap.
Real-dat verification (A8CellAudit portals, camera = cell centroid + a swept
pose toward/just past each portal plane). Sign-correct ONLY IF every interior pose
traverses ALL the camera cell's portals (neighbours, incl. the window/exit cell,
stay reachable). With the formula above, ALL of 0171's three portals AND 0170's
two portals (incl. its 0xFFFF exit) traverse from interior poses:
| from→portal | N | d | centroidDot | datPortalSide | PortalSide(bool) | traverse@centroid |
|---|---|---|---|---|---|---|
| 0171→0170 | (0,-1,0) | -7.65 | -4.400 | 0 | true → D<+eps |
-4.40 < eps → TRAVERSE |
| 0171→0173 | (-1,0,0) | +4.10 | -2.450 | 0 | true → D<+eps |
-2.45 < eps → TRAVERSE |
| 0171→0175 | (0,0,-1) | -0.00 | -4.600 | 0 | true → D<+eps |
-4.60 < eps → TRAVERSE |
| 0170→EXIT | (0,-.99,.10) | -8.56 | -0.346 | 0 | true → D<+eps |
-0.35 < eps → TRAVERSE |
| 0170→0171 | (0,-1,0) | -7.65 | +0.480 | 1 | false → D>-eps |
+0.48 > -eps → TRAVERSE |
⚠️ FINDING — the PortalSide swap is a NO-OP for the flap (BLOCKED 2026-05-31)
The swept-pose A8CellAudit portals run (camera lerped from cell centroid toward the
nearest portal vertex and a bit past the plane, t∈[0,1.3]) was meant to confirm the
sense. It instead falsified the whole premise of this fix. For EVERY portal of the
flap cells, three candidate senses were evaluated per pose:
- O — OLD centroid
InsideSide(pre-fixCameraOnInteriorSide) - A — winding-corrected
PortalSide(the formula above) - B — doc-§314 literal
PortalSide(opposite polarity)
0171→0170 N=(0,-1,0) d=-7.65 ourInsideSide=1 datPortalSide=0
t0.00:D=-4.40/O=T/A=T/B=x t0.25:D=-3.30/O=T/A=T/B=x ... t1.00:D=0.00/O=T/A=T/B=T
t1.15:D=0.66/O=x/A=x/B=T t1.30:D=1.32/O=x/A=x/B=T
0170→0171 N=(0,-1,0) d=-7.65 ourInsideSide=0 datPortalSide=1
t0.00:D=0.48/O=T/A=T/B=x ... t1.00:D=0.00/O=T/A=T/B=T t1.15:D=-0.07/O=x/A=x/B=T
(All of 0171's three portals + 0170's two + 0174 + 0175 show the same pattern.)
Two facts kill the side-test fix:
- A ≡ O at every pose, every portal. The "anti-correlation" the characterization
doc saw is in the RAW stored values (
InsideSide=1vsdatPortalSide=0); but each value maps to the SAME inequality direction, so the centroid sense and the winding-correctedPortalSidesense make the identical traverse/cull decision. Swapping centroidInsideSide→ datPortalSideis a literal no-op for these cells and cannot fix the flap. - B culls true-interior poses (
B=xat t0.00, the cell centroid) → the doc-literal polarity is wrong for our winding. So there is no viable alternative polarity either.
Root cause of the flap is NOT the side-test sense. It is structural: set membership
is rebuilt every frame from a plane-side test against the 3rd-person camera EYE
(camPos, GameWindow:7290), which legitimately CROSSES interior portal planes while
FindCameraCell still roots the camera in the cell (AABB + grace frames). Once the eye
is clearly on the far side of the 0171→0170 plane (D > +eps, poses t1.15+), ANY
plane-side test — centroid or PortalSide, either polarity — culls the portal, drops
0170, empties OutsideView, and flaps terrain/shells off. The audit proves no sense
choice avoids this.
The retail-faithful fix is PVS / stab_list grounding (spec §5.2 H1; U.4 handoff
2026-05-30-phase-u4-shipped-and-flap-handoff.md lines 91-115): seed every cell in the
camera cell's stab_list (add_views / grab_visible_cells, decomp 433382 / 311878)
as a participant BEFORE the per-frame walk, so the window/exit cell stays in the set
regardless of the side test. The side test then only refines WHERE each cell draws,
never WHICH cells exist. That is the spec's Layer 2 — out of scope for the task's
"swap the side test in the builder" framing, which is why this is reported BLOCKED.
ClipPlanes[i].Normal/D remain correct (plane geometry is fine); InsideSide
remains on the struct. The builder side test is left UNCHANGED — no no-op swap shipped.