logo
Kurashizu Blogwhere ideas flow
  • Home
  • News
  • Blog
  • About
  • Home
  • News
  • Blog
  • About

© 2026 Kurashizu. All rights reserved.

Admin·New Post·Service Status
Powered by Cloudflare·30.10.26
← Back to news

God of War on PSP, recompiled to WebAssembly and running in the browser

Original article
github.com|78 points|40 comments|by sn001|Oct 7, 2026

God of War on PSP: Recompiled for the Web

Project Author

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: PSPRecomp scans the decrypted executable to identify functions.
  • Translation: It generates C++ code in chunks of 16 KiB of 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.

GamePerformanceResolutionNotes
Chains of Olympus60 FPSUp to 4×4\times4× nativeFull boot, menus, and combat
Ghost of Sparta55-60 FPS3×3\times3× nativeRequired 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 60 Hz60\text{ Hz}60 Hz regardless of the host's speed.

The Graphics Engine (GE)

The rendering pipeline is split between the CPU and GPU:

  1. ge.cpp (CPU): Handles the heavy lifting of decoding vertex formats, skinning, lighting, clipping, and backface culling.
  2. 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:

  • Native Resolution=480×272\text{Native Resolution} = 480 \times 272Native Resolution=480×272 (Scalable up to 4×4\times4×).
  • 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 ≈8\approx 8≈8 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 →\rightarrow→ consumed 1/31/31/3 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 60 million60\text{ million}60 million pixels per frame at 4×4\times4× resolution. Optimization: 60M pixels→7M pixels\text{Optimization: } 60\text{M pixels} \rightarrow 7\text{M pixels}Optimization: 60M pixels→7M pixels This was achieved by tracking only the specific rectangles and stencil bits that actually changed.