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
mathtable 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
| Function | Signature | One line |
|---|---|---|
cls | cls([c]) | clear screen to color c, reset cursor and clip |
pset | pset(x, y, [c]) | set pixel |
pget | pget(x, y) | read pixel from screen |
line | line(x0, y0, x1, y1, [c]) | draw line |
rect | rect(x0, y0, x1, y1, [c]) | rectangle outline |
rectfill | rectfill(x0, y0, x1, y1, [c]) | filled rectangle |
circ | circ(cx, cy, r, [c]) | circle outline |
circfill | circfill(cx, cy, r, [c]) | filled circle |
oval | oval(x0, y0, x1, y1, [c]) | ellipse outline inscribed in the box |
ovalfill | ovalfill(x0, y0, x1, y1, [c]) | filled ellipse inscribed in the box |
tri | tri(x0, y0, x1, y1, x2, y2, [c]) | triangle outline (filled) |
trifill | trifill(x0, y0, x1, y1, x2, y2, [c]) | filled triangle |
print | print(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 |
cursor | cursor(x, y) | set print cursor |
color | color(c) | set default draw color |
fillp | fillp([p]) | set 16-bit fill pattern for filled shapes, no arg returns it |
spr | spr(n, x, y, [w, h, flipx, flipy]) | draw sprite n, w x h cells |
sspr | sspr(sx, sy, sw, sh, dx, dy, [dw, dh, flipx, flipy]) | draw stretched sprite region |
fget | fget(n, [f]) | sprite n flags: whole byte, or bit f as 0/1 |
fset | fset(n, [f,] v) | set sprite n flags: whole byte, or bit f |
camera | camera([x, y]) | offset all draw coordinates |
clip | clip([x, y, w, h]) | clip drawing region, no args resets |
pal | pal([c0, c1, [p]]) | remap color c0 to c1, p 0 draw 1 screen, no args resets palettes + transparency |
palt | palt([c, [t]]) | set sprite transparency for color c, or by 16-bit mask, no args resets |
mget | mget(mx, my) | read map cell, 0 outside bounds |
mset | mset(mx, my, v) | write map cell |
map | map(mx, my, dx, dy, [w, h]) | draw map region to screen |
max | max(x, y) | larger of x, y |
min | min(x, y) | smaller of x, y |
mid | mid(x, y, z) | middle value, use to clamp |
flr | flr(x) | round down |
ceil | ceil(x) | round up |
cos | cos(t) | cosine, t in turns |
sin | sin(t) | sine, t in turns, flipped |
atan2 | atan2(dy, dx) | angle in turns 0..1 |
sqrt | sqrt(x) | square root, x<0 gives 0 |
abs | abs(x) | absolute value |
sgn | sgn(x) | -1 if x<0 else 1 |
rnd | rnd([x]) | uniform 0..x exclusive, float, no arg gives 0..1 |
srand | srand(seed) | seed the deterministic prng |
sub | sub(s, i, [j]) | substring, 1-based inclusive, Lua index rules |
btn | btn([i]) | button held, no arg gives bitmask |
btnp | btnp([i]) | button pressed this frame, with key repeat |
peek | peek(addr) | read byte at addr 0x0-0xFFFF |
poke | poke(addr, v) | write byte at addr 0x0-0xFFFF |
peek2 | peek2(addr) | read u16 little-endian at addr |
poke2 | poke2(addr, v) | write u16 little-endian at addr |
sfx | sfx(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) |
music | music([n]) | play music from pattern n onward, music() or music(-1) stops |
time | time() | frame count since load, integer |
cartdata | cartdata() | activates the 256-byte save region, returns true |
dget | dget(index) | read unsigned 32-bit save slot 0–63 after cartdata() |
dset | dset(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.
| idx | hex | name | idx | hex | name |
|---|---|---|---|---|---|
| 0 | #1a1c2c | black | 8 | #29366f | indigo |
| 1 | #5d275d | plum | 9 | #3b5dc9 | blue |
| 2 | #b13e53 | maroon | 10 | #41a6f6 | sky |
| 3 | #ef7d57 | orange | 11 | #73eff7 | cyan |
| 4 | #ffcd75 | sand | 12 | #f4f4f4 | white |
| 5 | #a7f070 | lime | 13 | #94b0c2 | silver |
| 6 | #38b764 | green | 14 | #566c86 | steel |
| 7 | #257179 | teal | 15 | #333c57 | slate |
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).
| range | size | use |
|---|---|---|
| 0x0000-0x1FFF | 8KB | gfx: 128 sprites, 8x8, 1 byte per pixel |
| 0x2000-0x3FFF | 8KB | map: 128 x 64 cells, 1 byte per cell |
| 0x4000-0x7FFF | 16KB | screen: 128x128, 1 byte per pixel |
| 0x8000-0x8FFF | 4KB | sfx: 64 sfx x 32 notes x 2 bytes |
| 0x9000-0x93FF | 1KB | draw 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 | ~27KB | user 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/pokewrap addresses to 0x0000-0xFFFF.peek2/poke2read 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.
| bit | button | btn() index |
|---|---|---|
| 0 | left | 0 |
| 1 | right | 1 |
| 2 | up | 2 |
| 3 | down | 3 |
| 4 | A | 4 |
| 5 | B | 5 |
| 6-7 | reserved, 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
stateHashfield 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;
mathis absent. pairs()andnext()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.
rndis 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 torectfill,
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
circand
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: onlyfalse
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).
mgetreturns 0 for out-of-bounds cells.msetignores 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/mintake exactly two numbers in v1.sub(s, i, [j])— Luastring.subsemantics: 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):
| byte | bits | field |
|---|---|---|
| 0 | 0-5 | pitch 0-63 (33 = 440 Hz, one semitone per step) |
| 0 | 6-7 | wave, low 2 bits |
| 1 | 0 | wave, high bit |
| 1 | 1-2 | effect: 0 none, 1 slide (2-3 reserved) |
| 1 | 3-5 | volume 0-7 |
| 1 | 6-7 | reserved, 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:
| byte | bits | field |
|---|---|---|
| all | 0-6 | track: 0-63 plays that sfx slot on that channel, >= 64 silent (0x7F canonical) |
| 0 | 7 | loop-begin flag |
| 1 | 7 | loop-end flag |
| 2, 3 | 7 | reserved, 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:
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)
endPattern 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._updateruns 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):
| word | address (O8) | encoding | meaning |
|---|---|---|---|
| score | user + 0x100 (0x9500) | u32le | the leaderboard score: the value there when the run ends |
| dayseed | user + 0x110 (0x9510) | u16le | the 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
mathtable, nopeek4/poke4, nostat, no second gfx bank. time()returns frames, not seconds.- Default draw color is 13, not 6.
printfollows PICO-8 color semantics: any color argument (2-arg or
4-arg form) becomes the persistent draw color.
printwith embedded\nadvances 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.
sprwith w or h below 1 still draws a 1x1-cell sprite;circand
circfill with a negative radius plot the center pixel (O8 behavior, recorded as-is).
oval/ovalfilluse PICO-8's bounding-box signature, but the pixel
rule is console-defined (see Graphics) — like the circle rasterization note, not inherited.
sprskips source pixels outside the sheet;ssprclamps source
coordinates to the sheet and edge-replicates (see sspr above).
camera()andcursor()clamp to i16 instead of wrapping.fillpdiverges from PICO-8 as a family: no-argfillp()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.
maphas no layer argument.rndtakes no table.sfxtakes 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
| thing | O8 | O16 |
|---|---|---|
| screen | 128x128 | 256x224 |
| palette | 16 fixed colors | 32 fixed colors (0-15 byte-identical to O8) |
| sprite sheet | 128 sprites of 8x8 (128x64) | 256 sprites of 16x16 (256x256), n wraps & 0xFF |
| map | 1 layer, 128x64 | 2 layers, 128x128 each |
| code budget | 11,892 bytes | 159,216 bytes |
| cart PNG | 160x205, 32KB payload | 512x512, 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) |
| RAM | 64KB, 16-bit wrap | 256KB, 18-bit wrap (per-byte for peek2/poke2); map in M.3 |
| buttons | 6 | 8 (adds X, Y on bits 6-7) |
| state hash | FNV-1a-32 over 64KB + frame | FNV-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
| Function | Signature | One line |
|---|---|---|
tline | tline(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) |
map | map(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 |
mget | mget(l, mx, my) | read map layer l, 0 outside 0-127 on either axis |
mset | mset(l, mx, my, v) | write map layer l, ignores out-of-bounds |
spr | spr(n, x, y, [w, h, flipx, flipy]) | same shape; cells are 16x16, sheet is 256x256, n wraps & 0xFF |
palt | palt([c, [t]]) / palt(mask) | same semantics; the numeric mask form is u32 — bit 31 is color 0, bit 0 is color 31 |
fillp | fillp([p]) | same 16-bit pattern; the secondary color is (c + 1) % 32 |
btn/btnp | btn([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):
| idx | hex | name | idx | hex | name |
|---|---|---|---|---|---|
| 16 | #ff77aa | pink | 24 | #1e5a45 | pine |
| 17 | #b23a77 | magenta | 25 | #2d2244 | dusk |
| 18 | #8b5fd6 | violet | 26 | #3fa39f | sea |
| 19 | #a26843 | umber | 27 | #e04a4f | scarlet |
| 20 | #6e3f30 | rust | 28 | #6e1e33 | blood |
| 21 | #eec2a1 | peach | 29 | #fff2cc | cream |
| 22 | #c9932e | amber | 30 | #cfdcea | mist |
| 23 | #5d6b3a | olive | 31 | #0e0d15 | void |
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
| thing | O8 | O16 | O32 |
|---|---|---|---|
| screen | 128x128 | 256x224 | 320x240 |
| palette | 16 fixed colors | 32 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 sheet | 128 sprites of 8x8 (128x64) | 256 sprites of 16x16 (256x256), n wraps & 0xFF | 256x1024 sheet = FOUR 256x256 texture pages (N.18), 1 B/px — page 0 is the 16x16 sprite trinity; ttri's tex arg selects the page |
| map | 1 layer, 128x64 | 2 layers, 128x128 each | none — mget/mset/map error deterministically (below) |
| mesh bank | — | — | 524,288 B = 21,845 face records (N.9, below) |
| code budget | 11,892 bytes | 159,216 bytes | 257,520 bytes |
| cart PNG | 160x205, 32KB payload | 512x512, 256KB payload | 1024x1024, 1MB payload (format table below) |
| RAM | 64KB, 16-bit wrap | 256KB, 18-bit wrap (per-byte for peek2/poke2) | 1MB, 20-bit wrap (per-byte for peek2/poke2); map in N.10 |
| buttons | 6 | 8 (adds X, Y on bits 6-7) | 8 — the same set as O16 (X, Y on bits 6-7) |
| state hash | FNV-1a-32 over 64KB + frame | FNV-1a-32 over 256KB + frame | FNV-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
| Function | Signature | One line |
|---|---|---|
ttri | ttri(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) |
tline | tline(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) |
palt | palt([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) |
pal | pal([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 |
fillp | fillp([p]) | same 16-bit pattern; the secondary color is (c + 1) % 256; not applied to ttri (the shade ladder is the depth cue) |
pset/pget | pset(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/btnp | btn([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 (
texnil) 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) andtlinesee
page 0 only; sspr addresses the full 256x1024 sheet directly (region blit, not a tiling primitive).
fillpdoes not apply;ttrimutates 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.
| section | offset | size | notes |
|---|---|---|---|
| header | 0x00000 | 16 | tier 0x20, version 1 |
| code | 0x00010 | 257,520 max | UTF-8 Lua; u32le length in header |
| sprite flags | 0x3EE00 | 256 | 1 byte per 16x16 sprite (page 0's 256) |
| music | 0x3EF00 | 256 | 64 patterns x 4 B, scheme identical to O8/O16 |
| gfx sheet | 0x3F000 | 262,144 | 256x1024 = 4 texture pages, 1 B/px (N.18) |
| mesh bank | 0x7F000 | 524,288 | flat array of 24-byte face records (below) |
| sfx | 0xFF000 | 4,096 | byte-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.
| range | size | use |
|---|---|---|
| 0x00000-0x3FFFF | 262,144 | gfx sheet: 256x1024 (4 texture pages), 1 B/px |
| 0x40000-0xBFFFF | 524,288 | mesh bank (face records) |
| 0xC0000-0xD2BFF | 76,800 | screen: 320x240, 1 B/pixel |
| 0xD2C00-0xD3BFF | 4,096 | sfx: 64 slots x 64 B (O8 layout) |
| 0xD3C00-0xD3FFF | 1,024 | draw state (in-page offsets mirror O8: fillp +0x0D, audio channels +0x100, music sequencer +0x140) |
| 0xD4000-0xD40FF | 256 | palt flags: one byte per palette index |
| 0xD4100-0xD41FF | 256 | sprite flags |
| 0xD4200-0xD42FF | 256 | music patterns |
| 0xD4300-0xD43FF | 256 | draw palette remap table (pal — the single table, N.10) |
| 0xD4400-0xFFFFF | 179,200 | user data (first 256 = cartdata) |
Addressing formulas:
- Sheet pixel (x, y):
0x00000 + y * 256 + xover 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/pokewrap 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
-- hello, omibit
t = 0
function _draw()
cls(0)
color(1 + flr(t / 10) % 7)
print("hello, omibit", 34, 60)
t = t + 1
endBuild it into a cart PNG, run headless, and hash it:
omibit run hello.png --frames 60Worked 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.
-- 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)
endThe 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.
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 stdioMCP 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:
-- saveid: studio.mygame.v1
cartdata()
function _init()
level = dget(0)
end
function checkpoint(next_level)
dset(0, next_level)
endsaveid 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
| Call | Result |
|---|---|
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
- 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.