Files
Sylpheed/tools/re-capture/dump_image.py
sylph-decoder e07c1de1b5 re: I cannot measure this emulator's clock -- and that answers the port's 2%
The port put two of my pages against each other: settle->plate 2.135 s and one
focus-ring revolution 2.177 s, both a declared 120 units during a static hold,
2% apart against a 6 ms run-to-run agreement. Fair challenge.

The resolution is that the question assumes a stable wall clock. Same interval,
same container, same day: 2.138, 2.132, and 2.549 s -- a 19% swing, caused by
adding --log_ui_draws=true. The 2% is a fifth of that. The two pages were never
in conflict about the game; they are three readings of one declared quantity
through a clock that moves. What settles the quantity is the disc.

Wall clock cannot separate the hypotheses, so I tried to measure frames instead.
Both instruments are recorded as failures rather than published as numbers:

  * Canary's own [UI-CAP] counter -- the one that produced the corpus's 28.5 fps
    -- costs a third of the frame rate. 300 frames in 16.567 s = 18.11 fps on a
    screen that gives ~28 without it. That reclassifies 28.5 as a load-dependent
    lower bound; it does not overturn it.
  * A distinct-frame counter over the spinning ring FAILED its decisive control:
    15.88 fps against the game's own 17.59 in the same window, 10% low, so the
    ring does not change on every presented frame. Its static control also read
    2.63 instead of ~0. Dead, not tuneable, per METHOD.md.

The rule that follows, and it applies to everything I hand the port: a measured
interval landing near a round number of declared units almost certainly IS that
number of units. Ship the units.

Also recovered here, because the same question needed it: the static PPC route.
Four tools open /work/xenia-rs/sylpheed.db and nothing in this repository builds
it -- no disassembler, no PPC decoder, and default.xex is encrypted (zero
plaintext "GamePart"). Xenia decompresses the image at load, so dump_image.py
reads it out of guest memory and validates it against the corpus's own landmarks:
the 29-entry GamePart id table at 0x820A1630 and the Xbox 360 D3D runtime
strings. String search and table dumps work again; instruction-level work does
not, and the present interval I wanted is an immediate, not a string.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014voBspJ6kFncNErZJuZcLw
2026-08-29 13:16:02 +00:00

82 lines
2.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Dump the guest's DECOMPRESSED executable image out of live Xenia memory.
Why this exists: the static PPC route the corpus is built on ran against a
disassembly database at `/work/xenia-rs/sylpheed.db`, and that file is **not in
this container** — the same migration that took the Xenia storage root. Without
it, every finding that cites a `sub_82xxxxxx` is unre-checkable.
`/disc/default.xex` cannot substitute: it is encrypted and LZX-compressed. Its
header is intact (`XEX2`, original PE name `default.pe`) and everything after is
noise — `strings` finds **zero** occurrences of `GamePart` in it.
Xenia decompresses, decrypts and relocates the image at load, so a running guest
holds exactly the flat VA image the corpus calls the `.pe`. Dump it once and the
static route works offline, with no emulator and no disc.
Validated on write, and both checks are the corpus's own, not this tool's:
* `0x820A1630` must hold the **GamePart id table** — 29 pointers into `.rdata`
resolving to `GP_TITLE` … `GP_TEST`, with `GP_CHALLENGE` at id 26
(docs/re/challenge-mission-gate.md);
* the image must contain the Xbox 360 D3D runtime's own error strings, which a
mis-based or partial dump does not.
dump_image.py [OUT.pe] # with Canary running
"""
import os
import struct
import sys
SD = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, SD)
import gmem # noqa: E402
LO, HI = 0x82000000, 0x82400000
BASE = LO
EXPECT = {0: "GP_TITLE", 3: "GP_LOAD", 11: "GP_READY_ROOM", 26: "GP_CHALLENGE", 28: "GP_TEST"}
def validate(buf):
def s_at(va):
o = va - BASE
e = buf.find(b"\0", o, o + 64)
return buf[o:e].decode("ascii", "replace")
bad = []
for i, want in EXPECT.items():
p = struct.unpack_from(">I", buf, 0x820A1630 - BASE + 4 * i)[0]
got = s_at(p) if LO <= p < HI else f"<ptr {p:#x} out of range>"
if got != want:
bad.append(f"GamePart id {i}: expected {want!r}, got {got!r}")
if buf.count(b"ERR[D3D]") < 1:
bad.append("no Xbox 360 D3D runtime strings — this is not the game image")
return bad
def main(out):
path = gmem.mem_path()
off = gmem.va_to_off(LO)
with open(path, "rb") as f:
f.seek(off)
buf = f.read(HI - LO)
if len(buf) < HI - LO:
print(f"short read: {len(buf)} of {HI - LO}", file=sys.stderr)
return 1
bad = validate(buf)
for b in bad:
print("FAIL:", b, file=sys.stderr)
if bad:
return 2
open(out, "wb").write(buf)
pages = sum(1 for i in range(0, len(buf), 4096) if any(buf[i:i + 4096]))
print(f"wrote {out} {len(buf)} bytes VA {LO:#x}..{HI:#x}")
print(f"validated: GamePart id table + D3D runtime strings; "
f"{pages}/{len(buf)//4096} non-empty 4K pages")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "/sylph-home/re/sylpheed-image.pe"))