The fork's pin carries GTA IV 1.00's frame-rate patch (its render thread's frame lock, which RPCS3's database only unlocks for 1.08), savestates that resume a disc game kept in a dev_hdd0 folder and load over one another, and files in /app0 written through the console's FTP server once the title's write budget is spent (the SDK fork's offload streams: GTA IV reinstalled 1414 MiB in 4 minutes with the budget spent, byte for byte). From the baseline state at 300%: 50.4-51.5 fps where the game held itself to 30 (docs/RPCS3_PORT.md, GTA IV; docs/PHASE_LOG.md). The platform probe's "offload" word measures writing through that server from the title. The sampler bounds its frame walk by each thread's stack, which faulted with every core thread sampled, and reports each busy thread, which the most frequent groups hid.
41 KiB
RPCS3 as a libretro core: the port
This document is the audit behind porting RPCS3 (PlayStation 3) to the PS5
title as rpcs3_libretro.so, and the plan it leads to. Progress and run
results are recorded in docs/ACTIVE.md and docs/PHASE_LOG.md, not here.
The objective
On my console, rpcs3_libretro.so boots God of War HD (the PSN release
NPUA80490) through RetroArch's Load Content or /app0/args.txt. The game
reaches gameplay, rendered by RPCS3's Vulkan renderer on RADV at a 300%
resolution scale (3840×2160 internal for its 720p output). The evidence is:
- a RetroArch screenshot of gameplay whose picture I accept;
- ten minutes of play with no crash;
- the Close Content + reload battery passing;
- each 10 s audio window's speed, recorded. Full speed is not part of the objective, but its measurement is.
What the console holds (read in place, never copied anywhere):
| Item | Where on the console |
|---|---|
| PS3 firmware | /app0/system/RPCS3/PS3UPDAT.PUP (206 MB) |
| The game | /app0/content/PS3/God of War.pkg (6.3 GB), a PSN package |
| Its licence | /app0/content/PS3/UP9000-NPUA80490_00-GODOFWARHDUS0000.rap |
Because the game is a package and not a disc folder, the first content path is "install the package and its RAP, then boot the installed game". The installed copy needs another 6.3 GB on the console's storage.
The fork and the pin
../PS5_RPCS3 is my fork of RPCS3/rpcs3 (github.com/mihawk-99/PS5_RPCS3,
public), with an upstream remote. Its main starts at upstream
1707d7fc883e (2026-09-28, "remove unnecessary #ifdef"): the newest master
commit whose CI passed all thirteen checks, including RPCS3's FreeBSD build.
The PS5's system software is FreeBSD-based, so that is the configuration
closest to this port. Every port change is a commit on the fork's main, and
tools/build-rpcs3.sh pins the fork by revision. Submodules RPCS3 names by
relative URL (../../llvm/llvm-project.git and the rest) resolve from the fork
as from upstream. A submodule that needs changes gets its own fork,
../PS5_<Name>, referenced by relative URL, as PS5_Azahar does with
PS5_Dynarmic.
Licence
RPCS3 states its licence as GPL-2.0-only (its README; LICENSE is the GPLv2).
The rest of the title is GPL-3.0-or-later (RetroArch, this port, the payload SDK
fork and its platform layer), and GPL-2.0-only code and GPL-3.0 code cannot be
combined into one work and distributed. Whether the core and the title are one
work is not settled by the core being a separate .so; these are the facts that
bear on it (2026-09-29):
rpcs3_libretro.sostatically linkstooling/native/core_cxx_runtime.cpp(this repository, GPL-3.0-or-later by the repository's licence) throughPS5_CORE_LINK_INPUTS, beside LLVM (Apache-2.0 WITH LLVM-exception, whose exception addresses GPLv2 combinations), FFmpeg and GNU libiconv (LGPL-2.1-or-later).- At load time the title's native loader binds the core's libc and file-system
imports to the title's own wrappers and to the platform layer
(
tools/core-imports.py,build/core_imports.inc), and the core runs in the title's process, on the title's heap, through RetroArch's libretro and Vulkan context interfaces.
So it is not in a release. The ways out are a licence decision for the owner of
the GPL-3.0 parts that touch the core (dual-licensing core_cxx_runtime.cpp
and the platform layer's bound functions under a GPLv2-compatible licence, which
is possible only for code whose authors agree), a change of interface, or
counsel's view that the combination is not one work; the licence audit of
2026-09-29 sets them out for that decision.
The title bundles no PS3 firmware, keys or games; RPCS3 installs the firmware the user supplies and decrypts packages with the user's own licence files (RAP); that audit covers those paths too.
Phase 1: the audit
1. Dependencies
RPCS3 builds its emulator as the static library rpcs3_emu, and its Qt
application links that. Upstream already builds rpcs3_emu without Qt for its
Android port (ANDROID in CMakeLists.txt): Qt, the Qt UI and the OpenGL
renderer are left out, OpenAL is compiled out (WITHOUT_OPENAL) and the
bundled FFmpeg is not used. The libretro
build follows that path, with a LIBRETRO switch of its own where Android's
choices differ.
3rdparty/ at the pin (submodule revisions in brackets):
| Item | What RPCS3 uses it for | Decision |
|---|---|---|
llvm (llvm-project ca7933e4, LLVM 22.1) |
PPU and SPU recompilers | Build: a pinned static LLVM for x86_64-sie-ps5, X86 target only (below) |
asmjit (416f7356) |
SPU ASMJIT recompiler, trampolines, JIT helpers | Build, its code memory through the platform layer (gap a) |
ffmpeg (RPCS3/ffmpeg-core 649d9d93, prebuilt) |
video and audio decoding (cellVdec, cellAdec, ATRAC3+, cellMusic) | Build from source: a pinned FFmpeg release, static, software decoders (below); the prebuilt archive has no PS5 build |
glslang (f0bd0257) |
GLSL to SPIR-V for the Vulkan renderer's shaders | Build |
GPUOpen/VulkanMemoryAllocator (1d8f600f) |
Vulkan memory | Build (header library) |
zlib (da607da7), zstd (f8745da6) |
archives, save states, caches | Build |
libpng (3061454d), stblib (013ac3be), bcdec |
images, texture decoding | Build (bcdec and stb are headers) |
yaml-cpp (51a5d623), pugixml (c8033ce9) |
configuration, patches, XML | Build |
SoundTouch (a0fba77b) |
audio time-stretch in the resampler | Build |
fusion (93254240) |
motion-sensor fusion in PadHandler.cpp |
Build (small C library) |
| unordered_dense | hash maps | Build (header) |
protobuf (edaa823d) |
RPCN (NP) messages | Stub: RPCN is compiled into rpcs3_emu unconditionally, so a LIBRETRO option leaves the NP network clients out with an offline implementation. Networking is dropped |
wolfssl (3c5eead4), curl (01346829) |
RPCN and clans over TLS | Drop, with the network clients (above) |
miniupnp (d66872e3) |
UPnP port mapping | Drop, with upnp_handler stubbed |
libusb (87a55632) |
USB passthrough (sys_usbd, usb_device) |
Stub: no PS5 backend; sys_usbd reports no devices |
| hidapi | HID pads, only in rpcs3/Input |
Drop (the libretro pad handler replaces rpcs3/Input) |
rtmidi (1e5b4992) |
MIDI instruments (Rock Band) | Build with RtMidi's dummy API, or stub the MIDI devices |
| OpenAL | microphone capture in cellMic | Drop: WITHOUT_OPENAL, as on Android |
| cubeb, FAudio, XAudio2 | audio output backends | Drop: a libretro audio backend replaces them |
| libsdl-org/SDL | input, camera, FAudio's dependency | Drop |
| opencv | PS Move tracking in rpcs3/Input |
Drop |
| discord-rpc, feralinteractive (GameMode), pine | Discord, GameMode, PINE IPC | Drop |
| 7zip | not used by rpcs3_emu at the pin |
Drop |
| MoltenVK, GL | macOS Vulkan, OpenGL headers | Drop (no OpenGL renderer, as on Android) |
| Qt 6 | the UI | Drop |
CMake options (top-level CMakeLists.txt):
| Option | Default | Port |
|---|---|---|
USE_NATIVE_INSTRUCTIONS |
ON | OFF; -march=znver2 instead, the PS5's Zen 2 (AVX2, FMA, BMI2), set in the toolchain file |
WITH_LLVM |
ON | ON |
BUILD_LLVM |
OFF | OFF: LLVM is built separately and found through LLVM_DIR |
STATIC_LINK_LLVM |
OFF | ON |
USE_FAUDIO, USE_SDL, USE_LIBEVDEV, USE_DISCORD_RPC, USE_GAMEMODE |
ON/OFF | OFF |
USE_VULKAN |
ON | ON |
USE_PRECOMPILED_HEADERS |
OFF | OFF (ccache) |
USE_SYSTEM_* |
mixed | OFF throughout: every library is built for the PS5 or dropped |
HAS_MEMORY_BREAKPOINTS, BUILD_RPCS3_TESTS, RUN_RPCS3_TESTS |
OFF | OFF |
USE_LTO |
ON | OFF: a 1 GiB-class shared object links in time without it; revisit when it runs |
LLVM. RPCS3 requires LLVM 18 or newer and builds 22.1 from its submodule.
I build that same revision (ca7933e4) once, as a pinned static install
(tools/build-llvm.sh into .deps/native/llvm-ps5), with prospero-clang,
ccache, LLVM_TARGETS_TO_BUILD=X86, and no tools, tests, docs or examples, and
point RPCS3 at it with LLVM_DIR. Keeping it out of RPCS3's build tree means a
change to RPCS3 never rebuilds LLVM. LLVM's own Support layer assumes Unix
facilities (mmap, sysconf, dladdr, signals) that the platform layer and
the core loader's import table provide or refuse; the link shows which. The
archive size, and what it adds to the core, are measured at step 3; LLVM with
only the X86 target is on the order of a few hundred MB of archives, of which
the core links the code generator, MCJIT and RuntimeDyld.
FFmpeg. RPCS3's prebuilt ffmpeg-core has no build for this console, so I
build FFmpeg from a pinned source release: static, no programs, no network, no
hardware acceleration. The decoders are the ones the PS3's media libraries
need (H.264, MPEG-2, MPEG-4 Part 2, ATRAC3, ATRAC3+, AAC, MP3, AC-3, LPCM), with
libavformat's MP4, MPEG-PS and raw demuxers, libswscale and libswresample. No
encoders: video recording is dropped. Software decoding comes first. The
console's hardware video decoder, which ProsperoLight uses, is a later option
for H.264.
2. The frontend
RPCS3 drives Emu (the Emulator in rpcs3/Emu/System.h) from a frontend
that fills g_emu_callbacks (rpcs3/Emu/emu_callbacks.h: 49 std::function
hooks) and calls Emu.Init(). headless_application shows the minimum: it is
a QCoreApplication whose callbacks create null renderers and handlers, and
it refuses Vulkan. The libretro layer, rpcs3/libretro/ in the fork, replaces
it without Qt:
- Entry points.
retro_initsets RPCS3's directories (below) and fills the callbacks.retro_load_gameinstalls the firmware if it is missing, installs a package if the content is one, then boots.retro_runruns the queued main-thread calls (call_from_main_thread, which RPCS3 uses for UI-side work on the frontend's thread), presents the newest frame and pushes audio.retro_unload_gamestopsEmuand waits for its threads. - Vulkan. RPCS3 creates its own instance (API 1.2) and device
(
vkutils/instance.cpp,vkutils/device.cpp). Through RetroArch's context-negotiation interface (v2) the core creates the instance at the version RPCS3 asks for (create_instance), and the device with RPCS3's feature and extension choices (create_device2). RetroArch uses that instance and device. RetroArch's own instance is 1.1, which is why the core creates it. RPCS3's existingnative_swapchain_baseowns its presentable images. Aswapchain_libretroon top of it presents by handing the finished image to RetroArch withset_imageandset_signal_semaphore, in the layout RetroArch expects. EveryvkQueueSubmitand present that RPCS3's RSX thread makes on the queue it shares with RetroArch goes between the interface'slock_queueandunlock_queue. - Audio. A
LibretroBackend : AudioBackendtakes cellAudio's output through the backend's write callback, resamples it to RetroArch's rate (48 kHz; RPCS3's audio is 48 kHz already, so resampling is a copy until a game asks otherwise), converts it to 16-bit stereo and queues it.retro_runhands it over with the batch callback. - Input. A libretro pad handler (a
PadHandlerBase) maps the RetroPad to a DualShock 3: buttons, both sticks and the analogue triggers. Pressure sensitivity comes from the digital state. Keyboard, mouse, camera and music handlers are the null ones. - Firmware. The Qt application installs
PS3UPDAT.PUPinmain_window::HandlePupInstallation(about 320 lines). The libretro layer does the same work without Qt:pup_object(Loader/PUP), the update packages' TAR extraction and the SPRX decryption. It runs on first boot whenutils::get_firmware_version()is empty and reports progress through the RetroArch message interface. - Content. The formats, in order:
- A PSN package plus its RAP. This is the game at hand. The layer installs
it with
package_reader(Crypto/unpkg), asmain_window::HandlePackageInstallationdoes (about 380 lines), and copies the RAP todev_hdd0/home/<user>/exdata/(main_window::InstallFileInExData), then boots the installeddev_hdd0/game/<title>/USRDIR/EBOOT.BIN. An already installed package is not installed again. - A disc game's folder (
PS3_GAME/USRDIR/EBOOT.BIN). - An ISO, which the pinned revision supports (
Loader/ISO.cpp).
- A PSN package plus its RAP. This is the game at hand. The layer installs
it with
- Where RPCS3 keeps its files. RPCS3's emulator directory (
dev_flash,dev_hdd0, config, caches) is/app0/system/RPCS3/, next to the firmware. Folders are 0777 and files at least 0666, so everything stays reachable over FTP. - Core options and PS5 defaults. Resolution scale 300%. Shader mode
asynchronous with the shader interpreter (
async_with_interpreter, already upstream's default), so no game waits for a shader compile or relies on a warm cache. PPU and SPU decoders LLVM (upstream's defaults). Options are the libretro v2 kind, written to RPCS3's configuration when content loads (Phase 2 below). - Core info. libretro-core-info has no RPCS3 file, so
tooling/rpcs3/rpcs3_libretro.infois in this repository (name, extensionspkg|iso|bin|elf|self|sfo, firmware entry, GPLv2,needs_fullpath). - Reference ports. Dolphin's
Source/Core/DolphinLibretro/(the Vulkan negotiation, its instance and device hooks and main-thread handling), PPSSPP's libretro port and Azahar's.
3. Platform gaps
Each gap is either already measured on the console (docs/PROBE.md, the
platform layer's probes) or gets a probe step in the platform layer
(src/probe.c, host tests in tests/test_platform.c), run inside the title
with /app0/platform-probe.txt.
(a) Code memory for LLVM and asmjit. RPCS3 reserves code areas with
utils::memory_reserve(size, can_be_jit=true) and commits them
read-write-execute (protection::wx). Measured: direct memory mapped
read-write and then given execute runs, read-write-execute included, and a
protection change costs about 26 µs. ps5_exec_allocate has had no limit on
live blocks since SDK d3e8625: 2,000 4 KiB blocks and 300 64 KiB regions ran on
the console. LLVM uses the Small code model with PIC, so its code reaches
RPCS3's helpers through RuntimeDyld stubs and needs no placement near the
core. asmjit commits its 2 GiB area up front only under CAN_OVERCOMMIT
(Linux), which the PS5 branch leaves undefined, so it grows on demand.
To measure: a probe step that reserves a 2 GiB range, commits 64 KiB pieces
read-write-execute on demand, and runs code in each, with the time a commit
takes.
(b) Address-space reservations. RPCS3 reserves its guest memory in 4 GiB
steps from 0x3_0000_0000 upward (Emu/Memory/vm.cpp):
| Area | Size |
|---|---|
g_base_addr + g_sudo_addr |
8 GiB |
g_exec_addr |
12 GiB |
g_hook_addr |
32 GiB |
g_stat_addr |
4 GiB (g_free_addr, for SPU, starts right after it) |
That is 56 GiB, plus JIT areas (2 GiB for asmjit, 768 MiB for each LLVM
memory manager) and RSX's mappings. Measured (docs/PROBE.md): the GPU window
is 0x2_0000_0000–0x2_FFFF_FFFF; from 0x4_0000_0000 at most 15 GiB; from
0x10_0000_0000 to 0x80_0000_0000 16 GiB is reserved at the hint itself;
nothing is granted at or above 1 TiB; and a hint whose free space is too small
is refused, not moved. The PS5 branch of memory_reserve therefore starts at
0x10_0000_0000, through ps5_vrange_reserve, which never draws on the
title's 448 MiB flexible pool, where an anonymous mmap reservation would.
To measure: one probe step reserving RPCS3's layout, 56 GiB in the sizes
above from 0x10_0000_0000, to confirm 16 GiB is not a per-reservation cap
for 32 GiB.
(c) Guest memory as views. RPCS3's utils::shm creates a memory object
(memfd_create, or shm_open(SHM_ANON) on FreeBSD) and maps it at several
addresses: g_base_addr and g_sudo_addr see the same memory at different
protections. The PS5 branch uses ps5platform/shm.h: ps5_shm_create (one
direct-memory object), ps5_shm_map at fixed addresses inside a
ps5_vrange_reserve range, and ps5_memfd_create where RPCS3 wants a
descriptor. The platform tests already cover several views of one object.
To measure: 4 GiB of direct memory as two views, written through one and
read through the other, and the time a 64 KiB view takes to map.
(d) 16 KiB host pages. RPCS3 already handles hosts whose pages exceed
4 KiB (Apple Silicon): vm.cpp stops protecting 4 KiB guest blocks one by one
when utils::get_page_size() > 4096 and maps them read-write, and every
mprotect is rounded to the host page. The RSX texture cache still protects
pages to catch CPU writes, now 16 KiB at a time, so a write near a cached
texture invalidates a wider range. To measure: what sysconf(_SC_PAGESIZE)
returns in the title (it must be 16384; the platform layer answers it
otherwise), and the texture cache's protection changes per frame in the game,
from RPCS3's own counters, at 26 µs each.
(e) thread_local on emulated TLS. The core loader has no PT_TLS support,
so a core's thread_local goes through __emutls_get_address. RPCS3 has 93
thread_local uses. The hot ones are cpu_thread::g_tls_this_thread,
vm::g_tls_locked and atomic.cpp's wait callbacks, all read on every guest
reservation and wait. To measure: the cost of an emulated-TLS read on the
console (a probe step timing __emutls_get_address over a million reads),
then RPCS3's rate of those reads in the game. If it matters, the fork keeps
those few variables in a per-thread structure the thread already carries.
(f) Atomic waits. On FreeBSD, RPCS3's atomic.cpp uses its USE_STD
path (std::condition_variable), and Utilities/sync.h emulates futex()
off Linux, so it needs only pthread mutexes and condition variables, which the
console exports. _umtx_op is exported too, if a faster path is wanted later.
Nothing to add.
(g) Faults and ucontext. RPCS3's access-violation handler
(Utilities/Thread.cpp) reads uc_mcontext.mc_rip, mc_err and the general
registers on FreeBSD. The SDK's sys/_ucontext.h corrects the console's
machine-context layout, and the platform probe already backpatches 200 fault
sites from a handler. RPCS3 also installs SIGSEGV, SIGBUS, SIGILL and
SIGPIPE handlers process-wide. In the title those would sit beside the
title's own crash reporter. To measure: a guest page fault handled and
resumed inside the core, with the title's reporter still catching a genuine
crash.
(h) Threads. RPCS3's named_thread calls pthread_create, which the
title binds to its factory (src/core_threads_ps5.cpp, 2 MiB minimum stack).
RPCS3 creates a PPU thread for each guest thread, six SPU threads, the RSX
thread and a pool of LLVM compile workers sized from
sysconf(_SC_NPROCESSORS_ONLN). Measured by the thread probe (SDK 9a57ba7):
thirteen CPUs (0–12) are available to the title, and threads run at policy 1,
priority 700, in a range of 256 to 767. A priority set on a running thread
reads back. pthread_getaffinity_np with FreeBSD's cpuset_t returns ERANGE,
and RPCS3's set_thread_affinity_mask uses exactly that form. The platform
layer gets ps5_pthread_setaffinity_np and ps5_pthread_getaffinity_np (the
exported scePthreadSetaffinity/scePthreadGetaffinity with a 64-bit mask,
or the size the kernel accepts), bound through tools/core-imports.py.
sysconf(_SC_NPROCESSORS_ONLN) must answer 13. To measure: the affinity
calls round-tripping on the console, and what sysconf answers.
4. Risks
| Risk | How it is measured |
|---|---|
LLVM does not build for the PS5 target, or its Support layer needs what the console lacks |
the LLVM build (step 1) and the core's link and import report (step 3) |
| The core is too large for the loader (1 GiB image cap) | check-core.py and the loader's mapped_bytes (step 4) |
| Guest reservations do not fit below 1 TiB with the GPU window avoided | the reservation probe (gap b), then Emu.Init on the console |
| Emulated TLS costs too much in the hottest paths | the TLS probe (gap e), then the sampler in the game |
| RetroArch's Vulkan interface cannot give RPCS3 the device it needs (1.2 core, extensions) | device creation through create_device2 (step 4), and RPCS3's own capability log |
| Queue sharing with RetroArch stalls the RSX thread | the sampler on the RSX thread; frame timing |
| Firmware install needs host facilities the title lacks | step 5's install run, file by file |
| Shader compiles cost speed | windows during the game's first minutes with an empty cache |
| The package install fills the console's storage | the install run's free-space check before it starts |
| 16 KiB pages widen the texture cache's invalidations | RPCS3's texture-cache counters in the game |
| The CPU cannot hold full speed (not part of the objective) | the 10 s windows, reported as they are |
5. The ladder
Each step ends with its evidence in docs/ACTIVE.md and a dated entry in
docs/PHASE_LOG.md.
- Fork and build scripts. The fork at the pin;
tools/build-llvm.sh(the pinned static LLVM);tools/build-ffmpeg.sh(the pinned FFmpeg);tools/build-rpcs3.shontools/core-fork.sh;tooling/rpcs3/ps5-toolchain.cmake;rpcs3incore_names. Evidence: LLVM and FFmpeg archives with their sizes, andrpcs3_emucompiling for the PS5 target. - Host build (optional). The same libretro core for the host, debugged in a desktop RetroArch if this host's Vulkan driver allows. Evidence: the host core reaching the firmware check.
- Link and ABI. The PS5 core links and passes
tools/check-core.py; missing libc and kernel functions go into the platform layer with aliases intools/core-imports.py. Evidence:check-core.pyPASS and the import list. - Load. The core loads on the console and
retro_initreturns; the probe steps for gaps (a)–(h) run inside the title. Evidence: the loader'sreadyline, the probe's checks. - Firmware, then a homebrew program.
PS3UPDAT.PUPinstalls from/app0/system/RPCS3/, then a small PS3 homebrew or test program boots before any commercial game. Evidence: the firmware version RPCS3 reports, and the program's output. - The game. God of War HD installed from its package and RAP, booted at 100%, then at 300%: screenshots, the speed of each window, the Close Content + reload battery and a ten-minute run. Evidence: the screenshot I accept, the windows, the battery lines and the ten-minute trace.
Phase 2: what the port turned out to need
Where the build and the first code differ from the audit above.
Dependencies.
- LLVM is my fork
../PS5_LLVM(github.com/mihawk-99/PS5_LLVM), pinned intools/build-llvm.sh. The PS4/PS5 ABI ignoresalignason an empty base class, and LLVM relies on it twice.SmallVector<T, 0>'s layout assertions failed at build time; the fork alignsSmallVectoritself under__SCE__.TrailingObjectsfailed silently: its empty aligner base is what raises a class to its trailing objects' alignment, so on the PS5 a header of anintand somebools stayed 4-aligned and 12 bytes, and two trailing pointers were given 28 bytes whilegetTrailingObjectsread them from 16 to 32. The next allocation's first field overwrote the last pointer's upper half (MachineInstr::ExtraInfo's memory operands: God of War HD's SPU compiles crashed inMachineInstr::hasOrderedMemoryRefon0x2_xxxx_xxxx, heap pointers with their upper 32 bits replaced by the count 2). The fork gives the aligner a zero-length member there, a non-empty base without storage; the PS5 target then lays such a class out as the host does (8-aligned, 16 bytes, 32 allocated, trailing objects at 16). The other emptyalignasclasses in LLVM and RPCS3 are standalone objects, whose alignment the ABI keeps. The build uses host tablegen tools (LLVM_NATIVE_TOOL_DIR) and builds only the libraries (LLVM_INCLUDE_TOOLS=OFF), since no LLVM program links for the console. - FFmpeg 8.1.1 comes from its signed release tarball (
tools/build-ffmpeg.sh), with RPCS3's own component list and NASM for its x86 assembly. - GNU libiconv 1.18, static, comes from its signed release tarball
(
tools/build-libiconv.sh).cellL10nconverts the PS3's character sets with iconv, and the console's libc has none. - OpenAL Soft is built rather than compiled out:
cellMicincludes its headers outsideWITHOUT_OPENALoff Android. Every backend is off, so it opens no device. Its sources are C++20 modules; CMake's dependency scan runs throughtooling/rpcs3/clang-scan-deps, which gives the scanner the SDK's target and headers. - zlib is the frontend's pinned build. libpng's generated configuration does not see the submodule's headers.
- pkg-config answers nothing in the toolchain file
(
tooling/rpcs3/ps5-toolchain.cmake). The host's packages would add the host's/usr/include. - The network clients (RPCN, clans, UPnP) still build, with wolfSSL and curl. They are not stubbed yet; the title has no network use for them.
- The target defines
__FreeBSD__as 9, so RPCS3's FreeBSD branches take their pre-13 paths (shm_open(SHM_ANON)formemfd_create). The PS5 branch below replaces them where memory is concerned.
Vulkan (rpcs3/Emu/RSX/VK/vkutils/swapchain_libretro.* in the fork).
RetroArch creates the instance with the core version RPCS3 asks for (1.2), and
the device through create_device2. The core runs RPCS3's own
render_device::create there, so the device has RPCS3's features and
extensions plus RetroArch's; when the renderer starts, render_device::create
runs again and adopts that device instead of creating one. Features RPCS3
enables only for some settings are the same whatever the game's configuration,
so the device a boot needs is the one RetroArch made. Every submission and
device-wide wait takes RetroArch's queue lock inside RPCS3's own submit lock.
The swapchain is the core's own images, sampled by RetroArch:
- acquiring an image signals RPCS3's semaphore with an empty submission;
- presenting waits on RPCS3's semaphore and signals a fence;
retro_runhands RetroArch the newest finished image withset_image, after its fence;- an image RetroArch showed is reused only after RetroArch's frame index that last read it comes around again, which is when RetroArch has waited for that frame.
The images are not RPCS3's allocations, so they outlive the renderer while
RetroArch may still show one; they go when a newer frame replaces them or the
context is destroyed. The game boots in the first retro_run after RetroArch's
context is up, because RPCS3 builds its renderer while booting.
Input (rpcs3/libretro/libretro_pad_handler.*). RetroPads 1 to 4 are the
PS3's controllers, through a pad handler on RPCS3's generic machinery (SDL's
value conventions). The core compiles rpcs3/Input/pad_thread.cpp and what it
needs, without the HID handlers (no hidapi on the console), and writes the
input configuration that names the RetroPads before the pad thread loads it.
Memory (the __PROSPERO__ branch of rpcs3/util/vm_native.cpp). RPCS3's
reservations, commits and shared memory go through the platform layer:
- reservations are
ps5_vrange_reserveandps5_vrange_reserve_at; - commits and decommits are
ps5_vrange_commitandps5_vrange_decommit, direct memory in 64 KiB units (SDK 7f3d3ff); utils::shmis aps5_shmdirect-memory object with its views.
Guest memory starts at 64 GiB (0x10_0000_0000). The 32 GiB hook area is only
reserved: nothing reads it, and a 32 GiB copy-on-write view of zeros has no
direct-memory form. The page size is the kernel's 16 KiB. The core's
reservations are released, with the memory committed in them, when the core is
unloaded, so the next load finds its addresses free.
Faults. In the libretro core, a fault RPCS3 does not handle goes back to the handler it replaced (the title's crash reporter), and the title's handlers return when the core is unloaded.
Platform gaps closed in the SDK (8160289). Each is aliased in
tools/core-imports.py.
- No export at all:
accept4,getpagesizes,in6addr_any. - Exported, but resolved to nothing in a title:
gai_strerror,statfs,fstatfs,umask,fork,setsid,wait4. - Formerly taken from the SDK's static payload libc, whose system calls a title
may not make:
syscall(onlySYS_write),getpwnam_r,posix_madvise,strsignal. - Answered differently on the console:
pthread_getaffinity_npandpthread_setaffinity_np(the 64-bit mask),sysconf(13 CPUs, not 16),pthread_exit(a thread'sthread_localdestructors first). - Refused:
realpathanswers EPERM, even for/app0, so libc++'sstd::filesystem::canonicalandweakly_canonicalcame back empty and the package installer could not normalise its path.ps5_realpathmakes the path absolute, resolves.and..by name and checks each component withstat; it keeps a symbolic link's name. The title links it too (--wrap=realpath). - Memory: committed ranges (
ps5_vrange_commit), exact reservations, zeroing of reused direct memory, and placement up to 1 TiB.
The title's core loader answers a core's dlopen(NULL) with the process handle
(src/core_loader_ps5.cpp), which LLVM's JIT needs.
What the console run needed in the fork is recorded in docs/PHASE_LOG.md
(2026-09-28): RSX's start order, VMA's entry points under volk, the kqueue
trigger RSX audio's timer uses, releasing LLVM's memory-manager ranges, the
log listeners' lifetime, and quitting through Emulator::Quit.
Content loading and the loading screen (rpcs3/libretro/libretro_loader.*
in the fork). retro_load_game returns at once: the firmware and a PSN package
install on a worker thread while retro_run shows a loading screen, and the
game boots when they are done. The screen is drawn on the CPU (the stb
libraries RPCS3 already has) and handed to RetroArch as a Vulkan image, one
per RetroArch frame index, with the copy submitted through
set_command_buffers:
- a violet night behind the game's own art (its PIC1, blurred and tinted), its ICON0 on a glass card, the title in Inter, and a violet-to-orchid bar with a sweeping highlight;
- "Installing firmware" with its packages, "Installing" with the bytes, the speed, the time left and the percentage, then "Starting" with RPCS3's compile progress, until RPCS3's first frame replaces it;
- what failed, in rose, when something does.
Inter 4.1 (SIL Open Font License, its licence beside it) is fetched by
tools/build-rpcs3.sh, pinned by digest, and staged in system/RPCS3/fonts;
the firmware's Rodin is the fallback. A finished package install is recorded
in system/RPCS3/libretro/installed/<title ID>, since an interrupted one leaves
an EBOOT.BIN behind, and the record goes when an install starts. Closing
content during an install stops it, inside a large file too (the fork's
unpkg.cpp); a partial firmware is installed again next time.
RPCS3's folder, as staged. Besides the loading screen's fonts, the title
stages RPCS3's own overlay images (bin/Icons/ui into system/RPCS3/Icons/ui),
which its native dialogs draw. The configuration the core writes at load also
turns RPCS3's GDB server off: no debugger connects to a core, and it would only
open a socket on the console. Open question: the server's address did not
match RPCS3's IPv4 std::regex on the console (it fell through to the Unix
socket branch), which may mean std::regex misbehaves there; RPCS3 also parses
patches and its game database with it, so it wants a probe.
What loads. A PSN package (.pkg, installed once, then booted at
once), a game's EBOOT.BIN, an ISO, or a folder (RetroArch's "Use this
directory"): the folder's USRDIR/EBOOT.BIN (an installed game, such as
system/RPCS3/dev_hdd0/game/NPUA80490) or PS3_GAME/USRDIR/EBOOT.BIN (a
disc game) boots. RPCS3 itself treats a bare folder as "build the PPU cache of
everything in it", which compiled and stopped. Loading a folder also first
crashed the title: canonicalising a relative path asks libc++ for the working
directory, and the title's own getcwd resolves to nothing on the console
(libc's calls __getcwd, which only libkernel_sys has). The title links it to
the platform's ps5_getcwd (--wrap=getcwd), as the cores already were, and
tooling/native/app-symbols.map keeps every __wrap_ symbol local, since lld
exported __wrap_getcwd to interpose on libc's stubs and a title publishes
no exports.
Core options (rpcs3/libretro/libretro_options.* in the fork). Every RPCS3
setting with a set of values is a libretro v2 core option, one category per
section of RPCS3's configuration: CPU (Core), Virtual File System, GPU
(Video, with its Vulkan, performance overlay and shader loading dialog
subsections), Audio, Input, System, Network, Save States and Miscellaneous.
They are built from the configuration tree itself, so their names, values
(an enum's names; a number's range as a list: all of a small range, else the
1-2-5 series, even steps, the bounds and the default), defaults and whether
RPCS3 takes a change while a game runs are RPCS3's own. A change RPCS3 takes
live applies at once; the rest are written to config.yml when content loads,
as RPCS3's settings dialog writes them, so a game's custom configuration still
overrides them. Left out: what the core fixes itself (the Vulkan renderer,
RetroArch's audio backend, the RetroPads in place of keyboards, mice, cameras
and Move), the Qt application's window behaviour (auto-start and auto-exit,
fullscreen, focus pause, native UI, GameMode), settings for other hosts (Metal),
VSync (RetroArch paces frames) and RPCS3's savestate folder limits. Paths and
free-text settings have no set of values to offer. The resolution scale keeps
100% to 300% (the frame is 720p at the scale, 2160p at most), 300% by default.
Game patches (rpcs3/libretro/libretro_patches.*). RPCS3 has no
frame-rate unlock of its own: a game runs faster than it was made to only
through the patches in RPCS3's game patch database. The fork keeps that
database as RPCS3's patch manager downloads it (bin/patches/patch.yml, its
source, date and hash in bin/patches/SOURCE; 815 executables, 502 of them
with a frame-rate patch on 2026-09-29), and the title stages it where RPCS3
reads it (system/RPCS3/patches/). The "Frame-rate patches" core option, on by
default, turns each game's frame-rate patch on when content loads: "60 FPS"
where the database has one, else "Unlock FPS", which RPCS3's frame limit holds
at the display's rate; one per executable, serial and version, and another
rate (30, 50, 120), a fallback variant or a "No User Input" one stays off.
RPCS3's patch_config.yml is written only when that changes, and keeps the
state of every other patch. A patch for a game the database lacks goes in
patches/<serial>_patch.yml, which RPCS3 reads for that game.
Save states (rpcs3/libretro/libretro_state.*). RetroArch's save states
are RPCS3 savestates. RPCS3 writes one to a file system in memory, a virtual
device like its ISO one (Emulator::savestate_path_override points the
savestate there), and the game boots again from it, as RPCS3's own "save state
and continue" does; loading a state stops the game and boots the state in its
place. Nothing goes through the console's storage but RetroArch's own state
file. RetroArch asks for a state's size right before it serializes one, and
before it loads one (to back up what runs for "Undo Load State"), so the core
makes the state at the size query and keeps it until the next frame: one
request costs one savestate. RPCS3 refuses a savestate while an SPU cannot be
stopped (its "Compatible Savestate Mode" option), a video decoder context
exists (LLE libvdec avoids it) or the game is saving; the game then runs on
and RetroArch reports the failure. The core declares its states variable in
size and platform-dependent, and its .info offers basic save states only (no
rewind or run-ahead, which RPCS3 savestates cannot serve). The pad script's
SAVE_STATE and LOAD_STATE actions save and load the current slot, so a
test run can start from a known scene (and SCREENSHOT keeps the frame shown
then as /app0/pad-shot-<n>.png, so one run shows each screen it walks
through); RetroArch names the state after the
content, so a game loaded as its folder (dev_hdd0/game/GTA4) has
GTA4.state, where an EBOOT.BIN gives every game EBOOT.state.
A title's write budget. The console, not its NVMe drive, sets how fast a
title writes: about 1.3 GiB at 242 MiB/s, then about 2 MiB/s, whether through
write(), O_DIRECT or a shared mapping and with however many threads, and it
refills while the title does not write. Its FTP server, another process, is
held back less: about 7 MiB/s a connection and 22 MiB/s in all (the SDK's
docs/PROBE.md, "Sustained writes" and "Writing through another process"). So
RPCS3's files in the title's folder go through that server once the title's
burst is spent (the fork's Utilities/File.cpp, on the SDK's
include/ps5platform/offload.h): a file opened for writing in /app0 times
its writes, and once its first MiB is written and a MiB of the title's writes
ran slower than 50 MiB/s, a write at the file's end starts a stream that
carries the file's following writes through a loopback connection to the
server; anything else done with the file (reading, seeking, its size,
closing) first waits for the server to confirm the stream's bytes. The stream
keeps what the server has not confirmed (16 MiB at most) and writes it itself
if the server fails, so nothing is lost; with no server it never starts. The
server is found by its greeting and the folder it sees /app0 as by a marker
file, and the answer is kept in /app0/.ps5-offload. GTA IV reinstalling
1414 MiB of its install with the budget spent took 4 minutes, about 6 MiB/s,
where its first install ran at 0.7 MiB/s once the budget was spent, and the
nine files it wrote were byte for byte the ones it had written before
(2026-09-29). Several files streamed at once would reach the server's 22 MiB/s;
RPCS3 writes one at a time. Installing a package's large files without copying
them, read and decrypted from the package the way RPCS3 reads an ISO through a
virtual device, would remove the wait and the second copy.
GTA IV (BLES00229 1.00): the test game, toward 4K at 60 fps
GTA IV is the RPCS3 core's test game from 2026-09-29; the target is 4K at a stable 60 fps, measured from a save state in its first playable scene (Roman's taxi at the docks, the first mission). What it took, all in the fork:
- A FIFO in local memory (
RSX: a FIFO whose command buffer is in local memory). 1.00's libgcm allocates its context with system mode 0x210 and writes its first commands at local-memory offset 0x1000 with no IO memory mapped; RPCS3 read GET and PUT as IO offsets and stopped with "Dead FIFO commands queue state" (RPCS3 issue #14194, where 1.00 never boots). Mode 0x200 now makes GET, PUT and the jump and call targets local-memory offsets. - The ZCULL late fault (
ZCULL: a late fault on a report page unlocks it), RPCS3 issue #19610: on 16K-page hosts the RSX thread's semaphore labels share a page with the reports, and a declined late fault spun forever. - Its folder (
Disc games in dev_hdd0/game mount their folder whatever its name, andA disc game in a dev_hdd0 folder resumes from a savestate in that folder): GTA IV is kept asdev_hdd0/game/GTA4, which the boot took as a 9-character title ID, and a savestate's boot path carries a doubled separator. - Save states (
vm: memory restored from a savestate takes its block's page size): loading a state over a game resumed from one stopped on "Memory inconsistency found" at the stack block. A save costs about 20 s of the game, and so does a load: RPCS3 resumes by booting the state. - Its frame rate (
libretro: GTA IV 1.00's frame-rate patch): RPCS3's patch database unlocks 1.08 only. 1.00's render thread holds frames to the frame lock times 1/60 s, polling the timebase with 30 us sleeps (1.16 million of them in 15 s of gameplay), and skips that when the frame lock is 0; the fork'sbin/patches/BLES00229_patch.ymlsets it (0x0113b9f4) to 0, found in the game's code (a dump of the running executable, theppu-dump.txttest hook). A higher vblank rate (120) left it at 30: it counts time, not vblanks.
The game installs 3.3 GB of its disc to dev_hdd0/game/BLES00229 on its first
start; see "A title's write budget" above.
Measured from the baseline state (70 s runs, --appendconfig test config,
2026-09-29):
| Build | Resolution | Frames a second (10 s windows) |
|---|---|---|
| no frame-rate patch | 300% (2160p) | 27.9-30.0: the game's own 30 |
| no patch, Vblank Rate 120 | 300% | 27.8-30.0 |
| frame-rate patch | 300% | 50.4-51.5, loading screens 59.8 |
| frame-rate patch | 100% (720p) | 53.4-54.8 |
The game keeps its speed (the audio windows are full), and the resolution
costs little: the limit is the emulation, not the PS5's GPU. RPCS3's
performance overlay puts the guest RSX at 98% busy, while its host CPU
figures read nearly nothing on the console (5.5% PPU, 1.8% SPU: the thread
times do not work there). The title's sampler, every thread sampled (its walk
now bounded by each thread's stack), found one thread busy 85-89% of the time
in recompiled code, most likely an SPU running GTA IV's SPURS work, and five
more at 30-55% with vm::writer_lock their most frequent address, while the
RSX thread ran about half the time. With RPCS3's accurate SPU reservations
(the default, which stays), every SPU atomic write takes that lock, and a
PS5's Zen 2 has no transactional memory to take it faster. That lock and the
busiest SPU are where 60 fps is to be found next.