wasm96-sdk

Crates.iowasm96-sdk
lib.rswasm96-sdk
version0.1.2
created_at2025-12-20 16:58:47.127094+00
updated_at2025-12-26 20:21:16.698079+00
descriptionSDK for building WASM apps that run under the wasm96 libretro core.
homepagehttps://github.com/isaiahpettingill/wasm96
repositoryhttps://github.com/isaiahpettingill/wasm96
max_upload_size
id1996725
size46,543
Isaiah Pettingill (isaiahpettingill)

documentation

https://github.com/isaiahpettingill/wasm96

README

wasm96-sdk

A Rust SDK for building WebAssembly applications that run under the wasm96 libretro core.

Overview

wasm96-sdk provides safe, ergonomic bindings to the wasm96 guest ABI, allowing you to write games and applications in Rust that compile to WebAssembly and run in libretro frontends like RetroArch.

Key features:

  • Immediate Mode Graphics: Issue drawing commands (rects, circles, text, etc.) without managing framebuffers.
  • Audio Playback: Play WAV, QOA, and XM files with host-mixed channels.
  • Input Handling: Query joypad, keyboard, and mouse state.
  • Resource Management: Register and draw images (PNG, GIF, SVG), fonts, and other assets by key.
  • Storage: Save/load persistent data.
  • System Utilities: Logging and timing.

ABI model (mental model)

  • The host (wasm96-core) owns the framebuffer and all rendering backends.
  • The guest (your .wasm) issues drawing/audio/input calls.
  • Your guest exports:
    • setup() (required)
    • update() (optional)
    • draw() (optional)

Usage

Add this to your Cargo.toml:

[package]
name = "my-wasm96-app"
version = "0.1.0"
edition = "2024"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm96-sdk = "0.1.0"

In your src/lib.rs:

use wasm96_sdk::prelude::*;

// Required: Called once on startup
#[no_mangle]
pub extern "C" fn setup() {
    graphics::set_size(640, 480);
    // Register assets, initialize state, etc.
}

// Optional: Called once per frame to update logic
#[no_mangle]
pub extern "C" fn update() {
    // Handle input, update game state
}

// Optional: Called once per frame to draw
#[no_mangle]
pub extern "C" fn draw() {
    graphics::background(0, 0, 0); // Black background
    graphics::set_color(255, 255, 255, 255); // White
    graphics::rect(100, 100, 100, 100); // Draw a rectangle
}

Build for WebAssembly:

rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown --release

The output .wasm file can be loaded into the wasm96 core in RetroArch.

Fonts & text (immense documentation)

Text rendering is one of the easiest places to get surprised by ABI and lifecycle details. This section is intentionally long.

Keyed font model (no handles)

Fonts in wasm96 are keyed resources:

  • You register a font under a key.
  • You draw/measure text by providing the same key.
  • No numeric font handles are returned to the guest.

In Rust, the SDK exposes string-key APIs (like "ui" or "debug"), and the SDK hashes those strings to the underlying u64 key.

Host fallback behavior (very important)

The host (wasm96-core) implements a fallback if you try to measure/draw text with a key that has not been registered:

  • graphics::text_key(...) falls back to built-in Spleen at size 16
  • graphics::text_measure_key(...) uses the exact same fallback

This means:

  • text “just works” even if you never registered a font
  • but layout can become unstable if you assumed a different font/size

Best practice: register fonts in setup() and always measure/draw using those same keys.

Which font source should you use?

You have three practical options:

  1. Built-in Spleen (bundled pixel font family)
  • API: graphics::font_register_spleen(key, size)
  • Great for retro UIs, debug overlays, and “it should always work” text.
  • Supported sizes are finite (host-defined); common sizes are 8/16/24/32/64.
  1. BDF (custom bitmap fonts)
  • API: graphics::font_register_bdf(key, bdf_bytes)
  • Best for pixel-perfect fonts you ship.
  • Deterministic bitmap metrics.
  1. TTF/OTF (custom scalable fonts)
  • API: graphics::font_register_ttf(key, font_bytes)
  • Best when you want scalable typography (menus, titles, readability).

