<p align="center">
<img src="assets/doomso.jpg" alt="DOOMSQ — Doom running on the Turso VDBE" width="460"/>
</p>
<h1 align="center">Doom on the Turso VDBE</h1>
**[Turso](https://github.com/tursodatabase/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 `Gosub`s; 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)
```bash
# 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):
```bash
cargo test -p vdbecc
```
### Browser (playable, on a canvas)
Build the wasm engine (needs the WASI SDK; see `bindings/javascript`), then:
```bash
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](https://github.com/tursodatabase/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.