# NFS Most Wanted on itch.io: EA's 2005 speed.exe, recompiled ahead of time to WebAssembly

> Satyajit Ghana — Head of Engineering @ Inkers Technology
> canonical: https://ai.thesatyajit.com/articles/nfsmw-web-port
> date: 2026-10-23
> tags: webgpu, reverse-engineering, compilers

Two weeks ago I took apart [GTA V running in a browser tab](/articles/gta5-in-the-browser): Rockstar's engine compiled from its own C++ source to 64-bit WebAssembly. This week I read [NFSMW Online](/articles/nfsmw-online) from outside its login wall, and the best I could do there was place it on a map of four porting paths without seeing its code. This is the third one, and it settles a question the other two left open: what does a browser port look like when nobody has the source?

`dezm0n.itch.io/nfs-most-wanted-web-port` runs Need for Speed: Most Wanted, EA Black Box's 2005 racer, in desktop Chrome or Edge. The itch.io page calls it "an experimental browser port of the original PC game, powered by WebAssembly and WebGPU". Unlike NFSMW Online, everything it runs is served to anyone who opens the page, so this time I could read the engine itself.

What I found is a static recompilation. Somebody disassembled EA's own `speed.exe`, version 1.3, translated every function in it to code that compiles to WebAssembly, and wrapped the result in a rebuilt Windows: kernel threads and structured exceptions, DirectInput and XInput, DirectSound, and a Direct3D 9 that draws on WebGPU. The internal name is `SpeedRecomp`. The module is 76,982,114 bytes, 16,086,019 gzipped on the wire, and it ships next to a table of 42,545 addresses inside `speed.exe`. None of EA's function names are in it. They are Ghidra's names, `FUN_00612420` and so on.

The game's files do not come from itch.io. A manifest in the build lists 1,398 files, 2,972,904,233 bytes, all on `cdn.justplayweb.com`: the retail game's folders, its movies, its music and `speed.exe` itself. The tab copies the executable whole and reads everything else in 8 MiB HTTP Range requests, only where the game asks.

I read every script the page serves, parsed the module and its data package, compared its symbol table with the public decompilation of the same executable, and loaded the page in headless Chromium with every request to the game-file CDN blocked. Every claim below is labelled **measured** (I read it in a served file, a header or my own run), **reported** (the developer says so) or **reasoned** (my inference from the other two).

One thing first. Need for Speed: Most Wanted belongs to Electronic Arts, and so does the code being translated. The page says "Unofficial port. Original game and poster: Electronic Arts / EA Black Box." What the port distributes, and from where, is near the end.

## What the page is

The itch.io listing went up on 10 October 2026, is marked "In development", and was last updated on 21 October, when every file in the build was last modified (`Last-Modified: Wed, 21 Oct 2026 22:27:12 GMT`, **measured**). It has no devlog and no comments. Its three images are all the same file, EA's box art with the BMW M3 GTR, so there are no developer screenshots to show; the figures here are my own captures of what the page serves. The Russian site `cybersport.metaratings.ru` covered it on 21 October.

The page's instructions are short (**reported**): use a current desktop Chrome or Edge "with hardware acceleration and WebGPU", click Run game and then Start game, and "Game files load automatically as they are needed and stay cached in this browser." Saves go to browser storage and can be exported. Defaults are "Maximum graphics and 1920×1080". "Online/LAN modes are unsupported. Full-career driving and race-end autosave need further testing." The itch.io AI disclosure field reads "AI Assisted, Code".

The developer's profile lists other web ports: FlatOut 2, Hollow Knight and Worms: Open Warfare 2, all published within days of this one. They are not the same machine. FlatOut 2 loads its own `flatout2.wasm` builds through a different set of scripts, Hollow Knight is tagged as a Unity game, and Worms is tagged as running on PPSSPP, the PSP emulator (**measured** from their pages and loaders). The Most Wanted build is the only one of the four that carries the `recomp` runtime.

<Figure
  src="https://media.thesatyajit.com/ai/articles/nfsmw-web-port/launcher.jpg?v=f989235e95db1f17"
  alt="The launch page of the Most Wanted web port: on the left the headline 'Become the Most Wanted.', a gold Start game button, a resolution menu set to 'Game setting (Options → Video)', a line reading 'Graphics card: google · swiftshader' with the warning 'Software rendering selected. The game needs a hardware GPU.', and a 'Low FPS? Set up GPU' button; on the right EA's box art of the BMW M3 GTR."
  caption="The launch page in my headless run. It probes WebGPU before you click, and on Chrome's CPU renderer it says plainly that the game needs a hardware GPU (screenshot of the itch.io build, my headless run, 23 October 2026)."
