Skip to content

libuv

libuv is the cross-platform async-I/O library originally built for Node.js and now used by Luvit, Julia, uvloop, and others. It provides a single event loop with typed handles and requests over a unified backend (epoll on Linux, kqueue on BSD/macOS, IOCP on Windows, event ports on Solaris). README feature set:

  • Full-featured event loop (epoll / kqueue / IOCP / event ports)
  • Async TCP and UDP sockets
  • Async DNS resolution
  • Async file and filesystem operations
  • Filesystem events (watch for changes)
  • ANSI-escape-controlled TTY
  • IPC with socket sharing (Unix domain sockets / named pipes)
  • Child processes
  • Thread pool (for blocking work)
  • Signal handling
  • High-resolution clock; threading + synchronization primitives

The architecture: one loop thread runs the I/O-multiplexing core; a small worker thread pool (default 4, UV_THREADPOOL_SIZE) services operations that have no non-blocking OS primitive — file I/O, getaddrinfo/getnameinfo, and arbitrary user work submitted via uv_queue_work. Completion callbacks always fire back on the loop thread, so user code stays single-threaded even though blocking work ran off-thread.

A5 — evaluation as KN-86’s C-level event substrate

Section titled “A5 — evaluation as KN-86’s C-level event substrate”

1. Mapping libuv handle types to KN-86 hardware I/O

Section titled “1. Mapping libuv handle types to KN-86 hardware I/O”
KN-86 I/O surfacelibuv handle that would model itReality on KN-86
Display output (128×75 cell grid, ADR-0027 termbox2 path)uv_tty_t (ANSI-escape TTY)termbox2 already owns the tty1 console + present loop; redraw is event-idle, not fd-readiness-driven (15–30 fps cap, ADR-0004/0005). No libuv role — the render cadence is a frame-budget timer, not an I/O-ready wait.
Input device (keyboard/trackpoint HID)uv_poll_t on the evdev/HID fd, or uv_tty_t readA real fit in principle — keyboard input is genuinely fd-readiness-driven and is what would wake an idle event loop. But termbox2’s tb_poll_event already does exactly this (it blocks/selects on the input fd), and ADR-0027 mandates input policy stay in nOSh as classified semantic events. libuv would duplicate tb_poll_event.
Audio out (I2S/PSG)uv_async_t / a pipe handle to the audio pathOffloaded to the Pico 2 (ADR-0017). The Pi sends psg-reg N V over UART and walks away; the Pico synthesizes 44.1 kHz PCM. There is no audio buffer for the Pi event loop to feed. libuv has nothing to do here.
CIPHER-LINE OLEDa write handleAlso offloaded to the Pico 2 (ADR-0017) — oled-set-row over UART; the Pico runs the ticker on its own timer. No Pi-side loop involvement.
Cartridge / SD storageuv_fs_* (thread-pool file I/O) + uv_fs_event_t (card-detect)The cart is a USB-mass-storage SD block device (ADR-0019); the Pi mounts it and reads files. This is the one surface where libuv’s value is realuv_fs_* would keep a large cart read off the render thread, and uv_fs_event_t could watch for insert/remove. But cart reads happen at load boundaries (cart-load, mission-instance), not per-frame, and the Fe arena resets at exactly those boundaries (ADR-0004) — a brief synchronous read at a load boundary is acceptable and already the model.
Networkuv_tcp_t / uv_udp_t / DNSKN-86 has no networking in the gameplay surface. The LINK protocol is fiction rendered locally; there is no real socket. Firmware update (ADR-0011) is the only real network path, and it’s an out-of-band system-image operation, not a runtime concern. libuv’s entire networking half is dead weight here.
UART to Pico 2 (1 Mbps, the real realtime backchannel)uv_poll_t on /dev/serial0This is a genuine fd KN-86 reads asynchronously — Pico backchannel events (cart insert/remove, etc.). A uv_poll_t would model it. But it’s a single low-rate fd; a plain poll()/select() integrated into the existing main loop covers it without a library.

Summary of the mapping: of libuv’s handle types, the KN-86-relevant ones reduce to input fd, one UART fd, and occasional file I/O. The networking half is unused (no real sockets). The audio and OLED halves are already offloaded to the Pico 2, so the Pi’s loop never touches them. termbox2 already owns the display present-loop and the input-fd wait.

2. The thread pool vs KN-86’s single-threaded + coprocessor model

Section titled “2. The thread pool vs KN-86’s single-threaded + coprocessor model”

libuv’s headline value over a bare poll() loop is the worker thread pool that hides blocking work. The candidate KN-86 background tasks are audio mixing and procedural map generation. Examined against KN-86’s actual architecture:

  • Audio mixing / PSG synthesisalready off the Pi. ADR-0017 moves YM2149 synthesis + I2S to the Pico 2 precisely because scheduler jitter on Linux is the audio-underrun risk. A libuv thread pool on the Pi would re-import the exact problem ADR-0017 solved by moving synthesis to dedicated silicon. This is not a thread-pool candidate; it’s a solved-by-hardware case.
  • Procedural map generation — a real candidate for “expensive, bursty, off-the-render-thread.” But KN-86’s procgen is LFSR-driven and fast (the 80×25-grown-to-128×75 grid is small; generation is cheap), and Fe’s arena discipline is single-threaded by design (ADR-0004, no GC, arena reset at boundaries). Running generation on a libuv worker thread would mean either (a) generating into a thread-local buffer then copying back — fine but not obviously worth a library — or (b) touching the Fe arena from a worker thread, which breaks the arena’s single-threaded invariant. The honest path for a genuinely expensive generation step is to chunk it across frames on the main loop (Update-Method pattern, game-programming-patterns.md), not to thread it.

