#141 peer missiles: the launch frame read a STALE segment cache on replicants

Oracle: "missiles are firing in the direction the mech feet are facing ...
and then coming around to track the target", peer POV only -- the shooter's
own view is correct.

REPRODUCED AND MEASURED (scratchpad/night13/missileframe.sh, 2-node: only A
sweeps its torso and only A fires, so every REPLICANT line in B's log is the
mirror of one A salvo).  New [launchframe] receipt prints the yaw of the
launch forward vs the BODY forward on both nodes:

    master     n=165  |twistDelta| max=2.2962  mean=1.2283  >0.1rad: 100%
    REPLICANT  n=165  |twistDelta| max=0.0000  mean=0.0000  >0.1rad:   0%

segResolved=1 on BOTH, and segYaw == bodyYaw EXACTLY on the peer.

WHAT IT IS NOT.  Both sides already pass the mount segment (mislanch.cpp:363
master, :478 replicant mirror, both `GetSegmentIndex()` from task #67), and
the peer's torso data is fine end to end: records arrive (atUpd=2.44/-2.39,
rate 0.305), the copy extrapolates correctly (cur=-2.13987 target=-2.13987
copy=1), and the copy torso demonstrably writes its joint (PushTwist COPY
twist=-1.49601).  Hierarchy is identical too: same seg 18, same parentIdx 4,
non-null parent + joint subsystem on both.

ROOT CAUSE.  BTPushProjectile composed the frame BY HAND --
`mw.Multiply(seg->GetSegmentToEntity(), localToWorld)`.  But
EntitySegment::GetSegmentToEntity (SEGMENT.cpp:262) recomputes ONLY when
`segmentModified` is set, and the thing that sets it after a joint moves is
JointedMover::GetSegmentToWorld (JMOVER.cpp:136-146), which tests
AreJointsModified() and then marks every segment dirty.  Hand-composing skips
that, so you read whatever cache is sitting there.  On the MASTER that was
invisible -- the renderer/cockpit camera call GetSegmentToWorld for the local
mech every frame, AFTER the local torso pushes its joint, so the cache was
already correct.  A REPLICANT gets no such refresh: its cache stayed at the
BIND POSE, and the twist never reached the launch direction.

FIX.  Use the engine accessor, and set the joints-dirty flag first so it
actually refreshes (by fire time the frame's render pass has already consumed
and cleared it -- measured jointsDirty=0 on BOTH nodes).

RESULT (same bench):
    REPLICANT  max 0.0000 -> 2.1145   mean 0.0000 -> 0.8252   0% -> 64%

PARTIAL, and I am not claiming otherwise.  36% of peer salvos still read the
exact-zero stale signature while the master is 100%.  Forcing every per-joint
`jointModified` flag as well (GetSegmentToParent's own gate, SEGMENT.cpp:196)
was tried and moved the number by NOTHING -- 64% either way -- so the residual
is a different cause, most likely frame ORDER (the salvo mirror running before
the copy torso has posed that frame).  Cheap form kept.

Also fixes a SAMPLING TRAP in the torso probe: PushTwist sampled one shared
static every 30th call, and with a master torso and a copy torso ticking 1:1
every 30th call is always the SAME instance -- so the probe showed only the
local untwisted torso and hid the copy's writes entirely.  Now sampled per
instance-kind, which is what made the copy's correct joint writes visible and
moved the search downstream to the segment cache.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
This commit is contained in:
Joe DiPrima
2026-08-08 08:14:25 -05:00
co-authored by Claude Opus 5
parent 1ae57398f1
commit 05d7b5890a
4 changed files with 312 additions and 7 deletions
+12 -5
View File
@@ -897,14 +897,21 @@ void
// bring-up verification (env BT_TORSO_LOG; default OFF): show the first few
// joint writes so the per-frame path can be confirmed in a headless run.
static const int s_log = getenv("BT_TORSO_LOG") ? 1 : 0;
static int s_count = 0;
if (s_log && (s_count % 30) == 0 && s_count < 1800) // sample periodically to show the sweep
// ⚠ SAMPLING TRAP (fixed 2026-08-08, #141): this used to sample ONE shared
// static every 30th call. With a master torso and a replicant COPY torso
// both ticking, the calls alternate 1:1 -- so every 30th call is always the
// SAME instance, and the probe reported only the local (untwisted) torso
// while the copy's writes were invisible. Sample per instance-kind instead.
static const int s_log = getenv("BT_TORSO_LOG") ? 1 : 0;
static int s_count[2] = { 0, 0 };
const int kind = isDamagedCopy ? 1 : 0;
if (s_log && (s_count[kind] % 30) == 0 && s_count[kind] < 1800)
{
DEBUG_STREAM << "[torso] PushTwist node=" << (void*)node << " type=" << (int)jt
DEBUG_STREAM << "[torso] PushTwist " << (kind ? "COPY " : "master")
<< " node=" << (void*)node << " type=" << (int)jt
<< " twist=" << (float)twist << "\n" << std::flush;
}
++s_count;
++s_count[kind];
switch (jt) // node+0x10
{