diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index c3d696c1..1e6719ae 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 00000000..4ccf3c8d --- /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 00000000..f0019eb3 --- /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 00000000..5ee16369 --- /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=$?"