# 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 [FAT server](../../system/services/fat/fat.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](../../library/protocol/input/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`. A source publishes any of the three as one **`InputEvent`** tagged with a `DeviceKind`, so `publish` is a single verb; decode one with `asKeyboard()` / `asMouse()` / `asJoystick()` (each returns null unless the tag matches). On the *delivery* wire the class is the packet's own operation instead — the protocol declares one event per class ([protocol-namespace.md](../os-development/protocol-namespace.md)), so a pushed packet is the 16-byte header plus the typed event and nothing carries a tag twice. The client helpers re-tag what arrives back into an `InputEvent`, so a subscriber can still take a mix of classes on one stream. 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. **`subscribe` is not this protocol's verb.** Its shape — a synchronous call whose attached capability is the subscriber's own endpoint — is what the envelope's *reserved* subscribe means at every provider in the system, so the input protocol adopts it rather than defining a second spelling of the same thing. The interest mask rides as the packet's tail. `publish` is the one verb the protocol defines for itself. ## 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 a subscriber's endpoint is an *unregistered* capability the kernel's death path cannot reach (since display v2's V6, `killOwnedEndpointsLocked` in [ipc-synchronous.zig](../../system/kernel/ipc-synchronous.zig) marks a dead owner's *registered* endpoints dead and wakes parked callers with `-EPEER` — but unregistered ones just drop with the task's handle table). 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/client/input/input.zig](../../library/client/input/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. The service runs on the shared harness ([service.zig](../../library/kernel/service.zig)) like every other, so it answers the universal ping and exits on `terminate`; it was the last hand-rolled receive loop in the tree, and the last service a shutdown had to kill rather than ask. 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 - **The keyboard is real.** The `ps2-bus` driver owns PNP0303, which carries *both* the 0x60/0x64 ports and IRQ1, so reading the hardware lives in the bus, not in [keyboard.zig](../../system/drivers/ps2-bus/keyboard.zig): the bus binds IRQ1 and, on each interrupt, drains port 0x60, routing every byte by the status register's auxiliary-output bit to whichever child driver **attached** for that device (an `AttachRequest` to the well-known `ps2_bus` service, carrying the child's endpoint as a capability; the bytes then arrive as asynchronous `ForwardedByte` messages, so the IRQ path never blocks on a child). The keyboard driver decodes the stream — scancode **set 2**, what the keyboard sends with the 8042's legacy translation off, decoded by [scancode.zig](../../system/drivers/ps2-bus/scancode.zig) into USB HID usage keycodes with make/break, typematic-repeat, and modifier tracking (host-tested under `zig build test`) — and publishes real `key_down`/`key_press`/`key_up` events. - **Keycode → character** is wired in: the keyboard driver fills a `key_press` event's `character` through [`library/xkeyboard-config`](../../library/xkeyboard-config/README.md) (`xkb.map(layout, keycode, mods)` → keysym + Unicode character), synthesizing the ASCII control characters for Enter/Tab/Backspace/Escape, whose keysyms map to no Unicode. The layout defaults to `us`; the bus can pass another as the driver's argv[2] — the seam for a future settings source. - **The mouse is real too.** IRQ12 is enumerated on the auxiliary device's own ACPI node (PNP0F13), so the bus claims that node alongside the controller and routes both IRQs to its one endpoint, acking whichever line the notification's badge names. [mouse.zig](../../system/drivers/ps2-bus/mouse.zig) attaches the way the keyboard does and assembles the forwarded bytes with [mouse-packet.zig](../../system/drivers/ps2-bus/mouse-packet.zig) (three-byte stream-mode packets: sync/overflow handling, nine-bit movement, screen-convention `dy` — host-tested under `zig build test`) into `button_down`/`button_up` transitions and `motion` events. **Follow-up:** the IntelliMouse magic-knock for a scroll wheel (four-byte packets) and `scroll` events. The hardware-free `input-source` still rotates through all three classes synthetically (including a joystick, which has no driver yet) via the `input.synthetic*Event` helpers. - **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](../os-development/syscall.md) — the system-call surface, including `ipc_send`. - [driver-model.md](driver-model.md) — class drivers, capability passing (M13), the trust model.