⏱️ Reading time: 13 min
Need for Speed: Underground 2 runs in the browser at up to 120 fps in widescreen, with online races and support for controllers or steering wheels. The Underground 2 Revived project doesn’t imitate the original console: it translated the entire game using static recompilation, the technique that converts a binary from another architecture into source code that any modern compiler understands.
📑 En este artículo
- TL;DR
- What is static recompilation?
- Why software preservation matters
- How the process works, step by step
- Practical examples
- Real-world use cases: the family of projects
- Common pitfalls and best practices
- Comparison with alternatives
- Going deeper: what it takes under the hood
- Frequently Asked Questions
- References
The same idea drives an entire family of projects that rebuild Nintendo 64 and Xbox 360 games in C and bring them all the way to WebAssembly. None of them aim to make the player have more fun: they aim to make the software survive once the original hardware no longer exists.
TL;DR
- Static recompilation translates an entire x86 or PowerPC binary into C code, without emulating the CPU in real time.
- NFSU2Recomp turns Need for Speed: Underground 2 (Xbox, 2004) into an app that runs in Chrome and Firefox.
- XenonRecomp targets Xbox 360 binaries (Xenon CPU); N64: Recompiled does the same with N64 cartridges.
- The final compiler optimizes the resulting code, which is why these ports run faster than a classic emulator.
- Detecting each function’s boundaries and indirect jumps is harder than translating line by line.
What is static recompilation?
Static recompilation is a reverse engineering technique that translates a program’s compiled binary (a ROM, an .exe) instruction by instruction into C source code, without ever executing it. A regular compiler then processes that C to generate a native program for another architecture, including WebAssembly.
The difference with emulation is what matters. An emulator reinterprets every instruction in every session, cycle by cycle, while the game runs. Static recompilation does that work only once, before the player touches a button, and delivers an executable that the processor runs natively.
The result, when the target language is C, turns out portable. That same C can be compiled with GCC for Windows, with Clang for macOS, or with Emscripten to generate WebAssembly, which is exactly what the three projects in this article do.
Why software preservation matters
A console stops being manufactured, but the software that ran on it doesn’t have to disappear along with the hardware. Optical discs degrade, memory card batteries fail, and finding an Xbox 360 or a Nintendo 64 cartridge in good condition gets harder every year. Rewriting the original binary as readable source code turns a fragile physical object into a text file that any future compiler can rebuild.
Binary recompilation also solves a problem traditional emulation can’t. When an emulator has a timing bug, the entire core has to be patched, and that patch can affect a hundred other games running on the same emulator. If the bug lives in the generated C of a single recompiled game, it gets fixed there, without touching anything else.
Underground 2 Revived, for example, doesn’t charge for the game or sell upgrades. It’s funded through voluntary donations and reserves a Discord role for contributors. The recompiled code is what allows adding features the original Xbox never had, like online matches between friends.
How the process works, step by step
This is, broadly speaking, how static recompilation happens in these three projects.
Finding the functions inside the binary
The first obstacle isn’t translating instructions; it’s knowing where each function starts and ends inside a binary that doesn’t carry that information. A compiled executable loses function names and most of its debug symbols. That’s why the recompiler has to infer the boundaries by analyzing jump and call instructions. To do that, it builds a graph with every possible execution path.
Classic binary translation solves this on the fly, instruction by instruction, while the program runs. A static recompiler does the full analysis before generating a single line of C, which is why it can take minutes or hours to process a large binary the first time, even though the final result runs without that cost.
From CPU instructions to lines of C
Each instruction from the original CPU turns into one or several lines of C that read and write the same registers the real processor would use. The recompiler doesn’t understand the game: it translates mechanically, preserving the exact behavior of each instruction.
An addition in PowerPC like add r3, r4, r5 turns into something equivalent to this:
// r3 = r4 + r5 on the original PowerPC
void fn_08A3F120(PPCContext* ctx, uint8_t* base) {
ctx->r3.u64 = ctx->r4.u64 + ctx->r5.u64;
}
This function represents the effect of a single addition instruction, using a PPCContext structure that models the PowerPC’s register bank; base points to the game’s memory mapped as a C array. The expected output isn’t text in a terminal, but the same value the original chip would have put in the register: if r4 is 10 and r5 is 32, the function leaves ctx->r3.u64 at 42.
When the full binary has thousands of functions, nobody writes this by hand. The recompiler automatically generates thousands of functions like this one, one for each memory address where the analysis detected the start of a new function.
From C to WebAssembly
The generated C is useless on its own: it needs an additional layer, usually called a runtime or shim (compatibility layer), that replaces disk access, the graphics driver, sound, and the original console’s controller.
That layer translates the calls the original game made to the Xbox 360 GPU or the Nintendo 64’s sound microcode into modern APIs like WebGL or Web Audio.
For the result to run in a browser, the C (runtime included) gets compiled with Emscripten, the LLVM-based toolchain that converts C and C++ into WebAssembly. That same C, unchanged, can also be compiled natively with GCC or Clang to generate a Windows, macOS, or Linux executable. Binary recompilation thus separates the game’s logic, fixed in the C, from the target platform, which the compiler decides.
flowchart LR
A["Original binary (x86 / PowerPC)"] --> B["Function and jump analysis"]
B --> C["Generated C code"]
C --> D["Runtime: GPU, audio, input"]
D --> E["Emscripten + LLVM"]
E --> F["WebAssembly in the browser"]
Practical examples
Simple conditional jumps show the translation mechanics well. An x86 jump like this one:
cmp eax, 0
jne distinto
; block A
distinto:
; block B
turns into C like this:
if (eax != 0) {
goto distinto;
}
// block A
distinto:
// block B
What it does: it reproduces the cmp comparison and the jne jump (‘jump if not equal’) with an if and a goto. The expected output is identical to that of the original binary. If eax is 0, the program executes block A and continues to block B; with any other value, it jumps straight to block B.
Direct jumps like this one are the easy case. The real challenge for these recompilers is indirect jumps, meaning jump tables that get resolved in memory during the original execution; that’s explained further below, in the common pitfalls section.
Real-world use cases: the family of projects
None of these three projects was born to make someone play better. They were born to prove that binary recompilation works at real scale, with binaries of tens of megabytes and thousands of functions.
N64: Recompiled
N64: Recompiled is the name given to the wave of projects built on a toolchain that translates Nintendo 64 ROMs, compiled for the MIPS R4300i processor, into C code. Its best-known case rebuilt The Legend of Zelda: Ocarina of Time as a native PC port, with widescreen resolution and an uncapped frame rate, something the original 1998 console never offered.
XenonRecomp
XenonRecomp targets Xbox 360 binaries, compiled for the Xenon CPU, a tri-core chip based on PowerPC. Its flagship case recompiled Sonic Unleashed into a native PC port, built by the community that maintains the hedge-dev project.
NFSU2Recomp
NFSU2Recomp, created by developer Talha Müderrisoğlu, is the original x86-to-C static recompiler for Need for Speed: Underground 2. Within this family, it’s the first to tackle an x86 binary instead of a RISC CPU like MIPS or PowerPC, and x86 has variable-length instructions that make automatic analysis much harder.
Underground 2 Revived was forked from the MIT-licensed version of NFSU2Recomp dated October 16, 2026, adding the browser runtime, the networking code for online races, and WebAssembly compilation.
| Project | Original console / CPU | Source architecture | Flagship case |
|---|---|---|---|
| N64: Recompiled | Nintendo 64 (MIPS R4300i) | MIPS (RISC) | Native PC port of Ocarina of Time |
| XenonRecomp | Xbox 360 (Xenon CPU) | PowerPC (RISC) | Native PC port of Sonic Unleashed |
| NFSU2Recomp | Original Xbox / PC (x86) | x86 (CISC) | Underground 2 Revived, with a WebAssembly build |
flowchart TD
A["N64: Recompiled (MIPS to C)"] --> C["Inspires the approach"]
B["XenonRecomp (PowerPC to C)"] --> C
C --> D["NFSU2Recomp (x86 to C)"]
D --> E["Underground 2 Revived (fork + WebAssembly runtime)"]
Common pitfalls and best practices
Three problems come up in any serious static recompiler, plus one that’s not technical but legal.
- Indirect jumps: the jump tables generated by C switch statements or virtual function calls point to an address calculated at runtime, not fixed in the binary. The recompiler has to recognize the pattern each compiler generates or, if it doesn’t, fall back to a small interpreter just for that block.
- Heuristics tied to the original compiler: function boundary analysis depends on recognizing patterns typical of a specific compiler, like old versions of GCC or MSVC. A binary compiled with a different tool can break those heuristics and require manual adjustments.
- Floating-point determinism: x86, PowerPC, and ARM can round the same floating-point operation differently. In a racing game with simulated physics, that tiny difference accumulates frame by frame and can desync an online match between two different machines.
- Assets with copyright separate from the code: recompilation translates the executable, not the art, audio, or video files on the original disc, which remain protected.
⚠️ Heads up: these projects translate code, not assets. Textures, models, audio, and video from the original disc remain copyrighted, so they require the user to provide their own legal copy of the game; they don’t redistribute those files.
Comparison with alternatives
Static recompilation isn’t the only way to preserve an old game, and it’s not always the best option.
| Approach | When to use it | Advantage | Limitation |
|---|---|---|---|
| Dynamic emulation (JIT) | Covering many games with a single core | One emulator supports an entire library | Reinterprets instructions every session: more CPU, less faithful timing |
| HLE (system function replacement) | When the original system API is known and stable | Avoids simulating entire hardware | A bug in the reimplementation affects every game that uses it |
| Static recompilation | Preserving a specific binary with maximum performance | Runs natively; the code remains readable and patchable | Huge analysis effort for each different binary |
| Manual port with the original source code | When the studio keeps and releases the source code | Optimal quality and maintenance | Almost never available: most studios don’t publish their code |
flowchart TD
subgraph EM["Dynamic emulation"]
EA["Read instruction"] --> EB["Interpret or compile on the fly"]
EB --> EC["Execute"]
EC --> EA
end
subgraph RE["Static recompilation"]
RA["Analyze the full binary once"] --> RB["Generate C code"]
RB --> RC["Compile to native or WebAssembly"]
RC --> RD["Run already-compiled code"]
end
Going deeper: what it takes under the hood
Resolving a jump table in PowerPC or MIPS usually means recognizing the exact instruction pattern the original compiler used to implement a C switch statement: a load from an address table followed by an indirect jump. The recompiler compares that pattern against a library of known signatures and, if it matches, generates an equivalent C switch with all the possible cases already enumerated.
The other half of the work isn’t in the CPU but in the operating system that surrounded the game. The Xbox 360 exposed its own APIs, the XAM family, for memory, threads, and input; the Nintendo 64 used Nintendo’s low-level libraries known as libultra. No recompiler translates those calls instruction by instruction. Each project hand-reimplements a runtime that offers the same contract, backed by modern threads, memory, and drivers.
Determinism also matters for online play. Two Underground 2 Revived players in different browsers run the same C compiled to WebAssembly, so their cars’ physics match instruction by instruction; that’s far harder to guarantee if each platform had its own hand-written port.
Licensing also enables this wave of projects. NFSU2Recomp and Underground 2 Revived are distributed under the MIT license, which allows forking, modifying, and redistributing the code while only requiring that the original copyright notice be kept; that’s what allowed Underground 2 Revived to build directly on Müderrisoğlu’s work instead of starting from scratch.
Your next step: clone any of these open source static recompilers and open the folder where it stores the generated functions to compare, line by line, a C function against the instruction manual of the CPU it translated.
Frequently Asked Questions
What does static recompilation need to work?
Just the original program’s compiled binary. It doesn’t need or use the source code from the studio that created it, because it never had access to it.
Is it legal to use a static binary translator like NFSU2Recomp?
The recompiler and runtime code can be legal if it’s distributed under an open license like MIT, but the game’s assets (art, audio, video) remain protected. That’s why these projects require each user to provide their own legal copy of the original game.
How is a console recompiler different from an emulator?
The emulator reinterprets instructions every session, frame by frame. The recompiler does that analysis only once, before playing, and delivers a program the CPU runs natively, without that repeated cost.
Why was x86-to-C transpilation harder than MIPS or PowerPC?
Because x86 is a variable-length instruction set, with many different addressing modes, while MIPS and PowerPC are RISC with fixed-width instructions, much easier to disassemble automatically.
Is any binary a candidate for binary recompilation?
No. Code that modifies itself at runtime, or that heavily relies on dynamically calculated jumps, resists static analysis and usually needs a partial interpreter for those specific fragments.
References
- Underground 2 Revived: the project’s official site, with the port’s history, the NFSU2Recomp fork, and the donation-based funding model.
- Wikipedia: Binary translation: general background on static and dynamic binary translation.
- WebAssembly.org: official specification of the portable binary format these recompilers use as their final target.
- Emscripten: official documentation for the toolchain that compiles C and C++ to WebAssembly.
- Open Source Initiative: MIT License: official text of the license under which NFSU2Recomp and Underground 2 Revived are distributed.
📱 Enjoy this content? Follow @programacion on Telegram for daily tech content in Spanish: quick summaries, fresh content every day.
Featured image: Foto de Derek Story en Unsplash
Did it work for you? Got a different error? Say so below: questions get answered and help the next reader.
Leave a comment
0 Comments