platformlink* — Two-Deck Linked Play HAL
Two KN-86 operators link their decks so cartridges can run cooperative or
competitive sessions across both devices. platform_link_* is the deck-side
primitive: open a link, send/recv framed messages, close.
HAL Surface
Section titled “HAL Surface”Declared in kn86-emulator/src/platform_link.h.
/* Error codes */typedef enum { PLATFORM_LINK_OK = 0, PLATFORM_LINK_NOT_OPEN = 1, PLATFORM_LINK_PEER_LOST = 2, PLATFORM_LINK_TIMEOUT = 3, PLATFORM_LINK_BUFFER_FULL = 4, PLATFORM_LINK_INVALID_ARG = 5,} platform_link_err_t;
typedef struct platform_link_ctx platform_link_ctx_t; /* opaque */
/* Open a link (role: 0=initiator, 1=joiner) */platform_link_err_t platform_link_open(int role, platform_link_ctx_t **ctx_out);
/* Send one framed message; non-blocking */platform_link_err_t platform_link_send(platform_link_ctx_t *ctx, const uint8_t *buf, size_t len);
/* Recv one framed message; returns OK + len=0 when nothing pending */platform_link_err_t platform_link_recv(platform_link_ctx_t *ctx, uint8_t *buf, size_t cap, size_t *len_out);
/* True when link is open and peer has not closed */bool platform_link_is_connected(platform_link_ctx_t *ctx);
/* Close the link; signals PEER_LOST to the other side */void platform_link_close(platform_link_ctx_t *ctx);
/* Test fixture: create two pre-linked loopback contexts */platform_link_err_t platform_link_pair(platform_link_ctx_t **a_out, platform_link_ctx_t **b_out);Design decisions
Section titled “Design decisions”- Non-blocking.
sendreturnsBUFFER_FULL;recvreturns OK +len=0. The caller polls; no blocking calls, no threads, no mutexes needed in the emulator. Real transports may add a blocking variant in a future API rev. - Framed messages. Each call transports exactly one message, bounded by
PLATFORM_LINK_MSG_MAXbytes (256). Callers never see the internal 2-byte length header. - Role is advisory. In the emulator stub both sides are symmetric.
Real transport implementations use
rolefor listen vs. dial semantics (e.g. USB-CDC: initiator listens on/dev/ttyACM0, joiner dials). - No malloc. Contexts are allocated from a static pool of 8 slots
(
PLATFORM_LINK_POOL_SIZE). Sufficient for 4 simultaneous test pairs. The pool size is intentionally small; real transports will have their own allocation strategy.
Emulator Stub Semantics
Section titled “Emulator Stub Semantics”Implemented in kn86-emulator/src/platform_link_emu.c.
The stub uses two in-process ring buffers (one per direction) to implement loopback. There is no socket, no thread, and no system call.
platform_link_pair(&a, &b) a->peer_rx = &b->tx b->peer_rx = &a->tx
platform_link_send(a, buf, len) ring_push(&b->tx, buf, len) ← lands in b's recv ring
platform_link_recv(b, buf, cap, &len) ring_pop(&b->tx, buf, cap, &len)Queue depth: PLATFORM_LINK_QUEUE_DEPTH = 8 messages per direction.
send returns BUFFER_FULL when the ring is saturated.
Peer-lost detection:
- When
platform_link_close(a)is called, the stub walks the pool and setspeer_closed = trueon the context that points back ata’s tx ring. - Subsequent
sendonareturnsPEER_LOST. - Subsequent
recvonbdrains remaining queued messages first, then returnsPEER_LOSTonce the ring is empty.
Unlinked contexts (from platform_link_open, not platform_link_pair)
start with peer_rx = NULL. is_connected returns false; send returns
PEER_LOST. Real transports will populate peer_rx during handshake.
kn86-emulator/tests/test_platform_link.c — 14 cases, all pure-module
(no SDL, no Fe VM, no runtime):
| # | Test | Verifies |
|---|---|---|
| 1 | open_returns_connected | pair() → both sides connected |
| 2 | pair_both_connected | both contexts non-NULL + connected |
| 3 | send_recv_roundtrip | A sends, B recvs, bytes match |
| 4 | recv_empty_returns_ok_zero | empty ring → OK, len=0 |
| 5 | peer_closed_on_send | send after peer close → PEER_LOST |
| 6 | peer_closed_on_recv_drained | recv after peer close + empty → PEER_LOST |
| 7 | send_buffer_full | fill queue to depth → BUFFER_FULL |
| 8 | multi_message_order | 4 messages arrive in FIFO order |
| 9 | open_returns_not_connected | unlinked ctx → not connected |
| 10 | send_invalid_args | NULL/zero/oversized → INVALID_ARG |
| 11 | recv_invalid_args | NULL/zero-cap/NULL-out → INVALID_ARG |
| 12 | close_null_noop | close(NULL) does not crash |
| 13 | close_sets_not_connected | after close, peer sees not connected |
| 14 | recv_surviving_msgs_then_peer_lost | queued msgs before close delivered; then PEER_LOST |
Run: cd kn86-emulator/build && ctest -R test_platform_link
CLI Smoke Test
Section titled “CLI Smoke Test”./build/bin/kn86emu --link-stub# link-stub: OK — loopback round-trip verified (4 bytes)# exit 0--link-stub opens a loopback pair, sends the 4-byte probe KN86, receives
it on the other side, and exits 0 on success. No SDL window is opened. Useful
for CI and for verifying the HAL plumbing on a fresh emulator binary.
Physical Connectivity Context
Section titled “Physical Connectivity Context”On the KN-86 prototype, decks communicate over a spare USB port from the internal USB hub IC (ADR-0018 / ADR-0019). The Pi Zero 2 W exposes one physical USB-A OTG port via the hub; a USB-C link cable between two decks provides the physical channel. The Pico 2 coprocessor (ADR-0017) does not participate in deck-to-deck link — it owns only PSG synthesis and the CIPHER-LINE OLED driver.
Future Work
Section titled “Future Work”The following are explicitly out of scope for GWP-254 and will require separate ADRs and tasks:
- Real USB-CDC transport (
platform_link_usb.c) — hardware-gated; requires bring-up of the USB hub IC and a pair of prototype decks. - Wi-Fi / UDP transport (
platform_link_wifi.c) — optional; useful for development without a cable; requires wpa_supplicant config. - Lisp FFI bindings (
link-open,link-send,link-recvbuiltins) — wait until a cart actually needs deck-to-deck play; premature binding before the HAL stabilizes risks API churn. - Cart-side linked-play UX — Gameplay Designer territory once the HAL and bindings are stable.
- Multi-deck topologies (> 2 players) — requires a relay or broadcast API extension; the current HAL is strictly one matched pair.
References
Section titled “References”- ADR-0017 — Pico 2 coprocessor; USB connectivity context
- ADR-0018 — Internal USB hub IC; keyboard controller wiring
- ADR-0019 — USB mass-storage cartridge interface; free USB port context
kn86-emulator/src/platform_link.h— HAL declarationkn86-emulator/src/platform_link_emu.c— emulator stubkn86-emulator/tests/test_platform_link.c— unit tests- GWP-254 — Notion task tracking this deliverable