Skip to content

.kn86clip Region-Aware Playback Format Specification

This document describes the on-disk binary format for KN-86 clips — pre-rendered region-bound terminal animations that power cutscenes, loading screens, faked-computation panels, and attract-mode content.

For a higher-level introduction to the clip system (authoring workflow, cartridge API, integration patterns), see KN-86-Clip-System.md.


A .kn86clip file encodes a deterministic 80×25 character animation that paints into a declared rectangular region of the text grid. Content outside the region is left untouched, allowing clips to compose with cartridge chrome, firmware-owned rows (status/action bars), or other simultaneously-playing clips in disjoint regions.

Design goals:

  1. Tiny. A 30-second attract loop fits in ~5–7 KB; most single-screen clips are under 8 KB.
  2. Trivial to decode. Reference player (clip.c) is ~270 lines, zero malloc, zero floating-point.
  3. Region-aware. The header declares a sub-rectangle of the 80×25 grid; the player clips all writes to that region.
  4. Deterministic. Same file + same start frame = same byte-level output. No RNG, no wall-clock dependencies.

All clip files target the canonical 80×25 character grid (see CLAUDE.md §Canonical Hardware Specification). Region dimensions in the header MUST satisfy region_row + region_h ≤ 25 and region_col + region_w ≤ 80.


+------------------------------+
| Header (12 bytes) |
+------------------------------+
| Frame Index (4N bytes) | N = frame_count
+------------------------------+
| Frame Data (variable) |
+------------------------------+

All multi-byte integers are little-endian.


OffsetSizeFieldValue
0x004magicASCII "KC86" (0x4B 0x43 0x38 0x36)
0x041version0x01
0x051fpsPlayback frame rate (typical: 30)
0x062frame_countTotal frames (u16 LE, 1–65535)
0x081region_rowTop-left row of target region (0-based)
0x091region_colTop-left column of target region (0-based)
0x0A1region_hRegion height in rows (1–25)
0x0B1region_wRegion width in columns (1–80)

A loader MUST reject files where any of:

  • magic"KC86"
  • version ≠ 1
  • region_row + region_h > 25
  • region_col + region_w > 80
  • File size < 12 + frame_count * 4

Reference validation: clip.c:72-137.


Immediately after the header. frame_count entries, each 4 bytes:

SizeFieldDescription
4frame_offsetu32 LE byte offset from start of Frame Data to this frame’s opcode stream

The index enables O(1) seek via clip_seek(). Total size: frame_count × 4 bytes.


Starts immediately after the Frame Index (at file offset 12 + frame_count * 4). Contains frame_count opcode streams, each terminated by the END opcode.

ByteNameSemantics
0x00HOLDNo-op. Buffer retains previous frame’s content.
0x01DELTASparse update: N (row, col, char) triples.
0x02KEYFRAMEFull region dump: region_h × region_w bytes, row-major.
0x03CLEARFill region with 0x00.
0x04CUEFires a firmware SFX cue. Followed by one cue_id byte.
0xFFENDEnd of this frame’s opcode stream.

Multiple opcodes may appear in one frame (e.g. CUE + DELTA + END). The stream is walked until END; then the player advances to the next frame.

+0 1 opcode (0x01)
+1 1 count N (1..255)
+2 3N N triples of (row, col, char):
row: local row relative to region_row (0..region_h-1)
col: local col relative to region_col (0..region_w-1)
char: byte value written into the cell

Out-of-bounds (row, col) triples (beyond the declared region) are silently dropped by the reference player. Global text-buffer position is computed as (region_row + row) * 80 + (region_col + col).

The encoder emits multiple DELTA chunks per frame if more than 255 cells changed.

+0 1 opcode (0x02)
+1 H * W region bytes, row-major

Where H = region_h and W = region_w. A keyframe is emitted on frame 0 if the region is non-empty, and whenever the encoder detects that at least 30% of region cells changed from the previous frame (threshold in encoder.ts).

+0 1 opcode (0x03)

Fills every cell in the region with 0x00. Used when the entire region transitions to empty.

+0 1 opcode (0x04)
+1 1 cue_id (u8)

Sets pending_cue on the player. Consumers retrieve it via clip_next_cue() — one-shot: the pending cue clears on read. Cartridge-side, nosh_clip_tick() auto-fires cues through nosh_sfx_play().

Terminates the current frame’s opcode stream. The player then advances current_frame.


Measured from the shipped 103-scene library:

Content typeTypical size
Short animation (90 fr)~1.5 KB
Cartridge-load sequence~6 KB
Complex 30s attract loop~8 KB
Scene library total (103)~559 KB

Per-frame overhead: 4 bytes (index entry) + 1 byte (END opcode) + 1–3 bytes per changed cell.


LayerFile
Core decoderkn86-emulator/src/clip.c, clip.h
Cartridge API wrapperkn86-emulator/src/nosh_clip.c, nosh_clip.h
Attract-mode consumerkn86-emulator/src/attract.c
Decoder unit testskn86-emulator/tests/test_clip.c
Attract-mode testskn86-emulator/tests/test_attract.c
Scene simulatorkn86-attract/tools/simulator.ts
Binary encoderkn86-attract/tools/encoder.ts
Export CLIkn86-attract/tools/export-demo.ts

Version 0x01 is the current and only format version. Incompatible changes (new opcodes, header fields, etc.) MUST bump version in the header. Consumers reject unknown versions.

Reserved ranges:

  • Opcodes 0x05–0xFE are reserved for future extensions. Unknown opcodes terminate the frame (safe default in clip.c).
  • cue_id values are defined in sfx.h; 0x00–0x7F are nOSh-allocated, 0x80–0xFF are cartridge-defined.

  • Attribute channel — per-cell style (inverted, blink) via an optional side-band after the region byte.
  • Bitmap regions — target DISPLAY_MODE_SPLIT bitmap rows instead of text cells.
  • Streaming — wire protocol variant for serial/USB rather than file-backed.

None of the above requires a format v2 yet; all would add a header flag byte gated on a new version.