Sprint 4 Design Pack — GWP-256
Story narrative — Gameplay Designer take
Section titled “Story narrative — Gameplay Designer take”The publisher splash is a designed gameplay mechanic, not boot-time UX bloat.
The publisher layer in docs/software/runtime/orchestration.md §“Software Publishers & Loading Screens” (L468–676) is one of the deck’s most carefully crafted fiction surfaces. Six licensed publishers + one outside-tier pirate (Setec Astronomy) each have a distinctive 2–3 second loading-screen ritual that is the operator’s first contact with whoever made this software. Edgeware’s character-by-character wordmark assembly with soft keyclicks signals “first-party, polished, safe.” Zaibatsu’s hex-dump-into-glyph-block-mark with white noise signals “underground, possibly criminal, brilliant.” Bureau 9’s silent typewriter classification header signals “shadow government — the room feels colder.” These are diegetic moments. They build the world.
When the publisher splash is missing — when the cart loader skips header parse straight to mission board — operators don’t get to know who made the software they’re using. That sounds tonally minor, but in the capability-model architecture (where carts are the load-bearing collectibility/ritual artifact) it’s a meaningful absence. The publisher beat is the bridge between the physical object (the SD-card-sled clamshell with its label artwork per ADR-0019) and the digital experience (the mission board that follows). Without the splash, the cart load is a black box; with it, the cart load is a brief, characterful theater.
What does the splash do for the operator?
- Signals identity. “This is Edgeware. I trust this.” vs “This is Zaibatsu. I’m in for it.” Tonal preparation for the cart’s own aesthetic.
- Provides ritual punctuation between cart insertion and gameplay. The physical act of opening the case and slotting the SD-sled deserves a small theatrical response. Without one, the deck feels like a file browser.
- Carries metadata invisibly. Publisher (= which slot of trust the operator is mounting), module class header (Bureau 9’s
CLASS: FORENSIC ACCOUNTINGline), version (v1.3etc) — all conveyed in a 2-3 second beat that doesn’t feel like a UI dialog. - Sets the audio room tone. The PSG sting establishes channel ownership and amplitude reference for the SFX cues that follow. After Bureau 9’s silence, a single keyclick is louder; after Zaibatsu’s noise burst, normal SFX feels muted. This is intentional cross-cart sonic variation.
- Earns attention for the “no-publisher-splash” outside cart variant. Setec Astronomy’s UNLICENSED disclaimer + anagram-rearrangement is meaningful because every other cart performs the publisher-trust ritual; the pirate cart’s deviation reads as transgression. If no cart ships a splash, Setec Astronomy has nothing to deviate from and the joke dies.
Why exist as a hook (not a hard-coded sequence)?
The publishers are fiction-defined; the splashes are art-direction-defined; both should be authored cart-side, not runtime-side. The runtime owns the contract (3-second budget, 30 fps, exclusive PSG access, time-out clamp, SYS skip), not the content. A future cart from a publisher we haven’t designed yet (Phase 2 modules; community carts; sister-deck carts) authors its own splash without touching nOSh runtime code.
Player-facing semantics with worked example
Section titled “Player-facing semantics with worked example”The contract (what the cart sees):
A cart’s publisher-splash lambda is invoked once per frame for up to 90 frames (3 seconds at 30 fps). Each invocation receives the current frame number (0..89). The lambda returns:
nilto continue the splash (more frames to draw)- non-
nilto signal “splash complete; advance to mission board immediately”
The runtime grants the splash:
- Exclusive write access to the main 80×25 grid (the
text-*andgfx-*primitives) - Exclusive write access to the PSG (the
psg-*andsound-*primitives) - Read access to deck state (operator handle, credits — for splashes that personalize)
- NO access to CIPHER-LINE (CIPHER is OLED-exclusive per Spec Hygiene Rule 6; CIPHER-LINE continues to render its own ambient ticker independent of the splash)
- NO access to mission-context primitives (no mission is active during splash)
The runtime enforces:
- 3-second hard ceiling. If the lambda hasn’t returned non-
nilby frame 90, the runtime cuts to mission board regardless. No misbehaved cart can hold the device. - SYS-skip. If the operator presses SYS at any frame, the splash terminates immediately. (Per industry convention; impatient-operator affordance.)
- Pre-mission-board ordering. Splash runs after
cartridge_load_v2()completes header parse and FFI binding, before mission board generation. The cart’s PUBLISHER_SPLASH lambda sees a fully-loaded NoshAPI; it does not see registered cell types, mission templates, or phase handlers (those are mission-context concerns).
Worked example: Edgeware splash for ICE Breaker (NeonGrid, actually — ICE Breaker is Zaibatsu, but let’s use Edgeware as the cleanest example since they publish NeonGrid).
(publisher-splash (lambda (frame) (cond ;; Frame 0: clear screen ((= frame 0) (text-clear) nil) ;; Frames 1-26: assemble "EDGEWARE SYSTEMS CO., LTD." character by character ;; (one char every 2 frames, 13 chars in "EDGEWARE SYSTEMS" + spaces, 26 frames total) ((<= frame 26) (let ((char-idx (/ frame 2)) (msg "EDGEWARE SYSTEMS CO., LTD.")) (when (= (% frame 2) 0) (text-putc (+ 27 char-idx) 12 (string-ref msg char-idx)) (sfx-keyclick))) nil) ;; Frame 27: render the subline ((= frame 27) (text-puts 25 14 "A KINOSHITA ELECTRONICS COMPANY") nil) ;; Frames 28-30: clean tone via Channel A ((= frame 28) (sound-tone 0 440 12) nil) ((<= frame 60) nil) ; let the tone ring out ;; Frame 60+: done (else t))))Total runtime: ~2 seconds. Returns t at frame 60 to signal complete; runtime advances to mission board. Edgeware’s signature: restrained, corporate, exactly what you’d expect from first-party software.
The Bureau 9 splash as the hardest case — it’s the silent one (no PSG output at all). Validates that a cart can ship an audio-quiet splash; the runtime must not assume the splash uses sound. Bureau 9’s PUBLISHER_SPLASH is mostly text-puts calls at calibrated frame intervals with sfx-silence (or no PSG calls at all) — and sound-silence should be honored across the splash.
Acceptance criteria expanded (≥6 testable items)
Section titled “Acceptance criteria expanded (≥6 testable items)”kn86-emulator/src/cartridge.cgainspublisher_splash_run(SystemState *state, CartHandle *handle)— additive entry point called aftercartridge_load_v2()header parse and FFI binding, before mission template registration. Returnsvoid; the function blocks until splash completes or times out, then returns to caller.kn86-emulator/src/nosh_lisp_bridge.cregisters apublisher-splashLisp primitive that the cart-side(publisher-splash <lambda>)form binds to. The primitive stores the lambda in a per-cart slot (CartHandle.publisher_splash_fnor equivalent);publisher_splash_runinvokes it per-frame.- Per-frame loop invokes the lambda at 30 fps (33.33 ms per frame nominal; SDL frame pacing). Frame counter passed as integer arg. Lambda returns Fe
nil→ continue; Fe truthy → terminate splash early. - 3-second hard ceiling enforced. If lambda hasn’t terminated by frame 90, runtime breaks the loop and returns regardless. Test: a misbehaved cart that always returns
nildoes not hang the deck — the function returns within 3 seconds + epsilon. - SYS-key skip. During the splash loop, the runtime polls input. If SYS is pressed at any frame (using the existing input-dispatch surface), splash terminates immediately. Test: SDL synthetic SYS keypress at frame 30 → function returns by frame 31.
- Exclusive PSG access during splash. Runtime suppresses any other audio source (CIPHER-LINE OLED is unaffected; primary 80×25 grid is owned by the cart’s lambda). Test: an attempt to play a non-cart-initiated SFX during splash is no-op.
- CIPHER-LINE remains independent. OLED-side rendering loop continues normally during splash; if CIPHER has scheduled fragments, they tick. Test: schedule a CIPHER fragment, run a splash, verify the fragment renders on CIPHER-LINE per its normal cadence (not gated on splash completion).
- Sample cart authored. One launch cart (recommend
icebreaker.lsp— it’s already the hero cart) gets a PUBLISHER_SPLASH lambda matching the Zaibatsu Digital splash spec from orchestration.md L519–532 (hex-dump-into-glyph-mark with noise burst). Per the cartridge author UX, this is end-to-end authoring proof that the contract works. tests/test_publisher_splash.ccovers:- Full 90-frame run (lambda returns
nilfor 90 frames; runtime cuts at 90). - Early abort at frame 30 (lambda returns
t). - Time-out clamp (misbehaved lambda; runtime cuts at 90 regardless).
- SYS-key skip mid-splash.
- PSG isolation (non-cart audio attempts during splash are no-op).
- CIPHER-LINE pass-through (OLED render continues during splash).
- Lambda receives correct frame counter (0..89 inclusive).
- Full 90-frame run (lambda returns
- Cart-load latency budget unchanged at 3s splash + ~200 ms template parse. Document in the cart-load timing notes if any.
Cross-references (cart specs that consume + ADRs that constrain)
Section titled “Cross-references (cart specs that consume + ADRs that constrain)”docs/software/runtime/orchestration.md§“Software Publishers & Loading Screens” (L468–676) — design source. The 6 licensed publishers + Setec Astronomy each have a fully-specified splash; this implementation is the runtime that lets those specs ship.docs/software/runtime/orchestration.md§“Technical Implementation” (L668–680) — has a stub C macro example that GWP-261’s doc sweep replaces with the Lisp form. Coordinate with GWP-261 — both PRs touch related surface; landing order matters (GWP-256 ships the runtime + Lisp form, GWP-261 documents it).- ADR-0005 (FFI surface) —
publisher-splashis a new primitive; should it be added to the Tier 1 (all-carts) primitives table? Recommend yes, as a special case under a new “Lifecycle hooks” subsection. ADR amendment may be in scope for this PR. - ADR-0001 (embedded-lisp-scripting-layer) — confirms cart authoring is Lisp; the splash form authors here.
- ADR-0017 (coprocessor) — PSG calls during splash route through the coprocessor on the prototype; emulator-internal on the desktop. The contract is identical from the cart’s perspective. Cross-reference for the implementing engineer.
- ADR-0019 (cartridge storage) — splash runs after the SD-mount and
cartridge_load_v2; doesn’t see the SD path. No interaction. - All 14+1 cart design bibles under
docs/software/cartridges/modules/— each will eventually need a PUBLISHER_SPLASH lambda (follow-on tasks per cart). This task ships the contract + one sample; the rest get filed asGWP-256-followup-icebreakeretc.
Edge cases (≥3)
Section titled “Edge cases (≥3)”- Cart with no PUBLISHER_SPLASH form.
cartridge_load_v2finds no lambda registered. Runtime should skip the splash entirely (no frame loop, no time consumed) —publisher_splash_runreturns immediately. Pre-Phase-2 community carts may not author a splash; the deck must handle them gracefully. Default fallback: show a 1-frame “GENERIC PUBLISHER” placeholder text (UNKNOWN PUBLISHERcentered) for ~500 ms with no audio? Or skip entirely? Recommendation: skip entirely. The fiction is “carts have publishers”; absence is a community-cart signal that should be visible by not having the ritual, not by a runtime-faked stub. - Cart’s lambda errors out (Fe panic, division by zero, nil-arg). Runtime should terminate the splash loop, log to debug overlay, and continue to mission board. Don’t crash the deck on a malformed splash. Test: a deliberately-broken splash lambda → splash terminates with logged error → mission board appears.
- Splash blocks beyond 33ms in a single frame. A heavy lambda (e.g., a Zaibatsu-style hex dump that paints all 80×25 cells per frame) might exceed the 33ms frame budget. The 3-second wall clock should be enforced regardless of frame count — i.e., if 3 seconds elapse in 60 frames because each frame took 50ms, runtime still cuts at 3s wall. Time-budget enforcement should be wall-clock based, not frame-count based. (Or both — whichever fires first.)
- Operator inserts a cart while CIPHER is mid-fragment. CIPHER-LINE doesn’t pause; the OLED ticker continues. The splash on the main grid runs in parallel. This is the cockpit-voice-recorder effect described in
cipher-voice.md— verify the fragment doesn’t get truncated or restarted. - Splash for the Null cart (the diagnostic / Cipher-escape cart). Per Spec Hygiene Rule 6, Null is the only cart sanctioned to render CIPHER on the main grid. Its splash could render CIPHER glyphs on the main 80×25 — but that’s not “publisher splash” semantics; that’s Null’s gameplay surface. Recommend: Null’s splash is conventional (Edgeware-style — Edgeware is Null’s publisher per orchestration.md L489); the CIPHER-escape mechanic is a runtime mode separate from splash. Document this in Null’s design bible.
Engineering hand-off notes
Section titled “Engineering hand-off notes”- Files owned:
kn86-emulator/src/cartridge.c(additive —publisher_splash_runhelper).kn86-emulator/src/nosh_lisp_bridge.c(additive —publisher-splashprimitive registration). - Files added-to:
kn86-emulator/tests/test_publisher_splash.c(new).carts/icebreaker.lsp(additive — sample lambda for Zaibatsu splash). Possiblydocs/adr/ADR-0005-ffi-surface.mdAmendment Log if Lifecycle hooks subsection added. - Files NOT touched:
display.c,font.c,psg.c(per task constraint — splash is a cart-driven sequence using existing primitives, not a new render path).oled.c(CIPHER-LINE remains independent).main.cevent loop (splash is a blocking helper inside cart-load, not an event-loop mode). - Expected PR size: ~120 lines
cartridge.c(publisher_splash_run + per-frame loop + timing + skip handling), ~40 linesnosh_lisp_bridge.c(primitive registration), ~80 lines Zaibatsu sample lambda inicebreaker.lsp, ~200 lines tests. Single C engineer + Gameplay Design consult, ~1 day with TDD. - Test strategy: TDD. Author the 7 test cases first (frame counter, full run, early abort, time-out clamp, SYS skip, PSG isolation, CIPHER pass-through). Implement to pass. The Zaibatsu sample lambda is the end-to-end proof; review by playing it on the desktop emulator.
- Dispatch shape: single C engineer + Gameplay Design consult on the Zaibatsu sample’s frame-by-frame faithfulness to the orchestration.md spec. Dependent on GWP-242 (gfx-* primitives) and GWP-243 (sound-* primitives) per the task’s Dependencies — both shipped per ADR-0005’s 2026-04-25 amendment, so dependency is clear.
- Watch for: the SYS-key skip is the trickiest part — the splash loop has to poll input without re-entering the normal event-dispatch path. Recommend: a stripped-down input poll that only watches for SYS, ignores everything else, doesn’t update the input ring or recency counters. The Tier 1 input primitives are not for the splash; this is a meta-concern.
Open questions for Josh
Section titled “Open questions for Josh”- Sample-cart choice. Zaibatsu splash for ICE Breaker (per the worked example above) or a simpler Edgeware splash for a less-load-bearing cart? Recommendation: Zaibatsu for ICE Breaker — it exercises the full PSG + animation envelope and validates the contract under stress. Confirm.
- No-splash-cart behavior. Skip entirely (recommended) vs show a
UNKNOWN PUBLISHERplaceholder vs configurable per-cart-flag. Recommendation: skip entirely (preserves the “absence is meaningful” framing). Confirm. publisher-splashLisp primitive in ADR-0005? Recommend amending ADR-0005 with a new “Lifecycle hooks” subsection covering this primitive + future hooks (mission-end debrief, cart-eject cleanup, etc.). Confirm or defer to a separate ADR amendment task.- Splash-skip key: SYS (per task body) vs ENT vs any-key. Industry convention varies. Recommendation: SYS, because the operator’s right index finger naturally rests there and SYS is the conventional “system-control” key on this layout. Confirm.
- Time-budget enforcement: frame-count, wall-clock, or both? Recommendation: both — whichever fires first. Defends against both runaway lambdas (slow per-frame) and pathological 90-frame loops.