Overview
Mote is a native C game engine, console OS, and IDE for the Thumby Color — a dual-core RP2350 handheld with a 128×128 RGB565 screen. Games are plain C modules that link no engine code: the resident OS hands each game a jump table of engine functions and drives its frame loop.
This page documents the complete developer-facing surface:
- The engine API (
MoteApi) — the stable ABI of function pointers a game calls through. - The data types games construct and pass (meshes, sprites, bodies, splats…).
- The header-only math and SDK helpers compiled directly into the game (no ABI cost).
- The module / ABI format and version history.
Programming model
A game is loaded by the OS (on the host via dlopen of a .so; on the device by mapping a
.mote image and jumping to it). The contract is small and forever-stable:
- The OS reads the game's exported ABI version and refuses a mismatch (a game built against a version newer than the engine is rejected).
- The OS calls
mote_game_register(api); the game stashes theconst MoteApi*and returns itsMoteGameVtbl. - The OS owns the frame loop and drives the vtable callbacks (
init, thenupdate/render_band/overlayeach frame).
Everything stateful goes through MoteApi; math and data formats are header-only and compiled into the game directly.
update() starts the
scene and re-submits everything to draw; the draw lists are cleared for you next frame. Keep your own state and draw from it.
It holds 60 fps at thousands of triangles, so re-submitting the whole scene every frame is the intended pattern.MoteApi and MoteGameVtbl are only ever
appended to — never reordered or removed — and MOTE_ABI_VERSION is bumped on any change. A game built
against an older version runs unchanged on a newer engine; new fields it never calls stay harmless.memcpy, memmove, strcmp…), math (sinf, sqrtf… — -lm is linked),
and the printf family for formatting (snprintf into your own buffer, then draw it with
text()). The device build links tiny libc syscall stubs (sdk/mote_syscalls.c) so
stdio's formatting code resolves; a game does no real file I/O, so the stubs are never called. Do not use
malloc/free — the libc heap isn't wired up; use mote->alloc()
(load-time) and file-scope statics instead.Game skeleton
/* mygame/src/game.c */ #include "mote_api.h" #include "mote_build.h" // header-only helpers + auto-includes your baked launcher icon MOTE_GAME_MODULE(); // stashes `mote`, exports the abi-version symbol #ifdef MOTE_MODULE_BUILD // device build only #include "mote_module.h" MOTE_MODULE_HEADER(); #endif static const Mesh *cube; static void g_init(void) { mote->scene_set_background(MOTE_RGB565(10,12,26)); mote->scene_set_sun(v3_norm(v3(0.4f,0.7f,-0.6f))); cube = mote_mesh_box(mote, 1,1,1, MOTE_RGB565(120,180,230)); } static void g_update(float dt) { const MoteInput *in = mote->input(); Mat3 cam = mote_camera_look(v3(0,0,0), v3(0,0,1)); mote->scene_camera(&cam, v3(0,0,-5), 60.0f); mote_draw(mote, cube, v3(0,0,0)); } static const MoteGameVtbl k_vtbl = { .init = g_init, .update = g_update, .config = { .max_tris = 256, .depth = 1 }, }; static const MoteGameVtbl *mote_game_vtbl(void) { return &k_vtbl; }
Build / run / push
mote new mygame # scaffold (use -t 3d|physics|2d for a starter template) mote run mygame # build host .so + launch the SDL emulator mote push mygame --launch # cross-build the .mote, upload over USB, run it mote studio # the IDE (asset authoring + live emulator)
Lifecycle & the vtable
The game returns a MoteGameVtbl of callbacks the OS drives.
| Callback | When | Notes |
|---|---|---|
| init | once, after register | Build/load meshes, sounds, terrain; allocate from the arena. |
| update(dt) | every frame (core 0) | Read input, step state, submit the scene. dt in seconds. |
| render_band(fb, y0, y1) | every frame, both cores | Optional custom raster (raycaster games). NULL ⇒ the built-in 3D scene rasteriser. Disjoint row bands. |
| overlay(fb) | every frame (core 0) | Optional HUD drawn straight into the framebuffer after raster. |
| config | read before init | Resource pools (see below). Read by the OS to size the arena. |
The MENU button's 3-second solo long-hold is reserved by the OS (opens the engine menu / “return to lobby”); all other input is the game's.
Memory model & MoteConfig
The game declares the resource pools it needs in config. At load the OS sizes the engine's buffers to
exactly that (not a fixed worst case), allocates them from one shared SRAM arena, and hands the remainder to the game
through alloc(). A pool left 0 costs nothing and opts that subsystem out
entirely. If the declared pools plus the game's allocations don't fit, the loader refuses to launch (a clear failure, not a crash).
| Field | Type | Meaning |
|---|---|---|
| max_tris | uint16 | 3D triangle draw-list capacity. 0 = no 3D raster. Use a baked model's <name>_TRIS. |
| max_spheres | uint16 | Flat-shaded sphere impostors (scene_add_sphere). |
| max_tex_spheres | uint16 | Textured/oriented impostors (scene_add_sphere_tex). v25 |
| max_points / max_lines / max_discs | uint16 | Depth-tested FX: points, lines, discs. v24 |
| max_rings | uint16 | Camera-facing ring outlines (scene_add_ring). v31 |
| max_shadows | uint16 | Soft ground-shadow decals (scene_add_shadow / _ex). v28 |
| max_billboards | uint16 | Camera-facing textured quads / 3D sprites (scene_add_billboard). v33 |
| max_tex_tris | uint16 | Textured (UV-mapped) mesh triangles per frame. v35 |
| max_splats | uint16 | Gaussian splats. 0 = none. |
| max_sprites | uint16 | 2D sprites per frame. 0 = no 2D scene. |
| max_bodies | uint16 | Physics bodies. 0 = no physics engine. |
| max_contacts | uint16 | Physics contact manifolds. |
| max_mesh_tris | uint16 | Largest mesh collider's triangle count. 0 = none. |
| depth | uint8 | 1 allocates the 32 KB depth buffer (needed for 3D / splats). |
depth=1 game has ~248 KB of arena for the draw-list → roughly
7,000 triangles maximum; framerate gives out before memory does for most games, so size the pool to what you
actually draw. The new-game wizard (Studio / mote new -t) fills these in for you.Engine API — Input v1
All MoteApi calls are made through the mote pointer (e.g. mote->input()).
input
const MoteInput *(*input)(void);
Returns the current frame's derived button state (edges, held timers). Valid during update(). Read it with the
helpers mote_pressed / mote_just_pressed / mote_just_released
rather than poking the struct. See Input state.
Engine API — 3D scene
Build the frame in update(); the OS rasterises the triangle scene across both cores. Submit world-space
geometry once per frame.
scene_set_background v1
void (*scene_set_background)(uint16_t rgb565);
Set the clear colour for the frame.
scene_set_sun v1
void (*scene_set_sun)(Vec3 dir_toward_light_world);
Set the directional light (a world-space unit vector pointing toward the light). Usually set once in init().
scene_camera v13
void (*scene_camera)(const Mat3 *cam_basis, Vec3 cam_pos, float fov_deg);
Begin the 3D scene with a world-space camera. You then add objects at absolute world positions; the engine
subtracts cam_pos for you. This is the preferred entry point — pair it with the mote_draw
helpers and pass the same cam_pos to scene_set_splats for a consistent scene.
| Parameter | Description |
|---|---|
| cam_basis | Orientation basis (rows = right / up / forward). Build with mote_camera_look. |
| cam_pos | Camera world position. |
| fov_deg | Vertical field of view in degrees (≈60 typical). |
scene_begin v1
void (*scene_begin)(const Mat3 *cam_basis, float fov_deg);
Legacy camera entry: the camera sits at the origin and you add objects with camera-relative positions
(subtract the eye yourself). Equivalent to scene_camera with cam_pos = 0.
scene_add_object / scene_add_object_scaled v1
int (*scene_add_object)(const MoteObject *obj); int (*scene_add_object_scaled)(const MoteObject *obj, float scale);
Submit one MoteObject (a mesh + position + orientation, with an optional per-draw colour
override) to the 3D draw list. The scaled form applies a uniform scale. Most games use the mote_draw
helpers instead of building MoteObjects by hand. Returns the new draw-list count (or unchanged when full).
scene_add_sphere v4
int (*scene_add_sphere)(Vec3 cam_rel_pos, float radius, uint16_t color);
Add a cheap per-pixel-shaded sphere impostor, depth-tested with the meshes. Position is camera-relative. Great for
balls, particles, projectiles. Counts against max_spheres.
scene_add_object_ex v24
int (*scene_add_object_ex)(const MoteObject *obj, uint32_t flags);
scene_add_object with per-object draw flags. The one flag so far is
MOTE_DRAW_NO_DEPTH_WRITE — depth-test but don't write, for coplanar overlays (e.g. pocket lips, decals)
that later geometry must paint over.
scene_add_sphere_tex v25
int (*scene_add_sphere_tex)(Vec3 cam_rel_pos, float radius, const Mat3 *orient, const MoteSphereTex *tex);
A textured / oriented sphere impostor. The engine owns the geometry (disc, per-pixel normal, depth) and
rotates the normal into the sphere's local frame by orient (rows right/up/forward; NULL =
identity), so the surface turns as it spins. tex (a MoteSphereTex) supplies the surface: an
equirectangular texture (RGB565 texels, or indices+palette) or a per-pixel
albedo() callback, with a shade_mode (FLAT/LIT/SMOOTH/TOON/GLOSS, or CUSTOM via a
shade() callback). Numbered/striped balls, lit planets. Counts against max_tex_spheres.
scene_add_point / scene_add_line / scene_add_disc v24
int (*scene_add_point)(Vec3 cam_rel_pos, uint16_t color, int size); int (*scene_add_line)(Vec3 a_cam_rel, Vec3 b_cam_rel, uint16_t color); int (*scene_add_disc)(Vec3 cam_rel_pos, float radius, uint16_t color);
Depth-tested FX primitives, drawn in the 3D pass (tested against meshes but not depth-writing, so they layer like
sparks). A size-px point (debris, dust, stars), a 3D line segment (lasers/beams; near-clipped), and a
screen-facing filled disc of world radius (fireballs/glows). Sized by max_points /
max_lines / max_discs.
scene_add_ring v31
int (*scene_add_ring)(Vec3 cam_rel_pos, float radius, uint16_t color);
A camera-facing circle outline of world radius — depth-tested, always facing the camera, so it reads as an
object's circular silhouette (ghost ball at the contact point, target reticle, selection ring). Sized by
max_rings.
scene_add_tri v27
int (*scene_add_tri)(Vec3 a, Vec3 b, Vec3 c, uint16_t color, uint32_t flags);
An immediate-mode world triangle with a caller-supplied flat colour (the engine projects, near-clips, depth-tests
and fills it but does not light it). For dynamic / procedural geometry that isn't a baked Mesh —
generated tables, voxel faces, debug shapes. Double-sided (drawn regardless of winding, unlike
scene_add_object). Emits into the max_tris pool; flags are MOTE_DRAW_*.
scene_add_shadow / scene_add_shadow_ex v28 / v32
int (*scene_add_shadow)(Vec3 ground_pos, float radius, float strength); int (*scene_add_shadow_ex)(Vec3 ground_pos, Vec3 semi_a, Vec3 semi_b, float strength);
A soft ground-shadow decal — a darkening ellipse on the ground plane under an object, foreshortened with the view
and faded from the centre out (strength 0..1, 0 → default). Depth-tested so raised geometry occludes it.
The _ex form takes two world ground-plane semi-axes, so the footprint matches the object's shape and
heading (a tank passes right*halfW and forward*halfL for a long oval); scene_add_shadow
is the round special case. Sized by max_shadows.
scene_add_billboard v33
int (*scene_add_billboard)(Vec3 cam_rel_pos, const MoteImage *img, int fx, int fy, int fw, int fh, float world_h, uint8_t blend);
A camera-facing textured quad — a "3D sprite" — at a world position. It always stays upright and square to the
screen, sized by world_h (its full height in world units), so it shrinks with distance and keeps the
image's aspect. fx/fy/fw/fh pick a sub-rect of the image for sprite sheets (pass 0
sizes for the whole image). Depth-tested against the scene: opaque (MOTE_BLEND_NONE) sprites also write
depth, while blended ones (MOTE_BLEND_ALPHA / _ADD) layer on top. Colour-keyed by the image's
transparent colour. Use for trees, pickups, enemies, smoke, muzzle flashes, explosions. Sized by
max_billboards.
Textured meshes v35
/* on the Mesh: */ const MoteImage *texture; // non-NULL = draw this mesh textured const uint8_t *face_uvs; // nfaces*6: per-corner u0,v0,u1,v1,u2,v2 (0..255)
A Mesh with a non-NULL texture and a face_uvs array is drawn UV-mapped
instead of flat-coloured, through the ordinary scene_add_object path
(no new call). Each face's three corners get texture coordinates in 0..255 spanning the
texture; the texel is still lit by the sun (sampled colour × face shade). Set max_tex_tris to the number
of textured faces visible at once. Translucent meshes (water, glass): pass
MOTE_DRAW_BLEND(MOTE_BLEND_ALPHA) (or _ADD) to
scene_add_object_ex — works on flat-coloured and textured meshes
alike.
Texturing a model in the Studio
You don't have to fill in texture and face_uvs by hand. In the Studio's
Mesh and Rig views there's an Assign… button (and a Clear to remove it):
pick a PNG and it's applied to the model and shown textured live in the preview, and the bake
embeds it automatically. Works for OBJ and STL models.
The image you pick is saved as a <model>.png file next to the model, and the baker
embeds that. If the model is an OBJ that already names a texture through its .mtl
map_Kd, the assigned <model>.png takes priority. The Studio also fills
in the texture coordinates for you: an OBJ uses the texture coordinates already in the model, while an STL
or a decimated mesh (which has none) gets a triplanar projection so it textures cleanly without any.
set_background_cb v26
void (*set_background_cb)(void (*fn)(uint16_t *fb, int y0, int y1));
Registers a per-band background pass run before the 3D scene (depth already cleared), on both cores with
disjoint row bands [y0,y1) — so you can paint a gradient / starfield / nebula that the scene draws over
without owning the whole frame. NULL restores the solid scene_set_background colour.
scene_set_near v28
void (*scene_set_near)(float near_m);
Moves the near clip plane (metres). The 0.5 m default suits a space sim; small scenes (a snooker table) need
~0.05 m or close geometry clips. Also rescales the depth buffer. Set once in init().
scene_tri_count v1
int (*scene_tri_count)(void);
Triangles submitted so far this frame (for budgeting against max_tris).
Engine API — 2D scene & tiles v2
A screen-space 2D scene the OS rasters after the 3D scene (both banded across cores). A game can be pure-2D,
pure-3D, or hybrid (3D world + 2D HUD). Build it in update().
scene2d_begin v2
void (*scene2d_begin)(int cam_x, int cam_y);
Begin the 2D scene; (cam_x,cam_y) is the camera offset applied to all sprite/tile positions at raster time.
scene2d_add v2
int (*scene2d_add)(const MoteSprite *spr);
Add a MoteSprite (a frame of an image at a screen position, with layer + flip/rotate flags).
Returns 0 if the sprite pool is full. Counts against max_sprites.
scene2d_set_tilemap v2
void (*scene2d_set_tilemap)(const MoteTilemap *map, const MoteTileset *tiles);
Draw a plain index tilemap: a cols×rows grid of byte indices into a tileset atlas (0xFF = empty).
scene2d_set_autotiles v14
void (*scene2d_set_autotiles)(const uint8_t *terrain, int cols, int rows, const struct MoteAutotile *const *tiles, int n);
Render-time autotiling. Hand the engine a logical terrain map (one byte per cell: 0 = empty,
1..n index tiles) plus a MoteAutotile ruleset per terrain; the raster picks each
cell's atlas tile from its 8 neighbours. There is no resolved buffer — re-call it for free when the map changes (procgen,
destructible terrain). Build rulesets with mote_autotile_template().
scene2d_set_autotile_layers v15
void (*scene2d_set_autotile_layers)(const uint8_t *map, int cols, int rows, const struct MoteAutotile *const *tiles, int n);
Layered autotiling. map is one byte per cell where each bit is a layer (bit L set ⇒ tiles[L]
drawn there); up to 8 independent, overlapping layers, each autotiled against its own bit and drawn bottom-up. The map is
typically a const baked level (lives in flash, zero SRAM).
blit v2
void (*blit)(uint16_t *fb, const MoteImage *img, int x, int y, int fx, int fy, int fw, int fh, uint8_t flags, int y0, int y1);
Immediate-mode sprite blit straight into the framebuffer (HUD/UI), band-clipped to rows [y0,y1) and colour-keyed.
flags are MOTE_SPR_*. Use from overlay().
blit_ex v34
void (*blit_ex)(uint16_t *fb, const MoteImage *img, float cx, float cy, int fx, int fy, int fw, int fh, float angle, float scale, uint8_t blend, int yc0, int yc1);
A freely rotated and scaled sprite blit, centred at (cx,cy) in framebuffer pixels —
angle in radians, scale 1.0 = original size. Where blit only does axis-aligned
copies (and 90° steps), this handles any angle/zoom (twin-stick sprites, spinning pickups, HUD dials). fw/fh
of 0 mean the whole image. Colour-keyed, with blend = MOTE_BLEND_*
(NONE / ALPHA / ADD). Immediate-mode — call from overlay() with
0..128, or a render/background callback with its band.
Engine API — Physics v3
Impulse-based 3D rigid bodies (spheres, boxes, planes, capsules, convex hulls, static triangle meshes) under gravity, with friction, restitution, rotational inertia, sleeping, and a uniform-grid broad phase. The game owns the body array; the engine runs the solver on it. SI units (metres, kg, seconds). See Physics bodies.
phys_world_defaults v3
void (*phys_world_defaults)(MoteWorld *w);
Fill a MoteWorld with sensible defaults (earth gravity, ~unit walled box, lively bounce).
Then override fields (gravity, walls, bmin/bmax, substep…) before stepping.
phys_step v3
uint32_t (*phys_step)(MoteWorld *w, MoteBody *bodies, int n, float dt);
Advance the simulation by dt seconds over the game's bodies[0..n). Uses a fixed-substep accumulator,
so it is frame-rate independent. Returns an event bitmask — MOTE_PHYS_HIT when an impact occurred this step (for
sound/feedback).
phys_raycast v7
int (*phys_raycast)(const MoteWorld *w, const MoteBody *bodies, int n, Vec3 origin, Vec3 dir, float max_dist, int skip, MoteRayHit *hit);
Cast a ray (unit dir) up to max_dist against every body; returns 1 and fills the nearest
MoteRayHit, or 0. Pass skip = a body index to ignore (e.g. the shooter),
or <0 to test all. For aiming, ground checks, picking, AI.
phys_overlap v7
int (*phys_overlap)(const MoteWorld *w, const MoteBody *bodies, int n, Vec3 center, float radius, int *out, int max);
Fill out[] with up to max indices of bodies overlapping the test sphere; returns the count.
MoteBody/MoteWorld
fields — set them directly. The mesh-collider broad-phase grid is built once with the header-only
mote_phys_mesh_build_grid().phys2d_step v42
uint32_t (*phys2d_step)(MoteWorld2D *w, MoteBody2D *b, int n, float dt);
Advance the 2D (top-down) rigid-body world by dt — a planar sibling of phys_step
for driving games, air hockey, top-down brawlers. Circles + oriented boxes, impulse collisions with restitution and
Coulomb friction, and a single rotational DOF so bodies pick up spin from off-centre impacts. Sequential-impulse
solver, AABB broad phase, sleeping, collision-group masks, MOTE_B2D_SENSOR overlap-only bodies, and per-body
lat_damp — anisotropic lateral friction that behaves like tyre grip (slides are removed at a fixed rate, so fast
sideways motion drifts). The game owns the body array (MoteBody2D /
MoteWorld2D; header-only constructors mote_body2d_circle() /
mote_body2d_box()); the solver runs fixed internal substeps and returns the contacts resolved on the last one.
Pools come from MoteConfig.max_bodies / max_contacts, same as 3D. Map a top-down world's (x,z) to body (x,y).
Engine API — Gaussian splats v8
A 3D Gaussian-splat renderer (a fourth render path beside raster / raycast / 2D). Each MoteSplat
is an anisotropic 3D Gaussian; rendering projects, depth-sorts, and alpha-blends soft ellipses into the framebuffer.
scene_set_splats v8
void (*scene_set_splats)(const MoteSplat *splats, int n, int *order, const Mat3 *cam_basis, Vec3 cam_pos, float fov_deg, const uint16_t *depth);
Register a splat cloud to render this frame as a measured, dual-core banded pass after the 3D scene
(composites with depth; its cost shows in the perf graph). Call from update(). order is your scratch of
≥ n ints; depth is depth_buffer() (so rastered geometry occludes
splats behind it) or NULL. Preferred over splat_render for anything heavy.
splat_render v8
int (*splat_render)(uint16_t *fb, const MoteSplat *splats, int n, const Mat3 *cam_basis, Vec3 cam_pos, float fov_deg, int *order, const uint16_t *depth);
Immediate splat render from overlay() (blends over the rastered scene). Returns the number drawn.
depth_buffer v8
const uint16_t *(*depth_buffer)(void);
The scene depth buffer (K/z, larger = nearer). Pass to the splat calls for correct occlusion.
Engine API — Audio v10
A small polyphonic synth mixed to 22050 Hz (SDL on host, 12-bit PWM on device). Master volume follows the engine menu.
audio_note / audio_off v10
void (*audio_note)(float freq, float amp); void (*audio_off)(void);
Strike a one-shot note with a piano-ish decay (freq Hz, amp 0..1) — fire one per key press; voices are
stolen when all 8 are busy. audio_off() silences everything.
audio_play v12
void (*audio_play)(const MoteSound *snd, float gain);
Fire a one-shot mono PCM MoteSound (a baked WAV, or a recipe baked with
mote_sfx_bake). v40 The clip plays at its stored
quality — bits 8/16, rate up to 22050 Hz (0 = 22050) — resampled + expanded by the
mixer, so effects can ship at ~1/4 the flash. gain 0..1+. Up to 4 samples mix at once (oldest stolen); they
sum on top of the synth.
audio_render_sfx v19
int (*audio_render_sfx)(const MoteSfx *recipe, int16_t *out, int max);
Synthesise a MoteSfx recipe to 22050 Hz mono PCM. Returns the sample count; writes into
out only when out != NULL and bounded by max. Call twice (measure, then render into an arena
buffer) — exactly what mote_sfx_bake() does — so a game ships tiny recipes instead of WAVs.
audio_set_master / audio_get_master v29
void (*audio_set_master)(float v); float (*audio_get_master)(void);
The engine-owned master volume (0..1), applied to all audio (synth + samples) in the mixer — the same knob the engine menu's VOLUME drives. Route a game's own volume option here instead of scaling each sound, so the in-game setting and the system menu stay in sync. Persists across the session.
audio_set_stream v36
void (*audio_set_stream)(int (*fill)(int16_t *out, int n));
Register a PCM stream source the mixer pulls each audio block. fill(out, n) writes up to n
mono 16-bit samples at 22050 Hz and returns the count; the engine mixes them on top of its synth voices (master-scaled,
clamped). Pass NULL to stop. For games that run their own software synth (full music + SFX) rather than
the note/sample/sfx API — e.g. the ThumbyCraft port. The callback runs on the audio path, so keep it non-blocking and
read-only w.r.t. heavy game state. The engine clears it automatically when the game exits to the launcher.
audio_play_sfx v37
void (*audio_play_sfx)(const MoteSfx *recipe, float gain);
Play a SFX recipe by streaming it — the mixer synthesises the recipe on the fly (a dedicated pool of up to
8 voices), so a sound costs only its ~88-byte MoteSfx in flash and almost no RAM, for any length.
This is the recommended way to ship a whole game's hand-tuned sound effects (a weapon rack, UI blips): far smaller in
flash than baking each to a MoteSound clip. The oldest voice is stolen when all
8 are busy. gain 0..1. Author recipes in the Studio Audio tab (Save → <name>.sfx.h). Reach
for audio_play (baked clip) instead only when very heavy simultaneous polyphony
makes the synthesis cost matter.
Engine API — Text & overlay v5
text / text_2x v5
int (*text)(uint16_t *fb, const char *s, int x, int y, uint16_t color); int (*text_2x)(uint16_t *fb, const char *s, int x, int y, uint16_t color);
Draw bitmap text into the framebuffer (1× and 2× scale); returns the advanced x. Draw from overlay(). For
printf-style formatting use the helper mote_textf. This is the tiny built-in
font for fixed labels; for designed text use text_font.
text_font — anti-aliased proportional font v39
int (*text_font)(uint16_t *fb, const MoteFont *f, const char *s, int x, int y, uint16_t color);
Draws an anti-aliased, proportional font — each glyph keeps its own width and bearing, so text reads like real
type at any size (\n starts a new line; returns the advanced x). A MoteFont comes from the
asset pipeline: bake a TrueType .ttf, or hand-draw/edit a glyph sheet in the Studio's Font
tab — either way you get a static const MoteFont <name> in src/<name>.font.h.
Glyph coverage is packed at 1/2/4 bits per pixel (the baker picks the smallest lossless depth), so a font is a few KB.
A glyph sheet stores a pen origin + per-glyph advances, so hand-drawn fonts match baked TrueType fonts exactly —
even cursive scripts keep their letter joins.
#include "bigfont.font.h" // baked from assets/bigfont.ttf mote->text_font(fb, &bigfont, "Hello, Mote!", 4, 4, MOTE_RGB565(245,245,255));
ui_font — the engine's built-in UI fonts v47
const MoteFont *(*ui_font)(int size);
Returns the engine's own baked Audiowide UI font — the same face the launcher, gallery and engine
menu use — so a game gets crisp, consistent text with no font of its own (the glyph tables live in
firmware flash; zero per-game flash). Draw the returned MoteFont with text_font.
size picks one of three tiers: MOTE_FONT_MED (~11px, 1.5× — labels & standard text),
MOTE_FONT_READ (~13px, 1.66× — headers, banners, short briefings), MOTE_FONT_LARGE (~15px, 2× —
impact words). Never NULL on v47+; guard on abi_version ≥ 47 and fall back to
text (or your own baked font) on older firmware.
const MoteFont *body = (mote->abi_version>=47 && mote->ui_font) ? mote->ui_font(MOTE_FONT_MED) : 0; if (body) mote->text_font(fb, body, "$1250", 4, 2, MOTE_RGB565(120,230,120)); // SDK helpers in mote_build.h: mote_ftext / mote_ftextc / mote_ftextfc (centred, printf) + mote_dim_box (translucent panel)
draw_pixel / draw_line / draw_rect / draw_circle v30 / v31
void (*draw_pixel)(uint16_t *fb, int x, int y, uint16_t color); void (*draw_line)(uint16_t *fb, int x0, int y0, int x1, int y1, uint16_t color, int yc0, int yc1); void (*draw_rect)(uint16_t *fb, int x, int y, int w, int h, uint16_t color, int fill, int yc0, int yc1); void (*draw_circle)(uint16_t *fb, int cx, int cy, int r, uint16_t color, int fill, int yc0, int yc1);
Immediate-mode 2D framebuffer primitives (screen space, RGB565, no depth) for HUDs, overlays and custom
backgrounds — companions to text/blit. yc0/yc1 bound the rows written:
pass 0,128 from overlay(), or the band [y0,y1) from a set_background_cb.
draw_rect/draw_circle take a fill flag (non-zero = solid, else outline).
(In the 3D scene, scene_add_ring is the depth-tested camera-facing circle.)
Engine API — Control, arena & telemetry
micros v1
uint64_t (*micros)(void);
Monotonic microsecond clock (good RNG seed; manual timing).
exit_to_launcher v1
void (*exit_to_launcher)(void);
Return to the OS launcher (ends the game's frame loop).
set_fps_limit v21
void (*set_fps_limit)(int fps);
Cap the frame rate. 0 (default) runs uncapped — the device free-runs, the host emulator runs as fast as the
machine allows. A positive value paces the main loop to that many fps on both device and host, so a game can
lock 30/60 fps for steady timing. Call once from update() on the first frame (or change it any time); the async LCD
flush overlaps the wait, so capping adds no latency.
menu v11
int (*menu)(const char *title, const char *const *items, int n);
A styled modal menu in the system look. Lists n labels, UP/DOWN to move; returns the chosen index (A) or
-1 (B / quit). Blocking — call from update() for pause / game-over / level menus.
alloc / arena_free v9
void *(*alloc)(uint32_t bytes); uint32_t (*arena_free)(void);
Carve the game's own buffers out of the same arena the engine pools came from. 8-byte aligned and zeroed; returns
NULL if exhausted. Valid from init() onward; freed wholesale on exit. arena_free() reports
the bytes left for the game. There is no per-allocation free — regenerate game-owned buffers in place rather than re-allocating.
log / perf v6
void (*log)(const char *s); void (*perf)(uint32_t out[6]);
log() streams a line to the host (mote logs). perf() fills
[fps, update_us, raster_us, flush_us, core0_pct, core1_pct] for the latest frame.
rumble v23
void (*rumble)(float intensity, int ms);
Buzz the motor at intensity 0..1 for ms milliseconds (it eases out, then stops itself).
intensity<=0 or ms<=0 stops immediately. No-op on the host emulator and Studio.
save / load / save_slots v23
int (*save)(int slot, const void *data, int len); int (*load)(int slot, void *data, int max_len); int (*save_slots)(void);
Persistent fixed-size save slots. On device each slot is a flash sector that survives power-off and an OS
reflash; on host/Studio it's a file. save() writes len bytes (len==0 clears the slot),
returns len on success. load() copies up to max_len bytes and returns the saved length
(0 if empty — so load(slot, NULL, 0) tests for a save). save_slots() = the number of
slots. Keep your own magic/version inside the blob.
kv_save / kv_load / kv_list v38
int (*kv_save)(const char *key, const void *data, int len); int (*kv_load)(const char *key, void *data, int max); void (*kv_list)(const char *prefix, void (*cb)(const char *key, void *arg), void *arg);
Key-value blob storage — unlike the fixed numbered save slots, this is
arbitrary named blobs, for games that persist many pieces (e.g. a voxel world's per-chunk edits — it's how
the ThumbyCraft port saves). Each key is backed by a real file under the game's save folder, so blobs are any size and
survive reboots. kv_save writes/overwrites a blob (len==0 deletes); kv_load reads up
to max bytes and returns the blob's full size (0 if absent — pass data=NULL to
query size); kv_list calls cb(key, arg) for every key starting with prefix (the
callback must not call kv_* re-entrantly on a list it's iterating). Keys are short ASCII, no /,
case-insensitive on FAT.
Engine API — Multiplayer v43–v45
Two-player play over one raw byte pipe, transport-transparent: a USB-C cable between two units, the Studio's local preview↔device pipe, two Studios on a LAN, or an internet relay — a game written against these calls gets every transport for free. The engine owns the whole player experience (transport pick, rooms, pairing, handshake, link health); a game asks for an opponent with one call and then exchanges bytes.
net_lobby v44
int (*net_lobby)(const MoteNetCfg *cfg, int *out_is_host);
The standard multiplayer lobby. Blocks (like the engine pause menu) while the engine draws
transport selection (USB Cable / LAN / Internet per cfg->transports), room screens (quick match,
host with a 4-letter code, join by code, browse), connects, and runs its handshake. Returns
MOTE_NET_CONNECTED with a clean pipe — no handshake bytes left in the stream — or
MOTE_NET_CANCELLED if the player backed out. *out_is_host is 1 on exactly one side:
use it for every symmetry-breaking decision (colour, map seed, authority). Pairing is gated by
game_name + proto_version — two different games (or protocol versions) can never pair, even over a
cable. Bump proto_version whenever you change your wire format.
int host = 0; MoteNetCfg cfg = { "MyGame", 1, MOTE_NET_ALL }; if (mote->net_lobby(&cfg, &host) == MOTE_NET_CONNECTED) start_match(host); /* then link_send/recv as normal */
net_health v45
int (*net_health)(void);
Engine-measured transport truth for the current lobby session. The engine exchanges its own keepalives
whenever the game goes quiet (>500 ms) and strips inbound ones before link_recv, so silence
always means a real stall — turn-based games included. Returns MOTE_NET_OK,
MOTE_NET_STALLED (nothing received for >2.5 s — the engine is already drawing a
LINK STALLED banner over your game; keep simulating, blips recover), or MOTE_NET_LOST
(>20 s — end the match). Use this instead of your own silence timer: a 3-second timeout
turns every internet hiccup into a dead match.
link_start / link_stop / link_status / link_is_host v43
void (*link_start)(void); void (*link_stop)(void); int (*link_status)(void); int (*link_is_host)(void);
The raw layer beneath the lobby. link_status() returns MOTE_LINK_OFF/SEARCHING/CONNECTED.
Games normally never call link_start directly any more — net_lobby does — but
link_stop() ends a session and link_status() still detects a hard transport drop.
Do not use link_is_host() for authority: over relayed transports both ends can be
USB device-role and it reads 0 on both — that's what net_lobby's out_is_host is for.
On device the transport is USB-CDC dual-role (both units flip roles until one enumerates the other);
on the host emulator two instances pair through a local socket (MOTE_LINK_SOCK=/tmp/lk.sock).
link_send / link_recv v43
int (*link_send)(const void *data, int len); int (*link_recv)(void *buf, int max);
Non-blocking byte pipe; both return the byte count actually moved. It's a stream, not packets —
frame your messages (the shipped games use a magic byte + type + fixed payload, e.g. 0xA5 't' ..., and
resync on the magic). Proven conventions from the seven shipped 2P games: the out_is_host winner makes
every shared decision once and transmits it (never regenerate shared content from float maths on both sides — x86
previews and the ARM device disagree in the last bit of sinf); each unit simulates only its own player
and streams a small state packet ~15 Hz; damage is victim-authoritative (the shooter reports,
the victim applies to itself).
MoteNetCfg mote_api.h v44
typedef struct { const char *game_name; /* pairs only with the same name... */ uint16_t proto_version; /* ...and the same protocol version */ uint8_t transports; /* MOTE_NET_USB|LAN|INTERNET, or _ALL */ } MoteNetCfg;
Data types — Rendering
MoteObject mote_object.h
A mesh placed in the world with an orientation basis.
| Field | Type | Description |
|---|---|---|
| pos | Vec3 | World position (camera-relative with scene_begin; absolute with scene_camera). |
| basis | Mat3 | Orientation (rows: right / up / forward). |
| mesh | const Mesh* | The mesh to draw. |
| color | uint16_t | Optional per-draw RGB565 colour override; 0 = use the mesh's own colour(s). |
Data types — Meshes mote_mesh.h
Meshes live in flash as quantised int8 vertices + 6-byte faces. Winding: counter-clockwise viewed
from outside. world_pos = vert × (scale / 127), so an int8 of ±127 spans ±scale.
MeshVert
typedef struct { int8_t x, y, z; } MeshVert;
MeshFace
typedef struct { uint8_t a, b, c; int8_t nx, ny, nz; } MeshFace; /* 6 bytes */
a,b,c are vertex indices (uint8 ⇒ a single mesh caps at 256 vertices); nx,ny,nz
are the quantised model-space face normal (unit×127). Colour is not per-face — it lives on the Mesh.
Mesh
| Field | Type | Description |
|---|---|---|
| verts | const MeshVert* | Vertex array. |
| faces | const MeshFace* | Face array. |
| face_colors | const uint16_t* | Optional nfaces-long RGB565 array for multi-material / tinted meshes; NULL ⇒ use color. |
| nverts | uint16_t | Vertex count (≤ 256). |
| nfaces | uint16_t | Face count. |
| color | uint16_t | Base albedo (RGB565) when face_colors == NULL. |
| scale | float | Model half-extent in metres. |
| bound_r | float | Bounding-sphere radius (metres) for culling. |
| lod_lo | const Mesh* | Optional lower-detail swap, or NULL. |
MoteModel
A bakeable model is its chunk list — an STL is split into ≤255-vertex Mesh chunks (the uint8 index cap) and grouped
so a game draws the whole thing in one call (mote_model_draw).
| Field | Type | Description |
|---|---|---|
| chunks | const Mesh* | Chunk array. |
| count | uint16_t | Number of chunks. |
| tris | uint16_t | Total faces across all chunks (the baker emits this as <name>_TRIS — use it for max_tris). |
Data types — Images, sprites, tiles mote_2d.h
Images are RGB565 in flash. Transparency is a colour key (default magenta 0xF81F, MOTE_KEY_MAGENTA).
MoteImage
| Field | Type | Description |
|---|---|---|
| pixels | const uint16_t* | w*h RGB565, row-major (format 0; NULL when indexed). |
| w, h | uint16_t | Dimensions. |
| key | uint16_t | Transparent colour key (ignored when opaque). |
| opaque | uint8_t | 1 = draw every pixel, ignore key. |
| format | uint8_t | v41 0 = RGB565 (pixels) · 1 = 4-bit indexed · 2 = 8-bit indexed. Indexed art costs 1/4 (≤16 colours) or 1/2 (≤256) the flash, decoded by a palette lookup at sample time — works on every texture path (meshes, sprites, blits, billboards). The bakers emit it automatically and losslessly when an image has ≤256 colours. |
| indices | const uint8_t* | Packed palette indices — format 1: two per byte, high nibble first; format 2: one per byte. |
| palette | const uint16_t* | RGB565 palette — 16 entries (format 1) or up to 256 (format 2). |
Old {pixels, w, h, key[, opaque]} initialisers zero the new fields = format 0, unchanged.
MoteTileset
typedef struct { const MoteImage *sheet; uint16_t tile_w, tile_h; } MoteTileset;
An atlas divided into tile_w × tile_h cells, indexed left-to-right, top-to-bottom.
MoteTilemap
typedef struct { const uint8_t *cells; uint16_t cols, rows; } MoteTilemap;
cols*rows byte indices into a tileset (0xFF = empty).
MoteSprite
| Field | Type | Description |
|---|---|---|
| img | const MoteImage* | Source image / sheet. |
| x, y | int16_t | World position (camera offset applied at raster). |
| fx, fy, fw, fh | uint16_t | Source frame rect (a cell of the sheet). For a whole image: fx=fy=0, fw=img->w, fh=img->h. |
| layer | uint8_t | Draw order (lower first). |
| flags | uint8_t | MOTE_SPR_* (flip / rotate). |
MoteAutotile mote_tile.h
One terrain's rule-tile ruleset: a tileset atlas + a 256-entry LUT mapping the 8-neighbour mask to an atlas cell.
| Field | Type | Description |
|---|---|---|
| sheet | const MoteImage* | Atlas (ncell wide × nvar tall). |
| tile_w, tile_h | uint16_t | Cell size in the atlas. |
| lut[256] | uint8_t | Neighbour-mask → atlas cell index (row 0). |
| edge_is_solid | uint8_t | 1 = off-map neighbours count as the same terrain (seamless edges). |
| nvar | uint8_t | Random variant rows (atlas rows); 0/1 = none. Picked per cell from position so big areas don't repeat. |
| xform[256] | uint8_t | Per-config D4 transform on the chosen cell: bit0 HFLIP, bit1 VFLIP, bits2-3 rotation. Lets one source cell serve many configs. |
| var_weight[8] | uint8_t | Relative weight of each variant row (0 ⇒ 1). |
Data types — Physics bodies mote_phys.h
MoteBody
| Field | Type | Description |
|---|---|---|
| pos | Vec3 | Centre, world metres (plane: a point on the surface). |
| vel | Vec3 | Linear velocity (m/s). |
| w | Vec3 | Angular velocity (rad/s, world). |
| orient | Mat3 | Orientation (plane: r[1] = normal; capsule: r[1] = axis). |
| radius | float | Sphere/capsule radius; box: bounding radius (set it — forgetting disables box collisions). |
| inv_mass | float | 1/kg; 0 = immovable / static collider. |
| shape | uint32_t | One of MOTE_SHAPE_*. |
| half | Vec3 | Box half-extents; capsule: half.y = segment half-length. |
| friction | float | Per-body Coulomb coefficient; 0 ⇒ use world.friction. |
| restitution | float | Per-body bounce 0..1; 0 ⇒ use world.restitution. |
| shape_data | const void* | MOTE_SHAPE_HULL ⇒ const MoteHull*; MOTE_SHAPE_MESH ⇒ const MoteMesh*; else NULL. |
| _reserved[4] | uint32_t | Sleep state (counter + anchor). Do not repurpose if you use the solver. |
MoteWorld
| Field | Type | Description |
|---|---|---|
| gravity | Vec3 | e.g. (0,-9.8,0). |
| bmin, bmax | Vec3 | Bounding box (6 walls), used only when walls != 0. |
| walls | int | 1 = auto bounding-box walls; 0 = none (build your own from static PLANE/BOX bodies). |
| restitution | float | Default bounce 0..1. |
| friction | float | Coulomb coefficient. |
| linear_damp / angular_damp | float | Per-second velocity damping (drag). |
| substep | float | Fixed substep seconds (0 ⇒ ~1/240). Raise the rate for few/fast bodies, lower for many slow ones. |
| max_substeps | int | Cap substeps/frame (0 ⇒ default). Raise for high-rate games (e.g. pool). |
MoteHull
A convex hull in local space for MOTE_SHAPE_HULL: verts drive the support function; fnorm +
faceoff/facev + edges give SAT axes and face clipping for stable multi-point manifolds. Build with
the obj2hull baker or by hand.
MoteMesh
A static concave triangle-mesh collider for MOTE_SHAPE_MESH (verts + uint16 tris,
no vertex cap). Add an optional XZ broad-phase grid with the header-only builder so a body tests only nearby triangles.
mote_phys_mesh_build_grid header-only
int mote_phys_mesh_build_grid(MoteMesh *m, int grid_n, uint16_t *start, uint16_t *tri, int tri_cap);
Call once after filling verts/tris. Caller owns the scratch (start = grid_n*grid_n+1
uint16, tri ≥ ntris uint16). Returns 0, or −1 on bad args. Pick grid_n so a cell is ~one triangle wide.
MoteRayHit
| Field | Type | Description |
|---|---|---|
| body | int | Index of the hit body, -1 if none. |
| t | float | Distance along the ray. |
| point | Vec3 | World-space hit point. |
| normal | Vec3 | Surface normal (points back toward the ray origin). |
MoteBody2D mote_phys2d.h v42
A planar rigid body for phys2d_step — top-down games map world (x,z) to body (x,y).
| Field | Type | Description |
|---|---|---|
| x, y | float | Position on the plane (metres). |
| vx, vy | float | Linear velocity (m/s). |
| angle | float | Orientation, radians. |
| avel | float | Angular velocity, rad/s. |
| inv_mass | float | 0 = static/immovable (walls, kerbs). |
| inv_inertia | float | 0 = rotation locked (bollards, pedestrians). |
| restitution | float | Bounciness 0..1. |
| friction | float | Coulomb coefficient (pair-combined as sqrt). |
| lin_damp, ang_damp | float | Exponential damping per second. |
| shape | uint8_t | MOTE_C2D_CIRCLE or MOTE_C2D_BOX (oriented). |
| flags | uint8_t | MOTE_B2D_SENSOR (overlap only, no impulse); MOTE_B2D_ASLEEP is engine-managed. |
| mask | uint16_t | Collision groups: a pair collides if mask_a & mask_b ≠ 0 (0 = collide with everything). |
| radius | float | Circle radius. |
| hx, hy | float | Box half-extents (local +x / +y). |
| user | void* | Game back-reference (entity pointer). |
| lat_damp | float | Anisotropic lateral friction, m/s² (0 = off): each substep removes up to lat_damp·dt of the velocity perpendicular to angle — tyre grip that lets fast slides drift. Air hockey / curling leave it 0. |
Header-only constructors fill sane mass + inertia: mote_body2d_circle(x, y, r, mass),
mote_body2d_box(x, y, hx, hy, angle, mass) (mass 0 = static).
MoteWorld2D mote_phys2d.h v42
| Field | Type | Description |
|---|---|---|
| gx, gy | float | Gravity — (0,0) for top-down. |
| iterations | int | Velocity-solver iterations (0 = 8). |
| min_x, min_y, max_x, max_y | float | Optional static bounds; all 0 = none. |
Data types — Splats mote_splat.h
MoteSplat
| Field | Type | Description |
|---|---|---|
| pos | Vec3 | World-space centre. |
| cov[6] | float | Symmetric 3D covariance: xx,xy,xz,yy,yz,zz. |
| color | uint16_t | RGB565. |
| opacity | float | 0..1 peak alpha. |
mote_splat_make header-only
MoteSplat mote_splat_make(Vec3 pos, Vec3 scale, Mat3 rot, uint16_t color, float opacity);
Build a splat from anisotropic scale (3 std-devs) + an orthonormal basis rot. Flatten one axis for disc-like surface splats.
Data types — Input state mote_input.h
MoteInput
Derived per-frame button state, returned by input(). Read with the helpers below.
| Field | Type | Description |
|---|---|---|
| held[9] | bool | Currently down. |
| just_pressed[9] | bool | Went down this frame. |
| just_released[9] | bool | Went up this frame. |
| hold_ms[9] | uint32_t | Milliseconds held (0 if up). |
| ignore[9] | bool | Suppress-until-release mask. |
Indexed by MoteBtnId. MoteButtons is the raw 9-boolean poll the platform fills.
mote_pressed / mote_just_pressed / mote_just_released header-only
bool mote_pressed(const MoteInput *in, MoteBtnId b); bool mote_just_pressed(const MoteInput *in, MoteBtnId b); bool mote_just_released(const MoteInput *in, MoteBtnId b);
Data types — Audio
MoteSound mote_api.h
typedef struct { const void *pcm; int count; uint16_t rate; uint8_t bits; } MoteSound;
A one-shot mono PCM sample, played with audio_play. v40
The clip is stored at its native quality — bits 8 or 16, rate the source Hz (0 = 22050) —
and the mixer resamples + expands on playback, so an 8-bit/11025 Hz effect costs ~1/4 the flash of 16-bit/22050.
The bakers (Studio Audio tab, mote bake) keep the WAV's own quality; force a target with
MOTE_SFX_RATE / MOTE_SFX_BITS to shrink a whole game without editing its WAVs. Legacy
{pcm, count} initialisers read as 16-bit/22050, unchanged.
MoteSfx mote_api.h
An SFXR-style sound recipe (~88 bytes) — the editable source behind a SFX, authored in the Studio Audio tab. Baked to a
static const MoteSfx it is ~1000× smaller than the equivalent PCM, and the engine synthesises it on load.
| Field | Description |
|---|---|
| wave | 0 square · 1 saw · 2 sine · 3 noise. |
| base_freq … arp_mod | Frequency (base/limit/ramp/dramp), duty + ramp, vibrato (strength/speed), envelope (attack/sustain/punch/decay), low/high-pass filters (freq/ramp/resonance), phaser (offset/ramp), arpeggio (speed/mod) — units match the Studio synth exactly. |
Data types — Config & vtable mote_api.h
MoteConfig is documented under the memory model. MoteGameVtbl is the callback set the
game returns from mote_game_register — see Lifecycle.
Math — Vec3 & Mat3 mote_vec.h
Header-only float math. World is right-handed; Mat3 rows are basis axes: r[0] right, r[1]
up, r[2] forward.
typedef struct { float x, y, z; } Vec3; typedef struct { Vec3 r[3]; } Mat3; /* r[0]=right, r[1]=up, r[2]=forward */
Math — functions
| Function | Description |
|---|---|
| v3(x,y,z) | Construct a Vec3. |
| v3_add / v3_sub | Component add / subtract. |
| v3_scale(a,s) | Scalar multiply. |
| v3_dot / v3_cross | Dot / cross product. |
| v3_len / v3_len2 | Length / squared length. |
| v3_norm(a) | Normalise (returns (0,0,1) for a zero vector). |
| v3_lerp(a,b,t) | Linear interpolate. |
| m3_identity() | Identity basis. |
| m3_mul_v3(m,v) | Transform local→world (row-vector). |
| m3_mul_v3_t(m,v) | Transpose-transform world→view (inverse for orthonormal m). |
| m3_rotate_local(m,axis,a) | Rotate the basis about one of its own axes (axis 0/1/2). |
| m3_rotate_world(m,k,a) | Rotate about an arbitrary world unit axis (Rodrigues). |
| m3_orthonormalize(m) | Re-orthonormalise (drift control after many incremental rotations). |
SDK — Mesh builders mote_build.h
Build meshes the safe way: give world-unit dimensions and a colour; these pick the quantisation scale, wind faces
CCW-from-outside, and compute normals. They allocate from the load-time arena — call in init() and keep the pointer.
| Function | Description |
|---|---|
| mote_mesh_box(api, hx,hy,hz, col) | Axis-aligned box from half-extents. |
| mote_mesh_sphere(api, r, segs, col) | UV sphere. |
| mote_mesh_cylinder(api, r, halfh, segs, col) | Cylinder. |
| mote_mesh_revolve(api, prof, n, segs, col) | Lathe a {radius,height} profile around Y (a radius < 0.03 at an end = apex). |
| mote_mesh_grid(api, nx,nz, x0,z0,x1,z1, hf, cf, user, out, max_out, ¢er) | Sample an nx×nz heightfield into one or more arena meshes (auto-chunked past 255 verts); hf gives height, cf the per-face RGB565 colour. Writes the grid centre to *center. |
SDK — Draw helpers mote_build.h
Replace the MoteObject o={…}; scene_add_object(&o) boilerplate. Use with scene_camera()
(world positions).
| Function | Description |
|---|---|
| mote_draw(m, mesh, pos) | Draw a mesh at a world position (identity orientation). |
| mote_draw_ex(m, mesh, pos, basis, scale) | …with orientation + uniform scale. |
| mote_draw_tint(m, mesh, pos, basis, scale, color) | …tinted with an RGB565 override (team colours, damage flash). |
| mote_model_draw(m, model, pos) | Draw a whole baked MoteModel (all chunks) in one call. |
| mote_model_draw_ex(m, model, pos, basis, scale) | …with orientation + scale. |
| mote_model_draw_tint(m, model, pos, basis, scale, color) | …tinted. |
SDK — Camera mote_build.h
mote_camera_look
Mat3 mote_camera_look(Vec3 eye, Vec3 target);
Build a view basis pointing from eye toward target (Y-up). Pass it and eye to scene_camera.
SDK — Audio baking mote_build.h
mote_sfx_bake
MoteSound mote_sfx_bake(const MoteApi *m, const MoteSfx *recipe);
Synthesise a recipe into a playable MoteSound at load (measures, allocates from the arena, renders). Call once in
init(), then mote->audio_play(&snd, gain). Returns {0,0} on failure.
mote_synth.h — layered tone SFX mote_synth.h
#define MOTE_SYNTH_IMPL /* once, before including */ void mote_synth_tone(const MoteTone *layers, int n);
A header-only layered synth — the light middle ground between baked PCM (large flash) and streamed
MoteSfx recipes (heavy DSP). A sound is a stack of cheap voices (MoteTone: Square / Saw / Sine /
Noise, a linear frequency sweep f0→f1, amplitude, attack, length), authored live in the Studio Audio tab's
Tone view and exported as a MoteTone[] header. Wire it once with
mote->audio_set_stream(mote_synth_render) in init(), then fire sounds with
mote_synth_tone(laser, laser_N). ~0 flash, ~0 RAM, negligible CPU — the same approach Indemnity Run's
audio is built on.
SDK — RNG, clamps, timestep mote_build.h
| Function | Description |
|---|---|
| mote_rand_seed(s) / mote_rand() | Seed / raw 32-bit xorshift. |
| mote_frand() | Float in [0,1). |
| mote_randf(lo,hi) | Float in a range. |
| mote_clampf / mote_clampi | Clamp float / int to [lo,hi]. |
| mote_itoa(n, out) | Int → string, returns length. |
| MoteFixed + mote_fixed_feed / mote_fixed_step | Fixed-timestep accumulator: run game logic at a steady rate regardless of frame dt. |
| mote_body_sphere / mote_body_box | Build a MoteBody with shape set + box bounding radius auto-computed (mass ≤ 0 ⇒ static). |
SDK — Particles mote_build.h
A reusable pool drawn as impostor spheres (world positions; pairs with scene_camera). Set .gravity /
.drag before use.
| Function | Description |
|---|---|
| mote_particles_burst(s, pos, col, count, speed, life) | Spawn a burst. |
| mote_particles_tick(s, dt) | Integrate (gravity + drag). |
| mote_particles_draw(m, s, radius) | Submit live particles as spheres (size fades with life). |
SDK — UI & text helpers mote_build.h
| Function | Description |
|---|---|
| mote_textf(m, fb, x, y, col, fmt, …) | printf-style HUD text (%d %i %u %x %c %s %f %%; %f = 2 decimals). |
| mote_ui_rect / mote_ui_panel | Filled rect / bordered panel. |
| mote_ui_bar(fb, x,y,w,h, frac, fg, bg) | Progress / health bar. |
| mote_sprite / mote_sprite_cell | Build a MoteSprite (whole image / a sheet cell) without hand-ordering fields. |
| mote_sprite_add(m, img, x, y) | Build + submit a sprite in one call. |
SDK — Autotile mote_tile.h
| Function | Description |
|---|---|
| mote_autotile_template(at, kind) | Fill at->lut from a template (MOTE_AT_*). Set sheet/tile_w/tile_h/edge_is_solid yourself. |
| mote_autotile_cell_count(kind) | Distinct cells a template's sheet needs (47/16/9/16). |
| mote_autotile_mask(...) / _mask_layer(...) | The 8-neighbour "same terrain" mask for a cell (single map / bit-packed layered map). |
| mote_autotile_resolve(at, terrain, out, cols, rows, match) | Pre-resolve a logical map into a concrete MoteTilemap cells[]. |
SDK — Animation runtime mote_anim.h
Author clips in the Studio Anim tab; it bakes MoteAnimSheet + MoteAnimClips to a header. Keep a tiny
MoteAnimPlayer per animated sprite. The atlas holds only the cells the clips reference.
| Type / function | Description |
|---|---|
| MoteAnimFrame | { cell, dur_ms, event } — a cell index, display time, and an optional event tag fired on entry. |
| MoteAnimClip | { name, frames, count, loop, pivot_x/y }. loop ∈ MOTE_ANIM_*. |
| MoteAnimSheet | { image, tile_w, tile_h } — the atlas the cells index. |
| MoteAnimPlayer | Per-sprite cursor (clip, i, dir, done, t_ms, event). |
| mote_anim_play(p, clip) | Start a clip. |
| mote_anim_tick(p, dt) | Advance by dt seconds; honours per-frame durations + loop mode; sets p->event. |
| mote_anim_fx(p, sheet) / mote_anim_fy(p, sheet) | Current frame's source rect origin — feed a MoteSprite. |
| mote_anim_cell(p) / mote_anim_done(p) | Current cell index / finished flag. |
Enums & constants
Buttons — MoteBtnId mote_input.h
MOTE_BTN_UP, _DOWN, _LEFT, _RIGHT, _A, _B, _LB, _RB, _MENU (0..8), then MOTE_BTN_COUNT.
Physics shapes mote_phys.h
| Constant | Description |
|---|---|
| MOTE_SHAPE_SPHERE | Sphere (radius). |
| MOTE_SHAPE_BOX | Oriented box (half-extents). |
| MOTE_SHAPE_PLANE | Infinite static half-space; normal = orient.r[1], solid below. Always static. |
| MOTE_SHAPE_CAPSULE | Swept sphere along local +Y of half-length half.y, radius radius. |
| MOTE_SHAPE_HULL | Arbitrary convex hull (shape_data → MoteHull). |
| MOTE_SHAPE_MESH | Static concave triangle mesh (shape_data → MoteMesh); inv_mass 0. |
Step events: MOTE_PHYS_HIT (bit 0).
Sprite flags mote_2d.h
MOTE_SPR_HFLIP (0x01), MOTE_SPR_VFLIP (0x02), MOTE_SPR_ROT90 / _ROT180 / _ROT270 (bits 2-3,
square tiles; rotation applied before flips). Transparency key MOTE_KEY_MAGENTA (0xF81F).
Animation loop modes mote_anim.h
MOTE_ANIM_ONCE (0), MOTE_ANIM_LOOP (1), MOTE_ANIM_PINGPONG (2).
Autotile templates & neighbours mote_tile.h
Templates: MOTE_AT_BLOB47 (0) — 47-tile corner-aware terrain; MOTE_AT_EDGE16 (1) — 4-cardinal blocky
(Mario pipes); MOTE_AT_NINESLICE (2) — 3×3 rectangular regions; MOTE_AT_WANG16 (3) — 2-corner Wang
(complete 4×4 corner set: straight edges, corners, isolated, saddles).
Neighbour bits (clockwise from North): MOTE_NB_N, _NE, _E, _SE, _S, _SW, _W, _NW.
Display & colour mote_config.h
| Constant | Value / meaning |
|---|---|
| MOTE_FB_W / MOTE_FB_H | 128 × 128 (logical framebuffer). |
| MOTE_RGB565(r,g,b) | Pack 8-bit RGB into RGB565 (also mote_rgb565()). |
| MOTE_AUDIO_RATE | 22050 Hz. |
| MOTE_NEAR / MOTE_DEPTH_K | Near plane (0.5 m); depth d = K/z. |
| MOTE_HOT | Place a hot function in device SRAM (no-op on host). |
Module format & ABI mote_module.h
A built device game is a flat image: a MoteModuleHeader first, then .text/.rodata, then the
.data init image. It is linked at a fixed virtual window (MOTE_MODULE_VADDR = 0x10800000); the loader points
an RP2350 ATRANS window at wherever it lives in flash, so it executes in place (XIP) with no copy and no relocation. Mutable state
lives at the reserved RAM region (MOTE_MODULE_RAM = 0x2005E800, 134 KB).
| Header field | Type | Description |
|---|---|---|
| magic | uint32_t | MOTE_MODULE_MAGIC ('MOTE'). |
| abi_version | uint32_t | MOTE_ABI_VERSION the module was built for. |
| reg | fn ptr | Entry: mote_game_register. |
| data_load / data_start / data_end / bss_start / bss_end | uint32_t | Mini-crt ranges (copy .data, zero .bss). |
| ramtext_load / ramtext_start / ramtext_end | uint32_t | v36 Range of the RAM-resident hot code (.ramtext): the loader copies it from flash to SRAM at load so it executes from RAM, not XIP. Zero-length when the game ships none; only read for modules at ABI ≥ 36. |
| icon_vaddr | uint32_t | v20 Linked address of mote_game_icon_data[], or 0 for none. The launcher reads it straight from flash without loading the game. Format follows the module's ABI: a raw 60×60 RGB565 array up to v21, a compact paletted blob (sdk/mote_icon.h) from v22. |
| version_vaddr | uint32_t | v46 Linked address of the game's version string (mote_game_version[]), or 0 for none. Read straight from flash without loading the game, so the gallery can compare the installed version against the available one. Only read for modules at ABI ≥ 46. |
A game emits it with MOTE_MODULE_HEADER() (device build only, guarded by MOTE_MODULE_BUILD). The icon is
picked up via a weak symbol that the build auto-includes — mote_build.h pulls in the baked
src/icon.h via __has_include, so a game with an icon.png carries its icon with no
#include and no boilerplate. From v22 the baked data is a paletted, adaptive-bit-depth blob (~2–4× smaller
than raw RGB565); the launcher decodes it inline when drawing the selection.
Per-game version & the gallery v46
Stamp a version into your game so the gallery can tell an update from a fresh install. One line in your game source:
MOTE_GAME_VERSION("1.2.0"); // from <mote_api.h> — writes version_vaddr in the header
The macro emits mote_game_version[] and the build points version_vaddr at it. The gallery reads that
string from flash (no load) and compares it against the version published in the manifest: a higher published version shows
Update available, an equal one shows Installed, and a game you don't have shows
New. A game with no version string reads as version 0, so anything you publish with a real
version offers to update it in place — which is exactly what a returning player sees after upgrading from a pre-v46 build.
Good practice for gallery-ready games:
- Use
major.minor.patch(e.g.1.0.0) — the compare is dotted-numeric, left to right, so1.10.0 > 1.9.0. Bump patch for fixes, minor for features, major for changes that reset saves or break compatibility. Start at1.0.0, not0.x, so it beats the unversioned0baseline. - Bump it on every published change — the gallery only offers an update when the number goes up. A rebuild with the same version won't re-download.
- Keep the
.motefilename stable across versions — the device matches an installed game to a gallery entry by filename, so renaming the file makes an update look like a brand-new game (and orphans the old one). Pick a short, lowercase, stable name (e.g.grandthumbauto.mote) and keep it. - Metadata lives in the gallery manifest, not the game — display name, author, tagline, description and
screenshots come from the curated catalog (
docs/games.json, generated bytools/gen_gallery.py); the game binary only carries the version, ABI, icon and code. (The old per-gamegame.tomlis gone.)
RAM-resident hot code — .ramtext v36
Module code normally executes in place from flash (XIP) through a small CPU cache. When a tight inner loop and a large
const data set (e.g. a texture atlas) compete for that cache, the data reads evict the code and the loop stalls
re-fetching itself — exactly what held ThumbyCraft's voxel raycaster to 8–12 fps. Placing the loop in a
.ramtext.* section runs it from SRAM instead, so it never competes with data for the cache.
- Link: the function is linked at a
GAME_RAMaddress but its bytes ship in the flash image (load address in flash, run address in RAM — the classic "ramfunc" pattern). Calls out to functions still in flash get automatic linker veneers, so cross-region calls just work. - Load: right after the
.data/.bsscrt step, the loader copies the.ramtextimage (described by the three header fields above) from flash to its RAM address. - Cost: the copied code occupies the module's 134 KB static budget — use it only for genuinely hot, cache-contended loops (per-pixel raster/raycast). For ThumbyCraft (~9 KB of raycaster) it lifted the framerate to 12–20 fps, matching the native non-Mote build.
- Compatibility: backward-compatible — the loader only copies
.ramtextfor modules built at ABI ≥ 36; older modules (and any module shipping none) are untouched.
ABI version history
Each bump appends to MoteApi/MoteGameVtbl or extends a shared data struct. A game runs on any engine
whose version is ≥ the one it was built against. Changing the engine's shared structs needs a reflashed OS firmware;
header-only/SDK changes do not.
| Version | Added |
|---|---|
| v1 | Core: 3D scene (background/sun/begin/add_object/add_object_scaled/tri_count), input, micros, exit_to_launcher. |
| v2 | 2D scene: scene2d_begin / set_tilemap / add; immediate blit. |
| v3 | Rigid-body physics: phys_world_defaults, phys_step. |
| v4 | scene_add_sphere (shaded sphere impostor). |
| v5 | text, text_2x. |
| v6 | log, perf (telemetry). |
| v7 | phys_raycast, phys_overlap. |
| v8 | Gaussian splats: splat_render, depth_buffer, scene_set_splats. |
| v9 | Load-time arena: alloc, arena_free; MoteGameVtbl.config (pools). |
| v10 | Audio synth: audio_note, audio_off. |
| v11 | menu (modal). |
| v12 | audio_play (PCM samples). |
| v13 | scene_camera (world-space camera). |
| v14 | scene2d_set_autotiles (render-time autotiling). |
| v15 | scene2d_set_autotile_layers (bit-packed layers). |
| v16–v18 | Data-format changes: per-draw MoteObject.color override; 6-byte MeshFace + optional Mesh.face_colors (colour moved off every face); the MoteSfx recipe. |
| v19 | audio_render_sfx (synthesise recipes; mote_sfx_bake). |
| v20 | Per-game icons in the module header (icon_vaddr); no more firmware icon table. |
| v21 | set_fps_limit (optional frame-rate cap, honoured by device + host). |
| v22 | Compact paletted launcher icons (sdk/mote_icon.h); the launcher decodes the blob inline. ~2–4× smaller in flash. |
| v23 | rumble(intensity, ms) (motor pulse) + persistent per-slot save (save/load/save_slots; device = flash sectors, host = files). The load arena trims 280→276 KB to make OS room. |
| v24 | Depth-tested 3D FX: scene_add_point / scene_add_line / scene_add_disc; scene_add_object_ex + MOTE_DRAW_NO_DEPTH_WRITE. |
| v25 | scene_add_sphere_tex — textured/oriented sphere impostor (MoteSphereTex, mote_sphere.h). |
| v26 | set_background_cb — per-band background (sky) pass. |
| v27 | scene_add_tri — double-sided immediate-mode world triangle. |
| v28 | scene_add_shadow (soft ground shadow) + scene_set_near (runtime near plane). |
| v29 | audio_set_master / audio_get_master — engine-owned master volume (shared with the engine menu). |
| v30 | 2D framebuffer drawing: draw_pixel / draw_line / draw_rect. |
| v31 | scene_add_ring (camera-facing circle outline) + draw_circle (2D). |
| v32 | scene_add_shadow_ex — oriented elliptical (object-shaped) ground shadow. The load arena trims 276→272 KB. |
| v33 | scene_add_billboard — camera-facing textured quad (3D sprite) + the MOTE_BLEND_NONE/ALPHA/ADD modes. |
| v34 | blit_ex — freely rotated & scaled 2D sprite blit (any angle/zoom, with blend). |
| v35 | Textured (UV-mapped) meshes: Mesh.texture + Mesh.face_uvs, drawn through scene_add_object; per-mesh translucency via MOTE_DRAW_BLEND(). |
| v36 | audio_set_stream — register a PCM source mixed on top of the synth voices (for games with their own software synth). Plus .ramtext RAM-resident hot code (loader copies it flash→SRAM at load) and the module static RAM grew 128→134 KB. Both landed for the ThumbyCraft port. |
| v37 | audio_play_sfx — stream a MoteSfx recipe (synthesised on the fly by a pool of 8 voices); the small-flash, ~0-RAM way to ship a whole game's SFX, vs baking each to a clip. |
| v38 | kv_save/kv_load/kv_list — named-blob storage (arbitrary keys, any size, FatFs-backed) for games that persist many pieces, e.g. ThumbyCraft's per-chunk voxel edits. |
| v39 | text_font — anti-aliased proportional fonts (TTF-baked or hand-drawn): each glyph keeps its own width/bearing, packed at the smallest lossless depth (1/2/4-bit). Authored in the Studio Font tab. |
| v40 | MoteSound carries a sample rate + bit depth — clips stored at native (lower) quality, resampled at playback; an 8-bit/11025 Hz SFX costs ~1/4 the flash. Struct-extend only; legacy {pcm,count} headers unchanged. |
| v41 | MoteImage gains 4-/8-bit palette-indexed formats — low-colour textures at 1/4 or 1/2 the flash on every texture path; bakers emit indexed automatically + losslessly (≤256 colours). Struct-extend only. |
| v42 | phys2d_step — 2D top-down rigid-body solver (MoteBody2D/MoteWorld2D): circles + oriented boxes, spin from off-centre impacts, collision masks, sensors, tyre-grip lateral friction. |
| v43 | 2-player link: link_start/stop/status/is_host/send/recv — a raw byte pipe to a second unit. Device: USB CDC dual-role over one USB-C cable (both units flip device/host roles until one enumerates the other); host emulator: a local socket (MOTE_LINK_SOCK) between two instances. First game: DeepThumb’s 2P LINK mode. |
| v44 | net_lobby — the standard multiplayer lobby: the engine draws transport pick (USB / LAN / Internet), rooms (quick match / host with a 4-letter code / join / browse), connects, and hands back a clean pipe + the authority (out_is_host). Rooms gated per game + protocol version (MoteNetCfg). Online play is device-driven: a docked Studio auto-relays to the room server with zero clicks. |
| v45 | net_health — engine-owned link health: the OS keepalives quiet sessions (and strips them), reports OK / STALLED (>2.5s, engine draws the LINK STALLED banner) / LOST (>20s), so internet blips recover instead of killing matches and turn-based games don’t look dead between moves. |
| v46 | Per-game version string in the module header (version_vaddr, set by MOTE_GAME_VERSION()) — the gallery reads it from flash to tell New / Installed / Update-available. Backward-compatible: v46 firmware runs v45 games, and the version is simply absent (reads as 0) for games built before it. |
| v47 | ui_font(size) — the engine's baked Audiowide UI fonts (the ones the launcher/gallery/menu use) exposed to games. MOTE_FONT_MED (11px, 1.5×), MOTE_FONT_READ (13px, 1.66×), MOTE_FONT_LARGE (15px, 2×) — draw via text_font() for crisp, consistent text with no per-game font baked in and zero extra flash. Jump-table append only; guard on abi>=47 and fall back to text() on older firmware. |
Beyond the ABI, 0.3-alpha adds header-only 3D rigid-part animation
(sdk/mote_anim3d.h) — rigs + keyframe clips that compose through the normal draw path, so they need no engine
change. See the animation guide.