#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:
co-authored by
Claude Opus 5
parent
1ae57398f1
commit
05d7b5890a
@@ -1493,8 +1493,57 @@ void
|
||||
EntitySegment *seg = sm->GetSegment(muzzle_seg);
|
||||
if (seg != 0)
|
||||
{
|
||||
AffineMatrix mw;
|
||||
mw.Multiply(seg->GetSegmentToEntity(), sm->localToWorld);
|
||||
// #141 -- USE THE ENGINE ACCESSOR, not a hand-rolled
|
||||
// GetSegmentToEntity() x localToWorld.
|
||||
//
|
||||
// EntitySegment::GetSegmentToEntity (SEGMENT.cpp:262)
|
||||
// recomputes ONLY when `segmentModified` is set; otherwise it
|
||||
// returns the CACHED matrix. The thing that sets that flag
|
||||
// after a joint moves is JointedMover::GetSegmentToWorld
|
||||
// (JMOVER.cpp:136-146): it tests AreJointsModified() and, when
|
||||
// set, marks EVERY segment dirty and clears the flag. Compose
|
||||
// the matrix by hand and you skip that entirely -- you read
|
||||
// whatever cache happens to be sitting there.
|
||||
//
|
||||
// On the MASTER that was invisible: the renderer / cockpit
|
||||
// camera call GetSegmentToWorld for the LOCAL mech every frame,
|
||||
// so the cache was already fresh when we fired. A REPLICANT
|
||||
// gets no such refresh, so its cache stayed at the BIND POSE
|
||||
// and the torso twist never reached the launch frame.
|
||||
//
|
||||
// Measured (scratchpad/night13/missileframe.sh, 165 salvos
|
||||
// mirrored 1:1): master |twistDelta| max 2.2962 / mean 1.2283,
|
||||
// 100% > 0.1 rad -- REPLICANT max 0.0000, mean 0.0000, 0%,
|
||||
// with segResolved=1 and segYaw == bodyYaw EXACTLY, while that
|
||||
// same peer's copy torso was demonstrably writing its joint
|
||||
// (`PushTwist COPY ... twist=-1.49601`) off correctly
|
||||
// replicated records (`cur=-2.13987 target=-2.13987 copy=1`).
|
||||
// Twist arrived, joint moved, segment cache never refreshed.
|
||||
// That is #141: "missiles launch along the LEG/FOOT facing,
|
||||
// then curve to the target -- peer POV only".
|
||||
// FORCE the recompute. GetSegmentToWorld only refreshes when
|
||||
// AreJointsModified() is set, and by fire time the frame's
|
||||
// renderer/camera pass has already consumed and cleared that
|
||||
// flag on BOTH nodes (measured: jointsDirty=0 master AND peer).
|
||||
// On the master the cache it left behind is correct, because
|
||||
// that pass ran AFTER the local torso pushed its joint. On a
|
||||
// replicant the cache is stale, so seg 18 returned its
|
||||
// bind-pose matrix (segYaw == bodyYaw EXACTLY) even though the
|
||||
// hierarchy is identical -- same parentIdx 4, same non-null
|
||||
// parent + joint subsystem. Setting the flag makes
|
||||
// GetSegmentToWorld mark every segment dirty so the whole
|
||||
// chain re-derives from the CURRENT joint angles. Costs one
|
||||
// segment-table walk per salvo.
|
||||
// MEASURED: this alone takes the replicant from 0% to 64% of
|
||||
// salvos carrying the twist. Also forcing every per-joint
|
||||
// `jointModified` flag (GetSegmentToParent's own gate,
|
||||
// SEGMENT.cpp:196) was tried and moved the number by NOTHING
|
||||
// -- 64% either way -- so the residual 36% is a different
|
||||
// cause, not per-joint cache staleness. Kept the cheap form.
|
||||
if (JointSubsystem *jsf = sm->GetJointSubsystem())
|
||||
jsf->ModifyJoints(True);
|
||||
LinearMatrix mw;
|
||||
sm->GetSegmentToWorld(*seg, &mw);
|
||||
mw.GetFromAxis(X_Axis, &ax);
|
||||
mw.GetFromAxis(Y_Axis, &ay);
|
||||
mw.GetFromAxis(Z_Axis, &az);
|
||||
@@ -1507,6 +1556,45 @@ void
|
||||
sm->localToWorld.GetFromAxis(Y_Axis, &ay);
|
||||
sm->localToWorld.GetFromAxis(Z_Axis, &az);
|
||||
}
|
||||
// #141 DIAGNOSTIC (BT_PROJ_LOG). The peer-POV report is that the
|
||||
// round leaves along the LEG facing, ignoring torso twist, while the
|
||||
// shooter's own view is correct. Both nodes pass GetSegmentIndex()
|
||||
// as the mount frame, so if this is real the difference is whether
|
||||
// the SEGMENT actually carries the twist on a replicant. Print the
|
||||
// frame we launched through on BOTH sides: twistDelta is the yaw of
|
||||
// the launch forward vs the BODY forward, so it should equal the
|
||||
// torso twist on the master and MUST match on the replicant. A
|
||||
// replicant reading ~0 while the master reads non-zero IS the bug.
|
||||
if (getenv("BT_PROJ_LOG"))
|
||||
{
|
||||
UnitVector bz;
|
||||
sm->localToWorld.GetFromAxis(Z_Axis, &bz);
|
||||
const float kPi = 3.14159265f;
|
||||
float segYaw = atan2f(-(float)az.x, -(float)az.z);
|
||||
float bodyYaw = atan2f(-(float)bz.x, -(float)bz.z);
|
||||
float dYaw = segYaw - bodyYaw;
|
||||
while (dYaw > kPi) dYaw -= 2.0f * kPi;
|
||||
while (dYaw < -kPi) dYaw += 2.0f * kPi;
|
||||
// #141 probe 2: GetSegmentToEntity only RECOMPUTES when
|
||||
// (segmentModified && parentSegment). A null parent means it can
|
||||
// never recompute -- it returns the bind-pose baseOffset forever,
|
||||
// which would read as segYaw == bodyYaw exactly. Print the
|
||||
// hierarchy + joint-dirty state so master and peer can be diffed.
|
||||
EntitySegment *pseg = (muzzle_seg >= 0) ? sm->GetSegment(muzzle_seg) : 0;
|
||||
const void *parent = (pseg != 0) ? (const void *)pseg->GetParent() : 0;
|
||||
int parentIdx = (pseg != 0) ? pseg->GetParentIndex() : -99;
|
||||
JointSubsystem *js = sm->GetJointSubsystem();
|
||||
DEBUG_STREAM << "[launchframe] "
|
||||
<< (sm->GetInstance() == Entity::ReplicantInstance
|
||||
? "REPLICANT" : "master ")
|
||||
<< " seg=" << muzzle_seg << " segResolved=" << haveFrame
|
||||
<< " segYaw=" << segYaw << " bodyYaw=" << bodyYaw
|
||||
<< " twistDelta=" << dYaw
|
||||
<< " parent=" << parent << " parentIdx=" << parentIdx
|
||||
<< " joints=" << (void *)js
|
||||
<< " jointsDirty=" << (js != 0 ? (int)js->AreJointsModified() : -1)
|
||||
<< "\n" << std::flush;
|
||||
}
|
||||
p.vel.x = ax.x*launch_velocity->x + ay.x*launch_velocity->y - az.x*launch_velocity->z;
|
||||
p.vel.y = ax.y*launch_velocity->x + ay.y*launch_velocity->y - az.y*launch_velocity->z;
|
||||
p.vel.z = ax.z*launch_velocity->x + ay.z*launch_velocity->y - az.z*launch_velocity->z;
|
||||
|
||||
Reference in New Issue
Block a user