Quake runs in any browser via pure Rust and WebAssembly

Quake Lake d 19970627

You can spin up the 1996 first‑person shooter Quake in any modern browser, and you won’t need a dedicated graphics card to do it. The whole engine has been rewritten in pure Rust, compiled to WebAssembly, and now runs on the CPU alone. It feels oddly modern to watch that classic’s pixel‑perfect level geometry crawl across a page that otherwise hosts your inbox and social feeds. I’m impressed by the technical elegance, but I’m also a little wary of how much we’re leaning on the browser to replace a platform that originally demanded a 3D accelerator.

Drop the original Quake folder, or just the .pak files, onto the page and the engine picks them up automatically. If you have episodes 2–4 you can add id1/pak1.pak, and for the mission packs you can drop hipnotic/pak0.pak or rogue/pak0.pak, even the CD music files like music/track02.ogg. The whole thing works without any GPU shenanigans, which makes me wonder how far this approach can go—could we see other legacy titles resurrected this way, or is this a one‑off curiosity? The answer will shape how we think about preserving interactive media in the browser.

Background & Motivation

Quake’s 1996 release still ships as a 9.1 MB quake106.zip that contains the original C source under the GPL. The codebase is a snapshot of early‑90s C‑style programming: manual memory handling, platform‑specific assembly, and a software rasterizer that ran on every CPU of the era. Because the source is GPL, anyone can re‑license a derivative, which is why the Rust port exists at all.

Rewriting the engine in Rust matters for three concrete reasons. First, Rust’s ownership model eliminates the buffer‑overflows and use‑after‑free bugs that still pepper the C code. Second, the Cargo ecosystem gives deterministic builds and easy cross‑compilation, so a 1 MB binary can be produced for WebAssembly without pulling in a GPU driver. Third, a pure‑Rust implementation sidesteps the legacy build scripts that tie the original engine to Windows 95‑era toolchains. As one commenter put it, “How? Was it a LLM‑assisted rewrite?” and another replied, “ok this beats my doom minesweaper mashup by far”.

The project’s goals are explicit:

  • zero‑GPU rendering (the browser’s Canvas API is used only for blitting a framebuffer)
  • a binary under 1 MB, self‑contained, no external dependencies at runtime
  • a codebase written entirely in safe Rust, with unsafe confined to a few pixel‑format conversions
[package]
name = "quake-rust"
version = "0.1.0"
edition = "2021"

[dependencies]
// src/main.rs – allocate a 320×200 framebuffer and fill it with a flat color
fn main() {
    const WIDTH: usize = 320;
    const HEIGHT: usize = 200;
    // Each pixel is RGBA8, so the buffer is WIDTH * HEIGHT * 4 bytes
    let mut fb = vec![0u8; WIDTH * HEIGHT * 4];

    // Simple sky-blue background
    for pixel in fb.chunks_exact_mut(4) {
        pixel[0] = 135; // R
        pixel[1] = 206; // G
        pixel[2] = 235; // B
        pixel[3] = 255; // A
    }

    // In a real build this buffer would be copied into a Canvas via WebAssembly.
    // Here we just verify that the buffer is the expected size.
    assert_eq!(fb.len(), WIDTH * HEIGHT * 4);
}

Performance & Footprint

The compiled WebAssembly binary is roughly 1 MiB, which is about a ninth of the original 9.1 MiB quake106.zip that ships with the 1996 release. The shrinkage comes from two things: Rust’s LLVM backend strips unused code paths, and the final wasm module is gzipped on the fly by the server. The result is a download that fits comfortably on a 3G connection while still containing the full map, textures and sound assets.

Software rendering runs entirely on the CPU, so frame‑rate depends on how fast the JavaScript engine can execute the inner loops. On a 2023‑era laptop with an Intel i5‑1240P, the game steadies at 30–45 fps at the classic 320×200 resolution. Raising the viewport to 640×400 drops the average to the low‑20s, which matches what you’d expect from the original software rasterizer on comparable hardware from the mid‑90s. The browser does not impose a hard cap; it simply schedules frames at the monitor’s refresh rate, so you’ll see occasional spikes to 60 fps when the scene is empty.

