The same picture as before, cleaned the same way, scaled down to 3840x2160 instead of up from a 2000-pixel copy.
Mupen64Plus PS5
mupen64plus-core 2.6.0, the Nintendo 64 emulator, as a native home-screen app for a jailbroken PS5: a game
shelf with covers, DualSense support and save states. The release is the app folder itself (eboot.bin and
its files): no payload to send.
../mupen64plus-coreis the original core source, with one change: ROMs up to 252 MB (ROM hacks bigger than a 64 MB cartridge):src/device/memory/memory.handsrc/device/cart/cart_rom.{c,h}, markedMupen64Plus PS5.- Everything that makes it a PS5 app is in this folder:
n64/: the core's PS5 side and its built-in plugins;coreorbis/: the PS5 layer;frontend/: the shelf and menus;proto/native/: the eboot builder and signer;installer/: the helper the app carries, and the optional installer payload.
The PS5 layer and the build come from Snes9x PS5 (../../snes9xPS5-2.0/ps5), which follows the PS5SX2
model.
0.6.3: in-app updates work. Up to 0.6.2 the updater wrote the new files without execute permission, so the console refused to start the updated app ("Can't start the game or app", CE-107750-0). 0.6.3 unpacks the update next to the app folder with the permissions an FTP copy gets, closes, and its helper swaps the folders so ShadowMountPlus installs the app again. The release zip is now
Mupen64PlusPS5.zip, so 0.6.0–0.6.2 never offer an update they would break: install 0.6.3 by hand once.0.6: released as the app folder (
Mupen64PlusPS5.zip): copyPPSA99064/to/data/homebrew/or a USB drive'shomebrew/, and ShadowMountPlus lists it. No.elfto send: eboot.bin starts the helper it carries through the ELF loader by itself, and now prefers it over etaHEN's daemon. The app updates itself from the GitHub releases (Updates).0.5.2: rumble: the DualSense is switched to its compatible vibration mode (its default haptics mode ignores the classic rumble call); boot.log now shows the Rumble Pak commands, the vibration calls, and each game's save files when it starts and stops.
0.5.1: fixes a crash when a second game (or the same one again) is started in the same session with the dynarec: the code cache lost its execute permission when the first game ended.
Status (0.5): the dynarec runs on the console (
mprotect RWX works): GoldenEye, Rogue Squadron and Donkey Kong 64 hold 59-61 VI/s. 0.5 lowers the audio latency from ~120 ms to ~67 ms (rate control with an integral term, and a target that follows the game's buffer size, up to 125 ms for games with big buffers).0.4: runs on the console (Super Mario 64 at full speed; GoldenEye dipped to 75–90% with the cached interpreter, where the CPU took ~50% of the time and the RSP ~30%). 0.4 adds the x86-64 dynarec: on the host it cuts the CPU's share from ~30% to ~3%. Whether the PS5 lets the app run generated code is tested at start-up (
[emu] dynarec:inboot.log); if not, the cached interpreter runs as before.0.3: the per-part timing of the FPS counter uses the CPU's cycle counter (0.2's clock_gettime, a system call thousands of times per frame, slowed games down).
0.1: it builds (
make ps5), and the same code runs in the Linux host build with the PS5 calls simulated. There, F-Zero X (USA) boots, plays (menus, machine select, a race), saves its SRAM, makes and loads save states, and quits cleanly, all at full speed. The included test ROM checks the CPU, RDP, VI and audio paths. It has not run on a PS5 yet. If something fails there,/data/mupen64plus/logs/boot.logsays where.
How the emulator is put together
mupen64plus is a core plus four plugins (RSP, video, audio, input). On the desktop these are separate
libraries; here everything is linked into eboot.bin. n64/core/osal_dynlib_static.c replaces dlopen/dlsym
with tables of functions looked up by name.
| Part | What runs it on the PS5 |
|---|---|
| CPU (VR4300) | mupen64plus-core's x86-64 new dynarec when the console gives executable memory (n64/core/jit_ps5.c), else its cached interpreter |
| RSP | mupen64plus-rsp-hle for audio, MP3 and JPEG tasks (n64/plugins/rsp_hle_ps5.c), mupen64plus-rsp-cxd4 for the rest: low level, it runs the game's own microcode, graphics included (n64/plugins/rsp_cxd4_ps5.c) |
| RDP + VI | paraLLEl-RDP on the GPU through Vulkan (RADV), at 1x-8x internal resolution (n64/plugins/gfx_parallel_ps5.cpp, default), or angrylion-rdp-plus: software, pixel-accurate, on 6 CPU threads (n64/plugins/gfx_ps5.c) |
| Audio | n64/plugins/audio_ps5.cpp: AI samples resampled to 48 kHz, into libSceAudioOut |
| Input | n64/plugins/input_ps5.cpp: libScePad, rumble through scePadSetVibration |
| Picture | ProsperoVideo: the renderer's frame scaled (4:3 / integer / stretch, sharp or smooth) and tiled into libSceVideoOut |
The GPU renderer. PS5 homebrew has a Vulkan driver: RADV, Mesa's driver for AMD GPUs, as
mihawk-99/PS5_Vulkan builds it for the console; the app links it in (eboot.bin grows from ~4 MB to ~30 MB) and
runs a short self-test at start (coreorbis/orbis-shims/ProsperoVulkan.h, results in boot.log).
paraLLEl-RDP, the renderer RetroArch uses, runs the N64's RDP commands as compute shaders, reading and writing
the emulator's RDRAM directly (VK_EXT_external_memory_host), and draws at up to 8x the N64's resolution. The
picture is read back and shown like angrylion's. Its compiled shaders are kept in
/data/mupen64plus/cache/parallel-rdp-pipelines.bin. If Vulkan doesn't work, the app falls back to angrylion,
which renders on the CPU at the N64's own resolution.
Shaders shipped with the app. Every PS5 has the same GPU and runs the same RADV, built into eboot.bin, so
one console's compiled shaders suit the others. n64/plugins/assets/parallel-rdp-pipelines.bin, when present,
is a copy of a console's cache/parallel-rdp-pipelines.bin and goes into the app folder as
assets/parallel-rdp-pipelines.bin. At the first start of a new version, the app merges it into the player's own
cache (cache/parallel-rdp-pipelines.seed remembers it), so nobody waits for those compiles. A cache made by
another driver (its UUID differs) is left out. To refresh it, copy the cache from a console that has played
many games, after a build that changed RADV or paraLLEl-RDP.
eboot.bin stays under 32 MiB. The in-app updater of 0.6.3 to 0.7.2 refuses a file over 32 MiB, so those
versions could never update to a bigger eboot.bin; the build stops if it gets there. Large data goes into the
app folder as files of its own.
What the core needs from SDL, libpng and dlopen is replaced:
n64/sdl/provides threads, mutexes and the clock on pthreads.n64/core/vidext_ps5.c,eventloop_ps5.candscreenshot_ps5.cstand in for the files that need a real SDL, OpenGL or libpng.
Frame pacing (frontend/fe_emu.cpp):
- The game runs inside
CoreDoCommand(M64CMD_EXECUTE), and once per video interrupt the video plugin hands over the picture. - A presenter thread scales and flips it while the next frame is emulated.
- NTSC games are paced by the TV's 60 Hz flips (the core's speed limiter is off). The audio plugin stretches the sound by up to 0.5% to stay in step.
- PAL games keep the core's limiter.
Requirements
- A jailbroken PS5 with kstuff (or kstuff-lite) and an ELF loader on port 9021 (elfldr, etaHEN, or the one PS5 Payload Manager uses).
- ShadowMountPlus, so the icon appears on the home screen.
- Your own N64 ROMs (
.z64 .n64 .v64 .rom, or zipped). No games are included.
Install and play
-
Copy the
PPSA99064folder fromMupen64PlusPS5.zip(the folder itself, not just its contents) to one of:/data/homebrew/PPSA99064on the console (FTP, PS5Upload...);homebrew/PPSA99064on an exFAT USB drive.
ShadowMountPlus adds the icon to the home screen within its next scan (15 s by default). To update, close the app and copy the new folder over the old one.
-
Open the Mupen64Plus PS5 icon.
- The app sends the helper built into eboot.bin to the ELF loader (127.0.0.1:9021). The helper listens on
127.0.0.1:9064, lets only PPSA99064 out of its sandbox (
/data, USB drives, executable memory for the dynarec), and stays running until the console is turned off. - If no ELF loader answers, the app asks etaHEN (9028) or a 9069 jailbreak daemon instead.
- Optional:
Mupen64PS5-v<version>.elf, sent to port 9021, installs the same folder and starts the helper (the way before 0.6).
- The app sends the helper built into eboot.bin to the ELF loader (127.0.0.1:9021). The helper listens on
127.0.0.1:9064, lets only PPSA99064 out of its sandbox (
-
Copy your ROMs to
/data/mupen64plus/roms(over FTP, for example), or to amupen64plus/romsfolder on a USB drive. Subfolders work.
Title ID PPSA99064 and the folders below don't overlap with Snes9x PS5 (PPSA99009) or PS5SX2 (PPSA99203).
| Folder | Contents |
|---|---|
/data/mupen64plus/roms |
your games |
/data/mupen64plus/saves |
in-game saves (EEPROM, SRAM, FlashRAM, Controller Pak), named <GoodName>-<MD5> by the core |
/data/mupen64plus/states |
save states, <rom file>.st0 to .st9 |
/data/mupen64plus/config |
mupen64plus.cfg, the core's own settings (advanced) |
/data/mupen64plus/data |
mupen64plus.ini, the core's ROM catalog (save types, per-game fixes), written by the app |
/data/mupen64plus/covers |
downloaded covers and your own (<rom file name>.png / .jpg) |
/data/mupen64plus/logs |
boot.log, installer.log, helper.log and the 4 sessions before as .prev.log (the last) to .prev4.log |
/data/mupen64plus/mupen64plus-ps5.ini |
the menu settings |
Controls
In a game (the default layout: buttons by position, as on the N64 pad; Settings → Controller layout changes it, for every game or, from the pause menu, for one game)
| PS5 | N64 |
|---|---|
| left stick | analog stick (dead zone adjustable) |
| D-pad | D-pad |
| Cross / Square | A / B |
| L2 or R2 | Z |
| L1 / R1 | L / R |
| right stick | C buttons |
| Triangle / Circle | C-up / C-down |
| OPTIONS | Start |
| touchpad click | pause menu |
| R3 (press the right stick) | save the state in the current slot |
| L3 (press the left stick) | load the state of the current slot |
The pause menu (the touchpad) is a panel over the paused game, drawn on the CPU with the anti-aliased shapes
of frontend/fe_draw.cpp; Settings on the game list and the Controller Layout and Cheats pages share its look
(fe_menu.cpp, namespace pm). Its footer's buttons work anywhere in it: touchpad resume, Square reset the
game (after a question), Triangle back to the game list, Cross confirm, Circle back (resume). Its rows:
- save state and load state (Cross), and the state slot (0–9);
- cheats: the GameShark codes of the core's
data/mupencheat.txt(about 560 game versions, found by the ROM header's CRCs and country; built intoeboot.bincompressed,frontend/fe_cheats.cpp). Each one is off or on, or off or one of its choices, with the database's note beside it; the ones left on are on again the next time (cheats/<ROM file>.txt). Codes that need the GameShark's button are left out; - speed (normal or fast forward);
- every setting below but the game list's (Continue where you left off, Game list view, Download covers, Check for updates: Settings on the game list has them);
- quit (Cross).
Settings (marked * when they apply from the next game start):
| Setting | |
|---|---|
| Aspect ratio | 4:3 (default), integer scale, stretch 16:9 |
| Smooth scaling | bilinear (on) or sharp pixels |
| N64 video filter * | the N64's own VI filter (anti-aliasing, dither filter) |
| Hide overscan * | crops the black border the N64 draws around the picture |
| Renderer * | GPU (paraLLEl-RDP, default) or CPU (angrylion, pixel-accurate at the N64's resolution) |
| Internal resolution * | GPU renderer: 1x (native), 2x, 4x (default), 8x |
| GPU sync * | GPU renderer: Accurate (default: waits for the GPU wherever the game waits for the RDP) or Fast (may glitch) |
| CPU core * | Dynarec (default, when the console allows it) or Interpreter (slower; for a game the dynarec gets wrong) |
| CPU overclock * | Off (default), 2x, 4x: the N64's CPU gets 2 or 4 times the cycles per frame (the core's CountPerOpDenomPot), so games that slowed down on the console (GoldenEye, Perfect Dark, Conker) run smoother. A few games misbehave with it |
| Audio processing * | Fast (HLE, default): rsp-hle does the RSP's audio work; Accurate (LLE): cxd4 runs the game's audio microcode (slower; for a game whose sound HLE gets wrong) |
| Render threads * | CPU renderer (angrylion) workers: 1–12, default 6 |
| Controller pak * | Controller Pak (default), Rumble Pak, none |
| Stick dead zone | 0–30% |
| Invert left stick | turns the analog stick's X (left/right), Y (up/down) or both round; Off by default |
| Invert right stick | the same for the right stick's C buttons (Triangle / Circle stay C-up / C-down); Off by default |
| Controller layout | what each DualSense control presses on the N64 pad: Cross, Circle, Square, Triangle, L1, R1, L2, R2 (separately), OPTIONS, the D-pad and the right stick's four directions, each set to an N64 button or nothing (the left stick stays the analog stick). From the game list it is every game's layout (in mupen64plus-ps5.ini); from the pause menu, the running game's own (layouts/<ROM file>.txt, gone again when it matches every game's) |
| Continue where you left off | Ask (default): leaving a game from the pause menu saves where you were (states/<ROM file>.resume), and starting it again asks "Continue where you left off?" (✕ continue, □ start over, ○ back). Off: neither |
| Game list view | Shelf (3D, default), Grid or List; the touchpad on the game list switches too |
| Show FPS | the speed (VI/s, 100% = full speed) and, below it, where the emulation thread's time goes: cpu (VR4300 interpreter), rsp (rsp-hle + cxd4), rdp (the renderer: angrylion's rasteriser incl. waiting for its threads, or handing commands to the GPU), vi (the VI filter, or reading the GPU's picture back), idle (waiting for the TV: spare time). Also logged every 10 s in boot.log |
| Volume | the game's sound, 0–100% (default 100%) |
| Sound, Download covers | |
| Check for updates | ask at start-up when a newer GitHub release exists (see Updates) |
On the game list (three views: the 3D shelf, a grid of covers, a list of titles with the cover beside it):
- D-pad: choose a game (shelf: Left/Right; grid: all four directions; list: Up/Down)
- L1/R1: 10 games back or on (shelf), a page up or down (grid, list)
- Cross: play (a game you left from the pause menu asks whether to continue there)
- Touchpad: the next view (remembered; also Settings → Game list view)
- Triangle: settings
- Square: fetch the cover again
- OPTIONS: quit
Updates
At every start, before it asks for /data (the console's HTTPS works only then, as for the covers), the app asks
api.github.com for the latest release of TheRealRetro/mupen64plus-ps5 (GITHUB_REPO in the Makefile).
- When the release's tag is newer than the app's
contentVersion, it downloads the release'sMupen64PlusPS5.zipand checks its size and SHA-256 against the release (frontend/fe_update.cpp). - Once
/datais visible, it shows "Mupen64Plus PS5 X is available", with the release notes:- ✕ unpacks the new version next to each copy of the app folder it finds (
/data/homebrew/PPSA99064, a USB drive'shomebrew/PPSA99064...), in.mupen64plus-update/new/, with the permissions an FTP copy gets (0777: the console doesn't start aneboot.binwithout execute permission), writes a job file, starts its helper through the ELF loader and closes. The helper waits for the app to exit, moves the app folder aside, asks ShadowMountPlus to rescan (its API, 127.0.0.1:10101) until it has dropped the app, moves the new version in, rescans until it is installed again, and says so in a notification (coreorbis/orbis-shims/ProsperoUpdateJob.cpp; its steps go tologs/helper.log); - ○ carries on (asked again next start).
- ✕ unpacks the new version next to each copy of the app folder it finds (
- A release that was installed but didn't take (the next start still ran an older eboot) isn't offered
again (
/data/mupen64plus/update/installed.txt). - Settings → Check for updates: No skips the question.
- An app running from a
.ffpfscimage can't be updated this way (read-only): copy the new folder by hand. boot.logshows every step ([update]lines).
Game names and covers
- Names: a ROM's name comes from its file name when that is already a No-Intro name ("F-Zero X
(USA).z64"). Otherwise it comes from the CRC in its cartridge header, looked up in a table built from the
core's
mupen64plus.ini(1891 games,tools/make_n64db.py). Its GoodN64 names are matched against libretro-thumbnails' file list (tools/libretro-n64-boxarts.txt) and take their spelling there, language tags included ("Diddy Kong Racing (USA) (En,Fr) (Rev 1)"). - Covers: they come from libretro-thumbnails (Nintendo 64,
Named_Boxarts), downloaded at start-up, before the app asks for/data(HTTPS works only then). When the game list has new games without covers, the app closes and its helper starts it again (sceLncUtilLaunchApp), so the next start fetches them; if it can't, the covers come the next time you open the app. - When a cover doesn't download: the game isn't on libretro-thumbnails under that name. Name the ROM file the
No-Intro way, or put your own
.pngincovers/.
Known limitations
- The dynarec on the console is new in 0.4. The app tests at start-up whether it may run generated code:
first
mprotect, then executable direct memory mapped over the code cache (what RetroArch's PS5 port uses for its recompilers).boot.logsays which worked. If a game misbehaves with it, set CPU core to Interpreter. - The RSP is the next bottleneck. cxd4 interprets every graphics microcode instruction (GoldenEye: ~30% of the time); audio goes through rsp-hle (Audio processing: Fast).
- Unsupported: 64DD, the Transfer Pak, screenshots, netplay.
- PAL games are paced by the core's millisecond limiter, so motion is less smooth than NTSC.
Building
Requirements:
- the ps5-payload-dev SDK, v0.42 or newer;
- clang and lld 18, g++, glslang (
glslangValidator), and python3 (plus Pillow and numpy only to redraw the art); - for the GPU renderer, mihawk-99/PS5_Vulkan with its RADV archive
built: check it out (
PS5_VULKAN, default/root/gpu/PS5_Vulkan), run itstools/setup-native-dependencies.sh, thentools/build-radv.sh release(meson 1.12 or newer). Without it, build withVULKAN=0(build-native.bat Cpu): the same app with the CPU renderer only, inbuild-native-cpu/.
zlib is vendored.
From Windows, as the Homebrew Browser's build (WSL Ubuntu-24.04, the SDK of /root/ps5-native):
build-native.bat app folder + zip
build-native.bat Ffpfsc also a compressed .ffpfsc image
build-native.bat Clean from scratch (release.bat always does this)
build-native.bat Cpu without the GPU renderer, in build-native-cpu/
Output in build-native/ (next to ps5/):
PPSA99064/: the app folder to copy to/data/homebrew/.Mupen64PlusPS5.zip: the release artifact (just the folder). Its tag is thecontentVersion, made fromVERSIONin the Makefile (0.6.0 ->00.006.000); a copy is kept inrelease-assets/<contentVersion>/.PPSA99064.debug.elf: the program with its symbols, for reading crash reports.PPSA99064.ffpfsc: withFfpfsc, a compressed image ShadowMountPlus can mount.
By hand:
export PS5_PAYLOAD_SDK=/opt/ps5-payload-sdk
cd ps5
make native -j$(nproc) # ../build-native/ as above (FORMAT=Ffpfsc for the image; VULKAN=0: no GPU renderer)
make ps5 -j$(nproc) # build/ps5/Mupen64PS5.elf (installer + helper, with the app inside)
make send PS5_HOST=192.168.0.10 # sends it to the ELF loader (port 9021)
make dist # make native + build/dist/Mupen64PS5-v<version>.elf (optional installer)
# + the source zip
make app # only build/app/PPSA99064/, to copy by hand
make host # Linux build: build/host/mupen64plus-ps5-host [rom]
Host build and test ROM. tests/make_test_rom.py out.z64 writes a small N64 program that contains no
Nintendo code. It draws colour bars, sends the RDP a fill rectangle and plays a 440 Hz tone. Run it, or any
ROM, on Linux:
N64PS5_PS5_ROOT=/tmp/n64 N64PS5_HOST_DUMP=150 N64PS5_HOST_MAX_FLIPS=200 build/host/mupen64plus-ps5-host test.z64
The run writes flip 150 to a .ppm file. N64PS5_HOST_PAD="frame:buttons;..." scripts the pad (see
host/sce_host.cpp).
Dynarec. new_dynarec.c is built with -Dmprotect=n64ps5_jit_mprotect (no change to the core's source),
its NASM stubs are translated to GNU syntax in n64/core/linkage_x64.S, and the struct offsets they need are
generated from the core's asm_defines.c with its own tools/gen_asm_script.sh. On the host,
N64PS5_JIT=direct exercises the PS5's direct-memory way (an anonymous RWX mapping stands in for it).
Art. The home screen's icon and background are pictures in app/art/ (icon.png, background.png).
tools/make_art.py app/sce_sys --images app/art/icon.png app/art/background.png fits them to icon0.png
(512x512) and background-source.png (3840x2160) and writes the BC7 pic0.dds/pic1.dds; without
--images it draws the old art itself.
Source layout
n64/plugins/: the four plugins.rsp_cxd4_ps5.cbuilds cxd4 as one unit with its exports renamed;rsp_hle_ps5.cputs rsp-hle's core in front of it (what rsp-hle doesn't know, graphics above all, goes to cxd4).gfx_ps5.cis angrylion's plugin glue without OpenGL.audio_ps5.cppandinput_ps5.cpp.
n64/core/: the core's PS5 side.- the static
osal_dynliband the core's own function table; vidext,eventloopandscreenshotreplacements.
- the static
n64/sdl/: the bit of SDL2 the core uses, on pthreads.n64/n64_bridge.h: what the plugins and the frontend say to each other.frontend/fe_emu.cpp: core start-up, ROM loading (zip included), plugin attach, the per-VI hook, the presenter thread and save states.frontend/(everything else): the shelf, covers, the game library (fe_games.cpp, N64 header CRCs) and the menus. These come from Snes9x PS5.coreorbis/: the PS5 layer from Snes9x PS5: VideoOut (with the N64 scaler), AudioOut, pads (with rumble), the sandbox request, install, crash log, notifications and paths.third_party/: angrylion-rdp-plus (9c8b9ed), mupen64plus-rsp-cxd4 (00906a9) and mupen64plus-rsp-hle (2.6.0,src/unmodified, minusplugin.cand the dlopen osal), plus zlib. Two changes are markedM64PS5_*:- cxd4 doesn't probe RDRAM with SIGSEGV;
- angrylion's workers get 1 MiB stacks.
License and credits
- mupen64plus-core: GPL-2.0-or-later. The app as a whole is therefore distributed under the GPL (v3, because of the native tooling below).
- angrylion-rdp-plus: MAME license (non-commercial).
gfx_ps5.creplaces its GPL-2.0+ mupen64plus glue. - mupen64plus-rsp-cxd4: CC0.
- mupen64plus-rsp-hle: GPL-2.0-or-later (
rsp_hle_ps5.c, which builds on it, too). - New code in
ps5/n64and the PS5 layer: MIT. - Snes9x PS5 (github.com/MisterTemaki): the PS5 layer, the frontend and the build. PS5SX2 (Spyros): the model they follow.
- ps5-payload-dev SDK (John Törnblom, GPLv3+) and ps5-native-app-boilerplate (BlackBearReloaded,
GPL-3.0-or-later): the toolchain,
ps5-native-tool,app_crt.cppand thelibc.prxgenerator. - zlib, stb, and the UI fonts Roboto, PromptFont and Font Awesome Brands: see
frontend/assets/fonts/. - Covers: libretro-thumbnails, downloaded on the console, not included.