Skip to content

ADR-0014: Display Profile Redesign — 12×24 Font Cell on 960×600 Logical Framebuffer

The KN-86’s canonical display has been Elecrow 7” IPS at 1024×600 since the Pi Zero 2 W target was chosen. What was never authoritative was how the 80×25 text grid should land on that panel. Two contradictory specifications lived in the repo simultaneously:

  • Code (kn86-emulator/src/display_profile.c): 640×200 logical framebuffer (80×25 × 8×8 font), integer scale = 1, centered with a 192 px horizontal and 200 px vertical letterbox on each side. On the 7” panel this image covers ~20% of the visible area — a postage-stamp UI on a handheld display.
  • CLAUDE.md prose (font-cell row): “Composited onto the 1024×600 Elecrow frame with a centered letterbox (192/200 px); per-cell physical footprint on the panel is ≈ 12.8×24 px (non-integer horizontal is accepted).” This described a 1.6× horizontal / 3× vertical stretched composition. It was never implemented. Press Start 2P is designed for square pixels; a 1:1.875 pixel aspect would visibly distort every glyph.

The Definitive Guide’s 2026-04-16 reconciliation pass (Appendix B item 1) already named the right answer — “~12×24 pixels per character” — but that pass updated prose, not code or the canonical spec table, and the contradiction persisted.

  • Pi Zero prototype bring-up needs one authoritative display geometry. The Stage-1 assembly step in the Pi Zero Build Spec asks the implementer to “verify the text grid lays out correctly”; that verification is meaningless while code and spec disagree.
  • Spec Hygiene Rule 1 (“if a document contradicts these values, the document is wrong — fix it, don’t fork”) requires closing the drift before any further screen, cartridge, or ADR work builds on the stale geometry.
  • Font implementation work (Layer 1 per prompts/cc-layer1-font-implementation.md) is blocked on a committed cell size.
  • 80×25 grid is canonical and non-negotiable (CLAUDE.md). Any solution must land on cell dimensions C_w × C_h such that 80 × C_w ≤ 1024 and 25 × C_h ≤ 600, and ideally such that both fit cleanly with integer scale.
  • Row 0 (status bar) and Row 24 (action bar) are nOSh-runtime-owned. Cartridges get rows 1–23. The row layout is non-negotiable.
  • Press Start 2P 8×8 source bitmap is the existing font asset. A re-cut to native 12×24 is feasible but not in v0.1 scope.
  • Amber monochrome constraint is unaffected by this decision.

The KN-86 adopts a 12×24 physical font cell on a 960×600 logical framebuffer, centered in the 1024×600 Elecrow panel with a 32 px horizontal letterbox per side and zero vertical letterbox. This ratifies what the Definitive Guide’s reconciliation pass implied and brings code, spec prose, and canonical specification into alignment.

Concrete commitments:

  1. Logical framebuffer dimensions are 960 × 600 pixels. KN86_FRAMEBUFFER_WIDTH becomes 960; KN86_FRAMEBUFFER_HEIGHT becomes 600 in kn86-emulator/src/types.h.
  2. Cell dimensions are KN86_CELL_WIDTH = 12 and KN86_CELL_HEIGHT = 24, added as new constants in types.h. These are the physical-pixel footprint of one character on the panel.
  3. Source font dimensions remain KN86_FONT_WIDTH = 8 and KN86_FONT_HEIGHT = 8 — the Press Start 2P 8×8 bitmap in font.c is not re-cut.
  4. Glyph rendering strategy (v0.1): for each character at grid position (col, row), render the 8×8 source bitmap at 1× horizontal / 2× vertical scale, producing an 8×16 visible glyph, centered inside the 12×24 cell with a 2 px horizontal padding and 4 px vertical padding. Origin of the visible glyph is (col × 12 + 2, row × 24 + 4).
  5. Integer scale on every profile. display_profile.c sets scale = 1, viewport_x = (1024 − 960) / 2 = 32, viewport_y = (600 − 600) / 2 = 0 for both PROFILE_PROTOTYPE and PROFILE_DESKTOP_EMU. The desktop emulator may over-scale the window via --scale N at the SDL layer without changing logical dimensions.
  6. BITMAP mode uses the same 960×600 logical framebuffer. The CLAUDE.md “Display modes” row updates BITMAP (1024×600)BITMAP (960×600). Cartridges see 960×600 as the full pixel canvas; the 32 px horizontal letterbox is invisible to them.
  7. Grid layout is unchanged. Row 0 = firmware status bar (cell-rows y = 0..23). Rows 1–23 = cartridge content (y = 24..575). Row 24 = firmware action bar (y = 576..599). Row 24 sits at the physical bottom of the panel — no bottom letterbox.
  8. Native 12×24 font cut is explicit follow-up work. An artist/tool pass produces a true 12×24 per-glyph bitmap table that replaces the 8×8-plus-padding approach. That pass is a follow-on task, not a blocker. The v0.1 rendering is correct and shippable; v2 font is a quality upgrade.
  9. Cursor geometry updates to cell units: cursor box drawn from (col × 12, row × 24 + 22) to (col × 12 + 12, row × 24 + 24) (a 12×2 underscore on the final two rows of the cell). Exact pixel positions are implementer’s call within the cell; this is a guideline.
  10. kn86-emulator/docs/adr/002-display-specification.md remains superseded and gains a pointer note to ADR-0014 (the current live ADR for display geometry).

