God of War on PSP, recompiled to WebAssembly and running in the browser
God of War on PSP: Recompiled for the Web
This project achieves something remarkable: running PSP games directly in a web browser without a traditional emulator. Instead, it utilizes static recompilation to WebAssembly, paired with a High-Level Emulation (HLE) kernel and a WebGL2-based renderer.
🛠️ The Technical Pipeline
The core magic happens by translating the game's original MIPS machine code into C++ ahead of time. This C++ code is then compiled into WebAssembly (Wasm) and linked to a custom implementation of the PSP's hardware and OS.
How it Works
- Analysis:
PSPRecompscans the decrypted executable to identify functions. - Translation: It generates C++ code in chunks of
16 KiBof guest code per translation unit. - Execution: The resulting code operates on a simulated register file and a model of the PSP's memory.
🎮 Performance & Compatibility
The project has successfully implemented two major titles from the God of War series.
| Game | Performance | Resolution | Notes |
|---|---|---|---|
| Chains of Olympus | 60 FPS | Up to native | Full boot, menus, and combat |
| Ghost of Sparta | 55-60 FPS | native | Required DRM decryption & lighting fixes |
Deployment Workflow
To get a game running, the user follows these steps:
- Provide a disc image of a legally owned game.
- Run the repository's scripts.
- Generate a standalone web page on the local machine.
🏗️ System Architecture
The HLE Kernel (profile/host)
The project uses a "web profile" to handle OS requests. Instead of emulating every CPU cycle, it provides high-level answers to system calls:
- Threading: Cooperative threads utilizing callbacks, event flags, and semaphores.
- Memory: Managed memory partitions.
- File System: Data is streamed via
HTTP Range requests, meaning only the executable is downloaded initially. - Input/Output: Handles controllers, save data, audio, and message dialogs.
- Timing: Guest time is locked to frames, ensuring a consistent regardless of the host's speed.
The Graphics Engine (GE)
The rendering pipeline is split between the CPU and GPU:
ge.cpp(CPU): Handles the heavy lifting of decoding vertex formats, skinning, lighting, clipping, and backface culling.ge_gl.cpp(GPU): Takes the batched triangles and renders them via WebGL2.
Key Rendering Feature: Framebuffers are treated as WebGL render targets mapped to VRAM. This allows effects that read/write to textures to remain entirely on the GPU.
Supported Graphics Features:
- (Scalable up to ).
- Pixel format reinterpretation (switching between 16-bit and 32-bit).
- Fog, block transfers, and stencil-in-alpha.
- Parallelism: The GE runs on a worker thread using an
OffscreenCanvas, meaning the CPU and GPU work concurrently rather than sequentially.
Audio Implementation
- SFX: A custom reimplementation of the PSP voice synthesizer (32 ADPCM voices with ADSR envelopes).
- Music/Speech: ATRAC3+ streams decoded via
FFmpeg. - Output: All audio is routed through an
AudioWorklet.
🚀 Lessons in Optimization
The journey from a 6 FPS prototype to a 60 FPS experience involved several critical breakthroughs:
1. The Framebuffer Trap
God of War swaps its framebuffer without waiting for a vertical blank. Without a limit, the game would render frames for every 1 actually displayed. By implementing a trick used by PPSSPP (holding the thread that swaps twice within one blank), the workload was reduced by a factor of eight.
2. Timer Overhead
Using std::chrono in Wasm is expensive because it triggers a clock_gettime call and a JavaScript BigInt conversion.
- Old way: Reading the clock per primitive consumed of the frame time.
- New way: Using
performance.now()only when profiling is active.
3. Browser-Specific Quirks
- Firefox: The browser re-validates the entire buffer if an index buffer is modified. A shared 4 MB ring buffer dropped performance to 3 FPS. Solution: Give every draw call its own small index buffer.
- Mobile: Writing vertex data piece-by-piece into one large buffer caused GPU driver stalls. Solution: Avoid modifying buffers the GPU might still be reading.
4. Stencil Buffer Efficiency
The PSP stores the stencil buffer in the alpha channel. Initially, mirroring these via full-screen passes cost pixels per frame at resolution. This was achieved by tracking only the specific rectangles and stencil bits that actually changed.