gotcha #28: hand-composing an engine-derived transform reads a stale cache (replicant-only)

The #141 bug class, written up so it is not re-introduced.  GetSegmentToEntity
recomputes ONLY when segmentModified is set; JointedMover::GetSegmentToWorld is
what sets it -- and the binary's own GetMuzzlePoint @004b9948 goes through it
(FUN_00424da8), so every muzzle query in the 1995 image performs the
joints->segments refresh.  Four port sites hand-composed instead, one of them
commented "the faithful FUN_004b9948".

Records the four rules the investigation actually cost:
 (a) never hand-compose; call GetSegmentToWorld
 (b) never force the dirty flag to fix a stale read -- that stand-in scored
     IDENTICALLY to the faithful fix while patching only one consumer
 (c) "peer POV only" geometry bugs = suspect a cache the local render pass
     refreshes for free, before suspecting replication (it was provably fine)
 (d) a partial-looking score: check PREFIX vs interleaved before calling it
     partial -- these were a clean prefix ending when the peer first had a
     twist to carry, so the fix was complete and "64% fixed" was wrong
 (e) the probe trap: one shared static sampled every Nth call hides one of two
     alternating instances entirely

#141 closed with the full write-up; #148 filed for the torso replication
cadence (13 records in a 5-minute run) which is a separate, real problem and
likely bears on #37 and #70.

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 09:29:47 -05:00
co-authored by Claude Opus 5
parent e6c5ac951e
commit eebe7e61a4
2 changed files with 200 additions and 0 deletions
+53
View File
@@ -863,3 +863,56 @@ warning"), list what STARTS DRAWING at that event -- alarm-driven redraws,
state-change repaints -- before suspecting the event's logic; (d) the
operator's screenshot is worth ten theories: the red-faces capture identified
in one frame what three log-side hypotheses missed.
## 28. HAND-COMPOSING an engine-derived transform reads a STALE CACHE — and it only bites REPLICANTS (#141, 2026-08-08)
`EntitySegment::GetSegmentToEntity()` (`SEGMENT.cpp:262`) **recomputes only when
`segmentModified` is already set** — otherwise it hands back the cached matrix,
and if the segment has no parent it can never recompute at all. The thing that
sets that flag after a joint moves is `JointedMover::GetSegmentToWorld`
(`JMOVER.cpp:136-146`): it tests `AreJointsModified()` and, when set, walks the
whole segment table marking every entry dirty, then clears the joint flag.
The binary agrees exactly. `MechWeapon::GetMuzzlePoint @004b9948` ends in
`FUN_00424da8(owner, segment, out)`, which IS `GetSegmentToWorld`
instruction-for-instruction (`GetJointSubsystem``if (AreJointsModified())`
mark all → `ModifyJoints(False)``× localToWorld`). **So in the 1995 image
every muzzle query performs the joints→segments refresh.** [T1]
Four port sites had replaced that with `mw.Multiply(seg->GetSegmentToEntity(),
mech->localToWorld)` — including one commented "the faithful FUN_004b9948".
They skip the refresh and read whatever cache is present.
**Why it hid for a year:** the local mech is refreshed every frame anyway — the
renderer and cockpit camera call `GetSegmentToWorld` on it, *after* its torso
pushes the joint. So master-side output is correct and solo testing is clean.
A **replicant** gets no such pass: its cache stays at the BIND POSE. Measured on
a 2-node bench, peer missiles left along the LEG facing with `segYaw == bodyYaw`
EXACTLY (`twistDelta` 0.0000 over 165 salvos) while that same peer's copy torso
was demonstrably writing its joint (`PushTwist COPY twist=-1.49601`) from
correctly replicated records. Twist arrived, joint moved, segment never
re-derived.
Rules:
(a) **Never hand-compose `GetSegmentToEntity() × localToWorld`.** Call
`GetSegmentToWorld` — it is the binary's own path and it does the refresh.
(b) **Do NOT "fix" a stale transform by forcing the dirty flag.** Setting
`ModifyJoints(True)` at the read site made the symptom go away and scored
identically to the faithful fix — it was a stand-in that patched ONE
consumer and left every other peer segment reader stale. The binary only
ever *tests* that flag.
(c) A cached-transform bug is **master/replicant asymmetric by construction**.
If a geometry symptom is reported "peer POV only", suspect a cache that the
local render pass refreshes for free — before suspecting replication. Here
the replication was provably fine.
(d) When a fix lands at a partial percentage, **check whether the failures are
interleaved or a PREFIX** before calling it partial. These were a clean
prefix that ended the moment the peer first had a non-zero twist to carry —
i.e. the fix was complete and the remainder was correct behaviour. Reporting
it as "64% fixed" was wrong.
(e) Related probe trap: `Torso::PushTwist` sampled ONE shared static every 30th
call. With a master torso and a copy torso ticking 1:1, every 30th call is
always the SAME instance — the probe showed only the local untwisted torso
and hid the copy's writes entirely. Sample **per instance-kind** whenever
master and replicant objects share a diagnostic. (See also §gotcha on
process-wide statics serving the player's data as the replicant's.)