wasmcart
The reference path, and the one existing games arrive on.
The ABI header is C, the reference carts are C, and an existing C or SDL2 game ports without being rewritten.
Three exports and a struct. This is the complete program:
#include "wasmcart.h"
#define WIDTH 320
#define HEIGHT 240
static uint32_t framebuffer[WIDTH * HEIGHT];
static wc_info_t info;
__attribute__((export_name("wc_get_info")))
wc_info_t* wc_get_info(void) {
info.version = 3;
info.width = WIDTH;
info.height = HEIGHT;
info.fb_ptr = (uint32_t)(uintptr_t)framebuffer;
return &info;
}
__attribute__((export_name("wc_init")))
void wc_init(void) {}
__attribute__((export_name("wc_render")))
void wc_render(void) {
for (int i = 0; i < WIDTH * HEIGHT; i++)
framebuffer[i] = 0xFFFF0000;
}
emcc -sSTANDALONE_WASM=1 -sALLOW_MEMORY_GROWTH=1 --no-entry -O2 -o cart.wasm cart.c
npx wasmcart pack --wasm cart.wasm --name my-game \
--width 320 --height 240 -o my-game.wasc
npx wasmcart my-game.wasc
Emscripten with -sSTANDALONE_WASM=1, or wasi-sdk. The one thing that matters is that the output is standalone WASM rather than a JS-glue bundle expecting a browser to boot it.
The spec repo's include/ directory ships reusable headers, so a cart rarely writes the struct by hand:
wasmcart.hThe ABI header: wc_info_t and wc_pad_t, the flags, and the GL and host imports. Include this first.
wc_cart.hCart boilerplate: buffer declarations plus WC_FILL_INFO, and the opt-in debug fields a harness can read.
wc_fb.h2D drawing straight into the framebuffer: fill_rect, blit, alpha blend.
wc_gl.h / wc_gl_blit.hShader compile and link, VAO and VBO helpers, and a CPU-to-GPU blit for software renderers that still want to present through GL.
wc_math.h and friendssin, cos, sqrt and atan2 without libm, plus wc_mat4.h and wc_vec3.h for 3D.
wc_pcm_mixer.hA multi-channel PCM mixer and WAV parser, so audio does not need an external library either.
That is its own path, and the more common one. wasmcart-sdl2 is an SDL2 backend whose video and audio drivers target the cart's framebuffer and audio ring, so a game links against it instead of a windowing system. Its joystick backend surfaces the host pads as real SDL devices, rumble included.
C++ uses the same toolchain and the same headers. Engine-scale C++ works: the constraint is the GL surface, not the language.
Two requirements do the real filtering, and neither is about syntax. The toolchain must emit standalone WASM exporting wc_get_info, wc_init and wc_render. And for GPU carts, every GL function must be declared as a WASM import at compile time: there is no eglGetProcAddress in WebAssembly, so a runtime that discovers GL entry points dynamically cannot draw.