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
@@ -0,0 +1,147 @@
|
||||
"""Night 13: report #141 (fixed) and file the torso-cadence follow-up. ASCII only."""
|
||||
import sys
|
||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
||||
import gitea
|
||||
|
||||
BODY_141 = """**ROOT CAUSE FOUND AND FIXED** (`f01de8c` + sweep `e6c5ac9`), reproduced and measured on a
|
||||
2-node bench (`scratchpad/night13/missileframe.sh`).
|
||||
|
||||
## Reproduced
|
||||
|
||||
Only A sweeps its torso and only A fires, so every REPLICANT line in B's log mirrors one A salvo.
|
||||
New `[launchframe]` receipt (`BT_PROJ_LOG`) prints the yaw of the launch forward vs the BODY forward:
|
||||
|
||||
| | n | max abs(twistDelta) | mean | >0.1 rad |
|
||||
|---|---|---|---|---|
|
||||
| master | 165 | 2.2962 | 1.2283 | **100%** |
|
||||
| REPLICANT | 165 | **0.0000** | 0.0000 | **0%** |
|
||||
|
||||
`segResolved=1` on both, and `segYaw == bodyYaw` EXACTLY on the peer -- the launch frame was the
|
||||
bind pose.
|
||||
|
||||
## What it was NOT
|
||||
|
||||
Everything upstream was already correct, which is why it looked like netcode. Both sides pass the
|
||||
mount segment (`GetSegmentIndex()`, task #67 -- master `mislanch.cpp:363`, replicant mirror `:478`).
|
||||
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 identical on both nodes
|
||||
-- same seg 18, same `parentIdx=4`, non-null parent and joint subsystem.
|
||||
|
||||
The twist reached the joint and died at the **segment cache**.
|
||||
|
||||
## Root cause
|
||||
|
||||
`MechWeapon::GetMuzzlePoint` `@004b9948` ends in `FUN_00424da8(owner, segment, out)`, which is
|
||||
`JointedMover::GetSegmentToWorld` instruction-for-instruction:
|
||||
|
||||
```c
|
||||
iVar1 = FUN_00417ab4(param_1 + 0x31c); // GetJointSubsystem()
|
||||
if (*(int *)(iVar1 + 0xfc) != 0) { // AreJointsModified() <- TESTED, never set
|
||||
... walk owner+0x300, seg+0xc = 1 ... // ModifySegment() on every segment
|
||||
*(int *)(iVar1 + 0xfc) = 0; // ModifyJoints(False)
|
||||
}
|
||||
FUN_0040b104(out, FUN_004244dc(seg), owner+0xd0); // x localToWorld
|
||||
```
|
||||
|
||||
**In the 1995 image every muzzle query performs the joints->segments refresh.** Our
|
||||
`BTResolveWeaponMuzzle` -- labelled "the faithful FUN_004b9948" -- hand-composed
|
||||
`GetSegmentToEntity() x localToWorld` and skipped it. `GetSegmentToEntity` only recomputes when
|
||||
`segmentModified` is already set (`SEGMENT.cpp:262`), so it returned a stale cache. On the MASTER
|
||||
that was invisible (the render pass refreshes the local mech every frame, after its torso pushes
|
||||
the joint); a REPLICANT gets no such refresh, so peer muzzles sat at the bind pose.
|
||||
|
||||
## Fix
|
||||
|
||||
Route the muzzle path through the engine accessor, where the binary puts it. **No forced dirty
|
||||
flag** -- an earlier attempt set `ModifyJoints(True)` and scored the same, so it bought nothing and
|
||||
was removed; the binary only ever tests that flag.
|
||||
|
||||
Swept the same unfaithful pattern at three more sites: the generic segment->world bridge, the
|
||||
damage-effect anchor, and **the energy-beam gun port** -- a peer's BEAM had the identical exposure
|
||||
and would also have originated from the untwisted gun port. Repo-wide there is now exactly one
|
||||
`GetSegmentToEntity` call outside `SEGMENT.cpp`: inside `GetSegmentToWorld` itself, after the
|
||||
refresh.
|
||||
|
||||
## Result
|
||||
|
||||
| | n | max | mean | >0.1 rad |
|
||||
|---|---|---|---|---|
|
||||
| master | 165 | 2.1719 | 1.2781 | 100% |
|
||||
| REPLICANT | 165 | **2.0907** | 0.8201 | **64%** |
|
||||
|
||||
**The 64% is not a partial fix.** The failures are a contiguous PREFIX with zero interleaved cases:
|
||||
|
||||
```
|
||||
ZZZZ...(60)...ZZZZXXXX...(105)...XXXX
|
||||
```
|
||||
|
||||
and they end exactly when the peer acquires a twist to carry:
|
||||
|
||||
```
|
||||
first torso RECORD received : line 206
|
||||
first copy currentTwist != 0 : line 1016
|
||||
first CORRECT launch frame : line 1054 (38 lines = probe sampling granularity)
|
||||
```
|
||||
|
||||
Those 60 salvos fired while the replicated twist was genuinely 0, so launching along the body
|
||||
facing was CORRECT. Once the peer has a twist, 100% of launches carry it.
|
||||
|
||||
## Field-verify
|
||||
|
||||
Unreleased. Next playtest: have a peer watch a twisted mech fire missiles -- rounds should leave
|
||||
along the torso, not the feet. Also worth checking beams for the same reason (same fix).
|
||||
|
||||
Follow-up filed separately: the peer's torso takes far too long to FIRST acquire the master's
|
||||
twist."""
|
||||
|
||||
TITLE_NEW = "Peer torso twist takes far too long to first sync -- only 13 update records across a 5-minute run"
|
||||
|
||||
BODY_NEW = """Split out of #141, whose launch-frame defect is fixed. This is a separate, measured
|
||||
problem in the torso REPLICATION CADENCE.
|
||||
|
||||
## Measurement
|
||||
|
||||
From the #141 bench (`scratchpad/night13/missileframe.sh` / `missileframe2.sh`, 2 nodes, node A
|
||||
sweeping its torso continuously at 0.35 rad/s for the whole run):
|
||||
|
||||
```
|
||||
first torso RECORD received on the peer : line 206
|
||||
first copy currentTwist != 0 : line 1016
|
||||
```
|
||||
|
||||
and across the entire ~5 minute run the peer received only **13** `[torso-rec-rx]` records, despite
|
||||
the master's twist changing continuously the whole time.
|
||||
|
||||
So the master was twisted from very early on, while the peer's copy torso reported `currentTwist`
|
||||
of exactly 0 for a long stretch afterwards. The extrapolator itself is fine once fed --
|
||||
`ComputeTargetTwist` predicts `twistAtUpdate + twistRate * elapsed` and the copy tracks its target
|
||||
exactly (`cur=-2.13987 target=-2.13987 copy=1`). The problem is how rarely it is fed, and how late
|
||||
the first useful feed arrives.
|
||||
|
||||
## Why it matters
|
||||
|
||||
* It is the entire reason #141's fix reads 64% instead of 100% on the bench -- 60 salvos fired
|
||||
before the peer had any twist to carry.
|
||||
* A peer's torso will visibly LAG or sit straight while the mech is actually twisted. That is
|
||||
plausibly relevant to **#37** (MadCat torso is BACKWARDS) and **#70** (torso twist stops working
|
||||
after respawn) -- worth re-testing both against this once it is understood.
|
||||
|
||||
## Not yet investigated
|
||||
|
||||
Whether 13 records is the authentic cadence (the binary may deliberately send torso updates rarely
|
||||
and lean on `twistRate` extrapolation to cover the gaps -- in which case the bug is that our
|
||||
extrapolation is not running or not seeded until late), or whether our send-side gate is simply too
|
||||
conservative. `Torso::WriteUpdateRecord` snapshots `twistAtUpdate = currentTwist` at send, so the
|
||||
send trigger is the thing to read first.
|
||||
|
||||
Diagnostics already in place: `BT_TORSO_LOG` gives `[torso-rec-rx]` (receive), `[torso-copy]`
|
||||
(the copy's cur/target/atUpd/rate), and `[torso] PushTwist master|COPY` (per instance-kind -- note
|
||||
that probe previously sampled one shared static every 30th call, which with two torsos ticking 1:1
|
||||
always reported the SAME instance and hid the copy entirely; fixed in `05d7b58`)."""
|
||||
|
||||
gitea.comment(141, BODY_141)
|
||||
gitea.call("/issues/141", method="PATCH", payload={"state": "closed"})
|
||||
print("commented + closed #141")
|
||||
num = gitea.create(TITLE_NEW, BODY_NEW)
|
||||
print("created #%d" % num["number"])
|
||||
Reference in New Issue
Block a user