acdream/docs/research/2026-07-06-adjust-to-plane-pseudocode.md
Erik f10fe4e96c 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>
2026-07-06 17:20:46 +02:00

115 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# `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.