MP: replicant motion chop ROOT-CAUSED + FIXED -- stale peer-mirror spammed re-base
records; also the 2007 call-counter clock stub + two crash fixes
User-reported: peer mechs turn/move choppy ("missing frames"). Measured per-frame
(BT_REPL_HDG): 13% of frames the replicant's heading STALLED, 11% it JUMPED 2-6x.
THREE layered defects found:
1) THE CLOCK STUB (engine substrate): TIMESTUB.cpp's GetRTC/GetHiRes were 2007
`return time++` call-counters -- the "clock" advanced per CALL, not per ms --
and TIMESTUB won the /FORCE duplicate-symbol race over the REAL QPC clock in
L4TIME.cpp (LNK4006). Every Now()-domain consumer (dead-reckon above all) ran
on call-count pseudo-time. Removed TIMESTUB from the build; L4TIME covers every
symbol. [The real clock alone did NOT cure the chop -- but it was objectively
broken and un-gated the two latent bugs below.]
2) TWO CRASH FIXES the real clock exposed:
- legAnimationState@0x3b0 never ctor-initialized (the task-#56 0xCDCDCDCD
family): the type-3 writer re-dispatches SetBodyAnimation(legAnimationState)
on the WRITER; a record emitted before the leg SM's first tick passed raw
0xCDCDCDCD as a clip index -> AV (cdb-pinned, mech2.cpp:233). Init 0.
- Replicants were SERIALIZING update records: the tail WriteSimulationUpdate ran
for every instance, and the port's replicant leg-SM accommodation (task #50)
calls ForceUpdate -> replicants emitted derived/uninitialized state into the
stream. Master-gated; replicant marks discarded (master-authoritative).
3) THE CHOP ITSELF: the master's resync send-gate compares localOrigin vs
projectedOrigin -- the PEER-ESTIMATE mirror -- but the port master never
maintained projectedOrigin (the bring-up drive replaced Mover::Perform; the
engine only updates the projection inside replicant-only DeadReckon). Stale
mirror -> |local-projected| > deadband EVERY frame -> a type-4 re-base record
EVERY frame -> the replicant hard-copied the master's deadband-quantized
heading each frame (the engine lerp never engaged; nextUpdate always behind
till) -> stall/snap beat = the chop. FIX: advance the mirror each frame by the
last-SENT angular velocity (what the peer is extrapolating) and re-base it in
the type-4 writer. Records now flow only on TRUE drift (~1 per 5 frames in a
steady spin), the replicant extrapolates smoothly between them, and the lerp
horizon finally engages. Measured: STALLS 13%->1%, JUMPS 11%->1% (residue: tiny
sub-degree backward corrections on record arrival -- inherent dead-reckon
overshoot the lerp absorbs).
Diag probes: BT_REPL_HDG (per-frame replicant heading + dead-reckon internals),
BT_REPL_TRN (replicant turn/leg state). scratchpad/clockcrash_bp.txt (cdb).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
e6ad29d648
commit
a3b735d5da
+6
-1
@@ -157,7 +157,12 @@ add_library(munga_engine STATIC
|
||||
"engine/MUNGA/TERRAIN.cpp"
|
||||
"engine/MUNGA/TESTALL.cpp"
|
||||
"engine/MUNGA/TIME.cpp"
|
||||
"engine/MUNGA/TIMESTUB.cpp"
|
||||
# TIMESTUB.cpp REMOVED (2026-07-14): its GetRTC/GetHiRes were 2007 call-counter
|
||||
# stubs (`return time++` -- the "clock" advanced per CALL, not per ms) that WON
|
||||
# the /FORCE duplicate-symbol race over the REAL QueryPerformanceCounter clock
|
||||
# in L4TIME.cpp (LNK4006 "second definition ignored"). Every Now()-domain
|
||||
# consumer (dead-reckoning above all) ran on call-count pseudo-time -> the
|
||||
# replicant stall/jump chop. L4TIME.cpp covers every symbol TIMESTUB defined.
|
||||
"engine/MUNGA/TOOL.cpp"
|
||||
"engine/MUNGA/TRACE.cpp"
|
||||
"engine/MUNGA/TRACSTUB.cpp"
|
||||
|
||||
@@ -870,6 +870,13 @@ Mech::Mech(
|
||||
// dispatch (task #56) gates on it every frame; garbage
|
||||
// nonzero silently disabled the writers until death anims ran
|
||||
legResetLatch = 0; // @0x654 -- same latch family, same fill hazard
|
||||
legAnimationState = 0; // @0x3b0 -- same hazard (found 2026-07-14, the real-clock
|
||||
// crash): the type-3 record writer re-dispatches
|
||||
// SetBodyAnimation(legAnimationState) on itself; a record
|
||||
// emitted before the leg SM's first tick passed the raw
|
||||
// 0xCDCDCDCD as a clip index -> AV in SetBodyAnimation.
|
||||
// (The stub call-counter clock had merely re-ordered record
|
||||
// emission so the window was never hit.) Standing = 0.
|
||||
attrPad = 0.0f; // shared read pad for un-populated gauge attributes
|
||||
linearSpeed = 0.0f; // live forward ground speed (LinearSpeed gauge); set per frame
|
||||
radarRange = 1000.0f; // radar display scale (SetTargetRange is stubbed;
|
||||
@@ -2091,6 +2098,15 @@ void
|
||||
}
|
||||
updateOrigin.angularPosition = localOrigin.angularPosition; // FUN_00409968(0x138 <- 0x10c)
|
||||
updateVelocity.angularMotion = localVelocity.angularMotion; // FUN_00408440(0x2d4 <- 0x1d0)
|
||||
// RE-BASE the master's peer-estimate mirror (2026-07-14, replicant-chop
|
||||
// fix): projectedOrigin/projectedVelocity model what the PEER will now
|
||||
// extrapolate from this record. The send gate compares local vs projected
|
||||
// (the peer's drift); without the re-base the mirror went stale, the gate
|
||||
// fired EVERY frame, and the per-frame records re-based the replicant into
|
||||
// the stall/snap chop. (The binary maintains this in the un-decompiled
|
||||
// master perf; the writer-side re-base is the coherent reconstruction.)
|
||||
projectedOrigin.angularPosition = localOrigin.angularPosition;
|
||||
projectedVelocity.angularMotion = localVelocity.angularMotion;
|
||||
record->speedDemand = speedDemand; // rec+0x28
|
||||
bodyTargetSpeed = speedDemand;
|
||||
}
|
||||
|
||||
@@ -2000,6 +2000,24 @@ void
|
||||
<< " spd=" << replMppr->speedDemand << "\n" << std::flush;
|
||||
}
|
||||
}
|
||||
// PER-FRAME heading trace (BT_REPL_HDG, choppy-spin diagnosis): the
|
||||
// replicant's rendered yaw each frame + dt. A smooth dead-reckoned
|
||||
// spin advances ~yawRate*dt every frame; choppiness shows as heading
|
||||
// STALLS followed by SNAPS (records arriving without integration
|
||||
// between them).
|
||||
if (getenv("BT_REPL_HDG") && yawRate != 0.0f)
|
||||
{
|
||||
YawPitchRoll rh, ph, uh;
|
||||
rh = localOrigin.angularPosition;
|
||||
ph = projectedOrigin.angularPosition;
|
||||
uh = updateOrigin.angularPosition;
|
||||
DEBUG_STREAM << "[replhdg] yaw=" << (Scalar)rh.yaw
|
||||
<< " proj=" << (Scalar)ph.yaw
|
||||
<< " upd=" << (Scalar)uh.yaw
|
||||
<< " lp-lu=" << (lastPerformance.ticks - lastUpdate.ticks)
|
||||
<< " nu-lp=" << (nextUpdate.ticks - lastPerformance.ticks)
|
||||
<< " yawRate=" << yawRate << " dt=" << dt << "\n" << std::flush;
|
||||
}
|
||||
|
||||
// REPLICANT BEAMS (task #51): the emitters carry live replicated
|
||||
// discharge state (Emitter::ReadUpdateRecord); draw them with the
|
||||
@@ -3356,6 +3374,20 @@ 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);
|
||||
}
|
||||
Scalar angDb = (updateTurnAngleDeadband > 0.0f) ? updateTurnAngleDeadband : -1.0f;
|
||||
Scalar velDb = (updateTurnVelocityDeadband > 0.0f) ? updateTurnVelocityDeadband : -1.0f;
|
||||
Logical resync = False;
|
||||
@@ -4333,8 +4365,18 @@ void
|
||||
}
|
||||
|
||||
// Keep the simulation/networking bookkeeping consistent (this is exactly
|
||||
// what the base "no time / stasis" early-out does).
|
||||
WriteSimulationUpdate(update_stream);
|
||||
// what the base "no time / stasis" early-out does). MASTER ONLY (2026-07-14):
|
||||
// replication is master-authoritative -- a replicant must never serialize.
|
||||
// The port's replicant runs the leg SM for JOINTS (task #50 accommodation),
|
||||
// whose transitions call ForceUpdate() and mark updateModel; serializing those
|
||||
// emitted derived/uninitialized state back into the stream (and the type-3
|
||||
// writer re-dispatches SetBodyAnimation on the WRITER -- the real-clock crash
|
||||
// rode this path with legAnimationState still 0xCDCDCDCD). Discard replicant
|
||||
// marks instead.
|
||||
if (GetInstance() != ReplicantInstance)
|
||||
WriteSimulationUpdate(update_stream);
|
||||
else
|
||||
updateModel = 0; // drop accommodation-path marks
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user