Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cc941d2
refactor: from guest threads to EE scheduler
ran-j Jul 26, 2026
9f99d61
feat: bad wip mpeg fix for code veronica
ran-j Jul 26, 2026
c09e90a
Merge branch 'main' of https://github.com/ran-j/PS2Recomp into featur…
ran-j Aug 5, 2026
3e5fb2b
feat: cheap copy from host
ran-j Aug 5, 2026
d91d2c1
feat: added EE clock Hz
ran-j Aug 8, 2026
f9332d6
fix: fix lotr tests
ran-j Aug 11, 2026
4fff583
fix: fix cri dtx loading
ran-j Aug 12, 2026
7159330
Merge branch 'main' of https://github.com/ran-j/PS2Recomp into featur…
ran-j Aug 12, 2026
8b57926
feat: revert wrong changes
ran-j Aug 12, 2026
3b9b14c
refactor: change GS architecture
ran-j Aug 13, 2026
a673939
Merge branch 'main' of https://github.com/ran-j/PS2Recomp into featur…
ran-j Aug 13, 2026
a293fa4
feat: IOP emulator
ran-j Aug 19, 2026
cc9e075
feat: analyzer resolve the complete constant-producing sequence with …
ran-j Sep 2, 2026
b9dba60
feat: remove recompiled version of GetRomName
ran-j Sep 2, 2026
5ebc247
eat: enhance ELF parser with improved callable entry detection and co…
ran-j Sep 2, 2026
6560a37
feat: update memory hint handling and enhance entry point discovery l…
ran-j Sep 2, 2026
08dc217
feat: add SET_GPR_ZE32 macro for zero-extending loads with unsigned s…
ran-j Sep 2, 2026
c8c8666
refactor: Refactor PS2 IOP Host Adapter and Memory Management
ran-j Sep 2, 2026
2a61ba0
feat: added a lot of tests
ran-j Sep 2, 2026
590b884
Merge origin/main into feature/iop-emulator
ran-j Sep 7, 2026
78ecbae
fix: fix texture caching
ran-j Sep 14, 2026
39251a0
feat: remove LLE IOPs
ran-j Sep 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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`.

Expand Down Expand Up @@ -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:

Expand All @@ -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
Expand All @@ -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
11 changes: 4 additions & 7 deletions ps2xAnalyzer/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"`.
Expand Down Expand Up @@ -63,8 +62,7 @@ ps2_analyzer <input_elf> <output_toml> [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`.
Expand All @@ -74,15 +72,14 @@ 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.).

## Limitations

* 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).
74 changes: 29 additions & 45 deletions ps2xAnalyzer/src/elf_analyzer.cpp
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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 ||
Expand Down Expand Up @@ -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<int>(inst.address) - i * 4 >= static_cast<int>(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 &section : m_context.sections)
{
uint32_t targetAddr = baseAddr + static_cast<int16_t>(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 &section : 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; });

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;
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);
}
}

Expand Down
6 changes: 3 additions & 3 deletions ps2xAnalyzer/src/toml_generator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
52 changes: 39 additions & 13 deletions ps2xIOP/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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=$<BOOL:${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
Expand Down
Loading
Loading