Open-source project
ran-j/PS2Recomp avatar
ran-j/PS2Recomp

PS2Recomp: a MIPS instruction becomes one C++ statement

Playstation 2 Static Recompiler & Runtime Tool to make native PC ports

3,854 stars258 forksC++GPL-3.0

At a glance

What is it?
PS2Recomp translates PlayStation 2 ELF binaries into C++ and ships a runtime to execute the result, as four modules that split analysis, recompilation, execution and the IOP. What the documentation gives you is unusually specific about where the work is unfinished: TODO_NAMED wrappers, generic return handlers, and addresses that are explicitly not portable.
Who is it for?
PS2Recomp fits someone who already has an ELF they are allowed to work with and wants readable C++ rather than an emulator. The toolchain is unusually honest about its own limits, which is what makes it usable: skipped functions are labelled, unresolved calls fall back to a lookup instead of failing quietly, and address bindings are documented as valid for exactly one build.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 22 days ago.
What is it written in?
Mainly C++, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • ran-j/PS2Recomp3,854 ★ · +714
Star history
Repository2026-08-012026-11-14
ran-j/PS2Recomp3.1k3.9k

714 Stars in the last 105 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

Four modules split analysis, recompilation, execution and the IOP

The pipeline has four named pieces and each one owns a stage. ps2xAnalyzer scans the ELF and its functions and writes a TOML configuration holding stubs, skips and instruction patches. ps2xRecomp reads that TOML back together with the ELF, decodes R5900 instructions and generates the C++ output. ps2xRuntime hosts the guest memory model, function registration, syscall dispatch and hardware stubs. ps2xIOP executes R3000A IRX modules behind a virtual IOP kernel, with generic high level emulation fallbacks.

The description of the transformation is worth reading slowly, because it explains what the generated code looks like. Parsing the ELF yields functions, symbols and relocations; each function's instructions are decoded; those instructions are translated into equivalent C++; and a runtime is generated to execute the result. The translation is called very literal, with each MIPS instruction mapping to a C++ operation. The example given is `addiu $r4, $r4, 0x20` becoming `ctx->r4 = ADD32(ctx->r4, 0X20);`.

That one-statement-per-instruction property is the whole design. The output is large and slow to compile, and in exchange it is readable, diffable and patchable by address.

The documented preference is the slower Ghidra route

There are two workflows, and the document says which one to use.

The preferred path is for retail or stripped games. Open the ELF in Ghidra, run the script at ps2xRecomp/tools/ghidra/ExportPS2Functions.java, take the exported TOML and CSV map, and recompile with the exported configuration:

bash
./ps2_recomp config.toml

The fallback is for quick local experiments or ELFs that still carry debug symbols, and it skips Ghidra entirely:

bash
./ps2_analyzer your_game.elf config.toml

After either one you build the generated output and link it against ps2xRuntime. The analysis notes make the reasoning explicit: recompiling from a Ghidra export is preferred because the extra boundaries and synthetic entry points it produces usually matter more than early manual triage.

That is a real cost decision, not a formality. The analyzer route is one command; the Ghidra route needs a reverse engineering tool, a script run, and a CSV you then feed back in. The document is telling you the second is what you want for anything you intend to keep working on.

Address bindings are exact, and explicitly not portable

For stripped binaries the configuration accepts a handler bound to an address, written as handler@0xADDRESS. The example binds a function start of 0x00123456 to a runtime handler named sceCdRead, and the file shows the shapes you can use:

toml
# 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"]

Three constraints are stated, and they are the difference between a working config and a mystery. The address must be the function start in that exact ELF build. Addresses are not portable across different games, regions or builds. And the handler name has to exist in the runtime call lists, named PS2_SYSCALL_LIST or PS2_STUB_LIST.

The recompiler also tries harder before you hand-write anything: it attempts relocation symbol auto-binding at J and JAL callsites, so a call whose relocation symbol is known can reach a runtime handler without a manual mapping. It discovers additional internal static entry targets and emits entry_ wrappers for them, and where a static call site stays unresolved the generated code falls back to a runtime lookup by address rather than to nothing. The generic ret0, ret1 and reta0 handlers exist for triage, which is to say for finding out what a function should return before you implement it.

Skipped functions come out as TODO_NAMED wrappers

The compiler is explicit about the parts it did not translate. Skip entries are not recompiled at all, and they generate visible placeholder wrappers of the form ps2_stubs::TODO_NAMED(...), so an unimplemented function shows up in the output as a name to finish rather than as a silent zero.

Syscall handling is described with the same precision. A recompiled SYSCALL instruction calls the runtime's syscall handler with the encoded syscall immediate, and dispatch tries the encoded syscall ID first and then falls back to the value in $v1. There is a configuration switch for rewriting those instructions: general.patch_syscalls applies configured patches to SYSCALL instructions, and the documentation recommends leaving it false. Separate switches exist for COP0 instructions and for CACHE instructions, and a patches.instructions table gives raw instruction replacements by address.

Read together, these three mechanisms are the whole patching surface: skip a function you cannot translate yet, replace an instruction outright, or redirect a call to a runtime handler. The output tells you which of the three you did.

The runtime is called a foundation, in the project's own words

The runtime section lists what ps2xRuntime provides, and the phrasing is careful. There is a guest memory model and a function dispatch table. There is a syscall dispatcher with common kernel IDs, described as some dispatcher rather than a complete one. There are basic GS, VU, file and system stubs. And then the summary line, which calls the module a foundation to expand and port your game.

Read that as a scope statement rather than a feature list. The dispatcher covers the kernel calls the author needed, the stubs are basic, and everything past that is the porting work the tool is meant to enable. The title of the project carries its own warning, with the word Experimental in parentheses after the name.

