This release brings multibank ROM support over the finish line, adds a one-click EPROM programmer to the IDE, and fixes a long-standing issue where SET_INTENSITY had no effect on DRAW_VECTOR brightness. The Core compiler is now deprecated — Buildtools is the recommended backend for all projects.
Multibank ROMs — up to 4 MB
⚠️ Important — this is not standard Vectrex behaviour. The Vectrex has no native bank-switching protocol. Stock cartridges are limited to 32 KB and a vanilla console will not run a multibank
.binproduced by VPy. The scheme described here is a custom cartridge design specific to Vectrex Studio. The IDE emulator implements the same scheme natively so you can develop and test multibank ROMs without any hardware.
The standard Vectrex cartridge is a single 32 KB ROM mapped at $0000–$7FFF. VPy's custom cartridge breaks that region into two halves and adds a tiny address decoder so up to 4 MB of ROM can be addressed without changing how game code is written. You configure only the ROM size:
META ROM_TOTAL_SIZE = 65536 # pick your target size (see table below)
META ROM_BANK_SIZE = 16384 # always 16 KB per bankHow the custom cartridge works
| 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 |
Switching is triggered by a single write: STA $DF00 with the desired bank number in the A register. $DF00 is in the cartridge I/O range — the Vectrex BIOS, RAM, and VIA chips do not respond there, so the write is harmless on stock hardware and unambiguous on the custom cartridge. The cartridge address decoder latches A and remaps the bank visible at $0000–$3FFF.
The helpers bank at $4000–$7FFF is hardwired to the last ROM bank and never changes — that gives the compiler a stable place to put cross-bank call wrappers, the bank-switching routine itself, and any function called frequently from multiple banks.
; Generated cross-bank call wrapper (emitted by the compiler in the helpers bank)
CALL_BANKED_my_function:
LDA CURRENT_BANK ; save current
PSHS A
LDA #2 ; target bank
STA CURRENT_BANK
STA $DF00 ; <-- the only hardware-specific instruction
JSR my_function ; runs inside bank 2 ($0000-$3FFF window)
PULS A
STA CURRENT_BANK
STA $DF00 ; restore previous bank
RTSBank switching happens automatically — DRAW_VECTOR, SHOW_LEVEL, UPDATE_LEVEL, and PLAY_SFX all switch to the right bank and back without any VPy code involvement.
Hardware: build it yourself
The custom cartridge needs only off-the-shelf components — an EPROM (or Flash) and a 74-series latch wired to decode $DF00 writes. For the 64 KB tested configuration:
- 1× 27C512 EPROM (64 KB, ~$2)
- 1× 74HC373 octal latch (~$1)
- 1× 74HC138 or 74HC139 decoder (~$1)
- A standard 36-pin Vectrex cartridge PCB
Total: under $10. Larger ROMs use the same circuit with a wider latch.
A more polished cartridge — based on an RP2350 MCU that also acts as a debug bridge for the IDE — is in development on the
feat/mcu-cartridgebranch. Until that's released, the discrete-logic design above is the way to run multibank games on real hardware.
Supported ROM sizes and recommended chips
ROM_TOTAL_SIZE | Banks | Capacity | Recommended chip | Notes |
|---|---|---|---|---|
32768 | 2 | 32 KB | 27C256 | Single-bank; no switching needed |
65536 | 4 | 64 KB | 27C512 | ✅ Fully tested, recommended starting point |
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 | Maximum supported size |
Current status: The 4-bank (64 KB) configuration is fully tested. Larger configs compile and the bank-switching infrastructure is in place, but cross-bank symbol resolution for more than 4 banks is still being completed.
The LEVEL_LOADED bug
During testing we found a subtle but nasty bug: SHOW_LEVEL_RUNTIME used CMPX #0; BEQ DONE to detect "no level loaded yet". That works fine unless the level asset happens to be the first object assembled in its bank — which gives it address $0000. The guard fires, the level is never drawn, and there is no error message.
The fix is a dedicated 1-byte LEVEL_LOADED flag: LOAD_LEVEL sets it to 1; SHOW_LEVEL checks it with TST >LEVEL_LOADED; BEQ DONE before loading the pointer. The null-pointer guard is gone.
EPROM programmer in the IDE
The IDE now has a dedicated EPROM Programmer dialog. Click the button in the toolbar, insert your blank chip into the TL866II+, and write your ROM without leaving Vectrex Studio.
What the dialog provides
Chip selection — choose your EPROM model from the list (27C256, 27C512, 27C010, etc.). The selection is remembered between sessions.
Write options — three checkboxes let you tune the programming behaviour, all persisted across sessions:
| Option | What it does |
|---|---|
| Skip erase | Don't blank-erase before writing (useful for chips already erased) |
| Skip verify | Don't read back and verify after writing (faster, less safe) |
| Unprotect | Remove write-protection before programming |
minipro detection — on macOS, if minipro is not installed the dialog shows a Install via Homebrew button that runs brew install minipro for you. On other platforms the install path is shown.
Live log — the full minipro output streams into a scrollable log panel inside the dialog so you can see progress and any errors without switching to a terminal.
Workflow
- Compile your project (produces
build/mygame.bin). - Click EPROM Programmer in the toolbar.
- Select the chip type and adjust options if needed.
- Insert your blank EPROM into the TL866II+ and click Write.
The .bin file from the last successful build is used automatically.
SET_INTENSITY now controls DRAW_VECTOR brightness
DRAW_VECTOR renders .vec vector assets using a custom draw loop called DSWM (Draw Sync With Mirrors). On real Vectrex hardware, line brightness is set by the T1 timer and the VIA Port A DAC. For months, calling SET_INTENSITY(64) before DRAW_VECTOR("ship") had no visible effect — vectors always drew at full brightness.
The root cause was a chain of small issues:
SET_INTENSITYdidn't write toDRAW_VEC_INTENSITY— it updated the BIOS variable but theDSWMloop was reading a separate RAM cell that was never written.- VIA Port A + Z-axis strobe wasn't being set —
DSWMwas missing the write to Port A and the CB2 strobe that tells the hardware DAC to latch the value. - T1 setup was in the wrong place — the T1 timer load must immediately precede
CLR VIA_t1_cnt_hito take effect; a displaced load was silently ignored. - Per-path intensity from
.vecdata — each path in a.vecfile carries an intensity FCB byte.DSWMwas only reading it on the first path.
All four issues are now fixed. SET_INTENSITY correctly programs the DAC before each DRAW_VECTOR call, and the per-segment FCB values in .vec files are respected.
def loop():
# Dim background vectors
SET_INTENSITY(40)
DRAW_VECTOR("background")
# Bright foreground objects
SET_INTENSITY(127)
DRAW_VECTOR("player")
DRAW_VECTOR("enemies")Typed variables — u8, i8, u16, i16
VPy now has four integer types declared with Python type-hint syntax. Before this release, every variable was an implicit i16 (2 bytes). Now you can pick the right size for the job:
health: u8 = 100 # 8-bit unsigned — 0–255
direction: i8 = 1 # 8-bit signed — -128 to +127
score: u16 = 0 # 16-bit unsigned — 0–65535
pos_x: i16 = 0 # 16-bit signed — -32768 to +32767| Type | Size | Range |
|---|---|---|
u8 | 1 byte | 0–255 |
i8 | 1 byte | -128 to +127 |
u16 | 2 bytes | 0–65535 |
i16 | 2 bytes | -32768 to +32767 |
Arrays scale too — a u8 array of 10 elements uses 10 bytes instead of 20. All code without type annotations continues to compile unchanged (i16 default).
One known edge case: const arrays with a u8 annotation can cause issues in complex games. Keep const arrays untyped as a workaround.
Level scrolling — SET_CAMERA_X
.vplay levels can be wider than the Vectrex screen. The new SET_CAMERA_X(x) builtin moves the viewport horizontally through the world. Objects outside the visible range are culled automatically — nothing extra needed in game code.
camera_x: i16 = 0
def loop():
joy = J1_X()
if joy > 20:
camera_x = camera_x + 2
if joy < -20:
camera_x = camera_x - 2
camera_x = clamp(camera_x, 0, 800) # keep inside world bounds
SET_CAMERA_X(camera_x)
SHOW_LEVEL()Object positions in .vplay files are world coordinates. SHOW_LEVEL() subtracts CAMERA_X from each object's X before drawing. LOAD_LEVEL() always resets CAMERA_X to 0.
Only horizontal scrolling is supported in this release.
Core compiler deprecated
The Core compiler (vectrexc, core/) is now in maintenance mode. It will not receive new features. All active development targets the Buildtools pipeline (vpy_cli, buildtools/).
If you have an existing Core project, migration is straightforward:
- Create a
.vpyprojfile next to your.vpyfile (see the compiler docs for the format). - In the IDE Settings panel, switch the backend selector to Buildtools (New).
- Rebuild — the output should be identical for single-bank projects.
Buildtools handles both 32 KB (single-bank) and 64 KB+ (multibank) games. For new projects, always start with Buildtools.