Cart slot
Insert a cart
Every cart below is a plain WebAssembly module running on the reference renderer, 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.
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));
}
Add the SDK
wasm-ad = { git = "https://github.com/raskell-io/wasm-ad" }Keep out of the hardware region
Three linker flags put your data and stack above 0x28000. The README has the .cargo/config.toml.
Build and play
cargo build --release --target wasm32-unknown-unknownOff 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.
#include "wasm_ad.h"
WAD_CART(WAD_TITLE("My Cart") WAD_AUTHOR("me"));
WAD_EXPORT(start) void start(void) {
wad_bg_palette[0] = wad_rgb(40, 60, 160); /* backdrop */
wad_obj_palette[1] = wad_rgb(255, 216, 64);
for (int i = 0; i < 32; i++) wad_obj_tiles[0][i] = 0x11;
WAD_DISPCNT = WAD_MODE0 | WAD_DISP_OBJ | WAD_OBJ_1D;
}
WAD_EXPORT(update) void update(void) {
int32_t x = (int32_t)(wad_frame() % 240);
wad_oam_set(0, wad_sprite_new(x, 76, WAD_S8x8, 0));
}
Add the SDK
One header, sdk/c/wasm_ad.h: no library, no libc. It compiles as C11 and C++17.
Keep out of the hardware region
-Wl,--global-base=163840 -Wl,-z,stack-size=65536WAD_CART writes the metadata section itself.
Build and play
clang --target=wasm32 -O2 -nostdlib -Wl,--no-entry \
-Wl,--global-base=163840 cart.c -o cart.wasmNeeds clang with the WebAssembly backend and wasm-ld; on macOS, Homebrew’s llvm and lld.
The C / C++ SDK’s README has the full setup and API.
const ad = @import("wasm_ad");
comptime {
ad.cart(.{ .title = "My Cart", .author = "me" });
}
export fn start() void {
ad.bgPalette()[0] = ad.rgb(40, 60, 160); // backdrop
ad.objPalette()[1] = ad.rgb(255, 216, 64);
ad.objTiles()[0] = @splat(0x11); // a solid 8×8 tile
ad.reg.DISPCNT.write(ad.dispcnt.MODE0 | ad.dispcnt.OBJ | ad.dispcnt.OBJ_1D);
}
export fn update() void {
const x: i32 = @intCast(ad.frame() % 240);
ad.oam.set(0, ad.Sprite.init(x, 76, .s8x8, 0));
}
Add the SDK
.wasm_ad = .{ .path = "../wasm-ad/sdk/zig" },A path dependency in build.zig.zon, to a checkout or submodule.
Keep out of the hardware region
The SDK’s addCart build helper sets the target, data base, stack and memory size.
Build and play
zig buildThe Zig SDK’s README has the full setup and API.
import { write16, reg, dispcnt, rgb, setBgColor, setObjColor, objTile,
fillTile, frame, oam, sprite, SpriteSize } from "@raskell/wasm-ad/assembly";
export function start(): void {
setBgColor(0, rgb(40, 60, 160)); // backdrop
setObjColor(1, rgb(255, 216, 64));
fillTile(objTile(0), 0x11); // a solid 8×8 tile
write16(reg.DISPCNT, dispcnt.MODE0 | dispcnt.OBJ | dispcnt.OBJ_1D);
}
export function update(): void {
const x = <i32>(frame() % 240);
oam.set(0, sprite.make(x, 76, SpriteSize.S8x8, 0));
}
Add the SDK
npm install @raskell/wasm-ad [email protected]Keep out of the hardware region
{ "extends": "./node_modules/@raskell/wasm-ad/cart.asconfig.json" }The SDK’s config sets memoryBase. Keep state in linear memory, not module-level lets: the README shows how.
Build and play
npx asc cart.ts --config asconfig.json -o cart.wasm
npx wasmad meta cart.wasm title="My Cart"The AssemblyScript SDK’s README has the full setup and API.
package main
import ad "github.com/raskell-io/wasm-ad/sdk/tinygo"
//export start
func start() {
ad.BGPalette()[0] = ad.RGB(40, 60, 160) // backdrop
ad.ObjPalette()[1] = ad.RGB(255, 216, 64)
for i := range ad.ObjTiles()[0] {
ad.ObjTiles()[0][i] = 0x11 // a solid 8×8 tile
}
ad.DISPCNT.Write(ad.Mode0 | ad.DispObj | ad.Obj1D)
}
//export update
func update() {
x := int32(ad.Frame() % 240)
ad.OAMSet(0, ad.NewSprite(x, 76, ad.S8x8, 0))
}
func main() {}
Add the SDK
go mod edit -require=github.com/raskell-io/wasm-ad/sdk/[email protected]
go mod download github.com/raskell-io/wasm-ad/sdk/tinygoNot go get: the SDK only type-checks under TinyGo.
Keep out of the hardware region
Build with the SDK’s TinyGo target, wasm-ad.json (copy it from the module): no WASI, data above 0x28000, no heap.
Build and play
tinygo build -target wasm-ad.json -scheduler=none \
-panic=trap -opt=z -o cart.wasm .
node wasm-ad/tools/wasmad.mjs meta cart.wasm title="My Cart"The Go 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.