ADR-0014: Display Profile Redesign — 12×24 Font Cell on 960×600 Logical Framebuffer
Context
Section titled “Context”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.
Forcing functions
Section titled “Forcing functions”- 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.
Constraints
Section titled “Constraints”- 80×25 grid is canonical and non-negotiable (CLAUDE.md). Any solution must land on cell dimensions
C_w × C_hsuch that80 × C_w ≤ 1024and25 × 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.
Decision
Section titled “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:
- Logical framebuffer dimensions are
960 × 600pixels.KN86_FRAMEBUFFER_WIDTHbecomes960;KN86_FRAMEBUFFER_HEIGHTbecomes600inkn86-emulator/src/types.h. - Cell dimensions are
KN86_CELL_WIDTH = 12andKN86_CELL_HEIGHT = 24, added as new constants intypes.h. These are the physical-pixel footprint of one character on the panel. - Source font dimensions remain
KN86_FONT_WIDTH = 8andKN86_FONT_HEIGHT = 8— the Press Start 2P 8×8 bitmap infont.cis not re-cut. - Glyph rendering strategy (v0.1): for each character at grid position (col, row), render the 8×8 source bitmap at
1× horizontal/2× verticalscale, 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). - Integer scale on every profile.
display_profile.csetsscale = 1,viewport_x = (1024 − 960) / 2 = 32,viewport_y = (600 − 600) / 2 = 0for bothPROFILE_PROTOTYPEandPROFILE_DESKTOP_EMU. The desktop emulator may over-scale the window via--scale Nat the SDL layer without changing logical dimensions. - 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. - 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. - 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.
- 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. kn86-emulator/docs/adr/002-display-specification.mdremains superseded and gains a pointer note to ADR-0014 (the current live ADR for display geometry).
Options Considered
Section titled “Options Considered”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.
Trade-off Analysis
Section titled “Trade-off Analysis”Dimensions that matter:
| Dimension | A (status quo) | B (stretched) | C (12×24, ACCEPTED) |
|---|---|---|---|
| Screen coverage on 7” panel | 20% | 100% | 94% |
| Pixel aspect ratio | 1:1 square | 1:1.875 distorted | 1:1 square |
| Integer scale throughout | yes | no | yes |
| Press Start 2P integrity | perfect | visibly distorted | preserved via 1×2 integer scale |
| Framebuffer memory at 32 bpp | 128 KB (640×200×4) | 128 KB | 2.3 MB (960×600×4) |
| Bitmap render loop pixel count | 128,000 | 128,000 (logical) | 576,000 |
| Migration cost | zero | medium (non-integer compositor) | medium (framebuffer + cell refactor) |
| Follow-on clarity | none (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.
Consequences
Section titled “Consequences”Positive
Section titled “Positive”- 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.cships 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.
Negative / Accepted costs
Section titled “Negative / Accepted costs”- 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.ccell rendering can no longer dofont_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.cassertion 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.
Follow-on work this ADR creates
Section titled “Follow-on work this ADR creates”- 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.mdlegacy 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.mdand 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 fordisplay.c(640x200 logical framebuffer→960x600 logical framebuffer). -
docs/writing/CLAUDE.md— mirror the same Canonical Hardware Specification updates + thedisplay.ctree 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 any640×200or640x200references 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 the640×200language 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 thatKN86_FRAMEBUFFER_WIDTH/_HEIGHTchange 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.h—KN86_FRAMEBUFFER_WIDTH640→960,KN86_FRAMEBUFFER_HEIGHT200→600. AddKN86_CELL_WIDTH 12andKN86_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.c—PROFILE_PROTOTYPEandPROFILE_DESKTOP_EMUentries:logical_w640→960,logical_h200→600,viewport_xcomputes to 32,viewport_ycomputes to 0. Comment prose updated. TheCHROME_VIEWPORT_Y,CHROME_SOFT_KEYS_Y,CHROME_VIEWPORT_H,CHROME_STATUS_H,CHROME_SOFT_KEYS_Hdefines 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 usesKN86_CELL_WIDTH/_HEIGHTfor 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 fordisplay.c(640x200 logical framebuffer→960x600 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 rootdocs/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.
Narrative (for the design history)
Section titled “Narrative (for the design history)”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.