MP: FIX keyboard-driving peer skip -- non-pose records no longer kick the dead-reckon clock (task #50)

The discriminating user report: peer motion was smooth under the autonomous
harness but skipped when driven by KEYBOARD -- even pure walking / pure spinning.
The harness pins throttle/turn constant; the keyboard SWEEPS the throttle lever
every frame of a key-hold (mech4 sLever integrator), so mapper->speedDemand
changes every frame, and the speed-deadband gate (authentic exact !=) fires a
type-2 speed record EVERY FRAME for the whole accel/decel.  The base reader
Simulation::ReadUpdateRecord stamps lastUpdate=Now() on EVERY record (flagged
'HACK - should be based upon message->timeStamp' in the 1995 source) -- so each
type-2 SHRINKS the peer reckoner's projection span (nextUpdate-lastUpdate)
without refreshing updateOrigin: the position target jumps backward toward the
stale origin, the next pose record yanks it forward -> target oscillation every
frame during any input sweep.  Matches the session-long 'worst on accel/decel'.

FIX (mech.cpp Mech::ReadUpdateRecord): non-pose records (2,3,5,6,7,8) preserve
lastUpdate around the base call (keeping the real payload, simulationState).
Pose (0) and resync (4) keep their authentic clock behavior.  BT_T2_CLOCK
restores the old stamping for A/B.

REPRO HARNESS (mechmppr.cpp): BT_FORCE_SWEEP=<period> triangle-sweeps the forced
throttle 0.2..0.9 -- a type-2 record per frame, the keyboard-skip repro the
constant-throttle harness could never produce.

Verified A/B, autonomous circle+sweep (the keyboard regime):
  old clock:  worst frame spike 10.4x avg, path/net ratio 1.069 (backtracking)
  guarded:    worst 4.6x, ratio 1.0022 (no backtracking)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-14 19:12:52 -05:00
co-authored by Claude Opus 4.8
parent d78e77bf84
commit 93456be051
2 changed files with 120 additions and 6 deletions
+102 -6
View File
@@ -1848,7 +1848,23 @@ void
case 2: // commanded-speed update
{
Mech__SpeedUpdateRecord *record = (Mech__SpeedUpdateRecord *)message;
Simulation::ReadUpdateRecord(message); // FUN_0041bd34
{
// CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now()
// on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no
// pose, but that stamp shrinks the dead-reckoner's projection span
// (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the
// peer's position target jumps backward toward the stale origin, then the
// next pose record yanks it forward: target oscillation every frame while
// records churn. Keyboard driving churns them CONSTANTLY (the throttle
// lever sweeps every frame of a key-hold -> a type-2 speed record per
// frame), which autodrive's pinned throttle never does -- the 'skips when
// I drive, smooth when you drive' report. Preserve the clock; keep the
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
} // FUN_0041bd34
bodyTargetSpeed = record->speedDemand; // @0x6b4 <- rec+0x10
}
break;
@@ -1856,7 +1872,23 @@ void
case 3: // leg/body state + stability
{
Mech__StateUpdateRecord *record = (Mech__StateUpdateRecord *)message;
Simulation::ReadUpdateRecord(message);
{
// CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now()
// on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no
// pose, but that stamp shrinks the dead-reckoner's projection span
// (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the
// peer's position target jumps backward toward the stale origin, then the
// next pose record yanks it forward: target oscillation every frame while
// records churn. Keyboard driving churns them CONSTANTLY (the throttle
// lever sweeps every frame of a key-hold -> a type-2 speed record per
// frame), which autodrive's pinned throttle never does -- the 'skips when
// I drive, smooth when you drive' report. Preserve the clock; keep the
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
}
bodyResetLatch = record->legResetLatch; // @0x658 <- rec+0x10
stabilityAlarm.SetLevel(record->stability); // @0x4c4 <- rec+0x18
if (record->legState == 0)
@@ -1904,7 +1936,23 @@ void
case 5: // knockdown
{
Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
Simulation::ReadUpdateRecord(message);
{
// CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now()
// on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no
// pose, but that stamp shrinks the dead-reckoner's projection span
// (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the
// peer's position target jumps backward toward the stale origin, then the
// next pose record yanks it forward: target oscillation every frame while
// records churn. Keyboard driving churns them CONSTANTLY (the throttle
// lever sweeps every frame of a key-hold -> a type-2 speed record per
// frame), which autodrive's pinned throttle never does -- the 'skips when
// I drive, smooth when you drive' report. Preserve the clock; keep the
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
}
heatAlarm.SetLevel(record->heatLevel); // @0x450 <- rec+0x10
throttleState = record->throttleState; // @0x4a4 <- rec+0x14
fallDirection = record->fallDirection; // @0x4a8 <- rec+0x18
@@ -1919,7 +1967,23 @@ void
case 6: // death
{
Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
Simulation::ReadUpdateRecord(message);
{
// CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now()
// on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no
// pose, but that stamp shrinks the dead-reckoner's projection span
// (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the
// peer's position target jumps backward toward the stale origin, then the
// next pose record yanks it forward: target oscillation every frame while
// records churn. Keyboard driving churns them CONSTANTLY (the throttle
// lever sweeps every frame of a key-hold -> a type-2 speed record per
// frame), which autodrive's pinned throttle never does -- the 'skips when
// I drive, smooth when you drive' report. Preserve the clock; keep the
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
}
heatAlarm.SetLevel(record->heatLevel);
throttleState = record->throttleState;
fallDirection = record->fallDirection;
@@ -1936,7 +2000,23 @@ void
case 7: // impact (fields only, no side effects)
{
Mech__FallUpdateRecord *record = (Mech__FallUpdateRecord *)message;
Simulation::ReadUpdateRecord(message);
{
// CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now()
// on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no
// pose, but that stamp shrinks the dead-reckoner's projection span
// (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the
// peer's position target jumps backward toward the stale origin, then the
// next pose record yanks it forward: target oscillation every frame while
// records churn. Keyboard driving churns them CONSTANTLY (the throttle
// lever sweeps every frame of a key-hold -> a type-2 speed record per
// frame), which autodrive's pinned throttle never does -- the 'skips when
// I drive, smooth when you drive' report. Preserve the clock; keep the
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
}
heatAlarm.SetLevel(record->heatLevel);
throttleState = record->throttleState;
fallDirection = record->fallDirection;
@@ -1948,7 +2028,23 @@ void
case 8: // airborne/reverse cycle-rate selector
{
Mech__AirborneUpdateRecord *record = (Mech__AirborneUpdateRecord *)message;
Simulation::ReadUpdateRecord(message);
{
// CLOCK GUARD (keyboard-skip fix): the base reader stamps lastUpdate=Now()
// on EVERY record ('HACK' per SIMULATE.cpp:276). Non-pose records carry no
// pose, but that stamp shrinks the dead-reckoner's projection span
// (nextUpdate - lastUpdate) WITHOUT refreshing updateOrigin -- so the
// peer's position target jumps backward toward the stale origin, then the
// next pose record yanks it forward: target oscillation every frame while
// records churn. Keyboard driving churns them CONSTANTLY (the throttle
// lever sweeps every frame of a key-hold -> a type-2 speed record per
// frame), which autodrive's pinned throttle never does -- the 'skips when
// I drive, smooth when you drive' report. Preserve the clock; keep the
// real payload (simulationState). BT_T2_CLOCK restores the old stamp (A/B).
static const int s_t2clock = getenv("BT_T2_CLOCK") ? 1 : 0;
const Time savedLastUpdate = lastUpdate;
Simulation::ReadUpdateRecord(message);
if (!s_t2clock) lastUpdate = savedLastUpdate;
}
airborneSelect = record->airborne; // @0x3f4 <- rec+0x10
bodyTargetSpeed = record->speedDemand; // rec+0x14
}
+18
View File
@@ -649,6 +649,24 @@ void
key_throttle = 0.08f; // near-idle half-cycle
}
}
// BT_FORCE_SWEEP=<period>: triangle-sweep the forced throttle 0.2..0.9
// continuously -- mimics a keyboard key-hold sweeping the lever every
// frame (a type-2 speed record per frame), the keyboard-skip repro.
{
static float s_swpP = -1.0f, s_swpClock = 0.0f;
if (s_swpP < 0.0f)
{
const char *fw = getenv("BT_FORCE_SWEEP");
s_swpP = fw ? (float)atof(fw) : 0.0f;
}
if (s_swpP > 0.0f)
{
s_swpClock += time_slice;
float ph = fmodf(s_swpClock, 2.0f * s_swpP) / s_swpP; // 0..2
float tri = (ph < 1.0f) ? ph : (2.0f - ph); // 0..1..0
key_throttle = 0.2f + 0.7f * tri;
}
}
// BT_FORCE_FLIP=<t>: invert the forced throttle after t sim-seconds
// (the mid-stride direction-change repro).
static float s_hFlip = -1.0f;