diff --git a/engine/MUNGA_L4/L4GLASSWIN.cpp b/engine/MUNGA_L4/L4GLASSWIN.cpp index 2da5202..93fd88c 100644 --- a/engine/MUNGA_L4/L4GLASSWIN.cpp +++ b/engine/MUNGA_L4/L4GLASSWIN.cpp @@ -1030,8 +1030,15 @@ static void ++wrote; } fclose(f); + // #140 receipt (ungated): `live=` is the diagnostic that matters. A save + // that runs with live=0 is the corruption case -- before the remembered- + // geometry cache it wrote a file containing ONLY the plasma line, which is + // what testers reported. It stays in the log so the FIELD can tell us which + // caller does that (teardown ordering, a drag after a round boundary, ...), + // which no bench here managed to reach. DEBUG_STREAM << "[glasswin] saved " << wrote << " window position(s) to " - << layoutFileName << "\n" << std::flush; + << layoutFileName << " (live=" << gWinCount + << " remembered=" << gLastGeomCount << ")\n" << std::flush; } // Public save trigger for registered external windows -- their WndProc calls @@ -1789,6 +1796,16 @@ void void BTGlassPanels_Destroy() { + // #140 receipt (ungated, one line per teardown): this function has two + // callers on the desktop path and the ORDER is what broke the layout file. + // A run that shows `entry #2 windows=0` is the double-destroy, on the + // record, without needing to catch the cfg mid-corruption. + { + static int s_destroyN = 0; + DEBUG_STREAM << "[glasswin] destroy entry #" << ++s_destroyN + << " windows=" << gWinCount << "\n" << std::flush; + } + // Backstop for a clean teardown (WM_EXITSIZEMOVE already caught every // finished drag); no-op unless mode==save. GUARDED on there being windows: // this function has TWO callers (~PadRIO and ~LBE4ControlsManager), so on diff --git a/scratchpad/night13/close52.py b/scratchpad/night13/close52.py new file mode 100644 index 0000000..bbbacd7 --- /dev/null +++ b/scratchpad/night13/close52.py @@ -0,0 +1,90 @@ +"""Night 13: report the #52 root cause on the tracker. ASCII only.""" +import sys +sys.path.insert(0, r"C:\git\bt411\scratchpad\night7") +import gitea + +BODY = """**ROOT CAUSE FOUND AND FIXED (2026-08-07, commit `6a96fb6`)** -- but read the +"what is NOT proven" section before treating this as closed. + +## The defect + +A replicant cannot start walking between gait-change records. + +The port's body `case 4` (the task-#64 lockstep twin, mech2.cpp) is an INSERTION sitting between +`case 0` and the advance group. In the binary it is a MEMBER of that group -- `FUN_004a5678` +@004a5678 reads `case 2,3,`**`4`**`,5,8,...` with no turn block and no speed exit [T1] -- so case +0's fallthrough is meant to land on `Advance()`. The insertion intercepted it. + +On a replicant that is not a race, it is an identity: + +* case 0 arms walk iff `standSpeed < bodyTargetSpeed` +* the inserted block resets iff `standSpeed < bspd`, and `bspd` **IS** `bodyTargetSpeed` on a replicant + +Same expression. Arm and reset fire on the same frame, every frame. A peer parked at Standing with a +live replicated demand never cycles: `bodyCycleSpeed` stays 0 while position advances from dead +reckoning. That is the skate. (Reverse is dead the same way -- both sides test `< ZeroSpeed`.) + +## Why it appeared when it did + +This is the sequel to `e91d447` (#82). Before that commit the replicant branch read the LOCAL +mapper's `speedDemand` -- a dead cell on a peer, 0 forever -- so the exit never fired and the +fallthrough worked BY ACCIDENT. Fixing the dead cell (correctly) closed the accidental escape +hatch, and the trn-lock skate came back as a Standing-lock skate. + +## Why masters were unaffected + +Two reasons, either sufficient: their two tests read DIFFERENT cells (`bodyTargetSpeed` held at +last-sent by the gait mirror, vs the live mapper `speedDemand`), so they only stall in the window +where those disagree; and the master's body channel runs `mj=0` and writes no joints, so its stall +is invisible -- the leg channel, whose case 0 falls through correctly, drives pose and travel. + +## The load-bearing detail + +`mech4.cpp`'s "stand; case 0 walk-begins next tick" is not an aside. A peer's body state is set from +`record->legState` only on **type-3 edges**, and ENTERING Standing emits one while LEAVING it does +not. Between gait-change records a replicant is REQUIRED to derive walking itself from the +replicated demand. The insertion removed that ability. + +This also explains the shape of the field data: the lock needs a mech holding a STEADY demand, so a +mech whose gait keeps changing is continually rescued by records. Night 13's four episodes all carry +`bodyTargetSpeed` 39-48 held across 100-400 frames, and each ended when that mech next changed gait. + +## Fix + +`case 0` -> `goto advance_body_normally`, the leg twin's own idiom, restoring the binary's structure +without touching the #64/#82 turn logic. `BT_NO_BODY_FALLTHRU=1` reverts. + +## Measured (2-node, `scratchpad/night13/skatelock.sh`) + +| | legacy | fixed | +|---|---|---| +| STANDING-LOCK seconds | **336 consecutive**, `bspd=39.2324 bts=39.2324` identical every line | **0**, every pass | +| master body-Standing samples | 52 | 21 | +| turn-in-place | -- | body state 4 x9 / leg state 4 x8, armed in lockstep | + +## What is NOT proven [T3] + +That this accounts for the night-13 episodes. The lock is proven and proven removed; the link to the +field symptom is INFERENCE -- a locked peer has `bodyCycleSpeed==0` and never advances its clip, so +locked + translating IS the `[skate]` signature by construction -- but no bench caught the two +together. Four rigs failed to reproduce the symptom end-to-end. + +The `[skate]` line now carries `bstate=`, which is the diagnostic tonight's logs lacked. Next +playtest settles it: episodes gone -> confirmed; any survivor names its own state. + +**Correction for the record:** the night-12 `skatebench` "reproductions" (6 episodes, `sk_run.out`) +were a DETECTOR ARTIFACT, not this bug. The first detector build tested only `legCycleSpeed==0`, +which is NORMAL on a peer -- the body channel poses it -- so it fired on every healthy movement +phase. It was corrected the same day to require both channels idle. Old-format lines +(`legCycleSpeed=`, no `bodyCyc=`) are not evidence of anything. + +Related: #130 (Vulture skating) is very likely the same defect -- re-test it against this build +before spending separate effort.""" + +gitea.comment(52, BODY) +gitea.comment(130, + "Cross-ref: #52's root cause was found and fixed 2026-08-07 (`6a96fb6`) -- a replicant could not " + "start walking between gait-change records, so a peer parked at Standing with a live replicated " + "demand kept its position advancing with a dead animation channel. That is the same shape as the " + "skating reported here. **Re-test this against a build newer than `6a96fb6` before investigating " + "separately.** Full write-up in #52.") diff --git a/scratchpad/night13/layoutround.sh b/scratchpad/night13/layoutround.sh new file mode 100644 index 0000000..2e996a7 --- /dev/null +++ b/scratchpad/night13/layoutround.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# ========================================================================= +# #140 -- reach the teardown the way the FIELD does: a MISSION that ENDS. +# +# Three earlier attempts never exercised BTGlassPanels_Destroy at all: +# bt_kill_ours uses `taskkill /F` (no dtors), and a graceful WM_CLOSE on a +# solo -egg run did not reach it either. The path that matters is the one +# testers hit constantly -- the controls manager is destroyed and rebuilt at +# every ROUND boundary, which is exactly what the relay drives in MP. +# +# Panels ON for both nodes; the relay starts a round and the round ends. +# The receipt to read is `[glasswin] destroy entry #N windows=M`: +# #1 windows=7 then #2 windows=0 -> the double-destroy, confirmed +# and then exactly ONE "saved N window position(s)" line, with the cfg intact. +# ========================================================================= +set -x +. /c/git/bt411/scratchpad/night6/bench_common.sh +cd /c/git/bt411/content || exit 1 + +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2 +rm -f glass_layout.cfg lr_a.log lr_b.log lr_relay.log + +bt_expert_egg MP.EGG LR.EGG +sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" LR.EGG + +( export BT_GLASS_PANELS=1 BT_GLASS_LAYOUT=save BT_MP_LOG=1 + bt_launch lr_b.log LR.EGG 0x0C -net 1601 ) +sleep 2 +( export BT_GLASS_PANELS=1 BT_GLASS_LAYOUT=save BT_MP_LOG=1 + bt_launch lr_a.log LR.EGG 0x03 -net 1501 ) +sleep 5 +python ../tools/btconsole.py LR.EGG 127.0.0.1:1501 127.0.0.1:1601 > lr_relay.log 2>&1 & +RELAY=$! +sleep 180 +kill $RELAY 2>/dev/null +sleep 5 +bt_kill_ours +sleep 2 +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 3 + +echo "=================== #140 TEARDOWN RECEIPTS ===================" +for n in a b; do + echo "--- node $n: destroy entries ---" + grep -a "destroy entry" lr_${n}.log + echo "--- node $n: SaveLayout receipts ---" + grep -a "window position" lr_${n}.log +done +echo +echo "--- glass_layout.cfg entries ---" +grep -aE "^[^#]+=" glass_layout.cfg 2>/dev/null || echo "(no file)" +echo -n "entry count: "; grep -acE "^[^#]+=" glass_layout.cfg 2>/dev/null diff --git a/scratchpad/night13/layoutteardown.sh b/scratchpad/night13/layoutteardown.sh new file mode 100644 index 0000000..c9e0ea0 --- /dev/null +++ b/scratchpad/night13/layoutteardown.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# ========================================================================= +# #140 -- exercise the TEARDOWN path, which is where the bug lives. +# +# WHY THIS EXISTS: bench_common's bt_kill_ours uses `taskkill /F`. That is a +# HARD kill -- no destructors, so ~LBE4ControlsManager / ~PadRIO never run and +# BTGlassPanels_Destroy (the function that saves, and the one that ran twice) +# is never reached. Both of the first attempts died before teardown and the +# result looked like a difference between builds when it was really a +# difference in how far each run got. A graceful `taskkill` (NO /F) posts +# WM_CLOSE and lets the dtor chain run. +# +# Pass a label; run it once on the pre-fix build and once on the fixed one. +# ========================================================================= +LABEL="${1:-run}" +. /c/git/bt411/scratchpad/night6/bench_common.sh +cd /c/git/bt411/content || exit 1 + +taskkill //F //IM btl4.exe > /dev/null 2>&1 +sleep 2 +rm -f glass_layout.cfg lt_${LABEL}.log + +( export BT_GLASS_PANELS=1 BT_GLASS_LAYOUT=save + bt_launch lt_${LABEL}.log ARENA1.EGG 0x03 ) + +# wait for the panels to actually exist before asking for a shutdown +for i in $(seq 1 60); do + grep -aq "per-display cockpit up" lt_${LABEL}.log 2>/dev/null && break + sleep 2 +done +grep -aq "per-display cockpit up" lt_${LABEL}.log || { echo "PANELS NEVER CAME UP"; } +sleep 20 # let it settle into the mission + +PID=$(cat "$BT_PIDFILE" 2>/dev/null | head -1) +echo "graceful close of pid $PID" +taskkill //PID "$PID" > /dev/null 2>&1 # NO /F -- WM_CLOSE, dtors run +for i in $(seq 1 30); do + tasklist //FI "PID eq $PID" 2>/dev/null | grep -q btl4 || break + sleep 1 +done +taskkill //F //IM btl4.exe > /dev/null 2>&1 # backstop +rm -f "$BT_PIDFILE" +sleep 2 + +echo "=============== $LABEL ===============" +echo "--- every SaveLayout receipt (TWO lines = the double-save) ---" +grep -a "window position" lt_${LABEL}.log +echo +echo "--- glass_layout.cfg entries after teardown ---" +grep -aE "^[^#]+=" glass_layout.cfg 2>/dev/null || echo "(no file)" +echo +echo -n "entry count: "; grep -acE "^[^#]+=" glass_layout.cfg 2>/dev/null || echo 0