Deus Ex in the Browser
2026 · JavaScript · Rust · WebAssembly · three.js · WebGL2 · local only · BSP notes · Debugging notes

Deus Ex in the browser is a runtime that loads the original 2000 Deus Ex (Game of the Year Edition) straight from a local install and runs it in a browser tab. It is not a remake, and nothing is converted ahead of time. The game's own code runs: a JavaScript reader parses the Unreal Engine 1 packages, and the UnrealScript bytecode inside them, which holds the gameplay, is executed by an interpreter written in Rust and compiled to WebAssembly. What is rebuilt is the engine underneath: the native C++ the scripts call into, translated into Rust and JavaScript, with three.js drawing the result on WebGL2. Maps, scripts, textures, meshes, sounds and music all come from your own copy, read in place through the folder picker. Nothing is uploaded.
That is also why there is no demo link at the bottom of this page. The runtime is useless without the original game data, and that data stays on your disk. Everything below was captured on my machine in October 2026, from the build as it stands today.
Tonnochi Road, Hong Kong: a Luminous Path member walks up — "You are an unfamiliar face on Tonnochi Road" — and the game's own conversation camera takes over. Recorded from headless Chromium at 1280×720.
One rule: 100% original
The project has a single rule, and everything else falls out of it: every behaviour must come from evidence. That means the original data, its bytecode, analysis of its native code, or reproducible measurements of the original game running. Lookalike substitutes are banned. Passing our own tests does not count as evidence, and anything that has not been verified stays labelled unsupported.
UnrealScript is the easier half. The bytecode ships inside the packages, so an interpreter that follows it faithfully gets the gameplay logic without rewriting any of it, as far as the engine beneath it reaches. The hard half is that engine: the native C++ the scripts call into for physics, rendering, lighting, fire and water textures, music, the UI and the HUD, compiled for 32-bit x86 in 2000 and shipped as DLLs. That code has to be translated, and translation is exactly where lookalikes creep in.
Oracles: how you prove a translation is right
The original DLLs are the authority, each pinned by its SHA-256. They never run in the browser — the runtime does not execute Windows binaries. They run on the side, as references the translation has to match.
- Native oracles. Python harnesses load the original function bodies into the Unicorn CPU emulator, feed them the same inputs as the JavaScript or Rust translation, and diff the results byte for byte. Planted wrong bits and mutants have to be caught, or the oracle itself is not trusted. The repository has 354 oracle scripts and 576 validators.
- Captures of the running original.
DeusEx.exeruns under Wine. Both sides get the same console inputs —ghost,ShowHud False, an exact location and view rotation — the level is frozen withbPlayersOnly, and its clock is held withTimeDilation 0.001. Raw 1280×720 frames are then compared on mean absolute difference, Sobel edge recall and precision, and a 16×9 luminance grid. The October 7 pass covered 26 maps and 130 views, with 124 matching after a fix. - Live scenes. A small tool attaches to the original like a debugger and breaks on
ULevel::Tick. Each tick it records the time delta, the clock, the C runtime's random-number state and the fields of the watched actors. The browser then replays exactly those inputs. On three maps, every watched actor (59/59, 20/20 and 50/50) agrees tick by tick for ten seconds, and a sweep runs 28 maps for 1,200 ticks each; its latest replay agrees on 957 of 989 watched actors. - State digests. In headless runs of Training and Liberty Island, the JavaScript VM and the Rust VM produce the same state digest at all 30 checkpoints, one every ten ticks, and the worker build produces the same digests and frames as the inline one.
flowchart LR
D[Original DLLs, SHA-256 pinned] --> U[Unicorn harness]
T[Translation in JS or Rust] --> U
U -->|byte-for-byte diff| V{Match?}
W[DeusEx.exe under Wine] --> C[Frame captures and tick traces]
B[Browser runtime] --> C
C --> V
V -->|no| T
V -->|yes| A[Behaviour accepted]
Working this way turns up things you would never think to fake:
- The x87 FPU control word was read inside the running original, per phase of the frame (
0x007f,0x027f,0x037f), because rounding and precision change what the game computes. printfandatofbehaviour comes from the game's ownMSVCRT.dllunder Wine: 290k printf rows and 11.5k atof rows, so the options menus print numbers exactly the way the original does.- The x87
FCOSinstruction is not correctly rounded at 5,820 of 44,101 tested inputs, and the stored reverb coefficients still match the model. - A half-pixel offset between the two images was traced to Render.dll's
FX15 = (SizeX + 1.0001)·0.5and D3DDrv's −0.5 vertex offset. The browser now reproduces it. - When a colour argument is omitted, one UI window call leaves the alpha byte holding whatever sat in a reused argument slot: the high byte of the caller's stack-frame address.
Five weeks, 995 commits
The first commit landed on September 9, 2026 with the message Render original Deus Ex training map in the browser. By October 15 the repository had 995 commits across 32 days of work, 31,259 lines of Rust, about 175k lines of JavaScript in the runtime (288k with the tooling and tests), and 357 design documents.
The work is staged, and a stage only closes when its acceptance gate passes:
| Stage | Scope | Status |
|---|---|---|
| 0 | Validate every package | done |
| 1 | Render Training with an inspection camera | done |
| 1.5 | The original static lighting | done |
| 2 | A small playable slice | delivered |
| 3 | Script and native runtime, object lifecycle | done |
| 4 | Presentation | in progress |
| 5 | Gameplay systems: AI, combat, stealth, RPG | open |
| 6 | Saves, map travel, the full campaign | open |
| 7 | Browser compatibility, performance, reliability | open |
| Date | Milestone |
|---|---|
| Sep 9 | First commit renders the Training map; static lighting and the first playable slice land the same day |
| Sep 17 | A shared Unicorn probe harness |
| Sep 20 | The first Rust kernel compiled to wasm: mesh posing |
| Sep 27 | Stage 3 gate closed: 406 validators and 545/545 unit tests pass |
| Sep 28 | All-maps census: 83/83 single-player maps reach Stage 3; mesh lighting, moving lights, mirror floors in Training |
| Sep 30 | 120 fps becomes the target; the Rust interpreter becomes the default; the GPU mesh path |
| Oct 1 | Conversation camera and lip sync |
| Oct 2 | Every static mirror, Fire.dll fire and water, animated textures and lights, particles, coronas, volumetric fog, lasers, ambient sound; 77/83 maps clean in the conversation census |
| Oct 4 | BSP occlusion in Rust; the simulation moves to a worker by default |
| Oct 6 | Galaxy music in an AudioWorklet, sound effects and reverb through Galaxy's own mixer, decals; visual comparison on 14 maps |
| Oct 7 | 26 maps compared; mirror and sky child frames; mesh LOD; security computers in Play |
| Oct 8 | Options menus |
| Oct 11 | Camera views as full frames; CRT shaders |
| Oct 13 | Live scenes compared tick by tick; a 28-map sweep |
| Oct 14 | Frame projection bit-exact in the vertex shader; security camera views compared with the original |
| Oct 15 | Firefox defaults to the inline simulation |
Architecture
flowchart LR
I[Your Deus Ex install: 219 packages] -->|folder picker, read in place| R[Package reader, JS]
R --> M[Object memory: one WebAssembly.Memory]
subgraph Worker[Simulation worker]
VM[script-vm, Rust to wasm]
end
M --> VM
VM -->|snapshot, transferred buffers| P[Page: three.js on WebGL2]
P --> G[GPU mesh passes]
VM <-->|MessageChannel| A[AudioWorklet: galaxy-kernel]
The package reader is plain JavaScript, shared between Node and the browser. The install holds 219 packages — 88 maps, 38 object and script packages, 56 texture packages, 35 music packages and 2 sound packages — with 1,958,568 visible triangles across the maps. Training alone is 70 textures and 24,427 triangles.
Object memory is a single WebAssembly.Memory holding every object's property image, the interpreter's frames and statics, and the lowered script code, with object references stored as handles, so the Rust side reads and writes game state without copying it. Names and strings still live on the JavaScript side.
Scripts run from the original bytecode, translated on the fly. The first time a function is needed, its bytecode is read from the package and decoded into a tree, which becomes two things: JavaScript closures, the reference interpreter, and a compact word code for the Rust interpreter, which is the default. Anything the Rust side cannot evaluate yet drops back to the JavaScript closure for that one expression, so every function runs either way.
Three Rust crates compile to wasm32:
| Crate | Lines | What it runs |
|---|---|---|
script-vm | 24,195 | The UnrealScript VM, tick dispatch, physics, BSP occlusion, render and light scans, particles, Fire.dll, fog, lip sync |
galaxy-kernel | 5,552 | The Galaxy music and sound engine |
lodmesh-kernel | 1,512 | Mesh posing |
Natives not yet moved to Rust are JavaScript functions the interpreter calls through imports, and each port keeps its JavaScript or oracle reference behind a switch, a URL parameter or an environment variable, so the two can be compared on demand.
Why Rust
On September 29, UNATCO Island ran at about 46 to 50 ms per frame, and the simulation tick was 25 to 29 ms of it. The profile was damning: about 300 ns per expression node, 116,000 receiver-to-instance lookups per tick, and 2,584 of the 2,586 actor objects stuck in V8's dictionary mode, so every lookup was a hash. Exact x87 arithmetic also needed BigInt in JavaScript where Rust has plain u128. After the port the tick dropped to 13.5–14 ms, against about 25 ms in JavaScript. Almost every script statement now runs in Rust; most of what is left of a tick is the engine code around the scripts, natives and physics glue still in JavaScript, which is moving to Rust port by port.
Why a worker, and why the GPU
With the simulation on the main thread, a frame took 25.5–27.5 ms against 12.9 ms without it. The tick, HUD, scans, BSP occlusion and Galaxy updates now run in a module worker that posts a plain-data snapshot with transferred buffers, at the cost of one frame of latency. Mesh posing moved to WebGL2 passes for the same reason: the frame had 1.46 ms of GPU work against 30.6 ms of host work. The GPU path is bit-exact with the CPU kernels, down to a barrier that stops ANGLE on Metal from contracting multiplies and adds into fused operations.
Galaxy, the game's music engine, runs in an AudioWorklet. The page hands one end of a MessageChannel to the worklet and the other to the worker, so music costs the page nothing per frame: each 20 ms render takes 135 µs at the median.
The places
Every frame and clip here is the browser runtime, captured this week from headless Chromium on Metal (stills at 1920×1080, clips at 1280×720) with the viewer's page chrome hidden. The HUD, location titles, infolinks and conversation letterboxes are all the game's own. Hover a frame to swap the dither for the real thing, or click it to open the full 1920×1080 frame and step through the rest with the arrow keys.
The intro: Bob Page and Walton Simons, cut together by the game's own conversation camera, lip sync included.

