pixel8-runtime 0.2.0

Pixel8 fantasy console runtime: VM, framebuffer, input, audio, assets, carts
Documentation

Pixel8 (pronounced "pixelate") is a tiny, self-contained game console that never existed: a 128x128 screen, 16 fixed colors, a 4x7 pixel font, four audio channels, 256 sprites, a 128x64 tile map — and a Rust compiler where the Lua interpreter would be. You write a little Rust, it compiles to WebAssembly, and it runs inside the console's sandbox at a steady 60 fps (or 30, the cart's choice). Carts are shareable PNG images with the game embedded inside.

New to Pixel8? The Pixel8 Book is a hands-on tutorial — installation to shipped cartridge — with the example carts playable right inside it.

use pixel8::*;

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

impl Game for MyGame {
    fn update(&mut self, ctx: &mut Context) {
        if ctx.is_button_down(Button::Right) {
            self.x += 1;
        }
    }

    fn draw(&self, gfx: &mut Graphics) {
        gfx.clear(Color::BLACK);
        gfx.rect_fill(self.x, self.y, 8, 8, Color::WHITE).unwrap();
    }
}

pixel8::game!(MyGame { x: 64, y: 64 });

The state the preset game! forms name is a constant: it ships placed in the cart's memory image rather than built at start-up, so nothing runs to make it and no stack is spent on a copy of it (a constructor that must run says so with defer, and it — like the Default form — is built at start-up as ever). Whatever needs the console to answer first — saved data, the clock, seats in a physics world — goes in Game::boot, which runs once, on that state, before the first update.

The console

The console is the pixel8-console crate; installing it gives you a pixel8 command:

cargo install pixel8-console
pixel8

You land at the boot console. Type help. The workflow is PICO-8's:

> new mygame          create ./mygame (a real cargo crate!)
> run                 compile to wasm + run     (esc returns)
> save                save code + assets
> export mygame.png   write a shareable png cartridge
> load mygame.png     load a cart back

Esc flips between the console and the editors; the tab icons (or Alt+←/→) switch between code, sprite, map, sfx and music editors. All UI is drawn by the console itself on the same 128x128 screen the games use — there are no native widgets anywhere.

Games are played with the arrow keys plus Z/X (also C/V, N/M). Ctrl+R rebuilds and runs from anywhere; Ctrl+S saves and kicks off a background build, flashing saved / building... / build ok in the editor's bottom bar (compile errors land in the console). F6 while a game runs captures the screen as the cartridge label. Type keys in the console for the full list.

In the terminal

