Skip to content

Sprint 3 Design Pack — GWP-244 — split-view / display-mode FFI bindings

Notion task: GWP-244
Recovered: 2026-04-26 (lost to worktree contention; restored from Notion block children API)


Bind the two display-mode primitives (split-view, display-mode) that complete ADR-0005’s display API. nosh_split exists in nosh.c; display-mode is synthesized from internal state.

ADR-0005 §“Display mode control” specifies cartridges may switch between :text, :graphics, :split modes. Without split-view, no cart can compose a Pattern 5 detail inspector with a bitmap header (NeonGrid map preview, Depthcharge sonar overlay).

References: docs/adr/ADR-0005-ffi-surface.md §“Display mode control”, kn86-emulator/src/nosh.c L70 (nosh_split).

  • split-view (bitmap-rows) and display-mode () return symbol bound to Lisp
  • display-mode returns ‘text, ‘graphics, or ‘split per current state
  • split-view accepts 0..600 (clamps inclusive), raises :out-of-range otherwise
  • Test test_nosh_lisp_display_mode validates state transitions and read-back
  • Run regression vs. existing display tests to confirm no break
  • Owns: src/nosh_lisp_bridge.c (additive).
  • Touches additively: tests/test_nosh_lisp_display_mode.c (new), CMakeLists.txt.
  • Must NOT modify: display.c, display_profile.c, oled.c.
  • TDD; arena-allocation discipline (no malloc).

None.


Authored by PM/BA + Gameplay Designer. Full pack on disk at docs/sprints/2026-04-26-sprint3-gwp-244-design.md. This appendix is the structured summary; the on-disk file is load-bearing.

ADR-0005 §Display mode control defines two Tier 1 (all-carts) primitives — (split-view bitmap-rows) and (display-mode) — that gate every cart wanting a bitmap region above text content (Drift radar, Synth-Fence chart, Marty Glitch visual brief, Null diagnostic, Threshold cues). The C side is already done: nosh_split(uint16_t) at nosh.c:70, display_set_mode() at display.c:196, DisplayMode enum at types.h:175. The Lisp bindings are MISSING from nosh_lisp_bridge.c — bind(ctx, “split-view”, …) and bind(ctx, “display-mode”, …) are not present. This story closes the gap. Companion to GWP-242 (graphics, merged) and GWP-243 (sound, merged).

2. Gameplay Designer — Player-facing semantics

Section titled “2. Gameplay Designer — Player-facing semantics”

Cart Lisp call site example (Drift radar): (split-view 384) → (gfx-clear) → (draw-radar-sweep angle) → (text-puts 0 16 “BEARING 247 RANGE 8.2NM CLEAN”). State query example: (if (eq? (display-mode) ’:split) (skip-bitmap-region-draw) (full-grid-redraw)).

Rendered effect on the 80x25 grid with (split-view 384)

Section titled “Rendered effect on the 80x25 grid with (split-view 384)”
+--------------------------------------------------------------------------------+
| | <- pixel rows 0-383
| [ RADAR SWEEP HERE ] | (16 cell rows
| | suppressed for
| | bitmap)
+--------------------------------------------------------------------------------+
|BEARING 247 RANGE 8.2NM CLEAN | <- text row 16
|DEADDROP IDENT: K-ECHO-7 | <- text row 17
| | <- ...
|> CAR drill CDR back EVAL act INFO inspect | <- row 24 (firmware action bar)
+--------------------------------------------------------------------------------+

(split-view 0) returns to all-text. (split-view 600) is full-bitmap (rows 0/24 still drawn by firmware as edge overlays, occluding bitmap content there). (display-mode) returns the symbol :text, :bitmap, or :split.

