KN-86 Clip System
Last Updated: 2026-04-20
1. What is a Clip?
Section titled “1. What is a Clip?”A clip is a pre-rendered terminal animation bound to a rectangular region of the 80×25 text grid. Same primitive, different uses:
| Use case | Why a clip |
|---|---|
| Cutscenes | Hand-authored narrative beats played mid-session. Canned; cheap. |
| Loading animations | Mask real work (cartridge load, save persistence, network I/O). |
| Faked computation | Show “DECRYPTING…” / “SOLVING…” screens for operations the device doesn’t actually perform in real time — the fiction of computation, delivered as playback. |
| Attract mode | Idle-screen content that cycles when the operator is AFK. |
The mechanism is identical across all four: a .kn86clip file plays back into a region of the text buffer, frame by frame, at a declared fps. Cells outside the region are untouched, so the cartridge (or firmware) can draw chrome around a clip without the two fighting.
Named for what it is, not where it’s used. Earlier drafts called the primitive demo_player; that implied attract-only. “Clip” is neutral.
2. System at a Glance
Section titled “2. System at a Glance” AUTHORING (offline) PLAYBACK (device / emulator) --------------------- ------------------------------ +--------------------------+ TypeScript scene | Cartridge | | | nosh_clip_load_file() | v | nosh_clip_tick() | simulator.ts ---+ +------------+-------------+ | | | v | | paints into encoder.ts ---->+---> .kn86clip ------+ v | (binary) | g_system_state v | ->text_buffer export-demo.ts | +--> Attract mode attract_init() attract_tick()Same binary consumed by two independent consumers (attract and cartridges), with the cartridge API layered over the same core Clip struct.
3. Authoring a Clip
Section titled “3. Authoring a Clip”Scene authoring uses the Remotion-backed TypeScript DSL in kn86-attract/. A scene is deterministic — same inputs produce the same byte-identical .kn86clip.
3.1 Scene Skeleton
Section titled “3.1 Scene Skeleton”Scenes live in kn86-attract/src/scenes/library/ and are registered in index.ts:
import { scene, at, text, progressBar, audio } from "../create-scene";
export default scene( { id: "my-clip", name: "My Clip", description: "3s animated panel", fps: 30, duration: 3.0, // seconds tags: ["loader"], }, [ at(0.0, text(9, 20, "+--- LOADING ---+")), at(0.5, progressBar(11, 21, 15, 1.0, { speed: 10 })), audio(0.5, "confirm"), // auto-fires SFX at frame 15 at(2.9, text(9, 20, "+--- DONE ---+")), ]);Then npm run export:all in kn86-attract/ compiles every registered scene to clips/<scene-id>.kn86clip.
3.2 Targeting a Subregion
Section titled “3.2 Targeting a Subregion”The encoder auto-detects a bounding box around all painted cells and writes it to the clip header. Paint only inside the area you want to own. Anything outside your bounding box stays with the caller (cartridge chrome, firmware status bar, a different clip).
For a 40×7 subregion at rows 9–15, cols 20–59, keep all your row/col values inside that rectangle; the resulting clip will have region=(9, 20, 7, 40).
If painted content covers more than ~75% of the grid in both dimensions, the encoder falls back to full-screen region=(0, 0, 25, 80).
3.3 Available Scene Primitives
Section titled “3.3 Available Scene Primitives”See scenes/types.ts and create-scene.ts for the full API. Key primitives:
text(row, col, content, speed?)— place text, optionally typed at N chars/sectextBlock(row, col, lines, opts?)— multi-line block with per-line timingprogressBar(row, col, width, progress, opts?)— animated fillcounter(row, col, start, end, format, framesPerTick)— hex/decimal counterflicker(row, col, values, framesPerValue, totalFrames)— cycling textfill(row, col, char, count, speed?)— fill N cellsclear(rows?, cols?)— clear a rangeaudio(seconds, cueName)— schedule an SFX cue (see §6)hold(durationSeconds)— hold state
For larger layout patterns (boxes, dividers, status/action bars), see existing scenes in the library.
3.4 When to Use the kn86-scene-designer Skill
Section titled “3.4 When to Use the kn86-scene-designer Skill”If you’re authoring scenes interactively with Claude Code, the kn86-scene-designer skill loads the scene design principles, the row-ownership rules, and reference patterns. Trigger it with “design a scene for …” prompts.
4. Playing a Clip
Section titled “4. Playing a Clip”Two consumers in the emulator today, sharing the same core player (Clip).
4.1 Attract Mode (Firmware-Owned)
Section titled “4.1 Attract Mode (Firmware-Owned)”Firmware scans a directory for .kn86clip files at boot and rotates through them when the operator has been idle for 15 seconds. No cartridge code required. See attract.c.
The default directory on the emulator is {exe}/../clips/; override with --clip-dir <path>.
4.2 Cartridge API (nosh_clip)
Section titled “4.2 Cartridge API (nosh_clip)”Cartridges play clips through nosh_clip.h, which is auto-included by nosh_cart.h. The API owns a single-player instance per NoshClip struct.
Lifecycle
Section titled “Lifecycle”#include "nosh_cart.h"
CELL_DEFINE(viewer, NoshClip myclip; bool loaded;)
CELL_ON_ENTER(viewer) { SELF(viewer); self->loaded = nosh_clip_load_file(&self->myclip, "./clips/my-clip.kn86clip"); if (self->loaded) { nosh_clip_set_loop(&self->myclip, true); }}
CELL_ON_DISPLAY(viewer) { SELF(viewer); /* Paint whatever chrome you own this frame. */ draw_my_chrome();
/* Advance the clip by one frame. Paints inside the region only; * the chrome you just drew is untouched outside that rectangle. */ if (self->loaded) { nosh_clip_tick(&self->myclip); }}
CELL_ON_EXIT(viewer) { SELF(viewer); nosh_clip_free(&self->myclip);}Full API Reference
Section titled “Full API Reference”| Function | Purpose |
|---|---|
nosh_clip_load_file(nc, path) | Load a .kn86clip from disk. malloc-backed; freed by nosh_clip_free. |
nosh_clip_load_buffer(nc, data, len) | Zero-copy load from caller-owned memory. Suitable for clips packed into the cartridge binary. |
nosh_clip_tick(nc) | Advance one frame, paint into the active text buffer, auto-fire any SFX cue. |
nosh_clip_set_loop(nc, bool) | When true, seek to frame 0 automatically when the clip ends. |
nosh_clip_seek(nc, frame) | Jump to a specific frame (O(1)). |
nosh_clip_get_region(nc, ...) | Read region bounds out of the header. |
nosh_clip_next_cue(nc) | Peek the pending SFX cue (auto-fire already handled; mostly useful for intercepting). |
nosh_clip_is_loaded(nc) | Guard condition. |
nosh_clip_is_finished(nc) | True if the clip ended and loop is off. |
nosh_clip_free(nc) | Release the player and any owned buffer. |
load_file vs load_buffer
Section titled “load_file vs load_buffer”| API | Allocator | Use case |
|---|---|---|
nosh_clip_load_file | malloc | Desktop emulator, SD-card-backed device, any filesystem target. |
nosh_clip_load_buffer | none | Production cartridges that embed clip data statically. Zero heap cost. |
Caller owns buffer lifetime in the load_buffer path — the clip holds a read-only pointer.
5. Composing With Chrome
Section titled “5. Composing With Chrome”The region mechanic is what makes clips composable. Cells outside the region are never touched by the clip player, so cartridges (or firmware) can draw static chrome that survives playback without redrawing every frame.
The Chrome + Inlay Pattern
Section titled “The Chrome + Inlay Pattern”+--------------------------------------------------------------------------------+| KN-86 CLIP DEMO <-- cartridge chrome (rows 0-8, rows 16-24) || ================ || ...explanatory text... || || +--------------------------------------+ || INLAY -> | DECRYPTING PAYLOAD -- RUNTIME KEY | <-- clip region || | KEY FRAGMENT: 0x1005 | (rows 9-15, || | SOLVING: ############# | cols 20-59) || | CYCLES: 47281 | || +--------------------------------------+ || || USE CASES: CUTSCENES | LOADERS | CANNED CALCULATIONS | ATTRACT |+--------------------------------------------------------------------------------+Reference cartridge: clip_demo_cart.c. It draws the surrounding chrome once (on first on_display tick) and calls nosh_clip_tick() every subsequent tick. The clip auto-fires its SFX cue on completion.
Rules for Chrome/Inlay Coexistence
Section titled “Rules for Chrome/Inlay Coexistence”- Don’t call
nosh_text_clear()during playback. It nukes the whole buffer, including the clip’s just-painted content. Draw chrome once after a clear; leave the buffer alone on subsequent ticks. - Z-order: the last writer wins. Chrome drawn AFTER
nosh_clip_tick()will overwrite clip cells inside the region. Draw chrome first, then tick. - Disjoint regions can coexist. Two clips with non-overlapping regions can both play simultaneously from the same or different consumers.
- Firmware rows 0 and 24. Cartridges never paint rows 0 or 24 (status bar / action bar). A clip that writes into those rows would get overwritten on the next system image update — design subregions to respect the same rule (keep
region_row ≥ 1andregion_row + region_h ≤ 24).
6. Audio Cues
Section titled “6. Audio Cues”Scenes can embed audio(timeSeconds, cueName) markers. The encoder maps cue names to cue_id bytes (see CUE_MAP in encoder.ts). At playback:
clip_ticksetspending_cuewhen it hits a CUE opcode.clip_next_cuereturns the pending value, resetting on read (one-shot).- Cartridges using
nosh_clip_tick()get the cue auto-fired throughnosh_sfx_play(cue_id)— no extra code required. - Attract mode does the same via
sfx_play().
Cartridges that need to intercept cues (custom SFX, filtering) can call nosh_clip_next_cue() before calling nosh_clip_tick() in the same frame — but the simpler path is to let auto-fire do its job.
Cue Namespace
Section titled “Cue Namespace”0x00–0x7F— nOSh-allocated (see sfx.h).0x80–0xFF— reserved for cartridge-defined cues.
7. File Distribution
Section titled “7. File Distribution”Development
Section titled “Development”- Scenes:
kn86-attract/src/scenes/library/*.ts - Built clips:
kn86-attract/clips/*.kn86clip(gitignored; regenerated bynpm run export:all) - Emulator picks up clips from
{exe}/../clips/. The recommended setup symlinkskn86-emulator/build/clips -> ../../kn86-attract/clipsso attract mode and carts both see the same files.
Production
Section titled “Production”- Attract clips ship alongside firmware, probably on the SD card or embedded flash partition.
- Cartridge-owned clips are expected to be packed into the cartridge binary and loaded via
nosh_clip_load_buffer()with a staticconst uint8_t[]array — no filesystem required.
8. Performance Characteristics
Section titled “8. Performance Characteristics”Measured on the 103-scene reference library:
| Metric | Value |
|---|---|
| Per-tick cost | Bounded by opcode stream length; KEYFRAMES are region_h * region_w writes, DELTAs are 3N bytes where N ≤ count on that frame. |
| Per-player RAM | sizeof(Clip) ≈ 40 bytes + file buffer. |
| Per-player file buffer | Full clip size; static or malloc-backed. |
ATTRACT_MAX_CLIPS (attract mode) | 16 simultaneously loaded clips in RAM. |
ATTRACT_MAX_CLIP_SIZE | 64 KB per clip (soft cap in attract, enforced at load). |
| Typical clip size | 4–8 KB |
| Scene library total | 103 scenes / ~559 KB |
Zero malloc in the per-frame hot path. All allocations happen at load time.
9. Integration Roadmap
Section titled “9. Integration Roadmap”| Consumer | Status |
|---|---|
| Desktop emulator — attract | Shipped |
| Desktop emulator — cart API | Shipped |
| Cartridge demo (chrome+inlay) | Shipped (clip_demo_cart.c) |
| Pi Zero 2 W device | Pending nOSh runtime port (same API, SDL path swapped for framebuffer) |
10. Reference
Section titled “10. Reference”| Topic | Doc / File |
|---|---|
| Binary format spec | spikes/kn86clip-format-spec.md |
| Cartridge authoring grammar | KN-86-Cartridge-Grammar-Spec.md |
| nOSh API versioning | KN-86-NoshAPI-Versioning.md |
| Character set / font | KN-86-Character-Set-and-Font-Architecture.md |
| Canonical hardware spec | CLAUDE.md §Canonical Hardware Specification |
| Scene designer skill | .claude/skills/kn86-scene-designer/ |
| Core decoder | kn86-emulator/src/clip.c |
| Cartridge API | kn86-emulator/src/nosh_clip.h |
| Demo cartridge | kn86-emulator/carts/clip_demo_cart.c |
| Scene pipeline | kn86-attract/tools/ |
Appendix A: Quickstart for Cartridge Developers
Section titled “Appendix A: Quickstart for Cartridge Developers”- Author the clip. Add a scene file to
kn86-attract/src/scenes/library/my-clip.ts, register it inindex.ts. - Build it.
cd kn86-attract && npm run export:all— producesclips/my-clip.kn86clip. - Load in your cart.
NoshClip my_clip;nosh_clip_load_file(&my_clip, "./clips/my-clip.kn86clip");nosh_clip_set_loop(&my_clip, true);
- Tick each frame.
nosh_clip_tick(&my_clip); /* paints into text buffer, auto-fires SFX */
- Free on exit.
nosh_clip_free(&my_clip);
Done. The clip paints only inside its region; everything you draw outside survives.