fix #180 (root): retail adjust_to_plane - PathClipped stops now resolve to the contact point, not a step boundary

The autonomous visual loop on the stateful-sought camera exposed the true
root of the #176 stripes: a ~19Hz SAWTOOTH. The sought re-extends ~3mm/frame
and the sweep silently passes while the 0.3m viewer sphere presses up to
~0.25m past the wall plane, then clips a whole transition step (~0.27m) back.
Headless replay against the real Facility Hub corridor BSP (0x8A020164, the
captured ray) reproduced it exactly: pre-fix, embedded targets passed
unclipped and the first detection stopped at the PREVIOUS STEP BOUNDARY,
tracking the target (eyeBack = s - 0.27).

Root cause: BSPQuery.AdjustToPlane - copied from ACE's BSPTree.cs port -
was structurally inverted and ALWAYS returned false (the touchTime==1 branch
re-placed the sphere at the unchanged check position; touchTime<1 iterated
doing nothing; the <0.02 convergence exit returned false). With the
PerfectClip exact-contact machinery dead, CollideWithPt always fell to the
bare Collided path and the transition reverted the colliding step whole.
ACE never noticed: PERFECT_CLIP (0x40) is a client camera flag the server
never exercises (feedback_bn_decomp_field_names class 3 - the retail binary
outranks ACE in branches ACE never runs). The pre-stateful camera flipped
1-step vs 2-step backoffs on mm drift - the measured pulledIn 0.27 <-> 0.53
of the original #176 strobe was step quantization all along.

Rewritten per the retail binary (pseudocode doc
docs/research/2026-07-06-adjust-to-plane-pseudocode.md):

- BSPTREE::adjust_to_plane (0x00539bf0): clearTime/hitTime bounds (0.0/1.0),
  Phase 1 walks plane-touch times re-testing the whole tree (the tree test
  feeds a DIFFERENT blocking poly back into the next iteration), Phase 2
  binary-searches with the SHARED iteration counter, window < 0.02 =
  CONVERGED, final commit = last known-clear time. Only failure = Phase-1
  exhaustion.
- CPolygon::adjust_sphere_to_poly (0x00538170): early-out = plane-band test
  at the START position (was: precise-poly test at the check position);
  touch side = sign(dpPos)*radius (was: hard-coded -radius; ACE misdecoded
  it as movement.LengthSquared() <= r^2); result unclamped per retail.

Replay pin Issue180CorridorSweepHysteresisReplayTests: short-of-touch
targets pass, past-touch targets always clip, and the clipped stop is the
CONSTANT surface-contact point (eyeBack 1.609 across the band; spread
< 0.03m) instead of tracking the target.