Option A: Status quo — 8×8 cell, 640×200 framebuffer, 1:1 letterbox

Section titled “Option A: Status quo — 8×8 cell, 640×200 framebuffer, 1:1 letterbox”

Keep the current code as-is. The 640×200 logical canvas sits 1:1 in the 1024×600 panel with 192 px horizontal and 200 px vertical letterboxes. Integer math is clean. No font or rendering changes.

Rejected because: the image uses only ~20% of a 7 inch panel. On a handheld with a 7” screen the legibility and visual weight are wrong — it looks like a small TV inside a bezel, not a cyberdeck display. This is a UX failure for the product’s primary sensory surface.

Option B: Stretched non-integer — 8×8 cell, 1.6× horizontal / 3× vertical

Section titled “Option B: Stretched non-integer — 8×8 cell, 1.6× horizontal / 3× vertical”

Keep the 8×8 source font and the 640×200 logical canvas, but composite it onto the 1024×600 panel with non-integer scale (1.6× horizontal, 3× vertical). This is what CLAUDE.md’s prose described. Every physical cell becomes ~12.8×24 px; every logical pixel becomes a non-square 1.6×3 rectangle.

Rejected because: Press Start 2P and the box-drawing glyphs are designed for square pixels. At 1:1.875 pixel aspect ratio, horizontal strokes thicken, vertical strokes stay thin, and round/diagonal glyphs look visibly mangled. The non-integer horizontal scale also produces sub-pixel rendering choices (1.6× of 8 = 12.8, which is not a pixel grid) that either require anti-aliasing (inconsistent with the crisp retro aesthetic) or rounding (which produces column-width shimmer across the glyph row). This option maximises screen coverage but abandons the art direction.

Option C: 12×24 cell, 960×600 framebuffer, 32/0 letterbox (ACCEPTED)

Section titled “Option C: 12×24 cell, 960×600 framebuffer, 32/0 letterbox (ACCEPTED)”

Redesign the cell geometry. Logical framebuffer becomes 960×600 (80 × 12, 25 × 24). Integer scale = 1 maps this 1:1 onto the Elecrow with a 32 px horizontal letterbox per side and no vertical letterbox. 94% of the panel carries content; the remaining 6% is a thin vertical band on each side that reads as intentional bezel rather than accidental empty space.

The 8×8 Press Start 2P source renders inside the cell with integer 1×2 scaling and padding (2 px horizontal, 4 px vertical). Glyphs retain their designed shape with a horizontal elongation by factor 2 that matches the physical cell aspect ratio. A future native-12×24 font cut is a straight quality upgrade that reuses the same pipeline.

Chosen because: it preserves the 80×25 grid, fills the panel, stays on integer scale throughout the pipeline, reuses the existing 8×8 art asset, and leaves a clean path to a bespoke 12×24 font cut.


Dimensions that matter:

DimensionA (status quo)B (stretched)C (12×24, ACCEPTED)
Screen coverage on 7” panel20%100%94%
Pixel aspect ratio1:1 square1:1.875 distorted1:1 square
Integer scale throughoutyesnoyes
Press Start 2P integrityperfectvisibly distortedpreserved via 1×2 integer scale
Framebuffer memory at 32 bpp128 KB (640×200×4)128 KB2.3 MB (960×600×4)
Bitmap render loop pixel count128,000128,000 (logical)576,000
Migration costzeromedium (non-integer compositor)medium (framebuffer + cell refactor)
Follow-on claritynone (no path to better)none (dead-end)native 12×24 font cut

The honest cost of C is framebuffer memory (~4.5× growth) and pixel count in rendering loops. On the Pi Zero 2 W with 512 MB RAM this is not a problem — 2.3 MB for the framebuffer is noise against kernel + SDL + nOSh process footprint. Rendering-loop cost is a real concern only if we approach the event-driven redraw budget (20 fps animation cap, ~50 ms headroom per frame). Profiling the existing 640×200 bitmap path shows it well under budget; 4.5× of a small number is still a small number. We will measure on hardware as part of Pi Zero bring-up, but the expected outcome is “no perceptible difference.”

