From 75d729ce40d7eed9649fd4bb05628dee520f3d0c Mon Sep 17 00:00:00 2001 From: Ranieri Date: Sat, 19 Sep 2026 21:31:44 -0300 Subject: [PATCH] Feature/iop emulator (#244) * refactor: from guest threads to EE scheduler * feat: bad wip mpeg fix for code veronica * feat: cheap copy from host feat: small perf o vsync tick * feat: added EE clock Hz fix: fix MPEG out of sync with new EE refactor * fix: fix lotr tests * fix: fix cri dtx loading fix: fix wrong mmi instruction translation fix: fix thread info params feat: added EE timers decoder and consumer feat: split SFI and IOP memory to prevent collision and overrides * feat: revert wrong changes * refactor: change GS architecture * feat: IOP emulator refactor: codegen to catch callbacks on mips code feat: added a lot of entries or IOP emulator * feat: analyzer resolve the complete constant-producing sequence with five-instruction backward scan stopped at LUI and therefore * feat: remove recompiled version of GetRomName refactor: split IOP emulator logic feat: added more HLE IOP modules feat: added ps2_path * eat: enhance ELF parser with improved callable entry detection and control flow analysis * feat: update memory hint handling and enhance entry point discovery logic * feat: add SET_GPR_ZE32 macro for zero-extending loads with unsigned semantics * refactor: Refactor PS2 IOP Host Adapter and Memory Management feat: Added PS2Vfs for virtual file system operations, including file opening, reading, writing, and path resolution. feat: Improve VIF1 data processing to handle GIF image packets more efficiently. * feat: added a lot of tests * fix: fix texture caching feat: wip multi version on dbcman * feat: remove LLE IOPs --- README.md | 18 +- ps2xAnalyzer/Readme.md | 11 +- ps2xAnalyzer/src/elf_analyzer.cpp | 74 +- ps2xAnalyzer/src/toml_generator.cpp | 6 +- ps2xIOP/CMakeLists.txt | 52 +- ps2xIOP/PluginExample.md | 220 --- ps2xIOP/README.md | 244 +--- ps2xIOP/include/ps2x/iop/iop_host.h | 40 + ps2xIOP/include/ps2x/iop/iop_subsystem.h | 20 +- ps2xIOP/include/ps2x/iop/iop_types.h | 16 +- ps2xIOP/include/ps2x/iop/plugin_api.h | 307 ---- ps2xIOP/include/ps2x/iop/ps2_path.h | 35 + ps2xIOP/src/builtin_profiles.cpp | 150 -- ps2xIOP/src/emulator/core/iop_cpu.cpp | 497 +++++++ ps2xIOP/src/emulator/core/iop_cpu.h | 42 + ps2xIOP/src/emulator/core/iop_kernel.cpp | 744 ++++++++++ ps2xIOP/src/emulator/core/iop_kernel.h | 103 ++ ps2xIOP/src/emulator/core/iop_memory.cpp | 371 +++++ ps2xIOP/src/emulator/core/iop_memory.h | 91 ++ ps2xIOP/src/emulator/imports/iop_cdvd.cpp | 751 ++++++++++ ps2xIOP/src/emulator/imports/iop_cdvd.h | 43 + ps2xIOP/src/emulator/imports/iop_heaplib.cpp | 54 + ps2xIOP/src/emulator/imports/iop_heaplib.h | 20 + ps2xIOP/src/emulator/imports/iop_imports.cpp | 195 +++ ps2xIOP/src/emulator/imports/iop_imports.h | 50 + ps2xIOP/src/emulator/imports/iop_intrman.cpp | 113 ++ ps2xIOP/src/emulator/imports/iop_intrman.h | 33 + ps2xIOP/src/emulator/imports/iop_ioman.cpp | 91 ++ ps2xIOP/src/emulator/imports/iop_ioman.h | 32 + ps2xIOP/src/emulator/imports/iop_loadcore.cpp | 65 + ps2xIOP/src/emulator/imports/iop_loadcore.h | 22 + ps2xIOP/src/emulator/imports/iop_stdio.cpp | 67 + ps2xIOP/src/emulator/imports/iop_stdio.h | 26 + ps2xIOP/src/emulator/imports/iop_sysclib.cpp | 336 +++++ ps2xIOP/src/emulator/imports/iop_sysclib.h | 20 + ps2xIOP/src/emulator/imports/iop_sysmem.cpp | 72 + ps2xIOP/src/emulator/imports/iop_sysmem.h | 26 + ps2xIOP/src/emulator/imports/iop_timrman.cpp | 476 ++++++ ps2xIOP/src/emulator/imports/iop_timrman.h | 67 + ps2xIOP/src/emulator/imports/iop_vblank.cpp | 42 + ps2xIOP/src/emulator/imports/iop_vblank.h | 20 + ps2xIOP/src/emulator/iop_emulator.cpp | 830 +++++++++++ ps2xIOP/src/emulator/iop_emulator.h | 49 + ps2xIOP/src/emulator/iop_emulator_const.h | 15 + .../emulator/services/iop_module_loader.cpp | 561 +++++++ .../src/emulator/services/iop_module_loader.h | 48 + ps2xIOP/src/emulator/services/iop_rpc.cpp | 349 +++++ ps2xIOP/src/emulator/services/iop_rpc.h | 78 + ps2xIOP/src/iop_module_manager.cpp | 180 +++ ps2xIOP/src/iop_module_manager.h | 49 + ps2xIOP/src/iop_service.h | 19 +- ps2xIOP/src/iop_subsystem.cpp | 412 +++--- ps2xIOP/src/module_factories.h | 147 -- ps2xIOP/src/modules/clfile.cpp | 635 -------- ps2xIOP/src/modules/cri_dtx.cpp | 1299 ----------------- ps2xIOP/src/modules/dbcman.cpp | 37 +- ps2xIOP/src/modules/libsd.cpp | 6 + ps2xIOP/src/modules/mcserv.cpp | 168 +-- ps2xIOP/src/modules/sdrdrv.cpp | 335 ----- ps2xIOP/src/modules/sound_update_stub.cpp | 236 --- ps2xIOP/src/modules/tsnddrv.cpp | 628 -------- ps2xIOP/src/plugin_loader.cpp | 956 ------------ ps2xIOP/src/plugin_loader.h | 34 - ps2xIOP/src/ps2_path.cpp | 123 ++ ps2xIOP/src/rpc_reply.h | 22 + ps2xIOP/tests/iop_compat_test_support.h | 237 +++ ps2xIOP/tests/iop_compatibility_tests.cpp | 315 ++++ ps2xIOP/tests/iop_emulator_tests.cpp | 1147 +++++++++++++++ ps2xIOP/tests/iop_import_tests.cpp | 338 +++++ ps2xIOP/tests/iop_import_version_tests.cpp | 170 +++ ps2xRecomp/include/ps2recomp/instructions.h | 42 + ps2xRecomp/include/ps2recomp/ps2_recompiler.h | 10 +- ps2xRecomp/include/ps2recomp/types.h | 1 + ps2xRecomp/src/lib/config_manager.cpp | 22 + ps2xRecomp/src/lib/elf_parser.cpp | 1227 +++++++++++++++- ps2xRecomp/src/lib/function_emitter.cpp | 4 +- ps2xRecomp/src/lib/function_table_emitter.cpp | 4 +- ps2xRecomp/src/lib/instruction_translator.cpp | 16 +- ps2xRecomp/src/lib/ps2_recompiler.cpp | 206 ++- .../tools/ghidra/ExportPS2Functions.java | 419 +++++- ps2xRuntime/CMakeLists.txt | 6 +- ps2xRuntime/include/ps2_call_list.h | 1 - ps2xRuntime/include/ps2_runtime.h | 21 +- ps2xRuntime/include/ps2_runtime_macros.h | 11 + ps2xRuntime/include/ps2_syscalls.h | 2 - ps2xRuntime/include/runtime/ee_scheduler.h | 7 + ps2xRuntime/include/runtime/gs/gs_backend.h | 1 + .../include/runtime/gs/gs_cpu_backend.h | 14 +- .../runtime/gs/gs_texture_page_cache.h | 37 + .../include/runtime/gs/ps2_gs_memory.h | 15 +- ps2xRuntime/include/runtime/ps2_memory.h | 2 + ps2xRuntime/include/runtime/ps2_rom_device.h | 43 + ps2xRuntime/include/runtime/ps2_vfs.h | 82 ++ ps2xRuntime/src/lib/Kernel/EeScheduler.cpp | 56 +- .../src/lib/Kernel/Stubs/Helpers/Support.h | 44 +- ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp | 440 ++---- ps2xRuntime/src/lib/Kernel/Stubs/SIF.h | 9 +- ps2xRuntime/src/lib/Kernel/Syscalls/Common.h | 1 + .../src/lib/Kernel/Syscalls/Dispatcher.cpp | 3 + .../src/lib/Kernel/Syscalls/FileIO.cpp | 224 +-- .../src/lib/Kernel/Syscalls/Helpers/Loader.h | 72 +- .../src/lib/Kernel/Syscalls/Helpers/Runtime.h | 45 +- .../src/lib/Kernel/Syscalls/Helpers/State.h | 5 - ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp | 52 +- .../src/lib/Kernel/Syscalls/System.cpp | 59 +- ps2xRuntime/src/lib/Kernel/Syscalls/System.h | 1 - ps2xRuntime/src/lib/gs/gs_cpu_backend.cpp | 178 ++- ps2xRuntime/src/lib/gs/gs_frontend.cpp | 2 + ps2xRuntime/src/lib/gs/ps2_gs_memory.cpp | 35 + ps2xRuntime/src/lib/ps2_debug_panel.cpp | 103 +- ps2xRuntime/src/lib/ps2_iop_host.cpp | 68 +- ps2xRuntime/src/lib/ps2_iop_host.h | 5 + ps2xRuntime/src/lib/ps2_iop_transport.h | 14 +- ps2xRuntime/src/lib/ps2_memory.cpp | 168 ++- ps2xRuntime/src/lib/ps2_rom_device.cpp | 178 +++ ps2xRuntime/src/lib/ps2_runtime.cpp | 89 +- ps2xRuntime/src/lib/ps2_vfs.cpp | 312 ++++ ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp | 73 +- ps2xTest/CMakeLists.txt | 39 +- ps2xTest/gs_cache/CMakeLists.txt | 59 + ps2xTest/gs_cache/gs_clut_cache_tests.cpp | 283 ++++ ps2xTest/gs_cache/gs_memory_cache_tests.cpp | 99 ++ ps2xTest/gs_cache/gs_test_support.h | 190 +++ ps2xTest/gs_cache/gs_texture_cache_tests.cpp | 177 +++ ps2xTest/src/code_generator_tests.cpp | 128 ++ ps2xTest/src/elf_analyzer_tests.cpp | 2 + ps2xTest/src/fake_iop_bad_abi.c | 15 - ps2xTest/src/fake_iop_missing_symbol.cpp | 6 - ps2xTest/src/fake_iop_plugin.cpp | 350 ----- ps2xTest/src/ps2_gs_tests.cpp | 125 +- ps2xTest/src/ps2_iop_tests.cpp | 908 +----------- ps2xTest/src/ps2_memory_tests.cpp | 289 +++- ps2xTest/src/ps2_recompiler_tests.cpp | 724 ++++++++- ps2xTest/src/ps2_runtime_expansion_tests.cpp | 26 + ps2xTest/src/ps2_runtime_interrupt_tests.cpp | 135 ++ ps2xTest/src/ps2_runtime_io_tests.cpp | 154 +- ps2xTest/src/ps2_runtime_kernel_tests.cpp | 32 +- ps2xTest/src/ps2_sif_dma_tests.cpp | 715 +-------- ps2xTest/src/ps2_sif_rpc_tests.cpp | 833 ++--------- 139 files changed, 15625 insertions(+), 9178 deletions(-) delete mode 100644 ps2xIOP/PluginExample.md delete mode 100644 ps2xIOP/include/ps2x/iop/plugin_api.h create mode 100644 ps2xIOP/include/ps2x/iop/ps2_path.h delete mode 100644 ps2xIOP/src/builtin_profiles.cpp create mode 100644 ps2xIOP/src/emulator/core/iop_cpu.cpp create mode 100644 ps2xIOP/src/emulator/core/iop_cpu.h create mode 100644 ps2xIOP/src/emulator/core/iop_kernel.cpp create mode 100644 ps2xIOP/src/emulator/core/iop_kernel.h create mode 100644 ps2xIOP/src/emulator/core/iop_memory.cpp create mode 100644 ps2xIOP/src/emulator/core/iop_memory.h create mode 100644 ps2xIOP/src/emulator/imports/iop_cdvd.cpp create mode 100644 ps2xIOP/src/emulator/imports/iop_cdvd.h create mode 100644 ps2xIOP/src/emulator/imports/iop_heaplib.cpp create mode 100644 ps2xIOP/src/emulator/imports/iop_heaplib.h create mode 100644 ps2xIOP/src/emulator/imports/iop_imports.cpp create mode 100644 ps2xIOP/src/emulator/imports/iop_imports.h create mode 100644 ps2xIOP/src/emulator/imports/iop_intrman.cpp create mode 100644 ps2xIOP/src/emulator/imports/iop_intrman.h create mode 100644 ps2xIOP/src/emulator/imports/iop_ioman.cpp create mode 100644 ps2xIOP/src/emulator/imports/iop_ioman.h create mode 100644 ps2xIOP/src/emulator/imports/iop_loadcore.cpp create mode 100644 ps2xIOP/src/emulator/imports/iop_loadcore.h create mode 100644 ps2xIOP/src/emulator/imports/iop_stdio.cpp create mode 100644 ps2xIOP/src/emulator/imports/iop_stdio.h create mode 100644 ps2xIOP/src/emulator/imports/iop_sysclib.cpp create mode 100644 ps2xIOP/src/emulator/imports/iop_sysclib.h create mode 100644 ps2xIOP/src/emulator/imports/iop_sysmem.cpp create mode 100644 ps2xIOP/src/emulator/imports/iop_sysmem.h create mode 100644 ps2xIOP/src/emulator/imports/iop_timrman.cpp create mode 100644 ps2xIOP/src/emulator/imports/iop_timrman.h create mode 100644 ps2xIOP/src/emulator/imports/iop_vblank.cpp create mode 100644 ps2xIOP/src/emulator/imports/iop_vblank.h create mode 100644 ps2xIOP/src/emulator/iop_emulator.cpp create mode 100644 ps2xIOP/src/emulator/iop_emulator.h create mode 100644 ps2xIOP/src/emulator/iop_emulator_const.h create mode 100644 ps2xIOP/src/emulator/services/iop_module_loader.cpp create mode 100644 ps2xIOP/src/emulator/services/iop_module_loader.h create mode 100644 ps2xIOP/src/emulator/services/iop_rpc.cpp create mode 100644 ps2xIOP/src/emulator/services/iop_rpc.h create mode 100644 ps2xIOP/src/iop_module_manager.cpp create mode 100644 ps2xIOP/src/iop_module_manager.h delete mode 100644 ps2xIOP/src/modules/clfile.cpp delete mode 100644 ps2xIOP/src/modules/cri_dtx.cpp delete mode 100644 ps2xIOP/src/modules/sdrdrv.cpp delete mode 100644 ps2xIOP/src/modules/sound_update_stub.cpp delete mode 100644 ps2xIOP/src/modules/tsnddrv.cpp delete mode 100644 ps2xIOP/src/plugin_loader.cpp delete mode 100644 ps2xIOP/src/plugin_loader.h create mode 100644 ps2xIOP/src/ps2_path.cpp create mode 100644 ps2xIOP/src/rpc_reply.h create mode 100644 ps2xIOP/tests/iop_compat_test_support.h create mode 100644 ps2xIOP/tests/iop_compatibility_tests.cpp create mode 100644 ps2xIOP/tests/iop_emulator_tests.cpp create mode 100644 ps2xIOP/tests/iop_import_tests.cpp create mode 100644 ps2xIOP/tests/iop_import_version_tests.cpp create mode 100644 ps2xRuntime/include/runtime/gs/gs_texture_page_cache.h create mode 100644 ps2xRuntime/include/runtime/ps2_rom_device.h create mode 100644 ps2xRuntime/include/runtime/ps2_vfs.h create mode 100644 ps2xRuntime/src/lib/ps2_rom_device.cpp create mode 100644 ps2xRuntime/src/lib/ps2_vfs.cpp create mode 100644 ps2xTest/gs_cache/CMakeLists.txt create mode 100644 ps2xTest/gs_cache/gs_clut_cache_tests.cpp create mode 100644 ps2xTest/gs_cache/gs_memory_cache_tests.cpp create mode 100644 ps2xTest/gs_cache/gs_test_support.h create mode 100644 ps2xTest/gs_cache/gs_texture_cache_tests.cpp delete mode 100644 ps2xTest/src/fake_iop_bad_abi.c delete mode 100644 ps2xTest/src/fake_iop_missing_symbol.cpp delete mode 100644 ps2xTest/src/fake_iop_plugin.cpp diff --git a/README.md b/README.md index b1e6d93..469b279 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ This project statically recompiles PS2 ELF binaries into C++ and provides a runt * `ps2xAnalyzer`: scans ELF/functions and writes TOML config (`stubs`, `skip`, instruction patches). * `ps2xRecomp`: reads TOML + ELF, decodes R5900 instructions, and generates C++ output. * `ps2xRuntime`: hosts memory, function registration, syscall dispatch, and hardware stubs. -* `ps2xIOP`: portable, instance-owned IOP HLE services, game profiles, and the C plugin ABI. +* `ps2xIOP`: R3000A IRX execution, a virtual IOP kernel, and generic HLE fallbacks. ### Features @@ -79,9 +79,7 @@ Fallback workflow for quick local experiments or ELFs with debug symbol : ./ps2_analyzer your_game.elf config.toml ``` -Use this only when you do not have a Ghidra project yet. The native analyzer is faster to start, but it is less accurate on stripped retail games and more likely to miss internal callable entry points. - -See the [Ghidra Workflow](ps2xAnalyzer/Readme.md#3-ghidra-integration-for-retail-and-stripped-games-preferred) for the recommended path. +See the [Ghidra Workflow](ps2xAnalyzer/Readme.md#3-ghidra-integration-for-retail-and-stripped-games-preferred) for ghdira instructions. Then build generated output and link with `ps2xRuntime`. @@ -133,15 +131,15 @@ To execute the recompiled code. * Some syscall dispatcher with common kernel IDs. * Basic GS/VU/file/system stubs. * Foundation to expand and port your game. -* `ps2xIOP` profile selection and optional `.dll`/`.so` discovery for game-specific IOP HLE. +* `ps2xIOP` execution of original IRX modules with generic HLE fallbacks. -See [IOP HLE profiles and plugins](ps2xIOP/README.md) for the service boundary and external plugin workflow. +See [IOP emulation](ps2xIOP/README.md) for module execution and the service boundary. ### Game Override Hooks Game overrides are runtime-side, build-scoped patch modules. -A game override is C++ code that runs during `loadELF` and can replace EE function bindings by address for one specific game build. IOP RPC/DMA behavior belongs in a `ps2xIOP` profile instead. This is separate from recompilation output and separate from global runtime stubs/syscalls. +A game override is C++ code that runs during `loadELF` and can replace EE function bindings by address for one specific game build. IOP RPC/DMA behavior is handled by the `ps2xIOP` emulator and its runtime transport. This is separate from recompilation output and separate from global runtime stubs/syscalls. API: @@ -164,9 +162,8 @@ Use Game Override modules when: 6. Re-test from cold boot after each batch. ### Limitations - -* Graphics Synthesizer and other hardware components need external implementation -* VU1 microcode is not complete. + +* Performance is very bad for VU and GS * Hardware emulation is partial and many paths are stubbed. ### Acknowledgments @@ -175,3 +172,4 @@ Use Game Override modules when: * Uses ELFIO for ELF parsing * Uses toml11 for TOML parsing * Uses fmt for string formatting +* Reference for runtime PCSX2 \ No newline at end of file diff --git a/ps2xAnalyzer/Readme.md b/ps2xAnalyzer/Readme.md index e6f709a..503b16c 100644 --- a/ps2xAnalyzer/Readme.md +++ b/ps2xAnalyzer/Readme.md @@ -32,8 +32,7 @@ Japanese set, and depends on samples that retained relocations. Treat the result high-confidence hint rather than a complete SDK catalog: it can miss SDK variants that were not present in the sampled games, and ambiguous matches are intentionally ignored. -### 4. Ghidra Integration (For Retail and Stripped Games, Preferred) -This is the recommended workflow for almost every commercial game: +### 4. Ghidra Integration 1. Use the provided script: `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`. 2. Run it in Ghidra to export a CSV map of all functions. 3. Let the script generate the TOML, and keep the CSV path in `ghidra_output = "path/to/map.csv"`. @@ -63,8 +62,7 @@ ps2_analyzer [sce_symbol_db_dir] 1. Open `game.elf` in Ghidra. 2. Run `ps2xRecomp/tools/ghidra/ExportPS2Functions.java`. 3. Use the exported TOML and CSV. -4. Run the recompiler: - `ps2recomp config.toml` +4. Run the recompiler: `ps2recomp config.toml` Fallback: 1. Run `ps2_analyzer game.elf config.toml`. @@ -74,8 +72,8 @@ Fallback: The tool creates a TOML file with the following sections: * `[general]`: Paths to ELF and Ghidra maps. * `stubs`: Runtime-known functions to be replaced by C++ stubs or syscall handlers. -* `untracked_stubs`: Detected library-like functions without runtime handlers. This is - informational only and is ignored by the recompiler. +* `untracked_stubs`: Detected library-like functions without runtime handlers. This is informational only and is ignored by the recompiler. +* `entry_points`: Guest functions without runtime handlers that may be referenced by address. * `skip`: Legacy compatibility field. The analyzer no longer auto-populates it. * `[patches]`: Individual instructions that need to be replaced (SYSCALLs, COP0, etc.). @@ -83,6 +81,5 @@ The tool creates a TOML file with the following sections: * Heuristics may not catch all special cases in highly optimized code. * Self-modifying code is flagged but requires manual review. -* Indirect jumps (jump tables) are detected but complex ones might need manual TOML entries. For more details on the recompilation process, see the [Main README](../README.md). diff --git a/ps2xAnalyzer/src/elf_analyzer.cpp b/ps2xAnalyzer/src/elf_analyzer.cpp index b13ce20..5c57cdb 100644 --- a/ps2xAnalyzer/src/elf_analyzer.cpp +++ b/ps2xAnalyzer/src/elf_analyzer.cpp @@ -1,4 +1,5 @@ #include "ps2recomp/elf_analyzer.h" +#include "ps2recomp/gif_dma_kick_analyzer.h" #include "ps2recomp/analysis_passes.h" #include "ps2recomp/elf_parser.h" #include "ps2recomp/r5900_decoder.h" @@ -358,9 +359,11 @@ namespace ps2recomp } const auto &instructions = getDecodedInstructions(func); + ConstantRegisterState constantRegisters; for (const auto &inst : instructions) { + const MemoryAccessHint directAddress = resolveMemoryAccessHint(inst, constantRegisters); if (inst.opcode == OPCODE_LW || inst.opcode == OPCODE_SW || inst.opcode == OPCODE_LB || inst.opcode == OPCODE_SB || inst.opcode == OPCODE_LH || inst.opcode == OPCODE_SH || @@ -420,65 +423,46 @@ namespace ps2recomp } } } - // Also check for direct addressing with LUI+ADDIU combinations - else if (inst.opcode == OPCODE_LW || inst.opcode == OPCODE_SW) + + else if ((inst.opcode == OPCODE_LW || inst.opcode == OPCODE_SW) && directAddress.hasAddress) { - // Look for the LUI instruction that sets up the high bits - uint32_t baseAddr = 0; - for (int i = 1; i <= 5 && static_cast(inst.address) - i * 4 >= static_cast(func.start); i++) - { - uint32_t prevAddr = inst.address - i * 4; - uint32_t prevInst = 0; - if (!tryReadWord(m_elfParser.get(), prevAddr, prevInst)) - { - continue; - } + const uint32_t targetAddr = directAddress.address; - // Check if it's a LUI instruction for the same register - if (OPCODE(prevInst) == OPCODE_LUI && RT(prevInst) == inst.rs) - { - baseAddr = IMMEDIATE(prevInst) << 16; - break; - } + // Detect MMIO accesses + if ( + (targetAddr >= 0x10000000 && targetAddr < 0x14000000) || // I/O + (targetAddr >= 0x70000000 && targetAddr < 0x70004000) // Scratchpad + ) + { + m_mmioByInstructionAddress[inst.address] = targetAddr; + std::cout << "Detected MMIO access at " << std::hex << inst.address << " -> " << targetAddr << std::dec << std::endl; } - if (baseAddr != 0) + for (const auto §ion : m_context.sections) { - uint32_t targetAddr = baseAddr + static_cast(inst.immediate); - - // Detect MMIO accesses - if ((targetAddr >= 0x10000000 && targetAddr < 0x14000000) || // I/O - (targetAddr >= 0x70000000 && targetAddr < 0x70004000)) // Scratchpad + if (targetAddr >= section.address && targetAddr < section.address + section.size) { - m_mmioByInstructionAddress[inst.address] = targetAddr; - std::cout << "Detected MMIO access at " << std::hex << inst.address - << " -> " << targetAddr << std::dec << std::endl; - } + auto symIt = std::find_if(m_context.symbols.begin(), m_context.symbols.end(), + [targetAddr](const Symbol &s) + { return !s.isFunction && s.address <= targetAddr && + s.address + s.size > targetAddr; }); - for (const auto §ion : m_context.sections) - { - if (targetAddr >= section.address && targetAddr < section.address + section.size) + if (symIt != m_context.symbols.end()) { - auto symIt = std::find_if(m_context.symbols.begin(), m_context.symbols.end(), - [targetAddr](const Symbol &s) - { return !s.isFunction && s.address <= targetAddr && - s.address + s.size > targetAddr; }); + std::cout << "Function " << func.name << " directly accesses " + << (inst.opcode == OPCODE_LW ? "reads from" : "writes to") + << " data symbol " << symIt->name + << " at 0x" << std::hex << targetAddr << std::dec << std::endl; - if (symIt != m_context.symbols.end()) - { - std::cout << "Function " << func.name << " directly accesses " - << (inst.opcode == OPCODE_LW ? "reads from" : "writes to") - << " data symbol " << symIt->name - << " at 0x" << std::hex << targetAddr << std::dec << std::endl; - - m_functionDataUsage[func.name].insert(symIt->name); - } - break; + m_functionDataUsage[func.name].insert(symIt->name); } + break; } } } } + + updateConstantRegisters(inst, constantRegisters); } } diff --git a/ps2xAnalyzer/src/toml_generator.cpp b/ps2xAnalyzer/src/toml_generator.cpp index d083c80..cf4a251 100644 --- a/ps2xAnalyzer/src/toml_generator.cpp +++ b/ps2xAnalyzer/src/toml_generator.cpp @@ -138,9 +138,9 @@ namespace ps2recomp } file << "]\n\n"; - file << "# Detected library-like functions without runtime handlers.\n"; - file << "# This is informational only; PS2Recomp ignores this list and recompiles them normally.\n"; - file << "untracked_stubs = [\n"; + file << "# Guest functions without runtime handlers that may be referenced by address.\n"; + file << "# PS2Recomp keeps their guest implementation and exposes exact callable entries.\n"; + file << "entry_points = [\n"; for (const auto &func : untrackedStubEntries) { file << " \"" << func << "\",\n"; diff --git a/ps2xIOP/CMakeLists.txt b/ps2xIOP/CMakeLists.txt index e17b472..f8d3e44 100644 --- a/ps2xIOP/CMakeLists.txt +++ b/ps2xIOP/CMakeLists.txt @@ -2,20 +2,32 @@ cmake_minimum_required(VERSION 3.21) project(ps2xIOP LANGUAGES CXX) -option(PS2X_IOP_ENABLE_PLUGINS "Enable dynamic ps2xIOP plugins" OFF) +option(PS2X_IOP_BUILD_TESTS "Build ps2xIOP emulator smoke tests" OFF) add_library(ps2_iop STATIC + src/ps2_path.cpp + src/iop_module_manager.cpp src/iop_subsystem.cpp - src/builtin_profiles.cpp - src/plugin_loader.cpp + src/emulator/iop_emulator.cpp + src/emulator/core/iop_cpu.cpp + src/emulator/core/iop_kernel.cpp + src/emulator/core/iop_memory.cpp + src/emulator/services/iop_module_loader.cpp + src/emulator/services/iop_rpc.cpp + src/emulator/imports/iop_cdvd.cpp + src/emulator/imports/iop_heaplib.cpp + src/emulator/imports/iop_imports.cpp + src/emulator/imports/iop_intrman.cpp + src/emulator/imports/iop_ioman.cpp + src/emulator/imports/iop_loadcore.cpp + src/emulator/imports/iop_stdio.cpp + src/emulator/imports/iop_sysclib.cpp + src/emulator/imports/iop_sysmem.cpp + src/emulator/imports/iop_timrman.cpp + src/emulator/imports/iop_vblank.cpp src/modules/dbcman.cpp src/modules/libsd.cpp src/modules/mcserv.cpp - src/modules/tsnddrv.cpp - src/modules/cri_dtx.cpp - src/modules/clfile.cpp - src/modules/sound_update_stub.cpp - src/modules/sdrdrv.cpp ) target_compile_features(ps2_iop PUBLIC cxx_std_20) @@ -30,12 +42,26 @@ target_include_directories(ps2_iop add_library(ps2x::iop ALIAS ps2_iop) -target_compile_definitions(ps2_iop PUBLIC - PS2X_IOP_ENABLE_PLUGINS=$ -) +if(PS2X_IOP_BUILD_TESTS) + enable_testing() + add_executable(ps2_iop_emulator_tests tests/iop_emulator_tests.cpp) + target_link_libraries(ps2_iop_emulator_tests PRIVATE ps2_iop) + add_test(NAME ps2_iop_emulator_tests COMMAND ps2_iop_emulator_tests) + + add_executable(ps2_iop_import_tests tests/iop_import_tests.cpp) + target_link_libraries(ps2_iop_import_tests PRIVATE ps2_iop) + target_include_directories(ps2_iop_import_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + add_test(NAME ps2_iop_import_tests COMMAND ps2_iop_import_tests) + + add_executable(ps2_iop_compatibility_tests tests/iop_compatibility_tests.cpp) + target_link_libraries(ps2_iop_compatibility_tests PRIVATE ps2_iop) + add_test(NAME ps2_iop_compatibility_tests COMMAND ps2_iop_compatibility_tests) + + add_executable(ps2_iop_import_version_tests tests/iop_import_version_tests.cpp) + target_link_libraries(ps2_iop_import_version_tests PRIVATE ps2_iop) + target_include_directories(ps2_iop_import_version_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + add_test(NAME ps2_iop_import_version_tests COMMAND ps2_iop_import_version_tests) -if(PS2X_IOP_ENABLE_PLUGINS AND UNIX AND NOT APPLE) - target_link_libraries(ps2_iop PRIVATE ${CMAKE_DL_LIBS}) endif() install(TARGETS ps2_iop diff --git a/ps2xIOP/PluginExample.md b/ps2xIOP/PluginExample.md deleted file mode 100644 index 00d5f19..0000000 --- a/ps2xIOP/PluginExample.md +++ /dev/null @@ -1,220 +0,0 @@ -# Minimal plugin - -This plugin matches one ELF basename and handles one function on a synthetic -SID. It is synchronous: it signals NOWAIT completion and suppresses a second -dispatch through a registered EE server. - -```c -#include - -#include - -#define STRING_VIEW(literal) { (literal), sizeof(literal) - 1u } - -enum -{ - MY_SID = 0x6D795349u, - MY_FUNCTION = 1u, -}; - -struct my_state -{ - const ps2x_iop_host_api_v1 *host; -}; - -static void *my_create(const ps2x_iop_host_api_v1 *host, - const ps2x_iop_game_identity_v1 *identity) -{ - struct my_state *state; - (void)identity; - - if (!host || - host->abi_version != PS2X_IOP_ABI_VERSION_V1 || - host->struct_size < sizeof(*host)) - { - return NULL; - } - - state = (struct my_state *)calloc(1u, sizeof(*state)); - if (state) - { - state->host = host; - } - return state; -} - -static void my_destroy(void *instance) -{ - free(instance); -} - -static int32_t my_reset(void *instance) -{ - return instance ? PS2X_IOP_STATUS_OK_V1 - : PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; -} - -static int32_t my_handle_rpc(void *instance, - const ps2x_iop_rpc_request_v1 *request, - ps2x_iop_rpc_result_v1 *result) -{ - struct my_state *state = (struct my_state *)instance; - const uint32_t value = 1u; - int32_t status; - - if (!state || !request || !result || - request->struct_size < sizeof(*request) || - result->struct_size < sizeof(*result)) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - - result->handled = 0u; - result->result_address = 0u; - result->signal_nowait_completion = 0u; - result->signal_completion = 0u; - result->callback_policy = PS2X_IOP_CALLBACK_RUNTIME_DEFAULT_V1; - result->server_dispatch_policy = - PS2X_IOP_SERVER_DISPATCH_RUNTIME_DEFAULT_V1; - - if (request->sid != MY_SID || request->function != MY_FUNCTION) - { - return PS2X_IOP_STATUS_OK_V1; - } - if (request->receive.size < sizeof(value)) - { - return PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1; - } - if (!state->host->write_guest) - { - return PS2X_IOP_STATUS_UNSUPPORTED_V1; - } - - status = state->host->write_guest(state->host->userdata, - request->receive.address, - &value, - sizeof(value)); - if (status != PS2X_IOP_STATUS_OK_V1) - { - return status; - } - - result->handled = 1u; - result->result_address = request->receive.address; - result->signal_nowait_completion = 1u; - result->server_dispatch_policy = PS2X_IOP_SERVER_DISPATCH_SUPPRESS_V1; - return PS2X_IOP_STATUS_OK_V1; -} - -static const uint32_t my_sids[] = { MY_SID }; - -static const ps2x_iop_profile_api_v1 my_profiles[] = { - { - PS2X_IOP_ABI_VERSION_V1, - sizeof(ps2x_iop_profile_api_v1), - STRING_VIEW("my-game-profile"), - { - sizeof(ps2x_iop_game_matcher_v1), - STRING_VIEW("SLUS_000.00"), - 0u, - 0u, - }, - 1u, - my_sids, - my_create, - my_destroy, - my_reset, - NULL, - my_handle_rpc, - NULL, - NULL, - NULL, - }, -}; - -PS2X_IOP_PLUGIN_EXPORT int32_t -ps2x_iop_query_v1(uint32_t host_abi_version, - ps2x_iop_plugin_api_v1 *out) -{ - static const ps2x_iop_plugin_api_v1 plugin = { - PS2X_IOP_ABI_VERSION_V1, - sizeof(ps2x_iop_plugin_api_v1), - STRING_VIEW("my-iop-plugin"), - STRING_VIEW("1.0.0"), - 1u, - my_profiles, - }; - - if (host_abi_version != PS2X_IOP_ABI_VERSION_V1) - { - return PS2X_IOP_STATUS_UNSUPPORTED_V1; - } - if (!out) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - if (out->struct_size < sizeof(*out)) - { - return PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1; - } - - *out = plugin; - return PS2X_IOP_STATUS_OK_V1; -} -``` - -### Standalone CMake target - -The plugin consumes only the public ABI header; it does not link to `ps2xRuntime` or `ps2_iop`. - -```cmake -cmake_minimum_required(VERSION 3.21) -project(my_iop_plugin LANGUAGES C) - -set(PS2X_IOP_INCLUDE_DIR "" CACHE PATH - "Directory containing ps2x/iop/plugin_api.h" -) -if(NOT EXISTS "${PS2X_IOP_INCLUDE_DIR}/ps2x/iop/plugin_api.h") - message(FATAL_ERROR - "Set PS2X_IOP_INCLUDE_DIR to PS2Recomp/ps2xIOP/include" - ) -endif() - -add_library(my_iop_plugin MODULE my_iop_plugin.c) -target_include_directories(my_iop_plugin PRIVATE - "${PS2X_IOP_INCLUDE_DIR}" -) -set_target_properties(my_iop_plugin PROPERTIES - PREFIX "" - C_STANDARD 11 - C_STANDARD_REQUIRED YES - C_EXTENSIONS NO -) -install(TARGETS my_iop_plugin - RUNTIME DESTINATION . - LIBRARY DESTINATION . -) -``` - -On Windows: - -```powershell -cmake -S . -B build -A x64 ` - -DPS2X_IOP_INCLUDE_DIR="C:/path/to/PS2Recomp/ps2xIOP/include" -cmake --build build --config Release -cmake --install build --config Release ` - --prefix "C:/path/to/ps2EntryRunner/iop_plugins" -``` - -On Linux: - -```sh -cmake -S . -B build -DCMAKE_BUILD_TYPE=Release \ - -DPS2X_IOP_INCLUDE_DIR=/path/to/PS2Recomp/ps2xIOP/include -cmake --build build -j -cmake --install build --prefix /path/to/ps2EntryRunner/iop_plugins -``` - -Build or install the resulting `.dll`/`.so` before the runtime calls -`initialize()`. If it is not installed directly, copy it into the executable's -`iop_plugins/` directory. \ No newline at end of file diff --git a/ps2xIOP/README.md b/ps2xIOP/README.md index 4538f50..2f47d9e 100644 --- a/ps2xIOP/README.md +++ b/ps2xIOP/README.md @@ -1,219 +1,57 @@ # ps2xIOP -`ps2xIOP` is the IOP high-level emulation (HLE) subsystem used by -`ps2xRuntime`. It implements the behavior that games expect from IOP services -exposed through SIF RPC and DMA. +`ps2xIOP` runs original IRX modules on an R3000A interpreter, with a virtual +IOP kernel providing imports without a PS2 BIOS. The C++20 static library +`ps2_iop` / `ps2x::iop` is linked into `ps2xRuntime`. -This subsystem does not emulate the IOP's R3000A CPU and does not load or -execute IRX binaries. Its scope is the RPC/DMA behavior needed by recompiled -games. +## Execution policy -> [!IMPORTANT] -> `ps2_iop`/`ps2x::iop` is a C++20 static library linked into the runtime. -> Optional `.dll` and `.so` files are native profile plugins loaded by that -> library. They extend the profile catalog; they do not replace `ps2_iop`, its -> registry, its host bridge, the SIF transport, or execute PS2 IRX code. +Game-specific IOP code executes from IRX modules. There is no game-profile +selection or native profile-plugin loader. A physical IRX RPC server is +authoritative for its SID. -## Architecture +Generic HLE services remain available when no loaded IRX provides an endpoint: -```text -EE game - | - | SIF RPC / DMA - v -ps2xRuntime transport - | - | RpcRequest / RpcResult / SifTransfer - v -ps2x::iop::IopSubsystem - |-- selected game profile services - |-- core services - | - v -IopHost bridge -> validated guest memory, files, audio, memory card, - logging, and EE function invocation -``` - -### Modules, bindings, and profiles - -These terms describe different layers: - -- A **module implementation** is a reusable protocol engine, such as TSNDDRV, - CRI DTX, CLFILE, or SDRDRV. -- A **binding** contains build-specific values: SIDs, absolute EE addresses, - callback addresses, guest arenas, archive names, and protocol variants. -- A **profile** matches one game build and creates the required module - implementations with that build's bindings. - -For example, `cri_dtx.cpp` contains the reusable CRI DTX engine, while the -`recvx-us` profile supplies Code: Veronica X addresses. A second game should -reuse that engine only after its wire protocol has been compared with the -characterized variant; normally only its profile bindings should change. -Parameterized does not mean universally protocol-compatible. In particular, -`sound_update_stub.cpp` is a narrow LotR compatibility shim, not a complete -generic SOUND driver. - -## Built-in services and profiles - -Core services are created for every `IopSubsystem`: - -| Service | SID | Availability | +| Service | SID | Activation | | --- | --- | --- | -| MCSERV | `0x80000400`, `0x80000480` | Always active | -| LIBSD | `0x80000701` | Always active | -| DBCMAN | `0x80001300` | Always active | +| MCSERV | `0x80000400`, `0x80000480` | Recognized module load | +| LIBSD | `0x80000701` | Recognized module load | +| DBCMAN | `0x80001300` | Recognized module load | -The current built-in game profiles are: +These services are dormant before module load and after reset or the final +module stop. Unknown modules fail to load; unknown RPC SIDs remain unhandled. +Games previously using TSNDDRV, CRI DTX, CLFILE, SOUND or SDRDRV profiles now +require their IRX modules and support for the imports and hardware they use. -| Profile | Matcher | Services | -| --- | --- | --- | -| `recvx-us` | `slus_201.84` | TSNDDRV and CRI DTX | -| `lotr-two-towers-us` | `SLUS_205.78` | CLFILE and SOUND update compatibility | -| `fatal-frame-us` | `SLUS_203.88` | SDRDRV | +## Lifecycle and transport -All current built-ins declare only the ELF basename; they do not yet constrain -the entry point or CRC32. Basename matching is case-insensitive. +- `reset()` clears loaded modules, HLE service state and emulator state. +- `loadModule(...)` / `loadModuleBuffer(...)` load and start an IRX. +- `stopModule(...)` releases a module and its owned state. +- `runEeCycles(...)` advances the IOP from EE cycle accounting. +- `selectRpcAbi(...)`, `handleRpc(...)` and `onSifTransfer(...)` connect SIF transport. -If no profile matches, the subsystem still has MCSERV, LIBSD, and DBCMAN. It -does not create any game-specific service. An unknown SID remains unhandled so -the SIF transport can apply its normal fallback behavior and report it in the -debugger. +IOP RAM is separate from EE RAM. The transport copies data through the IOP +memory accessors; SIF notifications do not mirror bytes into equal-numbered EE +addresses. `RpcResult` describes completion and dispatch actions for the runtime. -## Profile selection - -When an ELF is loaded, the runtime calculates one `GameIdentity`: - -- ELF basename; -- entry point; -- CRC-32/IEEE (the common ZIP CRC-32) over the complete ELF file. - -A profile matcher may declare any combination of those fields. Every declared -field must match. The matcher with the greatest number of declared fields wins. -Two matching profiles with equal specificity are an error; `loadELF()` fails -instead of silently choosing one. - -A duplicate SID within the same service layer is an -error. Routing selects one service per SID: if a profile shadows a core SID and -then returns `handled = 0`, the subsystem does not make a second attempt through -the shadowed core service. - -## Dispatch and transfer flow - -`IopSubsystem` exposes five operations used by the runtime: - -1. `configure(GameIdentity)` selects and creates the active profile. -2. `reset()` resets core and profile services. -3. `selectRpcAbi(...)` lets a service choose the register or stack RPC layout when the default decoder is not sufficient. -4. `handleRpc(...)` routes a request by SID and returns both the payload result and the transport policy. -5. `onSifTransfer(...)` notifies services before and after SetDma and GetOtherData copies. - -`RpcResult::handled` indicates whether a service consumed the request. The -result can also request completion semaphore signals and can suppress the -runtime's default EE callback or registered-server dispatch. The transport -executes those actions; the service never reaches into runtime internals. - -The transfer hook is deliberately generic. TSNDDRV uses it for compatibility -backfill and CRI DTX uses it to observe DMA, but the SIF transport contains no -game names, game addresses, or branches for those modules. - -RPC ABI selection is offered to every active profile service before the core -services, and every active service receives each SIF transfer notification. -Implementations must filter the relevant SID/function or transfer -kind/phase/address range themselves. - -## Linking the static library - -```cmake -target_link_libraries(my_runtime PRIVATE ps2x::iop) -``` - -The public C++ API is -[`iop_subsystem.h`](include/ps2x/iop/iop_subsystem.h). Applications using -`PS2Runtime` normally do not construct it directly; the runtime creates the -subsystem and its `IopHost` adapter. - -## Dynamic profile plugins - -Dynamic plugins are optional and disabled by default. Enable them on Windows or -Linux with `PS2X_IOP_ENABLE_PLUGINS=ON`. - -| Platform | Plugin format | Status | -| --- | --- | --- | -| Windows | `.dll` | Supported | -| Linux | `.so` | Supported | - -When enable By default, the runtime scans `iop_plugins/` next to the executable. Discovery -is non-recursive. An embedding application can replace the search directories -before calling `initialize()`: - -```cpp -runtime.setIopPluginSearchPaths({ - std::filesystem::path{"path/to/my/iop_plugins"}, -}); -``` - -Each native module can publish one or more profiles. Missing query symbols, -incompatible ABI versions, malformed descriptors, and unsupported modules are -ignored with a diagnostic. Profile ambiguity, an active-layer SID conflict, or -failure to create the selected profile makes `loadELF()` fail with a clear -error. - -The v1 loader accepts at most 256 profiles per plugin and 256 SIDs per profile. -A profile needs a non-empty ID, at least one matcher field, at least one SID, -and valid `create`, `destroy`, `reset`, and `handle_rpc` callbacks. - -## Plugin ABI v1 - -Plugins include -[`plugin_api.h`](include/ps2x/iop/plugin_api.h) and export exactly one C entry -point: - -```c -PS2X_IOP_PLUGIN_EXPORT int32_t -ps2x_iop_query_v1(uint32_t host_abi_version, ps2x_iop_plugin_api_v1 *plugin_api); -``` - -The ABI uses only fixed C function tables and POD data: - -- validate `abi_version` and `struct_size` before accessing a structure; -- use pointer-plus-length string and buffer views; -- keep the profile instance behind an opaque `void *` handle; -- implement `create`, `destroy`, `reset`, and `handle_rpc`; -- optionally implement RPC ABI selection, SIF transfer hooks, and debug metrics; -- use host callbacks for guest memory, files, audio, memory cards, logging, and - EE function invocation; -- never retain request/result pointers after a callback returns; -- never pass STL types, C++ classes, exceptions, runtime objects, allocators, or - raw guest-memory pointers across the ABI. - -The plugin itself may be implemented in C or C++, but exceptions must not cross -the exported C boundary. Guest buffer fields are PS2 addresses, not host -pointers. - -The `host` function table passed to `create` may be retained until `destroy`. -The identity and its strings, RPC request/result, transfer, and metric pointers -are callback-scoped and must not be retained. `invoke_guest_function` is valid -only during `handle_rpc` and must use that request's `call_token`. Close file -handles and release guest allocations in `reset`/`destroy`. - -Most `int32_t`-returning host callbacks return a `PS2X_IOP_STATUS_*_V1` code. -Two are intentionally boolean-style: `has_guest_function` and -`invoke_guest_function` return `1` for yes/success, `0` for no/failure, and a -negative value for an API error. Do not compare their successful result with -`PS2X_IOP_STATUS_OK_V1`, which is zero. - -When compiling as C++, keep the exported query function under `extern "C"` -linkage. Including `plugin_api.h` provides the matching C declaration. - -FOr learn more you can check [PluginExample](./PluginExample.md) +Link with `target_link_libraries(my_runtime PRIVATE ps2x::iop)`. The public API +is [iop_subsystem.h](include/ps2x/iop/iop_subsystem.h); `PS2Runtime` owns its +subsystem and host adapter. ## Diagnostics and tests -`debugSnapshot()` exposes the active profile, its provider, registered core and -profile services, service metrics, loader diagnostics, and the last selection -error. The runtime debugger renders this data in the **IOP/SIF** tab. +`debugSnapshot()` exposes emulator cycle/instruction counts, loaded module, +thread and RPC-server counts, generic service metrics and load diagnostics. +The runtime debugger renders these in the **IOP/SIF** tab. -Registry behavior, instance isolation, reset, built-in services, profile -precedence, plugin discovery, ABI rejection, ambiguity, dispatch, destruction, -and module lifetime are covered by -[`ps2_iop_tests.cpp`](../ps2xTest/src/ps2_iop_tests.cpp). +Build standalone tests with: + +```sh +cmake -S ps2xIOP -B out/build/iop-tests -DPS2X_IOP_BUILD_TESTS=ON +cmake --build out/build/iop-tests +ctest --test-dir out/build/iop-tests --output-on-failure +``` + +The suites cover IRX execution, RPC, imports, version resolution and generic +HLE compatibility. `ps2x_tests` also covers runtime SIF RPC/DMA integration. diff --git a/ps2xIOP/include/ps2x/iop/iop_host.h b/ps2xIOP/include/ps2x/iop/iop_host.h index 1c872ef..1eceb08 100644 --- a/ps2xIOP/include/ps2x/iop/iop_host.h +++ b/ps2xIOP/include/ps2x/iop/iop_host.h @@ -62,6 +62,34 @@ namespace ps2x::iop virtual bool writeGuest(uint32_t address, const void *source, size_t size) = 0; virtual bool zeroGuest(uint32_t address, size_t size) = 0; virtual bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const = 0; + + // IOP RAM is a distinct address space from the EE guest. TODO remove this later + virtual bool readIopMemory(uint32_t address, void *destination, size_t size) const + { + (void)address; + (void)destination; + (void)size; + return false; + } + virtual bool writeIopMemory(uint32_t address, const void *source, size_t size) + { + (void)address; + (void)source; + (void)size; + return false; + } + virtual bool zeroIopMemory(uint32_t address, size_t size) + { + (void)address; + (void)size; + return false; + } + virtual bool normalizeIopAddress(uint32_t address, uint32_t &normalized) const + { + (void)address; + normalized = 0u; + return false; + } virtual uint32_t allocateIopHandle(IopHandleKind kind) = 0; virtual uint32_t allocateGuest(uint32_t size, uint32_t alignment) = 0; virtual void freeGuest(uint32_t address) = 0; @@ -90,6 +118,18 @@ namespace ps2x::iop uint32_t a3, uint32_t *resultAddress) = 0; + // Deliver an IOP -> EE SIF command packet. The default keeps hosts + // which do not emulate the EE command dispatcher source-compatible. + virtual bool sendSifCommand(uint32_t commandId, + const void *packet, + size_t packetSize) + { + (void)commandId; + (void)packet; + (void)packetSize; + return false; + } + virtual void log(LogLevel level, std::string_view message) = 0; }; } diff --git a/ps2xIOP/include/ps2x/iop/iop_subsystem.h b/ps2xIOP/include/ps2x/iop/iop_subsystem.h index 0d086a0..68fec81 100644 --- a/ps2xIOP/include/ps2x/iop/iop_subsystem.h +++ b/ps2xIOP/include/ps2x/iop/iop_subsystem.h @@ -3,9 +3,9 @@ #include "ps2x/iop/iop_host.h" #include "ps2x/iop/iop_types.h" -#include #include #include +#include #include namespace ps2x::iop @@ -21,16 +21,26 @@ namespace ps2x::iop IopSubsystem(IopSubsystem &&) noexcept; IopSubsystem &operator=(IopSubsystem &&) noexcept; - void setPluginSearchPaths(std::vector paths); - bool loadPlugins(std::string *error = nullptr); - - bool configure(const GameIdentity &identity, std::string *error = nullptr); void reset(); + [[nodiscard]] ModuleLoadResult loadModule(std::string_view path, const void *arguments = nullptr, uint32_t argumentSize = 0); + [[nodiscard]] ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments = nullptr, uint32_t argumentSize = 0); + [[nodiscard]] bool stopModule(int32_t moduleId, int32_t *result = nullptr); + void runEeCycles(uint64_t eeCycles) noexcept; + [[nodiscard]] RpcAbi selectRpcAbi(const RpcAbiRequest &request) const; + [[nodiscard]] bool canBindRpc(uint32_t sid) const noexcept; [[nodiscard]] RpcResult handleRpc(const RpcRequest &request); void onSifTransfer(const SifTransfer &transfer); + // Physical IOP RAM access shared by the emulator, SIF DMA, and HLE services. Addresses are IOP addresses. + [[nodiscard]] uint32_t allocateMemory(uint32_t size, uint32_t alignment = 16u); + [[nodiscard]] bool freeMemory(uint32_t address); + [[nodiscard]] bool readMemory(uint32_t address, void *destination, size_t size) const; + [[nodiscard]] bool writeMemory(uint32_t address, const void *source, size_t size); + [[nodiscard]] bool zeroMemory(uint32_t address, size_t size); + [[nodiscard]] bool isMemoryRange(uint32_t address, size_t size) const; + [[nodiscard]] DebugSnapshot debugSnapshot() const; private: diff --git a/ps2xIOP/include/ps2x/iop/iop_types.h b/ps2xIOP/include/ps2x/iop/iop_types.h index 37989ca..3491440 100644 --- a/ps2xIOP/include/ps2x/iop/iop_types.h +++ b/ps2xIOP/include/ps2x/iop/iop_types.h @@ -27,6 +27,13 @@ namespace ps2x::iop uint32_t crc32 = 0; }; + struct ModuleLoadResult + { + bool handled = false; + int32_t moduleId = -1; + int32_t startResult = -1; + }; + enum class RpcAbi : uint32_t { RuntimeDefault = 0, @@ -131,14 +138,17 @@ namespace ps2x::iop { std::string name; std::vector sids; - bool profileSpecific = false; + bool active = true; std::vector metrics; }; struct DebugSnapshot { - std::string activeProfile; - std::string activeProvider; + uint64_t emulatorCycles = 0; + uint64_t emulatorInstructions = 0; + uint32_t emulatorLoadedModules = 0; + uint32_t emulatorThreads = 0; + uint32_t emulatorRpcServers = 0; std::vector services; std::vector diagnostics; }; diff --git a/ps2xIOP/include/ps2x/iop/plugin_api.h b/ps2xIOP/include/ps2x/iop/plugin_api.h deleted file mode 100644 index 79748ac..0000000 --- a/ps2xIOP/include/ps2x/iop/plugin_api.h +++ /dev/null @@ -1,307 +0,0 @@ -#ifndef PS2X_IOP_PLUGIN_API_H -#define PS2X_IOP_PLUGIN_API_H - -#include -#include - -#if defined(_WIN32) -#define PS2X_IOP_PLUGIN_EXPORT __declspec(dllexport) -#elif defined(__GNUC__) || defined(__clang__) -#define PS2X_IOP_PLUGIN_EXPORT __attribute__((visibility("default"))) -#else -#define PS2X_IOP_PLUGIN_EXPORT -#endif - -#ifdef __cplusplus -extern "C" -{ -#endif - -#define PS2X_IOP_ABI_VERSION_V1 1u -#define PS2X_IOP_QUERY_SYMBOL_V1 "ps2x_iop_query_v1" - - enum ps2x_iop_status_v1 - { - PS2X_IOP_STATUS_OK_V1 = 0, - PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1 = 1, - PS2X_IOP_STATUS_INVALID_ARGUMENT_V1 = -1, - PS2X_IOP_STATUS_UNSUPPORTED_V1 = -2, - PS2X_IOP_STATUS_FAILED_V1 = -3, - }; - - enum ps2x_iop_rpc_abi_v1 - { - PS2X_IOP_RPC_ABI_DEFAULT_V1 = 0, - PS2X_IOP_RPC_ABI_REGISTERS_V1 = 1, - PS2X_IOP_RPC_ABI_STACK_V1 = 2, - }; - - enum ps2x_iop_callback_policy_v1 - { - PS2X_IOP_CALLBACK_RUNTIME_DEFAULT_V1 = 0, - PS2X_IOP_CALLBACK_SUPPRESS_V1 = 1, - }; - - enum ps2x_iop_server_dispatch_policy_v1 - { - PS2X_IOP_SERVER_DISPATCH_RUNTIME_DEFAULT_V1 = 0, - PS2X_IOP_SERVER_DISPATCH_SUPPRESS_V1 = 1, - }; - - enum ps2x_iop_transfer_kind_v1 - { - PS2X_IOP_TRANSFER_SET_DMA_V1 = 0, - PS2X_IOP_TRANSFER_GET_OTHER_DATA_V1 = 1, - }; - - enum ps2x_iop_transfer_phase_v1 - { - PS2X_IOP_TRANSFER_BEFORE_COPY_V1 = 0, - PS2X_IOP_TRANSFER_AFTER_COPY_V1 = 1, - }; - - enum ps2x_iop_host_path_kind_v1 - { - PS2X_IOP_PATH_ELF_DIRECTORY_V1 = 0, - PS2X_IOP_PATH_CD_ROOT_V1 = 1, - PS2X_IOP_PATH_CD_IMAGE_V1 = 2, - PS2X_IOP_PATH_HOST_ROOT_V1 = 3, - PS2X_IOP_PATH_MEMORY_CARD_ROOT_V1 = 4, - }; - - enum ps2x_iop_handle_kind_v1 - { - PS2X_IOP_HANDLE_RPC_SERVER_V1 = 0, - PS2X_IOP_HANDLE_RPC_PACKET_V1 = 1, - }; - - enum ps2x_iop_log_level_v1 - { - PS2X_IOP_LOG_DEBUG_V1 = 0, - PS2X_IOP_LOG_INFO_V1 = 1, - PS2X_IOP_LOG_WARNING_V1 = 2, - PS2X_IOP_LOG_ERROR_V1 = 3, - }; - - enum ps2x_iop_memory_card_operation_v1 - { - PS2X_IOP_MC_INIT_V1 = 0, - PS2X_IOP_MC_GET_INFO_V1 = 1, - PS2X_IOP_MC_OPEN_V1 = 2, - PS2X_IOP_MC_CLOSE_V1 = 3, - PS2X_IOP_MC_SEEK_V1 = 4, - PS2X_IOP_MC_READ_V1 = 5, - PS2X_IOP_MC_WRITE_V1 = 6, - PS2X_IOP_MC_FLUSH_V1 = 7, - PS2X_IOP_MC_CHDIR_V1 = 8, - PS2X_IOP_MC_GET_DIR_V1 = 9, - PS2X_IOP_MC_SET_FILE_INFO_V1 = 10, - PS2X_IOP_MC_DELETE_V1 = 11, - PS2X_IOP_MC_FORMAT_V1 = 12, - PS2X_IOP_MC_UNFORMAT_V1 = 13, - PS2X_IOP_MC_MKDIR_V1 = 14, - }; - - typedef struct ps2x_iop_string_view_v1 - { - const char *data; - size_t size; - } ps2x_iop_string_view_v1; - - typedef struct ps2x_iop_guest_buffer_v1 - { - uint32_t address; - uint32_t size; - } ps2x_iop_guest_buffer_v1; - - typedef struct ps2x_iop_game_identity_v1 - { - uint32_t struct_size; - ps2x_iop_string_view_v1 elf_name; - uint32_t entry_point; - uint32_t crc32; - } ps2x_iop_game_identity_v1; - - typedef struct ps2x_iop_game_matcher_v1 - { - uint32_t struct_size; - ps2x_iop_string_view_v1 elf_name; - uint32_t entry_point; - uint32_t crc32; - } ps2x_iop_game_matcher_v1; - - typedef struct ps2x_iop_rpc_candidate_v1 - { - uint32_t send_size; - uint32_t receive_address; - uint32_t receive_size; - uint32_t end_function; - uint32_t end_parameter; - uint32_t plausible; - } ps2x_iop_rpc_candidate_v1; - - typedef struct ps2x_iop_rpc_abi_request_v1 - { - uint32_t struct_size; - uint32_t bound_sid; - uint32_t function; - ps2x_iop_rpc_candidate_v1 registers; - ps2x_iop_rpc_candidate_v1 stack; - } ps2x_iop_rpc_abi_request_v1; - - typedef struct ps2x_iop_rpc_request_v1 - { - uint32_t struct_size; - uint64_t call_token; - uint32_t client_address; - uint32_t server_address; - uint32_t server_function; - uint32_t server_buffer; - uint32_t sid; - uint32_t function; - uint32_t mode; - ps2x_iop_guest_buffer_v1 send; - ps2x_iop_guest_buffer_v1 receive; - uint32_t end_function; - uint32_t end_parameter; - } ps2x_iop_rpc_request_v1; - - typedef struct ps2x_iop_rpc_result_v1 - { - uint32_t struct_size; - uint32_t handled; - uint32_t result_address; - uint32_t signal_nowait_completion; - uint32_t signal_completion; - uint32_t callback_policy; - uint32_t server_dispatch_policy; - } ps2x_iop_rpc_result_v1; - - typedef struct ps2x_iop_sif_transfer_v1 - { - uint32_t struct_size; - uint32_t kind; - uint32_t phase; - uint32_t source_address; - uint32_t destination_address; - uint32_t size; - } ps2x_iop_sif_transfer_v1; - - typedef struct ps2x_iop_debug_metric_v1 - { - uint32_t struct_size; - ps2x_iop_string_view_v1 name; - uint64_t value; - uint32_t hexadecimal; - } ps2x_iop_debug_metric_v1; - - typedef struct ps2x_iop_memory_card_request_v1 - { - uint32_t struct_size; - uint32_t operation; - uint32_t arguments[5]; - } ps2x_iop_memory_card_request_v1; - - typedef struct ps2x_iop_host_api_v1 - { - uint32_t abi_version; - uint32_t struct_size; - void *userdata; - - int32_t (*read_guest)(void *userdata, uint32_t address, void *destination, size_t size); - int32_t (*write_guest)(void *userdata, uint32_t address, const void *source, size_t size); - int32_t (*zero_guest)(void *userdata, uint32_t address, size_t size); - int32_t (*normalize_guest_address)(void *userdata, uint32_t address, uint32_t *normalized); - uint32_t (*allocate_iop_handle)(void *userdata, uint32_t kind); - uint32_t (*allocate_guest)(void *userdata, uint32_t size, uint32_t alignment); - void (*free_guest)(void *userdata, uint32_t address); - - int32_t (*audio_command)(void *userdata, - uint32_t sid, - uint32_t function, - ps2x_iop_guest_buffer_v1 send, - ps2x_iop_guest_buffer_v1 receive); - - int32_t (*get_host_path)(void *userdata, - uint32_t kind, - char *destination, - size_t capacity, - size_t *required_size); - int32_t (*translate_guest_path)(void *userdata, - ps2x_iop_string_view_v1 path, - char *destination, - size_t capacity, - size_t *required_size); - uint64_t (*open_host_file)(void *userdata, ps2x_iop_string_view_v1 path); - int32_t (*host_file_size)(void *userdata, uint64_t handle, uint64_t *size); - int32_t (*read_host_file)(void *userdata, - uint64_t handle, - uint64_t offset, - void *destination, - size_t size, - size_t *bytes_read); - void (*close_host_file)(void *userdata, uint64_t handle); - - int32_t (*memory_card)(void *userdata, const ps2x_iop_memory_card_request_v1 *request, int32_t *result); - - int32_t (*has_guest_function)(void *userdata, uint32_t address); - int32_t (*invoke_guest_function)(void *userdata, - uint64_t call_token, - uint32_t address, - uint32_t a0, - uint32_t a1, - uint32_t a2, - uint32_t a3, - uint32_t *result_address); - void (*log)(void *userdata, uint32_t level, ps2x_iop_string_view_v1 message); - } ps2x_iop_host_api_v1; - - typedef void *(*ps2x_iop_profile_create_v1)(const ps2x_iop_host_api_v1 *host, - const ps2x_iop_game_identity_v1 *identity); - typedef void (*ps2x_iop_profile_destroy_v1)(void *instance); - typedef int32_t (*ps2x_iop_profile_reset_v1)(void *instance); - typedef uint32_t (*ps2x_iop_profile_select_rpc_abi_v1)(void *instance, const ps2x_iop_rpc_abi_request_v1 *request); - typedef int32_t (*ps2x_iop_profile_handle_rpc_v1)(void *instance, - const ps2x_iop_rpc_request_v1 *request, - ps2x_iop_rpc_result_v1 *result); - typedef int32_t (*ps2x_iop_profile_on_sif_transfer_v1)(void *instance, const ps2x_iop_sif_transfer_v1 *transfer); - typedef size_t (*ps2x_iop_profile_debug_metric_count_v1)(void *instance); - typedef int32_t (*ps2x_iop_profile_debug_metric_v1)(void *instance, size_t index, ps2x_iop_debug_metric_v1 *metric); - - typedef struct ps2x_iop_profile_api_v1 - { - uint32_t abi_version; - uint32_t struct_size; - ps2x_iop_string_view_v1 id; - ps2x_iop_game_matcher_v1 matcher; - size_t sid_count; - const uint32_t *sids; - ps2x_iop_profile_create_v1 create; - ps2x_iop_profile_destroy_v1 destroy; - ps2x_iop_profile_reset_v1 reset; - ps2x_iop_profile_select_rpc_abi_v1 select_rpc_abi; - ps2x_iop_profile_handle_rpc_v1 handle_rpc; - ps2x_iop_profile_on_sif_transfer_v1 on_sif_transfer; - ps2x_iop_profile_debug_metric_count_v1 debug_metric_count; - ps2x_iop_profile_debug_metric_v1 debug_metric; - } ps2x_iop_profile_api_v1; - - typedef struct ps2x_iop_plugin_api_v1 - { - uint32_t abi_version; - uint32_t struct_size; - ps2x_iop_string_view_v1 name; - ps2x_iop_string_view_v1 version; - size_t profile_count; - const ps2x_iop_profile_api_v1 *profiles; - } ps2x_iop_plugin_api_v1; - - typedef int32_t (*ps2x_iop_query_v1_fn)(uint32_t host_abi_version, ps2x_iop_plugin_api_v1 *plugin_api); - - PS2X_IOP_PLUGIN_EXPORT int32_t ps2x_iop_query_v1(uint32_t host_abi_version, ps2x_iop_plugin_api_v1 *plugin_api); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/ps2xIOP/include/ps2x/iop/ps2_path.h b/ps2xIOP/include/ps2x/iop/ps2_path.h new file mode 100644 index 0000000..f9e02dc --- /dev/null +++ b/ps2xIOP/include/ps2x/iop/ps2_path.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include + +namespace ps2x::iop +{ + enum class Ps2PathDevice + { + Invalid, + Host, + Cdrom, + MemoryCard0, + Rom0, + NativeHost, + }; + + struct ParsedPs2Path + { + Ps2PathDevice device = Ps2PathDevice::Invalid; + std::string deviceName; + std::string path; + + [[nodiscard]] explicit operator bool() const noexcept + { + return device != Ps2PathDevice::Invalid; + } + }; + + [[nodiscard]] ParsedPs2Path parsePs2Path(std::string_view path); + + // Returns a lower-case module/file leaf without an optional .irx suffix. + [[nodiscard]] std::string ps2PathLeafKey(const ParsedPs2Path &path); + [[nodiscard]] std::string ps2PathLeafKey(std::string_view path); +} diff --git a/ps2xIOP/src/builtin_profiles.cpp b/ps2xIOP/src/builtin_profiles.cpp deleted file mode 100644 index ac537ad..0000000 --- a/ps2xIOP/src/builtin_profiles.cpp +++ /dev/null @@ -1,150 +0,0 @@ -#include "iop_service.h" -#include "module_factories.h" - -#include - -namespace ps2x::iop::detail -{ - namespace - { - TsnddrvBindings recvxTsnddrvBindings() - { - return { - .serviceName = "TSNDDRV", - .protocol = TsnddrvProtocolVariant::SndQueueV1, - .arena = { - .base = 0x00120000u, - .limit = 0x00200000u, - .statusAlignment = 0x100u, - .tableAlignment = 0x100u, - .storageAlignment = 0x1000u, - .hdBytes = 0x4000u, - .sqBytes = 0x18000u, - .dataBytes = 0x40000u, - }, - .checksumCandidates = { - {0x01E0EF10u, 0x01E0EF20u}, - {0x01E1EF10u, 0x01E1EF20u}, - }, - .busyFlagAddress = 0x01E212C8u, - .completionRules = { - {0x002EAC20u, true, true, false}, - {0x002EAC30u, true, true, true}, - {0x002FAC20u, true, true, false}, - {0x002FAC30u, true, true, true}, - }, - }; - } - - CriDtxBindings recvxCriDtxBindings() - { - return { - .serviceName = "CRI DTX", - .sid = 0x7D000000u, - .urpcObjectBase = 0x01F18000u, - .urpcObjectLimit = 0x01F1FF00u, - .urpcObjectStride = 0x20u, - .urpcFunctionTableBase = 0x0033FED0u, - .urpcObjectTableBase = 0x0033FFD0u, - .dispatcherFunctionAddress = 0x002FABC0u, - .rpcServerPoolBase = 0x01F10000u, - .rpcServerStride = 0x80u, - }; - } - - ClFileBindings lotrClFileBindings() - { - return { - .serviceName = "CLFILE", - .sid = 0x0000FF01u, - .rpc = {}, - }; - } - - SoundUpdateStubBindings lotrSoundBindings() - { - return { - .serviceName = "SOUND update compatibility stub", - .sid = 0x00012345u, - .activeStreamCountOffset = 0u, - .responseCounterOffset = 4u, - .zeroReceiveBuffer = true, - .signalNowaitCompletion = true, - .completeQueuedPlayStreams = true, - .suppressedCompletionCallbacks = {}, - }; - } - - SdrdrvBindings fatalFrameSdrdrvBindings() - { - return { - .serviceName = "SDRDRV", - .sid = 0x19740512u, - .imageHeaderAddress = 0x012F0000u, - .sectorSize = 2048u, - .statusOffset = 0x6Cu, - .statusStride = 8u, - .statusSlotMask = 0x1Fu, - .completeValue = 0u, - .imageHeaderLowerName = "img_hd.bin", - .imageHeaderUpperName = "IMG_HD.BIN", - .imageBodyLowerName = "img_bd.bin", - .imageBodyUpperName = "IMG_BD.BIN", - }; - } - } - - ServiceList createCoreServices(IopHost &host) - { - ServiceList services; - services.emplace_back(createMcservService(host)); - services.emplace_back(createDbcmanService(host)); - services.emplace_back(createLibSdService(host)); - return services; - } - - std::vector createBuiltinProfiles() - { - std::vector profiles; - - profiles.push_back({ - "recvx-us", - "builtin", - {.elfName = "slus_201.84"}, - [](IopHost &host, const GameIdentity &) - { - ServiceList services; - services.emplace_back(createTsnddrvService(host, recvxTsnddrvBindings())); - services.emplace_back(createCriDtxService(host, recvxCriDtxBindings())); - return services; - }, - }); - - profiles.push_back({ - "lotr-two-towers-us", - "builtin", - {.elfName = "SLUS_205.78"}, - [](IopHost &host, const GameIdentity &) - { - ServiceList services; - services.emplace_back(createClFileService(host, lotrClFileBindings())); - services.emplace_back(createSoundUpdateStubService(host, lotrSoundBindings())); - return services; - }, - }); - - profiles.push_back({ - "fatal-frame-us", - "builtin", - {.elfName = "SLUS_203.88"}, - [](IopHost &host, const GameIdentity &) - { - ServiceList services; - services.emplace_back(createSdrdrvService(host, fatalFrameSdrdrvBindings())); - return services; - }, - }); - - return profiles; - } -} diff --git a/ps2xIOP/src/emulator/core/iop_cpu.cpp b/ps2xIOP/src/emulator/core/iop_cpu.cpp new file mode 100644 index 0000000..e09642e --- /dev/null +++ b/ps2xIOP/src/emulator/core/iop_cpu.cpp @@ -0,0 +1,497 @@ +#include "iop_cpu.h" +#include "iop_memory.h" + +#include + +namespace ps2x::iop::detail +{ + IopCpuCore::IopCpuCore(IopMemory &memory) noexcept + : m_memory(memory) + { + } + + void IopCpuCore::writeRegister(IopCpuState &cpu, uint32_t reg, uint32_t value, uint32_t &writtenReg) + { + if (reg == 0u) + return; + cpu.gpr[reg] = value; + writtenReg = reg; + } + + void IopCpuCore::scheduleLoad(uint32_t reg, uint32_t value, bool &scheduled, uint32_t &scheduledReg, uint32_t &scheduledValue) + { + if (reg == 0u) + return; + scheduled = true; + scheduledReg = reg; + scheduledValue = value; + } + + void IopCpuCore::raiseException(IopCpuState &cpu, uint32_t code, uint32_t faultPc, bool delaySlot, std::optional badAddress) const + { + uint32_t cause = cpu.cop0[13] & ~0x7Cu; + cause |= (code & 0x1Fu) << 2u; + if (delaySlot) + { + cause |= 0x80000000u; + cpu.cop0[14] = faultPc - 4u; + } + else + { + cause &= ~0x80000000u; + cpu.cop0[14] = faultPc; + } + cpu.cop0[13] = cause; + if (badAddress) + cpu.cop0[8] = *badAddress; + const uint32_t status = cpu.cop0[12]; + cpu.cop0[12] = (status & ~0x3Fu) | ((status << 2u) & 0x3Fu); + cpu.pc = (status & (1u << 22u)) ? 0xBFC00180u : 0x80000080u; + cpu.branchPending = false; + cpu.pendingLoad = false; + cpu.exception = true; + } + + bool IopCpuCore::executeInstruction(IopCpuState &cpu) + { + const uint32_t pc = cpu.pc; + const uint32_t instruction = m_memory.read32(pc); + const bool wasDelaySlot = cpu.branchPending; + const uint32_t priorBranchTarget = cpu.branchTarget; + + cpu.branchPending = false; + cpu.exception = false; + cpu.yielded = false; + + const uint32_t opcode = instruction >> 26u; + const uint32_t rs = (instruction >> 21u) & 31u; + const uint32_t rt = (instruction >> 16u) & 31u; + const uint32_t rd = (instruction >> 11u) & 31u; + const uint32_t sa = (instruction >> 6u) & 31u; + const uint32_t funct = instruction & 63u; + const uint32_t imm = instruction & 0xFFFFu; + const int32_t simm = static_cast(imm); + const uint32_t nextPc = pc + 4u; + + uint32_t writtenReg = 0u; + bool scheduledLoad = false; + uint32_t scheduledReg = 0u; + uint32_t scheduledValue = 0u; + bool newBranch = false; + uint32_t newBranchTarget = 0u; + + auto branch = [&](bool condition) + { + if (condition) + { + newBranch = true; + newBranchTarget = nextPc + (static_cast(simm) << 2u); + } + }; + auto write = [&](uint32_t reg, uint32_t value) + { + writeRegister(cpu, reg, value, writtenReg); + }; + auto load = [&](uint32_t reg, uint32_t value) + { + scheduleLoad(reg, value, scheduledLoad, scheduledReg, scheduledValue); + }; + auto overflowAdd = [&](int32_t lhs, int32_t rhs, uint32_t reg) + { + const int64_t result = static_cast(lhs) + rhs; + if (result > std::numeric_limits::max() || result < std::numeric_limits::min()) + raiseException(cpu, 12u, pc, wasDelaySlot); + else + write(reg, static_cast(static_cast(result))); + }; + auto overflowSub = [&](int32_t lhs, int32_t rhs, uint32_t reg) + { + const int64_t result = static_cast(lhs) - rhs; + if (result > std::numeric_limits::max() || result < std::numeric_limits::min()) + raiseException(cpu, 12u, pc, wasDelaySlot); + else + write(reg, static_cast(static_cast(result))); + }; + + // TODO kill this magic number and make it a constant somewhere + switch (opcode) + { + case 0x00: + switch (funct) + { + case 0x00: + write(rd, cpu.gpr[rt] << sa); + break; + case 0x02: + write(rd, cpu.gpr[rt] >> sa); + break; + case 0x03: + write(rd, static_cast(static_cast(cpu.gpr[rt]) >> sa)); + break; + case 0x04: + write(rd, cpu.gpr[rt] << (cpu.gpr[rs] & 31u)); + break; + case 0x06: + write(rd, cpu.gpr[rt] >> (cpu.gpr[rs] & 31u)); + break; + case 0x07: + write(rd, static_cast(static_cast(cpu.gpr[rt]) >> (cpu.gpr[rs] & 31u))); + break; + case 0x08: + newBranch = true; + newBranchTarget = cpu.gpr[rs]; + break; + case 0x09: + write(rd ? rd : 31u, pc + 8u); + newBranch = true; + newBranchTarget = cpu.gpr[rs]; + break; + case 0x0C: + raiseException(cpu, 8u, pc, wasDelaySlot); + break; + case 0x0D: + raiseException(cpu, 9u, pc, wasDelaySlot); + break; + case 0x10: + write(rd, cpu.hi); + break; + case 0x11: + cpu.hi = cpu.gpr[rs]; + break; + case 0x12: + write(rd, cpu.lo); + break; + case 0x13: + cpu.lo = cpu.gpr[rs]; + break; + case 0x18: + { + const int64_t result = static_cast(static_cast(cpu.gpr[rs])) * static_cast(static_cast(cpu.gpr[rt])); + cpu.lo = static_cast(result); + cpu.hi = static_cast(static_cast(result) >> 32u); + break; + } + case 0x19: + { + const uint64_t result = static_cast(cpu.gpr[rs]) * cpu.gpr[rt]; + cpu.lo = static_cast(result); + cpu.hi = static_cast(result >> 32u); + break; + } + case 0x1A: + { + const int32_t lhs = static_cast(cpu.gpr[rs]); + const int32_t rhs = static_cast(cpu.gpr[rt]); + if (rhs == 0) + { + cpu.lo = lhs >= 0 ? 0xFFFFFFFFu : 1u; + cpu.hi = static_cast(lhs); + } + else if (lhs == std::numeric_limits::min() && rhs == -1) + { + cpu.lo = static_cast(lhs); + cpu.hi = 0u; + } + else + { + cpu.lo = static_cast(lhs / rhs); + cpu.hi = static_cast(lhs % rhs); + } + break; + } + case 0x1B: + if (cpu.gpr[rt] == 0u) + { + cpu.lo = 0xFFFFFFFFu; + cpu.hi = cpu.gpr[rs]; + } + else + { + cpu.lo = cpu.gpr[rs] / cpu.gpr[rt]; + cpu.hi = cpu.gpr[rs] % cpu.gpr[rt]; + } + break; + case 0x20: + overflowAdd(static_cast(cpu.gpr[rs]), static_cast(cpu.gpr[rt]), rd); + break; + case 0x21: + write(rd, cpu.gpr[rs] + cpu.gpr[rt]); + break; + case 0x22: + overflowSub(static_cast(cpu.gpr[rs]), static_cast(cpu.gpr[rt]), rd); + break; + case 0x23: + write(rd, cpu.gpr[rs] - cpu.gpr[rt]); + break; + case 0x24: + write(rd, cpu.gpr[rs] & cpu.gpr[rt]); + break; + case 0x25: + write(rd, cpu.gpr[rs] | cpu.gpr[rt]); + break; + case 0x26: + write(rd, cpu.gpr[rs] ^ cpu.gpr[rt]); + break; + case 0x27: + write(rd, ~(cpu.gpr[rs] | cpu.gpr[rt])); + break; + case 0x2A: + write(rd, static_cast(cpu.gpr[rs]) < static_cast(cpu.gpr[rt]) ? 1u : 0u); + break; + case 0x2B: + write(rd, cpu.gpr[rs] < cpu.gpr[rt] ? 1u : 0u); + break; + default: + raiseException(cpu, 10u, pc, wasDelaySlot); + break; + } + break; + case 0x01: + switch (rt) + { + case 0x00: + branch(static_cast(cpu.gpr[rs]) < 0); + break; + case 0x01: + branch(static_cast(cpu.gpr[rs]) >= 0); + break; + case 0x10: + write(31u, pc + 8u); + branch(static_cast(cpu.gpr[rs]) < 0); + break; + case 0x11: + write(31u, pc + 8u); + branch(static_cast(cpu.gpr[rs]) >= 0); + break; + default: + raiseException(cpu, 10u, pc, wasDelaySlot); + break; + } + break; + case 0x02: + newBranch = true; + newBranchTarget = (nextPc & 0xF0000000u) | ((instruction & 0x03FFFFFFu) << 2u); + break; + case 0x03: + write(31u, pc + 8u); + newBranch = true; + newBranchTarget = (nextPc & 0xF0000000u) | ((instruction & 0x03FFFFFFu) << 2u); + break; + case 0x04: + branch(cpu.gpr[rs] == cpu.gpr[rt]); + break; + case 0x05: + branch(cpu.gpr[rs] != cpu.gpr[rt]); + break; + case 0x06: + branch(static_cast(cpu.gpr[rs]) <= 0); + break; + case 0x07: + branch(static_cast(cpu.gpr[rs]) > 0); + break; + case 0x08: + overflowAdd(static_cast(cpu.gpr[rs]), simm, rt); + break; + case 0x09: + write(rt, cpu.gpr[rs] + static_cast(simm)); + break; + case 0x0A: + write(rt, static_cast(cpu.gpr[rs]) < simm ? 1u : 0u); + break; + case 0x0B: + write(rt, cpu.gpr[rs] < static_cast(simm) ? 1u : 0u); + break; + case 0x0C: + write(rt, cpu.gpr[rs] & imm); + break; + case 0x0D: + write(rt, cpu.gpr[rs] | imm); + break; + case 0x0E: + write(rt, cpu.gpr[rs] ^ imm); + break; + case 0x0F: + write(rt, imm << 16u); + break; + case 0x10: + { + const uint32_t copRs = rs; + if (copRs == 0x00) + load(rt, cpu.cop0[rd]); + else if (copRs == 0x04) + cpu.cop0[rd] = cpu.gpr[rt]; + else if (copRs == 0x10 && funct == 0x10) + { + const uint32_t status = cpu.cop0[12]; + cpu.cop0[12] = (status & ~0x0Fu) | ((status >> 2u) & 0x0Fu); + } + else + raiseException(cpu, 10u, pc, wasDelaySlot); + break; + } + case 0x20: + case 0x24: + { + const uint32_t address = cpu.gpr[rs] + static_cast(simm); + const uint8_t value = m_memory.read8(address); + load(rt, opcode == 0x20 + ? static_cast(static_cast(static_cast(value))) + : value); + break; + } + case 0x21: + case 0x25: + { + const uint32_t address = cpu.gpr[rs] + static_cast(simm); + if (address & 1u) + { + raiseException(cpu, 4u, pc, wasDelaySlot, address); + break; + } + const uint16_t value = m_memory.read16(address); + load(rt, opcode == 0x21 ? static_cast(static_cast(static_cast(value))) : value); + break; + } + case 0x22: + { + const uint32_t address = cpu.gpr[rs] + static_cast(simm); + const uint32_t memory = m_memory.read32(address & ~3u); + const uint32_t old = cpu.gpr[rt]; + static constexpr uint32_t masks[4] = {0x00FFFFFFu, 0x0000FFFFu, 0x000000FFu, 0x00000000u}; + static constexpr uint32_t shifts[4] = {24u, 16u, 8u, 0u}; + load(rt, (old & masks[address & 3u]) | (memory << shifts[address & 3u])); + break; + } + case 0x23: + { + const uint32_t address = cpu.gpr[rs] + static_cast(simm); + if (address & 3u) + { + raiseException(cpu, 4u, pc, wasDelaySlot, address); + break; + } + load(rt, m_memory.read32(address)); + break; + } + case 0x26: + { + const uint32_t address = cpu.gpr[rs] + static_cast(simm); + const uint32_t memory = m_memory.read32(address & ~3u); + const uint32_t old = cpu.gpr[rt]; + static constexpr uint32_t masks[4] = {0x00000000u, 0xFF000000u, 0xFFFF0000u, 0xFFFFFF00u}; + static constexpr uint32_t shifts[4] = {0u, 8u, 16u, 24u}; + load(rt, (old & masks[address & 3u]) | (memory >> shifts[address & 3u])); + break; + } + case 0x28: + m_memory.write8(cpu.gpr[rs] + static_cast(simm), static_cast(cpu.gpr[rt])); + break; + case 0x29: + { + const uint32_t address = cpu.gpr[rs] + static_cast(simm); + if (address & 1u) + { + raiseException(cpu, 5u, pc, wasDelaySlot, address); + break; + } + m_memory.write16(address, static_cast(cpu.gpr[rt])); + break; + } + case 0x2A: + { + const uint32_t address = cpu.gpr[rs] + static_cast(simm); + const uint32_t aligned = address & ~3u; + const uint32_t old = m_memory.read32(aligned); + const uint32_t value = cpu.gpr[rt]; + uint32_t result = old; + switch (address & 3u) + { + case 0u: + result = (old & 0xFFFFFF00u) | (value >> 24u); + break; + case 1u: + result = (old & 0xFFFF0000u) | (value >> 16u); + break; + case 2u: + result = (old & 0xFF000000u) | (value >> 8u); + break; + case 3u: + result = value; + break; + } + m_memory.write32(aligned, result); + break; + } + case 0x2B: + { + const uint32_t address = cpu.gpr[rs] + static_cast(simm); + if (address & 3u) + { + raiseException(cpu, 5u, pc, wasDelaySlot, address); + break; + } + m_memory.write32(address, cpu.gpr[rt]); + break; + } + case 0x2E: + { + const uint32_t address = cpu.gpr[rs] + static_cast(simm); + const uint32_t aligned = address & ~3u; + const uint32_t old = m_memory.read32(aligned); + const uint32_t value = cpu.gpr[rt]; + uint32_t result = old; + switch (address & 3u) + { + case 0u: + result = value; + break; + case 1u: + result = (old & 0x000000FFu) | (value << 8u); + break; + case 2u: + result = (old & 0x0000FFFFu) | (value << 16u); + break; + case 3u: + result = (old & 0x00FFFFFFu) | (value << 24u); + break; + } + m_memory.write32(aligned, result); + break; + } + default: + raiseException(cpu, 10u, pc, wasDelaySlot); + break; + } + + cpu.gpr[0] = 0u; + if (cpu.exception) + return !cpu.stopped; + + if (cpu.pendingLoad) + { + if (cpu.pendingLoadReg != 0u && cpu.pendingLoadReg != writtenReg) + cpu.gpr[cpu.pendingLoadReg] = cpu.pendingLoadValue; + cpu.pendingLoad = false; + } + if (scheduledLoad) + { + cpu.pendingLoad = true; + cpu.pendingLoadReg = scheduledReg; + cpu.pendingLoadValue = scheduledValue; + } + cpu.gpr[0] = 0u; + + if (wasDelaySlot) + { + cpu.pc = priorBranchTarget; + cpu.branchPending = false; + } + else + { + cpu.pc = nextPc; + cpu.branchPending = newBranch; + cpu.branchTarget = newBranchTarget; + } + return !cpu.stopped; + } +} diff --git a/ps2xIOP/src/emulator/core/iop_cpu.h b/ps2xIOP/src/emulator/core/iop_cpu.h new file mode 100644 index 0000000..f236050 --- /dev/null +++ b/ps2xIOP/src/emulator/core/iop_cpu.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +namespace ps2x::iop::detail +{ + class IopMemory; + + struct IopCpuState + { + std::array gpr{}; + uint32_t hi = 0; + uint32_t lo = 0; + uint32_t pc = 0; + std::array cop0{}; + uint32_t pendingLoadReg = 0; + uint32_t pendingLoadValue = 0; + bool pendingLoad = false; + bool branchPending = false; + uint32_t branchTarget = 0; + bool stopped = false; + bool yielded = false; + bool exception = false; + }; + + class IopCpuCore + { + public: + explicit IopCpuCore(IopMemory &memory) noexcept; + + [[nodiscard]] bool executeInstruction(IopCpuState &cpu); + void raiseException(IopCpuState &cpu, uint32_t code, uint32_t faultPc, bool delaySlot, std::optional badAddress = std::nullopt) const; + + private: + static void writeRegister(IopCpuState &cpu, uint32_t reg, uint32_t value, uint32_t &writtenReg); + static void scheduleLoad(uint32_t reg, uint32_t value, bool &scheduled, uint32_t &scheduledReg, uint32_t &scheduledValue); + + IopMemory &m_memory; + }; +} diff --git a/ps2xIOP/src/emulator/core/iop_kernel.cpp b/ps2xIOP/src/emulator/core/iop_kernel.cpp new file mode 100644 index 0000000..4832d8f --- /dev/null +++ b/ps2xIOP/src/emulator/core/iop_kernel.cpp @@ -0,0 +1,744 @@ +#include "iop_kernel.h" + +#include "iop_memory.h" +#include "../iop_emulator_const.h" + +#include + +namespace ps2x::iop::detail +{ + namespace + { + uint32_t alignUp(uint32_t value, uint32_t alignment) + { + if (alignment <= 1u) + return value; + const uint32_t mask = alignment - 1u; + return (value + mask) & ~mask; + } + } + + IopKernel::IopKernel(IopMemory &memory) noexcept + : m_memory(memory) + { + } + + void IopKernel::reset() + { + m_threads.clear(); + m_semaphores.clear(); + m_eventFlags.clear(); + m_nextThreadId = 1; + m_nextSemaphoreId = 1; + m_nextEventFlagId = 1; + m_currentThread = nullptr; + } + + bool IopKernel::dispatchThreadImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle) + { + const auto setV0 = [&](int32_t value) + { + cpu.gpr[2] = static_cast(value); + }; + + switch (ordinal) + { + case 4: // CreateThread + { + const uint32_t descriptor = cpu.gpr[4]; + IopThread thread; + thread.id = static_cast(m_nextThreadId++); + thread.attr = m_memory.read32(descriptor + 0u); + thread.option = m_memory.read32(descriptor + 4u); + thread.entry = m_memory.read32(descriptor + 8u); + thread.stackSize = std::max(m_memory.read32(descriptor + 12u), 0x100u); + thread.priority = std::clamp(m_memory.read32(descriptor + 16u), 1u, 126u); + thread.initialPriority = thread.priority; + thread.stackBase = m_memory.allocate(thread.stackSize + kStackGuardBytes, 16u); + if (thread.stackBase == 0u) + { + setV0(-400); + return true; + } + const int id = thread.id; + m_threads.emplace(id, std::move(thread)); + setV0(id); + return true; + } + case 5: // DeleteThread + { + const int id = static_cast(cpu.gpr[4]); + const auto it = m_threads.find(id); + if (it == m_threads.end()) + { + setV0(-1); + return true; + } + if (it->second.stackBase != 0u) + (void)m_memory.freeAllocation(it->second.stackBase); + m_threads.erase(it); + setV0(0); + return true; + } + case 6: // StartThread + case 7: // StartThreadArgs + { + const int id = static_cast(cpu.gpr[4]); + const auto it = m_threads.find(id); + if (it == m_threads.end()) + { + setV0(-1); + return true; + } + IopThread &thread = it->second; + thread.cpu = {}; + thread.cpu.pc = thread.entry; + thread.cpu.gpr[4] = cpu.gpr[5]; + thread.cpu.gpr[5] = ordinal == 7 ? cpu.gpr[6] : 0u; + thread.cpu.gpr[28] = cpu.gpr[28]; + thread.cpu.gpr[29] = alignUp(thread.stackBase + thread.stackSize, 16u) - 16u; + thread.cpu.gpr[31] = kThreadReturnSentinel; + thread.state = IopThreadState::Ready; + setV0(0); + return true; + } + case 8: // ExitThread + case 9: // ExitDeleteThread + if (m_currentThread != nullptr) + { + m_currentThread->state = ordinal == 9 ? IopThreadState::Dead : IopThreadState::Dormant; + cpu.stopped = true; + cpu.yielded = true; + } + setV0(0); + return true; + case 10: + case 11: // TerminateThread + { + const int id = static_cast(cpu.gpr[4]); + const auto it = m_threads.find(id); + if (it == m_threads.end()) + { + setV0(-1); + return true; + } + it->second.state = IopThreadState::Dormant; + setV0(0); + return true; + } + case 12: + case 13: + setV0(0); + return true; + case 14: + case 15: + { + int id = static_cast(cpu.gpr[4]); + if (id == 0 && m_currentThread != nullptr) + id = m_currentThread->id; + const auto it = m_threads.find(id); + if (it == m_threads.end()) + { + setV0(-1); + return true; + } + it->second.priority = std::clamp(cpu.gpr[5], 1u, 126u); + setV0(0); + return true; + } + case 16: + case 17: + setV0(0); + cpu.yielded = true; + return true; + case 18: + case 19: + { + const int id = static_cast(cpu.gpr[4]); + const auto it = m_threads.find(id); + if (it == m_threads.end()) + { + setV0(-1); + return true; + } + if (it->second.state == IopThreadState::Sleep || + it->second.state == IopThreadState::Delay || + it->second.state == IopThreadState::Semaphore || + it->second.state == IopThreadState::EventFlag) + { + it->second.state = IopThreadState::Ready; + } + setV0(0); + return true; + } + case 20: + setV0(m_currentThread != nullptr ? m_currentThread->id : 0); + return true; + case 21: + setV0(0x1000); + return true; + case 22: + case 23: + setV0(referThreadStatus(static_cast(cpu.gpr[4]), cpu.gpr[5]) ? 0 : -1); + return true; + case 24: // SleepThread + if (m_currentThread != nullptr) + { + if (m_currentThread->wakeupCount > 0) + --m_currentThread->wakeupCount; + else + sleepCurrent(cpu); + } + setV0(0); + return true; + case 25: + case 26: + { + const int id = static_cast(cpu.gpr[4]); + const auto it = m_threads.find(id); + if (it == m_threads.end()) + { + setV0(-1); + return true; + } + if (it->second.state == IopThreadState::Sleep) + it->second.state = IopThreadState::Ready; + else + ++it->second.wakeupCount; + setV0(0); + return true; + } + case 27: + case 28: + { + const int id = static_cast(cpu.gpr[4]); + const auto it = m_threads.find(id); + if (it == m_threads.end()) + { + setV0(-1); + return true; + } + const int old = it->second.wakeupCount; + it->second.wakeupCount = 0; + setV0(old); + return true; + } + case 29: + case 30: // SuspendThread / iSuspendThread + { + const int id = static_cast(cpu.gpr[4]); + const auto it = m_threads.find(id); + if (it == m_threads.end()) + { + setV0(-1); + return true; + } + it->second.state = IopThreadState::Suspended; + if (m_currentThread == &it->second) + cpu.yielded = true; + setV0(0); + return true; + } + case 31: + case 32: // ResumeThread / iResumeThread + { + const int id = static_cast(cpu.gpr[4]); + const auto it = m_threads.find(id); + if (it == m_threads.end()) + { + setV0(-1); + return true; + } + if (it->second.state == IopThreadState::Suspended) + it->second.state = IopThreadState::Ready; + setV0(0); + return true; + } + case 33: // DelayThread + if (m_currentThread != nullptr) + { + const uint64_t delayCycles = (static_cast(cpu.gpr[4]) * kIopClockHz + 999'999ull) / 1'000'000ull; + delayCurrentUntil(currentCycle + std::max(delayCycles, 1u), cpu); + } + setV0(0); + return true; + case 34: // GetSystemTime + m_memory.write32(cpu.gpr[4], static_cast(currentCycle)); + m_memory.write32(cpu.gpr[4] + 4u, static_cast(currentCycle >> 32u)); + setV0(0); + return true; + case 35: + case 36: + case 37: + case 38: + setV0(0); + return true; + case 39: // USec2SysClock + { + const uint64_t cycles = (static_cast(cpu.gpr[4]) * kIopClockHz) / 1'000'000ull; + m_memory.write32(cpu.gpr[5], static_cast(cycles)); + m_memory.write32(cpu.gpr[5] + 4u, static_cast(cycles >> 32u)); + setV0(0); + return true; + } + case 40: + { + const uint64_t cycles = static_cast(m_memory.read32(cpu.gpr[4])) | (static_cast(m_memory.read32(cpu.gpr[4] + 4u)) << 32u); + const uint64_t usec = (cycles * 1'000'000ull) / kIopClockHz; + if (cpu.gpr[5] != 0u) + m_memory.write32(cpu.gpr[5], static_cast(usec / 1'000'000ull)); + if (cpu.gpr[6] != 0u) + m_memory.write32(cpu.gpr[6], static_cast(usec % 1'000'000ull)); + setV0(0); + return true; + } + case 41: + setV0(0); + return true; + default: + return false; + } + } + + bool IopKernel::referThreadStatus(int id, uint32_t outputAddress) + { + if (id == 0 && m_currentThread != nullptr) + id = m_currentThread->id; + const auto it = m_threads.find(id); + if (it == m_threads.end() || outputAddress == 0u) + return false; + + const IopThread &thread = it->second; + uint32_t status = 0x10u; + switch (thread.state) + { + case IopThreadState::Running: + status = 0x01u; + break; + case IopThreadState::Ready: + status = 0x02u; + break; + case IopThreadState::Sleep: + case IopThreadState::Delay: + case IopThreadState::Semaphore: + case IopThreadState::EventFlag: + status = 0x04u; + break; + case IopThreadState::Suspended: + status = 0x08u; + break; + default: + status = 0x10u; + break; + } + + m_memory.write32(outputAddress + 0u, thread.attr); + m_memory.write32(outputAddress + 4u, thread.option); + m_memory.write32(outputAddress + 8u, status); + m_memory.write32(outputAddress + 12u, thread.entry); + m_memory.write32(outputAddress + 16u, thread.stackBase); + m_memory.write32(outputAddress + 20u, thread.stackSize); + m_memory.write32(outputAddress + 24u, thread.cpu.gpr[28]); + m_memory.write32(outputAddress + 28u, thread.initialPriority); + m_memory.write32(outputAddress + 32u, thread.priority); + m_memory.write32(outputAddress + 36u, thread.state == IopThreadState::Sleep ? 1u : thread.state == IopThreadState::Delay ? 2u + : thread.state == IopThreadState::Semaphore ? 3u + : thread.state == IopThreadState::EventFlag ? 4u + : 0u); + m_memory.write32(outputAddress + 40u, static_cast(thread.waitId)); + m_memory.write32(outputAddress + 44u, static_cast(thread.wakeupCount)); + return true; + } + + bool IopKernel::dispatchSemaphoreImport(uint16_t ordinal, IopCpuState &cpu) + { + const auto setV0 = [&](int32_t value) + { + cpu.gpr[2] = static_cast(value); + }; + + switch (ordinal) + { + case 4: + { + const uint32_t descriptor = cpu.gpr[4]; + Semaphore semaphore; + semaphore.id = static_cast(m_nextSemaphoreId++); + semaphore.attr = m_memory.read32(descriptor + 0u); + semaphore.option = m_memory.read32(descriptor + 4u); + semaphore.current = static_cast(m_memory.read32(descriptor + 8u)); + semaphore.maximum = std::max(1, static_cast(m_memory.read32(descriptor + 12u))); + m_semaphores.emplace(semaphore.id, semaphore); + setV0(semaphore.id); + return true; + } + case 5: + setV0(m_semaphores.erase(static_cast(cpu.gpr[4])) != 0u ? 0 : -1); + return true; + case 6: + case 7: + { + const int id = static_cast(cpu.gpr[4]); + const auto it = m_semaphores.find(id); + if (it == m_semaphores.end()) + { + setV0(-1); + return true; + } + if (it->second.current < it->second.maximum) + ++it->second.current; + wakeOneSemaphore(id); + setV0(0); + return true; + } + case 8: + case 9: + { + const int id = static_cast(cpu.gpr[4]); + const auto it = m_semaphores.find(id); + if (it == m_semaphores.end()) + { + setV0(-1); + return true; + } + if (it->second.current > 0) + { + --it->second.current; + setV0(0); + } + else if (ordinal == 9) + setV0(-419); + else if (m_currentThread != nullptr) + { + m_currentThread->state = IopThreadState::Semaphore; + m_currentThread->waitId = id; + cpu.yielded = true; + setV0(0); + } + return true; + } + case 11: + case 12: + { + const int id = static_cast(cpu.gpr[4]); + const auto it = m_semaphores.find(id); + if (it == m_semaphores.end()) + { + setV0(-1); + return true; + } + const uint32_t outputAddress = cpu.gpr[5]; + if (outputAddress != 0u) + { + m_memory.write32(outputAddress + 0u, it->second.attr); + m_memory.write32(outputAddress + 4u, it->second.option); + m_memory.write32(outputAddress + 8u, 0u); + m_memory.write32(outputAddress + 12u, static_cast(it->second.maximum)); + m_memory.write32(outputAddress + 16u, static_cast(it->second.current)); + uint32_t waiters = 0u; + for (const auto &[threadId, thread] : m_threads) + { + if (thread.state == IopThreadState::Semaphore && thread.waitId == id) + ++waiters; + } + m_memory.write32(outputAddress + 20u, waiters); + } + setV0(0); + return true; + } + default: + return false; + } + } + + void IopKernel::wakeOneSemaphore(int id) + { + IopThread *best = nullptr; + for (auto &[threadId, thread] : m_threads) + { + if (thread.state != IopThreadState::Semaphore || thread.waitId != id) + continue; + if (best == nullptr || thread.priority < best->priority) + best = &thread; + } + + const auto semaphore = m_semaphores.find(id); + if (best != nullptr && semaphore != m_semaphores.end() && semaphore->second.current > 0) + { + --semaphore->second.current; + best->state = IopThreadState::Ready; + best->waitId = 0; + } + } + + bool IopKernel::eventSatisfied(const EventFlag &event, uint32_t bits, uint32_t mode) + { + if (bits == 0u) + return false; + return (mode & 1u) != 0u ? (event.bits & bits) != 0u : (event.bits & bits) == bits; + } + + void IopKernel::wakeEventWaiters(EventFlag &event) + { + for (auto &[threadId, thread] : m_threads) + { + if (thread.state != IopThreadState::EventFlag || thread.waitId != event.id) + continue; + if (!eventSatisfied(event, thread.waitBits, thread.waitMode)) + continue; + + if (thread.waitResultAddress != 0u) + m_memory.write32(thread.waitResultAddress, event.bits); + thread.cpu.gpr[2] = 0u; + if ((thread.waitMode & 0x10u) != 0u) + event.bits = 0u; + thread.state = IopThreadState::Ready; + thread.waitId = 0; + thread.waitBits = 0; + thread.waitMode = 0; + thread.waitResultAddress = 0; + if (event.bits == 0u) + break; + } + } + + int IopKernel::createInternalEventFlag(uint32_t attr, uint32_t option, uint32_t bits) + { + EventFlag event; + event.id = static_cast(m_nextEventFlagId++); + event.attr = attr; + event.option = option; + event.bits = bits; + const int id = event.id; + m_eventFlags.emplace(id, event); + return id; + } + + bool IopKernel::setInternalEventFlag(int id, uint32_t bits) + { + const auto event = m_eventFlags.find(id); + if (event == m_eventFlags.end()) + return false; + event->second.bits |= bits; + wakeEventWaiters(event->second); + return true; + } + + bool IopKernel::dispatchEventImport(uint16_t ordinal, IopCpuState &cpu) + { + const auto setV0 = [&](int32_t value) + { + cpu.gpr[2] = static_cast(value); + }; + + switch (ordinal) + { + case 4: + { + const uint32_t descriptor = cpu.gpr[4]; + setV0(createInternalEventFlag(m_memory.read32(descriptor + 0u), + m_memory.read32(descriptor + 4u), + m_memory.read32(descriptor + 8u))); + return true; + } + case 5: + { + const int id = static_cast(cpu.gpr[4]); + if (m_eventFlags.erase(id) == 0u) + { + setV0(-1); + return true; + } + for (auto &[threadId, thread] : m_threads) + { + if (thread.state == IopThreadState::EventFlag && thread.waitId == id) + { + thread.state = IopThreadState::Ready; + thread.waitId = 0; + thread.cpu.gpr[2] = static_cast(-1); + } + } + setV0(0); + return true; + } + case 6: + case 7: + { + if (!setInternalEventFlag(static_cast(cpu.gpr[4]), cpu.gpr[5])) + { + setV0(-1); + return true; + } + setV0(0); + return true; + } + case 8: + case 9: + { + const auto event = m_eventFlags.find(static_cast(cpu.gpr[4])); + if (event == m_eventFlags.end()) + { + setV0(-1); + return true; + } + // IOP ClearEventFlag applies a mask: callers pass ~bitsToClear. + event->second.bits &= cpu.gpr[5]; + setV0(0); + return true; + } + case 10: // WaitEventFlag + case 11: // PollEventFlag + { + const auto event = m_eventFlags.find(static_cast(cpu.gpr[4])); + if (event == m_eventFlags.end()) + { + setV0(-1); + return true; + } + const uint32_t bits = cpu.gpr[5]; + const uint32_t mode = cpu.gpr[6]; + if (eventSatisfied(event->second, bits, mode)) + { + if (cpu.gpr[7] != 0u) + m_memory.write32(cpu.gpr[7], event->second.bits); + if ((mode & 0x10u) != 0u) + event->second.bits = 0u; + setV0(0); + } + else if (ordinal == 11) + setV0(-418); + else if (m_currentThread != nullptr) + { + m_currentThread->state = IopThreadState::EventFlag; + m_currentThread->waitId = event->second.id; + m_currentThread->waitBits = bits; + m_currentThread->waitMode = mode; + m_currentThread->waitResultAddress = cpu.gpr[7]; + setV0(0); + cpu.yielded = true; + } + else + setV0(-418); + return true; + } + case 13: + case 14: + { + const auto event = m_eventFlags.find(static_cast(cpu.gpr[4])); + if (event == m_eventFlags.end()) + { + setV0(-1); + return true; + } + if (cpu.gpr[5] != 0u) + { + uint32_t waiters = 0u; + for (const auto &[threadId, thread] : m_threads) + { + if (thread.state == IopThreadState::EventFlag && thread.waitId == event->second.id) + ++waiters; + } + m_memory.write32(cpu.gpr[5] + 0u, event->second.attr); + m_memory.write32(cpu.gpr[5] + 4u, event->second.option); + m_memory.write32(cpu.gpr[5] + 8u, event->second.bits); + m_memory.write32(cpu.gpr[5] + 12u, event->second.bits); + m_memory.write32(cpu.gpr[5] + 16u, waiters); + } + setV0(0); + return true; + } + default: + return false; + } + } + + void IopKernel::sleepCurrent(IopCpuState &cpu) + { + if (m_currentThread == nullptr) + return; + m_currentThread->state = IopThreadState::Sleep; + cpu.yielded = true; + } + + void IopKernel::delayCurrentUntil(uint64_t wakeCycle, IopCpuState &cpu) + { + if (m_currentThread == nullptr) + return; + m_currentThread->wakeCycle = wakeCycle; + m_currentThread->state = IopThreadState::Delay; + cpu.yielded = true; + } + + IopThread *IopKernel::beginNextReady(uint64_t currentCycle) + { + for (auto &[id, thread] : m_threads) + { + if (thread.state == IopThreadState::Delay && thread.wakeCycle <= currentCycle) + thread.state = IopThreadState::Ready; + } + + IopThread *next = nullptr; + for (auto &[id, thread] : m_threads) + { + if (thread.state != IopThreadState::Ready) + continue; + if (next == nullptr || thread.priority < next->priority || + (thread.priority == next->priority && thread.id < next->id)) + next = &thread; + } + if (next == nullptr) + return nullptr; + + m_currentThread = next; + next->state = IopThreadState::Running; + next->cpu.stopped = false; + next->cpu.yielded = false; + return next; + } + + uint64_t IopKernel::nextWakeCycle(uint64_t fallback) const + { + uint64_t nextWake = fallback; + for (const auto &[id, thread] : m_threads) + { + if (thread.state == IopThreadState::Delay) + nextWake = std::min(nextWake, thread.wakeCycle); + } + return nextWake; + } + + void IopKernel::endTimeslice(IopThread &thread, uint32_t returnSentinel) + { + if (thread.cpu.pc == returnSentinel || thread.cpu.stopped) + thread.state = IopThreadState::Dormant; + else if (thread.state == IopThreadState::Running) + thread.state = IopThreadState::Ready; + m_currentThread = nullptr; + cleanupDeadThreads(); + } + + void IopKernel::cleanupDeadThreads() + { + for (auto thread = m_threads.begin(); thread != m_threads.end();) + { + if (thread->second.state != IopThreadState::Dead) + { + ++thread; + continue; + } + if (thread->second.stackBase != 0u) + (void)m_memory.freeAllocation(thread->second.stackBase); + thread = m_threads.erase(thread); + } + } + + void IopKernel::terminateThreadsInRange(uint32_t base, uint32_t size) + { + for (auto &[id, thread] : m_threads) + { + const uint32_t pc = IopMemory::physicalAddress(thread.cpu.pc); + if (pc >= base && pc < base + size) + thread.state = IopThreadState::Dead; + } + } +} diff --git a/ps2xIOP/src/emulator/core/iop_kernel.h b/ps2xIOP/src/emulator/core/iop_kernel.h new file mode 100644 index 0000000..4977102 --- /dev/null +++ b/ps2xIOP/src/emulator/core/iop_kernel.h @@ -0,0 +1,103 @@ +#pragma once + +#include "iop_cpu.h" + +#include +#include +#include + +namespace ps2x::iop::detail +{ + class IopMemory; + + enum class IopThreadState : uint8_t + { + Dormant, + Ready, + Running, + Sleep, + Delay, + Semaphore, + EventFlag, + Suspended, + Dead, + }; + + struct IopThread + { + int id = 0; + IopThreadState state = IopThreadState::Dormant; + IopCpuState cpu; + uint32_t entry = 0; + uint32_t stackBase = 0; + uint32_t stackSize = 0; + uint32_t priority = 0x40; + uint32_t initialPriority = 0x40; + uint32_t option = 0; + uint32_t attr = 0; + uint64_t wakeCycle = 0; + int waitId = 0; + uint32_t waitBits = 0; + uint32_t waitMode = 0; + uint32_t waitResultAddress = 0; + int wakeupCount = 0; + }; + + class IopKernel + { + public: + explicit IopKernel(IopMemory &memory) noexcept; + + void reset(); + + [[nodiscard]] bool dispatchThreadImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle); + [[nodiscard]] bool dispatchSemaphoreImport(uint16_t ordinal, IopCpuState &cpu); + [[nodiscard]] bool dispatchEventImport(uint16_t ordinal, IopCpuState &cpu); + + [[nodiscard]] int createInternalEventFlag(uint32_t attr, uint32_t option, uint32_t bits); + [[nodiscard]] bool setInternalEventFlag(int id, uint32_t bits); + + void sleepCurrent(IopCpuState &cpu); + void delayCurrentUntil(uint64_t wakeCycle, IopCpuState &cpu); + + [[nodiscard]] IopThread *beginNextReady(uint64_t currentCycle); + [[nodiscard]] uint64_t nextWakeCycle(uint64_t fallback) const; + void endTimeslice(IopThread &thread, uint32_t returnSentinel); + void cleanupDeadThreads(); + void terminateThreadsInRange(uint32_t base, uint32_t size); + + [[nodiscard]] size_t threadCount() const noexcept { return m_threads.size(); } + + private: + struct Semaphore + { + int id = 0; + uint32_t attr = 0; + uint32_t option = 0; + int current = 0; + int maximum = 1; + }; + + struct EventFlag + { + int id = 0; + uint32_t bits = 0; + uint32_t attr = 0; + uint32_t option = 0; + }; + + [[nodiscard]] bool referThreadStatus(int id, uint32_t outputAddress); + void wakeOneSemaphore(int id); + [[nodiscard]] static bool eventSatisfied(const EventFlag &event, uint32_t bits, uint32_t mode); + void wakeEventWaiters(EventFlag &event); + + IopMemory &m_memory; + std::map m_threads; + std::map m_semaphores; + std::map m_eventFlags; + uint32_t m_nextThreadId = 1; + uint32_t m_nextSemaphoreId = 1; + uint32_t m_nextEventFlagId = 1; + IopThread *m_currentThread = nullptr; + }; +} diff --git a/ps2xIOP/src/emulator/core/iop_memory.cpp b/ps2xIOP/src/emulator/core/iop_memory.cpp new file mode 100644 index 0000000..25e9b36 --- /dev/null +++ b/ps2xIOP/src/emulator/core/iop_memory.cpp @@ -0,0 +1,371 @@ +#include "iop_memory.h" + +#include +#include + +namespace ps2x::iop::detail +{ + namespace + { + constexpr uint32_t kDmaSpu0Chcr = 0x1F8010C8u; + constexpr uint32_t kDmaSpu1Chcr = 0x1F801508u; + constexpr uint32_t kDmaStart = 1u << 24u; + constexpr int kDmaSpu0Irq = 0x24; + constexpr int kDmaSpu1Irq = 0x28; + + uint32_t alignUp(uint32_t value, uint32_t alignment) + { + return (value + alignment - 1u) & ~(alignment - 1u); + } + } + + IopMemory::IopMemory() + : m_ram(RamSize), m_owned(RamSize), m_scratch(ScratchSize) + { + reset(); + } + + void IopMemory::reset() + { + std::fill(m_ram.begin(), m_ram.end(), uint8_t{0}); + std::fill(m_owned.begin(), m_owned.end(), uint8_t{0}); + std::fill(m_scratch.begin(), m_scratch.end(), uint8_t{0}); + m_hardware.clear(); + m_allocations.clear(); + m_heapCursor = HeapBase; + m_interruptStatus = 0; + m_interruptMask = 0; + m_interruptControl = 1; + m_dmaStart.reset(); + } + + uint32_t IopMemory::physicalAddress(uint32_t address) noexcept + { + return address & 0x1FFFFFFFu; + } + + uint8_t IopMemory::read8(uint32_t address) const + { + const uint32_t phys = physicalAddress(address); + if (phys < RamSize) + return m_ram[phys]; + if (phys >= ScratchBase && phys < ScratchBase + ScratchSize) + return m_scratch[phys - ScratchBase]; + const uint32_t value = readHardware32(phys & ~3u); + return static_cast(value >> ((phys & 3u) * 8u)); + } + + uint16_t IopMemory::read16(uint32_t address) const + { + const uint32_t phys = physicalAddress(address); + if (phys + 1u < RamSize) + { + uint16_t value; + std::memcpy(&value, m_ram.data() + phys, sizeof(value)); + return value; + } + return static_cast(read8(address) | (static_cast(read8(address + 1u)) << 8u)); + } + + uint32_t IopMemory::read32(uint32_t address) const + { + const uint32_t phys = physicalAddress(address); + if ((phys & 3u) == 0u && phys + 3u < RamSize) + { + uint32_t value; + std::memcpy(&value, m_ram.data() + phys, sizeof(value)); + return value; + } + if ((phys & 3u) == 0u && phys >= ScratchBase && phys + 3u < ScratchBase + ScratchSize) + { + uint32_t value; + std::memcpy(&value, m_scratch.data() + (phys - ScratchBase), sizeof(value)); + return value; + } + if ((phys & 3u) == 0u && isHardwareAddress(phys)) + return readHardware32(phys); + + return static_cast(read8(address)) | + (static_cast(read8(address + 1u)) << 8u) | + (static_cast(read8(address + 2u)) << 16u) | + (static_cast(read8(address + 3u)) << 24u); + } + + void IopMemory::write8(uint32_t address, uint8_t value) + { + const uint32_t phys = physicalAddress(address); + if (phys < RamSize) + { + m_ram[phys] = value; + markOwned(phys, sizeof(value)); + return; + } + if (phys >= ScratchBase && phys < ScratchBase + ScratchSize) + { + m_scratch[phys - ScratchBase] = value; + return; + } + const uint32_t aligned = phys & ~3u; + uint32_t current = readHardware32(aligned); + const uint32_t shift = (phys & 3u) * 8u; + current = (current & ~(0xFFu << shift)) | (static_cast(value) << shift); + writeHardware32(aligned, current); + } + + void IopMemory::write16(uint32_t address, uint16_t value) + { + const uint32_t phys = physicalAddress(address); + if (phys + 1u < RamSize) + { + std::memcpy(m_ram.data() + phys, &value, sizeof(value)); + markOwned(phys, sizeof(value)); + return; + } + write8(address, static_cast(value)); + write8(address + 1u, static_cast(value >> 8u)); + } + + void IopMemory::write32(uint32_t address, uint32_t value) + { + const uint32_t phys = physicalAddress(address); + if ((phys & 3u) == 0u && phys + 3u < RamSize) + { + std::memcpy(m_ram.data() + phys, &value, sizeof(value)); + markOwned(phys, sizeof(value)); + return; + } + if ((phys & 3u) == 0u && phys >= ScratchBase && phys + 3u < ScratchBase + ScratchSize) + { + std::memcpy(m_scratch.data() + (phys - ScratchBase), &value, sizeof(value)); + return; + } + if ((phys & 3u) == 0u) + { + writeHardware32(phys, value); + return; + } + write8(address, static_cast(value)); + write8(address + 1u, static_cast(value >> 8u)); + write8(address + 2u, static_cast(value >> 16u)); + write8(address + 3u, static_cast(value >> 24u)); + } + + bool IopMemory::readRam(uint32_t address, void *destination, size_t size) const + { + const uint32_t phys = physicalAddress(address); + if ((!destination && size != 0u) || phys > RamSize || size > RamSize - phys) + return false; + if (size != 0u) + std::memcpy(destination, m_ram.data() + phys, size); + return true; + } + + bool IopMemory::writeRam(uint32_t address, const void *source, size_t size) + { + const uint32_t phys = physicalAddress(address); + if ((!source && size != 0u) || phys > RamSize || size > RamSize - phys) + return false; + if (size != 0u) + { + std::memcpy(m_ram.data() + phys, source, size); + markOwned(phys, size); + } + return true; + } + + bool IopMemory::zeroRam(uint32_t address, size_t size) + { + const uint32_t phys = physicalAddress(address); + if (phys > RamSize || size > RamSize - phys) + return false; + if (size != 0u) + { + std::memset(m_ram.data() + phys, 0, size); + markOwned(phys, size); + } + return true; + } + + bool IopMemory::ownsRamRange(uint32_t address, size_t size) const + { + const uint32_t phys = physicalAddress(address); + if (phys > RamSize || size > RamSize - phys) + return false; + return std::all_of(m_owned.begin() + phys, m_owned.begin() + phys + size, + [](uint8_t value) + { return value != 0u; }); + } + + void IopMemory::markOwned(uint32_t address, size_t size) + { + if (address > RamSize || size > RamSize - address) + return; + std::fill(m_owned.begin() + address, m_owned.begin() + address + size, uint8_t{1}); + } + + bool IopMemory::isHardwareAddress(uint32_t address) const + { + const uint32_t phys = physicalAddress(address); + return (phys >= HardwareBase && phys < HardwareEnd) || + (phys >= Spu2Base && phys < Spu2End) || + (phys >= SifBase && phys < SifEnd); + } + + uint32_t IopMemory::readHardware32(uint32_t address) const + { + const auto value = m_hardware.find(address); + if (value != m_hardware.end()) + return value->second; + switch (address) + { + case 0x1F801070u: + return m_interruptStatus; + case 0x1F801074u: + return m_interruptMask; + case 0x1F801078u: + return m_interruptControl; + default: + return 0u; + } + } + + void IopMemory::writeHardware32(uint32_t address, uint32_t value) + { + switch (address) + { + case 0x1F801070u: + m_interruptStatus &= value; + return; + case 0x1F801074u: + m_interruptMask = value; + return; + case 0x1F801078u: + m_interruptControl = value & 1u; + return; + default: + break; + } + + m_hardware[address] = value; + if ((address != kDmaSpu0Chcr && address != kDmaSpu1Chcr) || (value & kDmaStart) == 0u) + return; + + const bool secondCore = address == kDmaSpu1Chcr; + m_hardware[address] = value & ~kDmaStart; + + const uint32_t statusAddress = 0x1F900344u + (secondCore ? 0x400u : 0u); + const uint32_t alignedStatus = statusAddress & ~3u; + const uint32_t shift = (statusAddress & 2u) * 8u; + uint32_t status = 0u; + if (const auto current = m_hardware.find(alignedStatus); current != m_hardware.end()) + status = current->second; + status |= 0x80u << shift; + m_hardware[alignedStatus] = status; + + const uint32_t blockControlAddress = address - sizeof(uint32_t); + uint32_t blockControl = 0u; + if (const auto current = m_hardware.find(blockControlAddress); current != m_hardware.end()) + blockControl = current->second; + const uint32_t wordsPerBlock = std::max(blockControl & 0xFFFFu, 1u); + const uint32_t blockCount = std::max(blockControl >> 16u, 1u); + const uint64_t transferWords = static_cast(wordsPerBlock) * blockCount; + m_dmaStart = DmaStart{ + secondCore ? kDmaSpu1Irq : kDmaSpu0Irq, + std::max(transferWords * 2u, 64u), + }; + } + + std::optional IopMemory::takeDmaStart() noexcept + { + std::optional result = m_dmaStart; + m_dmaStart.reset(); + return result; + } + + uint32_t IopMemory::allocate(uint32_t size, uint32_t alignment, std::optional fixed) + { + size = alignUp(std::max(size, 1u), 16u); + alignment = std::max(alignment, 4u); + if (fixed) + { + const uint32_t address = *fixed; + if (address < HeapBase || address + size > HeapLimit) + return 0u; + for (const auto &block : m_allocations) + if (address < block.address + block.size && block.address < address + size) + return 0u; + m_allocations.push_back({address, size}); + markOwned(address, size); + return address; + } + + uint32_t candidate = alignUp(m_heapCursor, alignment); + for (;;) + { + bool overlap = false; + for (const auto &block : m_allocations) + { + if (candidate < block.address + block.size && block.address < candidate + size) + { + candidate = alignUp(block.address + block.size, alignment); + overlap = true; + break; + } + } + if (!overlap) + break; + } + if (candidate > HeapLimit || size > HeapLimit - candidate) + return 0u; + m_allocations.push_back({candidate, size}); + markOwned(candidate, size); + m_heapCursor = std::max(m_heapCursor, candidate + size); + return candidate; + } + + bool IopMemory::freeAllocation(uint32_t address) + { + const auto block = std::find_if(m_allocations.begin(), m_allocations.end(), + [&](const Allocation &candidate) + { return candidate.address == address; }); + if (block == m_allocations.end()) + return false; + std::fill(m_owned.begin() + block->address, + m_owned.begin() + block->address + block->size, + uint8_t{0}); + m_allocations.erase(block); + return true; + } + + uint32_t IopMemory::maxFreeMemory() const + { + return m_heapCursor < HeapLimit ? HeapLimit - m_heapCursor : 0u; + } + + std::optional IopMemory::allocationContaining(uint32_t address) const + { + const auto block = std::find_if(m_allocations.begin(), m_allocations.end(), + [&](const Allocation &candidate) + { + return address >= candidate.address && + address < candidate.address + candidate.size; + }); + if (block == m_allocations.end()) + return std::nullopt; + return *block; + } + + std::string IopMemory::readString(uint32_t address, size_t limit) const + { + std::string result; + result.reserve(std::min(limit, 64u)); + for (size_t i = 0; i < limit; ++i) + { + const char ch = static_cast(read8(address + static_cast(i))); + if (ch == '\0') + break; + result.push_back(ch); + } + return result; + } +} diff --git a/ps2xIOP/src/emulator/core/iop_memory.h b/ps2xIOP/src/emulator/core/iop_memory.h new file mode 100644 index 0000000..a932a5d --- /dev/null +++ b/ps2xIOP/src/emulator/core/iop_memory.h @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + class IopMemory + { + public: + static constexpr uint32_t RamSize = 2u * 1024u * 1024u; + static constexpr uint32_t ScratchBase = 0x1F800000u; + static constexpr uint32_t ScratchSize = 0x400u; + static constexpr uint32_t HardwareBase = 0x1F801000u; + static constexpr uint32_t HardwareEnd = 0x1F900000u; + static constexpr uint32_t Spu2Base = 0x1F900000u; + static constexpr uint32_t Spu2End = 0x1FA00000u; + static constexpr uint32_t SifBase = 0x1D000000u; + static constexpr uint32_t SifEnd = 0x1D001000u; + static constexpr uint32_t HeapBase = 0x00120000u; + static constexpr uint32_t HeapLimit = 0x001F0000u; + + struct Allocation + { + uint32_t address = 0; + uint32_t size = 0; + }; + + struct DmaStart + { + int irq = 0; + uint64_t delayCycles = 0; + }; + + IopMemory(); + + void reset(); + + [[nodiscard]] uint8_t read8(uint32_t address) const; + [[nodiscard]] uint16_t read16(uint32_t address) const; + [[nodiscard]] uint32_t read32(uint32_t address) const; + void write8(uint32_t address, uint8_t value); + void write16(uint32_t address, uint16_t value); + void write32(uint32_t address, uint32_t value); + + [[nodiscard]] bool readRam(uint32_t address, void *destination, size_t size) const; + [[nodiscard]] bool writeRam(uint32_t address, const void *source, size_t size); + [[nodiscard]] bool zeroRam(uint32_t address, size_t size); + [[nodiscard]] bool ownsRamRange(uint32_t address, size_t size) const; + [[nodiscard]] bool isHardwareAddress(uint32_t address) const; + [[nodiscard]] std::string readString(uint32_t address, size_t limit = 1024u) const; + + [[nodiscard]] uint32_t allocate(uint32_t size, uint32_t alignment = 16u, std::optional fixed = std::nullopt); + [[nodiscard]] bool freeAllocation(uint32_t address); + [[nodiscard]] uint32_t maxFreeMemory() const; + [[nodiscard]] std::optional allocationContaining(uint32_t address) const; + + [[nodiscard]] uint32_t interruptStatus() const noexcept { return m_interruptStatus; } + [[nodiscard]] uint32_t interruptMask() const noexcept { return m_interruptMask; } + [[nodiscard]] uint32_t interruptControl() const noexcept { return m_interruptControl; } + void setInterruptStatus(uint32_t value) noexcept { m_interruptStatus = value; } + void setInterruptMask(uint32_t value) noexcept { m_interruptMask = value; } + void setInterruptControl(uint32_t value) noexcept { m_interruptControl = value & 1u; } + + [[nodiscard]] std::optional takeDmaStart() noexcept; + [[nodiscard]] std::span ram() const noexcept { return m_ram; } + + [[nodiscard]] static uint32_t physicalAddress(uint32_t address) noexcept; + + private: + [[nodiscard]] uint32_t readHardware32(uint32_t address) const; + void writeHardware32(uint32_t address, uint32_t value); + void markOwned(uint32_t address, size_t size); + + std::vector m_ram; + std::vector m_owned; + std::vector m_scratch; + std::unordered_map m_hardware; + std::vector m_allocations; + uint32_t m_heapCursor = HeapBase; + uint32_t m_interruptStatus = 0; + uint32_t m_interruptMask = 0; + uint32_t m_interruptControl = 1; + std::optional m_dmaStart; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_cdvd.cpp b/ps2xIOP/src/emulator/imports/iop_cdvd.cpp new file mode 100644 index 0000000..ad58fb4 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_cdvd.cpp @@ -0,0 +1,751 @@ +#include "iop_cdvd.h" + +#include "../core/iop_cpu.h" +#include "../core/iop_kernel.h" +#include "../core/iop_memory.h" +#include "ps2x/iop/iop_host.h" +#include "ps2x/iop/ps2_path.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + namespace + { + constexpr uint32_t kSectorSize = 2048u; + constexpr uint32_t kPrimaryVolumeDescriptorLsn = 16u; + constexpr uint32_t kVolumeDescriptorTerminatorLsn = 17u; + constexpr uint32_t kFirstDirectoryLsn = 20u; + constexpr uint32_t kCdvdErrorNone = 0u; + constexpr uint32_t kCdvdErrorRead = 0x30u; + constexpr uint32_t kCdvdTypePs2Dvd = 0x14u; + constexpr uint32_t kCdvdReadyComplete = 2u; + constexpr uint32_t kCdvdStatusPause = 0x0Au; + constexpr uint32_t kCdvdInitExit = 5u; + constexpr uint32_t kCdvdCallbackRead = 1u; + constexpr uint32_t kCdvdCallbackSeek = 4u; + constexpr uint32_t kCdvdInterruptReadyBits = 0x29u; + constexpr uint32_t kEventFlagMulti = 2u; + constexpr uint32_t kCdvdStreamTimeout = 5000u; + constexpr uint32_t kCdvdSyncTimeout = 15000u; + constexpr uint32_t kCdvdmanVersion = 0x0226u; + + uint32_t alignSectors(uint64_t bytes) + { + return static_cast((bytes + kSectorSize - 1u) / kSectorSize); + } + + void writeLe16(uint8_t *destination, uint16_t value) + { + destination[0] = static_cast(value); + destination[1] = static_cast(value >> 8u); + } + + void writeBe16(uint8_t *destination, uint16_t value) + { + destination[0] = static_cast(value >> 8u); + destination[1] = static_cast(value); + } + + void writeLe32(uint8_t *destination, uint32_t value) + { + for (uint32_t i = 0u; i < 4u; ++i) + destination[i] = static_cast(value >> (i * 8u)); + } + + void writeBe32(uint8_t *destination, uint32_t value) + { + for (uint32_t i = 0u; i < 4u; ++i) + destination[i] = static_cast(value >> ((3u - i) * 8u)); + } + + void writeBoth16(uint8_t *destination, uint16_t value) + { + writeLe16(destination, value); + writeBe16(destination + 2u, value); + } + + void writeBoth32(uint8_t *destination, uint32_t value) + { + writeLe32(destination, value); + writeBe32(destination + 4u, value); + } + + std::string isoName(const std::filesystem::path &path, bool directory) + { + std::string name = path.filename().string(); + for (char &character : name) + { + const unsigned char byte = static_cast(character); + character = byte < 0x80u ? static_cast(std::toupper(byte)) : '_'; + } + if (name.size() > 200u) + name.resize(200u); + if (!directory && name.find(';') == std::string::npos) + name += ";1"; + return name; + } + + size_t directoryRecordSize(size_t identifierSize) + { + const size_t unpadded = 33u + identifierSize; + return unpadded + (unpadded & 1u); + } + + uint32_t directoryBytesFor(const std::vector &identifierSizes) + { + uint64_t cursor = 0u; + for (const size_t identifierSize : identifierSizes) + { + const uint64_t recordSize = directoryRecordSize(identifierSize); + const uint64_t sectorOffset = cursor % kSectorSize; + if (sectorOffset + recordSize > kSectorSize) + cursor += kSectorSize - sectorOffset; + cursor += recordSize; + } + return static_cast(std::max(kSectorSize, alignSectors(cursor) * kSectorSize)); + } + + std::string normalizedIsoComponent(std::string_view value) + { + std::string result(value); + const size_t semicolon = result.rfind(';'); + if (semicolon != std::string::npos && semicolon + 1u < result.size() && + std::all_of(result.begin() + static_cast(semicolon + 1u), result.end(), + [](unsigned char ch) + { return std::isdigit(ch) != 0; })) + { + result.resize(semicolon); + } + std::transform(result.begin(), result.end(), result.begin(), + [](unsigned char ch) + { return static_cast(std::toupper(ch)); }); + return result; + } + + size_t writeDirectoryRecord(uint8_t *destination, + uint32_t lsn, + uint32_t size, + bool directory, + const uint8_t *identifier, + size_t identifierSize) + { + const size_t recordSize = directoryRecordSize(identifierSize); + std::memset(destination, 0, recordSize); + destination[0] = static_cast(recordSize); + writeBoth32(destination + 2u, lsn); + writeBoth32(destination + 10u, size); + destination[25] = directory ? 2u : 0u; + writeBoth16(destination + 28u, 1u); + destination[32] = static_cast(identifierSize); + std::memcpy(destination + 33u, identifier, identifierSize); + return recordSize; + } + } + + class IopCdvd::Impl + { + public: + struct Callback + { + uint32_t address = 0u; + uint32_t gp = 0u; + }; + + struct IsoNode + { + std::filesystem::path hostPath; + std::string identifier; + size_t parent = 0u; + bool directory = false; + uint32_t lsn = 0u; + uint32_t size = 0u; + uint32_t sectors = 0u; + uint64_t handle = 0u; + std::vector children; + }; + + Impl(IopHost &hostRef, IopMemory &memoryRef, IopKernel &kernelRef) + : host(hostRef), memory(memoryRef), kernel(kernelRef) + { + } + + ~Impl() + { + closeFiles(); + } + + void reset() + { + closeFiles(); + callback = {}; + initialized = false; + mediaMode = 0u; + currentLsn = 0u; + lastError = kCdvdErrorNone; + streamFlag = 0u; + lastReadTimeout = 0u; + interruptEventFlagId = 0; + virtualIsoBuilt = false; + virtualIsoValid = false; + completionCallback.reset(); + nodes.clear(); + metadataSectors.clear(); + imageHandle = 0u; + } + + bool dispatchImport(uint16_t ordinal, IopCpuState &cpu) + { + const uint32_t a0 = cpu.gpr[4]; + const uint32_t a1 = cpu.gpr[5]; + const uint32_t a2 = cpu.gpr[6]; + + switch (ordinal) + { + case 4: // sceCdInit + initialized = a0 != kCdvdInitExit; + if (initialized) + { + callback = {}; + completionCallback.reset(); + } + lastError = kCdvdErrorNone; + cpu.gpr[2] = 1u; + return true; + + case 5: // sceCdStandby + cpu.gpr[2] = 1u; + return true; + + case 6: // sceCdRead + if (readSectors(a0, a1, a2)) + { + signalCommandComplete(); + if (callback.address != 0u) + { + completionCallback = CompletionCallback{ + callback.address, + callback.gp, + kCdvdCallbackRead, + }; + } + cpu.gpr[2] = 1u; + } + else + cpu.gpr[2] = 0u; + return true; + + case 7: // sceCdSeek + currentLsn = a0; + lastError = kCdvdErrorNone; + signalCommandComplete(); + if (callback.address != 0u) + { + completionCallback = CompletionCallback{ + callback.address, + callback.gp, + kCdvdCallbackSeek, + }; + } + cpu.gpr[2] = 1u; + return true; + + case 8: // sceCdGetError + cpu.gpr[2] = lastError; + return true; + + case 10: // sceCdSearchFile + cpu.gpr[2] = searchFile(a0, a1) ? 1u : 0u; + return true; + + case 11: // sceCdSync + cpu.gpr[2] = 0u; + return true; + + case 12: // sceCdGetDiskType + cpu.gpr[2] = kCdvdTypePs2Dvd; + return true; + + case 13: // sceCdDiskReady + cpu.gpr[2] = kCdvdReadyComplete; + return true; + + case 28: // sceCdStatus + cpu.gpr[2] = kCdvdStatusPause; + return true; + + case 37: // sceCdCallback + { + const uint32_t previous = callback.address; + callback = {a0, cpu.gpr[28]}; + cpu.gpr[2] = previous; + return true; + } + + case 50: // sceCdSC + { + const int32_t code = static_cast(a0); + switch (code) + { + case -23: // Translate a logical sector for a dual-layer disc. + // The host image exposes one continuous LSN space, so no layer offset is required. + cpu.gpr[2] = a1 != 0u ? memory.read32(a1) : 0u; + return true; + case -18: + lastReadTimeout = a1 != 0u ? memory.read32(a1) : 0u; + cpu.gpr[2] = 0u; + return true; + case -17: + cpu.gpr[2] = kCdvdStreamTimeout; + return true; + case -15: + cpu.gpr[2] = kCdvdSyncTimeout; + return true; + case -11: + cpu.gpr[2] = static_cast(ensureInterruptEventFlag()); + return true; + case -9: + cpu.gpr[2] = kCdvdmanVersion; + return true; + case -2: + lastError = a1 != 0u ? memory.read8(a1) : kCdvdErrorNone; + cpu.gpr[2] = lastError; + return true; + case -1: + case 0: + case 1: + case 2: + if (a1 != 0u) + memory.write32(a1, lastError & 0xFFu); + if (code != -1) + streamFlag = static_cast(code); + cpu.gpr[2] = streamFlag; + return true; + default: + // sceCdSC is intentionally extensible; unsupported controls are no-ops in cdvdman. + cpu.gpr[2] = 0u; + return true; + } + } + + case 75: // sceCdMmode + mediaMode = a0; + cpu.gpr[2] = 1u; + return true; + + default: + return false; + } + } + + std::optional takeCompletionCallback() noexcept + { + std::optional result = completionCallback; + completionCallback.reset(); + return result; + } + + private: + int ensureInterruptEventFlag() + { + if (interruptEventFlagId == 0) + { + interruptEventFlagId = kernel.createInternalEventFlag( + kEventFlagMulti, 0u, kCdvdInterruptReadyBits); + } + return interruptEventFlagId; + } + + void signalCommandComplete() + { + if (interruptEventFlagId != 0) + (void)kernel.setInternalEventFlag(interruptEventFlagId, kCdvdInterruptReadyBits); + } + + void closeFiles() + { + if (imageHandle != 0u) + host.closeHostFile(imageHandle); + imageHandle = 0u; + for (IsoNode &node : nodes) + { + if (node.handle != 0u) + host.closeHostFile(node.handle); + node.handle = 0u; + } + } + + bool addDirectory(size_t parent, const std::filesystem::path &path) + { + std::error_code error; + std::vector entries; + for ( + std::filesystem::directory_iterator iterator(path, std::filesystem::directory_options::skip_permission_denied, error), + end; + !error && iterator != end; + iterator.increment(error)) + { + const std::filesystem::directory_entry &entry = *iterator; + if (entry.is_symlink(error)) + { + error.clear(); + continue; + } + error.clear(); + if (entry.is_directory(error) || entry.is_regular_file(error)) + entries.push_back(entry); + error.clear(); + } + + std::sort(entries.begin(), entries.end(), + [](const auto &lhs, const auto &rhs) + { + return isoName(lhs.path(), lhs.is_directory()) < isoName(rhs.path(), rhs.is_directory()); + }); + + for (const auto &entry : entries) + { + error.clear(); + const bool directory = entry.is_directory(error); + if (error) + continue; + IsoNode node; + node.hostPath = entry.path(); + node.identifier = isoName(entry.path(), directory); + node.parent = parent; + node.directory = directory; + if (!directory) + { + const uint64_t fileSize = entry.file_size(error); + if (error) + continue; + node.size = static_cast(std::min(fileSize, std::numeric_limits::max())); + } + const size_t index = nodes.size(); + nodes.push_back(std::move(node)); + nodes[parent].children.push_back(index); + if (directory && !addDirectory(index, entry.path())) + return false; + } + return true; + } + + bool buildVirtualIso() + { + if (virtualIsoBuilt) + return virtualIsoValid; + virtualIsoBuilt = true; + + const std::string rootValue = host.hostPath(HostPathKind::CdRoot); + if (rootValue.empty()) + return false; + const std::filesystem::path rootPath(rootValue); + std::error_code error; + if (!std::filesystem::is_directory(rootPath, error) || error) + return false; + + nodes.clear(); + IsoNode root; + root.hostPath = rootPath; + root.identifier.clear(); + root.parent = 0u; + root.directory = true; + nodes.push_back(std::move(root)); + if (!addDirectory(0u, rootPath)) + return false; + + for (IsoNode &node : nodes) + { + if (!node.directory) + continue; + std::vector identifierSizes = {1u, 1u}; + identifierSizes.reserve(node.children.size() + 2u); + for (const size_t child : node.children) + identifierSizes.push_back(nodes[child].identifier.size()); + node.size = directoryBytesFor(identifierSizes); + node.sectors = node.size / kSectorSize; + } + + uint32_t cursor = kFirstDirectoryLsn; + for (IsoNode &node : nodes) + { + if (!node.directory) + continue; + node.lsn = cursor; + cursor += node.sectors; + } + for (IsoNode &node : nodes) + { + if (node.directory) + continue; + node.lsn = cursor; + node.sectors = alignSectors(node.size); + cursor += node.sectors; + } + volumeSectors = std::max(cursor, 32u); + + std::array primary{}; + primary[0] = 1u; + std::memcpy(primary.data() + 1u, "CD001", 5u); + primary[6] = 1u; + std::memset(primary.data() + 8u, ' ', 32u); + std::memcpy(primary.data() + 8u, "PS2XRECOMP", 10u); + std::memset(primary.data() + 40u, ' ', 32u); + std::memcpy(primary.data() + 40u, "PS2X VIRTUAL DISC", 17u); + writeBoth32(primary.data() + 80u, volumeSectors); + writeBoth16(primary.data() + 120u, 1u); + writeBoth16(primary.data() + 124u, 1u); + writeBoth16(primary.data() + 128u, static_cast(kSectorSize)); + const uint8_t rootIdentifier = 0u; + (void)writeDirectoryRecord(primary.data() + 156u, nodes[0].lsn, nodes[0].size, true, &rootIdentifier, 1u); + primary[881] = 1u; + metadataSectors[kPrimaryVolumeDescriptorLsn] = primary; + + std::array terminator{}; + terminator[0] = 255u; + std::memcpy(terminator.data() + 1u, "CD001", 5u); + terminator[6] = 1u; + metadataSectors[kVolumeDescriptorTerminatorLsn] = terminator; + + for (size_t nodeIndex = 0u; nodeIndex < nodes.size(); ++nodeIndex) + { + const IsoNode &node = nodes[nodeIndex]; + if (!node.directory) + continue; + std::vector bytes(node.size, 0u); + size_t offset = 0u; + const auto appendRecord = [&](const IsoNode &entry, const uint8_t *identifier, size_t identifierSize) + { + const size_t recordSize = directoryRecordSize(identifierSize); + const size_t sectorOffset = offset % kSectorSize; + if (sectorOffset + recordSize > kSectorSize) + offset += kSectorSize - sectorOffset; + offset += writeDirectoryRecord(bytes.data() + offset, + entry.lsn, + entry.size, + entry.directory, + identifier, + identifierSize); + }; + const uint8_t selfIdentifier = 0u; + const uint8_t parentIdentifier = 1u; + appendRecord(node, &selfIdentifier, 1u); + appendRecord(nodes[node.parent], &parentIdentifier, 1u); + for (const size_t childIndex : node.children) + { + const IsoNode &child = nodes[childIndex]; + appendRecord(child, reinterpret_cast(child.identifier.data()), child.identifier.size()); + } + for (uint32_t sector = 0u; sector < node.sectors; ++sector) + { + std::array contents{}; + std::memcpy(contents.data(), bytes.data() + sector * kSectorSize, kSectorSize); + metadataSectors[node.lsn + sector] = contents; + } + } + + virtualIsoValid = true; + return true; + } + + IsoNode *findVirtualIsoNode(std::string_view guestPath) + { + if (!buildVirtualIso()) + return nullptr; + + const ParsedPs2Path parsed = parsePs2Path(guestPath); + if (!parsed || parsed.device != Ps2PathDevice::Cdrom) + return nullptr; + + size_t current = 0u; + size_t begin = 0u; + while (begin <= parsed.path.size()) + { + const size_t end = parsed.path.find('/', begin); + const size_t length = (end == std::string::npos) ? parsed.path.size() - begin : end - begin; + const std::string_view component(parsed.path.data() + begin, length); + begin = (end == std::string::npos) ? parsed.path.size() + 1u : end + 1u; + + if (component.empty() || component == ".") + continue; + if (component == "..") + return nullptr; + + const std::string wanted = normalizedIsoComponent(component); + const auto child = std::find_if(nodes[current].children.begin(), nodes[current].children.end(), + [&](size_t childIndex) + { + return normalizedIsoComponent(nodes[childIndex].identifier) == wanted; + }); + if (child == nodes[current].children.end()) + return nullptr; + current = *child; + } + return &nodes[current]; + } + + bool searchFile(uint32_t resultAddress, uint32_t nameAddress) + { + if (resultAddress == 0u || nameAddress == 0u) + return false; + + const std::string guestPath = memory.readString(nameAddress, 1024u); + IsoNode *node = findVirtualIsoNode(guestPath); + if (!node) + return false; + + // sceCdlFILE: lsn, size, name[16], date/flags[8]. + std::array result{}; + writeLe32(result.data(), node->lsn); + writeLe32(result.data() + 4u, node->size); + const std::string leaf = normalizedIsoComponent(node->identifier); + std::memcpy(result.data() + 8u, leaf.data(), std::min(16u, leaf.size())); + result[24u] = node->directory ? 2u : 0u; + return memory.writeRam(resultAddress, result.data(), result.size()); + } + + IsoNode *fileForSector(uint32_t lsn) + { + for (IsoNode &node : nodes) + { + if (!node.directory && lsn >= node.lsn && lsn < node.lsn + node.sectors) + return &node; + } + return nullptr; + } + + bool readVirtualSector(uint32_t lsn, uint8_t *destination) + { + const auto metadata = metadataSectors.find(lsn); + if (metadata != metadataSectors.end()) + { + std::memcpy(destination, metadata->second.data(), kSectorSize); + return true; + } + + IsoNode *node = fileForSector(lsn); + if (!node) + { + std::memset(destination, 0, kSectorSize); + return lsn < volumeSectors; + } + if (node->handle == 0u) + node->handle = host.openHostFile(node->hostPath.string()); + if (node->handle == 0u) + return false; + + std::memset(destination, 0, kSectorSize); + const uint64_t offset = static_cast(lsn - node->lsn) * kSectorSize; + const size_t wanted = static_cast(std::min(kSectorSize, static_cast(node->size) - offset)); + size_t bytesRead = 0u; + return host.readHostFile(node->handle, offset, destination, wanted, bytesRead) && bytesRead == wanted; + } + + bool readSectors(uint32_t lsn, uint32_t sectors, uint32_t destination) + { + if (sectors == 0u) + { + lastError = kCdvdErrorNone; + return true; + } + const uint64_t byteCount64 = static_cast(sectors) * kSectorSize; + if (byteCount64 > IopMemory::RamSize || !memory.ownsRamRange(destination, static_cast(byteCount64))) + { + lastError = kCdvdErrorRead; + return false; + } + const size_t byteCount = static_cast(byteCount64); + std::vector bytes(byteCount, 0u); + + bool read = false; + const std::string imagePath = host.hostPath(HostPathKind::CdImage); + if (!imagePath.empty()) + { + if (imageHandle == 0u) + imageHandle = host.openHostFile(imagePath); + if (imageHandle != 0u) + { + size_t bytesRead = 0u; + read = host.readHostFile(imageHandle, + static_cast(lsn) * kSectorSize, + bytes.data(), + byteCount, + bytesRead) && + bytesRead == byteCount; + } + } + + if (!read && buildVirtualIso()) + { + read = true; + for (uint32_t sector = 0u; sector < sectors; ++sector) + { + if (!readVirtualSector(lsn + sector, bytes.data() + static_cast(sector) * kSectorSize)) + { + read = false; + break; + } + } + } + + if (!read || !memory.writeRam(destination, bytes.data(), bytes.size())) + { + lastError = kCdvdErrorRead; + return false; + } + lastError = kCdvdErrorNone; + return true; + } + + IopHost &host; + IopMemory &memory; + IopKernel &kernel; + Callback callback; + std::optional completionCallback; + bool initialized = false; + uint32_t mediaMode = 0u; + uint32_t currentLsn = 0u; + uint32_t lastError = kCdvdErrorNone; + uint32_t streamFlag = 0u; + uint32_t lastReadTimeout = 0u; + int interruptEventFlagId = 0; + uint64_t imageHandle = 0u; + bool virtualIsoBuilt = false; + bool virtualIsoValid = false; + uint32_t volumeSectors = 0u; + std::vector nodes; + std::unordered_map> metadataSectors; + }; + + IopCdvd::IopCdvd(IopHost &host, IopMemory &memory, IopKernel &kernel) + : m_impl(std::make_unique(host, memory, kernel)) + { + } + + IopCdvd::~IopCdvd() = default; + + void IopCdvd::reset() noexcept + { + m_impl->reset(); + } + + bool IopCdvd::dispatchImport(uint16_t ordinal, IopCpuState &cpu) + { + return m_impl->dispatchImport(ordinal, cpu); + } + + std::optional IopCdvd::takeCompletionCallback() noexcept + { + return m_impl->takeCompletionCallback(); + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_cdvd.h b/ps2xIOP/src/emulator/imports/iop_cdvd.h new file mode 100644 index 0000000..2ca6048 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_cdvd.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include +#include + +namespace ps2x::iop +{ + class IopHost; +} + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopKernel; + class IopMemory; + + class IopCdvd + { + public: + struct CompletionCallback + { + uint32_t address = 0u; + uint32_t gp = 0u; + uint32_t reason = 0u; + }; + + IopCdvd(IopHost &host, IopMemory &memory, IopKernel &kernel); + ~IopCdvd(); + + IopCdvd(const IopCdvd &) = delete; + IopCdvd &operator=(const IopCdvd &) = delete; + + void reset() noexcept; + + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu); + [[nodiscard]] std::optional takeCompletionCallback() noexcept; + + private: + class Impl; + std::unique_ptr m_impl; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_heaplib.cpp b/ps2xIOP/src/emulator/imports/iop_heaplib.cpp new file mode 100644 index 0000000..caa1417 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_heaplib.cpp @@ -0,0 +1,54 @@ +#include "iop_heaplib.h" + +#include "../core/iop_cpu.h" +#include "../core/iop_memory.h" + +namespace ps2x::iop::detail +{ + IopHeaplib::IopHeaplib(IopMemory &memory) noexcept + : m_memory(memory) + { + } + + bool IopHeaplib::dispatchImport(uint16_t ordinal, IopCpuState &cpu) + { + const uint32_t a0 = cpu.gpr[4]; + const uint32_t a1 = cpu.gpr[5]; + const auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + + switch (ordinal) + { + case 4: // CreateHeap + setV0(m_memory.allocate(16u, 16u)); + return true; + case 5: // DeleteHeap + if (a0 != 0u) + (void)m_memory.freeAllocation(a0); + setV0(0u); + return true; + case 6: + setV0(m_memory.allocate(a1, 16u)); + return true; + case 7: + setV0(m_memory.freeAllocation(a1) ? 0u : 0xFFFFFFFFu); + return true; + case 8: + setV0(m_memory.maxFreeMemory()); + return true; + case 11: + setV0(0u); + return true; + case 15: + if (const auto block = m_memory.allocationContaining(a0)) + setV0(block->size); + else + setV0(0xFFFFFFFFu); + return true; + default: + return false; + } + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_heaplib.h b/ps2xIOP/src/emulator/imports/iop_heaplib.h new file mode 100644 index 0000000..26b7ec5 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_heaplib.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopMemory; + + class IopHeaplib + { + public: + explicit IopHeaplib(IopMemory &memory) noexcept; + + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu); + + private: + IopMemory &m_memory; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_imports.cpp b/ps2xIOP/src/emulator/imports/iop_imports.cpp new file mode 100644 index 0000000..f13267f --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_imports.cpp @@ -0,0 +1,195 @@ +#include "iop_imports.h" + +#include "../core/iop_memory.h" + +#include +#include +#include + +namespace ps2x::iop::detail +{ + namespace + { + constexpr uint32_t kImportMagic = 0x41E00000u; + constexpr uint32_t kExportMagic = 0x41C00000u; + + bool equalsIgnoreCase(std::string_view lhs, std::string_view rhs) + { + if (lhs.size() != rhs.size()) + return false; + for (size_t i = 0; i < lhs.size(); ++i) + { + if (std::tolower(static_cast(lhs[i])) != + std::tolower(static_cast(rhs[i]))) + return false; + } + return true; + } + + std::string trimLibraryName(const char *name) + { + size_t length = 0u; + while (length < 8u && name[length] != '\0') + ++length; + return std::string(name, length); + } + } + + IopImportRegistry::IopImportRegistry(IopMemory &memory) noexcept + : m_memory(memory) + { + } + + void IopImportRegistry::reset() + { + m_libraries.clear(); + } + + std::optional IopImportRegistry::decode(uint32_t pc) const + { + if (m_memory.read32(pc) != 0x03E00008u) + return std::nullopt; + const uint32_t delay = m_memory.read32(pc + 4u); + if ((delay & 0xFFFF0000u) != 0x24000000u) + return std::nullopt; + + const uint32_t physicalPc = IopMemory::physicalAddress(pc); + const uint32_t searchBegin = physicalPc > 0x10000u ? physicalPc - 0x10000u : 0u; + for (uint32_t candidate = physicalPc & ~3u; candidate >= searchBegin + 20u; candidate -= 4u) + { + const uint32_t table = candidate - 20u; + if (m_memory.read32(table) != kImportMagic) + { + if (candidate == searchBegin + 20u) + break; + continue; + } + + char name[9]{}; + for (uint32_t i = 0; i < 8u; ++i) + name[i] = static_cast(m_memory.read8(table + 12u + i)); + const uint32_t stubs = table + 20u; + if (physicalPc < stubs || ((physicalPc - stubs) & 7u) != 0u) + continue; + + bool valid = false; + for (uint32_t stub = stubs; + stub + 7u < IopMemory::RamSize && stub <= physicalPc; + stub += 8u) + { + const uint32_t first = m_memory.read32(stub); + const uint32_t second = m_memory.read32(stub + 4u); + if (first == 0u && second == 0u) + break; + if (stub == physicalPc) + { + valid = true; + break; + } + } + if (valid) + { + return IopImportCall{ + trimLibraryName(name), + static_cast(delay & 0xFFFFu), + m_memory.read16(table + 8u), + }; + } + } + return std::nullopt; + } + + bool IopImportRegistry::registerExportTable(uint32_t address) + { + const uint32_t physical = IopMemory::physicalAddress(address); + if (physical + 20u > IopMemory::RamSize || + m_memory.read32(physical) != kExportMagic) + return false; + + char name[9]{}; + for (uint32_t i = 0; i < 8u; ++i) + name[i] = static_cast(m_memory.read8(physical + 12u + i)); + + ExportLibrary library; + library.tableAddress = physical; + library.version = m_memory.read16(physical + 8u); + library.name = trimLibraryName(name); + for (uint32_t cursor = physical + 20u; cursor + 3u < IopMemory::RamSize; cursor += 4u) + { + const uint32_t function = m_memory.read32(cursor); + if (function == 0u) + break; + library.functions.push_back(function); + if (library.functions.size() > 1024u) + return false; + } + m_libraries[physical] = std::move(library); + return true; + } + + bool IopImportRegistry::releaseExportTable(uint32_t address) + { + return m_libraries.erase(IopMemory::physicalAddress(address)) != 0u; + } + + const IopImportRegistry::ExportLibrary *IopImportRegistry::findLibrary(std::string_view name, std::optional version) const + { + const ExportLibrary *selected = nullptr; + for (const auto &[address, library] : m_libraries) + { + (void)address; + if (!equalsIgnoreCase(library.name, name) || + (version && (library.version >> 8u) != (*version >> 8u))) + continue; + // LOADCORE links by major version; a newer minor supersedes older exports. + if (!selected || library.version > selected->version) + selected = &library; + } + return selected; + } + + uint32_t IopImportRegistry::findTable(std::string_view library, std::optional version) const + { + const ExportLibrary *found = findLibrary(library, version); + return found ? found->tableAddress : 0u; + } + + uint32_t IopImportRegistry::resolve(std::string_view library, uint16_t ordinal, std::optional version) const + { + const ExportLibrary *found = findLibrary(library, version); + if (!found || ordinal >= found->functions.size()) + return 0u; + return found->functions[ordinal]; + } + + int32_t IopImportRegistry::setRebootTimeLibraryHandlingMode(uint32_t address, uint32_t mode) + { + constexpr int32_t kLibraryNotFound = -213; + constexpr int32_t kIllegalLibrary = -214; + + if (address == 0u) + return kIllegalLibrary; + const uint32_t physical = IopMemory::physicalAddress(address); + if (physical + 12u > IopMemory::RamSize) + return kLibraryNotFound; + + const bool registered = m_libraries.find(physical) != m_libraries.end(); + if (!registered && m_memory.read32(physical) != kExportMagic) + return kLibraryNotFound; + + const uint16_t oldMode = m_memory.read16(physical + 10u); + m_memory.write16(physical + 10u, static_cast((oldMode & ~6u) | (mode & 6u))); + return 0; + } + + void IopImportRegistry::eraseRange(uint32_t base, uint32_t size) + { + for (auto library = m_libraries.begin(); library != m_libraries.end();) + { + if (library->first >= base && library->first < base + size) + library = m_libraries.erase(library); + else + ++library; + } + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_imports.h b/ps2xIOP/src/emulator/imports/iop_imports.h new file mode 100644 index 0000000..cae8d91 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_imports.h @@ -0,0 +1,50 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + class IopMemory; + + struct IopImportCall + { + std::string library; + uint16_t ordinal = 0; + uint16_t version = 0; + }; + + class IopImportRegistry + { + public: + explicit IopImportRegistry(IopMemory &memory) noexcept; + + void reset(); + [[nodiscard]] std::optional decode(uint32_t pc) const; + [[nodiscard]] bool registerExportTable(uint32_t address); + [[nodiscard]] bool releaseExportTable(uint32_t address); + [[nodiscard]] uint32_t findTable(std::string_view library, std::optional version = std::nullopt) const; + [[nodiscard]] uint32_t resolve(std::string_view library, uint16_t ordinal, std::optional version = std::nullopt) const; + [[nodiscard]] int32_t setRebootTimeLibraryHandlingMode(uint32_t address, uint32_t mode); + void eraseRange(uint32_t base, uint32_t size); + + private: + struct ExportLibrary + { + uint32_t tableAddress = 0; + uint16_t version = 0; + std::string name; + std::vector functions; + }; + + [[nodiscard]] const ExportLibrary *findLibrary(std::string_view name, std::optional version) const; + + IopMemory &m_memory; + std::map m_libraries; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_intrman.cpp b/ps2xIOP/src/emulator/imports/iop_intrman.cpp new file mode 100644 index 0000000..da3be3b --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_intrman.cpp @@ -0,0 +1,113 @@ +#include "iop_intrman.h" + +#include "../core/iop_cpu.h" +#include "../core/iop_memory.h" +#include "../services/iop_rpc.h" + +namespace ps2x::iop::detail +{ + IopIntrman::IopIntrman(IopMemory &memory) noexcept + : m_memory(memory) + { + } + + void IopIntrman::reset() + { + m_handlers.clear(); + m_enabled.clear(); + } + + bool IopIntrman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor) + { + const uint32_t a0 = cpu.gpr[4]; + const uint32_t a1 = cpu.gpr[5]; + const uint32_t a2 = cpu.gpr[6]; + const uint32_t a3 = cpu.gpr[7]; + const auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + + switch (ordinal) + { + case 3: + setV0(0u); + return true; + case 4: // RegisterIntrHandler + m_handlers[static_cast(a0)] = {a2, a3, cpu.gpr[28]}; + setV0(0u); + return true; + case 5: // ReleaseIntrHandler + m_handlers.erase(static_cast(a0)); + setV0(0u); + return true; + case 6: // EnableIntr + m_enabled[static_cast(a0)] = true; + if (a0 < 32u) + m_memory.setInterruptMask(m_memory.interruptMask() | (1u << a0)); + setV0(0u); + return true; + case 7: // DisableIntr + if (a1 != 0u) + m_memory.write32(a1, a0); + if (a0 < 32u) + m_memory.setInterruptMask(m_memory.interruptMask() & ~(1u << a0)); + m_enabled[static_cast(a0)] = false; + setV0(0u); + return true; + case 8: // CpuDisableIntr + m_memory.setInterruptControl(0u); + setV0(0u); + return true; + case 9: // CpuEnableIntr + m_memory.setInterruptControl(1u); + setV0(0u); + return true; + case 14: + setV0(a0 != 0u + ? executor.executeGuestFunctionWithBudget(a0, a1, a2, a3, 0u, cpu.gpr[28], 100000u) + : 0u); + return true; + case 15: + case 16: + case 23: + case 24: + case 25: + case 28: + case 30: + setV0(0u); + return true; + case 17: + if (a0 != 0u) + m_memory.write32(a0, m_memory.interruptControl()); + m_memory.setInterruptControl(0u); + setV0(0u); + return true; + case 18: + m_memory.setInterruptControl(a0 != 0u ? 1u : 0u); + setV0(0u); + return true; + default: + return false; + } + } + + bool IopIntrman::dispatchInterrupt(int irq, IopGuestExecutor &executor) const + { + const auto enabled = m_enabled.find(irq); + if (enabled == m_enabled.end() || !enabled->second) + return false; + const auto handler = m_handlers.find(irq); + if (handler == m_handlers.end() || handler->second.function == 0u) + return false; + + (void)executor.executeGuestFunctionWithBudget(handler->second.function, + handler->second.argument, + 0u, + 0u, + 0u, + handler->second.gp, + 100000u); + return true; + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_intrman.h b/ps2xIOP/src/emulator/imports/iop_intrman.h new file mode 100644 index 0000000..48fdb14 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_intrman.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopGuestExecutor; + class IopMemory; + + class IopIntrman + { + public: + explicit IopIntrman(IopMemory &memory) noexcept; + + void reset(); + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor); + [[nodiscard]] bool dispatchInterrupt(int irq, IopGuestExecutor &executor) const; + + private: + struct Handler + { + uint32_t function = 0u; + uint32_t argument = 0u; + uint32_t gp = 0u; + }; + + IopMemory &m_memory; + std::map m_handlers; + std::map m_enabled; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_ioman.cpp b/ps2xIOP/src/emulator/imports/iop_ioman.cpp new file mode 100644 index 0000000..3eb4119 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_ioman.cpp @@ -0,0 +1,91 @@ +#include "iop_ioman.h" + +#include "../core/iop_cpu.h" +#include "../core/iop_memory.h" +#include "../services/iop_rpc.h" + +#include + +namespace ps2x::iop::detail +{ + IopIoman::IopIoman(IopMemory &memory) noexcept + : m_memory(memory) + { + } + + void IopIoman::reset() + { + m_devices.clear(); + } + + bool IopIoman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor) + { + constexpr size_t kMaxDevices = 16u; + const uint32_t a0 = cpu.gpr[4]; + const auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + + switch (ordinal) + { + case 20: // AddDrv + { + if (a0 == 0u || m_devices.size() >= kMaxDevices) + { + setV0(0xFFFFFFFFu); + return true; + } + + const uint32_t nameAddress = m_memory.read32(a0); + const uint32_t operations = m_memory.read32(a0 + 16u); + const std::string name = m_memory.readString(nameAddress, 64u); + if (nameAddress == 0u || operations == 0u || name.empty()) + { + setV0(0xFFFFFFFFu); + return true; + } + + m_devices.push_back({a0, cpu.gpr[28], name}); + const uint32_t init = m_memory.read32(operations); + if (init != 0u) + { + const int32_t result = static_cast( + executor.executeGuestFunction(init, a0, 0u, 0u, 0u, cpu.gpr[28])); + if (result < 0) + { + m_devices.pop_back(); + setV0(0xFFFFFFFFu); + return true; + } + } + + setV0(0u); + return true; + } + case 21: // DelDrv + { + const std::string name = m_memory.readString(a0, 64u); + const auto device = std::find_if( + m_devices.begin(), m_devices.end(), + [&](const Device &candidate) + { return candidate.name == name; }); + if (device == m_devices.end()) + { + setV0(0xFFFFFFFFu); + return true; + } + + const uint32_t operations = m_memory.read32(device->address + 16u); + const uint32_t deinit = operations != 0u ? m_memory.read32(operations + 4u) : 0u; + if (deinit != 0u) + (void)executor.executeGuestFunction(deinit, device->address, 0u, 0u, 0u, device->gp); + m_devices.erase(device); + setV0(0u); + return true; + } + default: + return false; + } + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_ioman.h b/ps2xIOP/src/emulator/imports/iop_ioman.h new file mode 100644 index 0000000..182f786 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_ioman.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include +#include + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopGuestExecutor; + class IopMemory; + + class IopIoman + { + public: + explicit IopIoman(IopMemory &memory) noexcept; + + void reset(); + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, IopGuestExecutor &executor); + + private: + struct Device + { + uint32_t address = 0u; + uint32_t gp = 0u; + std::string name; + }; + + IopMemory &m_memory; + std::vector m_devices; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_loadcore.cpp b/ps2xIOP/src/emulator/imports/iop_loadcore.cpp new file mode 100644 index 0000000..600bff8 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_loadcore.cpp @@ -0,0 +1,65 @@ +#include "iop_loadcore.h" + +#include "../core/iop_cpu.h" +#include "iop_imports.h" +#include "../core/iop_memory.h" + +namespace ps2x::iop::detail +{ + IopLoadcore::IopLoadcore(IopMemory &memory, IopImportRegistry &imports) noexcept + : m_memory(memory), m_imports(imports) + { + } + + bool IopLoadcore::dispatchImport(uint16_t ordinal, IopCpuState &cpu) + { + const uint32_t a0 = cpu.gpr[4]; + const auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + + switch (ordinal) + { + case 3: + case 4: + case 5: + case 8: + case 9: + case 12: + case 13: + case 14: + case 15: + case 16: + case 17: + case 20: + case 21: + setV0(0u); + return true; + case 6: + case 10: + setV0(m_imports.registerExportTable(a0) ? 0u : 0xFFFFFFFFu); + return true; + case 7: + setV0(m_imports.releaseExportTable(a0) ? 0u : 0xFFFFFFFFu); + return true; + case 11: // QueryLibraryEntryTable returns the function array, not the export header. + { + const uint32_t address = IopMemory::physicalAddress(a0); + if (a0 == 0u || address > IopMemory::RamSize - 20u) + { + setV0(0u); + return true; + } + const uint32_t table = m_imports.findTable(m_memory.readString(address + 12u, 8u), m_memory.read16(address + 8u)); + setV0(table != 0u ? table + 20u : 0u); + return true; + } + case 27: // SetRebootTimeLibraryHandlingMode + setV0(static_cast(m_imports.setRebootTimeLibraryHandlingMode(a0, cpu.gpr[5]))); + return true; + default: + return false; + } + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_loadcore.h b/ps2xIOP/src/emulator/imports/iop_loadcore.h new file mode 100644 index 0000000..e457a98 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_loadcore.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopImportRegistry; + class IopMemory; + + class IopLoadcore + { + public: + IopLoadcore(IopMemory &memory, IopImportRegistry &imports) noexcept; + + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu); + + private: + IopMemory &m_memory; + IopImportRegistry &m_imports; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_stdio.cpp b/ps2xIOP/src/emulator/imports/iop_stdio.cpp new file mode 100644 index 0000000..eed2b61 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_stdio.cpp @@ -0,0 +1,67 @@ +#include "iop_stdio.h" + +#include "../core/iop_cpu.h" +#include "../core/iop_memory.h" +#include "ps2x/iop/iop_host.h" + +#include + +namespace ps2x::iop::detail +{ + IopStdio::IopStdio(IopHost &host, IopMemory &memory) noexcept + : m_host(host), m_memory(memory) + { + } + + bool IopStdio::dispatchImport(uint16_t ordinal, IopCpuState &cpu) + { + const uint32_t a0 = cpu.gpr[4]; + const uint32_t a1 = cpu.gpr[5]; + const auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + const auto logString = [&](std::string_view prefix, uint32_t address, uint32_t resultBias = 0u) + { + const std::string text = m_memory.readString(address, 2048u); + m_host.log(LogLevel::Info, std::string(prefix) + text); + setV0(static_cast(text.size()) + resultBias); + }; + + switch (ordinal) + { + case 4: // printf + logString("[IOP printf] ", a0); + return true; + case 5: // getchar + case 10: + setV0(0xFFFFFFFFu); + return true; + case 6: // putchar + m_host.log(LogLevel::Info, std::string("[IOP putchar] ") + static_cast(a0 & 0xFFu)); + setV0(a0 & 0xFFu); + return true; + case 7: // puts + logString("[IOP puts] ", a0, 1u); + return true; + case 8: // gets + case 13: + setV0(0u); + return true; + case 9: // fdprintf + logString("[IOP fdprintf] ", a1); + return true; + case 11: + setV0(a0 & 0xFFu); + return true; + case 12: // fdputs + logString("[IOP fdputs] ", a0); + return true; + case 14: // vfdprintf + logString("[IOP vfdprintf] ", a1); + return true; + default: + return false; + } + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_stdio.h b/ps2xIOP/src/emulator/imports/iop_stdio.h new file mode 100644 index 0000000..4c7f9c0 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_stdio.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace ps2x::iop +{ + class IopHost; +} + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopMemory; + + class IopStdio + { + public: + IopStdio(IopHost &host, IopMemory &memory) noexcept; + + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu); + + private: + IopHost &m_host; + IopMemory &m_memory; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_sysclib.cpp b/ps2xIOP/src/emulator/imports/iop_sysclib.cpp new file mode 100644 index 0000000..0517478 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_sysclib.cpp @@ -0,0 +1,336 @@ +#include "iop_sysclib.h" + +#include "../core/iop_cpu.h" +#include "../core/iop_memory.h" + +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + IopSysclib::IopSysclib(IopMemory &memory) noexcept + : m_memory(memory) + { + } + + bool IopSysclib::dispatchImport(uint16_t ordinal, IopCpuState &cpu) + { + const uint32_t a0 = cpu.gpr[4]; + const uint32_t a1 = cpu.gpr[5]; + const uint32_t a2 = cpu.gpr[6]; + const auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + const auto compare = [&](uint32_t lhs, uint32_t rhs, uint32_t count) -> int32_t + { + for (uint32_t i = 0; i < count; ++i) + { + const uint8_t left = m_memory.read8(lhs + i); + const uint8_t right = m_memory.read8(rhs + i); + if (left != right) + return static_cast(left) - static_cast(right); + } + return 0; + }; + const auto copy = [&](uint32_t destination, uint32_t source, uint32_t count) + { + for (uint32_t i = 0; i < count; ++i) + m_memory.write8(destination + i, m_memory.read8(source + i)); + }; + const auto appendString = [&](uint32_t destination, uint32_t source, std::optional maxAppend = std::nullopt) + { + uint32_t destinationOffset = 0; + while (m_memory.read8(destination + destinationOffset) != 0u && destinationOffset < (1u << 20)) + ++destinationOffset; + + uint32_t sourceOffset = 0; + while (sourceOffset < (1u << 20) && (!maxAppend || sourceOffset < *maxAppend)) + { + const uint8_t character = m_memory.read8(source + sourceOffset); + m_memory.write8(destination + destinationOffset + sourceOffset, character); + ++sourceOffset; + if (character == 0u) + return; + } + m_memory.write8(destination + destinationOffset + sourceOffset, 0u); + }; + + switch (ordinal) + { + case 4: // setjmp - enough for callers which only test the initial return. + setV0(0); + return true; + case 5: // longjmp, TODO bc w can do it without the BIOS jmp_buf ABI. + setV0(a1 == 0u ? 1u : a1); + return true; + case 6: + setV0(static_cast(std::toupper(static_cast(a0)))); + return true; + case 7: + setV0(static_cast(std::tolower(static_cast(a0)))); + return true; + case 8: + case 9: // ctype table is optional for most IRXs. + setV0(0); + return true; + case 10: // memchr + for (uint32_t i = 0; i < a2; ++i) + { + if (m_memory.read8(a0 + i) == static_cast(a1)) + { + setV0(a0 + i); + return true; + } + } + setV0(0); + return true; + case 11: + setV0(static_cast(compare(a0, a1, a2))); + return true; + case 12: + copy(a0, a1, a2); + setV0(a0); + return true; + case 13: + { + std::vector temporary(a2); + for (uint32_t i = 0; i < a2; ++i) + temporary[i] = m_memory.read8(a1 + i); + (void)m_memory.writeRam(a0, temporary.data(), temporary.size()); + setV0(a0); + return true; + } + case 14: + for (uint32_t i = 0; i < a2; ++i) + m_memory.write8(a0 + i, static_cast(a1)); + setV0(a0); + return true; + case 15: // bcmp + setV0(static_cast(compare(a0, a1, a2))); + return true; + case 16: // bcopy(src,dst,n) + copy(a1, a0, a2); + setV0(0); + return true; + case 17: + for (uint32_t i = 0; i < a1; ++i) + m_memory.write8(a0 + i, 0u); + setV0(0); + return true; + case 18: // prnt + setV0(0); + return true; + case 19: // sprintf: preserve useful literal formats even before full vararg formatting. + case 42: // vsprintf fallback: copy format literal. + { + const std::string format = m_memory.readString(a1, 4096u); + for (size_t i = 0; i <= format.size(); ++i) + { + m_memory.write8(a0 + static_cast(i), i < format.size() ? static_cast(format[i]) : 0u); + } + setV0(static_cast(format.size())); + return true; + } + case 20: + appendString(a0, a1); + setV0(a0); + return true; + case 21: // strchr + case 25: // index + { + const uint8_t needle = static_cast(a1); + for (uint32_t i = 0; i < (1u << 20); ++i) + { + const uint8_t character = m_memory.read8(a0 + i); + if (character == needle) + { + setV0(a0 + i); + return true; + } + if (character == 0u) + break; + } + setV0(0); + return true; + } + case 22: // strcmp + for (uint32_t i = 0; i < (1u << 20); ++i) + { + const uint8_t left = m_memory.read8(a0 + i); + const uint8_t right = m_memory.read8(a1 + i); + if (left != right) + { + setV0(static_cast(static_cast(left) - static_cast(right))); + return true; + } + if (left == 0u) + break; + } + setV0(0); + return true; + case 23: // strcpy + { + uint32_t i = 0; + for (;; ++i) + { + const uint8_t character = m_memory.read8(a1 + i); + m_memory.write8(a0 + i, character); + if (character == 0u) + break; + } + setV0(a0); + return true; + } + case 24: // strcspn + { + const std::string reject = m_memory.readString(a1, 4096u); + uint32_t count = 0; + for (; count < (1u << 20); ++count) + { + const char character = static_cast(m_memory.read8(a0 + count)); + if (character == 0 || reject.find(character) != std::string::npos) + break; + } + setV0(count); + return true; + } + case 26: // rindex + case 32: // strrchr + { + const uint8_t needle = static_cast(a1); + uint32_t found = 0u; + for (uint32_t i = 0; i < (1u << 20); ++i) + { + const uint8_t character = m_memory.read8(a0 + i); + if (character == needle) + found = a0 + i; + if (character == 0u) + break; + } + setV0(found); + return true; + } + case 27: + setV0(static_cast(m_memory.readString(a0, 1u << 20).size())); + return true; + case 28: + appendString(a0, a1, a2); + setV0(a0); + return true; + case 29: // strncmp + for (uint32_t i = 0; i < a2; ++i) + { + const uint8_t left = m_memory.read8(a0 + i); + const uint8_t right = m_memory.read8(a1 + i); + if (left != right) + { + setV0(static_cast(static_cast(left) - static_cast(right))); + return true; + } + if (left == 0u) + break; + } + setV0(0); + return true; + case 30: // strncpy + { + bool ended = false; + for (uint32_t i = 0; i < a2; ++i) + { + const uint8_t character = ended ? 0u : m_memory.read8(a1 + i); + if (character == 0u) + ended = true; + m_memory.write8(a0 + i, character); + } + setV0(a0); + return true; + } + case 31: // strpbrk + { + const std::string accept = m_memory.readString(a1, 4096u); + for (uint32_t i = 0; i < (1u << 20); ++i) + { + const char character = static_cast(m_memory.read8(a0 + i)); + if (character == 0) + break; + if (accept.find(character) != std::string::npos) + { + setV0(a0 + i); + return true; + } + } + setV0(0); + return true; + } + case 33: // strspn + { + const std::string accept = m_memory.readString(a1, 4096u); + uint32_t count = 0; + for (; count < (1u << 20); ++count) + { + const char character = static_cast(m_memory.read8(a0 + count)); + if (character == 0 || accept.find(character) == std::string::npos) + break; + } + setV0(count); + return true; + } + case 34: // strstr + { + const std::string needle = m_memory.readString(a1, 4096u); + if (needle.empty()) + { + setV0(a0); + return true; + } + const std::string haystack = m_memory.readString(a0, 1u << 20); + const size_t position = haystack.find(needle); + setV0(position == std::string::npos + ? 0u + : a0 + static_cast(position)); + return true; + } + case 35: // strtok state is intentionally not shared across modules yet. + setV0(0); + return true; + case 36: + case 38: // strtol / strtoul + { + const std::string value = m_memory.readString(a0, 4096u); + char *end = nullptr; + const int base = static_cast(a2); + const unsigned long parsed = ordinal == 36 + ? static_cast(std::strtol(value.c_str(), &end, base)) + : std::strtoul(value.c_str(), &end, base); + if (a1 != 0u) + { + m_memory.write32(a1, a0 + static_cast(end - value.c_str())); + } + setV0(static_cast(parsed)); + return true; + } + case 37: // atob + setV0(0); + return true; + case 40: // _wmemcopy, count is 32-bit words + for (uint32_t i = 0; i < a2; ++i) + m_memory.write32(a0 + i * 4u, m_memory.read32(a1 + i * 4u)); + setV0(a0); + return true; + case 41: + for (uint32_t i = 0; i < a2; ++i) + m_memory.write32(a0 + i * 4u, a1); + setV0(a0); + return true; + case 43: + setV0(0); + return true; + default: + return false; + } + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_sysclib.h b/ps2xIOP/src/emulator/imports/iop_sysclib.h new file mode 100644 index 0000000..a462a6e --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_sysclib.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopMemory; + + class IopSysclib + { + public: + explicit IopSysclib(IopMemory &memory) noexcept; + + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu); + + private: + IopMemory &m_memory; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_sysmem.cpp b/ps2xIOP/src/emulator/imports/iop_sysmem.cpp new file mode 100644 index 0000000..2c8d8f7 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_sysmem.cpp @@ -0,0 +1,72 @@ +#include "iop_sysmem.h" + +#include "../core/iop_cpu.h" +#include "../core/iop_memory.h" +#include "ps2x/iop/iop_host.h" + +#include + +namespace ps2x::iop::detail +{ + IopSysmem::IopSysmem(IopHost &host, IopMemory &memory) noexcept + : m_host(host), m_memory(memory) + { + } + + bool IopSysmem::dispatchImport(uint16_t ordinal, IopCpuState &cpu) + { + const uint32_t a0 = cpu.gpr[4]; + const uint32_t a1 = cpu.gpr[5]; + const uint32_t a2 = cpu.gpr[6]; + const auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + + switch (ordinal) + { + case 4: // AllocSysMemory + { + const uint32_t address = a0 == 2u + ? m_memory.allocate(a1, 16u, a2) + : m_memory.allocate(a1, 16u); + setV0(address); + return true; + } + case 5: // FreeSysMemory + setV0(m_memory.freeAllocation(a0) ? 0u : 0xFFFFFFFFu); + return true; + case 6: // QueryMemSize + setV0(IopMemory::RamSize); + return true; + case 7: // QueryMaxFreeMemSize + case 8: // QueryTotalFreeMemSize + setV0(m_memory.maxFreeMemory()); + return true; + case 9: // QueryBlockTopAddress + if (const auto block = m_memory.allocationContaining(a0)) + setV0(block->address); + else + setV0(0u); + return true; + case 10: // QueryBlockSize + if (const auto block = m_memory.allocationContaining(a0)) + setV0(block->size); + else + setV0(0xFFFFFFFFu); + return true; + case 14: // Kprintf + { + const std::string format = m_memory.readString(a0, 512u); + m_host.log(LogLevel::Info, std::string("[IOP Kprintf] ") + format); + setV0(static_cast(format.size())); + return true; + } + case 15: + setV0(0u); + return true; + default: + return false; + } + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_sysmem.h b/ps2xIOP/src/emulator/imports/iop_sysmem.h new file mode 100644 index 0000000..95d9291 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_sysmem.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +namespace ps2x::iop +{ + class IopHost; +} + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopMemory; + + class IopSysmem + { + public: + IopSysmem(IopHost &host, IopMemory &memory) noexcept; + + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu); + + private: + IopHost &m_host; + IopMemory &m_memory; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_timrman.cpp b/ps2xIOP/src/emulator/imports/iop_timrman.cpp new file mode 100644 index 0000000..3d80897 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_timrman.cpp @@ -0,0 +1,476 @@ +#include "iop_timrman.h" + +#include "../core/iop_cpu.h" +#include "../services/iop_rpc.h" + +#include +#include +#include + +namespace ps2x::iop::detail +{ + namespace + { + constexpr int32_t kNoTimer = -150; + constexpr int32_t kIllegalTimerId = -151; + constexpr int32_t kIllegalSource = -152; + constexpr int32_t kIllegalPrescale = -153; + constexpr int32_t kTimerBusy = -154; + constexpr int32_t kTimerNotConfigured = -155; + constexpr int32_t kTimerNotRunning = -156; + constexpr int32_t kIllegalMode = -405; + constexpr uint64_t kIopClockHz = 36'864'000ull; + constexpr uint64_t kPixelClockHz = 13'500'000ull; + constexpr uint64_t kHlineClockHz = 15'734ull; + + constexpr std::array kAllocationOrder{2u, 5u, 4u, 3u, 0u, 1u}; + constexpr std::array kAddresses{ + 0xBF801100u, + 0xBF801110u, + 0xBF801120u, + 0xBF801480u, + 0xBF801490u, + 0xBF8014A0u, + }; + constexpr std::array kSources{0x0Bu, 0x0Du, 0x01u, 0x05u, 0x01u, 0x01u}; + constexpr std::array kWidths{16u, 16u, 16u, 32u, 32u, 32u}; + constexpr std::array kMaxPrescales{1u, 1u, 8u, 1u, 256u, 256u}; + constexpr std::array kIrqs{4u, 5u, 6u, 14u, 15u, 16u}; + + uint32_t errorValue(int32_t error) noexcept + { + return static_cast(error); + } + } + + void IopTimrman::reset() noexcept + { + for (size_t i = 0u; i < m_timers.size(); ++i) + { + m_timers[i] = {}; + m_timers[i].address = kAddresses[i]; + m_timers[i].sources = kSources[i]; + m_timers[i].width = kWidths[i]; + m_timers[i].maxPrescale = kMaxPrescales[i]; + m_timers[i].irq = kIrqs[i]; + } + m_holdMode = 0u; + m_servicing = false; + } + + uint32_t IopTimrman::timerId(size_t index) noexcept + { + return (static_cast(index + 1u) << 28u) | (kAddresses[index] >> 4u); + } + + IopTimrman::Timer *IopTimrman::timerFromId(uint32_t id) noexcept + { + const uint32_t encoded = id >> 28u; + if (encoded == 0u || encoded > m_timers.size()) + return nullptr; + Timer &timer = m_timers[encoded - 1u]; + return timer.users != 0u && (id & 0x0FFFFFFFu) == (timer.address >> 4u) + ? &timer + : nullptr; + } + + const IopTimrman::Timer *IopTimrman::timerFromId(uint32_t id) const noexcept + { + return const_cast(this)->timerFromId(id); + } + + uint64_t IopTimrman::ticksToCycles(const Timer &timer, uint64_t ticks) noexcept + { + const uint64_t prescale = std::max(timer.prescale, 1u); + const uint64_t sourceHz = timer.source == 2u + ? kPixelClockHz + : (timer.source == 4u ? kHlineClockHz : kIopClockHz); + if (ticks == 0u) + ticks = timer.width == 16u ? (1ull << 16u) : (1ull << 32u); + const unsigned long long scaled = ticks * prescale; + if (sourceHz == kIopClockHz) + return std::max(scaled, 1u); + const uint64_t whole = (scaled / sourceHz) * kIopClockHz; + const uint64_t remainder = scaled % sourceHz; + return std::max(1u, whole + (remainder * kIopClockHz + sourceHz - 1u) / sourceHz); + } + + uint64_t IopTimrman::elapsedTicks(const Timer &timer, uint64_t currentCycle) noexcept + { + if (!timer.running || currentCycle <= timer.counterBaseCycle) + return 0u; + const uint64_t elapsed = currentCycle - timer.counterBaseCycle; + const uint64_t sourceHz = timer.source == 2u + ? kPixelClockHz + : (timer.source == 4u ? kHlineClockHz : kIopClockHz); + return (elapsed * sourceHz) / (kIopClockHz * std::max(timer.prescale, 1u)); + } + + uint32_t IopTimrman::counterValue(const Timer &timer, uint64_t currentCycle) noexcept + { + const uint64_t value = static_cast(timer.counterBase) + elapsedTicks(timer, currentCycle); + return timer.width == 16u ? static_cast(value & 0xFFFFu) : static_cast(value); + } + + void IopTimrman::schedule(Timer &timer, uint64_t currentCycle) noexcept + { + timer.compareCycle = UINT64_MAX; + timer.overflowCycle = UINT64_MAX; + if (!timer.running) + return; + + const uint64_t current = counterValue(timer, currentCycle); + timer.counterBase = static_cast(current); + timer.counterBaseCycle = currentCycle; + + if (timer.compareCallback.function != 0u) + { + const uint64_t modulus = timer.width == 16u ? (1ull << 16u) : (1ull << 32u); + const uint64_t compare = timer.width == 16u ? (timer.compare & 0xFFFFu) : timer.compare; + uint64_t delta = (compare + modulus - current) % modulus; + if (delta == 0u) + delta = modulus; + timer.compareCycle = currentCycle + ticksToCycles(timer, delta); + } + + if (timer.overflowCallback.function != 0u) + { + const uint64_t modulus = timer.width == 16u ? (1ull << 16u) : (1ull << 32u); + uint64_t delta = modulus - current; + if (delta == 0u) + delta = modulus; + timer.overflowCycle = currentCycle + ticksToCycles(timer, delta); + } + } + + void IopTimrman::stop(Timer &timer, uint64_t currentCycle) noexcept + { + timer.counterBase = counterValue(timer, currentCycle); + timer.counterBaseCycle = currentCycle; + timer.running = false; + timer.liveMode = 0u; + timer.compareCycle = UINT64_MAX; + timer.overflowCycle = UINT64_MAX; + } + + bool IopTimrman::dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle) + { + const uint32_t a0 = cpu.gpr[4]; + const uint32_t a1 = cpu.gpr[5]; + const uint32_t a2 = cpu.gpr[6]; + const uint32_t a3 = cpu.gpr[7]; + const auto setV0 = [&](uint32_t value) { cpu.gpr[2] = value; }; + + switch (ordinal) + { + case 3: // GetTimersTable + setV0(0u); + return true; + case 4: // AllocHardTimer + for (const size_t index : kAllocationOrder) + { + Timer &timer = m_timers[index]; + if (timer.users != 0u || (timer.sources & a0) == 0u || timer.width != a1 || timer.maxPrescale < a2) + continue; + timer.users = 1u; + timer.source = a0; + timer.prescale = std::max(a2, 1u); + timer.counterBaseCycle = currentCycle; + setV0(timerId(index)); + return true; + } + setV0(errorValue(kNoTimer)); + return true; + case 5: // ReferHardTimer + for (size_t index = 0u; index < m_timers.size(); ++index) + { + Timer &timer = m_timers[index]; + if (timer.users == 0u || timer.liveMode == 0u || (timer.sources & a0) == 0u || + timer.width != a1 || (timer.liveMode & a3) != a2) + continue; + ++timer.users; + setV0(timerId(index)); + return true; + } + setV0(errorValue(kNoTimer)); + return true; + case 6: // FreeHardTimer + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + if (--timer->users == 0u) + { + const uint32_t address = timer->address; + const uint8_t sources = timer->sources; + const uint8_t width = timer->width; + const uint16_t maxPrescale = timer->maxPrescale; + const uint8_t irq = timer->irq; + *timer = {}; + timer->address = address; + timer->sources = sources; + timer->width = width; + timer->maxPrescale = maxPrescale; + timer->irq = irq; + } + setV0(0u); + return true; + } + case 7: // SetTimerMode + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + if (a1 == 0u) + stop(*timer, currentCycle); + else + { + timer->liveMode = a1; + timer->running = true; + timer->counterBaseCycle = currentCycle; + schedule(*timer, currentCycle); + } + setV0(0u); + return true; + } + case 8: // GetTimerStatus + case 17: // GetTimerMode + { + const Timer *timer = timerFromId(a0); + setV0(timer ? timer->liveMode : errorValue(kIllegalTimerId)); + return true; + } + case 9: // SetTimerCounter + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + timer->counterBase = timer->width == 16u ? (a1 & 0xFFFFu) : a1; + timer->counterBaseCycle = currentCycle; + schedule(*timer, currentCycle); + setV0(0u); + return true; + } + case 10: // GetTimerCounter + { + const Timer *timer = timerFromId(a0); + setV0(timer ? counterValue(*timer, currentCycle) : errorValue(kIllegalTimerId)); + return true; + } + case 11: // SetTimerCompare + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + timer->compare = timer->width == 16u ? (a1 & 0xFFFFu) : a1; + schedule(*timer, currentCycle); + setV0(0u); + return true; + } + case 12: // GetTimerCompare + { + const Timer *timer = timerFromId(a0); + setV0(timer ? timer->compare : errorValue(kIllegalTimerId)); + return true; + } + case 13: // SetHoldMode + m_holdMode = (m_holdMode & ~(0xFu << ((a0 & 7u) * 4u))) | ((a1 & 0xFu) << ((a0 & 7u) * 4u)); + setV0(0u); + return true; + case 14: // GetHoldMode + setV0((m_holdMode >> ((a0 & 7u) * 4u)) & 0xFu); + return true; + case 15: // GetHoldReg + setV0(0u); + return true; + case 16: // GetHardTimerIntrCode + { + const Timer *timer = timerFromId(a0); + setV0(timer ? timer->irq : errorValue(kIllegalTimerId)); + return true; + } + case 18: // GetTimerReadFunc + // Returning a host-side register reader as a guest function is not meaningful. + setV0(0u); + return true; + case 20: // SetTimerHandler + case 21: // SetOverflowHandler + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + if (timer->running) + { + setV0(errorValue(kTimerNotRunning)); + return true; + } + if (ordinal == 20u) + { + timer->compare = timer->width == 16u ? (a1 & 0xFFFFu) : a1; + timer->compareCallback = {a2, a3, cpu.gpr[28]}; + } + else + { + timer->overflowCallback = {a1, a2, cpu.gpr[28]}; + } + setV0(0u); + return true; + } + case 22: // SetupHardTimer + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + if (timer->running) + { + setV0(errorValue(kTimerBusy)); + return true; + } + if ((a2 != 0u && a2 != 1u && a2 != 3u && a2 != 5u && a2 != 7u)) + { + setV0(errorValue(kIllegalMode)); + return true; + } + if ((timer->sources & a1) == 0u) + { + setV0(errorValue(kIllegalSource)); + return true; + } + if (a3 == 0u || a3 > timer->maxPrescale) + { + setV0(errorValue(kIllegalPrescale)); + return true; + } + timer->source = a1; + timer->setupMode = a2; + timer->prescale = a3; + timer->configured = true; + setV0(0u); + return true; + } + case 23: // StartHardTimer + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + if (timer->running) + { + setV0(errorValue(kTimerBusy)); + return true; + } + if (!timer->configured) + { + setV0(errorValue(kTimerNotConfigured)); + return true; + } + timer->counterBase = 0u; + timer->counterBaseCycle = currentCycle; + timer->liveMode = 0x80000000u | timer->setupMode; + timer->running = true; + schedule(*timer, currentCycle); + setV0(0u); + return true; + } + case 24: // StopHardTimer + { + Timer *timer = timerFromId(a0); + if (!timer) + { + setV0(errorValue(kIllegalTimerId)); + return true; + } + if (!timer->running) + { + setV0(errorValue(kTimerNotRunning)); + return true; + } + stop(*timer, currentCycle); + setV0(0u); + return true; + } + default: + return false; + } + } + + void IopTimrman::serviceDue(uint64_t currentCycle, IopGuestExecutor &executor) + { + if (m_servicing) + return; + m_servicing = true; + struct ServiceGuard + { + bool &flag; + ~ServiceGuard() { flag = false; } + } guard{m_servicing}; + + for (Timer &timer : m_timers) + { + if (!timer.running) + continue; + + const bool compareDue = timer.compareCycle <= currentCycle; + const bool overflowDue = timer.overflowCycle <= currentCycle; + if (!compareDue && !overflowDue) + continue; + + const Callback callback = compareDue ? timer.compareCallback : timer.overflowCallback; + timer.compareCycle = UINT64_MAX; + timer.overflowCycle = UINT64_MAX; + const uint32_t result = callback.function != 0u + ? executor.executeGuestFunctionWithBudget(callback.function, + callback.common, + 0u, + 0u, + 0u, + callback.gp, + 100000u) + : 0u; + if (!timer.running) + continue; + if (result == 0u) + { + stop(timer, currentCycle); + continue; + } + if (compareDue) + timer.compare = timer.width == 16u ? (result & 0xFFFFu) : result; + timer.counterBase = 0u; + timer.counterBaseCycle = currentCycle; + schedule(timer, currentCycle); + } + + } + + uint64_t IopTimrman::nextEventCycle(uint64_t fallback) const noexcept + { + uint64_t next = fallback; + for (const Timer &timer : m_timers) + { + next = std::min(next, timer.compareCycle); + next = std::min(next, timer.overflowCycle); + } + return next; + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_timrman.h b/ps2xIOP/src/emulator/imports/iop_timrman.h new file mode 100644 index 0000000..11b53b6 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_timrman.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include +#include + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopGuestExecutor; + + class IopTimrman + { + public: + void reset() noexcept; + + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle); + void serviceDue(uint64_t currentCycle, IopGuestExecutor &executor); + [[nodiscard]] uint64_t nextEventCycle(uint64_t fallback) const noexcept; + + private: + struct Callback + { + uint32_t function = 0u; + uint32_t common = 0u; + uint32_t gp = 0u; + }; + + struct Timer + { + uint32_t address = 0u; + uint8_t sources = 0u; + uint8_t width = 0u; + uint16_t maxPrescale = 0u; + uint8_t irq = 0u; + uint8_t users = 0u; + + uint32_t source = 1u; + uint32_t prescale = 1u; + uint32_t setupMode = 0u; + uint32_t liveMode = 0u; + uint32_t counterBase = 0u; + uint32_t compare = 0u; + uint64_t counterBaseCycle = 0u; + uint64_t compareCycle = UINT64_MAX; + uint64_t overflowCycle = UINT64_MAX; + bool configured = false; + bool running = false; + + Callback compareCallback; + Callback overflowCallback; + }; + + [[nodiscard]] Timer *timerFromId(uint32_t timerId) noexcept; + [[nodiscard]] const Timer *timerFromId(uint32_t timerId) const noexcept; + [[nodiscard]] static uint32_t timerId(size_t index) noexcept; + [[nodiscard]] static uint64_t ticksToCycles(const Timer &timer, uint64_t ticks) noexcept; + [[nodiscard]] static uint64_t elapsedTicks(const Timer &timer, uint64_t currentCycle) noexcept; + [[nodiscard]] static uint32_t counterValue(const Timer &timer, uint64_t currentCycle) noexcept; + static void schedule(Timer &timer, uint64_t currentCycle) noexcept; + static void stop(Timer &timer, uint64_t currentCycle) noexcept; + + std::array m_timers{}; + uint32_t m_holdMode = 0u; + bool m_servicing = false; + }; +} diff --git a/ps2xIOP/src/emulator/imports/iop_vblank.cpp b/ps2xIOP/src/emulator/imports/iop_vblank.cpp new file mode 100644 index 0000000..c3bcf33 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_vblank.cpp @@ -0,0 +1,42 @@ +#include "iop_vblank.h" + +#include "../core/iop_cpu.h" +#include "../iop_emulator_const.h" +#include "../core/iop_kernel.h" + +namespace ps2x::iop::detail +{ + IopVblank::IopVblank(IopKernel &kernel) noexcept + : m_kernel(kernel) + { + } + + bool IopVblank::dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle) + { + switch (ordinal) + { + case 4: // WaitVblankStart + case 5: // WaitVblankEnd + case 6: // WaitVblank + case 7: // WaitNonVblank + { + const bool waitForEnd = ordinal == 5u || ordinal == 7u; + const uint64_t phase = waitForEnd ? kVblankEndPhaseCycles : 0u; + const uint64_t fieldStart = currentCycle - (currentCycle % kVblankPeriodCycles); + uint64_t wakeCycle = fieldStart + phase; + if (wakeCycle <= currentCycle) + wakeCycle += kVblankPeriodCycles; + m_kernel.delayCurrentUntil(wakeCycle, cpu); + cpu.gpr[2] = 0u; + return true; + } + case 8: // RegisterVblankHandler + case 9: // ReleaseVblankHandler + // Callback delivery is not required by the scheduler wait ABI yet. + cpu.gpr[2] = 0u; + return true; + default: + return false; + } + } +} diff --git a/ps2xIOP/src/emulator/imports/iop_vblank.h b/ps2xIOP/src/emulator/imports/iop_vblank.h new file mode 100644 index 0000000..1cabae7 --- /dev/null +++ b/ps2xIOP/src/emulator/imports/iop_vblank.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopKernel; + + class IopVblank + { + public: + explicit IopVblank(IopKernel &kernel) noexcept; + + [[nodiscard]] bool dispatchImport(uint16_t ordinal, IopCpuState &cpu, uint64_t currentCycle); + + private: + IopKernel &m_kernel; + }; +} diff --git a/ps2xIOP/src/emulator/iop_emulator.cpp b/ps2xIOP/src/emulator/iop_emulator.cpp new file mode 100644 index 0000000..e5f63a9 --- /dev/null +++ b/ps2xIOP/src/emulator/iop_emulator.cpp @@ -0,0 +1,830 @@ +#include "iop_emulator.h" +#include "imports/iop_cdvd.h" +#include "core/iop_cpu.h" +#include "imports/iop_heaplib.h" +#include "imports/iop_imports.h" +#include "imports/iop_intrman.h" +#include "imports/iop_ioman.h" +#include "core/iop_kernel.h" +#include "imports/iop_loadcore.h" +#include "core/iop_memory.h" +#include "services/iop_module_loader.h" +#include "services/iop_rpc.h" +#include "imports/iop_stdio.h" +#include "imports/iop_sysclib.h" +#include "imports/iop_sysmem.h" +#include "imports/iop_timrman.h" +#include "imports/iop_vblank.h" +#include "iop_emulator_const.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + namespace + { + constexpr uint32_t kRamSize = IopMemory::RamSize; + constexpr uint32_t kKernelHeapBase = IopMemory::HeapBase; + constexpr uint32_t kKernelHeapLimit = IopMemory::HeapLimit; + constexpr uint32_t kCallStackBase = kKernelHeapLimit; + constexpr uint32_t kCallStackLimit = 0x001FFF00u; + constexpr uint32_t kCallStackSize = 0x2000u; + constexpr uint32_t kCallStackCapacity = (kCallStackLimit - kCallStackBase) / kCallStackSize; + constexpr uint64_t kCdvdCompletionCycles = 128u; + + uint32_t physicalAddress(uint32_t address) + { + return IopMemory::physicalAddress(address); + } + + int32_t sign16(uint32_t value) + { + return static_cast(value & 0xFFFFu); + } + + bool iequals(std::string_view lhs, std::string_view rhs) + { + if (lhs.size() != rhs.size()) + return false; + for (size_t i = 0; i < lhs.size(); ++i) + { + if (std::tolower(static_cast(lhs[i])) != + std::tolower(static_cast(rhs[i]))) + return false; + } + return true; + } + + } + + class IopEmulator::Impl final : public IopGuestExecutor + { + public: + using CpuState = IopCpuState; + + struct Module + { + int id = 0; + std::string path; + std::string name; + uint32_t base = 0; + uint32_t size = 0; + uint32_t entry = 0; + uint32_t gp = 0; + bool resident = false; + }; + + struct GuestCallback + { + uint32_t function = 0; + uint32_t gp = 0; + }; + + struct ScheduledGuestCallback + { + uint32_t function = 0u; + uint32_t gp = 0u; + uint32_t argument = 0u; + }; + + explicit Impl(IopHost &hostRef) + : host(hostRef), + sysmem(host, memory), + kernel(memory), + cdvd(host, memory, kernel), + vblank(kernel), + rpc(host, memory, kernel), + sysclib(memory), + stdio(host, memory), + heaplib(memory), + intrman(memory), + timrman(), + ioman(memory), + cpuCore(memory), + imports(memory), + loadcore(memory, imports) + { + reset(); + } + + void reset() + { + memory.reset(); + kernel.reset(); + modules.clear(); + imports.reset(); + rpc.reset(); + cdvd.reset(); + intrman.reset(); + timrman.reset(); + ioman.reset(); + pendingDmaInterrupts.clear(); + pendingGuestCallbacks.clear(); + nextModuleId = 1; + moduleCursor = kModuleLoadBase; + totalCycles = 0; + totalInstructions = 0; + eeCycleCarry = 0; + activeCpu = nullptr; + lastError.clear(); + servicingDmaInterrupts = false; + servicingGuestCallbacks = false; + callDepth = 0u; + secrMcCommandHandler = {}; + secrMcDevIdHandler = {}; + checkKelfPathCallback = {}; + } + + uint8_t read8(uint32_t address) const + { + return memory.read8(address); + } + + uint16_t read16(uint32_t address) const + { + return memory.read16(address); + } + + uint32_t read32(uint32_t address) const + { + return memory.read32(address); + } + + void write8(uint32_t address, uint8_t value) + { + memory.write8(address, value); + schedulePendingDma(); + } + + void write16(uint32_t address, uint16_t value) + { + memory.write16(address, value); + schedulePendingDma(); + } + + void write32(uint32_t address, uint32_t value) + { + memory.write32(address, value); + schedulePendingDma(); + } + + void schedulePendingDma() + { + if (const auto dma = memory.takeDmaStart()) + pendingDmaInterrupts[dma->irq] = totalCycles + dma->delayCycles; + } + + bool readRam(uint32_t address, void *destination, size_t size) const + { + return memory.readRam(address, destination, size); + } + + bool writeRam(uint32_t address, const void *source, size_t size) + { + return memory.writeRam(address, source, size); + } + + bool zeroRam(uint32_t address, size_t size) + { + return memory.zeroRam(address, size); + } + + bool isHardwareAddress(uint32_t phys) const + { + return memory.isHardwareAddress(phys); + } + + uint32_t allocate(uint32_t size, uint32_t alignment = 16u, std::optional fixed = std::nullopt) + { + return memory.allocate(size, alignment, fixed); + } + + bool freeAllocation(uint32_t address) + { + return memory.freeAllocation(address); + } + + void log(LogLevel level, std::string_view text) + { + host.log(level, text); + } + + bool checkInterrupt(CpuState &cpu) + { + const uint32_t status = cpu.cop0[12]; + if ((status & 1u) == 0u) + return false; + if ((status & 0x2u) != 0u) + return false; + const bool pending = memory.interruptControl() != 0u && (memory.interruptStatus() & memory.interruptMask()) != 0u; + if (!pending) + return false; + cpu.cop0[13] |= 0x400u; + cpuCore.raiseException(cpu, 0u, cpu.pc, false); + return true; + } + + enum class ImportDisposition + { + Handled, + JumpToGuest, + Missing, + }; + + ImportDisposition dispatchImport(const IopImportCall &call, CpuState &cpu) + { + const uint32_t a0 = cpu.gpr[4]; + auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + + if (iequals(call.library, "sysmem") && sysmem.dispatchImport(call.ordinal, cpu)) + return ImportDisposition::Handled; + + if (iequals(call.library, "cdvdman") && cdvd.dispatchImport(call.ordinal, cpu)) + { + if (const auto callback = cdvd.takeCompletionCallback()) + { + pendingGuestCallbacks.emplace( + totalCycles + kCdvdCompletionCycles, + ScheduledGuestCallback{ + callback->address, + callback->gp, + callback->reason, + }); + } + return ImportDisposition::Handled; + } + + if (iequals(call.library, "loadcore") && loadcore.dispatchImport(call.ordinal, cpu)) + return ImportDisposition::Handled; + + if (iequals(call.library, "thbase") || iequals(call.library, "threadman")) + { + return kernel.dispatchThreadImport(call.ordinal, cpu, totalCycles) + ? ImportDisposition::Handled + : ImportDisposition::Missing; + } + if (iequals(call.library, "thsemap")) + { + return kernel.dispatchSemaphoreImport(call.ordinal, cpu) + ? ImportDisposition::Handled + : ImportDisposition::Missing; + } + if (iequals(call.library, "thevent")) + { + return kernel.dispatchEventImport(call.ordinal, cpu) + ? ImportDisposition::Handled + : ImportDisposition::Missing; + } + if (iequals(call.library, "sifcmd")) + { + return rpc.dispatchSifCmdImport(call.ordinal, cpu) + ? ImportDisposition::Handled + : ImportDisposition::Missing; + } + if (iequals(call.library, "intrman") && intrman.dispatchImport(call.ordinal, cpu, *this)) + return ImportDisposition::Handled; + if (iequals(call.library, "secrman")) + { + switch (call.ordinal) + { + case 4: // SecrSetMcCommandHandler + secrMcCommandHandler = {a0, cpu.gpr[28]}; + setV0(0); + return ImportDisposition::Handled; + case 5: // SecrSetMcDevIDHandler + secrMcDevIdHandler = {a0, cpu.gpr[28]}; + setV0(0); + return ImportDisposition::Handled; + default: + break; + } + } + if (iequals(call.library, "modload") && call.ordinal == 13u) + { + checkKelfPathCallback = {a0, cpu.gpr[28]}; + setV0(0); + return ImportDisposition::Handled; + } + if (iequals(call.library, "ioman") && ioman.dispatchImport(call.ordinal, cpu, *this)) + return ImportDisposition::Handled; + if (iequals(call.library, "sifman")) + { + return rpc.dispatchSifManImport(call.ordinal, cpu) + ? ImportDisposition::Handled + : ImportDisposition::Missing; + } + if (iequals(call.library, "vblank") && vblank.dispatchImport(call.ordinal, cpu, totalCycles)) + return ImportDisposition::Handled; + if (iequals(call.library, "timrman") && timrman.dispatchImport(call.ordinal, cpu, totalCycles)) + return ImportDisposition::Handled; + if (iequals(call.library, "dmacman")) + { + setV0(0); + return ImportDisposition::Handled; + } + if (iequals(call.library, "stdio") && stdio.dispatchImport(call.ordinal, cpu)) + return ImportDisposition::Handled; + if (iequals(call.library, "sysclib")) + { + return sysclib.dispatchImport(call.ordinal, cpu) + ? ImportDisposition::Handled + : ImportDisposition::Missing; + } + if (iequals(call.library, "heaplib") && heaplib.dispatchImport(call.ordinal, cpu)) + return ImportDisposition::Handled; + + const uint32_t target = imports.resolve(call.library, call.ordinal, call.version); + if (target != 0u) + { + cpu.pc = target; + cpu.branchPending = false; + return ImportDisposition::JumpToGuest; + } + + std::ostringstream out; + out << "[IOP] unhandled import " << call.library << ':' << call.ordinal + << " version=0x" << std::hex << call.version << " pc=0x" << cpu.pc; + log(LogLevel::Warning, out.str()); + setV0(0); + return ImportDisposition::Missing; + } + + bool step(CpuState &cpu) + { + if (cpu.stopped) + return false; + if (cpu.pc == kThreadReturnSentinel || cpu.pc == kCallReturnSentinel) + { + cpu.stopped = true; + return false; + } + if (physicalAddress(cpu.pc) >= kRamSize) + { + std::ostringstream out; + out << "[IOP] execution outside RAM pc=0x" << std::hex << cpu.pc; + log(LogLevel::Error, out.str()); + cpu.stopped = true; + return false; + } + if (checkInterrupt(cpu)) + return true; + + if (const auto import = imports.decode(cpu.pc)) + { + const ImportDisposition disposition = dispatchImport(*import, cpu); + ++totalInstructions; + ++totalCycles; + if (disposition == ImportDisposition::JumpToGuest) + return true; + cpu.pc = cpu.gpr[31]; + cpu.branchPending = false; + return !cpu.stopped; + } + + const bool running = cpuCore.executeInstruction(cpu); + schedulePendingDma(); + ++totalInstructions; + ++totalCycles; + return running; + } + + uint32_t runCpu(CpuState &cpu, uint32_t instructionBudget) + { + CpuState *previous = activeCpu; + activeCpu = &cpu; + const uint64_t start = totalInstructions; + while (!cpu.stopped && !cpu.yielded && totalInstructions - start < instructionBudget) + { + if (!step(cpu)) + break; + if (!servicingDmaInterrupts && !pendingDmaInterrupts.empty()) + servicePendingDmaInterrupts(); + if (!servicingGuestCallbacks && !pendingGuestCallbacks.empty()) + servicePendingGuestCallbacks(); + } + activeCpu = previous; + return static_cast(totalInstructions - start); + } + + uint32_t callFunction(uint32_t address, + uint32_t a0, + uint32_t a1, + uint32_t a2, + uint32_t a3, + uint32_t gp, + uint32_t budget = kMaxCallInstructions) + { + struct CallDepthGuard + { + uint32_t &depth; + ~CallDepthGuard() { --depth; } + }; + + const uint32_t depth = callDepth++; + const CallDepthGuard depthGuard{callDepth}; + CpuState cpu{}; + cpu.pc = address; + cpu.gpr[4] = a0; + cpu.gpr[5] = a1; + cpu.gpr[6] = a2; + cpu.gpr[7] = a3; + cpu.gpr[28] = gp; + if (depth < kCallStackCapacity) + { + const uint32_t stackTop = kCallStackLimit - depth * kCallStackSize; + cpu.gpr[29] = stackTop - 32u; + } + else if (activeCpu && activeCpu->gpr[29] > kCallStackBase + kStackGuardBytes) + { + // Extremely deep re-entrancy borrows unused space below the + // suspended caller's live frame. Stack growth remains away + // from the caller, so its saved registers stay intact. + cpu.gpr[29] = (activeCpu->gpr[29] - kStackGuardBytes) & ~15u; + } + else + { + cpu.gpr[29] = kCallStackBase - 32u; + } + cpu.gpr[31] = kCallReturnSentinel; + runCpu(cpu, budget); + return cpu.gpr[2]; + } + + uint32_t executeGuestFunction(uint32_t address, + uint32_t a0, + uint32_t a1, + uint32_t a2, + uint32_t a3, + uint32_t gp) override + { + return callFunction(address, a0, a1, a2, a3, gp); + } + + uint32_t executeGuestFunctionWithBudget(uint32_t address, + uint32_t a0, + uint32_t a1, + uint32_t a2, + uint32_t a3, + uint32_t gp, + uint32_t instructionBudget) override + { + return callFunction(address, a0, a1, a2, a3, gp, instructionBudget); + } + + // Not that good to use exception handling for control flow but will do for now + void servicePendingDmaInterrupts() + { + if (servicingDmaInterrupts || pendingDmaInterrupts.empty()) + return; + + servicingDmaInterrupts = true; + + std::vector completed; + for (auto it = pendingDmaInterrupts.begin(); it != pendingDmaInterrupts.end();) + { + if (it->second > totalCycles) + { + ++it; + continue; + } + completed.push_back(it->first); + it = pendingDmaInterrupts.erase(it); + } + try + { + for (const int irq : completed) + (void)intrman.dispatchInterrupt(irq, *this); + } + catch (...) + { + servicingDmaInterrupts = false; + throw; + } + servicingDmaInterrupts = false; + } + + void servicePendingGuestCallbacks() + { + if (servicingGuestCallbacks || pendingGuestCallbacks.empty()) + return; + + std::vector callbacks; + for (auto it = pendingGuestCallbacks.begin(); it != pendingGuestCallbacks.end();) + { + if (it->first > totalCycles) + break; + callbacks.push_back(it->second); + it = pendingGuestCallbacks.erase(it); + } + if (callbacks.empty()) + return; + + servicingGuestCallbacks = true; + try + { + for (const ScheduledGuestCallback &callback : callbacks) + { + if (callback.function != 0u) + { + (void)callFunction(callback.function, + callback.argument, + 0u, + 0u, + 0u, + callback.gp, + 100000u); + } + } + } + catch (...) + { + servicingGuestCallbacks = false; + throw; + } + servicingGuestCallbacks = false; + } + + void runCycles(uint64_t cycles) noexcept + { + try + { + const uint64_t target = totalCycles + cycles; + while (totalCycles < target) + { + servicePendingDmaInterrupts(); + servicePendingGuestCallbacks(); + timrman.serviceDue(totalCycles, *this); + IopThread *next = kernel.beginNextReady(totalCycles); + if (!next) + { + uint64_t nextWake = kernel.nextWakeCycle(target); + for (const auto &[irq, completionCycle] : pendingDmaInterrupts) + nextWake = std::min(nextWake, completionCycle); + if (!pendingGuestCallbacks.empty()) + nextWake = std::min(nextWake, pendingGuestCallbacks.begin()->first); + nextWake = timrman.nextEventCycle(nextWake); + totalCycles = std::max(totalCycles + 1u, std::min(target, nextWake)); + continue; + } + const uint64_t before = totalCycles; + runCpu(next->cpu, static_cast(std::min(kDefaultSlice, target - totalCycles))); + kernel.endTimeslice(*next, kThreadReturnSentinel); + if (totalCycles == before) + ++totalCycles; + } + } + catch (...) + { + // Runtime scheduling must never throw through EeScheduler::accountCycles(). + } + } + + ModuleLoadResult loadImage(std::string path, std::span image, const void *arguments, uint32_t argumentSize) + { + ModuleLoadResult result{true, -1, -1}; + const IopImageLoadResult loaded = IopModuleLoader::load(image, memory, moduleCursor); + moduleCursor = loaded.nextModuleCursor; + if (!loaded) + { + if (loaded.error == IopImageLoadError::InvalidElf) + log(LogLevel::Error, "[IOP] rejected invalid/non-MIPS IRX ELF"); + else if (loaded.error == IopImageLoadError::ArenaExhausted) + log(LogLevel::Error, "[IOP] module arena exhausted"); + return result; + } + if (!loaded.relocationsComplete) + log(LogLevel::Warning, "[IOP] one or more IRX relocations were unsupported"); + + Module module; + module.id = nextModuleId++; + module.path = std::move(path); + const size_t slash = module.path.find_last_of("/\\:"); + module.name = slash == std::string::npos ? module.path : module.path.substr(slash + 1u); + module.base = loaded.base; + module.size = loaded.size; + module.entry = loaded.entry; + module.gp = loaded.gp; + + uint32_t args = 0u; + if (arguments && argumentSize) + { + args = allocate(argumentSize + 1u, 16u); + if (args) + { + writeRam(args, arguments, argumentSize); + write8(args + argumentSize, 0u); + } + } + const uint32_t startResult = callFunction(module.entry, argumentSize, args, 0u, 0u, module.gp); + if (args) + freeAllocation(args); + module.resident = startResult == 0u || startResult == 2u; + result.moduleId = module.id; + result.startResult = static_cast(startResult); + modules[module.id] = std::move(module); + + std::ostringstream out; + out << "[IOP] loaded IRX id=" << result.moduleId + << " entry=0x" << std::hex << modules[result.moduleId].entry + << " base=0x" << modules[result.moduleId].base + << " start=" << std::dec << result.startResult; + log(LogLevel::Info, out.str()); + return result; + } + + ModuleLoadResult loadModule(std::string_view path, const void *arguments, uint32_t argumentSize) + { + std::vector image; + if (!IopModuleLoader::readWholeHostFile(host, path, image)) + { + log(LogLevel::Warning, std::string("[IOP] failed to open IRX '") + std::string(path) + "'"); + return {true, -1, -1}; + } + return loadImage(std::string(path), image, arguments, argumentSize); + } + + ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize) + { + std::vector image; + if (!IopModuleLoader::readElfFromGuest(host, guestAddress, image)) + return {true, -1, -1}; + std::ostringstream tag; + tag << "buffer@0x" << std::hex << guestAddress; + return loadImage(tag.str(), image, arguments, argumentSize); + } + + bool stopModule(int32_t moduleId, int32_t *result) + { + auto it = modules.find(moduleId); + if (it == modules.end()) + return false; + // A removable IRX normally exposes a stop entry through module metadata. We do not guess it; terminate owned execution and release the image cleanly. + kernel.terminateThreadsInRange(it->second.base, it->second.size); + rpc.removeServersInRange(it->second.base, it->second.size); + imports.eraseRange(it->second.base, it->second.size); + modules.erase(it); + kernel.cleanupDeadThreads(); + if (result) + *result = 0; + return true; + } + + IopHost &host; + IopMemory memory; + IopSysmem sysmem; + IopKernel kernel; + IopCdvd cdvd; + IopVblank vblank; + IopRpcBridge rpc; + IopSysclib sysclib; + IopStdio stdio; + IopHeaplib heaplib; + IopIntrman intrman; + IopTimrman timrman; + IopIoman ioman; + IopCpuCore cpuCore; + IopImportRegistry imports; + IopLoadcore loadcore; + std::map modules; + std::map pendingDmaInterrupts; + std::multimap pendingGuestCallbacks; + uint32_t nextModuleId = 1; + uint32_t moduleCursor = kModuleLoadBase; + uint64_t totalCycles = 0; + uint64_t totalInstructions = 0; + uint64_t eeCycleCarry = 0; + CpuState *activeCpu = nullptr; + std::string lastError; + bool servicingDmaInterrupts = false; + bool servicingGuestCallbacks = false; + uint32_t callDepth = 0u; + GuestCallback secrMcCommandHandler; + GuestCallback secrMcDevIdHandler; + GuestCallback checkKelfPathCallback; + }; + + IopEmulator::IopEmulator(IopHost &host) + : m_impl(std::make_unique(host)) + { + } + + IopEmulator::~IopEmulator() = default; + + void IopEmulator::reset() + { + m_impl->reset(); + } + + ModuleLoadResult IopEmulator::loadModule(std::string_view path, const void *arguments, uint32_t argumentSize) + { + return m_impl->loadModule(path, arguments, argumentSize); + } + + ModuleLoadResult IopEmulator::loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize) + { + return m_impl->loadModuleBuffer(guestAddress, arguments, argumentSize); + } + + bool IopEmulator::stopModule(int32_t moduleId, int32_t *result) + { + return m_impl->stopModule(moduleId, result); + } + + void IopEmulator::runEeCycles(uint64_t eeCycles) noexcept + { + const uint64_t total = m_impl->eeCycleCarry + eeCycles; + const uint64_t iopCycles = total / 8u; + m_impl->eeCycleCarry = total % 8u; + if (iopCycles) + m_impl->runCycles(iopCycles); + } + + RpcResult IopEmulator::handleRpc(const RpcRequest &request) + { + return m_impl->rpc.handleRpc(request, *m_impl); + } + + bool IopEmulator::hasRpcServer(uint32_t sid) const noexcept + { + return m_impl->rpc.hasServer(sid); + } + + void IopEmulator::onSifTransfer(const SifTransfer &transfer) + { + m_impl->rpc.onSifTransfer(transfer); + } + + uint32_t IopEmulator::allocateMemory(uint32_t size, uint32_t alignment) + { + return m_impl->memory.allocate(size, alignment); + } + + bool IopEmulator::freeMemory(uint32_t address) + { + return m_impl->memory.freeAllocation(address); + } + + bool IopEmulator::readMemory(uint32_t address, void *destination, size_t size) const + { + return isMemoryRange(address, size) && + m_impl->memory.readRam(address, destination, size); + } + + bool IopEmulator::writeMemory(uint32_t address, const void *source, size_t size) + { + return isMemoryRange(address, size) && + m_impl->memory.writeRam(address, source, size); + } + + bool IopEmulator::zeroMemory(uint32_t address, size_t size) + { + return isMemoryRange(address, size) && + m_impl->memory.zeroRam(address, size); + } + + bool IopEmulator::isMemoryRange(uint32_t address, size_t size) const + { + const bool physicalSegment = address < IopMemory::RamSize; + const bool cachedSegment = address >= 0x80000000u && address < 0x80200000u; + const bool uncachedSegment = address >= 0xA0000000u && address < 0xA0200000u; + if (!physicalSegment && !cachedSegment && !uncachedSegment) + return false; + const uint32_t physical = IopMemory::physicalAddress(address); + return physical <= IopMemory::RamSize && size <= IopMemory::RamSize - physical; + } + + uint64_t IopEmulator::cycles() const noexcept + { + return m_impl->totalCycles; + } + + uint64_t IopEmulator::instructions() const noexcept + { + return m_impl->totalInstructions; + } + + uint32_t IopEmulator::loadedModuleCount() const noexcept + { + return static_cast(m_impl->modules.size()); + } + + uint32_t IopEmulator::threadCount() const noexcept + { + return static_cast(m_impl->kernel.threadCount()); + } + + uint32_t IopEmulator::rpcServerCount() const noexcept + { + return static_cast(m_impl->rpc.serverCount()); + } + +} diff --git a/ps2xIOP/src/emulator/iop_emulator.h b/ps2xIOP/src/emulator/iop_emulator.h new file mode 100644 index 0000000..01b48df --- /dev/null +++ b/ps2xIOP/src/emulator/iop_emulator.h @@ -0,0 +1,49 @@ +#pragma once + +#include "ps2x/iop/iop_host.h" +#include "ps2x/iop/iop_types.h" + +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + class IopEmulator + { + public: + explicit IopEmulator(IopHost &host); + ~IopEmulator(); + + IopEmulator(const IopEmulator &) = delete; + IopEmulator &operator=(const IopEmulator &) = delete; + + void reset(); + [[nodiscard]] ModuleLoadResult loadModule(std::string_view path, const void *arguments, uint32_t argumentSize); + [[nodiscard]] ModuleLoadResult loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize); + [[nodiscard]] bool stopModule(int32_t moduleId, int32_t *result); + void runEeCycles(uint64_t eeCycles) noexcept; + [[nodiscard]] RpcResult handleRpc(const RpcRequest &request); + [[nodiscard]] bool hasRpcServer(uint32_t sid) const noexcept; + void onSifTransfer(const SifTransfer &transfer); + + [[nodiscard]] uint32_t allocateMemory(uint32_t size, uint32_t alignment = 16u); + [[nodiscard]] bool freeMemory(uint32_t address); + [[nodiscard]] bool readMemory(uint32_t address, void *destination, size_t size) const; + [[nodiscard]] bool writeMemory(uint32_t address, const void *source, size_t size); + [[nodiscard]] bool zeroMemory(uint32_t address, size_t size); + [[nodiscard]] bool isMemoryRange(uint32_t address, size_t size) const; + + [[nodiscard]] uint64_t cycles() const noexcept; + [[nodiscard]] uint64_t instructions() const noexcept; + [[nodiscard]] uint32_t loadedModuleCount() const noexcept; + [[nodiscard]] uint32_t threadCount() const noexcept; + [[nodiscard]] uint32_t rpcServerCount() const noexcept; + + private: + class Impl; + std::unique_ptr m_impl; + }; +} diff --git a/ps2xIOP/src/emulator/iop_emulator_const.h b/ps2xIOP/src/emulator/iop_emulator_const.h new file mode 100644 index 0000000..7004597 --- /dev/null +++ b/ps2xIOP/src/emulator/iop_emulator_const.h @@ -0,0 +1,15 @@ +#pragma once +#include + +constexpr uint32_t kThreadReturnSentinel = 0x1FFFFF00u; +constexpr uint32_t kCallReturnSentinel = 0x1FFFFF04u; +constexpr uint64_t kIopClockHz = 36'864'000ull; +// NTSC field cadence (approximately 59.94 Hz). VBlank imports are +// scheduler waits, not no-op timing hints: returning immediately lets +// high-priority IRX threads busy-loop and starve RPC server threads. +constexpr uint64_t kVblankPeriodCycles = (kIopClockHz * 1001ull + 30'000ull) / 60'000ull; +constexpr uint64_t kVblankEndPhaseCycles = kVblankPeriodCycles / 16ull; +constexpr uint32_t kDefaultSlice = 256u; +constexpr uint32_t kMaxCallInstructions = 2'000'000u; +constexpr uint32_t kModuleLoadBase = 0x00010000u; +constexpr uint32_t kStackGuardBytes = 64u; \ No newline at end of file diff --git a/ps2xIOP/src/emulator/services/iop_module_loader.cpp b/ps2xIOP/src/emulator/services/iop_module_loader.cpp new file mode 100644 index 0000000..80e59d6 --- /dev/null +++ b/ps2xIOP/src/emulator/services/iop_module_loader.cpp @@ -0,0 +1,561 @@ +#include "iop_module_loader.h" + +#include "../core/iop_memory.h" +#include "ps2x/iop/iop_subsystem.h" + +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + namespace + { + constexpr uint32_t kMaxImageSize = 64u * 1024u * 1024u; + constexpr uint32_t kModuleLoadBase = 0x00010000u; + + constexpr uint16_t ET_EXEC = 2; + constexpr uint16_t ET_SCE_IOPRELEXEC = 0xFF80u; + constexpr uint16_t ET_SCE_IOPRELEXEC2 = 0xFF81u; + constexpr uint16_t EM_MIPS = 8; + constexpr uint32_t PT_LOAD = 1; + constexpr uint32_t PT_SCE_IOPMOD = 0x70000080u; + constexpr uint32_t PT_MIPS_REGINFO = 0x70000000u; + constexpr uint32_t SHT_SYMTAB = 2; + constexpr uint32_t SHT_MIPS_REGINFO = 0x70000006u; + constexpr uint32_t SHT_RELA = 4; + constexpr uint32_t SHT_NOBITS = 8; + constexpr uint32_t SHT_REL = 9; + constexpr uint32_t SHF_ALLOC = 0x2; + constexpr uint32_t R_MIPS_NONE = 0; + constexpr uint32_t R_MIPS_16 = 1; + constexpr uint32_t R_MIPS_32 = 2; + constexpr uint32_t R_MIPS_REL32 = 3; + constexpr uint32_t R_MIPS_26 = 4; + constexpr uint32_t R_MIPS_HI16 = 5; + constexpr uint32_t R_MIPS_LO16 = 6; + +#pragma pack(push, 1) + struct Elf32Ehdr + { + unsigned char ident[16]; + uint16_t type; + uint16_t machine; + uint32_t version; + uint32_t entry; + uint32_t phoff; + uint32_t shoff; + uint32_t flags; + uint16_t ehsize; + uint16_t phentsize; + uint16_t phnum; + uint16_t shentsize; + uint16_t shnum; + uint16_t shstrndx; + }; + + struct Elf32Phdr + { + uint32_t type; + uint32_t offset; + uint32_t vaddr; + uint32_t paddr; + uint32_t filesz; + uint32_t memsz; + uint32_t flags; + uint32_t align; + }; + + struct Elf32Shdr + { + uint32_t name; + uint32_t type; + uint32_t flags; + uint32_t addr; + uint32_t offset; + uint32_t size; + uint32_t link; + uint32_t info; + uint32_t addralign; + uint32_t entsize; + }; + + struct Elf32Sym + { + uint32_t name; + uint32_t value; + uint32_t size; + uint8_t info; + uint8_t other; + uint16_t shndx; + }; + + struct Elf32Rel + { + uint32_t offset; + uint32_t info; + }; + + struct Elf32Rela + { + uint32_t offset; + uint32_t info; + int32_t addend; + }; +#pragma pack(pop) + + static_assert(sizeof(Elf32Ehdr) == 52); + static_assert(sizeof(Elf32Phdr) == 32); + static_assert(sizeof(Elf32Shdr) == 40); + static_assert(sizeof(Elf32Sym) == 16); + + struct PendingHi16 + { + uint32_t address = 0; + uint32_t symbolValue = 0; + uint32_t symbolIndex = 0; + }; + + uint32_t alignUp(uint32_t value, uint32_t alignment) + { + if (alignment <= 1u) + return value; + const uint32_t mask = alignment - 1u; + return (value + mask) & ~mask; + } + + bool checkedRange(size_t total, uint32_t offset, uint32_t size) + { + return offset <= total && size <= total - offset; + } + + bool validElfHeader(const Elf32Ehdr &header) + { + return header.ident[0] == 0x7Fu && + header.ident[1] == 'E' && + header.ident[2] == 'L' && + header.ident[3] == 'F' && + header.ident[4] == 1 && + header.ident[5] == 1 && + header.machine == EM_MIPS && + header.ehsize >= sizeof(Elf32Ehdr); + } + + bool applyRelocations(std::span image, + const std::vector §ions, + int64_t delta, + uint32_t loadBase, + bool isIopRelocatable, + IopMemory &memory) + { + if (sections.empty()) + return true; + + bool allSupported = true; + std::vector hi16; + for (size_t sectionIndex = 0; sectionIndex < sections.size(); ++sectionIndex) + { + const Elf32Shdr &relsec = sections[sectionIndex]; + if (relsec.type != SHT_REL && relsec.type != SHT_RELA) + continue; + if (relsec.info >= sections.size()) + continue; + + const Elf32Shdr &targetSection = sections[relsec.info]; + const uint32_t targetBase = static_cast(static_cast(targetSection.addr) + delta); + + std::span symbols; + std::vector symbolStorage; + if (relsec.link < sections.size()) + { + const Elf32Shdr &symsec = sections[relsec.link]; + if (symsec.type == SHT_SYMTAB && symsec.entsize >= sizeof(Elf32Sym) && checkedRange(image.size(), symsec.offset, symsec.size)) + { + const size_t count = symsec.size / symsec.entsize; + symbolStorage.resize(count); + for (size_t i = 0; i < count; ++i) + { + std::memcpy(&symbolStorage[i], image.data() + symsec.offset + i * symsec.entsize, sizeof(Elf32Sym)); + } + symbols = symbolStorage; + } + } + + const uint32_t entrySize = relsec.type == SHT_RELA + ? std::max(relsec.entsize, sizeof(Elf32Rela)) + : std::max(relsec.entsize, sizeof(Elf32Rel)); + if (entrySize == 0u || !checkedRange(image.size(), relsec.offset, relsec.size)) + continue; + + for (uint32_t offset = 0; offset + entrySize <= relsec.size; offset += entrySize) + { + uint32_t relocationOffset = 0u; + uint32_t relocationInfo = 0u; + int32_t explicitAddend = 0; + if (relsec.type == SHT_RELA) + { + Elf32Rela relocation{}; + std::memcpy(&relocation, image.data() + relsec.offset + offset, sizeof(relocation)); + relocationOffset = relocation.offset; + relocationInfo = relocation.info; + explicitAddend = relocation.addend; + } + else + { + Elf32Rel relocation{}; + std::memcpy(&relocation, image.data() + relsec.offset + offset, sizeof(relocation)); + relocationOffset = relocation.offset; + relocationInfo = relocation.info; + } + + const uint32_t type = relocationInfo & 0xFFu; + const uint32_t symbolIndex = relocationInfo >> 8u; + uint32_t symbolValue = isIopRelocatable ? loadBase : 0u; + if (symbolIndex < symbols.size()) + { + const Elf32Sym &symbol = symbols[symbolIndex]; + if (!isIopRelocatable || symbolIndex != 0u) + { + symbolValue = symbol.value; + if (symbol.shndx != 0u) + { + symbolValue = static_cast(static_cast(symbolValue) + delta); + } + } + } + + // Sony IOP relocatable executables use absolute image offsets + // and symbol index zero. loadcore applies them as loadBase + + // r_offset; normal ELF REL sections use a section-relative offset. + const uint64_t place64 = isIopRelocatable + ? static_cast(loadBase) + relocationOffset + : static_cast(targetBase) + relocationOffset; + if (place64 > std::numeric_limits::max()) + { + allSupported = false; + continue; + } + const uint32_t place = static_cast(place64); + if (place + 3u >= IopMemory::RamSize) + { + allSupported = false; + continue; + } + + const uint32_t word = memory.read32(place); + const int32_t addend = relsec.type == SHT_RELA + ? explicitAddend + : static_cast(word); + switch (type) + { + case R_MIPS_NONE: + break; + case R_MIPS_32: + case R_MIPS_REL32: + memory.write32(place, static_cast(static_cast(addend) + symbolValue)); + break; + case R_MIPS_26: + { + const uint32_t target = ((word & 0x03FFFFFFu) << 2u) + symbolValue; + memory.write32(place, (word & 0xFC000000u) | ((target >> 2u) & 0x03FFFFFFu)); + break; + } + case R_MIPS_HI16: + hi16.push_back({place, symbolValue, symbolIndex}); + break; + case R_MIPS_LO16: + { + const int32_t lo = static_cast(word & 0xFFFFu); + for (auto pending = hi16.begin(); pending != hi16.end();) + { + if (pending->symbolIndex != symbolIndex) + { + ++pending; + continue; + } + const uint32_t hiWord = memory.read32(pending->address); + const int32_t hi = static_cast(hiWord & 0xFFFFu) << 16u; + const int64_t full = static_cast(hi) + lo + pending->symbolValue; + const uint32_t relocatedHi = static_cast((full + 0x8000) >> 16u) & 0xFFFFu; + memory.write32(pending->address, (hiWord & 0xFFFF0000u) | relocatedHi); + pending = hi16.erase(pending); + } + const int64_t full = static_cast(lo) + symbolValue; + memory.write32(place, (word & 0xFFFF0000u) | (static_cast(full) & 0xFFFFu)); + break; + } + case R_MIPS_16: + memory.write32(place, (word & 0xFFFF0000u) | (static_cast(addend + symbolValue) & 0xFFFFu)); + break; + default: + allSupported = false; + break; + } + } + } + return allSupported; + } + } + + bool IopModuleLoader::readWholeHostFile(IopHost &host, std::string_view guestPath, std::vector &bytes) + { + const std::string translated = host.translateGuestPath(guestPath); + const std::string_view path = translated.empty() ? guestPath : std::string_view(translated); + const uint64_t handle = host.openHostFile(path); + if (handle == 0u) + return false; + + uint64_t size = 0u; + if (!host.hostFileSize(handle, size) || size == 0u || size > kMaxImageSize) + { + host.closeHostFile(handle); + return false; + } + + bytes.resize(static_cast(size)); + size_t bytesRead = 0u; + const bool ok = host.readHostFile(handle, 0u, bytes.data(), bytes.size(), bytesRead) && bytesRead == bytes.size(); + host.closeHostFile(handle); + return ok; + } + + bool IopModuleLoader::readElfFromGuest(IopHost &host, uint32_t guestAddress, std::vector &bytes) + { + Elf32Ehdr header{}; + if (!host.readGuest(guestAddress, &header, sizeof(header)) || !validElfHeader(header)) + return false; + + uint64_t required = sizeof(header); + required = std::max(required, static_cast(header.phoff) + static_cast(header.phentsize) * header.phnum); + required = std::max(required, static_cast(header.shoff) + static_cast(header.shentsize) * header.shnum); + + if (required > kMaxImageSize) + return false; // Should we log an error here? TODO check later + + bytes.resize(static_cast(required)); + if (!host.readGuest(guestAddress, bytes.data(), bytes.size())) + return false; + + if (header.shnum != 0u && header.shentsize >= sizeof(Elf32Shdr)) + { + for (uint16_t i = 0; i < header.shnum; ++i) + { + Elf32Shdr section{}; + const size_t offset = static_cast(header.shoff) + static_cast(i) * header.shentsize; + std::memcpy(§ion, bytes.data() + offset, sizeof(section)); + if (section.type != SHT_NOBITS) + { + required = std::max(required, static_cast(section.offset) + section.size); + } + } + } + if (header.phnum != 0u && header.phentsize >= sizeof(Elf32Phdr)) + { + for (uint16_t i = 0; i < header.phnum; ++i) + { + Elf32Phdr program{}; + const size_t offset = static_cast(header.phoff) + static_cast(i) * header.phentsize; + std::memcpy(&program, bytes.data() + offset, sizeof(program)); + required = std::max(required, static_cast(program.offset) + program.filesz); + } + } + if (required > kMaxImageSize) + return false; + + bytes.resize(static_cast(required)); + return host.readGuest(guestAddress, bytes.data(), bytes.size()); + } + + IopImageLoadResult IopModuleLoader::load(std::span image, IopMemory &memory, uint32_t moduleCursor) + { + IopImageLoadResult result; + result.nextModuleCursor = moduleCursor; + if (image.size() < sizeof(Elf32Ehdr)) + return result; + + Elf32Ehdr header{}; + std::memcpy(&header, image.data(), sizeof(header)); + if (!validElfHeader(header)) + { + result.error = IopImageLoadError::InvalidElf; + return result; + } + + uint32_t minVaddr = std::numeric_limits::max(); + uint32_t maxVaddr = 0u; + bool hasLoad = false; + std::vector programHeaders; + if (header.phnum != 0u && header.phentsize >= sizeof(Elf32Phdr) && checkedRange(image.size(), header.phoff, static_cast(header.phentsize) * header.phnum)) + { + programHeaders.reserve(header.phnum); + for (uint16_t i = 0; i < header.phnum; ++i) + { + Elf32Phdr program{}; + std::memcpy(&program, image.data() + header.phoff + static_cast(i) * header.phentsize, sizeof(program)); + programHeaders.push_back(program); + if (program.type == PT_LOAD && program.memsz != 0u) + { + hasLoad = true; + minVaddr = std::min(minVaddr, program.vaddr); + maxVaddr = std::max(maxVaddr, program.vaddr + program.memsz); + } + } + } + + std::vector sectionHeaders; + if (header.shnum != 0u && header.shentsize >= sizeof(Elf32Shdr) && checkedRange(image.size(), header.shoff, static_cast(header.shentsize) * header.shnum)) + { + sectionHeaders.reserve(header.shnum); + for (uint16_t i = 0; i < header.shnum; ++i) + { + Elf32Shdr section{}; + std::memcpy(§ion, image.data() + header.shoff + static_cast(i) * header.shentsize, sizeof(section)); + sectionHeaders.push_back(section); + if (!hasLoad && (section.flags & SHF_ALLOC) != 0u && section.size != 0u) + { + minVaddr = std::min(minVaddr, section.addr); + maxVaddr = std::max(maxVaddr, section.addr + section.size); + } + } + } + + if (minVaddr == std::numeric_limits::max()) + minVaddr = 0u; + uint32_t span = maxVaddr > minVaddr ? maxVaddr - minVaddr : 0x1000u; + span = alignUp(span, 0x100u); + const bool relocate = header.type != ET_EXEC || + maxVaddr > IopMemory::RamSize || + (minVaddr < kModuleLoadBase && minVaddr != 0u); + uint32_t base = 0u; + int64_t delta = 0; + if (relocate) + { + base = alignUp(moduleCursor, 0x100u); + if (base + span >= IopMemory::HeapBase) + { + result.error = IopImageLoadError::ArenaExhausted; + return result; + } + delta = static_cast(base) - minVaddr; + result.nextModuleCursor = base + span; + } + else + { + base = minVaddr; + } + + if (hasLoad) + { + for (const auto &program : programHeaders) + { + if (program.type != PT_LOAD || program.memsz == 0u) + continue; + if (!checkedRange(image.size(), program.offset, program.filesz) || + program.memsz < program.filesz) + return result; + const uint32_t destination = static_cast(static_cast(program.vaddr) + delta); + if (destination >= IopMemory::RamSize || program.memsz > IopMemory::RamSize - destination) + return result; + if (!memory.writeRam(destination, image.data() + program.offset, program.filesz)) + return result; + if (program.memsz > program.filesz && !memory.zeroRam(destination + program.filesz, program.memsz - program.filesz)) + return result; + } + } + else + { + uint32_t sectionCursor = base; + for (auto §ion : sectionHeaders) + { + if ((section.flags & SHF_ALLOC) == 0u || section.size == 0u) + continue; + uint32_t destination = 0u; + if (section.addr != 0u) + { + destination = static_cast(static_cast(section.addr) + delta); + } + else + { + sectionCursor = alignUp(sectionCursor, std::max(section.addralign, 4u)); + destination = sectionCursor; + section.addr = static_cast(static_cast(destination) - delta); + sectionCursor += section.size; + } + if (destination >= IopMemory::RamSize || section.size > IopMemory::RamSize - destination) + return result; + if (section.type == SHT_NOBITS) + { + if (!memory.zeroRam(destination, section.size)) + return result; + } + else + { + if (!checkedRange(image.size(), section.offset, section.size) || + !memory.writeRam(destination, image.data() + section.offset, section.size)) + return result; + } + } + } + + const bool isIopRelocatable = header.type == ET_SCE_IOPRELEXEC || header.type == ET_SCE_IOPRELEXEC2; + result.relocationsComplete = applyRelocations(image, + sectionHeaders, + delta, + base, + isIopRelocatable, + memory); + + result.base = base; + result.size = span; + result.entry = static_cast(static_cast(header.entry) + delta); + result.gp = 0u; + for (const auto &program : programHeaders) + { + if (program.type == PT_SCE_IOPMOD && program.filesz >= 12u && checkedRange(image.size(), program.offset, 12u)) + { + uint32_t entry = 0u; + uint32_t gp = 0u; + std::memcpy(&entry, image.data() + program.offset + 4u, sizeof(entry)); + std::memcpy(&gp, image.data() + program.offset + 8u, sizeof(gp)); + result.entry = static_cast(static_cast(entry) + delta); + result.gp = gp != 0u + ? static_cast(static_cast(gp) + delta) + : 0u; + break; + } + } + for (const auto &program : programHeaders) + { + if (result.gp != 0u) + break; + if (program.type == PT_MIPS_REGINFO && program.filesz >= 24u && checkedRange(image.size(), program.offset, 24u)) + { + uint32_t gp = 0u; + std::memcpy(&gp, image.data() + program.offset + 20u, sizeof(gp)); + result.gp = gp != 0u + ? static_cast(static_cast(gp) + delta) + : 0u; + break; + } + } + if (result.gp == 0u) + { + for (const auto §ion : sectionHeaders) + { + if (section.type == SHT_MIPS_REGINFO && section.size >= 24u && checkedRange(image.size(), section.offset, 24u)) + { + uint32_t gp = 0u; + std::memcpy(&gp, image.data() + section.offset + 20u, sizeof(gp)); + result.gp = gp != 0u + ? static_cast(static_cast(gp) + delta) + : 0u; + break; + } + } + } + + result.error = IopImageLoadError::None; + return result; + } +} diff --git a/ps2xIOP/src/emulator/services/iop_module_loader.h b/ps2xIOP/src/emulator/services/iop_module_loader.h new file mode 100644 index 0000000..6cd9ef6 --- /dev/null +++ b/ps2xIOP/src/emulator/services/iop_module_loader.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include +#include +#include + +namespace ps2x::iop +{ + class IopHost; +} + +namespace ps2x::iop::detail +{ + class IopMemory; + + enum class IopImageLoadError : uint8_t + { + None, + InvalidElf, + ArenaExhausted, + MalformedImage, + }; + + struct IopImageLoadResult + { + IopImageLoadError error = IopImageLoadError::MalformedImage; + uint32_t base = 0; + uint32_t size = 0; + uint32_t entry = 0; + uint32_t gp = 0; + uint32_t nextModuleCursor = 0; + bool relocationsComplete = true; + + [[nodiscard]] explicit operator bool() const noexcept + { + return error == IopImageLoadError::None; + } + }; + + class IopModuleLoader + { + public: + [[nodiscard]] static bool readWholeHostFile(IopHost &host, std::string_view guestPath, std::vector &bytes); + [[nodiscard]] static bool readElfFromGuest(IopHost &host, uint32_t guestAddress, std::vector &bytes); + [[nodiscard]] static IopImageLoadResult load(std::span image, IopMemory &memory, uint32_t moduleCursor); + }; +} diff --git a/ps2xIOP/src/emulator/services/iop_rpc.cpp b/ps2xIOP/src/emulator/services/iop_rpc.cpp new file mode 100644 index 0000000..cf4fdc6 --- /dev/null +++ b/ps2xIOP/src/emulator/services/iop_rpc.cpp @@ -0,0 +1,349 @@ +#include "iop_rpc.h" + +#include "../core/iop_cpu.h" +#include "../core/iop_kernel.h" +#include "../core/iop_memory.h" +#include "ps2x/iop/iop_host.h" + +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + IopRpcBridge::IopRpcBridge(IopHost &host, IopMemory &memory, IopKernel &kernel) noexcept + : m_host(host), m_memory(memory), m_kernel(kernel) + { + } + + void IopRpcBridge::reset() + { + m_servers.clear(); + m_nextDmaId = 1u; + m_sifInitialized = false; + } + + bool IopRpcBridge::dispatchSifManImport(uint16_t ordinal, IopCpuState &cpu) + { + const auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + switch (ordinal) + { + case 4: // sceSifDma2Init + case 5: // sceSifInit + m_sifInitialized = true; + setV0(0u); + return true; + case 7: // sceSifSetDma + { + constexpr uint32_t kDescriptorSize = 16u; + constexpr uint32_t kMaxDescriptors = 32u; + const uint32_t descriptorAddress = cpu.gpr[4]; + const uint32_t descriptorCount = cpu.gpr[5]; + if (descriptorAddress == 0u || descriptorCount == 0u || descriptorCount > kMaxDescriptors) + { + setV0(0u); + return true; + } + + struct PendingTransfer + { + uint32_t source = 0u; + uint32_t destination = 0u; + uint32_t size = 0u; + }; + + std::array descriptorWords{}; + const size_t descriptorBytes = static_cast(descriptorCount) * kDescriptorSize; + if (!m_memory.readRam(descriptorAddress, descriptorWords.data(), descriptorBytes)) + { + setV0(0u); + return true; + } + + std::array pending{}; + uint32_t pendingCount = 0u; + uint32_t largestTransfer = 0u; + for (uint32_t i = 0u; i < descriptorCount; ++i) + { + const uint32_t source = descriptorWords[i * 4u + 0u]; + const uint32_t destination = descriptorWords[i * 4u + 1u]; + const int32_t signedSize = static_cast(descriptorWords[i * 4u + 2u]); + if (signedSize <= 0) + continue; + + const uint32_t size = static_cast(signedSize); + if (!m_memory.ownsRamRange(source, size)) + { + setV0(0u); + return true; + } + pending[pendingCount++] = {source, destination, size}; + largestTransfer = std::max(largestTransfer, size); + } + + // IOP-side sceSifSetDma sends IOP RAM to the EE. Validate all EE + // destinations before committing any write so a bad chain cannot + // partially update guest memory, but maybe we could skip this check if we trust the EE-side SIF driver to validate the chain ?! + // TODO check later + std::vector scratch(largestTransfer); + for (uint32_t i = 0u; i < pendingCount; ++i) + { + const PendingTransfer &transfer = pending[i]; + if (!m_host.readGuest(transfer.destination, scratch.data(), transfer.size)) + { + setV0(0u); + return true; + } + } + + for (uint32_t i = 0u; i < pendingCount; ++i) + { + const PendingTransfer &transfer = pending[i]; + if (!m_memory.readRam(transfer.source, scratch.data(), transfer.size) || !m_host.writeGuest(transfer.destination, scratch.data(), transfer.size)) + { + setV0(0u); + return true; + } + } + + const uint32_t dmaId = m_nextDmaId++; + if (m_nextDmaId == 0u || m_nextDmaId > static_cast(std::numeric_limits::max())) + { + m_nextDmaId = 1u; + } + setV0(dmaId); + return true; + } + case 8: // sceSifDmaStat + setV0(0xFFFFFFFFu); + return true; + case 29: // sceSifCheckInit + setV0(m_sifInitialized ? 1u : 0u); + return true; + default: + setV0(0u); + return true; + } + } + + bool IopRpcBridge::dispatchSifCmdImport(uint16_t ordinal, IopCpuState &cpu) + { + const auto setV0 = [&](uint32_t value) + { + cpu.gpr[2] = value; + }; + switch (ordinal) + { + case 4: // InitCmd + case 5: + case 6: + case 7: + case 8: + case 9: + case 10: + case 11: + case 14: // InitRpc + case 15: + case 16: + setV0(0); + return true; + case 12: // sceSifSendCmd + case 13: // isceSifSendCmd + { + constexpr uint32_t kHeaderSize = 16u; + constexpr uint32_t kMaxPacketSize = 112u; + const uint32_t commandId = cpu.gpr[4]; + const uint32_t packetAddress = cpu.gpr[5]; + const uint32_t packetSize = cpu.gpr[6]; + const uint32_t extraSource = cpu.gpr[7]; + const uint32_t stackPointer = cpu.gpr[29]; + const uint32_t extraDestination = m_memory.read32(stackPointer + 16u); + const int32_t signedExtraSize = static_cast(m_memory.read32(stackPointer + 20u)); + + if (packetAddress == 0u || packetSize < kHeaderSize || packetSize > kMaxPacketSize || + !m_memory.ownsRamRange(packetAddress, packetSize)) + { + setV0(0u); + return true; + } + + std::array packet{}; + if (!m_memory.readRam(packetAddress, packet.data(), packetSize)) + { + setV0(0u); + return true; + } + + uint32_t extraSize = 0u; + if (signedExtraSize > 0) + { + extraSize = static_cast(signedExtraSize); + if (extraSource == 0u || extraDestination == 0u || + !m_memory.ownsRamRange(extraSource, extraSize) || + !m_host.writeGuest(extraDestination, m_memory.ram().data() + IopMemory::physicalAddress(extraSource), extraSize)) + { + setV0(0u); + return true; + } + } + + const uint32_t sizeWord = packetSize | (extraSize << 8u); + std::memcpy(packet.data() + 0u, &sizeWord, sizeof(sizeWord)); + std::memcpy(packet.data() + 4u, &extraDestination, sizeof(extraDestination)); + std::memcpy(packet.data() + 8u, &commandId, sizeof(commandId)); + + if (!m_host.sendSifCommand(commandId, packet.data(), packetSize)) + { + // A command without an EE handler is still a completed DMA on real hardware. Only malformed packets fail above. + } + + const uint32_t dmaId = m_nextDmaId++; + if (m_nextDmaId == 0u || m_nextDmaId > static_cast(std::numeric_limits::max())) + m_nextDmaId = 1u; + setV0(dmaId); + return true; + } + case 17: // sceSifRegisterRpc + { + RpcServer server; + server.serverData = cpu.gpr[4]; + server.sid = cpu.gpr[5]; + server.function = cpu.gpr[6]; + server.gp = cpu.gpr[28]; + server.buffer = cpu.gpr[7]; + const uint32_t stackPointer = cpu.gpr[29]; + server.callback = m_memory.read32(stackPointer + 16u); + server.callbackBuffer = m_memory.read32(stackPointer + 20u); + server.queue = m_memory.read32(stackPointer + 24u); + m_servers[server.sid] = server; + if (server.serverData != 0u) + { + m_memory.write32(server.serverData + 0x20u, server.sid); + m_memory.write32(server.serverData + 0x28u, server.function); + m_memory.write32(server.serverData + 0x2Cu, server.buffer); + } + setV0(server.serverData); + return true; + } + case 18: + setV0(0); + return true; + case 19: // SetRpcQueue + setV0(cpu.gpr[4]); + return true; + case 20: + case 21: + setV0(0); + return true; + case 22: // RpcLoop + m_kernel.sleepCurrent(cpu); + setV0(0); + return true; + case 23: + setV0(0); + return true; + case 24: // RemoveRpc + { + const uint32_t serverData = cpu.gpr[4]; + for (auto server = m_servers.begin(); server != m_servers.end(); ++server) + { + if (server->second.serverData == serverData) + { + m_servers.erase(server); + break; + } + } + setV0(0); + return true; + } + case 25: + case 26: + case 27: + case 28: + case 29: + setV0(0); + return true; + default: + return false; + } + } + + RpcResult IopRpcBridge::handleRpc(const RpcRequest &request, IopGuestExecutor &executor) + { + RpcResult result{}; + const auto serverIt = m_servers.find(request.sid); + if (serverIt == m_servers.end() || serverIt->second.function == 0u) + return result; + + RpcServer &server = serverIt->second; + if (request.send.size != 0u && server.buffer != 0u) + { + const uint32_t copySize = std::min(request.send.size, IopMemory::RamSize - std::min(server.buffer, IopMemory::RamSize)); + if (copySize != 0u) + { + std::vector payload(copySize); + if (m_host.readGuest(request.send.address, payload.data(), payload.size())) + (void)m_memory.writeRam(server.buffer, payload.data(), payload.size()); + } + } + + uint32_t returnPointer = executor.executeGuestFunction(server.function, + request.function, + server.buffer, + request.send.size, + 0u, + server.gp); + if (returnPointer == 0u) + returnPointer = server.buffer; + if (request.receive.address != 0u && request.receive.size != 0u && returnPointer != 0u) + { + const uint32_t physical = IopMemory::physicalAddress(returnPointer); + if (physical < IopMemory::RamSize) + { + const uint32_t copySize = std::min(request.receive.size, IopMemory::RamSize - physical); + (void)m_host.writeGuest(request.receive.address, m_memory.ram().data() + physical, copySize); + if (copySize < request.receive.size) + (void)m_host.zeroGuest(request.receive.address + copySize, request.receive.size - copySize); + } + } + + result.handled = true; + result.resultAddress = request.receive.address; + result.serverDispatchPolicy = ServerDispatchPolicy::Suppress; + result.signalNowaitCompletion = true; + result.signalCompletion = true; + return result; + } + + void IopRpcBridge::onSifTransfer(const SifTransfer &transfer) + { + // The EE SIF transport owns the actual directional memory movement. + // Services still receive both phases through IopSubsystem, but mirroring + // IOP bytes through an equal-numbered EE address would alias two distinct + // PS2 address spaces and can overwrite live game data. + (void)transfer; + } + + void IopRpcBridge::removeServersInRange(uint32_t base, uint32_t size) + { + for (auto server = m_servers.begin(); server != m_servers.end();) + { + const uint32_t function = IopMemory::physicalAddress(server->second.function); + if (function >= base && function < base + size) + server = m_servers.erase(server); + else + ++server; + } + } + + bool IopRpcBridge::hasServer(uint32_t sid) const noexcept + { + const auto server = m_servers.find(sid); + return server != m_servers.end() && server->second.function != 0u; + } +} diff --git a/ps2xIOP/src/emulator/services/iop_rpc.h b/ps2xIOP/src/emulator/services/iop_rpc.h new file mode 100644 index 0000000..a4286ba --- /dev/null +++ b/ps2xIOP/src/emulator/services/iop_rpc.h @@ -0,0 +1,78 @@ +#pragma once + +#include "ps2x/iop/iop_types.h" + +#include +#include +#include + +namespace ps2x::iop +{ + class IopHost; +} + +namespace ps2x::iop::detail +{ + struct IopCpuState; + class IopKernel; + class IopMemory; + + class IopGuestExecutor + { + public: + virtual ~IopGuestExecutor() = default; + + [[nodiscard]] virtual uint32_t executeGuestFunction(uint32_t address, + uint32_t a0, + uint32_t a1, + uint32_t a2, + uint32_t a3, + uint32_t gp) = 0; + [[nodiscard]] virtual uint32_t executeGuestFunctionWithBudget(uint32_t address, + uint32_t a0, + uint32_t a1, + uint32_t a2, + uint32_t a3, + uint32_t gp, + uint32_t instructionBudget) + { + return executeGuestFunction(address, a0, a1, a2, a3, gp); + } + }; + + class IopRpcBridge + { + public: + IopRpcBridge(IopHost &host, IopMemory &memory, IopKernel &kernel) noexcept; + + void reset(); + [[nodiscard]] bool dispatchSifManImport(uint16_t ordinal, IopCpuState &cpu); + [[nodiscard]] bool dispatchSifCmdImport(uint16_t ordinal, IopCpuState &cpu); + [[nodiscard]] RpcResult handleRpc(const RpcRequest &request, IopGuestExecutor &executor); + void onSifTransfer(const SifTransfer &transfer); + void removeServersInRange(uint32_t base, uint32_t size); + + [[nodiscard]] bool hasServer(uint32_t sid) const noexcept; + [[nodiscard]] size_t serverCount() const noexcept { return m_servers.size(); } + + private: + struct RpcServer + { + uint32_t sid = 0; + uint32_t serverData = 0; + uint32_t function = 0; + uint32_t gp = 0; + uint32_t buffer = 0; + uint32_t callback = 0; + uint32_t callbackBuffer = 0; + uint32_t queue = 0; + }; + + IopHost &m_host; + IopMemory &m_memory; + IopKernel &m_kernel; + std::unordered_map m_servers; + uint32_t m_nextDmaId = 1u; + bool m_sifInitialized = false; + }; +} diff --git a/ps2xIOP/src/iop_module_manager.cpp b/ps2xIOP/src/iop_module_manager.cpp new file mode 100644 index 0000000..56e4a8f --- /dev/null +++ b/ps2xIOP/src/iop_module_manager.cpp @@ -0,0 +1,180 @@ +#include "iop_module_manager.h" + +#include "ps2x/iop/ps2_path.h" + +#include + +namespace ps2x::iop::detail +{ + IopModuleManager::IopModuleManager() + { + // ROM modules that the no-BIOS HLE environment can legitimately provide. + // Entries with RPC services become routable only after load. + constexpr std::string_view modules[] = { + "sysmem", + "loadcore", + "intrman", + "sifman", + "sifcmd", + "sifinit", + "ioman", + "iomanx", + "modload", + "stdio", + "sysclib", + "thbase", + "thevent", + "thsemap", + "thmsgbx", + "timrman", + "vblank", + "secrman", + "sio2man", + "xsio2man", + "sio2d", + "padman", + "xpadman", + "mcman", + "xmcman", + "mcserv", + "libsd", + "cdvdman", + "cdvdfsv", + "dev9", + "usbd", + "usbhdfsd", + "udnl", + "fileio", + "poweroff", + "netman", + "ps2ip", + "dbcman", + "dbcm", + }; + for (const std::string_view module : modules) + m_builtinKeys.emplace(module); + } + + void IopModuleManager::reset() + { + m_records.clear(); + m_hleIdsByKey.clear(); + m_loadedKeyReferences.clear(); + m_nextHleId = 0x40000000; + } + + void IopModuleManager::setServiceModuleKeys(std::vector keys) + { + m_serviceKeys.clear(); + for (std::string &key : keys) + { + const std::string normalized = ps2PathLeafKey(key); + if (!normalized.empty()) + m_serviceKeys.emplace(normalized); + } + } + + ModuleLoadResult IopModuleManager::loadHle(std::string_view path) + { + ModuleLoadResult result{true, -1, -1}; + const std::string key = ps2PathLeafKey(path); + if (key.empty() || (!m_builtinKeys.contains(key) && !m_serviceKeys.contains(key))) + return result; + + const auto existing = m_hleIdsByKey.find(key); + if (existing != m_hleIdsByKey.end()) + { + Record &record = m_records[existing->second]; + ++record.references; + addLoadedKey(key); + result.moduleId = existing->second; + result.startResult = 0; + return result; + } + + if (m_nextHleId <= 0) + return result; + const int32_t id = m_nextHleId++; + m_records.emplace(id, Record{key, 1u, false}); + m_hleIdsByKey.emplace(key, id); + addLoadedKey(key); + result.moduleId = id; + result.startResult = 0; + return result; + } + + void IopModuleManager::observePhysicalLoad(int32_t moduleId, std::string_view path) + { + if (moduleId <= 0) + return; + const std::string key = ps2PathLeafKey(path); + if (key.empty()) + return; + m_records[moduleId] = Record{key, 1u, true}; + addLoadedKey(key); + } + + bool IopModuleManager::stopHle(int32_t moduleId, int32_t *result) + { + const auto found = m_records.find(moduleId); + if (found == m_records.end() || found->second.physical) + return false; + + Record &record = found->second; + removeLoadedKey(record.key); + if (record.references > 1u) + { + --record.references; + } + else + { + m_hleIdsByKey.erase(record.key); + m_records.erase(found); + } + if (result) + *result = 0; + return true; + } + + void IopModuleManager::observePhysicalStop(int32_t moduleId) + { + const auto found = m_records.find(moduleId); + if (found == m_records.end() || !found->second.physical) + return; + removeLoadedKey(found->second.key); + m_records.erase(found); + } + + bool IopModuleManager::isLoaded(std::span aliases) const + { + if (aliases.empty()) + return true; + return std::any_of(aliases.begin(), aliases.end(), [&](std::string_view alias) + { + const std::string key = ps2PathLeafKey(alias); + const auto found = m_loadedKeyReferences.find(key); + return found != m_loadedKeyReferences.end() && found->second != 0u; }); + } + + bool IopModuleManager::recognizes(std::string_view path) const + { + const std::string key = ps2PathLeafKey(path); + return m_builtinKeys.contains(key) || m_serviceKeys.contains(key); + } + + void IopModuleManager::addLoadedKey(std::string_view key) + { + ++m_loadedKeyReferences[std::string(key)]; + } + + void IopModuleManager::removeLoadedKey(std::string_view key) + { + const auto found = m_loadedKeyReferences.find(std::string(key)); + if (found == m_loadedKeyReferences.end()) + return; + if (found->second > 1u) + --found->second; + else + m_loadedKeyReferences.erase(found); + } +} diff --git a/ps2xIOP/src/iop_module_manager.h b/ps2xIOP/src/iop_module_manager.h new file mode 100644 index 0000000..33ab9f6 --- /dev/null +++ b/ps2xIOP/src/iop_module_manager.h @@ -0,0 +1,49 @@ +#pragma once + +#include "ps2x/iop/iop_types.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + class IopModuleManager + { + public: + IopModuleManager(); + + void reset(); + void setServiceModuleKeys(std::vector keys); + + [[nodiscard]] ModuleLoadResult loadHle(std::string_view path); + void observePhysicalLoad(int32_t moduleId, std::string_view path); + [[nodiscard]] bool stopHle(int32_t moduleId, int32_t *result); + void observePhysicalStop(int32_t moduleId); + + [[nodiscard]] bool isLoaded(std::span aliases) const; + [[nodiscard]] bool recognizes(std::string_view path) const; + + private: + struct Record + { + std::string key; + uint32_t references = 0u; + bool physical = false; + }; + + void addLoadedKey(std::string_view key); + void removeLoadedKey(std::string_view key); + + std::unordered_set m_builtinKeys; + std::unordered_set m_serviceKeys; + std::unordered_map m_records; + std::unordered_map m_hleIdsByKey; + std::unordered_map m_loadedKeyReferences; + int32_t m_nextHleId = 0x40000000; + }; +} diff --git a/ps2xIOP/src/iop_service.h b/ps2xIOP/src/iop_service.h index 7e3667a..79fed4f 100644 --- a/ps2xIOP/src/iop_service.h +++ b/ps2xIOP/src/iop_service.h @@ -3,7 +3,6 @@ #include "ps2x/iop/iop_host.h" #include "ps2x/iop/iop_types.h" -#include #include #include #include @@ -18,6 +17,12 @@ namespace ps2x::iop::detail [[nodiscard]] virtual std::string_view name() const = 0; [[nodiscard]] virtual std::span sids() const = 0; + // A service with aliases is dormant until one of these IOP modules is + // actually loaded. + [[nodiscard]] virtual std::span moduleAliases() const + { + return {}; + } virtual void reset() = 0; [[nodiscard]] virtual RpcAbi selectRpcAbi(const RpcAbiRequest &request) const @@ -40,16 +45,4 @@ namespace ps2x::iop::detail }; using ServiceList = std::vector>; - using ProfileFactory = std::function; - - struct ProfileDefinition - { - std::string id; - std::string provider = "builtin"; - GameMatcher matcher; - ProfileFactory factory; - }; - - ServiceList createCoreServices(IopHost &host); - std::vector createBuiltinProfiles(); } diff --git a/ps2xIOP/src/iop_subsystem.cpp b/ps2xIOP/src/iop_subsystem.cpp index d848f8d..b498155 100644 --- a/ps2xIOP/src/iop_subsystem.cpp +++ b/ps2xIOP/src/iop_subsystem.cpp @@ -1,123 +1,91 @@ #include "ps2x/iop/iop_subsystem.h" #include "iop_service.h" -#include "plugin_loader.h" +#include "iop_module_manager.h" +#include "emulator/iop_emulator.h" +#include "module_factories.h" +#include "ps2x/iop/ps2_path.h" -#include -#include #include -#include #include +#include #include namespace ps2x::iop { - namespace - { - bool equalsIgnoreCaseAscii(std::string_view lhs, std::string_view rhs) - { - if (lhs.size() != rhs.size()) - { - return false; - } - - for (size_t i = 0; i < lhs.size(); ++i) - { - const auto left = static_cast(lhs[i]); - const auto right = static_cast(rhs[i]); - if (std::tolower(left) != std::tolower(right)) - { - return false; - } - } - return true; - } - - int matchSpecificity(const GameMatcher &matcher, const GameIdentity &identity) - { - int specificity = 0; - if (!matcher.elfName.empty()) - { - if (!equalsIgnoreCaseAscii(matcher.elfName, identity.elfName)) - { - return -1; - } - ++specificity; - } - if (matcher.entryPoint != 0) - { - if (matcher.entryPoint != identity.entryPoint) - { - return -1; - } - ++specificity; - } - if (matcher.crc32 != 0) - { - if (matcher.crc32 != identity.crc32) - { - return -1; - } - ++specificity; - } - return specificity; - } - } - class IopSubsystem::Impl { public: explicit Impl(IopHost &hostRef) - : host(hostRef), pluginCatalog(hostRef), coreServices(detail::createCoreServices(hostRef)), profiles(detail::createBuiltinProfiles()) + : host(hostRef), + emulator(hostRef) { + coreServices.emplace_back(detail::createMcservService(host)); + coreServices.emplace_back(detail::createDbcmanService(host)); + coreServices.emplace_back(detail::createLibSdService(host)); + refreshServiceModuleKeys(); rebuildRoutes(); } + bool serviceActive(const detail::IopService &service) const + { + return moduleManager.isLoaded(service.moduleAliases()); + } + + void refreshServiceModuleKeys() + { + std::vector keys; + for (const auto &service : coreServices) + { + for (std::string_view alias : service->moduleAliases()) + keys.emplace_back(alias); + } + moduleManager.setServiceModuleKeys(std::move(keys)); + } + void rebuildRoutes() { routes.clear(); - auto addLayer = [&](detail::ServiceList &services, bool profileSpecific) -> bool + lastError.clear(); + for (const auto &service : coreServices) { - std::unordered_map layer; - for (const auto &service : services) + if (!serviceActive(*service)) + continue; + for (const uint32_t sid : service->sids()) { - if (!service) + if (!routes.emplace(sid, service.get()).second) { - continue; - } - for (const uint32_t sid : service->sids()) - { - if (!layer.emplace(sid, service.get()).second) - { - std::ostringstream out; - out << "duplicate IOP SID 0x" << std::hex << sid << " in " << (profileSpecific ? "profile" : "core") << " layer"; - lastError = out.str(); - return false; - } + std::ostringstream out; + out << "duplicate IOP SID 0x" << std::hex << sid << " in core services"; + lastError = out.str(); + routes.clear(); + return; } } - for (const auto &[sid, service] : layer) - { - routes[sid] = service; - } - return true; - }; + } + } - routesValid = addLayer(coreServices, false) && addLayer(profileServices, true); + void recordLoadOutcome(std::string_view path, bool hle) + { + constexpr size_t maxOutcomes = 32u; + if (loadOutcomes.size() >= maxOutcomes || !loggedLoadPaths.emplace(path).second) + return; + std::string message = hle ? "[IOP:HLE] fallback module='" : "[IOP:load-failed] module='"; + message.append(path); + message += hle ? "' physical IRX unavailable; using registered HLE provider" + : "' no HLE provider accepted the module; physical IRX was not loaded"; + loadOutcomes.push_back(message); + host.log(hle ? LogLevel::Info : LogLevel::Warning, message); } IopHost &host; - detail::PluginCatalog pluginCatalog; detail::ServiceList coreServices; - detail::ServiceList profileServices; - std::vector profiles; std::unordered_map routes; - std::vector pluginSearchPaths; - std::vector diagnostics; - std::string activeProfile; - std::string activeProvider; + std::vector loadOutcomes; + std::unordered_set loggedLoadPaths; std::string lastError; - bool routesValid = true; + detail::IopModuleManager moduleManager; + detail::IopEmulator emulator; }; IopSubsystem::IopSubsystem(IopHost &host) @@ -129,111 +97,11 @@ namespace ps2x::iop IopSubsystem::IopSubsystem(IopSubsystem &&) noexcept = default; IopSubsystem &IopSubsystem::operator=(IopSubsystem &&) noexcept = default; - void IopSubsystem::setPluginSearchPaths(std::vector paths) - { - m_impl->pluginSearchPaths = std::move(paths); - } - - bool IopSubsystem::loadPlugins(std::string *error) - { - return m_impl->pluginCatalog.load(m_impl->pluginSearchPaths, m_impl->profiles, m_impl->diagnostics, error); - } - - bool IopSubsystem::configure(const GameIdentity &identity, std::string *error) - { - m_impl->profileServices.clear(); - m_impl->activeProfile.clear(); - m_impl->activeProvider.clear(); - m_impl->lastError.clear(); - - const detail::ProfileDefinition *selected = nullptr; - const detail::ProfileDefinition *selectedTie = nullptr; - int selectedSpecificity = -1; - for (const auto &profile : m_impl->profiles) - { - const int specificity = matchSpecificity(profile.matcher, identity); - if (specificity < 0) - { - continue; - } - if (specificity > selectedSpecificity) - { - selected = &profile; - selectedTie = nullptr; - selectedSpecificity = specificity; - continue; - } - if (specificity == selectedSpecificity && selected) - { - selectedTie = &profile; - } - } - - if (selected && selectedTie) - { - m_impl->lastError = "ambiguous IOP profiles '" + selected->provider + ":" + - selected->id + "' and '" + selectedTie->provider + ":" + - selectedTie->id + "'"; - if (error) - { - *error = m_impl->lastError; - } - m_impl->rebuildRoutes(); - return false; - } - - if (selected) - { - try - { - m_impl->profileServices = selected->factory(m_impl->host, identity); - m_impl->activeProfile = selected->id; - m_impl->activeProvider = selected->provider; - } - catch (const std::exception &exception) - { - m_impl->lastError = "failed to create IOP profile '" + selected->id + "': " + exception.what(); - if (error) - { - *error = m_impl->lastError; - } - m_impl->rebuildRoutes(); - return false; - } - catch (...) - { - m_impl->lastError = "failed to create IOP profile '" + selected->id + "': unknown plugin exception"; - if (error) - { - *error = m_impl->lastError; - } - m_impl->rebuildRoutes(); - return false; - } - } - - m_impl->rebuildRoutes(); - if (!m_impl->routesValid) - { - const std::string routeError = m_impl->lastError; - m_impl->profileServices.clear(); - m_impl->activeProfile.clear(); - m_impl->activeProvider.clear(); - m_impl->rebuildRoutes(); - m_impl->lastError = routeError; - if (error) - { - *error = m_impl->lastError; - } - return false; - } - - reset(); - return true; - } - void IopSubsystem::reset() { + m_impl->moduleManager.reset(); + m_impl->loadOutcomes.clear(); + m_impl->loggedLoadPaths.clear(); for (auto &service : m_impl->coreServices) { if (service) @@ -241,31 +109,71 @@ namespace ps2x::iop service->reset(); } } - for (auto &service : m_impl->profileServices) + m_impl->emulator.reset(); + m_impl->refreshServiceModuleKeys(); + m_impl->rebuildRoutes(); + } + + ModuleLoadResult IopSubsystem::loadModule(std::string_view path, const void *arguments, uint32_t argumentSize) + { + const ParsedPs2Path parsed = parsePs2Path(path); + if (!parsed) + return {true, -1, -1}; + + if (parsed.device != Ps2PathDevice::Rom0) { - if (service) + ModuleLoadResult physical = m_impl->emulator.loadModule(path, arguments, argumentSize); + if (physical.moduleId > 0) { - service->reset(); + m_impl->moduleManager.observePhysicalLoad(physical.moduleId, path); + m_impl->rebuildRoutes(); + return physical; } } + + ModuleLoadResult hle = m_impl->moduleManager.loadHle(path); + if (hle.moduleId > 0) + { + m_impl->rebuildRoutes(); + if (parsed.device != Ps2PathDevice::Rom0) + m_impl->recordLoadOutcome(path, true); + } + else + { + m_impl->recordLoadOutcome(path, false); + } + return hle; + } + + ModuleLoadResult IopSubsystem::loadModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize) + { + return m_impl->emulator.loadModuleBuffer(guestAddress, arguments, argumentSize); + } + + bool IopSubsystem::stopModule(int32_t moduleId, int32_t *result) + { + if (m_impl->moduleManager.stopHle(moduleId, result)) + { + m_impl->rebuildRoutes(); + return true; + } + if (!m_impl->emulator.stopModule(moduleId, result)) + return false; + m_impl->moduleManager.observePhysicalStop(moduleId); + m_impl->rebuildRoutes(); + return true; + } + + void IopSubsystem::runEeCycles(uint64_t eeCycles) noexcept + { + m_impl->emulator.runEeCycles(eeCycles); } RpcAbi IopSubsystem::selectRpcAbi(const RpcAbiRequest &request) const { - for (const auto &service : m_impl->profileServices) - { - if (service) - { - const RpcAbi selected = service->selectRpcAbi(request); - if (selected != RpcAbi::RuntimeDefault) - { - return selected; - } - } - } for (const auto &service : m_impl->coreServices) { - if (service) + if (service && m_impl->serviceActive(*service)) { const RpcAbi selected = service->selectRpcAbi(request); if (selected != RpcAbi::RuntimeDefault) @@ -277,63 +185,93 @@ namespace ps2x::iop return RpcAbi::RuntimeDefault; } + bool IopSubsystem::canBindRpc(uint32_t sid) const noexcept + { + if (m_impl->routes.find(sid) != m_impl->routes.end()) + { + return true; + } + return m_impl->emulator.hasRpcServer(sid); + } + RpcResult IopSubsystem::handleRpc(const RpcRequest &request) { - const auto it = m_impl->routes.find(request.sid); - if (it == m_impl->routes.end() || !it->second) + const auto route = m_impl->routes.find(request.sid); + detail::IopService *hle = route != m_impl->routes.end() ? route->second : nullptr; + + RpcResult emulated = m_impl->emulator.handleRpc(request); + if (emulated.handled || !hle) { - return {}; + return emulated; } - return it->second->handleRpc(request); + return hle->handleRpc(request); } void IopSubsystem::onSifTransfer(const SifTransfer &transfer) { for (auto &service : m_impl->coreServices) { - if (service) - { - service->onSifTransfer(transfer); - } - } - for (auto &service : m_impl->profileServices) - { - if (service) + if (service && m_impl->serviceActive(*service)) { service->onSifTransfer(transfer); } } + m_impl->emulator.onSifTransfer(transfer); + } + + uint32_t IopSubsystem::allocateMemory(uint32_t size, uint32_t alignment) + { + return m_impl->emulator.allocateMemory(size, alignment); + } + + bool IopSubsystem::freeMemory(uint32_t address) + { + return m_impl->emulator.freeMemory(address); + } + + bool IopSubsystem::readMemory(uint32_t address, void *destination, size_t size) const + { + return m_impl->emulator.readMemory(address, destination, size); + } + + bool IopSubsystem::writeMemory(uint32_t address, const void *source, size_t size) + { + return m_impl->emulator.writeMemory(address, source, size); + } + + bool IopSubsystem::zeroMemory(uint32_t address, size_t size) + { + return m_impl->emulator.zeroMemory(address, size); + } + + bool IopSubsystem::isMemoryRange(uint32_t address, size_t size) const + { + return m_impl->emulator.isMemoryRange(address, size); } DebugSnapshot IopSubsystem::debugSnapshot() const { DebugSnapshot snapshot; - snapshot.activeProfile = m_impl->activeProfile; - snapshot.activeProvider = m_impl->activeProvider; - snapshot.diagnostics = m_impl->diagnostics; + snapshot.emulatorCycles = m_impl->emulator.cycles(); + snapshot.emulatorInstructions = m_impl->emulator.instructions(); + snapshot.emulatorLoadedModules = m_impl->emulator.loadedModuleCount(); + snapshot.emulatorThreads = m_impl->emulator.threadCount(); + snapshot.emulatorRpcServers = m_impl->emulator.rpcServerCount(); + snapshot.diagnostics = m_impl->loadOutcomes; if (!m_impl->lastError.empty()) { snapshot.diagnostics.push_back(m_impl->lastError); } - auto append = [&](const detail::ServiceList &services, bool profileSpecific) + for (const auto &service : m_impl->coreServices) { - for (const auto &service : services) - { - if (!service) - { - continue; - } - DebugService row; - row.name = service->name(); - row.sids.assign(service->sids().begin(), service->sids().end()); - row.profileSpecific = profileSpecific; - service->appendDebugMetrics(row.metrics); - snapshot.services.push_back(std::move(row)); - } - }; - append(m_impl->coreServices, false); - append(m_impl->profileServices, true); + DebugService row; + row.name = service->name(); + row.sids.assign(service->sids().begin(), service->sids().end()); + row.active = m_impl->serviceActive(*service); + service->appendDebugMetrics(row.metrics); + snapshot.services.push_back(std::move(row)); + } return snapshot; } } diff --git a/ps2xIOP/src/module_factories.h b/ps2xIOP/src/module_factories.h index 8e0f422..f0da295 100644 --- a/ps2xIOP/src/module_factories.h +++ b/ps2xIOP/src/module_factories.h @@ -2,156 +2,9 @@ #include "iop_service.h" -#include -#include -#include -#include - namespace ps2x::iop::detail { - struct CriDtxBindings - { - std::string serviceName; - uint32_t sid = 0u; - uint32_t urpcObjectBase = 0u; - uint32_t urpcObjectLimit = 0u; - uint32_t urpcObjectStride = 0u; - uint32_t urpcFunctionTableBase = 0u; - uint32_t urpcObjectTableBase = 0u; - uint32_t dispatcherFunctionAddress = 0u; - uint32_t rpcServerPoolBase = 0u; - uint32_t rpcServerStride = 0u; - }; - - enum class TsnddrvProtocolVariant - { - SndQueueV1, - }; - - struct TsnddrvGuestArena - { - uint32_t base = 0u; - uint32_t limit = 0u; - uint32_t statusAlignment = 0u; - uint32_t tableAlignment = 0u; - uint32_t storageAlignment = 0u; - uint32_t hdBytes = 0u; - uint32_t sqBytes = 0u; - uint32_t dataBytes = 0u; - }; - - struct TsnddrvChecksumTables - { - uint32_t seAddress = 0u; - uint32_t midiAddress = 0u; - }; - - struct TsnddrvCompletionRule - { - uint32_t eeFunction = 0u; - bool suppressGuestCallback = false; - bool signalCompletion = false; - bool clearBusy = false; - }; - - struct TsnddrvBindings - { - std::string serviceName; - TsnddrvProtocolVariant protocol = TsnddrvProtocolVariant::SndQueueV1; - TsnddrvGuestArena arena; - std::vector checksumCandidates; - uint32_t busyFlagAddress = 0u; - std::vector completionRules; - }; - - struct ClFileRpcLayout - { - uint32_t directLoadFunction = 0x01u; - uint32_t getStatusFunction = 0x03u; - uint32_t initializeFunction = 0x04u; - uint32_t waitFunction = 0x05u; - uint32_t getSizeFunction = 0x06u; - uint32_t openFunction = 0x08u; - uint32_t closeFunction = 0x09u; - uint32_t readFunction = 0x0Au; - uint32_t secondaryWaitFunction = 0x15u; - uint32_t setRootFunction = 0x16u; - uint32_t pathBytes = 0x100u; - uint32_t directLoadSizeOffset = 0x100u; - uint32_t directLoadDestinationOffset = 0x104u; - uint32_t responseStatusOffset = 0u; - uint32_t responseValueOffset = 4u; - uint32_t responseClearBytes = 0x40u; - uint32_t maximumReadBytes = 0x2000u; - uint32_t loadResultQueued = 5u; - uint32_t loadStatusFailed = 3u; - uint32_t loadStatusComplete = 7u; - uint32_t invalidHandleStatus = 9u; - uint32_t firstLoadHandle = 0x00010000u; - bool acknowledgeUnknownFunctions = true; - }; - - struct ClFileBindings - { - std::string serviceName; - uint32_t sid = 0u; - ClFileRpcLayout rpc; - }; - - // TODO This is for the lord of the rings better name for that one - struct SoundUpdateStubBindings - { - std::string serviceName; - uint32_t sid = 0u; - uint32_t activeStreamCountOffset = 0u; - uint32_t responseCounterOffset = 0u; - bool zeroReceiveBuffer = true; - bool signalNowaitCompletion = false; - bool completeQueuedPlayStreams = false; - std::vector suppressedCompletionCallbacks; - }; - - struct SdrdrvBindings - { - std::string serviceName; - uint32_t sid = 0u; - uint32_t imageHeaderAddress = 0u; - uint32_t sectorSize = 0u; - uint32_t statusOffset = 0u; - uint32_t statusStride = 0u; - uint32_t statusSlotMask = 0u; - uint8_t completeValue = 0u; - uint32_t initFunction = 0u; - uint32_t submitFunction = 1u; - uint32_t shutdownFunction = 2u; - uint32_t headerCommand = 0x0Cu; - uint32_t loadCommand = 0x0Eu; - uint32_t commandBytes = 32u; - uint32_t maxCommands = 32u; - uint32_t lbnWord = 2u; - uint32_t byteCountWord = 3u; - uint32_t destinationWord = 4u; - uint32_t destinationKindWord = 5u; - uint32_t loadIdWord = 6u; - uint32_t eeDestinationKind = 0u; - bool fallbackBodyToCdImage = true; - bool clearReceiveBeforeDispatch = true; - bool completeFailedLoads = true; - bool pretendNonEeLoadsComplete = true; - uint32_t headerWarningLimit = 4u; - uint32_t bodyWarningLimit = 8u; - std::string imageHeaderLowerName; - std::string imageHeaderUpperName; - std::string imageBodyLowerName; - std::string imageBodyUpperName; - }; - std::unique_ptr createDbcmanService(IopHost &host); std::unique_ptr createLibSdService(IopHost &host); std::unique_ptr createMcservService(IopHost &host); - std::unique_ptr createTsnddrvService(IopHost &host, TsnddrvBindings bindings); - std::unique_ptr createCriDtxService(IopHost &host, CriDtxBindings bindings); - std::unique_ptr createClFileService(IopHost &host, ClFileBindings bindings); - std::unique_ptr createSoundUpdateStubService(IopHost &host, SoundUpdateStubBindings bindings); - std::unique_ptr createSdrdrvService(IopHost &host, SdrdrvBindings bindings); } diff --git a/ps2xIOP/src/modules/clfile.cpp b/ps2xIOP/src/modules/clfile.cpp deleted file mode 100644 index 93bd236..0000000 --- a/ps2xIOP/src/modules/clfile.cpp +++ /dev/null @@ -1,635 +0,0 @@ -#include "module_factories.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace ps2x::iop::detail -{ - namespace - { - class ClFileService final : public IopService - { - public: - ClFileService(IopHost &host, ClFileBindings bindings) - : m_host(host), - m_bindings(std::move(bindings)), - m_sids{m_bindings.sid}, - m_nextLoadHandle(m_bindings.rpc.firstLoadHandle) - { - } - - ~ClFileService() override - { - reset(); - } - - [[nodiscard]] std::string_view name() const override - { - return m_bindings.serviceName; - } - - [[nodiscard]] std::span sids() const override - { - return m_sids; - } - - void reset() override - { - std::lock_guard lock(m_mutex); - for (auto &[handle, entry] : m_fileHandles) - { - (void)handle; - if (entry.handle != 0u) - { - m_host.closeHostFile(entry.handle); - entry.handle = 0u; - } - } - - m_fileHandles.clear(); - m_loads.clear(); - m_root.clear(); - m_nextFileHandle = 1u; - m_nextLoadHandle = m_bindings.rpc.firstLoadHandle; - } - - [[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override - { - RpcResult result; - if (request.sid != m_bindings.sid) - { - return result; - } - const Operation operation = decodeFunction(request.function); - if (operation == Operation::Unknown && - !m_bindings.rpc.acknowledgeUnknownFunctions) - { - return result; - } - - result.handled = true; - result.resultAddress = request.receive.address; - if (request.receive.address != 0u && request.receive.size != 0u) - { - (void)m_host.zeroGuest(request.receive.address, - std::min(request.receive.size, - m_bindings.rpc.responseClearBytes)); - } - - const auto writeRpcResult = [&](int32_t status, uint32_t value) - { - writeResult(request.receive, status, value); - }; - - switch (operation) - { - case Operation::DirectLoad: - { - const uint32_t stringBytes = request.send.size != 0u - ? std::min(request.send.size, - m_bindings.rpc.pathBytes) - : m_bindings.rpc.pathBytes; - const std::string guestPath = readGuestString(request.send.address, stringBytes); - uint32_t requestedBytes = 0u; - uint32_t destinationAddress = 0u; - (void)readGuestU32(request.send.address + m_bindings.rpc.directLoadSizeOffset, - requestedBytes); - (void)readGuestU32(request.send.address + m_bindings.rpc.directLoadDestinationOffset, - destinationAddress); - - uint32_t status = m_bindings.rpc.loadStatusFailed; - uint32_t fileSize = 0u; - const std::string hostPath = resolvePath(guestPath); - if (!hostPath.empty()) - { - const uint64_t file = m_host.openHostFile(hostPath); - uint64_t hostFileSize = 0u; - if (file != 0u && m_host.hostFileSize(file, hostFileSize)) - { - fileSize = static_cast( - std::min(hostFileSize, 0xFFFFFFFFull)); - const uint64_t maxRequestedBytes = requestedBytes != 0u - ? requestedBytes - : hostFileSize; - const uint64_t bytesToCopy = std::min(hostFileSize, maxRequestedBytes); - - status = m_bindings.rpc.loadStatusComplete; - if (destinationAddress != 0u && bytesToCopy != 0u) - { - if (!copyFileToGuest(file, destinationAddress, bytesToCopy)) - { - status = m_bindings.rpc.loadStatusFailed; - } - } - } - if (file != 0u) - { - m_host.closeHostFile(file); - } - } - - uint32_t loadHandle = 0u; - { - std::lock_guard lock(m_mutex); - loadHandle = allocateLoadLocked(status, fileSize); - } - writeRpcResult(static_cast(m_bindings.rpc.loadResultQueued), loadHandle); - return result; - } - - case Operation::Initialize: - writeRpcResult(0, 1u); - return result; - - case Operation::Wait: - case Operation::SecondaryWait: - writeRpcResult(0, 0u); - return result; - - case Operation::SetRoot: - { - const std::string root = readGuestString(request.send.address, - request.send.size != 0u - ? request.send.size - : m_bindings.rpc.pathBytes); - { - std::lock_guard lock(m_mutex); - m_root = root; - } - writeRpcResult(0, 1u); - return result; - } - - case Operation::Open: - { - const std::string guestPath = readGuestString(request.send.address, - request.send.size != 0u - ? request.send.size - : m_bindings.rpc.pathBytes); - const std::string hostPath = resolvePath(guestPath); - if (hostPath.empty()) - { - writeRpcResult(-1, 0u); - return result; - } - - const uint64_t file = m_host.openHostFile(hostPath); - if (file == 0u) - { - writeRpcResult(-1, 0u); - return result; - } - - uint64_t hostFileSize = 0u; - if (!m_host.hostFileSize(file, hostFileSize)) - { - m_host.closeHostFile(file); - writeRpcResult(-1, 0u); - return result; - } - const uint32_t fileSize = static_cast( - std::min(hostFileSize, 0x7FFFFFFFull)); - - uint32_t handle = 0u; - { - std::lock_guard lock(m_mutex); - handle = allocateFileHandleLocked(file, fileSize); - } - if (handle == 0u) - { - m_host.closeHostFile(file); - writeRpcResult(-1, 0u); - return result; - } - - writeRpcResult(0, handle); - return result; - } - - case Operation::Close: - { - uint32_t handle = 0u; - (void)readGuestU32(request.send.address, handle); - - uint64_t file = 0u; - bool closedLoad = false; - { - std::lock_guard lock(m_mutex); - const auto fileIt = m_fileHandles.find(handle); - if (fileIt != m_fileHandles.end()) - { - file = fileIt->second.handle; - m_fileHandles.erase(fileIt); - } - - const auto loadIt = m_loads.find(handle); - if (loadIt != m_loads.end()) - { - m_loads.erase(loadIt); - closedLoad = true; - } - } - if (file != 0u) - { - m_host.closeHostFile(file); - } - - const bool closed = file != 0u || closedLoad; - writeRpcResult(closed ? 0 : -1, closed ? 1u : 0u); - return result; - } - - case Operation::Read: - { - uint32_t handle = 0u; - uint32_t requestedBytes = 0u; - uint32_t destinationAddress = 0u; - (void)readGuestU32(request.send.address + 0u, handle); - (void)readGuestU32(request.send.address + 4u, requestedBytes); - (void)readGuestU32(request.send.address + 8u, destinationAddress); - - if (destinationAddress == 0u) - { - writeRpcResult(-1, 0u); - return result; - } - - std::vector bytes(std::min(requestedBytes, - m_bindings.rpc.maximumReadBytes)); - size_t bytesRead = 0u; - bool readFailed = false; - { - std::lock_guard lock(m_mutex); - const auto fileIt = m_fileHandles.find(handle); - if (fileIt == m_fileHandles.end() || fileIt->second.handle == 0u) - { - readFailed = true; - } - else if (!bytes.empty()) - { - size_t hostBytesRead = 0u; - if (!m_host.readHostFile(fileIt->second.handle, - fileIt->second.position, - bytes.data(), - bytes.size(), - hostBytesRead)) - { - readFailed = true; - } - else - { - bytesRead = hostBytesRead; - fileIt->second.position += hostBytesRead; - } - } - } - - if (readFailed || - (bytesRead != 0u && - !m_host.writeGuest(destinationAddress, bytes.data(), bytesRead))) - { - writeRpcResult(-1, 0u); - return result; - } - - writeRpcResult(0, static_cast(bytesRead)); - return result; - } - - case Operation::GetStatus: - { - uint32_t handle = 0u; - (void)readGuestU32(request.send.address, handle); - - bool loadFound = false; - uint32_t loadStatus = 0u; - bool fileFound = false; - { - std::lock_guard lock(m_mutex); - const auto loadIt = m_loads.find(handle); - if (loadIt != m_loads.end()) - { - loadFound = true; - loadStatus = loadIt->second.status; - } - else - { - fileFound = m_fileHandles.find(handle) != m_fileHandles.end(); - } - } - - if (loadFound) - { - writeRpcResult(static_cast(loadStatus), 0u); - } - else - { - writeRpcResult(0, fileFound ? 0u : m_bindings.rpc.invalidHandleStatus); - } - return result; - } - - case Operation::GetSize: - { - uint32_t handle = 0u; - (void)readGuestU32(request.send.address, handle); - - bool found = false; - uint32_t size = 0u; - { - std::lock_guard lock(m_mutex); - const auto loadIt = m_loads.find(handle); - if (loadIt != m_loads.end()) - { - found = true; - size = loadIt->second.size; - } - else - { - const auto fileIt = m_fileHandles.find(handle); - if (fileIt != m_fileHandles.end()) - { - found = true; - size = fileIt->second.size; - } - } - } - - writeRpcResult(found ? 0 : -1, found ? size : 0u); - return result; - } - - case Operation::Unknown: - writeRpcResult(0, 0u); - return result; - } - return result; - } - - void appendDebugMetrics(std::vector &metrics) const override - { - std::lock_guard lock(m_mutex); - metrics.push_back({"open_files", m_fileHandles.size(), false}); - metrics.push_back({"load_records", m_loads.size(), false}); - metrics.push_back({"next_file_handle", m_nextFileHandle, true}); - metrics.push_back({"next_load_handle", m_nextLoadHandle, true}); - } - - private: - enum class Operation - { - DirectLoad, - GetStatus, - Initialize, - Wait, - GetSize, - Open, - Close, - Read, - SecondaryWait, - SetRoot, - Unknown, - }; - - [[nodiscard]] Operation decodeFunction(uint32_t function) const - { - const ClFileRpcLayout &rpc = m_bindings.rpc; - if (function == rpc.directLoadFunction) return Operation::DirectLoad; - if (function == rpc.getStatusFunction) return Operation::GetStatus; - if (function == rpc.initializeFunction) return Operation::Initialize; - if (function == rpc.waitFunction) return Operation::Wait; - if (function == rpc.getSizeFunction) return Operation::GetSize; - if (function == rpc.openFunction) return Operation::Open; - if (function == rpc.closeFunction) return Operation::Close; - if (function == rpc.readFunction) return Operation::Read; - if (function == rpc.secondaryWaitFunction) return Operation::SecondaryWait; - if (function == rpc.setRootFunction) return Operation::SetRoot; - return Operation::Unknown; - } - - struct ClFileHandle - { - uint64_t handle = 0u; - uint32_t size = 0u; - uint64_t position = 0u; - }; - - struct ClFileLoad - { - uint32_t status = 0u; - uint32_t size = 0u; - }; - - [[nodiscard]] bool readGuestU32(uint32_t address, uint32_t &value) const - { - value = 0u; - return m_host.readGuest(address, &value, sizeof(value)); - } - - [[nodiscard]] std::string readGuestString(uint32_t address, uint32_t maxBytes) const - { - if (address == 0u || maxBytes == 0u) - { - return {}; - } - - std::vector bytes(maxBytes); - if (!m_host.readGuest(address, bytes.data(), bytes.size())) - { - return {}; - } - - size_t length = 0u; - while (length < bytes.size() && bytes[length] != '\0') - { - ++length; - } - return std::string(bytes.data(), length); - } - - [[nodiscard]] static bool hasDevice(std::string_view path) - { - return path.find(':') != std::string_view::npos; - } - - [[nodiscard]] static std::string joinGuestPath(const std::string &root, - const std::string &leaf) - { - if (root.empty() || leaf.empty() || hasDevice(leaf)) - { - return leaf; - } - - const char tail = root.back(); - if (tail == '/' || tail == '\\' || tail == ':') - { - return root + leaf; - } - return root + "/" + leaf; - } - - [[nodiscard]] std::string resolvePath(const std::string &path) const - { - std::string root; - { - std::lock_guard lock(m_mutex); - root = m_root; - } - - const std::string translated = m_host.translateGuestPath(joinGuestPath(root, path)); - return translated; - } - - [[nodiscard]] uint32_t allocateFileHandleLocked(uint64_t file, uint32_t size) - { - if (file == 0u) - { - return 0u; - } - - for (uint32_t attempt = 0u; attempt < 0xFFFFu; ++attempt) - { - uint32_t handle = m_nextFileHandle++; - if (handle == 0u) - { - handle = m_nextFileHandle++; - } - if (m_fileHandles.find(handle) == m_fileHandles.end() && - m_loads.find(handle) == m_loads.end()) - { - m_fileHandles.emplace(handle, ClFileHandle{file, size, 0u}); - return handle; - } - } - return 0u; - } - - [[nodiscard]] uint32_t allocateLoadLocked(uint32_t status, uint32_t size) - { - for (uint32_t attempt = 0u; attempt < 0xFFFFu; ++attempt) - { - uint32_t handle = m_nextLoadHandle++; - if (handle < 3u) - { - handle = m_bindings.rpc.firstLoadHandle; - m_nextLoadHandle = m_bindings.rpc.firstLoadHandle + 1u; - } - if (m_loads.find(handle) == m_loads.end() && - m_fileHandles.find(handle) == m_fileHandles.end()) - { - m_loads.emplace(handle, ClFileLoad{status, size}); - return handle; - } - } - return 0u; - } - - void writeResult(GuestBuffer receive, int32_t status, uint32_t value) - { - if (receive.address != 0u && - receive.size >= m_bindings.rpc.responseStatusOffset + sizeof(uint32_t)) - { - const uint32_t encodedStatus = static_cast(status); - (void)m_host.writeGuest(receive.address + m_bindings.rpc.responseStatusOffset, - &encodedStatus, - sizeof(encodedStatus)); - } - if (receive.address != 0u && - receive.size >= m_bindings.rpc.responseValueOffset + sizeof(uint32_t)) - { - (void)m_host.writeGuest(receive.address + m_bindings.rpc.responseValueOffset, - &value, - sizeof(value)); - } - } - - [[nodiscard]] bool copyFileToGuest(uint64_t file, - uint32_t destinationAddress, - uint64_t bytesToCopy) - { - constexpr size_t kChunkBytes = 16u * 1024u; - if (bytesToCopy > 0xFFFFFFFFull - static_cast(destinationAddress) + 1ull) - { - return false; - } - - std::vector chunk(kChunkBytes); - uint64_t copied = 0u; - while (copied < bytesToCopy) - { - const size_t wanted = static_cast( - std::min(chunk.size(), bytesToCopy - copied)); - size_t received = 0u; - if (!m_host.readHostFile(file, - copied, - chunk.data(), - wanted, - received) || - received != wanted) - { - return false; - } - - const uint32_t chunkAddress = destinationAddress + static_cast(copied); - if (!m_host.writeGuest(chunkAddress, chunk.data(), received)) - { - return false; - } - copied += received; - } - return true; - } - - IopHost &m_host; - ClFileBindings m_bindings; - std::array m_sids; - mutable std::mutex m_mutex; - std::unordered_map m_fileHandles; - std::unordered_map m_loads; - uint32_t m_nextFileHandle = 1u; - uint32_t m_nextLoadHandle = 0u; - std::string m_root; - }; - } - - std::unique_ptr createClFileService(IopHost &host, - ClFileBindings bindings) - { - const ClFileRpcLayout &rpc = bindings.rpc; - const std::array functions = { - rpc.directLoadFunction, - rpc.getStatusFunction, - rpc.initializeFunction, - rpc.waitFunction, - rpc.getSizeFunction, - rpc.openFunction, - rpc.closeFunction, - rpc.readFunction, - rpc.secondaryWaitFunction, - rpc.setRootFunction, - }; - std::unordered_set uniqueFunctions; - for (const uint32_t function : functions) - { - if (!uniqueFunctions.emplace(function).second) - { - throw std::invalid_argument("duplicate CLFILE RPC function binding"); - } - } - if (bindings.serviceName.empty() || bindings.sid == 0u || - rpc.pathBytes == 0u || rpc.maximumReadBytes == 0u || - rpc.firstLoadHandle < 3u) - { - throw std::invalid_argument("invalid CLFILE bindings"); - } - return std::make_unique(host, std::move(bindings)); - } -} diff --git a/ps2xIOP/src/modules/cri_dtx.cpp b/ps2xIOP/src/modules/cri_dtx.cpp deleted file mode 100644 index 55386f5..0000000 --- a/ps2xIOP/src/modules/cri_dtx.cpp +++ /dev/null @@ -1,1299 +0,0 @@ -#include "../module_factories.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace ps2x::iop::detail -{ - namespace - { - constexpr uint32_t kDtxHeaderSize = 16u; - constexpr uint32_t kDtxCommandSize = 16u; - constexpr uint32_t kMaxDtxCommands = 128u; - constexpr uint32_t kMinimumDtxWorkSize = 64u; - constexpr uint32_t kDefaultSjrmtCapacity = 0x4000u; - constexpr uint32_t kMaximumSjrmtCapacity = 0x01000000u; - - template - bool readGuestPod(const IopHost &host, uint32_t address, T &value) - { - value = {}; - return host.readGuest(address, &value, sizeof(value)); - } - - template - bool writeGuestPod(IopHost &host, uint32_t address, const T &value) - { - return host.writeGuest(address, &value, sizeof(value)); - } - - uint32_t normalizeSjrmtCapacity(uint32_t requestedBytes) - { - if (requestedBytes == 0u || requestedBytes > kMaximumSjrmtCapacity) - { - return kDefaultSjrmtCapacity; - } - return requestedBytes; - } - - bool looksLikeCreate34Candidate(const RpcCallCandidate &candidate) - { - return candidate.receiveAddress != 0u && - candidate.receiveSize >= sizeof(uint32_t) && - candidate.receiveSize <= 0x40u && - candidate.sendSize >= 12u && - candidate.sendSize <= 0x1000u; - } - - class CriDtxService final : public IopService - { - public: - CriDtxService(IopHost &host, CriDtxBindings bindings) - : m_host(host), - m_bindings(std::move(bindings)), - m_sids{m_bindings.sid} - { - reset(); - } - - [[nodiscard]] std::string_view name() const override - { - return m_bindings.serviceName; - } - - [[nodiscard]] std::span sids() const override - { - return m_sids; - } - - void reset() override - { - std::lock_guard lock(m_mutex); - m_remoteById.clear(); - m_transferById.clear(); - m_sjxByHandle.clear(); - m_ps2RnaByHandle.clear(); - m_sjrmtByHandle.clear(); - m_nextUrpcObject = m_bindings.urpcObjectBase; - m_dmaAcks = 0u; - m_dmaMisses = 0u; - m_urpcCalls = 0u; - } - - [[nodiscard]] RpcAbi selectRpcAbi(const RpcAbiRequest &request) const override - { - if (request.boundSid == m_bindings.sid && - request.function == 0x422u && - request.stack.plausible && - looksLikeCreate34Candidate(request.stack) && - !looksLikeCreate34Candidate(request.registers)) - { - return RpcAbi::Stack; - } - return RpcAbi::RuntimeDefault; - } - - [[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override - { - RpcResult result{}; - if (request.sid != m_bindings.sid) - { - return result; - } - - // DTX owns this SID. Its base protocol intentionally bypasses generic EE - // server dispatch, and URPC dispatch is gated by the guest function table. - result.serverDispatchPolicy = ServerDispatchPolicy::Suppress; - - const bool isUrpc = request.function >= 0x400u && request.function < 0x500u; - const uint32_t command = isUrpc ? (request.function & 0xFFu) : 0u; - uint32_t urpcFunction = 0u; - uint32_t urpcObject = 0u; - if (isUrpc && command < 64u) - { - (void)readGuestPod(m_host, - m_bindings.urpcFunctionTableBase + (command * sizeof(uint32_t)), - urpcFunction); - (void)readGuestPod(m_host, - m_bindings.urpcObjectTableBase + (command * sizeof(uint32_t)), - urpcObject); - } - (void)urpcObject; - - const bool hasUrpcHandler = isUrpc && command < 64u && urpcFunction != 0u; - if (hasUrpcHandler && request.serverFunction != 0u) - { - result.guestFunction = request.serverFunction; - result.guestArguments[0] = request.function; - result.guestArguments[1] = request.serverBuffer; - result.guestArguments[2] = request.send.size; - result.guestDefaultResultAddress = request.serverBuffer != 0u - ? request.serverBuffer - : request.receive.address; - return result; - } - - if (hasUrpcHandler && - request.send.address != 0u && - request.send.size > 0u) - { - result.guestFunction = m_bindings.dispatcherFunctionAddress; - result.guestArguments[0] = request.function; - result.guestArguments[1] = request.send.address; - result.guestArguments[2] = request.send.size; - result.guestDefaultResultAddress = request.send.address; - return result; - } - - if (request.function == 2u && - request.receive.address != 0u && - request.receive.size >= sizeof(uint32_t)) - { - return handleCreateTransport(request, result); - } - if (request.function == 3u) - { - return handleDestroyTransport(request, result); - } - if (isUrpc) - { - return emulateUrpc(request, command, result); - } - - // Unknown calls remain visibly unhandled, but must never escape to a - // generic registered server for this configured SID. - return result; - } - - void onSifTransfer(const SifTransfer &transfer) override - { - if (transfer.kind != SifTransferKind::SetDma || - transfer.phase != SifTransferPhase::AfterCopy || - transfer.size < kMinimumDtxWorkSize) - { - return; - } - - uint32_t normalizedSource = 0u; - uint32_t normalizedDestination = 0u; - if (!m_host.normalizeGuestAddress(transfer.sourceAddress, normalizedSource) || - !m_host.normalizeGuestAddress(transfer.destinationAddress, normalizedDestination)) - { - return; - } - - TransferState matched{}; - bool found = false; - { - std::lock_guard lock(m_mutex); - for (const auto &[id, state] : m_transferById) - { - (void)id; - if (matchesTransfer(state, - normalizedSource, - normalizedDestination, - transfer.size)) - { - matched = state; - found = true; - break; - } - } - - if (!found) - { - found = inferTransferFromPayloadLocked(normalizedSource, - normalizedDestination, - transfer.size, - matched); - } - - if (!found) - { - ++m_dmaMisses; - } - } - - if (!found) - { - return; - } - - const uint32_t footerAddress = - matched.eeWorkAddress + matched.workSize - sizeof(uint32_t); - uint32_t ticket = 0u; - if (!readGuestPod(m_host, footerAddress, ticket)) - { - return; - } - (void)writeGuestPod(m_host, footerAddress, ticket + 1u); - - if (matched.dtxId == 0u) - { - applySjxPayload(matched); - } - else if (matched.dtxId == 1u) - { - applyPs2RnaPayload(matched); - } - - std::lock_guard lock(m_mutex); - ++m_dmaAcks; - } - - void appendDebugMetrics(std::vector &metrics) const override - { - std::lock_guard lock(m_mutex); - metrics.push_back({"sid", m_bindings.sid, true}); - metrics.push_back({"remote_handles", m_remoteById.size(), false}); - metrics.push_back({"transfers", m_transferById.size(), false}); - metrics.push_back({"sjx_objects", m_sjxByHandle.size(), false}); - metrics.push_back({"ps2rna_objects", m_ps2RnaByHandle.size(), false}); - metrics.push_back({"sjrmt_objects", m_sjrmtByHandle.size(), false}); - metrics.push_back({"next_urpc_object", m_nextUrpcObject, true}); - metrics.push_back({"urpc_calls", m_urpcCalls, false}); - metrics.push_back({"dma_acks", m_dmaAcks, false}); - metrics.push_back({"dma_misses", m_dmaMisses, false}); - } - - private: - struct TransferState - { - uint32_t dtxId = 0u; - uint32_t remoteHandle = 0u; - uint32_t eeWorkAddress = 0u; - uint32_t iopWorkAddress = 0u; - uint32_t workSize = 0u; - }; - - struct SjxState - { - uint32_t handle = 0u; - uint32_t sourceSjHandle = 0u; - uint32_t destinationSjHandle = 0u; - uint32_t line = 0u; - uint32_t eeObjectAddress = 0u; - uint16_t xid = 0u; - }; - - struct Ps2RnaState - { - uint32_t handle = 0u; - uint32_t maxChannels = 0u; - uint32_t sjHandle0 = 0u; - uint32_t sjHandle1 = 0u; - uint32_t channelCount = 0u; - uint32_t sampleFrequency = 0u; - uint32_t volume = 0u; - bool playEnabled = false; - }; - - struct SjrmtState - { - uint32_t handle = 0u; - uint32_t mode = 0u; - uint32_t workAddress = 0u; - uint32_t workSize = 0u; - uint32_t readPosition = 0u; - uint32_t writePosition = 0u; - uint32_t roomBytes = 0u; - uint32_t dataBytes = 0u; - uint32_t uuid0 = 0u; - uint32_t uuid1 = 0u; - uint32_t uuid2 = 0u; - uint32_t uuid3 = 0u; - }; - - static bool matchesTransfer(const TransferState &state, - uint32_t sourceAddress, - uint32_t destinationAddress, - uint32_t size) - { - (void)destinationAddress; - return state.eeWorkAddress != 0u && - state.workSize >= kMinimumDtxWorkSize && - state.eeWorkAddress == sourceAddress && - state.workSize == size; - } - - uint32_t normalizeAddress(uint32_t address) const - { - uint32_t normalized = 0u; - (void)m_host.normalizeGuestAddress(address, normalized); - return normalized; - } - - RpcResult handleCreateTransport(const RpcRequest &request, RpcResult result) - { - uint32_t dtxId = 0u; - uint32_t eeWorkAddress = 0u; - uint32_t iopWorkAddress = 0u; - uint32_t workSize = 0u; - if (request.send.address != 0u && request.send.size >= sizeof(uint32_t)) - { - (void)readGuestPod(m_host, request.send.address, dtxId); - } - if (request.send.address != 0u && request.send.size >= 4u * sizeof(uint32_t)) - { - (void)readGuestPod(m_host, request.send.address + 4u, eeWorkAddress); - (void)readGuestPod(m_host, request.send.address + 8u, iopWorkAddress); - (void)readGuestPod(m_host, request.send.address + 12u, workSize); - } - - const uint32_t normalizedEeWorkAddress = normalizeAddress(eeWorkAddress); - const uint32_t normalizedIopWorkAddress = normalizeAddress(iopWorkAddress); - - uint32_t remoteHandle = 0u; - { - std::lock_guard lock(m_mutex); - const auto existing = m_remoteById.find(dtxId); - if (existing != m_remoteById.end()) - { - remoteHandle = existing->second; - } - - if (remoteHandle == 0u) - { - remoteHandle = m_host.allocateIopHandle(IopHandleKind::RpcServer); - if (remoteHandle == 0u) - { - remoteHandle = m_host.allocateIopHandle(IopHandleKind::RpcPacket); - } - if (remoteHandle == 0u) - { - remoteHandle = m_bindings.rpcServerPoolBase + - ((dtxId & 0xFFu) * m_bindings.rpcServerStride); - } - m_remoteById[dtxId] = remoteHandle; - } - - m_transferById[dtxId] = TransferState{ - dtxId, - remoteHandle, - normalizedEeWorkAddress, - normalizedIopWorkAddress, - workSize, - }; - } - - (void)writeGuestPod(m_host, request.receive.address, remoteHandle); - if (request.receive.size > sizeof(uint32_t)) - { - (void)m_host.zeroGuest(request.receive.address + sizeof(uint32_t), - request.receive.size - sizeof(uint32_t)); - } - - result.handled = true; - result.resultAddress = request.receive.address; - return result; - } - - RpcResult handleDestroyTransport(const RpcRequest &request, RpcResult result) - { - uint32_t remoteHandle = 0u; - if (request.send.address != 0u && - request.send.size >= sizeof(uint32_t) && - readGuestPod(m_host, request.send.address, remoteHandle) && - remoteHandle != 0u) - { - std::lock_guard lock(m_mutex); - for (auto it = m_remoteById.begin(); it != m_remoteById.end(); ++it) - { - if (it->second == remoteHandle) - { - m_transferById.erase(it->first); - m_remoteById.erase(it); - break; - } - } - } - - if (request.receive.address != 0u && request.receive.size > 0u) - { - (void)m_host.zeroGuest(request.receive.address, request.receive.size); - } - result.handled = true; - result.resultAddress = request.receive.address; - return result; - } - - uint32_t allocateUrpcHandleLocked() - { - if (m_nextUrpcObject < m_bindings.urpcObjectBase || - m_nextUrpcObject >= m_bindings.urpcObjectLimit) - { - m_nextUrpcObject = m_bindings.urpcObjectBase; - } - - for (uint32_t attempt = 0u; attempt < 4096u; ++attempt) - { - const uint32_t candidate = m_nextUrpcObject; - m_nextUrpcObject += m_bindings.urpcObjectStride; - if (m_nextUrpcObject < m_bindings.urpcObjectBase || - m_nextUrpcObject >= m_bindings.urpcObjectLimit) - { - m_nextUrpcObject = m_bindings.urpcObjectBase; - } - - if (candidate < m_bindings.urpcObjectBase || - candidate >= m_bindings.urpcObjectLimit || - m_sjrmtByHandle.find(candidate) != m_sjrmtByHandle.end() || - m_sjxByHandle.find(candidate) != m_sjxByHandle.end() || - m_ps2RnaByHandle.find(candidate) != m_ps2RnaByHandle.end()) - { - continue; - } - - const bool usedByRemote = - std::any_of(m_remoteById.begin(), m_remoteById.end(), - [candidate](const auto &entry) - { return entry.second == candidate; }); - if (!usedByRemote) - { - return candidate; - } - } - - return m_bindings.urpcObjectBase; - } - - RpcResult emulateUrpc(const RpcRequest &request, - uint32_t command, - RpcResult result) - { - std::array output = {1u, 0u, 0u, 0u}; - uint32_t outputWordCount = 1u; - - auto readSendWord = [&](uint32_t index, uint32_t &value) - { - const uint64_t byteOffset = - static_cast(index) * sizeof(uint32_t); - if (request.send.address == 0u || - request.send.size < byteOffset + sizeof(uint32_t)) - { - value = 0u; - return false; - } - return readGuestPod(m_host, - request.send.address + static_cast(byteOffset), - value); - }; - - switch (command) - { - case 0u: // SJX_CREATE - { - uint32_t sourceSjHandle = 0u; - uint32_t destinationSjHandle = 0u; - uint32_t line = 0u; - uint32_t eeObjectAddress = 0u; - (void)readSendWord(0u, sourceSjHandle); - (void)readSendWord(1u, destinationSjHandle); - (void)readSendWord(2u, line); - (void)readSendWord(3u, eeObjectAddress); - - std::lock_guard lock(m_mutex); - const uint32_t handle = allocateUrpcHandleLocked(); - m_sjxByHandle[handle] = SjxState{ - handle, - sourceSjHandle, - destinationSjHandle, - line, - eeObjectAddress, - 0u, - }; - output[0] = handle != 0u ? handle : 1u; - break; - } - case 1u: // SJX_DESTROY - { - uint32_t handle = 0u; - (void)readSendWord(0u, handle); - std::lock_guard lock(m_mutex); - m_sjxByHandle.erase(handle); - output[0] = 1u; - break; - } - case 2u: // SJX_RESET - { - uint32_t handle = 0u; - uint32_t xid = 0u; - (void)readSendWord(0u, handle); - (void)readSendWord(1u, xid); - std::lock_guard lock(m_mutex); - const auto it = m_sjxByHandle.find(handle); - if (it != m_sjxByHandle.end()) - { - it->second.xid = static_cast(xid & 0xFFFFu); - } - output[0] = 1u; - break; - } - case 8u: // PS2RNA_CREATE - { - uint32_t maxChannels = 0u; - uint32_t sjHandle0 = 0u; - uint32_t sjHandle1 = 0u; - (void)readSendWord(0u, maxChannels); - (void)readSendWord(2u, sjHandle0); - (void)readSendWord(3u, sjHandle1); - - std::lock_guard lock(m_mutex); - const uint32_t handle = allocateUrpcHandleLocked(); - m_ps2RnaByHandle[handle] = Ps2RnaState{ - handle, - maxChannels, - sjHandle0, - sjHandle1, - maxChannels, - 0u, - 0u, - false, - }; - output[0] = handle != 0u ? handle : 1u; - break; - } - case 9u: // PS2RNA_DESTROY - { - uint32_t handle = 0u; - (void)readSendWord(0u, handle); - std::lock_guard lock(m_mutex); - m_ps2RnaByHandle.erase(handle); - output[0] = 1u; - break; - } - case 32u: // SJRMT_RBF_CREATE - case 33u: // SJRMT_MEM_CREATE - case 34u: // SJRMT_UNI_CREATE - { - uint32_t argument0 = 0u; - uint32_t argument1 = 0u; - uint32_t argument2 = 0u; - (void)readSendWord(0u, argument0); - (void)readSendWord(1u, argument1); - (void)readSendWord(2u, argument2); - - uint32_t mode = 0u; - uint32_t workAddress = 0u; - uint32_t workSize = 0u; - if (command == 34u) - { - mode = argument0; - workAddress = argument1; - workSize = argument2; - } - else if (command == 33u) - { - workAddress = argument0; - workSize = argument1; - } - else - { - workAddress = argument0; - workSize = argument1 != 0u ? argument1 : argument2; - } - workSize = normalizeSjrmtCapacity(workSize); - - std::lock_guard lock(m_mutex); - const uint32_t handle = allocateUrpcHandleLocked(); - m_sjrmtByHandle[handle] = SjrmtState{ - handle, - mode, - workAddress, - workSize, - 0u, - 0u, - workSize, - 0u, - 0x53524D54u, - handle, - workAddress, - workSize, - }; - output[0] = handle != 0u ? handle : 1u; - break; - } - case 35u: // SJRMT_DESTROY - { - uint32_t handle = 0u; - (void)readSendWord(0u, handle); - std::lock_guard lock(m_mutex); - m_sjrmtByHandle.erase(handle); - output[0] = 1u; - break; - } - case 36u: // SJRMT_GET_UUID - { - uint32_t handle = 0u; - (void)readSendWord(0u, handle); - std::lock_guard lock(m_mutex); - const auto it = m_sjrmtByHandle.find(handle); - if (it != m_sjrmtByHandle.end()) - { - output[0] = it->second.uuid0; - output[1] = it->second.uuid1; - output[2] = it->second.uuid2; - output[3] = it->second.uuid3; - } - else - { - output = {0u, 0u, 0u, 0u}; - } - outputWordCount = 4u; - break; - } - case 37u: // SJRMT_RESET - { - uint32_t handle = 0u; - (void)readSendWord(0u, handle); - std::lock_guard lock(m_mutex); - const auto it = m_sjrmtByHandle.find(handle); - if (it != m_sjrmtByHandle.end()) - { - const uint32_t capacity = it->second.workSize == 0u - ? kDefaultSjrmtCapacity - : it->second.workSize; - it->second.readPosition = 0u; - it->second.writePosition = 0u; - it->second.roomBytes = capacity; - it->second.dataBytes = 0u; - } - output[0] = 1u; - break; - } - case 38u: // SJRMT_GET_CHUNK - { - uint32_t handle = 0u; - uint32_t streamId = 0u; - uint32_t requestedBytes = 0u; - (void)readSendWord(0u, handle); - (void)readSendWord(1u, streamId); - (void)readSendWord(2u, requestedBytes); - - uint32_t pointer = 0u; - uint32_t length = 0u; - std::lock_guard lock(m_mutex); - const auto it = m_sjrmtByHandle.find(handle); - if (it != m_sjrmtByHandle.end()) - { - SjrmtState &state = it->second; - const uint32_t capacity = state.workSize == 0u - ? kDefaultSjrmtCapacity - : state.workSize; - if (streamId == 0u) - { - length = std::min(requestedBytes, state.roomBytes); - pointer = state.workAddress + - (capacity != 0u ? state.writePosition % capacity : 0u); - if (capacity != 0u) - { - state.writePosition = (state.writePosition + length) % capacity; - } - state.roomBytes -= length; - } - else if (streamId == 1u) - { - length = std::min(requestedBytes, state.dataBytes); - pointer = state.workAddress + - (capacity != 0u ? state.readPosition % capacity : 0u); - if (capacity != 0u) - { - state.readPosition = (state.readPosition + length) % capacity; - } - state.dataBytes -= length; - } - } - output[0] = pointer; - output[1] = length; - outputWordCount = 2u; - break; - } - case 39u: // SJRMT_UNGET_CHUNK - { - uint32_t handle = 0u; - uint32_t streamId = 0u; - uint32_t length = 0u; - (void)readSendWord(0u, handle); - (void)readSendWord(1u, streamId); - (void)readSendWord(3u, length); - - std::lock_guard lock(m_mutex); - const auto it = m_sjrmtByHandle.find(handle); - if (it != m_sjrmtByHandle.end()) - { - SjrmtState &state = it->second; - const uint32_t capacity = state.workSize == 0u - ? kDefaultSjrmtCapacity - : state.workSize; - const uint32_t delta = capacity == 0u ? 0u : length % capacity; - if (streamId == 0u) - { - if (capacity != 0u) - { - state.writePosition = - (state.writePosition + capacity - delta) % capacity; - } - state.roomBytes = std::min(capacity, state.roomBytes + length); - } - else if (streamId == 1u) - { - if (capacity != 0u) - { - state.readPosition = - (state.readPosition + capacity - delta) % capacity; - } - state.dataBytes = std::min(capacity, state.dataBytes + length); - } - } - output[0] = 1u; - break; - } - case 40u: // SJRMT_PUT_CHUNK - { - uint32_t handle = 0u; - uint32_t streamId = 0u; - uint32_t length = 0u; - (void)readSendWord(0u, handle); - (void)readSendWord(1u, streamId); - (void)readSendWord(3u, length); - - std::lock_guard lock(m_mutex); - const auto it = m_sjrmtByHandle.find(handle); - if (it != m_sjrmtByHandle.end()) - { - SjrmtState &state = it->second; - const uint32_t capacity = state.workSize == 0u - ? kDefaultSjrmtCapacity - : state.workSize; - if (streamId == 0u) - { - state.roomBytes = std::min(capacity, state.roomBytes + length); - } - else if (streamId == 1u) - { - state.dataBytes = std::min(capacity, state.dataBytes + length); - } - } - output[0] = 1u; - break; - } - case 41u: // SJRMT_GET_NUM_DATA - { - uint32_t handle = 0u; - uint32_t streamId = 0u; - (void)readSendWord(0u, handle); - (void)readSendWord(1u, streamId); - - std::lock_guard lock(m_mutex); - const auto it = m_sjrmtByHandle.find(handle); - output[0] = it != m_sjrmtByHandle.end() - ? (streamId == 0u ? it->second.roomBytes - : it->second.dataBytes) - : 0u; - break; - } - case 42u: // SJRMT_IS_GET_CHUNK - { - uint32_t handle = 0u; - uint32_t streamId = 0u; - uint32_t requestedBytes = 0u; - (void)readSendWord(0u, handle); - (void)readSendWord(1u, streamId); - (void)readSendWord(2u, requestedBytes); - - uint32_t available = 0u; - std::lock_guard lock(m_mutex); - const auto it = m_sjrmtByHandle.find(handle); - if (it != m_sjrmtByHandle.end()) - { - available = streamId == 0u ? it->second.roomBytes - : it->second.dataBytes; - } - output[0] = available >= requestedBytes ? 1u : 0u; - output[1] = available; - outputWordCount = 2u; - break; - } - case 43u: // SJRMT_INIT - case 44u: // SJRMT_FINISH - output[0] = 1u; - break; - default: - { - uint32_t value = 1u; - if (request.send.address != 0u && - request.send.size >= sizeof(uint32_t)) - { - (void)readGuestPod(m_host, request.send.address, value); - } - if (command == 0u) - { - std::lock_guard lock(m_mutex); - value = allocateUrpcHandleLocked(); - } - output[0] = value != 0u ? value : 1u; - break; - } - } - - if (request.receive.address != 0u && request.receive.size > 0u) - { - const uint32_t receiveWordCapacity = - request.receive.size / sizeof(uint32_t); - const uint32_t wordsToWrite = - std::min(outputWordCount, receiveWordCapacity); - for (uint32_t index = 0u; index < wordsToWrite; ++index) - { - (void)writeGuestPod(m_host, - request.receive.address + - (index * sizeof(uint32_t)), - output[index]); - } - - // The original HLE deliberately makes rbuf[1] available to command - // 42 callers even when their declared output word count is one. - if (command == 42u && outputWordCount > 1u) - { - (void)writeGuestPod(m_host, - request.receive.address + sizeof(uint32_t), - output[1]); - } - - const uint32_t writtenBytes = wordsToWrite * sizeof(uint32_t); - if (request.receive.size > writtenBytes) - { - (void)m_host.zeroGuest(request.receive.address + writtenBytes, - request.receive.size - writtenBytes); - } - } - - { - std::lock_guard lock(m_mutex); - ++m_urpcCalls; - } - result.handled = true; - result.resultAddress = request.receive.address; - return result; - } - - bool hasReadableRange(uint32_t address, uint32_t length) const - { - if (length == 0u) - { - return true; - } - const uint32_t lastAddress = address + length - 1u; - if (address > lastAddress) - { - return false; - } - - uint8_t byte = 0u; - return m_host.readGuest(address, &byte, sizeof(byte)) && - m_host.readGuest(lastAddress, &byte, sizeof(byte)); - } - - bool readValidCommandCount(uint32_t workAddress, - uint32_t workSize, - uint32_t &commandCount) const - { - commandCount = 0u; - if (workSize < kMinimumDtxWorkSize || - !readGuestPod(m_host, workAddress, commandCount) || - commandCount == 0u || - commandCount > kMaxDtxCommands) - { - return false; - } - - const uint64_t commandBytes = - static_cast(commandCount) * kDtxCommandSize; - const uint64_t requiredBytes = - kDtxHeaderSize + commandBytes + sizeof(uint32_t); - return requiredBytes <= workSize; - } - - bool looksLikeSjxPayloadLocked(uint32_t workAddress, uint32_t workSize) const - { - uint32_t commandCount = 0u; - if (!readValidCommandCount(workAddress, workSize, commandCount)) - { - return false; - } - - for (uint32_t index = 0u; index < commandCount; ++index) - { - const uint32_t commandAddress = - workAddress + kDtxHeaderSize + (index * kDtxCommandSize); - std::array command{}; - if (!m_host.readGuest(commandAddress, command.data(), command.size())) - { - break; - } - - const uint8_t commandNumber = command[0]; - const uint8_t line = command[1]; - uint16_t xid = 0u; - uint32_t sjxHandle = 0u; - uint32_t chunkDataAddress = 0u; - uint32_t chunkLength = 0u; - std::memcpy(&xid, command.data() + 2u, sizeof(xid)); - std::memcpy(&sjxHandle, command.data() + 4u, sizeof(sjxHandle)); - std::memcpy(&chunkDataAddress, command.data() + 8u, - sizeof(chunkDataAddress)); - std::memcpy(&chunkLength, command.data() + 12u, sizeof(chunkLength)); - - if (commandNumber != 0u || chunkLength == 0u || - !hasReadableRange(chunkDataAddress, chunkLength)) - { - continue; - } - - const auto sjx = m_sjxByHandle.find(sjxHandle); - if (sjx == m_sjxByHandle.end() || - (sjx->second.xid != 0u && sjx->second.xid != xid) || - line != sjx->second.line || - m_sjrmtByHandle.find(sjx->second.destinationSjHandle) == - m_sjrmtByHandle.end()) - { - continue; - } - return true; - } - return false; - } - - bool looksLikePs2RnaPayloadLocked(uint32_t workAddress, - uint32_t workSize) const - { - uint32_t commandCount = 0u; - if (!readValidCommandCount(workAddress, workSize, commandCount)) - { - return false; - } - - for (uint32_t index = 0u; index < commandCount; ++index) - { - const uint32_t commandAddress = - workAddress + kDtxHeaderSize + (index * kDtxCommandSize); - std::array command{}; - if (!m_host.readGuest(commandAddress, command.data(), command.size())) - { - break; - } - - uint16_t commandNumber = 0u; - uint32_t rnaHandle = 0u; - std::memcpy(&commandNumber, command.data(), sizeof(commandNumber)); - std::memcpy(&rnaHandle, command.data() + 4u, sizeof(rnaHandle)); - if (commandNumber <= 5u && - m_ps2RnaByHandle.find(rnaHandle) != m_ps2RnaByHandle.end()) - { - return true; - } - } - return false; - } - - bool inferTransferFromPayloadLocked(uint32_t sourceAddress, - uint32_t destinationAddress, - uint32_t size, - TransferState &result) const - { - if (m_transferById.empty()) - { - return false; - } - - if (looksLikeSjxPayloadLocked(sourceAddress, size)) - { - result = TransferState{0u, 0u, sourceAddress, destinationAddress, size}; - return true; - } - if (looksLikePs2RnaPayloadLocked(sourceAddress, size)) - { - result = TransferState{1u, 0u, sourceAddress, destinationAddress, size}; - return true; - } - return false; - } - - bool copyBytes(uint32_t destinationAddress, - uint32_t sourceAddress, - uint32_t length) - { - for (uint32_t index = 0u; index < length; ++index) - { - uint8_t value = 0u; - if (!m_host.readGuest(sourceAddress + index, &value, sizeof(value)) || - !m_host.writeGuest(destinationAddress + index, &value, sizeof(value))) - { - return false; - } - } - return true; - } - - void appendToSjrmtData(SjrmtState &state, - uint32_t sourceDataAddress, - uint32_t requestedLength) - { - const uint32_t capacity = state.workSize == 0u - ? kDefaultSjrmtCapacity - : state.workSize; - if (capacity == 0u || state.workAddress == 0u || - state.roomBytes == 0u || requestedLength == 0u) - { - return; - } - - const uint32_t requestedCopyLength = - std::min(requestedLength, state.roomBytes); - uint32_t copiedLength = 0u; - for (uint32_t index = 0u; index < requestedCopyLength; ++index) - { - const uint32_t writeOffset = (state.writePosition + index) % capacity; - if (!copyBytes(state.workAddress + writeOffset, - sourceDataAddress + index, - 1u)) - { - break; - } - ++copiedLength; - } - - state.writePosition = (state.writePosition + copiedLength) % capacity; - state.roomBytes -= copiedLength; - state.dataBytes = std::min(capacity, state.dataBytes + copiedLength); - } - - static void consumeSjrmtData(SjrmtState &state) - { - const uint32_t capacity = state.workSize == 0u - ? kDefaultSjrmtCapacity - : state.workSize; - if (capacity == 0u || state.dataBytes == 0u) - { - return; - } - - const uint32_t consumed = std::min(capacity, state.dataBytes); - state.readPosition = (state.readPosition + consumed) % capacity; - state.dataBytes -= consumed; - state.roomBytes = std::min(capacity, state.roomBytes + consumed); - } - - void consumeActivePs2RnaStreamsLocked() - { - for (const auto &[handle, rna] : m_ps2RnaByHandle) - { - (void)handle; - if (!rna.playEnabled) - { - continue; - } - - auto consumeHandle = [&](uint32_t sjHandle) - { - if (sjHandle == 0u) - { - return; - } - const auto it = m_sjrmtByHandle.find(sjHandle); - if (it != m_sjrmtByHandle.end()) - { - consumeSjrmtData(it->second); - } - }; - consumeHandle(rna.sjHandle0); - consumeHandle(rna.sjHandle1); - } - } - - void applySjxPayload(const TransferState &transfer) - { - if (transfer.eeWorkAddress == 0u || - transfer.workSize < kMinimumDtxWorkSize) - { - return; - } - - uint32_t commandCount = 0u; - if (!readGuestPod(m_host, transfer.eeWorkAddress, commandCount)) - { - return; - } - commandCount = std::min(commandCount, kMaxDtxCommands); - if (commandCount == 0u) - { - return; - } - - std::lock_guard lock(m_mutex); - for (uint32_t index = 0u; index < commandCount; ++index) - { - const uint32_t commandAddress = - transfer.eeWorkAddress + kDtxHeaderSize + - (index * kDtxCommandSize); - std::array command{}; - if (!m_host.readGuest(commandAddress, command.data(), command.size())) - { - break; - } - - const uint8_t commandNumber = command[0]; - const uint8_t line = command[1]; - uint16_t xid = 0u; - uint32_t sjxHandle = 0u; - uint32_t chunkDataAddress = 0u; - uint32_t chunkLength = 0u; - std::memcpy(&xid, command.data() + 2u, sizeof(xid)); - std::memcpy(&sjxHandle, command.data() + 4u, sizeof(sjxHandle)); - std::memcpy(&chunkDataAddress, command.data() + 8u, - sizeof(chunkDataAddress)); - std::memcpy(&chunkLength, command.data() + 12u, sizeof(chunkLength)); - - if (commandNumber != 0u || chunkLength == 0u) - { - continue; - } - - const auto sjx = m_sjxByHandle.find(sjxHandle); - if (sjx == m_sjxByHandle.end() || - (sjx->second.xid != 0u && sjx->second.xid != xid)) - { - continue; - } - const auto sjrmt = - m_sjrmtByHandle.find(sjx->second.destinationSjHandle); - if (sjrmt == m_sjrmtByHandle.end() || line != sjx->second.line) - { - continue; - } - - appendToSjrmtData(sjrmt->second, chunkDataAddress, chunkLength); - (void)writeGuestPod(m_host, commandAddress + 4u, sjx->second.eeObjectAddress); - constexpr uint8_t roomLine = 0u; - (void)m_host.writeGuest(commandAddress + 1u, &roomLine, sizeof(roomLine)); - } - consumeActivePs2RnaStreamsLocked(); - } - - void applyPs2RnaPayload(const TransferState &transfer) - { - if (transfer.eeWorkAddress == 0u || - transfer.workSize < kMinimumDtxWorkSize) - { - return; - } - - uint32_t commandCount = 0u; - if (!readGuestPod(m_host, transfer.eeWorkAddress, commandCount)) - { - return; - } - commandCount = std::min(commandCount, kMaxDtxCommands); - if (commandCount == 0u) - { - return; - } - - std::lock_guard lock(m_mutex); - for (uint32_t index = 0u; index < commandCount; ++index) - { - const uint32_t commandAddress = - transfer.eeWorkAddress + kDtxHeaderSize + - (index * kDtxCommandSize); - std::array command{}; - if (!m_host.readGuest(commandAddress, command.data(), command.size())) - { - break; - } - - uint16_t commandNumber = 0u; - uint32_t rnaHandle = 0u; - uint32_t argument1 = 0u; - uint32_t argument2 = 0u; - std::memcpy(&commandNumber, command.data(), sizeof(commandNumber)); - std::memcpy(&rnaHandle, command.data() + 4u, sizeof(rnaHandle)); - std::memcpy(&argument1, command.data() + 8u, sizeof(argument1)); - std::memcpy(&argument2, command.data() + 12u, sizeof(argument2)); - - const auto it = m_ps2RnaByHandle.find(rnaHandle); - if (it == m_ps2RnaByHandle.end()) - { - continue; - } - - Ps2RnaState &state = it->second; - switch (commandNumber) - { - case 0u: - state.playEnabled = true; - break; - case 1u: - state.playEnabled = false; - break; - case 2u: - state.playEnabled = argument1 != 0u; - break; - case 3u: - state.channelCount = argument1; - break; - case 4u: - state.sampleFrequency = argument1; - break; - case 5u: - state.volume = argument2; - break; - default: - break; - } - } - consumeActivePs2RnaStreamsLocked(); - } - - IopHost &m_host; - CriDtxBindings m_bindings; - mutable std::mutex m_mutex; - std::unordered_map m_remoteById; - std::unordered_map m_transferById; - std::unordered_map m_sjxByHandle; - std::unordered_map m_ps2RnaByHandle; - std::unordered_map m_sjrmtByHandle; - uint32_t m_nextUrpcObject = 0u; - uint64_t m_dmaAcks = 0u; - uint64_t m_dmaMisses = 0u; - uint64_t m_urpcCalls = 0u; - const std::array m_sids; - }; - } - - std::unique_ptr createCriDtxService(IopHost &host, - CriDtxBindings bindings) - { - if (bindings.serviceName.empty() || bindings.sid == 0u || - bindings.urpcObjectBase == 0u || - bindings.urpcObjectLimit <= bindings.urpcObjectBase || - bindings.urpcObjectStride == 0u || - bindings.urpcFunctionTableBase == 0u || - bindings.urpcObjectTableBase == 0u || - bindings.dispatcherFunctionAddress == 0u || - bindings.rpcServerPoolBase == 0u || - bindings.rpcServerStride == 0u) - { - throw std::invalid_argument("invalid CRI DTX bindings"); - } - return std::make_unique(host, std::move(bindings)); - } -} diff --git a/ps2xIOP/src/modules/dbcman.cpp b/ps2xIOP/src/modules/dbcman.cpp index 6c5e63d..c698a34 100644 --- a/ps2xIOP/src/modules/dbcman.cpp +++ b/ps2xIOP/src/modules/dbcman.cpp @@ -1,4 +1,5 @@ #include "module_factories.h" +#include "rpc_reply.h" #include #include @@ -12,9 +13,11 @@ namespace ps2x::iop::detail { constexpr uint32_t kDbcManSid = 0x80001300u; constexpr uint32_t kRpcCheckVersion = 0x80001363u; - constexpr uint32_t kDbcManVersion = 0x0320u; constexpr uint32_t kMaxUnknownRpcLogs = 32u; + constexpr std::array kSupportedVersions{0x0310u, 0x0320u}; + constexpr uint16_t kReportedVersion = kSupportedVersions.front(); + class DbcmanService final : public IopService { public: @@ -33,10 +36,17 @@ namespace ps2x::iop::detail return kSids; } + [[nodiscard]] std::span moduleAliases() const override + { + return kModuleAliases; + } + void reset() override { std::lock_guard lock(m_mutex); m_unknownRpcLogCount = 0u; + m_versionQueryCount = 0u; + m_failedVersionReplies = 0u; } [[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override @@ -56,12 +66,21 @@ namespace ps2x::iop::detail if (request.function == kRpcCheckVersion) { - const uint32_t wordCount = request.receive.size / sizeof(uint32_t); - const uint32_t count = wordCount < 4u ? wordCount : 4u; - for (uint32_t index = 0u; index < count; ++index) + const uint32_t version = kReportedVersion; + const std::array reply{version, version, version, version}; + const bool written = writeRpcWords(m_host, request.receive, reply); + bool firstQuery = false; { - const uint32_t address = request.receive.address + index * sizeof(uint32_t); - (void)m_host.writeGuest(address, &kDbcManVersion, sizeof(kDbcManVersion)); + std::lock_guard lock(m_mutex); + firstQuery = m_versionQueryCount++ == 0u; + if (!written) + ++m_failedVersionReplies; + } + if (firstQuery) + { + std::ostringstream message; + message << "[DBCMAN:HLE] check-version reply=0x" << std::hex << version; + m_host.log(LogLevel::Info, message.str()); } return result; } @@ -94,15 +113,21 @@ namespace ps2x::iop::detail void appendDebugMetrics(std::vector &metrics) const override { std::lock_guard lock(m_mutex); + metrics.push_back({"reported_version", kReportedVersion, true}); + metrics.push_back({"version_queries", m_versionQueryCount, false}); + metrics.push_back({"failed_version_replies", m_failedVersionReplies, false}); metrics.push_back({"unknown_rpc_logs", m_unknownRpcLogCount, false}); } private: inline static constexpr std::array kSids{kDbcManSid}; + inline static constexpr std::array kModuleAliases{"dbcman", "dbcm", "dbcmserv"}; IopHost &m_host; mutable std::mutex m_mutex; uint32_t m_unknownRpcLogCount = 0u; + uint64_t m_versionQueryCount = 0u; + uint64_t m_failedVersionReplies = 0u; }; } diff --git a/ps2xIOP/src/modules/libsd.cpp b/ps2xIOP/src/modules/libsd.cpp index 56d9a5c..974b86c 100644 --- a/ps2xIOP/src/modules/libsd.cpp +++ b/ps2xIOP/src/modules/libsd.cpp @@ -27,6 +27,11 @@ namespace ps2x::iop::detail return kSids; } + [[nodiscard]] std::span moduleAliases() const override + { + return kModuleAliases; + } + void reset() override { } @@ -51,6 +56,7 @@ namespace ps2x::iop::detail private: inline static constexpr std::array kSids{kLibSdSid}; + inline static constexpr std::array kModuleAliases{"libsd"}; IopHost &m_host; }; diff --git a/ps2xIOP/src/modules/mcserv.cpp b/ps2xIOP/src/modules/mcserv.cpp index b3a71c2..ce23759 100644 --- a/ps2xIOP/src/modules/mcserv.cpp +++ b/ps2xIOP/src/modules/mcserv.cpp @@ -1,4 +1,5 @@ #include "../iop_service.h" +#include "../rpc_reply.h" #include #include @@ -82,43 +83,75 @@ namespace ps2x::iop::detail flavor = Flavor::NewXmcserv; switch (function) { - case 0xFEu: return Operation::Init; - case 0x01u: return Operation::GetInfo; - case 0x02u: return Operation::Open; - case 0x03u: return Operation::Close; - case 0x04u: return Operation::Seek; - case 0x05u: return Operation::Read; - case 0x06u: return Operation::Write; - case 0x0Au: return Operation::Flush; - case 0x0Cu: return Operation::Chdir; - case 0x0Du: return Operation::GetDir; - case 0x0Eu: return Operation::SetInfo; - case 0x0Fu: return Operation::Delete; - case 0x10u: return Operation::Format; - case 0x11u: return Operation::Unformat; - case 0x12u: return Operation::GetEnt; - case 0x14u: return Operation::ChangePriority; - default: break; + case 0xFEu: + return Operation::Init; + case 0x01u: + return Operation::GetInfo; + case 0x02u: + return Operation::Open; + case 0x03u: + return Operation::Close; + case 0x04u: + return Operation::Seek; + case 0x05u: + return Operation::Read; + case 0x06u: + return Operation::Write; + case 0x0Au: + return Operation::Flush; + case 0x0Cu: + return Operation::Chdir; + case 0x0Du: + return Operation::GetDir; + case 0x0Eu: + return Operation::SetInfo; + case 0x0Fu: + return Operation::Delete; + case 0x10u: + return Operation::Format; + case 0x11u: + return Operation::Unformat; + case 0x12u: + return Operation::GetEnt; + case 0x14u: + return Operation::ChangePriority; + default: + break; } flavor = Flavor::OldMcserv; switch (function) { - case 0x70u: return Operation::Init; - case 0x71u: return Operation::Open; - case 0x72u: return Operation::Close; - case 0x73u: return Operation::Read; - case 0x74u: return Operation::Write; - case 0x75u: return Operation::Seek; - case 0x76u: return Operation::GetDir; - case 0x77u: return Operation::Format; - case 0x78u: return Operation::GetInfo; - case 0x79u: return Operation::Delete; - case 0x7Au: return Operation::Flush; - case 0x7Bu: return Operation::Chdir; - case 0x7Cu: return Operation::SetInfo; - case 0x80u: return Operation::Unformat; - default: return Operation::Unknown; + case 0x70u: + return Operation::Init; + case 0x71u: + return Operation::Open; + case 0x72u: + return Operation::Close; + case 0x73u: + return Operation::Read; + case 0x74u: + return Operation::Write; + case 0x75u: + return Operation::Seek; + case 0x76u: + return Operation::GetDir; + case 0x77u: + return Operation::Format; + case 0x78u: + return Operation::GetInfo; + case 0x79u: + return Operation::Delete; + case 0x7Au: + return Operation::Flush; + case 0x7Bu: + return Operation::Chdir; + case 0x7Cu: + return Operation::SetInfo; + case 0x80u: + return Operation::Unformat; + default: + return Operation::Unknown; } } @@ -136,6 +169,7 @@ namespace ps2x::iop::detail [[nodiscard]] std::string_view name() const override { return "MCSERV"; } [[nodiscard]] std::span sids() const override { return m_sids; } + [[nodiscard]] std::span moduleAliases() const override { return m_moduleAliases; } void reset() override { @@ -156,8 +190,8 @@ namespace ps2x::iop::detail const Operation operation = decodeOperation(request.function, flavor); if (operation == Operation::Init) { - (void)call(MemoryCardOperation::Init); - writeInitResult(request.receive); + const int32_t result = call(MemoryCardOperation::Init); + writeInitResult(request.receive, flavor, result); return response; } @@ -173,7 +207,7 @@ namespace ps2x::iop::detail { NameParameter parameter{}; if (request.send.address != 0u && - request.send.size >= offsetof(NameParameter, name) && + request.send.size >= sizeof(parameter) && m_host.readGuest(request.send.address, ¶meter, sizeof(parameter))) { result = handleNameOperation(operation, request.send.address, parameter); @@ -189,22 +223,15 @@ namespace ps2x::iop::detail if (operation == Operation::Write && parameter.origin > 0 && parameter.origin <= static_cast(sizeof(parameter.data))) { - const uint32_t inlineAddress = - request.send.address + static_cast(offsetof(DescriptorParameter, data)); - const int32_t prefix = call(MemoryCardOperation::Write, - static_cast(parameter.fd), - inlineAddress, - static_cast(parameter.origin)); + const uint32_t inlineAddress = request.send.address + static_cast(offsetof(DescriptorParameter, data)); + const int32_t prefix = call(MemoryCardOperation::Write, static_cast(parameter.fd), inlineAddress, static_cast(parameter.origin)); if (prefix < 0) { result = prefix; } else { - const int32_t body = call(MemoryCardOperation::Write, - static_cast(parameter.fd), - parameter.buffer, - static_cast(std::max(parameter.size, 0))); + const int32_t body = call(MemoryCardOperation::Write, static_cast(parameter.fd), parameter.buffer, static_cast(std::max(parameter.size, 0))); result = body < 0 ? body : prefix + body; } } @@ -238,40 +265,20 @@ namespace ps2x::iop::detail void writeResult(GuestBuffer receive, int32_t result) { - if (receive.address == 0u || receive.size < sizeof(result)) - { - return; - } - (void)m_host.writeGuest(receive.address, &result, sizeof(result)); - if (receive.size > sizeof(result)) - { - (void)m_host.zeroGuest(receive.address + sizeof(result), - receive.size - sizeof(result)); - } + const std::array values{static_cast(result)}; + (void)writeRpcWords(m_host, receive, values); } - void writeInitResult(GuestBuffer receive) + void writeInitResult(GuestBuffer receive, Flavor flavor, int32_t result) { - if (receive.address == 0u || receive.size < sizeof(int32_t)) - { - return; - } - const std::array values = { - static_cast(kSucceeded), kMcservVersion, kMcmanVersion}; - const uint32_t bytes = std::min(receive.size, sizeof(values)); - (void)m_host.writeGuest(receive.address, values.data(), bytes); - if (receive.size > bytes) - { - (void)m_host.zeroGuest(receive.address + bytes, receive.size - bytes); - } + const std::array values = {static_cast(result), kMcservVersion, kMcmanVersion}; + const size_t count = flavor == Flavor::NewXmcserv ? values.size() : 1u; + (void)writeRpcWords(m_host, receive, std::span(values.data(), count)); } - int32_t handleNameOperation(Operation operation, - uint32_t sendAddress, - const NameParameter ¶meter) + int32_t handleNameOperation(Operation operation, uint32_t sendAddress, const NameParameter ¶meter) { - const uint32_t nameAddress = - sendAddress + static_cast(offsetof(NameParameter, name)); + const uint32_t nameAddress = sendAddress + static_cast(offsetof(NameParameter, name)); const uint32_t port = static_cast(parameter.port); const uint32_t slot = static_cast(parameter.slot); switch (operation) @@ -281,12 +288,9 @@ namespace ps2x::iop::detail { return call(MemoryCardOperation::Mkdir, port, slot, nameAddress); } - return call(MemoryCardOperation::Open, - port, slot, nameAddress, - static_cast(parameter.flags)); + return call(MemoryCardOperation::Open, port, slot, nameAddress, static_cast(parameter.flags)); case Operation::Chdir: - return call(MemoryCardOperation::Chdir, - port, slot, nameAddress, parameter.pointer); + return call(MemoryCardOperation::Chdir, port, slot, nameAddress, parameter.pointer); case Operation::SetInfo: return call(MemoryCardOperation::SetFileInfo, port, slot, nameAddress); case Operation::Delete: @@ -302,9 +306,7 @@ namespace ps2x::iop::detail } } - int32_t handleDescriptorOperation(Operation operation, - Flavor flavor, - const DescriptorParameter ¶meter) + int32_t handleDescriptorOperation(Operation operation, Flavor flavor, const DescriptorParameter ¶meter) { switch (operation) { @@ -341,8 +343,7 @@ namespace ps2x::iop::detail case Operation::Read: if (parameter.parameter != 0u) { - (void)m_host.zeroGuest(parameter.parameter, - flavor == Flavor::NewXmcserv ? 192u : 64u); + (void)m_host.zeroGuest(parameter.parameter, flavor == Flavor::NewXmcserv ? 192u : 64u); } return call(MemoryCardOperation::Read, static_cast(parameter.fd), @@ -392,6 +393,7 @@ namespace ps2x::iop::detail mutable std::mutex m_mutex; uint32_t m_unknownRpcLogCount = 0u; const std::array m_sids = {kMcservSid, kMcservDev9Sid}; + const std::array m_moduleAliases = {"mcserv", "xmcserv"}; }; } diff --git a/ps2xIOP/src/modules/sdrdrv.cpp b/ps2xIOP/src/modules/sdrdrv.cpp deleted file mode 100644 index bd86e26..0000000 --- a/ps2xIOP/src/modules/sdrdrv.cpp +++ /dev/null @@ -1,335 +0,0 @@ -#include "module_factories.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace ps2x::iop::detail -{ - namespace - { - constexpr uint32_t kEeRamSize = 32u * 1024u * 1024u; - - class SdrdrvService final : public IopService - { - public: - SdrdrvService(IopHost &host, SdrdrvBindings bindings) - : m_host(host), m_bindings(std::move(bindings)), m_sids{m_bindings.sid} - { - } - - std::string_view name() const override { return m_bindings.serviceName; } - std::span sids() const override { return m_sids; } - - void reset() override - { - std::lock_guard lock(m_mutex); - m_headerWarnCount = 0; - m_bodyWarnCount = 0; - } - - RpcResult handleRpc(const RpcRequest &request) override - { - RpcResult result; - if (request.sid != m_bindings.sid) - { - return result; - } - - result.handled = true; - result.resultAddress = request.receive.address; - if (m_bindings.clearReceiveBeforeDispatch && - request.receive.address && request.receive.size) - { - (void)m_host.zeroGuest(request.receive.address, request.receive.size); - } - - if (request.function == m_bindings.initFunction) - { - if (!loadImageHeader()) - { - warnHeader(); - } - return result; - } - if (request.function == m_bindings.shutdownFunction) - { - return result; - } - if (request.function != m_bindings.submitFunction) - { - return result; - } - - const uint32_t count = std::min(request.send.size / m_bindings.commandBytes, - m_bindings.maxCommands); - for (uint32_t commandIndex = 0; commandIndex < count; ++commandIndex) - { - std::vector words(m_bindings.commandBytes / sizeof(uint32_t)); - const uint32_t commandAddress = request.send.address + - commandIndex * m_bindings.commandBytes; - if (!m_host.readGuest(commandAddress, - words.data(), - m_bindings.commandBytes)) - { - continue; - } - - if (words[0] == m_bindings.headerCommand) - { - if (!loadImageHeader()) - { - warnHeader(); - } - continue; - } - if (words[0] != m_bindings.loadCommand) - { - continue; - } - - const uint32_t lbn = words[m_bindings.lbnWord]; - const uint32_t byteCount = words[m_bindings.byteCountWord]; - const uint32_t destination = words[m_bindings.destinationWord]; - const bool eeLoad = words[m_bindings.destinationKindWord] == - m_bindings.eeDestinationKind; - const uint32_t loadId = words[m_bindings.loadIdWord]; - const bool loaded = eeLoad - ? readBody(lbn, byteCount, destination) - : m_bindings.pretendNonEeLoadsComplete; - if (eeLoad && !loaded) - { - (void)m_host.zeroGuest(destination, byteCount); - bool shouldWarn = false; - { - std::lock_guard lock(m_mutex); - if (m_bodyWarnCount < m_bindings.bodyWarningLimit) - { - ++m_bodyWarnCount; - shouldWarn = true; - } - } - if (shouldWarn) - { - std::ostringstream message; - message << '[' << m_bindings.serviceName - << "] failed data read lbn=0x" << std::hex << lbn - << " bytes=0x" << byteCount << " dst=0x" << destination; - m_host.log(LogLevel::Warning, message.str()); - } - } - if (loaded || m_bindings.completeFailedLoads) - { - markLoadComplete(request.receive, loadId); - } - } - return result; - } - - void appendDebugMetrics(std::vector &metrics) const override - { - std::lock_guard lock(m_mutex); - metrics.push_back({"header_warnings", m_headerWarnCount, false}); - metrics.push_back({"body_warnings", m_bodyWarnCount, false}); - } - - private: - uint64_t openSiblingFile(const std::string &lowerName, - const std::string &upperName) - { - const std::array roots = { - m_host.hostPath(HostPathKind::CdRoot), - m_host.hostPath(HostPathKind::ElfDirectory), - }; - for (const std::string &rootValue : roots) - { - if (rootValue.empty()) - { - continue; - } - const std::filesystem::path root(rootValue); - for (const std::string *name : {&lowerName, &upperName}) - { - if (name->empty()) - { - continue; - } - const std::filesystem::path candidate = root / *name; - const uint64_t handle = m_host.openHostFile(candidate.string()); - if (handle != 0u) - { - return handle; - } - } - } - return 0u; - } - - bool copyHostRange(uint64_t handle, - uint64_t offset, - uint32_t destination, - uint64_t byteCount) - { - if (byteCount == 0) - { - return true; - } - std::array chunk{}; - uint64_t copied = 0; - while (copied < byteCount) - { - const size_t wanted = static_cast(std::min(chunk.size(), byteCount - copied)); - std::fill(chunk.begin(), chunk.begin() + static_cast(wanted), 0u); - size_t got = 0u; - if (!m_host.readHostFile(handle, - offset + copied, - chunk.data(), - wanted, - got) || - got > wanted || - !m_host.writeGuest(destination + static_cast(copied), - chunk.data(), - wanted)) - { - return false; - } - copied += wanted; - } - return true; - } - - bool loadImageHeader() - { - const uint64_t handle = openSiblingFile(m_bindings.imageHeaderLowerName, - m_bindings.imageHeaderUpperName); - if (handle == 0u) - { - return false; - } - uint64_t fileSize = 0u; - if (!m_host.hostFileSize(handle, fileSize)) - { - m_host.closeHostFile(handle); - return false; - } - uint32_t normalized = 0; - if (!m_host.normalizeGuestAddress(m_bindings.imageHeaderAddress, normalized) || - normalized >= kEeRamSize) - { - m_host.closeHostFile(handle); - return false; - } - const bool copied = copyHostRange(handle, - 0u, - m_bindings.imageHeaderAddress, - std::min(fileSize, - kEeRamSize - normalized)); - m_host.closeHostFile(handle); - return copied; - } - - bool readBody(uint32_t lbn, uint32_t byteCount, uint32_t destination) - { - uint32_t normalized = 0; - if (!m_host.normalizeGuestAddress(destination, normalized) || normalized >= kEeRamSize) - { - return false; - } - uint64_t handle = openSiblingFile(m_bindings.imageBodyLowerName, - m_bindings.imageBodyUpperName); - if (handle == 0u && m_bindings.fallbackBodyToCdImage) - { - handle = m_host.openHostFile(m_host.hostPath(HostPathKind::CdImage)); - } - if (handle == 0u) - { - return false; - } - const uint64_t bytes = std::min(byteCount, kEeRamSize - normalized); - const bool copied = copyHostRange(handle, - static_cast(lbn) * m_bindings.sectorSize, - destination, - bytes); - m_host.closeHostFile(handle); - return copied; - } - - void markLoadComplete(GuestBuffer receive, uint32_t loadId) - { - const uint32_t offset = m_bindings.statusOffset + - ((loadId & m_bindings.statusSlotMask) * - m_bindings.statusStride); - if (receive.address && offset < receive.size) - { - const uint8_t complete = m_bindings.completeValue; - (void)m_host.writeGuest(receive.address + offset, &complete, sizeof(complete)); - } - } - - void warnHeader() - { - bool shouldWarn = false; - { - std::lock_guard lock(m_mutex); - if (m_headerWarnCount < m_bindings.headerWarningLimit) - { - ++m_headerWarnCount; - shouldWarn = true; - } - } - if (shouldWarn) - { - m_host.log(LogLevel::Warning, - '[' + m_bindings.serviceName + "] failed to load image header"); - } - } - - IopHost &m_host; - SdrdrvBindings m_bindings; - std::array m_sids; - mutable std::mutex m_mutex; - uint32_t m_headerWarnCount = 0; - uint32_t m_bodyWarnCount = 0; - }; - } - - std::unique_ptr createSdrdrvService(IopHost &host, - SdrdrvBindings bindings) - { - const uint32_t largestWord = std::max({bindings.lbnWord, - bindings.byteCountWord, - bindings.destinationWord, - bindings.destinationKindWord, - bindings.loadIdWord}); - if (bindings.serviceName.empty() || - bindings.sid == 0u || - bindings.imageHeaderAddress == 0u || - bindings.commandBytes == 0u || - (bindings.commandBytes % sizeof(uint32_t)) != 0u || - largestWord >= bindings.commandBytes / sizeof(uint32_t) || - bindings.maxCommands == 0u || - bindings.sectorSize == 0u || - bindings.statusStride == 0u || - bindings.initFunction == bindings.submitFunction || - bindings.initFunction == bindings.shutdownFunction || - bindings.submitFunction == bindings.shutdownFunction || - bindings.headerCommand == bindings.loadCommand || - (bindings.imageHeaderLowerName.empty() && - bindings.imageHeaderUpperName.empty()) || - (bindings.imageBodyLowerName.empty() && - bindings.imageBodyUpperName.empty() && - !bindings.fallbackBodyToCdImage)) - { - throw std::invalid_argument("invalid SDRDRV bindings"); - } - return std::make_unique(host, std::move(bindings)); - } -} diff --git a/ps2xIOP/src/modules/sound_update_stub.cpp b/ps2xIOP/src/modules/sound_update_stub.cpp deleted file mode 100644 index 68c1c8c..0000000 --- a/ps2xIOP/src/modules/sound_update_stub.cpp +++ /dev/null @@ -1,236 +0,0 @@ -#include "module_factories.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -namespace ps2x::iop::detail -{ - namespace - { - constexpr uint16_t kPlayStreamCommand = 1u; - constexpr uint32_t kResponseRecordStride = 0x20u; - constexpr uint32_t kPackedStreamOffset = 4u; - constexpr uint32_t kStreamSlotMask = 0x3Fu; - constexpr uint32_t kStreamSlotCount = 48u; - constexpr uint32_t kCommandStreamSlotShift = 8u; - constexpr uint32_t kResponseStreamSlotShift = 4u; - - class SoundUpdateStubService final : public IopService - { - public: - SoundUpdateStubService(IopHost &host, SoundUpdateStubBindings bindings) - : m_host(host), m_bindings(std::move(bindings)), m_sids{m_bindings.sid} - { - } - - [[nodiscard]] std::string_view name() const override - { - return m_bindings.serviceName; - } - - [[nodiscard]] std::span sids() const override - { - return m_sids; - } - - void reset() override - { - std::lock_guard lock(m_mutex); - m_updateCounter = 0u; - m_completedStreamCount = 0u; - } - - [[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override - { - if (request.sid != m_bindings.sid) - { - return {}; - } - - RpcResult result; - result.handled = true; - result.resultAddress = request.receive.address; - result.signalNowaitCompletion = m_bindings.signalNowaitCompletion; - - if (std::find(m_bindings.suppressedCompletionCallbacks.begin(), - m_bindings.suppressedCompletionCallbacks.end(), - request.endFunction) != m_bindings.suppressedCompletionCallbacks.end()) - { - result.signalCompletion = true; - result.callbackPolicy = CallbackPolicy::Suppress; - } - - if (m_bindings.zeroReceiveBuffer && - request.receive.address != 0u && request.receive.size != 0u) - { - (void)m_host.zeroGuest(request.receive.address, request.receive.size); - } - - std::vector activeStreamSlots; - if (m_bindings.completeQueuedPlayStreams && request.receive.address != 0u) - { - // PlayStream leaves the EE slot in state 2. One active record moves it - // to state 1; the following empty update lets SOUND_CopyIOPBuffer clear it. - activeStreamSlots = findQueuedPlayStreams(request); - trimToReceiveCapacity(activeStreamSlots, request.receive.size); - } - - uint32_t counter = 0u; - { - std::lock_guard lock(m_mutex); - counter = ++m_updateCounter; - m_completedStreamCount += activeStreamSlots.size(); - } - - const uint32_t activeStreams = static_cast(activeStreamSlots.size()); - if (request.receive.address != 0u && - request.receive.size >= m_bindings.activeStreamCountOffset + sizeof(activeStreams)) - { - const uint32_t address = request.receive.address + m_bindings.activeStreamCountOffset; - (void)m_host.writeGuest(address, &activeStreams, sizeof(activeStreams)); - } - - for (size_t index = 0u; index < activeStreamSlots.size(); ++index) - { - const uint32_t packedStream = activeStreamSlots[index] << kResponseStreamSlotShift; - const uint32_t offset = m_bindings.activeStreamCountOffset + static_cast(index) * kResponseRecordStride + kPackedStreamOffset; - const uint32_t address = request.receive.address + offset; - (void)m_host.writeGuest(address, &packedStream, sizeof(packedStream)); - } - - const uint32_t counterOffset = m_bindings.responseCounterOffset + - activeStreams * kResponseRecordStride; - if (request.receive.address != 0u && - request.receive.size >= counterOffset + sizeof(counter)) - { - const uint32_t address = request.receive.address + counterOffset; - (void)m_host.writeGuest(address, &counter, sizeof(counter)); - } - - return result; - } - - void appendDebugMetrics(std::vector &metrics) const override - { - std::lock_guard lock(m_mutex); - metrics.push_back({"update_counter", m_updateCounter, false}); - metrics.push_back({"completed_streams", m_completedStreamCount, false}); - } - - private: - [[nodiscard]] std::vector findQueuedPlayStreams(const RpcRequest &request) const - { - std::vector slots; - if (request.send.address == 0u || request.send.size < sizeof(uint16_t)) - { - return slots; - } - - uint16_t commandCount = 0u; - if (!m_host.readGuest(request.send.address, &commandCount, sizeof(commandCount))) - { - return slots; - } - - uint32_t offset = sizeof(commandCount); - for (uint32_t commandIndex = 0u; commandIndex < commandCount; ++commandIndex) - { - constexpr uint32_t headerSize = sizeof(uint16_t) * 2u; - if (offset > request.send.size || request.send.size - offset < headerSize) - { - break; - } - - std::array header{}; - if (!m_host.readGuest(request.send.address + offset, - header.data(), - sizeof(header))) - { - break; - } - offset += headerSize; - - const uint32_t argumentBytes = - static_cast(header[1]) * sizeof(uint16_t); - if (argumentBytes > request.send.size - offset) - { - break; - } - - if (header[0] == kPlayStreamCommand && header[1] >= 2u) - { - uint16_t encodedSlot = 0u; - if (m_host.readGuest(request.send.address + offset + sizeof(uint16_t), - &encodedSlot, - sizeof(encodedSlot))) - { - const uint32_t slot = - (encodedSlot >> kCommandStreamSlotShift) & kStreamSlotMask; - if (slot < kStreamSlotCount && - std::find(slots.begin(), slots.end(), slot) == slots.end()) - { - slots.push_back(slot); - } - } - } - - offset += argumentBytes; - } - return slots; - } - - void trimToReceiveCapacity(std::vector &slots, uint32_t receiveSize) const - { - size_t count = 0u; - for (; count < slots.size(); ++count) - { - const uint64_t recordOffset = - static_cast(m_bindings.activeStreamCountOffset) + - static_cast(count) * kResponseRecordStride + - kPackedStreamOffset; - const uint64_t counterOffset = - static_cast(m_bindings.responseCounterOffset) + - static_cast(count + 1u) * kResponseRecordStride; - if (recordOffset + sizeof(uint32_t) > receiveSize || - counterOffset + sizeof(uint32_t) > receiveSize) - { - break; - } - } - slots.resize(count); - } - - IopHost &m_host; - SoundUpdateStubBindings m_bindings; - std::array m_sids; - mutable std::mutex m_mutex; - uint32_t m_updateCounter = 0u; - uint64_t m_completedStreamCount = 0u; - }; - } - - std::unique_ptr createSoundUpdateStubService(IopHost &host, - SoundUpdateStubBindings bindings) - { - if (bindings.serviceName.empty() || bindings.sid == 0u || - bindings.activeStreamCountOffset == bindings.responseCounterOffset) - { - throw std::invalid_argument("invalid SOUND update stub bindings"); - } - std::unordered_set callbacks; - for (const uint32_t callback : bindings.suppressedCompletionCallbacks) - { - if (callback == 0u || !callbacks.emplace(callback).second) - { - throw std::invalid_argument("invalid SOUND update callback binding"); - } - } - return std::make_unique(host, std::move(bindings)); - } -} diff --git a/ps2xIOP/src/modules/tsnddrv.cpp b/ps2xIOP/src/modules/tsnddrv.cpp deleted file mode 100644 index 155970c..0000000 --- a/ps2xIOP/src/modules/tsnddrv.cpp +++ /dev/null @@ -1,628 +0,0 @@ -#include "../module_factories.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace ps2x::iop::detail -{ - namespace - { - constexpr uint32_t kCommandSid = 0x00000000u; - constexpr uint32_t kStateSid = 0x00000001u; - constexpr uint32_t kSubmitFunction = 0x00000000u; - constexpr uint32_t kGetStatusAddressFunction = 0x00000012u; - constexpr uint32_t kGetAddressTableFunction = 0x00000013u; - - constexpr uint32_t kStatusSize = 0x42u; - constexpr uint32_t kSeInfoOffset = 0x00u; - constexpr uint32_t kMidiInfoOffset = 0x0Cu; - constexpr uint32_t kMidiSumOffset = 0x1Eu; - constexpr uint32_t kSeSumOffset = 0x26u; - constexpr uint32_t kAddressTableEntries = 16u; - constexpr uint32_t alignUp(uint32_t value, uint32_t alignment) - { - if (alignment == 0u) - { - return value; - } - return (value + (alignment - 1u)) & ~(alignment - 1u); - } - - template - bool readGuestPod(const IopHost &host, uint32_t address, T &value) - { - value = {}; - return host.readGuest(address, &value, sizeof(value)); - } - - template - bool writeGuestPod(IopHost &host, uint32_t address, const T &value) - { - return host.writeGuest(address, &value, sizeof(value)); - } - - template - bool hasAnyNonZero(const std::array &values) - { - return std::any_of(values.begin(), values.end(), [](const T value) - { return value != static_cast(0); }); - } - - size_t commandLength(uint8_t command) - { - const uint8_t hi = static_cast(command & 0xF0u); - switch (hi) - { - case 0x00u: - { - size_t length = 4u; - if ((command & 0x01u) != 0u) - { - ++length; - } - if ((command & 0x02u) != 0u) - { - ++length; - } - if ((command & 0x04u) != 0u) - { - length += 2u; - } - return length; - } - case 0x10u: - return command == 0x11u ? 3u : 1u; - case 0x20u: - if (command == 0x22u || command == 0x23u || command == 0x24u || command == 0x25u) - { - return 3u; - } - if (command == 0x26u) - { - return 4u; - } - if (command == 0x20u) - { - return 5u; - } - if (command == 0x27u || command == 0x28u || command == 0x29u || - command == 0x2Cu || command == 0x2Du) - { - return 8u; - } - return 2u; - case 0x40u: - if (command == 0x47u || command == 0x48u || command == 0x49u || command == 0x4Au || - command == 0x41u || command == 0x42u) - { - return 2u; - } - if (command == 0x4Bu) - { - return 3u; - } - if (command == 0x45u || command == 0x4Cu) - { - return 4u; - } - if (command == 0x44u) - { - return 6u; - } - if (command == 0x4Du || command == 0x4Eu) - { - return 3u; - } - if (command == 0x4Fu) - { - return 6u; - } - return 1u; - case 0x50u: - case 0x60u: - if (command == 0x51u || command == 0x52u || command == 0x53u || command == 0x54u) - { - return 8u; - } - return 2u; - default: - return 0u; - } - } - - class TsnddrvService final : public IopService - { - public: - TsnddrvService(IopHost &host, TsnddrvBindings bindings) - : m_host(host), m_bindings(std::move(bindings)) - { - } - - [[nodiscard]] std::string_view name() const override - { - return m_bindings.serviceName; - } - - [[nodiscard]] std::span sids() const override - { - return m_sids; - } - - void reset() override - { - std::lock_guard lock(m_mutex); - m_state = {}; - } - - [[nodiscard]] RpcResult handleRpc(const RpcRequest &request) override - { - RpcResult result{}; - - if (request.sid == kCommandSid && request.function == kSubmitFunction) - { - handleCommandBuffer(request.send); - result.handled = true; - } - else if (request.sid == kStateSid && - (request.function == kGetStatusAddressFunction || - request.function == kGetAddressTableFunction)) - { - uint32_t responseAddress = 0u; - { - std::lock_guard lock(m_mutex); - if (!ensureMemoryLocked()) - { - return result; - } - responseAddress = request.function == kGetStatusAddressFunction - ? m_state.statusAddress - : m_state.addressTableAddress; - } - - if (request.receive.address != 0u && request.receive.size >= sizeof(uint32_t)) - { - (void)writeGuestPod(m_host, request.receive.address, responseAddress); - if (request.receive.size > sizeof(uint32_t)) - { - (void)m_host.zeroGuest(request.receive.address + sizeof(uint32_t), - request.receive.size - sizeof(uint32_t)); - } - result.resultAddress = request.receive.address; - } - - result.handled = true; - result.signalNowaitCompletion = true; - } - - if (result.handled) - { - const auto rule = std::find_if( - m_bindings.completionRules.begin(), - m_bindings.completionRules.end(), - [&](const TsnddrvCompletionRule &candidate) { - return candidate.eeFunction == request.endFunction; - }); - if (rule != m_bindings.completionRules.end()) - { - if (rule->suppressGuestCallback) - { - result.callbackPolicy = CallbackPolicy::Suppress; - } - result.signalCompletion = rule->signalCompletion; - if (rule->clearBusy) - { - constexpr uint32_t idle = 0u; - (void)writeGuestPod(m_host, - m_bindings.busyFlagAddress, - idle); - } - } - } - - return result; - } - - void onSifTransfer(const SifTransfer &transfer) override - { - if (transfer.kind != SifTransferKind::GetOtherData || - transfer.phase != SifTransferPhase::BeforeCopy || - transfer.size != kStatusSize) - { - return; - } - - std::lock_guard lock(m_mutex); - if (!m_state.initialized || transfer.sourceAddress != m_state.statusAddress) - { - return; - } - backfillStatusLocked(); - } - - void appendDebugMetrics(std::vector &metrics) const override - { - std::lock_guard lock(m_mutex); - metrics.push_back({"initialized", m_state.initialized ? 1u : 0u, false}); - metrics.push_back({"status_address", m_state.statusAddress, true}); - metrics.push_back({"address_table", m_state.addressTableAddress, true}); - metrics.push_back({"hd_base", m_state.hdBaseAddress, true}); - metrics.push_back({"sq_base", m_state.sqBaseAddress, true}); - metrics.push_back({"data_base", m_state.dataBaseAddress, true}); - } - - private: - struct State - { - bool initialized = false; - uint32_t storageBaseAddress = 0u; - uint32_t storageSize = 0u; - uint32_t statusAddress = 0u; - uint32_t addressTableAddress = 0u; - uint32_t hdBaseAddress = 0u; - uint32_t sqBaseAddress = 0u; - uint32_t dataBaseAddress = 0u; - }; - - bool ensureMemoryLocked() - { - if (m_state.statusAddress == 0u) - { - const TsnddrvGuestArena &arena = m_bindings.arena; - const uint32_t statusAddress = alignUp(arena.base, arena.statusAlignment); - const uint32_t addressTableAddress = - alignUp(statusAddress + kStatusSize, arena.tableAlignment); - const uint32_t hdBaseAddress = - alignUp(addressTableAddress + (kAddressTableEntries * sizeof(uint32_t)), - arena.storageAlignment); - const uint32_t sqBaseAddress = - alignUp(hdBaseAddress + arena.hdBytes, arena.storageAlignment); - const uint32_t dataBaseAddress = - alignUp(sqBaseAddress + arena.sqBytes, arena.storageAlignment); - const uint32_t storageEnd = dataBaseAddress + arena.dataBytes; - if (storageEnd > arena.limit) - { - return false; - } - - m_state.statusAddress = statusAddress; - m_state.addressTableAddress = addressTableAddress; - m_state.hdBaseAddress = hdBaseAddress; - m_state.sqBaseAddress = sqBaseAddress; - m_state.dataBaseAddress = dataBaseAddress; - m_state.storageBaseAddress = hdBaseAddress; - m_state.storageSize = storageEnd - hdBaseAddress; - } - - if (m_state.statusAddress == 0u || - m_state.addressTableAddress == 0u || - m_state.storageBaseAddress == 0u) - { - return false; - } - - if (!m_state.initialized) - { - if (!m_host.zeroGuest(m_state.statusAddress, kStatusSize) || - !m_host.zeroGuest(m_state.addressTableAddress, - kAddressTableEntries * sizeof(uint32_t)) || - !m_host.zeroGuest(m_state.storageBaseAddress, m_state.storageSize)) - { - return false; - } - - if (!writeGuestPod(m_host, - m_state.addressTableAddress + (0u * sizeof(uint32_t)), - m_state.hdBaseAddress) || - !writeGuestPod(m_host, - m_state.addressTableAddress + (1u * sizeof(uint32_t)), - m_state.sqBaseAddress) || - !writeGuestPod(m_host, - m_state.addressTableAddress + (2u * sizeof(uint32_t)), - m_state.dataBaseAddress)) - { - return false; - } - m_state.initialized = true; - } - - return true; - } - - int16_t checkValue(bool seTable, - uint32_t index, - uint32_t count) const - { - if (index >= count) - { - return 0; - } - - for (const TsnddrvChecksumTables &candidate : m_bindings.checksumCandidates) - { - const uint32_t base = seTable ? candidate.seAddress : candidate.midiAddress; - int16_t value = 0; - if (readGuestPod(m_host, - base + (index * sizeof(int16_t)), - value) && - value != 0) - { - return value; - } - } - return 0; - } - - bool selectCompatChecks(uint32_t &seBase, uint32_t &midiBase) const - { - const TsnddrvChecksumTables *firstReadable = nullptr; - for (const TsnddrvChecksumTables &candidate : m_bindings.checksumCandidates) - { - std::array seValues{}; - std::array midiValues{}; - const bool seReadable = m_host.readGuest(candidate.seAddress, - seValues.data(), - sizeof(seValues)); - const bool midiReadable = m_host.readGuest(candidate.midiAddress, - midiValues.data(), - sizeof(midiValues)); - if (seReadable && midiReadable && !firstReadable) - { - firstReadable = &candidate; - } - const bool looksLive = - (seReadable && hasAnyNonZero(seValues)) || - (midiReadable && hasAnyNonZero(midiValues)); - if (seReadable && midiReadable && looksLive) - { - seBase = candidate.seAddress; - midiBase = candidate.midiAddress; - return true; - } - } - - if (firstReadable) - { - seBase = firstReadable->seAddress; - midiBase = firstReadable->midiAddress; - return true; - } - return false; - } - - void backfillStatusLocked() - { - uint32_t seBase = 0u; - uint32_t midiBase = 0u; - if (!selectCompatChecks(seBase, midiBase)) - { - return; - } - - auto backfillSlots = [&](uint32_t statusOffset, - uint32_t compatBase, - uint32_t slotCount) - { - for (uint32_t slot = 0u; slot < slotCount; ++slot) - { - int16_t liveValue = 0; - if (!readGuestPod(m_host, - m_state.statusAddress + statusOffset + - (slot * sizeof(int16_t)), - liveValue) || - liveValue != 0) - { - continue; - } - - int16_t compatValue = 0; - if (!readGuestPod(m_host, - compatBase + (slot * sizeof(int16_t)), - compatValue) || - compatValue == 0) - { - continue; - } - - (void)writeGuestPod(m_host, - m_state.statusAddress + statusOffset + - (slot * sizeof(int16_t)), - compatValue); - } - }; - - backfillSlots(kSeSumOffset, seBase, 5u); - backfillSlots(kMidiSumOffset, midiBase, 4u); - } - - void applyCommandLocked(const std::array &command) - { - if (m_state.statusAddress == 0u) - { - return; - } - - switch (command[0]) - { - case 0x20u: // SdrBgmReq - { - const uint32_t port = command[1] & 0x0Fu; - uint16_t midiInfo = 0u; - (void)readGuestPod(m_host, - m_state.statusAddress + kMidiInfoOffset, - midiInfo); - midiInfo = static_cast(midiInfo | - static_cast(1u << port)); - (void)writeGuestPod(m_host, - m_state.statusAddress + kMidiInfoOffset, - midiInfo); - break; - } - case 0x21u: // SdrBgmStop - { - const uint32_t port = command[1] & 0x0Fu; - uint16_t midiInfo = 0u; - (void)readGuestPod(m_host, - m_state.statusAddress + kMidiInfoOffset, - midiInfo); - midiInfo = static_cast(midiInfo & - ~static_cast(1u << port)); - (void)writeGuestPod(m_host, - m_state.statusAddress + kMidiInfoOffset, - midiInfo); - break; - } - case 0x28u: // SdrHDDataSet - { - const uint32_t port = command[1] & 0x0Fu; - if (port >= 4u) - { - break; - } - const int16_t checksum = checkValue(false, port, 4u); - (void)writeGuestPod(m_host, - m_state.statusAddress + kMidiSumOffset + - (port * sizeof(int16_t)), - checksum); - break; - } - case 0x29u: // SdrHDDataSet2 - { - const uint32_t port = command[1] & 0x0Fu; - if (port >= 5u) - { - break; - } - const int16_t checksum = checkValue(true, port, 5u); - (void)writeGuestPod(m_host, - m_state.statusAddress + kSeSumOffset + - (port * sizeof(int16_t)), - checksum); - break; - } - case 0x10u: // SdrSeAllStop - (void)m_host.zeroGuest(m_state.statusAddress + kSeInfoOffset, - 6u * sizeof(uint16_t)); - break; - default: - break; - } - } - - void handleCommandBuffer(GuestBuffer send) - { - if (send.address == 0u || send.size == 0u) - { - return; - } - - std::lock_guard lock(m_mutex); - if (!ensureMemoryLocked()) - { - return; - } - - for (uint32_t offset = 0u; offset < send.size;) - { - uint8_t operation = 0u; - if (!m_host.readGuest(send.address + offset, &operation, sizeof(operation)) || - operation == 0xFFu) - { - break; - } - - const size_t length = commandLength(operation); - if (length == 0u || - static_cast(offset) + length > send.size) - { - break; - } - - std::array command{}; - if (!m_host.readGuest(send.address + offset, command.data(), length)) - { - break; - } - applyCommandLocked(command); - offset += static_cast(length); - } - } - - IopHost &m_host; - TsnddrvBindings m_bindings; - mutable std::mutex m_mutex; - State m_state; - const std::array m_sids = {kCommandSid, kStateSid}; - }; - } - - std::unique_ptr createTsnddrvService(IopHost &host, - TsnddrvBindings bindings) - { - const auto isPowerOfTwo = [](uint32_t value) { - return value != 0u && (value & (value - 1u)) == 0u; - }; - const auto alignUp64 = [](uint64_t value, uint32_t alignment) { - return (value + (alignment - 1u)) & - ~static_cast(alignment - 1u); - }; - - const TsnddrvGuestArena &arena = bindings.arena; - if (bindings.serviceName.empty() || - arena.base >= arena.limit || - !isPowerOfTwo(arena.statusAlignment) || - !isPowerOfTwo(arena.tableAlignment) || - !isPowerOfTwo(arena.storageAlignment) || - arena.hdBytes == 0u || arena.sqBytes == 0u || arena.dataBytes == 0u || - bindings.checksumCandidates.empty()) - { - throw std::invalid_argument("invalid TSNDDRV bindings"); - } - - uint64_t end = alignUp64(arena.base, arena.statusAlignment) + kStatusSize; - end = alignUp64(end, arena.tableAlignment) + - (kAddressTableEntries * sizeof(uint32_t)); - end = alignUp64(end, arena.storageAlignment) + arena.hdBytes; - end = alignUp64(end, arena.storageAlignment) + arena.sqBytes; - end = alignUp64(end, arena.storageAlignment) + arena.dataBytes; - if (end > arena.limit || end > std::numeric_limits::max()) - { - throw std::invalid_argument("TSNDDRV guest arena is too small"); - } - - for (const TsnddrvChecksumTables &candidate : bindings.checksumCandidates) - { - if (candidate.seAddress == 0u || candidate.midiAddress == 0u) - { - throw std::invalid_argument("incomplete TSNDDRV checksum binding"); - } - } - - std::unordered_set callbacks; - for (const TsnddrvCompletionRule &rule : bindings.completionRules) - { - if (rule.eeFunction == 0u || !callbacks.emplace(rule.eeFunction).second || - (rule.clearBusy && bindings.busyFlagAddress == 0u)) - { - throw std::invalid_argument("invalid TSNDDRV completion rule"); - } - } - - switch (bindings.protocol) - { - case TsnddrvProtocolVariant::SndQueueV1: - break; - } - return std::make_unique(host, std::move(bindings)); - } -} diff --git a/ps2xIOP/src/plugin_loader.cpp b/ps2xIOP/src/plugin_loader.cpp deleted file mode 100644 index 5490786..0000000 --- a/ps2xIOP/src/plugin_loader.cpp +++ /dev/null @@ -1,956 +0,0 @@ -#include "plugin_loader.h" - -#include "ps2x/iop/plugin_api.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32) -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX -#include -#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__) -#include -#endif - -namespace ps2x::iop::detail -{ - namespace - { - constexpr size_t kMaxPluginProfiles = 256u; - constexpr size_t kMaxPluginSids = 256u; - constexpr size_t kMaxPluginStringBytes = 4096u; - - bool validStringView(ps2x_iop_string_view_v1 value) - { - return value.size <= kMaxPluginStringBytes && (value.size == 0u || value.data != nullptr); - } - - std::string copyString(ps2x_iop_string_view_v1 value) - { - if (!validStringView(value) || value.size == 0u) - { - return {}; - } - return std::string(value.data, value.size); - } - - ps2x_iop_string_view_v1 makeStringView(std::string_view value) - { - return {value.data(), value.size()}; - } - - int32_t copyHostString(const std::string &value, - char *destination, - size_t capacity, - size_t *requiredSize) - { - const size_t required = value.size() + 1; - if (requiredSize) - { - *requiredSize = required; - } - if (!destination || capacity < required) - { - return PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1; - } - std::memcpy(destination, value.c_str(), required); - return PS2X_IOP_STATUS_OK_V1; - } - - IopHandleKind toHandleKind(uint32_t kind) - { - return kind == PS2X_IOP_HANDLE_RPC_PACKET_V1 - ? IopHandleKind::RpcPacket - : IopHandleKind::RpcServer; - } - - HostPathKind toHostPathKind(uint32_t kind) - { - switch (kind) - { - case PS2X_IOP_PATH_CD_ROOT_V1: - return HostPathKind::CdRoot; - case PS2X_IOP_PATH_CD_IMAGE_V1: - return HostPathKind::CdImage; - case PS2X_IOP_PATH_HOST_ROOT_V1: - return HostPathKind::HostRoot; - case PS2X_IOP_PATH_MEMORY_CARD_ROOT_V1: - return HostPathKind::MemoryCardRoot; - default: - return HostPathKind::ElfDirectory; - } - } - - MemoryCardOperation toMemoryCardOperation(uint32_t operation) - { - const uint32_t last = static_cast(MemoryCardOperation::Mkdir); - if (operation > last) - { - throw std::out_of_range("invalid memory-card operation"); - } - return static_cast(operation); - } - - class HostApiBridge - { - public: - explicit HostApiBridge(IopHost &hostRef) - : host(hostRef) - { - api.abi_version = PS2X_IOP_ABI_VERSION_V1; - api.struct_size = sizeof(api); - api.userdata = this; - api.read_guest = &readGuest; - api.write_guest = &writeGuest; - api.zero_guest = &zeroGuest; - api.normalize_guest_address = &normalizeGuestAddress; - api.allocate_iop_handle = &allocateIopHandle; - api.allocate_guest = &allocateGuest; - api.free_guest = &freeGuest; - api.audio_command = &audioCommand; - api.get_host_path = &getHostPath; - api.translate_guest_path = &translateGuestPath; - api.open_host_file = &openHostFile; - api.host_file_size = &hostFileSize; - api.read_host_file = &readHostFile; - api.close_host_file = &closeHostFile; - api.memory_card = &memoryCard; - api.has_guest_function = &hasGuestFunction; - api.invoke_guest_function = &invokeGuestFunction; - api.log = &log; - } - - ps2x_iop_host_api_v1 api{}; - IopHost &host; - - private: - static HostApiBridge *self(void *userdata) - { - return static_cast(userdata); - } - - template - static int32_t guardedStatus(Callback &&callback) noexcept - { - try - { - return static_cast( - std::forward(callback)()); - } - catch (...) - { - return PS2X_IOP_STATUS_FAILED_V1; - } - } - - template - static Value guardedValue(Value fallback, Callback &&callback) noexcept - { - try - { - return static_cast( - std::forward(callback)()); - } - catch (...) - { - return fallback; - } - } - - template - static void guardedVoid(Callback &&callback) noexcept - { - try - { - std::forward(callback)(); - } - catch (...) - { - } - } - - static int32_t readGuest(void *userdata, uint32_t address, void *destination, size_t size) - { - if (!userdata || (!destination && size != 0)) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - return guardedStatus([&]() - { return self(userdata)->host.readGuest(address, destination, size) - ? PS2X_IOP_STATUS_OK_V1 - : PS2X_IOP_STATUS_FAILED_V1; }); - } - - static int32_t writeGuest(void *userdata, uint32_t address, const void *source, size_t size) - { - if (!userdata || (!source && size != 0)) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - return guardedStatus([&]() - { return self(userdata)->host.writeGuest(address, source, size) - ? PS2X_IOP_STATUS_OK_V1 - : PS2X_IOP_STATUS_FAILED_V1; }); - } - - static int32_t zeroGuest(void *userdata, uint32_t address, size_t size) - { - if (!userdata) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - return guardedStatus([&]() - { return self(userdata)->host.zeroGuest(address, size) - ? PS2X_IOP_STATUS_OK_V1 - : PS2X_IOP_STATUS_FAILED_V1; }); - } - - static int32_t normalizeGuestAddress(void *userdata, uint32_t address, uint32_t *normalized) - { - if (!userdata || !normalized) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - return guardedStatus([&]() - { return self(userdata)->host.normalizeGuestAddress(address, *normalized) - ? PS2X_IOP_STATUS_OK_V1 - : PS2X_IOP_STATUS_FAILED_V1; }); - } - - static uint32_t allocateIopHandle(void *userdata, uint32_t kind) - { - if (!userdata) - { - return 0; - } - return guardedValue(0u, [&]() - { return self(userdata)->host.allocateIopHandle(toHandleKind(kind)); }); - } - - static uint32_t allocateGuest(void *userdata, uint32_t size, uint32_t alignment) - { - if (!userdata) - { - return 0; - } - return guardedValue(0u, [&]() - { return self(userdata)->host.allocateGuest(size, alignment); }); - } - - static void freeGuest(void *userdata, uint32_t address) - { - if (userdata && address) - { - guardedVoid([&]() - { self(userdata)->host.freeGuest(address); }); - } - } - - static int32_t audioCommand(void *userdata, - uint32_t sid, - uint32_t function, - ps2x_iop_guest_buffer_v1 send, - ps2x_iop_guest_buffer_v1 receive) - { - if (!userdata) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - return guardedStatus([&]() - { - self(userdata)->host.audioCommand(sid, - function, - {send.address, send.size}, - {receive.address, receive.size}); - return PS2X_IOP_STATUS_OK_V1; }); - } - - static int32_t getHostPath(void *userdata, - uint32_t kind, - char *destination, - size_t capacity, - size_t *requiredSize) - { - if (!userdata) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - return guardedStatus([&]() - { return copyHostString(self(userdata)->host.hostPath(toHostPathKind(kind)), - destination, - capacity, - requiredSize); }); - } - - static int32_t translateGuestPath(void *userdata, - ps2x_iop_string_view_v1 path, - char *destination, - size_t capacity, - size_t *requiredSize) - { - if (!userdata || (!path.data && path.size != 0)) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - return guardedStatus([&]() - { - const std::string translated = self(userdata)->host.translateGuestPath( - std::string_view(path.data ? path.data : "", path.size)); - return copyHostString(translated, destination, capacity, requiredSize); }); - } - - static uint64_t openHostFile(void *userdata, - ps2x_iop_string_view_v1 path) - { - if (!userdata || (!path.data && path.size != 0u)) - { - return 0u; - } - return guardedValue(0u, [&]() - { return self(userdata)->host.openHostFile( - std::string_view(path.data ? path.data : "", path.size)); }); - } - - static int32_t hostFileSize(void *userdata, - uint64_t handle, - uint64_t *size) - { - if (!userdata || handle == 0u || !size) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - return guardedStatus([&]() - { return self(userdata)->host.hostFileSize(handle, *size) - ? PS2X_IOP_STATUS_OK_V1 - : PS2X_IOP_STATUS_FAILED_V1; }); - } - - static int32_t readHostFile(void *userdata, - uint64_t handle, - uint64_t offset, - void *destination, - size_t size, - size_t *bytesRead) - { - if (!userdata || handle == 0u || !bytesRead || - (!destination && size != 0u)) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - return guardedStatus([&]() - { return self(userdata)->host.readHostFile(handle, - offset, - destination, - size, - *bytesRead) - ? PS2X_IOP_STATUS_OK_V1 - : PS2X_IOP_STATUS_FAILED_V1; }); - } - - static void closeHostFile(void *userdata, uint64_t handle) - { - if (userdata && handle != 0u) - { - guardedVoid([&]() - { self(userdata)->host.closeHostFile(handle); }); - } - } - - static int32_t memoryCard(void *userdata, - const ps2x_iop_memory_card_request_v1 *request, - int32_t *result) - { - if (!userdata || !request || !result || request->struct_size < sizeof(*request)) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - try - { - MemoryCardRequest converted; - converted.operation = toMemoryCardOperation(request->operation); - std::copy(std::begin(request->arguments), std::end(request->arguments), converted.arguments.begin()); - *result = self(userdata)->host.memoryCard(converted); - return PS2X_IOP_STATUS_OK_V1; - } - catch (...) - { - return PS2X_IOP_STATUS_FAILED_V1; - } - } - - static int32_t hasGuestFunction(void *userdata, uint32_t address) - { - if (!userdata) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - return guardedStatus([&]() - { return self(userdata)->host.hasGuestFunction(address) ? 1 : 0; }); - } - - static int32_t invokeGuestFunction(void *userdata, - uint64_t callToken, - uint32_t address, - uint32_t a0, - uint32_t a1, - uint32_t a2, - uint32_t a3, - uint32_t *resultAddress) - { - if (!userdata) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - return guardedStatus([&]() - { return self(userdata)->host.invokeGuestFunction(callToken, - address, - a0, - a1, - a2, - a3, - resultAddress) - ? 1 - : 0; }); - } - - static void log(void *userdata, uint32_t level, ps2x_iop_string_view_v1 message) - { - if (!userdata || (!message.data && message.size != 0)) - { - return; - } - const uint32_t maxLevel = static_cast(LogLevel::Error); - const auto converted = static_cast(std::min(level, maxLevel)); - guardedVoid([&]() - { self(userdata)->host.log( - converted, - std::string_view(message.data ? message.data : "", message.size)); }); - } - }; - - ps2x_iop_rpc_candidate_v1 toPluginCandidate(const RpcCallCandidate &candidate) - { - return { - candidate.sendSize, - candidate.receiveAddress, - candidate.receiveSize, - candidate.endFunction, - candidate.endParameter, - candidate.plausible ? 1u : 0u, - }; - } - - class PluginService final : public IopService - { - public: - PluginService(IopHost &host, - std::shared_ptr libraryKeepAlive, - ps2x_iop_profile_api_v1 profileApi, - std::string serviceName, - std::vector serviceSids, - const GameIdentity &identity) - : m_libraryKeepAlive(std::move(libraryKeepAlive)), - m_api(profileApi), - m_name(std::move(serviceName)), - m_sids(std::move(serviceSids)), - m_host(host) - { - const ps2x_iop_game_identity_v1 pluginIdentity{ - sizeof(ps2x_iop_game_identity_v1), - makeStringView(identity.elfName), - identity.entryPoint, - identity.crc32, - }; - try - { - m_instance = m_api.create(&m_host.api, &pluginIdentity); - } - catch (...) - { - throw std::runtime_error("plugin profile create threw an exception"); - } - if (!m_instance) - { - throw std::runtime_error("plugin profile create returned null"); - } - } - - ~PluginService() override - { - if (m_instance && m_api.destroy) - { - try - { - m_api.destroy(m_instance); - } - catch (...) - { - m_host.host.log(LogLevel::Error, - "IOP plugin destroy threw for " + m_name); - } - } - m_instance = nullptr; - } - - std::string_view name() const override - { - return m_name; - } - - std::span sids() const override - { - return m_sids; - } - - void reset() override - { - if (!m_api.reset) - { - return; - } - int32_t status = PS2X_IOP_STATUS_FAILED_V1; - try - { - status = m_api.reset(m_instance); - } - catch (...) - { - } - if (status != PS2X_IOP_STATUS_OK_V1) - { - m_host.host.log(LogLevel::Warning, "IOP plugin reset failed for " + m_name); - } - } - - RpcAbi selectRpcAbi(const RpcAbiRequest &request) const override - { - if (!m_api.select_rpc_abi) - { - return RpcAbi::RuntimeDefault; - } - const ps2x_iop_rpc_abi_request_v1 converted{ - sizeof(ps2x_iop_rpc_abi_request_v1), - request.boundSid, - request.function, - toPluginCandidate(request.registers), - toPluginCandidate(request.stack), - }; - uint32_t result = PS2X_IOP_RPC_ABI_DEFAULT_V1; - try - { - result = m_api.select_rpc_abi(m_instance, &converted); - } - catch (...) - { - m_host.host.log(LogLevel::Warning, - "IOP plugin ABI selector threw for " + m_name); - } - if (result == PS2X_IOP_RPC_ABI_REGISTERS_V1) - { - return RpcAbi::Registers; - } - if (result == PS2X_IOP_RPC_ABI_STACK_V1) - { - return RpcAbi::Stack; - } - return RpcAbi::RuntimeDefault; - } - - RpcResult handleRpc(const RpcRequest &request) override - { - const ps2x_iop_rpc_request_v1 converted{ - sizeof(ps2x_iop_rpc_request_v1), - request.callToken, - request.clientAddress, - request.serverAddress, - request.serverFunction, - request.serverBuffer, - request.sid, - request.function, - request.mode, - {request.send.address, request.send.size}, - {request.receive.address, request.receive.size}, - request.endFunction, - request.endParameter, - }; - ps2x_iop_rpc_result_v1 result{}; - result.struct_size = sizeof(result); - int32_t status = PS2X_IOP_STATUS_FAILED_V1; - try - { - status = m_api.handle_rpc(m_instance, &converted, &result); - } - catch (...) - { - } - if (status != PS2X_IOP_STATUS_OK_V1 || - result.struct_size < sizeof(result)) - { - m_host.host.log(LogLevel::Warning, "IOP plugin RPC failed for " + m_name); - return {}; - } - return { - result.handled != 0, - result.result_address, - result.signal_nowait_completion != 0, - result.signal_completion != 0, - result.callback_policy == PS2X_IOP_CALLBACK_SUPPRESS_V1 - ? CallbackPolicy::Suppress - : CallbackPolicy::RuntimeDefault, - result.server_dispatch_policy == PS2X_IOP_SERVER_DISPATCH_SUPPRESS_V1 - ? ServerDispatchPolicy::Suppress - : ServerDispatchPolicy::RuntimeDefault, - }; - } - - void onSifTransfer(const SifTransfer &transfer) override - { - if (!m_api.on_sif_transfer) - { - return; - } - const ps2x_iop_sif_transfer_v1 converted{ - sizeof(ps2x_iop_sif_transfer_v1), - static_cast(transfer.kind), - static_cast(transfer.phase), - transfer.sourceAddress, - transfer.destinationAddress, - transfer.size, - }; - int32_t status = PS2X_IOP_STATUS_FAILED_V1; - try - { - status = m_api.on_sif_transfer(m_instance, &converted); - } - catch (...) - { - } - if (status != PS2X_IOP_STATUS_OK_V1) - { - m_host.host.log(LogLevel::Warning, "IOP plugin transfer hook failed for " + m_name); - } - } - - void appendDebugMetrics(std::vector &metrics) const override - { - if (!m_api.debug_metric_count || !m_api.debug_metric) - { - return; - } - size_t rawCount = 0u; - try - { - rawCount = m_api.debug_metric_count(m_instance); - } - catch (...) - { - return; - } - const size_t count = std::min(rawCount, 256); - for (size_t i = 0; i < count; ++i) - { - ps2x_iop_debug_metric_v1 metric{}; - metric.struct_size = sizeof(metric); - int32_t status = PS2X_IOP_STATUS_FAILED_V1; - try - { - status = m_api.debug_metric(m_instance, i, &metric); - } - catch (...) - { - } - if (status != PS2X_IOP_STATUS_OK_V1 || - metric.struct_size < sizeof(metric)) - { - continue; - } - metrics.push_back({copyString(metric.name), metric.value, metric.hexadecimal != 0}); - } - } - - private: - std::shared_ptr m_libraryKeepAlive; - ps2x_iop_profile_api_v1 m_api{}; - std::string m_name; - std::vector m_sids; - HostApiBridge m_host; - void *m_instance = nullptr; - }; - - bool hasPluginExtension(const std::filesystem::path &path) - { - std::string extension = path.extension().string(); - std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char value) - { return static_cast(std::tolower(value)); }); -#if defined(_WIN32) - return extension == ".dll"; -#elif defined(__linux__) - return extension == ".so"; -#else - (void)extension; - return false; -#endif - } - - std::string formatPluginDiagnostic(const std::filesystem::path &path, std::string_view reason) - { - return "IOP plugin '" + path.string() + "': " + std::string(reason); - } - } - - class PluginCatalog::DynamicLibrary - { - public: - explicit DynamicLibrary(std::filesystem::path sourcePath) - : path(std::move(sourcePath)) - { - } - - ~DynamicLibrary() - { -#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32) - if (handle) - { - FreeLibrary(static_cast(handle)); - } -#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__) - if (handle) - { - dlclose(handle); - } -#endif - } - - bool open(std::string &error) - { -#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32) - handle = LoadLibraryW(path.c_str()); - if (!handle) - { - error = "LoadLibraryW failed with code " + std::to_string(GetLastError()); - return false; - } - return true; -#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__) - handle = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); - if (!handle) - { - const char *message = dlerror(); - error = message ? message : "dlopen failed"; - return false; - } - return true; -#else - error = "dynamic IOP plugins are disabled on this platform"; - return false; -#endif - } - - void *symbol(const char *name) const - { -#if PS2X_IOP_ENABLE_PLUGINS && defined(_WIN32) - return handle ? reinterpret_cast(GetProcAddress(static_cast(handle), name)) : nullptr; -#elif PS2X_IOP_ENABLE_PLUGINS && defined(__linux__) - return handle ? dlsym(handle, name) : nullptr; -#else - (void)name; - return nullptr; -#endif - } - - std::filesystem::path path; - void *handle = nullptr; - }; - - PluginCatalog::PluginCatalog(IopHost &host) - : m_host(host) - { - } - - PluginCatalog::~PluginCatalog() = default; - - // TODO I never test this one - bool PluginCatalog::load(const std::vector &searchPaths, - std::vector &profiles, - std::vector &diagnostics, - std::string *error) - { - (void)error; -#if !PS2X_IOP_ENABLE_PLUGINS - if (!searchPaths.empty()) - { - diagnostics.push_back("dynamic IOP plugins are disabled on this platform"); - } - return true; -#else - for (const auto &searchPath : searchPaths) - { - std::error_code ec; - if (!std::filesystem::exists(searchPath, ec) || ec) - { - continue; - } - if (!std::filesystem::is_directory(searchPath, ec) || ec) - { - diagnostics.push_back(formatPluginDiagnostic(searchPath, "search path is not a directory")); - continue; - } - - for (std::filesystem::directory_iterator iterator(searchPath, ec), end; !ec && iterator != end; iterator.increment(ec)) - { - const std::filesystem::directory_entry &entry = *iterator; - if (!entry.is_regular_file(ec) || ec || !hasPluginExtension(entry.path())) - { - ec.clear(); - continue; - } - - std::filesystem::path canonicalPath = std::filesystem::weakly_canonical(entry.path(), ec); - if (ec) - { - ec.clear(); - canonicalPath = entry.path().lexically_normal(); - } - const std::string pathKey = canonicalPath.generic_string(); - if (!m_loadedPaths.insert(pathKey).second) - { - continue; - } - - auto library = std::make_shared(canonicalPath); - std::string openError; - if (!library->open(openError)) - { - diagnostics.push_back(formatPluginDiagnostic(canonicalPath, openError)); - continue; - } - - const auto query = reinterpret_cast( - library->symbol(PS2X_IOP_QUERY_SYMBOL_V1)); - if (!query) - { - diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "missing " PS2X_IOP_QUERY_SYMBOL_V1)); - continue; - } - - ps2x_iop_plugin_api_v1 plugin{}; - plugin.struct_size = sizeof(plugin); - int32_t queryStatus = PS2X_IOP_STATUS_FAILED_V1; - try - { - queryStatus = query(PS2X_IOP_ABI_VERSION_V1, &plugin); - } - catch (...) - { - diagnostics.push_back(formatPluginDiagnostic(canonicalPath, - "query entry threw an exception")); - continue; - } - if (queryStatus != PS2X_IOP_STATUS_OK_V1 || - plugin.abi_version != PS2X_IOP_ABI_VERSION_V1 || - plugin.struct_size < sizeof(plugin)) - { - diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "incompatible ABI or invalid descriptor")); - continue; - } - if (plugin.profile_count > 0 && !plugin.profiles) - { - diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "profile table is null")); - continue; - } - if (plugin.profile_count > kMaxPluginProfiles) - { - diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "too many profiles")); - continue; - } - - const std::string provider = copyString(plugin.name).empty() - ? canonicalPath.filename().string() - : copyString(plugin.name); - size_t acceptedProfiles = 0; - for (size_t index = 0; index < plugin.profile_count; ++index) - { - const ps2x_iop_profile_api_v1 &profile = plugin.profiles[index]; - if (profile.abi_version != PS2X_IOP_ABI_VERSION_V1 || - profile.struct_size < sizeof(profile) || - profile.matcher.struct_size < sizeof(profile.matcher)) - { - diagnostics.push_back(formatPluginDiagnostic(canonicalPath, - "ignored invalid profile at index " + std::to_string(index))); - continue; - } - - const std::string profileId = copyString(profile.id); - const bool validMatcherName = validStringView(profile.matcher.elf_name); - const bool matcherPresent = profile.matcher.elf_name.size != 0 || - profile.matcher.entry_point != 0 || - profile.matcher.crc32 != 0; - if (!validStringView(profile.id) || !validMatcherName || - profileId.empty() || !matcherPresent || - profile.sid_count == 0 || !profile.sids || - !profile.create || !profile.destroy || !profile.reset || - !profile.handle_rpc) - { - diagnostics.push_back(formatPluginDiagnostic(canonicalPath, - "ignored invalid profile at index " + std::to_string(index))); - continue; - } - - if (profile.sid_count > kMaxPluginSids) - { - diagnostics.push_back(formatPluginDiagnostic(canonicalPath, - "ignored profile with too many SIDs: " + profileId)); - continue; - } - std::vector sids(profile.sids, profile.sids + profile.sid_count); - - ProfileDefinition definition; - definition.id = profileId; - definition.provider = provider; - definition.matcher.elfName = copyString(profile.matcher.elf_name); - definition.matcher.entryPoint = profile.matcher.entry_point; - definition.matcher.crc32 = profile.matcher.crc32; - const std::shared_ptr keepAlive = library; - definition.factory = [keepAlive, profile, profileId, sids = std::move(sids)]( - IopHost &host, - const GameIdentity &identity) mutable - { - ServiceList services; - services.push_back(std::make_unique(host, - keepAlive, - profile, - profileId, - sids, - identity)); - return services; - }; - profiles.push_back(std::move(definition)); - ++acceptedProfiles; - } - - if (acceptedProfiles == 0) - { - diagnostics.push_back(formatPluginDiagnostic(canonicalPath, "no valid profiles")); - continue; - } - diagnostics.push_back(formatPluginDiagnostic(canonicalPath, - "loaded " + std::to_string(acceptedProfiles) + " profile(s)")); - m_libraries.push_back(std::move(library)); - } - - if (ec) - { - diagnostics.push_back(formatPluginDiagnostic(searchPath, ec.message())); - } - } - return true; -#endif - } -} diff --git a/ps2xIOP/src/plugin_loader.h b/ps2xIOP/src/plugin_loader.h deleted file mode 100644 index 012bce1..0000000 --- a/ps2xIOP/src/plugin_loader.h +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include "iop_service.h" - -#include -#include -#include -#include -#include - -namespace ps2x::iop::detail -{ - class PluginCatalog - { - public: - explicit PluginCatalog(IopHost &host); - ~PluginCatalog(); - - PluginCatalog(const PluginCatalog &) = delete; - PluginCatalog &operator=(const PluginCatalog &) = delete; - - bool load(const std::vector &searchPaths, - std::vector &profiles, - std::vector &diagnostics, - std::string *error); - - private: - class DynamicLibrary; - - IopHost &m_host; - std::vector> m_libraries; - std::unordered_set m_loadedPaths; - }; -} diff --git a/ps2xIOP/src/ps2_path.cpp b/ps2xIOP/src/ps2_path.cpp new file mode 100644 index 0000000..420f053 --- /dev/null +++ b/ps2xIOP/src/ps2_path.cpp @@ -0,0 +1,123 @@ +#include "ps2x/iop/ps2_path.h" + +#include +#include + +namespace ps2x::iop +{ + namespace + { + std::string lowerAscii(std::string_view value) + { + std::string result(value); + std::transform(result.begin(), result.end(), result.begin(), [](unsigned char ch) + { return static_cast(std::tolower(ch)); }); + return result; + } + + void normalizeSuffix(std::string &suffix) + { + std::replace(suffix.begin(), suffix.end(), '\\', '/'); + while (!suffix.empty() && suffix.front() == '/') + suffix.erase(suffix.begin()); + + const size_t semicolon = suffix.rfind(';'); + if (semicolon == std::string::npos || semicolon + 1u == suffix.size()) + return; + + const bool numeric = std::all_of(suffix.begin() + static_cast(semicolon + 1u), + suffix.end(), + [](unsigned char ch) + { return std::isdigit(ch) != 0; }); + if (numeric) + suffix.erase(semicolon); + } + } + + ParsedPs2Path parsePs2Path(std::string_view value) + { + ParsedPs2Path result; + if (value.empty()) + return result; + + const std::string lower = lowerAscii(value); + size_t prefixLength = 0u; + if (lower.rfind("host0:", 0u) == 0u) + { + result.device = Ps2PathDevice::Host; + result.deviceName = "host0"; + prefixLength = 6u; + } + else if (lower.rfind("host:", 0u) == 0u) + { + result.device = Ps2PathDevice::Host; + result.deviceName = "host"; + prefixLength = 5u; + } + else if (lower.rfind("cdrom0:", 0u) == 0u) + { + result.device = Ps2PathDevice::Cdrom; + result.deviceName = "cdrom0"; + prefixLength = 7u; + } + else if (lower.rfind("cdrom:", 0u) == 0u) + { + result.device = Ps2PathDevice::Cdrom; + result.deviceName = "cdrom"; + prefixLength = 6u; + } + else if (lower.rfind("mc0:", 0u) == 0u) + { + result.device = Ps2PathDevice::MemoryCard0; + result.deviceName = "mc0"; + prefixLength = 4u; + } + else if (lower.rfind("rom0:", 0u) == 0u) + { + result.device = Ps2PathDevice::Rom0; + result.deviceName = "rom0"; + prefixLength = 5u; + } + else if (value.size() > 2u && std::isalpha(static_cast(value[0])) && + value[1] == ':' && (value[2] == '/' || value[2] == '\\')) + { + result.device = Ps2PathDevice::NativeHost; + result.deviceName = "native"; + } + else if (value.find(':') != std::string_view::npos) + { + // TODO maybe log an error here, but don't fail the parse. This is a non-standard device name. + return result; + } + else + { + result.device = Ps2PathDevice::Cdrom; + result.deviceName = "cdrom0"; + } + + result.path.assign(value.substr(prefixLength)); + if (result.device != Ps2PathDevice::NativeHost) + normalizeSuffix(result.path); + return result; + } + + std::string ps2PathLeafKey(const ParsedPs2Path &parsed) + { + if (!parsed) + return {}; + std::string path = parsed.path; + std::replace(path.begin(), path.end(), '\\', '/'); + const size_t slash = path.find_last_of('/'); + if (slash != std::string::npos) + path.erase(0u, slash + 1u); + path = lowerAscii(path); + if (path.size() > 4u && path.ends_with(".irx")) + path.resize(path.size() - 4u); + return path; + } + + std::string ps2PathLeafKey(std::string_view path) + { + return ps2PathLeafKey(parsePs2Path(path)); + } +} diff --git a/ps2xIOP/src/rpc_reply.h b/ps2xIOP/src/rpc_reply.h new file mode 100644 index 0000000..62aae90 --- /dev/null +++ b/ps2xIOP/src/rpc_reply.h @@ -0,0 +1,22 @@ +#pragma once + +#include "ps2x/iop/iop_host.h" + +#include +#include +#include +#include + +namespace ps2x::iop::detail +{ + [[nodiscard]] inline bool writeRpcWords(IopHost &host, GuestBuffer receive, std::span words) + { + const size_t count = std::min(receive.size / sizeof(uint32_t), words.size()); + const size_t bytes = count * sizeof(uint32_t); + if (receive.address == 0u || bytes == 0u) + return false; + if (bytes - 1u > std::numeric_limits::max() - receive.address) + return false; + return host.writeGuest(receive.address, words.data(), bytes); + } +} diff --git a/ps2xIOP/tests/iop_compat_test_support.h b/ps2xIOP/tests/iop_compat_test_support.h new file mode 100644 index 0000000..21831fe --- /dev/null +++ b/ps2xIOP/tests/iop_compat_test_support.h @@ -0,0 +1,237 @@ +#pragma once + +#include "ps2x/iop/iop_subsystem.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace iop_test +{ + using namespace ps2x::iop; + + inline void require(bool condition, const char *message) + { + if (!condition) + throw std::runtime_error(message); + } + + class Host final : public IopHost + { + public: + explicit Host(size_t bytes = 0x20000u) : guest(bytes, 0xCCu) {} + + bool readGuest(uint32_t address, void *destination, size_t size) const override + { + if ((size != 0u && !destination) || address > guest.size() || size > guest.size() - address) + return false; + if (size != 0u) + std::memcpy(destination, guest.data() + address, size); + ++guestReads; + return true; + } + bool writeGuest(uint32_t address, const void *source, size_t size) override + { + if ((size != 0u && !source) || address > guest.size() || size > guest.size() - address) + return false; + if (size != 0u) + std::memcpy(guest.data() + address, source, size); + ++guestWrites; + return true; + } + bool zeroGuest(uint32_t address, size_t size) override + { + if (address > guest.size() || size > guest.size() - address) + return false; + std::fill_n(guest.begin() + address, size, uint8_t{0}); + ++guestWrites; + return true; + } + bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const override + { + normalized = address; + return address < guest.size(); + } + uint32_t allocateIopHandle(IopHandleKind) override { return nextHandle += 0x80u; } + uint32_t allocateGuest(uint32_t, uint32_t) override { return 0u; } + void freeGuest(uint32_t) override {} + void audioCommand(uint32_t, uint32_t, GuestBuffer, GuestBuffer) override { ++audioCalls; } + std::string hostPath(HostPathKind) const override { return {}; } + std::string translateGuestPath(std::string_view path) const override { return std::string(path); } + uint64_t openHostFile(std::string_view) override { return file.empty() ? 0u : 1u; } + bool hostFileSize(uint64_t handle, uint64_t &size) const override + { + size = file.size(); + return handle == 1u && !file.empty(); + } + bool readHostFile(uint64_t handle, uint64_t offset, void *destination, size_t size, + size_t &bytesRead) override + { + bytesRead = 0u; + if (handle != 1u || offset > file.size()) + return false; + bytesRead = std::min(size, file.size() - static_cast(offset)); + if (bytesRead != 0u) + std::memcpy(destination, file.data() + offset, bytesRead); + return true; + } + void closeHostFile(uint64_t) override {} + int32_t memoryCard(const MemoryCardRequest &request) override + { + cardCalls.push_back(request); + return request.operation == MemoryCardOperation::Init ? initResult : 0; + } + bool hasGuestFunction(uint32_t) const override { return false; } + bool invokeGuestFunction(uint64_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t *) override + { + return false; + } + void log(LogLevel, std::string_view message) override { logs.emplace_back(message); } + + uint32_t word(uint32_t address) const + { + uint32_t value = 0u; + require(readGuest(address, &value, sizeof(value)), "test read outside guest RAM"); + return value; + } + void fill(uint32_t address, size_t size, uint8_t value = 0xCCu) + { + require(address <= guest.size() && size <= guest.size() - address, "test fill outside RAM"); + std::fill_n(guest.begin() + address, size, value); + } + + std::vector guest; + std::vector file; + std::vector logs; + std::vector cardCalls; + mutable size_t guestReads = 0u; + size_t guestWrites = 0u; + size_t audioCalls = 0u; + int32_t initResult = 0; + uint32_t nextHandle = 0x1000u; + }; + + inline RpcRequest request(uint32_t sid, uint32_t function, uint32_t size = 16u) + { + RpcRequest result{}; + result.sid = sid; + result.function = function; + result.receive = {0x800u, size}; + return result; + } + + inline uint64_t metric(const IopSubsystem &iop, std::string_view service, std::string_view name) + { + for (const auto &row : iop.debugSnapshot().services) + if (row.name == service) + for (const auto &entry : row.metrics) + if (entry.name == name) + return entry.value; + throw std::runtime_error("missing debug metric"); + } + + class Irx + { + public: + explicit Irx(uint32_t base = 0x10000u, uint32_t imageBytes = 0x500u) + : bytes(0x100u + imageBytes, 0u) + { + put32(0u, 0x464C457Fu); + bytes[4] = bytes[5] = bytes[6] = 1u; + put16(16u, 2u); + put16(18u, 8u); + put32(20u, 1u); + put32(24u, base); + put32(28u, 52u); + put16(40u, 52u); + put16(42u, 32u); + put16(44u, 1u); + put32(52u, 1u); + put32(56u, 0x100u); + put32(60u, base); + put32(64u, base); + put32(68u, imageBytes); + put32(72u, imageBytes); + put32(76u, 7u); + put32(80u, 4u); + } + void words(uint32_t offset, std::initializer_list values) + { + for (uint32_t value : values) + { + put32(0x100u + offset, value); + offset += 4u; + } + } + void install(Host &host, uint32_t address = 0x1000u) const + { + require(host.writeGuest(address, bytes.data(), bytes.size()), "synthetic IRX does not fit"); + } + std::vector bytes; + + private: + void put16(uint32_t offset, uint16_t value) + { + require(offset + 2u <= bytes.size(), "IRX builder overflow"); + bytes[offset] = static_cast(value); + bytes[offset + 1u] = static_cast(value >> 8u); + } + void put32(uint32_t offset, uint32_t value) + { + put16(offset, static_cast(value)); + put16(offset + 2u, static_cast(value >> 16u)); + } + }; + + inline Irx rpcServer(uint32_t sid, uint32_t reply) + { + Irx image; + image.words(0u, { + 0x27BDFFE0u, 0xAFBF001Cu, // save ra + 0x3C040001u, 0x34840200u, + 0x3C050000u | (sid >> 16u), 0x34A50000u | (sid & 0xFFFFu), + 0x3C060001u, 0x34C60300u, + 0x3C070001u, 0x34E70400u, + 0xAFA00010u, 0xAFA00014u, 0xAFA00018u, + 0x0C00401Du, 0u, // jal 0x10074: sceSifRegisterRpc + 0x8FBF001Cu, 0x00001021u, 0x27BD0020u, 0x03E00008u, 0u, + }); + image.words(0x60u, {0x41E00000u, 0u, 0x0101u, 0x63666973u, 0x0000646Du, + 0x03E00008u, 0x24000011u, 0u, 0u}); + image.words(0x300u, {0x3C020001u, 0x34420400u, 0x03E00008u, 0u}); + image.words(0x400u, {reply, reply, reply, reply}); + return image; + } + + struct Test + { + const char *name; + void (*function)(); + }; + + inline int run(std::span tests) + { + size_t failures = 0u; + for (const Test &test : tests) + { + try + { + test.function(); + std::cout << "PASS " << test.name << '\n'; + } + catch (const std::exception &error) + { + ++failures; + std::cerr << "FAIL " << test.name << ": " << error.what() << '\n'; + } + } + std::cout << tests.size() - failures << '/' << tests.size() << " cases passed\n"; + return failures == 0u ? 0 : 1; + } +} diff --git a/ps2xIOP/tests/iop_compatibility_tests.cpp b/ps2xIOP/tests/iop_compatibility_tests.cpp new file mode 100644 index 0000000..5c3bf40 --- /dev/null +++ b/ps2xIOP/tests/iop_compatibility_tests.cpp @@ -0,0 +1,315 @@ +#include "iop_compat_test_support.h" + +#include + +namespace +{ + using namespace iop_test; + constexpr uint32_t dbcSid = 0x80001300u; + constexpr uint32_t dbcVersion = 0x80001363u; + constexpr uint32_t mcSid = 0x80000400u; + + void dbcDefault() + { + Host host; + IopSubsystem iop(host); + require(!iop.canBindRpc(dbcSid), "unloaded DBCMAN must stay dormant"); + require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "DBCMAN load failed"); + auto query = request(dbcSid, dbcVersion); + require(iop.handleRpc(query).handled, "version RPC not handled"); + for (uint32_t i = 0u; i < 4u; ++i) + require(host.word(0x800u + i * 4u) == 0x0310u, "DBCMAN target version changed"); + } + + + + void dbcResetAndReconfigure() + { + Host host; + IopSubsystem iop(host); + require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed"); + require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "RPC failed"); + require(host.word(0x800u) == 0x0310u, "unexpected DBCMAN version"); + iop.reset(); + require(!iop.canBindRpc(dbcSid), "reset retained a module route"); + require(metric(iop, "dbcman", "version_queries") == 0u, "query counter not reset"); + require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "reload failed"); + require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "RPC failed"); + require(host.word(0x800u) == 0x0310u, "IOP reboot changed target version"); + } + + + void dbcReplyBounds() + { + Host host; + IopSubsystem iop(host); + require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed"); + for (uint32_t size = 0u; size <= 24u; ++size) + { + host.fill(0x7FCu, 40u); + require(iop.handleRpc(request(dbcSid, dbcVersion, size)).handled, "RPC failed"); + const uint32_t written = std::min(size / 4u, 4u) * 4u; + for (uint32_t offset = written; offset < 32u; ++offset) + require(host.guest[0x800u + offset] == 0xCCu, "reply wrote past whole-word payload"); + require(host.word(0x7FCu) == 0xCCCCCCCCu, "reply underflow"); + } + auto query = request(dbcSid, dbcVersion); + query.receive.address = 0u; + const size_t writes = host.guestWrites; + require(iop.handleRpc(query).handled && host.guestWrites == writes, "null reply was written"); + } + + void dbcNoAddressWrap() + { + Host host; + IopSubsystem iop(host); + require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed"); + auto query = request(dbcSid, dbcVersion); + query.receive.address = 0xFFFFFFF8u; + require(iop.handleRpc(query).handled, "RPC failed"); + require(host.word(0u) == 0xCCCCCCCCu && host.word(4u) == 0xCCCCCCCCu, + "overflowed reply corrupted low guest addresses"); + require(metric(iop, "dbcman", "failed_version_replies") == 1u, "invalid reply not recorded"); + } + + void dbcNoRequestGuessing() + { + Host host; + IopSubsystem iop(host); + require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "load failed"); + auto query = request(dbcSid, dbcVersion); + const std::array randomArguments{0x0310u, 0x00010000u, 0u, 0xFFFFu}; + require(host.writeGuest(0x600u, randomArguments.data(), sizeof(randomArguments)), "write failed"); + query.send = {0x600u, sizeof(randomArguments)}; + require(iop.handleRpc(query).handled, "RPC failed"); + require(host.word(0x800u) == 0x0310u, "send buffer was guessed to be a requested version"); + } + + void dbcPhysicalServerWins() + { + Host host; + IopSubsystem iop(host); + require(iop.loadModule("rom0:DBCMAN").moduleId > 0, "HLE load failed"); + auto image = rpcServer(dbcSid, 0xDEADBEEFu); + image.install(host); + auto physical = iop.loadModuleBuffer(0x1000u); + require(physical.moduleId > 0 && physical.startResult == 0, "physical IRX failed"); + require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "physical RPC not handled"); + require(host.word(0x800u) == 0xDEADBEEFu, "HLE overwrote physical server version"); + require(metric(iop, "dbcman", "version_queries") == 0u, "HLE ran after physical service"); + require(iop.stopModule(physical.moduleId), "physical stop failed"); + require(iop.handleRpc(request(dbcSid, dbcVersion)).handled, "HLE fallback not restored"); + require(host.word(0x800u) == 0x0310u, "wrong HLE version after physical stop"); + } + + void mcNewInit() + { + Host host; + IopSubsystem iop(host); + require(iop.loadModule("rom0:XMCSERV").moduleId > 0, "XMCSERV load failed"); + require(iop.handleRpc(request(mcSid, 0xFEu, 16u)).handled, "init RPC failed"); + require(host.word(0x800u) == 0u && host.word(0x804u) == 0x0205u && host.word(0x808u) == 0x0206u, + "new memory-card init layout changed"); + require(host.word(0x80Cu) == 0xCCCCCCCCu, "new init wrote beyond 12-byte response"); + } + + void mcOldInit() + { + Host host; + IopSubsystem iop(host); + require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed"); + require(iop.handleRpc(request(mcSid, 0x70u, 16u)).handled, "init RPC failed"); + require(host.word(0x800u) == 0u && host.word(0x804u) == 0xCCCCCCCCu, + "old init leaked extended protocol versions"); + } + + void mcInitFailure() + { + Host host; + IopSubsystem iop(host); + require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed"); + host.initResult = -5; + for (uint32_t operation : {0x70u, 0xFEu}) + { + require(iop.handleRpc(request(mcSid, operation)).handled, "init RPC failed"); + require(static_cast(host.word(0x800u)) == -5, "init failure reported as success"); + } + } + + void mcReplyBounds() + { + Host host; + IopSubsystem iop(host); + require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed"); + for (uint32_t operation : {0x70u, 0xFEu}) + for (uint32_t size = 0u; size <= 20u; ++size) + { + host.fill(0x7FCu, 32u); + require(iop.handleRpc(request(mcSid, operation, size)).handled, "init RPC failed"); + const uint32_t words = operation == 0xFEu ? 3u : 1u; + const uint32_t written = std::min(size / 4u, words) * 4u; + for (uint32_t offset = written; offset < 24u; ++offset) + require(host.guest[0x800u + offset] == 0xCCu, "init clobbered response tail"); + require(host.word(0x7FCu) == 0xCCCCCCCCu, "init underflowed buffer"); + } + auto query = request(mcSid, 0xFEu); + query.receive.address = 0xFFFFFFF8u; + require(iop.handleRpc(query).handled, "RPC failed"); + require(host.word(0u) == 0xCCCCCCCCu, "init overflowed guest address"); + } + + void mcShortNamePacket() + { + Host host; + IopSubsystem iop(host); + require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed"); + auto query = request(mcSid, 0x02u, 4u); + query.send = {0x1000u, 20u}; + host.fill(0x1000u, 1044u, 0u); + const size_t calls = host.cardCalls.size(); + require(iop.handleRpc(query).handled, "RPC failed"); + require(host.cardCalls.size() == calls, "short packet read a filename beyond send.size"); + require(static_cast(host.word(0x800u)) == -5, "short packet not rejected"); + } + + void mcFullNamePacket() + { + Host host; + IopSubsystem iop(host); + require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed"); + const std::array header{1u, 0u, 1u, 0u, 0u}; + host.fill(0x1000u, 1044u, 0u); + require(host.writeGuest(0x1000u, header.data(), sizeof(header)), "packet header write failed"); + constexpr char name[] = "/save.dat"; + require(host.writeGuest(0x1014u, name, sizeof(name)), "packet filename write failed"); + for (uint32_t operation : {0x02u, 0x71u}) + { + auto query = request(mcSid, operation, 4u); + query.send = {0x1000u, 1044u}; + const size_t before = host.cardCalls.size(); + require(iop.handleRpc(query).handled, "open RPC failed"); + require(host.cardCalls.size() == before + 1u, "valid packet not dispatched"); + const auto &call = host.cardCalls.back(); + require(call.operation == MemoryCardOperation::Open && + call.arguments[0] == 1u && call.arguments[1] == 0u && + call.arguments[2] == 0x1014u && call.arguments[3] == 1u, + "valid name packet decoded incorrectly"); + } + } + + void mcStatusBounds() + { + Host host; + IopSubsystem iop(host); + require(iop.loadModule("rom0:MCSERV").moduleId > 0, "MCSERV load failed"); + for (uint32_t size = 0u; size <= 20u; ++size) + { + host.fill(0x7FCu, 32u); + require(iop.handleRpc(request(mcSid, 0xFFFFFFFFu, size)).handled, "RPC failed"); + const uint32_t written = size >= 4u ? 4u : 0u; + if (written != 0u) + require(static_cast(host.word(0x800u)) == -5, "missing error status"); + for (uint32_t offset = written; offset < 24u; ++offset) + require(host.guest[0x800u + offset] == 0xCCu, "status clobbered receive tail"); + require(host.word(0x7FCu) == 0xCCCCCCCCu, "status underflowed receive buffer"); + } + auto query = request(mcSid, 0xFFFFFFFFu, 16u); + query.receive.address = 0xFFFFFFFCu; + require(iop.handleRpc(query).handled, "RPC failed"); + require(host.word(0u) == 0xCCCCCCCCu && host.word(4u) == 0xCCCCCCCCu, + "status reply wrapped and zeroed low guest memory"); + } + + void moduleAliases() + { + Host host; + IopSubsystem iop(host); + for (const char *path : {"rom0:XSIO2MAN", "rom0:XPADMAN", "rom0:XMCMAN"}) + { + auto result = iop.loadModule(path); + require(result.moduleId > 0 && result.startResult == 0, "known extended module rejected"); + } + require(!iop.canBindRpc(mcSid), "XMCMAN alone enabled a memory-card RPC server"); + const auto module = iop.loadModule("CDROM0:\\IOP\\xMcSeRv.IrX;1"); + require(module.moduleId > 0 && iop.canBindRpc(mcSid), "normalized XMCSERV alias not activated"); + require(iop.stopModule(module.moduleId) && !iop.canBindRpc(mcSid), "stopped alias remained active"); + } + + void unknownModulesStayUnknown() + { + Host host; + IopSubsystem iop(host); + for (const char *name : {"MC2_D.IRX", "DS2U_D.IRX", "CDVDSTM.IRX", "SDRDRV.IRX", "EZPCM.IRX", "ANYTHING_D.IRX"}) + { + const auto result = iop.loadModule(std::string("host0:IOPModules/") + name); + require(result.moduleId < 0 && result.startResult < 0, "unsupported module got a fake success"); + } + require(!iop.canBindRpc(0x19740512u), "game-specific SDRDRV activated globally"); + } + + void moduleLifetime() + { + Host host; + IopSubsystem iop(host); + const auto a = iop.loadModule("rom0:DBCMAN"); + const auto b = iop.loadModule("rom0:dbcman.irx"); + const auto alias = iop.loadModule("rom0:DBCM"); + require(a.moduleId > 0 && a.moduleId == b.moduleId && alias.moduleId > 0, "module IDs unstable"); + require(iop.stopModule(a.moduleId) && iop.canBindRpc(dbcSid), "first release removed shared route"); + require(iop.stopModule(b.moduleId) && iop.canBindRpc(dbcSid), "remaining alias not honored"); + require(iop.stopModule(alias.moduleId) && !iop.canBindRpc(dbcSid), "last release retained route"); + } + + void loaderDiagnostics() + { + Host host; + IopSubsystem iop(host); + require(iop.loadModule("host0:LIBSD.IRX").moduleId > 0, "LIBSD fallback failed"); + require(iop.loadModule("host0:MISSING.IRX").moduleId < 0, "unknown load accepted"); + for (unsigned i = 0u; i < 100u; ++i) + (void)iop.loadModule("host0:MISSING.IRX"); + auto snapshot = iop.debugSnapshot(); + require(snapshot.diagnostics.size() == 2u, "final loader outcomes not deduplicated"); + require(snapshot.diagnostics[0].find("[IOP:HLE]") != std::string::npos, "no fallback diagnostic"); + require(snapshot.diagnostics[1].find("no HLE provider") != std::string::npos, "no final failure diagnostic"); + for (unsigned i = 0u; i < 100u; ++i) + (void)iop.loadModule("rom0:missing" + std::to_string(i)); + require(iop.debugSnapshot().diagnostics.size() <= 32u, "unbounded module diagnostics"); + iop.reset(); + require(iop.debugSnapshot().diagnostics.empty(), "stale load outcomes survived reset"); + } + + void libsdUnchanged() + { + Host host; + IopSubsystem iop(host); + require(iop.loadModule("rom0:LIBSD").moduleId > 0, "LIBSD load failed"); + require(iop.handleRpc(request(0x80000701u, 0x8010u)).handled, "LIBSD RPC not handled"); + require(host.audioCalls == 1u, "DBCMAN option intercepted LIBSD RPC"); + } +} + +int main() +{ + const Test tests[] = { + {"DBCMAN default and dormant route", dbcDefault}, + {"DBCMAN reboot and reconfiguration", dbcResetAndReconfigure}, + {"DBCMAN bounded whole-word response", dbcReplyBounds}, + {"DBCMAN rejects wrapping reply addresses", dbcNoAddressWrap}, + {"DBCMAN does not infer version from arbitrary RPC payload", dbcNoRequestGuessing}, + {"Physical DBCMAN server wins over configured HLE", dbcPhysicalServerWins}, + {"XMCSERV init status and two version fields", mcNewInit}, + {"Old MCSERV init is status only", mcOldInit}, + {"MCSERV propagates initialization failure", mcInitFailure}, + {"MCSERV response bounds for both dialects", mcReplyBounds}, + {"MCSERV rejects truncated name packet", mcShortNamePacket}, + {"MCSERV accepts complete name packets in both dialects", mcFullNamePacket}, + {"MCSERV status replies preserve bounds and cannot wrap", mcStatusBounds}, + {"Extended module aliases and activation", moduleAliases}, + {"Unsupported debug and game IRX stay unsupported", unknownModulesStayUnknown}, + {"HLE repeated loads and alias lifetime", moduleLifetime}, + {"Loader outcomes are bounded and resettable", loaderDiagnostics}, + {"LIBSD audio dispatch is unchanged", libsdUnchanged}, + }; + return run(tests); +} diff --git a/ps2xIOP/tests/iop_emulator_tests.cpp b/ps2xIOP/tests/iop_emulator_tests.cpp new file mode 100644 index 0000000..44c2494 --- /dev/null +++ b/ps2xIOP/tests/iop_emulator_tests.cpp @@ -0,0 +1,1147 @@ +#include "ps2x/iop/iop_subsystem.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + using namespace ps2x::iop; + + class TestHost final : public IopHost + { + public: + explicit TestHost(size_t guestBytes = 4096u) + : guest(guestBytes, 0u) + { + } + + bool readGuest(uint32_t address, void *destination, size_t size) const override + { + if ((!destination && size != 0u) || address > guest.size() || size > guest.size() - address) + return false; + if (size != 0u) + std::memcpy(destination, guest.data() + address, size); + return true; + } + + bool writeGuest(uint32_t address, const void *source, size_t size) override + { + if ((!source && size != 0u) || address > guest.size() || size > guest.size() - address) + return false; + if (size != 0u) + std::memcpy(guest.data() + address, source, size); + return true; + } + + bool zeroGuest(uint32_t address, size_t size) override + { + if (address > guest.size() || size > guest.size() - address) + return false; + if (size != 0u) + std::memset(guest.data() + address, 0, size); + return true; + } + + bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const override + { + normalized = address; + return address <= guest.size(); + } + + uint32_t allocateIopHandle(IopHandleKind) override { return 1u; } + uint32_t allocateGuest(uint32_t, uint32_t) override { return 0u; } + void freeGuest(uint32_t) override {} + void audioCommand(uint32_t, uint32_t, GuestBuffer, GuestBuffer) override {} + std::string hostPath(HostPathKind kind) const override + { + return kind == HostPathKind::CdRoot ? cdRoot : std::string{}; + } + std::string translateGuestPath(std::string_view path) const override { return std::string(path); } + uint64_t openHostFile(std::string_view) override { return 0u; } + bool hostFileSize(uint64_t, uint64_t &) const override { return false; } + bool readHostFile(uint64_t, uint64_t, void *, size_t, size_t &) override { return false; } + void closeHostFile(uint64_t) override {} + int32_t memoryCard(const MemoryCardRequest &) override { return 0; } + bool hasGuestFunction(uint32_t) const override { return false; } + bool invokeGuestFunction(uint64_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t *) override { return false; } + void log(LogLevel, std::string_view message) override { logs.emplace_back(message); } + + std::vector guest; + std::vector logs; + std::string cdRoot; + }; + +#pragma pack(push, 1) + struct ElfHeader + { + uint8_t ident[16]; + uint16_t type; + uint16_t machine; + uint32_t version; + uint32_t entry; + uint32_t phoff; + uint32_t shoff; + uint32_t flags; + uint16_t ehsize; + uint16_t phentsize; + uint16_t phnum; + uint16_t shentsize; + uint16_t shnum; + uint16_t shstrndx; + }; + + struct ProgramHeader + { + uint32_t type; + uint32_t offset; + uint32_t vaddr; + uint32_t paddr; + uint32_t filesz; + uint32_t memsz; + uint32_t flags; + uint32_t align; + }; + + struct SectionHeader + { + uint32_t name; + uint32_t type; + uint32_t flags; + uint32_t address; + uint32_t offset; + uint32_t size; + uint32_t link; + uint32_t info; + uint32_t alignment; + uint32_t entrySize; + }; + + struct Relocation + { + uint32_t offset; + uint32_t info; + }; +#pragma pack(pop) + + static_assert(sizeof(ElfHeader) == 52u); + static_assert(sizeof(ProgramHeader) == 32u); + static_assert(sizeof(SectionHeader) == 40u); + static_assert(sizeof(Relocation) == 8u); + + void writeMinimalIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; + header.ident[1] = 'E'; + header.ident[2] = 'L'; + header.ident[3] = 'F'; + header.ident[4] = 1u; + header.ident[5] = 1u; + header.ident[6] = 1u; + header.type = 2u; + header.machine = 8u; + header.version = 1u; + header.entry = loadAddress; + header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); + header.phentsize = sizeof(ProgramHeader); + header.phnum = 1u; + + // Branch is deliberately included so the test checks that its delay slot runs. + const uint32_t code[] = { + 0x24080001u, // addiu t0, zero, 1 + 0x11080002u, // beq t0, t0, +2 + 0x24020007u, // addiu v0, zero, 7 (delay slot) + 0x24020063u, // addiu v0, zero, 99 (must be skipped) + 0x03E00008u, // jr ra + 0x00000000u, // nop + }; + + ProgramHeader program{}; + program.type = 1u; + program.offset = codeOffset; + program.vaddr = loadAddress; + program.paddr = loadAddress; + program.filesz = sizeof(code); + program.memsz = sizeof(code); + program.flags = 5u; + program.align = 4u; + + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, code, sizeof(code)); + } + + void writeRpcServerIrx(TestHost &host, + uint32_t address, + uint32_t sid = 0xF00DCAFEu) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t importTableOffset = 0x60u; + constexpr uint32_t importStubAddress = loadAddress + importTableOffset + 20u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; + header.ident[1] = 'E'; + header.ident[2] = 'L'; + header.ident[3] = 'F'; + header.ident[4] = 1u; + header.ident[5] = 1u; + header.ident[6] = 1u; + header.type = 2u; + header.machine = 8u; + header.version = 1u; + header.entry = loadAddress; + header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); + header.phentsize = sizeof(ProgramHeader); + header.phnum = 1u; + + const uint32_t code[] = { + 0x27BDFFE0u, // addiu sp, sp, -0x20 + 0xAFBF001Cu, // sw ra, 0x1c(sp) + 0x3C040001u, // lui a0, 1 + 0x34840200u, // ori a0, a0, 0x200 (server data) + 0x3C050000u | ((sid >> 16u) & 0xFFFFu), // lui a1, SID upper + 0x34A50000u | (sid & 0xFFFFu), // ori a1, a1, SID lower + 0x3C060001u, // lui a2, 1 + 0x34C60300u, // ori a2, a2, 0x300 (server function) + 0x3C070001u, // lui a3, 1 + 0x34E70400u, // ori a3, a3, 0x400 (server buffer) + 0xAFA00010u, // sw zero, 0x10(sp) + 0xAFA00014u, // sw zero, 0x14(sp) + 0xAFA00018u, // sw zero, 0x18(sp) + 0x0C000000u | ((importStubAddress >> 2u) & 0x03FFFFFFu), // jal sceSifRegisterRpc + 0x00000000u, // nop + 0x8FBF001Cu, // lw ra, 0x1c(sp) + 0x00001021u, // addu v0, zero, zero + 0x27BD0020u, // addiu sp, sp, 0x20 + 0x03E00008u, // jr ra + 0x00000000u, // nop + }; + const uint32_t importTable[] = { + 0x41E00000u, // IRX import magic + 0x00000000u, + 0x00000101u, + 0x63666973u, // "sifc" + 0x0000646Du, // "md" + 0x03E00008u, // jr ra + 0x24000011u, // addiu zero, zero, 17 (sceSifRegisterRpc) + 0x00000000u, + 0x00000000u, + }; + + ProgramHeader program{}; + program.type = 1u; + program.offset = codeOffset; + program.vaddr = loadAddress; + program.paddr = loadAddress; + program.filesz = 0xA0u; + program.memsz = 0x500u; + program.flags = 7u; + program.align = 4u; + + std::fill(host.guest.begin() + address, host.guest.end(), 0u); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, code, sizeof(code)); + std::memcpy(host.guest.data() + address + codeOffset + importTableOffset, + importTable, sizeof(importTable)); + } + + void writeRelocatableRpcServerIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t entryAddress = 0x20u; + constexpr uint32_t importTableAddress = 0x80u; + constexpr uint32_t importStubAddress = importTableAddress + 20u; + constexpr uint32_t handlerAddress = 0xC0u; + constexpr uint32_t gpAddress = 0xD0u; + constexpr uint32_t iopModFileOffset = 0xE0u; + constexpr uint32_t relocationFileOffset = 0x200u; + constexpr uint32_t sectionTableOffset = 0x240u; + constexpr uint32_t rpcSid = 0xA11CE001u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; + header.ident[1] = 'E'; + header.ident[2] = 'L'; + header.ident[3] = 'F'; + header.ident[4] = 1u; + header.ident[5] = 1u; + header.ident[6] = 1u; + header.type = 0xFF80u; // ET_SCE_IOPRELEXEC + header.machine = 8u; + header.version = 1u; + header.entry = entryAddress; + header.phoff = sizeof(ElfHeader); + header.shoff = sectionTableOffset; + header.ehsize = sizeof(ElfHeader); + header.phentsize = sizeof(ProgramHeader); + header.phnum = 2u; + header.shentsize = sizeof(SectionHeader); + header.shnum = 3u; + + const uint32_t code[] = { + 0x27BDFFE0u, // addiu sp, sp, -0x20 + 0xAFBF001Cu, // sw ra, 0x1c(sp) + 0x00002021u, // addu a0, zero, zero + 0x3C05A11Cu, // lui a1, 0xa11c + 0x34A5E001u, // ori a1, a1, 0xe001 + 0x3C060000u, // lui a2, 0 (HI16 handler address) + 0x24C600C0u, // addiu a2, a2, 0xc0 (LO16 handler address) + 0x00003821u, // addu a3, zero, zero + 0xAFA00010u, // sw zero, 0x10(sp) + 0xAFA00014u, // sw zero, 0x14(sp) + 0xAFA00018u, // sw zero, 0x18(sp) + 0x0C000000u | ((importStubAddress >> 2u) & 0x03FFFFFFu), // jal sceSifRegisterRpc + 0x00000000u, // nop + 0x8FBF001Cu, // lw ra, 0x1c(sp) + 0x00001021u, // addu v0, zero, zero + 0x27BD0020u, // addiu sp, sp, 0x20 + 0x03E00008u, // jr ra + 0x00000000u, // nop + }; + const uint32_t importTable[] = { + 0x41E00000u, + 0x00000000u, + 0x00000101u, + 0x63666973u, // "sifc" + 0x0000646Du, // "md" + 0x03E00008u, + 0x24000011u, // sceSifRegisterRpc + 0x00000000u, + }; + const uint32_t handler[] = { + 0x03E00008u, // jr ra + 0x03801021u, // addu v0, gp, zero + }; + constexpr uint32_t gpData = 0x47505250u; // "GPRP" + const uint32_t iopModuleHeader[] = { + 0u, + entryAddress, + gpAddress, + }; + + ProgramHeader iopModule{}; + iopModule.type = 0x70000080u; // PT_SCE_IOPMOD + iopModule.offset = iopModFileOffset; + iopModule.filesz = sizeof(iopModuleHeader); + iopModule.memsz = sizeof(iopModuleHeader); + iopModule.align = 4u; + + ProgramHeader program{}; + program.type = 1u; + program.offset = codeOffset; + program.vaddr = 0u; + program.paddr = 0u; + program.filesz = gpAddress + sizeof(gpData); + program.memsz = 0x500u; + program.flags = 7u; + program.align = 4u; + + SectionHeader text{}; + text.type = 1u; // SHT_PROGBITS + text.flags = 0x6u; // SHF_ALLOC | SHF_EXECINSTR + text.address = entryAddress; + text.offset = codeOffset + entryAddress; + text.size = 0xB0u; + text.alignment = 4u; + + SectionHeader relocations{}; + relocations.type = 9u; // SHT_REL + relocations.offset = relocationFileOffset; + relocations.size = 3u * sizeof(Relocation); + relocations.info = 1u; // target .text + relocations.alignment = 4u; + relocations.entrySize = sizeof(Relocation); + + const Relocation relocationData[] = { + {entryAddress + 5u * sizeof(uint32_t), 5u}, // symbol 0, R_MIPS_HI16 + {entryAddress + 6u * sizeof(uint32_t), 6u}, // symbol 0, R_MIPS_LO16 + {entryAddress + 11u * sizeof(uint32_t), 4u}, // symbol 0, R_MIPS_26 + }; + + std::fill(host.guest.begin() + address, host.guest.end(), 0u); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &iopModule, sizeof(iopModule)); + std::memcpy(host.guest.data() + address + sizeof(header) + sizeof(iopModule), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + iopModFileOffset, + iopModuleHeader, sizeof(iopModuleHeader)); + std::memcpy(host.guest.data() + address + codeOffset + entryAddress, code, sizeof(code)); + std::memcpy(host.guest.data() + address + codeOffset + importTableAddress, + importTable, sizeof(importTable)); + std::memcpy(host.guest.data() + address + codeOffset + handlerAddress, + handler, sizeof(handler)); + std::memcpy(host.guest.data() + address + codeOffset + gpAddress, + &gpData, sizeof(gpData)); + std::memcpy(host.guest.data() + address + relocationFileOffset, + relocationData, sizeof(relocationData)); + std::memcpy(host.guest.data() + address + sectionTableOffset + sizeof(SectionHeader), + &text, sizeof(text)); + std::memcpy(host.guest.data() + address + sectionTableOffset + 2u * sizeof(SectionHeader), + &relocations, sizeof(relocations)); + } + + void writeExportProviderIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t targetAddress = loadAddress + 0x60u; + constexpr uint32_t exportTableAddress = loadAddress + 0x80u; + constexpr uint32_t importTableAddress = loadAddress + 0xC0u; + constexpr uint32_t importStubAddress = importTableAddress + 20u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0xF0u; program.memsz = 0xF0u; program.flags = 7u; program.align = 4u; + + const uint32_t code[] = { + 0x27BDFFE0u, 0xAFBF001Cu, + 0x3C040001u, 0x34840080u, + 0x0C000000u | ((importStubAddress >> 2u) & 0x03FFFFFFu), 0x00000000u, + 0x8FBF001Cu, 0x00001021u, + 0x27BD0020u, 0x03E00008u, 0x00000000u, + }; + const uint32_t target[] = {0x03E00008u, 0x24020042u}; + const uint32_t exportTable[] = { + 0x41C00000u, 0u, 0x00000101u, + 0x6C747374u, 0x00006269u, // "tstlib" + loadAddress, loadAddress, loadAddress, targetAddress, 0u, + }; + const uint32_t importTable[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x64616F6Cu, 0x65726F63u, // "loadcore" + 0x03E00008u, 0x24000006u, 0u, 0u, + }; + + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, code, sizeof(code)); + std::memcpy(host.guest.data() + address + codeOffset + 0x60u, target, sizeof(target)); + std::memcpy(host.guest.data() + address + codeOffset + 0x80u, exportTable, sizeof(exportTable)); + std::memcpy(host.guest.data() + address + codeOffset + 0xC0u, importTable, sizeof(importTable)); + } + + void writeExportConsumerIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00011000u; + constexpr uint32_t importTableAddress = loadAddress + 0x40u; + constexpr uint32_t importStubAddress = importTableAddress + 20u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0x80u; program.memsz = 0x80u; program.flags = 7u; program.align = 4u; + + const uint32_t code[] = { + 0x27BDFFF0u, 0xAFBF000Cu, + 0x0C000000u | ((importStubAddress >> 2u) & 0x03FFFFFFu), 0x00000000u, + 0x8FBF000Cu, 0x27BD0010u, + 0x03E00008u, 0x00000000u, + }; + const uint32_t importTable[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x6C747374u, 0x00006269u, // "tstlib" + 0x03E00008u, 0x24000003u, 0u, 0u, + }; + + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, code, sizeof(code)); + std::memcpy(host.guest.data() + address + codeOffset + 0x40u, importTable, sizeof(importTable)); + } + + void writeVblankSchedulingIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t highThreadAddress = loadAddress + 0x100u; + constexpr uint32_t lowThreadAddress = loadAddress + 0x140u; + constexpr uint32_t thbaseTableAddress = loadAddress + 0x180u; + constexpr uint32_t createThreadStub = thbaseTableAddress + 20u; + constexpr uint32_t startThreadStub = createThreadStub + 8u; + constexpr uint32_t vblankTableAddress = loadAddress + 0x1C0u; + constexpr uint32_t waitVblankEndStub = vblankTableAddress + 20u; + constexpr uint32_t highThreadDescriptor = loadAddress + 0x300u; + constexpr uint32_t lowThreadDescriptor = loadAddress + 0x320u; + constexpr uint32_t lowThreadMarker = loadAddress + 0x400u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0x500u; program.memsz = 0x500u; program.flags = 7u; program.align = 4u; + + const auto jal = [](uint32_t target) { return 0x0C000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const auto jump = [](uint32_t target) { return 0x08000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const uint32_t entry[] = { + 0x27BDFFE0u, 0xAFBF001Cu, + 0x3C040001u, 0x34840300u, jal(createThreadStub), 0x00000000u, + 0x00408021u, // move s0, v0 + 0x3C040001u, 0x34840320u, jal(createThreadStub), 0x00000000u, + 0x00408821u, // move s1, v0 + 0x02002021u, 0x00002821u, jal(startThreadStub), 0x00000000u, + 0x02202021u, 0x00002821u, jal(startThreadStub), 0x00000000u, + 0x8FBF001Cu, 0x00001021u, 0x27BD0020u, 0x03E00008u, 0x00000000u, + }; + const uint32_t highThread[] = { + jal(waitVblankEndStub), 0x00000000u, + jump(highThreadAddress), 0x00000000u, + }; + const uint32_t lowThread[] = { + 0x3C080001u, 0x35080400u, + 0x24090001u, 0xAD090000u, + 0x03E00008u, 0x00000000u, + }; + const uint32_t thbaseImports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x61626874u, 0x00006573u, // "thbase" + 0x03E00008u, 0x24000004u, // CreateThread + 0x03E00008u, 0x24000006u, // StartThread + 0u, 0u, + }; + const uint32_t vblankImports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x616C6276u, 0x00006B6Eu, // "vblank" + 0x03E00008u, 0x24000005u, // WaitVblankEnd + 0u, 0u, + }; + const uint32_t highDescriptor[] = {0u, 0u, highThreadAddress, 0x400u, 10u}; + const uint32_t lowDescriptor[] = {0u, 0u, lowThreadAddress, 0x400u, 20u}; + + std::vector segment(program.filesz, 0u); + const auto put = [&](uint32_t offset, const void *data, size_t size) + { + std::memcpy(segment.data() + offset, data, size); + }; + put(0u, entry, sizeof(entry)); + put(0x100u, highThread, sizeof(highThread)); + put(0x140u, lowThread, sizeof(lowThread)); + put(0x180u, thbaseImports, sizeof(thbaseImports)); + put(0x1C0u, vblankImports, sizeof(vblankImports)); + put(0x300u, highDescriptor, sizeof(highDescriptor)); + put(0x320u, lowDescriptor, sizeof(lowDescriptor)); + + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, segment.data(), segment.size()); + } + + void writeSifDmaIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t importTableAddress = loadAddress + 0x100u; + constexpr uint32_t setDmaStub = importTableAddress + 20u; + constexpr uint32_t dmaStatStub = setDmaStub + 8u; + constexpr uint32_t descriptorAddress = loadAddress + 0x180u; + constexpr uint32_t payloadAddress = loadAddress + 0x1A0u; + constexpr uint32_t eeDestination = 0xC00u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0x200u; program.memsz = 0x200u; program.flags = 7u; program.align = 4u; + + const auto jal = [](uint32_t target) { return 0x0C000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const uint32_t entry[] = { + 0x27BDFFF0u, 0xAFBF000Cu, + 0x3C040001u, 0x34840180u, 0x24050001u, + jal(setDmaStub), 0x00000000u, + 0x00402021u, // move a0, v0 + jal(dmaStatStub), 0x00000000u, + 0x8FBF000Cu, 0x27BD0010u, 0x03E00008u, 0x00000000u, + }; + const uint32_t imports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x6D666973u, 0x00006E61u, // "sifman" + 0x03E00008u, 0x24000007u, // sceSifSetDma + 0x03E00008u, 0x24000008u, // sceSifDmaStat + 0u, 0u, + }; + const uint32_t descriptor[] = { + payloadAddress, eeDestination, sizeof(uint32_t), 0u, + }; + constexpr uint32_t payload = 0x53494621u; // "SIF!" + + std::vector segment(program.filesz, 0u); + std::memcpy(segment.data(), entry, sizeof(entry)); + std::memcpy(segment.data() + 0x100u, imports, sizeof(imports)); + std::memcpy(segment.data() + 0x180u, descriptor, sizeof(descriptor)); + std::memcpy(segment.data() + 0x1A0u, &payload, sizeof(payload)); + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, segment.data(), segment.size()); + } + + void writeMcmanRegistrationIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t initAddress = loadAddress + 0x100u; + constexpr uint32_t deinitAddress = loadAddress + 0x120u; + constexpr uint32_t callbackAddress = loadAddress + 0x140u; + constexpr uint32_t deviceAddress = loadAddress + 0x200u; + constexpr uint32_t operationsAddress = loadAddress + 0x220u; + constexpr uint32_t nameAddress = loadAddress + 0x280u; + constexpr uint32_t markerAddress = loadAddress + 0x290u; + constexpr uint32_t secrmanTableAddress = loadAddress + 0x300u; + constexpr uint32_t secrCommandStub = secrmanTableAddress + 20u; + constexpr uint32_t secrDeviceIdStub = secrCommandStub + 8u; + constexpr uint32_t modloadTableAddress = loadAddress + 0x340u; + constexpr uint32_t setKelfCallbackStub = modloadTableAddress + 20u; + constexpr uint32_t iomanTableAddress = loadAddress + 0x380u; + constexpr uint32_t deleteMissingDriverStub = iomanTableAddress + 20u; + constexpr uint32_t addDriverStub = deleteMissingDriverStub + 8u; + constexpr uint32_t deleteDriverStub = addDriverStub + 8u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0x400u; program.memsz = 0x400u; program.flags = 7u; program.align = 4u; + + const auto jal = [](uint32_t target) { return 0x0C000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const auto jump = [](uint32_t target) { return 0x08000000u | ((target >> 2u) & 0x03FFFFFFu); }; + constexpr uint32_t epilogueAddress = loadAddress + 35u * sizeof(uint32_t); + const uint32_t entry[] = { + 0x27BDFFE0u, 0xAFBF001Cu, + 0x3C040001u, 0x34840140u, jal(secrCommandStub), 0x00000000u, + 0x3C040001u, 0x34840140u, jal(secrDeviceIdStub), 0x00000000u, + 0x3C040001u, 0x34840140u, jal(setKelfCallbackStub), 0x00000000u, + 0x3C040001u, 0x34840280u, jal(deleteMissingDriverStub), 0x00000000u, + 0x3C040001u, 0x34840200u, jal(addDriverStub), 0x00000000u, + 0x1440000Bu, 0x00000000u, // bnez v0, failure + 0x3C040001u, 0x34840280u, jal(deleteDriverStub), 0x00000000u, + 0x14400005u, 0x00000000u, // bnez v0, failure + 0x3C080001u, 0x8D020290u, jump(epilogueAddress), 0x00000000u, + 0x2402FFFFu, // failure: return -1 + 0x8FBF001Cu, 0x27BD0020u, 0x03E00008u, 0x00000000u, + }; + const uint32_t init[] = { + 0x3C080001u, 0x35080290u, 0x24090001u, 0xAD090000u, + 0x03E00008u, 0x00001021u, + }; + const uint32_t deinit[] = { + 0x3C080001u, 0x35080290u, 0x8D090000u, 0x00000000u, + 0x25290001u, 0xAD090000u, 0x03E00008u, 0x00001021u, + }; + const uint32_t callback[] = {0x03E00008u, 0x00001021u}; + const uint32_t device[] = {nameAddress, 0u, 0u, 0u, operationsAddress}; + uint32_t operations[17]{}; + operations[0] = initAddress; + operations[1] = deinitAddress; + const char name[] = "mc"; + const uint32_t secrmanImports[] = { + 0x41E00000u, 0u, 0x00000104u, + 0x72636573u, 0x006E616Du, // "secrman" + 0x03E00008u, 0x24000004u, + 0x03E00008u, 0x24000005u, + 0u, 0u, + }; + const uint32_t modloadImports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x6C646F6Du, 0x0064616Fu, // "modload" + 0x03E00008u, 0x2400000Du, + 0u, 0u, + }; + const uint32_t iomanImports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x616D6F69u, 0x0000006Eu, // "ioman" + 0x03E00008u, 0x24000015u, // DelDrv + 0x03E00008u, 0x24000014u, // AddDrv + 0x03E00008u, 0x24000015u, // DelDrv + 0u, 0u, + }; + + std::vector segment(program.filesz, 0u); + const auto put = [&](uint32_t offset, const void *data, size_t size) + { + std::memcpy(segment.data() + offset, data, size); + }; + put(0x000u, entry, sizeof(entry)); + put(0x100u, init, sizeof(init)); + put(0x120u, deinit, sizeof(deinit)); + put(0x140u, callback, sizeof(callback)); + put(0x200u, device, sizeof(device)); + put(0x220u, operations, sizeof(operations)); + put(0x280u, name, sizeof(name)); + put(0x300u, secrmanImports, sizeof(secrmanImports)); + put(0x340u, modloadImports, sizeof(modloadImports)); + put(0x380u, iomanImports, sizeof(iomanImports)); + + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, segment.data(), segment.size()); + } + + void writeCdvdLifecycleIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t importTableAddress = loadAddress + 0x100u; + constexpr uint32_t initStub = importTableAddress + 20u; + constexpr uint32_t callbackStub = initStub + 8u; + constexpr uint32_t epilogueAddress = loadAddress + 26u * sizeof(uint32_t); + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0x140u; program.memsz = 0x140u; program.flags = 7u; program.align = 4u; + + const auto jal = [](uint32_t target) { return 0x0C000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const auto jump = [](uint32_t target) { return 0x08000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const uint32_t entry[] = { + 0x27BDFFF0u, 0xAFBF000Cu, + 0x00002021u, jal(initStub), 0x00000000u, + 0x24080001u, 0x14480012u, 0x00000000u, + 0x3C041234u, 0x34845678u, jal(callbackStub), 0x00000000u, + 0x1440000Cu, 0x00000000u, + 0x3C0489ABu, 0x3484CDEFu, jal(callbackStub), 0x00000000u, + 0x3C081234u, 0x35085678u, 0x14480004u, 0x00000000u, + 0x00001021u, jump(epilogueAddress), 0x00000000u, + 0x2402FFFFu, + 0x8FBF000Cu, 0x27BD0010u, 0x03E00008u, 0x00000000u, + }; + const uint32_t imports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x64766463u, 0x006E616Du, // "cdvdman" + 0x03E00008u, 0x24000004u, // sceCdInit + 0x03E00008u, 0x24000025u, // sceCdCallback + 0u, 0u, + }; + + std::vector segment(program.filesz, 0u); + std::memcpy(segment.data(), entry, sizeof(entry)); + std::memcpy(segment.data() + 0x100u, imports, sizeof(imports)); + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, segment.data(), segment.size()); + } + + void writeCdvdPvdReadIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t readBuffer = loadAddress + 0x800u; + constexpr uint32_t importTableAddress = loadAddress + 0x100u; + constexpr uint32_t callbackStub = importTableAddress + 20u; + constexpr uint32_t readStub = callbackStub + 8u; + constexpr uint32_t callbackFunction = loadAddress + 0x180u; + constexpr uint32_t epilogueAddress = loadAddress + 26u * sizeof(uint32_t); + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0x200u; program.memsz = 0x1000u; program.flags = 7u; program.align = 4u; + + const auto jal = [](uint32_t target) { return 0x0C000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const auto jump = [](uint32_t target) { return 0x08000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const uint32_t entry[] = { + 0x27BDFFF0u, 0xAFBF000Cu, + 0x3C040001u, 0x34840180u, jal(callbackStub), 0x00000000u, + 0x24040010u, 0x24050001u, + 0x3C060001u, 0x34C60800u, jal(readStub), 0x00000000u, + 0x1040000Cu, 0x00000000u, + 0x90C80000u, 0x24090001u, 0x15090008u, 0x00000000u, + 0x90C80001u, 0x24090043u, 0x15090004u, 0x00000000u, + 0x00001021u, jump(epilogueAddress), 0x00000000u, + 0x2402FFFFu, + 0x8FBF000Cu, 0x27BD0010u, 0x03E00008u, 0x00000000u, + }; + const uint32_t imports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x64766463u, 0x006E616Du, // "cdvdman" + 0x03E00008u, 0x24000025u, // sceCdCallback + 0x03E00008u, 0x24000006u, // sceCdRead + 0u, 0u, + }; + const uint32_t callback[] = { + 0x3C080001u, 0x350801C0u, 0xAD040000u, + 0x03E00008u, 0x00000000u, + }; + + std::vector segment(program.filesz, 0u); + std::memcpy(segment.data(), entry, sizeof(entry)); + std::memcpy(segment.data() + 0x100u, imports, sizeof(imports)); + std::memcpy(segment.data() + (callbackFunction - loadAddress), callback, sizeof(callback)); + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, segment.data(), segment.size()); + } + + void writeCdvdSeekIrx(TestHost &host, uint32_t address) + { + constexpr uint32_t codeOffset = 0x100u; + constexpr uint32_t loadAddress = 0x00010000u; + constexpr uint32_t importTableAddress = loadAddress + 0x100u; + constexpr uint32_t initStub = importTableAddress + 20u; + constexpr uint32_t callbackStub = initStub + 8u; + constexpr uint32_t seekStub = callbackStub + 8u; + constexpr uint32_t callbackFunction = loadAddress + 0x180u; + + ElfHeader header{}; + header.ident[0] = 0x7Fu; header.ident[1] = 'E'; header.ident[2] = 'L'; header.ident[3] = 'F'; + header.ident[4] = 1u; header.ident[5] = 1u; header.ident[6] = 1u; + header.type = 2u; header.machine = 8u; header.version = 1u; + header.entry = loadAddress; header.phoff = sizeof(ElfHeader); + header.ehsize = sizeof(ElfHeader); header.phentsize = sizeof(ProgramHeader); header.phnum = 1u; + + ProgramHeader program{}; + program.type = 1u; program.offset = codeOffset; program.vaddr = loadAddress; program.paddr = loadAddress; + program.filesz = 0x200u; program.memsz = 0x200u; program.flags = 7u; program.align = 4u; + + const auto jal = [](uint32_t target) { return 0x0C000000u | ((target >> 2u) & 0x03FFFFFFu); }; + const uint32_t entry[] = { + 0x27BDFFF0u, 0xAFBF000Cu, + 0x00002021u, jal(initStub), 0x00000000u, + 0x3C040001u, 0x34840180u, jal(callbackStub), 0x00000000u, + 0x24041234u, jal(seekStub), 0x00000000u, + 0x8FBF000Cu, 0x27BD0010u, 0x03E00008u, 0x00000000u, + }; + const uint32_t imports[] = { + 0x41E00000u, 0u, 0x00000101u, + 0x64766463u, 0x006E616Du, // "cdvdman" + 0x03E00008u, 0x24000004u, // sceCdInit + 0x03E00008u, 0x24000025u, // sceCdCallback + 0x03E00008u, 0x24000007u, // sceCdSeek + 0u, 0u, + }; + const uint32_t callback[] = { + 0x3C080001u, 0x350801C0u, 0xAD040000u, + 0x03E00008u, 0x00001021u, + }; + + std::vector segment(program.filesz, 0u); + std::memcpy(segment.data(), entry, sizeof(entry)); + std::memcpy(segment.data() + 0x100u, imports, sizeof(imports)); + std::memcpy(segment.data() + (callbackFunction - loadAddress), callback, sizeof(callback)); + std::memset(host.guest.data() + address, 0, codeOffset + program.filesz); + std::memcpy(host.guest.data() + address, &header, sizeof(header)); + std::memcpy(host.guest.data() + address + sizeof(header), &program, sizeof(program)); + std::memcpy(host.guest.data() + address + codeOffset, segment.data(), segment.size()); + } + + bool expect(bool value, const char *message) + { + if (!value) + std::cerr << "FAIL: " << message << '\n'; + return value; + } +} + +int main() +{ + TestHost host(0x20000u); + IopSubsystem iop(host); + + writeMinimalIrx(host, 0x100u); + const ModuleLoadResult result = iop.loadModuleBuffer(0x100u); + const DebugSnapshot snapshot = iop.debugSnapshot(); + + if (!expect(result.handled, "Emulator must claim IRX loads")) return 1; + if (!expect(result.moduleId == 1, "First emulated IRX must get module id 1")) return 1; + if (!expect(result.startResult == 7, "R3000A branch delay slot result mismatch")) return 1; + if (!expect(snapshot.emulatorLoadedModules == 1u, "Loaded-module debug count mismatch")) return 1; + if (!expect(snapshot.emulatorInstructions >= 5u, "Instruction counter did not advance")) return 1; + + int32_t stopResult = -1; + if (!expect(iop.stopModule(result.moduleId, &stopResult), "Emulated module stop failed")) return 1; + if (!expect(stopResult == 0, "Emulated module stop result mismatch")) return 1; + if (!expect(iop.debugSnapshot().emulatorLoadedModules == 0u, "Module was not released")) return 1; + + constexpr uint32_t rpcSid = 0xF00DCAFEu; + writeRpcServerIrx(host, 0x100u); + const ModuleLoadResult rpcModule = iop.loadModuleBuffer(0x100u); + if (!expect(rpcModule.handled && rpcModule.startResult == 0, + "Synthetic RPC server IRX did not start")) return 1; + if (!expect(iop.debugSnapshot().emulatorRpcServers == 1u, + "Registered RPC server count mismatch")) return 1; + if (!expect(iop.canBindRpc(rpcSid), + "Emulator did not expose the SID registered by the IRX")) return 1; + + iop.reset(); + if (!expect(!iop.canBindRpc(rpcSid), + "IOP reset did not remove the registered RPC server")) return 1; + + constexpr uint32_t lotrSoundSid = 0x00012345u; + writeRpcServerIrx(host, 0x100u, lotrSoundSid); + const ModuleLoadResult physicalSoundModule = iop.loadModuleBuffer(0x100u); + if (!expect(physicalSoundModule.handled && physicalSoundModule.startResult == 0, + "Synthetic physical sound RPC server did not start")) return 1; + + const uint32_t soundHandler[] = { + 0x3C020001u, // lui v0, 1 + 0x34420400u, // ori v0, v0, 0x400 (registered server buffer) + 0x03E00008u, // jr ra + 0x00000000u, // nop + }; + if (!expect(iop.writeMemory(0x00010300u, soundHandler, sizeof(soundHandler)), + "Could not install the physical sound RPC handler")) return 1; + constexpr uint32_t soundPayload = 0x1234ABCDu; + std::memcpy(host.guest.data() + 0x800u, &soundPayload, sizeof(soundPayload)); + RpcRequest soundRequest{}; + soundRequest.sid = lotrSoundSid; + soundRequest.send = {0x800u, sizeof(soundPayload)}; + soundRequest.receive = {0x900u, sizeof(soundPayload)}; + const uint64_t soundInstructionsBefore = iop.debugSnapshot().emulatorInstructions; + const RpcResult soundResult = iop.handleRpc(soundRequest); + uint32_t soundResponse = 0u; + std::memcpy(&soundResponse, host.guest.data() + 0x900u, sizeof(soundResponse)); + if (!expect(soundResult.handled && soundResponse == soundPayload, + "Physical sound RPC did not return its own payload")) return 1; + if (!expect(iop.debugSnapshot().emulatorInstructions > soundInstructionsBefore, + "Sound RPC did not execute the physical IOP handler")) return 1; + + constexpr uint32_t relocatableRpcSid = 0xA11CE001u; + writeRelocatableRpcServerIrx(host, 0x100u); + const ModuleLoadResult relocatableRpcModule = iop.loadModuleBuffer(0x100u); + if (!expect(relocatableRpcModule.handled && relocatableRpcModule.startResult == 0, + "Relocatable RPC server IRX did not start")) return 1; + if (!expect(iop.canBindRpc(relocatableRpcSid), + "R_MIPS_26 did not relocate a symbol-less IRX import call")) return 1; + + RpcRequest relocatableRequest{}; + relocatableRequest.sid = relocatableRpcSid; + relocatableRequest.receive = {0x800u, sizeof(uint32_t)}; + const uint64_t rpcInstructionsBefore = iop.debugSnapshot().emulatorInstructions; + const RpcResult relocatableRpcResult = iop.handleRpc(relocatableRequest); + const uint64_t rpcInstructions = iop.debugSnapshot().emulatorInstructions - rpcInstructionsBefore; + if (!expect(relocatableRpcResult.handled, + "Relocatable IRX RPC handler was not dispatched")) return 1; + if (!expect(rpcInstructions < 100u, + "R_MIPS_HI16/LO16 did not relocate the IRX RPC handler")) return 1; + uint32_t relocatedGpData = 0u; + std::memcpy(&relocatedGpData, host.guest.data() + relocatableRequest.receive.address, + sizeof(relocatedGpData)); + if (!expect(relocatedGpData == 0x47505250u, + "Physical RPC callback did not inherit the registering module's GP")) return 1; + + iop.reset(); + writeExportProviderIrx(host, 0x100u); + const ModuleLoadResult exportProvider = iop.loadModuleBuffer(0x100u); + if (!expect(exportProvider.handled && exportProvider.startResult == 0, + "Synthetic export provider did not register")) return 1; + writeExportConsumerIrx(host, 0x500u); + const ModuleLoadResult exportConsumer = iop.loadModuleBuffer(0x500u); + if (!expect(exportConsumer.handled && exportConsumer.startResult == 0x42, + "IRX export ordinal was shifted by implicit function slots")) return 1; + + iop.reset(); + constexpr uint32_t eeSource = 0x100u; + constexpr uint32_t iopBuffer = 0x500u; + constexpr uint32_t eeDestination = 0x900u; + const uint32_t transferPayload = 0x53494621u; // "SIF!" + std::memcpy(host.guest.data() + eeSource, &transferPayload, sizeof(transferPayload)); + if (!expect(iop.writeMemory(iopBuffer, &transferPayload, sizeof(transferPayload)), + "Could not initialize physical IOP memory")) return 1; + iop.onSifTransfer({ + SifTransferKind::SetDma, + SifTransferPhase::AfterCopy, + eeSource, + iopBuffer, + sizeof(transferPayload), + }); + std::memset(host.guest.data() + iopBuffer, 0, sizeof(transferPayload)); + iop.onSifTransfer({ + SifTransferKind::GetOtherData, + SifTransferPhase::BeforeCopy, + iopBuffer, + eeDestination, + sizeof(transferPayload), + }); + uint32_t stagedIopPayload = 0u; + std::memcpy(&stagedIopPayload, host.guest.data() + iopBuffer, sizeof(stagedIopPayload)); + if (!expect(stagedIopPayload == 0u, + "SIF notification aliased physical IOP memory into EE RAM")) return 1; + uint32_t physicalIopPayload = 0u; + if (!expect(iop.readMemory(iopBuffer, &physicalIopPayload, sizeof(physicalIopPayload)) && + physicalIopPayload == transferPayload, + "SIF notification corrupted physical IOP memory")) return 1; + + iop.reset(); + constexpr uint32_t sifDmaDestination = 0xC00u; + writeSifDmaIrx(host, 0x100u); + const ModuleLoadResult sifDmaModule = iop.loadModuleBuffer(0x100u); + uint32_t sifDmaPayload = 0u; + std::memcpy(&sifDmaPayload, host.guest.data() + sifDmaDestination, + sizeof(sifDmaPayload)); + if (!expect(sifDmaModule.handled && sifDmaModule.startResult == -1, + "sceSifDmaStat did not report the synchronous transfer as complete")) return 1; + if (!expect(sifDmaPayload == 0x53494621u, + "IOP sceSifSetDma did not copy the payload into EE memory")) return 1; + + iop.reset(); + writeVblankSchedulingIrx(host, 0x100u); + const ModuleLoadResult vblankModule = iop.loadModuleBuffer(0x100u); + if (!expect(vblankModule.handled && vblankModule.startResult == 0, + "Synthetic VBlank scheduling IRX did not start")) return 1; + iop.runEeCycles(8u * 4096u); + iop.onSifTransfer({ + SifTransferKind::GetOtherData, + SifTransferPhase::BeforeCopy, + 0x00010400u, + 0x1000u, + sizeof(uint32_t), + }); + uint32_t lowPriorityMarker = 0u; + if (!expect(iop.readMemory(0x00010400u, &lowPriorityMarker, sizeof(lowPriorityMarker)), + "Could not read the IOP scheduling marker")) return 1; + if (!expect(lowPriorityMarker == 1u, + "WaitVblankEnd returned immediately and starved a lower-priority IOP thread")) return 1; + + iop.reset(); + host.logs.clear(); + writeMcmanRegistrationIrx(host, 0x100u); + const ModuleLoadResult mcmanRegistration = iop.loadModuleBuffer(0x100u); + if (mcmanRegistration.startResult != 2) + { + std::cerr << "MCMAN synthetic start result: " << mcmanRegistration.startResult << '\n'; + for (const auto &message : host.logs) + std::cerr << message << '\n'; + } + if (!expect(mcmanRegistration.handled && mcmanRegistration.startResult == 2, + "MCMAN callback registration or IOMAN AddDrv/DelDrv lifecycle failed")) return 1; + const bool emittedUnhandledImport = std::any_of( + host.logs.begin(), host.logs.end(), + [](const std::string &message) { return message.find("unhandled import") != std::string::npos; }); + if (!expect(!emittedUnhandledImport, + "MCMAN registration still emitted an unhandled IOP import")) return 1; + + iop.reset(); + host.logs.clear(); + writeCdvdLifecycleIrx(host, 0x100u); + const ModuleLoadResult cdvdLifecycle = iop.loadModuleBuffer(0x100u); + if (!expect(cdvdLifecycle.handled && cdvdLifecycle.startResult == 0, + "CDVD init or callback replacement semantics failed")) return 1; + const bool emittedUnhandledCdvdImport = std::any_of( + host.logs.begin(), host.logs.end(), + [](const std::string &message) { return message.find("unhandled import cdvdman") != std::string::npos; }); + if (!expect(!emittedUnhandledCdvdImport, + "CDVD lifecycle still emitted an unhandled IOP import")) return 1; + + iop.reset(); + writeCdvdLifecycleIrx(host, 0x100u); + const ModuleLoadResult cdvdAfterReset = iop.loadModuleBuffer(0x100u); + if (!expect(cdvdAfterReset.handled && cdvdAfterReset.startResult == 0, + "IOP reset did not clear the CDVD callback")) return 1; + + iop.reset(); + const auto uniqueSuffix = std::chrono::steady_clock::now().time_since_epoch().count(); + const std::filesystem::path virtualCdRoot = + std::filesystem::temp_directory_path() / + ("ps2x-iop-cdvd-" + std::to_string(uniqueSuffix)); + std::error_code cdRootError; + std::filesystem::create_directory(virtualCdRoot, cdRootError); + if (!expect(!cdRootError, "Could not create the virtual-CD test directory")) return 1; + host.cdRoot = virtualCdRoot.string(); + writeCdvdPvdReadIrx(host, 0x100u); + const ModuleLoadResult cdvdPvdRead = iop.loadModuleBuffer(0x100u); + iop.runEeCycles(4096u); + iop.onSifTransfer({ + SifTransferKind::GetOtherData, + SifTransferPhase::BeforeCopy, + 0x000101C0u, + 0x1000u, + sizeof(uint32_t), + }); + uint32_t cdvdCallbackReason = 0u; + if (!expect(iop.readMemory(0x000101C0u, &cdvdCallbackReason, sizeof(cdvdCallbackReason)), + "Could not read the physical CDVD callback result")) return 1; + host.cdRoot.clear(); + std::filesystem::remove(virtualCdRoot, cdRootError); + if (!expect(cdvdPvdRead.handled && cdvdPvdRead.startResult == 0, + "CDVD did not expose the extracted CD root as an ISO9660 PVD")) return 1; + if (!expect(cdvdCallbackReason == 1u, + "CDVD read completion did not invoke the registered guest callback")) return 1; + + iop.reset(); + host.logs.clear(); + writeCdvdSeekIrx(host, 0x100u); + const ModuleLoadResult cdvdSeek = iop.loadModuleBuffer(0x100u); + iop.runEeCycles(4096u); + iop.onSifTransfer({ + SifTransferKind::GetOtherData, + SifTransferPhase::BeforeCopy, + 0x000101C0u, + 0x1100u, + sizeof(uint32_t), + }); + uint32_t cdvdSeekCallbackReason = 0u; + if (!expect(iop.readMemory(0x000101C0u, &cdvdSeekCallbackReason, sizeof(cdvdSeekCallbackReason)), + "Could not read the physical CDVD callback result")) return 1; + if (!expect(cdvdSeek.handled && cdvdSeek.startResult == 1, + "sceCdSeek did not accept a valid LBN")) return 1; + if (!expect(cdvdSeekCallbackReason == 4u, + "sceCdSeek completion did not invoke the registered callback")) return 1; + const bool emittedUnhandledSeek = std::any_of( + host.logs.begin(), host.logs.end(), + [](const std::string &message) + { return message.find("unhandled import cdvdman:7") != std::string::npos; }); + if (!expect(!emittedUnhandledSeek, + "sceCdSeek still emitted an unhandled IOP import")) return 1; + + std::cout << "ps2xIOP emulator smoke tests passed\n"; + return 0; +} diff --git a/ps2xIOP/tests/iop_import_tests.cpp b/ps2xIOP/tests/iop_import_tests.cpp new file mode 100644 index 0000000..3035618 --- /dev/null +++ b/ps2xIOP/tests/iop_import_tests.cpp @@ -0,0 +1,338 @@ +#include "emulator/core/iop_cpu.h" +#include "emulator/core/iop_kernel.h" +#include "emulator/core/iop_memory.h" +#include "emulator/imports/iop_cdvd.h" +#include "emulator/imports/iop_imports.h" +#include "emulator/imports/iop_loadcore.h" +#include "emulator/imports/iop_timrman.h" +#include "emulator/services/iop_rpc.h" +#include "ps2x/iop/iop_host.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + using namespace ps2x::iop; + using namespace ps2x::iop::detail; + + constexpr uint32_t kExportMagic = 0x41C00000u; + constexpr int32_t kLibraryNotFound = -213; + constexpr int32_t kIllegalLibrary = -214; + + class NullHost : public IopHost + { + public: + bool readGuest(uint32_t, void *, size_t) const override { return false; } + bool writeGuest(uint32_t, const void *, size_t) override { return false; } + bool zeroGuest(uint32_t, size_t) override { return false; } + bool normalizeGuestAddress(uint32_t, uint32_t &) const override { return false; } + uint32_t allocateIopHandle(IopHandleKind) override { return 1u; } + uint32_t allocateGuest(uint32_t, uint32_t) override { return 0u; } + void freeGuest(uint32_t) override {} + void audioCommand(uint32_t, uint32_t, GuestBuffer, GuestBuffer) override {} + std::string hostPath(HostPathKind) const override { return {}; } + std::string translateGuestPath(std::string_view path) const override { return std::string(path); } + uint64_t openHostFile(std::string_view) override { return 0u; } + bool hostFileSize(uint64_t, uint64_t &) const override { return false; } + bool readHostFile(uint64_t, uint64_t, void *, size_t, size_t &) override { return false; } + void closeHostFile(uint64_t) override {} + int32_t memoryCard(const MemoryCardRequest &) override { return 0; } + bool hasGuestFunction(uint32_t) const override { return false; } + bool invokeGuestFunction(uint64_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t *) override { return false; } + void log(LogLevel, std::string_view) override {} + }; + + class CdRootHost final : public NullHost + { + public: + explicit CdRootHost(std::filesystem::path rootPath) + : root(std::move(rootPath)) + { + } + + std::string hostPath(HostPathKind kind) const override + { + return kind == HostPathKind::CdRoot ? root.string() : std::string{}; + } + + private: + std::filesystem::path root; + }; + + class RecordingExecutor final : public IopGuestExecutor + { + public: + uint32_t executeGuestFunction(uint32_t address, + uint32_t a0, + uint32_t, + uint32_t, + uint32_t, + uint32_t gp) override + { + ++calls; + lastAddress = address; + lastArgument = a0; + lastGp = gp; + return callbackResult; + } + + uint32_t callbackResult = 0u; + uint32_t calls = 0u; + uint32_t lastAddress = 0u; + uint32_t lastArgument = 0u; + uint32_t lastGp = 0u; + }; + + bool expect(bool condition, std::string_view message) + { + if (condition) + return true; + std::cerr << "FAIL: " << message << '\n'; + return false; + } + + bool testLoadcoreRebootLibraryMode() + { + IopMemory memory; + IopImportRegistry imports(memory); + IopLoadcore loadcore(memory, imports); + + IopCpuState cpu{}; + cpu.gpr[4] = 0u; + cpu.gpr[5] = 2u; + if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 was not handled") || + !expect(static_cast(cpu.gpr[2]) == kIllegalLibrary, + "loadcore:27 did not reject a null export table")) + return false; + + constexpr uint32_t table = 0x1000u; + memory.write32(table, kExportMagic); + memory.write16(table + 8u, 0x0101u); + memory.write16(table + 10u, 0x1234u); + const char name[8] = {'t', 'e', 's', 't', 'l', 'i', 'b', '\0'}; + (void)memory.writeRam(table + 12u, name, sizeof(name)); + memory.write32(table + 20u, 0u); + + cpu = {}; + cpu.gpr[4] = table; + cpu.gpr[5] = 2u; + if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 rejected a valid export table") || + !expect(cpu.gpr[2] == 0u, "loadcore:27 returned an error for a valid export table") || + !expect(memory.read16(table + 10u) == 0x1232u, + "loadcore:27 did not replace only export mode bits 1 and 2")) + return false; + + constexpr uint32_t invalidTable = 0x1100u; + memory.write32(invalidTable, 0xDEADBEEFu); + cpu = {}; + cpu.gpr[4] = invalidTable; + cpu.gpr[5] = 6u; + if (!expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 did not consume an invalid-table call") || + !expect(static_cast(cpu.gpr[2]) == kLibraryNotFound, + "loadcore:27 returned the wrong invalid-table error")) + return false; + + if (!expect(imports.registerExportTable(table), "test export table did not register")) + return false; + memory.write32(table, 0u); + cpu = {}; + cpu.gpr[4] = table; + cpu.gpr[5] = 6u; + return expect(loadcore.dispatchImport(27u, cpu), "loadcore:27 rejected a registered table") && + expect(cpu.gpr[2] == 0u, "loadcore:27 returned an error for a registered table") && + expect(memory.read16(table + 10u) == 0x1236u, + "loadcore:27 did not update a registered table's mode"); + } + + bool pollEvent(IopKernel &kernel, int eventId, uint32_t bits, uint32_t resultAddress, int32_t expected) + { + IopCpuState cpu{}; + cpu.gpr[4] = static_cast(eventId); + cpu.gpr[5] = bits; + cpu.gpr[6] = 0u; // WEF_AND + cpu.gpr[7] = resultAddress; + return expect(kernel.dispatchEventImport(11u, cpu), "PollEventFlag was not handled") && + expect(static_cast(cpu.gpr[2]) == expected, "PollEventFlag returned an unexpected result"); + } + + bool testCdvdSpecialControl() + { + NullHost host; + IopMemory memory; + IopKernel kernel(memory); + kernel.reset(); + IopCdvd cdvd(host, memory, kernel); + cdvd.reset(); + + constexpr uint32_t param = 0x2000u; + constexpr uint32_t eventResult = 0x2010u; + IopCpuState cpu{}; + cpu.gpr[4] = static_cast(-11); // sceCdSC: return cdvdman interrupt event flag + cpu.gpr[5] = param; + if (!expect(cdvd.dispatchImport(50u, cpu), "cdvdman:50 was not handled") || + !expect(static_cast(cpu.gpr[2]) > 0, "sceCdSC(-11) did not return a valid event flag")) + return false; + const int eventId = static_cast(cpu.gpr[2]); + + if (!pollEvent(kernel, eventId, 0x29u, eventResult, 0) || + !expect(memory.read32(eventResult) == 0x29u, "cdvdman event flag did not start with bits 0x29")) + return false; + + IopCpuState clear{}; + clear.gpr[4] = static_cast(eventId); + clear.gpr[5] = ~0x29u; + if (!expect(kernel.dispatchEventImport(8u, clear), "ClearEventFlag was not handled") || + !pollEvent(kernel, eventId, 0x29u, eventResult, -418)) + return false; + + cpu = {}; + cpu.gpr[4] = 0x12345u; + if (!expect(cdvd.dispatchImport(7u, cpu), "sceCdSeek was not handled") || + !pollEvent(kernel, eventId, 0x29u, eventResult, 0)) + return false; + + memory.write8(param, 0x30u); + cpu = {}; + cpu.gpr[4] = static_cast(-2); + cpu.gpr[5] = param; + if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(-2) was not handled") || + !expect(cpu.gpr[2] == 0x30u, "sceCdSC(-2) did not store the low-byte error")) + return false; + + memory.write32(param, 0u); + cpu = {}; + cpu.gpr[4] = static_cast(-1); + cpu.gpr[5] = param; + if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(-1) was not handled") || + !expect(cpu.gpr[2] == 0u, "sceCdSC(-1) returned the wrong initial stream state") || + !expect(memory.read32(param) == 0x30u, "sceCdSC(-1) did not publish the last error")) + return false; + + cpu = {}; + cpu.gpr[4] = 2u; + cpu.gpr[5] = param; + if (!expect(cdvd.dispatchImport(50u, cpu), "sceCdSC(2) was not handled") || + !expect(cpu.gpr[2] == 2u, "sceCdSC(2) did not update the stream state")) + return false; + + cpu = {}; + cpu.gpr[4] = static_cast(-1); + cpu.gpr[5] = param; + return expect(cdvd.dispatchImport(50u, cpu), "second sceCdSC(-1) was not handled") && + expect(cpu.gpr[2] == 2u, "sceCdSC(-1) did not preserve the stream state"); + } + + bool testCdvdSearchFile() + { + const auto suffix = std::to_string( + static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); + const std::filesystem::path root = + std::filesystem::temp_directory_path() / ("ps2x-iop-cdvd-search-" + suffix); + const std::filesystem::path movieDirectory = root / "MOVIE"; + const std::filesystem::path moviePath = movieDirectory / "OPENING.PSS"; + std::error_code error; + std::filesystem::create_directories(movieDirectory, error); + if (!expect(!error, "could not create the temporary CD root")) + return false; + { + std::ofstream movie(moviePath, std::ios::binary); + movie.write("PSS!", 4); + } + + CdRootHost host(root); + IopMemory memory; + IopKernel kernel(memory); + kernel.reset(); + IopCdvd cdvd(host, memory, kernel); + cdvd.reset(); + + constexpr uint32_t resultAddress = 0x2400u; + constexpr uint32_t pathAddress = 0x2480u; + const char path[] = "cdrom0:\\movie\\opening.pss;1"; + (void)memory.writeRam(pathAddress, path, sizeof(path)); + + IopCpuState cpu{}; + cpu.gpr[4] = resultAddress; + cpu.gpr[5] = pathAddress; + const bool handled = cdvd.dispatchImport(10u, cpu); + const bool passed = + expect(handled, "cdvdman:10 was not handled") && + expect(cpu.gpr[2] == 1u, "sceCdSearchFile did not find a case-insensitive ISO path") && + expect(memory.read32(resultAddress) >= 20u, "sceCdSearchFile returned an invalid LSN") && + expect(memory.read32(resultAddress + 4u) == 4u, "sceCdSearchFile returned the wrong size") && + expect(memory.readString(resultAddress + 8u, 16u) == "OPENING.PSS", + "sceCdSearchFile returned the wrong file name"); + + std::filesystem::remove_all(root, error); + return passed; + } + + bool testTimrmanPeriodicCallback() + { + IopTimrman timrman; + timrman.reset(); + IopCpuState cpu{}; + + cpu.gpr[4] = 1u; // SYSCLK + cpu.gpr[5] = 32u; + cpu.gpr[6] = 1u; + if (!expect(timrman.dispatchImport(4u, cpu, 100u), "AllocHardTimer was not handled") || + !expect(static_cast(cpu.gpr[2]) > 0, "AllocHardTimer did not allocate a 32-bit timer")) + return false; + const uint32_t timerId = cpu.gpr[2]; + + cpu = {}; + cpu.gpr[4] = timerId; + cpu.gpr[5] = 100u; + cpu.gpr[6] = 0x12340u; + cpu.gpr[7] = 0x45670u; + cpu.gpr[28] = 0x89AB0u; + if (!expect(timrman.dispatchImport(20u, cpu, 100u), "SetTimerHandler was not handled") || + !expect(cpu.gpr[2] == 0u, "SetTimerHandler failed")) + return false; + + cpu = {}; + cpu.gpr[4] = timerId; + cpu.gpr[5] = 1u; + cpu.gpr[6] = 0u; + cpu.gpr[7] = 1u; + if (!expect(timrman.dispatchImport(22u, cpu, 100u), "SetupHardTimer was not handled") || + !expect(cpu.gpr[2] == 0u, "SetupHardTimer failed")) + return false; + + cpu = {}; + cpu.gpr[4] = timerId; + if (!expect(timrman.dispatchImport(23u, cpu, 100u), "StartHardTimer was not handled") || + !expect(cpu.gpr[2] == 0u, "StartHardTimer failed") || + !expect(timrman.nextEventCycle(1000u) == 200u, "timer compare was scheduled at the wrong cycle")) + return false; + + RecordingExecutor executor; + executor.callbackResult = 100u; + timrman.serviceDue(199u, executor); + if (!expect(executor.calls == 0u, "timer callback ran too early")) + return false; + timrman.serviceDue(200u, executor); + return expect(executor.calls == 1u, "timer callback did not run") && + expect(executor.lastAddress == 0x12340u, "timer called the wrong handler") && + expect(executor.lastArgument == 0x45670u, "timer passed the wrong common argument") && + expect(executor.lastGp == 0x89AB0u, "timer callback lost the registering module GP") && + expect(timrman.nextEventCycle(1000u) == 300u, "timer callback return did not rearm compare"); + } +} + +int main() +{ + if (!testLoadcoreRebootLibraryMode() || !testCdvdSpecialControl() || !testCdvdSearchFile() || + !testTimrmanPeriodicCallback()) + return 1; + std::cout << "ps2xIOP import tests passed\n"; + return 0; +} diff --git a/ps2xIOP/tests/iop_import_version_tests.cpp b/ps2xIOP/tests/iop_import_version_tests.cpp new file mode 100644 index 0000000..af3346b --- /dev/null +++ b/ps2xIOP/tests/iop_import_version_tests.cpp @@ -0,0 +1,170 @@ +#include "iop_compat_test_support.h" + +#include "emulator/core/iop_cpu.h" +#include "emulator/core/iop_memory.h" +#include "emulator/imports/iop_imports.h" +#include "emulator/imports/iop_loadcore.h" + +namespace +{ + using namespace iop_test; + using namespace ps2x::iop::detail; + + void addExport(IopMemory &memory, IopImportRegistry &imports, uint32_t address, + uint16_t version, uint32_t target, uint32_t count = 4u) + { + require(memory.zeroRam(address, 128u), "export table does not fit"); + memory.write32(address, 0x41C00000u); + memory.write16(address + 8u, version); + constexpr char name[8] = "tstlib"; + require(memory.writeRam(address + 12u, name, sizeof(name)), "export name does not fit"); + for (uint32_t i = 0u; i < count; ++i) + memory.write32(address + 20u + 4u * i, target); + require(imports.registerExportTable(address), "export registration failed"); + } + + void importTable(IopMemory &memory, uint32_t address, uint16_t version) + { + require(memory.zeroRam(address, 64u), "import table does not fit"); + memory.write32(address, 0x41E00000u); + memory.write16(address + 8u, version); + constexpr char name[8] = "tstlib"; + require(memory.writeRam(address + 12u, name, sizeof(name)), "import name does not fit"); + memory.write32(address + 20u, 0x03E00008u); + memory.write32(address + 24u, 0x24000003u); + } + + void decodeVersion() + { + IopMemory memory; + IopImportRegistry imports(memory); + importTable(memory, 0x1000u, 0x0310u); + const auto call = imports.decode(0x1014u); + require(call && call->library == "tstlib" && call->ordinal == 3u && call->version == 0x0310u, + "decoder dropped the import library version"); + const auto alias = imports.decode(0x80001014u); + require(alias && alias->version == 0x0310u, "cached alias lost import version"); + } + + void majorIsolation() + { + IopMemory memory; + IopImportRegistry imports(memory); + addExport(memory, imports, 0x1000u, 0x0201u, 0x2100u); + addExport(memory, imports, 0x1800u, 0x0101u, 0x3100u); + require(imports.resolve("tstlib", 3u, 0x0101u) == 0x3100u, "linked to wrong library major"); + require(imports.resolve("tstlib", 3u, 0x0201u) == 0x2100u, "second major unavailable"); + require(imports.resolve("tstlib", 3u, 0x0300u) == 0u, "incompatible major silently linked"); + require(imports.findTable("tstlib", 0x0300u) == 0u, "query ignored requested major"); + } + + void newestMinor() + { + IopMemory memory; + IopImportRegistry imports(memory); + addExport(memory, imports, 0x1000u, 0x0101u, 0x2100u); + addExport(memory, imports, 0x1800u, 0x0104u, 0x3100u); + addExport(memory, imports, 0x1400u, 0x0103u, 0x4100u); + require(imports.resolve("tstlib", 3u, 0x0101u) == 0x3100u, "selected lowest address, not newest minor"); + require(imports.resolve("tstlib", 3u, 0x017Fu) == 0x3100u, + "invented a minimum-minor rule absent from LOADCORE linking"); + require(imports.releaseExportTable(0x1800u), "unregister failed"); + require(imports.resolve("tstlib", 3u, 0x0101u) == 0x4100u, "unregistered library remained selected"); + } + + void missingOrdinal() + { + IopMemory memory; + IopImportRegistry imports(memory); + addExport(memory, imports, 0x1000u, 0x0101u, 0x2100u, 8u); + addExport(memory, imports, 0x1800u, 0x0102u, 0x3100u, 4u); + require(imports.resolve("tstlib", 7u, 0x0101u) == 0u, + "missing ordinal fell back to a different export table"); + require(imports.resolve("missing", 0u, 0x0101u) == 0u, "missing library resolved"); + imports.reset(); + require(imports.resolve("tstlib", 0u, 0x0101u) == 0u, "registry reset left exports"); + } + + void queryFunctionArray() + { + IopMemory memory; + IopImportRegistry imports(memory); + IopLoadcore loadcore(memory, imports); + addExport(memory, imports, 0x1000u, 0x0201u, 0x2100u); + addExport(memory, imports, 0x1800u, 0x0101u, 0x3100u); + importTable(memory, 0x800u, 0x0102u); + IopCpuState cpu{}; + cpu.gpr[4] = 0x800u; + require(loadcore.dispatchImport(11u, cpu), "QueryLibraryEntryTable unhandled"); + require(cpu.gpr[2] == 0x1814u && memory.read32(cpu.gpr[2]) == 0x3100u, + "query returned an export header instead of function array"); + memory.write16(0x808u, 0x0300u); + require(loadcore.dispatchImport(11u, cpu) && cpu.gpr[2] == 0u, "query accepted wrong major"); + for (uint32_t address : {0u, 0xFFFFFFF8u, IopMemory::RamSize - 4u}) + { + cpu.gpr[4] = address; + require(loadcore.dispatchImport(11u, cpu) && cpu.gpr[2] == 0u, "invalid query pointer accepted"); + } + } + + Irx provider(uint32_t base, uint16_t version, uint32_t result) + { + Irx image(base); + const uint32_t table = base + 0x80u; + const uint32_t importStub = base + 0xC0u + 20u; + image.words(0u, {0x27BDFFE0u, 0xAFBF001Cu, + 0x3C040000u | (table >> 16u), 0x34840000u | (table & 0xFFFFu), + 0x0C000000u | (importStub >> 2u), 0u, + 0x8FBF001Cu, 0x00001021u, 0x27BD0020u, 0x03E00008u, 0u}); + image.words(0x60u, {0x03E00008u, 0x24020000u | result}); + image.words(0x80u, {0x41C00000u, 0u, version, 0x6C747374u, 0x00006269u, + base, base, base, base + 0x60u, 0u}); + image.words(0xC0u, {0x41E00000u, 0u, 0x0101u, 0x64616F6Cu, 0x65726F63u, + 0x03E00008u, 0x24000006u, 0u, 0u}); + return image; + } + + Irx consumer(uint16_t version) + { + constexpr uint32_t base = 0x13000u; + Irx image(base); + image.words(0u, {0x27BDFFF0u, 0xAFBF000Cu, + 0x0C000000u | ((base + 0x54u) >> 2u), 0u, + 0x8FBF000Cu, 0x27BD0010u, 0x03E00008u, 0u}); + image.words(0x40u, {0x41E00000u, 0u, version, 0x6C747374u, 0x00006269u, + 0x03E00008u, 0x24000003u, 0u, 0u}); + return image; + } + + void physicalImportsEndToEnd() + { + Host host; + IopSubsystem iop(host); + auto wrongMajor = provider(0x10000u, 0x0201u, 0x22u); + wrongMajor.install(host); + require(iop.loadModuleBuffer(0x1000u).startResult == 0, "provider 2 failed"); + auto oldMinor = provider(0x11000u, 0x0101u, 0x11u); + oldMinor.install(host); + require(iop.loadModuleBuffer(0x1000u).startResult == 0, "provider 1 failed"); + auto newMinor = provider(0x12000u, 0x0103u, 0x13u); + newMinor.install(host); + require(iop.loadModuleBuffer(0x1000u).startResult == 0, "provider 1.3 failed"); + auto client = consumer(0x0101u); + client.install(host); + const auto result = iop.loadModuleBuffer(0x1000u); + require(result.moduleId > 0 && result.startResult == 0x13, "R3000A called wrong export version"); + } +} + +int main() +{ + const Test tests[] = { + {"Import decoder preserves library ABI version", decodeVersion}, + {"Different major versions cannot cross-link", majorIsolation}, + {"Newest registered minor wins within the requested major", newestMinor}, + {"Ordinal lookup stays in the selected table", missingOrdinal}, + {"LOADCORE query returns function array and honors major", queryFunctionArray}, + {"Physical IRX consumer links correct version end to end", physicalImportsEndToEnd}, + }; + return run(tests); +} diff --git a/ps2xRecomp/include/ps2recomp/instructions.h b/ps2xRecomp/include/ps2recomp/instructions.h index bf138a6..1980a6f 100644 --- a/ps2xRecomp/include/ps2recomp/instructions.h +++ b/ps2xRecomp/include/ps2recomp/instructions.h @@ -5,6 +5,48 @@ namespace ps2recomp { + inline constexpr uint32_t MIPS_INSTRUCTION_SIZE = sizeof(uint32_t); + inline constexpr uint16_t MIPS_IMMEDIATE_SIGN_BIT = 0x8000u; + inline constexpr uint32_t MIPS_JUMP_TARGET_SHIFT = 2u; + inline constexpr uint32_t MIPS_JUMP_REGION_MASK = 0xF0000000u; + + // R5900 general-purpose register indices used by the encoded RS/RT/RD fields. + enum GprRegisters : uint32_t + { + GPR_ZERO = 0, + GPR_AT = 1, + GPR_V0 = 2, + GPR_V1 = 3, + GPR_A0 = 4, + GPR_A1 = 5, + GPR_A2 = 6, + GPR_A3 = 7, + GPR_T0 = 8, + GPR_T1 = 9, + GPR_T2 = 10, + GPR_T3 = 11, + GPR_T4 = 12, + GPR_T5 = 13, + GPR_T6 = 14, + GPR_T7 = 15, + GPR_S0 = 16, + GPR_S1 = 17, + GPR_S2 = 18, + GPR_S3 = 19, + GPR_S4 = 20, + GPR_S5 = 21, + GPR_S6 = 22, + GPR_S7 = 23, + GPR_T8 = 24, + GPR_T9 = 25, + GPR_K0 = 26, + GPR_K1 = 27, + GPR_GP = 28, + GPR_SP = 29, + GPR_FP = 30, + GPR_RA = 31, + }; + // Basic MIPS opcodes (shared with R4300i) enum MipsOpcodes { diff --git a/ps2xRecomp/include/ps2recomp/ps2_recompiler.h b/ps2xRecomp/include/ps2recomp/ps2_recompiler.h index ec21986..dd62275 100644 --- a/ps2xRecomp/include/ps2recomp/ps2_recompiler.h +++ b/ps2xRecomp/include/ps2recomp/ps2_recompiler.h @@ -42,9 +42,12 @@ namespace ps2recomp std::vector &functions, std::unordered_map> &decodedFunctions, const std::vector
§ions); - static size_t ResliceEntryFunctions( - std::vector &functions, - std::unordered_map> &decodedFunctions); + static size_t ResliceEntryFunctions(std::vector &functions, std::unordered_map> &decodedFunctions); + static size_t CollectInternalEntryTargets( + const std::vector &functions, + const std::unordered_map> &decodedFunctions, + const std::unordered_set &entryAddresses, + std::unordered_map> &targetsByOwner); static std::string ClampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength); @@ -67,6 +70,7 @@ namespace ps2recomp std::unordered_set m_stubFunctions; std::unordered_set m_stubFunctionStarts; std::unordered_map m_stubHandlerBindingsByStart; + std::unordered_set m_entryPointHintStarts; std::unordered_set m_correctnessCriticalFunctionStarts; std::map m_generatedStubs; std::unordered_map m_functionRenames; diff --git a/ps2xRecomp/include/ps2recomp/types.h b/ps2xRecomp/include/ps2recomp/types.h index 39adf9a..9bd687d 100644 --- a/ps2xRecomp/include/ps2recomp/types.h +++ b/ps2xRecomp/include/ps2recomp/types.h @@ -187,6 +187,7 @@ namespace ps2recomp std::vector skipFunctions; std::unordered_map patches; std::vector stubImplementations; + std::vector entryPointHints; std::unordered_map mmioByInstructionAddress; std::vector jumpTables; }; diff --git a/ps2xRecomp/src/lib/config_manager.cpp b/ps2xRecomp/src/lib/config_manager.cpp index b6f382d..97dc2dd 100644 --- a/ps2xRecomp/src/lib/config_manager.cpp +++ b/ps2xRecomp/src/lib/config_manager.cpp @@ -74,6 +74,27 @@ namespace ps2recomp config.stubImplementations = toml::find>(data, "stubs"); } + auto appendEntryPointHints = [&](const toml::value &table, const char *key) + { + if (!table.contains(key) || !table.at(key).is_array()) + { + return; + } + const auto values = toml::find>(table, key); + config.entryPointHints.insert( + config.entryPointHints.end(), values.begin(), values.end()); + }; + appendEntryPointHints(general, "entry_points"); + appendEntryPointHints(data, "entry_points"); + // Backward compatibility + appendEntryPointHints(general, "untracked_stubs"); + appendEntryPointHints(data, "untracked_stubs"); + + std::sort(config.entryPointHints.begin(), config.entryPointHints.end()); + config.entryPointHints.erase( + std::unique(config.entryPointHints.begin(), config.entryPointHints.end()), + config.entryPointHints.end()); + if (general.contains("skip") && general.at("skip").is_array()) { config.skipFunctions = toml::find>(general, "skip"); @@ -276,6 +297,7 @@ namespace ps2recomp general["patch_cache"] = config.patchCache; general["skip"] = config.skipFunctions; general["stubs"] = config.stubImplementations; + general["entry_points"] = config.entryPointHints; data["general"] = general; if (!config.mmioByInstructionAddress.empty()) diff --git a/ps2xRecomp/src/lib/elf_parser.cpp b/ps2xRecomp/src/lib/elf_parser.cpp index 91e43ee..9d3b9f0 100644 --- a/ps2xRecomp/src/lib/elf_parser.cpp +++ b/ps2xRecomp/src/lib/elf_parser.cpp @@ -1,4 +1,5 @@ #include "ps2recomp/elf_parser.h" +#include "ps2recomp/instructions.h" #include "ps2recomp/recompiler_reporter.h" #include "ps2recomp/types.h" #include @@ -116,6 +117,8 @@ namespace namespace { + using namespace ps2recomp; + bool HasDwarfSections(const ELFIO::elfio &elf) { for (ELFIO::Elf_Half i = 0; i < elf.sections.size(); ++i) @@ -453,7 +456,1156 @@ namespace } } - void ScanJalTargetsFallback(ps2recomp::ElfParser *parser, std::vector &outFunctions) + bool ReadSectionWord(const ps2recomp::Section §ion, uint32_t offset, uint32_t &outWord) + { + if (!section.data || offset > section.size || section.size - offset < sizeof(uint32_t)) + { + return false; + } + + std::memcpy(&outWord, section.data + offset, sizeof(uint32_t)); + return true; + } + + bool ReadWordByAddress(const std::vector §ions, + uint32_t address, + uint32_t &outWord) + { + // Prefer real ELF sections over the synthetic LOAD-section fallback. + // Both can cover the same address, but named sections have exact file + // bounds and avoid treating a segment's zero-filled alignment as data. + for (int pass = 0; pass < 2; ++pass) + { + for (const auto §ion : sections) + { + const bool isSyntheticLoad = section.name.rfind("LOAD", 0) == 0; + if ((pass == 0 && isSyntheticLoad) || + (pass == 1 && !isSyntheticLoad) || + section.isBSS || !section.data || address < section.address) + { + continue; + } + + const uint32_t offset = address - section.address; + if (ReadSectionWord(section, offset, outWord)) + { + return true; + } + } + } + return false; + } + + bool HasReachableReturnByControlFlow(const ps2recomp::Section §ion, + uint32_t startOffset); + + bool LooksLikeCallableEntry(const std::vector §ions, + uint32_t address, + bool allowLeafThunk, + bool allowLongLeaf = false, + bool restrictTailThunk = false) + { + if ((address % MIPS_INSTRUCTION_SIZE) != 0) + { + return false; + } + + const ps2recomp::Section *section = FindCodeSectionByAddress(sections, address); + if (!section || !section->data) + { + return false; + } + + const uint32_t startOffset = address - section->address; + constexpr uint32_t kStandardProbeWords = 8; + // Retail initializer tables sometimes point at functions that materialize + // a sizeable block of constants before allocating their stack frame. + // Keep the broader window exclusive to that LUI-preamble pattern so leaf + // returns and saved-RA sequences retain the tighter historical window. + constexpr uint32_t kDelayedPrologueProbeWords = 12; + const uint32_t probeWords = std::max(kDelayedPrologueProbeWords, kStandardProbeWords); + bool hasOnlyConstantPreamble = true; + + for (uint32_t index = 0; index < probeWords; ++index) + { + uint32_t raw = 0; + if (!ReadSectionWord(*section, startOffset + (index * MIPS_INSTRUCTION_SIZE), raw)) + { + break; + } + + const uint32_t opcode = OPCODE(raw); + const uint32_t rs = RS(raw); + const uint32_t rt = RT(raw); + const uint16_t immediate = static_cast(IMMEDIATE(raw)); + + // Non-leaf functions normally allocate their stack frame immediately. + // Static initializers may first issue a block of LUI instructions for + // callback and descriptor addresses, as long as no unrelated operation + // appears before the delayed prologue. + if ((opcode == OPCODE_ADDIU || opcode == OPCODE_DADDIU) && + rs == GPR_SP && rt == GPR_SP && + (immediate & MIPS_IMMEDIATE_SIGN_BIT) != 0 && + index < kDelayedPrologueProbeWords && + (index < 4 || hasOnlyConstantPreamble)) + { + return true; + } + + // Some prologues set up GP before saving RA, so also recognize the + // common SW/SD/SQ $ra,offset($sp) forms in the entry window. + if (index < kStandardProbeWords && + (opcode == OPCODE_SW || opcode == OPCODE_SD || opcode == OPCODE_SQ) && + rs == GPR_SP && rt == GPR_RA) + { + return true; + } + + // Leaf callbacks and vtable thunks often have no stack frame at all. + // Besides the usual `jr $ra`, constructor/destructor wrappers commonly + // prepare arguments and tail-call their implementation with a direct + // `j`. These addresses are only accepted by callers that already have + // strong address-taken evidence (for example a clustered pointer table), + // so recognizing the tail jump does not turn arbitrary code labels into + // function starts. + const bool isLeafReturn = + index < kStandardProbeWords && + opcode == OPCODE_SPECIAL && FUNCTION(raw) == SPECIAL_JR && rs == GPR_RA; + const uint32_t tailThunkProbeWords = restrictTailThunk ? 4u : kStandardProbeWords; + const bool isShortTailThunk = + index < tailThunkProbeWords && opcode == OPCODE_J; + if (allowLeafThunk && (isLeafReturn || isShortTailThunk)) + { + return true; + } + + if (opcode != OPCODE_LUI) + { + hasOnlyConstantPreamble = false; + } + } + + // A callback supported by strong address-taken evidence does not need a + // prologue and is not required to return within an arbitrary linear + // instruction window. Follow its reachable basic blocks instead. The + // traversal itself is guarded against malformed code, while branches and + // loops do not consume a caller-visible "function length" allowance. + if (allowLeafThunk && allowLongLeaf && + HasReachableReturnByControlFlow(*section, startOffset)) + { + return true; + } + + return false; + } + + bool WritesGpr(uint32_t raw, uint32_t reg) + { + if (reg == GPR_ZERO) + { + return false; + } + + const uint32_t opcode = OPCODE(raw); + const uint32_t rt = RT(raw); + const uint32_t rd = RD(raw); + + if (opcode == OPCODE_SPECIAL || opcode == OPCODE_MMI) + { + return rd == reg; + } + + if (opcode == OPCODE_JAL) + { + return reg == GPR_RA; + } + + bool writesRt = false; + switch (opcode) + { + case OPCODE_ADDI: + case OPCODE_ADDIU: + case OPCODE_SLTI: + case OPCODE_SLTIU: + case OPCODE_ANDI: + case OPCODE_ORI: + case OPCODE_XORI: + case OPCODE_LUI: + case OPCODE_DADDI: + case OPCODE_DADDIU: + case OPCODE_LDL: + case OPCODE_LDR: + case OPCODE_LQ: + case OPCODE_LB: + case OPCODE_LH: + case OPCODE_LWL: + case OPCODE_LW: + case OPCODE_LBU: + case OPCODE_LHU: + case OPCODE_LWR: + case OPCODE_LWU: + case OPCODE_LL: + case OPCODE_LLD: + case OPCODE_LD: + writesRt = true; + break; + default: + break; + } + + return writesRt && rt == reg; + } + + bool IsControlTransfer(uint32_t raw) + { + const uint32_t opcode = OPCODE(raw); + switch (opcode) + { + case OPCODE_REGIMM: + case OPCODE_J: + case OPCODE_JAL: + case OPCODE_BEQ: + case OPCODE_BNE: + case OPCODE_BLEZ: + case OPCODE_BGTZ: + case OPCODE_BEQL: + case OPCODE_BNEL: + case OPCODE_BLEZL: + case OPCODE_BGTZL: + return true; + default: + break; + } + + if (opcode != OPCODE_SPECIAL) + { + return false; + } + + const uint32_t function = FUNCTION(raw); + return function == SPECIAL_JR || function == SPECIAL_JALR; + } + + bool IsCallInstruction(uint32_t raw) + { + const uint32_t opcode = OPCODE(raw); + return opcode == OPCODE_JAL || + (opcode == OPCODE_SPECIAL && FUNCTION(raw) == SPECIAL_JALR); + } + + bool TryGetGprCopy(uint32_t raw, uint32_t &outDestination, uint32_t &outSource) + { + const uint32_t opcode = OPCODE(raw); + const uint32_t rs = RS(raw); + const uint32_t rt = RT(raw); + const uint32_t rd = RD(raw); + + if (opcode == OPCODE_SPECIAL) + { + const uint32_t function = FUNCTION(raw); + const bool zeroExtendedMove = + function == SPECIAL_ADDU || function == SPECIAL_DADDU || function == SPECIAL_OR; + if (zeroExtendedMove && rd != GPR_ZERO) + { + if (rs == GPR_ZERO && rt != GPR_ZERO) + { + outDestination = rd; + outSource = rt; + return true; + } + if (rt == GPR_ZERO && rs != GPR_ZERO) + { + outDestination = rd; + outSource = rs; + return true; + } + } + + if (function == SPECIAL_SLL && SA(raw) == 0 && + rd != GPR_ZERO && rt != GPR_ZERO) + { + outDestination = rd; + outSource = rt; + return true; + } + } + + if ((opcode == OPCODE_ADDIU || opcode == OPCODE_DADDIU || opcode == OPCODE_ORI) && + IMMEDIATE(raw) == 0 && rs != GPR_ZERO && rt != GPR_ZERO) + { + outDestination = rt; + outSource = rs; + return true; + } + + return false; + } + + uint32_t PropagateTrackedGprs(uint32_t raw, uint32_t trackedGprs) + { + uint32_t copyDestination = GPR_ZERO; + uint32_t copySource = GPR_ZERO; + const bool copiesTrackedValue = + TryGetGprCopy(raw, copyDestination, copySource) && + (trackedGprs & (1u << copySource)) != 0; + + for (uint32_t reg = 1; reg < 32; ++reg) + { + if (WritesGpr(raw, reg)) + { + trackedGprs &= ~(1u << reg); + } + } + + if (copiesTrackedValue) + { + trackedGprs |= 1u << copyDestination; + } + + return trackedGprs; + } + + bool IsConditionalBranch(uint32_t raw) + { + switch (OPCODE(raw)) + { + case OPCODE_BEQ: + case OPCODE_BNE: + case OPCODE_BLEZ: + case OPCODE_BGTZ: + case OPCODE_BEQL: + case OPCODE_BNEL: + case OPCODE_BLEZL: + case OPCODE_BGTZL: + return true; + case OPCODE_REGIMM: + switch (RT(raw)) + { + case REGIMM_BLTZ: + case REGIMM_BGEZ: + case REGIMM_BLTZL: + case REGIMM_BGEZL: + case REGIMM_BLTZAL: + case REGIMM_BGEZAL: + case REGIMM_BLTZALL: + case REGIMM_BGEZALL: + return true; + default: + return false; + } + default: + return false; + } + } + + bool IsLikelyBranch(uint32_t raw) + { + switch (OPCODE(raw)) + { + case OPCODE_BEQL: + case OPCODE_BNEL: + case OPCODE_BLEZL: + case OPCODE_BGTZL: + return true; + case OPCODE_REGIMM: + return RT(raw) == REGIMM_BLTZL || RT(raw) == REGIMM_BGEZL || + RT(raw) == REGIMM_BLTZALL || RT(raw) == REGIMM_BGEZALL; + default: + return false; + } + } + + uint32_t PcRelativeBranchTargetOffset(uint32_t instructionOffset, uint32_t raw) + { + return instructionOffset + MIPS_INSTRUCTION_SIZE + + static_cast(SIMMEDIATE(raw) * static_cast(MIPS_INSTRUCTION_SIZE)); + } + + bool TryGetDirectJumpTargetOffset(const ps2recomp::Section §ion, + uint32_t instructionOffset, + uint32_t raw, + uint32_t &outTargetOffset) + { + if (OPCODE(raw) != OPCODE_J && OPCODE(raw) != OPCODE_JAL) + { + return false; + } + + const uint32_t instructionAddress = section.address + instructionOffset; + const uint32_t targetAddress = + ((instructionAddress + MIPS_INSTRUCTION_SIZE) & MIPS_JUMP_REGION_MASK) | + (TARGET(raw) << MIPS_JUMP_TARGET_SHIFT); + if (targetAddress < section.address || targetAddress >= section.address + section.size) + { + return false; + } + + outTargetOffset = targetAddress - section.address; + return true; + } + + bool HasReachableReturnByControlFlow(const ps2recomp::Section §ion, uint32_t startOffset) + { + // This is a corruption/cycle guard + constexpr size_t kReachableInstructionSafetyBudget = 4096; + + std::vector pendingOffsets{startOffset}; + std::unordered_set visitedOffsets; + visitedOffsets.reserve(256); + + auto isReadableOffset = [§ion](uint32_t offset) + { + return (offset % MIPS_INSTRUCTION_SIZE) == 0 && offset <= section.size && section.size - offset >= sizeof(uint32_t); + }; + + while (!pendingOffsets.empty() && visitedOffsets.size() < kReachableInstructionSafetyBudget) + { + uint32_t offset = pendingOffsets.back(); + pendingOffsets.pop_back(); + + while (isReadableOffset(offset) && visitedOffsets.size() < kReachableInstructionSafetyBudget && visitedOffsets.insert(offset).second) + { + uint32_t raw = 0; + if (!ReadSectionWord(section, offset, raw)) + { + break; + } + + const uint32_t opcode = OPCODE(raw); + if (opcode == OPCODE_SPECIAL && FUNCTION(raw) == SPECIAL_JR) + { + if (RS(raw) == GPR_RA) + { + return true; + } + break; + } + + if (IsConditionalBranch(raw)) + { + const uint32_t targetOffset = PcRelativeBranchTargetOffset(offset, raw); + const bool isUnconditionalBranch = opcode == OPCODE_BEQ && RS(raw) == GPR_ZERO && RT(raw) == GPR_ZERO; + + if (isUnconditionalBranch) + { + if (!isReadableOffset(targetOffset)) + { + break; + } + offset = targetOffset; + continue; + } + + if (isReadableOffset(targetOffset)) + { + pendingOffsets.push_back(targetOffset); + } + + // Both ordinary and likely branches resume after their delay + // slot on the fallthrough path. The delay instruction cannot + // legally contain another control transfer. + offset += 2u * MIPS_INSTRUCTION_SIZE; + continue; + } + + if (opcode == OPCODE_J) + { + uint32_t targetOffset = 0; + if (!TryGetDirectJumpTargetOffset(section, offset, raw, targetOffset)) + { + break; + } + offset = targetOffset; + continue; + } + + if (opcode == OPCODE_JAL || (opcode == OPCODE_SPECIAL && FUNCTION(raw) == SPECIAL_JALR)) + { + // Calls return after their delay slot; the callee is a separate + // control-flow region and is intentionally not traversed here. + offset += 2u * MIPS_INSTRUCTION_SIZE; + continue; + } + + offset += MIPS_INSTRUCTION_SIZE; + } + } + + return false; + } + + struct TrackedGprState + { + uint32_t offset; + uint32_t trackedGprs; + }; + + constexpr uint32_t kCalleePreservedGprMask = + (1u << GPR_S0) | (1u << GPR_S1) | (1u << GPR_S2) | (1u << GPR_S3) | + (1u << GPR_S4) | (1u << GPR_S5) | (1u << GPR_S6) | (1u << GPR_S7) | + (1u << GPR_GP) | (1u << GPR_SP) | (1u << GPR_FP); + + bool IsConsumedAsCodePointerInControlFlow(const ps2recomp::Section §ion, + uint32_t firstInstructionOffset, + uint32_t trackedGprs, + uint32_t regionBegin, + uint32_t regionEnd) + { + // Retail EE code also uses t0-t3 as extended call arguments for localhelpers. + constexpr uint32_t kArgumentMask = + (1u << GPR_A0) | (1u << GPR_A1) | (1u << GPR_A2) | (1u << GPR_A3) | + (1u << GPR_T0) | (1u << GPR_T1) | (1u << GPR_T2) | (1u << GPR_T3); + std::vector worklist; + std::unordered_set visited; + auto enqueue = [&](uint32_t offset, uint32_t registers) + { + if (registers != 0 && offset >= regionBegin && + offset + MIPS_INSTRUCTION_SIZE <= regionEnd) + { + worklist.push_back({offset, registers}); + } + }; + enqueue(firstInstructionOffset, trackedGprs); + + while (!worklist.empty()) + { + const TrackedGprState state = worklist.back(); + worklist.pop_back(); + + const uint64_t visitKey = (static_cast(state.offset) << 32u) | state.trackedGprs; + if (!visited.insert(visitKey).second) + { + continue; + } + + uint32_t raw = 0; + if (!ReadSectionWord(section, state.offset, raw)) + { + continue; + } + + const uint32_t opcode = OPCODE(raw); + if ((opcode == OPCODE_SW || opcode == OPCODE_SD || opcode == OPCODE_SQ) && + (state.trackedGprs & (1u << RT(raw))) != 0) + { + // Some retail C++ runtimes construct vtables and interface + // descriptors in writable memory. In those cases the only + // address-taken evidence is a materialized code address being + // stored into an object; it is never passed to a registrar. + return true; + } + + if (IsCallInstruction(raw)) + { + const bool isTrackedIndirectTarget = OPCODE(raw) == OPCODE_SPECIAL && (state.trackedGprs & (1u << RS(raw))) != 0; + if (isTrackedIndirectTarget) + { + return true; + } + + uint32_t afterDelay = state.trackedGprs; + uint32_t delayRaw = 0; + if (ReadSectionWord(section, state.offset + MIPS_INSTRUCTION_SIZE, delayRaw)) + { + afterDelay = PropagateTrackedGprs(delayRaw, afterDelay); + } + if ((afterDelay & kArgumentMask) != 0) + { + return true; + } + + // A normal call may clobber caller-saved registers, but values + // deliberately parked in s0-s7/fp remain live afterwards. + enqueue(state.offset + (2u * MIPS_INSTRUCTION_SIZE), afterDelay & kCalleePreservedGprMask); + continue; + } + + if (IsConditionalBranch(raw)) + { + uint32_t delayTracked = state.trackedGprs; + uint32_t delayRaw = 0; + if (ReadSectionWord(section, state.offset + MIPS_INSTRUCTION_SIZE, delayRaw)) + { + delayTracked = PropagateTrackedGprs(delayRaw, delayTracked); + } + + enqueue(PcRelativeBranchTargetOffset(state.offset, raw), delayTracked); + enqueue(state.offset + (2u * MIPS_INSTRUCTION_SIZE), IsLikelyBranch(raw) ? state.trackedGprs : delayTracked); + continue; + } + + if (OPCODE(raw) == OPCODE_J) + { + uint32_t delayTracked = state.trackedGprs; + uint32_t delayRaw = 0; + if (ReadSectionWord(section, state.offset + MIPS_INSTRUCTION_SIZE, delayRaw)) + { + delayTracked = PropagateTrackedGprs(delayRaw, delayTracked); + } + + uint32_t targetOffset = 0; + if (TryGetDirectJumpTargetOffset(section, state.offset, raw, targetOffset)) + { + enqueue(targetOffset, delayTracked); + } + else if ((delayTracked & kArgumentMask) != 0) + { + // A direct jump outside the current coarse function is a + // tail call, and observes the same argument registers. + return true; + } + continue; + } + + if (OPCODE(raw) == OPCODE_SPECIAL && FUNCTION(raw) == SPECIAL_JR) + { + // A materialized code address used as a JR target is a tail + // callback even though it does not write $ra. + if ((state.trackedGprs & (1u << RS(raw))) != 0) + { + return true; + } + continue; + } + + if (IsControlTransfer(raw)) + { + continue; + } + + const uint32_t nextTracked = PropagateTrackedGprs(raw, state.trackedGprs); + enqueue(state.offset + MIPS_INSTRUCTION_SIZE, nextTracked); + } + + return false; + } + + bool TryCompleteCodeAddress(uint32_t raw, + uint32_t upperValue, + uint32_t upperAliases, + uint32_t &outTarget, + uint32_t &outValueReg) + { + const uint32_t opcode = OPCODE(raw); + const uint32_t rs = RS(raw); + if ((opcode != OPCODE_ADDIU && opcode != OPCODE_DADDIU && opcode != OPCODE_ORI) || (upperAliases & (1u << rs)) == 0) + { + return false; + } + + const uint16_t immediate = static_cast(IMMEDIATE(raw)); + outTarget = opcode == OPCODE_ORI + ? upperValue | static_cast(immediate) + : upperValue + static_cast(static_cast(static_cast(immediate))); + outValueReg = RT(raw); + return true; + } + + bool TryLoadCodeAddressFromInitializedData( + const std::vector §ions, + uint32_t raw, + uint32_t upperValue, + uint32_t upperAliases, + uint32_t &outTarget, + uint32_t &outValueReg) + { + const uint32_t opcode = OPCODE(raw); + const uint32_t baseReg = RS(raw); + if ((opcode != OPCODE_LW && opcode != OPCODE_LWU) || (upperAliases & (1u << baseReg)) == 0) + { + return false; + } + + const uint32_t slotAddress = upperValue + static_cast(SIMMEDIATE(raw)); + uint32_t target = 0; + if (!ReadWordByAddress(sections, slotAddress, target) || (target % MIPS_INSTRUCTION_SIZE) != 0) + { + return false; + } + + const ps2recomp::Section *targetSection = FindCodeSectionByAddress(sections, target); + if (!targetSection) + { + return false; + } + + const uint32_t targetOffset = target - targetSection->address; + if (targetOffset >= MIPS_INSTRUCTION_SIZE) + { + uint32_t predecessorRaw = 0; + if (ReadSectionWord(*targetSection, targetOffset - MIPS_INSTRUCTION_SIZE, predecessorRaw) && IsControlTransfer(predecessorRaw)) + { + // A loaded code-looking value that lands immediately after a + // control transfer names its delay slot. Promoting it would cut + // the real owner before the delay instruction, as happened at Silent Hill's 0x325350 (the delay slot of JALR 0x32534c). + return false; + } + } + + outTarget = target; + outValueReg = RT(raw); + return outValueReg != GPR_ZERO; + } + + bool IsMaterializedAddressConsumed(const std::vector §ions, + const ps2recomp::Section §ion, + uint32_t target, + uint32_t valueReg, + uint32_t firstUseOffset, + uint32_t regionBegin, + uint32_t regionEnd, + bool consumedByCurrentCall, + std::unordered_set &starts) + { + const bool materializedAsCodePointer = + consumedByCurrentCall || + IsConsumedAsCodePointerInControlFlow(section, + firstUseOffset, + 1u << valueReg, + regionBegin, + regionEnd); + + if (!LooksLikeCallableEntry(sections, + target, + materializedAsCodePointer, + materializedAsCodePointer, + materializedAsCodePointer)) + { + return false; + } + + starts.insert(target); + return true; + } + + void ScanMaterializedCodeAddresses(const std::vector §ions, + std::unordered_set &starts) + { + for (const auto §ion : sections) + { + if (!section.isCode || !section.data || section.size < (2u * MIPS_INSTRUCTION_SIZE)) + { + continue; + } + + // JAL targets known before this pass form conservative function + // boundaries. CFG traversal may cross basic blocks but never leaks + // into the next coarse function. + std::vector functionBoundaries; + for (uint32_t start : starts) + { + if (start >= section.address && start < section.address + section.size) + { + functionBoundaries.push_back(start - section.address); + } + } + std::sort(functionBoundaries.begin(), functionBoundaries.end()); + functionBoundaries.erase(std::unique(functionBoundaries.begin(), functionBoundaries.end()), functionBoundaries.end()); + + for (uint32_t offset = 0; offset + MIPS_INSTRUCTION_SIZE <= section.size; + offset += MIPS_INSTRUCTION_SIZE) + { + uint32_t upperRaw = 0; + if (!ReadSectionWord(section, offset, upperRaw) || OPCODE(upperRaw) != OPCODE_LUI) + { + continue; + } + + const uint32_t upperReg = RT(upperRaw); + if (upperReg == GPR_ZERO) + { + continue; + } + + const auto nextBoundary = std::upper_bound(functionBoundaries.begin(), functionBoundaries.end(), offset); + const uint32_t regionBegin = nextBoundary == functionBoundaries.begin() + ? 0u + : *std::prev(nextBoundary); + const uint32_t regionEnd = nextBoundary == functionBoundaries.end() + ? section.size + : *nextBoundary; + const uint32_t upperValue = IMMEDIATE(upperRaw) << 16; + const uint32_t initialAliases = 1u << upperReg; + + std::vector worklist; + std::unordered_set visited; + auto enqueue = [&](uint32_t nextOffset, uint32_t aliases) + { + if (aliases != 0 && nextOffset >= regionBegin && + nextOffset + MIPS_INSTRUCTION_SIZE <= regionEnd) + { + worklist.push_back({nextOffset, aliases}); + } + }; + + bool startsAfterPredecessorTransfer = false; + if (offset >= MIPS_INSTRUCTION_SIZE) + { + uint32_t predecessorRaw = 0; + if (ReadSectionWord(section, offset - MIPS_INSTRUCTION_SIZE, predecessorRaw)) + { + if (IsConditionalBranch(predecessorRaw)) + { + enqueue(PcRelativeBranchTargetOffset(offset - MIPS_INSTRUCTION_SIZE, predecessorRaw), initialAliases); + if (!IsLikelyBranch(predecessorRaw)) + { + enqueue(offset + MIPS_INSTRUCTION_SIZE, initialAliases); + } + startsAfterPredecessorTransfer = true; + } + else if (OPCODE(predecessorRaw) == OPCODE_J) + { + uint32_t targetOffset = 0; + if (TryGetDirectJumpTargetOffset( + section, + offset - MIPS_INSTRUCTION_SIZE, + predecessorRaw, + targetOffset)) + { + enqueue(targetOffset, initialAliases); + } + startsAfterPredecessorTransfer = true; + } + else if (IsCallInstruction(predecessorRaw) || (OPCODE(predecessorRaw) == OPCODE_SPECIAL && FUNCTION(predecessorRaw) == SPECIAL_JR)) + { + // The LUI ran in a call/jump delay slot. An incomplete + // address cannot survive through an unknown callee or + // indirect target in a caller-saved register. + startsAfterPredecessorTransfer = true; + } + } + } + if (!startsAfterPredecessorTransfer) + { + enqueue(offset + MIPS_INSTRUCTION_SIZE, initialAliases); + } + + while (!worklist.empty()) + { + const TrackedGprState state = worklist.back(); + worklist.pop_back(); + + const uint64_t visitKey = (static_cast(state.offset) << 32u) | state.trackedGprs; + if (!visited.insert(visitKey).second) + { + continue; + } + + uint32_t raw = 0; + if (!ReadSectionWord(section, state.offset, raw)) + { + continue; + } + + uint32_t target = 0; + uint32_t valueReg = GPR_ZERO; + if (TryLoadCodeAddressFromInitializedData(sections, + raw, + upperValue, + state.trackedGprs, + target, + valueReg)) + { + // A singleton callback slot is strong evidence only when + // the loaded value actually flows into a call/jump (or a + // callback store). The CFG worklist in the consumer check + // follows branches and register copies, so this does not + // promote every code-looking word found in arbitrary data. + IsMaterializedAddressConsumed(sections, + section, + target, + valueReg, + state.offset + MIPS_INSTRUCTION_SIZE, + regionBegin, + regionEnd, + false, + starts); + + // Keep following the data-slot base as well: one function + // can load several callbacks from the same descriptor. + const uint32_t nextAliases = PropagateTrackedGprs(raw, state.trackedGprs); + enqueue(state.offset + MIPS_INSTRUCTION_SIZE, nextAliases); + continue; + } + + if (TryCompleteCodeAddress(raw, upperValue, state.trackedGprs, target, valueReg)) + { + IsMaterializedAddressConsumed(sections, + section, + target, + valueReg, + state.offset + MIPS_INSTRUCTION_SIZE, + regionBegin, + regionEnd, + false, + starts); + continue; + } + + if (IsControlTransfer(raw)) + { + uint32_t delayAliases = state.trackedGprs; + uint32_t delayRaw = 0; + const bool hasDelay = ReadSectionWord(section, state.offset + MIPS_INSTRUCTION_SIZE, delayRaw); + if (hasDelay && TryCompleteCodeAddress(delayRaw, upperValue, delayAliases, target, valueReg)) + { + const bool currentIsCall = IsCallInstruction(raw); + const uint32_t firstUseOffset = state.offset + (2u * MIPS_INSTRUCTION_SIZE); + IsMaterializedAddressConsumed(sections, + section, + target, + valueReg, + firstUseOffset, + regionBegin, + regionEnd, + currentIsCall && + valueReg >= GPR_A0 && + valueReg <= GPR_A3, + starts); + continue; + } + if (hasDelay) + { + delayAliases = PropagateTrackedGprs(delayRaw, delayAliases); + } + + if (IsConditionalBranch(raw)) + { + enqueue(PcRelativeBranchTargetOffset(state.offset, raw), delayAliases); + enqueue(state.offset + (2u * MIPS_INSTRUCTION_SIZE), IsLikelyBranch(raw) ? state.trackedGprs : delayAliases); + } + else if (OPCODE(raw) == OPCODE_J) + { + uint32_t targetOffset = 0; + if (TryGetDirectJumpTargetOffset(section, state.offset, raw, targetOffset)) + { + enqueue(targetOffset, delayAliases); + } + } + else if (IsCallInstruction(raw)) + { + // An upper half deliberately kept in s0-s7/fp/gp + // survives ordinary calls. Retail callback builders + // commonly load that half once, perform setup calls, + // and only then complete the address into a0-a3. + enqueue(state.offset + (2u * MIPS_INSTRUCTION_SIZE), delayAliases & kCalleePreservedGprMask); + } + continue; + } + + const uint32_t nextAliases = PropagateTrackedGprs(raw, state.trackedGprs); + enqueue(state.offset + MIPS_INSTRUCTION_SIZE, nextAliases); + } + } + } + } + + bool IsDedicatedFunctionPointerSection(const std::string &name) + { + return name == ".ctors" || name == ".dtors" || name == ".init_array" || name == ".fini_array"; + } + + void ScanDataFunctionPointerTables(const std::vector §ions, std::unordered_set &starts) + { + struct PointerCandidate + { + uint32_t sourceOffset; + uint32_t target; + bool hasConservativeShape; + }; + + constexpr uint32_t kClusterDistanceBytes = 32; + + for (const auto §ion : sections) + { + if (!section.isData || section.isCode || section.isBSS || + !section.data || section.size < MIPS_INSTRUCTION_SIZE) + { + continue; + } + + std::vector candidates; + for (uint32_t offset = 0; offset + MIPS_INSTRUCTION_SIZE <= section.size; + offset += MIPS_INSTRUCTION_SIZE) + { + uint32_t target = 0; + if (ReadSectionWord(section, offset, target) && (target % MIPS_INSTRUCTION_SIZE) == 0 && FindCodeSectionByAddress(sections, target)) + { + candidates.push_back({offset, target, LooksLikeCallableEntry(sections, target, true)}); + } + } + + const bool dedicatedPointerSection = IsDedicatedFunctionPointerSection(section.name); + auto hasConservativeNeighborWithin = [&](size_t candidateIndex, uint32_t maxDistance) + { + for (size_t neighbor = candidateIndex; neighbor > 0;) + { + --neighbor; + if (candidates[candidateIndex].sourceOffset - candidates[neighbor].sourceOffset > maxDistance) + { + break; + } + if (candidates[neighbor].hasConservativeShape) + { + return true; + } + } + + for (size_t neighbor = candidateIndex + 1; neighbor < candidates.size(); ++neighbor) + { + if (candidates[neighbor].sourceOffset - candidates[candidateIndex].sourceOffset > maxDistance) + { + break; + } + if (candidates[neighbor].hasConservativeShape) + { + return true; + } + } + + return false; + }; + + auto hasClassDescriptorShape = [&](size_t candidateIndex) + { + // Several retail engines describe a class as: + // name pointer, ordinary method, 0, 0, long leaf method + // The ordinary method gives us a conservative executable anchor, + // while the two reserved words and data pointer distinguish this + // from an accidental code address embedded in arbitrary data. + const uint32_t methodOffset = candidates[candidateIndex].sourceOffset; + constexpr uint32_t kDescriptorPrefixBytes = 4u * sizeof(uint32_t); + if (methodOffset < kDescriptorPrefixBytes) + { + return false; + } + + uint32_t nameAddress = 0; + uint32_t conservativeMethod = 0; + uint32_t reserved0 = 0; + uint32_t reserved1 = 0; + if (!ReadSectionWord(section, methodOffset - 16u, nameAddress) || + !ReadSectionWord(section, methodOffset - 12u, conservativeMethod) || + !ReadSectionWord(section, methodOffset - 8u, reserved0) || + !ReadSectionWord(section, methodOffset - 4u, reserved1) || + reserved0 != 0 || reserved1 != 0) + { + return false; + } + + const ps2recomp::Section *nameSection = FindSectionByAddress(sections, nameAddress); + return nameSection && nameSection->isData && !nameSection->isCode && + LooksLikeCallableEntry(sections, conservativeMethod, true); + }; + + auto hasAlternatingPointerIdShape = [&](size_t candidateIndex) + { + const auto isSmallIdAt = [&](uint32_t offset) + { + uint32_t value = 0; + return offset + sizeof(uint32_t) <= section.size && + ReadSectionWord(section, offset, value) && + value <= 0xFFFFu; + }; + + const PointerCandidate &candidate = candidates[candidateIndex]; + if (!isSmallIdAt(candidate.sourceOffset + sizeof(uint32_t))) + return false; + + if (candidateIndex > 0) + { + const PointerCandidate &previous = candidates[candidateIndex - 1u]; + if (previous.hasConservativeShape && + previous.sourceOffset + (2u * sizeof(uint32_t)) == candidate.sourceOffset && + isSmallIdAt(previous.sourceOffset + sizeof(uint32_t))) + { + return true; + } + } + + if (candidateIndex + 1u < candidates.size()) + { + const PointerCandidate &next = candidates[candidateIndex + 1u]; + if (next.hasConservativeShape && + candidate.sourceOffset + (2u * sizeof(uint32_t)) == next.sourceOffset && + isSmallIdAt(next.sourceOffset + sizeof(uint32_t))) + { + return true; + } + } + return false; + }; + + for (size_t index = 0; index < candidates.size(); ++index) + { + if (candidates[index].hasConservativeShape) + { + const bool clustered = dedicatedPointerSection || hasConservativeNeighborWithin(index, kClusterDistanceBytes); + if (clustered) + { + starts.insert(candidates[index].target); + } + continue; + } + + const bool hasConservativeAnchor = dedicatedPointerSection || hasClassDescriptorShape(index) || hasAlternatingPointerIdShape(index); + if (hasConservativeAnchor && LooksLikeCallableEntry(sections, candidates[index].target, true, true)) + { + starts.insert(candidates[index].target); + } + } + } + } + + void ScanAdjacentLeafThunkRuns(const std::vector §ions, std::unordered_set &starts) + { + // Stripped retail ELFs occasionally pack trivial accessors back-to-back: + // + // known_entry: jr ra next_entry: jr ra + // + // + // A coarse function map can merge the middle accessor into its predecessor, + // even when runtime tables call it directly. Requiring the predecessor to + // already be a known function start keeps this deliberately narrower than + // treating every instruction after a return as a new function. + constexpr uint32_t kLeafThunkBytes = 2u * MIPS_INSTRUCTION_SIZE; + + for (const auto §ion : sections) + { + if (!section.isCode || !section.data || section.size < (2u * kLeafThunkBytes)) + { + continue; + } + + for (uint32_t offset = kLeafThunkBytes; offset + kLeafThunkBytes <= section.size; offset += MIPS_INSTRUCTION_SIZE) + { + const uint32_t previousAddress = section.address + offset - kLeafThunkBytes; + if (!starts.contains(previousAddress)) + { + continue; + } + + uint32_t previousRaw = 0; + uint32_t candidateRaw = 0; + if (!ReadSectionWord(section, offset - kLeafThunkBytes, previousRaw) || !ReadSectionWord(section, offset, candidateRaw)) + { + continue; + } + + const auto isReturn = [](uint32_t raw) + { + return OPCODE(raw) == OPCODE_SPECIAL && FUNCTION(raw) == SPECIAL_JR && RS(raw) == GPR_RA; + }; + + if (isReturn(previousRaw) && isReturn(candidateRaw)) + { + starts.insert(section.address + offset); + } + } + } + } + + void ScanFunctionStartsFallback(ps2recomp::ElfParser *parser, std::vector &outFunctions) { std::unordered_set starts; starts.reserve(4096); @@ -467,26 +1619,28 @@ namespace const auto §ions = parser->getSections(); for (const auto §ion : sections) { - if (!section.isCode || !section.data || section.size < 4) + if (!section.isCode || !section.data || section.size < MIPS_INSTRUCTION_SIZE) { continue; } - for (uint32_t offset = 0; offset + 4 <= section.size; offset += 4) + for (uint32_t offset = 0; offset + MIPS_INSTRUCTION_SIZE <= section.size; offset += MIPS_INSTRUCTION_SIZE) { const uint32_t pc = section.address + offset; uint32_t raw = 0; std::memcpy(&raw, section.data + offset, sizeof(uint32_t)); - const uint32_t op = (raw >> 26) & 0x3F; - if (op != 0x03) // JAL + const uint32_t op = OPCODE(raw); + if (op != OPCODE_JAL) { continue; } - const uint32_t index = raw & 0x03FFFFFF; - const uint32_t target = ((pc + 4) & 0xF0000000u) | (index << 2); + const uint32_t index = TARGET(raw); + const uint32_t target = + ((pc + MIPS_INSTRUCTION_SIZE) & MIPS_JUMP_REGION_MASK) | + (index << MIPS_JUMP_TARGET_SHIFT); if (FindCodeSectionByAddress(sections, target)) { @@ -495,6 +1649,10 @@ namespace } } + ScanMaterializedCodeAddresses(sections, starts); + ScanDataFunctionPointerTables(sections, starts); + ScanAdjacentLeafThunkRuns(sections, starts); + std::vector sortedStarts(starts.begin(), starts.end()); std::sort(sortedStarts.begin(), sortedStarts.end()); @@ -508,22 +1666,10 @@ namespace continue; } - const uint32_t secEnd = sec->address + sec->size; - - uint32_t end = secEnd; - if (i + 1 < sortedStarts.size()) - { - const uint32_t next = sortedStarts[i + 1]; - if (next > start && next < secEnd) - { - end = next; - } - } - ps2recomp::Function func{}; func.name = MakeAutoFunctionName(start); func.start = start; - func.end = (end > start) ? end : (start + 4); + func.end = 0; func.isRecompiled = false; func.isStub = false; func.isSkipped = false; @@ -543,7 +1689,17 @@ namespace ps2recomp bool ElfParser::isExecutableSection(const ELFIO::section *section) const { - return (section->get_flags() & ELFIO::SHF_EXECINSTR) != 0; + if ((section->get_flags() & ELFIO::SHF_EXECINSTR) == 0) + return false; + + // Sony's PS2 linker marks embedded VU microprogram sections executable, + // but their words use the VU ISA rather than the EE/R5900 ISA. Keep the + // bytes in the parsed ELF while excluding these ABI-defined section + // names from EE function discovery and recompilation. + const std::string name = section->get_name(); + const bool vuText = name == ".vutext" || name.rfind(".vutext.", 0) == 0; + const bool dvpOverlay = name == ".DVP.overlay" || name.rfind(".DVP.overlay.", 0) == 0; + return !vuText && !dvpOverlay; } bool ElfParser::isDataSection(const ELFIO::section *section) const @@ -1113,9 +2269,7 @@ namespace ps2recomp { if (m_reporter) { - m_reporter->warning("ghidra-map", "Loaded 0 functions from Ghidra map after filtering (" + - std::to_string(skippedNonExecutable) + " non-executable, " + - std::to_string(skippedInvalidRange) + " invalid range)."); + m_reporter->warning("ghidra-map", "Loaded 0 functions from Ghidra map after filtering (" + std::to_string(skippedNonExecutable) + " non-executable, " + std::to_string(skippedInvalidRange) + " invalid range)."); } } @@ -1192,8 +2346,7 @@ namespace ps2recomp { if (m_reporter) { - m_reporter->info("elf", "ELF has no section headers; using loadable segments as sections (" + - std::to_string(m_sections.size()) + " entries)."); + m_reporter->info("elf", "ELF has no section headers; using loadable segments as sections (" + std::to_string(m_sections.size()) + " entries)."); } } } @@ -1418,14 +2571,26 @@ namespace ps2recomp } } - if (m_extraFunctions.empty()) - { - ScanJalTargetsFallback(this, m_extraFunctions); - } + // DWARF in retail ELFs is often partial. + ScanFunctionStartsFallback(this, m_extraFunctions); std::sort(m_extraFunctions.begin(), m_extraFunctions.end(), [](const Function &a, const Function &b) - { return a.start < b.start; }); + { + if (a.start != b.start) + { + return a.start < b.start; + } + + const bool aAuto = IsAutoGeneratedName(a.name); + const bool bAuto = IsAutoGeneratedName(b.name); + if (aAuto != bAuto) + { + return !aAuto; + } + + return a.end > b.end; + }); m_extraFunctions.erase( std::unique(m_extraFunctions.begin(), m_extraFunctions.end(), diff --git a/ps2xRecomp/src/lib/function_emitter.cpp b/ps2xRecomp/src/lib/function_emitter.cpp index 328c0e0..3192e90 100644 --- a/ps2xRecomp/src/lib/function_emitter.cpp +++ b/ps2xRecomp/src/lib/function_emitter.cpp @@ -45,8 +45,8 @@ namespace ps2recomp ss << "#include \n"; ss << "#include \"ps2_runtime_macros.h\"\n"; ss << "#include \"ps2_runtime.h\"\n"; - ss << "#include \"ps2_recompiled_functions.h\"\n"; - ss << "#include \"ps2_recompiled_stubs.h\"\n\n"; + ss << "#include \n"; + ss << "#include \n\n"; ss << "#include \"ps2_syscalls.h\"\n"; ss << "#include \"ps2_stubs.h\"\n\n"; ss << "#ifdef PS2_FUNCTION_LOG_TRACKER\n"; diff --git a/ps2xRecomp/src/lib/function_table_emitter.cpp b/ps2xRecomp/src/lib/function_table_emitter.cpp index 27dc9b7..b82b259 100644 --- a/ps2xRecomp/src/lib/function_table_emitter.cpp +++ b/ps2xRecomp/src/lib/function_table_emitter.cpp @@ -147,9 +147,9 @@ namespace ps2recomp std::stringstream ss; ss << "#include \"ps2_runtime.h\"\n"; - ss << "#include \"ps2_recompiled_functions.h\"\n"; + ss << "#include \n"; ss << "#include \"ps2_stubs.h\"\n"; - ss << "#include \"ps2_recompiled_stubs.h\"//this will give duplicated erros because runtime maybe has it define already, just delete the TODOS ones\n"; + ss << "#include \n"; ss << "#include \"ps2_syscalls.h\"\n\n"; ss << "extern const uint32_t g_ps2RecompiledFunctionTableBase = 0x" << std::hex << tableBase << "u;\n"; diff --git a/ps2xRecomp/src/lib/instruction_translator.cpp b/ps2xRecomp/src/lib/instruction_translator.cpp index ae38ffa..7684097 100644 --- a/ps2xRecomp/src/lib/instruction_translator.cpp +++ b/ps2xRecomp/src/lib/instruction_translator.cpp @@ -69,14 +69,8 @@ namespace ps2recomp MemoryAccessHint InstructionTranslator::effectiveMemoryHintFor(const Instruction &inst, const MemoryAccessHint &memoryHint) const { - MemoryAccessHint effectiveMemoryHint = memoryHint; - if (inst.isMmio) - { - effectiveMemoryHint.hasAddress = true; - effectiveMemoryHint.address = inst.mmioAddress; - } - - return effectiveMemoryHint; + // TODO disable for now since it causing issues with some games. + return memoryHint; } std::string InstructionTranslator::translateMemoryRead(const Instruction &inst, @@ -190,11 +184,11 @@ namespace ps2recomp case OPCODE_LW: return fmt::format("SET_GPR_S32(ctx, {}, (int32_t){});", inst.rt, genRead(32, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate))); case OPCODE_LBU: - return fmt::format("SET_GPR_U32(ctx, {}, (uint8_t){});", inst.rt, genRead(8, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate))); + return fmt::format("SET_GPR_ZE32(ctx, {}, (uint8_t){});", inst.rt, genRead(8, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate))); case OPCODE_LHU: - return fmt::format("SET_GPR_U32(ctx, {}, (uint16_t){});", inst.rt, genRead(16, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate))); + return fmt::format("SET_GPR_ZE32(ctx, {}, (uint16_t){});", inst.rt, genRead(16, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate))); case OPCODE_LWU: - return fmt::format("SET_GPR_U32(ctx, {}, {});", inst.rt, genRead(32, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate))); + return fmt::format("SET_GPR_ZE32(ctx, {}, {});", inst.rt, genRead(32, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate))); case OPCODE_SB: return genWrite(8, fmt::format("ADD32(GPR_U32(ctx, {}), {})", inst.rs, inst.simmediate), fmt::format("(uint8_t)GPR_U32(ctx, {})", inst.rt)) + ";"; case OPCODE_SH: diff --git a/ps2xRecomp/src/lib/ps2_recompiler.cpp b/ps2xRecomp/src/lib/ps2_recompiler.cpp index ab779a4..bb35a7a 100644 --- a/ps2xRecomp/src/lib/ps2_recompiler.cpp +++ b/ps2xRecomp/src/lib/ps2_recompiler.cpp @@ -102,10 +102,10 @@ namespace ps2recomp void writeCombinedOutputPreamble(std::ostream &output) { output << "#include \n"; - output << "#include \"ps2_recompiled_functions.h\"\n\n"; + output << "#include \n\n"; output << "#include \"ps2_runtime_macros.h\"\n"; output << "#include \"ps2_runtime.h\"\n"; - output << "#include \"ps2_recompiled_stubs.h\"\n"; + output << "#include \n"; output << "#include \"ps2_syscalls.h\"\n"; output << "#include \"ps2_stubs.h\"\n"; output << "#ifdef _DEBUG\n"; @@ -288,7 +288,8 @@ namespace ps2recomp std::unordered_map> &decodedFunctions, const std::vector
§ions, CodeGenerator *codeGenerator, - const std::function &decodeExternalFunction) + const std::function &decodeExternalFunction, + const std::unordered_set &seedEntryAddresses = {}) { std::unordered_set existingStarts; for (const auto &function : functions) @@ -312,6 +313,19 @@ namespace ps2recomp return false; }; + auto executableSectionEnd = [&](uint32_t address) -> std::optional + { + for (const auto §ion : sections) + { + if (!section.isCode || address < section.address || address >= section.address + section.size) + { + continue; + } + return section.address + section.size; + } + return std::nullopt; + }; + auto isSimpleReturnThunkStart = [](const Instruction &inst) -> bool { return inst.opcode == OPCODE_SPECIAL && @@ -397,6 +411,14 @@ namespace ps2recomp pendingStarts.insert(target); }; + if (stats.passCount == 1u) + { + for (uint32_t target : seedEntryAddresses) + { + queuePendingEntry(target); + } + } + for (const auto &function : functions) { if (!function.isRecompiled || function.isStub || function.isSkipped) @@ -534,13 +556,24 @@ namespace ps2recomp } else { - auto nextStartOpt = findNextBoundaryStart(target); - if (!nextStartOpt.has_value() || nextStartOpt.value() <= target) + const auto sectionEndOpt = executableSectionEnd(target); + if (!sectionEndOpt.has_value()) { continue; } - entryFunction.end = nextStartOpt.value(); + uint32_t entryEnd = sectionEndOpt.value(); + auto nextStartOpt = findNextBoundaryStart(target); + if (nextStartOpt.has_value() && nextStartOpt.value() < entryEnd) + { + entryEnd = nextStartOpt.value(); + } + if (entryEnd <= target) + { + continue; + } + + entryFunction.end = entryEnd; if (!decodeExternalFunction(entryFunction)) { continue; @@ -725,6 +758,71 @@ namespace ps2recomp return reslicedCount; } + + size_t collectInternalEntryTargetsImpl( + const std::vector &functions, + const std::unordered_map> &decodedFunctions, + const std::unordered_set &entryAddresses, + std::unordered_map> &targetsByOwner) + { + std::unordered_set functionStarts; + functionStarts.reserve(functions.size()); + for (const auto &function : functions) + { + functionStarts.insert(function.start); + } + + size_t addedCount = 0u; + for (uint32_t entryAddress : entryAddresses) + { + if (functionStarts.contains(entryAddress)) + { + continue; + } + + const Function *owner = nullptr; + for (const auto &function : functions) + { + if (!function.isRecompiled || function.isStub || function.isSkipped || + entryAddress <= function.start || entryAddress >= function.end) + { + continue; + } + + const auto decodedIt = decodedFunctions.find(function.start); + if (decodedIt == decodedFunctions.end()) + { + continue; + } + + const bool containsInstruction = std::any_of(decodedIt->second.begin(), decodedIt->second.end(), [entryAddress](const Instruction &instruction) + { return instruction.address == entryAddress; }); + if (!containsInstruction) + { + continue; + } + + if (!owner || function.start > owner->start) + { + owner = &function; + } + } + + if (!owner) + { + continue; + } + + auto &targets = targetsByOwner[owner->start]; + if (std::find(targets.begin(), targets.end(), entryAddress) == targets.end()) + { + targets.push_back(entryAddress); + ++addedCount; + } + } + + return addedCount; + } } PS2Recompiler::PS2Recompiler(const std::string &configPath) @@ -751,6 +849,7 @@ namespace ps2recomp m_stubFunctions.clear(); m_stubFunctionStarts.clear(); m_stubHandlerBindingsByStart.clear(); + m_entryPointHintStarts.clear(); m_correctnessCriticalFunctionStarts.clear(); for (const auto &name : m_config.skipFunctions) @@ -792,6 +891,14 @@ namespace ps2recomp } } } + for (const auto &hint : m_config.entryPointHints) + { + const FunctionSelector selector = parseFunctionSelector(hint); + if (selector.start.has_value()) + { + m_entryPointHintStarts.insert(*selector.start); + } + } m_reporter.progress("parsing ELF"); m_elfParser = std::make_unique(m_config.inputPath); @@ -983,7 +1090,7 @@ namespace ps2recomp if (isStubFunction(function)) { - if (!correctnessCritical || hasResolvedStubHandler(function)) + if (hasResolvedStubHandler(function)) { function.isStub = true; function.isSkipped = false; @@ -991,12 +1098,15 @@ namespace ps2recomp continue; } - m_reporter.recordCorrectnessCriticalGuestFallback(); + if (correctnessCritical) + { + m_reporter.recordCorrectnessCriticalGuestFallback(); + } m_reporter.warningAt( - "correctness-critical", + "stub", function.name, function.start, - "Unresolved initializer stub ignored; recompiling the original guest function"); + "Configured stub has no runtime handler; recompiling the original guest function"); } if (shouldSkipFunction(function)) @@ -1792,6 +1902,63 @@ namespace ps2recomp return; } + std::unordered_set guestFallbackEntryAddresses = m_entryPointHintStarts; + for (uint32_t address : m_stubFunctionStarts) + { + const auto bindingIt = m_stubHandlerBindingsByStart.find(address); + if (bindingIt == m_stubHandlerBindingsByStart.end() || + resolveStubTarget(bindingIt->second) == StubTarget::Unknown) + { + guestFallbackEntryAddresses.insert(address); + } + } + + // Prefer the existing wrapper when a configured entry lies inside a + // decoded function. If Ghidra/analyzer omitted the whole routine, + // synthesize a standalone guest function bounded by the next known + // function instead of leaving a valid executable target unregistered. + collectInternalEntryTargetsImpl(m_functions, m_decodedFunctions, guestFallbackEntryAddresses, m_resumeEntryTargetsByOwner); + + std::unordered_set coveredEntryAddresses; + coveredEntryAddresses.reserve(m_functions.size() + guestFallbackEntryAddresses.size()); + for (const auto &function : m_functions) + { + coveredEntryAddresses.insert(function.start); + } + for (const auto &[owner, targets] : m_resumeEntryTargetsByOwner) + { + coveredEntryAddresses.insert(targets.begin(), targets.end()); + } + + std::unordered_set standaloneEntryAddresses; + for (uint32_t address : guestFallbackEntryAddresses) + { + if (!coveredEntryAddresses.contains(address)) + { + standaloneEntryAddresses.insert(address); + } + } + + if (!standaloneEntryAddresses.empty()) + { + const EntryDiscoveryStats configuredStats = discoverAdditionalEntryPointsImpl( + m_functions, + m_decodedFunctions, + m_sections, + nullptr, + [this](Function &function) + { return decodeFunction(function); }, + standaloneEntryAddresses); + if (configuredStats.discoveredCount > 0u) + { + m_reporter.recordAdditionalEntryPoints(configuredStats.discoveredCount); + std::ostringstream msg; + msg << "synthesized " << configuredStats.discoveredCount + << " standalone configured guest entry point(s)"; + m_reporter.progress(msg.str()); + } + } + auto findContainingFunction = [&](uint32_t address) -> const Function * { const Function *best = nullptr; @@ -2152,12 +2319,12 @@ namespace ps2recomp return outputPath; } - std::string PS2Recompiler::clampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength) + std::string PS2Recompiler::clampFilenameLength(const std::string &baseName, const std::string &extension, std::size_t maxLength) { if (maxLength == 0) { // Keep this static helper side-effect free; callers validate arguments. - //Better go over the limit than create files with an empty path + // Better go over the limit than create files with an empty path return baseName + extension; } @@ -2224,13 +2391,20 @@ namespace ps2recomp return stats.discoveredCount; } - size_t PS2Recompiler::ResliceEntryFunctions( - std::vector &functions, - std::unordered_map> &decodedFunctions) + size_t PS2Recompiler::ResliceEntryFunctions(std::vector &functions, std::unordered_map> &decodedFunctions) { return resliceEntryFunctionsImpl(functions, decodedFunctions); } + size_t PS2Recompiler::CollectInternalEntryTargets( + const std::vector &functions, + const std::unordered_map> &decodedFunctions, + const std::unordered_set &entryAddresses, + std::unordered_map> &targetsByOwner) + { + return collectInternalEntryTargetsImpl(functions, decodedFunctions, entryAddresses, targetsByOwner); + } + StubTarget PS2Recompiler::resolveStubTarget(const std::string &name) { if (!ps2_runtime_calls::resolveSyscallName(name).empty()) @@ -2244,7 +2418,7 @@ namespace ps2recomp return StubTarget::Unknown; } - std::string PS2Recompiler::ClampFilenameLength(const std::string& baseName, const std::string& extension, std::size_t maxLength) + std::string PS2Recompiler::ClampFilenameLength(const std::string &baseName, const std::string &extension, std::size_t maxLength) { return clampFilenameLength(baseName, extension, maxLength); } diff --git a/ps2xRecomp/tools/ghidra/ExportPS2Functions.java b/ps2xRecomp/tools/ghidra/ExportPS2Functions.java index f84e8a6..694c518 100644 --- a/ps2xRecomp/tools/ghidra/ExportPS2Functions.java +++ b/ps2xRecomp/tools/ghidra/ExportPS2Functions.java @@ -32,6 +32,70 @@ import java.util.regex.Pattern; public class ExportPS2Functions extends GhidraScript { + // Names and values mirror ps2recomp::MipsOpcodes/SpecialFunctions/GprRegisters. + // would be amazing cmake create this script coping the register from the header file + private static final int MIPS_INSTRUCTION_SIZE = 4; + private static final int MIPS_IMMEDIATE_BITS = 16; + private static final int MIPS_IMMEDIATE_SIGN_BIT = 0x8000; + private static final long UINT32_MASK = 0xFFFFFFFFL; + private static final long OPCODE_MASK = 0x3FL; + private static final long REGISTER_MASK = 0x1FL; + private static final long IMMEDIATE_MASK = 0xFFFFL; + private static final int OPCODE_SHIFT = 26; + private static final int RS_SHIFT = 21; + private static final int RT_SHIFT = 16; + private static final int RD_SHIFT = 11; + + private static final int GPR_ZERO = 0; + private static final int GPR_A0 = 4; + private static final int GPR_A3 = 7; + private static final int GPR_SP = 29; + private static final int GPR_RA = 31; + + private static final int OPCODE_SPECIAL = 0x00; + private static final int OPCODE_REGIMM = 0x01; + private static final int OPCODE_J = 0x02; + private static final int OPCODE_JAL = 0x03; + private static final int OPCODE_BEQ = 0x04; + private static final int OPCODE_BNE = 0x05; + private static final int OPCODE_BLEZ = 0x06; + private static final int OPCODE_BGTZ = 0x07; + private static final int OPCODE_ADDI = 0x08; + private static final int OPCODE_ADDIU = 0x09; + private static final int OPCODE_SLTI = 0x0A; + private static final int OPCODE_SLTIU = 0x0B; + private static final int OPCODE_ANDI = 0x0C; + private static final int OPCODE_ORI = 0x0D; + private static final int OPCODE_XORI = 0x0E; + private static final int OPCODE_LUI = 0x0F; + private static final int OPCODE_BEQL = 0x14; + private static final int OPCODE_BNEL = 0x15; + private static final int OPCODE_BLEZL = 0x16; + private static final int OPCODE_BGTZL = 0x17; + private static final int OPCODE_DADDI = 0x18; + private static final int OPCODE_DADDIU = 0x19; + private static final int OPCODE_LDL = 0x1A; + private static final int OPCODE_LDR = 0x1B; + private static final int OPCODE_MMI = 0x1C; + private static final int OPCODE_LQ = 0x1E; + private static final int OPCODE_SQ = 0x1F; + private static final int OPCODE_LB = 0x20; + private static final int OPCODE_LH = 0x21; + private static final int OPCODE_LWL = 0x22; + private static final int OPCODE_LW = 0x23; + private static final int OPCODE_LBU = 0x24; + private static final int OPCODE_LHU = 0x25; + private static final int OPCODE_LWR = 0x26; + private static final int OPCODE_LWU = 0x27; + private static final int OPCODE_SW = 0x2B; + private static final int OPCODE_LL = 0x30; + private static final int OPCODE_LLD = 0x34; + private static final int OPCODE_LD = 0x37; + private static final int OPCODE_SD = 0x3F; + + private static final int SPECIAL_JR = 0x08; + private static final int SPECIAL_JALR = 0x09; + // For now I have to copy all functions from the runtime handler list private static final Set RUNTIME_HANDLER_NAMES = new HashSet<>(Arrays.asList( "FlushCache", "iFlushCache", "ResetEE", "SetMemoryMode", "InitThread", "CreateThread", @@ -51,7 +115,7 @@ public class ExportPS2Functions extends GhidraScript { "fioWrite", "fioLseek", "fioMkdir", "fioChdir", "fioRmdir", "fioGetstat", "fioRemove", "SetGsCrt", "GsSetCrt", "GsGetIMR", "iGsGetIMR", "GsPutIMR", "iGsPutIMR", "SetVSyncFlag", "SetSyscall", "GsSetVideoMode", "GetOsdConfigParam", "SetOsdConfigParam", - "EnableCache", "DisableCache", "GetRomName", "SifLoadElfPart", "sceSifLoadElf", "sceSifLoadElfPart", + "EnableCache", "DisableCache", "SifLoadElfPart", "sceSifLoadElf", "sceSifLoadElfPart", "sceSifLoadModule", "sceSifLoadModuleBuffer", "SetupThread", "EndOfHeap", "GetMemorySize", "Deci2Call", "QueryBootMode", "GetThreadTLS", "Copy", "GetEntryAddress", "RegisterExitHandler", "ret0", "ret1", "reta0", "calloc_r", "free_r", "realloc_r", "memalign_r", "malloc_r", "malloc_extend_top", "malloc_trim_r", "mbtowc_r", "printf_r", @@ -207,6 +271,16 @@ public class ExportPS2Functions extends GhidraScript { boolean syntheticEntry = false; } + private static final class AddressTakenCandidate { + long sourceOffset; + long target; + + AddressTakenCandidate(long sourceOffset, long target) { + this.sourceOffset = sourceOffset; + this.target = target; + } + } + private enum ClassificationKind { STUB, UNTRACKED_STUB, @@ -224,7 +298,31 @@ public class ExportPS2Functions extends GhidraScript { } private static String hex(long value) { - return String.format("0x%08X", value & 0xFFFFFFFFL); + return String.format("0x%08X", value & UINT32_MASK); + } + + private static int opcode(long raw) { + return (int) ((raw >>> OPCODE_SHIFT) & OPCODE_MASK); + } + + private static int rs(long raw) { + return (int) ((raw >>> RS_SHIFT) & REGISTER_MASK); + } + + private static int rt(long raw) { + return (int) ((raw >>> RT_SHIFT) & REGISTER_MASK); + } + + private static int rd(long raw) { + return (int) ((raw >>> RD_SHIFT) & REGISTER_MASK); + } + + private static int function(long raw) { + return (int) (raw & OPCODE_MASK); + } + + private static int immediate(long raw) { + return (int) (raw & IMMEDIATE_MASK); } private static String tomlString(String value) { @@ -466,8 +564,8 @@ public class ExportPS2Functions extends GhidraScript { } MemoryBlock fromBlock = currentProgram.getMemory().getBlock(from); - if (fromBlock == null || !fromBlock.isExecute()) { - continue; // lets ignore DATA/non-code refs + if (fromBlock != null && fromBlock.isExecute()) { + return true; } } @@ -475,7 +573,311 @@ public class ExportPS2Functions extends GhidraScript { } private static String makeAnonymousEntryName(long start) { - return String.format("entry_%08x", start & 0xFFFFFFFFL); + return String.format("entry_%08x", start & UINT32_MASK); + } + + private Long readWord(Address address) { + if (address == null) { + return null; + } + + try { + return ((long) currentProgram.getMemory().getInt(address)) & UINT32_MASK; + } catch (Exception ignored) { + return null; + } + } + + private Address addressFromOffset(long offset) { + try { + return currentProgram.getAddressFactory().getDefaultAddressSpace().getAddress(offset & UINT32_MASK); + } catch (Exception ignored) { + return null; + } + } + + private boolean looksLikeCallableEntry(long target, boolean allowLeafThunk) { + if ((target % MIPS_INSTRUCTION_SIZE) != 0L) { + return false; + } + + Address address = addressFromOffset(target); + if (!isExecutableAddress(address) || currentProgram.getListing().getInstructionAt(address) == null) { + return false; + } + + for (int index = 0; index < 8; ++index) { + Address probe; + try { + probe = address.add(index * (long) MIPS_INSTRUCTION_SIZE); + } catch (Exception ignored) { + break; + } + + Long rawValue = readWord(probe); + if (rawValue == null) { + break; + } + + long raw = rawValue; + int opcode = opcode(raw); + int rs = rs(raw); + int rt = rt(raw); + int immediate = immediate(raw); + + if (index < 4 && (opcode == OPCODE_ADDIU || opcode == OPCODE_DADDIU) && rs == GPR_SP && rt == GPR_SP && (immediate & MIPS_IMMEDIATE_SIGN_BIT) != 0) { + return true; + } + + if ((opcode == OPCODE_SW || opcode == OPCODE_SD || opcode == OPCODE_SQ) && + rs == GPR_SP && rt == GPR_RA) { + return true; + } + + if (allowLeafThunk && opcode == OPCODE_SPECIAL && + function(raw) == SPECIAL_JR && rs == GPR_RA) { + return true; + } + } + + return false; + } + + private static boolean writesGpr(long raw, int register) { + if (register == GPR_ZERO) { + return false; + } + + int opcode = opcode(raw); + int rt = rt(raw); + int rd = rd(raw); + + if (opcode == OPCODE_SPECIAL || opcode == OPCODE_MMI) { + return rd == register; + } + if (opcode == OPCODE_JAL) { + return register == GPR_RA; + } + + boolean writesRt; + switch (opcode) { + case OPCODE_ADDI: + case OPCODE_ADDIU: + case OPCODE_SLTI: + case OPCODE_SLTIU: + case OPCODE_ANDI: + case OPCODE_ORI: + case OPCODE_XORI: + case OPCODE_LUI: + case OPCODE_DADDI: + case OPCODE_DADDIU: + case OPCODE_LDL: + case OPCODE_LDR: + case OPCODE_LQ: + case OPCODE_LB: + case OPCODE_LH: + case OPCODE_LWL: + case OPCODE_LW: + case OPCODE_LBU: + case OPCODE_LHU: + case OPCODE_LWR: + case OPCODE_LWU: + case OPCODE_LL: + case OPCODE_LLD: + case OPCODE_LD: + writesRt = true; + break; + default: + writesRt = false; + break; + } + return writesRt && rt == register; + } + + private static boolean isControlTransfer(long raw) { + int opcode = opcode(raw); + switch (opcode) { + case OPCODE_REGIMM: + case OPCODE_J: + case OPCODE_JAL: + case OPCODE_BEQ: + case OPCODE_BNE: + case OPCODE_BLEZ: + case OPCODE_BGTZ: + case OPCODE_BEQL: + case OPCODE_BNEL: + case OPCODE_BLEZL: + case OPCODE_BGTZL: + return true; + default: + break; + } + + if (opcode != OPCODE_SPECIAL) { + return false; + } + + int function = function(raw); + return function == SPECIAL_JR || function == SPECIAL_JALR; + } + + private static boolean isCallInstruction(long raw) { + int opcode = opcode(raw); + return opcode == OPCODE_JAL || + (opcode == OPCODE_SPECIAL && function(raw) == SPECIAL_JALR); + } + + private void addSyntheticEntry(List records, Set existingStarts, long target) { + target &= UINT32_MASK; + if (!existingStarts.add(target)) { + return; + } + + FunctionRecord record = new FunctionRecord(); + record.name = makeAnonymousEntryName(target); + record.start = target; + record.syntheticEntry = true; + records.add(record); + } + + private void collectMaterializedCodeEntries(List records, Set existingStarts) { + AddressSet executableAddresses = new AddressSet(); + for (MemoryBlock block : currentProgram.getMemory().getBlocks()) { + if (block != null && block.isExecute()) { + executableAddresses.addRange(block.getStart(), block.getEnd()); + } + } + + InstructionIterator instructions = currentProgram.getListing().getInstructions(executableAddresses, true); + while (instructions.hasNext() && !monitor.isCancelled()) { + Instruction instruction = instructions.next(); + if (instruction == null) { + continue; + } + + Long upperRawValue = readWord(instruction.getAddress()); + if (upperRawValue == null) { + continue; + } + + long upperRaw = upperRawValue; + if (opcode(upperRaw) != OPCODE_LUI) { + continue; + } + + int upperRegister = rt(upperRaw); + if (upperRegister == GPR_ZERO) { + continue; + } + + long upperValue = ((long) immediate(upperRaw)) << MIPS_IMMEDIATE_BITS; + boolean sawControlTransfer = false; + boolean sawCallTransfer = false; + + for (int lookahead = 1; lookahead <= 4; ++lookahead) { + Address lowAddress; + try { + lowAddress = instruction.getAddress().add(lookahead * (long) MIPS_INSTRUCTION_SIZE); + } catch (Exception ignored) { + break; + } + + Long lowRawValue = readWord(lowAddress); + if (lowRawValue == null) { + break; + } + + long lowRaw = lowRawValue; + int opcode = opcode(lowRaw); + int rs = rs(lowRaw); + int rt = rt(lowRaw); + + if ((opcode == OPCODE_ADDIU || opcode == OPCODE_ORI || opcode == OPCODE_DADDIU) && + rs == upperRegister) { + int immediate = immediate(lowRaw); + long target; + if (opcode == OPCODE_ORI) { + target = upperValue | immediate; + } else { + target = (upperValue + (short) immediate) & UINT32_MASK; + } + + Long nextRaw = readWord(addressFromOffset( + lowAddress.getOffset() + MIPS_INSTRUCTION_SIZE)); + boolean followedByCall = nextRaw != null && isCallInstruction(nextRaw); + boolean materializedAsCallArgument = + rt >= GPR_A0 && rt <= GPR_A3 && (sawCallTransfer || followedByCall); + + if (looksLikeCallableEntry(target, materializedAsCallArgument)) { + addSyntheticEntry(records, existingStarts, target); + } + break; + } + + if (sawControlTransfer) { + break; + } + if (writesGpr(lowRaw, upperRegister)) { + break; + } + if (isControlTransfer(lowRaw)) { + sawControlTransfer = true; + sawCallTransfer = isCallInstruction(lowRaw); + } + } + } + } + + private static boolean isDedicatedFunctionPointerBlock(String name) { + return ".ctors".equals(name) || ".dtors".equals(name) || + ".init_array".equals(name) || ".fini_array".equals(name); + } + + private void collectDataFunctionPointerEntries(List records, Set existingStarts) { + final long clusterDistanceBytes = 32L; + + for (MemoryBlock block : currentProgram.getMemory().getBlocks()) { + if (block == null || block.isExecute() || !block.isInitialized() || + block.getSize() < MIPS_INSTRUCTION_SIZE) { + continue; + } + + List candidates = new ArrayList<>(); + for (long offset = 0; + offset + MIPS_INSTRUCTION_SIZE <= block.getSize() && !monitor.isCancelled(); + offset += MIPS_INSTRUCTION_SIZE) { + Address source; + try { + source = block.getStart().add(offset); + } catch (Exception ignored) { + break; + } + + Long target = readWord(source); + if (target != null && looksLikeCallableEntry(target, true)) { + candidates.add(new AddressTakenCandidate(offset, target)); + } + } + + boolean dedicatedPointerBlock = isDedicatedFunctionPointerBlock(block.getName()); + for (int index = 0; index < candidates.size(); ++index) { + AddressTakenCandidate candidate = candidates.get(index); + boolean clustered = dedicatedPointerBlock; + + if (index > 0 && + candidate.sourceOffset - candidates.get(index - 1).sourceOffset <= clusterDistanceBytes) { + clustered = true; + } + if (index + 1 < candidates.size() && + candidates.get(index + 1).sourceOffset - candidate.sourceOffset <= clusterDistanceBytes) { + clustered = true; + } + + if (clustered) { + addSyntheticEntry(records, existingStarts, candidate.target); + } + } + } } private List collectExecutableLabelRecords(List functionRecords) { @@ -555,6 +957,13 @@ public class ExportPS2Functions extends GhidraScript { existingStarts.add(start); } + // Ghidra does not always promote function pointers to CALL references, + // especially when the low half is produced in a MIPS delay slot. Mirror + // the stripped-ELF fallback used by ElfParser so the CSV still contains + // callback and vtable entries that are only address-taken. + collectMaterializedCodeEntries(labelRecords, existingStarts); + collectDataFunctionPointerEntries(labelRecords, existingStarts); + if (labelRecords.isEmpty()) { return labelRecords; } diff --git a/ps2xRuntime/CMakeLists.txt b/ps2xRuntime/CMakeLists.txt index e4dc195..589d768 100644 --- a/ps2xRuntime/CMakeLists.txt +++ b/ps2xRuntime/CMakeLists.txt @@ -10,8 +10,8 @@ set(PS2X_RUNNER_UNITY_BUILD_BATCH_SIZE 32 CACHE STRING "Unity build batch size f option(PS2X_ENABLE_RUNNER_PCH "Precompile the heavy runtime headers for ps2EntryRunner" ON) option(PS2X_ENABLE_SCCACHE "Use sccache as compiler launcher when available" ON) -option(PS2X_ENABLE_RUNTIME_LOGS "Enable PS2 runtime logs" OFF) -option(PS2X_ENABLE_AGRESSIVE_LOGS "Enable very verbose/agressive PS2 runtime logs" OFF) +option(PS2X_ENABLE_RUNTIME_LOGS "Enable PS2 runtime logs" ON) +option(PS2X_ENABLE_AGRESSIVE_LOGS "Enable very verbose/agressive PS2 runtime logs" ON) option(PS2X_ENABLE_IOP_RPC_TRACE "Log unhandled IOP/SIF RPC trace suggestions" ON) option(PS2X_STRICT_RETURN_DIAGNOSTICS "Route generated JR $ra returns through runtime branch diagnostics" OFF) option(PS2X_SHOW_WINDOWS_CONSOLE "Show a console window for ps2EntryRunner on Windows release builds" ON) @@ -386,6 +386,8 @@ add_library(ps2_runtime STATIC src/lib/ps2_iop_host.cpp src/lib/ps2_memory.cpp src/lib/ps2_pad.cpp + src/lib/ps2_rom_device.cpp + src/lib/ps2_vfs.cpp src/lib/ps2_runtime.cpp src/lib/ps2_vif1_interpreter.cpp src/lib/vu/ps2_vu1_core.cpp diff --git a/ps2xRuntime/include/ps2_call_list.h b/ps2xRuntime/include/ps2_call_list.h index 9c03db2..94a6fa1 100644 --- a/ps2xRuntime/include/ps2_call_list.h +++ b/ps2xRuntime/include/ps2_call_list.h @@ -120,7 +120,6 @@ X(SetOsdConfigParam2) \ X(EnableCache) \ X(DisableCache) \ - X(GetRomName) \ X(SifLoadElfPart) \ X(sceSifLoadElf) \ X(sceSifLoadElfPart) \ diff --git a/ps2xRuntime/include/ps2_runtime.h b/ps2xRuntime/include/ps2_runtime.h index d365cb0..4b9242c 100644 --- a/ps2xRuntime/include/ps2_runtime.h +++ b/ps2xRuntime/include/ps2_runtime.h @@ -5,6 +5,7 @@ #include #include #include +#include #include #if defined(_MSC_VER) #include @@ -30,6 +31,8 @@ #include "runtime/ps2_vu1.h" #include "runtime/ps2_audio.h" #include "runtime/ps2_pad.h" +#include "runtime/ps2_rom_device.h" +#include "runtime/ps2_vfs.h" #include "ps2x/iop/iop_types.h" namespace ps2x::iop @@ -287,8 +290,16 @@ public: bool loadELF(const std::string &elfPath); void run(); - void setIopPluginSearchPaths(std::vector paths); + [[nodiscard]] ps2x::iop::ModuleLoadResult loadIopModule(std::string_view path, const void *arguments = nullptr, uint32_t argumentSize = 0); + [[nodiscard]] ps2x::iop::ModuleLoadResult loadIopModuleBuffer(uint32_t guestAddress, const void *arguments = nullptr, uint32_t argumentSize = 0); + [[nodiscard]] bool stopIopModule(int32_t moduleId, int32_t *result = nullptr); [[nodiscard]] ps2x::iop::DebugSnapshot iopDebugSnapshot() const; + uint32_t allocateIopMemory(uint32_t size, uint32_t alignment = 16u); + bool freeIopMemory(uint32_t address); + bool readIopMemory(uint32_t address, void *destination, size_t size) const; + bool writeIopMemory(uint32_t address, const void *source, size_t size); + bool zeroIopMemory(uint32_t address, size_t size); + bool isIopMemoryRange(uint32_t address, size_t size) const; using DebugUiCallback = void (*)(PS2Runtime &runtime, void *userData); void setDebugUiCallbacks(DebugUiCallback initCallback, @@ -440,6 +451,10 @@ public: inline const PS2AudioBackend &audioBackend() const { return m_audioBackend; } inline PSPadBackend &padBackend() { return m_padBackend; } inline const PSPadBackend &padBackend() const { return m_padBackend; } + inline PS2RomDevice &romDevice() { return m_romDevice; } + inline const PS2RomDevice &romDevice() const { return m_romDevice; } + inline PS2Vfs &vfs() { return m_vfs; } + inline const PS2Vfs &vfs() const { return m_vfs; } private: struct GuestHeapBlock @@ -463,8 +478,10 @@ private: void HandleIntegerOverflow(R5900Context *ctx); [[nodiscard]] ps2x::iop::RpcAbi selectIopRpcAbi(const ps2x::iop::RpcAbiRequest &request) const; + [[nodiscard]] bool canBindIopRpc(uint32_t sid) const noexcept; [[nodiscard]] ps2x::iop::RpcResult handleIopRpc(uint8_t *rdram, R5900Context *ctx, ps2x::iop::RpcRequest request); void notifyIopSifTransfer(uint8_t *rdram, const ps2x::iop::SifTransfer &transfer); + void advanceIopEeCycles(uint64_t eeCycles) noexcept; void resetIop(); friend class PS2IopTransport; @@ -478,6 +495,8 @@ private: std::unique_ptr m_iopSubsystem; PS2AudioBackend m_audioBackend; PSPadBackend m_padBackend; + PS2RomDevice m_romDevice; + PS2Vfs m_vfs; VU1Interpreter m_vu0{VU1Interpreter::Unit::VU0}; VU1Interpreter m_vu1{VU1Interpreter::Unit::VU1}; R5900Context m_cpuContext; diff --git a/ps2xRuntime/include/ps2_runtime_macros.h b/ps2xRuntime/include/ps2_runtime_macros.h index f7b3316..0f19c9f 100644 --- a/ps2xRuntime/include/ps2_runtime_macros.h +++ b/ps2xRuntime/include/ps2_runtime_macros.h @@ -758,6 +758,17 @@ static inline void Ps2SetGprLow64(R5900Context *ctx, int reg, __m128i new_low) } \ } while (0) + +#define SET_GPR_ZE32(ctx_ptr, reg_idx, val) \ + do \ + { \ + if ((reg_idx) != 0) \ + { \ + __m128i _newVal = _mm_cvtsi64_si128((int64_t)(uint32_t)(val)); \ + Ps2SetGprLow64(ctx_ptr, reg_idx, _newVal); \ + } \ + } while (0) + #define SET_GPR_S32(ctx_ptr, reg_idx, val) \ do \ { \ diff --git a/ps2xRuntime/include/ps2_syscalls.h b/ps2xRuntime/include/ps2_syscalls.h index 0360e60..e0c4f5b 100644 --- a/ps2xRuntime/include/ps2_syscalls.h +++ b/ps2xRuntime/include/ps2_syscalls.h @@ -11,8 +11,6 @@ std::string translatePs2Path(const char *ps2Path); -inline std::mutex g_sys_fd_mutex; - namespace ps2_syscalls { #define PS2_DECLARE_SYSCALL(name) void name(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); diff --git a/ps2xRuntime/include/runtime/ee_scheduler.h b/ps2xRuntime/include/runtime/ee_scheduler.h index fae8b85..f247ff9 100644 --- a/ps2xRuntime/include/runtime/ee_scheduler.h +++ b/ps2xRuntime/include/runtime/ee_scheduler.h @@ -90,6 +90,7 @@ enum class GuestInvocationKind : uint8_t SyscallOverride, ExitHandler, HleCall, + SifCommand, }; struct GuestInvocation @@ -181,6 +182,9 @@ struct EeThreadSnapshot { int id = 0; uint32_t pc = 0; + uint32_t ra = 0; + uint32_t sp = 0; + uint32_t contextGp = 0; uint32_t entry = 0; uint32_t stack = 0; uint32_t stackSize = 0; @@ -192,6 +196,7 @@ struct EeThreadSnapshot int waitId = 0; int suspendCount = 0; uint32_t wakeupCount = 0; + uint32_t invocationDepth = 0; }; struct EeSemaphoreSnapshot @@ -390,6 +395,8 @@ private: [[nodiscard]] bool hasReadyAtOrAbovePriority(int priority) const; void renewTimeSlice(); void copyMainContextToRuntime(); + void publishDebugContext(const R5900Context &context); + void publishIdleDebugContext(); PS2Runtime &m_runtime; uint8_t *m_rdram = nullptr; diff --git a/ps2xRuntime/include/runtime/gs/gs_backend.h b/ps2xRuntime/include/runtime/gs/gs_backend.h index 9419cc2..3f00d01 100644 --- a/ps2xRuntime/include/runtime/gs/gs_backend.h +++ b/ps2xRuntime/include/runtime/gs/gs_backend.h @@ -14,6 +14,7 @@ public: virtual void Reset() = 0; virtual void Submit(const GSPrimitiveBatch &batch) = 0; + virtual void LoadClut(const GSTex0Reg &tex0, const GSTexClutReg &texclut) = 0; virtual void BeginTransfer(const GSTransferCommand &command) = 0; virtual void UploadImage(const uint8_t *data, uint32_t sizeBytes) = 0; diff --git a/ps2xRuntime/include/runtime/gs/gs_cpu_backend.h b/ps2xRuntime/include/runtime/gs/gs_cpu_backend.h index 71e8032..2421f9e 100644 --- a/ps2xRuntime/include/runtime/gs/gs_cpu_backend.h +++ b/ps2xRuntime/include/runtime/gs/gs_cpu_backend.h @@ -1,9 +1,9 @@ #pragma once #include "runtime/gs/gs_backend.h" +#include "runtime/gs/gs_texture_page_cache.h" #include -#include #include #include @@ -16,6 +16,7 @@ public: void Reset() override; void Submit(const GSPrimitiveBatch &batch) override; + void LoadClut(const GSTex0Reg &tex0, const GSTexClutReg &texclut) override; void BeginTransfer(const GSTransferCommand &command) override; void UploadImage(const uint8_t *data, uint32_t sizeBytes) override; @@ -34,7 +35,9 @@ public: private: void ResetUnlocked(); + void LoadClutUnlocked(const GSTex0Reg &tex0, const GSTexClutReg &texclut); uint32_t ReadVramUnlocked(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y) const; + uint32_t ReadTextureVramUnlocked(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y); void WriteVramUnlocked(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y, uint32_t value); void DrawPrimitive(const GSPrimitiveBatch &batch); @@ -43,7 +46,7 @@ private: void DrawLine(const GSPrimitiveBatch &batch); void WritePixel(const GSDrawState &state, int x, int y, int z, uint8_t r, uint8_t g, uint8_t b, uint8_t a, uint8_t fog); uint32_t SampleTexture(const GSDrawState &state, float s, float t, float q, uint16_t u, uint16_t v); - uint32_t LookupCLUT(const GSDrawState &state, uint8_t index, uint32_t cbp, uint8_t cpsm, uint8_t csm, uint8_t csa, uint8_t sourcePsm); + uint32_t LookupCLUT(const GSDrawState &state, uint8_t index, uint8_t cpsm, uint8_t csm, uint8_t csa, uint8_t sourcePsm); void PerformLocalToLocalTransfer(); void PerformLocalToHostTransfer(); @@ -58,8 +61,8 @@ private: uint32_t sourceOriginX, uint32_t sourceOriginY) const; - using WriteVramFunc = std::function; - using ReadVramFunc = std::function; + using WriteVramFunc = void (*)(uint8_t *, uint32_t, uint32_t, uint32_t, uint32_t, uint32_t); + using ReadVramFunc = uint32_t (*)(uint8_t *, uint32_t, uint32_t, uint32_t, uint32_t); static constexpr size_t kPsmHandlerCount = 1u << 6u; mutable std::mutex m_mutex; @@ -67,6 +70,9 @@ private: uint32_t m_vramSize = 0; std::array m_readVramFuncs{}; std::array m_writeVramFuncs{}; + std::array m_clut{}; + std::array m_clutCbp{}; + GSMem::TexturePageCache m_texturePageCache; GSTransferCommand m_transfer{}; GSTransferSnapshot m_transferState{}; diff --git a/ps2xRuntime/include/runtime/gs/gs_texture_page_cache.h b/ps2xRuntime/include/runtime/gs/gs_texture_page_cache.h new file mode 100644 index 0000000..1cb1540 --- /dev/null +++ b/ps2xRuntime/include/runtime/gs/gs_texture_page_cache.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include +#include +#include + +namespace GSMem +{ + class TexturePageCache + { + public: + static constexpr uint32_t kPageSize = 8192u; + + void Invalidate() noexcept + { + m_pageBase = UINT32_MAX; + } + + // byteAddress is the wrapped, swizzled VRAM address. The returned + // pointer is valid only until the next miss or invalidation. + const uint8_t* Resolve(const uint8_t* vram, uint32_t byteAddress) noexcept + { + const uint32_t pageBase = byteAddress & ~(kPageSize - 1u); + if (m_pageBase != pageBase) + { + std::memcpy(m_bytes.data(), vram + pageBase, kPageSize); + m_pageBase = pageBase; + } + return m_bytes.data() + (byteAddress & (kPageSize - 1u)); + } + + private: + alignas(64) std::array m_bytes{}; + uint32_t m_pageBase = UINT32_MAX; + }; +} diff --git a/ps2xRuntime/include/runtime/gs/ps2_gs_memory.h b/ps2xRuntime/include/runtime/gs/ps2_gs_memory.h index 095e4e5..c784ff7 100644 --- a/ps2xRuntime/include/runtime/gs/ps2_gs_memory.h +++ b/ps2xRuntime/include/runtime/gs/ps2_gs_memory.h @@ -7,11 +7,12 @@ #include #include "types.h" +#include "runtime/gs/gs_texture_page_cache.h" namespace GSMem { constexpr usz MEMORY_SIZE = 4_mb; - constexpr usz GS_PAGE_SIZE = 8_kb; + constexpr usz GS_PAGE_SIZE = TexturePageCache::kPageSize; // these are all the same regardless of storage mode constexpr usz BLOCKS_PER_PAGE = 32; @@ -261,7 +262,7 @@ namespace GSMem static constexpr void Write(const PageLookupTableT& table, u8* data, u32 block, u32 bw, u32 x, u32 y, PackedT value); // reads the pixel - static constexpr auto Read(const PageLookupTableT& table, u8* data, u32 block, u32 bw, u32 x, u32 y) -> PackedT; + static constexpr auto Read(const PageLookupTableT& table, const u8* data, u32 block, u32 bw, u32 x, u32 y, TexturePageCache* cache = nullptr) -> PackedT; static_assert(BlocksPerPage() == BLOCKS_PER_PAGE); static_assert(IsValidPsm(psm)); @@ -501,15 +502,16 @@ namespace GSMem } template - constexpr auto PixelStorageTraits::Read(const PageLookupTableT& table, u8* data, u32 block, u32 bw, u32 x, u32 y) -> PackedT + constexpr auto PixelStorageTraits::Read(const PageLookupTableT& table, const u8* data, u32 block, u32 bw, u32 x, u32 y, TexturePageCache* cache) -> PackedT { const u32 pixel_addr = Address(table, block, bw, x, y); const u32 bits = pixel_addr * UnpackedBitWidth(psm) + BitOffset(); const u32 byte_addr = (bits / 8) & (MEMORY_SIZE - sizeof(PackedT)); const u32 shift = bits % 8; + const u8* source = cache ? cache->Resolve(data, byte_addr) : data + byte_addr; PackedT v; - std::memcpy(&v, &data[byte_addr], sizeof(PackedT)); + std::memcpy(&v, source, sizeof(PackedT)); switch (psm) { @@ -533,11 +535,14 @@ namespace GSMem break; } - return 0xFFFF00FFu; + return static_cast(0xFFFF00FFu); } void InitLookupTables(); + // Shares swizzle, VRAM wrapping, and lane extraction with the direct reads. + u32 ReadTexture(TexturePageCache& cache, const u8* data, u32 psm, u32 bp, u32 bw, u32 x, u32 y); + void WriteCT32(u8* data, u32 bp, u32 bw, u32 x, u32 y, u32 value); void WriteZ32(u8* data, u32 bp, u32 bw, u32 x, u32 y, u32 value); diff --git a/ps2xRuntime/include/runtime/ps2_memory.h b/ps2xRuntime/include/runtime/ps2_memory.h index cea5b98..ea86a9c 100644 --- a/ps2xRuntime/include/runtime/ps2_memory.h +++ b/ps2xRuntime/include/runtime/ps2_memory.h @@ -449,6 +449,8 @@ public: }; std::array m_eeTimers{}; + bool tryProcessScratchpadDma(uint32_t channelBase, uint32_t chcr); + void completeDmacChannel(uint32_t channelBase, uint32_t cause); void queueCompletedDmacCause(uint32_t cause); }; diff --git a/ps2xRuntime/include/runtime/ps2_rom_device.h b/ps2xRuntime/include/runtime/ps2_rom_device.h new file mode 100644 index 0000000..d205f6a --- /dev/null +++ b/ps2xRuntime/include/runtime/ps2_rom_device.h @@ -0,0 +1,43 @@ +#pragma once + +#include "ps2x/iop/iop_types.h" + +#include +#include +#include +#include +#include +#include +#include + +struct PS2RomProfile +{ + std::string id; + std::string provider = "application"; + ps2x::iop::GameMatcher matcher; + std::unordered_map> files; +}; + +class PS2RomDevice +{ +public: + PS2RomDevice(); + + static void registerProfile(PS2RomProfile profile); + + bool configure(const ps2x::iop::GameIdentity &identity, std::string *error = nullptr); + [[nodiscard]] bool readFile(std::string_view ps2Path, std::vector &bytes) const; + [[nodiscard]] bool fileSize(std::string_view ps2Path, uint64_t &size) const; + [[nodiscard]] bool contains(std::string_view ps2Path) const; + [[nodiscard]] std::string_view activeProfile() const noexcept { return m_activeProfile; } + [[nodiscard]] std::string_view activeProvider() const noexcept { return m_activeProvider; } + +private: + static std::string normalizePath(std::string_view path); + void mountBaseProfile(); + void mountFiles(const std::unordered_map> &files); + + std::unordered_map> m_files; + std::string m_activeProfile; + std::string m_activeProvider; +}; diff --git a/ps2xRuntime/include/runtime/ps2_vfs.h b/ps2xRuntime/include/runtime/ps2_vfs.h new file mode 100644 index 0000000..95b0bdc --- /dev/null +++ b/ps2xRuntime/include/runtime/ps2_vfs.h @@ -0,0 +1,82 @@ +#pragma once + +#include "ps2x/iop/ps2_path.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class PS2RomDevice; + +struct PS2VfsMounts +{ + std::filesystem::path hostRoot; + std::filesystem::path cdRoot; + std::filesystem::path memoryCard0Root; +}; + +struct PS2VfsStat +{ + bool directory = false; + bool readOnly = false; + uint64_t size = 0u; + std::time_t created = 0; + std::time_t accessed = 0; + std::time_t modified = 0; +}; + +struct PS2VfsDescriptorInfo +{ + int32_t descriptor = -1; + std::string device; + std::string path; +}; + +class IPS2OpenFile +{ +public: + virtual ~IPS2OpenFile() = default; + + [[nodiscard]] virtual int64_t read(void *destination, size_t size) = 0; + [[nodiscard]] virtual int64_t write(const void *source, size_t size) = 0; + [[nodiscard]] virtual int64_t seek(int64_t offset, int whence) = 0; +}; + +class PS2Vfs +{ +public: + PS2Vfs() = default; + ~PS2Vfs(); + + PS2Vfs(const PS2Vfs &) = delete; + PS2Vfs &operator=(const PS2Vfs &) = delete; + + [[nodiscard]] int32_t open(std::string_view path, uint32_t flags, const PS2VfsMounts &mounts, const PS2RomDevice &rom); + [[nodiscard]] int32_t close(int32_t descriptor); + [[nodiscard]] int64_t read(int32_t descriptor, void *destination, size_t size); + [[nodiscard]] int64_t write(int32_t descriptor, const void *source, size_t size); + [[nodiscard]] int64_t seek(int32_t descriptor, int64_t offset, int whence); + + [[nodiscard]] bool stat(std::string_view path, const PS2VfsMounts &mounts, const PS2RomDevice &rom, PS2VfsStat &result) const; + [[nodiscard]] bool resolveHostPath(std::string_view path, const PS2VfsMounts &mounts, std::filesystem::path &result) const; + [[nodiscard]] std::vector descriptors() const; + +private: + struct OpenDescriptor + { + std::unique_ptr file; + std::string device; + std::string path; + }; + + mutable std::mutex m_mutex; + std::unordered_map m_descriptors; + int32_t m_nextDescriptor = 3; +}; diff --git a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp index 3a6ec7d..c5e31c4 100644 --- a/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp +++ b/ps2xRuntime/src/lib/Kernel/EeScheduler.cpp @@ -170,6 +170,8 @@ void EeScheduler::run() GuestThread *next = selectReady(); if (!next && m_pendingInvocations.empty()) { + copyMainContextToRuntime(); + publishIdleDebugContext(); publishSnapshot(); waitForEvent(); continue; @@ -224,10 +226,7 @@ void EeScheduler::run() --m_debugPublishCountdown; } - m_runtime.m_debugPc.store(context.pc, std::memory_order_relaxed); - m_runtime.m_debugRa.store(getRegU32(&context, 31), std::memory_order_relaxed); - m_runtime.m_debugSp.store(getRegU32(&context, 29), std::memory_order_relaxed); - m_runtime.m_debugGp.store(getRegU32(&context, 28), std::memory_order_relaxed); + publishDebugContext(context); if (context.pc == 0u) { @@ -249,10 +248,13 @@ void EeScheduler::run() } makeDormant(*running); m_currentThreadId = 0; + copyMainContextToRuntime(); + publishIdleDebugContext(); + publishSnapshot(); continue; } - if (!m_pendingInvocations.empty()) + if (!m_pendingInvocations.empty() && running->invocations.empty()) { GuestInvocation invocation = std::move(m_pendingInvocations.front()); m_pendingInvocations.pop_front(); @@ -391,6 +393,7 @@ void EeScheduler::accountCycles(uint32_t cycles) noexcept const uint64_t elapsed = std::max(1u, cycles); m_eeCycle += elapsed; m_pendingEeTimerInterrupts |= m_runtime.memory().advanceEeTimers(elapsed); + m_runtime.advanceIopEeCycles(elapsed); if (m_pendingEeTimerInterrupts != 0u) { m_checkpointPending.store(true, std::memory_order_release); @@ -1278,7 +1281,7 @@ void EeScheduler::dispatchIrq(bool dmac, uint32_t cause) SET_GPR_U32(&invocation.context, 4, cause); SET_GPR_U32(&invocation.context, 5, handler.argument); SET_GPR_U32(&invocation.context, 28, handler.gp); - SET_GPR_U32(&invocation.context, 29, handler.sp); + SET_GPR_U32(&invocation.context, 29, 0u); SET_GPR_U32(&invocation.context, 31, 0u); queueInvocation(std::move(invocation)); } @@ -1496,7 +1499,11 @@ void EeScheduler::publishSnapshot() } EeThreadSnapshot snapshot{}; snapshot.id = id; - snapshot.pc = item.activeContext().pc; + const R5900Context &context = item.activeContext(); + snapshot.pc = context.pc; + snapshot.ra = getRegU32(&context, 31); + snapshot.sp = getRegU32(&context, 29); + snapshot.contextGp = getRegU32(&context, 28); snapshot.entry = item.entry; snapshot.stack = item.stack; snapshot.stackSize = item.stackSize; @@ -1508,6 +1515,7 @@ void EeScheduler::publishSnapshot() snapshot.waitId = waitObjectId(item.wait); snapshot.suspendCount = item.suspendCount; snapshot.wakeupCount = item.wakeupCount; + snapshot.invocationDepth = static_cast(item.invocations.size()); next.threads.push_back(snapshot); } std::sort(next.threads.begin(), next.threads.end(), [](const auto &left, const auto &right) @@ -1918,7 +1926,7 @@ void EeScheduler::processEvent(const EeEvent &event) SET_GPR_U32(&invocation.context, 5, static_cast(alarm.ticks)); SET_GPR_U32(&invocation.context, 6, alarm.argument); SET_GPR_U32(&invocation.context, 28, alarm.gp); - SET_GPR_U32(&invocation.context, 29, alarm.sp); + SET_GPR_U32(&invocation.context, 29, 0u); SET_GPR_U32(&invocation.context, 31, 0u); queueInvocation(std::move(invocation)); break; @@ -2104,3 +2112,35 @@ void EeScheduler::copyMainContextToRuntime() m_runtime.m_cpuContext = main->context; } } + +void EeScheduler::publishDebugContext(const R5900Context &context) +{ + m_runtime.m_debugPc.store(context.pc, std::memory_order_relaxed); + m_runtime.m_debugRa.store(getRegU32(&context, 31), std::memory_order_relaxed); + m_runtime.m_debugSp.store(getRegU32(&context, 29), std::memory_order_relaxed); + m_runtime.m_debugGp.store(getRegU32(&context, 28), std::memory_order_relaxed); +} + +void EeScheduler::publishIdleDebugContext() +{ + // Temporary IRQ/RPC/alarm invocations deliberately return to PC=0. Once + // the scheduler is idle, show a real EE thread context instead of leaving + // the debugger pinned to that completed dispatcher frame. + const GuestThread *selected = thread(kMainThreadId); + if (!selected) + { + for (const auto &[id, candidate] : m_threads) + { + if (id > 0 && candidate.status != EeThreadStatus::Dormant) + { + selected = &candidate; + break; + } + } + } + + if (selected) + { + publishDebugContext(selected->activeContext()); + } +} diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h b/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h index a7fae20..b1bb6d8 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/Helpers/Support.h @@ -29,11 +29,6 @@ namespace uint32_t g_cdStreamingEndLbn = 0xFFFFFFFFu; bool g_cdInitialized = false; - constexpr uint32_t kIopHeapBase = 0x04000000; - constexpr uint32_t kIopHeapLimit = 0x04500000; - constexpr uint32_t kIopHeapAlign = 64; - uint32_t g_iopHeapNext = kIopHeapBase; - std::string toLowerAscii(std::string value) { std::transform(value.begin(), value.end(), value.begin(), @@ -1360,7 +1355,11 @@ namespace uint32_t madr = 0; uint32_t qwc = 0; uint32_t tadr = payloadPhys; - uint32_t chcr = 0x00000181u; // DIR=1, TIE=1, STR=1 (normal mode). + PS2Memory &mem = runtime->memory(); + + const uint32_t configuredChcr = mem.readIORegister(channelBase + 0x00u); + const uint32_t transferTagEnable = configuredChcr & 0x00000040u; + uint32_t chcr = 0x00000181u | transferTagEnable; // DIR=1, TIE=1, STR=1 (normal mode). if (preferNormalCount) { @@ -1369,10 +1368,9 @@ namespace } else { - chcr = 0x00000185u; // MODE=1 chain, DIR=1, TIE=1, STR=1. + chcr = 0x00000185u | transferTagEnable; // MODE=1 chain, DIR=1, TIE=1, STR=1. } - PS2Memory &mem = runtime->memory(); mem.writeIORegister(channelBase + 0x20u, qwc & 0xFFFFu); mem.writeIORegister(channelBase + 0x10u, madr); mem.writeIORegister(channelBase + 0x30u, tadr); @@ -1402,10 +1400,10 @@ namespace if (g_dmaStubLogCount < kMaxDmaStubLogs) { RUNTIME_LOG("[sceDmaSend] ch=0x" << std::hex << channelBase - << " madr=0x" << madr - << " qwc=0x" << qwc - << " tadr=0x" << tadr - << " chcr=0x" << chcr << std::dec << std::endl); + << " madr=0x" << madr + << " qwc=0x" << qwc + << " tadr=0x" << tadr + << " chcr=0x" << chcr << std::dec << std::endl); if (!preferNormalCount && (channelBase == 0x10009000u || channelBase == 0x1000A000u)) { @@ -1418,13 +1416,13 @@ namespace std::memcpy(&w2, tagPtr + 8u, sizeof(w2)); std::memcpy(&w3, tagPtr + 12u, sizeof(w3)); RUNTIME_LOG("[sceDmaSend:head] ch=0x" << std::hex << channelBase - << " tagQwc=0x" << static_cast(tagLo & 0xFFFFu) - << " id=0x" << static_cast((tagLo >> 28u) & 0x7u) - << " irq=0x" << static_cast((tagLo >> 31u) & 0x1u) - << " addr=0x" << static_cast((tagLo >> 32u) & 0x7FFFFFFFu) - << " w2=0x" << w2 - << " w3=0x" << w3 - << std::dec << std::endl); + << " tagQwc=0x" << static_cast(tagLo & 0xFFFFu) + << " id=0x" << static_cast((tagLo >> 28u) & 0x7u) + << " irq=0x" << static_cast((tagLo >> 31u) & 0x1u) + << " addr=0x" << static_cast((tagLo >> 32u) & 0x7FFFFFFFu) + << " w2=0x" << w2 + << " w3=0x" << w3 + << std::dec << std::endl); } } ++g_dmaStubLogCount; @@ -1838,9 +1836,9 @@ namespace return true; } - static bool readGsDBuff(uint8_t* rdram, uint32_t addr, GsDBuffMem& out) + static bool readGsDBuff(uint8_t *rdram, uint32_t addr, GsDBuffMem &out) { - const uint8_t* ptr = getConstMemPtr(rdram, addr); + const uint8_t *ptr = getConstMemPtr(rdram, addr); if (!ptr) return false; std::memcpy(&out, ptr, sizeof(out)); @@ -1856,9 +1854,9 @@ namespace return true; } - static bool writeGsDBuff(uint8_t* rdram, uint32_t addr, const GsDBuffMem& db) + static bool writeGsDBuff(uint8_t *rdram, uint32_t addr, const GsDBuffMem &db) { - uint8_t* ptr = getMemPtr(rdram, addr); + uint8_t *ptr = getMemPtr(rdram, addr); if (!ptr) return false; std::memcpy(ptr, &db, sizeof(db)); diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp b/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp index 6a07fe2..dc975ed 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp +++ b/ps2xRuntime/src/lib/Kernel/Stubs/SIF.cpp @@ -3,10 +3,11 @@ #include "../Syscalls/RPC.h" #include "../../ps2_iop_transport.h" #include "runtime/ps2_address.h" +#include "runtime/ee_scheduler.h" #include +#include #include -#include #include namespace ps2_stubs @@ -28,15 +29,22 @@ namespace ps2_stubs const uint32_t size = readStackU32(rdram, ctx, 20); if (size != 0u && srcAddr != 0u && dstAddr != 0u) { + std::vector payload(size); + bool valid = runtime != nullptr; for (uint32_t i = 0; i < size; ++i) { const uint8_t *src = getConstMemPtr(rdram, srcAddr + i); - uint8_t *dst = getMemPtr(rdram, dstAddr + i); - if (!src || !dst) + if (!src) { + valid = false; break; } - *dst = *src; + payload[i] = *src; + } + if (!valid || !runtime->writeIopMemory(dstAddr, payload.data(), payload.size())) + { + setReturnS32(ctx, 0); + return; } } @@ -57,12 +65,15 @@ namespace ps2_stubs std::mutex g_sifDmaTransferMutex; uint32_t g_nextSifDmaTransferId = 1u; std::mutex g_sifCmdStateMutex; - std::mutex g_sifHeapMutex; std::unordered_map g_sifRegs; std::unordered_map g_sifSregs; - std::unordered_map g_sifCmdHandlers; - std::map g_sifHeapAllocations; - std::array g_sifHeapStorage{}; + struct SifCmdHandler + { + uint32_t function = 0u; + uint32_t argument = 0u; + }; + + std::unordered_map g_sifCmdHandlers; uint32_t g_sifCmdBuffer = 0u; uint32_t g_sifSysCmdBuffer = 0u; bool g_sifCmdInitialized = false; @@ -127,92 +138,6 @@ namespace ps2_stubs return id; } - uint32_t alignIopHeapSize(uint32_t size) - { - return (size + (kIopHeapAlign - 1u)) & ~(kIopHeapAlign - 1u); - } - - uint32_t allocateSifHeapBlock(uint32_t requestSize) - { - const uint32_t alignedSize = alignIopHeapSize(requestSize); - if (alignedSize == 0u) - { - return 0u; - } - - std::lock_guard lock(g_sifHeapMutex); - uint32_t candidate = kIopHeapBase; - for (const auto &[addr, size] : g_sifHeapAllocations) - { - if (candidate + alignedSize <= addr) - { - break; - } - - const uint32_t blockEnd = alignIopHeapSize(addr + size); - if (blockEnd > candidate) - { - candidate = blockEnd; - } - } - - if (candidate < kIopHeapBase || candidate + alignedSize > kIopHeapLimit) - { - return 0u; - } - - g_sifHeapAllocations[candidate] = alignedSize; - std::fill_n(g_sifHeapStorage.data() + (candidate - kIopHeapBase), - alignedSize, - uint8_t{0}); - g_iopHeapNext = candidate + alignedSize; - return candidate; - } - - bool freeSifHeapBlock(uint32_t addr) - { - std::lock_guard lock(g_sifHeapMutex); - const auto it = g_sifHeapAllocations.find(addr); - if (it == g_sifHeapAllocations.end()) - { - return false; - } - - g_sifHeapAllocations.erase(it); - if (g_sifHeapAllocations.empty()) - { - g_iopHeapNext = kIopHeapBase; - } - return true; - } - - void resetSifHeapState() - { - std::lock_guard lock(g_sifHeapMutex); - g_sifHeapAllocations.clear(); - g_sifHeapStorage.fill(0u); - g_iopHeapNext = kIopHeapBase; - } - - bool isAllocatedSifHeapRangeLocked(uint32_t address, size_t size) - { - if (address < kIopHeapBase || address >= kIopHeapLimit || size > static_cast(kIopHeapLimit - address)) - { - return false; - } - - auto it = g_sifHeapAllocations.upper_bound(address); - if (it == g_sifHeapAllocations.begin()) - { - return false; - } - --it; - - const uint64_t allocationEnd = static_cast(it->first) + it->second; - const uint64_t rangeEnd = static_cast(address) + size; - return address >= it->first && rangeEnd <= allocationEnd; - } - bool isCopyableGuestAddress(uint32_t addr) { if (Ps2AddressInRange(addr, PS2_SCRATCHPAD_BASE, PS2_SCRATCHPAD_SIZE)) @@ -238,13 +163,9 @@ namespace ps2_stubs return false; } - bool canCopyAddressRange(const uint8_t *rdram, uint32_t address, uint32_t sizeBytes) + bool canAccessEeRange(const uint8_t *rdram, uint32_t address, uint32_t sizeBytes) { - if (isSifIopHeapRange(address, sizeBytes)) - { - return true; - } - if (isSifIopHeapAddress(address) || !rdram) + if (!rdram) { return false; } @@ -259,7 +180,7 @@ namespace ps2_stubs for (uint32_t i = 0u; i < sizeBytes; ++i) { const uint32_t byteAddress = address + i; - if (!isCopyableGuestAddress(byteAddress) ||getConstMemPtr(rdram, byteAddress) == nullptr) + if (!isCopyableGuestAddress(byteAddress) || getConstMemPtr(rdram, byteAddress) == nullptr) { return false; } @@ -267,200 +188,129 @@ namespace ps2_stubs return true; } - bool canCopyGuestByteRange(const uint8_t *rdram, uint32_t dstAddr, uint32_t srcAddr, uint32_t sizeBytes) + bool readEeRange(const uint8_t *rdram, uint32_t address, void *destination, uint32_t sizeBytes) { - return canCopyAddressRange(rdram, srcAddr, sizeBytes) && canCopyAddressRange(rdram, dstAddr, sizeBytes); - } - - bool copyGuestByteRange(uint8_t *rdram, uint32_t dstAddr, uint32_t srcAddr, uint32_t sizeBytes) - { - if (!canCopyGuestByteRange(rdram, dstAddr, srcAddr, sizeBytes)) - { + if ((!destination && sizeBytes != 0u) || !canAccessEeRange(rdram, address, sizeBytes)) return false; - } - - if (sizeBytes == 0u) + auto *bytes = static_cast(destination); + for (uint32_t i = 0u; i < sizeBytes; ++i) { - return true; - } - - const bool sourceIsIop = isSifIopHeapRange(srcAddr, sizeBytes); - const bool destinationIsIop = isSifIopHeapRange(dstAddr, sizeBytes); - if (sourceIsIop || destinationIsIop) - { - std::vector payload(sizeBytes); - if (sourceIsIop) - { - if (!readSifIopHeap(srcAddr, payload.data(), payload.size())) - { - return false; - } - } - else - { - for (uint32_t i = 0u; i < sizeBytes; ++i) - { - const uint8_t *src = getConstMemPtr(rdram, srcAddr + i); - if (!src) - { - return false; - } - payload[i] = *src; - } - } - - if (destinationIsIop) - { - return writeSifIopHeap(dstAddr, payload.data(), payload.size()); - } - - ps2TraceGuestRangeWrite(rdram, dstAddr, sizeBytes, "sifCopyGuestByteRange", nullptr); - for (uint32_t i = 0u; i < sizeBytes; ++i) - { - uint8_t *dst = getMemPtr(rdram, dstAddr + i); - if (!dst) - { - return false; - } - *dst = payload[i]; - } - return true; - } - - ps2TraceGuestRangeWrite(rdram, dstAddr, sizeBytes, "sifCopyGuestByteRange", nullptr); - - const uint64_t srcBegin = srcAddr; - const uint64_t srcEnd = srcBegin + static_cast(sizeBytes); - const uint64_t dstBegin = dstAddr; - const bool copyBackward = (dstBegin > srcBegin) && (dstBegin < srcEnd); - - if (copyBackward) - { - for (uint32_t i = sizeBytes; i > 0u; --i) - { - const uint32_t index = i - 1u; - const uint8_t *src = getConstMemPtr(rdram, srcAddr + index); - uint8_t *dst = getMemPtr(rdram, dstAddr + index); - if (!src || !dst) - { - return false; - } - *dst = *src; - } - return true; - } - - for (uint32_t i = 0; i < sizeBytes; ++i) - { - const uint8_t *src = getConstMemPtr(rdram, srcAddr + i); - uint8_t *dst = getMemPtr(rdram, dstAddr + i); - if (!src || !dst) - { + const uint8_t *source = getConstMemPtr(rdram, address + i); + if (!source) return false; - } - *dst = *src; + bytes[i] = *source; } return true; } - } - bool isSifIopHeapAddress(uint32_t address) - { - return address >= kIopHeapBase && address < kIopHeapLimit; - } - - bool isSifIopHeapRange(uint32_t address, size_t size) - { - std::lock_guard lock(g_sifHeapMutex); - return isAllocatedSifHeapRangeLocked(address, size); - } - - bool readSifIopHeap(uint32_t address, void *destination, size_t size) - { - if (!destination && size != 0u) + bool writeEeRange(uint8_t *rdram, uint32_t address, const void *source, uint32_t sizeBytes) { - return false; + if ((!source && sizeBytes != 0u) || !canAccessEeRange(rdram, address, sizeBytes)) + return false; + ps2TraceGuestRangeWrite(rdram, address, sizeBytes, "SIF IOP-to-EE DMA", nullptr); + const auto *bytes = static_cast(source); + for (uint32_t i = 0u; i < sizeBytes; ++i) + { + uint8_t *destination = getMemPtr(rdram, address + i); + if (!destination) + return false; + *destination = bytes[i]; + } + return true; } - std::lock_guard lock(g_sifHeapMutex); - if (!isAllocatedSifHeapRangeLocked(address, size)) - { - return false; - } - if (size != 0u) - { - std::memcpy(destination, - g_sifHeapStorage.data() + (address - kIopHeapBase), - size); - } - return true; - } - - bool writeSifIopHeap(uint32_t address, const void *source, size_t size) - { - if (!source && size != 0u) - { - return false; - } - std::lock_guard lock(g_sifHeapMutex); - if (!isAllocatedSifHeapRangeLocked(address, size)) - { - return false; - } - if (size != 0u) - { - std::memcpy(g_sifHeapStorage.data() + (address - kIopHeapBase), - source, - size); - } - return true; - } - - bool zeroSifIopHeap(uint32_t address, size_t size) - { - std::lock_guard lock(g_sifHeapMutex); - if (!isAllocatedSifHeapRangeLocked(address, size)) - { - return false; - } - if (size != 0u) - { - std::memset(g_sifHeapStorage.data() + (address - kIopHeapBase), 0, size); - } - return true; } void resetSifState() { std::lock_guard lock(g_sifCmdStateMutex); seedDefaultSifRegsLocked(); - resetSifHeapState(); + } + + bool dispatchSifCommand(uint8_t *rdram, + PS2Runtime *runtime, + uint32_t commandId, + const void *packet, + size_t packetSize) noexcept + { + if (!rdram || !runtime || !packet || packetSize < 16u || packetSize > 112u) + return false; + + SifCmdHandler registered{}; + { + std::lock_guard lock(g_sifCmdStateMutex); + const auto handler = g_sifCmdHandlers.find(commandId); + if (handler == g_sifCmdHandlers.end() || handler->second.function == 0u) + return false; + registered = handler->second; + } + + if (!runtime->hasFunction(registered.function)) + return false; + + const uint32_t packetAddress = runtime->guestMalloc(static_cast(packetSize), 16u); + if (packetAddress == 0u) + return false; + + uint8_t *const first = getMemPtr(rdram, packetAddress); + uint8_t *const last = getMemPtr(rdram, packetAddress + static_cast(packetSize - 1u)); + if (!first || !last || last < first || static_cast(last - first) != packetSize - 1u) + { + runtime->guestFree(packetAddress); + return false; + } + + ps2TraceGuestRangeWrite(rdram, packetAddress, static_cast(packetSize), "SIF command packet", nullptr); + std::memcpy(first, packet, packetSize); + + try + { + GuestInvocation invocation{}; + invocation.kind = GuestInvocationKind::SifCommand; + invocation.tag = commandId; + invocation.context = runtime->cpu(); + invocation.context.pc = registered.function; + SET_GPR_U32(&invocation.context, 4, packetAddress); + SET_GPR_U32(&invocation.context, 5, registered.argument); + SET_GPR_U32(&invocation.context, 6, 0u); + SET_GPR_U32(&invocation.context, 7, 0u); + SET_GPR_U32(&invocation.context, 29, 0u); + SET_GPR_U32(&invocation.context, 31, 0u); + invocation.onComplete = [runtime, packetAddress](const R5900Context &, R5900Context &) + { + runtime->guestFree(packetAddress); + }; + runtime->eeScheduler().queueInvocation(std::move(invocation)); + return true; + } + catch (...) + { + runtime->guestFree(packetAddress); + return false; + } } void sceSifAddCmdHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { const uint32_t cid = getRegU32(ctx, 4); const uint32_t handler = getRegU32(ctx, 5); + const uint32_t argument = getRegU32(ctx, 6); std::lock_guard lock(g_sifCmdStateMutex); - g_sifCmdHandlers[cid] = handler; + g_sifCmdHandlers[cid] = SifCmdHandler{handler, argument}; setReturnS32(ctx, 0); } void sceSifAllocIopHeap(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { (void)rdram; - (void)runtime; - const uint32_t reqSize = getRegU32(ctx, 4); - setReturnU32(ctx, allocateSifHeapBlock(reqSize)); + setReturnU32(ctx, runtime ? runtime->allocateIopMemory(reqSize, 64u) : 0u); } void sceSifAllocSysMemory(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { (void)rdram; - (void)runtime; - const uint32_t size = getRegU32(ctx, 5); - setReturnU32(ctx, allocateSifHeapBlock(size)); + setReturnU32(ctx, runtime ? runtime->allocateIopMemory(size, 64u) : 0u); } void sceSifBindRpc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -503,19 +353,15 @@ namespace ps2_stubs void sceSifFreeIopHeap(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { (void)rdram; - (void)runtime; - const uint32_t addr = getRegU32(ctx, 4); - setReturnS32(ctx, freeSifHeapBlock(addr) ? 0 : -1); + setReturnS32(ctx, runtime && runtime->freeIopMemory(addr) ? 0 : -1); } void sceSifFreeSysMemory(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { (void)rdram; - (void)runtime; - const uint32_t addr = getRegU32(ctx, 4); - setReturnS32(ctx, freeSifHeapBlock(addr) ? 0 : -1); + setReturnS32(ctx, runtime && runtime->freeIopMemory(addr) ? 0 : -1); } void sceSifGetDataTable(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -564,15 +410,19 @@ namespace ps2_stubs if (runtime) { PS2IopTransport::notifyTransfer(runtime, rdram, { - ps2x::iop::SifTransferKind::GetOtherData, - ps2x::iop::SifTransferPhase::BeforeCopy, - srcAddr, - dstAddr, - size, - }); + ps2x::iop::SifTransferKind::GetOtherData, + ps2x::iop::SifTransferPhase::BeforeCopy, + srcAddr, + dstAddr, + size, + }); } - if (!copyGuestByteRange(rdram, dstAddr, srcAddr, size)) + std::vector payload(size); + if (!runtime || !runtime->isIopMemoryRange(srcAddr, size) || + !canAccessEeRange(rdram, dstAddr, size) || + !runtime->readIopMemory(srcAddr, payload.data(), payload.size()) || + !writeEeRange(rdram, dstAddr, payload.data(), size)) { static uint32_t warnCount = 0; if (warnCount < 32u) @@ -600,12 +450,12 @@ namespace ps2_stubs if (runtime) { PS2IopTransport::notifyTransfer(runtime, rdram, { - ps2x::iop::SifTransferKind::GetOtherData, - ps2x::iop::SifTransferPhase::AfterCopy, - srcAddr, - dstAddr, - size, - }); + ps2x::iop::SifTransferKind::GetOtherData, + ps2x::iop::SifTransferPhase::AfterCopy, + srcAddr, + dstAddr, + size, + }); } setReturnS32(ctx, 0); @@ -668,7 +518,7 @@ namespace ps2_stubs void sceSifInitIopHeap(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - resetSifHeapState(); + // The physical IOP allocator is initialized by IopSubsystem::reset(). setReturnS32(ctx, 0); } @@ -709,6 +559,7 @@ namespace ps2_stubs void sceSifRebootIop(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { + PS2IopTransport::reset(runtime); setReturnS32(ctx, 1); } @@ -737,6 +588,7 @@ namespace ps2_stubs void sceSifResetIop(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { + PS2IopTransport::reset(runtime); setReturnS32(ctx, 1); } @@ -838,7 +690,7 @@ namespace ps2_stubs ok = false; break; } - if (!canCopyGuestByteRange(rdram, xfer.dest, xfer.src, sizeBytes)) + if (!runtime || !canAccessEeRange(rdram, xfer.src, sizeBytes) || !runtime->isIopMemoryRange(xfer.dest, sizeBytes)) { ok = false; break; @@ -855,14 +707,16 @@ namespace ps2_stubs if (runtime) { PS2IopTransport::notifyTransfer(runtime, rdram, { - ps2x::iop::SifTransferKind::SetDma, - ps2x::iop::SifTransferPhase::BeforeCopy, - xfer.src, - xfer.dest, - static_cast(xfer.size), - }); + ps2x::iop::SifTransferKind::SetDma, + ps2x::iop::SifTransferPhase::BeforeCopy, + xfer.src, + xfer.dest, + static_cast(xfer.size), + }); } - if (!copyGuestByteRange(rdram, xfer.dest, xfer.src, static_cast(xfer.size))) + const uint32_t sizeBytes = static_cast(xfer.size); + std::vector payload(sizeBytes); + if (!readEeRange(rdram, xfer.src, payload.data(), sizeBytes) || !runtime->writeIopMemory(xfer.dest, payload.data(), payload.size())) { ok = false; break; @@ -870,12 +724,12 @@ namespace ps2_stubs if (runtime) { PS2IopTransport::notifyTransfer(runtime, rdram, { - ps2x::iop::SifTransferKind::SetDma, - ps2x::iop::SifTransferPhase::AfterCopy, - xfer.src, - xfer.dest, - static_cast(xfer.size), - }); + ps2x::iop::SifTransferKind::SetDma, + ps2x::iop::SifTransferPhase::AfterCopy, + xfer.src, + xfer.dest, + static_cast(xfer.size), + }); } } } diff --git a/ps2xRuntime/src/lib/Kernel/Stubs/SIF.h b/ps2xRuntime/src/lib/Kernel/Stubs/SIF.h index 777e1cb..8673e80 100644 --- a/ps2xRuntime/src/lib/Kernel/Stubs/SIF.h +++ b/ps2xRuntime/src/lib/Kernel/Stubs/SIF.h @@ -2,16 +2,9 @@ #include "ps2_stubs.h" -#include - namespace ps2_stubs { - bool isSifIopHeapAddress(uint32_t address); - bool isSifIopHeapRange(uint32_t address, size_t size); - bool readSifIopHeap(uint32_t address, void *destination, size_t size); - bool writeSifIopHeap(uint32_t address, const void *source, size_t size); - bool zeroSifIopHeap(uint32_t address, size_t size); - + bool dispatchSifCommand(uint8_t *rdram, PS2Runtime *runtime, uint32_t commandId, const void *packet, size_t packetSize) noexcept; void sceSifCmdIntrHdlr(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceSifLoadModule(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceSifSendCmd(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Common.h b/ps2xRuntime/src/lib/Kernel/Syscalls/Common.h index e7362e0..c0b13bf 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Common.h +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Common.h @@ -4,6 +4,7 @@ #include "runtime/ee_scheduler.h" #include "ps2_runtime_macros.h" #include "ps2_stubs.h" +#include "ps2x/iop/ps2_path.h" #include #include #include diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Dispatcher.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/Dispatcher.cpp index 89de596..3b2be31 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Dispatcher.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Dispatcher.cpp @@ -253,6 +253,9 @@ namespace ps2_syscalls case 0x64: FlushCache(rdram, ctx, runtime); return true; + case static_cast(-0x68): + iFlushCache(rdram, ctx, runtime); + return true; case 0x6E: SetOsdConfigParam2(rdram, ctx, runtime); return true; diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/FileIO.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/FileIO.cpp index 60c68c0..ccfca5e 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/FileIO.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/FileIO.cpp @@ -3,32 +3,10 @@ namespace ps2_syscalls { - static int allocatePs2Fd(FILE *file) + static PS2VfsMounts currentVfsMounts() { - if (!file) - return -1; - - std::lock_guard lock(g_fd_mutex); - int fd = g_nextFd++; - g_fileDescriptors[fd] = file; - return fd; - } - - static FILE *getHostFile(int ps2Fd) - { - std::lock_guard lock(g_fd_mutex); - auto it = g_fileDescriptors.find(ps2Fd); - if (it != g_fileDescriptors.end()) - { - return it->second; - } - return nullptr; - } - - static void releasePs2Fd(int ps2Fd) - { - std::lock_guard lock(g_fd_mutex); - g_fileDescriptors.erase(ps2Fd); + const PS2Runtime::IoPaths &paths = PS2Runtime::getIoPaths(); + return {paths.hostRoot, paths.cdRoot, paths.mcRoot}; } struct VagAccumEntry @@ -40,39 +18,6 @@ namespace ps2_syscalls static std::mutex g_vagAccumMutex; static constexpr size_t kVagAccumMaxBytes = 16 * 1024 * 1024; - static const char *translateFioMode(int ps2Flags) - { - bool read = (ps2Flags & PS2_FIO_O_RDONLY) || (ps2Flags & PS2_FIO_O_RDWR); - bool write = (ps2Flags & PS2_FIO_O_WRONLY) || (ps2Flags & PS2_FIO_O_RDWR); - bool append = (ps2Flags & PS2_FIO_O_APPEND); - bool create = (ps2Flags & PS2_FIO_O_CREAT); - bool truncate = (ps2Flags & PS2_FIO_O_TRUNC); - - if (read && write) - { - if (create && truncate) - return "w+b"; - if (create) - return "a+b"; - return "r+b"; - } - else if (write) - { - if (append) - return "ab"; - if (create && truncate) - return "wb"; - if (create) - return "wx"; - return "r+b"; - } - else if (read) - { - return "rb"; - } - return "rb"; - } - void fioOpen(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { uint32_t pathAddr = getRegU32(ctx, 4); // $a0 @@ -86,52 +31,32 @@ namespace ps2_syscalls return; } - std::string hostPath = translatePs2Path(ps2Path); - if (hostPath.empty()) + if (!runtime) { - std::cerr << "fioOpen error: Failed to translate path '" << ps2Path << "'" << std::endl; setReturnS32(ctx, -1); return; } - const char *mode = translateFioMode(flags); - RUNTIME_LOG("fioOpen: '" << hostPath << "' flags=0x" << std::hex << flags << std::dec << " mode='" << mode << "'"); - - FILE *fp = ::fopen(hostPath.c_str(), mode); - if (!fp) - { - std::cerr << "fioOpen error: fopen failed for '" << hostPath << "': " << strerror(errno) << std::endl; - setReturnS32(ctx, -1); // e.g., -ENOENT, -EACCES - return; - } - - int ps2Fd = allocatePs2Fd(fp); - if (ps2Fd < 0) - { - std::cerr << "fioOpen error: Failed to allocate PS2 file descriptor" << std::endl; - ::fclose(fp); - setReturnS32(ctx, -1); // e.g., -EMFILE - return; - } - - // returns the PS2 file descriptor - setReturnS32(ctx, ps2Fd); + const int32_t descriptor = runtime->vfs().open(ps2Path, static_cast(flags), currentVfsMounts(), runtime->romDevice()); + setReturnS32(ctx, descriptor); } void fioClose(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { int ps2Fd = (int)getRegU32(ctx, 4); - FILE *fp = getHostFile(ps2Fd); - if (!fp) + if (!runtime) { - std::cerr << "fioClose warning: Invalid PS2 file descriptor " << ps2Fd << std::endl; setReturnS32(ctx, -1); return; } - int ret = ::fclose(fp); - releasePs2Fd(ps2Fd); + const int32_t ret = runtime->vfs().close(ps2Fd); + if (ret < 0) + { + setReturnS32(ctx, -1); + return; + } { std::lock_guard lock(g_vagAccumMutex); @@ -161,7 +86,7 @@ namespace ps2_syscalls } } - setReturnS32(ctx, ret == 0 ? 0 : -1); + setReturnS32(ctx, 0); } void fioRead(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -171,15 +96,13 @@ namespace ps2_syscalls size_t size = getRegU32(ctx, 6); // $a2 uint8_t *hostBuf = getMemPtr(rdram, bufAddr); - FILE *fp = getHostFile(ps2Fd); - if (!hostBuf) { std::cerr << "fioRead error: Invalid buffer address for fd " << ps2Fd << std::endl; setReturnS32(ctx, -1); // -EFAULT return; } - if (!fp) + if (!runtime) { std::cerr << "fioRead error: Invalid file descriptor " << ps2Fd << std::endl; setReturnS32(ctx, -1); // -EBADF @@ -191,24 +114,18 @@ namespace ps2_syscalls return; } - size_t bytesRead = 0; + const int64_t readResult = runtime->vfs().read(ps2Fd, hostBuf, size); + if (readResult < 0) { - std::lock_guard lock(g_sys_fd_mutex); - bytesRead = fread(hostBuf, 1, size, fp); + setReturnS32(ctx, -1); + return; } + const size_t bytesRead = static_cast(readResult); if (bytesRead > 0) { ps2TraceGuestRangeWrite(rdram, bufAddr, static_cast(bytesRead), "fioRead", ctx); } - if (bytesRead < size && ferror(fp)) - { - std::cerr << "fioRead error: fread failed for fd " << ps2Fd << ": " << strerror(errno) << std::endl; - clearerr(fp); - setReturnS32(ctx, -1); - return; - } - { std::lock_guard lock(g_vagAccumMutex); auto it = g_vagAccum.find(ps2Fd); @@ -254,8 +171,7 @@ namespace ps2_syscalls return; } - FILE *fp = getHostFile(ps2Fd); - if (!fp) + if (!runtime) { setReturnS32(ctx, -1); // -EFAULT return; @@ -267,20 +183,15 @@ namespace ps2_syscalls return; } - size_t bytesWritten = 0; + const int64_t writeResult = runtime->vfs().write(ps2Fd, hostBuf, size); + if (writeResult < 0) { - std::lock_guard lock(g_sys_fd_mutex); - bytesWritten = ::fwrite(hostBuf, 1, size, fp); - if (bytesWritten < size && ferror(fp)) - { - clearerr(fp); - setReturnS32(ctx, -1); // -EIO, -ENOSPC etc. - return; - } + setReturnS32(ctx, -1); + return; } // returns number of bytes written - setReturnS32(ctx, (int32_t)bytesWritten); + setReturnS32(ctx, static_cast(writeResult)); } void fioLseek(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -289,8 +200,7 @@ namespace ps2_syscalls int32_t offset = getRegU32(ctx, 5); // $a1 (PS2 seems to use 32-bit offset here commonly) int whence = (int)getRegU32(ctx, 6); // $a2 (PS2 FIO_SEEK constants) - FILE *fp = getHostFile(ps2Fd); - if (!fp) + if (!runtime) { std::cerr << "fioLseek error: Invalid file descriptor " << ps2Fd << std::endl; setReturnS32(ctx, -1); // -EBADF @@ -315,22 +225,14 @@ namespace ps2_syscalls return; } - if (::fseek(fp, static_cast(offset), hostWhence) != 0) - { - std::cerr << "fioLseek error: fseek failed for fd " << ps2Fd << ": " << strerror(errno) << std::endl; - setReturnS32(ctx, -1); // Return error code - return; - } - - long newPos = ::ftell(fp); + const int64_t newPos = runtime->vfs().seek(ps2Fd, offset, hostWhence); if (newPos < 0) { - std::cerr << "fioLseek error: ftell failed after fseek for fd " << ps2Fd << ": " << strerror(errno) << std::endl; setReturnS32(ctx, -1); } else { - if (newPos > 0xFFFFFFFFL) + if (static_cast(newPos) > 0x7FFFFFFFu) { std::cerr << "fioLseek warning: New position exceeds 32-bit for fd " << ps2Fd << std::endl; setReturnS32(ctx, -1); @@ -354,8 +256,8 @@ namespace ps2_syscalls setReturnS32(ctx, -1); // -EFAULT return; } - std::string hostPath = translatePs2Path(ps2Path); - if (hostPath.empty()) + std::filesystem::path hostPath; + if (!runtime || !runtime->vfs().resolveHostPath(ps2Path, currentVfsMounts(), hostPath)) { std::cerr << "fioMkdir error: Failed to translate path '" << ps2Path << "'" << std::endl; setReturnS32(ctx, -1); @@ -366,13 +268,13 @@ namespace ps2_syscalls if (!success && ec) { - std::cerr << "fioMkdir error: create_directory failed for '" << hostPath + std::cerr << "fioMkdir error: create_directory failed for '" << hostPath.string() << "': " << ec.message() << std::endl; setReturnS32(ctx, -1); } else { - RUNTIME_LOG("fioMkdir: Created directory '" << hostPath << "'"); + RUNTIME_LOG("fioMkdir: Created directory '" << hostPath.string() << "'"); setReturnS32(ctx, 0); // Success } } @@ -388,27 +290,14 @@ namespace ps2_syscalls return; } - std::string hostPath = translatePs2Path(ps2Path); - if (hostPath.empty()) + PS2VfsStat status; + if (!runtime || !runtime->vfs().stat(ps2Path, currentVfsMounts(), runtime->romDevice(), status) || !status.directory) { - std::cerr << "fioChdir error: Failed to translate path '" << ps2Path << "'" << std::endl; - setReturnS32(ctx, -1); - return; - } - - std::error_code ec; - std::filesystem::current_path(hostPath, ec); - - if (ec) - { - std::cerr << "fioChdir error: current_path failed for '" << hostPath - << "': " << ec.message() << std::endl; setReturnS32(ctx, -1); } else { - RUNTIME_LOG("fioChdir: Changed directory to '" << hostPath << "'"); - setReturnS32(ctx, 0); // Success + setReturnS32(ctx, 0); } } @@ -422,8 +311,8 @@ namespace ps2_syscalls setReturnS32(ctx, -1); return; } - std::string hostPath = translatePs2Path(ps2Path); - if (hostPath.empty()) + std::filesystem::path hostPath; + if (!runtime || !runtime->vfs().resolveHostPath(ps2Path, currentVfsMounts(), hostPath)) { std::cerr << "fioRmdir error: Failed to translate path '" << ps2Path << "'" << std::endl; setReturnS32(ctx, -1); @@ -435,20 +324,18 @@ namespace ps2_syscalls if (!success || ec) { - std::cerr << "fioRmdir error: remove failed for '" << hostPath - << "': " << ec.message() << std::endl; + std::cerr << "fioRmdir error: remove failed for '" << hostPath.string() << "': " << ec.message() << std::endl; setReturnS32(ctx, -1); } else { - RUNTIME_LOG("fioRmdir: Removed directory '" << hostPath << "'"); + RUNTIME_LOG("fioRmdir: Removed directory '" << hostPath.string() << "'"); setReturnS32(ctx, 0); // Success } } void fioGetstat(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - // we wont implement this for now. uint32_t pathAddr = getRegU32(ctx, 4); // $a0 uint32_t statBufAddr = getRegU32(ctx, 5); // $a1 @@ -468,15 +355,29 @@ namespace ps2_syscalls return; } - std::string hostPath = translatePs2Path(ps2Path); - if (hostPath.empty()) + if (!runtime) { - std::cerr << "fioGetstat error: Bad path translate" << std::endl; setReturnS32(ctx, -1); return; } - setReturnS32(ctx, -1); + PS2VfsStat status; + if (!runtime->vfs().stat(ps2Path, currentVfsMounts(), runtime->romDevice(), status)) + { + setReturnS32(ctx, -1); + return; + } + + io_stat_t guest{}; + guest.mode = (status.directory ? kFioSoIfDir : kFioSoIfReg) | kFioSoIROth | kFioSoIXOth | (status.readOnly ? 0u : kFioSoIWOth); + guest.size = static_cast(status.size & 0xFFFFFFFFu); + guest.hisize = static_cast(status.size >> 32u); + encodePs2Time(status.created, guest.ctime); + encodePs2Time(status.accessed, guest.atime); + encodePs2Time(status.modified, guest.mtime); + std::memcpy(ps2StatBuf, &guest, sizeof(guest)); + ps2TraceGuestRangeWrite(rdram, statBufAddr, sizeof(guest), "fioGetstat", ctx); + setReturnS32(ctx, 0); } void fioRemove(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) @@ -490,8 +391,8 @@ namespace ps2_syscalls return; } - std::string hostPath = translatePs2Path(ps2Path); - if (hostPath.empty()) + std::filesystem::path hostPath; + if (!runtime || !runtime->vfs().resolveHostPath(ps2Path, currentVfsMounts(), hostPath)) { std::cerr << "fioRemove error: Path translate fail" << std::endl; setReturnS32(ctx, -1); @@ -503,13 +404,12 @@ namespace ps2_syscalls if (!success || ec) { - std::cerr << "fioRemove error: remove failed for '" << hostPath - << "': " << ec.message() << std::endl; + std::cerr << "fioRemove error: remove failed for '" << hostPath.string() << "': " << ec.message() << std::endl; setReturnS32(ctx, -1); } else { - RUNTIME_LOG("fioRemove: Removed file '" << hostPath << "'"); + RUNTIME_LOG("fioRemove: Removed file '" << hostPath.string() << "'"); setReturnS32(ctx, 0); // Success } } diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Loader.h b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Loader.h index d9da8ee..db46c5d 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Loader.h +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Loader.h @@ -46,6 +46,36 @@ namespace return hash; } + bool copyGuestBytesBounded(const uint8_t *rdram, + uint32_t guestAddr, + uint32_t byteCount, + uint32_t maxBytes, + std::vector &out) + { + out.clear(); + if (byteCount == 0u) + { + return true; + } + if (!rdram || guestAddr == 0u || byteCount > maxBytes) + { + return false; + } + + out.resize(byteCount); + for (uint32_t i = 0; i < byteCount; ++i) + { + const uint8_t *src = getConstMemPtr(rdram, guestAddr + i); + if (!src) + { + out.clear(); + return false; + } + out[i] = *src; + } + return true; + } + std::string makeSifModuleBufferTag(const uint8_t *rdram, uint32_t bufferAddr) { char key[96] = {}; @@ -75,9 +105,9 @@ namespace ++g_sif_module_log_count; } - int32_t trackSifModuleLoad(const std::string &path) + int32_t trackSifModuleLoadExternal(const std::string &path, int32_t moduleId) { - if (path.empty()) + if (path.empty() || moduleId <= 0) { return -1; } @@ -90,34 +120,50 @@ namespace std::lock_guard lock(g_sif_module_mutex); - auto byPathIt = g_sif_module_id_by_path.find(pathKey); - if (byPathIt != g_sif_module_id_by_path.end()) + auto idIt = g_sif_modules_by_id.find(moduleId); + if (idIt != g_sif_modules_by_id.end()) { - auto byIdIt = g_sif_modules_by_id.find(byPathIt->second); - if (byIdIt != g_sif_modules_by_id.end()) + SifModuleRecord &record = idIt->second; + if (record.pathKey == pathKey) { - SifModuleRecord &record = byIdIt->second; record.loaded = true; ++record.refCount; - return record.id; + return moduleId; + } + + if (!record.pathKey.empty()) + { + auto oldPathIt = g_sif_module_id_by_path.find(record.pathKey); + if (oldPathIt != g_sif_module_id_by_path.end() && oldPathIt->second == moduleId) + { + g_sif_module_id_by_path.erase(oldPathIt); + } } } - if (g_next_sif_module_id <= 0) + auto pathIt = g_sif_module_id_by_path.find(pathKey); + if (pathIt != g_sif_module_id_by_path.end() && pathIt->second != moduleId) { - g_next_sif_module_id = 1; + auto oldIt = g_sif_modules_by_id.find(pathIt->second); + if (oldIt != g_sif_modules_by_id.end()) + { + oldIt->second.loaded = false; + oldIt->second.refCount = 0; + } } - const int32_t moduleId = g_next_sif_module_id++; SifModuleRecord record; record.id = moduleId; record.path = path; record.pathKey = pathKey; record.refCount = 1; record.loaded = true; - g_sif_module_id_by_path[pathKey] = moduleId; - g_sif_modules_by_id[moduleId] = record; + g_sif_modules_by_id[moduleId] = std::move(record); + if (moduleId >= g_next_sif_module_id) + { + g_next_sif_module_id = moduleId + 1; + } return moduleId; } diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Runtime.h b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Runtime.h index 6909364..6204325 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Runtime.h +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/Runtime.h @@ -112,8 +112,11 @@ inline std::string translatePs2Path(const char *ps2Path) return {}; } - std::string pathStr(ps2Path); - std::string lower = toLowerAscii(pathStr); + const ps2x::iop::ParsedPs2Path parsed = ps2x::iop::parsePs2Path(ps2Path); + if (!parsed) + { + return {}; + } auto resolveWithBase = [&](const std::filesystem::path &base, const std::string &suffix) -> std::string { @@ -126,35 +129,19 @@ inline std::string translatePs2Path(const char *ps2Path) return resolved.lexically_normal().string(); }; - if (lower.rfind("host0:", 0) == 0 || lower.rfind("host:", 0) == 0) + switch (parsed.device) { - const std::size_t prefixLength = (lower.rfind("host0:", 0) == 0) ? 6 : 5; - return resolveWithBase(getConfiguredHostRoot(), pathStr.substr(prefixLength)); + case ps2x::iop::Ps2PathDevice::Host: + return resolveWithBase(getConfiguredHostRoot(), parsed.path); + case ps2x::iop::Ps2PathDevice::Cdrom: + return resolveWithBase(getConfiguredCdRoot(), parsed.path); + case ps2x::iop::Ps2PathDevice::MemoryCard0: + return resolveWithBase(getConfiguredMcRoot(), parsed.path); + case ps2x::iop::Ps2PathDevice::NativeHost: + return std::filesystem::path(parsed.path).lexically_normal().string(); + default: + return {}; } - - if (lower.rfind("cdrom0:", 0) == 0 || lower.rfind("cdrom:", 0) == 0) - { - const std::size_t prefixLength = (lower.rfind("cdrom0:", 0) == 0) ? 7 : 6; - return resolveWithBase(getConfiguredCdRoot(), pathStr.substr(prefixLength)); - } - - if (lower.rfind(kMc0Prefix, 0) == 0) - { - const std::size_t prefixLength = sizeof(kMc0Prefix) - 1; - return resolveWithBase(getConfiguredMcRoot(), pathStr.substr(prefixLength)); - } - - if (!pathStr.empty() && (pathStr.front() == '/' || pathStr.front() == '\\')) - { - return resolveWithBase(getConfiguredCdRoot(), pathStr); - } - - if (pathStr.size() > 1 && pathStr[1] == ':') - { - return pathStr; - } - - return resolveWithBase(getConfiguredCdRoot(), pathStr); } static bool localtimeSafe(const std::time_t *t, std::tm *out) diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h index 110b2f3..d6b7549 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/Helpers/State.h @@ -3,9 +3,6 @@ #include #include -inline std::unordered_map g_fileDescriptors; -inline int g_nextFd = 3; // Start after stdin, stdout, stderr - // Thread status #define THS_RUN 0x01 #define THS_READY 0x02 @@ -156,8 +153,6 @@ static constexpr uint32_t kFioSoIROth = 0x0004; static constexpr uint32_t kFioSoIWOth = 0x0002; static constexpr uint32_t kFioSoIXOth = 0x0001; -inline std::mutex g_fd_mutex; - struct RpcServerState { uint32_t sid = 0; diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp index f5dfcc5..07fcd29 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/RPC.cpp @@ -155,20 +155,22 @@ namespace ps2_syscalls const int32_t moduleId = static_cast(getRegU32(ctx, 4)); // $a0 const uint32_t resultAddr = getRegU32(ctx, 7); // $a3 (int* result, optional) + int32_t moduleResult = -1; + const bool stoppedByEmulator = runtime->stopIopModule(moduleId, &moduleResult); uint32_t refsLeft = 0; const bool knownModule = trackSifModuleStop(moduleId, &refsLeft); - const int32_t ret = knownModule ? 0 : -1; + const int32_t ret = (stoppedByEmulator || knownModule) ? 0 : -1; if (resultAddr != 0) { int32_t *hostResult = reinterpret_cast(getMemPtr(rdram, resultAddr)); if (hostResult) { - *hostResult = knownModule ? 0 : -1; + *hostResult = stoppedByEmulator ? moduleResult : (knownModule ? 0 : -1); } } - if (knownModule) + if (stoppedByEmulator || knownModule) { std::string modulePath; { @@ -179,7 +181,7 @@ namespace ps2_syscalls modulePath = it->second.path; } } - logSifModuleAction("stop", moduleId, modulePath, refsLeft); + logSifModuleAction(stoppedByEmulator ? "stop-emulated" : "stop", moduleId, modulePath, refsLeft); } setReturnS32(ctx, ret); @@ -187,7 +189,9 @@ namespace ps2_syscalls void SifLoadModule(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - const uint32_t pathAddr = getRegU32(ctx, 4); // $a0 + const uint32_t pathAddr = getRegU32(ctx, 4); // $a0 + const uint32_t argumentSize = getRegU32(ctx, 5); // $a1 + const uint32_t argumentAddr = getRegU32(ctx, 6); // $a2 const std::string modulePath = readGuestCStringBounded(rdram, pathAddr, kMaxSifModulePathBytes); if (modulePath.empty()) { @@ -195,34 +199,35 @@ namespace ps2_syscalls return; } - const int32_t moduleId = trackSifModuleLoad(modulePath); - if (moduleId <= 0) + std::vector arguments; + constexpr uint32_t kMaxIopModuleArguments = 64u * 1024u; + if (!copyGuestBytesBounded(rdram, argumentAddr, argumentSize, kMaxIopModuleArguments, arguments)) { setReturnS32(ctx, -1); return; } - uint32_t refs = 0; + if (!runtime) { - std::lock_guard lock(g_sif_module_mutex); - auto it = g_sif_modules_by_id.find(moduleId); - if (it != g_sif_modules_by_id.end()) - { - refs = it->second.refCount; - } + setReturnS32(ctx, -1); + return; } - logSifModuleAction("load", moduleId, modulePath, refs); - setReturnS32(ctx, moduleId); + const auto loaded = runtime->loadIopModule(modulePath, arguments.empty() ? nullptr : arguments.data(), static_cast(arguments.size())); + if (!loaded.handled || loaded.moduleId <= 0) + { + setReturnS32(ctx, -1); + return; + } + + trackSifModuleLoadExternal(modulePath, loaded.moduleId); + logSifModuleAction("load-emulated", loaded.moduleId, modulePath, 1u); + setReturnS32(ctx, loaded.moduleId); } void SifInitRpc(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { std::lock_guard lock(g_rpc_mutex); - if (runtime) - { - PS2IopTransport::reset(runtime); - } if (!g_rpc_initialized) { g_rpc_servers.clear(); @@ -290,9 +295,12 @@ namespace ps2_syscalls g_rpc_clients[clientPtr].sid = rpcId; } - if (!serverPtr) + if (!serverPtr && PS2IopTransport::canBindRpc(runtime, rpcId)) { - // Allocate a dummy server so bind loops can proceed. + // EE-side servers and HLE routes need a descriptor in guest RAM. + // With an emulated IOP, only publish it after the IRX has actually + // registered the SID; cd->server == nullptr is the SDK's retry + // signal while the IOP server thread is still starting. serverPtr = rpcAllocServerAddr(rdram); if (serverPtr) { diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp b/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp index 3530e65..9614016 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/System.cpp @@ -250,32 +250,6 @@ namespace ps2_syscalls setReturnS32(ctx, 0); } - void GetRomName(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) - { - uint32_t bufAddr = getRegU32(ctx, 4); // $a0 - size_t bufSize = getRegU32(ctx, 5); // $a1 - char *hostBuf = reinterpret_cast(getMemPtr(rdram, bufAddr)); - const char *romName = "ROMVER 0100"; - - if (!hostBuf) - { - std::cerr << "GetRomName error: Invalid buffer address" << std::endl; - setReturnS32(ctx, -1); // Error - return; - } - if (bufSize == 0) - { - setReturnS32(ctx, 0); - return; - } - - strncpy(hostBuf, romName, bufSize - 1); - hostBuf[bufSize - 1] = '\0'; - - // returns the length of the string (excluding null?) or error - setReturnS32(ctx, (int32_t)strlen(hostBuf)); - } - void SifLoadElfPart(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { const uint32_t pathAddr = getRegU32(ctx, 4); // $a0 - path @@ -313,33 +287,40 @@ namespace ps2_syscalls void sceSifLoadModuleBuffer(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) { - const uint32_t bufferAddr = getRegU32(ctx, 4); // $a0 + const uint32_t bufferAddr = getRegU32(ctx, 4); // $a0 + const uint32_t argumentSize = getRegU32(ctx, 5); // $a1 + const uint32_t argumentAddr = getRegU32(ctx, 6); // $a2 if (!rdram || bufferAddr == 0u) { setReturnS32(ctx, -1); return; } - // Match buffer-based module loads to stable synthetic tags so module ID lookup remains deterministic. const std::string moduleTag = makeSifModuleBufferTag(rdram, bufferAddr); - const int32_t moduleId = trackSifModuleLoad(moduleTag); - if (moduleId <= 0) + std::vector arguments; + constexpr uint32_t kMaxIopModuleArguments = 64u * 1024u; + if (!copyGuestBytesBounded(rdram, argumentAddr, argumentSize, kMaxIopModuleArguments, arguments)) { setReturnS32(ctx, -1); return; } - uint32_t refs = 0; + if (!runtime) { - std::lock_guard lock(g_sif_module_mutex); - auto it = g_sif_modules_by_id.find(moduleId); - if (it != g_sif_modules_by_id.end()) - { - refs = it->second.refCount; - } + setReturnS32(ctx, -1); + return; } - logSifModuleAction("load-buffer", moduleId, moduleTag, refs); - setReturnS32(ctx, moduleId); + + const auto loaded = runtime->loadIopModuleBuffer(bufferAddr, arguments.empty() ? nullptr : arguments.data(), static_cast(arguments.size())); + if (!loaded.handled || loaded.moduleId <= 0) + { + setReturnS32(ctx, -1); + return; + } + + trackSifModuleLoadExternal(moduleTag, loaded.moduleId); + logSifModuleAction("load-buffer-emulated", loaded.moduleId, moduleTag, 1u); + setReturnS32(ctx, loaded.moduleId); } void TODO(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime, uint32_t encodedSyscallId) diff --git a/ps2xRuntime/src/lib/Kernel/Syscalls/System.h b/ps2xRuntime/src/lib/Kernel/Syscalls/System.h index 3bfb232..c9b9991 100644 --- a/ps2xRuntime/src/lib/Kernel/Syscalls/System.h +++ b/ps2xRuntime/src/lib/Kernel/Syscalls/System.h @@ -16,7 +16,6 @@ namespace ps2_syscalls void SetOsdConfigParam(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void SetOsdConfigParam2(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void GetOsdConfigParam2(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); - void GetRomName(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void SifLoadElfPart(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceSifLoadElf(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); void sceSifLoadElfPart(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime); diff --git a/ps2xRuntime/src/lib/gs/gs_cpu_backend.cpp b/ps2xRuntime/src/lib/gs/gs_cpu_backend.cpp index 9c39ae2..f0ad507 100644 --- a/ps2xRuntime/src/lib/gs/gs_cpu_backend.cpp +++ b/ps2xRuntime/src/lib/gs/gs_cpu_backend.cpp @@ -13,6 +13,7 @@ #include #include #include +#include using namespace GSInternal; @@ -281,41 +282,14 @@ namespace return (index & ~0x18u) | ((index & 0x08u) << 1u) | ((index & 0x10u) >> 1u); } - // TODO: clut cache - uint32_t resolveClutIndex(uint8_t index, uint8_t cpsm, uint8_t csm, uint8_t csa, uint8_t sourcePsm) + bool isFourBitIndexedPsm(uint8_t psm) { - uint32_t clutIndex = static_cast(index); + return psm == GS_PSM_T4 || psm == GS_PSM_T4HL || psm == GS_PSM_T4HH; + } - // CSM2 addresses the source directly through TEXCLUT. CSA is required - // to be zero there, so it must not offset the source coordinates. - if (csm != 0u) - return (sourcePsm == GS_PSM_T4 || - sourcePsm == GS_PSM_T4HH || - sourcePsm == GS_PSM_T4HL) - ? (clutIndex & 0x0Fu) - : clutIndex; - - const bool is16BitClut = cpsm == GS_PSM_CT16 || cpsm == GS_PSM_CT16S; - const uint32_t csaMask = is16BitClut ? 0x1Fu : 0x0Fu; - const uint32_t clutIndexMask = is16BitClut ? 0x1FFu : 0x0FFu; - const uint32_t clutBase = (static_cast(csa) & csaMask) << 4u; - - switch (sourcePsm) - { - case GS_PSM_T4: - case GS_PSM_T4HH: - case GS_PSM_T4HL: - clutIndex = clutBase + (clutIndex & 0x0Fu); - break; - case GS_PSM_T8: - case GS_PSM_T8H: - clutIndex = clutBase + clutIndex; - break; - default: - return clutIndex; - } - - return swizzleClutIndexCSM1(clutIndex & clutIndexMask); + bool isEightBitIndexedPsm(uint8_t psm) + { + return psm == GS_PSM_T8 || psm == GS_PSM_T8H; } uint8_t lerpChannel(uint8_t c00, uint8_t c10, uint8_t c01, uint8_t c11, float fx, float fy) @@ -541,6 +515,9 @@ GSCpuBackend::GSCpuBackend() void GSCpuBackend::Initialize(uint8_t *vram, uint32_t vramSize) { + if (vram && vramSize < GSMem::MEMORY_SIZE) + throw std::invalid_argument("GS CPU backend requires at least 4 MiB of VRAM"); + std::lock_guard lock(m_mutex); m_vram = vram; m_vramSize = vramSize; @@ -555,6 +532,9 @@ void GSCpuBackend::Reset() void GSCpuBackend::ResetUnlocked() { + m_clut.fill(0u); + m_clutCbp.fill(0u); + m_texturePageCache.Invalidate(); m_transfer = {}; m_transfer.direction = 3u; m_transferState = {}; @@ -571,6 +551,91 @@ void GSCpuBackend::Submit(const GSPrimitiveBatch &batch) DrawPrimitive(batch); } +void GSCpuBackend::LoadClut(const GSTex0Reg &tex0, const GSTexClutReg &texclut) +{ + std::lock_guard lock(m_mutex); + if (!m_vram || (!isFourBitIndexedPsm(tex0.psm) && !isEightBitIndexedPsm(tex0.psm))) + return; + + switch (tex0.cld) + { + case 0u: + case 6u: + case 7u: + return; + case 1u: + break; + case 2u: + m_clutCbp[0] = tex0.cbp; + break; + case 3u: + m_clutCbp[1] = tex0.cbp; + break; + case 4u: + if (m_clutCbp[0] == tex0.cbp) + return; + m_clutCbp[0] = tex0.cbp; + break; + case 5u: + if (m_clutCbp[1] == tex0.cbp) + return; + m_clutCbp[1] = tex0.cbp; + break; + default: + return; + } + + LoadClutUnlocked(tex0, texclut); +} + +void GSCpuBackend::LoadClutUnlocked(const GSTex0Reg &tex0, const GSTexClutReg &texclut) +{ + const bool fourBit = isFourBitIndexedPsm(tex0.psm); + const bool sixteenBit = tex0.cpsm == GS_PSM_CT16 || tex0.cpsm == GS_PSM_CT16S; + const bool thirtyTwoBit = tex0.cpsm == GS_PSM_CT32 || tex0.cpsm == GS_PSM_CT24; + if (!sixteenBit && !thirtyTwoBit) + return; + + const uint32_t entryCount = fourBit ? 16u : 256u; + const uint32_t csaMask = sixteenBit ? 0x1Fu : 0x0Fu; + const uint32_t destinationBase = (static_cast(tex0.csa) & csaMask) << 4u; + + const bool loadCsm1Suffix = tex0.csm == 0u && thirtyTwoBit && !fourBit; + const uint32_t firstEntry = loadCsm1Suffix ? destinationBase : 0u; + + for (uint32_t entry = firstEntry; entry < entryCount; ++entry) + { + uint32_t sourceX = 0u; + uint32_t sourceY = 0u; + uint32_t sourceWidth = 1u; + + if (tex0.csm == 0u) + { + const uint32_t sourceIndex = swizzleClutIndexCSM1(entry); + sourceX = sourceIndex & 0x0Fu; + sourceY = sourceIndex >> 4u; + } + else + { + sourceWidth = texclut.cbw != 0u ? static_cast(texclut.cbw) : 1u; + sourceX = (static_cast(texclut.cou) << 4u) + entry; + sourceY = static_cast(texclut.cov); + } + + const uint32_t raw = ReadTextureVramUnlocked(tex0.cpsm, tex0.cbp, sourceWidth, sourceX, sourceY); + const uint32_t destination = (loadCsm1Suffix ? entry : destinationBase + entry) & (sixteenBit ? 0x1FFu : 0x0FFu); + if (sixteenBit) + { + m_clut[destination] = static_cast(raw); + } + else + { + m_clut[destination] = static_cast(raw & 0xFFFFu); + m_clut[destination + 256u] = static_cast(raw >> 16u); + } + } +} + void GSCpuBackend::Flush() { // CPU backend is immediate. GPU backends may submit command buffers here. @@ -578,8 +643,8 @@ void GSCpuBackend::Flush() void GSCpuBackend::TextureFlush() { - // CPU texture reads are coherent with local memory. Future cached/GPU - // backends use this boundary to invalidate texture views. + std::lock_guard lock(m_mutex); + m_texturePageCache.Invalidate(); } void GSCpuBackend::Sync(GSSyncReason) @@ -600,6 +665,14 @@ uint32_t GSCpuBackend::ReadVramUnlocked(uint32_t psm, uint32_t base, uint32_t bw return m_readVramFuncs[psm & 0x3Fu](m_vram, base, bw, x, y); } +uint32_t GSCpuBackend::ReadTextureVramUnlocked(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y) +{ + if (!m_vram) + return 0u; + + return GSMem::ReadTexture(m_texturePageCache, m_vram, psm, base, bw, x, y); +} + void GSCpuBackend::WriteVram(uint32_t psm, uint32_t base, uint32_t bw, uint32_t x, uint32_t y, uint32_t value) { std::lock_guard lock(m_mutex); @@ -941,27 +1014,40 @@ void GSCpuBackend::WritePixel(const GSDrawState &state, int x, int y, int z, uin uint32_t GSCpuBackend::LookupCLUT(const GSDrawState &state, uint8_t index, - uint32_t cbp, uint8_t cpsm, uint8_t csm, uint8_t csa, uint8_t sourcePsm) { - const uint32_t clutIndex = resolveClutIndex(index, cpsm, csm, csa, sourcePsm); - const uint32_t clutWidth = (state.texclut.cbw != 0u) ? static_cast(state.texclut.cbw) : 1u; - const uint32_t clutX = static_cast(state.texclut.cou) + (clutIndex & 0x0Fu); - const uint32_t clutY = static_cast(state.texclut.cov) + (clutIndex >> 4); + const bool sixteenBit = cpsm == GS_PSM_CT16 || cpsm == GS_PSM_CT16S; + const uint32_t csaMask = sixteenBit ? 0x1Fu : 0x0Fu; + const uint32_t clutBase = (static_cast(csa) & csaMask) << 4u; + const uint32_t sourceIndex = isFourBitIndexedPsm(sourcePsm) + ? (static_cast(index) & 0x0Fu) + : static_cast(index); + + uint32_t clutIndex = (clutBase + sourceIndex) & (sixteenBit ? 0x1FFu : 0x0FFu); + if (!sixteenBit && csm == 0u && isEightBitIndexedPsm(sourcePsm)) + { + const uint32_t block = std::min((sourceIndex & 0xF0u) + clutBase, 240u); + clutIndex = block + (sourceIndex & 0x0Fu); + } switch (cpsm) { case GS_PSM_CT32: - return applyTexa(state.texa, cpsm, GSMem::ReadCT32(m_vram, cbp, clutWidth, clutX, clutY)); + { + const uint32_t raw = static_cast(m_clut[clutIndex]) | (static_cast(m_clut[clutIndex + 256u]) << 16u); + return applyTexa(state.texa, cpsm, raw); + } case GS_PSM_CT24: - return applyTexa(state.texa, cpsm, GSMem::ReadCT24(m_vram, cbp, clutWidth, clutX, clutY)); + { + const uint32_t raw = static_cast(m_clut[clutIndex]) | (static_cast(m_clut[clutIndex + 256u]) << 16u); + return applyTexa(state.texa, cpsm, raw & 0x00FFFFFFu); + } case GS_PSM_CT16: - return applyTexa(state.texa, cpsm, Rgba5551ToRgba8888(GSMem::ReadCT16(m_vram, cbp, clutWidth, clutX, clutY))); case GS_PSM_CT16S: - return applyTexa(state.texa, cpsm, Rgba5551ToRgba8888(GSMem::ReadCT16S(m_vram, cbp, clutWidth, clutX, clutY))); + return applyTexa(state.texa, cpsm, Rgba5551ToRgba8888(m_clut[clutIndex])); default: break; } @@ -1002,7 +1088,7 @@ uint32_t GSCpuBackend::SampleTexture(const GSDrawState &state, float s, float t, sampleU = wrapTextureCoordinate(sampleU, texW, wrapU, minU, maxU); sampleV = wrapTextureCoordinate(sampleV, texH, wrapV, minV, maxV); - u32 out = ReadVramUnlocked(tex.psm, tex.tbp0, tex.tbw, sampleU, sampleV); + u32 out = ReadTextureVramUnlocked(tex.psm, tex.tbp0, tex.tbw, sampleU, sampleV); switch (tex.psm) { @@ -1021,7 +1107,7 @@ uint32_t GSCpuBackend::SampleTexture(const GSDrawState &state, float s, float t, case GS_PSM_T4: case GS_PSM_T4HL: case GS_PSM_T4HH: - return LookupCLUT(state, static_cast(out), tex.cbp, tex.cpsm, tex.csm, tex.csa, tex.psm); + return LookupCLUT(state, static_cast(out), tex.cpsm, tex.csm, tex.csa, tex.psm); } return 0xFFFF00FFu; diff --git a/ps2xRuntime/src/lib/gs/gs_frontend.cpp b/ps2xRuntime/src/lib/gs/gs_frontend.cpp index fd5a90d..5658928 100644 --- a/ps2xRuntime/src/lib/gs/gs_frontend.cpp +++ b/ps2xRuntime/src/lib/gs/gs_frontend.cpp @@ -1239,6 +1239,7 @@ void GS::writeRegisterUnlocked(uint8_t regAddr, uint64_t value) t.csm = static_cast((value >> 55) & 0x1); t.csa = static_cast((value >> 56) & 0x1F); t.cld = static_cast((value >> 61) & 0x7); + m_backend->LoadClut(t, m_texclut); break; } case GS_REG_CLAMP_1: @@ -1269,6 +1270,7 @@ void GS::writeRegisterUnlocked(uint8_t regAddr, uint64_t value) t.csm = static_cast((value >> 55) & 0x1); t.csa = static_cast((value >> 56) & 0x1F); t.cld = static_cast((value >> 61) & 0x7); + m_backend->LoadClut(t, m_texclut); break; } case GS_REG_XYOFFSET_1: diff --git a/ps2xRuntime/src/lib/gs/ps2_gs_memory.cpp b/ps2xRuntime/src/lib/gs/ps2_gs_memory.cpp index fecf9fd..a8f9fa8 100644 --- a/ps2xRuntime/src/lib/gs/ps2_gs_memory.cpp +++ b/ps2xRuntime/src/lib/gs/ps2_gs_memory.cpp @@ -220,6 +220,41 @@ namespace GSMem PixelStorageTraits::InitPageLookupTable(PageTableP4, BlockTableP4, ColumnTable4); } + u32 ReadTexture(TexturePageCache& cache, const u8* data, u32 psm, u32 bp, u32 bw, u32 x, u32 y) + { + switch (static_cast(psm & 0x3Fu)) + { + case C32: + return PixelStorageTraits::Read(PageTableC32, data, bp, bw, x, y, &cache); + case C24: + return PixelStorageTraits::Read(PageTableC32, data, bp, bw, x, y, &cache); + case C16: + return PixelStorageTraits::Read(PageTableC16, data, bp, bw, x, y, &cache); + case C16S: + return PixelStorageTraits::Read(PageTableC16S, data, bp, bw, x, y, &cache); + case P8: + return PixelStorageTraits::Read(PageTableP8, data, bp, bw, x, y, &cache); + case P4: + return PixelStorageTraits::Read(PageTableP4, data, bp, bw, x, y, &cache); + case P8H: + return PixelStorageTraits::Read(PageTableC32, data, bp, bw, x, y, &cache); + case P4HL: + return PixelStorageTraits::Read(PageTableC32, data, bp, bw, x, y, &cache); + case P4HH: + return PixelStorageTraits::Read(PageTableC32, data, bp, bw, x, y, &cache); + case Z32: + return PixelStorageTraits::Read(PageTableZ32, data, bp, bw, x, y, &cache); + case Z24: + return PixelStorageTraits::Read(PageTableZ32, data, bp, bw, x, y, &cache); + case Z16: + return PixelStorageTraits::Read(PageTableZ16, data, bp, bw, x, y, &cache); + case Z16S: + return PixelStorageTraits::Read(PageTableZ16S, data, bp, bw, x, y, &cache); + default: + return 0u; + } + } + void WriteCT32(u8* data, u32 bp, u32 bw, u32 x, u32 y, u32 value) { PixelStorageTraits::Write(PageTableC32, data, bp, bw, x, y, value); diff --git a/ps2xRuntime/src/lib/ps2_debug_panel.cpp b/ps2xRuntime/src/lib/ps2_debug_panel.cpp index cf0b439..c9aa2c6 100644 --- a/ps2xRuntime/src/lib/ps2_debug_panel.cpp +++ b/ps2xRuntime/src/lib/ps2_debug_panel.cpp @@ -81,7 +81,7 @@ namespace { for (const ps2x::iop::DebugService &service : snapshot.services) { - if (std::find(service.sids.begin(), service.sids.end(), sid) != + if (service.active && std::find(service.sids.begin(), service.sids.end(), sid) != service.sids.end()) { return service.name; @@ -699,13 +699,48 @@ namespace void drawCpuTab(PS2Runtime &runtime, bool showRegisters) { - const uint32_t pc = runtime.m_debugPc.load(std::memory_order_relaxed); - const uint32_t ra = runtime.m_debugRa.load(std::memory_order_relaxed); - const uint32_t sp = runtime.m_debugSp.load(std::memory_order_relaxed); - const uint32_t gp = runtime.m_debugGp.load(std::memory_order_relaxed); + const bool executingGuest = runtime.eeScheduler().isExecutingGuest(); + const EeKernelSnapshot schedulerSnapshot = runtime.eeScheduler().snapshot(); + const EeThreadSnapshot *selectedThread = nullptr; + if (!executingGuest) + { + const auto running = std::find_if(schedulerSnapshot.threads.begin(), + schedulerSnapshot.threads.end(), + [&](const EeThreadSnapshot &thread) + { return thread.id == schedulerSnapshot.runningThreadId; }); + if (running != schedulerSnapshot.threads.end()) + { + selectedThread = &*running; + } + else + { + const auto blocked = std::find_if(schedulerSnapshot.threads.begin(), + schedulerSnapshot.threads.end(), + [](const EeThreadSnapshot &thread) + { return thread.status != EeThreadStatus::Dormant; }); + if (blocked != schedulerSnapshot.threads.end()) + { + selectedThread = &*blocked; + } + else if (!schedulerSnapshot.threads.empty()) + { + selectedThread = &schedulerSnapshot.threads.front(); + } + } + } + + const uint32_t pc = selectedThread ? selectedThread->pc : runtime.m_debugPc.load(std::memory_order_relaxed); + const uint32_t ra = selectedThread ? selectedThread->ra : runtime.m_debugRa.load(std::memory_order_relaxed); + const uint32_t sp = selectedThread ? selectedThread->sp : runtime.m_debugSp.load(std::memory_order_relaxed); + const uint32_t gp = selectedThread ? selectedThread->contextGp : runtime.m_debugGp.load(std::memory_order_relaxed); ImGui::Text("Runtime: %s", runtime.isStopRequested() ? "stop requested" : "running"); - ImGui::Text("EE executor: %s", runtime.eeScheduler().isExecutingGuest() ? "guest" : "scheduler"); + ImGui::Text("EE executor: %s", executingGuest ? "guest" : "scheduler"); + if (selectedThread) + { + ImGui::SameLine(); + ImGui::TextDisabled("(thread %d: %s/%s)", selectedThread->id, threadStatusName(selectedThread->status), waitTypeName(selectedThread->waitReason)); + } ImGui::Separator(); textHex32("PC", pc); ImGui::SameLine(); @@ -771,19 +806,23 @@ namespace static_cast(snapshot.eeCycle), static_cast(snapshot.sliceEndCycle), static_cast(snapshot.nextEventCycle)); - if (ImGui::BeginTable("threads", 11, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, 320))) + if (ImGui::BeginTable("threads", 15, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable | ImGuiTableFlags_ScrollY, ImVec2(0, 320))) { ImGui::TableSetupColumn("ID"); ImGui::TableSetupColumn("Status"); ImGui::TableSetupColumn("Wait"); ImGui::TableSetupColumn("WaitId"); ImGui::TableSetupColumn("PC"); + ImGui::TableSetupColumn("RA"); + ImGui::TableSetupColumn("SP"); + ImGui::TableSetupColumn("Ctx GP"); ImGui::TableSetupColumn("Entry"); ImGui::TableSetupColumn("Stack"); ImGui::TableSetupColumn("GP"); ImGui::TableSetupColumn("Prio"); ImGui::TableSetupColumn("Wake"); ImGui::TableSetupColumn("Susp"); + ImGui::TableSetupColumn("Inv"); ImGui::TableHeadersRow(); for (const EeThreadSnapshot &row : snapshot.threads) { @@ -799,6 +838,12 @@ namespace ImGui::TableNextColumn(); ImGui::Text("0x%08X", row.pc); ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.ra); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.sp); + ImGui::TableNextColumn(); + ImGui::Text("0x%08X", row.contextGp); + ImGui::TableNextColumn(); ImGui::Text("0x%08X", row.entry); ImGui::TableNextColumn(); ImGui::Text("0x%08X/%u", row.stack, row.stackSize); @@ -810,6 +855,8 @@ namespace ImGui::Text("%u", row.wakeupCount); ImGui::TableNextColumn(); ImGui::Text("%d", row.suspendCount); + ImGui::TableNextColumn(); + ImGui::Text("%u", row.invocationDepth); } ImGui::EndTable(); } @@ -1020,22 +1067,10 @@ namespace } ImGui::SeparatorText("ps2xIOP HLE services"); - ImGui::Text("Profile: %s provider: %s", - iopSnapshot.activeProfile.empty() - ? "" - : iopSnapshot.activeProfile.c_str(), - iopSnapshot.activeProvider.empty() - ? "builtin" - : iopSnapshot.activeProvider.c_str()); - - if (ImGui::BeginTable("iop_hle_services", - 5, - ImGuiTableFlags_Borders | - ImGuiTableFlags_RowBg | - ImGuiTableFlags_Resizable)) + if (ImGui::BeginTable("iop_hle_services", 5, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) { ImGui::TableSetupColumn("Service"); - ImGui::TableSetupColumn("Layer"); + ImGui::TableSetupColumn("Active"); ImGui::TableSetupColumn("SID"); ImGui::TableSetupColumn("EE server"); ImGui::TableSetupColumn("Metrics"); @@ -1048,7 +1083,7 @@ namespace ImGui::TableNextColumn(); ImGui::TextUnformatted(service.name.c_str()); ImGui::TableNextColumn(); - ImGui::TextUnformatted(service.profileSpecific ? "profile" : "core"); + ImGui::TextUnformatted(service.active ? "yes" : "no"); ImGui::TableNextColumn(); ImGui::TextUnformatted("-"); ImGui::TableNextColumn(); @@ -1064,7 +1099,7 @@ namespace ImGui::TableNextColumn(); ImGui::TextUnformatted(service.name.c_str()); ImGui::TableNextColumn(); - ImGui::TextUnformatted(service.profileSpecific ? "profile" : "core"); + ImGui::TextUnformatted(service.active ? "yes" : "no"); ImGui::TableNextColumn(); ImGui::Text("0x%08X", sid); ImGui::TableNextColumn(); @@ -1761,27 +1796,25 @@ namespace struct FdRow { int fd = 0; - FILE *file = nullptr; + std::string device; + std::string path; }; std::vector fds; + for (const PS2VfsDescriptorInfo &descriptor : runtime.vfs().descriptors()) { - std::lock_guard lock(g_fd_mutex); - fds.reserve(g_fileDescriptors.size()); - for (const auto &[fd, file] : g_fileDescriptors) - { - fds.push_back(FdRow{fd, file}); - } + fds.push_back({descriptor.descriptor, descriptor.device, descriptor.path}); } std::sort(fds.begin(), fds.end(), [](const FdRow &a, const FdRow &b) { return a.fd < b.fd; }); ImGui::SeparatorText("FileIO descriptors"); - ImGui::Text("Open host FILE* descriptors: %zu", fds.size()); - if (ImGui::BeginTable("fileio_fds", 2, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable, ImVec2(0, 90))) + ImGui::Text("Open VFS descriptors: %zu", fds.size()); + if (ImGui::BeginTable("fileio_fds", 3, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable, ImVec2(0, 90))) { ImGui::TableSetupColumn("FD"); - ImGui::TableSetupColumn("FILE*"); + ImGui::TableSetupColumn("Device"); + ImGui::TableSetupColumn("Path"); ImGui::TableHeadersRow(); for (const FdRow &row : fds) { @@ -1789,7 +1822,9 @@ namespace ImGui::TableNextColumn(); ImGui::Text("%d", row.fd); ImGui::TableNextColumn(); - ImGui::Text("%p", static_cast(row.file)); + ImGui::TextUnformatted(row.device.c_str()); + ImGui::TableNextColumn(); + ImGui::TextUnformatted(row.path.c_str()); } ImGui::EndTable(); } diff --git a/ps2xRuntime/src/lib/ps2_iop_host.cpp b/ps2xRuntime/src/lib/ps2_iop_host.cpp index 8709ed1..8d46e03 100644 --- a/ps2xRuntime/src/lib/ps2_iop_host.cpp +++ b/ps2xRuntime/src/lib/ps2_iop_host.cpp @@ -135,11 +135,6 @@ bool PS2IopHostAdapter::readGuest(uint32_t address, void *destination, size_t si { return false; } - if (ps2_stubs::isSifIopHeapAddress(address)) - { - return ps2_stubs::readSifIopHeap(address, destination, size); - } - uint8_t *source = nullptr; if (!guestRange(address, size, source)) { @@ -158,11 +153,6 @@ bool PS2IopHostAdapter::writeGuest(uint32_t address, const void *source, size_t { return false; } - if (ps2_stubs::isSifIopHeapAddress(address)) - { - return ps2_stubs::writeSifIopHeap(address, source, size); - } - uint8_t *destination = nullptr; if (!guestRange(address, size, destination)) { @@ -179,11 +169,6 @@ bool PS2IopHostAdapter::writeGuest(uint32_t address, const void *source, size_t bool PS2IopHostAdapter::zeroGuest(uint32_t address, size_t size) { - if (ps2_stubs::isSifIopHeapAddress(address)) - { - return ps2_stubs::zeroSifIopHeap(address, size); - } - uint8_t *destination = nullptr; if (!guestRange(address, size, destination)) { @@ -200,12 +185,6 @@ bool PS2IopHostAdapter::zeroGuest(uint32_t address, size_t size) bool PS2IopHostAdapter::normalizeGuestAddress(uint32_t address, uint32_t &normalized) const { - if (ps2_stubs::isSifIopHeapAddress(address)) - { - normalized = address; - return ps2_stubs::isSifIopHeapRange(address, 0u); - } - bool scratchpad = false; if (!ps2ResolveGuestPointer(address, normalized, scratchpad) || scratchpad) { @@ -215,6 +194,32 @@ bool PS2IopHostAdapter::normalizeGuestAddress(uint32_t address, uint32_t &normal return true; } +bool PS2IopHostAdapter::readIopMemory(uint32_t address, void *destination, size_t size) const +{ + return m_runtime.readIopMemory(address, destination, size); +} + +bool PS2IopHostAdapter::writeIopMemory(uint32_t address, const void *source, size_t size) +{ + return m_runtime.writeIopMemory(address, source, size); +} + +bool PS2IopHostAdapter::zeroIopMemory(uint32_t address, size_t size) +{ + return m_runtime.zeroIopMemory(address, size); +} + +bool PS2IopHostAdapter::normalizeIopAddress(uint32_t address, uint32_t &normalized) const +{ + if (!m_runtime.isIopMemoryRange(address, 0u)) + { + normalized = 0u; + return false; + } + normalized = address & 0x1FFFFFFFu; + return true; +} + uint32_t PS2IopHostAdapter::allocateIopHandle(ps2x::iop::IopHandleKind kind) { uint8_t *const rdram = m_activeRdram @@ -283,7 +288,12 @@ std::string PS2IopHostAdapter::hostPath(ps2x::iop::HostPathKind kind) const std::string PS2IopHostAdapter::translateGuestPath(std::string_view path) const { - return translatePs2Path(std::string(path).c_str()); + const PS2Runtime::IoPaths &paths = PS2Runtime::getIoPaths(); + const PS2VfsMounts mounts{paths.hostRoot, paths.cdRoot, paths.mcRoot}; + std::filesystem::path hostPath; + if (!m_runtime.vfs().resolveHostPath(path, mounts, hostPath)) + return {}; + return hostPath.string(); } uint64_t PS2IopHostAdapter::openHostFile(std::string_view path) @@ -493,6 +503,20 @@ bool PS2IopHostAdapter::invokeGuestFunction(uint64_t callToken, return false; } +bool PS2IopHostAdapter::sendSifCommand(uint32_t commandId, + const void *packet, + size_t packetSize) +{ + uint8_t *const rdram = m_activeRdram + ? m_activeRdram + : m_runtime.memory().getRDRAM(); + return ps2_stubs::dispatchSifCommand(rdram, + &m_runtime, + commandId, + packet, + packetSize); +} + void PS2IopHostAdapter::log(ps2x::iop::LogLevel level, std::string_view message) { const char *prefix = "[ps2xIOP]"; diff --git a/ps2xRuntime/src/lib/ps2_iop_host.h b/ps2xRuntime/src/lib/ps2_iop_host.h index 48c5b22..89d8576 100644 --- a/ps2xRuntime/src/lib/ps2_iop_host.h +++ b/ps2xRuntime/src/lib/ps2_iop_host.h @@ -48,6 +48,10 @@ public: bool writeGuest(uint32_t address, const void *source, size_t size) override; bool zeroGuest(uint32_t address, size_t size) override; bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const override; + bool readIopMemory(uint32_t address, void *destination, size_t size) const override; + bool writeIopMemory(uint32_t address, const void *source, size_t size) override; + bool zeroIopMemory(uint32_t address, size_t size) override; + bool normalizeIopAddress(uint32_t address, uint32_t &normalized) const override; uint32_t allocateIopHandle(ps2x::iop::IopHandleKind kind) override; uint32_t allocateGuest(uint32_t size, uint32_t alignment) override; void freeGuest(uint32_t address) override; @@ -78,6 +82,7 @@ public: uint32_t a2, uint32_t a3, uint32_t *resultAddress) override; + bool sendSifCommand(uint32_t commandId, const void *packet, size_t packetSize) override; void log(ps2x::iop::LogLevel level, std::string_view message) override; diff --git a/ps2xRuntime/src/lib/ps2_iop_transport.h b/ps2xRuntime/src/lib/ps2_iop_transport.h index 988d6b2..6bd1835 100644 --- a/ps2xRuntime/src/lib/ps2_iop_transport.h +++ b/ps2xRuntime/src/lib/ps2_iop_transport.h @@ -8,15 +8,6 @@ class PS2IopTransport { public: - static bool configureForTesting( - PS2Runtime *runtime, - const ps2x::iop::GameIdentity &identity, - std::string *error = nullptr) - { - return runtime && runtime->m_iopSubsystem && - runtime->m_iopSubsystem->configure(identity, error); - } - [[nodiscard]] static ps2x::iop::RpcAbi selectRpcAbi( const PS2Runtime *runtime, const ps2x::iop::RpcAbiRequest &request) @@ -37,6 +28,11 @@ public: : ps2x::iop::RpcResult{}; } + [[nodiscard]] static bool canBindRpc(const PS2Runtime *runtime, uint32_t sid) + { + return !runtime || runtime->canBindIopRpc(sid); + } + static void notifyTransfer( PS2Runtime *runtime, uint8_t *rdram, diff --git a/ps2xRuntime/src/lib/ps2_memory.cpp b/ps2xRuntime/src/lib/ps2_memory.cpp index 7cb2ba4..37b6a4d 100644 --- a/ps2xRuntime/src/lib/ps2_memory.cpp +++ b/ps2xRuntime/src/lib/ps2_memory.cpp @@ -411,6 +411,12 @@ uint32_t PS2Memory::advanceEeTimers(uint64_t eeCycles) noexcept return 0u; } + constexpr uint32_t kGifStat = 0x10003020u; + constexpr uint32_t kGifFqcMask = 0x1F000000u; + auto gifStatIt = m_ioRegisters.find(kGifStat); + if (gifStatIt != m_ioRegisters.end()) + gifStatIt->second &= ~kGifFqcMask; + uint32_t interruptMask = 0u; for (size_t index = 0; index < m_eeTimers.size(); ++index) { @@ -1064,6 +1070,21 @@ void PS2Memory::write128(uint32_t address, __m128i value) const bool scratch = isScratchpad(address); uint32_t physAddr = translateAddress(address); + if (!scratch && physAddr == 0x10004000u) // VIF0_FIFO + { + alignas(16) uint8_t fifoData[16]; + _mm_storeu_si128(reinterpret_cast<__m128i *>(fifoData), value); + processVIF0Data(fifoData, sizeof(fifoData)); + return; + } + if (!scratch && physAddr == 0x10005000u) // VIF1_FIFO + { + alignas(16) uint8_t fifoData[16]; + _mm_storeu_si128(reinterpret_cast<__m128i *>(fifoData), value); + processVIF1Data(fifoData, sizeof(fifoData)); + return; + } + if (scratch) { inRange(physAddr, sizeof(__m128i), PS2_SCRATCHPAD_SIZE, "write128 scratchpad", address); @@ -1117,7 +1138,7 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) case kEeTimerModeOffset: { const uint32_t previousMode = timer.mode; - const uint32_t status = (previousMode & kEeTimerModeStatusMask) &~(value & kEeTimerModeStatusMask); + const uint32_t status = (previousMode & kEeTimerModeStatusMask) & ~(value & kEeTimerModeStatusMask); timer.mode = (value & kEeTimerModeConfigMask) | status; if (((previousMode ^ timer.mode) & (kEeTimerModeClksMask | kEeTimerModeCue)) != 0u) { @@ -1206,9 +1227,13 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) case 0x10003C10u: // VIF1_FBRST if (value & 0x1u) // RST { + const bool wasPath3Masked = m_path3Masked; std::memset(&vif1_regs, 0, sizeof(vif1_regs)); m_vif1PendingPath2ImageQwc = 0u; m_vif1PendingPath2DirectHl = false; + m_path3Masked = false; + if (wasPath3Masked) + flushMaskedPath3Packets(); } if (value & 0x8u) // STC { @@ -1281,8 +1306,12 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) const uint32_t qwc = m_ioRegisters[channelBase + 0x20]; m_dmaStartCount.fetch_add(1, std::memory_order_relaxed); - if ((channelBase == 0x1000A000u || channelBase == 0x10009000u || channelBase == 0x10008000u) && - (m_gsVRAM || channelBase == 0x10008000u)) + if (tryProcessScratchpadDma(channelBase, value)) + { + return true; + } + + if ((channelBase == 0x1000A000u || channelBase == 0x10009000u || channelBase == 0x10008000u) && (m_gsVRAM || channelBase == 0x10008000u)) { auto enqueueTransfer = [&](uint32_t srcAddr, uint32_t qwCount) { @@ -1353,7 +1382,7 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) } }; - auto appendCompactVif1TagData = [&](uint32_t localTagAddr, uint32_t qwCount) + auto appendVifTagData = [&](uint32_t localTagAddr) { uint32_t tagPhys = 0u; const bool tagScratch = isScratchpad(localTagAddr); @@ -1364,11 +1393,15 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) if (tagPhys + 16u > localMax) return; - // VIF packet helpers embed 8 bytes of VIF stream in the DMAtag's upper half. + // CHCR.TTE sends the DMAtag's upper 64 bits to the channel before + // the tag payload. VIF chains use those bytes for two VIFcodes. chainBuf.insert(chainBuf.end(), localBase + tagPhys + 8u, localBase + tagPhys + 16u); - appendData(localTagAddr + 16u, qwCount); }; + const bool isVifChannel = + channelBase == 0x10009000u || channelBase == 0x10008000u; + const bool transferTagData = isVifChannel && (chcr & 0x40u) != 0u; + int tagsProcessed = 0; uint32_t lastTagUpper = (chcr >> 16) & 0xFFFFu; @@ -1477,19 +1510,11 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) break; } - const bool compactVifLocalTag = - (channelBase == 0x10009000u || channelBase == 0x10008000u) && - (id == 1u || id == 2u || id == 5u || id == 6u || id == 7u); - if (compactVifLocalTag) - appendCompactVif1TagData(currentTagAddr, 0u); + if (transferTagData) + appendVifTagData(currentTagAddr); if (hasPayload) - { - if (compactVifLocalTag) - appendData(currentTagAddr + 16u, tagQwc); - else - appendData(dataAddr, tagQwc); - } + appendData(dataAddr, tagQwc); if (irq && tieEnabled) endChain = true; if (endChain) @@ -1559,9 +1584,101 @@ bool PS2Memory::writeIORegister(uint32_t address, uint32_t value) return false; } +bool PS2Memory::tryProcessScratchpadDma(uint32_t channelBase, uint32_t chcr) +{ + static constexpr uint32_t kSprFromChannel = 0x1000D000u; + static constexpr uint32_t kSprToChannel = 0x1000D400u; + if (channelBase != kSprFromChannel && channelBase != kSprToChannel) + return false; + + const uint32_t mode = (chcr >> 2u) & 0x3u; + if (mode != 0u) + return false; + + const uint32_t qwc = m_ioRegisters[channelBase + 0x20u] & 0xFFFFu; + const uint32_t byteCount = qwc * 16u; + const uint32_t originalMadr = m_ioRegisters[channelBase + 0x10u] & 0x7FFFFFF0u; + const uint32_t originalSadr = m_ioRegisters[channelBase + 0x80u] & 0x3FF0u; + + uint32_t mainOffset = 0u; + try + { + mainOffset = translateAddress(originalMadr); + } + catch (const std::exception &) + { + return false; + } + + if (mainOffset > PS2_RAM_SIZE || byteCount > PS2_RAM_SIZE - mainOffset) + return false; + + const bool fromScratchpad = channelBase == kSprFromChannel; + uint32_t scratchOffset = originalSadr; + uint32_t bytesLeft = byteCount; + uint32_t copied = 0u; + while (bytesLeft != 0u) + { + const uint32_t scratchChunk = PS2_SCRATCHPAD_SIZE - scratchOffset; + const uint32_t chunk = std::min(bytesLeft, scratchChunk); + if (fromScratchpad) + { + std::memcpy(m_rdram + mainOffset + copied, m_scratchpad + scratchOffset, chunk); + markModified(mainOffset + copied, chunk); + } + else + { + std::memcpy(m_scratchpad + scratchOffset, m_rdram + mainOffset + copied, chunk); + } + + copied += chunk; + bytesLeft -= chunk; + scratchOffset = (scratchOffset + chunk) & (PS2_SCRATCHPAD_SIZE - 1u); + } + + m_ioRegisters[channelBase + 0x10u] = (originalMadr + byteCount) & 0x7FFFFFF0u; + m_ioRegisters[channelBase + 0x20u] = 0u; + m_ioRegisters[channelBase + 0x80u] = (originalSadr + byteCount) & 0x3FF0u; + completeDmacChannel(channelBase, fromScratchpad ? 8u : 9u); + return true; +} + +void PS2Memory::completeDmacChannel(uint32_t channelBase, uint32_t cause) +{ + static constexpr uint32_t kDStat = 0x1000E010u; + m_ioRegisters[channelBase] &= ~0x100u; + + uint32_t dstat = m_ioRegisters.count(kDStat) ? m_ioRegisters[kDStat] : 0u; + dstat |= 1u << cause; + const uint32_t status = dstat & 0x3FFu; + const uint32_t mask = (dstat >> 16u) & 0x3FFu; + if ((status & mask) != 0u) + dstat |= 1u << 31u; + else + dstat &= ~(1u << 31u); + m_ioRegisters[kDStat] = dstat; + queueCompletedDmacCause(cause); +} + void PS2Memory::processPendingTransfers() { const bool hadGif = !m_pendingGifTransfers.empty(); + uint32_t observedGifQwc = 0u; + for (const auto &transfer : m_pendingGifTransfers) + { + const uint64_t transferQwc = !transfer.chainData.empty() + ? (transfer.chainData.size() / 16u) + : transfer.qwc; + observedGifQwc = static_cast(std::min(16u, static_cast(observedGifQwc) + transferQwc)); + } + if (observedGifQwc != 0u) + { + constexpr uint32_t kGifStat = 0x10003020u; + constexpr uint32_t kGifFqcMask = 0x1F000000u; + uint32_t &gifStat = m_ioRegisters[kGifStat]; + gifStat = (gifStat & ~kGifFqcMask) | (observedGifQwc << 24u); + } + for (size_t idx = 0; idx < m_pendingGifTransfers.size(); ++idx) { auto &p = m_pendingGifTransfers[idx]; @@ -2221,6 +2338,23 @@ uint32_t PS2Memory::readIORegister(uint32_t address) } return val; } + + if (address == 0x10003020u) // GIF_STAT + { + uint32_t stat = m_ioRegisters.count(address) ? m_ioRegisters[address] : 0u; + const uint32_t mode = m_ioRegisters.count(0x10003010u) ? m_ioRegisters[0x10003010u] : 0u; + const uint32_t ctrl = m_ioRegisters.count(0x10003000u) ? m_ioRegisters[0x10003000u] : 0u; + + // M3R and IMT mirror GIF_MODE, PSE mirrors GIF_CTRL, and M3P is the + // effective PATH3 mask controlled by the VIF1 MSKPATH3 command. + stat = (stat & ~0xFu) | + (mode & 0x1u) | + (m_path3Masked ? 0x2u : 0u) | + (mode & 0x4u) | + (ctrl & 0x8u); + return stat; + } + if (address >= 0x10000000 && address < 0x10010000) { if (address >= 0x10008000 && address < 0x1000F000) diff --git a/ps2xRuntime/src/lib/ps2_rom_device.cpp b/ps2xRuntime/src/lib/ps2_rom_device.cpp new file mode 100644 index 0000000..a72105b --- /dev/null +++ b/ps2xRuntime/src/lib/ps2_rom_device.cpp @@ -0,0 +1,178 @@ +#include "runtime/ps2_rom_device.h" + +#include +#include +#include + +namespace +{ + std::mutex &profileMutex() + { + static std::mutex mutex; + return mutex; + } + + std::vector &profileRegistry() + { + static std::vector profiles; + return profiles; + } + + bool equalsIgnoreCaseAscii(std::string_view lhs, std::string_view rhs) + { + if (lhs.size() != rhs.size()) + return false; + for (size_t i = 0; i < lhs.size(); ++i) + { + const auto left = static_cast(lhs[i]); + const auto right = static_cast(rhs[i]); + if (std::tolower(left) != std::tolower(right)) + return false; + } + return true; + } + + int matchSpecificity(const ps2x::iop::GameMatcher &matcher, const ps2x::iop::GameIdentity &identity) + { + int specificity = 0; + if (!matcher.elfName.empty()) + { + if (!equalsIgnoreCaseAscii(matcher.elfName, identity.elfName)) + return -1; + ++specificity; + } + if (matcher.entryPoint != 0u) + { + if (matcher.entryPoint != identity.entryPoint) + return -1; + ++specificity; + } + if (matcher.crc32 != 0u) + { + if (matcher.crc32 != identity.crc32) + return -1; + ++specificity; + } + return specificity; + } +} + +PS2RomDevice::PS2RomDevice() +{ + mountBaseProfile(); +} + +void PS2RomDevice::registerProfile(PS2RomProfile profile) +{ + std::lock_guard lock(profileMutex()); + profileRegistry().push_back(std::move(profile)); +} + +bool PS2RomDevice::configure(const ps2x::iop::GameIdentity &identity, std::string *error) +{ + m_files.clear(); + m_activeProfile.clear(); + m_activeProvider.clear(); + mountBaseProfile(); + + std::vector profiles; + { + std::lock_guard lock(profileMutex()); + profiles = profileRegistry(); + } + + const PS2RomProfile *selected = nullptr; + const PS2RomProfile *tie = nullptr; + int selectedSpecificity = -1; + for (const PS2RomProfile &profile : profiles) + { + const int specificity = matchSpecificity(profile.matcher, identity); + if (specificity < 0) + continue; + if (specificity > selectedSpecificity) + { + selected = &profile; + tie = nullptr; + selectedSpecificity = specificity; + } + else if (specificity == selectedSpecificity && selected) + { + tie = &profile; + } + } + + if (selected && tie) + { + if (error) + { + *error = "ambiguous ROM profiles '" + selected->provider + ":" + selected->id + "' and '" + tie->provider + ":" + tie->id + "'"; + } + return false; + } + + if (selected) + { + mountFiles(selected->files); + m_activeProfile = selected->id; + m_activeProvider = selected->provider; + } + return true; +} + +bool PS2RomDevice::readFile(std::string_view ps2Path, std::vector &bytes) const +{ + const auto file = m_files.find(normalizePath(ps2Path)); + if (file == m_files.end()) + { + bytes.clear(); + return false; + } + bytes = file->second; + return true; +} + +bool PS2RomDevice::fileSize(std::string_view ps2Path, uint64_t &size) const +{ + const auto file = m_files.find(normalizePath(ps2Path)); + if (file == m_files.end()) + { + size = 0u; + return false; + } + size = file->second.size(); + return true; +} + +bool PS2RomDevice::contains(std::string_view ps2Path) const +{ + return m_files.contains(normalizePath(ps2Path)); +} + +std::string PS2RomDevice::normalizePath(std::string_view path) +{ + constexpr std::string_view prefix = "rom0:"; + if (path.size() >= prefix.size() && equalsIgnoreCaseAscii(path.substr(0, prefix.size()), prefix)) + path.remove_prefix(prefix.size()); + while (!path.empty() && (path.front() == '/' || path.front() == '\\')) + path.remove_prefix(1u); + + std::string normalized(path); + std::replace(normalized.begin(), normalized.end(), '\\', '/'); + std::transform(normalized.begin(), normalized.end(), normalized.begin(), [](unsigned char value) + { return static_cast(std::tolower(value)); }); + return normalized; +} + +void PS2RomDevice::mountBaseProfile() +{ + // TODO expose this to cmake + constexpr char romVersion[] = "0200AC20040614"; + static_assert(sizeof(romVersion) - 1u == 14u); + m_files[normalizePath("ROMVER")] = std::vector(romVersion, romVersion + 14u); +} + +void PS2RomDevice::mountFiles(const std::unordered_map> &files) +{ + for (const auto &[path, bytes] : files) + m_files[normalizePath(path)] = bytes; +} diff --git a/ps2xRuntime/src/lib/ps2_runtime.cpp b/ps2xRuntime/src/lib/ps2_runtime.cpp index 0e8471e..45cb9d2 100644 --- a/ps2xRuntime/src/lib/ps2_runtime.cpp +++ b/ps2xRuntime/src/lib/ps2_runtime.cpp @@ -15,10 +15,10 @@ #include "ps2x/iop/iop_subsystem.h" #include +#include #include #include #include -#include #include #include #include @@ -481,15 +481,8 @@ PS2Runtime::PS2Runtime() { m_iopHost = std::make_unique(*this); m_iopSubsystem = std::make_unique(*m_iopHost); + m_eeScheduler = std::make_unique(*this); -#if defined(PS2X_IOP_ENABLE_PLUGINS) && PS2X_IOP_ENABLE_PLUGINS && \ - !defined(PLATFORM_VITA) && (defined(_WIN32) || defined(__linux__)) - if (const char *applicationDirectory = GetApplicationDirectory(); - applicationDirectory && applicationDirectory[0] != '\0') - { - m_iopSubsystem->setPluginSearchPaths({std::filesystem::path(applicationDirectory) / "iop_plugins"}); - } -#endif // Assign rather than memset: R5900Context's constructor zeroes itself and // then applies the COP0 reset values, which a memset here would discard. @@ -571,9 +564,22 @@ PS2Runtime::~PS2Runtime() } } -void PS2Runtime::setIopPluginSearchPaths(std::vector paths) +ps2x::iop::ModuleLoadResult PS2Runtime::loadIopModule(std::string_view path, const void *arguments, uint32_t argumentSize) { - m_iopSubsystem->setPluginSearchPaths(std::move(paths)); + auto scope = m_iopHost->enterCall(nullptr, m_memory.getRDRAM()); + return m_iopSubsystem->loadModule(path, arguments, argumentSize); +} + +ps2x::iop::ModuleLoadResult PS2Runtime::loadIopModuleBuffer(uint32_t guestAddress, const void *arguments, uint32_t argumentSize) +{ + auto scope = m_iopHost->enterCall(nullptr, m_memory.getRDRAM()); + return m_iopSubsystem->loadModuleBuffer(guestAddress, arguments, argumentSize); +} + +bool PS2Runtime::stopIopModule(int32_t moduleId, int32_t *result) +{ + auto scope = m_iopHost->enterCall(nullptr, m_memory.getRDRAM()); + return m_iopSubsystem->stopModule(moduleId, result); } ps2x::iop::RpcAbi PS2Runtime::selectIopRpcAbi(const ps2x::iop::RpcAbiRequest &request) const @@ -581,6 +587,11 @@ ps2x::iop::RpcAbi PS2Runtime::selectIopRpcAbi(const ps2x::iop::RpcAbiRequest &re return m_iopSubsystem->selectRpcAbi(request); } +bool PS2Runtime::canBindIopRpc(uint32_t sid) const noexcept +{ + return m_iopSubsystem->canBindRpc(sid); +} + ps2x::iop::RpcResult PS2Runtime::handleIopRpc(uint8_t *rdram, R5900Context *ctx, ps2x::iop::RpcRequest request) { auto scope = m_iopHost->enterCall(ctx, rdram); @@ -594,6 +605,11 @@ void PS2Runtime::notifyIopSifTransfer(uint8_t *rdram, const ps2x::iop::SifTransf m_iopSubsystem->onSifTransfer(transfer); } +void PS2Runtime::advanceIopEeCycles(uint64_t eeCycles) noexcept +{ + m_iopSubsystem->runEeCycles(eeCycles); +} + void PS2Runtime::resetIop() { m_iopSubsystem->reset(); @@ -604,6 +620,36 @@ ps2x::iop::DebugSnapshot PS2Runtime::iopDebugSnapshot() const return m_iopSubsystem->debugSnapshot(); } +uint32_t PS2Runtime::allocateIopMemory(uint32_t size, uint32_t alignment) +{ + return m_iopSubsystem ? m_iopSubsystem->allocateMemory(size, alignment) : 0u; +} + +bool PS2Runtime::freeIopMemory(uint32_t address) +{ + return m_iopSubsystem && m_iopSubsystem->freeMemory(address); +} + +bool PS2Runtime::readIopMemory(uint32_t address, void *destination, size_t size) const +{ + return m_iopSubsystem && m_iopSubsystem->readMemory(address, destination, size); +} + +bool PS2Runtime::writeIopMemory(uint32_t address, const void *source, size_t size) +{ + return m_iopSubsystem && m_iopSubsystem->writeMemory(address, source, size); +} + +bool PS2Runtime::zeroIopMemory(uint32_t address, size_t size) +{ + return m_iopSubsystem && m_iopSubsystem->zeroMemory(address, size); +} + +bool PS2Runtime::isIopMemoryRange(uint32_t address, size_t size) const +{ + return m_iopSubsystem && m_iopSubsystem->isMemoryRange(address, size); +} + bool PS2Runtime::syncCoreSubsystems() { uint8_t *const rdram = m_memory.getRDRAM(); @@ -682,15 +728,6 @@ bool PS2Runtime::initialize(const char *title) std::cerr << "Failed to bind runtime core subsystems" << std::endl; return false; } -#if defined(PS2X_IOP_ENABLE_PLUGINS) && PS2X_IOP_ENABLE_PLUGINS && \ - !defined(PLATFORM_VITA) && (defined(_WIN32) || defined(__linux__)) - std::string pluginError; - if (!m_iopSubsystem->loadPlugins(&pluginError)) - { - std::cerr << "Failed to load IOP plugins: " << pluginError << std::endl; - return false; - } -#endif #if defined(PLATFORM_VITA) InitWindow(HOST_WINDOW_WIDTH, HOST_WINDOW_HEIGHT, title); // raylib vita does not support audio #else @@ -957,13 +994,15 @@ bool PS2Runtime::loadELF(const std::string &elfPath) identity.elfName = module.name; identity.entryPoint = m_cpuContext.pc; identity.crc32 = elfCrc32; - std::string iopError; - if (!m_iopSubsystem->configure(identity, &iopError)) + std::string romError; + if (!m_romDevice.configure(identity, &romError)) { - std::cerr << "[ps2xIOP] failed to configure profile: " << iopError << std::endl; + std::cerr << "[ROM0] failed to configure profile: " << romError << std::endl; return false; } + m_iopSubsystem->reset(); + ps2_game_overrides::applyMatching(*this, elfPath, m_cpuContext.pc, @@ -1348,7 +1387,8 @@ bool PS2Runtime::dispatchGuestBranch(uint8_t *rdram, if (policy == MissingFunctionPolicy::ContinueToTarget) { ctx->pc = targetPc; - return true; + // if you need the app to keep open to open debug pannel change this to false + return false; } return false; @@ -2377,6 +2417,7 @@ void PS2Runtime::run() << " gsw=" << curGs << " vif=" << curVif << std::endl); + } }); uint32_t presentWidth = FB_WIDTH; diff --git a/ps2xRuntime/src/lib/ps2_vfs.cpp b/ps2xRuntime/src/lib/ps2_vfs.cpp new file mode 100644 index 0000000..aff25e6 --- /dev/null +++ b/ps2xRuntime/src/lib/ps2_vfs.cpp @@ -0,0 +1,312 @@ +#include "runtime/ps2_vfs.h" + +#include "runtime/ps2_memory.h" +#include "runtime/ps2_rom_device.h" + +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + class HostOpenFile final : public IPS2OpenFile + { + public: + explicit HostOpenFile(FILE *file) : m_file(file) {} + ~HostOpenFile() override + { + if (m_file) + std::fclose(m_file); + } + + int64_t read(void *destination, size_t size) override + { + if ((!destination && size != 0u) || !m_file) + return -1; + const size_t bytes = std::fread(destination, 1u, size, m_file); + if (bytes < size && std::ferror(m_file)) + { + std::clearerr(m_file); + return -1; + } + return static_cast(bytes); + } + + int64_t write(const void *source, size_t size) override + { + if ((!source && size != 0u) || !m_file) + return -1; + const size_t bytes = std::fwrite(source, 1u, size, m_file); + if (bytes < size && std::ferror(m_file)) + { + std::clearerr(m_file); + return -1; + } + return static_cast(bytes); + } + + int64_t seek(int64_t offset, int whence) override + { + if (!m_file || offset < std::numeric_limits::min() || offset > std::numeric_limits::max() || std::fseek(m_file, static_cast(offset), whence) != 0) + return -1; + const long position = std::ftell(m_file); + return position < 0 ? -1 : static_cast(position); + } + + private: + FILE *m_file = nullptr; + }; + + class MemoryOpenFile final : public IPS2OpenFile + { + public: + explicit MemoryOpenFile(std::vector bytes) : m_bytes(std::move(bytes)) {} + + int64_t read(void *destination, size_t size) override + { + if (!destination && size != 0u) + return -1; + const size_t available = m_position < m_bytes.size() ? m_bytes.size() - m_position : 0u; + const size_t count = std::min(size, available); + if (count != 0u) + std::memcpy(destination, m_bytes.data() + m_position, count); + m_position += count; + return static_cast(count); + } + + int64_t write(const void *, size_t) override + { + return -1; + } + + int64_t seek(int64_t offset, int whence) override + { + int64_t base = 0; + if (whence == SEEK_CUR) + base = static_cast(m_position); + else if (whence == SEEK_END) + base = static_cast(m_bytes.size()); + else if (whence != SEEK_SET) + return -1; + + const int64_t position = base + offset; + if (position < 0 || static_cast(position) > m_bytes.size()) + return -1; + m_position = static_cast(position); + return position; + } + + private: + std::vector m_bytes; + size_t m_position = 0u; + }; + + const char *hostMode(uint32_t flags) + { + const bool read = (flags & PS2_FIO_O_RDONLY) != 0u || (flags & PS2_FIO_O_RDWR) == PS2_FIO_O_RDWR; + const bool write = (flags & PS2_FIO_O_WRONLY) != 0u || (flags & PS2_FIO_O_RDWR) == PS2_FIO_O_RDWR; + const bool create = (flags & PS2_FIO_O_CREAT) != 0u; + const bool truncate = (flags & PS2_FIO_O_TRUNC) != 0u; + const bool append = (flags & PS2_FIO_O_APPEND) != 0u; + + if (read && write) + { + if (truncate) + return "w+b"; + if (append) + return "a+b"; + return "r+b"; + } + if (write) + { + if (append) + return "ab"; + if (create || truncate) + return "wb"; + return "r+b"; + } + return "rb"; + } + + bool safeRelativePath(std::string_view suffix, std::filesystem::path &relative) + { + relative = std::filesystem::path(suffix).lexically_normal(); + if (relative.is_absolute() || relative.has_root_name()) + return false; + for (const auto &part : relative) + { + if (part == "..") + return false; + } + return true; + } + + std::time_t toTimeT(std::filesystem::file_time_type value) + { + const auto systemValue = std::chrono::time_point_cast(value - std::filesystem::file_time_type::clock::now() + std::chrono::system_clock::now()); + return std::chrono::system_clock::to_time_t(systemValue); + } +} + +PS2Vfs::~PS2Vfs() = default; + +int32_t PS2Vfs::open(std::string_view path, uint32_t flags, const PS2VfsMounts &mounts, const PS2RomDevice &rom) +{ + const ps2x::iop::ParsedPs2Path parsed = ps2x::iop::parsePs2Path(path); + if (!parsed) + return -1; + + std::unique_ptr file; + if (parsed.device == ps2x::iop::Ps2PathDevice::Rom0) + { + const uint32_t access = flags & PS2_FIO_O_RDWR; + if (access != PS2_FIO_O_RDONLY || (flags & (PS2_FIO_O_CREAT | PS2_FIO_O_TRUNC)) != 0u) + return -1; + std::vector bytes; + if (!rom.readFile(parsed.path, bytes)) + return -1; + file = std::make_unique(std::move(bytes)); + } + else + { + std::filesystem::path hostPath; + if (!resolveHostPath(path, mounts, hostPath)) + return -1; + + std::error_code existsError; + const bool exists = std::filesystem::exists(hostPath, existsError); + if (existsError || (exists && (flags & (PS2_FIO_O_CREAT | PS2_FIO_O_EXCL)) == (PS2_FIO_O_CREAT | PS2_FIO_O_EXCL))) + { + return -1; + } + + FILE *stream = std::fopen(hostPath.string().c_str(), hostMode(flags)); + const uint32_t access = flags & PS2_FIO_O_RDWR; + if (!stream && !exists && (flags & PS2_FIO_O_CREAT) != 0u && + access == PS2_FIO_O_RDWR && + (flags & (PS2_FIO_O_TRUNC | PS2_FIO_O_APPEND)) == 0u) + { + stream = std::fopen(hostPath.string().c_str(), "w+b"); + } + if (!stream) + return -1; + file = std::make_unique(stream); + } + + std::lock_guard lock(m_mutex); + if (m_nextDescriptor < 3) + m_nextDescriptor = 3; + const int32_t descriptor = m_nextDescriptor++; + m_descriptors.emplace(descriptor, OpenDescriptor{std::move(file), parsed.deviceName, std::string(path)}); + return descriptor; +} + +int32_t PS2Vfs::close(int32_t descriptor) +{ + std::lock_guard lock(m_mutex); + return m_descriptors.erase(descriptor) == 1u ? 0 : -1; +} + +int64_t PS2Vfs::read(int32_t descriptor, void *destination, size_t size) +{ + std::lock_guard lock(m_mutex); + const auto found = m_descriptors.find(descriptor); + return found == m_descriptors.end() ? -1 : found->second.file->read(destination, size); +} + +int64_t PS2Vfs::write(int32_t descriptor, const void *source, size_t size) +{ + std::lock_guard lock(m_mutex); + const auto found = m_descriptors.find(descriptor); + return found == m_descriptors.end() ? -1 : found->second.file->write(source, size); +} + +int64_t PS2Vfs::seek(int32_t descriptor, int64_t offset, int whence) +{ + std::lock_guard lock(m_mutex); + const auto found = m_descriptors.find(descriptor); + return found == m_descriptors.end() ? -1 : found->second.file->seek(offset, whence); +} + +bool PS2Vfs::stat(std::string_view path, const PS2VfsMounts &mounts, const PS2RomDevice &rom, PS2VfsStat &result) const +{ + result = {}; + const ps2x::iop::ParsedPs2Path parsed = ps2x::iop::parsePs2Path(path); + if (!parsed) + return false; + if (parsed.device == ps2x::iop::Ps2PathDevice::Rom0) + { + if (!rom.fileSize(parsed.path, result.size)) + return false; + result.readOnly = true; + return true; + } + + std::filesystem::path hostPath; + if (!resolveHostPath(path, mounts, hostPath)) + return false; + std::error_code error; + const auto status = std::filesystem::status(hostPath, error); + if (error || !std::filesystem::exists(status)) + return false; + result.directory = std::filesystem::is_directory(status); + if (!result.directory) + { + result.size = std::filesystem::file_size(hostPath, error); + if (error) + return false; + } + const auto modified = std::filesystem::last_write_time(hostPath, error); + if (!error) + result.created = result.accessed = result.modified = toTimeT(modified); + result.readOnly = (status.permissions() & std::filesystem::perms::owner_write) == std::filesystem::perms::none; + return true; +} + +bool PS2Vfs::resolveHostPath(std::string_view path, const PS2VfsMounts &mounts, std::filesystem::path &result) const +{ + result.clear(); + const ps2x::iop::ParsedPs2Path parsed = ps2x::iop::parsePs2Path(path); + if (!parsed || parsed.device == ps2x::iop::Ps2PathDevice::Rom0) + return false; + if (parsed.device == ps2x::iop::Ps2PathDevice::NativeHost) + { + result = std::filesystem::path(parsed.path).lexically_normal(); + return !result.empty(); + } + + std::filesystem::path base; + switch (parsed.device) + { + case ps2x::iop::Ps2PathDevice::Host: + base = mounts.hostRoot; + break; + case ps2x::iop::Ps2PathDevice::Cdrom: + base = mounts.cdRoot; + break; + case ps2x::iop::Ps2PathDevice::MemoryCard0: + base = mounts.memoryCard0Root; + break; + default: + return false; + } + std::filesystem::path relative; + if (base.empty() || !safeRelativePath(parsed.path, relative)) + return false; + result = (base / relative).lexically_normal(); + return true; +} + +std::vector PS2Vfs::descriptors() const +{ + std::lock_guard lock(m_mutex); + std::vector result; + result.reserve(m_descriptors.size()); + for (const auto &[descriptor, entry] : m_descriptors) + result.push_back({descriptor, entry.device, entry.path}); + return result; +} diff --git a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp index 05fc764..b02c7ca 100644 --- a/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp +++ b/ps2xRuntime/src/lib/ps2_vif1_interpreter.cpp @@ -30,18 +30,54 @@ namespace { constexpr uint8_t kGifFmtImage = 2u; - uint32_t gifImageQwcFromTag(const uint8_t *data, uint32_t sizeBytes) + uint32_t pendingGifImageQwc(const uint8_t *data, uint32_t sizeBytes) { if (!data || sizeBytes < 16u) return 0u; - uint64_t tagLo = 0u; - std::memcpy(&tagLo, data, sizeof(tagLo)); - const uint8_t flg = static_cast((tagLo >> 58) & 0x3u); - if (flg != kGifFmtImage) - return 0u; + uint32_t offset = 0u; + while (offset + 16u <= sizeBytes) + { + uint64_t tagLo = 0u; + std::memcpy(&tagLo, data + offset, sizeof(tagLo)); + offset += 16u; - return static_cast(tagLo & 0x7FFFu); + const uint32_t nloop = static_cast(tagLo & 0x7FFFu); + const uint8_t flg = static_cast((tagLo >> 58) & 0x3u); + uint32_t nreg = static_cast((tagLo >> 60) & 0xFu); + if (nreg == 0u) + nreg = 16u; + + uint64_t payloadBytes = 0u; + if (flg == 0u) // PACKED + { + payloadBytes = static_cast(nloop) * nreg * 16ull; + } + else if (flg == 1u) // REGLIST, padded to a quadword + { + payloadBytes = static_cast(nloop) * nreg * 8ull; + payloadBytes = (payloadBytes + 15ull) & ~15ull; + } + else if (flg == kGifFmtImage) + { + payloadBytes = static_cast(nloop) * 16ull; + const uint64_t availableBytes = sizeBytes - offset; + if (payloadBytes > availableBytes) + { + return static_cast((payloadBytes - availableBytes) / 16ull); + } + } + else + { + return 0u; + } + + if (payloadBytes > static_cast(sizeBytes - offset)) + return 0u; + offset += static_cast(payloadBytes); + } + + return 0u; } } @@ -284,11 +320,7 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) (static_cast(kGifFmtImage) << 58); std::memcpy(imagePacket.data(), &imageTag, sizeof(imageTag)); std::memcpy(imagePacket.data() + 16u, data + pos, static_cast(chunkQw) * 16u); - submitGifPacket(GifPathId::Path2, - imagePacket.data(), - static_cast(imagePacket.size()), - true, - m_vif1PendingPath2DirectHl); + submitGifPacket(GifPathId::Path2, imagePacket.data(), static_cast(imagePacket.size()), true, m_vif1PendingPath2DirectHl); pos += chunkQw * 16u; m_vif1PendingPath2ImageQwc -= chunkQw; @@ -372,9 +404,6 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) { uint32_t startPC = (uint32_t)imm * 8u; - // Values visible to the VU program for this MSCAL. - // DobieStation semantics: ITOP = ITOPS; TOP = current TOPS; - // then TOPS/DBF are prepared for the next buffer. const uint32_t runTop = vif1_regs.tops & 0x3FFu; const uint32_t runItop = vif1_regs.itops & 0x3FFu; vif1_regs.top = runTop; @@ -438,8 +467,6 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) else if (opcode == VIF_MPG) { uint32_t destAddr = (uint32_t)imm * 8u; - // VIF MPG semantics: NUM==0 means 256 instructions (2048 bytes). - // MPG payload is instruction-packed and should not be QW-aligned. const uint32_t instructionCount = (num == 0u) ? 256u : static_cast(num); const uint32_t mpgBytes = instructionCount * 8u; if (m_vu1Code && destAddr < PS2_VU1_CODE_SIZE && mpgBytes > 0) @@ -473,15 +500,11 @@ void PS2Memory::processVIF1Data(const uint8_t *data, uint32_t sizeBytes) const bool directHl = (opcode == VIF_DIRECTHL); submitGifPacket(GifPathId::Path2, data + pos, qwCount * 16, true, directHl); - const uint32_t imageQw = gifImageQwcFromTag(data + pos, qwCount * 16u); - if (imageQw != 0u) + const uint32_t pendingImageQw = pendingGifImageQwc(data + pos, qwCount * 16u); + if (pendingImageQw != 0u) { - const uint32_t inlineImageQw = (qwCount > 0u) ? (qwCount - 1u) : 0u; - if (imageQw > inlineImageQw) - { - m_vif1PendingPath2ImageQwc = imageQw - inlineImageQw; - m_vif1PendingPath2DirectHl = directHl; - } + m_vif1PendingPath2ImageQwc = pendingImageQw; + m_vif1PendingPath2DirectHl = directHl; } } diff --git a/ps2xTest/CMakeLists.txt b/ps2xTest/CMakeLists.txt index 39dbc65..5db04b9 100644 --- a/ps2xTest/CMakeLists.txt +++ b/ps2xTest/CMakeLists.txt @@ -5,31 +5,9 @@ project(ps2xTest LANGUAGES C CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) -if(PS2X_IOP_ENABLE_PLUGINS AND (WIN32 OR (UNIX AND NOT APPLE))) - add_library(ps2_iop_fake_plugin MODULE - src/fake_iop_plugin.cpp - ) - add_library(ps2_iop_bad_abi_plugin MODULE - src/fake_iop_bad_abi.c - ) - add_library(ps2_iop_missing_symbol_plugin MODULE - src/fake_iop_missing_symbol.cpp - ) - - foreach(plugin_target IN ITEMS - ps2_iop_fake_plugin - ps2_iop_bad_abi_plugin - ps2_iop_missing_symbol_plugin) - target_compile_features(${plugin_target} PRIVATE cxx_std_20) - target_include_directories(${plugin_target} PRIVATE - ${CMAKE_SOURCE_DIR}/ps2xIOP/include - ) - set_target_properties(${plugin_target} PROPERTIES - PREFIX "" - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/iop_test_plugins" - LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/iop_test_plugins" - ) - endforeach() +include(CTest) +if(BUILD_TESTING) + add_subdirectory(gs_cache) endif() # Static library with test logic (no main), used by ps2xStudio @@ -80,17 +58,6 @@ if(UNIX AND NOT APPLE) target_link_libraries(ps2_test_lib PRIVATE ${CMAKE_DL_LIBS}) endif() -if(TARGET ps2_iop_fake_plugin) - target_compile_definitions(ps2_test_lib PRIVATE - PS2X_TEST_IOP_PLUGIN_DIR="$" - ) - add_dependencies(ps2_test_lib - ps2_iop_fake_plugin - ps2_iop_bad_abi_plugin - ps2_iop_missing_symbol_plugin - ) -endif() - add_executable(ps2x_tests src/main.cpp $ diff --git a/ps2xTest/gs_cache/CMakeLists.txt b/ps2xTest/gs_cache/CMakeLists.txt new file mode 100644 index 0000000..6b7210a --- /dev/null +++ b/ps2xTest/gs_cache/CMakeLists.txt @@ -0,0 +1,59 @@ +cmake_minimum_required(VERSION 3.21) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + project(PS2GSCacheTests LANGUAGES CXX) + include(CTest) +endif() + +get_filename_component(PS2_GS_DEFAULT_RUNTIME_DIR "${CMAKE_CURRENT_LIST_DIR}/../../ps2xRuntime" ABSOLUTE) +set(PS2_GS_RUNTIME_DIR "${PS2_GS_DEFAULT_RUNTIME_DIR}" CACHE PATH "Runtime source to test") +option(PS2_GS_CACHE_SANITIZERS "Enable AddressSanitizer and UndefinedBehaviorSanitizer" OFF) +option(PS2_GS_CACHE_BUILD_MEMORY_TESTS "Build tests for the new shared cached-reader API" ON) + +find_package(Threads REQUIRED) +add_library(ps2_gs_cache_backend_under_test STATIC + "${PS2_GS_RUNTIME_DIR}/src/lib/gs/gs_cpu_backend.cpp" + "${PS2_GS_RUNTIME_DIR}/src/lib/gs/gs_frontend.cpp" + "${PS2_GS_RUNTIME_DIR}/src/lib/gs/ps2_gs_memory.cpp" +) +target_include_directories(ps2_gs_cache_backend_under_test PUBLIC "${PS2_GS_RUNTIME_DIR}/include") +target_compile_features(ps2_gs_cache_backend_under_test PUBLIC cxx_std_20) +target_compile_definitions(ps2_gs_cache_backend_under_test PRIVATE PS2_RUNTIME_LOGS=0 AGRESSIVE_LOGS=0) +target_link_libraries(ps2_gs_cache_backend_under_test PUBLIC Threads::Threads) +set_target_properties(ps2_gs_cache_backend_under_test PROPERTIES CXX_EXTENSIONS OFF) + +if(PS2_GS_CACHE_SANITIZERS) + if(NOT CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" OR MSVC) + message(FATAL_ERROR "PS2_GS_CACHE_SANITIZERS requires a GCC/Clang sanitizer toolchain") + endif() + target_compile_options(ps2_gs_cache_backend_under_test PUBLIC + -fsanitize=address,undefined -fno-omit-frame-pointer -fno-sanitize-recover=all) + target_link_options(ps2_gs_cache_backend_under_test PUBLIC -fsanitize=address,undefined) +endif() + +function(add_gs_cache_suite target source prefix) + add_executable(${target} "${source}") + target_link_libraries(${target} PRIVATE ps2_gs_cache_backend_under_test) + set_target_properties(${target} PROPERTIES CXX_EXTENSIONS OFF) + foreach(test_name IN LISTS ARGN) + add_test(NAME "gs_cache.${prefix}.${test_name}" COMMAND ${target} "${test_name}") + set_tests_properties("gs_cache.${prefix}.${test_name}" PROPERTIES LABELS "gs;cache" TIMEOUT 60) + endforeach() +endfunction() + +add_gs_cache_suite(ps2_gs_texture_cache_tests gs_texture_cache_tests.cpp texture + unaligned_texture unaligned_wrap stale_mirror page_alternation + flush_visibility upload_visibility local_copy_visibility raster_visibility + reset_and_rebind invalid_vram_size reserved_psm) + +add_gs_cache_suite(ps2_gs_clut_cache_tests gs_clut_cache_tests.cpp clut + unaligned_csm1_ct32 unaligned_csm1_ct16 unaligned_csm1_ct16s wrapped_clut unaligned_csm2 + retained_palette clut_uses_page_cache cbp0_conditional cbp1_conditional + reserved_cld nonindexed_cld tex2_reload shared_contexts + csa_ct32 csa_ct16 csa_ct16s texa_without_reload palette_before_filtering high_planes) + +if(PS2_GS_CACHE_BUILD_MEMORY_TESTS) + add_gs_cache_suite(ps2_gs_memory_cache_tests gs_memory_cache_tests.cpp memory + ct32 ct24 ct16 ct16s t8 t4 t8h t4hl t4hh z32 z24 z16 z16s + alias_lanes physical_tag_aliases) +endif() diff --git a/ps2xTest/gs_cache/gs_clut_cache_tests.cpp b/ps2xTest/gs_cache/gs_clut_cache_tests.cpp new file mode 100644 index 0000000..5bde0fd --- /dev/null +++ b/ps2xTest/gs_cache/gs_clut_cache_tests.cpp @@ -0,0 +1,283 @@ +#include "gs_test_support.h" + +using namespace GSTest; + +namespace +{ + template + void unalignedCsm1() + { + FrontendFixture f; + auto tex = texture(GS_PSM_T8, 64); + tex.cbp = 31; + tex.cpsm = Cpsm; + f.index(tex, 128); + f.palette(tex, 128, Cpsm == GS_PSM_CT32 ? kRed : 0x801Fu); + f.bind(tex); + expectEqual(f.sample(), kRed, "CSM1 CLUT load crosses a physical page"); + } + + void wrappedClut() + { + FrontendFixture f; + auto tex = texture(GS_PSM_T8, 64); + tex.cbp = 16383; + f.index(tex, 128); + f.palette(tex, 128, kGreen); + f.bind(tex); + expectEqual(f.sample(), kGreen, "CLUT load wraps at the end of VRAM"); + } + + void unalignedCsm2() + { + FrontendFixture f; + auto tex = texture(GS_PSM_T8, 64); + tex.cbp = 31; + tex.cpsm = GS_PSM_CT16; + tex.csm = 1; + constexpr uint32_t entry = 193; + f.index(tex, entry); + f.gs.writeRegister(GS_REG_TEXCLUT, 4ull | (3ull << 6) | (2ull << 12)); + f.gs.WriteVram(GS_PSM_CT16, tex.cbp, 4, 48 + entry, 2, 0x83E0); + f.bind(tex); + expectEqual(f.sample(), kGreen, "CSM2 CBW/COU/COV and swizzle carry"); + } + + void retainedPalette() + { + FrontendFixture f; + auto tex = texture(GS_PSM_T4, 64); + f.index(tex, 8); + f.palette(tex, 8, kRed); + f.bind(tex); + expectEqual(f.sample(), kRed, "initial palette"); + f.palette(tex, 8, kGreen); + f.flush(); + tex.cld = 0; + f.bind(tex); + expectEqual(f.sample(), kRed, "TEXFLUSH and CLD=0 preserve the CLUT temporary buffer"); + tex.cld = 1; + f.bind(tex); + expectEqual(f.sample(), kGreen, "CLD=1 reloads the palette"); + } + + void clutUsesPageCache() + { + FrontendFixture f; + auto tex = texture(GS_PSM_T4, 64); + f.index(tex, 8); + f.palette(tex, 8, kRed); + f.bind(tex); + // No texture sampling between loads: the CLUT source page is still resident. + f.palette(tex, 8, kGreen); + f.bind(tex); + expectEqual(f.sample(), kRed, "CLD=1 alone does not invalidate the texture page buffer"); + f.flush(); + f.bind(tex); + expectEqual(f.sample(), kGreen, "identical TEX0 write still loads after TEXFLUSH"); + } + + template + void conditionalLoad() + { + FrontendFixture f; + auto tex = texture(GS_PSM_T4, 64); + tex.cld = 2 + Bank; + f.index(tex, 0); + f.palette(tex, 0, kRed); + f.bind(tex); + auto other = tex; + other.cbp = 192; + other.cld = 3 - Bank; + f.palette(other, 0, kGreen); + f.bind(other); + tex.cld = 4 + Bank; + f.bind(tex); + expectEqual(f.sample(), kGreen, "matching CBP skips load, not switches palettes"); + tex.cbp = 160; + f.palette(tex, 0, kBlue); + f.flush(); + f.bind(tex); + expectEqual(f.sample(), kBlue, "different CBP loads and updates comparison memory"); + f.palette(tex, 0, kRed); + f.flush(); + f.bind(tex); + expectEqual(f.sample(), kBlue, "repeated conditional CBP skips reload"); + } + + void reservedCld() + { + FrontendFixture f; + auto tex = texture(GS_PSM_T4, 64); + f.index(tex, 0); + f.palette(tex, 0, kRed); + f.bind(tex); + tex.cbp = 192; + f.palette(tex, 0, kGreen); + for (uint8_t cld : {6, 7}) + { + tex.cld = cld; + f.flush(); + f.bind(tex); + expectEqual(f.sample(), kRed, "reserved CLD leaves palette unchanged"); + } + } + + void nonIndexedCld() + { + FrontendFixture f; + auto tex = texture(GS_PSM_T4, 64); + tex.cld = 2; + f.index(tex, 0); + f.palette(tex, 0, kRed); + f.bind(tex); + auto direct = tex; + direct.psm = GS_PSM_CT32; + direct.cbp = 192; + f.bind(direct); + f.palette(tex, 0, kGreen); + f.flush(); + tex.cld = 4; + f.bind(tex); + expectEqual(f.sample(), kRed, "direct texture TEX0 must not modify CBP0"); + } + + void tex2Reload() + { + FrontendFixture f; + auto tex = texture(GS_PSM_T8, 64); + f.index(tex, 128); + f.palette(tex, 128, kRed); + f.bind(tex); + expectEqual(f.sample(), kRed, "initial TEX0 palette"); + tex.cbp = 31; + f.palette(tex, 128, kGreen); + tex.tbp0 = 2048; + tex.tbw = 8; + tex.tw = tex.th = 9; + f.flush(); + f.bind(tex, 0, true); + expectEqual(f.sample(), kGreen, "TEX2 reloads from a crossing CLUT without changing texture layout"); + const auto state = f.gs.getDebugSnapshot(); + expectEqual(state.ctx[0].tex0.tbp0, 64, "TEX2 preserves TBP"); + expectEqual(state.ctx[0].tex0.tbw, 2, "TEX2 preserves TBW"); + expectEqual(state.ctx[0].tex0.tw, 8, "TEX2 preserves TW"); + } + + void sharedContexts() + { + FrontendFixture f; + auto tex = texture(GS_PSM_T4, 64); + f.index(tex, 0); + f.palette(tex, 0, kRed); + f.bind(tex, 0); + tex.cbp = 192; + f.palette(tex, 0, kGreen); + f.bind(tex, 1); + expectEqual(f.sample(0, 0, 0), kGreen, "both drawing contexts share one CLUT temporary buffer"); + tex.cbp = 256; + f.palette(tex, 0, kBlue); + f.bind(tex, 1, true); + expectEqual(f.sample(0, 0, 0), kBlue, "context 1 TEX2 changes palette visible to context 0"); + } + + template + void csaBanks() + { + FrontendFixture f; + auto tex = texture(GS_PSM_T4, 64); + tex.cpsm = Cpsm; + f.index(tex, 15); + f.palette(tex, 15, Cpsm == GS_PSM_CT32 ? kRed : 0x801Fu); + f.bind(tex); + auto other = tex; + other.cbp = 192; + other.csa = Cpsm == GS_PSM_CT32 ? 15 : 31; + f.palette(other, 15, Cpsm == GS_PSM_CT32 ? kGreen : 0x83E0u); + f.bind(other); + expectEqual(f.sample(), kGreen, "highest CSA bank is readable"); + tex.cld = 0; + tex.csa = Cpsm == GS_PSM_CT32 ? 16 : 0; + f.bind(tex); + expectEqual(f.sample(), kRed, "partial CLUT load retains unrelated banks and masks CSA per CPSM"); + } + + void texaWithoutReload() + { + FrontendFixture f; + auto tex = texture(GS_PSM_T4, 64); + tex.cpsm = GS_PSM_CT16; + f.index(tex, 0); + f.index(tex, 1, 1); + f.index(tex, 2, 2); + f.palette(tex, 0, 0x001F); + f.palette(tex, 1, 0x8000); + f.palette(tex, 2, 0x0000); + f.bind(tex); + f.gs.writeRegister(GS_REG_TEXA, 0x20ull | (0x40ull << 32)); + expectEqual(f.sample(), 0x200000F8, "TA0 applied at lookup"); + expectEqual(f.sample(1), 0x40000000, "TA1 applied to CLUT alpha bit"); + f.gs.writeRegister(GS_REG_TEXA, 0x70ull | (1ull << 15) | (0x60ull << 32)); + expectEqual(f.sample(), 0x700000F8, "TEXA changes without reloading raw palette"); + expectEqual(f.sample(1), 0x60000000, "AEM does not clear black with alpha bit set"); + expectEqual(f.sample(2), 0, "AEM clears zero color with alpha bit clear"); + } + + void paletteBeforeFiltering() + { + FrontendFixture f; + auto tex = texture(GS_PSM_T4, 64); + f.index(tex, 0, 0, 0); + f.index(tex, 2, 1, 0); + f.index(tex, 4, 0, 1); + f.index(tex, 6, 1, 1); + f.palette(tex, 0, kRed); + f.palette(tex, 2, kGreen); + f.palette(tex, 4, kBlue); + f.palette(tex, 6, 0x80F8F8F8); + f.palette(tex, 3, 0x80FF00FF); + f.bind(tex); + f.gs.writeRegister(GS_REG_TEX1_1, (1ull << 5) | (1ull << 6)); + expectEqual(f.sample(1, 1), 0x807C7C7C, "bilinear filtering blends four colors, never four indices"); + } + + void highPlanes() + { + FrontendFixture f; + auto low = texture(GS_PSM_T4HL, 31); + auto high = low; + high.psm = GS_PSM_T4HH; + high.cbp = 192; + high.csa = 1; + f.gs.WriteVram(GS_PSM_CT32, 31, 2, 8, 0, 0x00ABCDEF); + f.index(low, 3, 8); + f.index(high, 12, 8); + f.palette(low, 3, kRed); + f.palette(high, 12, kGreen); + f.bind(low); + f.bind(high); + low.cld = high.cld = 0; + f.bind(low); + expectEqual(f.sample(8), kRed, "low nibble uses its own CSA bank"); + f.bind(high); + expectEqual(f.sample(8), kGreen, "same cached physical bytes supply the high nibble"); + expectEqual(f.gs.ReadVram(GS_PSM_CT24, 31, 2, 8, 0), 0xABCDEF, "index writes preserve the RGB plane"); + } +} + +int main(int argc, char** argv) +{ + return run(argc, argv, { + {"unaligned_csm1_ct32", unalignedCsm1}, + {"unaligned_csm1_ct16", unalignedCsm1}, + {"unaligned_csm1_ct16s", unalignedCsm1}, + {"wrapped_clut", wrappedClut}, {"unaligned_csm2", unalignedCsm2}, + {"retained_palette", retainedPalette}, {"clut_uses_page_cache", clutUsesPageCache}, + {"cbp0_conditional", conditionalLoad<0>}, {"cbp1_conditional", conditionalLoad<1>}, + {"reserved_cld", reservedCld}, {"nonindexed_cld", nonIndexedCld}, + {"tex2_reload", tex2Reload}, {"shared_contexts", sharedContexts}, + {"csa_ct32", csaBanks}, {"csa_ct16", csaBanks}, + {"csa_ct16s", csaBanks}, {"texa_without_reload", texaWithoutReload}, + {"palette_before_filtering", paletteBeforeFiltering}, {"high_planes", highPlanes} + }); +} diff --git a/ps2xTest/gs_cache/gs_memory_cache_tests.cpp b/ps2xTest/gs_cache/gs_memory_cache_tests.cpp new file mode 100644 index 0000000..7f340bc --- /dev/null +++ b/ps2xTest/gs_cache/gs_memory_cache_tests.cpp @@ -0,0 +1,99 @@ +#include "gs_test_support.h" +#include "runtime/gs/ps2_gs_memory.h" + +using namespace GSTest; + +namespace +{ + template + void addressCoverage() + { + BackendFixture f; + GSMem::TexturePageCache cache; + uint32_t random = 0x51375A9Du; + for (auto& byte : f.vram) + { + random ^= random << 13; + random ^= random >> 17; + random ^= random << 5; + byte = static_cast(random); + } + constexpr auto mode = static_cast(Psm); + constexpr auto extent = GSMem::PixelStorageTraits::PageExtent(); + uint32_t checked = 0; + const auto check = [&](uint32_t base, uint32_t bw, uint32_t x, uint32_t y) + { + const auto direct = f.backend.ReadVram(Psm, base, bw, x, y); + const auto cached = GSMem::ReadTexture(cache, f.vram.data(), Psm, base, bw, x, y); + if (cached != direct) + { + std::ostringstream error; + error << "PSM=" << unsigned(Psm) << " BP=" << base << " BW=" << bw << " XY=" << x << ',' << y; + expectEqual(cached, direct, error.str()); + } + ++checked; + }; + // Every local texel, for every 256-byte base offset inside an 8 KiB page. + for (uint32_t offset = 0; offset < 32; ++offset) + for (uint32_t y = 0; y < extent.y; ++y) + for (uint32_t x = 0; x < extent.x; ++x) + check(32 + offset, 2, x, y); + + for (uint32_t base : {0u, 31u, 32u, 12160u, 16256u, 16383u}) + for (uint32_t bw : {0u, 1u, 2u, 3u, 7u, 8u, 10u, 63u}) + for (uint32_t y : {0u, 1u, 7u, 8u, 15u, 16u, 31u, 32u, 63u, 64u, 127u, 128u, 255u, 256u, 511u, 512u, 1023u, 2047u}) + for (uint32_t x : {0u, 1u, 7u, 8u, 15u, 16u, 31u, 32u, 63u, 64u, 127u, 128u, 255u, 256u, 511u, 512u, 1023u, 2047u}) + check(base, bw, x, y); + std::cout << checked << " cached/direct comparisons\n"; + } + + void aliasLanes() + { + BackendFixture f; + GSMem::TexturePageCache cache; + constexpr uint32_t base = 31; + const auto read = [&](uint32_t psm) + { + return GSMem::ReadTexture(cache, f.vram.data(), psm, base, 2, 8, 0); + }; + f.backend.WriteVram(GS_PSM_CT32, base, 2, 8, 0, 0xAB123456); + expectEqual(read(GS_PSM_T8H), 0xAB, "8H lane on a crossing page"); + f.backend.WriteVram(GS_PSM_CT24, base, 2, 8, 0, 0x654321); + expectEqual(read(GS_PSM_CT32), 0xAB123456, "changing PSM does not refresh the physical page"); + cache.Invalidate(); + expectEqual(read(GS_PSM_CT32), 0xAB654321, "CT24 upload preserves alpha"); + f.backend.WriteVram(GS_PSM_T4HL, base, 2, 8, 0, 5); + expectEqual(read(GS_PSM_T4HL), 11, "low nibble remains cached before flush"); + cache.Invalidate(); + expectEqual(read(GS_PSM_T4HL), 5, "low nibble after flush"); + expectEqual(read(GS_PSM_T4HH), 10, "high nibble preserved"); + expectEqual(read(GS_PSM_CT24), 0x654321, "RGB plane preserved"); + } + + void physicalTagAliases() + { + BackendFixture f; + GSMem::TexturePageCache cache; + f.backend.WriteVram(GS_PSM_CT32, 32, 2, 0, 0, kRed); + expectEqual(GSMem::ReadTexture(cache, f.vram.data(), GS_PSM_CT32, 32, 2, 0, 0), kRed, "prime aligned view"); + f.backend.WriteVram(GS_PSM_CT32, 32, 2, 0, 0, kGreen); + // Both descriptors resolve to the very same physical byte, so this is a hit. + expectEqual(GSMem::ReadTexture(cache, f.vram.data(), GS_PSM_CT32, 31, 2, 8, 0), kRed, "alias descriptor keeps the same cached bytes"); + cache.Invalidate(); + expectEqual(GSMem::ReadTexture(cache, f.vram.data(), GS_PSM_CT32, 31, 2, 8, 0), kGreen, "alias after flush"); + } +} + +int main(int argc, char** argv) +{ + return run(argc, argv, { + {"ct32", addressCoverage}, {"ct24", addressCoverage}, + {"ct16", addressCoverage}, {"ct16s", addressCoverage}, + {"t8", addressCoverage}, {"t4", addressCoverage}, + {"t8h", addressCoverage}, {"t4hl", addressCoverage}, + {"t4hh", addressCoverage}, {"z32", addressCoverage}, + {"z24", addressCoverage}, {"z16", addressCoverage}, + {"z16s", addressCoverage}, {"alias_lanes", aliasLanes}, + {"physical_tag_aliases", physicalTagAliases} + }); +} diff --git a/ps2xTest/gs_cache/gs_test_support.h b/ps2xTest/gs_cache/gs_test_support.h new file mode 100644 index 0000000..2b2c3ee --- /dev/null +++ b/ps2xTest/gs_cache/gs_test_support.h @@ -0,0 +1,190 @@ +#pragma once + +#include "runtime/gs/gs_cpu_backend.h" +#include "runtime/gs/gs_frontend.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace GSTest +{ + constexpr uint32_t kVramSize = 4u * 1024u * 1024u; + constexpr uint32_t kOutputPage = 200u; + constexpr uint32_t kRed = 0x800000F8u; + constexpr uint32_t kGreen = 0x8000F800u; + constexpr uint32_t kBlue = 0x80F80000u; + + inline void require(bool condition, std::string_view message) + { + if (!condition) + throw std::runtime_error(std::string(message)); + } + + inline void expectEqual(uint32_t actual, uint32_t expected, std::string_view message) + { + if (actual != expected) + { + std::ostringstream error; + error << message << ": expected 0x" << std::hex << expected << ", got 0x" << actual; + throw std::runtime_error(error.str()); + } + } + + struct Test + { + std::string_view name; + void (*run)(); + }; + + inline int run(int argc, char** argv, std::initializer_list tests) + { + try + { + if (argc != 2) + throw std::invalid_argument("Pass one test name; run the complete suite with CTest."); + for (const Test& test : tests) + { + if (test.name == argv[1]) + { + test.run(); + std::cout << "PASS " << test.name << '\n'; + return 0; + } + } + throw std::invalid_argument("Unknown test name: " + std::string(argv[1])); + } + catch (const std::exception& error) + { + std::cerr << "FAIL: " << error.what() << '\n'; + return 1; + } + } + + inline GSTex0Reg texture(uint8_t psm = GS_PSM_CT32, uint32_t base = 32u) + { + GSTex0Reg tex{}; + tex.tbp0 = base; + tex.tbw = 2; + tex.psm = psm; + tex.tw = tex.th = 8; + tex.tcc = tex.tfx = 1; + tex.cbp = 128; + tex.cpsm = GS_PSM_CT32; + tex.cld = 1; + return tex; + } + + inline uint64_t encodeTex0(const GSTex0Reg& tex) + { + return uint64_t(tex.tbp0) | (uint64_t(tex.tbw) << 14) | (uint64_t(tex.psm) << 20) | + (uint64_t(tex.tw) << 26) | (uint64_t(tex.th) << 30) | (uint64_t(tex.tcc) << 34) | + (uint64_t(tex.tfx) << 35) | (uint64_t(tex.cbp) << 37) | (uint64_t(tex.cpsm) << 51) | + (uint64_t(tex.csm) << 55) | (uint64_t(tex.csa) << 56) | (uint64_t(tex.cld) << 61); + } + + inline GSPrimitiveBatch sprite(const GSTex0Reg& tex, uint32_t x, uint32_t y, bool linear = false) + { + GSPrimitiveBatch batch{}; + batch.vertexCount = 2; + auto& state = batch.state; + state.prim.type = GS_PRIM_SPRITE; + state.prim.tme = state.prim.fst = true; + state.context.frame.fbp = kOutputPage; + state.context.frame.fbw = 1; + state.context.zbuf.zmask = true; + state.context.test = 1ull << 17; + state.context.tex0 = tex; + state.context.clamp = 5; // Clamp both axes. + state.textureWidth = state.textureHeight = 256; + state.texa.ta0 = state.texa.ta1 = 128; + state.linearFilter = linear; + for (auto& vertex : batch.vertices) + { + vertex.r = vertex.g = vertex.b = vertex.a = 128; + vertex.u = static_cast(x * 16u); + vertex.v = static_cast(y * 16u); + } + batch.vertices[1].x = batch.vertices[1].y = 1; + return batch; + } + + struct BackendFixture + { + std::vector vram = std::vector(kVramSize); + GSCpuBackend backend; + + BackendFixture() + { + backend.Initialize(vram.data(), static_cast(vram.size())); + } + + uint32_t sample(const GSTex0Reg& tex, uint32_t x = 0, uint32_t y = 0, bool linear = false) + { + backend.Submit(sprite(tex, x, y, linear)); + return backend.ReadVram(GS_PSM_CT32, kOutputPage * 32u, 1u, 0u, 0u); + } + }; + + struct FrontendFixture + { + std::vector vram = std::vector(kVramSize); + GS gs; + + FrontendFixture() + { + gs.init(vram.data(), static_cast(vram.size()), nullptr); + for (uint8_t context = 0; context < 2; ++context) + { + gs.writeRegister(context ? GS_REG_FRAME_2 : GS_REG_FRAME_1, kOutputPage | (1ull << 16)); + gs.writeRegister(context ? GS_REG_ZBUF_2 : GS_REG_ZBUF_1, 1ull << 32); + gs.writeRegister(context ? GS_REG_SCISSOR_2 : GS_REG_SCISSOR_1, 0); + gs.writeRegister(context ? GS_REG_TEST_2 : GS_REG_TEST_1, 0x30000); + gs.writeRegister(context ? GS_REG_CLAMP_2 : GS_REG_CLAMP_1, 5); + } + gs.writeRegister(GS_REG_TEXA, 128ull | (128ull << 32)); + } + + void bind(const GSTex0Reg& tex, uint8_t context = 0, bool tex2 = false) + { + const uint8_t reg = tex2 ? (context ? GS_REG_TEX2_2 : GS_REG_TEX2_1) + : (context ? GS_REG_TEX0_2 : GS_REG_TEX0_1); + gs.writeRegister(reg, encodeTex0(tex)); + } + + void flush() + { + gs.writeRegister(GS_REG_TEXFLUSH, 0); + } + + void index(const GSTex0Reg& tex, uint32_t value, uint32_t x = 0, uint32_t y = 0) + { + gs.WriteVram(tex.psm, tex.tbp0, tex.tbw, x, y, value); + } + + void palette(const GSTex0Reg& tex, uint32_t entry, uint32_t value) + { + // CSM1 source layout; CSA selects the destination, not this source. + const uint32_t position = (entry & ~0x18u) | ((entry & 8u) << 1u) | ((entry & 16u) >> 1u); + gs.WriteVram(tex.cpsm, tex.cbp, 1, position & 15u, position >> 4u, value); + } + + uint32_t sample(uint32_t x = 0, uint32_t y = 0, uint8_t context = 0) + { + gs.writeRegister(GS_REG_PRIM, GS_PRIM_SPRITE | (1ull << 4) | (1ull << 8) | (uint64_t(context) << 9)); + gs.writeRegister(GS_REG_RGBAQ, 0x80808080); + const uint64_t uv = (uint64_t(y * 16u) << 16) | uint64_t(x * 16u); + gs.writeRegister(GS_REG_UV, uv); + gs.writeRegister(GS_REG_XYZ2, 0); + gs.writeRegister(GS_REG_UV, uv); + gs.writeRegister(GS_REG_XYZ2, 16ull | (16ull << 16)); + return gs.ReadVram(GS_PSM_CT32, kOutputPage * 32u, 1, 0, 0); + } + }; +} diff --git a/ps2xTest/gs_cache/gs_texture_cache_tests.cpp b/ps2xTest/gs_cache/gs_texture_cache_tests.cpp new file mode 100644 index 0000000..efd10e7 --- /dev/null +++ b/ps2xTest/gs_cache/gs_texture_cache_tests.cpp @@ -0,0 +1,177 @@ +#include "gs_test_support.h" + +#include + +using namespace GSTest; + +namespace +{ + void unalignedTexture() + { + BackendFixture f; + auto tex = texture(GS_PSM_CT32, 31); + // TBP=31, CT32(8,0): block 31 + swizzled block 1 = physical page 1. + std::memcpy(f.vram.data() + 8192u, &kRed, sizeof(kRed)); + expectEqual(f.sample(tex, 8), kRed, "non-page-aligned texture base"); + } + + void unalignedWrap() + { + BackendFixture f; + auto tex = texture(GS_PSM_CT32, 16383); + std::memcpy(f.vram.data(), &kGreen, sizeof(kGreen)); + expectEqual(f.sample(tex, 8), kGreen, "swizzle carry wraps through the 4 MiB boundary"); + } + + void staleMirror() + { + BackendFixture f; + auto tex = texture(); + f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kRed); + expectEqual(f.sample(tex), kRed, "prime the following physical page"); + f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kGreen); + f.backend.TextureFlush(); + tex.tbp0 = 31; + expectEqual(f.sample(tex, 8), kGreen, "TEXFLUSH must not expose stale bytes in the old mirror"); + } + + void pageAlternation() + { + BackendFixture f; + auto tex = texture(GS_PSM_CT32, 31); + f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kRed); + f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 8, 0, kGreen); + for (unsigned i = 0; i < 8; ++i) + { + expectEqual(f.sample(tex), kRed, "first physical page"); + expectEqual(f.sample(tex, 8), kGreen, "second physical page in the same logical page"); + } + } + + void flushVisibility() + { + BackendFixture f; + auto tex = texture(); + f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kRed); + expectEqual(f.sample(tex), kRed, "initial cache fill"); + f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kGreen); + expectEqual(f.backend.ReadVram(tex.psm, tex.tbp0, tex.tbw, 0, 0), kGreen, "canonical VRAM changes immediately"); + f.backend.Flush(); + f.backend.Sync(GSSyncReason::Finish); + expectEqual(f.sample(tex), kRed, "ordinary flush and FINISH do not invalidate texels"); + f.backend.TextureFlush(); + expectEqual(f.sample(tex), kGreen, "TEXFLUSH exposes the updated texels"); + } + + void uploadVisibility() + { + BackendFixture f; + auto tex = texture(); + f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kRed); + expectEqual(f.sample(tex), kRed, "prime destination"); + GSTransferCommand transfer{}; + transfer.direction = 0; + transfer.bitbltbuf.dbp = tex.tbp0; + transfer.bitbltbuf.dbw = tex.tbw; + transfer.bitbltbuf.dpsm = tex.psm; + transfer.trxreg.rrw = transfer.trxreg.rrh = 1; + f.backend.BeginTransfer(transfer); + f.backend.UploadImage(reinterpret_cast(&kGreen), sizeof(kGreen)); + expectEqual(f.sample(tex), kRed, "host upload does not implicitly flush texels"); + f.backend.TextureFlush(); + expectEqual(f.sample(tex), kGreen, "host upload visible after TEXFLUSH"); + } + + void localCopyVisibility() + { + BackendFixture f; + auto tex = texture(); + f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kRed); + f.backend.WriteVram(tex.psm, 96, tex.tbw, 0, 0, kGreen); + expectEqual(f.sample(tex), kRed, "prime destination"); + GSTransferCommand transfer{}; + transfer.direction = 2; + transfer.bitbltbuf.sbp = 96; + transfer.bitbltbuf.sbw = transfer.bitbltbuf.dbw = tex.tbw; + transfer.bitbltbuf.spsm = transfer.bitbltbuf.dpsm = tex.psm; + transfer.bitbltbuf.dbp = tex.tbp0; + transfer.trxreg.rrw = transfer.trxreg.rrh = 1; + f.backend.BeginTransfer(transfer); + expectEqual(f.sample(tex), kRed, "local copy does not implicitly flush texels"); + f.backend.TextureFlush(); + expectEqual(f.sample(tex), kGreen, "local copy visible after TEXFLUSH"); + } + + void rasterVisibility() + { + BackendFixture f; + auto tex = texture(); + f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kRed); + expectEqual(f.sample(tex), kRed, "prime render target as texture"); + auto batch = sprite(tex, 0, 0); + batch.state.prim.tme = false; + batch.state.context.frame.fbp = tex.tbp0 / 32; + for (auto& vertex : batch.vertices) + { + vertex.r = 0; + vertex.g = 248; + vertex.b = 0; + } + f.backend.Submit(batch); + expectEqual(f.sample(tex), kRed, "raster writes do not implicitly flush texels"); + f.backend.TextureFlush(); + expectEqual(f.sample(tex), kGreen, "render-to-texture visible after TEXFLUSH"); + } + + void resetAndRebind() + { + BackendFixture f; + auto tex = texture(); + f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kRed); + expectEqual(f.sample(tex), kRed, "prime cache"); + f.backend.WriteVram(tex.psm, tex.tbp0, tex.tbw, 0, 0, kGreen); + f.backend.Reset(); + expectEqual(f.sample(tex), kGreen, "reset invalidates without clearing VRAM"); + std::vector other(kVramSize); + std::memcpy(other.data() + 8192u, &kBlue, sizeof(kBlue)); + f.backend.Initialize(other.data(), static_cast(other.size())); + expectEqual(f.sample(tex), kBlue, "initialize invalidates the previous VRAM allocation"); + } + + void invalidVramSize() + { + BackendFixture f; + std::vector shortVram(8192); + bool rejected = false; + try { f.backend.Initialize(shortVram.data(), static_cast(shortVram.size())); } + catch (const std::invalid_argument&) { rejected = true; } + require(rejected, "undersized VRAM must be rejected before masked accesses can escape it"); + f.backend.WriteVram(GS_PSM_CT32, 32, 2, 0, 0, kGreen); + expectEqual(f.sample(texture()), kGreen, "failed initialize preserves the existing backend binding"); + f.backend.Initialize(nullptr, 0); + expectEqual(f.backend.ReadVram(GS_PSM_CT32, 32, 2, 0, 0), 0, "null binding is safe"); + } + + void reservedPsm() + { + BackendFixture f; + auto tex = texture(0x3F); + f.backend.WriteVram(GS_PSM_CT32, 32, 2, 0, 0, kGreen); + f.backend.WriteVram(0x3F, 32, 2, 0, 0, kRed); + expectEqual(f.backend.ReadVram(GS_PSM_CT32, 32, 2, 0, 0), kGreen, "reserved writes are no-op"); + expectEqual(f.backend.ReadVram(0x3F, 32, 2, 0, 0), 0, "reserved raw reads use null semantics"); + expectEqual(f.sample(tex), 0xFFFF00FFu, "reserved sampling preserves the existing magenta diagnostic"); + } +} + +int main(int argc, char** argv) +{ + return run(argc, argv, { + {"unaligned_texture", unalignedTexture}, {"unaligned_wrap", unalignedWrap}, + {"stale_mirror", staleMirror}, {"page_alternation", pageAlternation}, + {"flush_visibility", flushVisibility}, {"upload_visibility", uploadVisibility}, + {"local_copy_visibility", localCopyVisibility}, {"raster_visibility", rasterVisibility}, + {"reset_and_rebind", resetAndRebind}, {"invalid_vram_size", invalidVramSize}, + {"reserved_psm", reservedPsm} + }); +} diff --git a/ps2xTest/src/code_generator_tests.cpp b/ps2xTest/src/code_generator_tests.cpp index 1a1f370..463b6cf 100644 --- a/ps2xTest/src/code_generator_tests.cpp +++ b/ps2xTest/src/code_generator_tests.cpp @@ -168,6 +168,43 @@ void register_code_generator_tests() { MiniTest::Case("CodeGenerator", [](TestCase &tc) { + tc.Run("Generated sources cannot be shadowed by stale local declaration headers", [](TestCase &t) { + Function func; + func.name = "header_lookup"; + func.start = 0x8F00; + func.end = 0x8F04; + func.isRecompiled = true; + + CodeGenerator gen({}, {}); + gen.setRenamedFunctions({{func.start, "header_lookup_0x8f00"}}); + + const std::string generated = gen.generateFunction(func, {makeNop(func.start)}, true); + const std::string registration = gen.generateFunctionRegistration({func}, {}); + + t.IsTrue(generated.find("#include ") != std::string::npos, + "function sources must resolve declarations through the configured include path"); + t.IsTrue(generated.find("#include ") != std::string::npos, + "function sources must resolve stub declarations through the configured include path"); + t.IsTrue(registration.find("#include ") != std::string::npos, + "the registration source must use the same unambiguous declaration header"); + t.IsTrue(registration.find("#include ") != std::string::npos, + "the registration source must use the same unambiguous stub header"); + }); + + tc.Run("unsigned integer loads use explicit zero extension", [](TestCase &t) { + CodeGenerator gen({}, {}); + const std::string lbu = gen.translateInstruction(makeIType(0x8F10, OPCODE_LBU, 1, 2, 0x10)); + const std::string lhu = gen.translateInstruction(makeIType(0x8F14, OPCODE_LHU, 1, 3, 0x12)); + const std::string lwu = gen.translateInstruction(makeIType(0x8F18, OPCODE_LWU, 1, 4, 0x14)); + + t.IsTrue(lbu.find("SET_GPR_ZE32(ctx, 2") != std::string::npos, + "LBU must zero-extend into the low 64-bit scalar lane"); + t.IsTrue(lhu.find("SET_GPR_ZE32(ctx, 3") != std::string::npos, + "LHU must zero-extend into the low 64-bit scalar lane"); + t.IsTrue(lwu.find("SET_GPR_ZE32(ctx, 4") != std::string::npos, + "LWU must not sign-extend bit 31 into the allocator bitmap value"); + }); + tc.Run("SYSCALL publishes its continuation before entering the runtime", [](TestCase &t) { Function func; func.name = "syscall_resume"; @@ -196,6 +233,42 @@ void register_code_generator_tests() "the continuation PC must be visible before a syscall can transfer to the scheduler"); }); + tc.Run("SYSCALL fallthrough is a resumable entry", [](TestCase &t) { + Function func; + func.name = "syscall_resume_entry"; + func.start = 0x9100; + func.end = 0x9108; + func.isRecompiled = true; + + Instruction syscall{}; + syscall.address = 0x9100; + syscall.opcode = OPCODE_SPECIAL; + syscall.function = SPECIAL_SYSCALL; + syscall.raw = (0x83u << 6) | SPECIAL_SYSCALL; + + Instruction after = makeNop(0x9104); + CodeGenerator gen({}, {}); + CodeGenerator::AnalysisResult analysis = + gen.collectInternalBranchTargets(func, {syscall, after}); + + t.IsTrue(analysis.resumeEntryPoints.contains(0x9104u), + "a syscall can yield through a guest override, so its fallthrough must be resumable"); + + const std::string generated = gen.generateFunction(func, {syscall, after}, false); + t.IsTrue(generated.find("case 0x9104u: goto label_9104;") != std::string::npos, + "the owner wrapper must resume directly after the syscall"); + + gen.setRenamedFunctions({{0x9100u, "syscall_resume_entry_0x9100"}}); + gen.setResumeEntryTargets({{0x9100u, + std::vector(analysis.resumeEntryPoints.begin(), + analysis.resumeEntryPoints.end())}}); + const std::string registration = gen.generateFunctionRegistration({func}, {}); + t.IsTrue(registration.find( + "g_ps2RecompiledFunctionTable[1] = syscall_resume_entry_0x9100; // 0x9104") != + std::string::npos, + "the syscall continuation must register to the owner wrapper"); + }); + tc.Run("R5900 MULT writes rd when rd is non-zero", [](TestCase &t) { CodeGenerator gen({}, {}); @@ -258,6 +331,21 @@ void register_code_generator_tests() "constant MMIO SW should not go through WRITE32 address classification"); }); + tc.Run("stale MMIO annotation does not replace the guest effective address", [](TestCase &t) { + Instruction store = makeSw(0x1100, 2, 1, 0); + store.isMmio = true; + store.mmioAddress = 0x10000000u; // A stale analyzer hint; $at still owns the real address. + + CodeGenerator gen({}, {}); + const std::string generated = gen.translateInstruction(store); + printGeneratedCode("stale MMIO annotation does not replace the guest effective address", generated); + + t.IsTrue(generated.find("runtime->Store32(rdram, ctx, ADD32(GPR_U32(ctx, 1), 0), GPR_U32(ctx, 2))") != std::string::npos, + "MMIO annotations should select runtime access without hard-coding a possibly stale address"); + t.IsTrue(generated.find("0x10000000u") == std::string::npos, + "stale MMIO address should not replace the address calculated by guest registers"); + }); + tc.Run("constant RDRAM load and store emit fast memory access", [](TestCase &t) { Function func; func.name = "rdram_access"; @@ -646,6 +734,46 @@ void register_code_generator_tests() "multiple resume pcs should register to the same owner wrapper"); }); + tc.Run("configured internal guest handlers register to their owner wrapper", [](TestCase &t) { + Function owner; + owner.name = "sdk_bootstrap_owner"; + owner.start = 0x7000; + owner.end = 0x7020; + owner.isRecompiled = true; + owner.isStub = false; + + std::vector instructions{ + makeNop(0x7000), makeNop(0x7004), makeNop(0x7008), makeNop(0x700C), + makeNop(0x7010), makeNop(0x7014), makeNop(0x7018), makeNop(0x701C)}; + std::vector functions{owner}; + std::unordered_map> decoded{{owner.start, instructions}}; + std::unordered_map> targetsByOwner; + + const size_t added = PS2Recompiler::CollectInternalEntryTargets( + functions, decoded, {0x7008u, 0x7018u}, targetsByOwner); + + t.IsTrue(added == 2u, + "both address-qualified internal handlers should be promoted"); + t.IsTrue(targetsByOwner.at(owner.start).size() == 2u, + "both handlers should belong to the containing generated wrapper"); + + CodeGenerator gen({}, {}); + gen.setRenamedFunctions({{owner.start, "sdk_bootstrap_owner_0x7000"}}); + gen.setResumeEntryTargets(targetsByOwner); + + const std::string generated = gen.generateFunction(owner, instructions, false); + const std::string registration = gen.generateFunctionRegistration(functions, {}); + + t.IsTrue(generated.find("case 0x7008u: goto label_7008;") != std::string::npos, + "the owner must enter directly at the first installed handler"); + t.IsTrue(generated.find("case 0x7018u: goto label_7018;") != std::string::npos, + "the owner must enter directly at the second installed handler"); + t.IsTrue(registration.find("sdk_bootstrap_owner_0x7000; // 0x7008") != std::string::npos, + "the first handler address must dispatch to its owner wrapper"); + t.IsTrue(registration.find("sdk_bootstrap_owner_0x7000; // 0x7018") != std::string::npos, + "the second handler address must dispatch to its owner wrapper"); + }); + tc.Run("external mid-function entry can register to the owner wrapper", [](TestCase &t) { Function caller; caller.name = "caller"; diff --git a/ps2xTest/src/elf_analyzer_tests.cpp b/ps2xTest/src/elf_analyzer_tests.cpp index 0a6dc10..8cba9b5 100644 --- a/ps2xTest/src/elf_analyzer_tests.cpp +++ b/ps2xTest/src/elf_analyzer_tests.cpp @@ -72,6 +72,8 @@ void register_elf_analyzer_tests() "libdma memclr should resolve to a runtime stub"); t.IsTrue(FunctionClassifier::hasRuntimeHandler("__divdi3"), "libgcc 64-bit division should resolve to a runtime stub"); + t.IsFalse(FunctionClassifier::hasRuntimeHandler("GetRomName"), + "ABI-incompatible GetRomName variants must be recompiled instead of name-stubbed"); t.IsFalse(FunctionClassifier::hasRuntimeHandler("__sbprintf"), "optional stdio internals should not resolve as automatic runtime stubs"); t.IsFalse(FunctionClassifier::hasRuntimeHandler("__sprint"), diff --git a/ps2xTest/src/fake_iop_bad_abi.c b/ps2xTest/src/fake_iop_bad_abi.c deleted file mode 100644 index 5d794b8..0000000 --- a/ps2xTest/src/fake_iop_bad_abi.c +++ /dev/null @@ -1,15 +0,0 @@ -#include "ps2x/iop/plugin_api.h" - -PS2X_IOP_PLUGIN_EXPORT int32_t ps2x_iop_query_v1( - uint32_t host_abi_version, - ps2x_iop_plugin_api_v1 *plugin_api) -{ - (void)host_abi_version; - if (!plugin_api || plugin_api->struct_size < sizeof(*plugin_api)) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - plugin_api->abi_version = PS2X_IOP_ABI_VERSION_V1 + 1u; - plugin_api->struct_size = sizeof(*plugin_api); - return PS2X_IOP_STATUS_OK_V1; -} diff --git a/ps2xTest/src/fake_iop_missing_symbol.cpp b/ps2xTest/src/fake_iop_missing_symbol.cpp deleted file mode 100644 index 08701f2..0000000 --- a/ps2xTest/src/fake_iop_missing_symbol.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "ps2x/iop/plugin_api.h" - -extern "C" PS2X_IOP_PLUGIN_EXPORT int32_t ps2x_iop_not_the_query_symbol() -{ - return PS2X_IOP_STATUS_OK_V1; -} diff --git a/ps2xTest/src/fake_iop_plugin.cpp b/ps2xTest/src/fake_iop_plugin.cpp deleted file mode 100644 index dc8b13b..0000000 --- a/ps2xTest/src/fake_iop_plugin.cpp +++ /dev/null @@ -1,350 +0,0 @@ -#include "ps2x/iop/plugin_api.h" - -#include -#include -#include - -namespace -{ - constexpr uint32_t kSyntheticSid = 0xF00DCAFEu; - constexpr uint32_t kCoreCollisionSid = 0x80001300u; - constexpr uint32_t kSyntheticFunction = 0x42u; - constexpr uint32_t kCoreCollisionFunction = 0x99u; - constexpr uint32_t kSyntheticEntryPoint = 0x00123456u; - constexpr uint32_t kSpecificRecvXEntryPoint = kSyntheticEntryPoint + 0x100u; - constexpr uint32_t kSyntheticCrc32 = 0xA1B2C3D4u; - constexpr uint32_t kResponseXor = 0xA5A55A5Au; - constexpr uint32_t kCoreCollisionResponse = 0xC0DEF00Du; - - template - constexpr ps2x_iop_string_view_v1 stringView(const char (&value)[Size]) - { - return {value, Size - 1u}; - } - - struct FakePluginState - { - const ps2x_iop_host_api_v1 *host = nullptr; - uint64_t resetGeneration = 0; - uint64_t rpcCalls = 0; - uint64_t transfers = 0; - }; - - void log(FakePluginState *state, uint32_t level, ps2x_iop_string_view_v1 message) - { - if (state && state->host && state->host->log) - { - state->host->log(state->host->userdata, level, message); - } - } - - void *createProfile(const ps2x_iop_host_api_v1 *host, - const ps2x_iop_game_identity_v1 *identity) - { - if (!host || host->abi_version != PS2X_IOP_ABI_VERSION_V1 || - host->struct_size < sizeof(*host) || !identity || - identity->struct_size < sizeof(*identity) || - (identity->entry_point != kSyntheticEntryPoint && - identity->entry_point != kSpecificRecvXEntryPoint) || - identity->crc32 != kSyntheticCrc32) - { - return nullptr; - } - - auto *state = new (std::nothrow) FakePluginState{}; - if (!state) - { - return nullptr; - } - state->host = host; - log(state, PS2X_IOP_LOG_INFO_V1, stringView("fake-plugin-create")); - return state; - } - - void destroyProfile(void *instance) - { - auto *state = static_cast(instance); - log(state, PS2X_IOP_LOG_INFO_V1, stringView("fake-plugin-destroy")); - delete state; - } - - int32_t resetProfile(void *instance) - { - auto *state = static_cast(instance); - if (!state) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - ++state->resetGeneration; - state->rpcCalls = 0; - state->transfers = 0; - return PS2X_IOP_STATUS_OK_V1; - } - - uint32_t selectRpcAbi(void *instance, const ps2x_iop_rpc_abi_request_v1 *request) - { - if (!instance || !request || request->struct_size < sizeof(*request)) - { - return PS2X_IOP_RPC_ABI_DEFAULT_V1; - } - if (request->bound_sid == kSyntheticSid && request->function == kSyntheticFunction) - { - return PS2X_IOP_RPC_ABI_STACK_V1; - } - return PS2X_IOP_RPC_ABI_DEFAULT_V1; - } - - int32_t handleRpc(void *instance, - const ps2x_iop_rpc_request_v1 *request, - ps2x_iop_rpc_result_v1 *result) - { - auto *state = static_cast(instance); - if (!state || !request || request->struct_size < sizeof(*request) || - !result || result->struct_size < sizeof(*result)) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - - result->handled = 0u; - result->result_address = 0u; - result->signal_nowait_completion = 0u; - result->callback_policy = PS2X_IOP_CALLBACK_RUNTIME_DEFAULT_V1; - if (request->sid == kCoreCollisionSid && - request->function == kCoreCollisionFunction) - { - if (request->receive.size < sizeof(kCoreCollisionResponse) || - !state->host->write_guest || - state->host->write_guest(state->host->userdata, - request->receive.address, - &kCoreCollisionResponse, - sizeof(kCoreCollisionResponse)) != - PS2X_IOP_STATUS_OK_V1) - { - return PS2X_IOP_STATUS_FAILED_V1; - } - ++state->rpcCalls; - result->handled = 1u; - result->result_address = request->receive.address; - return PS2X_IOP_STATUS_OK_V1; - } - - if (request->sid != kSyntheticSid || request->function != kSyntheticFunction) - { - return PS2X_IOP_STATUS_OK_V1; - } - - uint32_t input = 0u; - if (request->send.size < sizeof(input) || !state->host->read_guest || - state->host->read_guest(state->host->userdata, - request->send.address, - &input, - sizeof(input)) != PS2X_IOP_STATUS_OK_V1) - { - return PS2X_IOP_STATUS_FAILED_V1; - } - - const uint32_t output = input ^ kResponseXor; - if (request->receive.size < sizeof(output) || !state->host->write_guest || - state->host->write_guest(state->host->userdata, - request->receive.address, - &output, - sizeof(output)) != PS2X_IOP_STATUS_OK_V1) - { - return PS2X_IOP_STATUS_FAILED_V1; - } - - ++state->rpcCalls; - result->handled = 1u; - result->result_address = request->receive.address; - result->signal_nowait_completion = 1u; - result->callback_policy = PS2X_IOP_CALLBACK_SUPPRESS_V1; - return PS2X_IOP_STATUS_OK_V1; - } - - int32_t onSifTransfer(void *instance, const ps2x_iop_sif_transfer_v1 *transfer) - { - auto *state = static_cast(instance); - if (!state || !transfer || transfer->struct_size < sizeof(*transfer)) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - ++state->transfers; - return PS2X_IOP_STATUS_OK_V1; - } - - size_t debugMetricCount(void *instance) - { - return instance ? 3u : 0u; - } - - int32_t debugMetric(void *instance, size_t index, ps2x_iop_debug_metric_v1 *metric) - { - const auto *state = static_cast(instance); - if (!state || !metric || metric->struct_size < sizeof(*metric) || index >= 3u) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - - metric->struct_size = sizeof(*metric); - metric->hexadecimal = 0u; - switch (index) - { - case 0u: - metric->name = stringView("reset_generation"); - metric->value = state->resetGeneration; - break; - case 1u: - metric->name = stringView("rpc_calls"); - metric->value = state->rpcCalls; - break; - case 2u: - metric->name = stringView("sif_transfers"); - metric->value = state->transfers; - break; - default: - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - return PS2X_IOP_STATUS_OK_V1; - } - - constexpr uint32_t kSids[] = {kSyntheticSid, kCoreCollisionSid}; - constexpr uint32_t kDuplicateSids[] = {kSyntheticSid, kSyntheticSid}; - constexpr uint32_t kAmbiguousSids[] = {kSyntheticSid}; - const ps2x_iop_profile_api_v1 kProfiles[] = { - { - PS2X_IOP_ABI_VERSION_V1, - sizeof(ps2x_iop_profile_api_v1), - stringView("synthetic-test-profile"), - { - sizeof(ps2x_iop_game_matcher_v1), - stringView("synthetic_iop_test.elf"), - kSyntheticEntryPoint, - kSyntheticCrc32, - }, - 2u, - kSids, - &createProfile, - &destroyProfile, - &resetProfile, - &selectRpcAbi, - &handleRpc, - &onSifTransfer, - &debugMetricCount, - &debugMetric, - }, - { - PS2X_IOP_ABI_VERSION_V1, - sizeof(ps2x_iop_profile_api_v1), - stringView("synthetic-duplicate-sid-profile"), - { - sizeof(ps2x_iop_game_matcher_v1), - stringView("synthetic_duplicate.elf"), - kSyntheticEntryPoint, - kSyntheticCrc32, - }, - 2u, - kDuplicateSids, - &createProfile, - &destroyProfile, - &resetProfile, - &selectRpcAbi, - &handleRpc, - &onSifTransfer, - &debugMetricCount, - &debugMetric, - }, - { - PS2X_IOP_ABI_VERSION_V1, - sizeof(ps2x_iop_profile_api_v1), - stringView("synthetic-ambiguous-recvx-profile"), - { - sizeof(ps2x_iop_game_matcher_v1), - stringView("slus_201.84"), - 0u, - 0u, - }, - 1u, - kAmbiguousSids, - &createProfile, - &destroyProfile, - &resetProfile, - &selectRpcAbi, - &handleRpc, - &onSifTransfer, - &debugMetricCount, - &debugMetric, - }, - { - PS2X_IOP_ABI_VERSION_V1, - sizeof(ps2x_iop_profile_api_v1), - stringView("synthetic-specific-recvx-profile"), - { - sizeof(ps2x_iop_game_matcher_v1), - stringView("slus_201.84"), - kSpecificRecvXEntryPoint, - kSyntheticCrc32, - }, - 1u, - kAmbiguousSids, - &createProfile, - &destroyProfile, - &resetProfile, - &selectRpcAbi, - &handleRpc, - &onSifTransfer, - &debugMetricCount, - &debugMetric, - }, - { - PS2X_IOP_ABI_VERSION_V1, - sizeof(ps2x_iop_profile_api_v1), - stringView("synthetic-invalid-large-sid-profile"), - { - sizeof(ps2x_iop_game_matcher_v1), - stringView("synthetic_invalid.elf"), - kSyntheticEntryPoint, - kSyntheticCrc32, - }, - 257u, - kAmbiguousSids, - &createProfile, - &destroyProfile, - &resetProfile, - &selectRpcAbi, - &handleRpc, - &onSifTransfer, - &debugMetricCount, - &debugMetric, - }, - }; - - const ps2x_iop_plugin_api_v1 kPlugin = { - PS2X_IOP_ABI_VERSION_V1, - sizeof(ps2x_iop_plugin_api_v1), - stringView("ps2x-test-plugin"), - stringView("1.0.0"), - 5u, - kProfiles, - }; -} - -extern "C" PS2X_IOP_PLUGIN_EXPORT int32_t ps2x_iop_query_v1( - uint32_t hostAbiVersion, - ps2x_iop_plugin_api_v1 *pluginApi) -{ - if (hostAbiVersion != PS2X_IOP_ABI_VERSION_V1) - { - return PS2X_IOP_STATUS_UNSUPPORTED_V1; - } - if (!pluginApi) - { - return PS2X_IOP_STATUS_INVALID_ARGUMENT_V1; - } - if (pluginApi->struct_size < sizeof(*pluginApi)) - { - return PS2X_IOP_STATUS_BUFFER_TOO_SMALL_V1; - } - - *pluginApi = kPlugin; - return PS2X_IOP_STATUS_OK_V1; -} diff --git a/ps2xTest/src/ps2_gs_tests.cpp b/ps2xTest/src/ps2_gs_tests.cpp index 8343775..0458a8c 100644 --- a/ps2xTest/src/ps2_gs_tests.cpp +++ b/ps2xTest/src/ps2_gs_tests.cpp @@ -1885,7 +1885,8 @@ void register_ps2_gs_tests() (1ull << 35) | (static_cast(kClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51) | - (1ull << 55); + (1ull << 55) | + (1ull << 61); constexpr uint64_t kPrim = static_cast(GS_PRIM_TRIANGLE) | (1ull << 4); @@ -2590,7 +2591,7 @@ void register_ps2_gs_tests() } }); - tc.Run("GS T4 CSM1 lookup matches Veronica ClutCopy layout", [](TestCase &t) + tc.Run("GS T4 CSM1 CLUT cache survives source VRAM reuse", [](TestCase &t) { std::vector vram(PS2_GS_VRAM_SIZE, 0u); GS gs; @@ -2612,7 +2613,10 @@ void register_ps2_gs_tests() (1ull << 34) | (1ull << 35) | (static_cast(kClutCbp) << 37) | - (static_cast(GS_PSM_CT32) << 51); + (static_cast(GS_PSM_CT32) << 51) | + (2ull << 61); // CLD=2: load and remember CBP0 + constexpr uint64_t kTex0LoadIfCbp0Changed = + (kTex0 & ~(7ull << 61)) | (4ull << 61); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | // TME @@ -2624,8 +2628,7 @@ void register_ps2_gs_tests() const uint32_t texByteOff = texNibbleAddr >> 1; vram[texByteOff] = static_cast((vram[texByteOff] & 0xF0u) | 0x08u); - // Veronica uploads CSM1 CLUT rows with a 64-pixel GS stride, so logical entry 8 - // resolves to row 1, column 0 after the CSM1 swizzle. + // CSM1 stores logical entry 8 at row 1, column 0 after the CLUT swizzle. const uint32_t wrongClutOff = GSPSMCT32::addrPSMCT32(kClutCbp, 1u, 8u, 0u); const uint32_t expectedClutOff = GSPSMCT32::addrPSMCT32(kClutCbp, 1u, 0u, 1u); std::memcpy(vram.data() + wrongClutOff, &kWrongColor, sizeof(kWrongColor)); @@ -2638,6 +2641,13 @@ void register_ps2_gs_tests() gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); gs.writeRegister(GS_REG_TEX0_1, kTex0); + + // TEX0 loads the palette into the GS CLUT temporary buffer. The + // source VRAM can subsequently be reused without changing the + // palette seen by this draw. + std::memcpy(vram.data() + expectedClutOff, &kWrongColor, sizeof(kWrongColor)); + gs.writeRegister(GS_REG_TEX0_1, kTex0LoadIfCbp0Changed); + gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); gs.writeRegister(GS_REG_UV, 0ull); @@ -2648,7 +2658,69 @@ void register_ps2_gs_tests() uint32_t pixel = 0u; std::memcpy(&pixel, vram.data(), sizeof(pixel)); t.Equals(pixel, kExpectedColor, - "T4 CSM1 lookup should follow Veronica's swizzled CLUT row layout for logical index 8"); + "T4 CSM1 lookup should keep the cached palette after its source VRAM is reused"); + }); + + tc.Run("GS texture page buffer hides local-memory writes until TEXFLUSH", [](TestCase &t) + { + std::vector vram(PS2_GS_VRAM_SIZE, 0u); + GS gs; + gs.init(vram.data(), static_cast(vram.size()), nullptr); + + constexpr uint32_t kTextureTbp = 32u; // Physical GS page 1. + constexpr uint64_t kFrame = + (0ull << 0) | + (1ull << 16) | + (static_cast(GS_PSM_CT32) << 24); + constexpr uint64_t kZbuf = (1ull << 32); + constexpr uint64_t kScissor = 2ull << 16; + constexpr uint64_t kTex0 = + (static_cast(kTextureTbp) << 0) | + (1ull << 14) | + (static_cast(GS_PSM_CT32) << 20) | + (1ull << 34) | + (1ull << 35); + constexpr uint64_t kPrim = + static_cast(GS_PRIM_SPRITE) | + (1ull << 4) | // TME + (1ull << 8); // FST + constexpr uint32_t kInitialColor = 0x80112233u; + constexpr uint32_t kUpdatedColor = 0x80445566u; + + gs.WriteVram(GS_PSM_CT32, kTextureTbp, 1u, 0u, 0u, kInitialColor); + gs.writeRegister(GS_REG_FRAME_1, kFrame); + gs.writeRegister(GS_REG_ZBUF_1, kZbuf); + gs.writeRegister(GS_REG_SCISSOR_1, kScissor); + gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); + gs.writeRegister(GS_REG_TEST_1, 0x30000ull); + gs.writeRegister(GS_REG_ALPHA_1, 0ull); + gs.writeRegister(GS_REG_TEX0_1, kTex0); + gs.writeRegister(GS_REG_PRIM, kPrim); + gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); + + const auto drawPixel = [&gs](uint32_t x) + { + const uint64_t xy0 = static_cast(x * 16u); + const uint64_t xy1 = static_cast((x + 1u) * 16u) | + (static_cast(16u) << 16u); + gs.writeRegister(GS_REG_UV, 0ull); + gs.writeRegister(GS_REG_XYZ2, xy0); + gs.writeRegister(GS_REG_UV, 0ull); + gs.writeRegister(GS_REG_XYZ2, xy1); + }; + + drawPixel(0u); // Fills the hardware texture page buffer. + gs.WriteVram(GS_PSM_CT32, kTextureTbp, 1u, 0u, 0u, kUpdatedColor); + drawPixel(1u); + gs.writeRegister(GS_REG_TEXFLUSH, 0ull); + drawPixel(2u); + + t.Equals(gs.ReadVram(GS_PSM_CT32, 0u, 1u, 0u, 0u), kInitialColor, + "the first sample should read the original texture page"); + t.Equals(gs.ReadVram(GS_PSM_CT32, 0u, 1u, 1u, 0u), kInitialColor, + "writes to local memory must remain hidden by the cached texture page"); + t.Equals(gs.ReadVram(GS_PSM_CT32, 0u, 1u, 2u, 0u), kUpdatedColor, + "TEXFLUSH must make the updated local-memory page visible to texture reads"); }); tc.Run("GS T8 CT32-uploaded CSM1 CLUT follows swizzled palette layout", [](TestCase &t) @@ -2673,7 +2745,8 @@ void register_ps2_gs_tests() (1ull << 34) | (1ull << 35) | (static_cast(kClutCbp) << 37) | - (static_cast(GS_PSM_CT32) << 51); + (static_cast(GS_PSM_CT32) << 51) | + (1ull << 61); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | // TME @@ -2756,7 +2829,8 @@ void register_ps2_gs_tests() (1ull << 35) | (static_cast(kClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51) | - (17ull << 56); + (17ull << 56) | + (1ull << 61); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | @@ -2817,7 +2891,8 @@ void register_ps2_gs_tests() (1ull << 35) | (static_cast(kClutCbp) << 37) | (static_cast(GS_PSM_CT16) << 51) | - (16ull << 56); + (16ull << 56) | + (1ull << 61); constexpr uint64_t kTexa = (0x80ull << 32); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | @@ -2829,10 +2904,14 @@ void register_ps2_gs_tests() writePSMT4Texel(vram, kTexTbp, 1u, 0u, 0u, 1u); - // CSA=16 selects the upper half of a CT16 CLUT. CSM1 swaps bits - // 3 and 4 but must preserve address bit 8. + // CSA selects the destination in the temporary buffer, not a + // different source coordinate. Seed the lower half first, then + // replace the same source palette before loading CSA=16. gs.WriteVram(GS_PSM_CT16, kClutCbp, 1u, 1u, 0u, kWrongGreen); - gs.WriteVram(GS_PSM_CT16, kClutCbp, 1u, 1u, 16u, kExpectedRed); + const uint64_t kTex0Lower = kTex0 & ~(0x1Full << 56); + gs.writeRegister(GS_REG_TEX0_1, kTex0Lower); + gs.WriteVram(GS_PSM_CT16, kClutCbp, 1u, 1u, 0u, kExpectedRed); + gs.writeRegister(GS_REG_TEXFLUSH, 0ull); gs.writeRegister(GS_REG_FRAME_1, kFrameReg); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); @@ -2951,12 +3030,14 @@ void register_ps2_gs_tests() (1ull << 35) | (static_cast(kWrongClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51) | - (1ull << 55); + (1ull << 55) | + (1ull << 61); constexpr uint64_t kTex2 = (static_cast(GS_PSM_T8) << 20) | (static_cast(kExpectedClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51) | - (1ull << 55); + (1ull << 55) | + (1ull << 61); constexpr uint64_t kPrim = static_cast(GS_PRIM_SPRITE) | (1ull << 4) | @@ -3016,7 +3097,8 @@ void register_ps2_gs_tests() (1ull << 35) | (static_cast(kClutCbp) << 37) | (static_cast(GS_PSM_CT32) << 51) | - (1ull << 55); + (1ull << 55) | + (1ull << 61); constexpr uint64_t kTexClut = (1ull << 0) | (3ull << 6) | @@ -3032,7 +3114,7 @@ void register_ps2_gs_tests() vram[texOff] = 0u; const uint32_t wrongClutOff = GSPSMCT32::addrPSMCT32(kClutCbp, 1u, 0u, 0u); - const uint32_t expectedClutOff = GSPSMCT32::addrPSMCT32(kClutCbp, 1u, 3u, 2u); + const uint32_t expectedClutOff = GSPSMCT32::addrPSMCT32(kClutCbp, 1u, 48u, 2u); std::memcpy(vram.data() + wrongClutOff, &kWrongColor, sizeof(kWrongColor)); std::memcpy(vram.data() + expectedClutOff, &kExpectedColor, sizeof(kExpectedColor)); @@ -3042,8 +3124,8 @@ void register_ps2_gs_tests() gs.writeRegister(GS_REG_XYOFFSET_1, 0ull); gs.writeRegister(GS_REG_TEST_1, 0x30000ull); gs.writeRegister(GS_REG_ALPHA_1, 0ull); - gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_TEXCLUT, kTexClut); + gs.writeRegister(GS_REG_TEX0_1, kTex0); gs.writeRegister(GS_REG_PRIM, kPrim); gs.writeRegister(GS_REG_RGBAQ, 0x80808080ull); gs.writeRegister(GS_REG_UV, 0ull); @@ -3349,7 +3431,8 @@ void register_ps2_gs_tests() (1ull << 34) | (1ull << 35) | (static_cast(kClutCbp) << 37) | - (static_cast(GS_PSM_CT32) << 51); + (static_cast(GS_PSM_CT32) << 51) | + (1ull << 61); constexpr uint64_t kPrim = static_cast(GS_PRIM_TRIANGLE) | (1ull << 4) | @@ -4369,7 +4452,8 @@ void register_ps2_gs_tests() (1ull << 34) | (1ull << 35) | (static_cast(kClutCbpA) << 37) | - (static_cast(GS_PSM_CT32) << 51); + (static_cast(GS_PSM_CT32) << 51) | + (1ull << 61); gs.writeRegister(GS_REG_FRAME_1, kFrameReg); gs.writeRegister(GS_REG_ZBUF_1, kZbuf); @@ -4399,7 +4483,8 @@ void register_ps2_gs_tests() (1ull << 34) | (1ull << 35) | (static_cast(kClutCbpB) << 37) | - (static_cast(GS_PSM_CT32) << 51); + (static_cast(GS_PSM_CT32) << 51) | + (1ull << 61); gs.writeRegister(GS_REG_TEX0_1, kTex0HH); gs.writeRegister(GS_REG_UV, 0ull); diff --git a/ps2xTest/src/ps2_iop_tests.cpp b/ps2xTest/src/ps2_iop_tests.cpp index d75b213..f1172e9 100644 --- a/ps2xTest/src/ps2_iop_tests.cpp +++ b/ps2xTest/src/ps2_iop_tests.cpp @@ -1,360 +1,63 @@ #include "MiniTest.h" -#include "ps2x/iop/iop_subsystem.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#if defined(_WIN32) -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX -#include -#elif defined(__linux__) -#include -#endif - -namespace -{ - using namespace ps2x::iop; - - constexpr uint32_t kSyntheticSid = 0xF00DCAFEu; - constexpr uint32_t kCoreCollisionSid = 0x80001300u; - constexpr uint32_t kSyntheticFunction = 0x42u; - constexpr uint32_t kCoreCollisionFunction = 0x99u; - constexpr uint32_t kSyntheticEntryPoint = 0x00123456u; - constexpr uint32_t kSpecificRecvXEntryPoint = kSyntheticEntryPoint + 0x100u; - constexpr uint32_t kSyntheticCrc32 = 0xA1B2C3D4u; - constexpr uint32_t kResponseXor = 0xA5A55A5Au; - constexpr uint32_t kCoreCollisionResponse = 0xC0DEF00Du; - - class FakeIopHost final : public IopHost - { - public: - explicit FakeIopHost(size_t memorySize = 0x10000u) - : memory(memorySize, 0u) - { - } - - bool readGuest(uint32_t address, void *destination, size_t size) const override - { - if ((!destination && size != 0u) || !contains(address, size)) - { - return false; - } - if (size != 0u) - { - std::memcpy(destination, memory.data() + address, size); - } - return true; - } - - bool writeGuest(uint32_t address, const void *source, size_t size) override - { - if ((!source && size != 0u) || !contains(address, size)) - { - return false; - } - if (size != 0u) - { - std::memcpy(memory.data() + address, source, size); - } - return true; - } - - bool zeroGuest(uint32_t address, size_t size) override - { - if (!contains(address, size)) - { - return false; - } - std::fill(memory.begin() + address, memory.begin() + address + size, 0u); - return true; - } - - bool normalizeGuestAddress(uint32_t address, uint32_t &normalized) const override - { - normalized = address & 0x1FFFFFFFu; - return normalized < memory.size(); - } - - uint32_t allocateIopHandle(IopHandleKind kind) override - { - const uint32_t value = nextHandle; - nextHandle += (kind == IopHandleKind::RpcPacket) ? 0x40u : 0x80u; - return value; - } - - uint32_t allocateGuest(uint32_t size, uint32_t alignment) override - { - if (size == 0u) - { - return 0u; - } - const uint64_t effectiveAlignment = alignment == 0u ? 1u : alignment; - const uint64_t aligned = ((static_cast(nextGuestAddress) + effectiveAlignment - 1u) / - effectiveAlignment) * - effectiveAlignment; - if (aligned + size > memory.size()) - { - return 0u; - } - nextGuestAddress = static_cast(aligned + size); - guestAllocations.push_back(static_cast(aligned)); - return static_cast(aligned); - } - - void freeGuest(uint32_t address) override - { - freedGuestAddresses.push_back(address); - } - - void audioCommand(uint32_t sid, - uint32_t function, - GuestBuffer send, - GuestBuffer receive) override - { - lastAudioSid = sid; - lastAudioFunction = function; - lastAudioSend = send; - lastAudioReceive = receive; - ++audioCalls; - } - - std::string hostPath(HostPathKind kind) const override - { - switch (kind) - { - case HostPathKind::CdRoot: - return "fake/cd"; - case HostPathKind::CdImage: - return "fake/disc.iso"; - case HostPathKind::HostRoot: - return "fake/host"; - case HostPathKind::MemoryCardRoot: - return "fake/mc0"; - default: - return "fake/elf"; - } - } - - std::string translateGuestPath(std::string_view path) const override - { - return "translated/" + std::string(path); - } - - uint64_t openHostFile(std::string_view path) override - { - const auto file = hostFileContents.find(std::string(path)); - if (file == hostFileContents.end()) - { - return 0u; - } - const uint64_t handle = nextHostFileHandle++; - openHostFiles.emplace(handle, file->first); - return handle; - } - - bool hostFileSize(uint64_t handle, uint64_t &size) const override - { - size = 0u; - const auto open = openHostFiles.find(handle); - if (open == openHostFiles.end()) - { - return false; - } - const auto file = hostFileContents.find(open->second); - if (file == hostFileContents.end()) - { - return false; - } - size = file->second.size(); - return true; - } - - bool readHostFile(uint64_t handle, - uint64_t offset, - void *destination, - size_t size, - size_t &bytesRead) override - { - bytesRead = 0u; - if (!destination && size != 0u) - { - return false; - } - const auto open = openHostFiles.find(handle); - if (open == openHostFiles.end()) - { - return false; - } - const auto file = hostFileContents.find(open->second); - if (file == hostFileContents.end() || offset > file->second.size()) - { - return false; - } - bytesRead = std::min(size, file->second.size() - static_cast(offset)); - if (bytesRead != 0u) - { - std::memcpy(destination, - file->second.data() + static_cast(offset), - bytesRead); - } - return true; - } - - void closeHostFile(uint64_t handle) override - { - if (openHostFiles.erase(handle) != 0u) - { - closedHostFileHandles.push_back(handle); - } - } - - int32_t memoryCard(const MemoryCardRequest &request) override - { - lastMemoryCardRequest = request; - ++memoryCardCalls; - return 0; - } - - bool hasGuestFunction(uint32_t address) const override - { - return address == guestFunctionAddress; - } - - bool invokeGuestFunction(uint64_t callToken, - uint32_t address, - uint32_t a0, - uint32_t a1, - uint32_t a2, - uint32_t a3, - uint32_t *resultAddress) override - { - if (!hasGuestFunction(address)) - { - return false; - } - lastCallToken = callToken; - lastGuestArguments = {a0, a1, a2, a3}; - if (resultAddress) - { - *resultAddress = guestFunctionResult; - } - return true; - } - - void log(LogLevel level, std::string_view message) override - { - logs.emplace_back(level, std::string(message)); - } - - bool writeWord(uint32_t address, uint32_t value) - { - return writeGuest(address, &value, sizeof(value)); - } - - uint32_t readWord(uint32_t address) const - { - uint32_t value = 0u; - (void)readGuest(address, &value, sizeof(value)); - return value; - } - - bool hasLog(std::string_view expected) const - { - return std::any_of(logs.begin(), logs.end(), [&](const auto &entry) - { return entry.second == expected; }); - } - - std::vector memory; - uint32_t nextHandle = 0x8000u; - uint32_t nextGuestAddress = 0x4000u; - std::vector guestAllocations; - std::vector freedGuestAddresses; - uint32_t audioCalls = 0u; - uint32_t lastAudioSid = 0u; - uint32_t lastAudioFunction = 0u; - GuestBuffer lastAudioSend{}; - GuestBuffer lastAudioReceive{}; - uint32_t memoryCardCalls = 0u; - MemoryCardRequest lastMemoryCardRequest{}; - uint32_t guestFunctionAddress = 0x2000u; - uint32_t guestFunctionResult = 0x3000u; - uint64_t lastCallToken = 0u; - std::vector lastGuestArguments; - std::vector> logs; - std::unordered_map> hostFileContents; - std::unordered_map openHostFiles; - std::vector closedHostFileHandles; - uint64_t nextHostFileHandle = 1u; - - private: - bool contains(uint32_t address, size_t size) const - { - const uint64_t end = static_cast(address) + static_cast(size); - return end <= memory.size(); - } - }; - - bool containsDiagnostic(const DebugSnapshot &snapshot, std::string_view text) - { - return std::any_of(snapshot.diagnostics.begin(), snapshot.diagnostics.end(), [&](const std::string &diagnostic) - { return diagnostic.find(text) != std::string::npos; }); - } - - const DebugService *findService(const DebugSnapshot &snapshot, std::string_view name) - { - const auto it = std::find_if(snapshot.services.begin(), snapshot.services.end(), [&](const DebugService &service) - { return service.name == name; }); - return it == snapshot.services.end() ? nullptr : &*it; - } - - uint64_t metricValue(const DebugService &service, std::string_view name) - { - const auto it = std::find_if(service.metrics.begin(), service.metrics.end(), [&](const DebugMetric &metric) - { return metric.name == name; }); - return it == service.metrics.end() ? std::numeric_limits::max() : it->value; - } - - bool pluginModuleIsLoaded(const std::filesystem::path &path) - { -#if defined(_WIN32) - return GetModuleHandleW(path.c_str()) != nullptr; -#elif defined(__linux__) - void *handle = dlopen(path.c_str(), RTLD_NOW | RTLD_NOLOAD); - if (!handle) - { - return false; - } - dlclose(handle); - return true; -#else - (void)path; - return false; -#endif - } -} +#include "ps2x/iop/ps2_path.h" +#include "../../ps2xIOP/tests/iop_compat_test_support.h" void register_ps2_iop_tests() { MiniTest::Case("PS2IopSubsystem", [](TestCase &tc) { - tc.Run("unknown SID remains unhandled without a matching profile", [](TestCase &t) + tc.Run("PS2 path parsing is shared and normalizes ISO/module names", [](TestCase &t) { - FakeIopHost host; - ps2x::iop::IopSubsystem subsystem(host); + const ps2x::iop::ParsedPs2Path cd = ps2x::iop::parsePs2Path("CDROM0:\\MODULES\\LIBSD.IRX;1"); + t.Equals(cd.device, ps2x::iop::Ps2PathDevice::Cdrom, + "device names should be case-insensitive"); + t.Equals(cd.path, std::string("MODULES/LIBSD.IRX"), + "separators and ISO version suffixes should normalize once"); + t.Equals(ps2x::iop::ps2PathLeafKey(cd), std::string("libsd"), + "module lookup should use a normalized IRX leaf key"); - std::string error; - const bool configured = subsystem.configure({"unmatched.elf", 0x100000u, 0x12345678u}, &error); - t.IsTrue(configured, "configuring an unmatched game should keep core-only IOP services available"); + const ps2x::iop::ParsedPs2Path rom = ps2x::iop::parsePs2Path("rom0:ROMVER"); + t.Equals(rom.device, ps2x::iop::Ps2PathDevice::Rom0, + "ROM0 should remain a distinct virtual device"); + t.IsFalse(static_cast(ps2x::iop::parsePs2Path("unknown0:file.irx")), + "unsupported devices must not fall through to cdrom0"); + }); + + tc.Run("HLE services activate only after a recognized module load", [](TestCase &t) + { + iop_test::Host host; + ps2x::iop::IopSubsystem subsystem(host); + t.IsFalse(subsystem.canBindRpc(0x80000701u), + "LIBSD RPC must not exist before LIBSD is loaded"); + + const ps2x::iop::ModuleLoadResult unknown = subsystem.loadModule("rom0:NOT_A_REAL_MODULE"); + t.IsTrue(unknown.handled, "the module manager should return a real load result"); + t.IsTrue(unknown.moduleId < 0, "unknown ROM modules must fail instead of receiving fake IDs"); + + const ps2x::iop::ModuleLoadResult loaded = subsystem.loadModule("rom0:LIBSD"); + t.IsTrue(loaded.moduleId > 0, "a registered no-BIOS HLE module should load"); + t.IsTrue(subsystem.canBindRpc(0x80000701u), + "loading LIBSD should activate its HLE RPC endpoint"); + + ps2x::iop::RpcRequest request{}; + request.sid = 0x80000701u; + request.function = 3u; + t.IsTrue(subsystem.handleRpc(request).handled, + "the activated LIBSD service should handle its RPC"); + t.Equals(host.audioCalls, 1u, "the RPC should reach the HLE audio contract"); + + int32_t stopResult = -1; + t.IsTrue(subsystem.stopModule(loaded.moduleId, &stopResult), + "an HLE module should have a real stoppable lifecycle"); + t.Equals(stopResult, 0, "stopping an HLE module should report success"); + t.IsFalse(subsystem.canBindRpc(0x80000701u), + "stopping LIBSD should deactivate its RPC endpoint"); + }); + + tc.Run("unknown SID remains unhandled without a loaded IRX", [](TestCase &t) + { + iop_test::Host host; + ps2x::iop::IopSubsystem subsystem(host); ps2x::iop::RpcRequest request{}; request.sid = 0xDEADC0DEu; @@ -366,514 +69,7 @@ void register_ps2_iop_tests() t.Equals(result.callbackPolicy, ps2x::iop::CallbackPolicy::RuntimeDefault, "an unknown SID should preserve runtime callback handling"); - const ps2x::iop::DebugSnapshot snapshot = subsystem.debugSnapshot(); - t.IsTrue(snapshot.activeProfile.empty(), "an unmatched game should not activate a profile"); - t.IsTrue(snapshot.activeProvider.empty(), "an unmatched game should not report a profile provider"); }); - tc.Run("built-in profiles select by ELF basename and keep core services active", [](TestCase &t) - { - FakeIopHost host; - ps2x::iop::IopSubsystem subsystem(host); - std::string error; - - t.IsTrue(subsystem.configure({"SLUS_201.84", 0u, 0u}, &error), - "RECVX profile should match case-insensitively by basename"); - ps2x::iop::DebugSnapshot snapshot = subsystem.debugSnapshot(); - t.Equals(snapshot.activeProfile, std::string("recvx-us"), - "RECVX ELF should select its built-in profile"); - t.IsNotNull(findService(snapshot, "TSNDDRV"), - "RECVX profile should register TSNDDRV"); - t.IsNotNull(findService(snapshot, "CRI DTX"), - "RECVX profile should register CRI DTX"); - t.IsNotNull(findService(snapshot, "dbcman"), - "core DBCMAN should remain active with a game profile"); - t.IsNotNull(findService(snapshot, "libsd"), - "core LIBSD should remain active with a game profile"); - t.IsNotNull(findService(snapshot, "MCSERV"), - "core MCSERV should remain active with a game profile"); - - error.clear(); - t.IsTrue(subsystem.configure({"slus_203.88", 0u, 0u}, &error), - "Fatal Frame profile should configure after a different game"); - snapshot = subsystem.debugSnapshot(); - t.Equals(snapshot.activeProfile, std::string("fatal-frame-us"), - "reload should replace the active profile"); - t.IsNull(findService(snapshot, "CRI DTX"), - "reload should destroy services from the previous profile"); - t.IsNotNull(findService(snapshot, "SDRDRV"), - "Fatal Frame profile should expose SDRDRV"); - }); - - tc.Run("two subsystem instances isolate profile state and reset deterministically", [](TestCase &t) - { - FakeIopHost hostA; - FakeIopHost hostB; - ps2x::iop::IopSubsystem subsystemA(hostA); - ps2x::iop::IopSubsystem subsystemB(hostB); - std::string error; - t.IsTrue(subsystemA.configure({"SLUS_205.78", 0u, 0u}, &error), - "first LotR instance should configure"); - t.IsTrue(subsystemB.configure({"SLUS_205.78", 0u, 0u}, &error), - "second LotR instance should configure"); - - ps2x::iop::RpcRequest request{}; - request.sid = 0x00012345u; - request.receive = {0x1000u, 8u}; - - t.IsTrue(subsystemA.handleRpc(request).handled, - "first instance should handle LotR sound RPC"); - t.Equals(hostA.readWord(0x1004u), 1u, - "first instance should start its counter at one"); - (void)subsystemA.handleRpc(request); - t.Equals(hostA.readWord(0x1004u), 2u, - "first instance should advance independently"); - - t.IsTrue(subsystemB.handleRpc(request).handled, - "second instance should handle LotR sound RPC"); - t.Equals(hostB.readWord(0x1004u), 1u, - "second instance must not inherit the first counter"); - - subsystemA.reset(); - (void)subsystemA.handleRpc(request); - t.Equals(hostA.readWord(0x1004u), 1u, - "reset should restore per-instance service state"); - }); - - tc.Run("LotR sound update completes queued PlayStream slots", [](TestCase &t) - { - FakeIopHost host; - ps2x::iop::IopSubsystem subsystem(host); - std::string error; - t.IsTrue(subsystem.configure({"SLUS_205.78", 0u, 0u}, &error), - "LotR profile should configure"); - - constexpr uint32_t kSendAddress = 0x0800u; - constexpr uint32_t kReceiveAddress = 0x1000u; - constexpr uint16_t kStreamSlot = 7u; - const std::array playStreamPacket = { - 1u, // command count - 1u, // PlayStream - 7u, // argument count - 0u, - static_cast(kStreamSlot << 8u), - 0u, - 0u, - 0u, - 0u, - 0u, - }; - t.IsTrue(host.writeGuest(kSendAddress, - playStreamPacket.data(), - sizeof(playStreamPacket)), - "PlayStream command packet should fit in guest memory"); - - ps2x::iop::RpcRequest request{}; - request.sid = 0x00012345u; - request.send = {kSendAddress, sizeof(playStreamPacket)}; - request.receive = {kReceiveAddress, 0x100u}; - - t.IsTrue(subsystem.handleRpc(request).handled, - "LotR sound service should handle PlayStream"); - t.Equals(host.readWord(kReceiveAddress), 1u, - "PlayStream response should expose one active record"); - const uint32_t packedStream = host.readWord(kReceiveAddress + 4u); - t.Equals((packedStream >> 4u) & 0x3Fu, - static_cast(kStreamSlot), - "active record should identify the queued EE stream slot"); - t.Equals(host.readWord(kReceiveAddress + 0x24u), 1u, - "response counter should follow the active record"); - - const std::array statusPacket = { - 1u, // command count - 9u, // GetStatus - 2u, // argument count - kStreamSlot, - 0u, - }; - t.IsTrue(host.writeGuest(kSendAddress, statusPacket.data(), sizeof(statusPacket)), - "GetStatus command packet should fit in guest memory"); - request.send.size = sizeof(statusPacket); - - t.IsTrue(subsystem.handleRpc(request).handled, - "LotR sound service should handle the following status update"); - t.Equals(host.readWord(kReceiveAddress), 0u, - "the update after PlayStream should report no active records"); - t.Equals(host.readWord(kReceiveAddress + 4u), 2u, - "empty response counter should return to the base offset"); - }); - - tc.Run("TSNDDRV uses profile checksum bindings without writing invalid ports", [](TestCase &t) - { - FakeIopHost host(0x02000000u); - ps2x::iop::IopSubsystem subsystem(host); - std::string error; - t.IsTrue(subsystem.configure({"slus_201.84", 0u, 0u}, &error), - "RECVX profile should configure for TSNDDRV command testing"); - - constexpr uint32_t kResponseAddress = 0x1000u; - ps2x::iop::RpcRequest stateRequest{}; - stateRequest.sid = 1u; - stateRequest.function = 0x12u; - stateRequest.receive = {kResponseAddress, sizeof(uint32_t)}; - t.IsTrue(subsystem.handleRpc(stateRequest).handled, - "TSNDDRV should return its configured status buffer"); - const uint32_t statusAddress = host.readWord(kResponseAddress); - t.IsTrue(statusAddress != 0u, "TSNDDRV status buffer should be allocated"); - - constexpr int16_t kChecksum = 0x1234; - t.IsTrue(host.writeGuest(0x01E0EF10u, &kChecksum, sizeof(kChecksum)), - "RECVX primary checksum binding should be writable in the fake guest"); - - constexpr uint32_t kCommandAddress = 0x2000u; - std::array command{}; - command[0] = 0x29u; - command[1] = 0u; - t.IsTrue(host.writeGuest(kCommandAddress, command.data(), command.size()), - "valid TSNDDRV command should be writable"); - - ps2x::iop::RpcRequest commandRequest{}; - commandRequest.sid = 0u; - commandRequest.function = 0u; - commandRequest.send = {kCommandAddress, static_cast(command.size())}; - t.IsTrue(subsystem.handleRpc(commandRequest).handled, - "TSNDDRV should handle the characterized command queue"); - - int16_t writtenChecksum = 0; - t.IsTrue(host.readGuest(statusAddress + 0x26u, - &writtenChecksum, - sizeof(writtenChecksum)), - "TSNDDRV SE checksum slot should be readable"); - t.Equals(writtenChecksum, kChecksum, - "valid port should mirror the profile-bound checksum table"); - - constexpr uint32_t kPastStatusAddress = 0x44u; - constexpr uint16_t kSentinel = 0xBEEFu; - t.IsTrue(host.writeGuest(statusAddress + kPastStatusAddress, - &kSentinel, - sizeof(kSentinel)), - "sentinel after the status structure should be writable"); - command[1] = 0x0Fu; - (void)host.writeGuest(kCommandAddress, command.data(), command.size()); - (void)subsystem.handleRpc(commandRequest); - - uint16_t sentinelAfter = 0u; - (void)host.readGuest(statusAddress + kPastStatusAddress, - &sentinelAfter, - sizeof(sentinelAfter)); - t.Equals(sentinelAfter, kSentinel, - "invalid port must not overwrite memory past the 0x42-byte status structure"); - }); - - tc.Run("RECVX reset clears CRI object maps without global state", [](TestCase &t) - { - FakeIopHost host(0x02000000u); - ps2x::iop::IopSubsystem subsystem(host); - std::string error; - t.IsTrue(subsystem.configure({"slus_201.84", 0u, 0u}, &error), - "RECVX profile should configure"); - - constexpr uint32_t kSendAddress = 0x2000u; - constexpr uint32_t kReceiveAddress = 0x2100u; - host.writeWord(kSendAddress + 0u, 0u); - host.writeWord(kSendAddress + 4u, 0x4000u); - host.writeWord(kSendAddress + 8u, 0x100u); - - ps2x::iop::RpcRequest request{}; - request.sid = 0x7D000000u; - request.function = 0x422u; - request.send = {kSendAddress, 12u}; - request.receive = {kReceiveAddress, 4u}; - t.IsTrue(subsystem.handleRpc(request).handled, - "SJRMT create should be emulated by the RECVX profile"); - - ps2x::iop::DebugSnapshot snapshot = subsystem.debugSnapshot(); - const ps2x::iop::DebugService *service = - findService(snapshot, "CRI DTX"); - if (!service) - { - t.Fail("CRI DTX service should be visible in the debug snapshot"); - return; - } - t.Equals(metricValue(*service, "sjrmt_objects"), uint64_t{1}, - "created CRI object should be tracked by this instance"); - - subsystem.reset(); - snapshot = subsystem.debugSnapshot(); - service = findService(snapshot, "CRI DTX"); - if (!service) - { - t.Fail("CRI DTX service should survive reset"); - return; - } - t.Equals(metricValue(*service, "sjrmt_objects"), uint64_t{0}, - "reset should clear CRI object maps"); - }); - - tc.Run("reset closes profile-owned host file handles", [](TestCase &t) - { - FakeIopHost host; - host.hostFileContents["translated/test.bin"] = {0x10u, 0x20u, 0x30u}; - - ps2x::iop::IopSubsystem subsystem(host); - std::string error; - t.IsTrue(subsystem.configure({"SLUS_205.78", 0u, 0u}, &error), - "LotR profile should configure for file lifecycle testing"); - - constexpr uint32_t kPathAddress = 0x1000u; - constexpr uint32_t kReceiveAddress = 0x1100u; - constexpr char kPath[] = "test.bin"; - t.IsTrue(host.writeGuest(kPathAddress, kPath, sizeof(kPath)), - "fake guest path should be writable"); - - ps2x::iop::RpcRequest request{}; - request.sid = 0x0000FF01u; - request.function = 0x08u; - request.send = {kPathAddress, sizeof(kPath)}; - request.receive = {kReceiveAddress, 8u}; - t.IsTrue(subsystem.handleRpc(request).handled, - "LotR CLFILE open should be handled"); - t.Equals(host.openHostFiles.size(), size_t{1}, - "open RPC should retain one opaque host file handle"); - - ps2x::iop::DebugSnapshot snapshot = subsystem.debugSnapshot(); - const ps2x::iop::DebugService *service = - findService(snapshot, "CLFILE"); - if (!service) - { - t.Fail("LotR CLFILE service should be visible before reset"); - return; - } - t.Equals(metricValue(*service, "open_files"), uint64_t{1}, - "debug state should report the open file"); - - subsystem.reset(); - t.IsTrue(host.openHostFiles.empty(), - "reset should release every retained host file handle"); - t.Equals(host.closedHostFileHandles.size(), size_t{1}, - "host close callback should run exactly once"); - snapshot = subsystem.debugSnapshot(); - service = findService(snapshot, "CLFILE"); - if (!service) - { - t.Fail("LotR CLFILE service should survive reset"); - return; - } - t.Equals(metricValue(*service, "open_files"), uint64_t{0}, - "reset should clear the CLFILE handle registry"); - }); - -#if defined(PS2X_TEST_IOP_PLUGIN_DIR) - tc.Run("plugin module remains loaded through instances and unloads after subsystem destruction", [](TestCase &t) - { - const std::filesystem::path pluginDirectory(PS2X_TEST_IOP_PLUGIN_DIR); -#if defined(_WIN32) - const std::filesystem::path pluginPath = - pluginDirectory / "ps2_iop_fake_plugin.dll"; -#else - const std::filesystem::path pluginPath = - pluginDirectory / "ps2_iop_fake_plugin.so"; -#endif - t.IsFalse(pluginModuleIsLoaded(pluginPath), - "synthetic plugin should not be loaded before discovery"); - { - FakeIopHost host; - ps2x::iop::IopSubsystem subsystem(host); - subsystem.setPluginSearchPaths({pluginDirectory}); - std::string error; - t.IsTrue(subsystem.loadPlugins(&error), - "synthetic plugins should load for lifetime testing"); - t.IsTrue(subsystem.configure({"synthetic_iop_test.elf", - kSyntheticEntryPoint, - kSyntheticCrc32}, - &error), - "synthetic plugin instance should be created"); - t.IsTrue(pluginModuleIsLoaded(pluginPath), - "module must stay loaded while a profile instance exists"); - } - t.IsFalse(pluginModuleIsLoaded(pluginPath), - "module should unload after profile destruction and catalog teardown"); - }); - - tc.Run("plugin discovery matches all identity fields and dispatches through the host bridge", [](TestCase &t) - { - FakeIopHost host; - ps2x::iop::IopSubsystem subsystem(host); - const std::filesystem::path pluginDirectory(PS2X_TEST_IOP_PLUGIN_DIR); - - t.IsTrue(std::filesystem::is_directory(pluginDirectory), - "the synthetic IOP plugin directory should be staged by the test build"); - subsystem.setPluginSearchPaths({pluginDirectory}); - - std::string error; - t.IsTrue(subsystem.loadPlugins(&error), "synthetic IOP plugin discovery should succeed"); - ps2x::iop::DebugSnapshot discoverySnapshot = subsystem.debugSnapshot(); - t.IsTrue(containsDiagnostic(discoverySnapshot, "loaded 4 profile(s)"), - "plugin discovery diagnostics should report all accepted synthetic profiles"); - t.IsTrue(containsDiagnostic(discoverySnapshot, "too many SIDs"), - "an invalid profile descriptor should be ignored with a diagnostic"); - t.IsTrue(containsDiagnostic(discoverySnapshot, "bad_abi"), - "an ABI-incompatible plugin should be ignored with a diagnostic"); - t.IsTrue(containsDiagnostic(discoverySnapshot, "incompatible ABI"), - "the incompatible-plugin diagnostic should explain the ABI failure"); - t.IsTrue(containsDiagnostic(discoverySnapshot, "missing_symbol"), - "a plugin without the query symbol should be ignored with a diagnostic"); - t.IsTrue(containsDiagnostic(discoverySnapshot, "missing ps2x_iop_query_v1"), - "the missing-symbol diagnostic should name the required entry point"); - - auto expectNoProfile = [&](const ps2x::iop::GameIdentity &identity, const std::string &reason) { - error.clear(); - t.IsTrue(subsystem.configure(identity, &error), "mismatching plugin identity should configure core-only services"); - const ps2x::iop::DebugSnapshot snapshot = subsystem.debugSnapshot(); - t.IsTrue(snapshot.activeProfile.empty(), reason); - - ps2x::iop::RpcRequest request{}; - request.sid = kSyntheticSid; - request.function = kSyntheticFunction; - t.IsFalse(subsystem.handleRpc(request).handled, - "a mismatching profile must not expose its synthetic SID"); - }; - - expectNoProfile({"different.elf", kSyntheticEntryPoint, kSyntheticCrc32}, - "a different ELF basename should not match the plugin profile"); - expectNoProfile({"synthetic_iop_test.elf", kSyntheticEntryPoint + 4u, kSyntheticCrc32}, - "a different entry point should not match the plugin profile"); - expectNoProfile({"synthetic_iop_test.elf", kSyntheticEntryPoint, kSyntheticCrc32 ^ 1u}, - "a different CRC32 should not match the plugin profile"); - - error.clear(); - t.IsTrue(subsystem.configure({"synthetic_iop_test.elf", kSyntheticEntryPoint, kSyntheticCrc32}, &error), - "the synthetic ELF identity should activate the plugin profile"); - - ps2x::iop::DebugSnapshot snapshot = subsystem.debugSnapshot(); - t.Equals(snapshot.activeProfile, std::string("synthetic-test-profile"), - "debug snapshot should expose the active plugin profile id"); - t.Equals(snapshot.activeProvider, std::string("ps2x-test-plugin"), - "debug snapshot should expose the plugin provider name"); - const ps2x::iop::DebugService *service = findService(snapshot, "synthetic-test-profile"); - if (!service) - { - t.Fail("debug snapshot should include the synthetic profile service"); - return; - } - t.IsTrue(service->profileSpecific, "plugin service should be marked profile-specific"); - t.IsTrue(std::find(service->sids.begin(), service->sids.end(), kSyntheticSid) != service->sids.end(), - "plugin service should advertise its synthetic SID"); - t.Equals(metricValue(*service, "reset_generation"), uint64_t{1}, - "profile configuration should reset a new plugin instance once"); - - ps2x::iop::RpcAbiRequest abiRequest{}; - abiRequest.boundSid = kSyntheticSid; - abiRequest.function = kSyntheticFunction; - abiRequest.registers.plausible = true; - abiRequest.stack.plausible = true; - t.Equals(subsystem.selectRpcAbi(abiRequest), ps2x::iop::RpcAbi::Stack, - "plugin should be able to select the stack RPC ABI"); - abiRequest.function = kSyntheticFunction + 1u; - t.Equals(subsystem.selectRpcAbi(abiRequest), ps2x::iop::RpcAbi::RuntimeDefault, - "plugin ABI selection should fall back for unrelated functions"); - - constexpr uint32_t kSendAddress = 0x1000u; - constexpr uint32_t kReceiveAddress = 0x1100u; - constexpr uint32_t kInput = 0x1234ABCDu; - t.IsTrue(host.writeWord(kSendAddress, kInput), "fake host should seed the plugin send buffer"); - t.IsTrue(host.writeWord(kReceiveAddress, 0u), "fake host should clear the plugin receive buffer"); - - ps2x::iop::RpcRequest request{}; - request.callToken = 0x1122334455667788ull; - request.sid = kSyntheticSid; - request.function = kSyntheticFunction; - request.send = {kSendAddress, sizeof(uint32_t)}; - request.receive = {kReceiveAddress, sizeof(uint32_t)}; - const ps2x::iop::RpcResult result = subsystem.handleRpc(request); - - t.IsTrue(result.handled, "matching synthetic SID/function should dispatch to the plugin"); - t.Equals(result.resultAddress, kReceiveAddress, "plugin should return its receive-buffer address"); - t.IsTrue(result.signalNowaitCompletion, "plugin should request nowait completion signaling"); - t.Equals(result.callbackPolicy, ps2x::iop::CallbackPolicy::Suppress, - "plugin should be able to suppress the runtime callback"); - t.Equals(host.readWord(kReceiveAddress), kInput ^ kResponseXor, - "plugin should read and write guest memory through the IopHost bridge"); - - ps2x::iop::RpcRequest unknownRequest{}; - unknownRequest.sid = 0xDEADC0DEu; - unknownRequest.function = kSyntheticFunction; - t.IsFalse(subsystem.handleRpc(unknownRequest).handled, - "unknown SID should remain unhandled while a plugin profile is active"); - - constexpr uint32_t kCoreCollisionReceiveAddress = 0x1200u; - ps2x::iop::RpcRequest collisionRequest{}; - collisionRequest.sid = kCoreCollisionSid; - collisionRequest.function = kCoreCollisionFunction; - collisionRequest.receive = {kCoreCollisionReceiveAddress, sizeof(uint32_t)}; - const ps2x::iop::RpcResult collisionResult = subsystem.handleRpc(collisionRequest); - t.IsTrue(collisionResult.handled, - "a profile service should take precedence over a core service for the same SID"); - t.Equals(host.readWord(kCoreCollisionReceiveAddress), kCoreCollisionResponse, - "the profile collision route should reach the plugin implementation"); - - subsystem.onSifTransfer({ps2x::iop::SifTransferKind::SetDma, - ps2x::iop::SifTransferPhase::AfterCopy, - kSendAddress, - kReceiveAddress, - sizeof(uint32_t)}); - snapshot = subsystem.debugSnapshot(); - service = findService(snapshot, "synthetic-test-profile"); - if (!service) - { - t.Fail("synthetic profile service should remain visible after dispatch"); - return; - } - t.Equals(metricValue(*service, "rpc_calls"), uint64_t{2}, - "plugin debug metrics should count dispatched RPCs"); - t.Equals(metricValue(*service, "sif_transfers"), uint64_t{1}, - "plugin debug metrics should count SIF transfer hooks"); - - subsystem.reset(); - snapshot = subsystem.debugSnapshot(); - service = findService(snapshot, "synthetic-test-profile"); - if (!service) - { - t.Fail("synthetic profile service should remain visible after reset"); - return; - } - t.Equals(metricValue(*service, "reset_generation"), uint64_t{2}, - "explicit subsystem reset should reach the plugin instance"); - t.Equals(metricValue(*service, "rpc_calls"), uint64_t{0}, - "plugin reset should clear per-instance RPC state"); - t.Equals(metricValue(*service, "sif_transfers"), uint64_t{0}, - "plugin reset should clear per-instance transfer state"); - - error.clear(); - t.IsFalse(subsystem.configure({"synthetic_duplicate.elf", kSyntheticEntryPoint, kSyntheticCrc32}, &error), - "duplicate SIDs inside one profile layer should reject configuration"); - t.IsTrue(error.find("duplicate IOP SID") != std::string::npos, - "duplicate-SID failure should clearly identify the registry conflict"); - - error.clear(); - t.IsFalse(subsystem.configure({"slus_201.84", kSyntheticEntryPoint, kSyntheticCrc32}, &error), - "equally specific built-in and plugin matchers should be ambiguous"); - t.IsTrue(error.find("ambiguous IOP profiles") != std::string::npos, - "ambiguous profile selection should fail clearly"); - - error.clear(); - t.IsTrue(subsystem.configure({"slus_201.84", - kSpecificRecvXEntryPoint, - kSyntheticCrc32}, - &error), - "a more-specific matcher should win over a lower-specificity tie"); - t.Equals(subsystem.debugSnapshot().activeProfile, - std::string("synthetic-specific-recvx-profile"), - "the most specific plugin profile should be selected"); - - error.clear(); - t.IsTrue(subsystem.configure({"different.elf", kSyntheticEntryPoint, kSyntheticCrc32}, &error), - "switching to an unmatched ELF should destroy the active plugin profile"); - t.IsTrue(host.hasLog("fake-plugin-destroy"), - "plugin profile destroy callback should run when the active profile is replaced"); - t.IsTrue(subsystem.debugSnapshot().activeProfile.empty(), - "switching to an unmatched ELF should leave no active profile"); - }); -#endif }); } diff --git a/ps2xTest/src/ps2_memory_tests.cpp b/ps2xTest/src/ps2_memory_tests.cpp index 7c3e8ea..0b22fe0 100644 --- a/ps2xTest/src/ps2_memory_tests.cpp +++ b/ps2xTest/src/ps2_memory_tests.cpp @@ -861,6 +861,57 @@ void register_ps2_memory_tests() t.IsFalse(mem.isPath3Masked(), "MSKPATH3 with imm bit15 clear should disable PATH3 mask"); }); + tc.Run("VIF1 FIFO MSKPATH3 is visible through GIF_STAT", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + const uint32_t setMask = makeVifCmd(0x06u, 0u, 0x8000u); + const __m128i setPacket = _mm_set_epi32(0, 0, 0, static_cast(setMask)); + mem.write128(0x10005000u, setPacket); + t.IsTrue(mem.isPath3Masked(), "a direct VIF1 FIFO command should execute MSKPATH3"); + t.IsTrue((mem.readIORegister(0x10003020u) & 0x2u) != 0u, + "GIF_STAT.M3P should report the VIF1 PATH3 mask"); + + const uint32_t clearMask = makeVifCmd(0x06u, 0u, 0x0000u); + const __m128i clearPacket = _mm_set_epi32(0, 0, 0, static_cast(clearMask)); + mem.write128(0x10005000u, clearPacket); + t.IsFalse(mem.isPath3Masked(), "a direct VIF1 FIFO command should clear MSKPATH3"); + t.IsTrue((mem.readIORegister(0x10003020u) & 0x2u) == 0u, + "GIF_STAT.M3P should clear with the VIF1 PATH3 mask"); + + mem.writeIORegister(0x10003010u, 0x5u); // GIF_MODE: M3R | IMT + mem.writeIORegister(0x10003000u, 0x8u); // GIF_CTRL: PSE + t.Equals(mem.readIORegister(0x10003020u) & 0xDu, 0xDu, + "GIF_STAT should mirror GIF_MODE and GIF_CTRL status bits"); + }); + + tc.Run("GIF_STAT exposes synchronously drained DMA occupancy for one EE quantum", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kGifChannel = 0x1000A000u; + constexpr uint32_t kSource = 0x00020000u; + mem.setGifPacketCallback([](const uint8_t *, uint32_t) {}); + + t.IsTrue(mem.writeIORegister(kGifChannel + 0x10u, kSource), + "write GIF MADR should succeed"); + t.IsTrue(mem.writeIORegister(kGifChannel + 0x20u, 1u), + "write GIF QWC should succeed"); + t.IsTrue(mem.writeIORegister(kGifChannel + 0x00u, 0x100u), + "start GIF normal DMA should succeed"); + + const uint32_t visibleFqc = (mem.readIORegister(0x10003020u) >> 24u) & 0x1Fu; + t.Equals(visibleFqc, 1u, + "a synchronously consumed qword should remain observable through GIF_STAT.FQC"); + + mem.advanceEeTimers(1u); + const uint32_t drainedFqc = (mem.readIORegister(0x10003020u) >> 24u) & 0x1Fu; + t.Equals(drainedFqc, 0u, + "the synthetic FIFO observation should expire at the next EE scheduling boundary"); + }); + tc.Run("PATH3 mask queues packets until unmask", [](TestCase &t) { PS2Memory mem; @@ -1396,7 +1447,8 @@ void register_ps2_memory_tests() }); t.IsTrue(mem.writeIORegister(kVif1Ch + 0x30u, kTag), "write VIF1 TADR should succeed"); - t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x104u), "write VIF1 CHCR STR|CHAIN should succeed"); + t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x144u), + "write VIF1 CHCR STR|CHAIN|TTE should succeed"); mem.processPendingTransfers(); @@ -1435,7 +1487,8 @@ void register_ps2_memory_tests() std::memcpy(rdram + kTag + 12u, &itopCmd, sizeof(itopCmd)); t.IsTrue(mem.writeIORegister(kVif1Ch + 0x30u, kTag), "write VIF1 TADR should succeed"); - t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x104u), "write VIF1 CHCR STR|CHAIN should succeed"); + t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x144u), + "write VIF1 CHCR STR|CHAIN|TTE should succeed"); mem.processPendingTransfers(); @@ -1445,6 +1498,84 @@ void register_ps2_memory_tests() "qwc-zero compact VIF1 chain should clear the STR bit after drain"); }); + tc.Run("VIF1 DMA chain transfers REF tag high bytes when TTE is enabled", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kVif1Ch = 0x10009000u; + constexpr uint32_t kTag0 = 0x00025200u; + constexpr uint32_t kTag1 = kTag0 + 0x10u; + constexpr uint32_t kRefPayload = 0x00025300u; + + uint8_t *rdram = mem.getRDRAM(); + writeDmaTag(rdram, kTag0, makeDmaTag(1u, 3u, kRefPayload, false)); // REF + writeDmaTag(rdram, kTag1, makeDmaTag(0u, 7u, 0u, false)); // END + + // With CHCR.TTE set, both VIFcodes stored in every DMAtag's upper half + // precede that tag's payload, including tags whose payload is referenced. + const uint32_t directCmd = makeVifCmd(0x50u, 0u, 1u); + std::memcpy(rdram + kTag0 + 12u, &directCmd, sizeof(directCmd)); + for (uint32_t i = 0; i < 16u; ++i) + { + rdram[kRefPayload + i] = static_cast(0xA0u + i); + } + + std::vector> captured; + mem.setGifPacketCallback([&](const uint8_t *data, uint32_t sizeBytes) + { + captured.emplace_back(data, data + sizeBytes); + }); + + t.IsTrue(mem.writeIORegister(kVif1Ch + 0x30u, kTag0), "write VIF1 TADR should succeed"); + t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x144u), + "write VIF1 CHCR STR|CHAIN|TTE should succeed"); + + mem.processPendingTransfers(); + + t.Equals(captured.size(), static_cast(1u), + "REF tag high-half DIRECT should emit one GIF packet"); + if (!captured.empty()) + { + t.Equals(captured[0].size(), static_cast(16u), + "REF tag high-half DIRECT packet should be 1 QW"); + + bool payloadOk = true; + for (uint32_t i = 0; i < 16u; ++i) + { + if (captured[0][i] != static_cast(0xA0u + i)) + { + payloadOk = false; + break; + } + } + t.IsTrue(payloadOk, "REF payload should reach the GIF callback without VIF desynchronization"); + } + }); + + tc.Run("VIF1 DMA chain ignores tag high bytes when TTE is disabled", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kVif1Ch = 0x10009000u; + constexpr uint32_t kTag = 0x00025400u; + + uint8_t *rdram = mem.getRDRAM(); + writeDmaTag(rdram, kTag, makeDmaTag(0u, 7u, 0u, false)); // END + const uint32_t itopCmd = makeVifCmd(0x04u, 0u, 0x55u); + std::memcpy(rdram + kTag + 12u, &itopCmd, sizeof(itopCmd)); + + t.IsTrue(mem.writeIORegister(kVif1Ch + 0x30u, kTag), "write VIF1 TADR should succeed"); + t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x104u), + "write VIF1 CHCR STR|CHAIN without TTE should succeed"); + + mem.processPendingTransfers(); + + t.Equals(mem.vif1_regs.itops, 0u, + "tag high-half VIFcodes must stay hidden when CHCR.TTE is clear"); + }); + tc.Run("VIF1 packet builders keep chain qwc live before terminate", [](TestCase &t) { PS2Memory mem; @@ -1497,7 +1628,8 @@ void register_ps2_memory_tests() }); t.IsTrue(mem.writeIORegister(kVif1Ch + 0x30u, kBaseAddr), "write VIF1 TADR should succeed"); - t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x104u), "write VIF1 CHCR STR|CHAIN should succeed"); + t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x144u), + "write VIF1 CHCR STR|CHAIN|TTE should succeed"); mem.processPendingTransfers(); @@ -1780,6 +1912,74 @@ void register_ps2_memory_tests() } }); + tc.Run("DMAC SPR_FROM copies scratchpad to RDRAM and completes channel 8", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kChannel = 0x1000D000u; + constexpr uint32_t kMadr = 0x00028000u; + constexpr uint32_t kSadr = 0x00000120u; + constexpr uint32_t kQwc = 2u; + constexpr uint32_t kBytes = kQwc * 16u; + + for (uint32_t i = 0; i < kBytes; ++i) + mem.getScratchpad()[kSadr + i] = static_cast(0x30u + i); + + t.IsTrue(mem.writeIORegister(kChannel + 0x10u, kMadr), "write SPR_FROM MADR should succeed"); + t.IsTrue(mem.writeIORegister(kChannel + 0x20u, kQwc), "write SPR_FROM QWC should succeed"); + t.IsTrue(mem.writeIORegister(kChannel + 0x80u, kSadr), "write SPR_FROM SADR should succeed"); + t.IsTrue(mem.writeIORegister(kChannel + 0x00u, 0x100u), "start SPR_FROM should succeed"); + + bool copied = true; + for (uint32_t i = 0; i < kBytes; ++i) + copied = copied && mem.getRDRAM()[kMadr + i] == static_cast(0x30u + i); + t.IsTrue(copied, "SPR_FROM should copy every qword from scratchpad to RDRAM"); + t.IsTrue((mem.readIORegister(kChannel + 0x00u) & 0x100u) == 0u, "SPR_FROM completion should clear CHCR.STR"); + t.Equals(mem.readIORegister(kChannel + 0x20u), 0u, "SPR_FROM completion should consume QWC"); + t.Equals(mem.readIORegister(kChannel + 0x10u), kMadr + kBytes, "SPR_FROM should advance MADR"); + t.Equals(mem.readIORegister(kChannel + 0x80u), (kSadr + kBytes) & 0x3FFFu, "SPR_FROM should advance SADR"); + t.IsTrue((mem.readIORegister(0x1000E010u) & (1u << 8u)) != 0u, "SPR_FROM should raise D_STAT channel 8"); + + const std::vector causes = mem.consumeCompletedDmacCauses(); + t.Equals(causes.size(), static_cast(1u), "SPR_FROM should queue one DMAC completion"); + if (!causes.empty()) + t.Equals(causes[0], 8u, "SPR_FROM completion should use DMAC cause 8"); + }); + + tc.Run("DMAC SPR_TO copies RDRAM to scratchpad and completes channel 9", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kChannel = 0x1000D400u; + constexpr uint32_t kMadr = 0x00028400u; + constexpr uint32_t kSadr = 0x00000240u; + constexpr uint32_t kQwc = 2u; + constexpr uint32_t kBytes = kQwc * 16u; + + for (uint32_t i = 0; i < kBytes; ++i) + mem.getRDRAM()[kMadr + i] = static_cast(0x70u + i); + + t.IsTrue(mem.writeIORegister(kChannel + 0x10u, kMadr), "write SPR_TO MADR should succeed"); + t.IsTrue(mem.writeIORegister(kChannel + 0x20u, kQwc), "write SPR_TO QWC should succeed"); + t.IsTrue(mem.writeIORegister(kChannel + 0x80u, kSadr), "write SPR_TO SADR should succeed"); + t.IsTrue(mem.writeIORegister(kChannel + 0x00u, 0x100u), "start SPR_TO should succeed"); + + bool copied = true; + for (uint32_t i = 0; i < kBytes; ++i) + copied = copied && mem.getScratchpad()[kSadr + i] == static_cast(0x70u + i); + t.IsTrue(copied, "SPR_TO should copy every qword from RDRAM to scratchpad"); + t.IsTrue((mem.readIORegister(kChannel + 0x00u) & 0x100u) == 0u, "SPR_TO completion should clear CHCR.STR"); + t.Equals(mem.readIORegister(kChannel + 0x20u), 0u, "SPR_TO completion should consume QWC"); + t.IsTrue((mem.readIORegister(0x1000E010u) & (1u << 9u)) != 0u, "SPR_TO should raise D_STAT channel 9"); + + const std::vector causes = mem.consumeCompletedDmacCauses(); + t.Equals(causes.size(), static_cast(1u), "SPR_TO should queue one DMAC completion"); + if (!causes.empty()) + t.Equals(causes[0], 9u, "SPR_TO completion should use DMAC cause 9"); + }); + tc.Run("sceDmaReset re-enables DMAC DMAE", [](TestCase &t) { PS2Runtime runtime; @@ -1812,6 +2012,38 @@ void register_ps2_memory_tests() t.Equals(mem.readIORegister(kDstadr), 0u, "sceDmaReset should clear D_STADR"); }); + tc.Run("sceDmaSend preserves guest-configured VIF1 TTE", [](TestCase &t) + { + PS2Runtime runtime; + t.IsTrue(runtime.memory().initialize(), "PS2Memory initialize should succeed"); + + constexpr uint32_t kVif1Ch = 0x10009000u; + constexpr uint32_t kTag = 0x00028500u; + + PS2Memory &mem = runtime.memory(); + uint8_t *rdram = mem.getRDRAM(); + writeDmaTag(rdram, kTag, makeDmaTag(0u, 7u, 0u, false)); // END + const uint32_t itopCmd = makeVifCmd(0x04u, 0u, 0x66u); + std::memcpy(rdram + kTag + 12u, &itopCmd, sizeof(itopCmd)); + + // Fatal Frame follows this exact sequence: get channel, set CHCR.TTE, + // then submit the chain through sceDmaSend. + t.IsTrue(mem.writeIORegister(kVif1Ch + 0x00u, 0x40u), + "guest should be able to configure VIF1 CHCR.TTE before submission"); + + R5900Context ctx{}; + setRegU32(ctx, 4, 1u); // sceDmaGetChan(1) / VIF1 + setRegU32(ctx, 5, kTag); + ps2_stubs::sceDmaSend(rdram, &ctx, &runtime); + + t.Equals(static_cast(::getRegU32(&ctx, 2)), 0, + "sceDmaSend should accept the VIF1 chain"); + t.IsTrue((mem.readIORegister(kVif1Ch + 0x00u) & 0x40u) != 0u, + "sceDmaSend must preserve guest-configured CHCR.TTE"); + t.Equals(mem.vif1_regs.itops, 0x66u, + "preserved TTE should deliver the tag high-half VIFcode"); + }); + tc.Run("VIF1 DMA DIRECT image packet reaches GS through arbiter", [](TestCase &t) { PS2Memory mem; @@ -1933,6 +2165,57 @@ void register_ps2_memory_tests() t.IsTrue(imageOk, "raw qwords after a DIRECT image tag should continue the PATH2 image upload"); }); + tc.Run("VIF1 DIRECT finds an image continuation after packed setup", [](TestCase &t) + { + PS2Memory mem; + t.IsTrue(mem.initialize(), "PS2Memory initialize should succeed"); + + GS gs; + gs.init(mem.getGSVRAM(), static_cast(PS2_GS_VRAM_SIZE), &mem.gs()); + GifArbiter arbiter([&](const uint8_t *data, uint32_t sizeBytes) + { + gs.processGIFPacket(data, sizeBytes); + }); + mem.setGifArbiter(&arbiter); + + const uint64_t bitblt = + (static_cast(1u) << 16) | + (static_cast(1u) << 48); + gs.writeRegister(GS_REG_BITBLTBUF, bitblt); + gs.writeRegister(GS_REG_TRXPOS, 0ull); + gs.writeRegister(GS_REG_TRXREG, (4ull << 0) | (1ull << 32)); + gs.writeRegister(GS_REG_TRXDIR, 0ull); + + std::vector packet; + appendU32(packet, makeVifCmd(0x50u, 0u, 3u)); // PACKED tag + A+D + IMAGE tag. + appendU64(packet, makeGifTag(1u, GIF_FMT_PACKED, 1u, false)); + appendU64(packet, 0x0Eull); + appendU64(packet, 0x8000008000ull); // TEXA, harmless setup preceding the IMAGE tag. + appendU64(packet, GS_REG_TEXA); + appendU64(packet, makeGifTag(1u, GIF_FMT_IMAGE, 0u, true)); + appendU64(packet, 0ull); + for (uint32_t i = 0; i < 16u; ++i) + packet.push_back(static_cast(0xC0u + i)); + + mem.processVIF1Data(packet.data(), static_cast(packet.size())); + + const uint8_t *vramOut = mem.getGSVRAM(); + bool imageOk = true; + for (uint32_t x = 0; x < 4u && imageOk; ++x) + { + const uint32_t off = GSPSMCT32::addrPSMCT32(0u, 1u, x, 0u); + for (uint32_t c = 0; c < 4u; ++c) + { + if (vramOut[off + c] != static_cast(0xC0u + x * 4u + c)) + { + imageOk = false; + break; + } + } + } + t.IsTrue(imageOk, "raw image continuation after packed setup should not be decoded as VIF/GIF registers"); + }); + tc.Run("unaligned accesses throw", [](TestCase &t) { PS2Memory mem; diff --git a/ps2xTest/src/ps2_recompiler_tests.cpp b/ps2xTest/src/ps2_recompiler_tests.cpp index 4dce622..86e5e3f 100644 --- a/ps2xTest/src/ps2_recompiler_tests.cpp +++ b/ps2xTest/src/ps2_recompiler_tests.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -155,6 +156,489 @@ static bool writeMinimalMipsElfWithJalFallbackTarget(const std::filesystem::path return writer.save(elfPath.string()); } +static bool writeMinimalMipsElfWithVuMicroprogramSection(const std::filesystem::path &elfPath) +{ + ELFIO::elfio writer; + writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB); + writer.set_os_abi(ELFIO::ELFOSABI_NONE); + writer.set_type(ELFIO::ET_EXEC); + writer.set_machine(ELFIO::EM_MIPS); + writer.set_entry(0x00100000u); + + ELFIO::section *text = writer.sections.add(".text"); + text->set_type(ELFIO::SHT_PROGBITS); + text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR); + text->set_addr_align(4); + text->set_address(0x00100000u); + const std::array textWords{0x03E00008u, 0x00000000u}; + text->set_data(reinterpret_cast(textWords.data()), sizeof(textWords)); + + ELFIO::section *vuText = writer.sections.add(".vutext"); + vuText->set_type(ELFIO::SHT_PROGBITS); + vuText->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR); + vuText->set_addr_align(16); + vuText->set_address(0x00250000u); + const std::array vuWords{0x01EC48BDu, 0u, 0x01FA717Du, 0u}; + vuText->set_data(reinterpret_cast(vuWords.data()), sizeof(vuWords)); + + ELFIO::section *strtab = writer.sections.add(".strtab"); + strtab->set_type(ELFIO::SHT_STRTAB); + strtab->set_addr_align(1); + + ELFIO::section *symtab = writer.sections.add(".symtab"); + symtab->set_type(ELFIO::SHT_SYMTAB); + symtab->set_info(1); + symtab->set_link(strtab->get_index()); + symtab->set_addr_align(4); + symtab->set_entry_size(writer.get_default_entry_size(ELFIO::SHT_SYMTAB)); + + ELFIO::symbol_section_accessor symbols(writer, symtab); + ELFIO::string_section_accessor strings(strtab); + symbols.add_symbol(strings, "", 0, 0, ELFIO::STB_LOCAL, ELFIO::STT_NOTYPE, 0, ELFIO::SHN_UNDEF); + symbols.add_symbol(strings, "ee_entry", text->get_address(), text->get_size(), + ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, text->get_index()); + symbols.add_symbol(strings, "vu_program", vuText->get_address(), vuText->get_size(), + ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, vuText->get_index()); + + ELFIO::segment *textSegment = writer.segments.add(); + textSegment->set_type(ELFIO::PT_LOAD); + textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X); + textSegment->set_align(0x1000); + textSegment->add_section_index(text->get_index(), text->get_addr_align()); + + ELFIO::segment *vuSegment = writer.segments.add(); + vuSegment->set_type(ELFIO::PT_LOAD); + vuSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X); + vuSegment->set_align(0x1000); + vuSegment->add_section_index(vuText->get_index(), vuText->get_addr_align()); + + return writer.save(elfPath.string()); +} + +static bool writeMinimalMipsElfWithUnmappedEntryHint(const std::filesystem::path &elfPath) +{ + ELFIO::elfio writer; + writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB); + writer.set_os_abi(ELFIO::ELFOSABI_NONE); + writer.set_type(ELFIO::ET_EXEC); + writer.set_machine(ELFIO::EM_MIPS); + writer.set_entry(0x00100000u); + + ELFIO::section *text = writer.sections.add(".text"); + text->set_type(ELFIO::SHT_PROGBITS); + text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR); + text->set_addr_align(4); + text->set_address(0x00100000u); + const std::array textWords = { + 0x03E00008u, 0x00000000u, // known function at 0x00100000 + 0x00000000u, 0x00000000u, + 0x03E00008u, 0x00000000u, // omitted entry at 0x00100010 + 0x00000000u, 0x00000000u, + 0x03E00008u, 0x00000000u, // next known function at 0x00100020 + }; + text->set_data(reinterpret_cast(textWords.data()), + static_cast(textWords.size() * sizeof(uint32_t))); + + ELFIO::section *strtab = writer.sections.add(".strtab"); + strtab->set_type(ELFIO::SHT_STRTAB); + strtab->set_addr_align(1); + + ELFIO::section *symtab = writer.sections.add(".symtab"); + symtab->set_type(ELFIO::SHT_SYMTAB); + symtab->set_info(1); + symtab->set_link(strtab->get_index()); + symtab->set_addr_align(4); + symtab->set_entry_size(writer.get_default_entry_size(ELFIO::SHT_SYMTAB)); + + ELFIO::symbol_section_accessor symbols(writer, symtab); + ELFIO::string_section_accessor strings(strtab); + symbols.add_symbol(strings, "", 0, 0, + ELFIO::STB_LOCAL, ELFIO::STT_NOTYPE, 0, ELFIO::SHN_UNDEF); + symbols.add_symbol(strings, "known_before", 0x00100000u, 8u, + ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, text->get_index()); + symbols.add_symbol(strings, "stubbed_owner", 0x00100008u, 0x18u, + ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, text->get_index()); + symbols.add_symbol(strings, "known_after", 0x00100020u, 8u, + ELFIO::STB_GLOBAL, ELFIO::STT_FUNC, 0, text->get_index()); + + ELFIO::segment *textSegment = writer.segments.add(); + textSegment->set_type(ELFIO::PT_LOAD); + textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X); + textSegment->set_align(0x1000); + textSegment->add_section_index(text->get_index(), text->get_addr_align()); + + return writer.save(elfPath.string()); +} + +static bool writeMinimalMipsElfWithAddressTakenCallbacks(const std::filesystem::path &elfPath, + bool includePartialDwarf = false) +{ + ELFIO::elfio writer; + writer.create(ELFIO::ELFCLASS32, ELFIO::ELFDATA2LSB); + writer.set_os_abi(ELFIO::ELFOSABI_NONE); + writer.set_type(ELFIO::ET_EXEC); + writer.set_machine(ELFIO::EM_MIPS); + writer.set_entry(0x00100000u); + + ELFIO::section *text = writer.sections.add(".text"); + text->set_type(ELFIO::SHT_PROGBITS); + text->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_EXECINSTR); + text->set_addr_align(4); + text->set_address(0x00100000u); + + std::array textWords{}; + textWords[0] = 0x3C040010u; // lui a0,0x10 + textWords[1] = 0xAC800000u; // sw zero,0(a0) + textWords[2] = 0x0C040008u; // jal 0x00100020 (callback registrar) + textWords[3] = 0x24840040u; // addiu a0,a0,0x40 (delay slot) + textWords[4] = 0x03E00008u; // jr ra + textWords[5] = 0x00000000u; // nop + textWords[6] = 0x3C080010u; // lui t0,0x10 + textWords[7] = 0x25080300u; // addiu t0,t0,0x300 (code label, not a callback argument) + + textWords[8] = 0x03E00008u; // registrar at 0x00100020 + textWords[9] = 0x00000000u; + + textWords[16] = 0x27BDFFF0u; // callback at 0x00100040: addiu sp,sp,-0x10 + textWords[17] = 0xFFBF0000u; // sd ra,0(sp) + textWords[18] = 0xDFBF0000u; // ld ra,0(sp) + textWords[19] = 0x03E00008u; // jr ra + textWords[20] = 0x27BD0010u; // addiu sp,sp,0x10 + + textWords[24] = 0x08040008u; // table leaf thunk at 0x00100060: j 0x00100020 + textWords[25] = 0x00000000u; // nop (delay slot) + textWords[26] = 0x03E00008u; // table leaf at 0x00100068 + textWords[27] = 0x00000000u; + textWords[28] = 0x03E00008u; // adjacent leaf thunk at 0x00100070 + textWords[29] = 0x00000000u; + + // Address-taken initializer at 0x00100080 with a long constant-setup + // preamble before its stack frame, matching retail constructor tables. + textWords[32] = 0x3C020010u; // lui v0,0x10 + textWords[33] = 0x3C030010u; // lui v1,0x10 + textWords[34] = 0x3C050010u; // lui a1,0x10 + textWords[35] = 0x3C060010u; // lui a2,0x10 + textWords[36] = 0x3C070010u; // lui a3,0x10 + textWords[37] = 0x3C080010u; // lui t0,0x10 + textWords[38] = 0x3C090010u; // lui t1,0x10 + textWords[39] = 0x3C0A0010u; // lui t2,0x10 + textWords[40] = 0x3C0B0010u; // lui t3,0x10 + textWords[41] = 0x27BDFFF0u; // addiu sp,sp,-0x10 + textWords[42] = 0x03E00008u; // jr ra + textWords[43] = 0x27BD0010u; // addiu sp,sp,0x10 (delay slot) + + // A callback address completed three instructions before the registrar call. + // Its leaf body is deliberately longer than a small thunk and begins after a + // preceding function's return, matching stripped retail ELF callback ranges. + textWords[44] = 0x3C060010u; // lui a2,0x10 + textWords[45] = 0x7FB00010u; // sq s0,0x10(sp) + textWords[46] = 0xFFBF0000u; // sd ra,0(sp) + textWords[47] = 0x24C600E0u; // addiu a2,a2,0xE0 (callback at 0x001000E0) + textWords[48] = 0x24040008u; // addiu a0,zero,8 + textWords[49] = 0x2405040Fu; // addiu a1,zero,0x40F + textWords[50] = 0x0C040008u; // jal 0x00100020 (callback registrar) + textWords[51] = 0x00000000u; // nop (delay slot) + + textWords[56] = 0x3C020010u; // long leaf callback at 0x001000E0 + textWords[57] = 0x8C420200u; + textWords[58] = 0x3C030020u; + textWords[59] = 0x24630100u; + textWords[60] = 0x3C068000u; + textWords[61] = 0x24420001u; + textWords[62] = 0x00A31821u; + textWords[63] = 0x3C010010u; + textWords[64] = 0xAC420200u; + textWords[65] = 0xAC660004u; + textWords[66] = 0x0080102Du; + textWords[67] = 0x3C010010u; + textWords[68] = 0xAC450204u; + textWords[69] = 0x03E00008u; // jr ra, beyond the old eight-word leaf window + textWords[70] = 0xAC600000u; // sw zero,0(v1) (delay slot) + + // A long leaf method referenced only by a clustered descriptor table. Its + // return is deliberately beyond the materialized-callback probe distance. + textWords[72] = 0x8C850014u; // lw a1,0x14(a0), method at 0x00100120 + for (size_t index = 73; index < 121; ++index) + { + textWords[index] = 0x24420001u; // addiu v0,v0,1 + } + textWords[121] = 0x03E00008u; // jr ra at method instruction 49 + textWords[122] = 0x00000000u; // nop (delay slot) + + // Some retail callback registrars keep an address in a saved register while + // assembling the remaining arguments, then copy it into a2 immediately before + // the JAL. The call is deliberately well beyond any small lookahead window. + textWords[128] = 0x3C140010u; // lui s4,0x10 + textWords[129] = 0x26940280u; // addiu s4,s4,0x280 (callback at 0x00100280) + textWords[130] = 0x7FB00060u; // sq s0,0x60(sp) + textWords[131] = 0x7FB10050u; // sq s1,0x50(sp) + textWords[132] = 0x24070001u; // addiu a3,zero,1 + textWords[133] = 0x7FB20040u; // sq s2,0x40(sp) + textWords[134] = 0x0000202Du; // daddu a0,zero,zero + textWords[135] = 0x7FB30030u; // sq s3,0x30(sp) + textWords[136] = 0xFFBF0000u; // sd ra,0(sp) + textWords[137] = 0x2405011Fu; // addiu a1,zero,0x11F + textWords[138] = 0x0C040008u; // setup call; s4 must preserve the incomplete address + textWords[139] = 0x00000000u; // nop (delay slot) + textWords[144] = 0x0280302Du; // daddu a2,s4,zero + textWords[146] = 0x0C040008u; // jal 0x00100020 (callback registrar) + textWords[147] = 0x00000000u; // nop (delay slot) + + // A retail-style conditional initializes a callback register in its delay + // slot, then completes the address only in the taken successor block. A + // linear lookahead cannot connect these two halves; CFG traversal must. + textWords[148] = 0x04410004u; // bgez v0,0x00100264 + textWords[149] = 0x3C060010u; // lui a2,0x10 (delay slot) + textWords[150] = 0x10000008u; // b 0x0010027C (not-taken path) + textWords[151] = 0x00000000u; // nop (delay slot) + textWords[153] = 0x24C602C0u; // addiu a2,a2,0x2C0 + textWords[154] = 0x0C040008u; // jal 0x00100020 (callback registrar) + textWords[155] = 0x24040008u; // addiu a0,zero,8 (delay slot) + + textWords[160] = 0x3C030010u; // callback at 0x00100280 + textWords[161] = 0x8C630200u; + textWords[162] = 0x24630001u; + textWords[163] = 0xAC630200u; + textWords[164] = 0x03E00008u; // jr ra + textWords[165] = 0x0080102Du; // daddu v0,a0,zero (delay slot) + + textWords[176] = 0x8F830000u; // callback at 0x001002C0: lw v1,0(gp) + textWords[177] = 0x0080102Du; // daddu v0,a0,zero + textWords[178] = 0x2405FFFFu; // addiu a1,zero,-1 + textWords[179] = 0x00832021u; // addu a0,a0,v1 + textWords[180] = 0x03E00008u; // jr ra + textWords[181] = 0xAC850000u; // sw a1,0(a0) (delay slot) + + // Retail class constructors often build their method tables in writable + // memory instead of shipping literal function pointers in .rodata. The + // materialized code address is never passed to a registrar; storing it in + // the descriptor is the only address-taken evidence. + textWords[184] = 0x3C040010u; // lui a0,0x10 + textWords[185] = 0x24840340u; // addiu a0,a0,0x340 (method at 0x00100340) + textWords[186] = 0xAE44001Cu; // sw a0,0x1c(s2) + textWords[187] = 0x0000202Du; // daddu a0,zero,zero (clobber) + + textWords[208] = 0x3C020020u; // stored leaf method at 0x00100340 + textWords[209] = 0x03E00008u; // jr ra + textWords[210] = 0x24420100u; // addiu v0,v0,0x100 (delay slot) + + // Long leaf in an alternating (function pointer, numeric id) table. This + // is a common stripped retail dispatch-table layout and provides strong + // address evidence through the adjacent ordinary function pointer. + textWords[224] = 0x3C010020u; // long leaf at 0x00100380 + for (size_t index = 225; index < 235; ++index) + { + textWords[index] = 0x24420001u; + } + textWords[235] = 0x03E00008u; + textWords[236] = 0x00000000u; + + // A stripped function map may merge the middle member of a run of trivial + // leaf accessors into its predecessor. Only the first accessor is reached by + // a direct call; the second still needs its own callable entry. + textWords[188] = 0x0C0400F0u; // jal 0x001003C0 + textWords[189] = 0x00000000u; // nop (delay slot) + textWords[192] = 0x03E00008u; // isolated pointer target at 0x00100300 + textWords[193] = 0x00000000u; // nop (delay slot) + textWords[240] = 0x03E00008u; // known leaf at 0x001003C0: jr ra + textWords[241] = 0x0080102Du; // daddu v0,a0,zero (delay slot) + textWords[242] = 0x03E00008u; // merged leaf at 0x001003C8: jr ra + textWords[243] = 0x0080102Du; // daddu v0,a0,zero (delay slot) + + // Some retail registrars take more than four register arguments. The fifth + // callback is passed in physical t0, followed by unrelated argument setup + // before the call. It must remain distinguishable from the dead t0 code + // materialization at 0x00100018 above. + textWords[196] = 0x3C080010u; // lui t0,0x10 + textWords[197] = 0x250803E0u; // addiu t0,t0,0x3E0 (callback at 0x001003E0) + textWords[198] = 0x24040014u; // addiu a0,zero,0x14 + textWords[199] = 0x0C040008u; // jal 0x00100020 (callback registrar) + textWords[200] = 0x24050A0Bu; // addiu a1,zero,0xA0B (delay slot) + + textWords[248] = 0x03E00008u; // extended-argument leaf at 0x001003E0: jr ra + textWords[249] = 0x0080102Du; // daddu v0,a0,zero (delay slot) + + // The fifth register argument can also reference a substantial leaf body. + // Its return deliberately lies beyond the old fixed 64-instruction scan + // window, so discovery must follow the candidate's reachable control flow. + textWords[201] = 0x3C080010u; // lui t0,0x10 + textWords[202] = 0x25080400u; // addiu t0,t0,0x400 (callback at 0x00100400) + textWords[203] = 0x24040030u; // addiu a0,zero,0x30 + textWords[204] = 0x0C040008u; // jal 0x00100020 (callback registrar) + textWords[205] = 0x24050A06u; // addiu a1,zero,0xA06 (delay slot) + + textWords[256] = 0x3C080048u; // long extended-argument leaf at 0x00100400 + for (size_t index = 257; index < 336; ++index) + { + textWords[index] = 0x24420001u; // addiu v0,v0,1 + } + textWords[336] = 0x03E00008u; // jr ra at instruction 80 + textWords[337] = 0x00000000u; // nop (delay slot) + + // Stripped function maps can merge a normal non-leaf function into the + // preceding function even though the boundary is unambiguous in the bytes: + // `jr ra`, its delay slot, then a fresh stack allocation. + textWords[338] = 0x27BDFFF0u; // post-return function at 0x00100548 + textWords[339] = 0xFFBF0000u; // sd ra,0(sp) + textWords[340] = 0x03E00008u; // jr ra + textWords[341] = 0x27BD0010u; // addiu sp,sp,0x10 (delay slot) + + // The target at 0x00100580 has only a singleton initialized-data pointer. + // A known function loads that slot and invokes it through JALR, matching + // retail callback slots that are not large enough to look like a table. + textWords[30] = 0x0C0400D4u; // jal 0x00100350 + textWords[31] = 0x00000000u; // nop (delay slot) + textWords[212] = 0x27BDFFF0u; // indirect caller at 0x00100350 + textWords[213] = 0xFFBF0000u; // sd ra,0(sp) + textWords[214] = 0x3C100020u; // lui s0,0x20 + textWords[215] = 0x8E021000u; // lw v0,0x1000(s0) -> [0x00201000] + textWords[216] = 0x00000000u; // nop + textWords[217] = 0x0040F809u; // jalr v0 + textWords[218] = 0x00000000u; // nop (delay slot) + textWords[219] = 0xDFBF0000u; // ld ra,0(sp) + textWords[220] = 0x03E00008u; // jr ra + textWords[221] = 0x27BD0010u; // addiu sp,sp,0x10 (delay slot) + textWords[352] = 0x27BDFFF0u; // singleton data target at 0x00100580 + textWords[353] = 0x0320F809u; // jalr t9 + textWords[354] = 0x0200202Du; // daddu a0,s0,zero (delay slot at 0x00100588) + textWords[355] = 0x24420001u; // addiu v0,v0,1 + textWords[356] = 0x24420001u; // addiu v0,v0,1 + textWords[357] = 0x03E00008u; // jr ra + textWords[358] = 0x27BD0010u; // addiu sp,sp,0x10 (delay slot) + + // A second initialized-data word deliberately points at 0x00100588, the + // delay slot of the JALR above. Its following body can look callable to a + // reachability probe, but splitting there would truncate the real owner. + textWords[12] = 0x0C040170u; // jal 0x001005C0 + textWords[13] = 0x00000000u; // nop (delay slot) + textWords[368] = 0x27BDFFF0u; // delay-slot pointer caller at 0x001005C0 + textWords[369] = 0x3C100020u; // lui s0,0x20 + textWords[370] = 0x8E021040u; // lw v0,0x1040(s0) -> [0x00201040] + textWords[371] = 0x0040F809u; // jalr v0 + textWords[372] = 0x00000000u; // nop (delay slot) + textWords[373] = 0x03E00008u; // jr ra + textWords[374] = 0x27BD0010u; // addiu sp,sp,0x10 (delay slot) + + text->set_data(reinterpret_cast(textWords.data()), + static_cast(textWords.size() * sizeof(uint32_t))); + + ELFIO::section *rodata = writer.sections.add(".rodata"); + rodata->set_type(ELFIO::SHT_PROGBITS); + rodata->set_flags(ELFIO::SHF_ALLOC); + rodata->set_addr_align(4); + rodata->set_address(0x00200000u); + + std::array tableWords{}; + tableWords[1] = 0x00100060u; + tableWords[3] = 0x00100068u; + // Retail class descriptor: name pointer, ordinary method, two reserved + // words, then a long leaf method. + tableWords[5] = 0x0020004Cu; + tableWords[6] = 0x00100080u; + tableWords[7] = 0; + tableWords[8] = 0; + tableWords[9] = 0x00100120u; + tableWords[16] = 0x00100300u; // plausible entry, but not part of a pointer cluster + tableWords[28] = 0x00100080u; // ordinary function, followed by a numeric id + tableWords[29] = 0x0000000Du; + tableWords[30] = 0x00100380u; // long leaf, followed by a numeric id + tableWords[31] = 0x0000000Bu; + rodata->set_data(reinterpret_cast(tableWords.data()), + static_cast(tableWords.size() * sizeof(uint32_t))); + + ELFIO::section *data = writer.sections.add(".data"); + data->set_type(ELFIO::SHT_PROGBITS); + data->set_flags(ELFIO::SHF_ALLOC | ELFIO::SHF_WRITE); + data->set_addr_align(4); + data->set_address(0x00201000u); + std::array singletonCallbacks{}; + singletonCallbacks[0] = 0x00100580u; + singletonCallbacks[16] = 0x00100588u; + data->set_data(reinterpret_cast(singletonCallbacks.data()), + static_cast(singletonCallbacks.size() * sizeof(uint32_t))); + + if (includePartialDwarf) + { + // A retail ELF can retain debug information for only part of its code. + // The parser must still supplement that incomplete map with static + // address-taken discovery instead of treating any DWARF as exhaustive. + const std::array abbrevBytes = { + 0x01, 0x11, 0x01, // abbrev 1: compile_unit, has children + 0x03, 0x08, // DW_AT_name, DW_FORM_string + 0x00, 0x00, + 0x02, 0x2E, 0x00, // abbrev 2: subprogram, no children + 0x03, 0x08, // DW_AT_name, DW_FORM_string + 0x11, 0x01, // DW_AT_low_pc, DW_FORM_addr + 0x12, 0x06, // DW_AT_high_pc, DW_FORM_data4 + 0x00, 0x00, // end of attribute list + 0x00}; // end of abbreviation table + + ELFIO::section *debugAbbrev = writer.sections.add(".debug_abbrev"); + debugAbbrev->set_type(ELFIO::SHT_PROGBITS); + debugAbbrev->set_addr_align(1); + debugAbbrev->set_data(reinterpret_cast(abbrevBytes.data()), + static_cast(abbrevBytes.size())); + + std::vector infoBytes(sizeof(uint32_t), 0); + auto appendU8 = [&infoBytes](uint8_t value) + { infoBytes.push_back(value); }; + auto appendU16 = [&infoBytes](uint16_t value) + { + const auto *bytes = reinterpret_cast(&value); + infoBytes.insert(infoBytes.end(), bytes, bytes + sizeof(value)); + }; + auto appendU32 = [&infoBytes](uint32_t value) + { + const auto *bytes = reinterpret_cast(&value); + infoBytes.insert(infoBytes.end(), bytes, bytes + sizeof(value)); + }; + auto appendString = [&infoBytes](std::string_view value) + { + infoBytes.insert(infoBytes.end(), value.begin(), value.end()); + infoBytes.push_back(0); + }; + + appendU16(4); // DWARF version + appendU32(0); // abbreviation table offset + appendU8(4); // address size + appendU8(1); // compile-unit DIE + appendString("partial-unit"); + appendU8(2); // subprogram DIE + appendString("known_partial_function"); + appendU32(0x00100000u); + appendU32(0x20u); // DWARF 4 high_pc offset + appendU8(2); // a later known subprogram bounds fallback ranges + appendString("known_tail_function"); + appendU32(0x001003F0u); + appendU32(0x10u); + appendU8(0); // end compile-unit children + + const uint32_t unitLength = static_cast(infoBytes.size() - sizeof(uint32_t)); + std::memcpy(infoBytes.data(), &unitLength, sizeof(unitLength)); + + ELFIO::section *debugInfo = writer.sections.add(".debug_info"); + debugInfo->set_type(ELFIO::SHT_PROGBITS); + debugInfo->set_addr_align(1); + debugInfo->set_data(reinterpret_cast(infoBytes.data()), + static_cast(infoBytes.size())); + } + + ELFIO::segment *textSegment = writer.segments.add(); + textSegment->set_type(ELFIO::PT_LOAD); + textSegment->set_flags(ELFIO::PF_R | ELFIO::PF_X); + textSegment->set_align(0x1000); + textSegment->add_section_index(text->get_index(), text->get_addr_align()); + + ELFIO::segment *dataSegment = writer.segments.add(); + dataSegment->set_type(ELFIO::PT_LOAD); + dataSegment->set_flags(ELFIO::PF_R); + dataSegment->set_align(0x1000); + dataSegment->add_section_index(rodata->get_index(), rodata->get_addr_align()); + dataSegment->add_section_index(data->get_index(), data->get_addr_align()); + + return writer.save(elfPath.string()); +} + static bool writeMinimalMipsElfWithInitializer(const std::filesystem::path &elfPath, const std::string &functionName, uint32_t initializerTarget) @@ -223,7 +707,8 @@ static bool writeRecompilerTestConfig(const std::filesystem::path &configPath, const std::filesystem::path &elfPath, const std::filesystem::path &outputPath, const std::vector &skip, - const std::vector &stubs = {}) + const std::vector &stubs = {}, + const std::vector &entryPoints = {}) { std::ofstream config(configPath); if (!config) @@ -248,6 +733,14 @@ static bool writeRecompilerTestConfig(const std::filesystem::path &configPath, config << '"' << stubs[i] << '"'; } config << "]\n"; + config << "entry_points = ["; + for (size_t i = 0; i < entryPoints.size(); ++i) + { + if (i != 0u) + config << ", "; + config << '"' << entryPoints[i] << '"'; + } + config << "]\n"; return static_cast(config); } @@ -838,6 +1331,87 @@ void register_ps2_recompiler_tests() std::filesystem::remove(configPath, removeError); }); + tc.Run("config manager loads modern and legacy guest entry hints", [](TestCase &t) { + const auto uniqueSuffix = std::to_string( + static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); + const std::filesystem::path configPath = + std::filesystem::temp_directory_path() / ("ps2recomp-entry-hints-" + uniqueSuffix + ".toml"); + + std::ofstream configFile(configPath); + t.IsTrue(static_cast(configFile), "temp config file should be writable"); + if (!configFile) + { + return; + } + + configFile << "[general]\n"; + configFile << "input = \"dummy.elf\"\n"; + configFile << "output = \"out\"\n"; + configFile << "entry_points = [\"callback@0x7008\"]\n"; + configFile << "untracked_stubs = [\"legacy_callback@0x7018\"]\n"; + configFile.close(); + + ConfigManager manager(configPath.string()); + const RecompilerConfig config = manager.loadConfig(); + + t.Equals(config.entryPointHints.size(), static_cast(2), + "modern and legacy entry metadata should be merged"); + t.IsTrue(std::find(config.entryPointHints.begin(), config.entryPointHints.end(), + "callback@0x7008") != config.entryPointHints.end(), + "modern entry_points metadata should load"); + t.IsTrue(std::find(config.entryPointHints.begin(), config.entryPointHints.end(), + "legacy_callback@0x7018") != config.entryPointHints.end(), + "legacy untracked_stubs metadata should remain compatible"); + + std::error_code removeError; + std::filesystem::remove(configPath, removeError); + }); + + tc.Run("configured entry hint synthesizes an omitted standalone function", [](TestCase &t) { + const std::string uniqueSuffix = + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const std::filesystem::path tempRoot = + std::filesystem::temp_directory_path() / ("ps2recomp-entry-synthesis-" + uniqueSuffix); + const std::filesystem::path elfPath = tempRoot / "entry-hint.elf"; + const std::filesystem::path configPath = tempRoot / "entry-hint.toml"; + const std::filesystem::path outputPath = tempRoot / "output"; + std::filesystem::create_directories(tempRoot); + + const bool elfWritten = writeMinimalMipsElfWithUnmappedEntryHint(elfPath); + const bool configWritten = writeRecompilerTestConfig( + configPath, + elfPath, + outputPath, + {}, + {"InitAlarm@0x00100008"}, + {"omitted_callback@0x00100010"}); + t.IsTrue(elfWritten && configWritten, + "standalone entry regression inputs should be generated"); + + if (elfWritten && configWritten) + { + PS2Recompiler recompiler(configPath.string()); + t.IsTrue(recompiler.initialize(), + "standalone entry regression config should initialize"); + t.IsTrue(recompiler.recompile(), + "configured executable entry should be decoded even without a symbol"); + recompiler.generateOutput(); + + const std::filesystem::path registrationPath = outputPath / "register_functions.cpp"; + std::ifstream registrationFile(registrationPath); + const std::string registration{ + std::istreambuf_iterator(registrationFile), + std::istreambuf_iterator()}; + t.IsTrue(registration.find("// 0x100010") != std::string::npos, + "synthesized entry address should be registered for guest dispatch"); + t.IsTrue(recompiler.reportCounters().additionalEntryPoints >= 1u, + "synthesized entry should be visible in the report"); + } + + std::error_code removeError; + std::filesystem::remove_all(tempRoot, removeError); + }); + tc.Run("elf parser ignores STT_FUNC symbols in non-executable sections", [](TestCase &t) { const auto uniqueSuffix = std::to_string( static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -876,6 +1450,41 @@ void register_ps2_recompiler_tests() std::filesystem::remove(elfPath, removeError); }); + tc.Run("elf parser keeps VU microprograms out of EE code discovery", [](TestCase &t) { + const auto uniqueSuffix = std::to_string( + static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); + const std::filesystem::path elfPath = + std::filesystem::temp_directory_path() / ("ps2recomp-vutext-" + uniqueSuffix + ".elf"); + + const bool writeOk = writeMinimalMipsElfWithVuMicroprogramSection(elfPath); + t.IsTrue(writeOk, "temporary ELF with .vutext should be generated"); + if (!writeOk) + return; + + ElfParser parser(elfPath.string()); + const bool parseOk = parser.parse(); + t.IsTrue(parseOk, "ELF with .vutext should parse"); + if (parseOk) + { + const auto sections = parser.getSections(); + const auto vuSection = std::find_if(sections.begin(), sections.end(), + [](const Section §ion) + { return section.name == ".vutext"; }); + t.IsTrue(vuSection != sections.end(), ".vutext bytes should remain available"); + if (vuSection != sections.end()) + t.IsFalse(vuSection->isCode, ".vutext must not be decoded as R5900 code"); + + const auto functions = parser.extractFunctions(); + const bool hasVuFunction = std::any_of(functions.begin(), functions.end(), + [](const Function &function) + { return function.start == 0x00250000u; }); + t.IsFalse(hasVuFunction, "VU symbol must not become an EE function"); + } + + std::error_code removeError; + std::filesystem::remove(elfPath, removeError); + }); + tc.Run("ghidra map replaces JAL fallback-only auto starts", [](TestCase &t) { const auto uniqueSuffix = std::to_string( static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); @@ -947,6 +1556,119 @@ void register_ps2_recompiler_tests() std::filesystem::remove(mapPath, removeError); }); + tc.Run("elf parser discovers address-taken callbacks in stripped ELFs", [](TestCase &t) { + const auto uniqueSuffix = std::to_string( + static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); + const std::filesystem::path elfPath = + std::filesystem::temp_directory_path() / ("ps2recomp-address-taken-" + uniqueSuffix + ".elf"); + + const bool writeOk = writeMinimalMipsElfWithAddressTakenCallbacks(elfPath); + t.IsTrue(writeOk, "temporary stripped ELF should be generated"); + if (!writeOk) + { + return; + } + + ElfParser parser(elfPath.string()); + const bool parseOk = parser.parse(); + t.IsTrue(parseOk, "generated ELF should parse"); + if (!parseOk) + { + std::error_code removeError; + std::filesystem::remove(elfPath, removeError); + return; + } + + const auto functions = parser.extractFunctions(); + auto hasStart = [&functions](uint32_t start) + { + return std::any_of(functions.begin(), functions.end(), + [start](const Function &function) + { return function.start == start; }); + }; + + t.IsTrue(hasStart(0x00100040u), + "LUI plus delay-slot ADDIU should discover the callback entry"); + t.IsTrue(hasStart(0x00100060u), + "clustered rodata pointers should discover the first leaf callback"); + t.IsTrue(hasStart(0x00100068u), + "clustered rodata pointers should discover the second leaf callback"); + t.IsTrue(hasStart(0x00100080u), + "clustered pointers should discover an initializer with a delayed stack prologue"); + t.IsTrue(hasStart(0x001000E0u), + "a nearby registrar call should discover a longer leaf callback"); + t.IsTrue(hasStart(0x00100120u), + "a clustered descriptor should discover a long leaf method"); + t.IsTrue(hasStart(0x00100280u), + "a callback should flow through a saved register into a call argument"); + t.IsTrue(hasStart(0x001002C0u), + "a delay-slot LUI should flow into the taken branch successor"); + t.IsTrue(hasStart(0x00100340u), + "a materialized method stored into a runtime descriptor should be discovered"); + t.IsTrue(hasStart(0x00100380u), + "an alternating pointer/id table should discover a neighboring long leaf"); + t.IsTrue(hasStart(0x001003C8u), + "an adjacent two-instruction leaf thunk should be split from a known thunk"); + t.IsTrue(hasStart(0x001003E0u), + "a callback passed as the fifth register argument should be discovered"); + t.IsTrue(hasStart(0x00100400u), + "a long callback passed as the fifth register argument should be discovered"); + t.IsFalse(hasStart(0x00100548u), + "a post-return prologue without a cross-reference must remain only a hint"); + t.IsTrue(hasStart(0x00100580u), + "a singleton data pointer loaded and consumed by JALR should be discovered"); + t.IsFalse(hasStart(0x00100588u), + "a data pointer must not split a function at a control-transfer delay slot"); + t.IsFalse(hasStart(0x00100300u), + "an isolated data pointer or non-callback code materialization must not become a function"); + + std::error_code removeError; + std::filesystem::remove(elfPath, removeError); + }); + + tc.Run("elf parser supplements partial DWARF with address-taken callbacks", [](TestCase &t) { + const auto uniqueSuffix = std::to_string( + static_cast(std::chrono::steady_clock::now().time_since_epoch().count())); + const std::filesystem::path elfPath = + std::filesystem::temp_directory_path() / ("ps2recomp-partial-dwarf-" + uniqueSuffix + ".elf"); + + const bool writeOk = writeMinimalMipsElfWithAddressTakenCallbacks(elfPath, true); + t.IsTrue(writeOk, "temporary ELF with partial DWARF should be generated"); + if (!writeOk) + { + return; + } + + ElfParser parser(elfPath.string()); + const bool parseOk = parser.parse(); + t.IsTrue(parseOk, "generated ELF with partial DWARF should parse"); + if (parseOk) + { + const auto functions = parser.extractFunctions(); + const auto callbackIt = std::find_if( + functions.begin(), functions.end(), + [](const Function &function) + { return function.start == 0x00100040u; }); + t.IsTrue(callbackIt != functions.end(), + "partial DWARF must not suppress static callback discovery"); + + const auto lastFallbackIt = std::find_if( + functions.begin(), functions.end(), + [](const Function &function) + { return function.start == 0x001003E0u; }); + t.IsTrue(lastFallbackIt != functions.end(), + "the last inferred callback should still be discovered"); + if (lastFallbackIt != functions.end()) + { + t.Equals(0x001003F0u, lastFallbackIt->end, + "later partial DWARF should bound an inferred callback"); + } + } + + std::error_code removeError; + std::filesystem::remove(elfPath, removeError); + }); + tc.Run("runtime call resolution includes Veronica compatibility aliases", [](TestCase &t) { t.Equals(ps2_runtime_calls::resolveSyscallName("ReleaseAlarm"), std::string_view{"ReleaseAlarm"}, "ReleaseAlarm should resolve as a syscall name"); diff --git a/ps2xTest/src/ps2_runtime_expansion_tests.cpp b/ps2xTest/src/ps2_runtime_expansion_tests.cpp index ad7df46..4772f3f 100644 --- a/ps2xTest/src/ps2_runtime_expansion_tests.cpp +++ b/ps2xTest/src/ps2_runtime_expansion_tests.cpp @@ -481,6 +481,32 @@ void register_ps2_runtime_expansion_tests() "missing target should remain visible in ctx->pc for diagnostics"); }); + tc.Run("ContinueToTarget unwinds a missing call without skipping it", [](TestCase &t) + { + PS2Runtime runtime; + runtime.setMissingFunctionPolicy( + PS2Runtime::MissingFunctionPolicy::ContinueToTarget); + + R5900Context ctx{}; + ctx.pc = 0x2000u; + + const bool continuedInCaller = runtime.dispatchGuestBranch( + nullptr, + &ctx, + 0x3210u, + 0x2000u, + 0x2008u, + PS2Runtime::GuestBranchKind::IndirectCall, + "test-missing-unwind"); + + t.IsFalse(continuedInCaller, + "ContinueToTarget must unwind the generated caller"); + t.Equals(ctx.pc, 0x3210u, + "the unresolved target should remain visible to the dispatcher"); + t.IsFalse(runtime.isStopRequested(), + "ContinueToTarget should remain a non-stopping debug policy"); + }); + tc.Run("MPEG init and callback stubs return success instead of TODO errors", [](TestCase &t) { std::vector rdram(PS2_RAM_SIZE, 0u); diff --git a/ps2xTest/src/ps2_runtime_interrupt_tests.cpp b/ps2xTest/src/ps2_runtime_interrupt_tests.cpp index ccf4b70..4261bfa 100644 --- a/ps2xTest/src/ps2_runtime_interrupt_tests.cpp +++ b/ps2xTest/src/ps2_runtime_interrupt_tests.cpp @@ -8,7 +8,9 @@ #include #include #include +#include #include +#include #include using namespace ps2_syscalls; @@ -52,6 +54,11 @@ namespace constexpr uint32_t kIrqWaitPc = 0x00160200u; constexpr uint32_t kIrqResumePc = 0x00160210u; constexpr uint32_t kIntcHandlerPc = 0x00160220u; + constexpr uint32_t kIrqStackWaitPc = 0x00160230u; + constexpr uint32_t kIrqStackResumePc = 0x00160240u; + constexpr uint32_t kIrqStackHandlerPc = 0x00160250u; + constexpr uint32_t kIrqRegistrationSp = 0x001E0000u; + constexpr uint32_t kIrqRegistrationGuardAddr = kIrqRegistrationSp - 16u; constexpr uint32_t kISemaWaitPc = 0x00160300u; constexpr uint32_t kISemaResumePc = 0x00160310u; constexpr uint32_t kISemaDriverPc = 0x00160320u; @@ -62,6 +69,9 @@ namespace constexpr uint32_t kTimer2WaitPc = 0x00160500u; constexpr uint32_t kTimer2ResumePc = 0x00160510u; constexpr uint32_t kTimer2HandlerPc = 0x00160520u; + constexpr uint32_t kInvocationQueuePc = 0x00160530u; + constexpr uint32_t kInvocationQueueResumePc = 0x00160540u; + constexpr uint32_t kInvocationQueueHandlerPc = 0x00160550u; constexpr uint32_t kTimer2Count = 0x10001000u; constexpr uint32_t kTimer2Mode = 0x10001010u; @@ -83,6 +93,10 @@ namespace uint64_t g_vsyncTick = 0; uint64_t g_vsyncCsr = 0; std::atomic g_timer2Resumed{false}; + uint32_t g_irqObservedSp = 0u; + uint32_t g_invocationQueueRuns = 0u; + uint32_t g_invocationQueueSp = 0u; + bool g_invocationQueueSpChanged = false; void setRegU32(R5900Context &ctx, int reg, uint32_t value) { @@ -184,6 +198,34 @@ namespace runtime->requestStop(); } + void schedulerIrqStackHandler(uint8_t *rdram, R5900Context *ctx, PS2Runtime *) + { + g_irqObservedSp = getRegU32(ctx, 29); + const uint64_t clobber = 0u; + std::memcpy(rdram + g_irqObservedSp - sizeof(clobber), &clobber, sizeof(clobber)); + ctx->pc = 0u; + } + + void schedulerIrqStackWait(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) + { + EeScheduler &scheduler = runtime->eeScheduler(); + scheduler.addIrqHandler(false, + 2u, + kIrqStackHandlerPc, + true, + 0u, + 0u, + kIrqRegistrationSp); + ctx->pc = kIrqStackResumePc; + scheduler.waitVSync(scheduler.currentVSyncTick()); + } + + void schedulerIrqStackResume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) + { + ctx->pc = 0u; + runtime->requestStop(); + } + void schedulerISemaHandler(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) { g_dispatchTrace.push_back(3); @@ -294,6 +336,43 @@ namespace ctx->pc = 0u; runtime->requestStop(); } + + void schedulerInvocationQueueHandler(uint8_t *, R5900Context *ctx, PS2Runtime *) + { + const uint32_t sp = getRegU32(ctx, 29); + if (g_invocationQueueSp == 0u) + { + g_invocationQueueSp = sp; + } + else if (g_invocationQueueSp != sp) + { + g_invocationQueueSpChanged = true; + } + ++g_invocationQueueRuns; + ctx->pc = 0u; + } + + void schedulerQueueManyInvocations(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) + { + constexpr uint32_t kInvocationCount = 96u; + EeScheduler &scheduler = runtime->eeScheduler(); + for (uint32_t i = 0u; i < kInvocationCount; ++i) + { + GuestInvocation invocation{}; + invocation.kind = GuestInvocationKind::Interrupt; + invocation.tag = i; + invocation.context.pc = kInvocationQueueHandlerPc; + setRegU32(invocation.context, 31, 0u); + scheduler.queueInvocation(std::move(invocation)); + } + ctx->pc = kInvocationQueueResumePc; + } + + void schedulerInvocationQueueResume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) + { + ctx->pc = 0u; + runtime->requestStop(); + } } void register_ps2_runtime_interrupt_tests() @@ -467,6 +546,62 @@ void register_ps2_runtime_interrupt_tests() "the IRQ frame should receive its registered argument"); }); + tc.Run("IRQ callbacks use an isolated invocation stack", [](TestCase &t) + { + TestEnv env; + env.runtime.registerFunction(kIrqStackWaitPc, schedulerIrqStackWait); + env.runtime.registerFunction(kIrqStackResumePc, schedulerIrqStackResume); + env.runtime.registerFunction(kIrqStackHandlerPc, schedulerIrqStackHandler); + + constexpr uint64_t guard = 0x1122334455667788ull; + std::memcpy(env.rdram.data() + kIrqRegistrationGuardAddr, &guard, sizeof(guard)); + g_irqObservedSp = 0u; + + R5900Context mainContext{}; + mainContext.pc = kIrqStackWaitPc; + env.runtime.eeScheduler().reset(env.rdram.data(), mainContext); + env.runtime.eeScheduler().run(); + + uint64_t guardAfter = 0u; + std::memcpy(&guardAfter, + env.rdram.data() + kIrqRegistrationGuardAddr, + sizeof(guardAfter)); + t.IsTrue(g_irqObservedSp != 0u && g_irqObservedSp != kIrqRegistrationSp, + "IRQ handler must not reuse the transient stack captured at registration"); + t.Equals(guardAfter, guard, + "IRQ handler stack writes must not clobber the registering thread's live frame"); + }); + + tc.Run("pending async callbacks execute sequentially on a reusable invocation stack", [](TestCase &t) + { + TestEnv env; + env.runtime.registerFunction(kInvocationQueuePc, schedulerQueueManyInvocations); + env.runtime.registerFunction(kInvocationQueueResumePc, schedulerInvocationQueueResume); + env.runtime.registerFunction(kInvocationQueueHandlerPc, schedulerInvocationQueueHandler); + + g_invocationQueueRuns = 0u; + g_invocationQueueSp = 0u; + g_invocationQueueSpChanged = false; + + R5900Context mainContext{}; + mainContext.pc = kInvocationQueuePc; + env.runtime.eeScheduler().reset(env.rdram.data(), mainContext); + + bool exhausted = false; + try + { + env.runtime.eeScheduler().run(); + } + catch (const std::runtime_error &error) + { + exhausted = std::string_view(error.what()) == "EE invocation stack space exhausted"; + } + + t.IsFalse(exhausted, "queued callbacks must not consume one invocation stack per pending item"); + t.Equals(g_invocationQueueRuns, 96u, "every queued callback should execute exactly once"); + t.IsFalse(g_invocationQueueSpChanged, "sequential callbacks should reuse the same stack depth"); + }); + tc.Run("iSignalSema defers selection until IRQ return", [](TestCase &t) { TestEnv env; diff --git a/ps2xTest/src/ps2_runtime_io_tests.cpp b/ps2xTest/src/ps2_runtime_io_tests.cpp index 1e92051..4f979be 100644 --- a/ps2xTest/src/ps2_runtime_io_tests.cpp +++ b/ps2xTest/src/ps2_runtime_io_tests.cpp @@ -5,6 +5,8 @@ #include #include +#include +#include #include #include #include @@ -50,6 +52,19 @@ namespace static_assert(sizeof(SceMcTblGetDir) == 64, "sceMcTblGetDir size mismatch"); + struct GuestIoStat + { + uint32_t mode; + uint32_t attr; + uint32_t size; + uint8_t ctime[8]; + uint8_t atime[8]; + uint8_t mtime[8]; + uint32_t hisize; + }; + + static_assert(sizeof(GuestIoStat) == 40u); + void setRegU32(R5900Context &ctx, int reg, uint32_t value) { ctx.r[reg] = _mm_set_epi64x(0, static_cast(value)); @@ -135,6 +150,7 @@ namespace TempPaths paths; std::vector rdram; R5900Context ctx; + PS2Runtime runtime; TestContext() : paths(makeTempPaths()), rdram(PS2_RAM_SIZE, 0) { @@ -152,6 +168,112 @@ void register_ps2_runtime_io_tests() { MiniTest::Case("PS2RuntimeIO", [](TestCase &tc) { + tc.Run("ROM0 ROMVER is exposed as the 14-byte firmware pseudo-file", [](TestCase &t) + { + TestContext test; + constexpr uint32_t kPathAddr = GUEST_STRING_AREA_START; + constexpr uint32_t kBufferAddr = GUEST_BUFFER_AREA_START; + constexpr char kExpectedRomVersion[] = "0200AC20040614"; + static_assert(sizeof(kExpectedRomVersion) - 1u == 14u); + + writeGuestString(test.rdram.data(), kPathAddr, "rom0:ROMVER"); + std::memset(test.rdram.data() + kBufferAddr, 0xA5, 16u); + setRegU32(test.ctx, 4, kPathAddr); + setRegU32(test.ctx, 5, PS2_FIO_O_RDONLY); + fioOpen(test.rdram.data(), &test.ctx, &test.runtime); + const int32_t fd = getRegS32(&test.ctx, 2); + t.IsTrue(fd >= 0, "fioOpen should recognize rom0:ROMVER without a host file"); + + setRegU32(test.ctx, 4, static_cast(fd)); + setRegU32(test.ctx, 5, kBufferAddr); + setRegU32(test.ctx, 6, 14u); + fioRead(test.rdram.data(), &test.ctx, &test.runtime); + t.Equals(getRegS32(&test.ctx, 2), 14, "fioRead should return the complete ROMVER payload"); + t.IsTrue(std::memcmp(test.rdram.data() + kBufferAddr, + kExpectedRomVersion, + sizeof(kExpectedRomVersion) - 1u) == 0, + "ROMVER should use the normal consumer-console format"); + t.Equals(static_cast(test.rdram[kBufferAddr + 14u]), 0xA5u, + "ROMVER reads must not append a terminator"); + + setRegU32(test.ctx, 4, static_cast(fd)); + fioClose(test.rdram.data(), &test.ctx, &test.runtime); + t.Equals(getRegS32(&test.ctx, 2), 0, "fioClose should release the ROMVER descriptor"); + }); + + tc.Run("ROM0 profiles can extend and override files without a BIOS", [](TestCase &t) + { + PS2RomProfile profile; + profile.id = "runtime-io-test"; + profile.provider = "test-extension"; + profile.matcher.elfName = "rom_profile_test.elf"; + profile.files["CONFIG"] = {'p', 'r', 'o', 'f', 'i', 'l', 'e'}; + profile.files["ROMVER"] = {'9', '9', '9', '9', 'T', '2', '0', '2', '6', '0', '8', '2', '4', 'X'}; + PS2RomDevice::registerProfile(std::move(profile)); + + TestContext test; + std::string error; + t.IsTrue(test.runtime.romDevice().configure({"rom_profile_test.elf", 0u, 0u}, &error), + "a uniquely matched ROM0 profile should configure"); + t.Equals(std::string(test.runtime.romDevice().activeProvider()), std::string("test-extension"), + "the selected ROM0 profile should expose its provider"); + + constexpr uint32_t kPathAddr = GUEST_STRING_AREA_START; + constexpr uint32_t kBufferAddr = GUEST_BUFFER_AREA_START; + writeGuestString(test.rdram.data(), kPathAddr, "rom0:CONFIG"); + setRegU32(test.ctx, 4, kPathAddr); + setRegU32(test.ctx, 5, PS2_FIO_O_RDONLY); + fioOpen(test.rdram.data(), &test.ctx, &test.runtime); + const int32_t fd = getRegS32(&test.ctx, 2); + t.IsTrue(fd >= 0, "a profile-provided ROM0 file should open through normal FileIO"); + + setRegU32(test.ctx, 4, static_cast(fd)); + setRegU32(test.ctx, 5, kBufferAddr); + setRegU32(test.ctx, 6, 7u); + fioRead(test.rdram.data(), &test.ctx, &test.runtime); + t.Equals(getRegS32(&test.ctx, 2), 7, "profile-provided ROM0 bytes should be readable"); + t.IsTrue(std::memcmp(test.rdram.data() + kBufferAddr, "profile", 7u) == 0, + "ROM0 profile contents should reach the guest unchanged"); + + setRegU32(test.ctx, 4, static_cast(fd)); + fioClose(test.rdram.data(), &test.ctx, &test.runtime); + }); + + tc.Run("ROM0 uses VFS stat and per-runtime descriptors", [](TestCase &t) + { + TestContext owner; + TestContext other; + constexpr uint32_t kPathAddr = GUEST_STRING_AREA_START; + constexpr uint32_t kBufferAddr = GUEST_BUFFER_AREA_START; + constexpr uint32_t kStatAddr = GUEST_BUFFER_AREA_START + 0x100u; + writeGuestString(owner.rdram.data(), kPathAddr, "rom0:ROMVER"); + + setRegU32(owner.ctx, 4, kPathAddr); + setRegU32(owner.ctx, 5, PS2_FIO_O_RDONLY); + fioOpen(owner.rdram.data(), &owner.ctx, &owner.runtime); + const int32_t fd = getRegS32(&owner.ctx, 2); + t.IsTrue(fd >= 3, "ROM0 should return a normal VFS descriptor"); + + setRegU32(owner.ctx, 4, static_cast(fd)); + setRegU32(owner.ctx, 5, kBufferAddr); + setRegU32(owner.ctx, 6, 4u); + fioRead(owner.rdram.data(), &owner.ctx, &other.runtime); + t.Equals(getRegS32(&owner.ctx, 2), -1, + "a descriptor must not leak into a different runtime instance"); + + setRegU32(owner.ctx, 4, kPathAddr); + setRegU32(owner.ctx, 5, kStatAddr); + fioGetstat(owner.rdram.data(), &owner.ctx, &owner.runtime); + t.Equals(getRegS32(&owner.ctx, 2), 0, "fioGetstat should see ROM0 virtual files"); + GuestIoStat stat{}; + std::memcpy(&stat, owner.rdram.data() + kStatAddr, sizeof(stat)); + t.Equals(stat.size, 14u, "ROMVER stat should report its exact payload size"); + t.Equals(stat.mode & 0x38u, 0x10u, "ROMVER should be reported as an ioman regular file"); + + setRegU32(owner.ctx, 4, static_cast(fd)); + fioClose(owner.rdram.data(), &owner.ctx, &owner.runtime); + }); + tc.Run("mc0 directory creation", [](TestCase &t) { TestContext test; @@ -161,7 +283,7 @@ void register_ps2_runtime_io_tests() writeGuestString(test.rdram.data(), dirAddr, dirPath); setRegU32(test.ctx, 4, dirAddr); - fioMkdir(test.rdram.data(), &test.ctx, nullptr); + fioMkdir(test.rdram.data(), &test.ctx, &test.runtime); const int32_t result = getRegS32(&test.ctx, 2); t.IsTrue(result >= 0, "fioMkdir should succeed for mc0: directory"); @@ -182,7 +304,7 @@ void register_ps2_runtime_io_tests() const uint32_t dirAddr = GUEST_STRING_AREA_START; writeGuestString(test.rdram.data(), dirAddr, dirPath); setRegU32(test.ctx, 4, dirAddr); - fioMkdir(test.rdram.data(), &test.ctx, nullptr); + fioMkdir(test.rdram.data(), &test.ctx, &test.runtime); // Test: open file for writing const std::string filePath = "mc0:/SAVEDATA/test.txt"; @@ -191,7 +313,7 @@ void register_ps2_runtime_io_tests() setRegU32(test.ctx, 4, fileAddr); setRegU32(test.ctx, 5, PS2_FIO_WRITE_CREATE_TRUNC); - fioOpen(test.rdram.data(), &test.ctx, nullptr); + fioOpen(test.rdram.data(), &test.ctx, &test.runtime); const int32_t fd = getRegS32(&test.ctx, 2); t.IsTrue(fd >= 0, "fioOpen should return valid file descriptor"); @@ -204,7 +326,7 @@ void register_ps2_runtime_io_tests() setRegU32(test.ctx, 4, static_cast(fd)); setRegU32(test.ctx, 5, bufAddr); setRegU32(test.ctx, 6, static_cast(payload.size())); - fioWrite(test.rdram.data(), &test.ctx, nullptr); + fioWrite(test.rdram.data(), &test.ctx, &test.runtime); const int32_t bytesWritten = getRegS32(&test.ctx, 2); t.Equals(bytesWritten, static_cast(payload.size()), @@ -212,7 +334,7 @@ void register_ps2_runtime_io_tests() // Close file setRegU32(test.ctx, 4, static_cast(fd)); - fioClose(test.rdram.data(), &test.ctx, nullptr); + fioClose(test.rdram.data(), &test.ctx, &test.runtime); const int32_t closeResult = getRegS32(&test.ctx, 2); t.IsTrue(closeResult >= 0, "fioClose should succeed"); @@ -239,7 +361,7 @@ void register_ps2_runtime_io_tests() const uint32_t dirAddr = GUEST_STRING_AREA_START; writeGuestString(test.rdram.data(), dirAddr, dirPath); setRegU32(test.ctx, 4, dirAddr); - fioMkdir(test.rdram.data(), &test.ctx, nullptr); + fioMkdir(test.rdram.data(), &test.ctx, &test.runtime); const std::string filePath = "mc0:/SAVEDATA/test.txt"; const uint32_t fileAddr = GUEST_STRING_AREA_START + 0x100; @@ -252,21 +374,21 @@ void register_ps2_runtime_io_tests() setRegU32(test.ctx, 4, fileAddr); setRegU32(test.ctx, 5, PS2_FIO_WRITE_CREATE_TRUNC); - fioOpen(test.rdram.data(), &test.ctx, nullptr); + fioOpen(test.rdram.data(), &test.ctx, &test.runtime); int32_t fd = getRegS32(&test.ctx, 2); setRegU32(test.ctx, 4, static_cast(fd)); setRegU32(test.ctx, 5, writeBufAddr); setRegU32(test.ctx, 6, static_cast(payload.size())); - fioWrite(test.rdram.data(), &test.ctx, nullptr); + fioWrite(test.rdram.data(), &test.ctx, &test.runtime); setRegU32(test.ctx, 4, static_cast(fd)); - fioClose(test.rdram.data(), &test.ctx, nullptr); + fioClose(test.rdram.data(), &test.ctx, &test.runtime); // Test: read back via fioRead setRegU32(test.ctx, 4, fileAddr); setRegU32(test.ctx, 5, PS2_FIO_O_RDONLY); - fioOpen(test.rdram.data(), &test.ctx, nullptr); + fioOpen(test.rdram.data(), &test.ctx, &test.runtime); fd = getRegS32(&test.ctx, 2); t.IsTrue(fd >= 0, "fioOpen for reading should succeed"); @@ -277,7 +399,7 @@ void register_ps2_runtime_io_tests() setRegU32(test.ctx, 4, static_cast(fd)); setRegU32(test.ctx, 5, readBufAddr); setRegU32(test.ctx, 6, static_cast(payload.size())); - fioRead(test.rdram.data(), &test.ctx, nullptr); + fioRead(test.rdram.data(), &test.ctx, &test.runtime); const int32_t bytesRead = getRegS32(&test.ctx, 2); t.Equals(bytesRead, static_cast(payload.size()), @@ -290,7 +412,7 @@ void register_ps2_runtime_io_tests() t.Equals(readback, payload, "fioRead content should match original"); setRegU32(test.ctx, 4, static_cast(fd)); - fioClose(test.rdram.data(), &test.ctx, nullptr); + fioClose(test.rdram.data(), &test.ctx, &test.runtime); }); tc.Run("mc0 paths isolated from cdRoot", [](TestCase &t) @@ -307,11 +429,11 @@ void register_ps2_runtime_io_tests() // Create directory and file on mc0: setRegU32(test.ctx, 4, dirAddr); - fioMkdir(test.rdram.data(), &test.ctx, nullptr); + fioMkdir(test.rdram.data(), &test.ctx, &test.runtime); setRegU32(test.ctx, 4, fileAddr); setRegU32(test.ctx, 5, PS2_FIO_WRITE_CREATE_TRUNC); - fioOpen(test.rdram.data(), &test.ctx, nullptr); + fioOpen(test.rdram.data(), &test.ctx, &test.runtime); const int32_t fd = getRegS32(&test.ctx, 2); const std::string payload = "isolation test"; @@ -321,10 +443,10 @@ void register_ps2_runtime_io_tests() setRegU32(test.ctx, 4, static_cast(fd)); setRegU32(test.ctx, 5, bufAddr); setRegU32(test.ctx, 6, static_cast(payload.size())); - fioWrite(test.rdram.data(), &test.ctx, nullptr); + fioWrite(test.rdram.data(), &test.ctx, &test.runtime); setRegU32(test.ctx, 4, static_cast(fd)); - fioClose(test.rdram.data(), &test.ctx, nullptr); + fioClose(test.rdram.data(), &test.ctx, &test.runtime); // Verify isolation const std::filesystem::path expectedMc = diff --git a/ps2xTest/src/ps2_runtime_kernel_tests.cpp b/ps2xTest/src/ps2_runtime_kernel_tests.cpp index 2542d48..63b12d9 100644 --- a/ps2xTest/src/ps2_runtime_kernel_tests.cpp +++ b/ps2xTest/src/ps2_runtime_kernel_tests.cpp @@ -549,6 +549,29 @@ void register_ps2_runtime_kernel_tests() { MiniTest::Case("PS2RuntimeKernel", [](TestCase &tc) { + tc.Run("unsigned loads and ABI word writes extend independently", [](TestCase &t) + { + constexpr uint64_t kUpper = 0x1122334455667788ull; + R5900Context ctx{}; + ctx.r[2] = _mm_set_epi64x(static_cast(kUpper), 0); + + SET_GPR_ZE32(&ctx, 2, 0x80000000u); + t.Equals(static_cast(_mm_extract_epi64(ctx.r[2], 0)), + 0x0000000080000000ull, + "SET_GPR_ZE32 must zero-extend values used by LWU/LHU/LBU"); + t.Equals(static_cast(_mm_extract_epi64(ctx.r[2], 1)), + kUpper, + "SET_GPR_ZE32 must preserve the upper 64 bits of the 128-bit GPR"); + + SET_GPR_U32(&ctx, 2, 0x80000000u); + t.Equals(static_cast(_mm_extract_epi64(ctx.r[2], 0)), + 0xFFFFFFFF80000000ull, + "SET_GPR_U32 must retain the existing EE 32-bit ABI extension semantics"); + t.Equals(static_cast(_mm_extract_epi64(ctx.r[2], 1)), + kUpper, + "SET_GPR_U32 must preserve the upper 64 bits of the 128-bit GPR"); + }); + tc.Run("CreateThread and CreateSema decode the exact PS2SDK EE layouts", [](TestCase &t) { TestEnv env; @@ -977,7 +1000,7 @@ void register_ps2_runtime_kernel_tests() t.Equals(getRegS32(env.ctx, 2), -4, "__divdi3 should divide signed 64-bit values"); }); - tc.Run("ReleaseAlarm aliases CancelAlarm and cache toggles succeed", [](TestCase &t) + tc.Run("ReleaseAlarm aliases CancelAlarm and cache syscalls succeed", [](TestCase &t) { TestEnv env; @@ -1005,6 +1028,13 @@ void register_ps2_runtime_kernel_tests() DisableCache(env.rdram.data(), &env.ctx, &env.runtime); t.Equals(getRegS32(env.ctx, 2), KE_OK, "DisableCache should succeed as a no-op"); + + setRegU32(env.ctx, 2, 0xDEADBEEFu); + setRegU32(env.ctx, 4, 0u); // PS2SDK WRITEBACK_DCACHE + t.IsTrue(callSyscall(static_cast(-0x68), env.rdram.data(), &env.ctx, &env.runtime), + "-0x68 should dispatch iFlushCache"); + t.Equals(getRegS32(env.ctx, 2), KE_OK, + "iFlushCache should succeed when guest and host memory are coherent"); }); tc.Run("setup heap and thread invalid ids use documented kernel errors", [](TestCase &t) diff --git a/ps2xTest/src/ps2_sif_dma_tests.cpp b/ps2xTest/src/ps2_sif_dma_tests.cpp index 46eddea..aace83c 100644 --- a/ps2xTest/src/ps2_sif_dma_tests.cpp +++ b/ps2xTest/src/ps2_sif_dma_tests.cpp @@ -10,9 +10,6 @@ #include #include #include -#include -#include -#include #include namespace ps2_stubs @@ -37,17 +34,6 @@ namespace } }; - void configureProfile(TestEnv &env, std::string_view elfName) - { - std::string error; - const bool configured = PS2IopTransport::configureForTesting( - &env.runtime, {std::string(elfName), 0u, 0u}, &error); - if (!configured) - { - throw std::runtime_error("failed to configure test IOP profile: " + error); - } - } - #pragma pack(push, 1) struct Ps2SifDmaTransfer { @@ -99,18 +85,6 @@ namespace return value; } - void writeGuestS16(uint8_t *rdram, uint32_t addr, int16_t value) - { - std::memcpy(rdram + addr, &value, sizeof(value)); - } - - int16_t readGuestS16(const uint8_t *rdram, uint32_t addr) - { - int16_t value = 0; - std::memcpy(&value, rdram + addr, sizeof(value)); - return value; - } - uint32_t g_dmacHandlerWriteAddr = 0u; uint32_t g_dmacHandlerValue = 0u; uint32_t g_dmacHandlerLastCause = 0u; @@ -176,7 +150,7 @@ void register_ps2_sif_dma_tests() payload[i] = static_cast(0x30u + i); } std::memcpy(env.rdram.data() + kSrcAddr, payload.data(), payload.size()); - std::memset(env.rdram.data() + kDstAddr, 0, payload.size()); + std::memset(env.rdram.data() + kDstAddr, 0x5A, payload.size()); const Ps2SifDmaTransfer desc{ kSrcAddr, @@ -191,8 +165,15 @@ void register_ps2_sif_dma_tests() const int32_t dmaId = getRegS32(env.ctx, 2); t.IsTrue(dmaId > 0, "sceSifSetDma should return a positive transfer id on success"); - t.IsTrue(std::memcmp(env.rdram.data() + kDstAddr, payload.data(), payload.size()) == 0, - "sceSifSetDma should copy transfer payload to destination"); + std::array iopReadback{}; + t.IsTrue(env.runtime.readIopMemory(kDstAddr, iopReadback.data(), iopReadback.size()) && + iopReadback == payload, + "sceSifSetDma should copy EE payload into IOP RAM"); + const std::array eeSentinel = { + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, + 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A}; + t.IsTrue(std::memcmp(env.rdram.data() + kDstAddr, eeSentinel.data(), eeSentinel.size()) == 0, + "sceSifSetDma must not alias an equal-numbered EE address"); setRegU32(env.ctx, 4, static_cast(dmaId)); ps2_stubs::sceSifDmaStat(env.rdram.data(), &env.ctx, &env.runtime); @@ -206,7 +187,6 @@ void register_ps2_sif_dma_tests() constexpr uint32_t kDescAddr = 0x00020040u; constexpr uint32_t kSrcAddr = 0x00020140u; constexpr uint32_t kRoundTripAddr = 0x00020240u; - constexpr uint32_t kFormerAliasAddr = 0x01A53880u; constexpr uint32_t kIopBlockSize = 0x880u; std::array payload{}; @@ -216,13 +196,13 @@ void register_ps2_sif_dma_tests() } std::memcpy(env.rdram.data() + kSrcAddr, payload.data(), payload.size()); std::memset(env.rdram.data() + kRoundTripAddr, 0, payload.size()); - std::memset(env.rdram.data() + kFormerAliasAddr, 0x5Au, payload.size()); setRegU32(env.ctx, 4, kIopBlockSize); ps2_stubs::sceSifAllocIopHeap(env.rdram.data(), &env.ctx, &env.runtime); const uint32_t iopAddress = ::getRegU32(&env.ctx, 2); - t.IsTrue(iopAddress >= PS2_RAM_SIZE, - "sceSifAllocIopHeap should return an address outside EE RDRAM"); + t.IsTrue(iopAddress >= 0x00120000u && iopAddress < 0x00200000u, + "sceSifAllocIopHeap should return an address in physical IOP RAM"); + std::memset(env.rdram.data() + iopAddress, 0x5Au, payload.size()); Ps2SifDmaTransfer desc{ kSrcAddr, @@ -241,32 +221,25 @@ void register_ps2_sif_dma_tests() 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A, 0x5A}; - t.IsTrue(std::memcmp(env.rdram.data() + kFormerAliasAddr, + t.IsTrue(std::memcmp(env.rdram.data() + iopAddress, aliasSentinel.data(), aliasSentinel.size()) == 0, - "IOP DMA must not overwrite the old 0x01A00000 EE alias range"); + "IOP DMA must not overwrite the equal-numbered EE range"); PS2IopHostAdapter host(env.runtime); auto scope = host.enterCall(&env.ctx, env.rdram.data()); - uint32_t normalized = 0u; std::array hostReadback{}; - t.IsTrue(host.normalizeGuestAddress(iopAddress, normalized) && - normalized == iopAddress, - "IOP modules should preserve private IOP heap addresses"); - t.IsTrue(host.readGuest(iopAddress, hostReadback.data(), hostReadback.size()) && + t.IsTrue(host.readIopMemory(iopAddress, hostReadback.data(), hostReadback.size()) && hostReadback == payload, - "IOP modules should read the private heap backing"); + "IOP modules should read the shared physical IOP RAM"); - desc = { - iopAddress, - kRoundTripAddr, - static_cast(payload.size()), - 0}; - std::memcpy(env.rdram.data() + kDescAddr, &desc, sizeof(desc)); - setRegU32(env.ctx, 4, kDescAddr); - setRegU32(env.ctx, 5, 1u); - ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime); - t.IsTrue(getRegS32(env.ctx, 2) > 0, - "IOP-to-EE DMA should accept a private IOP heap source"); + constexpr uint32_t kRdAddr = 0x00020340u; + setRegU32(env.ctx, 4, kRdAddr); + setRegU32(env.ctx, 5, iopAddress); + setRegU32(env.ctx, 6, kRoundTripAddr); + setRegU32(env.ctx, 7, static_cast(payload.size())); + ps2_stubs::sceSifGetOtherData(env.rdram.data(), &env.ctx, &env.runtime); + t.Equals(getRegS32(env.ctx, 2), 0, + "IOP-to-EE transfer should accept a physical IOP source"); t.IsTrue(std::memcmp(env.rdram.data() + kRoundTripAddr, payload.data(), payload.size()) == 0, "IOP-to-EE DMA should round-trip the payload"); @@ -286,7 +259,7 @@ void register_ps2_sif_dma_tests() payload[i] = static_cast(0x50u + i); } std::memcpy(env.rdram.data() + kSrcAddr, payload.data(), payload.size()); - std::memset(env.rdram.data() + kDstAddr, 0, payload.size()); + std::memset(env.rdram.data() + kDstAddr, 0x5A, payload.size()); const Ps2SifDmaTransfer desc{ kSrcAddr, @@ -299,8 +272,10 @@ void register_ps2_sif_dma_tests() setRegU32(env.ctx, 5, 1u); ps2_stubs::isceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime); t.IsTrue(getRegS32(env.ctx, 2) > 0, "isceSifSetDma should report a successful transfer id"); - t.IsTrue(std::memcmp(env.rdram.data() + kDstAddr, payload.data(), payload.size()) == 0, - "isceSifSetDma should copy transfer payload like sceSifSetDma"); + std::array iopReadback{}; + t.IsTrue(env.runtime.readIopMemory(kDstAddr, iopReadback.data(), iopReadback.size()) && + iopReadback == payload, + "isceSifSetDma should copy EE payload into IOP RAM"); ps2_stubs::isceSifSetDChain(env.rdram.data(), &env.ctx, &env.runtime); t.Equals(getRegS32(env.ctx, 2), 0, "isceSifSetDChain should mirror sceSifSetDChain"); @@ -350,505 +325,6 @@ void register_ps2_sif_dma_tests() "DMAC handler should receive registered argument"); }); - tc.Run("sceSifSetDma acknowledges DTX work-buffer transfers by advancing the EE footer ticket", [](TestCase &t) - { - TestEnv env; - configureProfile(env, "slus_201.84"); - - constexpr uint32_t kClientAddr = 0x0002D000u; - constexpr uint32_t kDtxSid = 0x7D000000u; - constexpr uint32_t kSendAddr = 0x0002D100u; - constexpr uint32_t kRecvAddr = 0x0002D200u; - constexpr uint32_t kDescAddr = 0x0002D300u; - constexpr uint32_t kEeWorkAddr = 0x0002D400u; - constexpr uint32_t kIopWorkAddr = 0x0002D800u; - constexpr uint32_t kDtxId = 3u; - constexpr uint32_t kWorkLen = 0x100u; - constexpr uint32_t kFooterTicketAddr = kEeWorkAddr + kWorkLen - sizeof(uint32_t); - - ps2_syscalls::SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, kDtxSid); - setRegU32(env.ctx, 6, 0u); - ps2_syscalls::SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for the DTX sid"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, kDtxId); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kEeWorkAddr); - writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kIopWorkAddr); - writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, kWorkLen); - writeGuestU32(env.rdram.data(), kRecvAddr + 0x00u, 0u); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 2u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 16u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc should create the DTX transport"); - t.IsTrue(readGuestU32(env.rdram.data(), kRecvAddr) != 0u, "DTX create should return a remote handle"); - - std::memset(env.rdram.data() + kEeWorkAddr, 0x44, kWorkLen); - std::memset(env.rdram.data() + kIopWorkAddr, 0x00, kWorkLen); - writeGuestU32(env.rdram.data(), kFooterTicketAddr, 1u); - - const Ps2SifDmaTransfer desc{ - kEeWorkAddr, - kIopWorkAddr, - static_cast(kWorkLen), - 0}; - std::memcpy(env.rdram.data() + kDescAddr, &desc, sizeof(desc)); - - setRegU32(env.ctx, 4, kDescAddr); - setRegU32(env.ctx, 5, 1u); - ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime); - t.IsTrue(getRegS32(env.ctx, 2) > 0, "sceSifSetDma should succeed for the DTX transfer"); - - t.Equals(readGuestU32(env.rdram.data(), kFooterTicketAddr), 2u, - "sceSifSetDma should advance the EE footer ticket so DTX clears wait_flag"); - }); - - tc.Run("sceSifSetDma applies SJX DTX payloads into the emulated SJRMT data ring", [](TestCase &t) - { - TestEnv env; - configureProfile(env, "slus_201.84"); - - constexpr uint32_t kClientAddr = 0x0002E000u; - constexpr uint32_t kDtxSid = 0x7D000000u; - constexpr uint32_t kRecvAddr = 0x0002E100u; - constexpr uint32_t kSendAddr = 0x0002E200u; - constexpr uint32_t kDescAddr = 0x0002E300u; - constexpr uint32_t kEeWorkAddr = 0x0002E400u; - constexpr uint32_t kIopWorkAddr = 0x0002E800u; - constexpr uint32_t kRingAddr = 0x0002EC00u; - constexpr uint32_t kChunkDataAddr = 0x0002ED00u; - constexpr uint32_t kWorkLen = 0x100u; - constexpr uint32_t kChunkLen = 8u; - - ps2_syscalls::SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, kDtxSid); - setRegU32(env.ctx, 6, 0u); - ps2_syscalls::SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should bind the DTX sid"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 1u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kRingAddr); - writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kWorkLen); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 0x422u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 12u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - const uint32_t sjrmtHandle = readGuestU32(env.rdram.data(), kRecvAddr); - t.IsTrue(sjrmtHandle != 0u, "SJRMT_UNI_CREATE should return a handle"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 0u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, sjrmtHandle); - writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, 1u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, 0x12345678u); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 0x400u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 16u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - const uint32_t sjxHandle = readGuestU32(env.rdram.data(), kRecvAddr); - t.IsTrue(sjxHandle != 0u, "SJX_CREATE should return a handle"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 0u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kEeWorkAddr); - writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kIopWorkAddr); - writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, kWorkLen); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 2u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 16u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "DTX create should succeed"); - - std::memset(env.rdram.data() + kEeWorkAddr, 0, kWorkLen); - std::memset(env.rdram.data() + kIopWorkAddr, 0, kWorkLen); - std::memset(env.rdram.data() + kRingAddr, 0, kWorkLen); - for (uint32_t i = 0; i < kChunkLen; ++i) - { - env.rdram[kChunkDataAddr + i] = static_cast(0xA0u + i); - } - - writeGuestU32(env.rdram.data(), kEeWorkAddr + 0x00u, 1u); - env.rdram[kEeWorkAddr + 0x10u] = 0u; - env.rdram[kEeWorkAddr + 0x11u] = 1u; - std::memcpy(env.rdram.data() + kEeWorkAddr + 0x12u, "\0\0", 2u); - writeGuestU32(env.rdram.data(), kEeWorkAddr + 0x14u, sjxHandle); - writeGuestU32(env.rdram.data(), kEeWorkAddr + 0x18u, kChunkDataAddr); - writeGuestU32(env.rdram.data(), kEeWorkAddr + 0x1Cu, kChunkLen); - writeGuestU32(env.rdram.data(), kEeWorkAddr + kWorkLen - sizeof(uint32_t), 1u); - - const Ps2SifDmaTransfer desc{ - kEeWorkAddr, - kIopWorkAddr, - static_cast(kWorkLen), - 0}; - std::memcpy(env.rdram.data() + kDescAddr, &desc, sizeof(desc)); - - setRegU32(env.ctx, 4, kDescAddr); - setRegU32(env.ctx, 5, 1u); - ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime); - t.IsTrue(getRegS32(env.ctx, 2) > 0, "sceSifSetDma should succeed for the SJX transport"); - t.Equals(env.rdram[kEeWorkAddr + 0x11u], static_cast(0u), - "SJX DMA ack should rewrite the response line to room so EE recycles the chunk"); - t.Equals(readGuestU32(env.rdram.data(), kEeWorkAddr + 0x14u), 0x12345678u, - "SJX DMA ack should translate the remote handle back to the EE callback object"); - t.Equals(readGuestU32(env.rdram.data(), kEeWorkAddr + kWorkLen - sizeof(uint32_t)), 2u, - "SJX DMA ack should still advance the EE footer ticket"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, sjrmtHandle); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, 1u); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 0x429u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 8u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(readGuestU32(env.rdram.data(), kRecvAddr), kChunkLen, - "SJX DMA should make SJRMT report available data"); - t.IsTrue(std::memcmp(env.rdram.data() + kRingAddr, env.rdram.data() + kChunkDataAddr, kChunkLen) == 0, - "SJX DMA should copy the chunk payload into the emulated SJRMT ring"); - }); - - tc.Run("sceSifSetDma recognizes SJX DTX payloads from rotated EE work buffers", [](TestCase &t) - { - TestEnv env; - configureProfile(env, "slus_201.84"); - - constexpr uint32_t kClientAddr = 0x00031000u; - constexpr uint32_t kDtxSid = 0x7D000000u; - constexpr uint32_t kRecvAddr = 0x00031100u; - constexpr uint32_t kSendAddr = 0x00031200u; - constexpr uint32_t kDescAddr = 0x00031300u; - constexpr uint32_t kRegisteredEeWorkAddr = 0x00031400u; - constexpr uint32_t kRegisteredIopWorkAddr = 0x00031800u; - constexpr uint32_t kAltEeWorkAddr = 0x00031C00u; - constexpr uint32_t kAltIopWorkAddr = 0x00032000u; - constexpr uint32_t kRingAddr = 0x00032400u; - constexpr uint32_t kChunkDataAddr = 0x00032500u; - constexpr uint32_t kRegisteredWorkLen = 0x100u; - constexpr uint32_t kAltWorkLen = 0x180u; - constexpr uint32_t kChunkLen = 12u; - - ps2_syscalls::SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, kDtxSid); - setRegU32(env.ctx, 6, 0u); - ps2_syscalls::SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should bind the DTX sid"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 1u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kRingAddr); - writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kRegisteredWorkLen); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 0x422u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 12u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - const uint32_t sjrmtHandle = readGuestU32(env.rdram.data(), kRecvAddr); - t.IsTrue(sjrmtHandle != 0u, "SJRMT_UNI_CREATE should return a handle"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 0u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, sjrmtHandle); - writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, 1u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, 0x87654321u); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 0x400u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 16u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - const uint32_t sjxHandle = readGuestU32(env.rdram.data(), kRecvAddr); - t.IsTrue(sjxHandle != 0u, "SJX_CREATE should return a handle"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 0u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kRegisteredEeWorkAddr); - writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kRegisteredIopWorkAddr); - writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, kRegisteredWorkLen); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 2u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 16u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "DTX create should succeed"); - - std::memset(env.rdram.data() + kRegisteredEeWorkAddr, 0, kRegisteredWorkLen); - std::memset(env.rdram.data() + kRegisteredIopWorkAddr, 0, kRegisteredWorkLen); - std::memset(env.rdram.data() + kAltEeWorkAddr, 0, kAltWorkLen); - std::memset(env.rdram.data() + kAltIopWorkAddr, 0, kAltWorkLen); - std::memset(env.rdram.data() + kRingAddr, 0, kRegisteredWorkLen); - for (uint32_t i = 0; i < kChunkLen; ++i) - { - env.rdram[kChunkDataAddr + i] = static_cast(0xC0u + i); - } - - writeGuestU32(env.rdram.data(), kAltEeWorkAddr + 0x00u, 1u); - env.rdram[kAltEeWorkAddr + 0x10u] = 0u; - env.rdram[kAltEeWorkAddr + 0x11u] = 1u; - std::memcpy(env.rdram.data() + kAltEeWorkAddr + 0x12u, "\0\0", 2u); - writeGuestU32(env.rdram.data(), kAltEeWorkAddr + 0x14u, sjxHandle); - writeGuestU32(env.rdram.data(), kAltEeWorkAddr + 0x18u, kChunkDataAddr); - writeGuestU32(env.rdram.data(), kAltEeWorkAddr + 0x1Cu, kChunkLen); - writeGuestU32(env.rdram.data(), kAltEeWorkAddr + kAltWorkLen - sizeof(uint32_t), 9u); - - const Ps2SifDmaTransfer desc{ - kAltEeWorkAddr, - kAltIopWorkAddr, - static_cast(kAltWorkLen), - 0}; - std::memcpy(env.rdram.data() + kDescAddr, &desc, sizeof(desc)); - - setRegU32(env.ctx, 4, kDescAddr); - setRegU32(env.ctx, 5, 1u); - ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime); - t.IsTrue(getRegS32(env.ctx, 2) > 0, "sceSifSetDma should succeed for the rotated SJX transport"); - t.Equals(env.rdram[kAltEeWorkAddr + 0x11u], static_cast(0u), - "rotated SJX DMA ack should rewrite the response line to room"); - t.Equals(readGuestU32(env.rdram.data(), kAltEeWorkAddr + kAltWorkLen - sizeof(uint32_t)), 10u, - "rotated SJX DMA ack should advance the alternate EE footer ticket"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, sjrmtHandle); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, 1u); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 0x429u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 8u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(readGuestU32(env.rdram.data(), kRecvAddr), kChunkLen, - "rotated SJX DMA should make SJRMT report available data"); - t.IsTrue(std::memcmp(env.rdram.data() + kRingAddr, env.rdram.data() + kChunkDataAddr, kChunkLen) == 0, - "rotated SJX DMA should copy the chunk payload into the emulated SJRMT ring"); - }); - - tc.Run("sceSifSetDma lets active PS2RNA playback drain emulated SJRMT data", [](TestCase &t) - { - TestEnv env; - configureProfile(env, "slus_201.84"); - - constexpr uint32_t kClientAddr = 0x0002F000u; - constexpr uint32_t kDtxSid = 0x7D000000u; - constexpr uint32_t kRecvAddr = 0x0002F100u; - constexpr uint32_t kSendAddr = 0x0002F200u; - constexpr uint32_t kDesc0Addr = 0x0002F300u; - constexpr uint32_t kDesc1Addr = 0x0002F320u; - constexpr uint32_t kEeWork0Addr = 0x0002F400u; - constexpr uint32_t kIopWork0Addr = 0x0002F800u; - constexpr uint32_t kEeWork1Addr = 0x0002FC00u; - constexpr uint32_t kIopWork1Addr = 0x00030000u; - constexpr uint32_t kRingAddr = 0x00030400u; - constexpr uint32_t kChunkDataAddr = 0x00030500u; - constexpr uint32_t kWorkLen = 0x100u; - constexpr uint32_t kChunkLen = 8u; - - ps2_syscalls::SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, kDtxSid); - setRegU32(env.ctx, 6, 0u); - ps2_syscalls::SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should bind the DTX sid"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 1u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kRingAddr); - writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kWorkLen); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 0x422u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 12u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - const uint32_t sjrmtHandle = readGuestU32(env.rdram.data(), kRecvAddr); - t.IsTrue(sjrmtHandle != 0u, "SJRMT_UNI_CREATE should return a handle"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 0u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, sjrmtHandle); - writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, 1u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, 0xCAFEBABEu); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 0x400u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 16u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - const uint32_t sjxHandle = readGuestU32(env.rdram.data(), kRecvAddr); - t.IsTrue(sjxHandle != 0u, "SJX_CREATE should return a handle"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 1u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, 0u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, sjrmtHandle); - writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, 0u); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 0x408u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 16u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - const uint32_t ps2RnaHandle = readGuestU32(env.rdram.data(), kRecvAddr); - t.IsTrue(ps2RnaHandle != 0u, "PS2RNA_CREATE should return a handle"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 0u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kEeWork0Addr); - writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kIopWork0Addr); - writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, kWorkLen); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 2u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 16u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "DTX create should succeed for SJX transport"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 1u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, kEeWork1Addr); - writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, kIopWork1Addr); - writeGuestU32(env.rdram.data(), kSendAddr + 0x0Cu, kWorkLen); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 2u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 16u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "DTX create should succeed for PS2RNA transport"); - - std::memset(env.rdram.data() + kEeWork0Addr, 0, kWorkLen); - std::memset(env.rdram.data() + kIopWork0Addr, 0, kWorkLen); - std::memset(env.rdram.data() + kEeWork1Addr, 0, kWorkLen); - std::memset(env.rdram.data() + kIopWork1Addr, 0, kWorkLen); - std::memset(env.rdram.data() + kRingAddr, 0, kWorkLen); - for (uint32_t i = 0; i < kChunkLen; ++i) - { - env.rdram[kChunkDataAddr + i] = static_cast(0xB0u + i); - } - - writeGuestU32(env.rdram.data(), kEeWork1Addr + 0x00u, 1u); - writeGuestU32(env.rdram.data(), kEeWork1Addr + 0x10u, 2u); - writeGuestU32(env.rdram.data(), kEeWork1Addr + 0x14u, ps2RnaHandle); - writeGuestU32(env.rdram.data(), kEeWork1Addr + 0x18u, 1u); - writeGuestU32(env.rdram.data(), kEeWork1Addr + 0x1Cu, 0u); - writeGuestU32(env.rdram.data(), kEeWork1Addr + kWorkLen - sizeof(uint32_t), 1u); - - const Ps2SifDmaTransfer desc1{ - kEeWork1Addr, - kIopWork1Addr, - static_cast(kWorkLen), - 0}; - std::memcpy(env.rdram.data() + kDesc1Addr, &desc1, sizeof(desc1)); - - setRegU32(env.ctx, 4, kDesc1Addr); - setRegU32(env.ctx, 5, 1u); - ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime); - t.IsTrue(getRegS32(env.ctx, 2) > 0, "sceSifSetDma should succeed for the PS2RNA control transport"); - t.Equals(readGuestU32(env.rdram.data(), kEeWork1Addr + kWorkLen - sizeof(uint32_t)), 2u, - "PS2RNA control DMA should advance the EE footer ticket"); - - writeGuestU32(env.rdram.data(), kEeWork0Addr + 0x00u, 1u); - env.rdram[kEeWork0Addr + 0x10u] = 0u; - env.rdram[kEeWork0Addr + 0x11u] = 1u; - std::memcpy(env.rdram.data() + kEeWork0Addr + 0x12u, "\0\0", 2u); - writeGuestU32(env.rdram.data(), kEeWork0Addr + 0x14u, sjxHandle); - writeGuestU32(env.rdram.data(), kEeWork0Addr + 0x18u, kChunkDataAddr); - writeGuestU32(env.rdram.data(), kEeWork0Addr + 0x1Cu, kChunkLen); - writeGuestU32(env.rdram.data(), kEeWork0Addr + kWorkLen - sizeof(uint32_t), 1u); - - const Ps2SifDmaTransfer desc0{ - kEeWork0Addr, - kIopWork0Addr, - static_cast(kWorkLen), - 0}; - std::memcpy(env.rdram.data() + kDesc0Addr, &desc0, sizeof(desc0)); - - setRegU32(env.ctx, 4, kDesc0Addr); - setRegU32(env.ctx, 5, 1u); - ps2_stubs::sceSifSetDma(env.rdram.data(), &env.ctx, &env.runtime); - t.IsTrue(getRegS32(env.ctx, 2) > 0, "sceSifSetDma should succeed for the SJX transport"); - t.Equals(env.rdram[kEeWork0Addr + 0x11u], static_cast(0u), - "SJX DMA ack should still rewrite the response line to room"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, sjrmtHandle); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, 1u); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 0x429u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 8u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(readGuestU32(env.rdram.data(), kRecvAddr), 0u, - "active PS2RNA playback should drain remote SJRMT data instead of leaving it queued forever"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, sjrmtHandle); - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, 0u); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 0x429u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 8u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(readGuestU32(env.rdram.data(), kRecvAddr), kWorkLen, - "drained PS2RNA playback should return remote SJRMT room to full capacity"); - }); - tc.Run("resetSifState seeds boot-ready SIF registers", [](TestCase &t) { TestEnv env; @@ -958,7 +434,8 @@ void register_ps2_sif_dma_tests() { payload[i] = static_cast((i * 7u) & 0xFFu); } - std::memcpy(env.rdram.data() + kSrcAddr, payload.data(), payload.size()); + t.IsTrue(env.runtime.writeIopMemory(kSrcAddr, payload.data(), payload.size()), + "test setup should populate physical IOP RAM"); std::memset(env.rdram.data() + kDstAddr, 0, payload.size()); std::memset(env.rdram.data() + kRdAddr, 0, sizeof(SifRpcReceiveData)); @@ -978,134 +455,6 @@ void register_ps2_sif_dma_tests() t.Equals(static_cast(rd.size), kSize, "receive metadata size should be populated"); }); - tc.Run("sceSifGetOtherData preserves live sound-status sums when compat backfill is enabled", [](TestCase &t) - { - TestEnv env; - configureProfile(env, "slus_201.84"); - - constexpr uint32_t kRdAddr = 0x00023300u; - constexpr uint32_t kDstAddr = 0x00023400u; - constexpr uint32_t kSize = 0x42u; - constexpr uint32_t kPrimarySeCheckAddr = 0x01E0EF10u; - constexpr uint32_t kPrimaryMidiCheckAddr = 0x01E0EF20u; - constexpr uint32_t kMidiSumOffset = 0x1Eu; - constexpr uint32_t kSeSumOffset = 0x26u; - constexpr uint32_t kBank = 1u; - - constexpr uint32_t kClientAddr = 0x00023500u; - constexpr uint32_t kRecvAddr = 0x00023600u; - constexpr uint32_t kSid = 1u; - - ps2_syscalls::SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, kSid); - setRegU32(env.ctx, 6, 0u); - ps2_syscalls::SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for sound-driver sid"); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 0x12u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, 0u); - setRegU32(env.ctx, 8, 0u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - const uint32_t kSrcAddr = readGuestU32(env.rdram.data(), kRecvAddr); - - std::memset(env.rdram.data() + kDstAddr, 0, kSize); - std::memset(env.rdram.data() + kRdAddr, 0, sizeof(SifRpcReceiveData)); - - writeGuestS16(env.rdram.data(), kSrcAddr + kSeSumOffset + (kBank * 2u), static_cast(0x1357)); - writeGuestS16(env.rdram.data(), kSrcAddr + kMidiSumOffset + (kBank * 2u), static_cast(0x2468)); - - writeGuestS16(env.rdram.data(), kPrimarySeCheckAddr + (kBank * 2u), static_cast(0x7B7B)); - writeGuestS16(env.rdram.data(), kPrimaryMidiCheckAddr + (kBank * 2u), static_cast(0x6A6A)); - - setRegU32(env.ctx, 4, kRdAddr); - setRegU32(env.ctx, 5, kSrcAddr); - setRegU32(env.ctx, 6, kDstAddr); - setRegU32(env.ctx, 7, kSize); - ps2_stubs::sceSifGetOtherData(env.rdram.data(), &env.ctx, &env.runtime); - - t.Equals(getRegS32(env.ctx, 2), 0, - "sceSifGetOtherData should succeed for sound-status transfer"); - t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kSeSumOffset + (kBank * 2u)), - static_cast(0x1357), - "live se_sum for the active bank should not be clobbered by compat check arrays"); - t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kMidiSumOffset + (kBank * 2u)), - static_cast(0x2468), - "live midi_sum for the active bank should not be clobbered by compat check arrays"); - }); - - tc.Run("sceSifGetOtherData backfills zero sound-status sums for later banks", [](TestCase &t) - { - TestEnv env; - configureProfile(env, "slus_201.84"); - - constexpr uint32_t kRdAddr = 0x00023700u; - constexpr uint32_t kDstAddr = 0x00023800u; - constexpr uint32_t kSize = 0x42u; - constexpr uint32_t kPrimarySeCheckAddr = 0x01E0EF10u; - constexpr uint32_t kPrimaryMidiCheckAddr = 0x01E0EF20u; - constexpr uint32_t kMidiSumOffset = 0x1Eu; - constexpr uint32_t kSeSumOffset = 0x26u; - constexpr uint32_t kLiveBank = 0u; - constexpr uint32_t kPendingBank = 1u; - - constexpr uint32_t kClientAddr = 0x00023900u; - constexpr uint32_t kRecvAddr = 0x00023A00u; - - ps2_syscalls::SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 1u); - setRegU32(env.ctx, 6, 0u); - ps2_syscalls::SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for sound-driver sid"); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 0x12u); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, 0u); - setRegU32(env.ctx, 8, 0u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 4u); - setRegU32(env.ctx, 11, 0u); - ps2_syscalls::SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - const uint32_t kSrcAddr = readGuestU32(env.rdram.data(), kRecvAddr); - - std::memset(env.rdram.data() + kDstAddr, 0, kSize); - std::memset(env.rdram.data() + kRdAddr, 0, sizeof(SifRpcReceiveData)); - - writeGuestS16(env.rdram.data(), kSrcAddr + kSeSumOffset + (kLiveBank * 2u), static_cast(0x1111)); - writeGuestS16(env.rdram.data(), kSrcAddr + kMidiSumOffset + (kLiveBank * 2u), static_cast(0x2222)); - - writeGuestS16(env.rdram.data(), kPrimarySeCheckAddr + (kPendingBank * 2u), static_cast(0x3333)); - writeGuestS16(env.rdram.data(), kPrimaryMidiCheckAddr + (kPendingBank * 2u), static_cast(0x4444)); - - setRegU32(env.ctx, 4, kRdAddr); - setRegU32(env.ctx, 5, kSrcAddr); - setRegU32(env.ctx, 6, kDstAddr); - setRegU32(env.ctx, 7, kSize); - ps2_stubs::sceSifGetOtherData(env.rdram.data(), &env.ctx, &env.runtime); - - t.Equals(getRegS32(env.ctx, 2), 0, - "sceSifGetOtherData should succeed for later-bank sound-status transfer"); - t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kSeSumOffset + (kLiveBank * 2u)), - static_cast(0x1111), - "existing live se_sum values should remain intact"); - t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kMidiSumOffset + (kLiveBank * 2u)), - static_cast(0x2222), - "existing live midi_sum values should remain intact"); - t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kSeSumOffset + (kPendingBank * 2u)), - static_cast(0x3333), - "zero se_sum slots should backfill from compat tables for later banks"); - t.Equals(readGuestS16(env.rdram.data(), kDstAddr + kMidiSumOffset + (kPendingBank * 2u)), - static_cast(0x4444), - "zero midi_sum slots should backfill from compat tables for later banks"); - }); - tc.Run("sceSifGetOtherData rejects unsupported guest segments", [](TestCase &t) { TestEnv env; diff --git a/ps2xTest/src/ps2_sif_rpc_tests.cpp b/ps2xTest/src/ps2_sif_rpc_tests.cpp index eefb495..7242e65 100644 --- a/ps2xTest/src/ps2_sif_rpc_tests.cpp +++ b/ps2xTest/src/ps2_sif_rpc_tests.cpp @@ -7,14 +7,10 @@ #include "runtime/ee_scheduler.h" #include -#include #include #include #include -#include #include -#include -#include #include using namespace ps2_syscalls; @@ -31,16 +27,8 @@ namespace constexpr uint32_t K_SIF_RPC_MODE_NOWAIT = 0x01u; constexpr uint32_t K_STACK_ADDR = 0x00100000u; - constexpr uint32_t IOP_SID_SNDDRV_COMMAND = 0x00000000u; - constexpr uint32_t IOP_SID_SNDDRV_STATE = 0x00000001u; - constexpr uint32_t IOP_SID_LOTR_CLFILE = 0x0000FF01u; - constexpr uint32_t IOP_SID_LOTR_SOUND = 0x00012345u; constexpr uint32_t IOP_SID_MCSERV = 0x80000400u; constexpr uint32_t IOP_SID_LIBSD = 0x80000701u; - constexpr uint32_t IOP_SID_FATAL_FRAME_SDRDRV = 0x19740512u; - constexpr uint32_t IOP_RPC_SNDDRV_SUBMIT = 0x00000000u; - constexpr uint32_t IOP_RPC_SNDDRV_GET_STATUS_ADDR = 0x00000012u; - constexpr uint32_t IOP_RPC_SNDDRV_GET_ADDR_TABLE = 0x00000013u; #pragma pack(push, 1) struct SifRpcHeader @@ -126,17 +114,6 @@ namespace } }; - void configureProfile(TestEnv &env, std::string_view elfName) - { - std::string error; - const bool configured = PS2IopTransport::configureForTesting( - &env.runtime, {std::string(elfName), 0u, 0u}, &error); - if (!configured) - { - throw std::runtime_error("failed to configure test IOP profile: " + error); - } - } - ps2x::iop::RpcResult callIop(TestEnv &env, uint32_t sid, uint32_t function, @@ -154,107 +131,6 @@ namespace &env.runtime, env.rdram.data(), &env.ctx, std::move(request)); } - std::atomic g_lotrSoundCallbackHits{0u}; - std::atomic g_recvxSoundCallbackHits{0u}; - std::atomic g_dtxDispatcherHits{0u}; - std::atomic g_dtxDispatcherRpcNum{0u}; - std::atomic g_dtxDispatcherSendBuf{0u}; - std::atomic g_dtxDispatcherSendSize{0u}; - - constexpr uint32_t K_DTX_DISPATCH_RESULT_ADDR = 0x0002D800u; - constexpr uint32_t K_DTX_DISPATCH_RESULT_MARKER = 0xD15CA7C1u; - constexpr uint32_t K_DTX_SCHEDULER_CALL = 0x00102000u; - constexpr uint32_t K_DTX_SCHEDULER_RESUME = 0x00102010u; - constexpr uint32_t K_LOTR_SOUND_SCHEDULER_CALL = 0x00102020u; - constexpr uint32_t K_LOTR_SOUND_SCHEDULER_RESUME = 0x00102030u; - uint32_t g_schedulerRpcClient = 0u; - uint32_t g_schedulerRpcNumber = 0u; - uint32_t g_schedulerRpcSend = 0u; - uint32_t g_schedulerRpcReceive = 0u; - uint32_t g_schedulerRpcResult = 0u; - uint32_t g_schedulerLotrSoundClient = 0u; - uint32_t g_schedulerLotrSoundSend = 0u; - uint32_t g_schedulerLotrSoundReceive = 0u; - uint32_t g_schedulerLotrSoundEndFunction = 0u; - uint32_t g_schedulerLotrSoundSemaphore = 0u; - uint32_t g_schedulerLotrSoundResult = 0u; - - void writeGuestU32(uint8_t *rdram, uint32_t addr, uint32_t value); - - void lotrSoundEndCallback(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) - { - (void)rdram; - (void)runtime; - ++g_lotrSoundCallbackHits; - ctx->pc = ::getRegU32(ctx, 31); - } - - void recvxSoundEndCallbackShouldNotRun(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) - { - (void)rdram; - (void)runtime; - ++g_recvxSoundCallbackHits; - ctx->pc = ::getRegU32(ctx, 31); - } - - void schedulerLotrSoundRpcCall(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) - { - SET_GPR_U32(ctx, 4, g_schedulerLotrSoundClient); - SET_GPR_U32(ctx, 5, 0u); - SET_GPR_U32(ctx, 6, K_SIF_RPC_MODE_NOWAIT); - SET_GPR_U32(ctx, 7, g_schedulerLotrSoundSend); - SET_GPR_U32(ctx, 8, 0x2000u); - SET_GPR_U32(ctx, 9, g_schedulerLotrSoundReceive); - SET_GPR_U32(ctx, 10, 0x2000u); - SET_GPR_U32(ctx, 11, g_schedulerLotrSoundEndFunction); - writeGuestU32(rdram, ::getRegU32(ctx, 29), g_schedulerLotrSoundSemaphore); - ctx->pc = K_LOTR_SOUND_SCHEDULER_RESUME; - SifCallRpc(rdram, ctx, runtime); - } - - void schedulerLotrSoundRpcResume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) - { - g_schedulerLotrSoundResult = ::getRegU32(ctx, 2); - ctx->pc = 0u; - runtime->requestStop(); - } - - void schedulerDtxRpcCall(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) - { - SET_GPR_U32(ctx, 4, g_schedulerRpcClient); - SET_GPR_U32(ctx, 5, g_schedulerRpcNumber); - SET_GPR_U32(ctx, 6, 0u); - SET_GPR_U32(ctx, 7, g_schedulerRpcSend); - SET_GPR_U32(ctx, 8, 8u); - SET_GPR_U32(ctx, 9, g_schedulerRpcReceive); - SET_GPR_U32(ctx, 10, sizeof(uint32_t)); - SET_GPR_U32(ctx, 11, 0u); - ctx->pc = K_DTX_SCHEDULER_RESUME; - SifCallRpc(rdram, ctx, runtime); - } - - void schedulerDtxRpcResume(uint8_t *, R5900Context *ctx, PS2Runtime *runtime) - { - g_schedulerRpcResult = ::getRegU32(ctx, 2); - ctx->pc = 0u; - runtime->requestStop(); - } - - void recvxDtxDispatcher(uint8_t *rdram, R5900Context *ctx, PS2Runtime *runtime) - { - (void)runtime; - ++g_dtxDispatcherHits; - g_dtxDispatcherRpcNum = ::getRegU32(ctx, 4); - g_dtxDispatcherSendBuf = ::getRegU32(ctx, 5); - g_dtxDispatcherSendSize = ::getRegU32(ctx, 6); - - std::memcpy(rdram + K_DTX_DISPATCH_RESULT_ADDR, - &K_DTX_DISPATCH_RESULT_MARKER, - sizeof(K_DTX_DISPATCH_RESULT_MARKER)); - ctx->r[2] = _mm_set_epi64x(0, static_cast(K_DTX_DISPATCH_RESULT_ADDR)); - ctx->pc = ::getRegU32(ctx, 31); - } - void setRegU32(R5900Context &ctx, int reg, uint32_t value) { ctx.r[reg] = _mm_set_epi64x(0, static_cast(value)); @@ -294,12 +170,6 @@ namespace } }; - void writeFile(const std::filesystem::path &path, const std::vector &data) - { - std::ofstream out(path, std::ios::binary); - out.write(reinterpret_cast(data.data()), static_cast(data.size())); - } - template void writeGuestStruct(uint8_t *rdram, uint32_t addr, const T &value) { @@ -319,6 +189,74 @@ void register_ps2_sif_rpc_tests() { MiniTest::Case("PS2SifRpc", [](TestCase &tc) { + tc.Run("SifInitRpc does not reset the running IOP", [](TestCase &t) + { + TestEnv env; + + env.runtime.eeScheduler().accountCycles(80u); + const uint64_t cyclesBeforeInit = env.runtime.iopDebugSnapshot().emulatorCycles; + t.IsTrue(cyclesBeforeInit != 0u, "IOP cycle counter should advance before RPC initialization"); + + SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); + + t.Equals(env.runtime.iopDebugSnapshot().emulatorCycles, cyclesBeforeInit, + "SifInitRpc must not reboot or reset the IOP"); + }); + + tc.Run("SifLoadModule validates ROM modules and activates their HLE service", [](TestCase &t) + { + TestEnv env; + constexpr uint32_t kPathAddress = 0x00021000u; + + const auto load = [&](std::string_view path) + { + std::memcpy(env.rdram.data() + kPathAddress, path.data(), path.size()); + env.rdram[kPathAddress + path.size()] = 0u; + setRegU32(env.ctx, 4, kPathAddress); + setRegU32(env.ctx, 5, 0u); + setRegU32(env.ctx, 6, 0u); + SifLoadModule(env.rdram.data(), &env.ctx, &env.runtime); + return getRegS32(env.ctx, 2); + }; + + t.Equals(load("rom0:NOT_A_REAL_MODULE"), -1, + "SifLoadModule must reject unknown ROM modules instead of fabricating success"); + + const int32_t libsdId = load("rom0:LIBSD"); + t.IsTrue(libsdId > 0, "registered no-BIOS ROM module should receive a real managed ID"); + + const auto snapshot = env.runtime.iopDebugSnapshot(); + bool libsdActive = false; + for (const auto &service : snapshot.services) + { + if (service.name == "libsd") + { + libsdActive = service.active; + break; + } + } + t.IsTrue(libsdActive, "loading LIBSD should activate its HLE RPC route"); + }); + + tc.Run("emulated RPC bind waits for a registered IOP server", [](TestCase &t) + { + TestEnv env; + + constexpr uint32_t kClientAddr = 0x00021F00u; + constexpr uint32_t kUnregisteredSid = 0x13572468u; + + SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); + setRegU32(env.ctx, 4, kClientAddr); + setRegU32(env.ctx, 5, kUnregisteredSid); + setRegU32(env.ctx, 6, 0u); + SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); + + t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc transport should complete"); + const SifRpcClientData client = readGuestStruct(env.rdram.data(), kClientAddr); + t.Equals(client.server, 0u, + "client server pointer must stay null until the emulated IRX registers its SID"); + }); + tc.Run("register bind call updates descriptors and payload", [](TestCase &t) { TestEnv env; @@ -411,73 +349,11 @@ void register_ps2_sif_rpc_tests() t.Equals(getRegS32(env.ctx, 2), 0, "SifCheckStatRpc should report not busy after synchronous completion"); }); - tc.Run("Fatal Frame SDRDRV RPC initializes header and loads body archives", [](TestCase &t) - { - TestEnv env; - configureProfile(env, "SLUS_203.88"); - ScopedTempDir temp("fatal_frame_sdrdrv"); - - std::vector header(64u, 0); - const char headerPayload[] = "img-header"; - std::memcpy(header.data(), headerPayload, sizeof(headerPayload) - 1u); - writeFile(temp.path / "img_hd.bin", header); - - constexpr uint32_t kSectorSize = 2048u; - std::vector body(kSectorSize * 2u, 0); - const char bodyPayload[] = "archive-data"; - std::memcpy(body.data() + kSectorSize, bodyPayload, sizeof(bodyPayload) - 1u); - writeFile(temp.path / "img_bd.bin", body); - - const PS2Runtime::IoPaths oldPaths = PS2Runtime::getIoPaths(); - PS2Runtime::IoPaths ioPaths; - ioPaths.elfDirectory = temp.path; - ioPaths.hostRoot = temp.path; - ioPaths.cdRoot = temp.path; - ioPaths.mcRoot = temp.path / "mc0"; - PS2Runtime::setIoPaths(ioPaths); - - constexpr uint32_t kSendAddr = 0x00030000u; - constexpr uint32_t kRecvAddr = 0x00031000u; - constexpr uint32_t kDstAddr = 0x00032000u; - constexpr uint32_t kImgHeaderAddr = 0x012F0000u; - constexpr uint32_t kLoadId = 3u; - - std::array commands{}; - commands[0] = 0x0Eu; - commands[2] = 1u; // lbn - commands[3] = sizeof(bodyPayload) - 1u; - commands[4] = kDstAddr; - commands[6] = kLoadId; - std::memcpy(env.rdram.data() + kSendAddr, commands.data(), commands.size() * sizeof(uint32_t)); - std::memset(env.rdram.data() + kRecvAddr, 0xCC, 0x180u); - - const ps2x::iop::RpcResult initResult = - callIop(env, IOP_SID_FATAL_FRAME_SDRDRV, 0u, - 0u, 0u, kRecvAddr, 0x180u); - t.IsTrue(initResult.handled, "Fatal Frame SDRDRV init RPC should be handled"); - t.Equals(std::memcmp(env.rdram.data() + kImgHeaderAddr, "img-header", 10), 0, - "SDRDRV init RPC should load img_hd.bin into the arrangement table"); - - const ps2x::iop::RpcResult result = - callIop(env, IOP_SID_FATAL_FRAME_SDRDRV, 1u, - kSendAddr, - static_cast(commands.size() * sizeof(uint32_t)), - kRecvAddr, 0x180u); - - PS2Runtime::setIoPaths(oldPaths); - - t.IsTrue(result.handled, "Fatal Frame SDRDRV SID should be handled"); - t.Equals(result.resultAddress, kRecvAddr, "SDRDRV RPC should return recv buffer"); - t.IsFalse(result.signalNowaitCompletion, "SDRDRV RPC should not request special nowait signaling"); - t.Equals(std::memcmp(env.rdram.data() + kDstAddr, "archive-data", 12), 0, - "SDRDRV load command should copy bytes from img_bd.bin by LBN"); - t.Equals(env.rdram[kRecvAddr + 0x6Cu + (kLoadId * 8u)], static_cast(0), - "SDRDRV load status should report completed"); - }); - tc.Run("MCSERV RPC init and get info report a formatted PS2 card", [](TestCase &t) { TestEnv env; + const auto mcservModule = env.runtime.loadIopModule("rom0:MCSERV"); + t.IsTrue(mcservModule.moduleId > 0, "MCSERV test should load its IOP module first"); ScopedTempDir temp("mcserv_rpc"); const PS2Runtime::IoPaths oldPaths = PS2Runtime::getIoPaths(); @@ -533,14 +409,16 @@ void register_ps2_sif_rpc_tests() "get info should report formatted card"); }); - tc.Run("DBCMAN version RPC returns the 3.20 compatibility response", [](TestCase &t) + tc.Run("DBCMAN version RPC returns the 3.10 compatibility response", [](TestCase &t) { TestEnv env; + const auto dbcmanModule = env.runtime.loadIopModule("rom0:DBCMAN"); + t.IsTrue(dbcmanModule.moduleId > 0, "DBCMAN test should load its IOP module first"); constexpr uint32_t kDbcManSid = 0x80001300u; constexpr uint32_t kCheckVersionRpc = 0x80001363u; constexpr uint32_t kRecvAddr = 0x00035A00u; - constexpr uint32_t kDbcManVersion = 0x0320u; + constexpr uint32_t kDbcManVersion = 0x0310u; std::memset(env.rdram.data() + kRecvAddr, 0xCC, 16u); const ps2x::iop::RpcResult result = @@ -554,13 +432,15 @@ void register_ps2_sif_rpc_tests() { t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + (index * 4u)), kDbcManVersion, - "DBCMAN should repeat version 3.20 across the response words"); + "DBCMAN should repeat version 3.10 across the response words"); } }); tc.Run("LIBSD RPC routes through the IOP audio service", [](TestCase &t) { TestEnv env; + const auto libsdModule = env.runtime.loadIopModule("rom0:LIBSD"); + t.IsTrue(libsdModule.moduleId > 0, "LIBSD test should load its IOP module first"); constexpr uint32_t kSetVoiceRpc = 0x8010u; constexpr uint32_t kSendAddr = 0x00035B00u; @@ -587,246 +467,7 @@ void register_ps2_sif_rpc_tests() } }); - tc.Run("LotR ClFile RPC opens reads and reports EOF", [](TestCase &t) - { - TestEnv env; - configureProfile(env, "SLUS_205.78"); - ScopedTempDir temp("lotr_clfile_rpc"); - - const std::vector payload = {'a', 'b', 'c'}; - const std::vector loadPayload = {'x', 'y', 'z', '!'}; - writeFile(temp.path / "boot.cfg", payload); - writeFile(temp.path / "load.bin", loadPayload); - - const PS2Runtime::IoPaths oldPaths = PS2Runtime::getIoPaths(); - PS2Runtime::IoPaths ioPaths; - ioPaths.elfDirectory = temp.path; - ioPaths.hostRoot = temp.path; - ioPaths.cdRoot = temp.path; - ioPaths.mcRoot = temp.path / "mc0"; - PS2Runtime::setIoPaths(ioPaths); - - constexpr uint32_t kSendAddr = 0x00036000u; - constexpr uint32_t kRecvAddr = 0x00037000u; - constexpr uint32_t kDstAddr = 0x00038000u; - - auto callClFileRpc = [&](uint32_t rpcNum, uint32_t sendSize) { - const ps2x::iop::RpcResult result = - callIop(env, IOP_SID_LOTR_CLFILE, rpcNum, - kSendAddr, sendSize, kRecvAddr, 0x40u); - t.IsTrue(result.handled, "LotR ClFile SID should be handled"); - t.Equals(result.resultAddress, kRecvAddr, "ClFile RPC should return recv buffer"); - t.IsFalse(result.signalNowaitCompletion, "ClFile RPC should not request special nowait signaling"); - }; - - std::memset(env.rdram.data() + kSendAddr, 0, 0x100u); - std::memcpy(env.rdram.data() + kSendAddr, "boot.cfg", 9u); - std::memset(env.rdram.data() + kRecvAddr, 0xAA, 0x40u); - callClFileRpc(0x08u, 0x100u); - - t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 0u), 0u, - "open should report success status"); - const uint32_t handle = readGuestStruct(env.rdram.data(), kRecvAddr + 4u); - t.IsTrue(handle != 0u, "open should return a non-zero remote file handle"); - - std::memset(env.rdram.data() + kSendAddr, 0, 0x100u); - std::memcpy(env.rdram.data() + kSendAddr, "missing.cfg", 12u); - std::memset(env.rdram.data() + kRecvAddr, 0xAA, 0x40u); - callClFileRpc(0x08u, 0x100u); - t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 4u), 0u, - "missing file open should not manufacture a handle from path bytes"); - - constexpr uint32_t kLoadDstAddr = 0x00039000u; - std::memset(env.rdram.data() + kSendAddr, 0, 0x110u); - std::memcpy(env.rdram.data() + kSendAddr, "load.bin", 9u); - writeGuestU32(env.rdram.data(), kSendAddr + 0x100u, static_cast(loadPayload.size())); - writeGuestU32(env.rdram.data(), kSendAddr + 0x104u, kLoadDstAddr); - std::memset(env.rdram.data() + kLoadDstAddr, 0xCC, 0x20u); - callClFileRpc(0x01u, 0x110u); - - t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 0u), 5u, - "direct load should report a queued load result"); - const uint32_t loadHandle = readGuestStruct(env.rdram.data(), kRecvAddr + 4u); - t.IsTrue(loadHandle >= 3u, "direct load should return a status handle usable by getStatus"); - t.Equals(std::memcmp(env.rdram.data() + kLoadDstAddr, loadPayload.data(), loadPayload.size()), 0, - "direct load should copy file bytes into the requested guest destination"); - - writeGuestU32(env.rdram.data(), kSendAddr, loadHandle); - callClFileRpc(0x05u, 0u); - writeGuestU32(env.rdram.data(), kSendAddr, loadHandle); - callClFileRpc(0x03u, sizeof(uint32_t)); - t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 0u), 7u, - "direct load getStatus should report completed"); - - writeGuestU32(env.rdram.data(), kSendAddr, loadHandle); - callClFileRpc(0x06u, sizeof(uint32_t)); - t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 4u), - static_cast(loadPayload.size()), - "direct load getSize should report the loaded host file size"); - - writeGuestU32(env.rdram.data(), kSendAddr, loadHandle); - callClFileRpc(0x09u, sizeof(uint32_t)); - - if (handle != 0u) - { - std::array readPacket = {handle, 2u, kDstAddr, 0u}; - writeGuestStruct(env.rdram.data(), kSendAddr, readPacket); - std::memset(env.rdram.data() + kDstAddr, 0, 8u); - callClFileRpc(0x0Au, static_cast(readPacket.size() * sizeof(uint32_t))); - t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 4u), 2u, - "first read should report actual byte count"); - t.Equals(std::memcmp(env.rdram.data() + kDstAddr, "ab", 2), 0, - "first read should copy file bytes into guest destination"); - - writeGuestStruct(env.rdram.data(), kSendAddr, readPacket); - std::memset(env.rdram.data() + kDstAddr, 0, 8u); - callClFileRpc(0x0Au, static_cast(readPacket.size() * sizeof(uint32_t))); - t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 4u), 1u, - "short read should report remaining byte count"); - t.Equals(std::memcmp(env.rdram.data() + kDstAddr, "c", 1), 0, - "short read should copy remaining file byte"); - - writeGuestStruct(env.rdram.data(), kSendAddr, readPacket); - std::memset(env.rdram.data() + kDstAddr, 0xCC, 8u); - callClFileRpc(0x0Au, static_cast(readPacket.size() * sizeof(uint32_t))); - t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 4u), 0u, - "EOF read should report zero bytes"); - - writeGuestU32(env.rdram.data(), kSendAddr, handle); - callClFileRpc(0x09u, sizeof(uint32_t)); - } - - PS2Runtime::setIoPaths(oldPaths); - }); - - tc.Run("LotR sound RPC invokes guest callback to consume HLE response", [](TestCase &t) - { - TestEnv env; - configureProfile(env, "SLUS_205.78"); - - constexpr uint32_t kClientAddr = 0x00039000u; - constexpr uint32_t kSendAddr = 0x0003A000u; - constexpr uint32_t kRecvAddr = 0x0003C000u; - constexpr uint32_t kEndFunc = 0x001FFD70u; - - env.runtime.registerFunction(kEndFunc, lotrSoundEndCallback); - g_lotrSoundCallbackHits = 0u; - - SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, IOP_SID_LOTR_SOUND); - setRegU32(env.ctx, 6, 0u); - SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for LotR sound SID"); - - writeGuestU32(env.rdram.data(), kSendAddr, 3u); - std::memset(env.rdram.data() + kRecvAddr, 0xAA, 0x2000u); - env.runtime.registerFunction(K_LOTR_SOUND_SCHEDULER_CALL, schedulerLotrSoundRpcCall); - env.runtime.registerFunction(K_LOTR_SOUND_SCHEDULER_RESUME, schedulerLotrSoundRpcResume); - g_schedulerLotrSoundClient = kClientAddr; - g_schedulerLotrSoundSend = kSendAddr; - g_schedulerLotrSoundReceive = kRecvAddr; - g_schedulerLotrSoundEndFunction = kEndFunc; - g_schedulerLotrSoundResult = static_cast(-1); - - R5900Context mainContext{}; - mainContext.pc = K_LOTR_SOUND_SCHEDULER_CALL; - setRegU32(mainContext, 29, K_STACK_ADDR); - env.runtime.eeScheduler().reset(env.rdram.data(), mainContext); - const int32_t semaId = env.runtime.eeScheduler().createSemaphore(0, 1, 0u, 0u); - t.IsTrue(semaId > 0, "scheduler should create a positive semaphore id"); - g_schedulerLotrSoundSemaphore = static_cast(semaId); - env.runtime.eeScheduler().run(); - - t.Equals(g_schedulerLotrSoundResult, static_cast(KE_OK), - "SifCallRpc should resume with KE_OK for LotR sound RPC"); - t.Equals(g_lotrSoundCallbackHits.load(), 1u, - "LotR SOUND_JP callback should consume the HLE response"); - t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr + 0u), 0u, - "LotR sound response should report no active stream records"); - t.IsTrue(readGuestStruct(env.rdram.data(), kRecvAddr + 4u) != 0u, - "LotR sound response should advance the IOP update counter"); - - t.Equals(env.runtime.eeScheduler().pollSemaphore(semaId), semaId, - "LotR sound callback completion should signal the sema"); - }); - - tc.Run("RECVX sound callbacks complete in HLE and only clear busy on designated callbacks", [](TestCase &t) - { - TestEnv env; - configureProfile(env, "slus_201.84"); - - constexpr uint32_t kClientAddr = 0x0003D000u; - constexpr uint32_t kSemaParamAddr = 0x0003D100u; - constexpr uint32_t kCompletionOnlyCallback = 0x002EAC20u; - constexpr uint32_t kClearBusyCallback = 0x002EAC30u; - constexpr uint32_t kBusyFlagAddr = 0x01E212C8u; - - env.runtime.registerFunction(kCompletionOnlyCallback, recvxSoundEndCallbackShouldNotRun); - env.runtime.registerFunction(kClearBusyCallback, recvxSoundEndCallbackShouldNotRun); - g_recvxSoundCallbackHits = 0u; - - SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); - - const uint32_t semaParam[6] = {0u, 1u, 0u, 0u, 0u, 0u}; - std::memcpy(env.rdram.data() + kSemaParamAddr, semaParam, sizeof(semaParam)); - setRegU32(env.ctx, 4, kSemaParamAddr); - CreateSema(env.rdram.data(), &env.ctx, &env.runtime); - const int32_t semaId = getRegS32(env.ctx, 2); - t.IsTrue(semaId > 0, "CreateSema should return a positive semaphore id"); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, IOP_SID_SNDDRV_COMMAND); - setRegU32(env.ctx, 6, 0u); - SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for RECVX snddrv command SID"); - - auto callSoundDriver = [&](uint32_t endFunc) { - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_SUBMIT); - setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT); - setRegU32(env.ctx, 7, 0u); - setRegU32(env.ctx, 8, 0u); - setRegU32(env.ctx, 9, 0u); - setRegU32(env.ctx, 10, 0u); - setRegU32(env.ctx, 11, endFunc); - setRegU32(env.ctx, 29, K_STACK_ADDR); - writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, static_cast(semaId)); - - SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "RECVX snddrv submission should complete"); - }; - - constexpr uint32_t kBusyBeforeCompletion = 0x11111111u; - writeGuestU32(env.rdram.data(), kBusyFlagAddr, kBusyBeforeCompletion); - callSoundDriver(kCompletionOnlyCallback); - - t.Equals(g_recvxSoundCallbackHits.load(), 0u, - "recognized RECVX completion callback should not execute guest code"); - t.Equals(readGuestStruct(env.rdram.data(), kBusyFlagAddr), kBusyBeforeCompletion, - "completion-only callback should preserve the RECVX busy flag"); - setRegU32(env.ctx, 4, static_cast(semaId)); - PollSema(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), semaId, "completion-only callback should signal its semaphore"); - - writeGuestU32(env.rdram.data(), kBusyFlagAddr, 0x22222222u); - callSoundDriver(kClearBusyCallback); - - t.Equals(g_recvxSoundCallbackHits.load(), 0u, - "recognized RECVX clear-busy callback should not execute guest code"); - t.Equals(readGuestStruct(env.rdram.data(), kBusyFlagAddr), 0u, - "designated RECVX callback should clear the guest busy flag"); - setRegU32(env.ctx, 4, static_cast(semaId)); - PollSema(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), semaId, "clear-busy callback should signal its semaphore"); - - setRegU32(env.ctx, 4, kClientAddr); - SifCheckStatRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), 0, "RECVX snddrv client should no longer be RPC-busy"); - }); - - tc.Run("bind before register creates placeholder then remaps", [](TestCase &t) + tc.Run("hybrid bind before register waits then remaps", [](TestCase &t) { TestEnv env; @@ -846,11 +487,9 @@ void register_ps2_sif_rpc_tests() t.Equals(getRegS32(env.ctx, 2), KE_OK, "initial bind without registered server should still succeed"); const SifRpcClientData clientBeforeRegister = readGuestStruct(env.rdram.data(), kClientAddr); - t.IsTrue(clientBeforeRegister.server != 0u, "bind should allocate placeholder server when sid is missing"); - t.IsTrue(clientBeforeRegister.server >= 0x01F10000u && clientBeforeRegister.server < 0x01F20000u, - "placeholder server should come from rpc server pool"); - t.Equals(clientBeforeRegister.buf, 0u, "placeholder server starts with empty buf"); - t.Equals(clientBeforeRegister.cbuf, 0u, "placeholder server starts with empty cbuf"); + t.Equals(clientBeforeRegister.server, 0u, "bind must wait until a hybrid backend owns the SID"); + t.Equals(clientBeforeRegister.buf, 0u, "unbound client starts with empty buf"); + t.Equals(clientBeforeRegister.cbuf, 0u, "unbound client starts with empty cbuf"); setRegU32(env.ctx, 4, kQdAddr); setRegU32(env.ctx, 5, 0x44u); @@ -873,7 +512,7 @@ void register_ps2_sif_rpc_tests() t.Equals(clientAfterRegister.server, kSdAddr, "register should remap pre-bound clients to concrete server descriptor"); t.Equals(clientAfterRegister.buf, kServerBufAddr, "register should update client buf from server descriptor"); t.Equals(clientAfterRegister.cbuf, kServerCbufAddr, "register should update client cbuf from server descriptor"); - t.IsTrue(clientAfterRegister.server != clientBeforeRegister.server, "client server pointer should switch from placeholder to real server"); + t.IsTrue(clientAfterRegister.server != clientBeforeRegister.server, "client server pointer should switch from unbound to real server"); setRegU32(env.ctx, 4, kSdAddr); setRegU32(env.ctx, 5, kQdAddr); @@ -911,167 +550,6 @@ void register_ps2_sif_rpc_tests() t.Equals(getRegU32Result(env.ctx, 2), 0u, "removing the same queue twice should return 0"); }); - tc.Run("snddrv state RPC returns stable buffers and signals sema", [](TestCase &t) - { - TestEnv env; - configureProfile(env, "slus_201.84"); - - constexpr uint32_t kClientAddr = 0x00028000u; - constexpr uint32_t kSemaParamAddr = 0x00028100u; - constexpr uint32_t kRecvAddr = 0x00028200u; - constexpr uint32_t kSid = IOP_SID_SNDDRV_STATE; - - SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); - - const uint32_t semaParam[6] = { - 0u, // count (unused by runtime decode) - 1u, // max_count - 0u, // init_count - 0u, // wait_threads - 0u, // attr - 0u // option - }; - std::memcpy(env.rdram.data() + kSemaParamAddr, semaParam, sizeof(semaParam)); - - setRegU32(env.ctx, 4, kSemaParamAddr); - CreateSema(env.rdram.data(), &env.ctx, &env.runtime); - const int32_t semaId = getRegS32(env.ctx, 2); - t.IsTrue(semaId > 0, "CreateSema should return a positive semaphore id"); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, kSid); - setRegU32(env.ctx, 6, 0u); - SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for snddrv state sid"); - - setRegU32(env.ctx, 4, static_cast(semaId)); - PollSema(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_SEMA_ZERO, "semaphore should start at zero before nowait rpc"); - - std::memset(env.rdram.data() + kRecvAddr, 0, 16u); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_GET_STATUS_ADDR); - setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT); - setRegU32(env.ctx, 7, 0u); - setRegU32(env.ctx, 8, 0u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 16u); - setRegU32(env.ctx, 11, 0u); - setRegU32(env.ctx, 29, K_STACK_ADDR); - writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, static_cast(semaId)); - SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc(sound status) should succeed"); - const uint32_t statusAddr = readGuestStruct(env.rdram.data(), kRecvAddr); - t.IsTrue(statusAddr != 0u, "rpc 0x12 should return a nonzero sound-status pointer"); - - setRegU32(env.ctx, 4, static_cast(semaId)); - PollSema(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), semaId, "nowait rpc should signal completion sema"); - - std::memset(env.rdram.data() + kRecvAddr, 0, 16u); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_GET_ADDR_TABLE); - setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT); - setRegU32(env.ctx, 7, 0u); - setRegU32(env.ctx, 8, 0u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 16u); - setRegU32(env.ctx, 11, 0u); - writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, static_cast(semaId)); - SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc(sound addr table) should succeed"); - const uint32_t addrTableAddr = readGuestStruct(env.rdram.data(), kRecvAddr); - t.IsTrue(addrTableAddr != 0u, "rpc 0x13 should return a nonzero address-table pointer"); - t.IsTrue(addrTableAddr != statusAddr, "sound-status and address-table pointers should be distinct"); - - setRegU32(env.ctx, 4, static_cast(semaId)); - PollSema(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), semaId, "each nowait rpc should signal completion sema"); - - std::memset(env.rdram.data() + kRecvAddr, 0, 16u); - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_GET_STATUS_ADDR); - setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT); - setRegU32(env.ctx, 7, 0u); - setRegU32(env.ctx, 8, 0u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 16u); - setRegU32(env.ctx, 11, 0u); - writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, static_cast(semaId)); - SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr), statusAddr, - "sound-status pointer should remain stable across repeated rpc 0x12 calls"); - }); - - tc.Run("snddrv state RPC returns low guest sound-driver addresses", [](TestCase &t) - { - TestEnv env; - configureProfile(env, "slus_201.84"); - - constexpr uint32_t kClientAddr = 0x00028300u; - constexpr uint32_t kSemaParamAddr = 0x00028400u; - constexpr uint32_t kRecvAddr = 0x00028500u; - constexpr uint32_t kSid = IOP_SID_SNDDRV_STATE; - - SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); - - const uint32_t semaParam[6] = {0u, 1u, 0u, 0u, 0u, 0u}; - std::memcpy(env.rdram.data() + kSemaParamAddr, semaParam, sizeof(semaParam)); - - setRegU32(env.ctx, 4, kSemaParamAddr); - CreateSema(env.rdram.data(), &env.ctx, &env.runtime); - const int32_t semaId = getRegS32(env.ctx, 2); - t.IsTrue(semaId > 0, "CreateSema should return a positive semaphore id"); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, kSid); - setRegU32(env.ctx, 6, 0u); - SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for snddrv state sid"); - - SifRpcClientData client = readGuestStruct(env.rdram.data(), kClientAddr); - client.hdr.sema_id = semaId; - writeGuestStruct(env.rdram.data(), kClientAddr, client); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_GET_STATUS_ADDR); - setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT); - setRegU32(env.ctx, 7, 0u); - setRegU32(env.ctx, 8, 0u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 16u); - setRegU32(env.ctx, 11, 0u); - setRegU32(env.ctx, 29, K_STACK_ADDR); - writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, static_cast(semaId)); - SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - const uint32_t statusAddr = readGuestStruct(env.rdram.data(), kRecvAddr); - t.IsTrue(statusAddr > 0u && statusAddr < 0x00200000u, - "rpc 0x12 should return a low guest address like an IOP pointer"); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, IOP_RPC_SNDDRV_GET_ADDR_TABLE); - setRegU32(env.ctx, 6, K_SIF_RPC_MODE_NOWAIT); - setRegU32(env.ctx, 7, 0u); - setRegU32(env.ctx, 8, 0u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, 16u); - setRegU32(env.ctx, 11, 0u); - SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - const uint32_t addrTableAddr = readGuestStruct(env.rdram.data(), kRecvAddr); - t.IsTrue(addrTableAddr > 0u && addrTableAddr < 0x00200000u, - "rpc 0x13 should return a low guest address like an IOP pointer"); - - const uint32_t hdBaseAddr = readGuestStruct(env.rdram.data(), addrTableAddr + 0u); - const uint32_t sqBaseAddr = readGuestStruct(env.rdram.data(), addrTableAddr + 4u); - const uint32_t dataBaseAddr = readGuestStruct(env.rdram.data(), addrTableAddr + 8u); - t.IsTrue(hdBaseAddr > 0u && hdBaseAddr < 0x00200000u, - "sound-driver hd base should stay in low guest address space"); - t.IsTrue(sqBaseAddr > hdBaseAddr && sqBaseAddr < 0x00200000u, - "sound-driver sq base should be a later low guest address"); - t.IsTrue(dataBaseAddr > sqBaseAddr && dataBaseAddr < 0x00200000u, - "sound-driver data base should be a later low guest address"); - }); - tc.Run("SifCallRpc falls back to stack ABI when register pack is implausible", [](TestCase &t) { TestEnv env; @@ -1148,152 +626,5 @@ void register_ps2_sif_rpc_tests() "recv payload should match stack-selected transfer size"); }); - tc.Run("DTX URPC uses the guest dispatcher only when its function-table slot is registered", [](TestCase &t) - { - TestEnv env; - configureProfile(env, "slus_201.84"); - - constexpr uint32_t kClientAddr = 0x0002C000u; - constexpr uint32_t kDtxSid = 0x7D000000u; - constexpr uint32_t kSendAddr = 0x0002C100u; - constexpr uint32_t kRecvAddr = 0x0002C200u; - constexpr uint32_t kUrpcCommand = 7u; - constexpr uint32_t kRpcNum = 0x400u | kUrpcCommand; - constexpr uint32_t kFnTableSlot = 0x0033FED0u + (kUrpcCommand * sizeof(uint32_t)); - constexpr uint32_t kObjTableSlot = 0x0033FFD0u + (kUrpcCommand * sizeof(uint32_t)); - constexpr uint32_t kWrongFnTableSlot = 0x0034FED0u + (kUrpcCommand * sizeof(uint32_t)); - constexpr uint32_t kWrongObjTableSlot = 0x0034FFD0u + (kUrpcCommand * sizeof(uint32_t)); - constexpr uint32_t kDispatcherAddr = 0x002FABC0u; - constexpr uint32_t kRegisteredHandlerAddr = 0x002FADE0u; - constexpr uint32_t kRegisteredObjectAddr = 0x01F18100u; - constexpr uint32_t kFallbackValue = 0x13579BDFu; - - g_dtxDispatcherHits = 0u; - g_dtxDispatcherRpcNum = 0u; - g_dtxDispatcherSendBuf = 0u; - g_dtxDispatcherSendSize = 0u; - env.runtime.registerFunction(kDispatcherAddr, recvxDtxDispatcher); - - SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, kDtxSid); - setRegU32(env.ctx, 6, 0u); - SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for DTX dispatcher test"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0u, kFallbackValue); - writeGuestU32(env.rdram.data(), kSendAddr + 4u, 0x2468ACE0u); - - auto callUrpc = [&]() { - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, kRpcNum); - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - setRegU32(env.ctx, 8, 8u); - setRegU32(env.ctx, 9, kRecvAddr); - setRegU32(env.ctx, 10, sizeof(uint32_t)); - setRegU32(env.ctx, 11, 0u); - setRegU32(env.ctx, 29, K_STACK_ADDR); - writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, 0u); - - SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "DTX URPC should complete"); - }; - - writeGuestU32(env.rdram.data(), kFnTableSlot, 0u); - writeGuestU32(env.rdram.data(), kObjTableSlot, kRegisteredObjectAddr); - writeGuestU32(env.rdram.data(), kWrongFnTableSlot, kRegisteredHandlerAddr); - writeGuestU32(env.rdram.data(), kWrongObjTableSlot, kRegisteredObjectAddr); - writeGuestU32(env.rdram.data(), kRecvAddr, 0u); - callUrpc(); - - t.Equals(g_dtxDispatcherHits.load(), 0u, - "empty DTX function-table slot should use fallback emulation"); - t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr), kFallbackValue, - "fallback DTX emulation should return the first send word for an unknown command"); - - writeGuestU32(env.rdram.data(), kFnTableSlot, kRegisteredHandlerAddr); - writeGuestU32(env.rdram.data(), kRecvAddr, 0u); - env.runtime.registerFunction(K_DTX_SCHEDULER_CALL, schedulerDtxRpcCall); - env.runtime.registerFunction(K_DTX_SCHEDULER_RESUME, schedulerDtxRpcResume); - g_schedulerRpcClient = kClientAddr; - g_schedulerRpcNumber = kRpcNum; - g_schedulerRpcSend = kSendAddr; - g_schedulerRpcReceive = kRecvAddr; - g_schedulerRpcResult = static_cast(-1); - R5900Context mainContext{}; - mainContext.pc = K_DTX_SCHEDULER_CALL; - setRegU32(mainContext, 29, K_STACK_ADDR); - writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, 0u); - env.runtime.eeScheduler().reset(env.rdram.data(), mainContext); - env.runtime.eeScheduler().run(); - - t.Equals(g_schedulerRpcResult, static_cast(KE_OK), - "DTX URPC should resume its base context with KE_OK"); - - t.Equals(g_dtxDispatcherHits.load(), 1u, - "registered DTX function-table slot should enter the guest dispatcher"); - t.Equals(g_dtxDispatcherRpcNum.load(), kRpcNum, - "DTX dispatcher should receive the full URPC number"); - t.Equals(g_dtxDispatcherSendBuf.load(), kSendAddr, - "DTX dispatcher should receive the send-buffer address"); - t.Equals(g_dtxDispatcherSendSize.load(), 8u, - "DTX dispatcher should receive the send-buffer size"); - t.Equals(readGuestStruct(env.rdram.data(), kRecvAddr), K_DTX_DISPATCH_RESULT_MARKER, - "SifCallRpc should copy the dispatcher result into the receive buffer"); - }); - - tc.Run("SifCallRpc prefers stack ABI for DTX URPC when both packs look plausible", [](TestCase &t) - { - TestEnv env; - configureProfile(env, "slus_201.84"); - - constexpr uint32_t kClientAddr = 0x0002B000u; - constexpr uint32_t kDtxSid = 0x7D000000u; - constexpr uint32_t kSendAddr = 0x0002B100u; - constexpr uint32_t kRecvStackAddr = 0x0002B200u; - constexpr uint32_t kRecvRegAddr = 0x0002B300u; - - SifInitRpc(env.rdram.data(), &env.ctx, &env.runtime); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, kDtxSid); - setRegU32(env.ctx, 6, 0u); - SifBindRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifBindRpc should succeed for DTX sid"); - - writeGuestU32(env.rdram.data(), kSendAddr + 0x00u, 1u); // mode - writeGuestU32(env.rdram.data(), kSendAddr + 0x04u, 0x1E21440u); // wk addr - writeGuestU32(env.rdram.data(), kSendAddr + 0x08u, 0x100u); // wk size - writeGuestU32(env.rdram.data(), kRecvStackAddr, 0u); - writeGuestU32(env.rdram.data(), kRecvRegAddr, 0u); - - setRegU32(env.ctx, 29, K_STACK_ADDR); - writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x10u, 12u); - writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x14u, kRecvStackAddr); - writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x18u, 4u); - writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x1Cu, 0u); - writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x20u, 0u); - writeGuestU32(env.rdram.data(), K_STACK_ADDR + 0x00u, 0u); - - setRegU32(env.ctx, 4, kClientAddr); - setRegU32(env.ctx, 5, 0x422u); // DTX URPC command 34 (SJUNI create) - setRegU32(env.ctx, 6, 0u); - setRegU32(env.ctx, 7, kSendAddr); - // Plausible but intentionally wrong register-side packed args. - setRegU32(env.ctx, 8, 4u); - setRegU32(env.ctx, 9, kRecvRegAddr); - setRegU32(env.ctx, 10, 12u); - setRegU32(env.ctx, 11, 0u); - - SifCallRpc(env.rdram.data(), &env.ctx, &env.runtime); - t.Equals(getRegS32(env.ctx, 2), KE_OK, "SifCallRpc should succeed for DTX URPC"); - - const uint32_t stackHandle = readGuestStruct(env.rdram.data(), kRecvStackAddr); - const uint32_t regHandle = readGuestStruct(env.rdram.data(), kRecvRegAddr); - t.IsTrue(stackHandle != 0u, "DTX handle should be written to stack-selected recv buffer"); - t.Equals(regHandle, 0u, "register recv buffer should remain untouched when stack ABI is preferred"); - }); }); }