README.md

July 16, 2026 · View on GitHub

DOOMSQ — Doom running on the Turso VDBE

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 .ll text 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 shareware DOOM1.WAD.
  • bindings/javascript — a demo-doom feature exposing doomPrepare (compile + return a streaming statement) and bindIntAt (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 ram blob of the one row of _vdbecc_mem (globals from 0x1000, stack at the top). A cursor is opened on that row once and stays open; loads and stores are BlobRead / BlobWrite into it. The B‑tree row is the linear memory, backed by the pager.
  • Bytes become integers with get_byte / set_byte. A load is BlobRead + get_byte; a store is set_byte + BlobWrite; a memcpy is a bare BlobRead + 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 and Gosubs; indirect calls dispatch on a function ID. Values are kept sign‑extended at their IR width; xor is 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.WAD is id Software's shareware Doom, redistributable under its shareware license. All rights to Doom belong to id Software.
  • Regenerating the IR. The .ll are checked in for convenience; regenerating them requires doomgeneric, 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.