The same console also runs inside a terminal — editors, carts and all — as the separate pixel8-tui binary (so neither frontend drags in the other's dependencies):

cargo install pixel8-tui
pixel8-tui                    boot the console in the terminal
pixel8-tui run mygame.png     boot, load, and run immediately

Terminals with sixel support (foot, WezTerm, Konsole, iTerm2, xterm with -ti vt340...) get real pixels via a pure-Rust encoder; everywhere else the screen is drawn with unicode half-blocks. Ctrl+Q quits. On Linux, game input needs either a terminal with the kitty keyboard protocol or read access to /dev/input (one-time: sudo usermod -aG input $USER). See docs/TUI.md for input details and tuning knobs.

Constraints (they are the point)

thing size
screen 128 x 128, 16 fixed colors
sprites 256 of 8x8 pixels, 8 flags each
map 128 x 64 tiles
sfx 64 slots, 32 steps, 8 waveforms
music 64 patterns, 4 channels
framerate 60 fps (or 30, the cart's choice)
cart one PNG file

Carts also have runtime limits: 128 KiB cart size, 128 KiB RAM, and a 128 K per-frame work budget. By default a cart is #![no_std] and depends only on the pixel8 SDK — that is what pixel8 new scaffolds, and it keeps carts tiny. When one needs a growable vector, string or map, it can pull in heapless for fixed-size collections. Full details in docs/LIMITS.md.

PNG cartridges

export produces a real PNG image — cartridge art, label, title — with the compiled wasm, all assets and (by default) the compressed Rust source embedded in a private chunk. Anyone can see the cart; Pixel8 can play it; and if the source is included, import turns it back into an editable project. See docs/CART_FORMAT.md.

export mygame.html instead produces a single self-contained web page: the cart and the whole console runtime (compiled to wasm) embedded in one file you can double-click or host anywhere, PICO-8-web style. See docs/WEB_EXPORT.md.

Carts also run via pixel8-player, a pure-Rust player with a console-style cart picker. On the desktop it opens a window with keyboard input; on retro handhelds (PowKiddy RGB10S, Anbernic RG351/353 and friends on ArkOS/ROCKNIX) it runs as a static-musl KMS/evdev/ALSA binary — copy it into the ports folder, drop .png carts next to it, play. See docs/HANDHELD.md.

Inspired by PICO-8

Pixel8 is heavily inspired by PICO-8. The palette, the fixed constraints, the editor modes, the > prompt and the overall charm all come from it. What differs is the whole point of the project: a cart is Rust compiled to WebAssembly rather than Lua, the font, code and cartridge formats are entirely original, and Pixel8 is free and open source (GPL-3.0) rather than a paid product.

That shared heritage — the same palette, waveforms and sprite layout — means a PICO-8 cart's assets import almost one-to-one:

pixel8 import-pico8 mygame.p8 mygame      # or mygame.p8.png

The graphics, sprite flags, map, sound effects and music transfer into a new project. Only the assets come across — the cart's Lua code is ignored — and the project gets a stub src/lib.rs to write your game in Rust. See docs/PICO8_IMPORT.md.

Projects are real crates

A Pixel8 project is an ordinary Cargo crate that builds a cdylib for wasm32-unknown-unknown, plus an assets.pixel8.json bundle. The integrated editor is the charming way to work, but $EDITOR + cargo build works exactly the same — the console hot-reloads the wasm when it changes on disk. Headless commands support scripts and CI:

pixel8 new <dir>                  create a project
pixel8 build <dir>                compile it to wasm
pixel8 export <dir> <out.png>     build + write a png cart
pixel8 extract <cart.png> <dir>   editable cart -> project
pixel8 import-pico8 <c> <dir>     pico-8 cart (.p8/.p8.png) -> project
pixel8 export-web <dir> <o.html>  one playable web page
pixel8 verify <cart.png>          run 60 frames headless

The sandbox

Carts execute inside wasmi with no WASI, no filesystem, no network and no host memory access. The only imports a cart gets are the ~50 small, C-like functions of the Pixel8 ABI (docs/ABI.md) — draw, input, audio, map, log. Fuel metering turns infinite loops into a friendly error screen instead of a hung console.

Crates

Pixel8 is a handful of crates. Most people only ever touch the first two — the SDK a cart is written against, and the console that builds and runs it.

  • pixel8 — the SDK your cart depends on, and the only crate a game links against. Zero-dependency out of the box and #![no_std]-friendly: the Game trait, the Context (update-time) and Graphics (draw-time) handles, the 16-color palette and the game! macro that wires it all up. cargo add pixel8 in a cdylib crate and you have a cart. Optional features add ready-made effect families, each in a module of its own — plume-effects brings plume, with the Fire, SmokingFire and Smoke particle plumes and the Explosion burst in it, and physics brings physics, with Gravity, Atmosphere, Wind, collision against the map and between entities, the Force trait a cart writes its own against, and a World that owns a scene's cast and steps and draws it.
  • pixel8-console — the desktop console and toolchain. cargo install pixel8-console gives you the pixel8 command: the boot prompt, the five editors (code, sprite, map, sfx, music), the build-and-hot-reload loop and every headless subcommand (new, build, export, extract, import-pico8, export-web, verify). The crate is pixel8-console; the binary it installs is pixel8. The shell and editors are also exposed as a library (the windowed frontend sits behind the default-on window feature), which is how pixel8-tui reuses them.
  • pixel8-tui — the console in your terminal: the same shell and editors rendered over sixel (pure-Rust encoder) or unicode half-blocks, with crossterm input. A thin frontend over the pixel8-console library that never builds the winit/wgpu window stack.
  • pixel8-runtime — the console's engine, as a reusable library: the 128x128 indexed framebuffer and software rasterizer, font and palette, the wasmi VM with ABI linking and fuel metering, the input model, the 4-channel synth, the shared asset model and the PNG cart codec. Depend on it to embed Pixel8 in your own frontend or build tools around carts — the console and both players are thin shells over it.
  • pixel8-player — a standalone cart player, no editors. Its default window backend opens a desktop window with keyboard input; its kms backend is a static-musl KMS/evdev/ALSA binary for retro handhelds. Point it at a folder of .png carts and play.

The browser player — pixel8-runtime compiled to wasm and wrapped in a small C-like export surface — lives in the repo as pixel8-web and powers export mygame.html. It ships inside exported web pages rather than to crates.io.

Building from source

Requires Rust (with the wasm32-unknown-unknown target for building carts) and, on Linux, ALSA headers for audio:

rustup target add wasm32-unknown-unknown
sudo apt install libasound2-dev        # debian/ubuntu
sudo dnf install alsa-lib-devel        # fedora
# (or build silent with `--no-default-features --features window`)
cargo console                          # alias for: cargo run --release -p pixel8-console
cargo tui                              # alias for: cargo run --release -p pixel8-tui

Try a bundled cart:

cargo console -- examples/platformer

then type run.