/>

## What loads, in order

The game runs in itch's iframe from `html.itch.zone`, which sends `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp` (**measured**). Those two headers make the page cross-origin isolated, which is what unlocks `SharedArrayBuffer` and therefore threads. itch.io can send them for an HTML5 upload, so this port gets threads without a host of its own; the GTA V port ran its own.

I loaded the page in headless Chromium with SwiftShader's WebGPU adapter, clicked Start game, and logged every request. Before the click, the page fetches seven files, 955,474 bytes on the wire, three-quarters of it the box art (**measured**). It also asks WebGPU for a high-performance adapter and sorts it into dedicated, integrated, software or unknown by matching the adapter's description against vendor patterns. SwiftShader is "software", so the page warned me and still let me start.

The click does four things before any game code exists (**measured**, from `ui/player.mjs` and `nfsmw/runtime.html`):

1. It takes a Web Lock named `recomp-player-nfsmw`, so a second tab gets "The game is already running in another tab".
2. It asks for persistent storage with `navigator.storage.persist()`.
3. It opens a second iframe, `nfsmw/runtime.html`, which refuses to continue unless `crossOriginIsolated` is true and `navigator.gpu` exists.
4. It starts a worker that reads `games.json` and the asset manifest, `nfsmw/assets.json`.

Then the worker downloads one game file whole: `speed.exe`, 6,029,312 bytes, into the Origin Private File System. That is where my run stopped, because I aborted it. Its size came from a HEAD request. The engine files come next. I did not let the page load them, but I fetched each one directly to measure it.

<LoadWaterfall />

In bytes on the wire, the sequence is (**measured**, except the total, which is **reasoned** by adding them):

| Phase | Files | Bytes | Host |
|---|---|---|---|
| Launch page | 7 | 955,474 | `html.itch.zone` |
| Start game: checks, worker, manifest | 6 | 36,818 | `html.itch.zone` |
| `speed.exe` | 1 | 6,029,312 | `cdn.justplayweb.com` |
| Engine: glue, wasm, data package, worklet, settings | 6 | 16,650,180 | `html.itch.zone` |
| Total before the first game-file read | 20 | 23,671,784 | |

About 70% of that is the engine. Then the game starts reading its own files, and that part has no fixed size, because it depends on where you drive.

## Where the game's files come from

`games.json` describes one game, and it is the key file in the build (**measured**):

```json
{
  "id": "nfsmw",
  "executable": "speed.exe",
  "sha256": "80774c2e5d619b4f120b48d4462896fd504c263399d203a238769cffde1d253c",
  "requiredDirs": ["CARS", "FRONTEND", "GLOBAL", "SOUND", "TRACKS"],
  "exclude": ["Uninstall", "Support", "scripts", "SAVE", "foobar", "*.dll", "*_inst.exe", "*.txt", "server.cfg"],
  "installNames": ["Need for Speed Most Wanted", "NFS Most Wanted", "Most Wanted"],
  "streamAssets": true,
  "hostedAssets": "./nfsmw/assets.json?v=16af54b079f739ab",
  "assetBase": "https://cdn.justplayweb.com/nfsmw-pc13-20261010"
}
```

Read it top to bottom and you see two designs in one file. The first half is for a player who owns the game. `requiredDirs`, `exclude` and `installNames` are rules for finding an installed copy, skipping its uninstaller and DLLs, and checking it. The build has a second page, `library.html`, titled "Recomp games", that does exactly that: "Import your own copy of a game once; this browser keeps it for playing here. Choose the folder the game is installed in, or a ZIP of it". The import logic in `core.js` hashes the imported `speed.exe` and refuses anything whose SHA-256 is not the one above (`wrongVersion`). Its header comment says it follows "the same rules as host/launcher/launcher.cpp", a native launcher that is not part of the web build.

<Figure
  src="https://media.thesatyajit.com/ai/articles/nfsmw-web-port/library.jpg?v=b046ad248778c92d"
  alt="A plain light page titled 'Recomp games' with the line 'Import your own copy of a game once; this browser keeps it for playing here.', a storage bar reading '0 KB used of 880 MB available to this site', a Keep game data button, and one card for Need for Speed: Most Wanted marked 'Not imported' with buttons Play, Import folder and Import ZIP."
  caption="The build's second page, reached from Files & saves: a launcher that imports an installed copy of the game from a folder or ZIP into the browser's private storage. The itch.io launch page does not use this path; it streams the files instead (screenshot of the itch.io build, my headless run, 23 October 2026)."
/>

