From cc5631a2f3632956122ec3a167fc25af28b9f9f9 Mon Sep 17 00:00:00 2001 From: Sylpheed RE agent Date: Mon, 24 Aug 2026 14:54:57 +0000 Subject: [PATCH] =?UTF-8?q?re:=20the=20mission=20clock=20is=20running=20?= =?UTF-8?q?=E2=80=94=20refute=20the=20stopped-clock=20explanation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prime suspect from the previous iteration was that the phase scheduler simply is not running, which would have made every arrival result meaningless. It is refuted. timer_probe.py takes three equally-spaced snapshots of the 32 MB game heap with no pilot -- exactly the condition where nothing had been observed to change -- and keeps words whose two successive deltas are both positive and agree within 12 %, so linear rather than merely noisy. 286 words qualify, with a large cluster advancing in lockstep at 16.5 per second. That rate is not a coincidence: the existing performance notes put Canary playback on this box at roughly 14-19 fps, and the dominant counter sits inside that band, so these read as per-frame counters. Which gives a mundane explanation for six arrival-free runs. If the scheduler is frame-driven and the title targets 30 Hz, game time advances at about 55 % of wall-clock here, so the 168, 190 and 240 second runs covered roughly 92, 105 and 132 seconds of game time. Route_S02 schedules phase-1 arrivals at 90, 120, 170, 210 and 240, so the longest run passed the first two and came nowhere near the last three. No model has to be wrong for the observations to be empty. Kept at 🟡, not promoted: two links are assumed rather than measured -- that these counters are frame counters, and that the game's tick is 30 Hz. The values also do not fit a naive frame count, since the cluster read 14193 about 255 s after emulator start, which matches neither 16.5 nor 30 per second, so their origin is genuinely unknown and no counter is claimed to be the mission clock. Next is one long run, ~350 s of probe, watching for a 0 -> n transition near t = 163 s and 218 s wall, where the t = 90 and t = 120 route entries land at 55 % speed. If 350 s still yields nothing, the frame-rate explanation is itself refuted and the event-gated model returns as the front-runner. The ~210 s title movie at boot is the binding constraint on how much game time one turn can observe. --- docs/re/BACKLOG.md | 15 ++++-- docs/re/mission-clock-advances.md | 80 +++++++++++++++++++++++++++++++ tools/re-capture/timer_probe.py | 69 ++++++++++++++++++++++++++ tools/re-capture/timer_session.sh | 10 ++++ 4 files changed, 170 insertions(+), 4 deletions(-) create mode 100644 docs/re/mission-clock-advances.md create mode 100755 tools/re-capture/timer_probe.py create mode 100755 tools/re-capture/timer_session.sh diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index c3d696c..1e6719a 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -360,10 +360,17 @@ search cannot find a *schedule*. 2×24/4×1/8×4/18×12). ✅ **Losses require the player**: an unpiloted run held at exactly 300 craft for 240 s / 22 samples, vs 16–20 losses in each piloted run — so NPC crossfire destroys nothing on its own. 🔴 **Zero `0→n` arrivals in either - condition**, ~15 min cumulative. ❔ **Prime suspect: the phase clock may not be - advancing at all** — never checked. **Next: find the mission timer** (HUD digits - or a fixed-rate counter); if it is stopped, every arrival result so far is - measuring a stopped clock. + condition**, ~15 min cumulative. 🔴 **Prime suspect REFUTED (2026-08-24): + the clock is running** — [`mission-clock-advances.md`](mission-clock-advances.md). + With no pilot, 286 heap words advance linearly, a large cluster in lockstep at + **16.5/s**, which is the emulator's known ~14–19 fps. 🟡 New leading + explanation: **the runs were far too short in GAME time** — at ~55% of + wall-clock, the longest 240 s run reached only t≈132, past the t=90/120 route + entries but nowhere near t=170/210/240. **Next: one long run** (~350 s probe) + watching for `0→n` near t≈163 s and 218 s wall. If still nothing, the + frame-rate explanation is refuted and event-gating returns as front-runner. + ⚠️ The ~210 s title movie at boot is the binding constraint on observable game + time per turn. Earlier framing: [`mission-per-record-strength.md`](mission-per-record-strength.md) — one run gives 116 records/300 craft, the next 42/170, same disc, save and script. Save diff --git a/docs/re/mission-clock-advances.md b/docs/re/mission-clock-advances.md new file mode 100644 index 0000000..4ccf3c8 --- /dev/null +++ b/docs/re/mission-clock-advances.md @@ -0,0 +1,80 @@ +# The mission clock is running — the runs were too short in *game* time + +Status: 🔴 "the phase clock is stopped" is refuted; ✅ something advances +linearly at ~16.5–17 /s; 🟡 the leading explanation for six arrival-free runs is +now that they covered far less game time than wall-clock time; ❔ the counters' +exact identity is not established. + +## 🔴 Refuted: the clock is not stopped + +[mission-arrival-watch.md](mission-arrival-watch.md) named this the prime +suspect — every "no arrival" result was equally consistent with a scheduler that +simply was not running, and nothing had checked. + +`tools/re-capture/timer_probe.py` takes three equally-spaced snapshots of the +game heap (`0xBC000000–0xBE000000`, 32 MB) **with no pilot**, i.e. in exactly the +condition where nothing was observed to change, and looks for words whose two +successive deltas are both positive and agree within 12 % — linear, not merely +noisy. + +``` +actual gaps: 20.1s, 20.5s region 32.0 MB +linearly-increasing words: 286 +rate histogram (/s): [(17, 111), (27, 27), (44, 19), (1, 10), (47, 10), ...] + +va 0xbc4428a8 14193 -> 14519 -> 14863 16.5/s +va 0xbc65c948 14195 -> 14521 -> 14865 16.5/s +va 0xbc668648 14193 -> 14519 -> 14863 16.5/s (and many more identical) +``` + +286 counters advance steadily, a large cluster of them in lockstep at **16.5 /s**. +The guest is running. The stopped-clock explanation is dead. + +## 🟡 What this probably means: the runs were far too short + +**16.5 /s is the emulator's frame rate, not a coincidence.** The existing +performance work records Canary playback on this box at roughly **14–19 fps**, +and the dominant counter rate sits squarely inside that band. These read as +per-frame counters. + +If the mission's scheduler is frame-driven and the title targets 30 fps, then +game time advances at about **55 % of wall-clock** here. Every run so far: + +| run window (wall) | approximate game time | +|---|---| +| 168 s | ~92 s | +| 190 s | ~105 s | +| 240 s | ~132 s | + +`Route_S02.tbl` schedules phase-1 arrivals at t = 90, 120, 170, 210, 240. So the +longest run reached roughly t = 132 of game time — **past the first two +scheduled arrivals but nowhere near the last three**, and the shorter runs barely +reached the first. That is a mundane and sufficient explanation for a string of +empty results, and it does not require the event-gated model to be wrong or the +timetable to be in frames. + +It is 🟡 rather than ✅ because two links in it are assumed, not measured: that +these counters are frame counters, and that the game's own tick is 30 Hz. + +## ❔ What the counters are is not established + +The value does not fit a naive frame count. The dominant cluster read 14193 at +the first snapshot, roughly 255 s of wall-clock after emulator start — 14193 +frames at 16.5 /s would be ~860 s, and at 30 /s ~473 s. Neither matches, so +these are not simply "frames since boot". They advance in lockstep and at frame +rate, but their origin is unknown and no claim is made about which one, if any, +is *the* mission clock. + +## Next: one long run + +The decisive test is now cheap in design and expensive only in wall-clock: run +the arrival watch for the longest window the turn allows (~350 s of probe after a +~210 s boot) and check for `0 → n` around **t ≈ 163 s and 218 s wall**, which is +where the t = 90 and t = 120 route entries land at 55 % speed. If arrivals appear +there, the timetable is in game-seconds and the clock model is confirmed. If 350 s +still yields nothing, the frame-rate explanation is itself refuted and the +event-gated model returns as the front-runner. + +The boot's ~210 s title movie is the binding constraint on how much game time a +single turn can observe, and is worth attacking separately if this becomes the +limiting factor. diff --git a/tools/re-capture/timer_probe.py b/tools/re-capture/timer_probe.py new file mode 100755 index 0000000..f0019eb --- /dev/null +++ b/tools/re-capture/timer_probe.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Does mission time advance at all? Scan for a linearly-increasing counter. + +Six runs have shown no arrival, and one surviving explanation is that the +mission's phase clock is simply not running under these conditions +(mission-arrival-watch.md). That has never been checked. If nothing in the game +heap advances linearly, the clock is stopped and every arrival result so far is +measuring a stopped clock. If something does, that explanation is dead and a +timer has been located as a bonus. + +Three snapshots, equally spaced. A counter is a word whose two successive deltas +are both positive and agree within a tolerance -- i.e. it is linear, not just +noisy -- at a plausible rate. +""" +import os, sys, time, struct, collections +sys.path.insert(0, __file__.rsplit('/', 1)[0]) +import gmem, gworld + +LO, HI = 0xBC000000, 0xBE000000 # the heap holding roster records + craft + +def snap(fd): + lo, hi = gmem.va_to_off(LO), gmem.va_to_off(HI) + out, pos = bytearray(), lo + while pos < hi: + n = min(1 << 24, hi - pos) + out += os.pread(fd, n, pos) + pos += n + return bytes(out), lo + +def main(): + dt = int(sys.argv[1]) if len(sys.argv) > 1 else 20 + w = gworld.World(); fd = w.fd + print('scanning %#x..%#x, %ds apart' % (LO, HI, dt), flush=True) + a, base = snap(fd); ta = time.time() + time.sleep(dt) + b, _ = snap(fd); tb = time.time() + time.sleep(dt) + c, _ = snap(fd); tc = time.time() + d1, d2 = tb - ta, tc - tb + print('actual gaps: %.1fs, %.1fs region %.1f MB' % (d1, d2, len(a) / 2**20)) + + n = min(len(a), len(b), len(c)) + cand = [] + for k in range(0, n - 3, 4): + va = struct.unpack_from('>I', a, k)[0] + vb = struct.unpack_from('>I', b, k)[0] + vc = struct.unpack_from('>I', c, k)[0] + if not (va < vb < vc): continue + e1, e2 = vb - va, vc - vb + if e1 > 1 << 28 or e2 > 1 << 28: continue + r1, r2 = e1 / d1, e2 / d2 + if r1 < 0.4 or r1 > 200: continue + if abs(r1 - r2) > 0.12 * max(r1, r2): continue # linear, not noisy + cand.append((k, va, vb, vc, (r1 + r2) / 2)) + print('\nlinearly-increasing words: %d' % len(cand)) + if not cand: + print('NONE -- nothing in this heap advances at a steady rate.') + return 0 + rates = collections.Counter(round(r[4]) for r in cand) + print('rate histogram (per second):', rates.most_common(10)) + print('\nsample candidates:') + for k, va, vb, vc, r in cand[:14]: + gva = gmem.primary_va(base + k) + print(' va %s %10d -> %10d -> %10d %.1f/s' + % (('%#010x' % gva) if gva else '?', va, vb, vc, r)) + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/re-capture/timer_session.sh b/tools/re-capture/timer_session.sh new file mode 100755 index 0000000..5ee1636 --- /dev/null +++ b/tools/re-capture/timer_session.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Deliberately NO pilot: the question is whether the game's own clock advances +# in exactly the condition where nothing was observed to change. +set -u +export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 +export PYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages +SD="$(cd "$(dirname "$0")" && pwd)" +"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; } +echo "--- NO pilot (unattended, by design)" +python3 "$SD/timer_probe.py" "${1:-20}"; echo "TIMER DONE rc=$?"