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>
162 lines
9.4 KiB
Markdown
162 lines
9.4 KiB
Markdown
# 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.
|