The second half is what the launch page actually uses. `streamAssets` is true, `hostedAssets` names a manifest, and `assetBase` names a bucket. When the runtime finds no imported copy, it runs `streamSetup`, and the worker's own comment explains it: "A streamed view needs the full directory catalogue, but only its pinned executable is downloaded up front." So the tab writes `speed.exe` to OPFS, writes a list of every other path to `.stream-index`, and leaves the rest on the CDN.

The manifest lists what is on that CDN (**measured**):

| Folder | Files | Bytes | What it holds |
|---|---|---|---|
| `SOUND` | 530 | 950,176,167 | music, speech, effects; `MW_Music.mus` alone is 533,877,888 |
| `MOVIES` | 37 | 844,184,828 | the story cutscenes as `.vp6` video |
| `TRACKS` | 273 | 621,656,659 | Rockport; `STREAML2RA.BUN` alone is 532,663,556 |
| `CARS` | 344 | 390,153,664 | car geometry, textures, vinyls |
| `FRONTEND` | 9 | 63,192,835 | menus |
| `GLOBAL` | 39 | 53,503,164 | shared data |
| `NIS` | 87 | 40,403,632 | in-engine cutscene data |
| `speed.exe` | 1 | 6,029,312 | the executable |
| `LANGUAGES`, `MEMCARD`, `SUBTITLES` | 78 | 3,603,972 | text and stubs |
| Total | 1,398 | 2,972,904,233 | |

That is a retail installation, minus what `exclude` drops. The path says so too: `nfsmw-pc13-20261010` reads as PC version 1.3, uploaded on 10 October, and the files' recorded times and the CDN's `Last-Modified` headers both fall on 9 October (**measured**; the reading of the path is **reasoned**). The CDN answers with `Access-Control-Allow-Origin: *`, `Accept-Ranges: bytes` and `Cache-Control: public, max-age=31536000, immutable`, behind Cloudflare, and a large file's ETag has the `-8` suffix of a multipart upload, as R2 and S3 produce (**measured**; that it is R2 is **reasoned**, and the fetch backend's own comment mentions "Object stores such as R2").

`justplayweb.com` is a portal styled as a Windows 98 desktop that lists 16 browser games, this one among them. Its Most Wanted page says "Runs from: justplayweb.com" and "Hosted on justplayweb.com. Also on itch.io" (**measured**). Its about page says some games "are made by us and hosted here; others are embedded from their developers' own sites". Neither page names who runs the portal, so I cannot tell you whether it is the developer's own site. What I can tell you is that the itch.io build fetches every game file from that portal's CDN.

## A file system made of 8 MiB ranges

The engine sees the game where Windows would: `C:\Program Files\EA GAMES\Need for Speed Most Wanted\speed.exe`, with `PATH` set to that folder (**measured** strings). Underneath, it is Emscripten's WasmFS with three backends stacked up. The profile and the copied executable live in OPFS under `/opfs/nfsmw/`. The engine's own resources come from the preloaded `SpeedRecomp.data`. Every other game file is a fetch-backed file whose bytes arrive by HTTP.

The porter replaced Emscripten's stock fetch backend with one of their own, in the glue, with comments (**measured**). Its constants carry the design:

```js
const blockSize = 8 * 1024 * 1024;   // every game-file read is rounded out to 8 MiB blocks
const limit = 128 * 1024 * 1024;     // in-memory LRU of recent blocks
// Shared with the launcher's delete handler (web/launcher/worker.js).
caches.open('recomp-stream-v1')       // every block also goes to Cache Storage
```

A read works like this. The engine asks for `length` bytes at `offset`. The backend works out which 8 MiB blocks cover the range, looks each one up first in memory and then in Cache Storage under the file's URL with `?recomp-stream=<start>-<end>` appended, and fetches the rest with `Range: bytes=start-end`. It checks that the answer is `206` with exactly the `Content-Range` it asked for, then copies the bytes into the wasm heap. "Concurrent guest reads of one block share a single request." Sizes come from a one-byte range, and the comment says why it is not a HEAD: "CDN caches answer ranges from the edge but pass HEAD through to the origin, which bills every request."