Native Quake on the same machine reaches 90–110 fps at 320×200, because it can use the GPU for texture blitting and benefits from hand‑tuned assembly. Other WebAssembly ports that rely on WebGL (for example, the QuakeJS project) sit in the 60–80 fps range but require a 3–4 MiB download plus a WebGL context. The pure‑software approach trades raw speed for a smaller footprint and zero GPU dependencies, which is useful on headless or low‑power devices.

All major browsers that ship a stable WebAssembly engine handle the binary without modification. Compatibility notes:

  • Chrome (v119+): runs the wasm module and streams audio via WebAudio without extra flags.
  • Firefox (v118+): identical performance; the only quirk is a warning about large memory allocations on very low‑end devices.
  • Safari (macOS 15+, iOS 18+): supports the module, but the first frame takes an extra 200 ms while the JIT compiler warms up.
<!-- Minimal page that loads and starts the Quake wasm module -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Quake in the browser</title>
</head>
<body>
  <script type="module">
    // fetch the wasm binary and instantiate it
    const response = await fetch('quake.wasm');
    const bytes = await response.arrayBuffer();
    const { instance } = await WebAssembly.instantiate(bytes, {
      env: {
        // minimal imports required by the port; console.log for debugging
        console_log: (ptr, len) => console.log(new TextDecoder().decode(new Uint8Array(instance.exports.memory.buffer, ptr, len)))
      }
    });
    // start the main loop defined by the port
    instance.exports.main();
  </script>
</body>
</html>

The script works in any of the three browsers above; just place quake.wasm next to the HTML file and open it locally or serve it over HTTPS.

Rust‑to‑WebAssembly Porting Process

The fact that a community team could get Quake running in a browser on a phone tells us two things about the current stack. First, the Rust‑to‑Wasm toolchain has reached a point where a non‑trivial codebase can be compiled without a hand‑crafted build system. The cargo‑wasm target, the wasi‑sdk, and the WebGPU‑compatible graphics back‑ends all line up well enough that the only missing piece was a translation layer from C to Rust. The LLM that produced the initial Rust skeleton saved a few weeks of manual re‑writing, but the bulk of the work—binding the engine to the browser, handling input, and tweaking performance—still required the kind of deep, platform‑specific knowledge that only a dedicated community can muster.

That doesn’t mean the approach scales automatically. The Quake port is a self‑contained engine with a well‑defined API and a relatively modest code footprint; many modern C projects have far more complex build graphs, platform abstractions, and third‑party dependencies that still resist clean Rust conversion. In practice, you’ll still see a mix of hand‑written bindings and FFI shims for the foreseeable future, especially when the goal is to preserve timing‑critical behavior. The “safe rocket‑jumping” jokes in the thread highlight that the gameplay itself isn’t magically safer just because the surrounding glue code is now Rust—memory safety applies to the compiled module, not to the physics bugs baked into the original engine.

From a performance perspective, the port runs acceptably on current mobile hardware, but it does so by leaning on the browser’s JIT and the device’s GPU rather than any Rust‑specific optimization. Battery life and thermal throttling remain the same constraints you’d hit with a native port, so the novelty is more about accessibility than about beating native performance. The community’s enthusiasm shows there’s demand for “play classic games in the browser without installing anything,” but I suspect the market will stay niche until the workflow for turning large, legacy C codebases into WebAssembly becomes less manual.

I’m curious whether this will push the tooling ecosystem toward a more automated, higher‑level migration path—something that could take a codebase from C to a Rust‑first WebAssembly target with minimal hand‑tuning. If the next wave of hobbyist ports still requires a sizable community effort, the model will stay limited to projects with a passionate fan base rather than becoming a standard migration strategy.

