Skip to content

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.

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);
  • Non-blocking. send returns BUFFER_FULL; recv returns 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_MAX bytes (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 role for 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.

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 sets peer_closed = true on the context that points back at a’s tx ring.
  • Subsequent send on a returns PEER_LOST.
  • Subsequent recv on b drains remaining queued messages first, then returns PEER_LOST once 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):

#TestVerifies
1open_returns_connectedpair() → both sides connected
2pair_both_connectedboth contexts non-NULL + connected
3send_recv_roundtripA sends, B recvs, bytes match
4recv_empty_returns_ok_zeroempty ring → OK, len=0
5peer_closed_on_sendsend after peer close → PEER_LOST
6peer_closed_on_recv_drainedrecv after peer close + empty → PEER_LOST
7send_buffer_fullfill queue to depth → BUFFER_FULL
8multi_message_order4 messages arrive in FIFO order
9open_returns_not_connectedunlinked ctx → not connected
10send_invalid_argsNULL/zero/oversized → INVALID_ARG
11recv_invalid_argsNULL/zero-cap/NULL-out → INVALID_ARG
12close_null_noopclose(NULL) does not crash
13close_sets_not_connectedafter close, peer sees not connected
14recv_surviving_msgs_then_peer_lostqueued msgs before close delivered; then PEER_LOST

Run: cd kn86-emulator/build && ctest -R test_platform_link

Terminal window
./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.

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.

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-recv builtins) — 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.
  • 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 declaration
  • kn86-emulator/src/platform_link_emu.c — emulator stub
  • kn86-emulator/tests/test_platform_link.c — unit tests
  • GWP-254 — Notion task tracking this deliverable