README.md
July 16, 2026 · View on GitHub
Doom on the Turso VDBE
Turso is the LLVM of databases. Underneath the SQL, it's a virtual machine with its own bytecode — and to prove that VM is the real deal, we ported Doom to it.
That's the actual id Software Doom, compiled to Turso's VDBE bytecode and executed by the
stock database engine. No emulator, no reimplementation: the same virtual machine that runs
your SELECT is running the game above.
How? When you query Turso (like SQLite), it compiles your SQL to bytecode for its own
machine — the VDBE — and runs that. SQL is just one front‑end to the VM. So we built
another: we compiled doomgeneric (Doom's C engine) with clang to LLVM IR, wrote a small
compiler — vdbecc — that lowers LLVM IR to VDBE bytecode, and fed the result to Turso
unchanged. Doom's whole memory is a single blob in a one‑row table; each frame is a result
row streamed from one running statement.
If a database's bytecode can run Doom, it can run anything. That's the vision: one
reliable engine, many front‑ends. SQL is one. vdbecc (LLVM IR) is another.
Loading a hand-built program
Doom runs on Turso's stock VDBE — no new opcodes, no Doom‑specific engine code. The
byte↔integer access the memory model needs is done with get_byte / set_byte (standard
PostgreSQL binary‑string functions), and reads and writes use the stock BlobRead /
BlobWrite opcodes.
The one thing Turso can't do yet is load a program you built yourself. Normally you
prepare a statement and Turso compiles your SQL into a VDBE program internally, then runs
it — but that compiled program is never exposed, and there's no way to hand the engine a
program you produced some other way. So this repo forks Turso to add a small load path
that takes a vdbecc‑built program and turns it into a runnable Statement. If executing
pre‑compiled programs ever proves useful beyond a demo, Turso could grow a first‑class
interface for it — and this fork would go away.
Architecture
Doom (doomgeneric, C) + a tiny libc/port shim
│
│ clang -O2 -emit-llvm
▼
82 LLVM IR modules (.ll) ← ahead of time
│
│ vdbecc (this repo — LLVM IR → VDBE bytecode)
▼
A VDBE bytecode program == a Turso `Statement`
│
│ the Turso engine steps it, one frame per yield
▼
frames streamed as result rows → canvas / terminal
vdbecc emits the VDBE program directly, the engine loads it into an ordinary
Statement — the same type prepare("SELECT …") returns — and executes it. Doom's memory
is the single ram blob of one row of a table, which the program reads and writes as it
runs; each frame is streamed out as a result row. You step the statement like any
cursor, and each step runs the game to the next frame.
Components
vdbecc/— the LLVM‑IR‑to‑VDBE compiler. A.lltext parser (parse.rs) and a code generator (codegen.rs) that lowers SSA to VDBE bytecode.core/— Turso itself (the VDBE interpreter, B‑tree storage, pager, WAL), stock except for the small load path described above.doom/— the compiled IR modules (doom/ll/*.ll) and the sharewareDOOM1.WAD.bindings/javascript— ademo-doomfeature exposingdoomPrepare(compile + return a streaming statement) andbindIntAt(input), for the browser.examples/javascript/vdbe-doom-vite— a browser page that compiles Doom in‑tab and plays it on a<canvas>.
The compiler (vdbecc)
vdbecc's job is to make the VDBE look like a CPU. The trick is that the VDBE register
file is unbounded, so there is no register allocator: every SSA value gets its own
register, whole‑program, with disjoint per‑function ranges. From there:
- Memory is a blob. The C address space is the single
ramblob of the one row of_vdbecc_mem(globals from0x1000, stack at the top). A cursor is opened on that row once and stays open; loads and stores areBlobRead/BlobWriteinto it. The B‑tree row is the linear memory, backed by the pager. - Bytes become integers with
get_byte/set_byte. A load isBlobRead+get_byte; a store isset_byte+BlobWrite; amemcpyis a bareBlobRead+BlobWrite. These are standard Postgres functions — the engine needs no special support. - Calling convention. Fixed transfer registers (
LINK,ARG0..7,RET); a direct call writes the args andGosubs; indirect calls dispatch on a function ID. Values are kept sign‑extended at their IR width;xoris synthesized ((a|b)-(a&b)), and so on.
vdbecc is checked against native clang: a differential C corpus (cargo test -p vdbecc)
runs small C programs both on the VDBE and natively and requires byte‑identical results,
and Doom's framebuffer comes out byte‑for‑byte identical to the native build.
How the game is loaded and driven
doomPrepare(llSources, wad, …) (or the vdbecc CLI) runs vdbecc, which parses and
links the .ll, emits a VDBE program, seeds _vdbecc_mem with the initial RAM image
(globals + WAD patched in), and returns a Statement.
You then step that statement in a loop. Inside the VDBE, Doom's DG_DrawFrame calls a
vdbe_present intrinsic that emits a ResultRow and pauses; your next step resumes
where it paused. So one step ≈ one frame, and each frame arrives as a row [ret, frame]
where frame is the 320×200 RGBA framebuffer. Keyboard input is pushed the other way, as
mid‑statement bind parameters. The host is the clock; the game only advances when you
pull.
Run it locally
Native (terminal or raw framebuffer)
# Build the compiler + engine (release — Doom needs it)
cargo build --release -p vdbecc --bin vdbecc
# Run N tics and dump the final 320x200 RGBA framebuffer to a file
./target/release/vdbecc doom/ll/*.ll \
--entry vdbe_run --arg 200 --ram 67108864 \
--patch vdbe_wad=doom/DOOM1.WAD \
--fb-raw vdbe_fb:/tmp/fb.raw
# Or render live in the terminal as 256-color half-blocks:
./target/release/vdbecc doom/ll/*.ll \
--entry vdbe_run --ram 67108864 \
--patch vdbe_wad=doom/DOOM1.WAD --fb vdbe_fb:320x200 --frames 400
Verify correctness (byte‑identical to native clang on a C corpus):
cargo test -p vdbecc
Browser (playable, on a canvas)
Build the wasm engine (needs the WASI SDK; see bindings/javascript), then:
cd examples/javascript/vdbe-doom-vite
npm install
npm run dev # opens a page that compiles Doom in-tab and plays it
The page needs cross‑origin isolation (COOP: same-origin + COEP: require-corp or
credentialless) because the threaded wasm uses SharedArrayBuffer.
Notes
- The WAD.
doom/DOOM1.WADis id Software's shareware Doom, redistributable under its shareware license. All rights to Doom belong to id Software. - Regenerating the IR. The
.llare checked in for convenience; regenerating them requiresdoomgeneric, a small libc/port shim, and clang.
Built on Turso. The engine, its reliability
work, and its architecture are all Turso's; this repo only adds the vdbecc front‑end and
the Doom port. See the upstream project for the database itself.