|
| 1 | +# NFSU2 on Apple Silicon — static recompilation of a Win32/x86 game to native ARM64 |
| 2 | + |
| 3 | +This repository contains the tools and runtime that turn the original Windows |
| 4 | +executable of *Need for Speed: Underground 2* (2004, 32-bit x86, Direct3D 9) |
| 5 | +into a **native ARM64 macOS binary**. The game's x86 machine code is lifted to |
| 6 | +C ahead of time and compiled with clang. There is no Wine, no emulator, no JIT |
| 7 | +and no Windows involved at run time. |
| 8 | + |
| 9 | +> **This repository contains no game code and no game assets.** It contains the |
| 10 | +> recompiler and the runtime only. You need your own copy of the game; the C |
| 11 | +> code generated from its executable and the resulting binaries are derived |
| 12 | +> from the game and are not, and must not be, distributed here. |
| 13 | +
|
| 14 | + |
| 15 | + |
| 16 | +## Status |
| 17 | + |
| 18 | +The game boots, reaches the menus, and races are playable with sound. |
| 19 | + |
| 20 | +| Area | State | |
| 21 | +|---|---| |
| 22 | +| x86-32 → C lifter (integer, x87, MMX, SSE/SSE2, 3DNow!, string ops) | 23,278 functions, 132.5 MB of generated C | |
| 23 | +| Win32 layer (CRT startup, heap, TLS, threads, files, registry, window) | working | |
| 24 | +| Direct3D 9 → Metal, shader bytecode (vs/ps 1.x–2.x) → MSL | working | |
| 25 | +| DirectSound → CoreAudio, DirectInput (keyboard, mouse) | working | |
| 26 | +| Windowed, fullscreen, Retina-native and 1080p widescreen | working | |
| 27 | +| Double-clickable `.app` (`./pack.sh`) | working | |
| 28 | +| Optional "HD" build: 4× MSAA, SSR, ray-traced reflections | working, see `GRAFIK_ARASTIRMA.md` | |
| 29 | + |
| 30 | +### Known limitations |
| 31 | + |
| 32 | +- **Exactly one build of `SPEED2.EXE` is supported** (see [Requirements](#requirements)). |
| 33 | + Game addresses are hard-coded in the runtime, and the pipeline relies on a |
| 34 | + property of that particular file (below). |
| 35 | +- Videos are skipped: there is no VP6 decoder. |
| 36 | +- Keyboard and mouse only; no gamepad or wheel support. No network play. |
| 37 | +- Structured exception handling is not implemented (`RtlUnwind` / |
| 38 | + `RaiseException` are stubs). |
| 39 | +- x87 is modelled with 64-bit doubles, not 80-bit extended precision. |
| 40 | +- One workaround with an unknown root cause remains: the front end's |
| 41 | + world-view-projection matrix arrives broken and an identity matrix is injected |
| 42 | + instead (`FIXMAT`, see `run.sh`). |
| 43 | +- 400 instructions in rarely used opcodes are still emitted as traps. None are |
| 44 | + hit in the measured race scene. |
| 45 | +- Some visuals are unverified against a Windows reference (crowds / skinned |
| 46 | + meshes, HUD seams). See "Kalan grafik maddeleri" in `PROJECT.md`. |
| 47 | + |
| 48 | +## How it works |
| 49 | + |
| 50 | +``` |
| 51 | +SPEED2.EXE (PE32, x86) |
| 52 | + │ tools/reloc2.py recover the relocation table → out/relocs.txt |
| 53 | + │ tools/genall.py recursive-descent discovery + tools/lift.py |
| 54 | + ▼ x86 → C, one C function per guest function |
| 55 | +gen/all/shard_*.c (128 files) + fntab.c (guest address → native function) |
| 56 | + │ build.sh clang -O3, in parallel |
| 57 | + ▼ |
| 58 | +runtime/ (hand-written) CPU state, guest memory, Win32, D3D9→Metal, audio |
| 59 | + │ link.sh |
| 60 | + ▼ |
| 61 | +out/nfsu2_boot 46 MB arm64 Mach-O |
| 62 | +``` |
| 63 | + |
| 64 | +- **Pointers vs. constants.** The hard problem of x86 static recompilation is |
| 65 | + telling a 4-byte address from a 4-byte number. This executable has its |
| 66 | + relocation *directory entry* zeroed, but the relocation *data* is still |
| 67 | + physically present in an unnamed section. `tools/reloc2.py` parses it by hand: |
| 68 | + 91,106 absolute addresses, all of which point into the image. Function |
| 69 | + discovery and jump-table recovery are seeded from it. |
| 70 | +- **Guest memory.** The 32-bit guest address space is reserved with `mmap`; |
| 71 | + guest→host translation is a single ARM64 addressing mode |
| 72 | + (`ldr w8, [x_base, w_addr, uxtw]`). |
| 73 | +- **Indirect calls** go through an O(1) table indexed by guest address. |
| 74 | +- **Code generated at run time.** The game writes small x86 thunks to its stack |
| 75 | + and calls them. `runtime/genexec.c` detects these and runs them in a small |
| 76 | + interpreter, handing calls back to lifted functions. |
| 77 | +- **D3DX effects.** The statically linked D3DX9 effect framework took ~47% of |
| 78 | + guest time when executed as lifted code; `runtime/d3dx_hle.c` short-circuits |
| 79 | + parts of it (`D3DX_HLE.md`). |
| 80 | + |
| 81 | +| Path | What it is | |
| 82 | +|---|---| |
| 83 | +| `tools/lift.py` | x86-32 → C lifter, with lazy EFLAGS (flag liveness) | |
| 84 | +| `tools/genall.py` | function discovery, parallel generation, sharding | |
| 85 | +| `tools/reloc2.py` | relocation table recovery | |
| 86 | +| `tools/argcount.py` | stdcall argument counts for imports | |
| 87 | +| `tools/isatest.py`, `tools/fuzzisa.py` | instruction-semantics tests and differential fuzzer | |
| 88 | +| `runtime/cpu.h`, `mem.c`, `dispatch.c`, `genexec.c` | CPU state, guest memory, dispatch, run-time code | |
| 89 | +| `runtime/win32.c`, `win32_ext.c`, `fileio.c`, `res.c` | Win32 API layer | |
| 90 | +| `runtime/com.c`, `metal.m`, `vsemu.c` | Direct3D 9 / DirectInput / DirectSound COM layer, Metal renderer, shader translation | |
| 91 | +| `runtime/audio.m` | CoreAudio output | |
| 92 | +| `runtime/overrides.c`, `d3dx_hle.c` | hand-written replacements for specific game functions | |
| 93 | +| `runtime/enhance.c` | HD build only (`-DNFS_ENHANCED=1`) | |
| 94 | +| `runtime/bundle.m`, `pack.sh` | `.app` behaviour and packaging | |
| 95 | +| `tests/boot.c` | program entry point (runs the game from its PE entry point) | |
| 96 | + |
| 97 | +## Verification |
| 98 | + |
| 99 | +- **Instruction semantics:** 169 hand-written tests, 439 checks |
| 100 | + (`tools/isatest.py` → `out/isatest`). |
| 101 | +- **Differential fuzzing against real x86:** `tools/fuzzisa.py` assembles random |
| 102 | + instruction sequences, runs them as real x86-64 code under Rosetta 2 and as |
| 103 | + lifted C on ARM64, and compares registers, flags and memory. |
| 104 | +- **Function-level tests:** 15 checks against lifted game functions |
| 105 | + (`out/nfsu2_all`). |
| 106 | +- **Reproducible generation:** the generated C is a pure function of the |
| 107 | + executable and the tools; regenerating gives byte-identical output. |
| 108 | + |
| 109 | +`./run.sh test` runs the first and third. |
| 110 | + |
| 111 | +## Performance |
| 112 | + |
| 113 | +Measured on an M1 Pro at 1280×960, same scene and settings, alternating runs |
| 114 | +(method, raw numbers and caveats in `WINE_KIYAS.md`): |
| 115 | + |
| 116 | +| | This port | Wine 11.18 (wined3d, Rosetta 2) | |
| 117 | +|---|---|---| |
| 118 | +| Race | 119 fps (capped by the 120 Hz display) | 10–15 fps | |
| 119 | +| 3D menus | 119 fps | 45 fps | |
| 120 | +| CPU time per frame | ~12.6 ms | ~105 ms | |
| 121 | + |
| 122 | +## Requirements |
| 123 | + |
| 124 | +- Apple Silicon Mac, macOS 11 or later. Developed on an M1 Pro, macOS 27. |
| 125 | +- Xcode Command Line Tools (`clang`). |
| 126 | +- Python 3 with `capstone` 5.x and `pefile` (`requirements.txt`). |
| 127 | +- Rosetta 2, only for `tools/fuzzisa.py`. |
| 128 | +- Your own installation of the game, with this exact executable: |
| 129 | + |
| 130 | + | | | |
| 131 | + |---|---| |
| 132 | + | File | `SPEED2.EXE`, 4,788,224 bytes | |
| 133 | + | SHA-256 | `8f6a9ae2c4cfa01181d2fd755d99c6f4b7103f60273908ea1de1e719dfe2a297` | |
| 134 | + |
| 135 | + Other builds have not been tested and will not work without changes. |
| 136 | + |
| 137 | +## Build and run |
| 138 | + |
| 139 | +Run everything from the repository root. |
| 140 | + |
| 141 | +```bash |
| 142 | +export NFSU2_DATA="/path/to/Need for Speed Underground 2" # folder containing SPEED2.EXE |
| 143 | +EXE="$NFSU2_DATA/SPEED2.EXE" |
| 144 | + |
| 145 | +python3 -m venv .venv && .venv/bin/pip install -r requirements.txt |
| 146 | + |
| 147 | +# 1) relocation table -> out/relocs.txt |
| 148 | +.venv/bin/python tools/reloc2.py "$EXE" |
| 149 | + |
| 150 | +# 2) lift every function to C -> gen/all/ (~20 s) |
| 151 | +# OVERRIDE lists the functions that runtime/overrides.c and |
| 152 | +# runtime/d3dx_hle.c wrap; it must match them or the link fails. |
| 153 | +OVERRIDE=60fab0,61e390,61f130,5c7ed0,50d510,65ae35,6623ea,66aa3f,66ace2,6691b1,669296,664a2f,663a42,688e89,663275,662edc,660a52,668e0d,664792,66ac27,65addb,65acf5,66d13a,66d303,66d7e9,66e8a9,66f501,66ca38,65ab24 \ |
| 154 | + .venv/bin/python tools/genall.py "$EXE" 128 |
| 155 | + |
| 156 | +# 3) import argument table -> gen/all/impargs.c |
| 157 | +.venv/bin/python tools/argcount.py "$EXE" |
| 158 | + |
| 159 | +# 4) compile the shards, then build the runtime and link |
| 160 | +./build.sh && ./link.sh |
| 161 | + |
| 162 | +./run.sh test # ISA + function tests |
| 163 | +./run.sh # windowed |
| 164 | +./run.sh 1080p # 1920x1080, widescreen-corrected |
| 165 | +./run.sh hd # HD build |
| 166 | +./pack.sh # out/NFSU2.app |
| 167 | +``` |
| 168 | + |
| 169 | +`run.sh` documents the environment switches; most are diagnostics. |
| 170 | + |
| 171 | +## Related work |
| 172 | + |
| 173 | +Static recompilation of x86 is not new, and this is not the first project to do |
| 174 | +it: |
| 175 | + |
| 176 | +- [M-HT/SR](https://github.com/M-HT/SR) statically recompiles DOS and Windows |
| 177 | + game executables (Septerra Core, Battle Isle 3 and others) at assembly level, |
| 178 | + for targets including macOS arm64. |
| 179 | +- [sp00nznet/xboxrecomp](https://github.com/sp00nznet/xboxrecomp) lifts original |
| 180 | + Xbox (x86) games to C with a Python + Capstone pipeline very similar to this |
| 181 | + one. |
| 182 | +- [N64: Recompiled](https://github.com/N64Recomp/N64Recomp) and |
| 183 | + [XenonRecomp](https://github.com/hedge-dev/XenonRecomp) established the |
| 184 | + approach for MIPS and PowerPC consoles, and are where the method here comes |
| 185 | + from. |
| 186 | +- McSema, rev.ng, RetDec and BinRec are IR-based binary lifters from the research |
| 187 | + side. |
| 188 | + |
| 189 | +What this project adds is narrower: recovering a relocation table whose |
| 190 | +directory entry was zeroed and using it to separate pointers from constants; a |
| 191 | +differential instruction fuzzer that uses Rosetta 2 as the oracle; a hybrid |
| 192 | +interpreter for x86 code the game generates at run time; and an end-to-end, |
| 193 | +playable Win32 + Direct3D 9 → macOS ARM64 + Metal port of a 23,000-function 3D |
| 194 | +game. |
| 195 | + |
| 196 | +## Documentation |
| 197 | + |
| 198 | +The engineering notes are in Turkish, as are the code comments. |
| 199 | + |
| 200 | +- `PROJECT.md` — chronological engineering log: findings, bugs and root causes, |
| 201 | + measurements. |
| 202 | +- `D3DX_HLE.md` — high-level emulation of the D3DX effect framework. |
| 203 | +- `GRAFIK_ARASTIRMA.md` — graphics research and the HD build. |
| 204 | +- `WINE_KIYAS.md` — comparison against Wine. |
| 205 | +- `sunum_ss/rapor/nfsu2-rapor.html` — illustrated project report. |
| 206 | + |
| 207 | +## How this was built |
| 208 | + |
| 209 | +Development was carried out with an AI coding assistant (Claude Code) under the |
| 210 | +direction of the project owner. Goals, constraints and priorities, observations |
| 211 | +from playing the game, Windows reference material and direction decisions came |
| 212 | +from the project owner; implementation, measurement and debugging were done |
| 213 | +together with the assistant. |
| 214 | + |
| 215 | +## Legal |
| 216 | + |
| 217 | +Copyright (c) 2026 Talha Müderrisoğlu. All rights reserved. |
| 218 | + |
| 219 | +This repository is **not open source**. You may download, build, run and |
| 220 | +modify it for your own personal, non-commercial use with your own copy of the |
| 221 | +game. Publishing, distributing or sharing it — or any modified version, port, |
| 222 | +fork or build of it — and any commercial use require prior written |
| 223 | +permission. See `LICENSE`. |
| 224 | + |
| 225 | +**Permission:** to publish, share or build on this code, ask first by opening |
| 226 | +an issue in this repository. |
| 227 | + |
| 228 | +This project is not affiliated with or endorsed by Electronic Arts. *Need for |
| 229 | +Speed* and *Need for Speed: Underground 2* are trademarks of Electronic Arts |
| 230 | +Inc. The repository contains none of the game's code or data. Screenshots show |
| 231 | +the game running through this port and are included for documentation; the |
| 232 | +game content in them belongs to its owners. |
0 commit comments