mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 02:56:36 -04:00
bd705bfcec
Adapt Douglas's opt-in reachable discovery onto current exact/static dispatch. Validate configured static-analysis bounds before truncation, preserve the whole-image default and canonical page reservations, and leave unresolved indirect or out-of-bound targets to runtime fallback. Source: https://github.com/douglasjv/mm8 Source-commit: https://github.com/douglasjv/mm8/commit/8415f3a95458c41c7f48fbe36caaf0ee82730720 Co-authored-by: douglasjv <vanderveerdouglas@gmail.com>
157 lines
5.8 KiB
Python
157 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""End-to-end config and codegen checks for reachable main-EXE discovery.
|
|
|
|
Usage: python test_reachable_discovery_codegen.py \
|
|
[--recompiler <psxrecomp-game.exe>]
|
|
"""
|
|
import argparse
|
|
import os
|
|
import struct
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
LOAD = 0x80010000
|
|
|
|
|
|
def jal(target):
|
|
return 0x0C000000 | ((target >> 2) & 0x03FFFFFF)
|
|
|
|
|
|
def put32(data, offset, value):
|
|
struct.pack_into("<I", data, offset, value)
|
|
|
|
|
|
def make_psxexe():
|
|
size = 0x2000
|
|
header = bytearray(2048)
|
|
header[0:8] = b"PS-X EXE"
|
|
put32(header, 0x10, LOAD)
|
|
put32(header, 0x18, LOAD)
|
|
put32(header, 0x1C, size)
|
|
text = bytearray(size)
|
|
root = [
|
|
jal(LOAD + 0x100), 0, # reachable direct call
|
|
0x0320F809, 0, # unresolved jalr $ra,$t9
|
|
jal(LOAD + 0x1100), 0, # outside configured bound
|
|
0x03E00008, 0,
|
|
]
|
|
for i, word in enumerate(root):
|
|
put32(text, i * 4, word)
|
|
for offset in (0x100, 0x200, 0x1100):
|
|
for i, word in enumerate((0x27BDFFF0, 0x03E00008, 0x27BD0010)):
|
|
put32(text, offset + i * 4, word)
|
|
return bytes(header + text)
|
|
|
|
|
|
def write_config(path, discovery="reachable", text_size="0x1000"):
|
|
discovery_line = (f'discovery = "{discovery}"\n'
|
|
if discovery is not None else "")
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
f.write(f'''[game]
|
|
name = "Reachable discovery test"
|
|
exe = "test.exe"
|
|
load_address = "0x80010000"
|
|
entry_pc = "0x80010000"
|
|
text_size = "{text_size}"
|
|
stack_base = "0x801FFFF0"
|
|
|
|
[recompiler]
|
|
seeds = "seeds.txt"
|
|
out_dir = "generated"
|
|
{discovery_line}
|
|
''')
|
|
|
|
|
|
def run(recompiler, config):
|
|
return subprocess.run(
|
|
[recompiler, "--config", config], capture_output=True, text=True)
|
|
|
|
|
|
def main():
|
|
here = os.path.dirname(os.path.abspath(__file__))
|
|
default_recompiler = os.path.normpath(os.path.join(
|
|
here, "..", "build", "psxrecomp-game.exe"))
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--recompiler", default=default_recompiler)
|
|
args = parser.parse_args()
|
|
if not os.path.isfile(args.recompiler):
|
|
raise SystemExit(f"recompiler not found: {args.recompiler}")
|
|
|
|
failures = []
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
exe = os.path.join(tmp, "test.exe")
|
|
seeds = os.path.join(tmp, "seeds.txt")
|
|
config = os.path.join(tmp, "game.toml")
|
|
with open(exe, "wb") as f:
|
|
f.write(make_psxexe())
|
|
with open(seeds, "w", encoding="utf-8") as f:
|
|
f.write("0x80010000\n")
|
|
|
|
write_config(config)
|
|
result = run(args.recompiler, config)
|
|
output = result.stdout + result.stderr
|
|
if result.returncode != 0:
|
|
failures.append(f"reachable recompile failed:\n{output}")
|
|
else:
|
|
full_path = os.path.join(tmp, "generated", "test.exe_full.c")
|
|
dispatch_path = os.path.join(
|
|
tmp, "generated", "test.exe_dispatch.c")
|
|
with open(full_path, encoding="utf-8") as f:
|
|
full = f.read()
|
|
with open(dispatch_path, encoding="utf-8") as f:
|
|
dispatch = f.read()
|
|
|
|
if "void func_80010000" not in full:
|
|
failures.append("main EXE entry was not emitted")
|
|
if "void func_80010100" not in full:
|
|
failures.append("reachable direct-JAL target was not emitted")
|
|
if "void func_80010200" in full:
|
|
failures.append("unseen indirect callback was emitted")
|
|
if "void func_80011100" in full:
|
|
failures.append("target beyond analysis bound was emitted")
|
|
if "phys < 0x00011000u" not in dispatch:
|
|
failures.append("dispatch text range did not use verified bound")
|
|
if "dirty_ram_text_native_ok_ranges(" not in dispatch:
|
|
failures.append("exact static-range dispatch guard was lost")
|
|
if "psx_native_bad_entry(" in full:
|
|
failures.append("reachable main image incorrectly used overlay codegen")
|
|
if "=== Exact-Entry Function Analysis ===" not in output:
|
|
failures.append("reachable config did not select exact-entry analysis")
|
|
|
|
# Omitting the key must retain the established whole-image default.
|
|
write_config(config, discovery=None, text_size="0x2000")
|
|
default_mode = run(args.recompiler, config)
|
|
default_output = default_mode.stdout + default_mode.stderr
|
|
if default_mode.returncode != 0 or "=== Function Boundary Detection ===" not in default_output:
|
|
failures.append("omitted discovery key did not preserve whole-image default")
|
|
|
|
write_config(config, discovery="invent-functions")
|
|
invalid_mode = run(args.recompiler, config)
|
|
if invalid_mode.returncode == 0 or "must be 'whole-image' or 'reachable'" not in (
|
|
invalid_mode.stdout + invalid_mode.stderr):
|
|
failures.append("invalid discovery mode did not fail closed")
|
|
|
|
write_config(config, text_size="0x1800")
|
|
invalid_bound = run(args.recompiler, config)
|
|
if invalid_bound.returncode == 0 or "must be 4 KiB page-aligned" not in (
|
|
invalid_bound.stdout + invalid_bound.stderr):
|
|
failures.append("invalid analysis bound did not fail closed")
|
|
|
|
write_config(config, text_size="0x0")
|
|
zero_bound = run(args.recompiler, config)
|
|
if zero_bound.returncode == 0 or "game.text_size must be nonzero" not in (
|
|
zero_bound.stdout + zero_bound.stderr):
|
|
failures.append("zero analysis bound did not fail closed")
|
|
|
|
if failures:
|
|
for failure in failures:
|
|
print("FAIL:", failure)
|
|
return 1
|
|
print("PASS: reachable parser, analysis, and exact static codegen behavior")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|