Skip to content

ADR-0047: The experience tier — pixel-addressed minigames on the cell-API

Sibling of: ADR-0046 — the other half of the foreground-surface split. ADR-0046 took the data tier (traditional-software surfaces as knEmacs modes); this ADR takes the pixel-rendered tiers (custom game surfaces on the cell-API). The two are siblings in placement (the foreground split), not in construction discipline — see the note under Decision 7. There is no “experience engine” symmetric to knEmacs. Related / composes: ADR-0045 (the background substrate — the hunt keeps ticking under a minigame), ADR-0036 (the native framebuffer renderer — the single pixel surface, the system-tier blit, the 20 fps cap), ADR-0027 (the constrained cart cell-API + the privilege boundary), ADR-0040 (DeckRunner / screen router — gains the typed surface kind), ADR-0042 (first-party programs — a different surface category), ADR-0043 (mission objectives — consume the typed outcome), ADR-0006 (cart format / schema — the return schema), ADR-0005 (NoshAPI — gains experience-tier verbs; future amendment), ADR-0015 (the Null-cart chrome-exception precedent). Design sources (mined, not embedded): the BearLibTerminal Design Overview (foo.wyrd.name/en:bearlibterminal:design), RandyGaul/cute_headers, raylib’s reasings.h / raymath.h, and PICO-8 / TIC-80.


ADR-0046 drew the scope line and built one side — the data tier, where traditional-software-shaped programs are modes on the knEmacs engine. It deferred the other side: the “minigames that pop up during a run” — piloting a drone, making moves against an ICE on the ICEBREAKER board. This ADR is that other side.

Two independent axes describe a foreground surface, and conflating them was the original error this ADR (post-review) avoids:

  • Address axiscell-addressed (8×8 glyphs on the grid) vs pixel-addressed (arbitrary-size sprites at pixel offsets).
  • Interaction axisevent-driven + command (idle until input; keys→commands) vs active-loop + control (ticks every frame; keys→controls + trackpoint).

The placement heuristic Josh landed on — traditional software → mode; custom/bespoke game surface → cell-API — is really a statement about the address axis (bespoke game surfaces want pixels). It does not dictate the interaction axis. The ICE board is the proof: it is bespoke and wants the full pixel surface (pixel-addressed), yet it is turn-based — discrete-state, command-driven, event-redrawn (mode-like interaction). So it is neither a knEmacs mode nor a real-time experience; it is a third thing.

