acdream/docs/research/2026-05-31-u4c-initcell-pseudocode.md
Erik b5f2bf2b8f research(render): Phase U.4c — DISPROVE the side-test fix (PortalSide port is a no-op)
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>
2026-05-31 10:32:52 +02:00

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):

  1. Signed distance D = dot(viewerLocal, planeN) + planeD (we already compute this in CameraOnInteriorSide; the plane geometry is fine).
  2. A near-plane band is always traversable (the on-plane eax_9=1 branch + the ±eps tolerance). 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.
  3. The traverse/cull SENSE is keyed on the authored PortalSide bit, 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-fix CameraOnInteriorSide)
  • 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:

  1. A ≡ O at every pose, every portal. The "anti-correlation" the characterization doc saw is in the RAW stored values (InsideSide=1 vs datPortalSide=0); but each value maps to the SAME inequality direction, so the centroid sense and the winding-corrected PortalSide sense make the identical traverse/cull decision. Swapping centroid InsideSide → dat PortalSide is a literal no-op for these cells and cannot fix the flap.
  2. B culls true-interior poses (B=x at 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.