MP: root-motion peer-position EXPERIMENT (env-gated BT_ROOT_POS; default unchanged) + KB notes (task #50)

The user's architectural question -- 'should animation and velocity even be
allowed to be uncoupled?' -- is decomp-CONFIRMED correct: the original peer
(FUN_004ab430 -> FUN_004ab1c8) drove POSITION FROM THE CLIP'S ROOT TRAVEL
between records (feet<->ground locked by construction) with a pose-sync
offset decay absorbing record corrections.  Our port dead-reckons position
from velocity while the legs run on commanded speed -- the two only agree at
steady state, mismatching exactly during speed changes (the reported glitch).

This commit lands the peer half of the coupled architecture, env-gated:
 - BT_ROOT_POS=1: peer position += clip travel rotated by heading (mirror of
   the master world-step @3325, == IntegrateMotion tail @004ab1c8); pose
   records absorbed via the authentic offset-decay (motionEventVector
   mechanism) instead of snapping.
 - Measured A/B (through-zero sweep circle, harshest speed-change regime):
     velocity-lerp (default): step ratio 1.0022 (clean)
     root-motion + sparse records: 11-17u anchor snaps (master's velocity
       mirror no longer models a gait-driven peer -> under-sends)
     root-motion + dense + offset-decay: evenness OK but ratio 1.86 --
       authority tug-of-war (double-authoring, exactly the D5 risk).
   CONCLUSION: the coupled peer requires the MASTER side of the original
   architecture too (gait-driven send-gate mirror / channel-B IntegrateMotion
   projection) -- a coherent rebuild for a fresh session, not a peer-only
   patch.  Default therefore stays velocity dead-reckon.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-14 19:59:22 -05:00
co-authored by Claude Opus 4.8
parent 2c6db6a2de
commit 702822f9c5
+58 -1
View File
@@ -1944,6 +1944,16 @@ void
// (the keyboard-steering skip); pure spin/walk never showed it (one stream).
// Save the heading, let DeadReckon own the LINEAR channel, then override
// the angular result with the original's incremental integration.
// ROOT-MOTION peer POSITION (authentic FUN_004ab1c8: the clip's travel
// drives the body between records; pose records re-anchor). Save the
// position so the engine velocity-lerp result can be discarded below
// (BT_DR_POS=1 restores the velocity dead-reckon for A/B).
// DEFAULT: engine velocity dead-reckon (best-measured: ratio 1.0022). The
// coupled root-motion experiment (BT_ROOT_POS=1) needs the MASTER-side
// gait mirror to stop the authority tug-of-war (measured ratio 1.86) --
// the completion path is mapped in the session notes.
static const int s_drPos = getenv("BT_ROOT_POS") ? 0 : 1;
Point3D replPrevPos = localOrigin.linearPosition;
Quaternion replPrevHeading = localOrigin.angularPosition;
DeadReckon(dt); // engine reckoner: LINEAR position/velocity
if (angSyncLatch)
@@ -1960,6 +1970,8 @@ void
&replPrevHeading, &replAngStep); // exact FUN_00409f58
}
localOrigin.angularPosition.Normalize();
if (!s_drPos)
localOrigin.linearPosition = replPrevPos; // root-motion mode: gait applies travel below
localToWorld = localOrigin;
// RENDER-vs-SIM decoupling probe: publish the heading the RENDER will
@@ -2122,7 +2134,48 @@ void
// once so a direct run-state entry can't slew from -4.3e8.
if (legCycleSpeed < -100.0f || legCycleSpeed > 200.0f)
legCycleSpeed = 0.0f;
(void)AdvanceLegAnimation(dt); // joints only; travel = DeadReckon
const Scalar replLegAdv = AdvanceLegAnimation(dt); // joints + ROOT TRAVEL
// AUTHENTIC coupled position (mirror of the master's world-step at
// mech4.cpp:3325-3336 == Mech::IntegrateMotion tail @004ab1c8): between
// records the body advances by the CLIP'S OWN travel rotated by the
// heading -- feet and ground are locked BY CONSTRUCTION, so cadence-vs-
// travel mismatch (the speed-change glitch) is structurally impossible.
// A pose record re-anchors onto the authoritative position
// (poseSyncLatch, armed by every type-0 read). adv is SIGNED by the SM
// (reverse clips return negative travel; master dir==1.0 in SM mode).
if (!s_drPos)
{
// POSE-SYNC OFFSET DECAY (authentic FUN_004ab1c8 motionEventVector
// mechanism): a record does NOT snap the body -- the correction offset
// is absorbed over ~1/3s while the clip's root motion keeps driving, so
// the feet never pop and the position converges to authority.
static Vector3D s_rmErr[2]; // per-instance would be a member; one replicant per node in practice
Vector3D &rmErr = s_rmErr[0];
if (poseSyncLatch)
{
rmErr.Subtract(updateOrigin.linearPosition, localOrigin.linearPosition);
poseSyncLatch = 0;
}
{
Scalar k = dt * 3.0f; if (k > 1.0f) k = 1.0f; // ~1/3s absorption
localOrigin.linearPosition.x += rmErr.x * k;
localOrigin.linearPosition.y += rmErr.y * k;
localOrigin.linearPosition.z += rmErr.z * k;
rmErr.x -= rmErr.x * k; rmErr.y -= rmErr.y * k; rmErr.z -= rmErr.z * k;
}
if (replLegAdv != 0.0f)
{
Vector3D rmLocal(0.0f, 0.0f, -replLegAdv); // forward = -Z, per-frame DISTANCE
Matrix34 rmOrient;
Matrix34::FromQuaternion(&rmOrient, &localOrigin.angularPosition);
Vector3D rmWorld;
FUN_00408744(&rmWorld, (Scalar *)&rmLocal, &rmOrient);
localOrigin.linearPosition.x += rmWorld.x;
localOrigin.linearPosition.y += rmWorld.y;
localOrigin.linearPosition.z += rmWorld.z;
}
localToWorld = localOrigin; // refresh with the coupled position
}
// GAIT-EVENNESS probe (BT_GAITEV, 1s stats): the metric of the VISIBLE
// speed-change glitch -- per-frame leg-clip advance regularity + leg-state
@@ -3707,6 +3760,10 @@ void
// makes the cadence regular + dense (records ~1 frame apart), so the
// horizon prediction matches and the lerp tracks smoothly. On a LAN
// this is a handful of small packets/frame -- negligible.
// Dense while moving: with the gait-driven peer, the master's velocity
// mirror no longer models the peer, so the deadband under-sends and the
// peer diverges (measured 11u snaps). Dense records keep the correction
// offset tiny; the peer absorbs it via the authentic pose-sync decay.
static const int s_denseTx = getenv("BT_NO_DENSE_TX") ? 0 : 1;
const Logical moving = (liveSpdSq > 0.25f);
if (