omibit — API Reference

O8 API — pre-release 1.0 candidate. The regression corpus covers representative behavior; the final compatibility declaration is still pending. The O16 sibling machine (same repo, tier byte 0x10) is specified in The O16 machine and the O32 sibling (tier byte 0x20) in The O32 machine, both at the bottom of this file. Extended creator contracts add versioned player/pointer input, priority audio, PCM banks and O32 depth rendering.

Canonical reference for the O8 machine. This document doubles as agent context: the quick-reference table below covers the legacy O8 surface; the sibling and extended sections specify their additions.

Machine model

  • Screen: 128x128, one palette index per pixel. 60 logical frames per second.
  • Cart language: Lua 5.4 via a frozen interpreter. The math table does not

exist — all numeric functions are console globals with fixed implementations (see Math). Base, string, table, and coroutine libraries are loaded. No os, io, or debug. load and string.dump/load round-trips are part of the frozen surface (bytes-to-behavior is the contract; binary chunks load deterministically under the same interpreter artifact, pinned by the lua-load-dump corpus entry). No require, no JS bridge, no proxy objects cross the sandbox boundary.

  • Frame loop: top-level cart code runs once, then _init() if defined, then

per frame _update() followed by _draw(). All of this happens inside frame 0 for the boot code.

  • Instruction budget: 4,000,000 Lua instructions per frame. Exceeding it is a

deterministic runtime error for that frame (instruction budget exceeded). print draws on the same budget: it charges 16 budget instructions per character processed — inked glyphs and \n layout advances alike — so an oversized print (of any content) fails deterministically mid-string with the same error.

  • A runtime error stops the frame and is reported by the host; the error

state is itself hashable.

Quick reference

FunctionSignatureOne line
clscls([c])clear screen to color c, reset cursor and clip
psetpset(x, y, [c])set pixel
pgetpget(x, y)read pixel from screen
lineline(x0, y0, x1, y1, [c])draw line
rectrect(x0, y0, x1, y1, [c])rectangle outline
rectfillrectfill(x0, y0, x1, y1, [c])filled rectangle
circcirc(cx, cy, r, [c])circle outline
circfillcircfill(cx, cy, r, [c])filled circle
ovaloval(x0, y0, x1, y1, [c])ellipse outline inscribed in the box
ovalfillovalfill(x0, y0, x1, y1, [c])filled ellipse inscribed in the box
tritri(x0, y0, x1, y1, x2, y2, [c])triangle outline (filled)
trifilltrifill(x0, y0, x1, y1, x2, y2, [c])filled triangle
printprint(s, [x, y,] [c])draw text; charges the instruction budget 16 per character processed (glyph or \n); a color argument becomes the persistent draw color, returns end x
cursorcursor(x, y)set print cursor
colorcolor(c)set default draw color
fillpfillp([p])set 16-bit fill pattern for filled shapes, no arg returns it
sprspr(n, x, y, [w, h, flipx, flipy])draw sprite n, w x h cells
ssprsspr(sx, sy, sw, sh, dx, dy, [dw, dh, flipx, flipy])draw stretched sprite region
fgetfget(n, [f])sprite n flags: whole byte, or bit f as 0/1
fsetfset(n, [f,] v)set sprite n flags: whole byte, or bit f
cameracamera([x, y])offset all draw coordinates
clipclip([x, y, w, h])clip drawing region, no args resets
palpal([c0, c1, [p]])remap color c0 to c1, p 0 draw 1 screen, no args resets palettes + transparency
paltpalt([c, [t]])set sprite transparency for color c, or by 16-bit mask, no args resets
mgetmget(mx, my)read map cell, 0 outside bounds
msetmset(mx, my, v)write map cell
mapmap(mx, my, dx, dy, [w, h])draw map region to screen
maxmax(x, y)larger of x, y
minmin(x, y)smaller of x, y
midmid(x, y, z)middle value, use to clamp
flrflr(x)round down
ceilceil(x)round up
coscos(t)cosine, t in turns
sinsin(t)sine, t in turns, flipped
atan2atan2(dy, dx)angle in turns 0..1
sqrtsqrt(x)square root, x<0 gives 0
absabs(x)absolute value
sgnsgn(x)-1 if x<0 else 1
rndrnd([x])uniform 0..x exclusive, float, no arg gives 0..1
srandsrand(seed)seed the deterministic prng
subsub(s, i, [j])substring, 1-based inclusive, Lua index rules
btnbtn([i])button held, no arg gives bitmask
btnpbtnp([i])button pressed this frame, with key repeat
peekpeek(addr)read byte at addr 0x0-0xFFFF
pokepoke(addr, v)write byte at addr 0x0-0xFFFF
peek2peek2(addr)read u16 little-endian at addr
poke2poke2(addr, v)write u16 little-endian at addr
sfxsfx(n[, channel])play slot n on the first free channel, or address channel 0-3 explicitly; sfx(-1, channel) stops one effect and sfx(-1) stops all effects (music tracks keep sounding)
musicmusic([n])play music from pattern n onward, music() or music(-1) stops
timetime()frame count since load, integer
cartdatacartdata()activates the 256-byte save region, returns true
dgetdget(index)read unsigned 32-bit save slot 0–63 after cartdata()
dsetdset(index, value)write unsigned 32-bit save slot after cartdata()
_init_init()optional hook, runs once at load
_update_update()optional hook, runs each frame before draw
_draw_draw()optional hook, runs each frame after update

Palette

Index is the color. Fixed, shared by gfx, screen, sfx UI, and the site.

idxhexnameidxhexname
0#1a1c2cblack8#29366findigo
1#5d275dplum9#3b5dc9blue
2#b13e53maroon10#41a6f6sky
3#ef7d57orange11#73eff7cyan
4#ffcd75sand12#f4f4f4white
5#a7f070lime13#94b0c2silver
6#38b764green14#566c86steel
7#257179teal15#333c57slate

Memory map

64KB RAM, zero-initialized at load, then cart sections are expanded into it and the draw state is initialized (identity palettes, color 0 transparent for sprites, draw color 13, full-screen clip, fill pattern 0, prng seed 1, sfx channels idle).

rangesizeuse
0x0000-0x1FFF8KBgfx: 128 sprites, 8x8, 1 byte per pixel
0x2000-0x3FFF8KBmap: 128 x 64 cells, 1 byte per cell
0x4000-0x7FFF16KBscreen: 128x128, 1 byte per pixel
0x8000-0x8FFF4KBsfx: 64 sfx x 32 notes x 2 bytes
0x9000-0x93FF1KBdraw state (color, camera, clip, cursor, fill pattern at 0x900D, palettes, input, audio channels, music sequencer at 0x9140; music patterns at 0x9200 and sprite flags at 0x9380 homes)
0x9400-0xFFFF~27KBuser data, first 256 bytes are cartdata

