Skip to content

Instantly share code, notes, and snippets.

@peterhellberg
Last active March 16, 2026 22:12
Show Gist options
  • Select an option

  • Save peterhellberg/b331a0330325084ffcb3ab1e48302061 to your computer and use it in GitHub Desktop.

Select an option

Save peterhellberg/b331a0330325084ffcb3ab1e48302061 to your computer and use it in GitHub Desktop.
WASMCarts in Zig

WASMCarts in Zig

I just found out about the in development fantasy console

https://github.com/pgattic/wasm-experiment

Since I'm on Pop_OS! which is currently based on 24.04 I need to install a newer version of cmake than what is in the default repos:

I installed SDL3 from source, such as what is described in this article:

wasmcarts-buildtool

This tool is written in Rust, so I just had to cargo build --release and then symlink the resulting binary to somewhere in my $PATH

wasmcarts (Linux engine)

$ export WASM3_SOURCE=/home/peter/Code/GitHub/wasm3/wasm3
$ CC="zig cc -fno-sanitize=undefined" cmake -S . -B build/linux -DCMAKE_C_FLAGS="-std=c23" -DCMAKE_C_FLAGS="-D_GNU_SOURCE"

Example cart in Zig

WASMCarts.toml

[package]
name = "wc-zig"
author = "Peter Hellberg"
version = "0.0.1"

[build.code]
command = ["zig", "build"]
output = "zig-out/bin/cart.wasm"

[build.assets]
dir = "assets"
sprite_tiles = "spr.4bpp"
background_tiles = "bg.4bpp"
background_map = "bg.map"

build.zig

const std = @import("std");

pub fn build(b: *std.Build) !void {
    const mod = b.createModule(.{
        .root_source_file = b.path("src/cart.zig"),
        .target = b.resolveTargetQuery(.{
            .cpu_arch = .wasm32,
            .os_tag = .wasi,
        }),
        .optimize = .ReleaseSmall,
    });

    mod.export_symbol_names = &[_][]const u8{
        "setup",
        "update",
    };

    const exe = b.addExecutable(.{
        .name = "cart",
        .root_module = mod,
    });

    exe.entry = .disabled;
    exe.import_memory = false;

    b.installArtifact(exe);
}

src/wc.zig

pub extern "env" fn _rand() u32;
pub extern "env" fn _clearScreen(c: u8) void;
pub extern "env" fn _pSet(x: i32, y: i32, c: u8) void;
pub extern "env" fn _rect(x: i32, y: i32, w: u32, h: u32, c: u8) void;
pub extern "env" fn _rectFill(x: i32, y: i32, w: u32, h: u32, c: u8) void;
pub extern "env" fn _sprite(x: i32, y: i32, id: u8) void;
pub extern "env" fn _btn(btn: u8) bool;
pub extern "env" fn _btnP(btn: u8) bool;
pub extern "env" fn _printLnDbg(ptr: usize) void;

pub fn rand() u32 {
    return _rand();
}

pub fn clearScreen(c: u8) void {
    _clearScreen(c);
}

pub fn pSet(x: i32, y: i32, c: u8) void {
    _pSet(x, y, c);
}

pub fn rect(x: i32, y: i32, w: u32, h: u32, c: u8) void {
    _rect(x, y, w, h, c);
}

pub fn rectFill(x: i32, y: i32, w: u32, h: u32, c: u8) void {
    _rectFill(x, y, w, h, c);
}

pub fn sprite(x: i32, y: i32, id: u8) void {
    _sprite(x, y, id);
}

pub const Button = enum(u8) {
    Left = 0,
    Right = 1,
    Up = 2,
    Down = 3,
    A = 4,
    B = 5,
    X = 6,
    Y = 7,
    L = 8,
    R = 9,
    Start = 10,
    Select = 11,
};

pub fn btn(button: Button) bool {
    return _btn(@intFromEnum(button));
}

pub fn btnP(button: Button) bool {
    return _btnP(@intFromEnum(button));
}

pub fn print(str: []const u8) void {
    _printLnDbg(@intFromPtr(str.ptr));
}

src/cart.zig

const wc = @import("wc.zig");

export fn setup() void {
    wc.print("Hello from Zig!");
}

export fn update() void {
    const w = 24;
    const h = 24;

    wc.clearScreen(0);

    wc.rectFill(12, 12, w, h, 1);
    wc.rectFill(24, 24, w, h, 2);
    wc.rectFill(36, 36, w, h, 3);
    wc.rectFill(48, 48, w, h, 4);
    wc.rectFill(60, 60, w, h, 5);
}
@peterhellberg
Copy link
Copy Markdown
Author

