When a computed jump cannot be resolved to a jump table, every instruction in the function becomes an entry point, because the jump could land on any of them. That fallback was also applied to JALR, which is not a jump but a call: it transfers control to another function and returns to the instruction after the delay slot. That return address is already queued as a resume target a few lines above, so nothing else in the function needs to be reachable from outside. Indirect calls are ordinary code -- function pointers, virtual dispatch, callbacks -- so the fallback fired constantly. On a 3 MB PS2 executable, 2,210 of the 2,422 unresolved sites were JALR, and 1,078 of the 1,282 affected functions contained no unresolved jump at all. Restrict the fallback to JR. Promoted entries drop from 189,876 to 1,688, registered table entries from 156,783 to 75,386, the generated registration file from 13 MB to 6 MB, and total output from 180 MB to 163 MB. Every indirect call site in real code keeps its return-address resume entry (the only sites that lose one are bogus functions carved out of rodata, where the address is outside the function anyway).
PS2Recomp: PlayStation 2 Static Recompiler (Experimental)
Also check our WIKI
This project statically recompiles PS2 ELF binaries into C++ and provides a runtime to execute the generated code.
Modules
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.
Features
- Translates MIPS R5900 instructions to C++ code
- PS2-specific MMI and VU0 macro support.
- Single-file or multi-file output.
- Configurable stubs, skips, and instruction patches.
- Instruction-driven syscall handling.
How It Works
PS2Recomp works by:
- Parsing a PS2 ELF file to extract functions, symbols, and relocations
- Decoding the MIPS R5900 instructions in each function
- Translating those instructions to equivalent C++ code
- Generating a runtime that can execute the recompiled code
The translated code is very literal, with each MIPS instruction mapping to a C++ operation. For example, addiu $r4, $r4, 0x20 becomes ctx->r4 = ADD32(ctx->r4, 0X20);.
Current Behavior
stubsentries generate wrappers that call known runtime syscall/stub handlers by name.stubsalso supports address bindings withhandler@0xADDRESSfor stripped games (for examplesceCdRead@0x00123456).- Address bindings also support generic return handlers for triage:
ret0,ret1,reta0. - Recompiler now tries relocation-symbol auto-binding at callsites (
J/JAL) before raw address dispatch; when relocation symbol is known (for examplesceCdRead), it can call runtime handlers without manual address mapping. - Recompiler discovers additional internal static entry targets and emits
entry_...wrappers for those addresses. - For unresolved static
J/JALsites, generated code falls back toruntime->lookupFunction(0x...). skipentries are not recompiled and generate explicitps2_stubs::TODO_NAMED(...)wrappers.- Recompiled
SYSCALLnow callsruntime->handleSyscall(...)with the encoded syscall immediate. - Runtime syscall dispatch tries encoded syscall ID first, then falls back to
$v1.
Requirements
- CMake 3.20+
- C++20 compiler (currently tested mainly with MSVC)
- SSE4/AVX host support for some vector paths
Build
git clone --recurse-submodules https://github.com/ran-j/PS2Recomp.git
cd PS2Recomp
cmake -S . -B out/build
cmake --build out/build --config Debug
Usage
Preferred workflow for retail or stripped games:
- Open the ELF in Ghidra.
- Run
ps2xRecomp/tools/ghidra/ExportPS2Functions.java. - Use the exported TOML and CSV map.
- Recompile with the exported TOML:
./ps2_recomp config.toml
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 for the recommended path.
Then build generated output and link with ps2xRuntime.
Configuration
Main fields in config.toml:
general.input: source ELF path.general.ghidra_output: recommended function map CSV exported from Ghidra.general.output: generated C++ output folder.general.single_file_output: one combined cpp or one file per function.general.low_memory_mode: reduce peak output-generation memory by avoiding retained disassembly strings and forcing serial output generation. Generated instruction comments are still emitted; disassembly text is produced while writing each output file instead of being kept in memory.general.output_worker_threads: number of output-generation workers (clamped to nproc * 2). A positive value uses exactly that many workers.0usesnproc - 1when at least two hardware threads are available, otherwise serial output generation.1forces serial output generation.general.patch_syscalls: apply configured patches toSYSCALLinstructions (falserecommended).general.patch_cop0: apply configured patches to COP0 instructions.general.patch_cache: apply configured patches to CACHE instructions.general.stubs: names to force as stubs. Also acceptshandler@0xADDRESSto bind a stripped function address directly to a runtime syscall/stub handler. Includes generic handlersret0,ret1,reta0.general.skip: names to force as skipped wrappers.patches.instructions: raw instruction replacements by address.
Address binding for stripped ELFs:
- Use
handler@0xADDRESSinsidegeneral.stubsto map a stripped function start directly to a runtime handler. - Example:
sceCdRead@0x00123456binds function start0x00123456tops2_stubs::sceCdRead(...). - Generic temporary handlers are available:
ret0@0xADDR,ret1@0xADDR,reta0@0xADDR. - Before manual binding, prefer recompilation from a Ghidra-exported TOML/CSV first. The extra boundaries and synthetic entry points are usually more important than manual early triage.
- The address must be the function start in that exact ELF build.
- Addresses are not portable across different games/regions/builds.
- The handler name must exist in runtime call lists (
PS2_SYSCALL_LISTorPS2_STUB_LIST).
Example:
# stripped function binding by address:
stubs = ["sceCdRead@0x00123456", "SifLoadModule@0x00127890"]
# temporary return handlers:
stubs = ["ret0@0x001D9410", "ret1@0x001D5BC8", "reta0@0x0024B7C0"]
# mixed example:
stubs = ["printf", "sceCdRead@0x00123456", "SifLoadModule@0x00127890"]
Runtime
To execute the recompiled code.
ps2xRuntime currently provides:
- Guest memory model and function dispatch table.
- Some syscall dispatcher with common kernel IDs.
- Basic GS/VU/file/system stubs.
- Foundation to expand and port your game.
ps2xIOPprofile selection and optional.dll/.sodiscovery for game-specific IOP HLE.
See IOP HLE profiles and plugins for the service boundary and external plugin workflow.
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.
API:
- Header:
ps2xRuntime/include/game_overrides.h - Register macro:
PS2_REGISTER_GAME_OVERRIDE(name, elfName, entry, crc32, applyFn) - Direct bind helper:
ps2_game_overrides::bindAddressHandler(runtime, addr, "handler")
Use Game Override modules when:
- You need per-game/per-build routing or patches without polluting global behavior.
- You need to bind many addresses, or install custom replacement logic for a specific title.
Recommended Iteration Loop
- Run with minimal config and no aggressive skipping.
- Fix hard blockers first (
function not found, syscall TODO, critical IO stubs). - Use temporary return stubs only to classify call importance.
- Promote temporary fixes to real implementations.
- Move per-game hacks into game overrides keyed by ELF metadata.
- Re-test from cold boot after each batch.
Limitations
- Graphics Synthesizer and other hardware components need external implementation
- VU1 microcode is not complete.
- Hardware emulation is partial and many paths are stubbed.
Acknowledgments
- Inspired by N64Recomp
- Uses ELFIO for ELF parsing
- Uses toml11 for TOML parsing
- Uses fmt for string formatting