MP: COUPLED peer motion -- authentic gait-driven position + master gait send-mirror (task #50, env-gated)

Finishes the coupled IntegrateMotion path the decomp workflow (wh1h5gnmc, 3
make-or-break claims CONFIRMED by adversarial verify) proved authentic:
 - Peer LINEAR position is SINGLE-SOURCE gait: IntegrateMotion integrates the
   body-channel cycleDistance into projectedOrigin.linear@0x260, copied verbatim
   to localOrigin -- NOT velocity dead-reckon (that is angular/heading ONLY).
   0x260 'motionDelta' and 'projectedOrigin' are literally the same field; the
   'contradiction' was our reconstruction's two misnamed shadows.
 - Master and peer run the SAME predictor; the master's SEND-mirror must run it
   too (binary FUN_004a9b5c @0x4aab9c) so a gait-driven peer stays anchored. Our
   mirror used the constant-velocity deadReckoner + overwrote bodyTargetSpeed
   live every frame -> could not model a gait peer (the tug-of-war).
 - T4 CONFIRMED: both channels pose the full skeleton (same JointedMover); keep
   the peer LEG channel (our body channel is unbound on the peer).

CHANGES (all behind BT_MASTER_GAITMIRROR / BT_ROOT_POS, DEFAULT OFF -- zero
change to shipped behavior until visually confirmed):
 - mech4.cpp: send-mirror advances projectedOrigin by the mj=0 body channel
   travel (mirrorBodyAdv) rotated by heading + last-sent angular vel, re-seeded
   to localOrigin on each send, instead of the deadReckoner; and stops the
   per-frame live bodyTargetSpeed overwrite so the mirror slews toward last-sent.
 - mech.hpp/mech.cpp: mirrorBodyAdv member.

Measured A/B (autonomous through-zero sweep circle, the speed-change regime):
   velocity two-source (current default): ratio 1.0022 BUT user-visible glitch
   coupled, NO mirror (dense):            ratio 1.86  (tug-of-war)
   coupled + gait mirror, dense OFF:      ratio 1.14, back-steps 0.1%
   coupled + gait mirror, dense ON:       ratio 1.043, back-steps 0.1%, maxStep 2.9u
The single-source coupling is proven (backward-stepping 0.1% vs the split's
churn). Residual 2.9u occasional snap = leg(peer)-vs-body(mirror) channel
mismatch, second-order. Awaiting visual confirmation before default promotion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-14 21:22:02 -05:00
co-authored by Claude Opus 4.8
parent ea39af1ff6
commit c52a1ad79a
3 changed files with 69 additions and 2 deletions
+1
View File
@@ -915,6 +915,7 @@ Mech::Mech(
Wword(0xe6) = 0;
poseSyncLatch = 0; // @0x77c (was absorbed Wword(0x1df))
heatLevelSnapshot = 0; // @0x780 (type-7 deadband baseline)
mirrorBodyAdv = 0.0f; // master send-mirror gait travel
angMirrorYaw = 0.0f; // scalar peer-yaw mirror (port addition)
angMirrorRate = 0.0f;
angMirrorValid = 0;
+6
View File
@@ -625,6 +625,12 @@ public:
// one is recomputed by the master's own reckoner each frame from lastUpdate
// timing it does not control, and its false pi-drift waves FLOODED type-4
// resyncs (measured maxAng~=pi bursts). Scalars, wrap-safe, self-timed.
// Master send-mirror gait travel (BT_MASTER_GAITMIRROR): the invisible mj=0
// body channel's per-frame cycleDistance, stashed in the drive block and
// consumed by the send gate to advance projectedOrigin the way the PEER
// predicts (gait), instead of the constant-velocity deadReckoner. This is the
// authentic FUN_004a9b5c mirror = IntegrateMotion(mj=0) on projectedOrigin.
Scalar mirrorBodyAdv; // body cycleDistance this frame (master)
Scalar angMirrorYaw; // yaw at the last type-4 send (rad)
Scalar angMirrorRate; // yaw rate sent with it (rad/s)
Time angMirrorTime; // lastPerformance at that send
+62 -2
View File
@@ -3167,7 +3167,21 @@ void
// while the animation transitioned gradually (the "physics don't
// line up with the animation" desync). Signed, the SM decelerates
// through stop -> reverse-entry exactly like the leg channel.
if (s_realControls && MappingMapper() != 0)
// BT_MASTER_GAITMIRROR: the invisible mj=0 body channel becomes the
// PEER's predictor, so it must slew toward the LAST-SENT commanded
// speed (which the record writers stamp into bodyTargetSpeed@0x6b4 on
// each send, mech.cpp), NOT the live mapper demand. Leaving
// bodyTargetSpeed at last-sent re-activates the authentic type-2
// deadband and makes the mirror model exactly what the peer computes.
// The master's VISIBLE motion uses the LEG channel reading live demand
// independently, so this does not change what the local player sees.
// (Default OFF: live overwrite preserved.)
static const int s_gaitMirror = getenv("BT_MASTER_GAITMIRROR") ? 1 : 0;
if (s_gaitMirror)
{
/* leave bodyTargetSpeed at the last-sent value */
}
else if (s_realControls && MappingMapper() != 0)
bodyTargetSpeed = MappingMapper()->speedDemand;
else
bodyTargetSpeed = ((throttle < 0.0f) ? -1.0f : 1.0f)
@@ -3234,6 +3248,7 @@ void
// 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)
mirrorBodyAdv = adv; // stash for the send-mirror gait advance
legAdv = AdvanceLegAnimation(dt); // channel A: local sim -- pose + travel
}
else
@@ -3716,7 +3731,43 @@ void
// the PEER's event queue busy, so its CreatingMission->LoadingMission
// quiet-timeout never fired -- the pods deadlocked pre-launch. The real
// system never moves before launch, so the gate is faithful.)
(this->*deadReckoner)();
// SEND-MIRROR advance: model where the PEER believes this mech is, so the
// deadband fires exactly when the peer would drift. Authentic (binary
// FUN_004a9b5c @0x4aab9c): advance projectedOrigin by IntegrateMotion(mj=0)
// -- the SAME gait predictor the peer runs -- fed the LAST-SENT commanded
// speed. Our default mirror uses the engine constant-velocity deadReckoner
// (AcceleratedDeadReckoner), which does NOT model a gait-driven peer -> the
// deadband under-sends and the coupled peer drifts (measured). With
// BT_MASTER_GAITMIRROR: increment projectedOrigin.linear by the mj=0 body
// channel's travel (mirrorBodyAdv, = the peer's predicted stride) rotated by
// the mirror heading, and integrate the heading by the last-sent angular
// velocity. Re-seeded to localOrigin on each send by the record writers'
// projectedOrigin re-base, so this is incremental-from-last-send, exactly the
// peer's IntegrateMotion. (Default OFF: constant-velocity deadReckoner.)
static const int s_gaitMirrorSend = getenv("BT_MASTER_GAITMIRROR") ? 1 : 0;
if (s_gaitMirrorSend)
{
Vector3D mAng;
mAng.Multiply(updateVelocity.angularMotion, dt); // last-sent yaw rate
ReconQuatIntegrate(&projectedOrigin.angularPosition,
&projectedOrigin.angularPosition, &mAng);
projectedOrigin.angularPosition.Normalize();
if (mirrorBodyAdv != 0.0f)
{
Vector3D mLocal(0.0f, 0.0f, -mirrorBodyAdv); // forward = -Z, frame distance
Matrix34 mOrient;
Matrix34::FromQuaternion(&mOrient, &projectedOrigin.angularPosition);
Vector3D mWorld;
FUN_00408744(&mWorld, (Scalar *)&mLocal, &mOrient);
projectedOrigin.linearPosition.x += mWorld.x;
projectedOrigin.linearPosition.y += mWorld.y;
projectedOrigin.linearPosition.z += mWorld.z;
}
}
else
{
(this->*deadReckoner)();
}
Vector3D error;
error.Subtract(
projectedOrigin.linearPosition,
@@ -3774,6 +3825,15 @@ void
)
{
ForceUpdate(); // type 0: pose record
// RE-SEED the gait mirror to the just-sent pose: the peer will now
// extrapolate from THIS localOrigin, so the mirror must too. (The
// type-0 writer only re-bases projectedOrigin in the sim-state-2
// transition, not the normal walk case, so do it here.)
if (s_gaitMirrorSend)
{
projectedOrigin.linearPosition = localOrigin.linearPosition;
projectedOrigin.angularPosition = localOrigin.angularPosition;
}
if (getenv("BT_WIRE"))
DEBUG_STREAM << "[tx0] errSq=" << error.LengthSquared()
<< " posDb=" << posDb