peterhellberg commented Mar 14, 2026

Updated src/cart.zig

const wc = @import("wc.zig");

const State = struct {
    score: u32 = 0,
    pos: @Vector(2, i32) = .{ 0, 0 },
};

var state = State{};

export fn setup() void {
    wc.log("Hello from Zig!");
}

export fn update() void {
    // Update the score
    if (wc.btnp(.X)) state.score +|= 1;
    if (wc.btnp(.Y)) state.score -|= 1;

    // Update the position
    if (wc.btn(.Left)) state.pos[0] -|= 1;
    if (wc.btn(.Right)) state.pos[0] +|= 1;
    if (wc.btn(.Up)) state.pos[1] -|= 1;
    if (wc.btn(.Down)) state.pos[1] +|= 1;

    if (wc.btnp(.X) or wc.btnp(.Y) or
        wc.btnp(.Left) or wc.btnp(.Right) or
        wc.btnp(.Up) or wc.btnp(.Down))
    {
        wc.logf("State: {}", .{state});
    }

    draw();
}

fn draw() void {
    // Clear the screen with black
    wc.cls(0);

    // Draw colored dots on the background
    for (0..wc.SCREEN_HEIGHT / 16) |uy| {
        for (0..wc.SCREEN_WIDTH / 16) |ux| {
            const x: i32 = @intCast(ux * 16);
            const y: i32 = @intCast(uy * 16);
            wc.pset(x + 8, y + 8, @intCast(1 + @mod((x ^ y), 15)));
        }
    }

    // Yellow rectangle
    wc.rect(2, 2, 8, 8, 10);

    // Draw a few pixels
    wc.pset(4, 4, 11);
    wc.pset(5, 5, 12);
    wc.pset(6, 6, 13);
    wc.pset(7, 7, 14);

    const w = 24;
    const h = 24;

    // A few rectangles
    wc.rectFill(12, 12, w, h, 1);
    wc.rect(24, 24, w, h, 2);
    wc.rectFill(36, 36, w, h, 3);
    wc.rect(48, 48, w, h, 4);
    wc.rectFill(60, 60, w, h, 5);

    // Rectangles that only show up when A or B is pressed
    if (wc.btn(.A)) wc.rectFill(72, 72, w, h, 6);
    if (wc.btn(.B)) wc.rectFill(84, 84, w, h, 7);

    // Log button presses
    if (wc.btnp(.Left)) wc.log("Left");
    if (wc.btnp(.Right)) wc.log("Right");
    if (wc.btnp(.Up)) wc.log("Up");
    if (wc.btnp(.Down)) wc.log("Down");
    if (wc.btnp(.A)) wc.log("A");
    if (wc.btnp(.B)) wc.log("B");
    if (wc.btnp(.X)) wc.log("X");
    if (wc.btnp(.Y)) wc.log("Y");
    if (wc.btnp(.L)) wc.log("L");
    if (wc.btnp(.R)) wc.log("R");
    if (wc.btnp(.Start)) wc.log("Start");
    if (wc.btnp(.Select)) wc.log("Select");

    // Display some text
    wc.rectFill(12, 110, 208, 28, 0);
    wc.print(12, 110, "1234567890+!#%&/([{}])=?@$");
    wc.print(12, 120, "abcdefghijklmnopqrstuvwxyz");
    wc.print(12, 130, "ABCDEFGHIJKLMNOPQRSTUVWXYZ");

    // Display a single character
    wc.chr(12, 145, '#');

    // Draw some sprites
    wc.spr(90, 10, 0);
    wc.spr(90, 20, 1);
    wc.spr(90, 30, 2);
    wc.spr(90, 40, 3);
    wc.spr(90, 50, 4);
    wc.spr(90, 60, 5);

    wc.printf(122, 6, "Score: {}", .{state.score});

    // Draw a sprite at the current position
    wc.spr(state.pos[0], state.pos[1], 0);
}
Screenshot_2026-03-14_20-47-50

@peterhellberg
Copy link
Copy Markdown
Author

peterhellberg commented Mar 16, 2026

Basic conversion of 4bpp➤png and png➤4bpp

https://gist.github.com/peterhellberg/5f881735c6e91eadeefd8e0a7e6b3bab

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment