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
@@ -595,18 +595,39 @@ Scalar
|
||||
else
|
||||
{
|
||||
distance = 0.0f;
|
||||
// TURN-IN-PLACE entry (bring-up TRIGGER, real machinery): the trn
|
||||
// clip/state-4 advance/exit are binary-verified, but the authentic
|
||||
// entry DISPATCHER isn't yet located in the decomp -- enter from
|
||||
// Standing on a live turn demand with no speed demand. The clip's
|
||||
// finished-callback drops back to stand; while the demand persists
|
||||
// this trigger re-arms it (a looping pivot step).
|
||||
// TURN-IN-PLACE entry. The authentic entry DISPATCHER lives in the
|
||||
// un-decompiled master-perf gap (0x4a9b5c-0x4ab188), the sole reader of
|
||||
// turnDemand(+0x12c) + turnCapable(+0x588); the decompiled lifecycle is
|
||||
// case-4 advance/exit (below) + the clip-finish drop-to-stand. The
|
||||
// authentic gate is turn-IN-PLACE == NEAR-ZERO commanded speed. The old
|
||||
// stand-in gated on `ZeroSpeed <= commandedSpeed` -- i.e. it re-armed trn
|
||||
// for ANY forward speed in [0, standSpeed~6.83], so pushing forward mid-
|
||||
// turn SWALLOWED the throttle (the mech kept pivoting) until spd crossed
|
||||
// standSpeed, then lurched to walk (task #64 user-reported stutter). Gate
|
||||
// on near-zero speed instead: the instant real throttle is applied the trn
|
||||
// re-arm stops, the in-flight pivot finishes to Standing, and case 0 hands
|
||||
// off to walk normally. turnEntrySpeed = 0.25*standSpeed is a STAND-IN for
|
||||
// the gap's threshold constant [T3]; the deadband mirrors the replicant's.
|
||||
const Scalar turnEntrySpeed = standSpeed * 0.25f;
|
||||
if (hasCrashSet != 0 && mppr != 0
|
||||
&& ZeroSpeed <= commandedSpeed
|
||||
&& commandedSpeed >= ZeroSpeed && commandedSpeed < turnEntrySpeed
|
||||
&& (mppr->turnDemand > 0.05f
|
||||
|| mppr->turnDemand < -0.05f))
|
||||
|| mppr->turnDemand < -0.05f)
|
||||
&& bodyAnimationState == StandingAnimation) // weld: enter only when BOTH can
|
||||
{
|
||||
SetLegAnimation(4); // turn-in-place (trn)
|
||||
// LOCKSTEP (task #64): arm BOTH channels on the same frame. Arming
|
||||
// only the leg let the body enter walk on its own schedule ~7-20
|
||||
// frames apart -> the two walk cycles ran permanently out of phase
|
||||
// and the body's pose flashed through on leg clip-boundary frames
|
||||
// (the rhythmic gait skip + reduced bob). Both SequenceControllers
|
||||
// bind the same trn clip at the same frame + advance at the same
|
||||
// rate (case 4 twins), so they complete + re-enter walk together --
|
||||
// the same phase-weld a standstill start gets for free. This is the
|
||||
// workflow plan's "master perf arms both channels" reconstruction
|
||||
// (the authentic dispatcher in the un-decompiled 0x4a9b5c gap arms
|
||||
// both -- the body's case-4/finish machinery is dead code otherwise).
|
||||
SetLegAnimation(4); // turn-in-place (trn), channel A
|
||||
SetBodyAnimation(4); // channel B, same frame [lockstep]
|
||||
goto advance_normally;
|
||||
}
|
||||
if (ZeroSpeed <= commandedSpeed)
|
||||
@@ -642,26 +663,48 @@ Scalar
|
||||
break;
|
||||
|
||||
case 4: // TURN-IN-PLACE (trn clip)
|
||||
// BINARY-VERIFIED (raw FUN_004a5028 case 4, part_012.c:12013): state 4 is
|
||||
// the turn-in-place animation (animationClips[4] = the "trn" clip, loaded
|
||||
// under turnCapable@0x588). It EXITS to stand when a real speed is
|
||||
// commanded (standSpeed < demand -- the earlier draft had this comparison
|
||||
// INVERTED, the same bug class as the Standing case) or when reverse is
|
||||
// commanded; otherwise it ADVANCES the turn clip while the mech pivots.
|
||||
if (standSpeed < commandedSpeed)
|
||||
{
|
||||
legStateAlarm.SetLevel(0);
|
||||
ForceUpdate(8); // FUN_004a4c54(this,8): type-3 record
|
||||
break;
|
||||
}
|
||||
distance = 0.0f;
|
||||
// state 4 is the turn-in-place animation (animationClips[4] = the "trn" clip,
|
||||
// loaded under turnCapable@0x588). THE POSE-SNAP RULE (task #64, root-caused
|
||||
// from the user's BT_GAIT_TRACE): this engine has NO pose blending -- every
|
||||
// authored transition is pose-MATCHED at its boundary (swr starts from the
|
||||
// stand pose; the walk clips alternate at matched extremes). Any exit that
|
||||
// abandons the pivot MID-CLIP (SetLevel(0) or SetLegAnimation(5)) hard-resets
|
||||
// the next clip to frame 0 while the legs are mid-step -> the legs TELEPORT
|
||||
// (legFrm 7->1 in the trace) = the 100%-repro turn-then-walk stutter. Three
|
||||
// prior fixes all failed because each still cut the clip mid-step, just at a
|
||||
// different speed threshold.
|
||||
//
|
||||
// The reconstruction: when forward is commanded past the turn-in-place band,
|
||||
// FAST-FORWARD the pivot to completion (accelerated advance) -- the feet
|
||||
// hurry down to the plant, the clip's own finished-callback (case 4 ->
|
||||
// SetLevel(0), the DECOMPILED one-shot tail) drops to Standing AT THE STAND
|
||||
// POSE, and the normal pose-matched stand->walk path takes over. No mid-clip
|
||||
// cut anywhere. trn has zero root translation, so the fast-forward moves no
|
||||
// distance. [T3: the release threshold (0.25*standSpeed) + hurry rate (4x)
|
||||
// stand in for the un-decompiled master-perf dispatcher constants (gap
|
||||
// 0x4a9b5c); the completion->Standing tail itself is T1 decompiled.]
|
||||
//
|
||||
// The decompiled REVERSE abort is kept verbatim; the decompiled standSpeed
|
||||
// abort is deliberately SUBSUMED by the hurry (it was the mid-clip pose cut
|
||||
// itself -- any spd above the band fast-forwards to the plant instead, and
|
||||
// Standing picks up the walk one frame after the callback). [T3 deviation
|
||||
// from the literal case-4 exit, in service of the pose-match invariant.]
|
||||
if (commandedSpeed < ZeroSpeed)
|
||||
{
|
||||
legStateAlarm.SetLevel(0);
|
||||
ForceUpdate(8); // type-3 record
|
||||
distance = 0.0f;
|
||||
break;
|
||||
}
|
||||
goto advance_normally;
|
||||
{
|
||||
Scalar trnRate = 1.0f;
|
||||
if (standSpeed * 0.25f < commandedSpeed)
|
||||
trnRate = 4.0f; // forward commanded: hurry the plant [T3]
|
||||
distance = legAnimation.Advance( // same advance advance_normally does,
|
||||
time_slice * globalTimeScale * idleStrideScale * trnRate, 1); // rate-scaled
|
||||
legCycleSpeed = distance / time_slice; // trn z=0 -> stays 0 (no travel)
|
||||
}
|
||||
break;
|
||||
|
||||
case 6: case 7: // WalkToRun
|
||||
//
|
||||
@@ -869,7 +912,38 @@ Scalar
|
||||
}
|
||||
// FALLTHROUGH
|
||||
|
||||
case 2: case 3: case 4: case 5: case 8: case 9: case 10: case 0x0b:
|
||||
case 4: // TURN-IN-PLACE, LOCKSTEP twin (task #64)
|
||||
// The body channel runs trn in LOCKSTEP with the leg: armed together at
|
||||
// entry (leg Standing cross-arms both), advanced at the SAME rate keyed on
|
||||
// the SAME live speedDemand, so both clips complete on the same frame and
|
||||
// both channels re-enter walk on the same frame. Without this the body
|
||||
// entered walk ~7-20 frames apart from the leg and the two walk cycles ran
|
||||
// permanently out of phase -- the body's pose flashes through on the leg's
|
||||
// clip-boundary frames (body advances mj=1 FIRST, leg overwrites LAST;
|
||||
// boundary frames the leg doesn't write) = the rhythmic gait skip + halved
|
||||
// bob the user reproduced 100% from turn-then-walk (standstill starts weld
|
||||
// the phases, which is why they never stuttered). [T3 rates/threshold; the
|
||||
// per-channel case-4 machinery itself is T1 decompiled.]
|
||||
{
|
||||
MechControlsMapper *bm = MappingMapper();
|
||||
const Scalar bspd = (bm != 0) ? bm->speedDemand : 0.0f;
|
||||
if (bspd < ZeroSpeed) // reverse abort (leg-symmetric)
|
||||
{
|
||||
bodyStateAlarm.SetLevel(0);
|
||||
ForceUpdate(8);
|
||||
distance = 0.0f;
|
||||
break;
|
||||
}
|
||||
Scalar trnRate = 1.0f;
|
||||
if (standSpeed * 0.25f < bspd)
|
||||
trnRate = 4.0f; // hurry the plant (leg-symmetric)
|
||||
distance = bodyAnimation.Advance(
|
||||
time_slice * globalTimeScale * idleStrideScale * trnRate, loop);
|
||||
bodyCycleSpeed = distance / time_slice;
|
||||
}
|
||||
break;
|
||||
|
||||
case 2: case 3: case 5: case 8: case 9: case 10: case 0x0b:
|
||||
case 0x0e: case 0x0f: case 0x10: case 0x11: case 0x14: case 0x15:
|
||||
case 0x1c: case 0x1d: case 0x1e: case 0x1f: case 0x20:
|
||||
distance = bodyAnimation.Advance( // FUN_0042790c(this+0x6bc, ...)
|
||||
|
||||
@@ -2251,6 +2251,26 @@ void
|
||||
{
|
||||
gBTDrive.forced = 1;
|
||||
gBTDrive.forcedThrottle = sAutoDrive;
|
||||
// task #64 harness: BT_WALK_DELAY holds the FORCED throttle (the
|
||||
// mapper's actual input -- the local `throttle` gate below never
|
||||
// reached it) at 0 for n sim-secs (turn-in-place phase, with
|
||||
// BT_FORCE_TURN active), then ramps it over 1.2s -- the manual
|
||||
// "lever dwell through the walk threshold" repro.
|
||||
{
|
||||
static float s_wdD = -1.0f, s_wdC = 0.0f;
|
||||
if (s_wdD < -0.5f)
|
||||
{
|
||||
const char *v = getenv("BT_WALK_DELAY");
|
||||
s_wdD = v ? (float)atof(v) : 0.0f;
|
||||
}
|
||||
if (s_wdD > 0.0f)
|
||||
{
|
||||
s_wdC += dt;
|
||||
float r = (s_wdC - s_wdD) / 1.2f;
|
||||
gBTDrive.forcedThrottle = (r <= 0.0f) ? 0.0f
|
||||
: (r < 1.0f ? sAutoDrive * r : sAutoDrive);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (sGotoDrive)
|
||||
{
|
||||
@@ -2388,6 +2408,35 @@ void
|
||||
s_forceTurn = ft ? (float)atof(ft) : 0.0f;
|
||||
}
|
||||
turn = s_forceTurn;
|
||||
// Publish the forced turn to gBTDrive too: mechmppr derives turnDemand
|
||||
// from gBTDrive.turn (NOT this local), so without this the headless rig
|
||||
// never entered the trn state -- BT_FORCE_TURN was silently inert for
|
||||
// the gait (task #64 harness fix).
|
||||
gBTDrive.turn = s_forceTurn;
|
||||
// BT_WALK_DELAY=<n> (stutter repro, task #64): hold throttle at 0 for the
|
||||
// first n sim-seconds (TURN-IN-PLACE, turn stays active) then release the
|
||||
// forced throttle -- reproduces "turn in place, then start walking BEFORE
|
||||
// the turn stops" exactly. Turn (BT_FORCE_TURN) persists across the seam.
|
||||
static float s_walkDelay = -1.0f, s_wdClock = 0.0f;
|
||||
if (s_walkDelay < -0.5f)
|
||||
{
|
||||
const char *wd = getenv("BT_WALK_DELAY");
|
||||
s_walkDelay = wd ? (float)atof(wd) : 0.0f;
|
||||
}
|
||||
if (s_walkDelay > 0.0f)
|
||||
{
|
||||
s_wdClock += dt;
|
||||
if (s_wdClock < s_walkDelay)
|
||||
throttle = 0.0f; // turn-in-place phase
|
||||
else
|
||||
{
|
||||
// RAMP the throttle up over 1.2s (simulate the manual lever
|
||||
// sweep DWELLING through standSpeed) with the turn still active
|
||||
// -- the continuous-stutter repro.
|
||||
float r = (s_wdClock - s_walkDelay) / 1.2f;
|
||||
if (r < 1.0f) throttle *= r;
|
||||
}
|
||||
}
|
||||
if (s_forceLimit > 0.0f)
|
||||
{
|
||||
s_forceClock += dt;
|
||||
@@ -2723,10 +2772,27 @@ void
|
||||
// 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
|
||||
// v5 ordered body-first/leg-last believing the leg's overwrite made
|
||||
// body drift invisible -- WRONG (task #64): the leakage merely
|
||||
// inverted (the body's pose flashed through instead). v6 (below)
|
||||
// removes the body from the skeleton entirely (mj=0).
|
||||
// task #64 RESOLUTION (user-verified): the body channel must NOT
|
||||
// write joints. The v5 claim "body writes first, leg overwrites,
|
||||
// so body drift can never show" was FALSE -- whenever the two
|
||||
// channels phase-split (the turn-in-place entry armed only the
|
||||
// leg), the body's out-of-phase pose leaked into the displayed
|
||||
// skeleton: rhythmic leg skips + visibly reduced bob, 100% repro
|
||||
// on turn-then-walk, while every leg-channel trace read clean
|
||||
// (the leg data was fine -- the RENDERED pose wasn't the leg's).
|
||||
// mj=0 advances + projects the body channel identically (records,
|
||||
// cycle speeds, travel projection all unchanged -- same engine
|
||||
// mode the replicant leg path uses) but structurally removes the
|
||||
// second skeleton writer. This also matches the binary's own
|
||||
// observable: "the body channel's phase drift is locally
|
||||
// INVISIBLE in the binary" (locomotion.md) -- now it is here too.
|
||||
// BT_BODY_MJ=1 restores the old double-writer for A/B.
|
||||
static const int s_bodyMj = BTEnvOn("BT_BODY_MJ", 0);
|
||||
adv = AdvanceBodyAnimation(dt, s_bodyMj); // channel B: replication projection (mj=0)
|
||||
legAdv = AdvanceLegAnimation(dt); // channel A: local sim -- pose + travel
|
||||
}
|
||||
else
|
||||
@@ -2781,7 +2847,10 @@ void
|
||||
static float s_smlog = 0.0f; s_smlog += dt;
|
||||
static double s_advSum = 0.0, s_legSum = 0.0;
|
||||
s_advSum += adv; s_legSum += legAdv;
|
||||
if (s_smlog >= 1.0f) { s_smlog = 0.0f;
|
||||
// BT_GAIT_TRACE=1 (task #64): log the gait state EVERY frame (not 1 Hz)
|
||||
// so a frame-level stutter at a transition is visible.
|
||||
static const int s_gtrace = getenv("BT_GAIT_TRACE") ? 1 : 0;
|
||||
if (s_smlog >= 1.0f || s_gtrace) { s_smlog = 0.0f;
|
||||
extern float gBTEyeBobY;
|
||||
static Joint *s_dbgRoot = 0; static int s_dbgTried = 0;
|
||||
if (!s_dbgTried) { s_dbgTried = 1; s_dbgRoot = ResolveJoint("jointlocal"); }
|
||||
@@ -2791,10 +2860,14 @@ void
|
||||
<< " legSum=" << (float)s_legSum
|
||||
<< " posZ=" << localOrigin.linearPosition.z
|
||||
<< " rootZ=" << dbgRt.z << " rootX=" << dbgRt.x << "\n" << std::flush;
|
||||
MechControlsMapper *s_tm = MappingMapper(); // input demands (task #64)
|
||||
DEBUG_STREAM << "[gaitSM] adv=" << adv << " legAdv=" << legAdv
|
||||
<< " cycleSpeed=" << bodyCycleSpeed << " legCycle=" << legCycleSpeed
|
||||
<< " state=" << bodyAnimationState << " legState=" << legAnimationState
|
||||
<< " kfCur=" << bodyAnimation.currentFrame
|
||||
<< " legFrm=" << legAnimation.currentFrame
|
||||
<< " spd=" << (s_tm ? s_tm->speedDemand : 0.0f)
|
||||
<< " turn=" << (s_tm ? s_tm->turnDemand : 0.0f)
|
||||
<< " bob=" << gBTEyeBobY << "\n" << std::flush; }
|
||||
}
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user