~/satyajit

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

mdjsonmcp

2026-10-23 · 30 min · webgpu · reverse-engineering · compilers

A 1:55 narrated explainer, drawn in code. Every number and picture in it is this article's own; the sources are below.

› transcript

Hi, I'm Wainscot! Need for Speed Most Wanted now runs in a browser tab, and nobody had its source code. So how? The idea: translate the game's own executable to WebAssembly once, ahead of time, and rebuild the Windows it calls. Start with the executable a player installs. A disassembler finds every function and jump target in it. Each is translated once, at build time. The names are the disassembler's, never EA's. Out comes one WebAssembly module. There is no just in time compiler. The game still calls Windows, so a host answers: threads, exceptions, input, and Direct three D nine, drawn with WebGPU. Translating ahead of time has two classic failure points. A jump to a computed address must land on a block the translator already knows. And an instruction it does not model must never run. Old mods patched bytes. Here, fixes replace whole functions by address. Only the executable is copied whole. Every other file stays on a third-party CDN. When the game reads, the tab fetches eight megabyte blocks and keeps them. The game just sees a folder under Program Files. 42,545 addresses inside speed.exe in the symbol table it ships. The GTA Five port compiled the engine's own source, names and all, and swapped its platform layer. This one had only the executable. So it runs exactly the original code, and rebuilds Windows around it. With no source, the game's own logic survives exactly. The hard work moves to the edges: Windows, the shaders and the files. The executable, translated once. Windows, rebuilt on WebGPU. And the game's files, streamed from someone else's server. Every source is in the full article. I'm Wainscot. Bye!

Why read this

Notabletop 60%

A teardown of a statically recompiled 2005 Windows game in the browser: the wasm, its Ghidra symbol table and its file streaming, read from the served files.

  • Analysis found nowhere else
  • A lasting reference
  • A new technique

GPUs, kernels & systemsNothing to runProprietaryPractitioner teardown

How this was scored
Is it new?
2 of 3: A real new idea, method or capability
Can I trust it?
2 of 3: Measures key facts from files, code or configs
Can I run it?
0 of 3: Closed, nothing to run
Will I understand it?
2 of 3: Mechanism from first principles with figures
Can I act on it?
2 of 3: A concrete recipe, numbers or comparison
Will it last?
2 of 3: A reference for a year or more
Does it affect many?
1 of 3: A specialist community
Only here?
3 of 3: The only place this analysis exists

Score 65 of 100, ranked 230 of 607 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

Two weeks ago I took apart GTA V running in a browser tab: Rockstar's engine compiled from its own C++ source to 64-bit WebAssembly. This week I read 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.

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.
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.

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

PhaseFilesBytesHost
Launch page7955,474html.itch.zone
Start game: checks, worker, manifest636,818html.itch.zone
speed.exe16,029,312cdn.justplayweb.com
Engine: glue, wasm, data package, worklet, settings616,650,180html.itch.zone
Total before the first game-file read2023,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):

{
  "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.

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.
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):

FolderFilesBytesWhat it holds
SOUND530950,176,167music, speech, effects; MW_Music.mus alone is 533,877,888
MOVIES37844,184,828the story cutscenes as .vp6 video
TRACKS273621,656,659Rockport; STREAML2RA.BUN alone is 532,663,556
CARS344390,153,664car geometry, textures, vinyls
FRONTEND963,192,835menus
GLOBAL3953,503,164shared data
NIS8740,403,632in-engine cutscene data
speed.exe16,029,312the executable
LANGUAGES, MEMCARD, SUBTITLES783,603,972text and stubs
Total1,3982,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:

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. 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:

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

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, whose PC 1.3 symbol table names 15,263 of its 46,360 functions:

AddressPort's namePort's aliasDecompilation's name
0x005d59f0FUN_005d59f0nfsmw_query_interfaceUTL::COM::Object::_IList::Find
0x006123b0sub_006123b0.blk_006123b0nfsmw_force_aiGame_ForceAIControl
0x00612420sub_00612420.blk_00612420nfsmw_clear_aiGame_ClearAIControl
0x0064b380FUN_0064b380nfsmw_main_loop_debugMaybeDoMemoryProfile
0x006c1780FUN_006c1780nfsmw_clamp_graphicsunnamed (sub_6C1780)
0x006c1990FUN_006c1990nfsmw_graphics_presetunnamed (sub_6C1990)
0x0078fa00FUN_0078fa00nfsmw_read_online_configOnlineCfg::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. 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.

dbalatoni13/nfsmw@80993f3 · snapshot 2026-10-23
tracked files
3,672
license
CC0-1.0
branch
main
tests
none found
source
23.5 MB
commit date
2026-10-10
source by language
C13.2 MB(1687)C++9.6 MB(1633)Python713.4 kB(48)JavaScript37.0 kB(1)Dockerfile1.0 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-10-23 at 80993f3 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow

shallow clone: counts describe the pinned tree, not the history

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):

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 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):

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:

# 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 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).

ThirteenAG/WidescreenFixesPack@708f752 · snapshot 2026-10-23
tracked files
2,990
license
MIT
branch
master
tests
none found
source
29.2 MB
commit date
2026-10-08
source by language
C++27.9 MB(215)C1.1 MB(50)Python56.1 kB(9)PowerShell56.0 kB(10)Lua43.6 kB(3)JavaScript8.4 kB(2)Makefile6.8 kB(20)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

local clone, 2026-10-23 at 708f752 — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow

shallow clone: counts describe the pinned tree, not the history

A kit, not a one-off

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

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 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:

For readers without the widget:

EvidenceOriginal sourceDecompiledBinary, ahead of timeBinary, at run timeNew engine
Starts from speed.exe, pinned by SHA-256strainsstrainsfitsfitsstrains
42,545 Ghidra-named addresses, no EA namesstrainsstrainsfitsstrainsstrains
74 MB of code, no run-time code generationneutralneutralfitsstrainsneutral
"no block entry for indirect jump"strainsstrainsfitsstrainsstrains
51 Windows DLLs, SEH, CreateThread rebuiltstrainsneutralfitsfitsstrains
Fixes as hooks, not byte patchesstrainsstrainsfitsstrainsstrains
20,417 entries share the decompilation's startsneutralstrainsfitsneutralneutral

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 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 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. 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

How I checked

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

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "NFS Most Wanted on itch.io: EA's 2005 speed.exe, recompiled ahead of time to WebAssembly", ai.thesatyajit.com, October 2026.

bibtex
@misc{ghana2026nfsmwwebport,
  author = {Satyajit Ghana},
  title  = {NFS Most Wanted on itch.io: EA's 2005 speed.exe, recompiled ahead of time to WebAssembly},
  url    = {https://ai.thesatyajit.com/articles/nfsmw-web-port},
  year   = {2026}
}
share