What C costs us aesthetically: the 32 px horizontal letterbox is not zero, and a very large glyph (e.g., the lambda) in the 8×16 visible rectangle has 2 px of horizontal breathing room. Whether that reads as “characters floating in a monospace grid” (good) or “characters look narrow” (bad) is a judgement call that the native 12×24 font cut can later correct. v0.1 is acceptable; v2 improves.


  • 94% panel coverage. The UI feels like a 7” handheld display, not a small inset.
  • Integer scale throughout. Every pixel on the Elecrow is one logical pixel. No sub-pixel rendering decisions. No anti-aliasing. The retro aesthetic is preserved structurally.
  • Grid preserved. 80×25 is and remains canonical. Zero impact on cartridge code, gameplay specs, or screen designs that reason in grid units.
  • Reuses existing art. Press Start 2P 8×8 bitmap in font.c ships unchanged. Re-rendering is a code change, not an art change.
  • Clean upgrade path. A future native 12×24 font cut drops into the same rendering pipeline without changing any public surface.
  • Spec drift resolved. CLAUDE.md, display_profile.c, tests, and the Definitive Guide all state the same geometry after this ADR lands.
  • Framebuffer memory grows 4.5× — from ~128 KB to ~2.3 MB at 32 bpp. Acceptable on Pi Zero 2 W; would not be acceptable on a microcontroller target, but that target has been retired.
  • Bitmap-mode render loop runs 4.5× more pixels. Expected to remain well under the 20 fps animation budget; to be measured during Pi Zero bring-up.
  • Glyph rendering is padding-aware. display.c cell rendering can no longer do font_y = row × 8; it must respect the 2/4 padding. Small refactor, but a real one.
  • Tests reference specific pixel coordinates. Every test_display_render.c / test_display_profile.c / test_attrib_buffer.c assertion that hard-codes a pixel position needs updating.
  • 32 px horizontal letterbox is visible. Not invisible bezel; it’s 6% of panel width. Could be read as “wasted space.” Native 12×24 font cut does not close this — only a 12.8×24 design would, and we rejected that.
  • F1. Native 12×24 font cut. Cut Press Start 2P (or a KN-86 custom) at true 12×24 resolution, replacing the 8×8-plus-padding approach. Improves glyph fidelity.
  • F2. Bitmap-rendering perf pass on Pi Zero 2 W. Measure the 960×600 render path under typical cartridge load. File a perf-optimisation task only if we find a real budget miss.
  • F3. Retire the KN-86-Prototype-Architecture.md legacy prose if it still references 640×200 or 80×24 — per the 2026-04-16 reconciliation and this ADR it should already be cleaned, but verify.
  • F4. BITMAP-mode documentation sweep. Cartridges that author raw bitmap content need a clear statement that the canvas is 960×600, not 1024×600. Update docs/architecture/KN-86-Capability-Model-Spec.md and cartridge-authoring docs if they state otherwise.

Documentation Updates (REQUIRED — part of the decision, not aspirational)

Section titled “Documentation Updates (REQUIRED — part of the decision, not aspirational)”

