psp-web-recomp runs PSP God of War in the browser via WebAssembly

psp-web-recomp runs PSP God of War in the browser via WebAssembly

psp-web-recomp is a GitHub project that runs PSP games in the browser without an emulator. The game's MIPS machine code is translated ahead of time into C++, compiled to WebAssembly with Emscripten, and linked against a small reimplementation of the PSP's operating system and graphics chip that draws with WebGL2.

The first title brought up is God of War: Chains of Olympus. According to the README, it plays from boot through menus, cutscenes and combat, at 60 frames per second in the scenes measured so far in Chrome and Firefox on a laptop, at up to four times the PSP's resolution, and on phones with on-screen touch controls. Music, speech and sound effects work; movies are skipped for now. God of War: Ghost of Sparta followed through the same scripts. It needed the PSP's DRM decryption for one small file, a handful of system calls and a lighting fix, and no performance work: it runs at 55 to 60 frames per second at three times the native resolution.

No game data is included. You bring a disc image of a game you own, and the scripts turn it into a web page on your machine. The pipeline has four parts. For recompilation, PSPRecomp (by its contributors, MIT) analyses the decrypted executable, finds its functions and emits C++ in translation units of 16 KiB of guest code each; the project adds a handful of fixes to PSPRecomp and a new web profile target. A small PSP kernel answers the game's operating system requests with high-level emulation: cooperative threads with semaphores, event flags and callbacks, memory partitions, the file system (disc data is streamed over HTTP Range requests, so only the executable is downloaded up front), controller, audio output, save data and message dialogs. Guest time advances in frames, so a game sees a steady 60 Hz. For graphics, the PSP's GE chip is fed display lists that are decoded on the CPU (vertex formats, skinning, lighting, texture generation, clipping, backface culling) and handed as batched triangles to a WebGL layer; framebuffers become WebGL render targets keyed by their place in VRAM, and everything can render at one to four times the native 480×272. The GE and its WebGL context run on a worker thread with an OffscreenCanvas, so a frame costs whichever thread is busier rather than the sum of both. For sound, a reimplementation of the PSP voice synthesizer (32 voices of ADPCM with pitch and ADSR envelopes) handles effects, while music and speech come from ATRAC3+ streams decoded with the FFmpeg decoder, with the mix going to an AudioWorklet.

The write-up is candid about performance. The first playable build ran at 6 frames per second, and most of the way to 60 came from finding where the time went rather than from making the renderer faster. Four fixes stand out. God of War swaps its framebuffer without waiting for the vertical blank, so the game drew around eight frames for every one displayed; holding the swapping thread until the next blank, a trick PPSSPP also uses, cut work per displayed frame by a factor of eight on its own. Profiling timers that read std::chrono per primitive took about a third of the frame in WebAssembly, because the call goes through clock_gettime and a BigInt conversion in JavaScript; they now read performance.now() only when profiling is on. Firefox copies every WebGL buffer upload to its GPU process and re-validates a whole index buffer after any change, so a shared 4 MB index ring dropped Firefox to 3 frames per second; giving every draw a small index buffer of its own fixed it, while writing vertex data piece by piece into one large buffer helped no browser and made phones stall. The PSP keeps stencil in the framebuffer's alpha channel, and mirroring the two with full-screen passes cost 60 million extra pixels per frame at 4×; tracking which rectangles, stencil bits and constants changed brought that to about 7 million. Finally, moving the GE onto its own thread turned the cost of a busy fight in Firefox from game plus graphics, around 17 ms, into the larger of the two, around 10 ms.

Ghost of Sparta went from a ZIP to a page without changes to the scripts or the recompiler, then waited forever at boot. It opens a 176-byte file with the PSP's DRM flag, hands its key to sceIoIoctl and checks what it reads back. The file is in PGD format, decrypted on the PSP by amctrl.prx with the KIRK crypto engine; for disc games this comes down to AES-128 with three keys from KIRK's key vault, a CMAC-based header check and a counter mode for the data. The project implements it in profile/host/pgd.cpp. Next came a white sky and white haze in menus; bisecting the draws of one frame found a cloud layer drawn with lighting on, whose opacity comes from the alpha of the global ambient light, a factor the lighting code had left out. A frame then costs 6 to 8 ms, as in Chains of Olympus.

