Gait: turn-then-walk stutter ROOT-CAUSED + FIXED -- one skeleton writer (task #64)
User repro (100%): turn in place, push forward before the turn stops -> the gait
skips/stutters + visibly reduced bob; standstill starts always clean. Three-layer
fix, user-verified:
1) THE DISPLAY BUG (the actual visible artifact): v5's "body advances first with
mj=1, leg overwrites last, so body drift can't show" was FALSE. Whenever the two
gait channels phase-split, the BODY channel's out-of-phase joint writes leaked
into the rendered skeleton (rhythmic leg skips, averaged-down bob) while every
leg-channel trace read clean -- the leg DATA was fine, the RENDERED pose wasn't
the leg's. v6: AdvanceBodyAnimation(dt, mj=0) -- the body still advances +
projects for replication (records/cycle speeds unchanged) but no longer touches
the skeleton. One writer, structurally; matches the binary's own observable
("body phase drift is locally INVISIBLE in the binary"). BT_BODY_MJ=1 = old A/B.
2) THE SPLIT SEED: the bring-up trn trigger armed only the LEG channel, so a
turn-in-place entry guaranteed the channels re-entered walk frames apart and the
walk cycles ran permanently out of phase. Lockstep: the Standing trn entry arms
BOTH channels the same frame (body case-4 twin added, same rate/keying), gated on
both Standing. The authentic dispatcher (un-decompiled master-perf gap 0x4a9b5c-
0x4ab188, the sole reader of turnDemand/turnCapable) armed both -- the body's
case-4 machinery is dead code otherwise [T1].
3) THE POSE-MATCH INVARIANT: the engine has NO pose blending; transitions avoid
pops purely by authored pose-matched boundaries. The old trn exits cut the pivot
clip MID-STEP (legFrm 7->1 teleport). Now: entry gated on near-zero speed
(turn-IN-place); on forward command the pivot FAST-FORWARDS to completion (x4)
and the authentic finish callback lands Standing at the stand pose -> the normal
pose-matched stand->walk runs. Decompiled reverse abort kept; the standSpeed
mid-clip abort subsumed (it WAS the pose cut). [T3: 0.25*standSpeed threshold +
4x rate stand in for the gap's constants.]
Harness: BT_FORCE_TURN now reaches gBTDrive.turn (was silently inert for the
gait), BT_WALK_DELAY=<s> holds forced throttle then ramps (the turn-first repro),
BT_GAIT_TRACE=1 per-frame gait trace. Regressions: standstill start, turn-entry,
pure pivot loops, run cycle -- all clean, bob full amplitude (1.33/1.32).
KB: locomotion.md v6 section + trn reconstruction + symptom-family closure.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c0a0ec5e69
commit
bfdd41bb9d
+42
-11
@@ -77,7 +77,8 @@ node) — which is why the bug was intermittent and direction-dependent. FIX: a
|
||||
Advance*Animation Standing cases zero their channel's cycle on entry + ForceUpdate(8) so the
|
||||
peer hears the stop immediately. (The signed-demand change (task #15) is what let negative
|
||||
cycles reach machinery written for forward decel.) The earlier "bob reduction + stutter"
|
||||
symptom family likely shares this root — re-observe after this fix before chasing separately.
|
||||
symptom family did NOT share this root — it was the body channel's mj=1 joint writes leaking
|
||||
through on channel phase-splits, closed by v6 (task #64, see the two-channel section).
|
||||
|
||||
## The two-channel gait (real controls)
|
||||
The BT `SequenceController` (`seqctl.cpp`, from `SelectSequence@004277a8`+`Advance@0042790c`) drives
|
||||
@@ -93,20 +94,50 @@ raw disasm** (both Advance fns had NO static decomp caller; found by byte-scanni
|
||||
locally INVISIBLE in the binary. The literal projectedVelocity store is unwired, but the
|
||||
update-record velocity feed is live via the localVelocity/worldLinearVelocity maintenance
|
||||
(mech4.cpp VELOCITY STORAGE block) — replicant gait (task #50) runs off it. [T1]
|
||||
**v5 (current): display == travel BY CONSTRUCTION through the LEG** (body advances first, leg last).
|
||||
History: v3 (travel=leg, display=body) foot-slipped — two channels, different consumers; v4 unified
|
||||
BOTH on the body (mis-reading 0x4ab430 as the travel source) — feet planted, but whenever the two
|
||||
state machines phase-split, the out-of-phase LEG pose showed through on frames the body didn't write
|
||||
(Standing/wind-down) = the RECURRING "legs stutter / lose sync" report. The split mechanism [T2,
|
||||
reproduced via `BT_FORCE_OSC` + `BT_FORCE_TURN` + `BT_SYNC_LOG`]: (a) SEED — the bring-up trn trigger
|
||||
arms only the leg (authentic trn dispatcher still unlocated), so the walk re-entry lands 1 frame
|
||||
apart; (b) AMPLIFIER — with offset clips the end-of-clip callbacks fire on different frames, so a
|
||||
demand change landing between them picks DIFFERENT next states (one winds down, one keeps walking)
|
||||
→ opposite-phase churn. Under v5 both effects still exist but only skew the projection channel. [T2]
|
||||
**v6 (current, task #64 2026-07-13): the body channel does NOT write joints — `AdvanceBodyAnimation(dt, mj=0)`.**
|
||||
Display == travel == the LEG channel STRUCTURALLY (one skeleton writer), matching the binary's own
|
||||
observable ("the body channel's phase drift is locally INVISIBLE in the binary"). History: v3
|
||||
(travel=leg, display=body) foot-slipped; v4 unified BOTH on the body — the out-of-phase LEG pose
|
||||
showed through on frames the body didn't write = the recurring stutter; **v5 (body-first/leg-last,
|
||||
believing the leg's overwrite hid body drift) was WRONG the same way MIRRORED** — user-verified
|
||||
2026-07-13: whenever the channels phase-split, the BODY's out-of-phase mj=1 writes leaked into the
|
||||
displayed skeleton (rhythmic leg skips + visibly REDUCED BOB) while every leg-channel trace read
|
||||
clean — the leg DATA was fine; the RENDERED pose wasn't the leg's. The turn-then-walk repro (below)
|
||||
made it 100% reproducible. `BT_BODY_MJ=1` restores the double-writer for A/B. The old split
|
||||
mechanism [T2]: (a) SEED — the trn trigger armed only the leg → walk re-entry frames apart;
|
||||
(b) AMPLIFIER — offset clips → end-of-clip callbacks on different frames → divergent transitions.
|
||||
Both are ALSO mitigated at the source now (the lockstep trn arming below), but mj=0 is what makes any
|
||||
residual split invisible, structurally. ⚠ The "bob reduction + stutter" symptom family (previously
|
||||
parked as "likely shares the reverse-stop root") is THIS — CLOSED. [T2 user-verified]
|
||||
The gait STATE MACHINE (`AdvanceBodyAnimation`, mech2.cpp) slews `bodyCycleSpeed` toward the demand
|
||||
through walk/run caps (`LoadLocomotionClips`, mech3.cpp:326 — measures `walkStride`/`standSpeed` via
|
||||
`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 `0x4a9b5c–0x4ab188` (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]
|
||||
⚠ **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.
|
||||
|
||||
Reference in New Issue
Block a user