All posts

v0.2.0 — Multibank ROMs, EPROM programmer, typed variables, and scrolling

Multibank ROMs up to 4 MB, a built-in EPROM programmer dialog, DRAW_VECTOR brightness fixes, typed variables (u8/i8/u16/i16), and horizontal level scrolling via SET_CAMERA_X.

releasevectrexvpymultibankepromhardwaretypesscrolling

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 .bin produced 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 bank

How the custom cartridge works

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

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
    RTS

Bank 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-cartridge branch. 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_SIZEBanksCapacityRecommended chipNotes
32768232 KB27C256Single-bank; no switching needed
65536464 KB27C512✅ Fully tested, recommended starting point
1310728128 KB27C010
26214416256 KB27C020
52428832512 KB27C040 / 29F040 Flash
1048576641 MB27C080 / 29F080
20971521282 MBM27C160
41943042564 MB29F320 FlashMaximum 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:

OptionWhat it does
Skip eraseDon't blank-erase before writing (useful for chips already erased)
Skip verifyDon't read back and verify after writing (faster, less safe)
UnprotectRemove 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

  1. Compile your project (produces build/mygame.bin).
  2. Click EPROM Programmer in the toolbar.
  3. Select the chip type and adjust options if needed.
  4. 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:

  1. SET_INTENSITY didn't write to DRAW_VEC_INTENSITY — it updated the BIOS variable but the DSWM loop was reading a separate RAM cell that was never written.
  2. VIA Port A + Z-axis strobe wasn't being setDSWM was missing the write to Port A and the CB2 strobe that tells the hardware DAC to latch the value.
  3. T1 setup was in the wrong place — the T1 timer load must immediately precede CLR VIA_t1_cnt_hi to take effect; a displaced load was silently ignored.
  4. Per-path intensity from .vec data — each path in a .vec file carries an intensity FCB byte. DSWM was 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
TypeSizeRange
u81 byte0–255
i81 byte-128 to +127
u162 bytes0–65535
i162 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:

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