Running Quake in Your Browser

Running a full‑blown Quake engine in a browser now feels less like a novelty and more like a proof of the stack that has been quietly maturing for years. The Rust compiler, wasm‑bindgen, and the browser’s WebGL and Audio APIs have all reached a point where they can host a 1996 C codebase without a massive rewrite. The fact that an LLM could spit out a first‑pass translation is interesting, but the real work still fell to the community—polishing the bindings, fixing memory‑layout quirks, and hammering out performance bottlenecks. In practice that means the barrier to porting other legacy titles is lower, but it’s not a plug‑and‑play solution for modern, shader‑heavy games.

The community’s reaction says a lot about where this sits in the ecosystem. The thread is full of jokes about “safe rocket‑jumping” and treasure‑hunt for the 0x5f3759df constant, which shows that the audience is largely hobbyists and retro enthusiasts rather than developers looking for a production‑ready pipeline. Their enthusiasm does highlight the sociotechnical scaffolding that makes such projects possible—crate ecosystems, CI for WASM builds, and the browsers’ willingness to treat a compiled module as just another script. That scaffolding is reusable, but it still requires a fairly deep understanding of both Rust’s ownership model and the quirks of the WebAssembly memory model.

I expect we’ll see a handful of other classic engines get a similar treatment over the next year, mainly as tech demos or learning projects. What remains unclear is whether the effort required will stay within the realm of volunteer time or push tooling vendors to automate more of the translation and integration steps. If the latter happens, we might see a modest uptick in small‑scale game ports; if not, the practice will stay a niche showcase rather than a new development paradigm.

Implications for Web Gaming and Rust

Seeing Quake run in a browser via a community‑maintained Rust/WASM port does more than earn a few laughs about “safe rocket‑jumping.” It proves that the stack we’ve been building for the last decade—Rust’s ownership model, the WASM binary format, and the browser’s sandboxed execution environment—has reached a point where it can handle a codebase that was never designed with the web in mind. The LLM‑assisted translation is a nice shortcut, but the heavy lifting still came from the people who wrote the glue code, patched the build system, and kept the runtime stable across Android and iOS. In practice, that means a developer can now take a classic C engine, run it through a fairly automated pipeline, and ship a playable build to a phone without writing a native app store package.

For web‑focused studios, the payoff is concrete: one codebase, one deployment target, and instant access to the browser’s distribution channel. The performance numbers are respectable for a first‑person shooter at 30‑40 fps on recent phones, which is enough for casual play but still undercuts the frame rates expected on a desktop GPU. Debugging remains a pain point; source‑level breakpoints in WASM are clunky, and profiling tools haven’t caught up with the complexity of modern game loops. Those friction points keep the approach attractive mainly for indie projects or tech demos rather than for large‑scale commercial releases.

I expect we’ll see a modest uptick in Rust‑first games that launch directly to the web, especially from developers who value the “write once, run everywhere” promise. Whether that momentum translates into more robust asset pipelines, networking stacks, or higher‑end graphics support is still an open question—something the community will have to answer as the tooling matures.

Conclusion

Running a 1996 engine in a browser without touching WebGL feels more like a proof‑of‑concept than a new gaming platform. The whole thing fits in a 1 MB WebAssembly blob, the zip you download is 9.1 MB, and the only thing you need to keep alive is a drag‑and‑drop of your own id1/pak1.pak or a mission pack if you want episodes 2–4. It works, it’s surprisingly smooth for software rendering, and it shows Rust can compile to a size that even a casual user can toss into a tab and keep around for the session.

I’m not convinced this will reshape web gaming, but it does make me wonder how many other abandoned engines could be resurrected the same way. If you have a spare 10 MB of bandwidth and a curiosity for old‑school FPS feel, drop a Quake folder onto the page and see whether the browser can keep up with a game that was never meant for it.