All docs

Compiler & Build Targets

The 9-phase Rust compiler pipeline, build targets, multibank support, asset formats, and project file reference.

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

TargetBackendStatusNotes
vectrexMC6809✅ ProductionStandard Vectrex cartridge
pitrexARM32✅ Tested on hardwarePiTrex expansion board
rp2350ARM Thumb2⚠️ ExperimentalNo physical cartridge built yet
uvm2⚠️ ExperimentalROM 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:

PhaseCrateResponsibility
1vpy_loaderLoad .vpyproj project file (TOML)
2vpy_parserParse VPy source to AST
3vpy_unifierMerge multi-file ASTs, resolve imports
4vpy_bank_allocatorAssign functions/data to ROM banks
5vpy_codegenAST → assembly (with tree shaking, per target)
6vpy_assemblerAssembly → machine code (two-pass)
7vpy_linkerSymbol resolution, inter-bank references
8vpy_binary_writerEmit final .bin / ELF ROM image
9vpy_debug_genGenerate .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 bank

The 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 rangeContentsSwitching
$0000–$3FFFBanked window — one of N selectable 16 KB ROM banksChanges on demand
$4000–$7FFFHelpers bank — last 16 KB of the ROMFixed, 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_SIZEBanksCapacityRecommended chip
32768232 KB27C256 (single-bank, no switching)
65536464 KB27C512 ✅ fully tested
1310728128 KB27C010
26214416256 KB27C020
52428832512 KB27C040 / 29F040 Flash
1048576641 MB27C080 / 29F080
20971521282 MBM27C160
41943042564 MB29F320 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_typeBehaviour
1Static (no movement)
2Patrol — walks between waypoints or walkable-area endpoints
3Frog — patrol + jump attack
4Wander — 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 enemy

Enemy 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"]
FieldRequiredDescription
project.entryYesMain .vpy file
build.outputYesOutput .bin / ELF path
build.targetNoDefault vectrex
build.debug_symbolsNoEnable PDB generation (vectrex only)
build.optimizationNo0–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 --all

Migrating from Core to Buildtools

  1. Create a .vpyproj file next to your main .vpy (see the project format section above).
  2. In the IDE Settings panel, switch the backend selector to Buildtools (New).
  3. 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.