The renderer (ADR-0036) is a real pixel framebuffer underneath the 8×8 cell grid — which is what makes a pixel-addressed tier possible — but it is a single, process-lifetime-stable surface, the pixel blit is a privileged system-tier primitive, the firmware chrome rows are non-negotiable, and the redraw is capped at 20 fps. Those four facts bound everything below; see Foundation reconciliation.

  • ICEBREAKER’s ICE board and the drone set-pieces are next to design; the surface kinds, the render/capability contract, and the invoke/resolve lifecycle should be fixed first.
  • A run weaves between data-tier surfaces and pixel-rendered set-pieces; the screen router needs to know which kind it routes to, to drive transitions and switch input/tick behavior.
  • Monochrome amber, arena-bounded, no-malloc-in-hot-paths, single-threaded Pi Zero 2 W; carts are KEC Lisp in a constrained (cart-tier) KEC context that, by design, cannot resolve system-tier primitives.
  • The runtime already owns its renderer (ADR-0036), PSG audio, and input — an off-the-shelf terminal gamekit would import a second, competing renderer/IO layer (termbox2 was already evicted by ADR-0036). The device’s real gap is game logic, not a screen layer.

  1. Three foreground surface kinds, along the two axes (not a binary):

    • mode — cell-addressed + event/command. The knEmacs data tier (ADR-0046).
    • tacticalpixel-addressed + event/command. Bespoke, full-render, but discrete-state and command-driven. The ICE board charters this kind on day one.
    • experience — pixel-addressed + active-loop/control. Real-time, keys+trackpoint as controls. The drone pilot.

    (The fourth quadrant — cell-addressed + active-loop — is unused.) tactical and experience are the two pixel-rendered kinds this ADR governs; they share the render/capability contract below and differ only on the interaction axis.

  2. Pixel rendering is system-tier; carts get it invoke-only. The pixel blit (render_bitmap / render_fill_rect / render_set_pixel, ADR-0036) is a privileged system primitive — it is not bound into a cart-tier KEC context (ADR-0027’s boundary holds, or the cell-API sandbox is decorative). A cart invokes a tactical/experience surface and supplies its content and logic (sprite data, behavior tables, per-tick lambdas); the runtime owns the pixels. The cart-facing surface is constrained, system-mediated verbs (e.g. draw-sprite by handle, move-toward, ease-to) — never raw pixel primitives. “Bespoke” means a cart’s own content/logic, still invoke-mediated — not raw pixel access.

  3. The render primitive is one blit. An 8×8 glyph, a multi-cell sprite, and a full-content-area splash are the same render_bitmap() operation with size + sub-cell pixel offset + alignment + a 1-bit transparency mask (monochrome — mask yes, color blending no). One blit keeps the cart-facing draw verb-set tiny.

  4. Surfaces composite inside the cart-usable content area; the chrome rows persist. Row 0 / Row 74 are firmware-owned and non-negotiable (Spec Hygiene Rule 5); a tactical/experience surface draws within the Rows 1–73 pixel band, and the status/action chrome stays live (you want battery/timer/mode visible mid-set-piece). A modal full-surface takeover (suspending the chrome) is not granted by this ADR; if ever wanted it requires an explicit exception + Josh + a canonical-spec.md update, on the precedent of ADR-0015’s Null-cart CIPHER exception.

  5. The screen router carries the typed surface kind. The type lets the router drive transitions/animations and switch the two contracts at the seam: the runtime contract (mode/tactical are event-driven; experience is an active per-frame tick loop) and the input contract (mode/tactical take keys→commands; experience takes keys→controls + trackpoint).

  6. Lifecycle: push → play → typed outcome → pop. A mission phase or cart handler pushes a surface onto the router (interrupting the data-tier flow); it runs; it resolves to a typed outcome; the outcome feeds the phase chain / objective model (ADR-0043). A minigame is a phase gate with a real-time (or turn-based) resolution. Outcomes are cart-declared schema, materialized in UDS: at cart load the cart registers its return schema into Universal Deck State and owns the outcome→state mapping; the runtime/objective layer consumes typed returns without hardcoding any minigame’s outcomes (composes ADR-0006 schema + ADR-0043 objectives).

  7. Build-then-extract an experience kit — runtime-reusable, invoked across carts. Start a surface bespoke in its first cart; extract the kit (the one blit primitive + painter’s-order draw + sub-cell offset + mask + the constrained verbs) when a second needs the same machinery. Pure-logic helpers (collision, easing, vector math) live in the C/system layer and are exposed to carts as high-level verbs, not raw primitives.

    Construction asymmetry with ADR-0046 (deliberate). The data tier has one shared engine, built framework-first (knEmacs). The pixel tiers are bespoke-first, extract-a-kit-later. The kit is a harvested convenience, not a mandatory substrate — do not expect an “experience engine” symmetric to knEmacs. The siblinghood is in placement, not construction.

  8. Composition with the background tier. A surface runs in the foreground while the ADR-0045 scheduler keeps background systems ticking (the hunt), surfacing/intruding via the event bus. ADR-0045 (background) + ADR-0046 (data foreground) + this ADR (pixel foreground) are the three layers of a run — the “minigame with a hunt in the background” that opened this design line.


Foundation reconciliation (renderer + sandbox)

Section titled “Foundation reconciliation (renderer + sandbox)”

