diff --git a/docs/re/BACKLOG.md b/docs/re/BACKLOG.md index 1e6907b0..22e53a3b 100644 --- a/docs/re/BACKLOG.md +++ b/docs/re/BACKLOG.md @@ -719,6 +719,19 @@ search cannot find a *schedule*. **Revises the previous entry**, which called route 1 "per-frame archaeology" and route 2 (RelWithDebInfo rebuild) the way to make it easy. **Next: execute on a frozen run** — two gdb commands per thread. +* ✅🟡 **(2026-08-25) Wait-object read WORKS: the object is an `xe::kernel::XEvent`.** + Executed under gdb: `frame 3` → `rbx` → `x/1gx $rbx` = **`0x5555562db8f0`** = + PIE base + `0xd878f0` = **`vtable for xe::kernel::XEvent` (+16 for + offset-to-top/RTTI)** — exact. Same vtable on every sampled waiting thread, + different `this` each, so many threads waiting on *different XEvent instances*. + No DWARF, no rebuild, as predicted. 🟡 **Two caveats, both recorded not glossed:** + (1) this is a **healthy-play** snapshot (`screen_id` = `flight`), not the freeze + — boot under gdb costs ~300 s and the whole thing must fit one call (a `timeout` + kills the process group and took the emulator with it once), leaving too little + to reach the ~267 s freeze; (2) the `this` addresses (`0x7ffd…`, `0x7ffc…`) look + like **host stack**, not heap, so either xenia places them unusually or `rbx` + isn't `Wait`'s `this` after unwind. **Check: dump a few words at `$rbx` — + XEvent-like (vtable, KernelState*, handle) vs saved registers.** * ~~🚧 BLOCKER: t=210/240 unreachable in one turn~~ — **superseded, see above**; it rested on an untested assumption that a turn is one shell call. 595 s shell cap − ~220 s boot (a ~190 s title movie that cannot be tapped through) − ~25 s startup = **~350 s observation ≈ 193 game-seconds**. diff --git a/docs/re/mission-freeze-resume-spin.md b/docs/re/mission-freeze-resume-spin.md index d512972c..8208ec50 100644 --- a/docs/re/mission-freeze-resume-spin.md +++ b/docs/re/mission-freeze-resume-spin.md @@ -770,3 +770,54 @@ oracle stays byte-identical to the binary every other measurement used. Not yet executed on a frozen run — that is the next step, and it is now a small one. + +## ✅ 2026-08-25 — the technique works end to end: the waited object is an `XEvent` + +Executed on a live run under gdb. For every thread whose frame 3 is +`XObject::Wait`: + +``` +(gdb) === T106 === +#3 0x0000555555e4fd80 in xe::kernel::XObject::Wait(...) +(gdb) rbx 0x7ffd097178d0 +(gdb) 0x7ffd097178d0: 0x00005555562db8f0 +``` + +`0x5555562db8f0` minus the PIE base `0x555555554000` is `0xd878f0`, and the +symbol table has: + +``` +0000000000d878e0 D vtable for xe::kernel::XEvent +``` + +The stored pointer is the vtable symbol **+16** — offset-to-top and RTTI come +first — so it matches exactly. **The object being waited on is an +`xe::kernel::XEvent`**, and the same vtable appears for every waiting thread +sampled (T106, T97, T96, T80, …), while the `this` pointers differ, i.e. many +threads waiting on *different `XEvent` instances*. + +The whole chain — `frame 3` → `rbx` → `x/1gx` → symtab — needs no DWARF and no +rebuild, exactly as the static groundwork predicted. + +## 🟡 Two honest caveats + +**This is a healthy-play snapshot, not the freeze.** The capture landed at 195 s +of flight and `screen_id` reported `flight` with `mean=(17.4, 19.1, 25.1)` — not +the black screen. Boot under gdb costs ~300 s and the whole experiment must fit +in one call (a `timeout` kills the process group, taking the emulator with it — +an earlier attempt lost its run that way), which left too little time to reach +the ~267 s freeze. So this says what threads wait on **during normal play**. That +is the control the frozen capture never had, but it is not the frozen case. + +**The `this` addresses look like host stack, which is odd.** `0x7ffd097178d0`, +`0x7ffc8c1c2f10` and the rest sit in the range host thread stacks occupy, not +where a heap-allocated kernel object would. Either xenia places these objects +somewhere unusual, or `rbx` at frame 3 is not `Wait`'s `this` after the unwind +and the vtable match is a coincidence — an exact +16 match on a known symbol is +hard to get by accident, but the address range is not what was expected and is +recorded rather than explained away. + +**The check that would settle it:** read the same value two ways — compare `rbx` +against the object pointer recovered from the shim frame above, or dump a few +words at `$rbx` and see whether they look like an `XEvent` (a vtable, then a +`KernelState*`, then handle/type fields) or like saved registers. diff --git a/tools/re-capture/freeze_waitobj.sh b/tools/re-capture/freeze_waitobj.sh new file mode 100755 index 00000000..2075cfb9 --- /dev/null +++ b/tools/re-capture/freeze_waitobj.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Read WHICH object the frozen guest threads are waiting on. +# +# Groundwork (mission-freeze-resume-spin.md): XObject::Wait keeps `this` in %rbx, +# and .eh_frame lets gdb restore callee-saved registers during the unwind, so no +# DWARF and no rebuild are needed -- select the Wait frame, read rbx, then read +# the vtable pointer at [rbx] and resolve it in the symtab. +set -u +export HOME=/sylph-home/re SDL_AUDIODRIVER=dummy DISPLAY=:98 +export PYTHONPATH=/sylph-home/.local/lib/python3.12/site-packages +export XENIA_BIN=/sylph-home/re/bin/gdb-wrap/xenia_canary +SD="$(cd "$(dirname "$0")" && pwd)" +FLY="${1:-240}" +CMD=/tmp/gdb-cmd; OUT=/tmp/gdb-out.log +"$SD/launch_mission.sh" fly || { echo "BOOT FAILED"; exit 1; } +CFG=/tmp/nav-fz.json +for t in 1 2 3; do + python3 "$SD/pad.py" set "rt=1" >/dev/null 2>&1 || true; sleep 3 + python3 "$SD/pad.py" clear >/dev/null 2>&1 || true + if python3 "$SD/entities2.py" self 0x130 "$CFG" >/dev/null 2>&1; then + SYLPH_HUNT=1 SYLPH_KEEPOUT=1400 nohup python3 "$SD/pilot.py" "$CFG" "$FLY" \ + /tmp/fz-pilot.log 2>&1 & P=$!; echo "--- pilot flying"; break + fi +done +echo "--- flying ${FLY}s (boot under gdb costs ~300s, so this must leave room" +echo "--- for the gdb step inside one call: timeout kills the whole group)" +python3 -c "import time,sys; time.sleep(int(sys.argv[1]))" "$FLY" +[ -n "${P:-}" ] && kill "$P" 2>/dev/null +pid=$(pgrep -x gdb | head -1) +[ -n "$pid" ] || { echo "no gdb"; exit 2; } +before=$(wc -c < "$OUT") +kill -INT "$pid"; sleep 3 +{ + echo 'echo === HOT THREAD WAIT OBJECTS ===\n' + echo 'thread apply all bt 6' + echo 'echo === END BT ===\n' +} >> "$CMD" +sleep 10 +tail -c +$((before + 1)) "$OUT" > /tmp/fz-bt.txt +# find threads whose frame 3 is XObject::Wait, then read rbx there +python3 - <<'PY' >> "$CMD" +import re +txt=open('/tmp/fz-bt.txt').read() +cur=None +for line in txt.splitlines(): + m=re.match(r'Thread (\d+) ', line) + if m: cur=m.group(1) + if cur and 'XObject::Wait' in line: + print(r'echo === T%s ===\n' % cur) + print('thread %s' % cur) + print('frame 3') + print('info registers rbx') + print('x/1gx $rbx') + cur=None +print(r'echo === END OBJ ===\n') +print('continue') +PY +sleep 12 +screenshot /tmp/fz-screen.png >/dev/null 2>&1 +echo "--- screen at capture time:" +python3 "$SD/screen_id.py" /tmp/fz-screen.png 2>/dev/null | head -2 +tail -c +$((before + 1)) "$OUT" | sed -n '/HOT THREAD WAIT OBJECTS/,$p' | grep -vE "^\s*$" | tail -70 +echo "FREEZE WAITOBJ DONE"