Every file below must change in the same PR that lands this ADR. The audit agent enforces this list; failing to tick a box is treated as a live contradiction per Spec Hygiene Rule 3.

  • CLAUDE.md — Canonical Hardware Specification: update Font cell row prose (retire “12.8×24 / non-integer”, state “Logical framebuffer is 960×600; each cell is a 12×24 px physical footprint on the Elecrow; 32 px horizontal letterbox, 0 px vertical letterbox; integer scale = 1. Glyph rendering in v0.1: 8×8 Press Start 2P scaled 1×2 and centered in-cell with 2/4 padding.”). Update Display modes row: BITMAP (1024×600)BITMAP (960×600). Update Architecture tree line for display.c (640x200 logical framebuffer960x600 logical framebuffer).
  • docs/writing/CLAUDE.md — mirror the same Canonical Hardware Specification updates + the display.c tree line.
  • docs/KN-86-Definitive-Guide.md — Appendix B item 1: refine the “~12×24 pixels per character” language to match the ADR precisely (“12×24 cell on 960×600 logical framebuffer”). Scan the rest of the guide for any 640×200 or 640x200 references and update.
  • docs/ui-design/KN-86-Marty-Glitch-Visual-Prompt.md — “Logical framebuffer 640×200” → “Logical framebuffer 960×600”.
  • docs/plans/2026-04-14-multi-resolution-and-repl-design.md — if it contains any hard-coded geometry, align; otherwise add a note linking to ADR-0014.
  • docs/architecture/KN-86-Prototype-Architecture.md — sweep for stale dims (one hit flagged in grep).
  • docs/game-design/KN-86-Strategy-Passive-System-Modules-Spec.md — sweep for stale dims.
  • prompts/spike-80x25-display-validation.md — update the 640×200 language and the “do not change grid constants” caution to reflect the new framebuffer size (grid is still 80×25; framebuffer changes).
  • prompts/cc-layer1-font-implementation.md — update pixel coordinates + the “do not change types.h grid constants” language. Note that KN86_FRAMEBUFFER_WIDTH/_HEIGHT change per ADR-0014; the grid constants (KN86_TEXT_COLS / KN86_TEXT_ROWS) do not.
  • prompts/implement-kn86demo-playback.md — scan and update if it hard-codes 640×200.
  • kn86-emulator/src/types.hKN86_FRAMEBUFFER_WIDTH 640→960, KN86_FRAMEBUFFER_HEIGHT 200→600. Add KN86_CELL_WIDTH 12 and KN86_CELL_HEIGHT 24. Update the header comment block to cite ADR-0014 as the authoritative source.
  • kn86-emulator/src/display_profile.h — comment on line 62 (“currently 640x200 everywhere”) updated to cite 960×600.
  • kn86-emulator/src/display_profile.cPROFILE_PROTOTYPE and PROFILE_DESKTOP_EMU entries: logical_w 640→960, logical_h 200→600, viewport_x computes to 32, viewport_y computes to 0. Comment prose updated. The CHROME_VIEWPORT_Y, CHROME_SOFT_KEYS_Y, CHROME_VIEWPORT_H, CHROME_STATUS_H, CHROME_SOFT_KEYS_H defines update to cell-scaled values (Row 0 = y 0..23 h 24; rows 1–23 viewport = y 24..575 h 552; Row 24 = y 576..599 h 24).
  • kn86-emulator/src/display.c — text rendering loop uses KN86_CELL_WIDTH/_HEIGHT for cell positioning, and applies the 2/4 padding for glyph placement. Cursor geometry updated to cell units.
  • kn86-emulator/src/font.c — no code change (source bitmap unchanged), but verify the file’s header comment doesn’t pin the render strategy to 8×8.
  • kn86-emulator/src/main.c — line 315 comment (“1× scale, 192/200 letterbox”) → “1× scale, 32/0 letterbox”.
  • kn86-emulator/src/nosh.c — sweep for stale dims.
  • kn86-emulator/README.md — tree line for display.c (640x200 logical framebuffer960x600 logical framebuffer).
  • kn86-emulator/tests/test_display_render.c — update any hard-coded pixel-coordinate assertions to match the new framebuffer and cell geometry.
  • kn86-emulator/tests/test_display_profile.c — update the prototype/desktop_emu expectation (line 168 comment + assertions) to the new 960×600 geometry, scale=1, viewport 32/0.
  • kn86-emulator/tests/test_attrib_buffer.c — verify no hard-coded pixel dims; update if present.
  • kn86-emulator/docs/adr/002-display-specification.md — add a note in the existing “Superseded” banner pointing to ADR-0014 (repo root docs/architecture/adr/) as the current live geometry ADR. Do not change the body; this is a cross-reference update only.
  • docs/hardware/archive/KN-86-Sourcing-Guide.md — no update needed (this is the archived pre-Pi-Zero sourcing doc; its “DEPRECATED HTM640200” reference is to an old panel, unrelated to framebuffer dims). Verify and move on.

A PR that lands this ADR without ticking every non-verification box above fails review. The verification boxes (files where we only check for stale refs) close when the grep confirms the file is clean.


The KN-86’s canonical display has always been the Elecrow 7” IPS at 1024×600 — that was never in question. What had drifted was how the 80×25 character grid should land on that panel. For a year the emulator composed an 8×8 font × 80×25 grid into a 640×200 logical framebuffer, letterboxed 1:1 into the Elecrow — which rendered a postage-stamp UI on a 7 inch screen. Meanwhile CLAUDE.md’s prose described an alternative stretched composition (1.6×3, 12.8×24 px per cell) that would have filled the panel but distorted every Press Start 2P glyph. That prose was never implemented, and for as long as it sat in the canonical spec it was a live contradiction with the code. The Definitive Guide’s 2026-04-16 reconciliation pass named the right answer — “~12×24 pixels per character” — but updated docs without updating code or the canonical spec, and the drift persisted. ADR-0014 ratifies that reconciliation: 12×24 cell, 960×600 logical framebuffer, 32 px horizontal letterbox, zero vertical letterbox, integer scale throughout. The existing 8×8 Press Start 2P art ships unchanged, rendered at 1×2 scale centered in the new cell. A native 12×24 font cut is explicit follow-up work. A future reader should take away three things: (1) the grid did not change — 80×25 is and will be canonical; (2) the cell geometry moved in the direction the physical panel was always asking for; (3) this ADR is the single authoritative statement on display geometry, and any doc that contradicts it is wrong and must be fixed, not forked.