This ADR asserts nothing the ADR-0036 foundation and the cart sandbox don’t fund:

  • Capability boundary — resolved invoke-only (Decision 2): pixel-blit stays system-tier; carts never get raw pixel primitives. The ADR-0027/0036 privilege boundary is preserved.
  • Chrome rows — resolved composite-in-1–73 (Decision 4): Rule 5 holds; modal takeover is deferred behind an explicit exception.
  • Render budget — sized here, not assumed:
    • “Layers” = painter’s-order redraw into the single, process-lifetime-stable ADR-0036 surface (background → entities → HUD, in draw order) — not N retained 1024×600 RGB565 buffers (~1.2 MB each, incompatible with the no-malloc/fixed-arena discipline). No compositor, no per-layer allocation.
    • Double-buffering / page-flip / tearing is a presentation follow-on, now owned by the single SDL3 host on both targets (KMSDRM on the Pi). (2026-07-27: there is no separate /dev/fb0 backend and never will be — one SDL3 host runs on both targets over KMSDRM. See ADR-0036 Amendment Log.) Tear-free on-device presentation is still not promised; the work moved, it did not land.
    • The 20 fps redraw cap (ADR-0036, shared single-threaded with the ADR-0045 scheduler + PSG audio) is the action-minigame ceiling. Sub-cell motion is smooth in position (pixel offset) but the frame rate is 20 fps; fast action set-pieces must be designed to that ceiling.

Option A: Pixel-rendered tiers (tactical + experience) on the cell-API — system-owned pixels invoked by carts, typed router, schema-in-UDS outcomes, build-then-extract kit. (ACCEPTED)

Section titled “Option A: Pixel-rendered tiers (tactical + experience) on the cell-API — system-owned pixels invoked by carts, typed router, schema-in-UDS outcomes, build-then-extract kit. (ACCEPTED)”

Chosen because it gives bespoke game surfaces the full pixel surface the heuristic sends here, keeps the cart sandbox real (invoke-only), separates render from interaction so turn-based and real-time surfaces each get the right contract, gives the run a clean push→play→outcome lifecycle feeding objectives, and composes with the background hunt — all within the single-surface / system-blit / chrome-row / 20 fps foundation.

Option B: Build minigames as knEmacs modes (force them into the engine).

Section titled “Option B: Build minigames as knEmacs modes (force them into the engine).”

Rejected. The buffer/cell model is for traditional structured-data surfaces; a drone pilot is not a buffer. The heuristic sends bespoke game surfaces to the pixel tiers, not the engine.

Option C: Pure-cart minigames with raw pixel access — each cart hand-rolls its own.

Section titled “Option C: Pure-cart minigames with raw pixel access — each cart hand-rolls its own.”

Rejected on two counts. It re-pays the machinery per cart (no reuse), and giving carts raw render_bitmap collapses the ADR-0027/0036 capability boundary — the cell-API sandbox becomes decorative. Invoke-only + build-then-extract gets the reuse and keeps the sandbox.

Option D: Embed an off-the-shelf terminal gamekit (ncurses / notcurses / BearLibTerminal / raylib).

Section titled “Option D: Embed an off-the-shelf terminal gamekit (ncurses / notcurses / BearLibTerminal / raylib).”

Rejected — wrong layer. Those are renderer/IO frameworks that duplicate and compete with the native renderer the runtime already owns (ADR-0036); termbox2, the closest, was already evicted by ADR-0036. The device’s gap is game logic, not a screen. Instead: mine the design (BearLibTerminal’s layers + sub-cell offset + alignment + mask-compositing model is directly transferable) and lift pure-logic single-headers (collision/easing/vector-math) — see Open.


