Gait v5: display+travel from the LEG channel (kills the stutter class)
User-reported recurrence of "legs stutter and lose sync". Live [sync] showed
advSum/legSum drifting 5->92 during walk-up churn, then locking. Reproduced
headlessly (BT_FORCE_OSC throttle feathering + BT_FORCE_TURN + the new
per-frame BT_SYNC_LOG probe) and root-caused in two parts:
- SEED: the bring-up turn-in-place trigger arms only the LEG channel, so the
walk re-entry lands one frame apart -> a persistent clip phase offset.
- AMPLIFIER: with offset clips the two end-of-clip callbacks fire on different
frames; a demand change landing between them picks DIFFERENT next states
(one channel winds down, the other keeps walking) -> opposite-phase churn
(observed bs=6 ls=7 -- body on walk-R, leg on walk-L).
Under v4 (display+travel = BODY) the out-of-phase LEG pose showed through on
every frame the body didn't write joints (Standing/wind-down) = the visible
stutter/pop.
DISASM GROUND TRUTH (neither Advance fn has a static decomp caller; found by
byte-scanning the CODE section for e8 calls):
- master perf (0x4a9b5c gap) -> AdvanceLegAnimation @0x4aa399 (air @0x4aa388)
-> -dist/dt into localVelocity (+0x1cc): the LEG drives LOCAL travel and
(writing last) the displayed pose;
- IntegrateMotion (0x4ab1c8, body advance @0x4ab312, caller FUN_004ab430 =
the projected-origin updater) -> -dist/dt into projectedVelocity (+0x2a0):
the BODY is the dead-reckoning PROJECTOR -- locally invisible.
v4's reading of FUN_004ab430 as the travel source was wrong.
FIX: advance body FIRST (projection; its writes get overwritten), leg LAST
(displayed pose); travel = legAdv under the two-channel split. Display ==
travel through the LEG by construction -- body-channel drift can no longer
become visible, structurally (not by input-symmetry luck).
Verified: trn+feathering repro clean; straight drive locked (adv==proj);
solo goto=enemy kill chain intact; MP 2-node drive-to-range cross-pod kill
intact (241 hits, DESTROYED); no crashes. KB corrected (locomotion.md roles
+ P3_LOCOMOTION.md v5 entry). New gated instruments: BT_SYNC_LOG,
BT_FORCE_OSC.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
9353d59eb9
commit
d4e139a0d9
@@ -1819,23 +1819,56 @@ void
|
||||
}
|
||||
if (s_realControls)
|
||||
{
|
||||
// FOOT-PLANT FIDELITY (raw FUN_004ab430:15076 -> FUN_004ab1c8):
|
||||
// the binary advances the BODY controller with move_joints=1 --
|
||||
// the DISPLAYED pose and the travel distance come from the SAME
|
||||
// Advance (same clip, same phase), so feet plant by construction.
|
||||
// The leg channel (local-sim, hardcoded move_joints=1 in the raw,
|
||||
// FUN_004a5028:12006) runs FIRST; the body's writes land after and
|
||||
// win the frame, exactly the binary's order. The earlier split
|
||||
// (body silenced with move_joints=0, leg pose displayed) showed
|
||||
// leg-channel joints against body-channel travel -- two
|
||||
// independently-phased state machines -> visible foot glide.
|
||||
legAdv = AdvanceLegAnimation(dt); // channel A: local sim (live demand)
|
||||
adv = AdvanceBodyAnimation(dt, 1); // channel B: displayed pose + motion
|
||||
// CHANNEL ROLES (task #49, disasm-corrected [T1]): in the binary
|
||||
// - the MASTER PERF (0x4a9b5c region) advances the LEG channel
|
||||
// (call @0x4aa399, airborne @0x4aa388) and stores -dist/dt into
|
||||
// localVelocity (+0x1cc) -- the LEG drives the LOCAL mech's
|
||||
// travel AND (writing last, mj=1) the displayed pose;
|
||||
// - IntegrateMotion (0x4ab1c8, body advance @0x4ab312) stores
|
||||
// -dist/dt into the PROJECTED velocity (+0x2a0) -- the BODY
|
||||
// channel is the dead-reckoning/replication PROJECTOR, whose
|
||||
// phase drift is locally INVISIBLE.
|
||||
// The previous arrangement here (v4: display+travel from the BODY)
|
||||
// had the roles swapped: whenever the two state machines phase-split
|
||||
// (the leg-only trn pivot seed, or a demand change landing between
|
||||
// the channels' end-of-clip callbacks -> divergent transitions), the
|
||||
// out-of-phase LEG pose showed through on every frame the body
|
||||
// didn't write (Standing/wind-down) = the visible leg stutter.
|
||||
// Body first (projection; writes get overwritten), leg LAST (the
|
||||
// displayed pose): display == travel through the LEG channel, by
|
||||
// construction -- body-channel drift can no longer show.
|
||||
adv = AdvanceBodyAnimation(dt, 1); // channel B: replication projection
|
||||
legAdv = AdvanceLegAnimation(dt); // channel A: local sim -- pose + travel
|
||||
}
|
||||
else
|
||||
{
|
||||
adv = AdvanceBodyAnimation(dt, 1); // single-channel: body does both
|
||||
}
|
||||
// [syncF] per-frame divergence probe (task #49): log every frame
|
||||
// where the two channels return different distances or either
|
||||
// state machine changed state -- pinpoints WHICH frames the
|
||||
// advSum/legSum drift and the visible pose pops come from.
|
||||
if (getenv("BT_SYNC_LOG"))
|
||||
{
|
||||
static int s_lastBS = -1, s_lastLS = -1;
|
||||
const int bs = (int)bodyStateAlarm.GetLevel();
|
||||
const int ls = (int)legStateAlarm.GetLevel();
|
||||
const float dD = (float)(adv - legAdv);
|
||||
if (bs != s_lastBS || ls != s_lastLS
|
||||
|| dD > 0.001f || dD < -0.001f)
|
||||
{
|
||||
DEBUG_STREAM << "[syncF] dt=" << dt
|
||||
<< " bs=" << bs << " ls=" << ls
|
||||
<< " adv=" << adv << " legAdv=" << legAdv
|
||||
<< " bCyc=" << bodyCycleSpeed << " lCyc=" << legCycleSpeed
|
||||
<< " bFrm=" << bodyAnimation.currentFrame
|
||||
<< " bT=" << bodyAnimation.currentTime
|
||||
<< " lFrm=" << legAnimation.currentFrame
|
||||
<< " lT=" << legAnimation.currentTime
|
||||
<< "\n" << std::flush;
|
||||
s_lastBS = bs; s_lastLS = ls;
|
||||
}
|
||||
}
|
||||
// COCKPIT BOB (task #15): the leg channel writes the clip's vertical
|
||||
// root motion into jointlocal (balltranslate) every frame, but the
|
||||
// camera's DCS chain doesn't consume animated joints -- publish the
|
||||
@@ -1924,25 +1957,23 @@ void
|
||||
// accumulated distance over the elapsed time since the last
|
||||
// keyframe contribution, HELD across zero-distance frames, hard
|
||||
// zero when Standing (and after 0.3s of silence, e.g. paused clip).
|
||||
// FOOT-PLANT BY CONSTRUCTION (v4 -- BINARY-FAITHFUL: travel = the
|
||||
// BODY channel, the SAME Advance that writes the displayed joints;
|
||||
// raw FUN_004ab430:15076 advances the body with move_joints=1 and
|
||||
// takes the travel from it). v3 sourced travel from the LEG channel
|
||||
// while the BODY drew the joints -- TWO state machines that could
|
||||
// (and did) drift apart whenever any event touched one channel's
|
||||
// state without the other: the knockdown (fixed by staggering both),
|
||||
// then AGAIN on the Mad Cat / analog-lever sweeps crossing gait
|
||||
// boundaries (each channel's end-of-clip callback samples the demand
|
||||
// at a different instant, so near a walk/run threshold they can pick
|
||||
// DIFFERENT next states -> permanent phase split = the foot-slip).
|
||||
// Sourcing travel from the displayed channel kills the whole desync
|
||||
// CLASS: display == travel by construction. The earlier "tunnels
|
||||
// through obstacles" objection to this is OBSOLETE -- the knockdown
|
||||
// now staggers the BODY channel too (SetBodyAnimation(0x20)), so a
|
||||
// hard impact freezes travel exactly as before (re-verified). The
|
||||
// leg channel keeps running as the local sim it is in the binary
|
||||
// (its joint writes land first and are overwritten by the body's).
|
||||
const Scalar localAdv = adv * dir;
|
||||
// FOOT-PLANT BY CONSTRUCTION (v5 -- task #49, disasm-corrected):
|
||||
// display AND travel BOTH come from the LEG channel (the binary's
|
||||
// master perf advances the leg and writes -dist/dt into
|
||||
// localVelocity @+0x1cc; the leg writes the pose last). The v4
|
||||
// reading ("raw FUN_004ab430:15076 takes the travel from the body
|
||||
// advance") was WRONG -- 0x4ab312's IntegrateMotion stores its
|
||||
// -dist/dt into the PROJECTED velocity (+0x2a0), the dead-reckoning
|
||||
// feed, not local travel. v3 (travel=leg, display=body) foot-slipped
|
||||
// because display and travel were DIFFERENT channels; v4 unified on
|
||||
// the body, which planted feet but let the out-of-phase LEG pose show
|
||||
// through wherever the body didn't write (the visible stutter). v5
|
||||
// unifies on the leg exactly as the binary does: display == travel by
|
||||
// construction, live-demand channel authoritative, body drift local-
|
||||
// invisible. (Knockdown still staggers BOTH channels, so a hard
|
||||
// impact freezes travel as before.)
|
||||
const Scalar travelAdv = s_realControls ? legAdv : adv;
|
||||
const Scalar localAdv = travelAdv * dir;
|
||||
linearSpeed = (localAdv < 0.0f ? -localAdv : localAdv) * invDt; // forward ground speed -> LinearSpeed gauge
|
||||
Vector3D localVel(0.0f, 0.0f, -localAdv * invDt); // exact frame distance as velocity
|
||||
Matrix34 orient; // rotation from the heading (set @ line ~626)
|
||||
@@ -1957,7 +1988,8 @@ void
|
||||
if (gBodyAnimLog >= 1.0f)
|
||||
{
|
||||
gBodyAnimLog = 0.0f;
|
||||
DEBUG_STREAM << "[gait] adv=" << adv << " pos=("
|
||||
DEBUG_STREAM << "[gait] adv=" << travelAdv << " (proj=" << adv
|
||||
<< ") pos=("
|
||||
<< localOrigin.linearPosition.x << ", "
|
||||
<< localOrigin.linearPosition.z << ")\n" << std::flush;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user