Fantasy console · Spec v0.2

The handheld that never shipped. Carts are WebAssembly.

WASM-AD is an open standard for a 240×160 console with Game Boy Advance–style hardware. A cart is one .wasm file that draws and plays sound by writing registers and memory, in any language that compiles to WebAssembly.

L R POWER AD A B START SELECT

Booting…

←↑↓→ D-pad X A Z B A/S L/R Enter Start · gamepads and touch work too ·

Cart slot

Insert a cart

Every cart below is a plain WebAssembly module running on the reference core, right in this page.

Hardware

GBA-era specs, bit for bit

Palettes, tiles, maps and sprite attributes use the Game Boy Advance's own formats, so decades of documentation and tools still apply.

240×160pixels at 60 frames per second
32,768colors, RGB555, in 16+16 palette banks
4tile layers, two that rotate and scale, or a bitmap mode
128sprites, 8×8 up to 64×64, with 32 rotation matrices
6sound channels: square, wave, noise and 8-bit samples
αalpha blending, fades, windows, mosaic
160scanline callbacks per frame for raster effects
32 KBbattery-backed save RAM

How it works

The hardware is memory

There is no drawing API. A cart writes tiles, maps, palettes and sprites into fixed addresses of its own linear memory, laid out like the GBA's. Sixty times a second the console calls update() and turns that memory into a frame.

Any language

Rust, Zig, C, AssemblyScript, Go: if it compiles to wasm and can write to an address, it can make a cart. No bindings needed.

Save states for free

All state lives in linear memory. Snapshot it between frames and you have save states, rewind and replays.

Deterministic

No clock, no random syscalls: one boot seed plus the inputs fully determine a run. Netplay and golden-image tests just work.

One file

Code, art and metadata ship in a single .wasm, at most 32 MiB, like a cartridge. One import: trace.

Get started

A cart in twenty lines, in your language

SDKs for Rust, C and C++, Zig, AssemblyScript and Go wrap the registers under the spec’s names. Each ships the same starter cart, and all of them render it identically, frame for frame.

use wasm_ad::*;

wasm_ad::cart! { title: "My Cart", author: "me" }

#[unsafe(no_mangle)]
pub extern "C" fn start() {
    bg_palette()[0] = rgb(40, 60, 160);  // backdrop
    obj_palette()[1] = rgb(255, 216, 64);
    obj_tiles()[0] = [0x11; 32];         // a solid 8×8 tile
    reg::DISPCNT.write(dispcnt::MODE0 | dispcnt::OBJ | dispcnt::OBJ_1D);
}

#[unsafe(no_mangle)]
pub extern "C" fn update() {
    let x = (frame() % 240) as i32;
    oam::set(0, Sprite::new(x, 76, SpriteSize::S8x8, 0));
}
1

Add the SDK

wasm-ad = { git = "https://github.com/raskell-io/wasm-ad" }
2

Keep out of the hardware region

Three linker flags put your data and stack above 0x28000. The README has the .cargo/config.toml.

3

Build and play

cargo build --release --target wasm32-unknown-unknown

Off wasm32 the SDK emulates the hardware, so cart logic is testable with cargo test.

The Rust SDK’s README has the full setup and API.

Any language that compiles to WebAssembly can make a cart without an SDK. node tools/wasmad.mjs check cart.wasm validates one against the spec.