# The input module: broadcasting input events A keyboard driver has one keystroke and *many* programs that might want it — a shell, a window server, a logger. None of them owns the hardware, and the driver should not know who is listening. So between the drivers and the listeners sits the **input service** (`system/services/input/`): drivers **publish** events to it, programs **subscribe**, and it fans each event out to every interested subscriber. It is an ordinary ring-3 process reached over IPC, like the [VFS server](../system/services/vfs/vfs.zig) — no kernel knows what a key is. ## One service, several device classes The service carries three device classes today — **keyboard**, **mouse**, and **joystick/gamepad** — and is built to take more ([protocol.zig](../system/services/input/protocol.zig)). Each class has its own typed event: - `KeyEvent` — `key_down`/`key_up` (physical make/break) and `key_press` (a character was produced, carrying the Unicode scalar); plus a layout-independent `keycode` and a `modifiers` bitmask. - `MouseEvent` — relative `motion` (`dx`/`dy`), `button_down`/`button_up`, and `scroll`. - `JoystickEvent` — `axis` moves (a signed value on a `control` index) and `button_down`/`button_up`. All three travel in one **`InputEvent` envelope** tagged with a `DeviceKind`, so the fan-out is a single code path and a subscriber can take a mix of classes on one stream. Decode an envelope with `asKeyboard()` / `asMouse()` / `asJoystick()` (each returns null unless the tag matches). A subscriber names the classes it wants with a **`device_mask`**, and the service routes each event only to subscribers whose mask includes its class — so a mouse-only listener never wakes for keystrokes. ## Why this needed a new kernel primitive The interesting part is delivery, and it runs straight into the shape of danos IPC. [ipc.md](ipc.md) describes a **synchronous rendezvous**: a server holds exactly one pending reply (`Task.ipc_client`) and *must* answer it on its next `replyWait`. Two consequences decide the whole design: 1. **You cannot block N subscribers waiting for "the next event".** A server can hold only one caller at a time, so the natural "subscriber calls `next_event()` and blocks" API is impossible for more than one subscriber. Delivery therefore has to be **push** — the service reaching out to subscribers — not pull. 2. **A synchronous push can hang the whole service.** If the service delivered with `ipc_call`, it would block until each subscriber replied. `ipc_call` has no timeout, and the kernel does **not** wake a caller parked on a *dead* peer's endpoint (it only fails a peer that was mid-reply — see [process.zig](../system/kernel/process.zig) `releaseTaskResourcesLocked`). One subscriber that exits mid-delivery would wedge input for everyone. That is the opposite of the resilience the microkernel is for. The fix is the asynchronous send that [ipc.md](ipc.md) had already earmarked as future work ("asynchronous / buffered send … for notifications between servers"): ``` ipc_send(handle, message_ptr, message_len) -> 0 / -errno ``` `ipc_send` copies a small payload into the endpoint's **bounded queue** and wakes a receiver, then returns immediately — it never blocks and so can never hang on a dead or slow subscriber. The receiver picks it up through the same `replyWait` it already runs: the wake arrives as a **buffered message** — `notify_badge_bit | notify_message_bit` set in the badge (distinguishing it from a bare IRQ/child-exit notification), the sender's task id in the low bits, and the payload in the receive buffer, with no reply owed. The queue holds 16 messages per endpoint; a full queue **drops the oldest**, because a buffered message is discrete data, not a coalescing "level" like an interrupt. See [ipc-synchronous.zig](../system/kernel/ipc-synchronous.zig) (`sendLocked`, `popPost`, and the `replyWait` receive loop). This is the async counterpart of `ipc_call`, and the input service is its first consumer. ## How the pieces fit ``` keyboard/mouse driver, input-source input service subscriber(s) ----------------------------------- ------------- ------------- connectSource(); loop: replyWait: subscribeKeyboard()/…All: publishKeyboardEvent(k) ─ ipc_call ─▶ publish → broadcast: createIpcEndpoint() publishMouseEvent(m) for each sub whose callCap(subscribe, publishJoystickEvent(j) mask matches event.device: send_cap = ep, ipc_send(sub_ep) ──────▶ device_mask) reply ok loop: next() subscribe → store {ep cap, └─ replyWait(ep) task id, device_mask} → InputEvent ``` - A **subscriber** calls `input.subscribe(mask)` — or a typed helper: `subscribeKeyboard()`, `subscribeMouse()`, `subscribeJoystick()` (one class, `next()` returns the decoded event), or `subscribeAll()` (every class, `next()` returns a tagged `InputEvent`) ([library/runtime/input.zig](../library/runtime/input.zig)). It creates its own endpoint and hands it to the service as a **capability** (M13 capability passing — the input service is that feature's first real user), along with its `device_mask`. Then it loops on `next()`, a `replyWait` on that endpoint returning each pushed event. - A **source** (a keyboard, mouse, or joystick driver) calls `input.connectSource()` and the method for its class: `publishKeyboardEvent`, `publishMouseEvent`, or `publishJoystickEvent`. Publishing is a short synchronous `ipc_call` the service answers at once; the service's own fan-out is asynchronous, so publishing never blocks on a slow subscriber. - The **service** ([input.zig](../system/services/input/input.zig)) keeps a small subscriber table (endpoint handle + owning task id + `device_mask`). On `publish` it `ipc_send`s the event to every subscriber whose mask includes the event's device class. On `subscribe` it stores the passed capability and mask and, as housekeeping, prunes any slot whose owning process has exited (checked against `process_enumerate`) — not for correctness (an async send to an orphaned endpoint is harmless) but to reclaim the slot. Publisher and subscriber must be **separate processes**: a single thread that both published and serviced its own subscription would deadlock (its `publish` call blocks until the service delivers to its endpoint, which only the same thread could receive). ## Status and follow-ups - **Synthetic sources, for now.** The `ps2-bus` driver owns PNP0303, which carries *both* the 0x60/0x64 ports and IRQ1, so reading real scancodes/packets has to live in the bus, not in [keyboard.zig](../system/drivers/ps2-bus/keyboard.zig) / [mouse.zig](../system/drivers/ps2-bus/mouse.zig). Until that lands, the keyboard driver publishes a synthetic key stream, the mouse driver a synthetic motion/click stream, and the hardware-free `input-source` rotates through all three classes (including a synthetic joystick, which has no driver yet) — all via the `input.synthetic*Event` helpers. The fan-out and per-device routing are real; only the bytes are placeholder. **Follow-up:** the bus binds IRQ1/IRQ12, reads port 0x60, and `ps2-library` decodes scan-set-1 → keycodes and mouse packets; the drivers publish decoded events. - **Keycode → character** is a keymap, and danos has one: [`library/xkeyboard-config`](../library/xkeyboard-config/README.md) compiles the X11 xkeyboard-config layouts (us, gb, de, fr, …) to native Zig — `xkb.map(layout, keycode, mods)` → keysym + Unicode character. Wiring it in to fill a `key_press` event's `character` (in the keyboard driver, or a small keymap service) is the natural next step. - **Drop-oldest under overflow** is a defined loss; the 16-slot ring absorbs normal bursts. Real backpressure/flow-control is future work. - **`publish` is unauthenticated** — any process may publish, consistent with the current bring-up trust model (see [driver-model.md](driver-model.md)). A source capability is future work. ## Verifying it The `input` case (`python3 test/qemu_test.py input`, in [tests.zig](../system/kernel/tests.zig) `inputTest`) boots the real kernel and spawns the service, the synthetic source (which cycles keyboard, mouse, and joystick events), and a subscriber that took all three classes. It passes only when the subscriber heartbeats `input-test: ok` — proof that an event travelled source → service → subscriber over IPC, exercising `ipc_send`, capability-passing subscription, and per-device routing. Each serial line names the class received, so the log shows all three arriving on one stream. ## See also - [ipc.md](ipc.md) — the synchronous rendezvous and the notification path `ipc_send` extends. - [syscall.md](syscall.md) — the system-call surface, including `ipc_send`. - [driver-model.md](driver-model.md) — class drivers, capability passing (M13), the trust model.