Vectrex Studio compiles VPy source through a modular 9-phase Rust pipeline called Buildtools. The output depends on the target: a Vectrex ROM (.bin), a PiTrex ELF binary, or an experimental RP2350 binary.
An older single-pass compiler called Core is still present but deprecated — it receives no new features.
Build Targets
| Target | Backend | Status | Notes |
|---|---|---|---|
vectrex | MC6809 | ✅ Production | Standard Vectrex cartridge |
pitrex | ARM32 | ✅ Tested on hardware | PiTrex expansion board |
rp2350 | ARM Thumb2 | ⚠️ Experimental | No physical cartridge built yet |
uvm2 | — | ⚠️ Experimental | ROM format incomplete — cannot run |
Set the target in your .vpyproj:
[build]
target = "vectrex" # or "pitrex", "rp2350", "uvm2"
output = "build/mygame.bin"vectrex — MC6809
The standard target. Produces a .bin ROM image for real Vectrex hardware or the IDE emulator. Supports single-bank (32 KB) and multibank (64 KB–4 MB) ROMs via a bank-switching circuit on the cartridge PCB.
pitrex — ARM32
Produces ARM32 assembly that links against the PiTrex SDK. PiTrex is a Raspberry Pi Zero expansion board for the Vectrex that drives the beam directly at ARM speeds, allowing far more geometry per frame than the original hardware.
The IDE emulator includes a PiTrex renderer and PSG audio emulation so you can develop and test without hardware. Overlay images (PNG) can be configured per project and appear behind the vector output in the emulator.
rp2350 — ARM Thumb2 ⚠️
Compiles to ARM Thumb2 for the Raspberry Pi RP2350 microcontroller. The IDE emulator can run the generated ELF binaries. No physical debug cartridge based on RP2350 has been built, so this target has not been verified on real hardware. Treat as a development preview.
uvm2 ⚠️
Early scaffolding for a third-party vector display platform. The compiler can emit code for this target but the ROM format is not fully documented and no UVM2 ROM has been successfully executed. Not useful for production games.
Compiler Pipeline (Buildtools)
A modular multi-crate Rust pipeline with one crate per phase:
| Phase | Crate | Responsibility |
|---|---|---|
| 1 | vpy_loader | Load .vpyproj project file (TOML) |
| 2 | vpy_parser | Parse VPy source to AST |
| 3 | vpy_unifier | Merge multi-file ASTs, resolve imports |
| 4 | vpy_bank_allocator | Assign functions/data to ROM banks |
| 5 | vpy_codegen | AST → assembly (with tree shaking, per target) |
| 6 | vpy_assembler | Assembly → machine code (two-pass) |
| 7 | vpy_linker | Symbol resolution, inter-bank references |
| 8 | vpy_binary_writer | Emit final .bin / ELF ROM image |
| 9 | vpy_debug_gen | Generate .pdb debug symbol file |
Phase 5 has separate codegen backends for each target (m6809/, pitrex/, arm/, uvm2/). The earlier phases are target-agnostic.
Core (Deprecated)
Status: ⚠️ DEPRECATED — maintenance mode, no new features.
Single-pass pipeline: lexer → parser → AST → optimizer → codegen → MC6809 assembly → binary. Always outputs a fixed 32 KB ROM. No PDB debug symbol generation. Use Buildtools for all new projects.
Multibank ROMs (vectrex target)
⚠️ Not a Vectrex standard feature. The Vectrex has no native bank-switching protocol — stock cartridges are limited to 32 KB and a vanilla console will not run a multibank ROM. The scheme below is a custom cartridge design specific to Vectrex Studio. The IDE emulator implements it natively, so you can develop multibank games without any hardware. To run on a real Vectrex you must build (or buy) a cartridge that implements this scheme.
VPy supports ROMs from 32 KB up to 4 MB with no changes to game code. Add two META directives:
META ROM_TOTAL_SIZE = 65536 # pick your target size (see table)
META ROM_BANK_SIZE = 16384 # always 16 KB per bankThe compiler divides the ROM into 16 KB banks, assigns functions and assets via call graph analysis, and generates all bank-switching wrappers automatically.
The bank-switching mechanism
The custom cartridge splits the cartridge address space into two halves:
| Address range | Contents | Switching |
|---|---|---|
$0000–$3FFF | Banked window — one of N selectable 16 KB ROM banks | Changes on demand |
$4000–$7FFF | Helpers bank — last 16 KB of the ROM | Fixed, always visible |
The bank visible at $0000–$3FFF is selected by writing the bank number to $DF00:
LDA #2 ; bank number 0..N-2
STA $DF00 ; cartridge address decoder latches A
; → bank 2 is now mapped at $0000-$3FFF$DF00 sits in the Vectrex I/O hole — BIOS, RAM, and VIA chips do not respond there, so writes are harmless on stock hardware and unambiguous on the custom cartridge. The decoder is typically a 74HC373 latch plus a single decoder chip.
The compiler emits cross-bank call wrappers (in the helpers bank, so they are always visible) that save the current bank, switch to the target bank, call the function, and restore the original bank before returning. Builtins that touch banked assets — DRAW_VECTOR, SHOW_LEVEL, UPDATE_LEVEL, PLAY_SFX — do this automatically.
Supported sizes
ROM_TOTAL_SIZE | Banks | Capacity | Recommended chip |
|---|---|---|---|
32768 | 2 | 32 KB | 27C256 (single-bank, no switching) |
65536 | 4 | 64 KB | 27C512 ✅ fully tested |
131072 | 8 | 128 KB | 27C010 |
262144 | 16 | 256 KB | 27C020 |
524288 | 32 | 512 KB | 27C040 / 29F040 Flash |
1048576 | 64 | 1 MB | 27C080 / 29F080 |
2097152 | 128 | 2 MB | M27C160 |
4194304 | 256 | 4 MB | 29F320 Flash |
The 4-bank (64 KB) configuration is fully tested. Configurations larger than 4 banks compile, but cross-bank symbol resolution is still being completed.
Building the cartridge
The minimum-viable custom cartridge is an EPROM plus a latch and a decoder — under $10 in parts. A more polished cartridge based on an RP2350 microcontroller (also acting as a debug bridge for the IDE) is in active development. Until that is released, you need to build (or have someone build) a discrete-logic cartridge that implements the $DF00 bank-switch register.
Asset Formats
VPy projects reference several asset file types. All are authored in the IDE and referenced in the .vpyproj.
.vec — Vector graphics
JSON files defining vector paths. Each path is a list of (x, y) points, an intensity, and a closed/open flag. Paths can also contain cubic Bézier control points.
The IDE VectorEditor edits .vec files. New in v0.3: bezier drawing tool, rotate tool, walkable-area annotation tool, and RDP path simplification.
Walkable areas can be embedded directly in a .vec file to define platform shelves for the wander AI:
{
"version": "1.0",
"name": "platform_wide",
"walkable_areas": [
{ "y": 8, "x_min": -60, "x_max": 60 }
],
"layers": [...]
}.vanim — Vector animations
JSON files that define frame-by-frame animations by referencing sequences of .vec assets:
{
"version": "1.0",
"name": "titchi_walk",
"loop": true,
"base_refs": ["titchi_body"],
"frames": [
{ "index": 0, "duration_ticks": 8, "vec_refs": ["titchi_feet_a"], "paths": [] },
{ "index": 1, "duration_ticks": 8, "vec_refs": ["titchi_feet_b"], "paths": [] }
]
}base_refs is a static-cel layer (string array of .vec names) drawn before every frame. Each frame combines vec_refs (also a string array) with an optional paths array of inline vector geometry. Inline paths are converted to synthetic vec assets at compile time and render identically on M6809, PiTrex, and RP2350. The runtime advances frames based on duration_ticks (50 Hz screen ticks) — no per-frame timing code in game logic.
Play with DRAW_ANIM("name", x, y). The IDE AnimationEditor edits .vanim files with a filmstrip view and canvas preview.
.venemy — Enemy types
JSON files that define enemy types: their AI parameters, sprite references, and state machine transitions. The compiler reads .venemy files at build time and emits ROM tables; there is no runtime interpreter.
{
"name": "titchi",
"ai_type": 2,
"patrol_speed": 1,
"sprite": "titchi_walk",
"state_machine": {
"walk": {
"sprite": "titchi_walk",
"transitions": [
{ "event": "hit_snow", "next": "snow1" }
]
},
"snow1": {
"sprite": "titchi_snow1",
"transitions": [
{ "event": "hit_snow", "next": "snow2" },
{ "event": "timer_out", "next": "walk" }
]
}
}
}AI types:
ai_type | Behaviour |
|---|---|
1 | Static (no movement) |
2 | Patrol — walks between waypoints or walkable-area endpoints |
3 | Frog — patrol + jump attack |
4 | Wander — autonomous platform navigation via walkable areas |
.vmus — Music
Chiptune music for the AY-3-8910 PSG. Authored in the IDE Music Editor. Play with PLAY_MUSIC("name").
.vsfx — Sound effects
Short sound effects. Authored in the IDE SFX Editor. Play with PLAY_SFX("name").
.vplay — Level files
JSON files authored in the IDE Playground that define the game world: object placement, walkable areas, enemy spawn points, transitions, and scroll limits. Loaded at runtime with LOAD_LEVEL("name").
{
"version": "2.0",
"worldBounds": { "xMin": -96, "xMax": 863, "yMin": -128, "yMax": 127 },
"scrollLimits": { "left": 0, "right": 800, "top": 127, "bottom": -128 },
"layers": {
"background": [],
"gameplay": [
{
"vectorName": "platform_wide",
"x": 0, "y": -60,
"walkable_areas": [
{ "y": 8, "x_min": -60, "x_max": 60 }
]
}
],
"foreground": []
},
"spawnPoints": {
"enemies": [
{ "enemyType": "titchi", "x": -40, "y": 20 }
]
}
}Level System
Camera and scrolling
camera_x: i16 = 0
def loop():
camera_x += J1_X() / 16
camera_x = clamp(camera_x, GET_SCROLL_LIMIT_LEFT(), GET_SCROLL_LIMIT_RIGHT())
SET_CAMERA_X(camera_x)
SHOW_LEVEL()GET_SCROLL_LIMIT_* returns the boundaries set by the scroll-limit markers in the level editor. Object positions in .vplay are world coordinates; SHOW_LEVEL() subtracts the camera offset before drawing and culls objects outside the viewport.
Enemy system
def loop():
SPAWN_ENEMIES() # activate enemies whose spawn point is on screen
UPDATE_ENEMIES() # advance AI, physics, state machines
SHOW_LEVEL()
DRAW_ENEMIES() # draw each active enemyEnemy type definitions come from .venemy files referenced in the level's spawn points. The compiler builds ROM tables at build time; the runtime walks the table each frame with no dynamic allocation.
Wander AI and walkable areas
Enemies with ai_type = 4 use walkable areas for navigation. Define areas on individual .vec platform assets (inherited by every level that uses that asset) or override them per-object in the .vplay file.
Transitions between areas define how enemies move from one platform to another. They can be explicit (with precise takeoff/landing X coordinates) or auto-derived by the compiler from geometry proximity.
All of this is authored visually in the Playground panel. See the v0.3 release notes for a detailed walkthrough.
Project File Format (.vpyproj)
[project]
name = "mygame"
version = "0.1.0"
entry = "src/main.vpy"
description = "My Vectrex game"
author = "Your Name"
[build]
output = "build/mygame.bin"
target = "vectrex" # vectrex | pitrex | rp2350 | uvm2
optimization = 2
debug_symbols = true # required for source-level debugging
[sources]
vpy = ["src/**/*.vpy"]
[resources]
vectors = ["assets/vectors/*.vec"]
animations = ["assets/animations/*.vanim"]
enemies = ["assets/enemies/*.venemy"]
music = ["assets/music/*.vmus"]
sfx = ["assets/sfx/*.vsfx"]
playground = ["assets/playground/*.vplay"]| Field | Required | Description |
|---|---|---|
project.entry | Yes | Main .vpy file |
build.output | Yes | Output .bin / ELF path |
build.target | No | Default vectrex |
build.debug_symbols | No | Enable PDB generation (vectrex only) |
build.optimization | No | 0–2, default 2 |
Building from the Command Line
# Build a project
cd buildtools && cargo run --bin vpy_cli -- build path/to/project.vpyproj
# Emit assembly only (useful for debugging codegen)
cd buildtools && cargo run --bin vpy_cli -- asm src/main.vpy > /tmp/main.asm
# Run all compiler tests
cd buildtools && cargo test --allMigrating from Core to Buildtools
- Create a
.vpyprojfile next to your main.vpy(see the project format section above). - In the IDE Settings panel, switch the backend selector to Buildtools (New).
- Rebuild — single-bank output is identical to Core.
The only common issue is that Buildtools requires a .vpyproj file; there is no bare .vpy build mode.