diff --git a/.gitmodules b/.gitmodules index 6adc28bc..bbef4ec1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -6,4 +6,4 @@ [submodule "lib/retcomm-rbengine"] path = lib/retcomm-rbengine url = https://github.com/TechnicallyComputers/retcomm-rbengine.git - branch = main + branch = master diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index adfcfd06..9ee3bf6d 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -455,6 +455,12 @@ if(BUILD_TESTING) ${CMAKE_CURRENT_SOURCE_DIR}/tests/test_fmv_cycle_batch_codegen.py --recompiler $ 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 $ + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) add_test( NAME overlay_shim_compile_contract COMMAND ${Python3_EXECUTABLE} diff --git a/recompiler/src/main_psx.cpp b/recompiler/src/main_psx.cpp index 97e1ff63..437137de 100644 --- a/recompiler/src/main_psx.cpp +++ b/recompiler/src/main_psx.cpp @@ -1507,9 +1507,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 @@ -1591,15 +1595,24 @@ 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 << " extern uint32_t psx_ram_canon_code_addr(uint32_t);\n"; ds << " addr = psx_ram_canon_code_addr(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"; diff --git a/recompiler/tests/test_kuseg_dispatch_lookup.py b/recompiler/tests/test_kuseg_dispatch_lookup.py new file mode 100644 index 00000000..cb14c4b7 --- /dev/null +++ b/recompiler/tests/test_kuseg_dispatch_lookup.py @@ -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 ] +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("> 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() diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 59937666..5e771d43 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -6303,6 +6303,8 @@ static void rewind_host_pause_loop(void) { rewind_pause_present(); SDL_Delay(8); } + /* Swallow the still-held close press so it doesn't bleed into the game. */ + savestate_input_guard_arm(); } /* Freeze guest in vblank present while the save-state slot menu is open. */ @@ -6344,6 +6346,8 @@ static void savestate_menu_host_pause_loop(void) { rewind_pause_present(); SDL_Delay(8); } + /* Swallow the close press; a just-queued save must not snapshot it. */ + savestate_input_guard_arm(); } /* Epilogue for netplay admit/pace AFTER all C++ RAII in the present body @@ -13264,6 +13268,20 @@ int main(int argc, char** argv) { uint32_t text_lo = gc.load_address & 0x1FFFFFFFu; if (text_lo > 0x00010000u && text_lo < g_overlay_region_floor) g_text_image_lo = text_lo; + /* PSX_OVERLAY_REGION_FLOOR: per-title override for games whose TEXT + * range is itself partially overwritten by streamed level data + * (Driver 2 streams mission code over pages inside its static text + * range). Lowering the floor routes those regions through local-flow + * chaining and makes them overlay-cache candidates, so live-byte + * closures can own them instead of single-instruction dispatch + * thrash. Clamped to stay above the kernel window. */ + { + const char* fenv = std::getenv("PSX_OVERLAY_REGION_FLOOR"); + if (fenv && fenv[0]) { + uint32_t v = (uint32_t)strtoul(fenv, nullptr, 0) & 0x1FFFFFFFu; + if (v >= 0x00010000u) g_overlay_region_floor = v; + } + } std::fprintf(stdout, "psxrecomp: overlay_region_floor = 0x%05X (game text end), " "text_image_lo = 0x%05X\n", @@ -13953,6 +13971,19 @@ int main(int argc, char** argv) { std::string assets_dir_str = exe_dir_from_argv(argv[0]).string(); /* Same keybinds.ini / config.ini the runtime reads — never cwd. */ ae_rui_set_sidecar_paths(argv[0]); + /* A CLI --disc must seed the launcher's initial disc. Without this, + * the override suppresses the remembered settings/disc.cfg pick (the + * has_disc_path gate above) while contributing nothing itself, so the + * launcher opens with "No disc selected" and forces a manual pick on + * every launch. resolve_disc_for_runtime still applies the override + * authoritatively after the launcher returns. */ + if (disc_override_path && disc_override_path[0]) { + std::filesystem::path cli_disc = normalize_disc_path_for_launch( + std::filesystem::path(disc_override_path)); + std::error_code cli_ec; + if (std::filesystem::exists(cli_disc, cli_ec)) + resolved_disc = cli_disc; + } std::string rui_initial_disc = resolved_disc.string(); std::string rui_title = (game_name.empty() ? std::string("PSX") : game_name) + " - Launcher";