.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.
1. Purpose
Section titled “1. Purpose”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:
- Tiny. A 30-second attract loop fits in ~5–7 KB; most single-screen clips are under 8 KB.
- Trivial to decode. Reference player (clip.c) is ~270 lines, zero malloc, zero floating-point.
- Region-aware. The header declares a sub-rectangle of the 80×25 grid; the player clips all writes to that region.
- Deterministic. Same file + same start frame = same byte-level output. No RNG, no wall-clock dependencies.
2. Grid Dimensions
Section titled “2. Grid Dimensions”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.
3. File Layout
Section titled “3. File Layout”+------------------------------+| Header (12 bytes) |+------------------------------+| Frame Index (4N bytes) | N = frame_count+------------------------------+| Frame Data (variable) |+------------------------------+All multi-byte integers are little-endian.
4. Header (12 bytes)
Section titled “4. Header (12 bytes)”| Offset | Size | Field | Value |
|---|---|---|---|
| 0x00 | 4 | magic | ASCII "KC86" (0x4B 0x43 0x38 0x36) |
| 0x04 | 1 | version | 0x01 |
| 0x05 | 1 | fps | Playback frame rate (typical: 30) |
| 0x06 | 2 | frame_count | Total frames (u16 LE, 1–65535) |
| 0x08 | 1 | region_row | Top-left row of target region (0-based) |
| 0x09 | 1 | region_col | Top-left column of target region (0-based) |
| 0x0A | 1 | region_h | Region height in rows (1–25) |
| 0x0B | 1 | region_w | Region width in columns (1–80) |
Validation
Section titled “Validation”A loader MUST reject files where any of:
magic≠"KC86"version≠ 1region_row + region_h > 25region_col + region_w > 80- File size <
12 + frame_count * 4
Reference validation: clip.c:72-137.
5. Frame Index
Section titled “5. Frame Index”Immediately after the header. frame_count entries, each 4 bytes:
| Size | Field | Description |
|---|---|---|
| 4 | frame_offset | u32 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.
6. Frame Data
Section titled “6. Frame Data”Starts immediately after the Frame Index (at file offset 12 + frame_count * 4). Contains frame_count opcode streams, each terminated by the END opcode.
6.1 Opcodes
Section titled “6.1 Opcodes”| Byte | Name | Semantics |
|---|---|---|
| 0x00 | HOLD | No-op. Buffer retains previous frame’s content. |
| 0x01 | DELTA | Sparse update: N (row, col, char) triples. |
| 0x02 | KEYFRAME | Full region dump: region_h × region_w bytes, row-major. |
| 0x03 | CLEAR | Fill region with 0x00. |
| 0x04 | CUE | Fires a firmware SFX cue. Followed by one cue_id byte. |
| 0xFF | END | End 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.
6.2 DELTA (0x01)
Section titled “6.2 DELTA (0x01)”+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 cellOut-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.
6.3 KEYFRAME (0x02)
Section titled “6.3 KEYFRAME (0x02)”+0 1 opcode (0x02)+1 H * W region bytes, row-majorWhere 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).
6.4 CLEAR (0x03)
Section titled “6.4 CLEAR (0x03)”+0 1 opcode (0x03)Fills every cell in the region with 0x00. Used when the entire region transitions to empty.
6.5 CUE (0x04)
Section titled “6.5 CUE (0x04)”+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().
6.6 END (0xFF)
Section titled “6.6 END (0xFF)”Terminates the current frame’s opcode stream. The player then advances current_frame.
7. Size Estimates
Section titled “7. Size Estimates”Measured from the shipped 103-scene library:
| Content type | Typical 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.
8. Reference Implementation
Section titled “8. Reference Implementation”| Layer | File |
|---|---|
| Core decoder | kn86-emulator/src/clip.c, clip.h |
| Cartridge API wrapper | kn86-emulator/src/nosh_clip.c, nosh_clip.h |
| Attract-mode consumer | kn86-emulator/src/attract.c |
| Decoder unit tests | kn86-emulator/tests/test_clip.c |
| Attract-mode tests | kn86-emulator/tests/test_attract.c |
| Scene simulator | kn86-attract/tools/simulator.ts |
| Binary encoder | kn86-attract/tools/encoder.ts |
| Export CLI | kn86-attract/tools/export-demo.ts |
9. Versioning
Section titled “9. Versioning”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–0xFEare reserved for future extensions. Unknown opcodes terminate the frame (safe default in clip.c). cue_idvalues are defined in sfx.h;0x00–0x7Fare nOSh-allocated,0x80–0xFFare cartridge-defined.
10. Future Extensions (not in v1)
Section titled “10. Future Extensions (not in v1)”- Attribute channel — per-cell style (inverted, blink) via an optional side-band after the region byte.
- Bitmap regions — target
DISPLAY_MODE_SPLITbitmap 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.