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.

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):
- It takes a Web Lock named
recomp-player-nfsmw, so a second tab gets "The game is already running in another tab". - It asks for persistent storage with
navigator.storage.persist(). - It opens a second iframe,
nfsmw/runtime.html, which refuses to continue unlesscrossOriginIsolatedis true andnavigator.gpuexists. - It starts a worker that reads
games.jsonand 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):
| 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):
{
"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.

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:
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 StorageA 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):
- 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
namesection, noproducerssection 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 withINITIAL_MEMORY=536870912and pre-spawns a pool of 8 pthread workers. A 32-bit game's pointers fit 32-bit WebAssembly, so this port does not need thewasm64that GTA V did. - Imports. 353 functions: 56 WebGPU, through Dawn's
emdawnwebgpubindings; 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.16in its version string, with anSDL2_COMPAThint), 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, 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. 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.
- license
- CC0-1.0
- branch
- main
- tests
- none found
- source
- 23.5 MB
- commit date
- 2026-10-10
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):
recomp: no block entry for indirect jump to 0x%08x from 0x%08xis the missed target.unmodelled SSE instruction at %08x ... the guest used a CPU extension recomp_cpuid does not advertiseis an instruction the translator does not model. The runtime answersCPUIDitself, so the game only takes code paths it knows.recomp_shim_call: %08x is not an allocated trampolineis 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 stdcallis 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 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.
CreateThreadcan run a guest thread for real, run it "synchronously to completion" or skip it, chosen byRECOMP_CREATETHREAD. A scheduler logs[sched] %s (thread %zu); baton %zu, and one error readsblock 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: aRenderThreadwithsync_on_pumpwrappers aroundhost_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:
# 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).
- license
- MIT
- branch
- master
- tests
- none found
- source
- 29.2 MB
- commit date
- 2026-10-08
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):
- The DLL list includes
d3d11.dll,dxgi.dll,d3dcompiler_47.dll,DDRAW.dllwith the Direct3D interfaces up to version 7,binkw32.dll,smackw32.dll,mss32.dll(Miles),fmod.dll,QMIXER.dllandCGalaxy.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.pyplaces 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 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:
| 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 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):
- The engine, from itch.io:
SpeedRecomp.wasm, its glue, andsymbols.json. The module is not EA's file, but its 74 MB of code is a mechanical translation of the machine code in EA'sspeed.exe, function by function. The symbol table is a map of EA's executable. - The game, from
cdn.justplayweb.com: 1,398 files and 2,972,904,233 bytes of the retail PC game, includingspeed.exe, served withAccess-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
- 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.exebecause 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 ingames.jsonis 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,kitand 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.jsonandnfsmw/assets.jsonin full. - Downloaded
SpeedRecomp.js,.wasmand.datainto 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 wasspeed.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/nfsmwand compared itsconfig/SPEED_EXE_1_3/symbols.jsonwith the port's table address by address. ClonedThirteenAG/WidescreenFixesPackto confirm the Most Wanted fix and its MIT licence. - Read the developer's other ports' loaders,
justplayweb.com's game and about pages, andnfsmwo.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.