Locomotion: replace task-#64 trn stand-ins with the AUTHENTIC dispatcher (disasm-decoded)

The turn-in-place dispatcher lives in the master-perf gap 0x4a9b5c-0x4ab188 that
Ghidra never lifted. Disassembled it with objdump (scratchpad/masterperf.asm) and
hand-decoded the real logic, replacing the two [T3] stand-ins I had invented:

- Entry gate was `commandedSpeed < 0.25*standSpeed` (a near-zero guess). AUTHENTIC
  (0x4aa505): arm trn when TURNING (|angVel|>1e-4, port proxy = turnDemand deadband)
  AND speed in the FULL [0, standSpeed] sub-walk range AND turnCapable. trn is gated
  on TURNING, not on being slow.
- Case-4 exit had a ×4 fast-forward + SetLegAnimation(5) invention. Restored the
  VERBATIM decompiled leg-SM exits (part_012.c:12013): standSpeed<spd or reverse ->
  Standing, else advance the pivot. Added the master-perf turn-STOP exit (0x4aa5c6):
  turn stopped -> Standing + legResetLatch=1 (folded into case 4 since the port has
  no separate master-perf frame).
- Body case-4 twin mirrors the authentic exits (mj=0, tracks state for lockstep).

The fast-forward was invented to dodge a turn->walk stutter that was ACTUALLY the
body channel leaking joints (mj=1) -- already root-caused + fixed by mj=0. The
trn->walk seam (legFrm7 cycle-end -> swr legFrm1) is the authentic pose-matched
boundary and the same seam the fast-forward version hit, just at the correct release
speed. Also decoded (documented, not yet wired) the authentic turn-RATE lerp
(walkingTurnRate@0x574 -> runningTurnRate@0x578 by speed; angVel = turnDemand*rate);
the bring-up drive still yaws at the constant kDriveTurnRate.

