01 / flagship 2026 WebAssembly · OpenJDK · Emscripten · C · TypeScript
JavaBox
A full JVM inside a browser tab.
OpenJDK 21 compiled to WebAssembly. Write Java, compile it, run it — with no server and no JVM installed. Then it runs Doom at 30 frames a second.
- cold boot
- 3–5s
- compile + run
- <1s
- Doom FPS
- ~30
- JDK payload
- 72MB
Running Java in a browser usually means sending it somewhere else. A server compiles it, runs it, and ships you the output. JavaBox removes the server: it compiles OpenJDK 21’s Zero interpreter to WebAssembly with Emscripten, which means the JVM itself — garbage collector, class loader, bytecode interpreter — executes inside the tab.
The naive version of this is unusable. Booting a JVM per run costs seconds, and nobody waits seconds to see `Hello World`. So JavaBox keeps a persistent CompileServer daemon alive inside the WASM JVM and does compilation in-process through `javax.tools.JavaCompiler`, loading results with a custom in-memory class loader. You pay the boot cost once, then compile-and-run lands under a second.
Then there is the part that makes people check whether it is a video. Mocha Doom — a Doom engine written in pure Java — runs on top of it at roughly 30 FPS, with sound, in a browser, on a JVM that is itself running inside the browser.
Drive the CRT
Take the corridor CRT's controls and walk a toy raycaster — 62,208 pixels software-rendered on the CPU and pushed to a canvas. It is a nod to the technique, not the real thing: the real JavaBox runs actual Mocha Doom on a JVM compiled to WebAssembly. Open the real demo.
WASD or arrow keys walk · Esc releases
Video
Java renders into a `BufferedImage`. A JNI native method copies the ARGB pixels into the shared WASM heap as RGBA, and the browser draws them with `putImageData` on each `requestAnimationFrame`. No canvas abstraction in the Java layer at all — it thinks it is writing to a framebuffer.
Audio
An 8-channel Java mixer produces 16-bit stereo PCM at 22050Hz. JNI writes it into a shared ring buffer that the Web Audio API drains through an `AudioBufferSourceNode`.
Input, and why there are no threads
Emscripten’s pthread creation is unreliable enough that the usual event-listener-plus-worker design kept deadlocking. Instead the browser pushes key events straight into a C-side queue via a WASM export, and Java polls that queue once per frame through JNI. Single-threaded, boring, and it does not break.
What had to be forked
OpenJDK does not build for Emscripten, so there is a fork of OpenJDK 21u carrying an Emscripten OS abstraction layer, plus a fork of libffi. Both ship as submodules.