mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
build+test: harden cg-tag against drift + codegen guard regression test
(1) OBJECT_DEPENDS on overlay_codegen_hash.h for the cg-tag CONSUMERS (overlay_loader.c, boot_state.c). overlay_api.h pulls that header via __has_include, which the compiler depfile does not record when the header is absent at first compile -- so a later hash change left a STALE baked-in PSX_OVERLAY_CODEGEN_HASH, making the loader READ cg<old> while autocompile WROTE cg<new>: the runtime silently ignored freshly compiled shards and the area ran ~97% interpreted (the recurring read!=write overlay-lag class). The explicit dependency makes the runtime cg tag unable to drift from the headers/autocompile. (2) recompiler/tests/test_overlay_guard_codegen.py: deterministic codegen regression test (no game) -- synthesizes a tiny PS-EXE, runs the recompiler in overlay vs static mode, and asserts overlay functions emit the fail-closed psx_native_bad_entry guard in their entry-switch default while static functions keep the legacy fall-through. Guards the native-overlay blue-screen wedge fix. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Recompiler codegen regression test for the native-overlay blue-screen wedge.
|
||||
|
||||
Deterministic integration test (no game, no capture): synthesize a tiny PS-EXE
|
||||
with a function that makes a call (so its CPS entry-switch carries a continuation),
|
||||
run psxrecomp-game in BOTH --overlay and static mode, and assert:
|
||||
|
||||
OVERLAY mode -> the function's entry-switch FAILS CLOSED on an unknown entry:
|
||||
it emits `psx_native_bad_entry(` in the `default:` case instead
|
||||
of falling through and running the function from its top. This
|
||||
is the fix for the foreign-interior-entry wedge (a native overlay
|
||||
fn entered at a PC it doesn't own must bail to the interpreter,
|
||||
not run from the top and corrupt shared CPU/RAM state).
|
||||
|
||||
STATIC mode -> keeps the legacy `default: break;` (no psx_native_bad_entry):
|
||||
static ranges are discovered once with no multi-variant entry
|
||||
ambiguity, so the guard is overlay-scoped (config_.overlay_mode).
|
||||
|
||||
Usage: python test_overlay_guard_codegen.py [--recompiler <psxrecomp-game.exe>]
|
||||
Exit 0 = PASS.
|
||||
"""
|
||||
import argparse, os, struct, subprocess, sys, tempfile
|
||||
|
||||
LOAD = 0x80010000
|
||||
|
||||
|
||||
def w(words):
|
||||
return b"".join(struct.pack("<I", x) for x in words)
|
||||
|
||||
|
||||
def make_psxexe(entry, data):
|
||||
h = bytearray(2048)
|
||||
h[0:8] = b"PS-X EXE"
|
||||
struct.pack_into("<I", h, 0x10, entry) # initial PC
|
||||
struct.pack_into("<I", h, 0x18, LOAD) # load address
|
||||
struct.pack_into("<I", h, 0x1C, len(data)) # text size
|
||||
return bytes(h) + data
|
||||
|
||||
|
||||
def jal(target):
|
||||
return 0x0C000000 | ((target >> 2) & 0x03FFFFFF)
|
||||
|
||||
|
||||
def build_exe():
|
||||
# func A @ 0x80010000: addiu sp,-8 ; jal B ; nop(delay) ; addiu sp,8 ; jr ra ; nop
|
||||
# -> the return point after the jal is a CPS continuation, so A gets an
|
||||
# entry-switch (where the fail-closed default lives).
|
||||
# func B @ 0x80010020: jr ra ; nop
|
||||
a = [0x27BDFFF8, jal(0x80010020), 0x00000000, 0x27BD0008, 0x03E00008, 0x00000000]
|
||||
body = bytearray(w(a))
|
||||
body += b"\x00" * (0x20 - len(body)) # pad to B's offset
|
||||
body += w([0x03E00008, 0x00000000]) # func B
|
||||
return make_psxexe(LOAD, bytes(body))
|
||||
|
||||
|
||||
def gen_c(recompiler, overlay, tmp):
|
||||
psx = os.path.join(tmp, "t.psx")
|
||||
seeds = os.path.join(tmp, "seeds.txt")
|
||||
out = os.path.join(tmp, "out")
|
||||
os.makedirs(out, exist_ok=True)
|
||||
with open(psx, "wb") as f:
|
||||
f.write(build_exe())
|
||||
with open(seeds, "w") as f:
|
||||
f.write("0x80010000\n0x80010020\n")
|
||||
cmd = [recompiler, psx, "--seeds", seeds, "--out-dir", out]
|
||||
if overlay:
|
||||
cmd.append("--overlay")
|
||||
r = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
raise SystemExit(f"recompiler failed ({'overlay' if overlay else 'static'}):\n"
|
||||
f"{r.stderr or r.stdout}")
|
||||
full = [f for f in os.listdir(out) if f.endswith("_full.c")]
|
||||
if not full:
|
||||
raise SystemExit(f"no _full.c emitted in {out}")
|
||||
with open(os.path.join(out, full[0])) as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def main():
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
default_recomp = os.path.normpath(os.path.join(
|
||||
here, "..", "build", "psxrecomp-game.exe"))
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--recompiler", default=default_recomp)
|
||||
args = ap.parse_args()
|
||||
if not os.path.isfile(args.recompiler):
|
||||
raise SystemExit(f"recompiler not found: {args.recompiler} (build it first)")
|
||||
|
||||
fails = []
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
ov = gen_c(args.recompiler, True, tmp)
|
||||
st = gen_c(args.recompiler, False, tmp)
|
||||
|
||||
if "psx_native_bad_entry(" not in ov:
|
||||
fails.append("OVERLAY codegen is MISSING the fail-closed guard "
|
||||
"(no psx_native_bad_entry in the entry-switch default) — the "
|
||||
"foreign-interior-entry wedge would run the fn from its top.")
|
||||
else:
|
||||
print("PASS: overlay codegen emits the fail-closed guard (psx_native_bad_entry).")
|
||||
|
||||
if "psx_native_bad_entry(" in st:
|
||||
fails.append("STATIC codegen unexpectedly contains psx_native_bad_entry — "
|
||||
"the guard must be overlay-scoped (config_.overlay_mode).")
|
||||
else:
|
||||
print("PASS: static codegen keeps the legacy fall-through (no guard).")
|
||||
|
||||
if fails:
|
||||
for m in fails:
|
||||
print("FAIL:", m)
|
||||
return 1
|
||||
print("PASS: native-overlay fail-closed guard codegen is correct.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -270,6 +270,21 @@ function(psxrecomp_add_runtime_target target)
|
||||
endif()
|
||||
add_dependencies(${target} psxrecomp_codegen_hash)
|
||||
|
||||
# Force the cg-tag CONSUMERS to recompile whenever overlay_codegen_hash.h
|
||||
# changes. overlay_api.h pulls that header via __has_include, which the
|
||||
# compiler depfile does NOT record when the header is absent at first compile —
|
||||
# so a later hash change left a STALE baked-in PSX_OVERLAY_CODEGEN_HASH in the
|
||||
# binary, making the LOADER read cg<old> while autocompile WROTE cg<new> (the
|
||||
# read≠write overlay lag/wedge class — the runtime silently ignored the freshly
|
||||
# compiled shards). An explicit OBJECT_DEPENDS makes the dependency
|
||||
# unconditional, so the runtime's cg tag can never drift from the headers /
|
||||
# autocompile again. (add_dependencies above only orders header generation; it
|
||||
# does not force object recompiles on content change.)
|
||||
set_source_files_properties(
|
||||
${PSXRECOMP_ROOT}/runtime/src/overlay_loader.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/boot_state.c
|
||||
PROPERTIES OBJECT_DEPENDS ${_codegen_hash_hdr})
|
||||
|
||||
target_include_directories(${target} PRIVATE
|
||||
${PSXRECOMP_RUNTIME_INCLUDE_DIRS}
|
||||
${SDL2_INCLUDE_DIRS}
|
||||
|
||||
Reference in New Issue
Block a user