Module consumers needing these bindings: drift.md (radar), synth-fence.md (commodity chart), null.md (diagnostic bitmap + register dump text), marty-glitch-visual-brief.md (visual brief is the cart’s premise), threshold.md (paradigm cues). Without these bindings, those carts CANNOT switch modes — runtime defaults to text at boot, gfx-* writes go to a framebuffer the display loop is not rendering.

  1. split-view binding added in kn86-emulator/src/nosh_lisp_bridge.c near gfx-* bind block. lisp_split_view() parses one integer arg (range 0–600 inclusive), calls nosh_split((uint16_t)bitmap_rows). Out-of-range returns Fe nil + stderr log per established silent-clamp pattern (ADR-0005 Amendment 2026-04-25).
  2. display-mode binding added likewise. lisp_display_mode() takes zero args, reads g_system_state->display_mode, returns Fe symbol :text / :bitmap / :split. Returns Fe nil if g_system_state is null (defensive; matches gfx-* pattern).
  3. Bind site updated — both bindings added in alphabetical order to existing display-API bind block (after gfx-rect at line 2066, before sfx-* at line 2068). New ordering: display-mode → gfx-blit → gfx-circle → gfx-clear → gfx-line → gfx-pixel → gfx-rect → split-view.
  4. Bridge symbol-interning — display-mode returns Fe symbols (not strings). Cache the three symbol references in static module-locals to avoid per-call interning.
  5. Tests at kn86-emulator/tests/test_nosh_lisp_display_mode.c (NEW, mirrors test_nosh_lisp_gfx.c). Cases: split-view 0 → :text, split_row=KN86_FRAMEBUFFER_HEIGHT; split-view 384 → :split, split_row=216; split-view 600 → split_row=0; split-view 601 → silent-clamp + log, mode unchanged; display-mode after each returns correct symbol; cold-init returns :text; cross-call sequence verifies state transitions.
  6. No FFI shape change — (split-view N) accepts one int arg; (display-mode) accepts zero. ADR-0005 contract preserved verbatim.
  7. Existing tests pass — test_nosh_lisp_gfx.c, test_nosh_lisp_text.c, test_display_render.c, test_display_profile.c stay green.
  8. CMakeLists.txt — add test_nosh_lisp_display_mode to the test target list (mirror GWP-242 pattern).
  • Negative integer to split-view: bridge clamps negatives to 0 (text mode) + stderr log (don’t silently wrap to 65535). Test asserts log line.
  • Non-integer arg to split-view (string/symbol): bridge returns Fe nil immediately + log [BRIDGE] split-view: expected integer, got . C entry never invoked.
  • display-mode called from REPL while cart is mid-frame (Tier 3 REPL-Read-Only allowed per ADR-0005 §Tier 3): single-byte read of g_system_state->display_mode, no side effects. Test verifies REPL read does not crash.
  • split-view called inside gfx-blit callback or sub-handler (re-entrancy): C-side nosh_split is not mutex-protected. v0.1 documents this as ‘safe within single-threaded render loop; do not call from audio callback or other thread.’ No runtime guard; matches all gfx-* primitives.
  • split-view 600 with Row 0/24 firmware bars: firmware always renders Row 0 (status) and Row 24 (action). Cart authors targeting full canvas should know rows 0–23 and 576–599 of pixel space are visually occluded by firmware bars. NOT a bug — it’s the rendering contract from CLAUDE.md Spec Hygiene Rule 5. Document in code comment + ADR-0005 amendment cross-link.
  • Grammar test stability: cart loads cipher-grammar block while in :split mode — CIPHER engine is OLED-bound (ADR-0015), unaffected by main-grid mode. Defensive sanity check in tests.

ADRs: ADR-0005 §Display mode control (the contract — split-view raises :out-of-range if > 600; display-mode returns :text/:bitmap/:split), ADR-0014 (960×600 logical framebuffer; KN86_FRAMEBUFFER_HEIGHT=600; split-view arg is in logical pixels), CLAUDE.md Spec Hygiene Rule 5 (Row 0 / Row 24 ownership boundaries).

Module specs: drift.md (radar), synth-fence.md (commodity chart), null.md (diagnostic bitmap), marty-glitch-visual-brief.md (visual brief), threshold.md (paradigm cues).

Adjacent precedent: GWP-242 (graphics-FFI bindings, merged 2026-04-25 — established silent-clamp + stderr-log pattern), GWP-243 (sound-FFI bindings, merged 2026-04-25 — same pattern).

Files to touch: nosh_lisp_bridge.c (add lisp_split_view ~25 LOC + lisp_display_mode ~30 LOC + 2 bind() calls); tests/test_nosh_lisp_display_mode.c (NEW, ~150 LOC mirrors test_nosh_lisp_gfx.c); CMakeLists.txt (one line). Files NOT to touch: nosh.c/nosh.h (nosh_split is correct), display.c/display.h (display_set_mode is correct), types.h (DisplayMode enum is correct).

Expected PR size: ~200 LOC total (~80 LOC bridge + ~150 LOC tests + 1 CMakeLists line). SMALL. Half-day of focused work including tests. Smallest of the three Sprint 3 stories.

Test strategy: TDD against the bridge. Write the test file first (fails at link time — no lisp_split_view); add the bridge functions; watch the seven test cases pass in order. Then run broader suite to verify no regression.

Scope guardrail (NOT in scope): any change to C-side nosh_split / display_set_mode signatures (stable and tested); adding split-view animation (smooth transitions); cart-side helper macros for ‘open 384px bitmap + 9-row text panel’ (Lisp stdlib concern, not binding); module-spec updates (separate stories per cart).

None — pure binding story. Contract is in ADR-0005, C side is already correct, bridge pattern is established by GWP-242/243.

Blocks: every cart needing a bitmap region (Drift, Synth-Fence, Null diagnostic bitmap, Marty Glitch visual brief, Threshold paradigm cues) cannot switch out of pure text without these bindings. Unblocks: visual half of all bitmap-using launch carts. Parallel-safe with GWP-259 (FSM) and GWP-260 (CIPHER engine) — entirely independent file region. Suggested wave: Sprint 3 Wave 1 — small, low-risk, same-day landable. Good warm-up task while FSM and CIPHER engine work spins up. Lowest priority only because the dependent carts aren’t shipping this sprint regardless; worth doing now since it’s tiny and unblocks several follow-up stories.