Third independent line for yesterday's built-in 100 rename, from the callee this time. sub_8226E3B8 was labelled "push", which is what made built-in 100 look like push_trigger. It reads the element count, returns immediately when the container is EMPTY, and otherwise walks the node list splicing nodes out until it is empty. A push links one node; this unlinks all of them. It is clear(). The append is sub_8226E160, reached from built-ins 19 and 25. So the rename now rests on the handler, the usage (all 12 Stage 02 sites sit in the phase terminator next to timer_stop / clear_flag(-1) / MARK_LAST_PHASE), and the callee. The dynamic half did NOT run, and the write-up says so. phase_watch.py now samples [phase+272+20] (triggers queued) and [phase+216+8] (coroutines alive) so a phase terminator's effect on the VM is visible in one line — written here, never yet exercised against a live guest. Boot-nav could not reach the title in 381 s. Diagnosed rather than retried: skip_intro.sh only runs the title test on a static frame, gated at rmse <= 1500, and this run measured 1503 at 104 s and 1549 at 139 s — just above the cut — so is_title.py was never called and the one allowed press was never spent. Recorded in BACKLOG with the explicit instruction NOT to raise the constant: the first step is to log rmse and the glyph count through a whole boot and look at the two distributions, because tuning a threshold to make one run pass is fitting to a single sample. Also reaped a stale lock: a gdb orphaned 2h14m earlier was holding /tmp/xenia-canary.lock with an already-defunct emulator child. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMRJjbxLqZtsb5Vb7KunPE
113 lines
5.0 KiB
Python
113 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""Watch the REAL phase counter and every squadron's state, together.
|
|
|
|
Three earlier runs polled `[*(0x828F35F8)+236]` and saw 0 forever -- that mirror
|
|
is only written once the ordinal exceeds 1 (script-runtime-probe.md). This reads
|
|
`[ScriptMission+40]`, the counter itself, plus the per-unit records the phase
|
|
conditions actually test, so a phase advance and the state change that caused it
|
|
are visible in one sample.
|
|
|
|
It also does two things the earlier probes had to learn the hard way:
|
|
|
|
* witnesses the freeze every 60 s (a frozen guest produces a perfectly
|
|
clean-looking negative -- see mission-freeze-resume-spin.md);
|
|
* re-locates the ScriptMission if the pointer stops validating, rather than
|
|
silently reporting stale numbers.
|
|
|
|
`--mission 0x...` skips the two full guest-memory sweeps `find_mission()` does
|
|
and validates the given address with a handful of reads instead. That exists
|
|
because the sweeps are the leading suspect for the in-mission freeze: runs with
|
|
this probe attached froze at ~70/126/253 s, while the same runs without it went
|
|
936 s and 1064 s clean (mission-freeze-resume-spin.md). Sweep-free mode is the
|
|
experiment that separates "the sweeps do it" from "the per-sample reads do it".
|
|
|
|
Usage: phase_watch.py <Stage02.ssb> [secs] [every_s] [--mission 0xADDR]
|
|
"""
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
sys.path.insert(0, __file__.rsplit('/', 1)[0])
|
|
import gmem
|
|
import isl
|
|
import squadron_state as S
|
|
|
|
WATCH = ['ADN110', 'ADN111', 'ADN112']
|
|
|
|
|
|
def main():
|
|
ssb = isl.load(sys.argv[1])
|
|
sym2 = isl.symbols(ssb, 2)
|
|
secs = float(sys.argv[2]) if len(sys.argv) > 2 else 900
|
|
every = float(sys.argv[3]) if len(sys.argv) > 3 else 5
|
|
given = None
|
|
if '--mission' in sys.argv:
|
|
given = int(sys.argv[sys.argv.index('--mission') + 1], 0)
|
|
path = gmem.mem_path()
|
|
size = os.path.getsize(path)
|
|
import frozen
|
|
t0 = time.time()
|
|
with open(path, 'rb', buffering=0) as f:
|
|
if given is not None:
|
|
# Validate WITHOUT sweeping: the mission's symtab1 pointer must equal
|
|
# the phase's, and the code base must be a plausible pointer. Both
|
|
# are self-consistency checks internal to the two objects, so they
|
|
# cost four reads instead of two full scans.
|
|
code = S.u32(f, given + 24)
|
|
s44 = S.u32(f, given + 44)
|
|
ph = S.u32(f, given + 4)
|
|
p244 = S.u32(f, ph + 244) if ph else None
|
|
ok = (code and 0x10000 <= code < 0xFFFFFFF0 and s44 and s44 == p244)
|
|
if not ok:
|
|
print('given ScriptMission 0x%08X does not validate '
|
|
'(code=%s s44=%s phase244=%s)' % (given, code, s44, p244))
|
|
return 1
|
|
m, fb = given, None
|
|
print('ScriptMission 0x%08X (given, validated, NO sweeps)' % m, flush=True)
|
|
else:
|
|
m, fb = S.find_mission(f, size, ssb)
|
|
if m is None:
|
|
print('ScriptMission not located'); return 1
|
|
print('file base 0x%08X ScriptMission 0x%08X' % (fb, m), flush=True)
|
|
last = None
|
|
next_frozen = 0.0
|
|
while time.time() - t0 < secs:
|
|
now = time.time() - t0
|
|
if now >= next_frozen:
|
|
next_frozen = now + 60
|
|
try:
|
|
dead, _ = frozen.frozen(5.0)
|
|
except Exception:
|
|
dead = False
|
|
if dead:
|
|
print(' [%6.1fs] *** GUEST FROZEN -- readings below are about '
|
|
'a dead world ***' % now, flush=True)
|
|
r = S.read_states(f, m, sym2, WATCH)
|
|
# Two script-VM counters, so a phase boundary and what it does to the
|
|
# VM are visible in the same sample:
|
|
# [phase+272+20] triggers queued (the container's element count)
|
|
# [phase+216+8] coroutines alive (the active-thread list's count)
|
|
# Built-in 100 is `reset_phase_threads`: it clears the container and
|
|
# frees every thread but the caller, so BOTH should collapse at a
|
|
# phase terminator. If they climb straight through one instead, that
|
|
# reading is wrong -- which is the point of printing them.
|
|
ph = S.u32(f, m + 4)
|
|
pending = S.u32(f, ph + 272 + 20) if ph else None
|
|
threads = S.u32(f, ph + 216 + 8) if ph else None
|
|
key = (r['phase_ordinal'], r['finished'], r['active_records'], pending, threads,
|
|
tuple((n, (v or {}).get('state')) for n, v in r['units'].items()))
|
|
if key != last:
|
|
print(' [%6.1fs] phase=%s finished=%s active=%3d pending=%s threads=%s %s' % (
|
|
now, r['phase_ordinal'], r['finished'], r['active_records'],
|
|
pending, threads,
|
|
' '.join('%s:%s' % (n, (v or {}).get('state'))
|
|
for n, v in r['units'].items())), flush=True)
|
|
last = key
|
|
time.sleep(every)
|
|
print('done', flush=True)
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|