Skip to content

Repository files navigation

BeamVM

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.

Screens

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 launcher The machine's own front page, drawn by the machine. the shell A terminal written for the machine it runs on, after ls -l.
a breakpoint hit A breakpoint hit: the tube, the line, the cycle. splitstorm splitstorm, mid-run — one of the lessons.
nano The editor, typing a program into the machine. marquee The Pro board's coprocessor holding a sign while the CPU idles.
DOOM And then it ran DOOM — E1M1, in play, on the Pro.

The machine

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
        yield

Programs 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 debugger

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.

What ships inside

Seventeen programs are built into the binary, listed by the launcher:

  • Lessons (/examples): lissajous, bounce, pilot, rockfield, then the repair jobs rockfield_dim, splitstorm, zigzag, a sevenseg font toolbox, marquee for the Pro, and e1m1 / 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 with ls, cd, cat, grep, cp, mv, rm, tree, …), nano (a real editor: nano name in the shell edits files on disk), keytest, pad_test and testpattern.

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.

Building and running

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, 15, 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.

Packaging

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.

Gatekeeper / quarantine

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.

Layout

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+. Its src/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 bakers doombake and doomsprites.
  • 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/).

License

MIT — see LICENSE.

About

A programming game where a Python-flavored DSL steers a simulated vector CRT beam, played entirely inside a time-travel debugger.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages