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:
parent
48aaab811c
commit
f10fe4e96c
3 changed files with 348 additions and 52 deletions
115
docs/research/2026-07-06-adjust-to-plane-pseudocode.md
Normal file
115
docs/research/2026-07-06-adjust-to-plane-pseudocode.md
Normal 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.25–0.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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue