Open-source project
snuri00/ps2-web-recomp avatar
snuri00/ps2-web-recomp

PS2 Web Recomp: recompiling PlayStation 2 games to WebAssembly

PS2 games in the browser: static recompilation to WebAssembly with a native runtime

1,249 stars106 forksC++MIT

At a glance

What is it?
PS2 Web Recomp translates PlayStation 2 game code ahead of time into C++ and compiles it to WebAssembly, while a native runtime emulates the rest of the console and draws with WebGPU. It is a research-scale project with two games brought up so far, and it is not a way to play games you do not own.
Who is it for?
Adopt PS2 Web Recomp if you are studying static recompilation, writing a console emulator, or you own a disc image of one of the two supported games and want it running in a browser tab. Do not adopt it expecting a general PlayStation 2 emulator, since the porting work is per game and no game data ships with the repository.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 4 days ago.
What is it written in?
Mainly C++, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The main CPU is recompiled, everything else is emulated

The idea that makes this project unusual is where the line between recompilation and emulation is drawn. A PlayStation 2 game's Emotion Engine machine code is translated ahead of time into C++ and compiled to WebAssembly, so the main CPU is never interpreted. Everything else, the console's operating system and its hardware, is handled by a runtime that draws with WebGPU.

That split is a response to a specific problem with running a console in a browser. An interpreter on the main CPU is slow, and a browser tab on a laptop is not a console. A software rasterizer for the Graphics Synthesizer, which is what a straightforward emulator would use, is slower still, and the project documents where that road ends.

The results are specific and worth quoting from the project itself. Gran Turismo 4 plays from boot through the opening movie, the menus and races, with music and sound effects, and saves to a memory card kept in the browser. A race runs at full speed, 50 frames per second for the PAL release, in Chrome on a laptop's integrated GPU at twice the PS2's resolution. God of War II plays from the menus through its cutscenes into combat and holds 60 frames per second in heavy combat at the PS2's resolution in Chrome, dropping to 48 to 60 at twice the resolution, limited by the integrated GPU.

The scale of the achievement is easier to appreciate against the choice of game. Gran Turismo 4 was picked because it is among the most demanding titles on the system: it relies on both vector units, on exact floating point behaviour for its replays and license tests, and on graphics tricks that read rendered images back as palettes.

The project is the sibling of psp-web-recomp and follows the same method. It is research and preservation work, and it carries an MIT licence.

eerecomp walks control flow and emits 16 KiB units of C++

The recompiler is the part that does the unusual work. eerecomp reads the game's executable, follows its control flow to find the functions, and emits C++ for them in units of 16 KiB of guest code. The output is not a generic translation. It keeps the R5900's quirks, which is the entire point, because a game that depends on exact floating point behaviour and on pipeline timing will not survive a translation that normalises those away.

The preserved behaviours are listed. There are 128-bit registers and the multimedia instructions that work on them, branch delay slots, the PS2's floating point unit with no infinities or denormals and its own rounding behaviour, and vector unit 0 used as a coprocessor. Games schedule their code around the pipeline delays of the hardware, so a version that treated them as ordinary instructions would misbehave in ways that are hard to diagnose.

Two structural decisions follow from how a console kernel actually works. Jumps between units become tail calls, and a thread switch unwinds the host stack and resumes at the guest address, so the generated code never needs a stack per guest thread. Both of these are the sort of detail that only shows up when you compile a real game, and they are the reason the recompiler produces units of a fixed size rather than one function per translation unit.

The top level of the repository shows where the code lives: recomp for the recompiler, runtime for the emulated console, host for the host side, web for the page, scripts for the per-game porting work, plus CMakeLists.txt and a cmake directory for the build.

A small PS2 kernel in C++, and an IOP running the game's own modules

The operating system side is answered by high-level emulation rather than by a faithful kernel. The EE side provides threads, semaphores, interrupt handlers for the DMA controller and the vertical blank, alarms, and the SIF channel that connects the main CPU to the I/O processor.

The I/O processor is where the design gets interesting, because it runs the game's own code. The IOP executes the game's IRX modules, the sound driver, the disc streaming code and the controller and memory card drivers that ship on the disc, on an interpreter of its R3000 CPU. The IOP kernel and the libraries those modules call are reimplemented in C++.

So the division of labour is: the Emotion Engine is recompiled ahead of time, the IOP is interpreted, and the IOP kernel underneath it is native code. That sounds arbitrary until you look at the workload. The IOP work is interrupt-driven and mostly about moving data, while the EE work is the game logic and the graphics setup, which is exactly the part where interpretation hurts most.

The disc is read over HTTP Range requests, so only the executable is downloaded up front. For a game image measured in gigabytes, that is the difference between a page that loads and a page that does not, and it is also why the browser can host the whole thing from a static file server.

Audio follows the same pattern. The SPU2 is reimplemented with its 48 voices of ADPCM, ADSR envelopes and the streaming DMA games use for music, and the output goes to an AudioWorklet through shared memory. Movies are decoded by an implementation of the IPU, the PS2's MPEG-2 decoder.

Vector unit 1 and the Graphics Synthesizer on the GPU

Two hardware pieces dominate the work, and the project handles both in a way that is specific to PlayStation 2 rather than generic.

Most of a game's 3D work happens in microprograms the game uploads to vector unit 1. Those run with the pipeline delays of the MAC, status and clip flags modelled, because games schedule their code around them, and with the PS2's float rules for special values: no infinities or NaNs, denormals flushed to zero and overflow clamped to the largest float. The arithmetic itself is the host's, so a result can differ from the console's in its last bit, since the console truncates where the host rounds and its multiplier is not exact. Microprograms seen during a test run are compiled ahead of time into SIMD C++, and any the page meets for the first time are translated into a WebAssembly module on the spot, which takes a few milliseconds per program.

The Graphics Synthesizer's 4 MB of video memory lives on the GPU, in its native swizzled layouts, and drawing is done by WebGPU compute shaders. Triangles are binned into tiles and rasterized with the GS's own rules for texturing, palettes, alpha blending, depth and destination alpha tests. Because everything stays in GS memory, effects that render into a texture or reinterpret a framebuffer as a palette work the way they do on the console. The internal resolution can be one, two or three times the native one, and a software rasterizer of the same GS remains in the tree as the reference the GPU output is compared against.

That last detail is the mark of a serious project. Keeping a software rasterizer around as a correctness reference is what makes it possible to say the GPU path matches the console rather than merely looks right.

The browser was the bottleneck, not the emulation

The performance notes read as a debugging diary, and the interesting part is where the time was going. The first race ran at 3 frames per second natively, with a software rasterizer and an interpreter for vector unit 1. Moving the Graphics Synthesizer to WebGPU compute shaders and compiling the vector unit programs brought it to about 70 natively, and most of the remaining work turned out to be about where the browser spent its time rather than the emulation.

Three specific fixes stand out. The first version of the GPU renderer submitted around 650 compute dispatches per frame, because every texture upload and palette load split the batch of triangles around it. Hoisting uploads ahead of the triangles that do not depend on them, with the same output, brought that to about 80.

Gran Turismo 4 uploads the same 3 MB of textures every frame of a race. GS memory is swapped between frames, so the uploads cannot simply be skipped, but a 16 MB cache of recently uploaded contents on the GPU lets most of them be served without crossing from WebAssembly to the GPU again, which cut the data sent per frame by about 60 percent.

At three times the native resolution the frame rate dropped to 23. Profiling the shader showed that the per-triangle setup was being loaded after a branch that differed between threads, which made the whole setup divergent, and that uploads split into 1 MB pieces blocked Chrome's page thread. Loading the setup uniformly and writing each frame's data in one piece brought three times the resolution to 48 to 50 frames per second on the same integrated GPU.

Browser scheduling mattered too. Nested setTimeout calls are clamped to 4 ms, so the emulation loop now yields through a MessageChannel instead, and moving the emulation into a worker, so that the page thread only feeds the GPU, steadied a race that had wandered between 40 and 50 frames per second at a constant 50.

This is the most transferable part of the project. The emulated hardware was never the hard part by the end; the scheduling of a browser thread was.

Bringing up a second game meant fixing the first game's blind spots

The second title, God of War II, went from an ISO to a page through port.sh with only generic changes. It needed no game-specific code, but it found two bugs that Gran Turismo 4 had never triggered, one in the recompiler and one in the vector unit timing, and a few missing pieces of the PS2's input/output processor.

That sentence is the honest measure of this project. One title working tells you that one title works. A second title needing no bespoke code but exposing two real bugs tells you the abstraction is right and the implementation was not yet complete.

No game data is included in the repository. You bring a disc image of a game you own, and the scripts in the repository turn it into a web page on your machine. That is the licensing position stated plainly, and it is the reason the repository ships a porting pipeline rather than a set of prebuilt games.

The runtime architecture is threaded in a way that mirrors the console. The emulation runs in a worker, vector unit 1 has a worker of its own the way it runs in parallel on the console, and the GS commands go through a ring buffer to the page thread, which owns the WebGPU device.

The repository is not archived and the last push was on 2026-10-19, so work is landing on the default branch. There are no GitHub releases, so there is no tag to pin and no release notes to read, and the CHANGELOG-style history that would normally tell you what broke lives outside the release system. The MIT licence is permissive and carries no copyleft obligation for a modified build.

What a new game costs, and what the documentation does not tell you

The project is incomplete by design, and the gaps are worth spelling out before anyone plans around it.

There is no documented install procedure and no documented build command for the host code. The README describes what the scripts do with a disc image you already own, and the repository layout with its CMakeLists.txt and cmake directory implies a native build, but the steps for producing a runnable page are not written down in the project description. Anyone picking this up should read the scripts directory directly.

There is no game data and no list of supported titles beyond the two described. The emulation of the console is only as complete as the games that have exercised it, and the two titles here are a heavy rasterisation game and an action game. A title that uses a different video mode, a different peripheral, or a disc layout the streaming code does not handle will need work, and there is no catalogue of known gaps.

Floating point is a soft spot by construction. The vector unit arithmetic runs on the host, so a result can differ from the console in its last bit, the console truncating where the host rounds, with a multiplier that is not exact. Games that check licence data or replay determinism may be sensitive to that, which is presumably why Gran Turismo 4 was considered a demanding test rather than an easy win.

The practical alternative is a dynamic PlayStation 2 emulator, which runs the whole console in an interpreter and needs no per-game porting. The trade is the one the whole project is about. An emulator works for any disc image with no modification, while a recompiled page can run at console speed or better in a browser, and does so with the game running on WebGPU. For a game you own and want to play in a browser tab, only one of those two is likely to be satisfying, and that is the trade this repository makes deliberately.

Editorial conclusion

Adopt PS2 Web Recomp if you are studying static recompilation, writing a console emulator, or you own a disc image of one of the two supported games and want it running in a browser tab. Do not adopt it expecting a general PlayStation 2 emulator, since the porting work is per game and no game data ships with the repository. First read the scripts directory and the port.sh flow for your own disc image, because the README does not document a build or install procedure and the compile step is where a new title usually fails, as it did for Gran Turismo 4 and God of War II.

Frequently asked questions

Does PS2 Web Recomp include any PlayStation 2 games?

No. The project states that no game data is included, and that you bring a disc image of a game you own and run the repository's scripts on it to produce a web page on your own machine.

Which games have been brought up with PS2 Web Recomp?

Gran Turismo 4 and God of War II. Gran Turismo 4 was chosen because it is among the most demanding titles on the console, relying on both vector units, exact floating point behaviour and palette readback tricks, and God of War II followed through the same scripts.

How is the graphics output produced in PS2 Web Recomp?

The Graphics Synthesizer's 4 MB of video memory lives on the GPU in its native swizzled layouts, and drawing is done by WebGPU compute shaders that bin triangles into tiles and apply the GS's own texturing, palette, blending, depth and alpha test rules. A software rasterizer of the same GS is kept as the reference.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. snuri00/ps2-web-recomp on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/snuri00-ps2-web-recomp.svg)](https://hysenlabs.com/projects/snuri00-ps2-web-recomp)

Was this useful?