Addressing formulas:

  • Sprite sheet pixel: sprite n occupies x = (n % 16) * 8, y = flr(n / 16) * 8

on a 128 x 64 sheet; byte address 0x0000 + (y * 128) + x.

  • Sprite n flags byte: 0x9380 + n (n wraps to 0-127).
  • Map cell (mx, my): 0x2000 + my * 128 + mx.
  • Screen pixel (x, y): 0x4000 + y * 128 + x.
  • Sfx note i of slot n: 0x8000 + n * 64 + i * 2. The slot speed lives

at note position 31 (0x8000 + n * 64 + 62), byte 0.

  • Draw state lives at fixed offsets in 0x9000-0x93FF; treat it as

read-mostly and drive it through color, camera, clip, cursor, pal, palt, fillp instead of poking it.

  • peek/poke wrap addresses to 0x0000-0xFFFF. peek2/poke2 read and

write a u16 little-endian and wrap each byte address the same way (the high byte of peek2(0xFFFF) is at 0x0000). No peek4/poke4 in O8.

  • Music pattern n: 0x9200 + n * 4, four bytes, one per channel (see

Audio).

RAM is not the cart format. The cart payload (32KB: code, sprite flags, music, gfx, map, sfx sections) is expanded into RAM at load. Sfx data is live-tweakable: poke note bytes then call sfx(n) to hear them. The v2 header (magic, tier, version byte, code length) sits outside the cart CRC — a bit flip there is caught by the decode rules, not the checksum (accepted; any future format version covers the header in the CRC).

The versioning invariant (for whoever performs the next format surgery). A decode path, once shipped, is never removed or reordered: new versions append to the (tier, version) dispatch and old corpus/tests must keep passing unchanged. v1 decode-forever is additionally pinned by a frozen byte fixture (packages/core/fixtures/v1-frozen.png + src/v1fixture.test.ts) — committed bytes, not synthetic construction. Recipe for an O16 v2, someday: bump the version byte, add the section table, keep the v1 dispatch untouched, pin v2 with new append-only corpus entries while the v1 fixture and every existing entry stay green.

Input

Six buttons, no more in v1.

bitbuttonbtn() index
0left0
1right1
2up2
3down3
4A4
5B5
6-7reserved, zero-
  • btn(i) is true while the button is held. btn() returns the current

frame's mask as a number.

  • btnp(i) is true only on the frame the button went down, then repeats

like a keyboard: again after 4 held frames, then every 15 frames. The hold counter saturates at 255; from there btnp repeats every frame, so long holds never go silent.

  • Input log byte (for headless runs and replays): one byte per frame, same

bit layout. Buttons held across frames keep their bits set in consecutive bytes. Bits 6-7 are masked to zero by the machine itself: an input log with them set runs bit-identically to one without (the X/Y buttons are O16-only — see the O16 section).

Memory and execution limits

Each Lua state has a fixed 64 MiB allocator ceiling, including Lua tables, strings and engine objects. A single oversized native allocation fails with not enough memory; reset creates a fresh state. This bounds Lua allocations, not the entire browser or service process RSS.

The Lua instruction budget remains deterministic. Host raster work and native library calls can take longer than their instruction count suggests. Browser players, previews and exports execute in terminable workers (2 seconds per command; 10 seconds to initialize). Publishing runs use isolated subprocesses with a hard 60-second deadline and a 128 MiB JavaScript old-space limit, in addition to the Lua ceiling. These host watchdogs report execution failure; they do not claim a deterministic partial-frame result. Local MCP calls also run in cancellable subprocesses, with a ten-minute deadline.

Determinism contract

Reproducing a run requires the same initial save bytes as well as cart, seed and input. Ranked/replay/headless runs use zero save bytes; casual players may restore local progress.

  • Supported cartridge programs use the same pinned Lua/WASM interpreter and

console operations across hosts. The golden corpus verifies concrete state, screen, audio and error behavior in Node, Chromium, Firefox and WebKit.

  • The legacy stateHash field is a RAM/frame checksum: FNV-1a-32 over all

machine RAM followed by the frame counter as u32 little-endian. Lua heap, closures and coroutine state are not serialized into it. Equal checksums do not prove equal complete execution state or identical future behavior; the checksum is not cryptographic. Replay scores are verified by re-executing the supplied cart and input, rather than trusting a submitted checksum.

  • Cart-facing trigonometry uses fixed console implementations; math is absent.
  • pairs() and next() on tables containing string keys use a stable order:

numbers ascending, strings byte-lexicographically, booleans false then true. Numeric/boolean-only tables preserve their established Lua traversal order. Reference keys (table, function, thread, userdata) are rejected by the default iterator. Custom __pairs methods remain responsible for their own order. Adding keys during iteration has no supported ordering guarantee.

  • Address-bearing reflection (for example default tostring(table) and %p)

is not a portable game-state input. Do not use addresses to drive gameplay.

  • rnd is xorshift32 over RAM, seed 1 at load. srand(0) selects seed 1.
  • Instruction-budget failures are part of machine behavior. Host termination,

process OOM and browser resource failures are execution failures outside the deterministic result contract.

Graphics

Unless noted, PICO-8 semantics. All draw calls are offset by the camera and restricted to the clip region, and take an optional final color argument that defaults to the color set by color() (which is 13, silver, at load).

  • cls(c) — clears to c (through the draw palette), resets the cursor to

0,0 and the clip to full screen. Does not reset the camera.

  • pset/pget — pget reads the screen through the camera offset, returns 0

outside the screen.

  • print(s) — prints at the cursor using the current color.

print(s, c) — prints at the cursor and sets the persistent color to c. print(s, x, y) — prints at x, y using the current color. print(s, x, y, c) — prints at x, y in color c and sets the persistent color to c (PICO-8 semantics: any color argument becomes the draw color). Returns the x position after the last glyph.

  • Font: 3x5 pixels, 4px advance, 6px line height. Printable ASCII.