Headless-verified (BT_AUTODRIVE/BT_FORCE_TURN/BT_WALK_DELAY/BT_GAIT_TRACE): turn-in-
place enters state 4, ramp past standSpeed -> 4->stand->swr->walk, both channels in
lockstep. Feel re-verification pending. [T1 logic / T2 runtime]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-14 00:31:56 -05:00
co-authored by Claude Fable 5
parent bfdd41bb9d
commit 35c30bda67
2 changed files with 107 additions and 84 deletions
+44 -24
View File
@@ -114,30 +114,50 @@ through walk/run caps (`LoadLocomotionClips`, mech3.cpp:326 — measures `walkSt
`legAnimation.SelectSequence`). Transition callbacks (`Mech::BodyClipFinished @0x4a6d8c`,
`LegClipFinished @0x4a6928` — PE-parsed jump tables) alternate walk 6↔7 / run 12↔13 + walk→run. [T2]
## Turn-in-place (trn, state 4) — reconstructed (task #64, 2026-07-13)
**The authentic entry dispatcher is NOT in the decomp** — it lives in the un-decompiled master-perf
gap `0x4a9b5c0x4ab188` (functions_index.tsv jumps FUN_004a9770→FUN_004ab188; that gap is the SOLE
reader of `turnDemand`@mapper+0x12c and `turnCapable`@0x588 — both grep-empty elsewhere) [T1 proof of
absence]. The decompiled lifecycle: `animationClips[4]` = the `trn`/`trni` clip (loaders 13767/13971);
leg case-4 advance/exits @part_012.c:12013 (exit at `standSpeed<spd` or reverse → Standing);
clip-finish case 4 → Standing (one-shot pivot step); the BODY channel has identical case-4 machinery
(dead unless armed — proof the dispatcher armed BOTH). Port reconstruction (mech2.cpp):
- **THE POSE-MATCH INVARIANT (the key insight):** this engine has NO pose blending — `SelectSequence`
hard-resets to frame 0 — and avoids pops purely by AUTHORING (every transition clip starts in the
pose its predecessor ends in; swr starts from stand). Cutting the pivot clip MID-STEP (any exit
while `legFrm` is mid-clip) teleports the legs = the 100%-repro turn-then-walk stutter. NEVER cut a
clip mid-step; route through its end pose.
- **Entry** (leg Standing case): near-zero speed gate (`spd < 0.25·standSpeed` [T3 stand-in for the
gap's constant]) + turnDemand deadband ±0.05 [T3] + `turnCapable` + **LOCKSTEP: arms BOTH channels
the same frame** (SetLegAnimation(4) + SetBodyAnimation(4), gated on body also Standing) — arming
only the leg is what seeded the channel phase-split.
- **Release** (both case-4s): when `spd > 0.25·standSpeed`, the pivot is FAST-FORWARDED to completion
(advance rate ×4 [T3]) — feet hurry down to the plant, the authentic finish callback drops to
Standing AT the stand pose, then the normal pose-matched stand→walk runs. Decompiled reverse abort
kept; the decompiled `standSpeed` mid-clip abort deliberately subsumed (it WAS the pose cut). [T3]
- Verified: seam trace `frm 15→17→18→19 → stand(frm holds 19) → swr frm1` — no mid-clip reset; both
channels enter walk the same frame. Harness: `BT_FORCE_TURN` (now reaches gBTDrive.turn),
`BT_WALK_DELAY=<s>` (turn-first-then-ramp repro), `BT_GAIT_TRACE=1` (per-frame gait trace). [T2]
## Turn-in-place (trn, state 4) — AUTHENTIC dispatcher decoded (task #64, 2026-07-14)
**The entry dispatcher is NOT in the Ghidra decomp** — it lives in the master-perf gap
`0x4a9b5c0x4ab188` (functions_index.tsv jumps FUN_004a9770→FUN_004ab188; the SOLE reader of
`turnDemand`@mapper+0x12c and `turnCapable`@0x588) [T1 proof of absence]. **Ghidra never lifted it, so
it was disassembled with objdump** (`scratchpad/masterperf.asm`, 1530 lines) and hand-decoded — this
REPLACES the earlier [T3] stand-in guesses (`0.25·standSpeed` entry gate + `×4` fast-forward) with the
real logic. `animationClips[4]` = the `trn`/`trni` clip (loaders 13767/13971). Port recon (mech2.cpp):
- **THE POSE-MATCH INVARIANT:** this engine has NO pose blending — `SelectSequence` hard-resets to
frame 0 — and avoids pops purely by AUTHORING (every transition clip starts in its predecessor's end
pose; a turn-in-place CYCLE returns to the neutral/stand pose at its last frame). The trn→walk seam
therefore steps `trn legFrm7 (cycle end) → swr legFrm1` — a small ONE-TIME bob step that is authentic
(the binary cuts on speed, not at an arbitrary frame). **What made it STUTTER was NOT this cut** — it
was the body channel leaking out-of-phase joints into the rendered skeleton every walk cycle (mj=1);
fixed at the source by `AdvanceBodyAnimation(dt, mj=0)` (see two-channel section). With mj=0 the
authentic cut renders cleanly.
- **Turn rate (disasm 0x4aa3d30x4aa4ff) [T1]:** `spd = |linearSpeed|`; base `turnRate =
walkingTurnRate`(mech+0x574, rad/s). If `spd ≥ reverseSpeedMax`(0x538): `t = (spd walkStride
0x534)/(topSpeed 0x34c walkStride)`; `turnRate = t≤1 ? lerp(walkingTurnRate, runningTurnRate
0x578, t) : runningTurnRate/t²` (over-run falloff). `turnRate = max(0, turnRate)`. Then
`angularVelocity`(0x1d4) `= turnDemand(mapper+0x12c) × turnRate`, ZEROED if legState∈{1,2,3} or
latch 0x650 set. ⚠ **Not yet wired into the port** — the bring-up drive still yaws at the constant
`kDriveTurnRate` (mech4.cpp:2618/2628/3062); `walkingTurnRate`/`runningTurnRate` are stored
(mech.cpp:1334-5, Wword(0x15d)/(0x15e)→0x574/0x578, deg→rad) but only the ANIMATION dispatcher is
wired. Wiring the rate lerp is the next locomotion polish item.
- **Entry** (leg Standing case, disasm 0x4aa505 [T1]): arm trn when TURNING (`|angVel|>1e-4`, port
proxy = turnDemand deadband ±0.05) AND `0 ≤ spd ≤ standSpeed` (the FULL sub-walk range — CORRECTS the
`0.25·standSpeed` stand-in) AND `turnCapable`(hasCrashSet@0x588). **LOCKSTEP: arms BOTH channels the
same frame** (SetLegAnimation(4)+SetBodyAnimation(4), gated on body also Standing) — arming only the
leg seeded the channel phase-split. The disasm also gates on `legResetLatch`(0x654), a master-perf
ONE-FRAME debounce (cleared every frame @0x4a9bff, set on wind-down/turn-stop) — **omitted in the
port** because its split leg-SM never per-frame-clears the latch, so gating on it would wrongly block
trn after every walk.
- **Exits:** leg SM (part_012.c:12013 [T1], restored VERBATIM) — `standSpeed<spd` → Standing (walk
takes over) or `spd<ZeroSpeed` → Standing (reverse), else advance the pivot at `idleStrideScale`
(goto advance_normally). PLUS the master-perf turn-STOP exit (disasm 0x4aa5c6 [T1]) folded into
case 4: turn stopped (turnDemand back in deadband) → Standing + `legResetLatch=1`. No fast-forward,
no 0.25× — both were stand-ins, now removed. Body case-4 twin mirrors the exits (does NOT set the
latch; mj=0, so it only tracks state for lockstep).
- Verified headless (DEV.EGG, `BT_AUTODRIVE=0.5 BT_FORCE_TURN=1 BT_WALK_DELAY=2 BT_GAIT_TRACE=1`):
turn-in-place enters state 4; throttle ramp past standSpeed → 4→Standing→5(swr)→walk cycle;
legFrm7(trn end)→legFrm1(swr) at the pose-matched boundary; both channels enter walk the same frame.
Harness: `BT_FORCE_TURN` (reaches gBTDrive.turn), `BT_WALK_DELAY=<s>` (turn-then-ramp repro),
`BT_GAIT_TRACE=1` (per-frame [gaitSM]). [T2] **Feel re-verification pending** (release now at
standSpeed vs the invented 0.25×). [T1 logic / T2 runtime]
⚠ **Aliasing bug class (namedClip):** `namedClip[]`@0x5e0 == `animationClips[]`@0x5cc+0x14 (ONE array);
`gimpBaseClip`@0x64c == `animationClips[0x20]`; `bodyAnimationState`@0x728 == `bodyStateAlarm.level`.
Declared-separate members don't see each other's writes — alias them.