diff --git a/docs/re/mission-freeze-resume-spin.md b/docs/re/mission-freeze-resume-spin.md index dedc7a4..9bf49b1 100644 --- a/docs/re/mission-freeze-resume-spin.md +++ b/docs/re/mission-freeze-resume-spin.md @@ -213,3 +213,36 @@ before the region summary existed, so "these regions are busy while frozen" has nothing to be compared against. The regions may simply be the busiest regions in the game at all times. That comparison is one run away and is the obvious next measurement. + +--- + +## 2026-08-24 β€” scoping the "which guest PC is spinning" experiment + +The next step this file named turns out to be **more expensive than it looked**, +and the reason is worth writing down before someone starts it: + +* πŸ”΄ **Xenia's stack walker is a stub on POSIX.** `stack_walker_posix.cc` is + twenty lines: `StackWalker::Create` logs *"Stack walker unimplemented on + posix"* and returns `nullptr`. So `ThreadDebugInfo::guest_pc` β€” the field the + debugger would fill β€” is never populated on this platform, and the clean + in-emulator route does not exist. +* πŸ”΄ **`PPCContext` carries no live PC either.** The guest PC lives in host + registers between block boundaries; there is no field to read. +* 🟑 **A reverse hostβ†’guest map is buildable but is a real feature.** The code + cache already learns the mapping when it emits code + (`X64CodeCache::OnCodePlaced(guest_address, function_info, …)`); a sorted + host-range β†’ guest-function index would make a host RIP interpretable. That is + ~50 lines *plus* a way to sample another thread's RIP, which on POSIX means + piggybacking on the signal machinery xenia already uses to suspend threads. + +**The cheap route that exists instead: `gdb`.** It is installed +(`/usr/bin/gdb`), and `ptrace_scope` is **1**, so a debugger may only attach to +its own descendants β€” which means `gdb -p ` on a running emulator will be +refused, but launching the emulator **under** gdb works. `run-canary` execs +`$XENIA_BIN`, so pointing `XENIA_BIN` at a small wrapper that `exec`s +`gdb --args "$@"` keeps the lockfile and the flags intact and makes +gdb the parent. A `thread apply all bt` on a frozen run would not name JIT +frames, but it would immediately separate *"spinning inside guest JIT code"* from +*"spinning in a xenia loop"* β€” which is the fork this investigation is stuck on. + +Not attempted here; recorded so the next pass starts from the right end. diff --git a/tools/re-capture/ob_bitflag.py b/tools/re-capture/ob_bitflag.py index 20d197d..4d0818a 100755 --- a/tools/re-capture/ob_bitflag.py +++ b/tools/re-capture/ob_bitflag.py @@ -40,13 +40,30 @@ import gmem # noqa: E402 import gworld # noqa: E402 import ob_read # noqa: E402 -VA = int(os.environ.get("OB_VA", "0xBDB59668"), 0) +# The counter's address is per-run, but not arbitrary: every run measured so far +# has put it in a narrow band, and three exact addresses have now been confirmed +# or strongly implicated. Trying the known ones against the HUD costs nothing and +# no transitions, where ob_hunt.py needs two β€” and with roughly half of all runs +# ending early, transitions are the scarce resource. +KNOWN_VAS = [0xBDB59668, 0xBDB49668, 0xBDB58668] +VA = int(os.environ["OB_VA"], 0) if "OB_VA" in os.environ else None LO, HI = -0x400, 0xC00 # window around the position triple DELTA = 0x130 -def counter(fd): - return struct.unpack(">I", os.pread(fd, 4, gmem.va_to_off(VA)))[0] +def counter(fd, va=None): + return struct.unpack(">I", os.pread(fd, 4, gmem.va_to_off(va or VA)))[0] + + +def locate(fd, hud_value): + """Pick whichever known address currently equals the HUD, or None.""" + for va in ([VA] if VA else KNOWN_VAS): + try: + if counter(fd, va) == hud_value: + return va + except Exception: + continue + return None def hud(shot): @@ -92,18 +109,26 @@ def main(): w = gworld.World() defs = entities2.definitions(w) + global VA v = None for _ in range(8): v = hud(f"{out}/a.png") - n0 = counter(w.fd) if v is not None: break time.sleep(3) - print(f"HUD={v} RAM={n0}", flush=True) - if v is None or v != n0: - print("HUD and RAM disagree (or unreadable) β€” re-scan with ob_hunt.py", + if v is None: + print("could not read the HUD counter at all β€” is this in flight?", flush=True) return 2 + found = locate(w.fd, v) + if found is None: + tried = " ".join(f"{a:#x}" for a in ([VA] if VA else KNOWN_VAS)) + print(f"HUD={v} but none of the known addresses holds it ({tried}) β€” " + f"re-scan with ob_hunt.py and pass OB_VA", flush=True) + return 2 + VA = found + n0 = counter(w.fd) + print(f"HUD={v} RAM={n0} at {VA:#x}", flush=True) for _ in range(6): nA, namesA, wordsA = sample(w, defs, "A")