The IOP sits apart from that, since ps2xIOP executes original IRX modules with generic high level fallbacks and has its own documentation for module execution and the service boundary. That separation matters for anyone planning a port, because the input processing unit has its own compatibility story rather than sharing the main runtime's.

Memory and threading settings describe a machine-dependent build

Three configuration fields are really about your hardware rather than your input.

general.low_memory_mode reduces peak memory during output generation by not retaining disassembly strings and by forcing serial generation. Instruction comments are still emitted, and the disassembly text is produced while each output file is written instead of being held, so the comments you read in the output cost less memory but arrive later.

general.output_worker_threads takes a number and then clamps it to twice the processor count. A positive value uses exactly that many workers, 1 forces serial generation, and 0 uses one fewer than the processor count when at least two hardware threads exist and falls back to serial otherwise.

general.single_file_output chooses between one combined cpp file and one file per function, which is the difference between a single enormous translation unit and a directory of small ones.

The rest of the configuration is plumbing: general.input for the source ELF path, general.output for the destination folder, general.ghidra_output for the recommended function map CSV, general.stubs and general.skip for the two lists above. The settings are unusually well specified for a project at version 0.4, and each one answers a question a large ELF will otherwise answer for you, unpleasantly.

Game overrides are per build and keyed by a checksum

There is a second patching layer, and it is deliberately separate from recompilation output. Game overrides are runtime-side, build-scoped patch modules: C++ code that runs during loadELF and can replace EE function bindings by address for one specific game build. IOP RPC and DMA behaviour is handled by the ps2xIOP emulator and its runtime transport, and the document states that this is separate from both the recompilation output and the global runtime stubs and syscalls.

The API surface is three things. The header is ps2xRuntime/include/game_overrides.h. The registration macro is PS2_REGISTER_GAME_OVERRIDE(name, elfName, entry, crc32, applyFn), which is where the scoping comes from: a name, the ELF it applies to, an entry point, and a CRC32 to match the build against. And there is a direct bind helper, ps2_game_overrides::bindAddressHandler(runtime, addr, "handler"), for the case where you want to point one address at one handler.

The stated reasons to reach for it are per game or per build routing without polluting global behaviour, and needing to bind many addresses at once. The list of reasons stops mid line after the words install cu, so anything past that second item is not written down here.

Android and Vita directories sit next to a Windows-first story

The top level of the repository is where the portability picture gets interesting. Alongside the four modules named in the documentation there are android/ and vita/ directories, plus ps2xStudio/ and ps2xTest/, a CMakeLists.txt at the root and a CMakeSettings.json, which is a Visual Studio generator file.

The requirements, meanwhile, ask for CMake 3.20 or newer, a C++20 compiler currently tested mainly with MSVC, and SSE4 or AVX host support for some of the vector paths. So the build story that is verified is a Windows one, and the two extra platform directories are ports rather than targets the documentation claims to support.

The build itself is three commands after a clone that pulls submodules:

bash
git clone --recurse-submodules https://github.com/ran-j/PS2Recomp.git
cd PS2Recomp

cmake -S . -B out/build
cmake --build out/build --config Debug

On versions, three releases appear in quick succession: v0.2 on 2026-03-09, v0.3 on 2026-03-18 and v0.4 on 2026-04-05. The last recorded push to main is 2026-09-30, roughly six months after that tag. Every release is numbered below 1.0, which matches the Experimental label better than a version number would.

Editorial conclusion

PS2Recomp fits someone who already has an ELF they are allowed to work with and wants readable C++ rather than an emulator. The toolchain is unusually honest about its own limits, which is what makes it usable: skipped functions are labelled, unresolved calls fall back to a lookup instead of failing quietly, and address bindings are documented as valid for exactly one build. Three things to settle before an afternoon goes into it. The requirements name MSVC as the compiler that is actually tested, while the tree carries Android and Vita directories that imply something wider. The documented preference is the slower Ghidra route, not the analyzer, and skipping it costs you the function map. And the address bindings you write by hand are per build, so keep the TOML next to the ELF it was made for. Expect an experimental tool: three releases in March and April 2026, all numbered 0.x, with the last recorded push six months after the newest tag.

Frequently asked questions

how to use ps2 recomp

The documented preference for retail or stripped games is to open the ELF in Ghidra, run ps2xRecomp/tools/ghidra/ExportPS2Functions.java, take the exported TOML and CSV map, and run ./ps2_recomp config.toml, then build the generated output and link it with ps2xRuntime. For quick experiments or ELFs with debug symbols, ./ps2_analyzer your_game.elf config.toml is the fallback.

What is PS2Recomp?

An experimental static recompiler that translates PS2 ELF binaries into C++ and ships a runtime to execute the generated code. The work is split across ps2xAnalyzer for scanning, ps2xRecomp for translation, ps2xRuntime for execution and ps2xIOP for IRX module execution behind a virtual IOP kernel.

What does PS2Recomp need to build?

CMake 3.20 or newer, a C++20 compiler that is currently tested mainly with MSVC, and SSE4 or AVX support on the host for some vector paths. The clone step is documented with submodules included.

Are PS2Recomp address bindings portable between games or builds?

No. The address must be the function start in that exact ELF build, and the documentation states that addresses are not portable across different games, regions or builds. The handler name must also exist in the runtime call lists PS2_SYSCALL_LIST or PS2_STUB_LIST.

Official sources

  1. Official README
  2. Project repository
  3. Release notes
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ran-j-ps2recomp.svg)](https://hysenlabs.com/projects/ran-j-ps2recomp)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/ran-j-ps2recomp.svg)](https://hysenlabs.com/projects/ran-j-ps2recomp)

Was this useful?