-
Notifications
You must be signed in to change notification settings - Fork 101
PS2Recomp Stripped Game Walkthrough For LLMs
This is a practical guide for games that have no useful symbols and mostly look like:
sub_0010B1A0sub_0031D5F8entry_00123456
It focuses on:
- getting first boot
- moving from boot to menu
- debugging "stuck" states
- using address bindings and overrides safely
Runtime dispatch is address based.
The important identity is:
0xADDR -> function pointer
Not:
-
sub_xxxtext name
So:
- Renaming generated C++ functions does not fix execution by itself.
- If
0x00123456should behave likesceCdRead, bind that address to the handler. - Address mappings are build specific. Do not assume they are portable across region/revision ELFs.
From repo root:
cmake -S . -B out/build
cmake --build out/build --config Debug --target ps2_analyzer ps2_recomp ps2EntryRunnerTypical binaries:
out/build/ps2xAnalyzer/Debug/ps2_analyzer.exeout/build/ps2xRecomp/Debug/ps2_recomp.exeout/build/ps2xRuntime/Debug/ps2EntryRunner.exe
.\out\build\ps2xAnalyzer\Debug\ps2_analyzer.exe game.elf game.tomlIf possible, export a Ghidra function map CSV and set:
[general]
ghidra_output = "path/to/ghidra_functions.csv"This reduces bad function splits and missing starts.
.\out\build\ps2xRecomp\Debug\ps2_recomp.exe game.tomlGenerated output must end up in:
-
ps2xRuntime/src/runner/for.cpp -
ps2xRuntime/include/for generated headers
Important:
- Replace the placeholder
ps2xRuntime/src/runner/register_functions.cppwith the generated one. - If
registerAllFunctions(...)stays empty, runtime cannot dispatch anything useful.
cmake --build out/build --config Debug --target ps2EntryRunner
.\out\build\ps2xRuntime\Debug\ps2EntryRunner.exe game.elfUse general.stubs with address bindings:
[general]
stubs = [
"sceCdRead@0x00123456",
"SifLoadModule@0x00127890"
]Temporary triage bindings:
[general]
stubs = [
"ret0@0x001D9410",
"ret1@0x001D5BC8",
"reta0@0x0024B7C0"
]Meaning:
-
ret0: returns0 -
ret1: returns1 -
reta0: returns argument$a0
Use them to test control flow quickly, then replace with real behavior.
Run, read logs, fix the first hard blocker, repeat.
Log pattern:
Warning: Function at address 0x... not found
Actions:
- Confirm generated
register_functions.cppis compiled. - Confirm
registerAllFunctions(runtime)is called beforeloadELF. - Search generated code for unresolved raw dispatch:
rg -n "lookupFunction\\(0x" ps2xRuntime/src/runner- For missing targets, add address mapping in TOML or via override.
- If many targets are missing, improve boundaries (Ghidra map).
Log pattern:
Warning: Unimplemented PS2 stub called. name=...
Actions:
- If address is known, bind
handler@0xADDRin TOML. - If behavior unknown, test
ret0/ret1/reta0. - If game progresses with triage return, implement a proper handler later.
Log pattern:
[Syscall TODO] ...
Actions:
- Implement syscall semantics in
ps2xRuntime/src/lib/ps2_syscalls.cppand included syscall.inlfiles. - Return values matter. Many games only need expected success/failure contract first.
Log pattern:
CPU is doing some work at PC 0x... PC not updating.
Actions:
- Open generated function for that address.
- Check loop exit conditions and called helpers.
- Set breakpoints in:
PS2Runtime::lookupFunctionps2_stubs::TODO_NAMEDps2_syscalls::TODO
It is possible often, but not from address alone.
Use a combined method:
- all callsites of that address
- argument setup (
$a0..$a3) - return checks on
$v0(==0,<0, pointer use) - immediate constants and nearby strings
- side effects (buffer writes, DMA, RPC payloads)
- stub log gives
PC,RA, args - syscall TODO log gives encoded and fallback syscall IDs
- repeated call frequency at same PC tells you if it is a probe loop or hard dependency
- Try
ret0. - Try
ret1. - Try
reta0. - Observe branch direction after return in caller.
If one value clearly unlocks progress, you learned the caller contract.
Runtime has override registration keyed by ELF metadata:
- ELF basename
- optional entry point
- optional CRC32
Header:
ps2xRuntime/include/game_overrides.h
Recommended for zero CMake edits:
- place in
ps2xRuntime/src/runner/(runner usessrc/runner/*.cppglob)
Alternative:
- place in
ps2xRuntime/src/lib/and add file tops2xRuntime/CMakeLists.txt
#include "game_overrides.h"
#include "ps2_runtime.h"
#include "ps2_stubs.h"
#include "ps2_syscalls.h"
namespace
{
void applyMyGameOverrides(PS2Runtime &runtime)
{
// Safe direct custom wrapper that auto-returns if callee did not move PC.
runtime.registerFunction(0x00123456u,
[](uint8_t *rdram, R5900Context *ctx, PS2Runtime *rt)
{
const uint32_t entryPc = ctx->pc;
ps2_stubs::sceCdRead(rdram, ctx, rt);
if (ctx->pc == entryPc)
{
ctx->pc = getRegU32(ctx, 31);
}
});
// Good for explicit triage handlers that already return.
ps2_game_overrides::bindAddressHandler(runtime, 0x0031D5F8u, "ret0");
}
}
PS2_REGISTER_GAME_OVERRIDE(
"my-game-us",
"SLUS_XXXX.XX",
0x00100008u,
0u,
applyMyGameOverrides);ps2_game_overrides::bindAddressHandler(...) binds the raw handler directly.
Many stub/syscall handlers do not advance ctx->pc on their own.
If bound directly and pc is unchanged, you can get infinite redispatch to the same address.
For non-trivial handlers, prefer runtime.registerFunction(...lambda...) and add:
if (ctx->pc == entryPc) { ctx->pc = getRegU32(ctx, 31); }Use TOML handler@0xADDR when:
- you are already recompiling
- you want generated stub wrappers (with return-to-RA fallback behavior)
- you need simple, reproducible mapping in config
Use runtime overrides when:
- you want per-build routing with ELF metadata matching
- you need custom logic around handler calls
- you want to avoid editing the generated output directly
Goal:
- no immediate crash
- entry function dispatch works
Do:
- verify function registration
- resolve first unknown function addresses
- resolve critical syscall TODOs
Goal:
- game loads assets
- transitions past logo/intro
Do:
- prioritize
sceCd*,fio*,Sif*, module loading paths - watch for unresolved CD/file requests in logs
Goal:
- frame loop reaches menu logic
Do:
- remove temporary returns from hot loops
- replace with stable semantics
- keep overrides targeted to this title/build
- Forgetting to replace placeholder
register_functions.cpp. - Assuming symbol names matter more than addresses.
- Copying address map from a different ELF build.
- Binding raw handlers in overrides without PC fallback.
- Leaving too many
ret0/ret1stubs in final build and then debugging random late failures.
Find unresolved static address calls in generated code:
rg -n "lookupFunction\\(0x" ps2xRuntime/src/runnerFind everything about a specific address:
rg -n "0x00123456|_0x123456|sub_00123456|entry_00123456" ps2xRuntime/src/runnerFind where a runtime handler exists:
rg -n "sceCdRead|SifLoadModule|ret0|ret1|reta0" ps2xRuntime/include/ps2_call_list.h ps2xRuntime/src/lib-
registerAllFunctions(runtime)is non-empty and built. - Entry point function is registered.
- First unresolved function addresses are mapped or implemented.
- First unknown syscalls are handled.
- No obvious same-PC infinite loop from direct raw handler binding.
- Temporary return stubs are limited and documented.
If all 6 are true, you are in real behavior emulation territory, not plumbing failure.