MP: FIX peer spin hang/divergence -- exact quaternion integration + frequent orientation (task #50)

Answers 'how did the original handle this?' from the decomp (subagent hunt):
the 1995 binary's replicant reckoner (FUN_004ab1c8 -> FUN_00409f58, part_000.c:9359)
integrates heading EXACTLY: build a unit axis-angle rotation quaternion from
angularVelocity*dt ({axis*sin(t/2), cos(t/2)}) and Hamilton-multiply it onto the
heading (FUN_00409d9c) -- exact for any timestep, stays on the unit sphere.  It
further carries the full orientation quaternion in the FREQUENT pose record
(FUN_0040a938, 7-float pose), so the dead-reckon gap stays tiny.

Our reconstruction diverged two ways, both fixed:
 1. ReconQuatIntegrate (mechrecon.hpp) -- the reconstruction of FUN_00409f58 -- was
    STUBBED as , a crude small-angle VECTOR add.  Restored to
    the real exact axis-angle composition.  (A 'no stand-ins' violation: the comment
    even wrongly claimed Quaternion::Add == FUN_00409f58.)
 2. The engine Mover reckoner (MOVER.cpp AcceleratedDeadReckoner/LinearDeadReckoner)
    also did the vector Add on projectedOrigin.angularPosition -> over a long peer
    record gap it diverged to ~180deg then snapped (the reported spin HANG/hesitation).
    Routed both through a new ExactAngularProject() helper (same exact math).
 3. Orientation only rode the sparse type-4 resync; during a PURE spin the linear
    dense-send never fires (not translating), so the gap ballooned (~1.6s) and the now-
    exact projection sat far ahead -> the slerp jumped.  Added an ANGULAR dense-send
    (resync every frame while |yawRate|>0.1), mirroring the original's frequent-
    orientation model -> gap stays tiny -> smooth.

Verified live-autonomous (BT_AUTODRIVE+BT_FORCE_TURN + BT_RENDHDG render-rate probe):
the ~180deg divergence + multi-radian snaps are GONE (rendered maxStep 0.05-0.10 rad,
no jumps).  User confirms: no frame hang/hesitation.  MOVER.cpp change is strictly
more correct (exact==crude for the small per-frame master case; only large-gap peer
extrapolation changes), so walking is unaffected.

KNOWN REMAINING (separate, smaller): the turn-STEP leg animation (trn clip, mech2.cpp
advance_normally) runs at a FIXED idleStrideScale cadence that does not scale with the
rotation rate, so the legs lag/skip vs the (now-correct) body rotation.  Next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
arcattack
2026-07-14 17:05:16 -05:00
co-authored by Claude Opus 4.8
parent 9eff043973
commit a8eb8a427f
3 changed files with 69 additions and 12 deletions
+12 -1
View File
@@ -1930,7 +1930,7 @@ void
{
if (dt > 0.0001f && dt < 0.5f)
{
DeadReckon(dt); // engine: reckoner + lerp
DeadReckon(dt); // engine: reckoner (exact angular) + lerp
localToWorld = localOrigin;
// RENDER-vs-SIM decoupling probe: publish the heading the RENDER will
@@ -3672,6 +3672,17 @@ void
{
resync = True; // stand-in when unstreamed
}
// ANGULAR dense-send: the original refreshed orientation in its FREQUENT
// pose record so the peer's dead-reckon gap stayed tiny (decomp: 7-float
// pose carries the quaternion, refreshed every broadcast). Our orientation
// rides only the type-4 resync, and during a PURE spin the linear dense-send
// never fires (not translating), so the gap balloons (~1.6s) and even the
// exact integrator's projection is far ahead -> the slerp jumps. Send the
// resync every frame while rotating to keep updateOrigin.angular fresh
// (small gap -> smooth), mirroring the original's frequent-orientation model.
static const int s_denseRot = getenv("BT_NO_DENSE_TX") ? 0 : 1;
if (s_denseRot && Abs(localVelocity.angularMotion.y) > 0.1f)
resync = True;
if (resync)
{
ForceUpdate(1 << MechResyncUpdateModelBit); // type 4