Suites green (Core 2600+2skip / App 729+2skip / UI 425 / Net 385).
Pending: visual-loop re-verify + the user gate (#180 + #176 re-gate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-06 17:20:46 +02:00
parent 48aaab811c
commit f10fe4e96c
3 changed files with 348 additions and 52 deletions

View file

@ -0,0 +1,115 @@
# `BSPTREE::adjust_to_plane` + `CPolygon::adjust_sphere_to_poly` — pseudocode (#180 sawtooth fix)
Source: `docs/research/named-retail/acclient_2013_pseudo_c.txt`. Read 2026-07-06
while root-causing the #180 residual (the camera-sweep sawtooth: PathClipped
stops landing a whole transition step short of the wall contact).
| Function | VA | pseudo-C line |
|---|---|---|
| `BSPTREE::adjust_to_plane` | `0x00539bf0` | 323440 |
| `BSPTREE::collide_with_pt` (caller) | `0x0053a090` | 323615 |
| `CPolygon::adjust_sphere_to_poly` | `0x00538170` | 321999 |
## ⚠️ ACE'S PORT OF BOTH FUNCTIONS IS WRONG — do not re-import it
ACE `BSPTree.cs:43-85 adjust_to_plane` + `Polygon.cs:99-114
adjust_sphere_to_poly` decode the retail x87 control flow incorrectly, and the
result is a function that **always returns false** (trace it: `touchTime==1`
re-places the sphere at t=1 — the unchanged check position — and re-tests; any
`touchTime<1` iterates 15 times doing nothing; the `<0.02` convergence exit
returns false — inverted). ACE never noticed because `PERFECT_CLIP` (0x40) is
a viewer/camera flag the SERVER never meaningfully exercises. acdream's
pre-fix `BSPQuery.AdjustToPlane`/`AdjustSphereToPoly` copied ACE's shape and
inherited the dead machinery — the memory lesson
`feedback_bn_decomp_field_names` class 3 (ACE mush-decode wrong in branches
ACE never runs; the retail binary outranks it).
**Consequence (the #180 sawtooth / the ORIGINAL #176 strobe):** with
`AdjustToPlane` always failing, `collide_with_pt`'s PerfectClip branch always
fell to the bare `Collided` return → the transition reverted the colliding
step to the PREVIOUS step boundary. A PathClipped camera sweep therefore
stopped a whole sub-step (≤ 1 sphere radius, ~0.250.3 m) short of the actual
contact — quantized to step boundaries. The pre-fix full-boom camera flipped
between 1-step and 2-step backoffs on mm input drift (the measured
`pulledIn` 0.27 ↔ 0.53 = one step vs two steps of the ~1.6 m/6-step sweep);
the stateful-sought camera turned it into a re-extend/clip-0.27 m sawtooth at
~19 Hz. Same root: the stop never resolved to the contact point.
## `CPolygon::adjust_sphere_to_poly` — 0x00538170
Returns the parametric time along `movement` at which the SPHERE SURFACE
touches the polygon's plane, approaching from the side the start position is
on. (The retail tail is x87 compare-bit mush; the reconstruction below is the
only shape consistent with the visible operand loads + the caller's use.)
```
adjust_sphere_to_poly(this, CSphere* sphere, Vector3 curPos, Vector3 movement) -> float:
dpPos = dot(plane.N, curPos) + plane.d // plane distance of the START (0x0053818d)
if |dpPos| < sphere.radius: return 1.0 // start already within one radius of the
// plane — no forward touch time (0x005381a4)
dpMove = dot(plane.N, movement) // (0x005381ca)
if |dpMove| <= 2e-4: return 0.0 // moving parallel to the plane (0x005381d2)
r = (dpPos < 0) ? -radius : +radius // touch on the start's side (0x005381ea)
return (r - dpPos) / dpMove // UNCLAMPED — retail has no [0,1] clamp
```
Divergences fixed in acdream's port: the early-out was a precise-poly overlap
test at the CHECK position (wrong test, wrong endpoint); the touch side was
hard-coded `-radius`; the result was clamped to [0,1]. ACE's version keys the
± sign on `movement.LengthSquared() <= r²` — a misdecode of the `dpPos < 0`
compare.
## `BSPTREE::adjust_to_plane` — 0x00539bf0
Finds the closest non-penetrating position along `[curPos → checkPos]` and
commits it into the sphere. Two known-time bounds: `clearTime` (var_50/var_4c,
init 0.0 — the start is clear) and `hitTime` (var_48/var_44, init **1.0**
the check position hit). ONE iteration counter shared by both phases.
```
adjust_to_plane(this, CSphere* sphere /*in: at checkPos; out: adjusted*/,
Vector3 curPos, CPolygon*& hitPoly, Vector3* contactPt) -> success:
movement = sphere.center - curPos // (0x00539c16..0x00539c3c)
clearTime = 0.0 ; hitTime = 1.0 ; i = 0
// Phase 1 (0x00539c4d): walk plane-touch times.
loop:
t = hitPoly->adjust_sphere_to_poly(sphere, curPos, movement)
if t == 1.0: break // start within radius → pure [0,1] search
sphere.center = curPos + movement * t // (0x00539ca8-0x00539cb9)
if !root->sphere_intersects_poly(sphere, movement, &hitPoly, contactPt):
clearTime = t // touch position clear (0x00539d02)
break
// still blocked — by a possibly DIFFERENT poly (the tree test wrote hitPoly)
i += 1 ; hitTime = t // (0x00539cdd-0x00539ce5)
if i >= 15: return FAIL // the only failure exit (0x00539ce9)
// Phase 2 (0x00539ddb): binary-search [clearTime, hitTime]; i continues.
do:
avg = (clearTime + hitTime) * 0.5
sphere.center = curPos + movement * avg
if !sphere_intersects_poly(...): clearTime = avg
else: hitTime = avg
if hitTime - clearTime < 0.02: break // CONVERGED (0x00539dca) NOT a failure
i += 1
while i < 15
sphere.center = curPos + movement * clearTime // commit the last CLEAR time (0x00539de1)
return SUCCESS // Phase 2 always succeeds (even unconverged)
```
Caller (`collide_with_pt` 0x0053a090, PERFECT_CLIP branch): on success —
`set_collision_normal(globalized hitPoly.plane)`, `add_offset_to_check_pos(
globalize(adjusted original) · scale)`, return **Adjusted(3)**; on the rare
Phase-1 failure — return **Collided(2)** with NO normal write. acdream's
`CollideWithPt` already had this caller shape correct; only the two functions
above were broken.
## Verification
Headless replay `Issue180CorridorSweepHysteresisReplayTests` (real Facility
Hub cell 0x8A020164 BSP, the exact captured ray): pre-fix, targets embedded up
to 0.25 m into the wall behind the camera passed unclipped and the first
detection (s=1.625) stopped a full step back (eyeBack 1.354, tracking the
target); post-fix the stop must sit at the surface-contact point (eyeBack ≈
constant ≈ 1.61) for every target past first touch.