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
This commit is contained in:
81
tools/re-capture/dump_image.py
Executable file
81
tools/re-capture/dump_image.py
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/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"))
|
||||
Reference in New Issue
Block a user