updateGsCsrFieldForVSync runs on the detached vsync worker and updated the FIELD bit with a non-atomic read-modify-write of GSRegisters::csr while guest threads concurrently read/write the same word (MMIO read32/read64 and the W1C handling in write32/write64) and the GIF path sets SIGNAL/FINISH. Even though the writers touch disjoint bits, a whole-word RMW loses the other side's update: a clobbered SIGNAL/FINISH set hangs a game synchronizing on GS completion, a clobbered W1C clear re-asserts a handled interrupt, and a clobbered FIELD toggle stalls interlace field polling. ThreadSanitizer flags the race on main (updateGsCsrFieldForVSync vs PS2Memory::write64 and GS::writeRegister). Make the field std::atomic<uint64_t> and perform every update as a single atomic RMW: - vsync FIELD toggle -> fetch_or / fetch_and - MMIO W1C writes -> compare_exchange loop in shared helpers (a load-then-store pair would still race); a 32-bit store to the CSR's upper dword previously bypassed the W1C special case entirely and went through the plain merge branch - both halves now share the same atomic helper with unchanged guest-visible semantics - GIF SIGNAL/FINISH -> fetch_or - reads -> load() std::atomic<uint64_t> is lock-free on all supported targets (static_assert added), the struct's size/alignment asserts are unchanged, GSRegisters is never copied by value, and default (seq_cst) ordering is used throughout - these operations are rare (vblank ticks, GIF signals, CSR MMIO), so reviewability wins over micro-optimization. New regression test: two racer threads each own one status bit (SIGNAL / FINISH) and loop 80k GIF-set + W1C-clear cycles verifying their own bit after each half-op while the vsync worker toggles FIELD. Fails 20/20 runs against the previous code, passes 50/50 with the fix, ~350ms runtime, no sanitizer needed.
Runtime Library
The runtime library provides the execution environment for recompiled code, including:
- Memory management (32MB main RAM, scratchpad, etc.)
- Register context (128-bit GPRs, VU0 registers, etc.)
- Function table for dynamic linking
- Basic PS2 system call stubs
How to use:
Take your decompiled code and place the cpp files on ps2xRuntime/src/runner and header files on ps2xRuntime/include and compile/be happy.
Vita Build Notes
The Vita runtime uses Quenom/raylib-5.5-vita for vita build. I recommend build runtime only.
Expected environment:
VITASDKpoints to your VitaSDK root.Quenom/raylib-5.5-vitahas already been built and installed into$VITASDK/arm-vita-eabi.- SDL2 with the PVR backend required by that raylib fork is also installed into the same VitaSDK prefix.
PS2X_DEFAULT_BOOT_ELFis mandatory, you need to define where your game is like "ux0:data/RANJ00001/game/SLUS_201.84".
The CMake for ps2xRuntime consumes those preinstalled headers and libraries from VitaSDK. It does not fetch or install the Vita raylib fork for you.
Adding Custom Function Implementations
You can add custom implementations for PS2 system calls or game functions by:
- Creating function implementations that match the signature:
void function_name(uint8_t* rdram, R5900Context* ctx, PS2Runtime *runtime);
- Registering them with the runtime:
runtime.registerFunction(address, function_name);
Advanced Features
Memory Translation The runtime handles PS2's memory addressing, including:
- KSEG0/KSEG1 direct mapping
- TLB lookups for user memory
- Special memory areas (scratchpad, I/O registers)
Vector Unit Support
PS2-specific 128-bit MMI instructions and VU0 macro mode instructions are supported via SSE/AVX intrinsics.
Instruction Patching
You can patch specific instructions in the recompiled code to fix game issues or implement custom behavior.
Limitations
- Graphics and sound output require external implementations
- Some PS2-specific hardware features may not be fully supported
- Performance may vary based on the complexity of the game