MP: FIX walk+turn peer skip -- incremental heading integration + scalar yaw mirror (task #50)
The user-keyboard regime (steering WHILE walking) fired BOTH dense record streams at once and exposed the last divergence: our peer heading used the engine Mover::DeadReckon slerp-toward-projection, whose angular projection reads the SHARED lastUpdate/nextUpdate timebase. The dense type-0 pose stream resets that timebase every frame while walking while RESTORING a stale orientation (the authentic case-0 strip, verified against FUN_004a1232 case 0) -- so the angular target barely advances from a stale base and the slerp DRAGS the heading back every frame. Measured: peer yaw advancing at ~40% rate with half the frames stepping BACKWARD. Pure-spin and pure-walk tests never showed it (single stream) -- why autonomous looked smooth while keyboard play skipped. AUTHENTIC FIX (decomp FUN_004ab1c8 -> FUN_004ab188/FUN_00409f58): the original replicant integrates its heading INCREMENTALLY from the CURRENT pose -- exact rotation of (replicated yaw rate * dt) composed on each frame -- and re-anchors on type-4 receipt. It never slerps toward a projected angular target. - mech4.cpp peer branch: save heading, let DeadReckon own LINEAR only, then integrate heading incrementally (ReconQuatIntegrate); on angSyncLatch (new type-4) re-anchor to updateOrigin. - mech.hpp/mech.cpp: angSyncLatch member (angular analog of poseSyncLatch), armed by ReadUpdateRecord case 4. - SCALAR peer-yaw mirror (angMirrorYaw/Rate/Time, re-based in the type-4 writer): replaces the quaternion projectedOrigin mirror for the ANGLE deadband -- the old one was recomputed each frame by the master's own reckoner from timing it does not control and false-fired in pi-waves (measured maxAng~=pi bursts -> periodic resync floods). - Dense-rot type-4 send REMOVED (was masking the old crude projection; not authentic; churned the shared horizon). Orientation now rides the sparse angle/velocity deadband resyncs exactly as the binary's. Verified live-autonomous: - pure spin: 59/59 perfectly regular peer yaw steps; master resyncs 0/s with mirror drift ~5e-7 (records near-silent, authentic sparse model). - walk+turn circle (the user regime): peer sim yaw monotonic at exactly the master's rate (0.00556/frame @ 0.327 rad/s), no backward steps, no stalls. 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
b7be95b584
commit
d78e77bf84
@@ -1930,7 +1930,36 @@ void
|
||||
{
|
||||
if (dt > 0.0001f && dt < 0.5f)
|
||||
{
|
||||
DeadReckon(dt); // engine: reckoner (exact angular) + lerp
|
||||
// AUTHENTIC PEER HEADING (decomp FUN_004ab188/FUN_00409f58 via FUN_004ab1c8):
|
||||
// the original replicant integrates its heading INCREMENTALLY from the
|
||||
// CURRENT pose -- compose an exact rotation of (replicated yaw rate * dt)
|
||||
// onto the rendered heading each frame -- and re-anchors it when a type-4
|
||||
// resync lands. It does NOT slerp toward a projected angular target: the
|
||||
// engine Mover::DeadReckon lerp uses the shared lastUpdate/nextUpdate
|
||||
// timebase, which the DENSE type-0 pose stream resets every frame while
|
||||
// walking while RESTORING a stale orientation (the authentic case-0 strip)
|
||||
// -- so the angular projection barely advances from a stale base and the
|
||||
// slerp DRAGS the heading back each frame. Measured: walking+turning, the
|
||||
// peer yaw advanced at ~40% rate with half the frames stepping BACKWARD
|
||||
// (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.
|
||||
Quaternion replPrevHeading = localOrigin.angularPosition;
|
||||
DeadReckon(dt); // engine reckoner: LINEAR position/velocity
|
||||
if (angSyncLatch)
|
||||
{
|
||||
// type-4 arrived: re-anchor on the authoritative orientation
|
||||
localOrigin.angularPosition = updateOrigin.angularPosition;
|
||||
angSyncLatch = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector3D replAngStep;
|
||||
replAngStep.Multiply(updateVelocity.angularMotion, dt);
|
||||
ReconQuatIntegrate(&localOrigin.angularPosition,
|
||||
&replPrevHeading, &replAngStep); // exact FUN_00409f58
|
||||
}
|
||||
localOrigin.angularPosition.Normalize();
|
||||
localToWorld = localOrigin;
|
||||
|
||||
// RENDER-vs-SIM decoupling probe: publish the heading the RENDER will
|
||||
@@ -3615,44 +3644,34 @@ void
|
||||
// UpdateTurnVelocityDiffrence. [T1 expressions; zero-deadband
|
||||
// guard falls back to the old quat-w stand-in]
|
||||
{
|
||||
// PEER-ESTIMATE MIRROR (2026-07-14, replicant-chop fix): advance the
|
||||
// projection by the last-SENT angular velocity each frame -- this is
|
||||
// what the replicant's dead-reckoner is doing with the last record.
|
||||
// The gate below then measures the peer's TRUE drift, so a steady
|
||||
// turn (matching velocity) sends almost nothing and the replicant
|
||||
// extrapolates smoothly, instead of the stale mirror firing a
|
||||
// re-base record EVERY frame (the stall/snap chop). The type-4
|
||||
// writer re-bases this mirror on each send (mech.cpp case 4).
|
||||
{
|
||||
Vector3D angStepM;
|
||||
angStepM.Multiply(updateVelocity.angularMotion, dt);
|
||||
projectedOrigin.angularPosition.Add(
|
||||
projectedOrigin.angularPosition, angStepM);
|
||||
// Adding a scaled angular-velocity VECTOR to a quaternion denormalizes
|
||||
// it (the reckoner MOVER.cpp:463 does the same); a non-unit quaternion's
|
||||
// extracted yaw is garbage (~pi), which spiked angDrift and FLOODED angle
|
||||
// resyncs in bursts -> peer horizon reset -> uneven (max/avg ~2.8x) render.
|
||||
projectedOrigin.angularPosition.Normalize();
|
||||
}
|
||||
// SCALAR PEER-YAW MIRROR (replaces the quaternion projectedOrigin mirror):
|
||||
// the peer renders yaw ~= angMirrorYaw + angMirrorRate*(now - angMirrorTime)
|
||||
// (re-based by the type-4 writer on every send, mech.cpp case 4). The old
|
||||
// quaternion mirror was ALSO recomputed each frame by the master's own
|
||||
// reckoner from lastUpdate timing it does not control, so it drifted in
|
||||
// slow pi-waves and FLOODED angle resyncs in bursts (measured maxAng~=pi)
|
||||
// -- the walk+turn record churn. Scalars: wrap-safe, self-timed, exact
|
||||
// for the constant-rate spin the peer's exact integrator reproduces.
|
||||
Scalar angDb = (updateTurnAngleDeadband > 0.0f) ? updateTurnAngleDeadband : -1.0f;
|
||||
Scalar velDb = (updateTurnVelocityDeadband > 0.0f) ? updateTurnVelocityDeadband : -1.0f;
|
||||
Logical resync = False;
|
||||
int rzn = 0; // 1=angleDrift 2=velDrift 4=cameToRestAng
|
||||
// TRUE yaw drift in RADIANS (wrap-safe), to match the radian deadband
|
||||
// angDb. The prior `Abs(localOrigin.angularPosition.y -
|
||||
// projectedOrigin.angularPosition.y)` compared raw QUATERNION y-components
|
||||
// (sin(yaw/2)-ish, unitless) against a radian deadband -- both a units
|
||||
// mismatch and wrap-unsafe: at the +-pi heading wrap the quaternion-y diff
|
||||
// spikes toward its max (~2) and fired byAngle=56/56 EVERY frame (measured),
|
||||
// flooding resyncs at every wrap. Take the yaw difference in euler space
|
||||
// and unwrap it to [-pi,pi] so a wrap is not seen as a ~2pi drift.
|
||||
YawPitchRoll _yprL, _yprP;
|
||||
_yprL = localOrigin.angularPosition;
|
||||
_yprP = projectedOrigin.angularPosition;
|
||||
Scalar _dyaw = (Scalar)(_yprL.yaw - _yprP.yaw);
|
||||
while (_dyaw > 3.14159265f) _dyaw -= 6.28318531f;
|
||||
while (_dyaw < -3.14159265f) _dyaw += 6.28318531f;
|
||||
const Scalar angDrift = Abs(_dyaw);
|
||||
int rzn = 0; // 1=angleDrift 2=velDrift 4=cameToRestAng 8=mirrorInvalid
|
||||
Scalar angDrift = 0.0f;
|
||||
if (angMirrorValid)
|
||||
{
|
||||
YawPitchRoll _yprL;
|
||||
_yprL = localOrigin.angularPosition;
|
||||
const Scalar mirrorYaw = angMirrorYaw
|
||||
+ angMirrorRate * (Scalar)(lastPerformance - angMirrorTime);
|
||||
Scalar _dyaw = (Scalar)_yprL.yaw - mirrorYaw;
|
||||
while (_dyaw > 3.14159265f) _dyaw -= 6.28318531f;
|
||||
while (_dyaw < -3.14159265f) _dyaw += 6.28318531f;
|
||||
angDrift = (_dyaw < 0.0f) ? -_dyaw : _dyaw;
|
||||
}
|
||||
else
|
||||
{
|
||||
resync = True; rzn |= 8; // no baseline yet -> establish one
|
||||
}
|
||||
// Compare the live yaw rate against the LAST-SENT rate (updateVelocity,
|
||||
// which the type-4 writer copies straight from localVelocity, mech.cpp:2107)
|
||||
// -- NOT projectedVelocity, which the master's dead-reckoner recomputes in a
|
||||
@@ -3698,17 +3717,6 @@ 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
|
||||
|
||||
Reference in New Issue
Block a user