mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Merge pull request #155 from ClaudioVarandas/fix/kuseg-dispatch-lookup
recompiler: match dispatch entries for KUSEG-addressed games
This commit is contained in:
@@ -399,6 +399,12 @@ if(BUILD_TESTING)
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_fmv_cycle_batch_codegen.py
|
||||
--recompiler $<TARGET_FILE:psxrecomp-game>
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..)
|
||||
add_test(
|
||||
NAME kuseg_dispatch_lookup
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_kuseg_dispatch_lookup.py
|
||||
--recompiler $<TARGET_FILE:psxrecomp-game>
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..)
|
||||
add_test(
|
||||
NAME overlay_shim_compile_contract
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
|
||||
@@ -1502,9 +1502,13 @@ int main(int argc, char** argv) {
|
||||
records.push_back({cont, cont, owner, 0, 0});
|
||||
}
|
||||
}
|
||||
// Sort by PHYSICAL address. psx_game_find_entry() compares masked
|
||||
// addresses so a KUSEG-executing guest still matches KSEG-normalized
|
||||
// keys; the search invariant must therefore be the masked order too.
|
||||
// Within one segment this is identical to sorting by the raw address.
|
||||
std::sort(records.begin(), records.end(),
|
||||
[](const DispatchRecord& a, const DispatchRecord& b) {
|
||||
return a.addr < b.addr;
|
||||
return (a.addr & 0x1FFFFFFFu) < (b.addr & 0x1FFFFFFFu);
|
||||
});
|
||||
|
||||
// Attach the exact CFG instruction ranges from the manifest to every
|
||||
@@ -1586,13 +1590,22 @@ int main(int argc, char** argv) {
|
||||
}
|
||||
ds << "};\n";
|
||||
ds << fmt::format("#define PSX_GAME_DISPATCH_COUNT {}u\n\n", records.size());
|
||||
ds << "/* PS1 segments alias the same physical RAM. A game whose PS-X EXE\n";
|
||||
ds << " * header carries KUSEG addresses (load address and entry PC without the\n";
|
||||
ds << " * KSEG bit) executes with a KUSEG PC, while this table is keyed by the\n";
|
||||
ds << " * recompiler's KSEG-normalized addresses. Comparing raw values made every\n";
|
||||
ds << " * lookup fail for such a title: 0x0001xxxx is always below 0x8001xxxx, so\n";
|
||||
ds << " * the search collapsed and returned no entry, silently routing all game\n";
|
||||
ds << " * code to the interpreter. Compare the 29-bit physical address instead;\n";
|
||||
ds << " * the table is sorted by the same masked key. */\n";
|
||||
ds << "static const PsxGameDispatchEntry* psx_game_find_entry(uint32_t addr) {\n";
|
||||
ds << " const uint32_t want = addr & 0x1FFFFFFFu;\n";
|
||||
ds << " uint32_t lo = 0, hi = PSX_GAME_DISPATCH_COUNT;\n";
|
||||
ds << " while (lo < hi) {\n";
|
||||
ds << " uint32_t mid = lo + (hi - lo) / 2;\n";
|
||||
ds << " uint32_t key = k_psx_game_dispatch[mid].addr;\n";
|
||||
ds << " if (addr < key) hi = mid;\n";
|
||||
ds << " else if (addr > key) lo = mid + 1;\n";
|
||||
ds << " uint32_t key = k_psx_game_dispatch[mid].addr & 0x1FFFFFFFu;\n";
|
||||
ds << " if (want < key) hi = mid;\n";
|
||||
ds << " else if (want > key) lo = mid + 1;\n";
|
||||
ds << " else return &k_psx_game_dispatch[mid];\n";
|
||||
ds << " }\n";
|
||||
ds << " return 0;\n";
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Recompiler codegen regression test: KUSEG-addressed games must dispatch.
|
||||
|
||||
PS1 segments alias the same physical RAM. Most PS-X EXE headers carry KSEG
|
||||
addresses (0x8001xxxx), but a header may legally carry KUSEG ones instead
|
||||
(0x0001xxxx) -- Alien Resurrection (SLUS-00633) does, and it then executes with
|
||||
a KUSEG PC.
|
||||
|
||||
The recompiler normalizes the dispatch table to KSEG keys. When the lookup
|
||||
compared raw values, a KUSEG PC could never match: 0x0001xxxx is always below
|
||||
0x8001xxxx, so the binary search collapsed to the start and returned no entry.
|
||||
Every dispatch fell through to the interpreter -- silently, because the
|
||||
text-image guard was never even reached (it reported zero blocks and zero
|
||||
mismatches while the whole game ran interpreted).
|
||||
|
||||
This test synthesizes a KUSEG-addressed PS-EXE and asserts the emitted lookup
|
||||
compares 29-bit physical addresses, and that the table is ordered by that same
|
||||
key so the binary-search invariant holds.
|
||||
|
||||
Usage: python test_kuseg_dispatch_lookup.py [--recompiler <psxrecomp-game>]
|
||||
Exit 0 = PASS.
|
||||
"""
|
||||
import argparse, os, re, struct, subprocess, sys, tempfile
|
||||
|
||||
LOAD = 0x00010000 # KUSEG: no KSEG bit, the case under test
|
||||
PHYS_MASK = 0x1FFFFFFF
|
||||
|
||||
|
||||
def w(words):
|
||||
return b"".join(struct.pack("<I", x) for x in words)
|
||||
|
||||
|
||||
def make_psxexe(entry, load, data):
|
||||
h = bytearray(2048)
|
||||
h[0:8] = b"PS-X EXE"
|
||||
struct.pack_into("<I", h, 0x10, entry)
|
||||
struct.pack_into("<I", h, 0x18, load)
|
||||
struct.pack_into("<I", h, 0x1C, len(data))
|
||||
struct.pack_into("<I", h, 0x30, 0x801FFFF0) # stack_base, as real headers carry
|
||||
return bytes(h) + data
|
||||
|
||||
|
||||
def jal(target):
|
||||
return 0x0C000000 | ((target >> 2) & 0x03FFFFFF)
|
||||
|
||||
|
||||
def build_exe():
|
||||
# func A @ LOAD: addiu sp,-8 ; jal B ; nop ; addiu sp,8 ; jr ra ; nop
|
||||
# func B @ LOAD+0x20: jr ra ; nop
|
||||
a = [0x27BDFFF8, jal(LOAD + 0x20), 0x00000000, 0x27BD0008, 0x03E00008, 0x00000000]
|
||||
body = bytearray(w(a))
|
||||
body += b"\x00" * (0x20 - len(body))
|
||||
body += w([0x03E00008, 0x00000000])
|
||||
return make_psxexe(LOAD, LOAD, bytes(body))
|
||||
|
||||
|
||||
def gen_dispatch(recompiler, 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("0x%08X\n0x%08X\n" % (LOAD, LOAD + 0x20))
|
||||
r = subprocess.run([recompiler, psx, "--seeds", seeds, "--out-dir", out],
|
||||
capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
raise SystemExit("recompiler failed:\n" + (r.stderr or r.stdout))
|
||||
disp = [f for f in os.listdir(out) if f.endswith("_dispatch.c")]
|
||||
if not disp:
|
||||
raise SystemExit("no _dispatch.c emitted in " + out)
|
||||
with open(os.path.join(out, disp[0])) as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def main():
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--recompiler",
|
||||
default=os.path.normpath(os.path.join(here, "..", "build",
|
||||
"psxrecomp-game")))
|
||||
args = ap.parse_args()
|
||||
if not os.path.isfile(args.recompiler):
|
||||
raise SystemExit("recompiler not found: %s (build it first)" % args.recompiler)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
src = gen_dispatch(args.recompiler, tmp)
|
||||
|
||||
m = re.search(r"static const PsxGameDispatchEntry\* psx_game_find_entry"
|
||||
r"\(uint32_t addr\) \{(.*?)\n\}", src, re.DOTALL)
|
||||
if not m:
|
||||
raise SystemExit("psx_game_find_entry not found in emitted dispatch")
|
||||
body = m.group(1)
|
||||
|
||||
# The incoming PC must be reduced to a physical address before comparing.
|
||||
if "0x1FFFFFFFu" not in body:
|
||||
raise SystemExit(
|
||||
"psx_game_find_entry does not mask to a physical address; a KUSEG "
|
||||
"guest PC can never match KSEG-normalized table keys")
|
||||
# ... and so must the stored key, otherwise the comparison is still mixed.
|
||||
if not re.search(r"k_psx_game_dispatch\[mid\]\.addr\s*&\s*0x1FFFFFFFu", body):
|
||||
raise SystemExit(
|
||||
"psx_game_find_entry compares against an unmasked table key")
|
||||
|
||||
# The binary search requires the table to be ordered by the same key it
|
||||
# compares. Parse the emitted entries and assert monotonic physical order.
|
||||
tbl = re.search(r"k_psx_game_dispatch\[\] = \{(.*?)\n\};", src, re.DOTALL)
|
||||
if not tbl:
|
||||
raise SystemExit("dispatch table not found in emitted dispatch")
|
||||
keys = [int(x, 16) & PHYS_MASK
|
||||
for x in re.findall(r"\{0x([0-9A-Fa-f]{8})u,", tbl.group(1))]
|
||||
if len(keys) < 2:
|
||||
raise SystemExit("expected at least two dispatch entries, got %d" % len(keys))
|
||||
if keys != sorted(keys):
|
||||
raise SystemExit("dispatch table is not sorted by physical address; "
|
||||
"the masked binary search would miss entries")
|
||||
|
||||
print("KUSEG dispatch lookup test passed (%d entries, physical-keyed)" % len(keys))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user