Compare that with the [GTA V port's file system](/articles/gta5-in-the-browser#a-file-system-made-of-http-requests). GTA V used 4 KB blocks, an append-only store in OPFS, a journal, read hints from the engine's streamer and up to 128 requests in flight, because its host was slow per stream and its game folder was 20.9 GB. Most Wanted is 2.97 GB of fewer, bigger files, and the port takes the simple route: big blocks, one request each, and the browser's Cache Storage as the disk cache (**reasoned** from the two designs).

Big blocks have a cost. A game that reads a 4 KB header from each of a hundred large files pulls 100 × 8 MiB, about 839 MB, where GTA V's 4 KB blocks would pull about 410 KB (**reasoned**; a block stops at the end of its file, so a small file costs only itself). Most Wanted's files probably do not look like that: most of its 2.97 GB is in a few big bundles, and `STREAML2RA.BUN` alone is 532,663,556 bytes of world that the game streams as you drive (**reasoned** from the manifest; I did not trace the reads).

The rest of the backend is about not leaving a player on a black screen. A request in flight longer than 300 ms posts a `loading` message on a `BroadcastChannel`, and the runtime page draws a small loading bar with the megabytes of the blocks in flight. A server that keeps failing after three tries, waiting 1 s and 4 s between them, posts `unavailable`, and the page checks `navigator.onLine` to say whether the player's connection or the file server is down. One branch of that dialog reads "The game's free file server has reached its daily request limit" and counts down to midnight UTC. It is switched by an `assetQuota` setting that this game's `games.json` does not set (**measured**).

## Inside SpeedRecomp.wasm

I parsed the module's sections with a short script of my own. Here is what is in it:

<WasmExplorer />

The numbers, for readers without the widget (**measured**):

- **Size.** 76,982,114 bytes. The code section is 73,962,373 of them, in 46,868 function bodies with a median size of 509 bytes and a largest of 129,441. Data is 2,831,679 bytes in 536 segments.
- **No names.** There is no `name` section, no `producers` section and no DWARF. The GTA V module kept 91,111 function names; this one keeps none.
- **Memory.** One imported memory, flags `0x3`: it has a maximum and it is shared between threads, and it is 32-bit. 8,192 pages at the start (512 MiB) and 65,536 at most (4 GiB). The glue creates it with `INITIAL_MEMORY=536870912` and pre-spawns a pool of 8 pthread workers. A 32-bit game's pointers fit 32-bit WebAssembly, so this port does not need the `wasm64` that GTA V did.
- **Imports.** 353 functions: 56 WebGPU, through Dawn's `emdawnwebgpu` bindings; 167 WebGL, which arrive with SDL3; 44 WasmFS; 29 HTML5 event and gamepad hooks; 17 for threads and the main loop; 17 for C++ exceptions; 13 for libc; 5 inline JavaScript; 5 WASI.
- **Libraries.** SDL3 (`SDL-3.4.16` in its version string, with an `SDL2_COMPAT` hint), Lua 5.4.6, stb_image and stb_truetype, and the Open Sans font.

The build paths leak where it was made: `C:/Projects/Reverse Engineering/cases/nfs-most-wanted/most-wanted-web/kit/host/d3d_render.cpp`, `.../kit/host/audio/mixer.cpp`, and the original executable at `C:/Projects/Reverse Engineering/cases/nfs-most-wanted/original/retail/speed.exe`. A static library is named `librecomp_gen.a`, and a comment in a resource names `tools/recomp/translate.py --discovered`. So the generated code is a library built by a translator script, linked against a hand-written host called `kit`, on a Windows machine, in a folder of reverse-engineering "cases" (**measured** strings; the reading is **reasoned**).

### The symbol table, and whose names are in it

With no names in the module, the best map of the code is an 8,522,262-byte file packed inside `SpeedRecomp.data` as `/app/resources/symbols.json`. It opens with the same SHA-256 as `games.json` and `"image_base": "00400000"`, the default load address of a 32-bit Windows executable, and lists 42,545 entries, each an address in `speed.exe` between `0x00401000` and `0x0088f283` (**measured**).

Each entry has a kind: 26,447 `entry` (function starts), 13,052 `block` (jump targets inside functions), 2,948 `continuation` and 98 `alternate`. Each has a provenance: 25,543 `listed`, 8,575 found as addresses in the executable's data, 3,454 as instruction operands, 3,324 from jump tables, 847 from the C runtime's constructor tables, 686 named in a config file and 116 branch targets. This is the work a static recompiler has to do before it translates anything: find every place execution can arrive, including the ones only reachable through a pointer.

The names tell you which disassembler found them. 20,732 are `FUN_` followed by the address, 235 are `thunk_FUN_`, 4,851 are `Unwind@` or `Catch@` funclets, and duplicates are prefixed `FID_conflict:`. Those are Ghidra's naming conventions (**measured** patterns; that Ghidra produced them is **reasoned**, because no other tool names things that way). Only 629 entries carry real names, and all of them are Microsoft's C runtime: `_malloc`, `_qsort`, `__CxxFrameHandler`, `_sprintf`. Ghidra's Function ID recognises those by their bytes. Not one EA class or function name appears.

Seven entries do carry a second name, added by the porter as hook points (**measured**). I looked each address up in the public decompilation of the same executable, [dbalatoni13/nfsmw](https://github.com/dbalatoni13/nfsmw), whose PC 1.3 symbol table names 15,263 of its 46,360 functions:

| Address | Port's name | Port's alias | Decompilation's name |
|---|---|---|---|
| `0x005d59f0` | `FUN_005d59f0` | `nfsmw_query_interface` | `UTL::COM::Object::_IList::Find` |
| `0x006123b0` | `sub_006123b0.blk_006123b0` | `nfsmw_force_ai` | `Game_ForceAIControl` |
| `0x00612420` | `sub_00612420.blk_00612420` | `nfsmw_clear_ai` | `Game_ClearAIControl` |
| `0x0064b380` | `FUN_0064b380` | `nfsmw_main_loop_debug` | `MaybeDoMemoryProfile` |
| `0x006c1780` | `FUN_006c1780` | `nfsmw_clamp_graphics` | unnamed (`sub_6C1780`) |
| `0x006c1990` | `FUN_006c1990` | `nfsmw_graphics_preset` | unnamed (`sub_6C1990`) |
| `0x0078fa00` | `FUN_0078fa00` | `nfsmw_read_online_config` | `OnlineCfg::ReadConfigFile` |

The aliases agree with the decompilation's names wherever it has one, so the porter identified the same functions independently. More broadly, 20,417 of the port's 26,447 function entries start at an address where the decompilation's PC table also starts a function (**measured**). Two tools disassembled the same bytes and mostly agree on where functions begin. That confirms the executable is PC 1.3, which the decompilation says it targets. It does not mean the port uses the decompilation, and nothing in the build suggests it does: no name, type or line of its C++ appears (**reasoned**).

That matters, because the decompilation is far from runnable. On 23 October decomp.dev reported 13.94% of the PC code matched, the figure the [NFSMW Online piece measured](/articles/nfsmw-online#the-decompilation-next-door). A static recompiler does not wait for that. It translates all of the machine code, matched or not, and leaves it as machine code in spirit: a function that adds two registers becomes a function that adds two variables named after them.

The decompilation's README also has a section headed "SAY NO TO SLOP". It expects that "vibecoders will attempt to take what we have and slop out a broken port before us", asks people not to give such ports attention, and says the project's own ports will come from its organisation (**reported**). The itch.io port does not take anything from that repository that I can find, and it discloses AI-assisted code. I note the README because it is the decompilation community's stated position on exactly this kind of project, not because it applies to this one.

<RepoCard repo="dbalatoni13/nfsmw" />

### How translated code runs

Static recompilation of x86 is older than browsers, and its hard parts are well known. Every instruction is translated once, at build time, into C or straight into the target's IR, with the CPU's registers and flags as variables and guest memory as one big array. The program counter stops existing. That breaks the moment the game computes an address and jumps to it, through a `switch` table, a virtual call or a function pointer. The translator has to know every such target in advance, and the runtime needs a lookup from a guest address to the translated block. Whatever the translator missed, it cannot run.

The module's error messages are those failure modes, written out (**measured** strings):

- `recomp: no block entry for indirect jump to 0x%08x from 0x%08x` is the missed target.
- `unmodelled SSE instruction at %08x ... the guest used a CPU extension recomp_cpuid does not advertise` is an instruction the translator does not model. The runtime answers `CPUID` itself, so the game only takes code paths it knows.
- `recomp_shim_call: %08x is not an allocated trampoline` is a call into Windows that no shim registered.
- `%s has an unknown stdcall argument count: not adjusting ESP, the guest stack will drift if it is really stdcall` is the 32-bit Windows calling convention, where the callee pops its own arguments, leaking into the shim layer.

Nothing in the glue generates WebAssembly at run time. The only instantiation besides the main one is each pthread worker instantiating the same module, which is standard Emscripten (**measured**). So there is no JIT. That is what separates this from running the binary under an emulator, and it is why the build can be fast enough to play at all: the browser compiles 74 MB of code once, with its optimising tier, and the game's inner loops run as native wasm (**reasoned**).

The [AnyPS5 article](/articles/anyps5-static-relinker#is-it-really-not-an-emulator) compares the same family of tools for consoles: N64Recomp turns MIPS into C, XenonRecomp turns Xbox 360 PowerPC into C, and each is a per-game project with function boundaries found by hand and per-game patches. SpeedRecomp has the same shape for 32-bit x86 Windows, with the 686 `config` entries in its table as the hand-found boundaries.

## Rebuilding Windows inside the module

A binary calls Windows, so whatever runs it has to be Windows. The strings name 51 Windows DLLs (**measured**), from `KERNEL32.dll`, `USER32.dll`, `GDI32.dll` and `ADVAPI32.dll` to `d3d9.dll`, `DDRAW.dll`, `DSOUND.dll`, `DINPUT8.dll`, `XINPUT1_3.dll`, `WINMM.dll` and `WS2_32.dll`. A `LoadLibrary` of anything else gets "no shims for that module, reporting it as missing", and `GetProcAddress` of an unknown function gets "-> 0 (no shim registered)".

Some of what the shims do is visible in their log formats (**measured** strings, **reasoned** reading):

- **Structured exception handling.** MSVC's C++ exceptions sit on Windows SEH: a chain of registration records hanging off `FS:[0]`. The runtime keeps that chain itself (`SEH enter: registration=%08x established=%08x ESP=%08x handler=%08x`) and checks it (`SEH chain violation`). When translated code calls a bad address, it raises "an access violation, as Windows would, for the guest's handlers".
- **Threads.** `CreateThread` can run a guest thread for real, run it "synchronously to completion" or skip it, chosen by `RECOMP_CREATETHREAD`. A scheduler logs `[sched] %s (thread %zu); baton %zu`, and one error reads `block without the baton`. Guest threads appear to take turns holding a single baton, like a global lock, rather than running in parallel. The engine's own WebAssembly threads do run in parallel: a `RenderThread` with `sync_on_pump` wrappers around `host_d9_present`, texture uploads and texture reads.
- **Video.** Most Wanted's cutscenes are VP6. The runtime also has shims for Bink and Smacker, which this game does not use; Bink opens return "finished video record ... (no decoder)".
- **A watchdog.** "watchdog: %.0fs elapsed with no response" ends a run whose guest has wedged instead of leaving a frozen tab.

Input goes through SDL3 and Emscripten's HTML5 layer: keys, wheel, pointer lock, full screen and the Gamepad API, delivered to the game through the DirectInput and XInput shims. The runtime even names its own device, "Recomp Virtual Pad" (**measured**). Audio is a ring of stereo float samples in shared memory with 16 control words, read by an `AudioWorkletNode` named `recomp-pcm` (**measured**, from `audio-worklet.js`). It waits for 1,024 frames before starting and counts underruns, which is the same shape as the GTA V port's audio ring. Before the ring, a DirectSound shim and a host mixer stand in for the game's sound card; one message reports fading a two-millisecond seam when "the guest is writing close behind the cursor".

### Direct3D 9 on WebGPU

The renderer is Direct3D 9, re-implemented in the host and drawn with WebGPU (**measured**: the WebGPU imports, and strings such as `d3d9 webgpu: frame %llu: %.1f fps, %.0f draws/frame, %zu textures, %zu pipelines`). The runtime page requests the device itself, asking for `texture-compression-bc`, `depth32float-stencil8`, `float32-filterable`, `unorm16-texture-formats` and `rg11b10ufloat-renderable` when the adapter has them, and passes it to Emscripten as `preinitializedWebGPUDevice`.

Shaders are the interesting part. A Direct3D 9 game hands the driver compiled vertex and pixel shader bytecode, shader models 1 to 3. The strings `d3d9 dis %s:`, `d3d9 wgsl %s %s:`, a `D3D9_WGSL_DUMP` switch and `d3d9 webgpu: a %s shader is not translated: %s` say the host disassembles that bytecode and writes WGSL from it while the game runs (**measured** strings; **reasoned** that translation happens per shader at run time, since the build ships no shader pack and the game's shaders are in its own files). GTA V's port translated 4,919 shaders ahead of time and shipped them as packs; this one does it on the fly, which a 2005 game's smaller, simpler shaders make affordable (**reasoned**; I did not time it).

There are seams where the two APIs disagree, and the host logs them: "a render target read is not supported in the browser", and "texture format %08x is drawn as BGRA8" for formats WebGPU lacks. There is also a fallback: "no WebGPU Direct3D 9 renderer; the game draws on the CPU", with a `d3d9 raster` path that skips any draw with no shader bound because "the fixed-function pipeline is not rendered" there.

### Mods instead of patches

The data package carries two mod manifests (**measured**). The first, `core.nfsmw`, explains itself:

```toml
# The game's own settings that the original PC release fixed in code: the
# engine's simulation rate and the rendering resolution. What the community
# widescreen fix (ThirteenAG, MIT) did with binary patches, done here with
# hooks and data writes, because patched code bytes mean nothing to a
# recompiled game. See NOTICE and docs/analysis.md.
id = "core.nfsmw"
game = "80774c2e5d619b4f120b48d4462896fd504c263399d203a238769cffde1d253c"
affects_simulation = true

[settings]
sim_rate = { type = "int", default = 120, min = 30, max = 480, label = "Simulation and frame rate (Hz)" }
widescreen = { type = "bool", default = true, label = "Widescreen view and HUD" }
fov_scaling = { type = "int", default = 1, min = 0, max = 2, label = "Field of view (0 original, 1 Xbox 360, 2 exact)" }
```

That comment is the cleanest statement of what static recompilation changes for modding. For two decades the PC game has been modded by patching its machine code, and [ThirteenAG's widescreen fix](https://github.com/ThirteenAG/WidescreenFixesPack) for Most Wanted is one of the best-known examples. Under a recompiler, those bytes were already translated before the game ever ran, so a patch to them changes nothing. A fix has to replace a translated function, by its address, which is what the hookable entries in the symbol table are for. The second manifest, `core.testdrive`, is a test-drive mod for the same executable. Both name a `.dylib` plugin, a macOS library, so the same runtime is built natively for a Mac as well as for the browser (**reasoned**).

<RepoCard repo="ThirteenAG/WidescreenFixesPack" />

## A kit, not a one-off

Several things in this build were not written for Most Wanted (**measured** strings and resources):

- The DLL list includes `d3d11.dll`, `dxgi.dll`, `d3dcompiler_47.dll`, `DDRAW.dll` with the Direct3D interfaces up to version 7, `binkw32.dll`, `smackw32.dll`, `mss32.dll` (Miles), `fmod.dll`, `QMIXER.dll` and `CGalaxy.dll`. Most Wanted uses none of these.
- The data package includes `classic-modes.json`, a test report for "seven shipping RGB565 modes" of a DirectDraw game, recorded on a machine named "Apple M5 Max".
- A comment in the runtime page says `tools/web_launcher.py` places each game's web build in `<launcher>/<game id>/`, and the library page is titled "Recomp games", plural.

So `SpeedRecomp` is one game built on a general kit for recompiling 32-bit Windows games, with a Windows layer wide enough for several eras of DirectX, and Most Wanted is the game it ships with (**reasoned**). I searched GitHub for three of the kit's distinctive strings (`RECOMP_WEB_ASSET_BASE`, `recomp-stream-v1`, `SpeedRecomp`) and found no public copy. As far as I can tell, the kit is not open source.

## Placing it

The [NFSMW Online article](/articles/nfsmw-online#what-it-takes-to-put-a-2005-windows-game-in-a-tab) laid out four ways to put a 2005 Windows game in a tab: recompile the original source, recompile decompiled source, run the shipped binary, or write a new engine for the original data. With the engine in hand, the third path splits in two. Translating the binary ahead of time and translating it as it runs leave different files behind, and every row below is something I measured:

<PortPlacer />

For readers without the widget:

| Evidence | Original source | Decompiled | Binary, ahead of time | Binary, at run time | New engine |
|---|---|---|---|---|---|
| Starts from `speed.exe`, pinned by SHA-256 | strains | strains | fits | fits | strains |
| 42,545 Ghidra-named addresses, no EA names | strains | strains | fits | strains | strains |
| 74 MB of code, no run-time code generation | neutral | neutral | fits | strains | neutral |
| "no block entry for indirect jump" | strains | strains | fits | strains | strains |
| 51 Windows DLLs, SEH, CreateThread rebuilt | strains | neutral | fits | fits | strains |
| Fixes as hooks, not byte patches | strains | strains | fits | strains | strains |
| 20,417 entries share the decompilation's starts | neutral | strains | fits | neutral | neutral |

Every row fits ahead-of-time translation, and no other path fits more than two. This is the shipped binary, translated once.

That also sharpens the comparison with the two sites I covered before. The GTA V port compiled Rockstar's own C++ (its build is consistent with GTA V's leaked source, as that article says), so it had names, types and a platform layer to swap. This port had only the executable a player installs, and it got there anyway, at the price of rebuilding Windows. NFSMW Online, from its changelog, keeps restoring "original game" behaviours one at a time, which points away from running the original code. This port runs exactly the original code, so the police already route on the road graph and the speeding line is already wherever EA put it, and the hard problems move to the edges: the API layer, the shaders, the files.

The sibling question was whether any of these share code. I found nothing linking this build to NFSMW Online. Its client sits behind a login, and [a look behind it](/articles/nfsmw-online#23-october-behind-the-login) found a different path altogether: a new engine written in Rust on wgpu, reading game data converted by the project's own tools, with no trace of `speed.exe` or of this build's kit. `nfsmwo.com`, Most Wanted Online, is a different kind of project again: its main page asks players to install the PC game, version 1.3 in English, and .NET Framework 4.7.2, a mod for the native game (**measured** from its public page). It also runs a browser build at `web.nfsmwo.com`, but a different one: the shipped `speed.exe` under Wine compiled to WebAssembly, drawing with WebGL 2, as [the NFSMW Online article's 23 October section](/articles/nfsmw-online#23-october-nfsmwocom-runs-the-shipped-game) measured. That is run-time translation of the same binary this build translates ahead of time (**reasoned**). All three target PC 1.3. That is the version the decompilation targets too, and the final PC patch, so it is common ground rather than a connection (**reasoned**).

## Who serves what

Neutrally, then.

EA owns Need for Speed: Most Wanted: its code, its executable and its data. The port distributes two things, from two places (**measured**):

1. **The engine**, from itch.io: `SpeedRecomp.wasm`, its glue, and `symbols.json`. The module is not EA's file, but its 74 MB of code is a mechanical translation of the machine code in EA's `speed.exe`, function by function. The symbol table is a map of EA's executable.
2. **The game**, from `cdn.justplayweb.com`: 1,398 files and 2,972,904,233 bytes of the retail PC game, including `speed.exe`, served with `Access-Control-Allow-Origin: *` and fetched automatically by the itch.io build for anyone who clicks Start game. No account, purchase or ownership check is involved in that path.

The build also contains a path that does not distribute the game: import your own installed copy into the browser, checked against the same SHA-256. The launch page on itch.io does not use it.

What the developer says is on the page: "Unofficial port. Original game and poster: Electronic Arts / EA Black Box." The page makes no claim of permission, and I found no statement from EA about the port either way. The listing is free. The decompilation's README, quoted above, states that community's view of ports built ahead of it.

The nearest precedents are on this site already: Take-Two's DMCA notice against the reVC browser build, which asked players to supply their own files, and the takedown of `playgta5.com`, which served Rockstar's data to every visitor. Both are in [the ports that came before](/articles/gta5-in-the-browser#the-ports-that-came-before). EA has not, as far as I can find, acted against either this port or NFSMW Online. I am describing, not predicting, and I have not linked the CDN's files or described how to use them.

## What I did not see

- **A frame of the game.** My headless Chromium had only SwiftShader, which the page itself calls software rendering, and I stopped the run at `speed.exe` because the next step is EA's files. I have not seen it render, and I cannot say how fast it runs.
- **The executable.** I did not download `speed.exe`, so I cannot compare it with a retail 1.3 install or measure how much the 74 MB of code expands from its code section. The SHA-256 in `games.json` is the build's claim.
- **Which files a session reads.** Without running the game I cannot say how much of the 2.97 GB a first race pulls. The 8 MiB blocks and the 128 MiB in-memory cache are from the code; the traffic is unmeasured.
- **The translator.** `translate.py`, `kit` and the native launcher are not served. I have their output and their error strings, not their source.
- **Who runs the CDN.** The itch.io build points at it and the portal says it hosts the game. Neither says who is behind the portal.

## How I checked

- Downloaded the itch.io page, the developer's profile, the comments page (empty) and the three listed images (all the same 725,804-byte box art). Read the HTML5 iframe's `index.html`, `ui/player.mjs`, `ui/gpu.mjs`, `ui/launch-settings.mjs`, `nfsmw/runtime.html`, `nfsmw/profile-settings.js`, `nfsmw/input-hints.js`, `nfsmw/audio-worklet.js`, `worker.js`, `core.js`, `library.html`, `games.json` and `nfsmw/assets.json` in full.
- Downloaded `SpeedRecomp.js`, `.wasm` and `.data` into a scratch folder, parsed the module's sections, imports, exports, code bodies and table with a Python script I wrote, extracted the data package's four files, and searched 60,206 printable strings. Deleted all of them afterwards. I committed none.
- Loaded the launch page in headless Chromium (Playwright, SwiftShader WebGPU, 1280x800), clicked Start game and logged every request, with request interception aborting everything bound for `cdn.justplayweb.com`. The only CDN request was `speed.exe`, aborted after a HEAD for its size.
- HEAD requests for two CDN files' headers. I downloaded no game file and no byte of one.
- Cloned `dbalatoni13/nfsmw` and compared its `config/SPEED_EXE_1_3/symbols.json` with the port's table address by address. Cloned `ThirteenAG/WidescreenFixesPack` to confirm the Most Wanted fix and its MIT licence.
- Read the developer's other ports' loaders, `justplayweb.com`'s game and about pages, and `nfsmwo.com`'s public page. Searched GitHub code for the kit's strings.

Everything here is from 23 October 2026. The build was updated two days earlier and will have changed by the time you read this.