To build it you need git, CMake, Ninja, a C++20 compiler and Python 3. Everything has been run on Linux; macOS should be able to build the browser version but is untested. Expect about 2 GB of disk space per game. Retail executables are encrypted, so you name a decrypter tool (such as DecEboot or pspdecrypt) through PSP_DECRYPT. The port.sh script extracts the disc, decrypts the executable, translates it to C++, writes the streaming manifest and builds the page in a few minutes; God of War takes about four minutes on an 8-core laptop. The page needs SharedArrayBuffer, so it must be served cross-origin isolated (COOP same-origin, COEP require-corp); the bundled serve.py does this and supports Range requests. Browsers that cannot draw WebGL2 on an OffscreenCanvas fall back to a single thread. Code is MIT-licensed, and the bundled at3_standalone keeps its LGPL 2.1 license. The README says the generated C++ and WebAssembly belong to the game's owners and should not be published, and that the project is not affiliated with or endorsed by Sony or any game publisher.

Key facts

  • psp-web-recomp translates PSP MIPS code ahead of time into C++, builds it to WebAssembly with Emscripten and runs it with a small PSP OS reimplementation and a WebGL2 renderer, with no emulator.
  • God of War: Chains of Olympus plays from boot through combat at 60 fps in the scenes measured so far in Chrome and Firefox on a laptop, at up to four times PSP resolution, and on phones with touch controls; movies are skipped for now.
  • Ghost of Sparta needed only DRM decryption for one 176-byte file, a few system calls and a lighting fix, and runs at 55 to 60 fps at three times native resolution.
  • The first build ran at 6 fps; the biggest gains came from profiling, including a factor of eight from holding the framebuffer-swapping thread until the next vertical blank.
  • No game data ships with the repository: you supply your own disc image, and only two games, both from Ready at Dawn on one engine, have been brought up so far.

Why it matters

Running a console game in a browser usually means running a full emulator. This project takes a different route: it recompiles the game's executable ahead of time and pairs it with a thin layer that stands in for the PSP's operating system and graphics chip. The result reaches 60 fps for Chains of Olympus on a laptop in Chrome and Firefox, and the README reports it also works on phones. The write-up is also a practical log of WebAssembly and WebGL pitfalls: BigInt-heavy clock reads, Firefox's index buffer validation, mobile driver stalls when a live buffer is modified, and stencil-in-alpha emulation.

Who it affects

Developers interested in static recompilation, console preservation and browser graphics will find the most here, as will anyone porting a native renderer to WebGL2 and WebAssembly. Players need to be comfortable building from source: the workflow involves a C++20 toolchain, CMake, Ninja, Python 3, the Emscripten SDK and a decrypter tool for the retail executable. Players with a legally owned disc image of a supported game are the intended audience.

How to use it

Clone the repository, run scripts/setup.sh (it fetches PSPRecomp, the patches and the Emscripten SDK), then run PSP_DECRYPT=/path/to/decrypter scripts/port.sh mygame "My Game.iso" and finally scripts/serve.sh mygame, which serves the page at http://localhost:8613/. The disc can be an ISO, a ZIP containing one, or an extracted folder. Adding --native builds a headless runner that dumps frames to images and records audio, the quickest way to see how far a new game gets. Appending ?profile to the page URL adds a per-frame timing breakdown, and ?threads=0 forces single-thread mode. Expect about 2 GB of disk per game. No hosted live demo is offered; the page has to be built locally from your own disc image. Code is under the MIT License.

How solid is it

The numbers come from the project's own README and are measured by its author or authors in scenes "so far", on a laptop in Chrome and Firefox; no hardware or browser versions are given. A recording of 1:46 is linked from the page. The performance story is specific and internally consistent (6 fps first build, 6 to 8 ms per frame at the end). Two games have been brought up, both Ready at Dawn titles on the same engine.

Risks and caveats

Compatibility is narrow. The README says two games work, both from one studio on one engine, so they say little about games from other studios; another game will most likely stop at an unimplemented system call or use a GE feature the renderer does not handle. Movies are skipped for now. Everything has been run on Linux; macOS should work for the browser build but is untested. The page must be served cross-origin isolated because it uses SharedArrayBuffer. On legal points, the README says the generated C++ and WebAssembly belong to the game's owners and should stay on your own machine, not be published, and that God of War is a Sony Interactive Entertainment trademark with no affiliation or endorsement from Sony. If you expose the page through a tunnel to test on a phone, anyone with the address can load the game while the tunnel runs.

“Most of the way to 60 came from finding out where the time actually went rather than from making the renderer faster.”

— psp-web-recomp README