Training's lobby, the first map that ever rendered, now with its mirror floor · Liberty Island from the dock, statue and skyline included.

Alex Jacobson's infolink typing itself in on the dock · a UNATCO trooper at the headquarters door.

Anna Navarre at Battery Park, in a letterboxed conversation · the subway station under Hell's Kitchen.

Location: unknown — the MJ12 cell · then Hong Kong, on the helipad atop the VersaLife building.

Tonnochi Road · the Wan Chai market.

VersaLife's front doors · Tracer Tong's laboratory.

The Chateau DuClare garden outside Paris · Toby Atanwe, offering a ride to Everett.

A rescued scientist at Vandenberg · Area 51, with an incoming transmission.
Chasing 120 fps
The target is 120 fps — 8.3 ms per frame — on an Apple M2 Pro at 1440×960. It has been a long road, and most of it is in the measure first school: interleaved A/B runs, medians, and no fix without a number behind it.
| When | Where | Before → after |
|---|---|---|
| Sep 26 | Training, JavaScript tuning | 29.7 → 17.3 ms per frame |
| Sep 28 | Liberty Island, walking | 86 → 57 ms per frame |
| Sep 30 | Liberty Island tick, JavaScript VM → Rust VM | ~25 → 13.5–14 ms |
| Oct 4 | Paris club tunnel, BSP occlusion | 15 → 59 fps |
| Oct 4 | Firefox, light-map uploads with the mirror floor in view | 38.9 → 0.31 ms per frame |
| Oct 14 | Worker, one temporary list per BSP leaf removed | 10.2–10.6 → 7.9–8.3 ms per frame (median) |
| Oct 14 | Chromium worker heap after the leak fixes | 601 / 818 / 1,124 → 387 / 305 / 386 MB |
The leak hunt read like a detective story. On Paris's Everett map, three.js called createBuffer 14,657 times and deleteBuffer 20 times in five minutes, and the previous map's runtime stayed alive after every map change. The fix released its handles and deliberately pushed two V8 retainers into dictionary mode — the same mode that had been the cost on September 29.
Firefox had one more surprise: Gecko runs a full compacting garbage collection on an idle worker about every 5.5 seconds. Even after the leak fix those pauses still cost 208–398 ms on my Mac, down from 1.1–2.4 s. The inline simulation had no pause over 200 ms at the same 111 fps, so Firefox now defaults to inline, and the worker stays one URL switch away.
Built with agents
I could not have done this alone in five weeks, and I didn't. 815 of the 995 commits carry an AI co-author line, 787 of them Claude's, from September 13 on; Devin and Cursor also appear on a few dozen in the second week. The agents read the bytecode, wrote the Unicorn harnesses, ported the natives, ran the captures against the original and wrote most of the 357 docs. My job looked more like a lead's: set the rule, pick what to attack next, and decide when a piece of evidence was good enough.
The fidelity rule is what makes that work. An agent is very good at producing something that looks right, and looking right is exactly what the rule bans. Every claim has to land on an oracle the agent did not write — the original DLL in an emulator, a frame from the running game, a tick trace from the real process. An agent can be wrong as often as it likes; it cannot be wrong and still pass.
My favourite part was the moment it started to feel alive: not a render of a map any more, but the game itself running in a tab.
Liberty Island: the location title types in, JC walks the dock, and Alex Jacobson's infolink arrives. The funniest was a night when Claude was playing the game on its own, going back and forth between the native and the web versions in the Paris club to compare them, and woke me up.
It feels like a new era for reverse engineering. Reimplementations like this used to be multi-year community efforts; now one person with agents can take a closed binary apart, measure it, and rebuild its behaviour in the open in a few weeks. I see people pointing the same kind of tooling at much bigger closed targets, from the PS5 to Photoshop, and that raises real questions about proprietary software. If the behaviour of a binary can be recovered this cheaply, what does closed source still protect — the code, or mostly the licence, the assets and the law? This project stays on the careful side of that line: it ships none of the original data, runs only on your own copy, and never executes the original binaries in the browser. But the question is not going away.
Where it stands
Stage 4, presentation, is in progress. All 83 single-player maps start, tick for ten seconds, reload from the player start and tick again without a crash or an unexplained boundary. With up to ten conversations per map played to their end, 77 run clean; the other six stop at the AI navigation code, path-finding natives and iterators, that belongs to Stage 5. Stage 4 got its own gate on October 15: 70 criteria, 44 of them passing on the first run. Combat, stealth and the RPG systems come next, then saves, map travel and the full campaign.
It is very much ongoing, rough edges and open bugs included. It is also the kind of project where the scaffolding is the real work. The renderer is a few weeks old; the harnesses, oracles and captures that keep it honest are what will let it reach the end of the game without turning into a lookalike.
Back to Experiments · Spatial acceleration structures · Debugging Graphics Without Guessing · Home.
















