Console32 games are Rust crates. You write a struct, implement three methods, and the SDK handles everything else. The same binary compiles to WebAssembly for the emulator and to bare-metal ARM for real hardware.

This guide takes you from an empty directory to a running game in the desktop emulator. Hardware deployment is covered at the end.


Prerequisites

You need Rust installed via rustup, then:

# Embedded target for hardware builds
rustup target add thumbv8m.main-none-eabihf

# WASM target for the emulator
rustup target add wasm32-unknown-unknown

# CLI tooling
cargo install cargo-console32
cargo install wasm-bindgen-cli
cargo install console32-tauri

wasm-bindgen-cli must match the version of the wasm-bindgen crate in your game's Cargo.lock. If you see a version mismatch error after the first build, run cargo install wasm-bindgen-cli --version <version> with the version from your lock file.


Create a project

cargo console32 new my_game
cd my_game

This scaffolds a complete project:

my_game/
├── src/
│   ├── game.rs       ← your game logic (edit this)
│   ├── main.rs       ← hardware entry point (rarely touch this)
│   └── lib.rs        ← WASM entry point (rarely touch this)
├── Cargo.toml
├── Console32.toml    ← name, version, signing key path
├── memory_slot.x     ← linker script for hardware
├── build.rs
└── .cargo/config.toml

You write your game in src/game.rs. The main.rs and lib.rs files are thin wrappers that wire the game up to the hardware ABI or the WASM emulator respectively — you won't normally need to touch them.


Run in the emulator

cargo console32 run

That's it. The CLI builds your game to WASM, then opens a native desktop window running the emulator with your game loaded. The window behaves like a real Console32: 320×240 pixels, keyboard mapped to buttons.

To rebuild and relaunch after changes, just run it again. For a browser tab instead of a native window:

cargo console32 run --target browser

The game loop

Open src/game.rs. You'll see a struct implementing the Game trait:

use console32_sdk::prelude::*;

pub struct MyGame {
    x: i16,
    y: i16,
}

impl Game for MyGame {
    fn init(_c: &mut Console) -> Self {
        MyGame { x: 160, y: 120 }
    }

    fn update(&mut self, c: &mut Console) {
        if c.input.is_held(buttons::LEFT)  { self.x -= 2; }
        if c.input.is_held(buttons::RIGHT) { self.x += 2; }
        if c.input.is_held(buttons::UP)    { self.y -= 2; }
        if c.input.is_held(buttons::DOWN)  { self.y += 2; }
    }

    fn render(&mut self, c: &mut Console) {
        c.display.clear(BLACK);
        c.display.fill_circle(self.x, self.y, 10, GREEN);
        c.display.draw_text("Hello, Console32!", 8, 8, WHITE);
    }
}

The three methods are called every frame at 60 Hz:

MethodCalledPurpose
initOnce at startupConstruct initial game state
updateEvery frameRead input, advance simulation
renderEvery frame after updateDraw the current state

Console is your interface to everything the hardware provides. It gets passed in as &mut Console — there are no globals, no singletons.


Drawing

The screen is 320×240 pixels. Origin (0, 0) is top-left. All coordinates are i16.

// Fill the screen with a colour
c.display.clear(BLACK);

// Primitives
c.display.fill_rect(x, y, width, height, RED);
c.display.draw_rect(x, y, width, height, WHITE);    // outline only
c.display.fill_circle(cx, cy, radius, CYAN);
c.display.draw_circle(cx, cy, radius, YELLOW);
c.display.draw_line(x0, y0, x1, y1, GREEN);

// Text
c.display.draw_text("score: 0", x, y, WHITE);

Colours are u16 RGB565. The prelude exports named constants: BLACK, WHITE, RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA. For anything else:

let orange = rgb(255, 140, 0);
let dark_grey = rgb(40, 40, 40);

Input

Console32 has ten buttons across three groups:

GroupConstants
FaceA, B, X, Y
D-padUP, DOWN, LEFT, RIGHT
ShoulderL, R
use buttons::*;

// True every frame the button is physically held down
if c.input.is_held(UP)    { player.y -= 2; }
if c.input.is_held(DOWN)  { player.y += 2; }
if c.input.is_held(LEFT)  { player.x -= 2; }
if c.input.is_held(RIGHT) { player.x += 2; }

// True only on the first frame the button goes down
if c.input.is_pressed(A) { /* jump, confirm */ }
if c.input.is_pressed(L) { /* pause menu */ }

// True only on the frame the button is released
if c.input.is_released(B) { /* charge-release mechanic */ }

The constants can be OR'd together: is_held(L | R) returns true only when both are held simultaneously.


Fixed-point maths

There's no floating point on Console32. Positions use Fixed (16.16 fixed-point, wrapping i32):

let speed = Fixed::from(3);          // 3.0 as fixed-point
let x = Fixed::from(160);

let angle: u16 = 8192;               // u16 full-circle; 8192 = 45°
let dx = c.math.cos(angle);          // returns i16 in 1.15 fixed-point
let dy = c.math.sin(angle);          // >> 15 to get a ratio

// Move in a direction
let new_x = x + Fixed::from((speed.to_i32() * dx as i32) >> 15);

For positions and velocities, prefer Vec2i from the prelude — it pairs two Fixed values and has helpers for length, distance, and lerp.


No heap allocation

Console32 games cannot use Vec, Box, or any dynamic allocator. All data structures must have sizes known at compile time.

For collections with a runtime-varying count, use Pool<T, N>:

// At most 64 bullets in flight at once
let mut bullets: Pool<Bullet, 64> = Pool::new();

bullets.spawn_with(Bullet { x: player.x, y: player.y, vx: 0, vy: -4 });

for bullet in bullets.iter_mut() {
    bullet.y -= bullet.vy;
}

bullets.retain(|b| b.y > 0);   // remove off-screen bullets

This is a firm constraint, not a guideline. Attempting to use a global allocator will fail to compile.


Building for hardware

When you have real hardware and the firmware flashed:

# Build a signed .c32 package
cargo console32 build --release

# Copy to the device over USB
cargo console32 deploy

deploy puts the device into USB mass storage mode and copies the .c32 file. The firmware verifies the binary's header and signature, copies it into RAM, and starts the game loop.

For iterative on-hardware testing with a debug probe connected:

cargo console32 run --target hardware

This uses picotool to flash and restart in one step.


Key constraints at a glance

ConstraintWhy
No heap allocationBare-metal, no allocator, no fragmentation surprises
No floating pointDeterministic frame budget; WASM/ARM FPU parity
192 KB code limitGame binary runs from a reserved RAM region
320×240 framebuffer150 KB — DMA-flushed to display after render returns
60 Hz target~7 ms budget for update + render combined

Next steps

  • Read No OS, No Linker, No Problem for the architecture behind the ABI
  • Check console32-games on GitHub for Snake and Hello World as complete examples
  • Browse the API reference for the full display, input, audio, storage, and maths method signatures