Case-fold set — a deliberate pixel-font convention: C/c, J/j, N/n, O/o, S/s share glyphs (letter folds per PICO-8 precedent, whose own 3x5 font case-folds the same pairs), and (/{ share a glyph at 3x5.

  • fillp(p) — sets a 16-bit fill pattern applied to rectfill,

circfill, trifill (and tri) pixels. fillp() with no argument returns the current pattern. The pattern is a 4x4 grid, one bit per cell, read right to left, top to bottom: the cell covering screen pixel (x, y) is bit y%4 * 4 + 3 - x%4. A 1 bit draws the call's color; a 0 bit draws (c + 1) % 16, or nothing when bit 0x8000 (the transparency flag) is set. Bit 0x8000 doubles as the bottom-left cell's pattern bit, so that cell draws the primary color whenever the flag is set. Pattern 0 (the default) draws solid. The pattern is anchored to screen pixels after the camera offset and is not applied to line, pset, print, or sprite draws. Checkered example: fillp(0x33cc) gives 2x2 blocks of c and c+1; fillp(0xb3cc) gives the same checker with the off cells transparent. Secondary colors pass through the draw palette like any color.

  • spr(n, x, y, [w, h, flipx, flipy]) — draws sprite n (0-127) plus w x h

cells right and down. Respects transparency (see palt) and the draw palette. Flags (below) are for cart logic only; spr never reads them. Flip arguments follow Lua truthiness: only false and nil mean off — 0 and 1 both flip.

  • sspr — copies any gfx rectangle to any screen rectangle, stretching when

dw/dh differ from sw/sh. dw and dh default to sw and sh. Flip arguments follow the same Lua truthiness. Sheet-edge policy (divergence): spr skips source pixels outside the sheet; sspr clamps source coordinates to the sheet and edge-replicates.

  • camera(x, y) — subsequent draw coordinates are shifted by -x, -y.

camera() resets to 0,0. Values clamp to i16 (-32768..32727), they do not wrap (divergence).

  • clip(x, y, w, h) — clips drawing. clip() resets to the full screen.

Clip bytes live in draw state and are pokeable, but they can only ever restrict: pixel writes are clamped to the 128x128 screen regardless.

  • pal(c0, c1, p) — p 0 (default) remaps the draw palette: color c0 draws

as c1. p 1 remaps the screen palette: c0 is displayed as c1 in the final framebuffer. pal() resets both palettes to identity and transparency to the default (color 0 transparent, rest opaque).

  • palt(c, t) — marks color c transparent (t follows Lua truthiness: only

false and nil mean opaque) for spr, sspr, and map draws. Color 0 is transparent at load. palt() resets to that default. palt(mask) — the one-argument numeric form is a 16-bit transparency mask: bit 15 is color 0, bit 0 is color 15; a set bit marks the color transparent. Primitives are never transparent.

  • Circle rasterization (floor vs round radius handling between circ and

circfill) is defined by the console, not inherited — trust the corpus.

  • oval(x0, y0, x1, y1, [c]) / ovalfill(x0, y0, x1, y1, [c]) — the

ellipse inscribed in the inclusive pixel box, PICO-8's bounding-box signature (corners in any order; coordinates floor like rect). The rasterization is console-defined, exact integer arithmetic with zero freedom: with l..r, t..b the normalized box, w = r - l, h = b - t, a pixel of the box is filled iff X^2*(h+1)^2 + Y^2*(w+1)^2 <= (w+1)^2*(h+1)^2 where X = 2x - l - r, Y = 2y - t - b — the inscribed ellipse touching the box sides at the extreme pixels' centers. ovalfill fills that region through fillp exactly like circfill. oval draws the region's 4-connected boundary — the leftmost/rightmost filled pixel of every row plus the topmost/bottommost of every column — as primitive pixels, so fillp does not apply to the outline (same family as circ's outline). Degenerate boxes fall out of the same inequality: w = h = 0 is a single pixel, w = 0 a vertical line, h = 0 a horizontal line (the fill form routes those through fillp like any filled shape). Camera and clip apply as for every primitive.

Sprite flags

Each sprite n (0-127) has one flags byte at RAM 0x9380 + n, expanded from the cart flags section at load. Flags are cart logic only — no draw call reads them.

  • fget(n) — the whole byte.
  • fget(n, f) — bit f as 0 or 1.
  • fset(n, v) — sets the whole byte.
  • fset(n, f, v) — sets bit f. v follows Lua truthiness: only false

and nil clear the bit — fset(n, f, 0) sets it, so use fset(n, f, false) to clear.

  • n wraps to 0-127, f wraps to 0-7. Typical use: collision classes,

if fget(mget(mx, my), 0) then ... style tile tests.

Map

  • Map is 128 x 64 cells, one byte each. Cell value 0 is empty, 1-127 draw

sprite cell-value when rendered.

  • map(mx, my, dx, dy, [w, h]) — draws the w x h cell region starting at

map cell mx, my to screen pixel dx, dy. Defaults w 128, h 64. No layer argument in v1 (PICO-8 divergence).

  • mget returns 0 for out-of-bounds cells. mset ignores out-of-bounds

writes.

Math

PICO-8 semantics. Angles are in turns, not radians: a full circle is 1.

  • sin(t) — flipped like screen coordinates: sin(0) = 0, sin(0.25) = -1.

For a point moving clockwise on screen: x = cx + r*cos(t), y = cy + r*sin(t) with t increasing.

  • cos(t) = -sin(t + 0.25).
  • atan2(dy, dx) — returns turns in 0..1, deterministic fixed-point.
  • sqrt(x) — negative input returns 0.
  • sgn(x) — -1 or 1; never 0 (sgn(0) is 1).
  • mid(x, y, z) — clamp idiom: mid(lo, v, hi).
  • max/min take exactly two numbers in v1.
  • sub(s, i, [j]) — Lua string.sub semantics: 1-based inclusive, negative

indices count from the end (sub("hello", -3) is "llo"), out-of-range indices clamp, and i > j gives the empty string.

  • rnd() — uniform float in 0..1 exclusive of 1. rnd(x) — uniform float in

0..x exclusive. No table argument in v1 (PICO-8 divergence).

  • Numbers print with up to 4 decimal places, trailing zeros stripped.

Audio

  • 4 channels, 64 sfx slots. Each slot is 64 bytes at 0x8000 + n * 64:

32 note positions of 2 bytes each; positions 0-30 are playable notes, position 31 carries the slot speed.

  • Note layout (the pair at 0x8000 + n * 64 + i * 2):
bytebitsfield
00-5pitch 0-63 (33 = 440 Hz, one semitone per step)
06-7wave, low 2 bits
10wave, high bit
11-2effect: 0 none, 1 slide (2-3 reserved)
13-5volume 0-7
16-7reserved, zero
  • Wave 0 square, 1 triangle, 2 saw, 3 noise, 4 buzz (square with 25%

duty). Waves 5-7 are undefined by the format and render as buzz (the wave lookup's fallback); no build tool emits them. Volume scales amplitude: 7 is full level (the v1 loudness), 0 is silent.

  • Effect 1 (slide): a linear pitch glide in semitone space from this

note's pitch to the next note's pitch, spread across the note's whole duration. A slide whose next position is the end marker or the speed carrier does nothing.

  • End marker: a note with both bytes zero ends the slot (so pitch 0

square at vol 0 is the end marker, as in v1).

  • Speed: position 31 byte 0 holds frames per note, 1-63; 0 (a zeroed

slot) means the default 6 — exactly the v1 note length of 2205 samples. Position 31 byte 1 is reserved, zero.

  • sfx(n) plays slot n on the first free channel. sfx(n, channel)

addresses channel 0-3 explicitly, replacing a sound effect already on that channel. sfx(-1, channel) stops only that channel's effect. An explicit call cannot address a channel assigned in the active music pattern, even if that track has finished; invalid channel numbers do nothing. sfx(-1) or sfx() stops all sound effects while live music tracks keep sounding. Calls with no channel argument retain their original first-free behavior.

  • The VM renders mono 22050 Hz Int16 PCM per frame as part of the frame

output; hosts play the buffers. Audio is inside the determinism contract — synthesis is integer-exact, bit-identical on every host.

  • Carts saved in format v1 (1-byte notes wave << 6 | pitch) decode

into this representation with volume 7, effect 0, speed 6, and sound identical to how they always did. v1 slots longer than 31 notes truncate at the expansion (position 31 is the speed carrier in v2).

Music

64 patterns, four bytes each, expanded into RAM at 0x9200 + n * 4. Byte i of a pattern drives channel i:

bytebitsfield
all0-6track: 0-63 plays that sfx slot on that channel, >= 64 silent (0x7F canonical)
07loop-begin flag
17loop-end flag
2, 37reserved, zero

A zero byte addresses sfx slot 0 (slot 0 is a valid instrument); use 0x7F bytes for silent channels. A pattern is empty — skipped during playback — iff all four tracks are silent or all four bytes are zero.

  • music(n) starts at the first non-empty pattern >= n (n wraps to

0-63) and plays patterns in order, skipping empty ones. music() or music(-1) stops the sequencer; tracks already sounding finish their slots naturally.

  • Each pattern's non-silent tracks start their sfx slots on their

channels when the pattern starts, overwriting whatever is there. The pattern lasts until all its tracks' slots have finished (pattern length = longest track, PICO-8 style); the next pattern starts the sample after the last track's last sample — advance is gapless at every speed, not just frame-aligned ones. The sequencer tracks its own pattern progress: an sfx() stealing a freed music channel can neither stall nor skip pattern advance, and sfx(-1) leaves the music timeline running. Music and sfx() share the 4 channels: sfx() keeps the v1 policy of taking the first free channel, so it lands on channels not held by a music track. A cart can leave one music channel silent and address it with sfx(n, channel) when an important effect should replace a less important effect there.

  • Looping: when a pattern with the loop-begin flag starts playing, the

sequencer bookmarks it; after a pattern with the loop-end flag finishes, playback jumps to the bookmark (the pattern where the current music() started, if no begin flag was seen). Running past pattern 63 without an end flag stops the music.

  • Sequencer state lives in RAM and hashes into the state: 0x9140 on,

0x9141 pattern pointer, 0x9142 loop bookmark, 0x9143 started flag. Music data is live-tweakable: poke pattern bytes and the next pattern change hears them. 0x9141 is consumed raw: a poked pattern pointer above 63 reads pattern bytes past the music home into the flags home / user RAM — deterministic and in-bounds, but poke-at-your-own-risk; the music() API itself always wraps n to 0-63 (documented divergence).

Worked example — a two-pattern loop built entirely from code:

lua
function _init()
  -- sfx 0: two square notes, sfx 1: one triangle note
  poke(0x8000, 40) poke(0x8001, 0x38)
  poke(0x8002, 45) poke(0x8003, 0x38)
  poke(0x8040, 33 | 0x40) poke(0x8041, 0x38)
  -- pattern 0: slot 0 on ch0 + slot 1 on ch1, loop begins here
  poke(0x9200, 0x80 | 0) poke(0x9201, 1) poke(0x9202, 0x7f) poke(0x9203, 0x7f)
  -- pattern 1: slot 1 on ch0 + slot 0 on ch1, loop ends here
  poke(0x9204, 1) poke(0x9205, 0x80 | 0) poke(0x9206, 0x7f) poke(0x9207, 0x7f)
  music(0)
end

Pattern 0 sounds slot 0 and slot 1 together for their longest track, then pattern 1 swaps the channels, then the end flag jumps back to pattern 0 — forever, across frames, until music().

System

  • time() — frame count since load, an integer. Not seconds (PICO-8

divergence): divide by 60 yourself.

  • cartdata() — reserves the 256 byte region at 0x9400; returns true.

Read and write it with peek/poke or dget/dset; see the persistent-progress amendment below.

  • _init, _update, _draw — all optional. _update runs before _draw

every frame.

Host conventions: the score word and the daily seed word

Two words in user RAM (just past the 256-byte cartdata region) belong to hosts, not the machine. The machine never reads or writes them; they are zero unless a host sets them, so every corpus run and every plain cart is unaffected. Addresses are relative to the user region start (O8 0x9400, O16 0x27600, O32 0x44400):

wordaddress (O8)encodingmeaning
scoreuser + 0x100 (0x9500)u32lethe leaderboard score: the value there when the run ends
dayseeduser + 0x110 (0x9510)u16lethe daily-challenge seed, written by the host before frame 0
  • Score. A cart that wants its runs ranked on verified leaderboards

keeps its score in the score word — write the low half with poke2(0x9500, v & 0xffff) and the high half with poke2(0x9502, v >> 16) (no poke4; the shift yields an integer for scores under 2^32). The publish service reads it as u32le after verifying a run and sorts leaderboards by it, descending. Carts that never touch it read as score 0.

  • Daily seed. A host running a daily challenge writes the day's seed

at the seed word before frame 0 (the publish service derives it from the date; default 0 everywhere else). A seeded cart reads it with peek2(0x9510) — the daily idiom is srand(peek2(0x9510)) at boot, so everyone playing the same cart on the same day rolls identical dice. The seed is part of the replay receipt: a seeded run's state hash covers the seeded RAM.

PICO-8 divergences

Deliberate subset plus fixes. Everything not listed follows documented PICO-8 behavior.

  • No math table, no peek4/poke4, no stat, no second gfx bank.
  • time() returns frames, not seconds.
  • Default draw color is 13, not 6.
  • print follows PICO-8 color semantics: any color argument (2-arg or

4-arg form) becomes the persistent draw color.

  • print with embedded \n advances one line but parks the cursor one

line below the last output line (print("a\nb") then print("c") overprints), and cursor-position print never scrolls — lines past y=122 are clipped, not scrolled up.

  • spr with w or h below 1 still draws a 1x1-cell sprite; circ and

circfill with a negative radius plot the center pixel (O8 behavior, recorded as-is).

  • oval/ovalfill use PICO-8's bounding-box signature, but the pixel

rule is console-defined (see Graphics) — like the circle rasterization note, not inherited.

  • spr skips source pixels outside the sheet; sspr clamps source

coordinates to the sheet and edge-replicates (see sspr above).

  • camera() and cursor() clamp to i16 instead of wrapping.
  • fillp diverges from PICO-8 as a family: no-arg fillp() returns the

current pattern where PICO-8 resets to solid (use fillp(0) to reset; O8's getter is deliberate and corpus-pinned); the bit order within each row is mirrored relative to PICO-8's (O8's cell bit is y%4*4 + 3 - x%4); the transparency flag is fused into bit 0x8000, doubling as the bottom-left cell's pattern bit, instead of PICO-8's separate fractional flag; and the secondary color is hardwired to (c+1)%16 with no two-color p2 parameter. fillp is also screen-anchored (camera does not scroll it) and applies to rectfill/circfill/trifill only, not line.

  • map has no layer argument. rnd takes no table. sfx takes no

channel or length arguments. max/min are two-argument.

  • Six buttons only. Sin/cos/sqrt/atan2 are console-fixed implementations;

never bit-compatible with a host libm, by design.

The O16 machine

O16 is a sibling machine per DECISIONS H/M — a second frozen profile, not an O8 superset. The tier byte on the cart selects it (0x10); every host (browser player/editor, CLI, MCP server) dispatches on it, so agents build and verify O16 carts with the same workflow as O8: buildCart16 + encodeCartPng (512x512 label), then omibit run/hash/screenshot. The constitution is DECISIONS M (palette table M.1, cart format M.2, RAM map M.3, dialect M.4); the user-facing walkthrough is docs/guide/o16.md.

Machine model deltas

thingO8O16
screen128x128256x224
palette16 fixed colors32 fixed colors (0-15 byte-identical to O8)
sprite sheet128 sprites of 8x8 (128x64)256 sprites of 16x16 (256x256), n wraps & 0xFF
map1 layer, 128x642 layers, 128x128 each
code budget11,892 bytes159,216 bytes
cart PNG160x205, 32KB payload512x512, 256KB payload (format v1-O16: 16-byte header, u32le code length, sections at 0x26E00/0x26F00/0x27000/0x37000/0x3B000/0x3F000 — full table in DECISIONS M.2)
RAM64KB, 16-bit wrap256KB, 18-bit wrap (per-byte for peek2/poke2); map in M.3
buttons68 (adds X, Y on bits 6-7)
state hashFNV-1a-32 over 64KB + frameFNV-1a-32 over 256KB + frame
affine—tline()

Everything not listed carries from O8 verbatim: the frame loop, the 4M instruction/frame budget with identical error semantics (print charges the same pool), the math library, print/font, oval/ovalfill (Q.2 — identical rule, 32-color palette), pal/palt/camera/clip/cursor semantics, sspr edge policy, sfx/music byte formats and the audio renderer (O8 sfx bytes sound identical on O16), rnd/srand, time, cartdata (first 256 bytes of the user region).

API deltas

FunctionSignatureOne line
tlinetline(x0, y0, x1, y1, [sx, sy,] [sxstep, systep])draw the line (x0,y0)-(x1,y1) — the same Bresenham as line() — sampling the gfx sheet: the first inked pixel samples sheet pixel (sx, sy) (default 0, 0), each subsequent inked pixel advances by (sxstep, systep) (defaults 1, 0)
mapmap(l, mx, my, dx, dy, [w, h])draw the w x h cell region of layer l (0/1, wraps & 1); defaults w, h = 128, 128
mgetmget(l, mx, my)read map layer l, 0 outside 0-127 on either axis
msetmset(l, mx, my, v)write map layer l, ignores out-of-bounds
sprspr(n, x, y, [w, h, flipx, flipy])same shape; cells are 16x16, sheet is 256x256, n wraps & 0xFF
paltpalt([c, [t]]) / palt(mask)same semantics; the numeric mask form is u32 — bit 31 is color 0, bit 0 is color 31
fillpfillp([p])same 16-bit pattern; the secondary color is (c + 1) % 32
btn/btnpbtn([i])same semantics; i 6 = X, i 7 = Y (input-log bits 6-7)

tline semantics, precisely:

  • Texture arguments are sheet-pixel units accumulated in Q32.32 fixed

point (integer-exact doubles; no host libm). The sample at each step is floor of the accumulated position, wrapped mod 256 on both axes — the sheet is an infinitely tiling 256x256 texture plane.

  • "Inked" means the screen pixel landed inside the camera/clip rect —

it is sampled and its accumulator step consumed whether or not the sample is ultimately drawn. palt-transparent samples skip drawing but still advance (floor holes keep alignment). Samples pass through the draw palette; camera and clip apply to the screen line exactly as line() does.

  • The raster loop is the line() Bresenham, statement-identical and

corpus-pinned.

The O16 additions beyond this table: none. No rotation/scaling/priority bits/HDMA/second font (charm ceiling, DECISIONS M.6) — a technique that matters composes from the base set.

Palette 16-31

Indices 0-15 are byte-identical to the O8 table above. The extension (hex values ratified in DECISIONS M.1):

idxhexnameidxhexname
16#ff77aapink24#1e5a45pine
17#b23a77magenta25#2d2244dusk
18#8b5fd6violet26#3fa39fsea
19#a26843umber27#e04a4fscarlet
20#6e3f30rust28#6e1e33blood
21#eec2a1peach29#fff2cccream
22#c9932eamber30#cfdceamist
23#5d6b3aolive31#0e0d15void

Index is the color; gfx and screen stay 1 byte per pixel, values masked to 5 bits (v & 31) at render. Draw-state homes move with the RAM map (draw state page at 0x27000, sprite flags at 0x27400, music patterns at 0x27500, user/cartdata at 0x27600 — in-page offsets mirror O8; DECISIONS M.3 is the full table).

The O32 machine

O32 is the third machine per DECISIONS N — the PS1-class 3D rung, a sibling profile selected by tier byte 0x20 (not an O16 superset). Every host (browser editor/player, CLI, MCP server) dispatches on it, so agents build and verify O32 carts with the same workflow as O8: emptyCart32 + buildCart32 + encodeCartPng (1024x1024 RGBA label), then omibit run/hash/screenshot. The constitution is DECISIONS N (primitive and rasterization N.1-N.2 with the N.17 span-slope erratum, lighting N.4, palette N.5, payload N.8, mesh bank N.9, RAM N.10, dialect N.11, charm ceiling N.13, texture pages N.18); the walkthrough is docs/guide/o32.md. The identity is geometry wobble — integer vertex snap, affine texture crawl, painter's depth — not color depth.

Machine model deltas

thingO8O16O32
screen128x128256x224320x240
palette16 fixed colors32 fixed colors (0-15 byte-identical to O8)256 fixed = 32 families x 8 shade steps (0-31 byte-identical to O16, 0-15 to O8)
gfx sheet128 sprites of 8x8 (128x64)256 sprites of 16x16 (256x256), n wraps & 0xFF256x1024 sheet = FOUR 256x256 texture pages (N.18), 1 B/px — page 0 is the 16x16 sprite trinity; ttri's tex arg selects the page
map1 layer, 128x642 layers, 128x128 eachnone — mget/mset/map error deterministically (below)
mesh bank——524,288 B = 21,845 face records (N.9, below)
code budget11,892 bytes159,216 bytes257,520 bytes
cart PNG160x205, 32KB payload512x512, 256KB payload1024x1024, 1MB payload (format table below)
RAM64KB, 16-bit wrap256KB, 18-bit wrap (per-byte for peek2/poke2)1MB, 20-bit wrap (per-byte for peek2/poke2); map in N.10
buttons68 (adds X, Y on bits 6-7)8 — the same set as O16 (X, Y on bits 6-7)
state hashFNV-1a-32 over 64KB + frameFNV-1a-32 over 256KB + frameFNV-1a-32 over 1MB + frame
affine—tline()tline() + ttri()

Everything not listed carries from O8/O16 verbatim: the frame loop, the 4M instruction/frame budget with identical error semantics (print charges the same pool), the math library, print/font, oval/ovalfill (Q.2 — identical rule, full-byte colors), camera/clip/cursor semantics, sspr edge policy, sfx/music byte formats and the audio renderer (O8 sfx sound identical on O32 — the third parity proof), rnd/srand, time, cartdata (first 256 bytes of the user region), spr/sspr/fget/fset (16x16 trinity, n wraps & 0xFF).

API deltas

FunctionSignatureOne line
ttrittri(x0,y0,u0,v0, x1,y1,u1,v1, x2,y2,u2,v2, [tex], [s0], [s1], [s2])textured affine triangle — the one new primitive; tex non-nil samples texture page tex (floored, wrapped mod 4 — N.18), nil = solid fill with the current draw color; shades 0-7, each defaults to 0 (semantics below)
tlinetline(x0, y0, x1, y1, [sx, sy,] [sxstep, systep])carried from O16 verbatim — samples texture PAGE 0 ONLY (no page argument in the signature; documented divergence, N.18)
paltpalt([c, [t]])per-color transparency over all 256 indices, one flag byte each; palt() resets (color 0 transparent); the numeric mask form is dropped — it throws palt mask form is unavailable on O32 (N.5.3)
palpal([c0, c1, [p]])one 256-entry remap table — the p 0 (draw) and p 1 (screen) forms write the same bytes (N.10 sanctions exactly one table; display remap ≡ draw remap); pal() resets the table to identity and transparency to the default
fillpfillp([p])same 16-bit pattern; the secondary color is (c + 1) % 256; not applied to ttri (the shade ladder is the depth cue)
pset/pgetpset(x, y, [c]) / pget(x, y)full bytes — no color mask on O32 (N.5.3)
mget/mset/map—absent — no map sections; each errors deterministically (mget is unavailable on O32 (no map sections, N.11), same shape for the others). A documented divergence, not silence
btn/btnpbtn([i])the O16 eight-button set: dpad on bits 0-3, A, B on bits 4-5, X, Y on bits 6-7 (input-log bits 64 and 128)

ttri semantics, precisely (N.1-N.2, N.17):

  • Vertices are screen-space and snap exactly: camera subtracted first,

then floor(x + 1/2) per axis, then i16 clamp — no sub-pixel precision survives anywhere. UVs are sheet-texel units of any magnitude: U = floor(u * 65536 + 1/2) mod 2^32 (Q16.16, exact for any finite input). Shades floor then clamp to [0,7]; omitted shades are 0 — an unshaded call is bit-identical to one with s = 0,0,0.

  • Degenerate triangles (zero doubled area after the snap) draw nothing

and mutate no state. Both windings rasterize: no backface cull, no z-buffer, no machine sorting — the cart sorts faces far-to-near and issues ttri calls in that order (painter's algorithm, N.3).

  • Coverage is the top-left fill rule: covered rows Y0..Y2-1, covered

columns lx..rx-1 with each edge rounded up by ceilQ(a) = floor((a + 65535) / 65536). Two triangles sharing an exact edge tile the plane with zero gap and zero double-write.

  • Span interpolation per the N.17 erratum: with left/right edge state

XL..XR (Q16.16) and DXq = XR − XL, dudx = floor(DUs · 65536 / DXq) (DUs = signed-32-bit value of UR − UL), and the first column starts at u = UL + floor(dudx · phase / 65536) with phase = lx · 65536 − XL; stepping per column u += dudx (mod 2^32) is congruent with closed-form evaluation.

  • Textured mode samples NEAREST, per page: page p (p = tex wrapped mod 4)

occupies sheet rows p*256..p*256+255; within the page the texel address tiles mod 256 on both axes — an infinitely tiling texture plane PER PAGE; UV magnitude never bleeds across a page boundary. An edge UV delta always takes the path under 32769 texels. The raw texel is palt-tested (transparent samples skip the write; stepping continues), remapped through the draw palette, then darkened by the ladder: LADDER(i, sp) = (i AND 31) + 32 * min(7, (i >> 5) + sp) — light only darkens, clamped at step 7 (N.4).

  • Solid mode (tex nil) fills with the current draw color — a full

byte, no palt test, no palette remap — then the ladder applies. color() may itself address a shaded index; gouraud deepens further.

  • Page addressing outside ttri: spr (n wraps & 0xFF) and tline see

page 0 only; sspr addresses the full 256x1024 sheet directly (region blit, not a tiling primitive).

  • fillp does not apply; ttri mutates no persistent state (like

line/rectfill); camera and clip apply exactly as for every other draw call.

The 256-color ladder

Index = family + 32 * step. Families 0-31 are the O16 palette byte-identical (so 0-15 ride along at O8's colors); step k of base channel value C is floor(C * (8 - k) / 8) per channel — linear-to-black ladders, integer-exact, zero freedom. The full 256-entry table is ratified in DECISIONS N.5.1; the core's PALETTE_O32 is generated from the formula and byte-verified against that table. The ladder doubles as the lighting model (N.4): ttri's per-vertex shades walk a family's steps through the LADDER op at draw time — no separate shade tables exist anywhere in the machine. Art-time rule: pick the family for the hue you want at full bright, let the ladder do the dark.

Cart payload — 1MB in a 1024x1024 PNG

PNG 1024x1024 RGBA; payload = low 2 bits of each RGBA channel, 1 byte per pixel, row-major from (0,0) — same encoding as O8/O16. 1,048,576 payload bytes = 0x100000 exact, zero slack. Header 16 bytes, M.2-shaped: magic O8+, tier 0x20, version 1, code length u32le at 0x05, 3 reserved bytes, CRC32 at 0x0C over the body 0x10-0xFFFFF.

sectionoffsetsizenotes
header0x0000016tier 0x20, version 1
code0x00010257,520 maxUTF-8 Lua; u32le length in header
sprite flags0x3EE002561 byte per 16x16 sprite (page 0's 256)
music0x3EF0025664 patterns x 4 B, scheme identical to O8/O16
gfx sheet0x3F000262,144256x1024 = 4 texture pages, 1 B/px (N.18)
mesh bank0x7F000524,288flat array of 24-byte face records (below)
sfx0xFF0004,096byte-identical to O8/O16 (audio continuity)

Sum check: 16 + 257,520 + 256 + 256 + 262,144 + 524,288 + 4,096 = 1,048,576 exact. CODE_MAX = 257,520. No map sections — deliberate (N.11): level layout lives in code and mesh data; sprites survive (HUD/menus) via page 0's 16x16 trinity.

Mesh bank and RAM map — 1MB exact

The mesh bank is a flat array of 24-byte face records with no headers, slots, or names — meshes are ranges the cart (and the editor's notes) define. One record: three model-space vertices as i16le x,y,z (18 bytes), then six u8 texel coords u0,v0,u1,v1,u2,v2 (6 bytes), sheet-texel units, tiled at draw. Face n occupies bank + n*24; 524,288 / 24 = 21,845 faces, the last 8 bytes of the bank are padding, defined zero. Winding is cart convention — the machine rasterizes both. Shade is never stored; it is computed per draw.

rangesizeuse
0x00000-0x3FFFF262,144gfx sheet: 256x1024 (4 texture pages), 1 B/px
0x40000-0xBFFFF524,288mesh bank (face records)
0xC0000-0xD2BFF76,800screen: 320x240, 1 B/pixel
0xD2C00-0xD3BFF4,096sfx: 64 slots x 64 B (O8 layout)
0xD3C00-0xD3FFF1,024draw state (in-page offsets mirror O8: fillp +0x0D, audio channels +0x100, music sequencer +0x140)
0xD4000-0xD40FF256palt flags: one byte per palette index
0xD4100-0xD41FF256sprite flags
0xD4200-0xD42FF256music patterns
0xD4300-0xD43FF256draw palette remap table (pal — the single table, N.10)
0xD4400-0xFFFFF179,200user data (first 256 = cartdata)

Addressing formulas:

  • Sheet pixel (x, y): 0x00000 + y * 256 + x over the full 1024-row

sheet; texture page p starts at p * 0x10000 (256 rows of 256).

  • Face n: 0x40000 + n * 24 (payload mirror at 0x7F000 + n * 24);

peek2 reads the i16s — unwrap with v - 0x10000 when >= 0x8000.

  • Screen pixel (x, y): 0xC0000 + y * 320 + x.
  • Sfx note i of slot n: 0xD2C00 + n * 64 + i * 2 (speed at note 31).
  • peek/poke wrap addresses mod 0x100000 (20-bit); peek2/poke2

wrap each byte address the same way.

  • State hash: FNV-1a-32 over the full 1MB RAM followed by the frame

counter as u32 little-endian.

The O32 charm ceiling (deliberate exclusions)

No z-buffer or per-pixel depth test, no perspective divide ever (affine forever), no filtering, no hardware lights beyond the shade ladder, no mesh-level draw API, no transforms or matrices in the console, no fog unit, no map sections, no more than FOUR texture pages (N.18 — the count is frozen there), no palt mask form, no alpha blending beyond palt. O64 is dropped as an O32 mode — tier id 0x40 stays reserved for a hypothetical sibling profile with its own frozen rasterizer (N.11). Compose or do without; the recipes (projection, painter sort, fog ramps) live in docs/guide/o32.md.

Hello cart

lua
-- hello, omibit
t = 0

function _draw()
  cls(0)
  color(1 + flr(t / 10) % 7)
  print("hello, omibit", 34, 60)
  t = t + 1
end

Build it into a cart PNG, run headless, and hash it:

sh
omibit run hello.png --frames 60

Worked example — controller and sprites

A cart that paints its own sprite at load (gfx RAM is pokeable), then lets the D-pad move it and A play a two-note blip defined the same way. No editor required — everything below is code.

lua
-- pad + sprite, built from code
px, py = 60, 60

function _init()
  -- paint sprite 1: an 8x8 arrow, sand on transparent
  local rows = {
    "...4....",
    "..44....",
    ".444444.",
    "..44....",
    "...4....",
    "...4....",
    "........",
    "........",
  }
  local base = 8 -- sprite 1: x = (1 % 16) * 8, y = flr(1 / 16) * 8
  for y = 0, 7 do
    for x = 0, 7 do
      local ch = rows[y + 1]:sub(x + 1, x + 1)
      poke(base + y * 128 + x, ch == "." and 0 or tonumber(ch, 16))
    end
  end
  -- sfx 0: square blip at full volume, then end marker
  poke(0x8000, 45) poke(0x8001, 0x38)
  poke(0x8002, 50) poke(0x8003, 0x38)
  poke(0x8004, 0)  poke(0x8005, 0)
end

function _update()
  if btn(0) then px = px - 1 end
  if btn(1) then px = px + 1 end
  if btn(2) then py = py - 1 end
  if btn(3) then py = py + 1 end
  px = mid(0, px, 120)
  py = mid(0, py, 120)
  if btnp(4) then sfx(0) end
end

function _draw()
  cls(0)
  spr(1, px, py)
  color(13)
  print("dpad move  a blip", 30, 120)
end

The input log for a headless run is one byte per frame: hold right for 30 frames (byte 0x02), tap A (byte 0x10), and the hash proves it.

Headless verification

The same core runs the CLI, the MCP server, and the browser player.

sh
omibit run cart.png --frames 600          # final state hash
omibit screenshot cart.png --frame 600    # framebuffer as PNG
omibit hash cart.png --frames 600         # first and last hash
omibit serve                              # MCP over stdio

MCP tools: load_cart(path), run(path, frames, input, perFrame), screenshot(path, frame), state_hash(path, frames). Input is the input log, base64. The workflow that matters: load, run with an input log, compare the state hash — a match is a regression receipt for the covered machine state, not a snapshot or proof of equality of arbitrary Lua globals and closures.

Persistent progress (pre-release amendment, 2026-09-24)

cartdata() activates saving for the first 256 bytes of the profile's user RAM and returns true (the established return value is unchanged). dget(index) and dset(index, value) access 64 little-endian unsigned 32-bit slots there. Call cartdata() first. Indices must be integers 0–63; values must be integers 0–4294967295. Invalid arguments error rather than silently corrupting adjacent RAM. peek/poke can also pack bytes after activation. This is game-authored progress, not a snapshot of Lua globals, stacks or execution.

Player and standalone HTML hosts restore save bytes before top-level code and _init, and automatically persist changes from successful frames. The editor, mesh preview, homepage demos and default headless runs start clean and never consume player saves. All three tiers retain 256 bytes; score/seed words remain outside this region. VM reset itself clears RAM; persistence is a host policy.

By default a save is scoped to the exact cart payload (SHA-256) and tier. To retain progress across game revisions, put a unique header comment in Lua:

lua
-- saveid: studio.mygame.v1
cartdata()
function _init()
  level = dget(0)
end
function checkpoint(next_level)
  dset(0, next_level)
end

saveid is 1–64 ASCII letters, digits, underscores, dots or hyphens. Different cart versions with the same ID and tier share progress on the same origin. Namespace your ID; change its version when the save schema is incompatible. This identity is not proof of authorship. Browser storage is local to the origin/profile and can be cleared or unavailable; game-progress controls export and import validated JSON backups and explicitly erase saved progress. Offline file: storage policy is browser-dependent; backups remain portable where storage is available. Storage errors are shown instead of claiming a save succeeded. Conflicting writes detected from another tab stop automatic writes; export the current run and reload to choose which progress to keep.

Ranked runs, daily challenges, jams and replay playback always start from zero save RAM and do not write player saves. Runs restored from nonzero saved data cannot be submitted through the replay UI: choose start ranked run. The server still verifies every submission by re-execution from the clean initial cart and seed. Thus local save editing cannot manufacture a verified score.

Successful frames update an in-memory save copy. Writes coalesce over 250 ms, and the latest received data flushes when hiding/closing a page or stopping the VM. Abrupt process/OS failure can lose the last unflushed change; there is no claim of transactional crash recovery or cross-device cloud synchronization.

Extended creator contracts

The reference above describes the legacy APIs. Opt-in O8 v3, O16 v2 and O32 v2 cartridges add the following APIs. New editor projects use these contracts; importing an old cart preserves its original contract. Source projects record o8-v3, o16-v2 or o32-v2 as their machine contract, and the shared compiler emits that version. An unchanged legacy import remains byte-for-byte; changing contracts requires an explicit source upgrade rather than changing the meaning of an old cartridge.

Player and pointer input

CallResult
btn(bit, player) / btnp(bit, player)Held/repeated button for bit 0–15 and player 0–3; player defaults to 0. Omitting the bit returns that player's mask.
axis(index, player)Axis 0–3 as an integer −127…127; invalid index/player returns 0.
trigger(index, player)Trigger 0–1 as an integer 0…255.
playerconnected(player)Whether the input packet marks that player present.
pointerpresent()Whether a pointer is present in the current frame.
pointerx() / pointery()Integer coordinates in the cart's screen space.
pointerbtn(bit)Pointer button 0–2; omit the bit to get the mask.
pointerwheel()Quantized signed wheel input.

For example, axis(0, 1) reads player 1's horizontal axis and btn(4, 1) reads their first action button. The host quantizes devices once per simulation frame. All four player slots plus pointer occupy one canonical 42-byte packet; replays use that exact packet, not a later device poll. Versioned O8RP records identify the cart contract, payload hash, seed and clean initial state. Legacy recordings retain their byte-per-frame path. Focus loss and device disconnect supply neutral input; controller access still depends on the browser.

Priority audio and PCM samples

Extended contracts expose sfxp(slot, priority[, gain, pitch, attack, release]), sfxparam(channel, gain, pitch), sfxenv(channel, attack, release) and sfxstop(channel). Extended O16/O32 sample banks support sample(slot, priority[, gain, pitch, loop, attack, release]), sampleparam(channel, gain, pitch), sampleenv(channel, attack, release) and samplestop(channel). A missing/empty sample slot errors; O8 has no PCM bank.

Priority and gain are 0–255; pitch is Q8.8 with 256 as native rate and a 64–1024 range. Attack/release count output samples, 0–22050, and default to

  1. Start calls return channel 0–3 or −1 if no eligible voice is available.

Higher priority wins; equal priority keeps the existing voice. Music reserves its synth channels. Four sample voices are separate from the four synth voices. Mixing, ramps, arbitration and sample cursors are deterministic machine state.

The SFX tab's enable PCM samples explicitly upgrades a legacy O16/O32 source contract. Import/audition WAV slots in the sample bank editor and keep the source archive to retain the original audio. Banks hold 16 clips totaling at most 65536 bytes on O16 or 131072 on O32, including 128 descriptor bytes: about 1.48 or 2.97 seconds of 22050 Hz mono signed 16-bit PCM. Samples consume the cartridge's code-region budget; builds reject overflow instead of truncating.

O32 depth and perspective

Extended O32 adds tri3d(x1,y1,z1,u1,v1,shade1, x2,y2,z2,u2,v2,shade2, x3,y3,z3,u3,v3,shade3, material) for camera-space vertices, bounded clipping, depth testing and perspective-correct texture interpolation. material packs color in bits 0–7, texture page in 8–9, textured in bit 10, cutout in 11 and half blend in 12; higher bits are rejected. ztrans() begins the transparent pass; draw opaque geometry first. The depth buffer resets each frame.

The renderer caps 2048 raster triangles after clipping and 250000 covered fragments per frame. Legacy ttri keeps its affine/painter behavior. Scene hierarchy, transforms, materials and camera authoring use the mesh tab; gameplay still controls its runtime camera and invokes the supported scene/collision helpers. See the O32 guide and support limits.