#include "mod_runtime.h" #include "mod_packages.h" #include "mod_plugins.h" #include "psx_sha256.h" #include "gpu.h" #include "cpu_state.h" #include #include #include #include #include #include #include #include #include namespace fs = std::filesystem; static std::array ram; static int failures; static int activation_calls; static int plugin_calls; static int restore_calls; extern "C" uint8_t psx_read_byte(uint32_t address) { return ram[address & 0x1fffffu]; } extern "C" void psx_write_byte(uint32_t address, uint8_t value) { ram[address & 0x1fffffu] = value; } extern "C" uint16_t psx_read_half(uint32_t address) { const uint32_t offset = address & 0x1fffffu; return (uint16_t)(ram[offset] | ((uint16_t)ram[offset + 1] << 8)); } extern "C" void psx_write_half(uint32_t address, uint16_t value) { const uint32_t offset = address & 0x1fffffu; ram[offset] = (uint8_t)value; ram[offset + 1] = (uint8_t)(value >> 8); } extern "C" uint32_t psx_read_word(uint32_t address) { const uint32_t offset = address & 0x1fffffu; return (uint32_t)ram[offset] | ((uint32_t)ram[offset + 1] << 8) | ((uint32_t)ram[offset + 2] << 16) | ((uint32_t)ram[offset + 3] << 24); } extern "C" void psx_write_word(uint32_t address, uint32_t value) { const uint32_t offset = address & 0x1fffffu; ram[offset] = (uint8_t)value; ram[offset + 1] = (uint8_t)(value >> 8); ram[offset + 2] = (uint8_t)(value >> 16); ram[offset + 3] = (uint8_t)(value >> 24); } /* Mod writes are host stores (memory.c psx_host_write_*). */ extern "C" void psx_host_write_byte(uint32_t address, uint8_t value) { psx_write_byte(address, value); } extern "C" void psx_host_write_half(uint32_t address, uint16_t value) { psx_write_half(address, value); } extern "C" void psx_host_write_word(uint32_t address, uint32_t value) { psx_write_word(address, value); } extern "C" uint32_t psx_mod_memory_alloc(uint32_t, uint32_t) { return 0; } extern "C" uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t, uint32_t) { return 0; } extern "C" void psx_ram_reset_size_request(void) {} extern "C" void psx_projection_reset_session(void) {} extern "C" void gpu_ws_set_native_scene_predicate(int (*)(void)) {} extern "C" int psx_ws_x_margin(void) { return 0; } /* Stand-in for the GPU's display geometry. psx_mod_display_width/height must * report exactly what the presenter reports -- a plugin drawing an overlay * uses this as the screen edge, so a substituted or rounded value would put * HUD elements in the wrong place. */ static GpuDisplayInfo g_test_display; extern "C" void gpu_get_display_info(GpuDisplayInfo* out) { *out = g_test_display; } extern "C" void dirty_ram_mark_executable_range(uint32_t, uint32_t) {} extern "C" int fntrace_is_game_started(void) { return 1; } /* Widescreen tag passthroughs mod_runtime forwards to the GPU; unused here. */ extern "C" void gpu_ws_tag_hud_primitive(uint32_t, int) {} extern "C" void gpu_ws_tag_world_primitive(uint32_t, int) {} extern "C" void gpu_ws_set_adaptive_backdrop_preload(int) {} extern "C" int gpu_ws_configured_x_reveal(void) { return 0; } extern "C" void gpu_ws_tag_hud_prim(uint32_t, int) {} extern "C" void gpu_ws_tag_screen_mask_quad(uint32_t) {} extern "C" void gpu_ws_tag_radial_screen_mask_quad(uint32_t, float) {} static CPUState* interrupted_entry_cpu; static void test_vblank_plugin(void) { plugin_calls++; if (interrupted_entry_cpu && psx_mod_finish_function(interrupted_entry_cpu)) failures++; } /* Function-entry hooks: one owned by the plan's active plugin, one by a * plugin whose feature is disabled, one by an id no package selects. */ static int active_entry_hits; static int disabled_entry_hits; static int unselected_entry_hits; static uint32_t active_entry_last; static int entry_test_mode, nested_result; static uint32_t nested_pc; static void test_active_entry(CPUState* cpu, uint32_t address) { active_entry_hits++; active_entry_last = address; if (entry_test_mode == 1) { CPUState unrelated{}; if (psx_mod_finish_function(&unrelated) || !psx_mod_finish_function(cpu)) failures++; cpu->gpr[2] = 0x12345678u; } else if (entry_test_mode == 2 || entry_test_mode == 3) { const int mode = entry_test_mode; if (mode == 3 && !psx_mod_finish_function(cpu)) failures++; CPUState nested{}; nested.gpr[31] = 0x80004000u; entry_test_mode = mode == 2 ? 1 : 0; nested_result = psx_mod_function_entry(&nested, address); nested_pc = nested.pc; entry_test_mode = mode; } else if (entry_test_mode == 4) { /* A native call made by a hook can cross a guest VBlank. Its * callbacks must neither inherit nor discard the interrupted hook. */ interrupted_entry_cpu = cpu; mod_runtime_on_vblank(); interrupted_entry_cpu = nullptr; if (!psx_mod_finish_function(cpu)) failures++; cpu->gpr[2] = 0x87654321u; } } static void test_disabled_entry(CPUState*, uint32_t) { disabled_entry_hits++; } static void test_unselected_entry(CPUState*, uint32_t) { unselected_entry_hits++; } static bool filter_handles; static bool filter_context_active; static unsigned active_filter_hits; static unsigned inactive_filter_hits; static uint32_t active_filter_last; static int test_active_filter(CPUState* cpu, uint32_t address) { ++active_filter_hits; active_filter_last = address; filter_context_active = psx_mod_function_entry_active() != 0; if (!filter_handles) return 0; cpu->gpr[2] = 42; return 1; } static int test_inactive_filter(CPUState*, uint32_t) { ++inactive_filter_hits; return 1; } static int guest_function_hits; static int instruction_hits; static void test_instruction(CPUState* cpu, uint32_t) { instruction_hits++; cpu->gpr[5] = 0x80301008u; if (psx_mod_finish_function(cpu)) failures++; } static void test_guest_function(CPUState* cpu, uint32_t) { guest_function_hits++; const uint32_t ra = cpu->gpr[31]; CPUState nested{}; nested.gpr[31] = 0x80007000u; const int mode = entry_test_mode; entry_test_mode = 1; if (!psx_mod_function_entry(&nested, 0x80003000u) || nested.pc != nested.gpr[31]) failures++; entry_test_mode = mode; interrupted_entry_cpu = cpu; mod_runtime_on_vblank(); interrupted_entry_cpu = nullptr; if (!psx_mod_finish_function(cpu)) failures++; cpu->gpr[2] = cpu->gpr[4] + 7u; if (cpu->gpr[31] != ra) failures++; } static void test_activation_plugin(void) { activation_calls++; } static int big_ram_activations; static void test_big_ram_activation(void) { big_ram_activations++; } static void test_restore_plugin(void) { restore_calls++; if (ram[0x1000] != 0xa1) failures++; /* main plan must be reapplied first */ } static void check(bool value, const char* message) { if (!value) { std::cerr << "FAIL: " << message << "\n"; failures++; } } static void write_text(const fs::path& path, const std::string& text) { fs::create_directories(path.parent_path()); std::ofstream out(path); out << text; } static void write_bytes(const fs::path& path, const std::vector& bytes) { fs::create_directories(path.parent_path()); std::ofstream out(path, std::ios::binary); out.write((const char*)bytes.data(), (std::streamsize)bytes.size()); } static std::string sha256_hex(const std::vector& bytes) { uint8_t digest[32]; psx_sha256_compute(bytes.data(), bytes.size(), digest); static const char hex[] = "0123456789abcdef"; std::string out(64, '0'); for (size_t i = 0; i < 32; ++i) { out[i * 2] = hex[digest[i] >> 4]; out[i * 2 + 1] = hex[digest[i] & 15]; } return out; } int main() { const fs::path root = fs::temp_directory_path() / "psxrecomp-mod-runtime-test"; std::error_code ec; fs::remove_all(root, ec); const std::vector stock(8 * 2352, 0); std::vector overlay(3000); for (size_t i = 0; i < overlay.size(); ++i) overlay[i] = (uint8_t)(i * 17u + 3u); const fs::path stock_path = root / "stock.bin"; write_bytes(stock_path, stock); write_bytes(root / "audio.bin", std::vector(2 * 2352, 0x5a)); const fs::path cue_path = root / "stock.cue"; write_text(cue_path, "FILE \"stock.bin\" BINARY\n" " TRACK 01 MODE2/2352\n" " INDEX 01 00:00:00\n" "FILE \"audio.bin\" BINARY\n" " TRACK 02 AUDIO\n" " INDEX 01 00:00:00\n"); write_bytes(root / "packages/runtime.test/1.0.0/assets/overlay.bin", overlay); write_text(root / "packages/runtime.test/1.0.0/manifest.toml", "format_version = 5\n" "id = \"runtime.test\"\n" "version = \"1.0.0\"\n" "name = \"Runtime Test\"\n" "[[target]]\n" "game_id = \"SLUS-RUNTIME\"\n" "disc_sha256 = \"" + sha256_hex(stock) + "\"\n" "[[feature]]\n" "id = \"main-code\"\n" "name = \"Main Code\"\n" "[[feature]]\n" "id = \"disc-byte\"\n" "name = \"Disc Byte\"\n" "[[feature]]\n" "id = \"asset-overlay\"\n" "name = \"Asset Overlay\"\n" "[[feature]]\n" "id = \"user-byte\"\n" "name = \"User Byte\"\n" "[[feature]]\n" "id = \"dynamic-main\"\n" "name = \"Dynamic Main\"\n" "[[feature]]\n" "id = \"sparse-main\"\n" "name = \"Sparse Main\"\n" "[[feature]]\n" "id = \"sparse-flag\"\n" "name = \"Sparse Flag\"\n" "[[feature]]\n" "id = \"sparse-disc\"\n" "name = \"Sparse Disc\"\n" "[[feature]]\n" "id = \"vblank-plugin\"\n" "name = \"VBlank Plugin\"\n" "[[option]]\n" "feature = \"dynamic-main\"\n" "id = \"count\"\n" "label = \"Count\"\n" "type = \"integer\"\n" "min = 0\n" "max = 254\n" "default = 42\n" "[[option]]\n" "feature = \"sparse-main\"\n" "id = \"frames\"\n" "label = \"Frames\"\n" "type = \"integer\"\n" "min = 0\n" "max = 99\n" "default = 2\n" "[[patch]]\n" "feature = \"main-code\"\n" "target = \"main_exe\"\n" "address = 2147487744\n" "expected = \"01020304\"\n" "replace = \"a1a2a3a4\"\n" "[[patch]]\n" "feature = \"disc-byte\"\n" "target = \"disc_raw\"\n" "offset = 4714\n" "expected = \"aa\"\n" "replace = \"bb\"\n" "[[patch]]\n" "feature = \"user-byte\"\n" "target = \"disc_user\"\n" "offset = 6154\n" "expected = \"cc\"\n" "replace = \"dd\"\n" "[[patch]]\n" "feature = \"dynamic-main\"\n" "target = \"main_exe\"\n" "address = 2147488000\n" "expected = \"0000\"\n" "replace_from = { option = \"count\", encoding = \"u16le\" }\n" "[[patch]]\n" "feature = \"dynamic-main\"\n" "target = \"main_exe\"\n" "address = 2147488002\n" "expected = \"0100\"\n" "replace_from = { option = \"count\", encoding = \"u16le\", addend = 1 }\n" "[[patch]]\n" "feature = \"sparse-main\"\n" "target = \"main_exe\"\n" "address = 2147488256\n" "expected = \"02000132\"\n" "fields = [{ offset = 0, option = \"frames\", encoding = \"u8\" }]\n" "when_integer = { option = \"frames\", op = \"gt\", value = 0 }\n" "[[patch]]\n" "feature = \"sparse-main\"\n" "target = \"main_exe\"\n" "address = 2147488256\n" "expected = \"02000132\"\n" "fields = [{ offset = 0, replace = \"01\" }, " "{ offset = 2, replace = \"00\" }]\n" "when_integer = { option = \"frames\", op = \"eq\", value = 0 }\n" "[[patch]]\n" "feature = \"sparse-flag\"\n" "target = \"main_exe\"\n" "address = 2147488256\n" "expected = \"02000132\"\n" "fields = [{ offset = 1, replace = \"42\" }]\n" "[[patch]]\n" "feature = \"sparse-disc\"\n" "target = \"disc_user\"\n" "offset = 6164\n" "expected = \"11223344\"\n" "fields = [{ offset = 0, replace = \"aa\" }, " "{ offset = 2, replace = \"bb\" }]\n" "[[overlay]]\n" "feature = \"asset-overlay\"\n" "target = \"disc_raw\"\n" "offset = 11408\n" "file = \"assets/overlay.bin\"\n" "sha256 = \"" + sha256_hex(overlay) + "\"\n" "expected_sha256 = \"" + sha256_hex(std::vector(overlay.size(), 0)) + "\"\n" "[[plugin]]\n" "feature = \"vblank-plugin\"\n" "id = \"runtime.test-vblank\"\n" "[[feature]]\n" "id = \"entry-disabled\"\n" "name = \"Disabled Entry\"\n" "[[plugin]]\n" "feature = \"entry-disabled\"\n" "id = \"runtime.test-disabled-entry\"\n"); write_text(root / "state.toml", "format_version = 2\n" "[[package]]\n" "id = \"runtime.test\"\n" "version = \"1.0.0\"\n" "[[feature]]\n" "package_id = \"runtime.test\"\n" "id = \"main-code\"\n" "enabled = true\n" "[[feature]]\n" "package_id = \"runtime.test\"\n" "id = \"disc-byte\"\n" "enabled = true\n" "[[feature]]\n" "package_id = \"runtime.test\"\n" "id = \"asset-overlay\"\n" "enabled = true\n" "[[feature]]\n" "package_id = \"runtime.test\"\n" "id = \"user-byte\"\n" "enabled = true\n" "[[feature]]\n" "package_id = \"runtime.test\"\n" "id = \"dynamic-main\"\n" "enabled = true\n" "[feature.values]\n" "count = 42\n" "[[feature]]\n" "package_id = \"runtime.test\"\n" "id = \"sparse-main\"\n" "enabled = true\n" "[feature.values]\n" "frames = 0\n" "[[feature]]\n" "package_id = \"runtime.test\"\n" "id = \"sparse-flag\"\n" "enabled = true\n" "[[feature]]\n" "package_id = \"runtime.test\"\n" "id = \"sparse-disc\"\n" "enabled = true\n" "[[feature]]\n" "package_id = \"runtime.test\"\n" "id = \"vblank-plugin\"\n" "enabled = true\n"); std::string error; PSXRecompV4::mod_clear_plugins_for_tests(); check(PSXRecompV4::mod_register_activation_plugin( "runtime.test-vblank", test_activation_plugin), "runtime test activation hook must register"); check(PSXRecompV4::mod_register_vblank_plugin( "runtime.test-vblank", test_vblank_plugin), "runtime test plugin must register"); check(psx_mod_register_function_entry_plugin( "runtime.test-vblank", 0x80003000u, test_active_entry) == 1, "active plugin's function-entry hook must register"); check(psx_mod_register_function_entry_plugin( "runtime.test-disabled-entry", 0x80003000u, test_disabled_entry) == 1, "disabled feature's function-entry hook must register"); check(psx_mod_register_function_entry_plugin( "runtime.unselected-entry", 0x80003000u, test_unselected_entry) == 1, "unselected function-entry hook must register"); CPUState entry_cpu{}; check(psx_mod_register_instruction_plugin("runtime.test-vblank", 0x80003004u, 0x90A30014u, test_instruction), "register guarded instruction callback"); check(!psx_mod_register_instruction_plugin("runtime.test-vblank", 0xA0003004u, 0x90A30014u, test_instruction) && !psx_mod_register_instruction_plugin("runtime.other", 0x80003005u, 0x90A30014u, test_instruction), "reject aliased duplicate and unaligned sites"); psx_mod_write_word(0x80003004u, 0x90A30014u); check(psx_mod_register_guest_function_plugin( "runtime.test-vblank", 0x8FFF0000u, test_guest_function), "trusted callback registers outside the hardware map"); check(!psx_mod_register_guest_function_plugin( "runtime.other", 0xAFFF0000u, test_guest_function) && !psx_mod_register_guest_function_plugin("runtime.other", 0x80003000u, test_guest_function) && !psx_mod_register_guest_function_plugin("runtime.other", 0x8FFF0001u, test_guest_function) && !psx_mod_register_guest_function_plugin("runtime.other", 0xCFFF0000u, test_guest_function), "guest callbacks refuse alias collisions, game addresses, misalignment and KSEG2"); check(psx_mod_register_guest_function_plugin( "runtime.unselected-entry", 0x8FFF0004u, test_unselected_entry), "unselected callback implementation can register"); check(psx_mod_register_savestate_plugin("runtime.test-vblank", test_restore_plugin), "restore callback must register through the C API"); check(!psx_mod_register_savestate_plugin("runtime.test-vblank", test_restore_plugin), "duplicate restore callback must be rejected"); check(PSXRecompV4::mod_runtime_initialize( root, "SLUS-RUNTIME", 0x80002000, {}, &error), error.c_str()); check(PSXRecompV4::mod_runtime_commit(cue_path, &error), "CUE and its data-track BIN must have the same mod target identity"); /* Committed but not yet activated: no hook may run. */ psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u); check(instruction_hits == 0 && !g_psx_mod_instruction_hooks, "instruction hooks await activation"); check(!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0000u) && g_psx_mod_guest_functions == 0, "committed guest functions remain unavailable until activation"); psx_mod_function_entry(&entry_cpu, 0x80003000u); check(g_psx_mod_function_entry_hooks == 0 && active_entry_hits == 0, "function-entry hooks must not run before plugin activation"); mod_runtime_activate_plugins(); check(activation_calls == 1, "resolved trusted plugin must activate before runtime startup"); entry_cpu.pc = 0x80004000u; psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u); psx_mod_instruction(&entry_cpu, 0xA0003004u, 0x90A30014u); check(instruction_hits == 2 && entry_cpu.gpr[5] == 0x80301008u && entry_cpu.pc == 0x80004000u, "instruction aliases preserve control flow and edit registers"); psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30015u); psx_mod_write_word(0x80003004u, 0x90A30015u); psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u); check(instruction_hits == 2, "fetched and live instruction guards reject changed code"); psx_mod_write_word(0x80003004u, 0x90A30014u); mod_runtime_on_vblank(); check(plugin_calls == 1, "resolved trusted plugin must run on guest VBlank"); check(g_psx_mod_function_entry_hooks == 1, "activation must flatten exactly the active plan's entry hooks"); psx_mod_function_entry(&entry_cpu, 0x80003000u); check(active_entry_hits == 1 && active_entry_last == 0x80003000u, "active plan's function-entry hook must run at its address"); psx_mod_function_entry(&entry_cpu, 0x00003000u); psx_mod_function_entry(&entry_cpu, 0xA0003000u); check(active_entry_hits == 3 && active_entry_last == 0xA0003000u, "function-entry hooks must match every segment alias of the code address"); psx_mod_function_entry(&entry_cpu, 0x80003004u); psx_mod_function_entry(&entry_cpu, 0x80203000u); check(active_entry_hits == 3, "function-entry hooks must not run for other addresses or RAM mirrors"); check(disabled_entry_hits == 0 && unselected_entry_hits == 0, "hooks of plugins the plan does not activate must never run"); /* A replaced plan drops every hook until its own activation. */ check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), error.c_str()); psx_mod_instruction(&entry_cpu, 0x80003004u, 0x90A30014u); check(instruction_hits == 2 && !g_psx_mod_instruction_hooks, "clearing removes instruction hooks"); check(!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0000u) && g_psx_mod_guest_functions == 0, "clearing the plan drops guest callback availability"); psx_mod_function_entry(&entry_cpu, 0x80003000u); check(g_psx_mod_function_entry_hooks == 0 && active_entry_hits == 3, "clearing the plan must drop its function-entry hooks"); check(PSXRecompV4::mod_runtime_commit(cue_path, &error), error.c_str()); psx_mod_function_entry(&entry_cpu, 0x80003000u); check(active_entry_hits == 3, "a re-committed plan must not run hooks before activation"); mod_runtime_activate_plugins(); psx_mod_function_entry(&entry_cpu, 0x80003000u); check(active_entry_hits == 4 && disabled_entry_hits == 0 && unselected_entry_hits == 0, "re-activation restores exactly the active plan's hooks"); /* The lobby rematch, in the order main.cpp's start_mod_session() drives * it: a netplay match (plan cleared, then activated) and an offline * rematch that commits and activates exactly like a first boot. */ check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), error.c_str()); mod_runtime_activate_plugins(); mod_runtime_on_vblank(); psx_mod_function_entry(&entry_cpu, 0x80003000u); check(activation_calls == 2 && plugin_calls == 1 && g_psx_mod_function_entry_hooks == 0 && active_entry_hits == 4, "a netplay session must stay vanilla: no activation, VBlank or hook"); check(PSXRecompV4::mod_runtime_commit(cue_path, &error), error.c_str()); mod_runtime_activate_plugins(); check(activation_calls == 3, "an offline rematch after netplay must run activation callbacks"); mod_runtime_on_vblank(); check(plugin_calls == 2, "an offline rematch after netplay must run VBlank plugins"); psx_mod_function_entry(&entry_cpu, 0x80003000u); check(g_psx_mod_function_entry_hooks == 1 && active_entry_hits == 5 && disabled_entry_hits == 0 && unselected_entry_hits == 0, "an offline rematch after netplay must arm exactly its plan's hooks"); check(psx_mod_register_function_filter_plugin( "runtime.test-vblank", 0x80003008u, test_active_filter) == 1, "active plan's return filter must register"); check(psx_mod_register_function_filter_plugin( "runtime.test-vblank", 0xA0003008u, test_active_filter) == 0, "filter aliases must not register duplicate hooks"); check(psx_mod_register_function_filter_plugin( "runtime.test-disabled-entry", 0x80003008u, test_inactive_filter) == 1 && psx_mod_register_function_filter_plugin( "runtime.unselected-entry", 0x80003008u, test_inactive_filter) == 1, "inactive filter implementations must register without running"); entry_cpu.pc = 0x80003008u; entry_cpu.gpr[31] = 0x80004000u; entry_cpu.gpr[2] = 99; check(!psx_mod_function_entry(&entry_cpu, entry_cpu.pc) && active_filter_hits == 0, "new filters must wait for active-plan table rebuild"); mod_runtime_activate_plugins(); check(g_psx_mod_function_entry_hooks == 2 && !psx_mod_function_entry(&entry_cpu, entry_cpu.pc) && active_filter_hits == 1 && filter_context_active && entry_cpu.pc == 0x80003008u && entry_cpu.gpr[2] == 99, "unhandled active filter must preserve native fallthrough"); filter_handles = true; entry_cpu.pc = 0xA0003008u; check(psx_mod_function_entry(&entry_cpu, entry_cpu.pc) == 1 && active_filter_hits == 2 && active_filter_last == 0xA0003008u && entry_cpu.pc == entry_cpu.gpr[31] && entry_cpu.gpr[2] == 42 && inactive_filter_hits == 0 && !psx_mod_function_entry_active(), "handled alias filter must publish return registers and pc=$ra"); check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), error.c_str()); check(!psx_mod_function_entry(&entry_cpu, 0x80003008u) && active_filter_hits == 2 && g_psx_mod_function_entry_hooks == 0, "clearing the active plan must also disarm return filters"); check(PSXRecompV4::mod_runtime_commit(cue_path, &error), error.c_str()); mod_runtime_activate_plugins(); entry_cpu.gpr[31] = 0x80005000u; check(!psx_mod_finish_function(&entry_cpu) && !psx_mod_function_entry(nullptr, 0x80003000u), "completion outside an entry callback or with no CPU is refused"); entry_test_mode = 1; check(psx_mod_function_entry(&entry_cpu, 0x80003000u) == 1 && entry_cpu.pc == 0x80005000u && entry_cpu.gpr[2] == 0x12345678u, "explicit completion returns handled and publishes guest return PC/results"); entry_test_mode = 2; entry_cpu.pc = 0; check(!psx_mod_function_entry(&entry_cpu, 0x80003000u) && entry_cpu.pc == 0 && nested_result == 1 && nested_pc == 0x80004000u, "inner completion cannot finish an ordinary outer callback"); entry_test_mode = 3; check(psx_mod_function_entry(&entry_cpu, 0x80003000u) == 1 && entry_cpu.pc == 0x80005000u && nested_result == 0, "outer completion survives a nested ordinary callback"); entry_test_mode = 4; check(psx_mod_function_entry(&entry_cpu, 0x80003000u) == 1 && entry_cpu.pc == 0x80005000u && entry_cpu.gpr[2] == 0x87654321u, "entry completion survives VBlank callbacks during a native call"); entry_test_mode = 0; entry_cpu.gpr[4] = 35u; const uint32_t guest_sp = entry_cpu.gpr[29]; check(g_psx_mod_guest_functions == 1 && psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0000u) && entry_cpu.gpr[2] == 42u && entry_cpu.pc == 0x80005000u && entry_cpu.gpr[29] == guest_sp, "active callback returns its result through normal guest PC/stack state"); check(psx_mod_dispatch_guest_function(&entry_cpu, 0x0FFF0000u) && psx_mod_dispatch_guest_function(&entry_cpu, 0xAFFF0000u) && guest_function_hits == 3, "guest callback aliases retain nested entry and VBlank scope"); check(!psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0004u) && !psx_mod_dispatch_guest_function(&entry_cpu, 0x8FFF0001u) && !psx_mod_dispatch_guest_function(&entry_cpu, 0xCFFF0000u) && !psx_mod_dispatch_guest_function(nullptr, 0x8FFF0000u) && unselected_entry_hits == 0, "unselected, unknown, misaligned and invalid callback requests fall through"); ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4; ram[0x1100] = 0; ram[0x1101] = 0; ram[0x1102] = 1; ram[0x1103] = 0; ram[0x1200] = 2; ram[0x1201] = 0; ram[0x1202] = 1; ram[0x1203] = 0x32; mod_runtime_on_dispatch(0x80001000); check(ram[0x1000] == 1, "patch must wait for the configured entry point"); mod_runtime_on_dispatch(0x80002000); check(ram[0x1000] == 0xa1 && ram[0x1003] == 0xa4, "main-EXE patch must apply before entry execution"); check(ram[0x1100] == 42 && ram[0x1101] == 0 && ram[0x1102] == 43 && ram[0x1103] == 0, "dynamic main-EXE patches must encode all sites before entry"); check(ram[0x1200] == 1 && ram[0x1201] == 0x42 && ram[0x1202] == 0 && ram[0x1203] == 0x32, "adjacent sparse fields must compose while preserving guard-only " "bytes"); /* A full-machine savestate replaces main RAM after the entry-point plan * has already applied. Loading a stock checkpoint must not silently turn * the currently enabled main-EXE features back off. */ ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4; ram[0x1100] = 0; ram[0x1101] = 0; ram[0x1102] = 1; ram[0x1103] = 0; ram[0x1200] = 2; ram[0x1201] = 0; ram[0x1202] = 1; ram[0x1203] = 0x32; const int activation_before_restore = activation_calls; const int vblank_before_restore = plugin_calls; mod_runtime_on_savestate_loaded(); check(restore_calls == 1 && activation_calls == activation_before_restore && plugin_calls == vblank_before_restore, "restore must rebind hooks without activation or a gameplay VBlank"); check(ram[0x1000] == 0xa1 && ram[0x1003] == 0xa4 && ram[0x1100] == 42 && ram[0x1102] == 43 && ram[0x1200] == 1 && ram[0x1201] == 0x42 && ram[0x1202] == 0 && ram[0x1203] == 0x32, "savestate restore must reapply the complete enabled main plan"); std::array sector{}; sector[10] = 0xaa; mod_runtime_patch_disc_sector(2, 1, sector.data(), (uint32_t)sector.size()); check(sector[10] == 0xaa, "disc overlay must stay off during reference reads"); mod_runtime_enable_disc_patches(); mod_runtime_patch_disc_sector(2, 1, sector.data(), (uint32_t)sector.size()); check(sector[10] == 0xbb, "raw disc overlay must patch matching sectors"); std::array overlay_sector{}; mod_runtime_patch_disc_sector( 4, 1, overlay_sector.data(), (uint32_t)overlay_sector.size()); check(overlay_sector[1999] == 0 && overlay_sector[2000] == overlay[0] && overlay_sector[2351] == overlay[351], "file overlay must patch the tail of its first sector"); overlay_sector.fill(0); mod_runtime_patch_disc_sector( 5, 1, overlay_sector.data(), (uint32_t)overlay_sector.size()); check(overlay_sector.front() == overlay[352] && overlay_sector.back() == overlay[2703], "file overlay must patch complete middle sectors"); overlay_sector.fill(0); mod_runtime_patch_disc_sector( 6, 1, overlay_sector.data(), (uint32_t)overlay_sector.size()); check(overlay_sector[0] == overlay[2704] && overlay_sector[295] == overlay[2999] && overlay_sector[296] == 0, "file overlay must patch the head of its final sector"); std::array mode2_sector{}; mode2_sector[15] = 2; mode2_sector[18] = 0; mode2_sector[24 + 10] = 0xcc; mode2_sector[24 + 20] = 0x11; mode2_sector[24 + 21] = 0x22; mode2_sector[24 + 22] = 0x33; mode2_sector[24 + 23] = 0x44; mod_runtime_patch_disc_sector( 3, 1, mode2_sector.data(), (uint32_t)mode2_sector.size()); check(mode2_sector[24 + 10] == 0xdd, "disc_user operations must apply to raw Mode2 Form1 user data"); check(mode2_sector[24 + 20] == 0xaa && mode2_sector[24 + 21] == 0x22 && mode2_sector[24 + 22] == 0xbb && mode2_sector[24 + 23] == 0x44, "sparse disc writes must validate a complete guard and modify only " "owned fields"); std::array audio_sector{}; audio_sector[24 + 10] = 0xcc; mod_runtime_patch_disc_sector( 3, 1, audio_sector.data(), (uint32_t)audio_sector.size()); check(audio_sector[24 + 10] == 0xcc, "disc_user operations must not modify CDDA/non-data sectors"); check(PSXRecompV4::mod_runtime_initialize( root, "SLUS-RUNTIME", 0x80002000, {}, &error), error.c_str()); check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str()); ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4; ram[0x1100] = 0; ram[0x1101] = 0; ram[0x1102] = 2; ram[0x1103] = 0; /* second dynamic guard is wrong */ mod_runtime_on_dispatch(0x80002000); check(ram[0x1000] == 1 && ram[0x1003] == 4 && ram[0x1100] == 0 && ram[0x1101] == 0, "one failed generated guard must leave the complete main plan untouched"); check(PSXRecompV4::mod_runtime_initialize( root, "SLUS-RUNTIME", 0x80002000, {}, &error), error.c_str()); check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str()); ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4; ram[0x1100] = 0; ram[0x1101] = 0; ram[0x1102] = 1; ram[0x1103] = 0; ram[0x1200] = 2; ram[0x1201] = 0; ram[0x1202] = 1; ram[0x1203] = 0x33; /* guard-only byte is wrong */ mod_runtime_on_dispatch(0x80002000); check(ram[0x1000] == 1 && ram[0x1003] == 4 && ram[0x1200] == 2 && ram[0x1201] == 0 && ram[0x1202] == 1 && ram[0x1203] == 0x33, "a failed sparse guard-only byte must leave the complete main plan " "untouched"); /* Loading can be requested while the boot executable is still running, * before the configured game entry point has dispatched. The restored * checkpoint itself supplies the bytes used to validate and apply the * plan in that case. */ check(PSXRecompV4::mod_runtime_initialize( root, "SLUS-RUNTIME", 0x80002000, {}, &error), error.c_str()); check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str()); ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4; ram[0x1100] = 0; ram[0x1101] = 0; ram[0x1102] = 1; ram[0x1103] = 0; ram[0x1200] = 2; ram[0x1201] = 0; ram[0x1202] = 1; ram[0x1203] = 0x32; mod_runtime_on_savestate_loaded(); check(ram[0x1000] == 0xa1 && ram[0x1003] == 0xa4 && ram[0x1100] == 42 && ram[0x1102] == 43 && ram[0x1200] == 1 && ram[0x1201] == 0x42 && ram[0x1202] == 0 && ram[0x1203] == 0x32, "pre-entry savestate restore must validate and apply the main plan"); mod_runtime_enable_disc_patches(); std::array bad_sparse_disc{}; bad_sparse_disc[15] = 2; bad_sparse_disc[18] = 0; bad_sparse_disc[24 + 10] = 0xcc; bad_sparse_disc[24 + 20] = 0x11; bad_sparse_disc[24 + 21] = 0x99; /* guard-only byte is wrong */ bad_sparse_disc[24 + 22] = 0x33; bad_sparse_disc[24 + 23] = 0x44; mod_runtime_patch_disc_sector( 3, 1, bad_sparse_disc.data(), (uint32_t)bad_sparse_disc.size()); check(bad_sparse_disc[24 + 10] == 0xcc && bad_sparse_disc[24 + 20] == 0x11 && bad_sparse_disc[24 + 22] == 0x33, "a failed sparse disc guard must leave every write in the sector " "untouched"); /* Implicit requirement through the real commit/activation path: the * hidden required feature's activation plugin runs, the state.toml commit * writes never gains the derived activation (it is not a choice), * and a requirement on an absent package makes the launch commit fail * rather than boot the requiring build without it. */ { const fs::path req_root = root / "requirement-runtime"; write_text(req_root / "bundled/runtime.ram/1.0.0/manifest.toml", "format_version = 5\n" "id = \"runtime.ram\"\n" "version = \"1.0.0\"\n" "name = \"Runtime RAM\"\n" "[[target]]\n" "game_id = \"*\"\n" "[[feature]]\n" "id = \"big-ram\"\n" "name = \"Big RAM\"\n" "hidden = true\n" "[[option]]\n" "feature = \"big-ram\"\n" "id = \"size\"\n" "label = \"Size\"\n" "type = \"choice\"\n" "default = \"eight\"\n" "[[option.choice]]\nvalue = \"eight\"\nlabel = \"8 MB\"\n" "[[plugin]]\n" "feature = \"big-ram\"\n" "id = \"runtime.big-ram\"\n"); write_text(req_root / "bundled/runtime.mode/1.0.0/manifest.toml", "format_version = 7\n" "id = \"runtime.mode\"\n" "version = \"1.0.0\"\n" "name = \"Runtime Mode\"\n" "[[target]]\n" "game_id = \"SLUS-REQ\"\n" "[[feature]]\n" "id = \"mode\"\n" "name = \"Mode\"\n" "[[option]]\n" "feature = \"mode\"\n" "id = \"extras\"\n" "label = \"Extras\"\n" "type = \"choice\"\n" "default = \"none\"\n" "[[option.choice]]\nvalue = \"none\"\nlabel = \"None\"\n" "[[option.choice]]\nvalue = \"full\"\nlabel = \"Full\"\n" "[[requirement]]\n" "feature = \"mode\"\n" "package = \"runtime.ram\"\n" "requires_feature = \"big-ram\"\n" "when = { extras = \"full\" }\n"); const std::string state_text = "format_version = 2\n" "\n[[feature]]\npackage_id = \"runtime.mode\"\nid = \"mode\"\n" "enabled = true\n" "[feature.values]\nextras = \"full\"\n"; write_text(req_root / "state.toml", state_text); const auto read_state = [&]() { std::ifstream in(req_root / "state.toml", std::ios::binary); return std::string((std::istreambuf_iterator(in)), std::istreambuf_iterator()); }; big_ram_activations = 0; check(PSXRecompV4::mod_register_activation_plugin( "runtime.big-ram", test_big_ram_activation), "big-ram activation hook must register"); check(PSXRecompV4::mod_runtime_initialize( req_root, "SLUS-REQ", 0x80002000, {}, &error), error.c_str()); check(PSXRecompV4::mod_runtime_commit(stock_path, &error), "a satisfied implicit requirement must commit"); mod_runtime_activate_plugins(); check(big_ram_activations == 1, "the implicitly required feature's plugin must activate"); char value[32]; check(psx_mod_option_value("runtime.ram", "big-ram", "size", value, sizeof(value)) == 1 && std::string(value) == "eight", "an implicit feature's plugin must read its option values"); /* commit rewrites state.toml in canonical form; what matters is that * the player's choice survives and the derived one is never added. */ const std::string after = read_state(); check(after.find("runtime.ram") == std::string::npos && after.find("extras = \"full\"") != std::string::npos, "committing must not write the derived activation to state.toml"); /* Condition false: the required plugin stays off. */ write_text(req_root / "state.toml", "format_version = 2\n" "\n[[feature]]\npackage_id = \"runtime.mode\"\nid = \"mode\"\n" "enabled = true\n" "[feature.values]\nextras = \"none\"\n"); big_ram_activations = 0; check(PSXRecompV4::mod_runtime_initialize( req_root, "SLUS-REQ", 0x80002000, {}, &error), error.c_str()); check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str()); mod_runtime_activate_plugins(); check(big_ram_activations == 0, "a requirement whose condition is false must not activate"); /* Required package absent (e.g. the title excluded the builtin). */ write_text(req_root / "state.toml", state_text); fs::remove_all(req_root / "bundled/runtime.ram", ec); check(PSXRecompV4::mod_runtime_initialize( req_root, "SLUS-REQ", 0x80002000, {}, &error), error.c_str()); std::string commit_error; check(!PSXRecompV4::mod_runtime_commit(stock_path, &commit_error) && commit_error.find("runtime.mode/mode requires " "runtime.ram/big-ram") != std::string::npos, "an unmet requirement must fail the launch commit loudly"); } /* * Display geometry passthrough. Ape Escape scans out 384 while its mode * width is 368, which is precisely why a plugin cannot derive this from * GPUSTAT: the horizontal range that makes the difference is write-only. */ g_test_display.width = 384; g_test_display.height = 240; check(psx_mod_display_width() == 384u, "psx_mod_display_width must report the presenter's visible width, " "not the coarse mode width"); check(psx_mod_display_height() == 240u, "psx_mod_display_height must report the presenter's visible height"); g_test_display.width = 0; g_test_display.height = 0; check(psx_mod_display_width() == 0u && psx_mod_display_height() == 0u, "unestablished display geometry must report zero so callers skip " "drawing instead of guessing"); /* Source-owned ISO: nested file spanning two sectors, without a derived * disc. Host reads must choose the original mount and leave sizes honest. */ std::vector iso(24*2048); auto le32 = [&](size_t at,uint32_t n) { for(unsigned i=0;i<4;++i) iso[at+i]=(uint8_t)(n>>(i*8)); }; auto record = [&](size_t at,uint32_t lba,uint32_t bytes,bool directory,const std::string& name) { iso[at]=(uint8_t)((33+name.size()+1)&~size_t(1)); le32(at+2,lba); le32(at+10,bytes); iso[at+25]=directory?2:0; iso[at+28]=1;iso[at+31]=1;iso[at+32]=(uint8_t)name.size(); std::copy(name.begin(),name.end(),iso.begin()+at+33); }; iso[16*2048]=1;std::copy_n("CD001",5,iso.begin()+16*2048+1);iso[16*2048+6]=1; record(16*2048+156,20,2048,true,std::string(1,'\0')); record(20*2048,21,2048,true,"S0"); record(21*2048,22,3000,false,"LEVEL.NSF;1"); for(unsigned i=0;i<3000;++i)iso[22*2048+i]=(uint8_t)(i*7); const auto disc_root=root/"host-reader"; const auto iso_path=disc_root/"original.iso"; write_bytes(iso_path,iso); check(PSXRecompV4::mod_runtime_initialize(disc_root,"READER",0,{},&error),"reader initialize"); check(PSXRecompV4::mod_runtime_commit(iso_path,&error),"reader mount original ISO"); uint32_t bytes=0; check(psx_mod_read_disc_file("S0/LEVEL.NSF",nullptr,0,&bytes) && bytes==3000,"query original nested file size"); std::vector result(3000); check(!psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),2999,&bytes) && bytes==0,"undersized destination rejected"); check(psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),(uint32_t)result.size(),&bytes) && bytes==3000 && std::equal(result.begin(),result.end(),iso.begin()+22*2048),"complete original file bytes"); check(!psx_mod_read_disc_file("S0/MISSING.NSF",nullptr,0,&bytes) && bytes==0,"missing file explicit"); check(!psx_mod_read_disc_file("S0",nullptr,0,&bytes),"directory rejected"); std::vector raw_iso(24*2352); for(unsigned i=0;i<24;++i) { raw_iso[i*2352+15]=2; std::copy_n(iso.begin()+i*2048,2048,raw_iso.begin()+i*2352+24); } const auto raw_path=disc_root/"original.bin"; write_bytes(raw_path,raw_iso); check(PSXRecompV4::mod_runtime_commit(raw_path,&error),"reader mount raw disc"); check(psx_mod_read_disc_file("S0/LEVEL.NSF",result.data(),(uint32_t)result.size(),&bytes) && std::equal(result.begin(),result.end(),iso.begin()+22*2048),"raw and ISO reads identical"); fs::remove_all(root, ec); if (failures) return 1; std::cout << "mod runtime tests passed\n"; return 0; }