A programming game where a Python-flavored DSL steers a simulated vector CRT beam, played entirely inside a time-travel debugger.
The whole thing is one clock. Every instruction the program executes and every unit of distance the beam travels is charged against the same cycle counter, and the phosphor forgets whatever the beam has not visited lately. There is no frame and no vsync: a picture stays on the tube only as often as the program can get back around to drawing it. Thinking harder makes the screen dimmer. The puzzle is fitting working game logic into the gaps between beam strokes, and the tool for solving it is a debugger that can scrub, rewind and single-step any run, exactly, at any point.
The full design is in docs/spec/beamvm-spec.md.
Every picture below is a whole-window capture the app took of itself —
make screenshots regenerates the lot; the full set with captions and commands
is in docs/SCREENSHOTS.md.
The beam. The tube is the square [-1, 1]². moveto hops the beam with
it blanked; lineto drags it with the beam on. A drawing move costs
ceil(distance / slew_rate) cycles, a blanked move is faster but not free, and
the light deposited per unit length is proportional to the time the beam spent
there — so a slow, short stroke is bright and a long one is dim. The phosphor
is modelled as two exponentials (a fast flash and a slow persistence) with
P31, P7 and P1 presets, and the image on screen is always derived from the
append-only stroke log, never stored. That is what makes exact scrubbing and
live phosphor sliders possible.
The clock. 1 MHz, Vectrex territory. Arithmetic, comparisons, calls and
beam travel all draw on it. A yield costs one cycle and marks a pass boundary
for the debugger; it does not stop anything.
Two boards. The Home board is the machine above. The Pro board
(--pro) adds a 4× tube, a display-list coprocessor (the AVG: the CPU builds
a vector list, the coprocessor walks it while the CPU thinks) and a Math Box
(rotate, divide, multiply-add). The Pro is what marquee and DOOM run on.
The language. Indentation blocks, def, for/while, yield, lists,
familiar operators; no classes, no closures, no recursion. The heart of
lissajous, the first lesson:
def main():
phase = 0.0
while True:
moveto(AMP * sin(phase), 0.0)
for i in range(1, SEGMENTS + 1):
u = TWO_PI * i / SEGMENTS
lineto(AMP * sin(FREQ_X * u + phase), AMP * sin(FREQ_Y * u))
phase = phase + DRIFT
yieldPrograms can chain-load other programs (exec), read the pad and the keyboard
(getkey), draw text from the machine's own character ROM (text), and read
and write files in a small Unix-shaped filesystem (/examples, /bin, /etc,
/user/documents, /include, …). Source files are .bvm; include "stem"
pastes a .bvi at compile time.
Determinism. A run is exactly (source, seed record, input tape, machine config). Every run can be saved as a trace (.bvmt) and replayed byte for
byte, including runs that asked for host entropy — the seed is recorded.
The game runs nowhere else. Panels dock and undock freely (egui):
- Transport — play, pause, step by instruction, playback from 0.01× to 100×, and a scrub bar over the whole run. Scrubbing into the past is exact; editing the source while scrubbed back forks a new run from that instant.
- Budget strip and meters — where each pass's cycles went: lit travel, blanked travel, logic, builtins. This is the panel the game is about.
- Source — the editor, with inline values from the live run, hover sparklines, heat per line, completion with builtin costs, and breakpoints (by line, by cycle budget, or spatial: break when the beam enters a region).
- State and Watch — the frame stack, globals, lists, watched expressions.
- Config — machine revision, cost table, phosphor time constants and the four tube effects (glass, halation, beam spot, grain), all live.
- Brief — each shipped program is a lesson with a brief beside it.
- AVG — on the Pro, the display list the coprocessor is walking.
Seventeen programs are built into the binary, listed by the launcher:
- Lessons (
/examples):lissajous,bounce,pilot,rockfield, then the repair jobsrockfield_dim,splitstorm,zigzag, asevensegfont toolbox,marqueefor the Pro, ande1m1/e1m2. - Tools (
/bin), all written in the DSL and running on the machine:launcher(the front page, boots by default),shell(a bash-minimal terminal withls,cd,cat,grep,cp,mv,rm,tree, …),nano(a real editor:nano namein the shell edits files on disk),keytest,pad_testandtestpattern.
DOOM. e1m1 and e1m2 are id's first two maps, wireframe-walked on the
Pro board in first person, with the menu, weapons, monsters, items, switches,
the exit that chains into the next level, a tally card and a cheats card. The
maps and sprites are baked from the shareware doom1.wad into DSL include
files by two tools in this repository; the baked files are committed, the WAD
never is. cargo run --release -p beamvm-core --bin doombake -- --help has
the recipe if you want to re-bake.
You need a stable Rust toolchain (1.95 or newer) and a GPU the platform can drive: Vulkan on Linux, Metal on macOS. Developed and tested on both.
make setup # one-time: system packages (Debian-family apt) or Xcode CLT, rustup if missing
make run # release build, launches on the Home board
make run-pro # the same, on the Pro board — where e1m1, e1m2 and marquee live
make check # cargo fmt --check + the workspace test suite
make # the full target list
Or with cargo directly:
cargo run --release -p beamvm-app # boots the launcher
cargo run --release -p beamvm-app -- program.bvm # opens a file, paused
cargo run --release -p beamvm-app -- --builtin bounce # a shipped program, with its brief
cargo run --release -p beamvm-app -- --trace run.bvmt # replays a saved run
cargo run --release -p beamvm-app -- --help # every flag, incl. --screenshot
Controls. Click the tube to give the machine the keyboard; Esc gives it back. While the machine has it, the virtual pad is live: arrows and WASD are the four axes, Z/Enter, X, C, V, 1–5, E and Space are the buttons, and everything else is typed into the machine as key codes. While it does not, Space is the transport's play/pause.
make dist # dist/beamvm — stripped portable Unix binary (what Linux ships)
make app # dist/BEAMVM.app — macOS bundle; drag it to /Applications
make dmg # dist/BEAMVM-<version>.dmg — the thing you hand to someone
make install # copy dist/BEAMVM.app into /Applications (no sudo)
make icon # re-render beamvm-app/assets/icon.png (needs a GPU; commit it)
make screenshots # regenerate docs/images (needs a display and a GPU)
make app, make dmg and make install are macOS-only (Apple Silicon);
anywhere else each says so and exits 0. They consume the committed
assets/icon.png, so packaging never needs a GPU — only make icon does, and
only when the CRT look changes.
The bundle is ad-hoc signed — not Developer ID signed, not notarized (there
is no Apple developer account; this is deliberate). A copy that reaches another
Mac by download, AirDrop or zip picks up com.apple.quarantine, and Gatekeeper
will refuse to open it, claiming the app is damaged or from an unidentified
developer. It isn't. Strip the flag:
xattr -d com.apple.quarantine /Applications/BEAMVM.app
Or right-click → Open the first time and confirm. Either is a one-time step.
The dmg from make dmg is the sane way to hand the app to someone: a disk image
carries the bundle across intact, where a plain zip of a .app can mangle its
structure, and it opens to a window with the app beside an /Applications
symlink to drag it onto. It still arrives quarantined, so the xattr line above
still applies once the app is installed.
A Cargo workspace of two crates:
beamvm-core/— the deterministic machine: lexer, parser, compiler, bytecode VM, cycle accounting, stroke log, snapshots, input tape, trace format, the include resolver and the character ROM. Zero dependencies; builds on Rust 1.75+. Itssrc/bin/holds the command-line tools:passes(a cost analyzer for a program: cycles per pass, phosphor retention,CSV:lines for sweeps),bench(VM throughput), and the DOOM bakersdoombakeanddoomsprites.beamvm-app/— the desktop app: eframe/egui on wgpu, egui_dock, the phosphor renderer and every debugger panel.examples/holds the shipped programs, their briefs and the include files, all embedded at build time.
Documentation lives in docs/: the design spec and one feature spec per
feature in docs/spec/, the screenshot index in docs/SCREENSHOTS.md, and
docs/DEFERRED.md for what was deliberately left out.
The UI icon font is Phosphor (MIT; license
beside it in beamvm-app/assets/).
MIT — see LICENSE.