What exactly is a “key”?

A key is an arbitrary string you choose, such as:

  • "ui"
  • "debug"
  • "title_32"

You should treat keys as part of your app’s “asset namespace”. They should be:

  • stable (don’t generate random keys)
  • consistent across measure/draw calls
  • registered once, reused forever (unless you intentionally unload)

Recommended usage pattern

  1. In setup():
  • set a resolution
  • register fonts under stable keys
  1. For layout:
  • call graphics::text_measure_key(font_key, text)
  • compute positions
  1. For drawing:
  • set a color
  • call graphics::text_key(x, y, font_key, text)

Example (built-in Spleen):

use wasm96_sdk::graphics;

#[no_mangle]
pub extern "C" fn setup() {
    graphics::set_size(320, 240);
    // Register a predictable font key for UI.
    graphics::font_register_spleen("ui", 16);
}

#[no_mangle]
pub extern "C" fn draw() {
    graphics::background(0, 0, 0);
    graphics::set_color(255, 255, 255, 255);

    let msg = "Hello wasm96";
    let size = graphics::text_measure_key("ui", msg);

    let x = (320i32 - size.width as i32) / 2;
    let y = 20;
    graphics::text_key(x, y, "ui", msg);
}

Example (custom TTF/OTF):

use wasm96_sdk::graphics;

static UI_TTF: &[u8] = include_bytes!("../assets/YourFont.ttf");

#[no_mangle]
pub extern "C" fn setup() {
    graphics::set_size(640, 480);

    // Choose a stable key and register once.
    let ok = graphics::font_register_ttf("ui", UI_TTF);
    if !ok {
        // If you want, log/fallback in your app.
        // The host will still be able to render via fallback Spleen 16 for unknown keys.
    }
}

#[no_mangle]
pub extern "C" fn draw() {
    graphics::background(16, 16, 16);
    graphics::set_color(240, 240, 240, 255);
    graphics::text_key(20, 20, "ui", "TTF text");
}

Measuring vs drawing: keep them consistent

Because the host fallback is defined, if you accidentally measure with "ui" but draw with "UI" (different key), you can end up measuring one font and drawing another.

Best practice:

  • define constants for keys
  • treat keys as case-sensitive

Memory / lifetime rules (guest-side)

All text/font registration APIs that take byte pointers behave like “read immediately”:

  • When you call font_register_*, the host reads the font bytes during the call and parses/copies them host-side.
  • When you call text_key / text_measure_key, the host reads the UTF-8 text bytes during the call.

So:

  • it is safe to pass &str and &[u8] that live only for the duration of the call
  • you do not need to keep the byte buffers alive after registration completes

Unregistering fonts

If you need to reclaim host-side resources (rare for typical games), you can unregister:

  • graphics::font_unregister(key)

After unregistering:

  • the key no longer maps to the registered font
  • drawing/measuring using that key will hit the host fallback (Spleen 16) unless you register again

Troubleshooting font issues

If text is not drawing as expected:

  • Verify you call graphics::set_color(r,g,b,a) before drawing text.
  • Verify your font key matches exactly between registration and draw/measure.
  • If using font_register_spleen, verify you are using a supported size.
  • If using font_register_ttf, verify the font bytes are valid and included correctly.
  • Remember: the host fallback may be masking registration failures by still rendering “something”.

Features

  • std (default): Enables standard library features for convenience.
  • wee_alloc: Optional global allocator for wasm32-unknown-unknown targets.

Examples

See the wasm96 repository for complete examples:

  • rust-guest/: Basic hello-world
  • rust-guest-showcase/: Comprehensive feature demo

Documentation

Generate and view docs locally:

cargo doc --open

ABI Compatibility

This SDK targets the wasm96 ABI as defined in the WIT interface. Ensure your wasm96-core version matches the SDK version for compatibility.

License

MIT License - see LICENSE for details.

Contributing

Contributions are welcome! Please see the main repository for development guidelines.

Commit count: 0

cargo fmt