Option A buys full pixel render + cross-cart reuse + a clean run-composition + a preserved sandbox, at the cost of (1) a typed surface kind + two contracts in the screen router, (2) an experience kit to build (mitigated by build-then-extract), and (3) a third surface kind (tactical) to keep the turn-based/real-time distinction honest. All accepted: the pixel tiers are first-class in runs, invoke-only keeps them from puncturing the sandbox, and the painter’s-order/20 fps sizing keeps the render claims funded.


  • Bespoke surfaces get the full pixel surface — arbitrary sprites, smooth sub-cell motion, painter’s-order layering, masking — without a competing renderer and without breaking the sandbox.
  • Runtime-reusable across carts; missions invoke shared set-pieces.
  • Render and interaction are separated, so turn-based boards and real-time set-pieces each get the right contract.
  • Clean push → play → typed-outcome lifecycle feeding objectives; the cart owns its outcome contract, so the runtime stays generic.
  • Composes with the background substrate — the three-layer run model.
  • One blit primitive ⇒ a tiny cart-facing draw API.
  • The screen router gains a typed surface kind and two contracts.
  • An experience kit must be built (build-then-extract is the mitigation).
  • Three surface kinds instead of two.
  • Painter’s-order redraw (no retained-layer compositor) and the 20 fps ceiling bound what action set-pieces can do; tear-free on-device presentation is still unproven under the SDL3/KMSDRM host.
  • Typed surface kind in the screen router (ADR-0040), with the mode/tactical/experience contract table.
  • The experience-tier invoke verb + the constrained draw/sprite/easing verbs in NoshAPI (ADR-0005 amendment, future) — explicitly not raw pixel primitives.
  • The cart return-schema → UDS mechanism (ADR-0006 + UDS + ADR-0043).
  • Build ICEBREAKER’s ICE board (the first tactical surface) + the drone (the first experience), design-for-extraction → the kit.
  • Evaluate the lift candidates (cute_c2.h, reasings.h, raymath.h) + the SDK-side asset pipeline.

Explicitly Not Decided (open — recorded so nothing here reads as committed)

Section titled “Explicitly Not Decided (open — recorded so nothing here reads as committed)”

Posture only. Left open:

  • The minigame roster and per-surface mechanics / controls / layouts. Per-cart and per-program design. (The kinds are decided; the tactical interaction contract details — turn structure, selection model — are per-design.)
  • The exact experience-kit API and NoshAPI verbs. The PICO-8 spr / map / sfx / btn shape is the illustrative reference, not the committed surface — and whatever lands is constrained/system-mediated per Decision 2.
  • A modal full-surface (chrome-suspending) takeover — not granted here; an open question requiring an explicit exception + Josh (ADR-0015 precedent).
  • Which external libraries are actually vendored. cute_c2.h / reasings.h / raymath.h are lift candidates to evaluate, not committed dependencies; the SDK-side asset pipeline (cute_tiled / cute_aseprite / cute_png) and any PICO-8/TIC-80-style on-device authoring tooling are directions, not decisions.
  • The exact UDS return-schema format and versioning.

Documentation Updates (REQUIRED — Spec Hygiene Rule 3)

Section titled “Documentation Updates (REQUIRED — Spec Hygiene Rule 3)”
  • docs/adr/ADR-0047-experience-tier-minigames.md — this file.
  • docs/adr/README.md — index row added (sibling to ADR-0046).
  • docs/adr/ADR-0005-ffi-surface.md — future amendment: experience-tier invoke + constrained draw/sprite verbs (when they land; non-blocking).
  • Per-surface design docs note their kind (tactical/experience), pixel-addressed construction, and their cart return schema. (Non-blocking; per-program/per-cart.)

ADR-0046 split the foreground and built one half — the deck’s traditional software, modes on the knEmacs engine. This is the other half: the set-pieces that interrupt a run, where you stop reading the system and start acting in it — flying the drone, fighting the ICE. The first cut of this ADR called those “experiences” and waved at “the full rendering surface,” but a coherence pass against the renderer caught the seams: the pixel blit is the system’s to do, not a cart’s, so a cart invokes a surface and the runtime draws it; the chrome rows are non-negotiable, so a set-piece lives inside the content area with the battery and the timer still showing; and “layers” on a device with one surface and no spare megabytes means painter’s order, not a stack of buffers, at twenty frames a second. The pass also found a quieter thing: bespoke and real-time aren’t the same axis. The ICE board wants every pixel but takes its turns — pixel-rendered, yet command-driven — so it isn’t a mode and isn’t a real-time experience; it’s a third kind, and naming it keeps a turn-based board from being forced to fly like a drone. The renderer to draw all this, we already built; the game logic we lift as small libraries; the design we take from the fantasy consoles and the pseudo-terminals without taking their engines. And underneath, the background keeps ticking — the run you’re not looking at, the thing hunting you — exactly the picture this whole line started from, now with all three layers named and none of them claiming more than the foundation can pay for.