Sprint 4 Design Pack — GWP-263
Story narrative
Section titled “Story narrative”ADR-0014 §F1 explicitly creates this follow-on. The current rendering pipeline uses 8×8 Press Start 2P scaled 1× horizontal / 2× vertical with 2/4 padding centered in the 12×24 cell — readable but stretched-vertical. Native 12×24 cuts the artifact: each glyph is designed-and-drawn at the cell’s true physical resolution, no scaling, optional descender refinement on Q J p q y g, optional refined box-drawing strokes for the CP437 set. Quality upgrade, not a behavior change; the 80×25 grid contract is unchanged.
This is the largest engineering task in the Wave 2 batch and deserves an honest scope conversation before dispatch. Three reasons:
- It’s not a code task; it’s a font-cutting task with code wrapping. A native 12×24 glyph atlas requires per-glyph art decisions — pixel-perfect drawing for 256 glyphs (Press Start 2P 96-printable + box drawing 128 + the KN-86 Code Page extras). That’s a few days of art work, not a few hours of code work. The code surface (atlas loader, build flag for A/B comparison, tests) is small; the art surface is the load-bearing piece.
- Cart authors have already adapted to the 8×8-scaled path. Every screen wireframe in
docs/software/cartridges/modules/*.md, every clip in the GWP-264 attract pipeline, every cart that’s been visually validated (NeonGrid grid layouts, Black Ledger tabular UIs) was designed against the 8×8-scaled rendering. Switching to native 12×24 changes glyph proportions slightly — wider strokes, tighter letterspacing, possibly different visual rhythm. Visual regression on every cart is on the table. Required: the A/B build flag (per task body’s AC #2) lets carts run against either renderer during the transition; the screenshot validation has to run against every existing cart, not just ICE Breaker. - The “or KN-86 custom” path adds even more art scope. Press Start 2P is an existing TTF; cutting at 12×24 from TTF gives one set of design decisions (slightly thicker strokes, slightly more detail at descenders). A custom KN-86 face authored from scratch is a 2-3 week design exercise (per-glyph hand-drawing in 12×24 monochrome, design-language consistency, kerning irrelevant for monospace but visual rhythm matters). Recommend: start with the Press Start 2P 12×24 cut from TTF (cleaner pipeline, deterministic output) and treat custom-face-authoring as a possible follow-on if Josh wants further refinement.
Recommended path: Press Start 2P TTF → ImageMagick or Python-Pillow script → 16×16 glyph atlas at 12×24 each (= 192 × 384 PNG) → C atlas loader + new font.c rendering path → A/B flag for old-vs-new comparison → cart-by-cart visual regression sweep → flip default to native after Josh signs off.
Player-facing semantics with worked example
Section titled “Player-facing semantics with worked example”What changes from the operator’s perspective:
The 80×25 grid is unchanged. Cell positions are unchanged. The amber-on-black palette is unchanged. What changes is the shape of each character within its cell. Today’s A is rendered as the 8×8 Press Start 2P bitmap stretched 1×2 vertically to 8×16, centered in 12×24 with 2 px horizontal padding and 4 px vertical padding — so the visible glyph is 8 px wide × 16 px tall, with a 2 px horizontal gap on each side. After GWP-263: the A is a native 12×24 glyph drawn to fill the cell more completely (recommended target: 10 px wide × 20 px tall visible glyph, with 1 px padding all sides — but the per-glyph dimensions are art decisions, not strict targets).
Worked example: Press Start 2P ‘A’ before-and-after.
8×8 source bitmap (today, scaled 1×2 to 8×16 in cell):
.XXXXXX. .XXXXXX.X......X X......XX......X → X......X (× 2 vertical)X......X X......XXXXXXXXX XXXXXXXXX......X X......XX......X X......XX......X X......X X......X X......X X......X X......X X......X X......X X......X X......XVisible: 8w × 16h glyph in 12w × 24h cell, with 2 px L/R padding and 4 px T/B padding. Vertical strokes are 1 px wide; horizontal strokes appear to be 2 px wide because of the 2× vertical scale. Asymmetry is the artifact — the “X” character looks taller-than-square, every horizontal bar reads chunky vs every vertical stroke.
12×24 native (after, no scaling):
..XXXXXXXXXX......XXXXXXXXXX.....X..........X....X..........X....X..........X... (intermediate strokes preserved).X..........X...XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX............X..X............X..X............X..X............X..X............X..X............X..X............X..X............X..X............X..X............X..X............X..X............X..Visible: ~10w × 20h glyph in 12w × 24h cell, ~1 px padding all sides. Strokes are uniform 1-2 px regardless of orientation. The character reads more even.
The visual diff is subtle but pervasive — every cart screen’s “feel” changes slightly. The A/B flag is the load-bearing risk-management tool.
Acceptance criteria expanded (≥6 testable items with file paths)
Section titled “Acceptance criteria expanded (≥6 testable items with file paths)”- Source 12×24 font atlas delivered as PNG under
kn86-emulator/assets/fonts/kn86_12x24.png. Layout: 16-col × 16-row at 12×24 each = 192 × 384 px image. KN-86 Code Page glyph order (perdocs/software/api-reference/grammars/character-set.md): rows 0-7 = Press Start 2P printable ASCII (32-127), rows 8-15 = CP437 extended set (128-255). Amber pixels =#E6A020, transparent or black background — atlas loader treats non-background pixels as glyph-on. - Atlas generation script under
kn86-emulator/tools/generate_font_atlas.py(or similar). Input: Press Start 2P TTF + glyph map. Output: the PNG above + afont_glyph_metrics.hC header with per-glyph bounding-box info (for cursor-positioning math). Reproducible — running the script twice with same TTF + glyph map produces byte-identical PNG. kn86-emulator/src/font.cgainsfont_load_12x24(const char *atlas_path)path that consumes the atlas. Old 8×8-scaled path retained behind a build flag (KN86_FONT_LEGACY_8X8, default OFF). When the flag is OFF, the new path is the default; when ON, the legacy path is the default. CMake exposes the flag.- All 256 glyphs of the KN-86 Code Page included — Layer 1 per
character-set.md. The Layer 2 ~2,000-glyph Unicode subset is out of scope for this task (filed as follow-on if Josh wants). kn86-emulator/tests/test_font_12x24.ccovers:- Glyph at known cell renders the expected pixel pattern (compare a glyph render to a reference PNG saved under
tests/fixtures/). - Bounding-box metrics in
font_glyph_metrics.hmatch the actual atlas (no drift between art and metadata). - Padding correct: glyph centered in cell with the expected per-glyph padding.
- Build flag correctly switches between paths: with
KN86_FONT_LEGACY_8X8=ON, render output matches the existing 8×8-scaled output; with=OFF, render output matches the new 12×24 atlas.
- Glyph at known cell renders the expected pixel pattern (compare a glyph render to a reference PNG saved under
- A/B visual regression sweep: for each launch cart (icebreaker, neongrid, blackledger, depthcharge), render the cart’s home screen + 1-2 representative gameplay screens with both font paths and capture as PNGs. Side-by-side comparison committed under
kn86-emulator/tests/fixtures/font_ab/. Manual review by Josh; sign-off committed in PR description. - CLAUDE.md Canonical Hardware Specification “Font” row updated to cite native 12×24 cut as the v0.1 default per ADR-0014 F1. The “Glyph rendering (v0.1)” prose in the “Font cell” row gets a new sentence explaining the cut. ADR-0014’s F1 follow-on box ticked in its Documentation Updates section.
- Performance check: the new render path is no slower than the old at 30 fps (the animation cap). Measured on the dev machine via the existing
bench_*harness or a newbench_font_render.c. If the new path is slower (it shouldn’t be — both are bitmap reads, just from different source dimensions), document the perf delta.
Cross-references (cart specs that consume + ADRs that constrain)
Section titled “Cross-references (cart specs that consume + ADRs that constrain)”- ADR-0014 (display-profile-redesign) — F1 follow-on; the load-bearing decision. Cites this task explicitly.
docs/software/api-reference/grammars/character-set.md— Layer 1 (256-glyph KN-86 Code Page) is the spec source for which glyphs to include. Layer 2 (~2,000-glyph Unicode) explicitly out of scope.kn86-emulator/src/types.h—KN86_CELL_WIDTH = 12,KN86_CELL_HEIGHT = 24,KN86_FONT_WIDTH = 8,KN86_FONT_HEIGHT = 8. The font-source constants (KN86_FONT_WIDTH/HEIGHT) become misnamed when the source is 12×24. Recommendation: leaveKN86_FONT_WIDTH = 8for the legacy path; introduceKN86_FONT_NATIVE_WIDTH = 12/KN86_FONT_NATIVE_HEIGHT = 24for the new path. Update header comments to cite ADR-0014 F1 + GWP-263.kn86-emulator/src/display.c— text rendering loop. Should not require changes (cell dimensions unchanged); only the glyph-source dimensions change. Verify during implementation.- All cart design bibles in
docs/software/cartridges/modules/— visual consumers. The A/B sweep validates none of them broke. - GWP-264 (attract clip pipeline) — clips are pre-rendered terminal frames; if clips were captured against the 8×8-scaled output, they remain valid (clips are pixel arrays, not glyph references). Confirm during implementation that clips don’t need re-capture.
Edge cases (≥3)
Section titled “Edge cases (≥3)”- Press Start 2P TTF licensing. Press Start 2P is licensed SIL Open Font License. Re-cutting it at 12×24 produces a derivative that’s also OFL — fine for an open project, fine for the desktop emulator. For the device build, the OFL allows redistribution; confirm the OFL notice ships with the system image (probably already does). No license-blocker, but worth a 5-minute check before art work begins.
- Box-drawing glyphs (CP437 128-255) at 12×24. The CP437 set has heavy reliance on edge-aligned strokes (
─,│,┌,└, etc.) that need to align cell-to-cell to draw clean borders. At 8×8 these are simple — strokes are 1 px and fall on cell-grid lines naturally. At 12×24, designer must decide stroke weight (1 px? 2 px?) and edge alignment (left-aligned? centered?). Wrong choice and Black Ledger’s bordered tables look broken. Test specifically that a 5-cell-wide horizontal box border draws as a continuous line, not as 5 disconnected strokes. - Glyph rendering in BITMAP mode (960×600). Cartridges in BITMAP mode draw raw pixels, not glyphs. The font-render path is only invoked in TEXT and SPLIT modes. Verify during implementation that BITMAP mode is unaffected — no regression in
gfx-*primitive output. - Asymmetric padding tolerance (per the wave-1 GWP-246 design pack’s open question). If the cart-author API ever adopts a “max 1-char asymmetry” tolerance for centered text, the cell-padding semantics shift slightly. Not a blocker — flag for the engineer to consider when implementing the cell-render math.
- Descender behavior on Q J p q y g. These glyphs in Press Start 2P at 8×8 have no real descender (they fit within the 8-pixel cell). At 12×24 native there’s room for actual descenders below the baseline. Decision: cut with descenders (more authentic Press Start 2P revival look) or without (matches existing 8×8-scaled behavior; less visual drift). Recommendation: cut with subtle 2-pixel descenders for the lowercase letters; cap-height letters (Q, J) stay within main glyph body. Discuss with Josh before art work commits.
- A/B sweep timing. Each cart’s home + 2 screens × 4 carts = 12 screenshots × 2 font paths = 24 captures. ImageMagick or a headless SDL capture mode handles this in seconds, but manual review is the time sink. Allow ~1 hour for Josh’s review pass.
Engineering hand-off notes
Section titled “Engineering hand-off notes”- Honest scope estimate: 1.5 to 3 days depending on art-decision iteration loops. Breakdown:
- Atlas-generation script + Press Start 2P 12×24 cut: 0.5–1 day (depends on whether Josh wants descender refinement / box-drawing stroke decisions on the first pass or iterates).
- C atlas loader + new font.c path: 0.25 day (mechanical).
- Build flag + CMake plumbing: 0.25 day.
- Tests (atlas pixel comparison, metrics validation, build-flag switching): 0.5 day.
- A/B sweep capture + Josh review pass: 0.5 day.
- CLAUDE.md + ADR-0014 doc updates + cross-reference sweep: 0.25 day.
- Total: 2.25 days nominal. With one art-decision iteration cycle (Josh wants box-drawing strokes thicker, etc.), 3 days. With multiple iterations or descender redesign, 4-5 days. The art surface is the variance.
- Files owned:
kn86-emulator/src/font.c(additive — new render path),kn86-emulator/assets/fonts/kn86_12x24.png(new),kn86-emulator/tools/generate_font_atlas.py(new). - Files added-to:
kn86-emulator/tests/test_font_12x24.c(new),kn86-emulator/CMakeLists.txt(build flag + new source files),kn86-emulator/src/types.h(new constants for native font dims),CLAUDE.md(Font row hygiene update),docs/adr/ADR-0014-display-profile-redesign.md(F1 box ticked). - Files NOT touched:
kn86-emulator/src/display.c(cell size already 12×24; per task constraint). - Test strategy: TDD where possible (unit tests for atlas loader, metrics validation, build-flag switching). The A/B visual regression is inherently manual — the test fixtures get captured + reviewed by Josh; no automated visual diff (yet — that’s a possible follow-on perceptual-hash test). PR description carries the A/B comparison images.
- Dispatch shape: single C engineer with art-direction consultation from Josh at two checkpoints — (a) after the first 12×24 cut is produced (review glyph quality before atlas integration); (b) after A/B sweep is captured (sign-off before flipping default). Independent of every other Sprint 4 candidate; can run in parallel.
- Scope guardrail: DO NOT start designing a custom KN-86 face from scratch. The TTF cut is the v0.1 path. A custom face is a future task (“KN-86 Custom Display Face” — 2-3 weeks design + author + ship), not this sprint.
- Watch for: the box-drawing alignment problem (edge case #2). Black Ledger’s tabular UI is the worst-case visual regression target; capture its bordered tables in the A/B sweep.
Open questions for Josh
Section titled “Open questions for Josh”- Press Start 2P 12×24 (TTF cut) or KN-86 custom face? Recommendation: TTF cut for v0.1, custom face as later refinement task. Confirm or defer.
- Descender behavior (Q J p q y g). Subtle 2-pixel descenders on lowercase (recommended) or stay above-baseline like the 8×8 source? Discuss before art work commits.
- Box-drawing stroke weight. 1 px (minimal, matches 8×8 baseline at scale) or 2 px (heavier, more legible at viewing distance)? Black Ledger’s tables are the worst-case test — recommend trying both and picking via A/B comparison.
- A/B sweep granularity. Home screen + 1 representative gameplay screen per cart (4 carts × 2 = 8 captures) or wider (multiple gameplay screens, debug overlay, attract clips)? Recommendation: home + 1 gameplay × 4 carts as minimum; expand if early review flags concerns.
- Build-flag default after merge. Flip to native 12×24 default in this PR, or land the renderer with legacy as default and flip in a follow-on after wider playtesting? Recommendation: flip to native default in this PR if the A/B sweep passes Josh’s review; otherwise keep legacy default and file a follow-on flip-task. Risk-tolerance call.