The deeper point: KN-86 is single-threaded by design, and its realtime work is already offloaded to the Pico 2. libuv’s thread pool exists to rescue a single-threaded loop from blocking I/O on a general-purpose host. KN-86 doesn’t have the blocking-I/O problem (no sockets, file I/O only at load boundaries) and doesn’t have the realtime-on-the-loop problem (audio/OLED are on the Pico). The two problems the thread pool solves are both absent or already solved.

3. The libuv → KEC Lisp FFI boundary (how the loop would sit under the interpreter)

Section titled “3. The libuv → KEC Lisp FFI boundary (how the loop would sit under the interpreter)”

If libuv were adopted, the composition would be: libuv owns the loop; each ready handle’s C callback classifies the event and dispatches into a KEC Lisp handler (the cart’s registered handler for that event), which runs synchronously to completion within the frame budget, then yields back to the loop. That is structurally identical to what KN-86 already does — except KN-86’s loop is termbox2’s tb_poll_event plus a frame timer, not libuv:

[ libuv loop ] [ KN-86 today (ADR-0004/0005/0027) ]
uv_run(loop) main loop: tb_poll_event() + frame timer
→ handle ready → semantic input event (nOSh-classified)
→ C callback → C dispatch
→ Fe handler (sync, in-frame) → Fe handler (sync, in-frame, arena-bound)
→ yield to loop → yield; redraw if dirty

The crucial constraint either way: Fe handlers must run on one thread and complete within the frame budget (~50 ms event-driven, ADR-0004; the Cipher engine and cell handlers already obey this). libuv doesn’t change that — Fe still runs single-threaded on the loop thread; libuv callbacks would just be a different source of the events that trigger Fe dispatch. This composes cleanly with the existing event-driven redraw model (render loop idles, fires on event or at the animation cap) — but it composes cleanly because the existing model is already an event loop. libuv would be a heavier engine under the same shape, not a new capability.

4. Verdict — pattern-reference, not adoption

Section titled “4. Verdict — pattern-reference, not adoption”

KN-86 does not need libuv. Do not adopt it. The honest argument:

  1. The networking half is unused. No real sockets in the gameplay surface; the LINK protocol is local fiction. Half of libuv’s value proposition is dead weight.
  2. The realtime half is already offloaded. Audio (PSG/I2S) and the OLED ticker live on the Pico 2 (ADR-0017) — specifically to keep realtime work off Linux’s non-realtime scheduler. The Pi’s event loop never touches them; the UART backchannel is one low-rate fd.
  3. The thread pool solves problems KN-86 doesn’t have. Audio is on the Pico; procgen is LFSR-cheap and arena-bound (single-threaded by design, ADR-0004). Threading generation would break the arena invariant, not help it.
  4. termbox2 already owns the loop’s real job. Per ADR-0027, termbox2 owns the display present-path and the input-fd wait (tb_poll_event). Adding libuv means running two loops (libuv + termbox) or rewriting termbox’s input path onto libuv handles — strictly more complexity for the one fd (input) and one-and-a-bit fds (UART, occasional file I/O) KN-86 actually waits on.
  5. The frame-budget cadence is a timer, not I/O readiness. KN-86’s redraw is capped at 15–30 fps and is event-idle most of the time (ADR-0004/0005). That’s a frame timer plus an input wait — the simplest possible loop. libuv’s multiplexing core is built for many concurrent fds; KN-86 has ~three.

What KN-86 should take from libuv is the pattern, documented: a single event loop, typed event sources, callbacks that dispatch into a higher-level interpreter and run to completion before yielding, and a bounded worker pool for the rare genuinely-blocking op — held in reserve as the answer if a real blocking need ever surfaces (e.g. a large cart asset decompressed at load that measurably stalls the frame). Until that need is measured on hardware, the existing tb_poll_event + frame-timer + load-boundary-synchronous-file-read model is correct, smaller, and matches the project’s single-threaded-by-design / realtime-on-the-Pico posture. libuv is the reference design for the loop KN-86 already has, not a library KN-86 should link.

  • A5 conclusion in one line: pattern-reference, not adoption — KN-86 is single-threaded-by-design with realtime offloaded to the Pico 2, so libuv’s networking and thread-pool value propositions are respectively unused and already-solved. Revisit only if a measured, on-hardware, frame-stalling blocking op appears that can’t be chunked across frames.
  • Cross-link game-programming-patterns.md — Game Loop, Update Method, Service Locator are the patterns KN-86’s loop already implements; libuv is a production C realization of the same Game-Loop/event-source pattern. Chunk-expensive-work-across-frames (Update Method) is the answer libuv’s thread pool would otherwise be reached for.
  • Cross-link tuibox.md — tuibox’s event-driven render loop + per-box dirty bit is the small C realization of the same loop; libuv is the large one. KN-86 sits closer to tuibox’s scale.
  • Cross-link ADR-0017 (Pico 2 coprocessor) — the reason the realtime half of libuv is irrelevant: audio + OLED are already off the Pi.
  • Cross-link ADR-0027 (termbox2 display + input) — the reason the loop’s real job is already owned: termbox tb_poll_event is the input wait libuv would otherwise provide.
  • Cross-link ADR-0004 (Fe VM arena discipline) — the single-threaded arena invariant that makes the thread pool a hazard rather than a help for procgen.