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:
co-authored by
Claude Opus 5
parent
e6c5ac951e
commit
eebe7e61a4
@@ -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.)
|
||||
|
||||
Reference in New Issue
Block a user