· via Hacker News – Front Page (native)
God of War PSP titles recompiled to WebAssembly run in the browser without an emulator
A static recompilation project translates the PSP God of War games' MIPS code into WebAssembly, running both titles at up to 60 fps in desktop browsers with an HLE kernel and a WebGL2 renderer instead of an emulator.
Both of Ready at Dawn's God of War PSP titles now run in a web browser with no emulator behind them. According to the project's GitHub repository, snuri00/psp-web-recomp, the games' original MIPS machine code is translated ahead of time into C++, compiled to WebAssembly, and linked against a compact reimplementation of the PSP's operating system and graphics hardware that draws through WebGL2. The work surfaced on the Hacker News front page. God of War: Chains of Olympus 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 native resolution, and on phones with on-screen touch controls. Music, speech and sound effects work; movies are skipped for now.
How the port works
The recompilation is handled by PSPRecomp, which analyses a game's decrypted executable, identifies its functions and emits C++ in translation units of 16 KiB of guest code each; the generated code runs against a register file and a model of the PSP's memory. The project adds patches to PSPRecomp and a new web profile target for it.
Everything the game asks of the PSP's operating system is answered by high-level emulation: cooperative threads with semaphores, event flags and callbacks, memory partitions, a file system that streams disc data over HTTP Range requests so only the executable downloads up front, plus controller, audio, save data and dialog handling. Guest time advances in frames, so a game sees a steady 60 Hz however fast the host machine runs.
On the graphics side, the PSP graphics engine's display lists are decoded on the CPU — vertex formats, skinning, lighting, texture generation, clipping and backface culling — and batched into a WebGL2 renderer. Framebuffers become render targets keyed by their place in VRAM, and behaviours like pixel-format reinterpretation, the PSP's stencil-in-alpha, fog and block transfers are handled on the GPU. Audio comes from a reimplementation of the PSP's 32-voice ADPCM synthesizer, ATRAC3+ decoding via FFmpeg and an AudioWorklet mixer. Mirroring the real hardware, the graphics engine runs on a worker thread with an OffscreenCanvas, so a frame costs whichever thread is busier rather than the sum of both.
From 6 fps to 60
The first playable build ran at 6 frames per second, and the project's write-up is clear that most of the road to 60 came from finding where time actually went, not from making the renderer faster. The biggest single win: God of War swaps its framebuffer without waiting for vertical blank, so the game drew roughly eight frames for every one that reached the screen. Holding a thread that swaps twice within one blank until the next — a trick the PPSSPP emulator also uses — cut the work per displayed frame by a factor of eight on its own.
Several other findings were browser-specific. In WebAssembly, reading the clock through std::chrono crosses into JavaScript via clock_gettime and a BigInt conversion; profiling timers that did this per primitive consumed about a third of a frame until they were changed to read performance.now() only while profiling. Firefox copies every WebGL buffer upload to its GPU process and re-validates an index buffer after any change, so a shared 4 MB index ring dropped Firefox to 3 fps until each draw was given its own small buffer. And mirroring the PSP's stencil-in-alpha channel for God of War's projected shadows initially cost 60 million extra pixels per frame at 4× resolution; tracking which rectangles and stencil values had actually changed brought that down to about 7 million.
A second game proves the pipeline
Ghost of Sparta went from a ZIP archive to a playable page using the same scripts unchanged, then hung at boot. The cause was a 176-byte file carrying the PSP's DRM flag, decrypted through the KIRK crypto engine — in practice AES-128 with three keys from KIRK's vault, a CMAC header check and counter-mode data — which the project has now implemented. A white-sky bug traced back to a cloud layer whose opacity comes from the alpha of the global ambient light, a factor the lighting code had left out. No performance tuning was required: frames cost 6–8 ms and the game runs at 55–60 fps at three times native resolution.
The repository includes no game data; you supply a disc image of a game you own, and the scripts convert it into a web page locally. Porting needs git, CMake, Ninja, a C++20 compiler and Python 3, with roughly 2 GB of disk per game; God of War goes from ZIP to playable page in about four minutes on an 8-core laptop, according to the repository.
Why it matters
Static recompilation turns a closed-source console binary into ordinary web code, avoiding both the per-frame overhead of dynamic emulation and the restrictions browsers place on runtime code generation. It also demonstrates that the modern browser stack — WebAssembly, WebGL2, worker threads, AudioWorklet — can sustain 60 fps handheld-console gameplay on laptops and phones alike. The limits are stated honestly: only two games have been brought up, both Ready at Dawn titles built on the same engine, and a game from another studio will likely stop at an unimplemented system call or an unsupported graphics feature. As a demonstration of the technique, though, it is hard to beat, and the repository ships the tooling for others to attempt further titles.
- #webassembly
- #emulation
- #browser-gaming
- #webgl
- #static-recompilation