9.8 KiB
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 — 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). Each class has its own typed event:
KeyEvent—key_down/key_up(physical make/break) andkey_press(a character was produced, carrying the Unicode scalar); plus a layout-independentkeycodeand amodifiersbitmask.MouseEvent— relativemotion(dx/dy),button_down/button_up, andscroll.JoystickEvent—axismoves (a signed value on acontrolindex) andbutton_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 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:
-
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. -
A synchronous push can hang the whole service. If the service delivered with
ipc_call, it would block until each subscriber replied.ipc_callhas 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.zigreleaseTaskResourcesLocked). 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 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 (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), orsubscribeAll()(every class,next()returns a taggedInputEvent) (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 itsdevice_mask. Then it loops onnext(), areplyWaiton 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, orpublishJoystickEvent. Publishing is a short synchronousipc_callthe service answers at once; the service's own fan-out is asynchronous, so publishing never blocks on a slow subscriber. - The service (input.zig) keeps a small subscriber
table (endpoint handle + owning task id +
device_mask). Onpublishitipc_sends the event to every subscriber whose mask includes the event's device class. Onsubscribeit stores the passed capability and mask and, as housekeeping, prunes any slot whose owning process has exited (checked againstprocess_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
- The keyboard is real. The
ps2-busdriver owns PNP0303, which carries both the 0x60/0x64 ports and IRQ1, so reading the hardware lives in the bus, not in 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 (anAttachRequestto the well-knownps2_busservice, carrying the child's endpoint as a capability; the bytes then arrive as asynchronousForwardedBytemessages, 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 into USB HID usage keycodes with make/break, typematic-repeat, and modifier tracking (host-tested underzig build test) — and publishes realkey_down/key_press/key_upevents. - Keycode → character is wired in: the keyboard driver fills a
key_pressevent'scharacterthroughlibrary/xkeyboard-config(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 tous; the bus can pass another as the driver's argv[2] — the seam for a future settings source. - The mouse is still synthetic. mouse.zig
publishes a placeholder motion/click stream; the hardware-free
input-sourcelikewise rotates through all three classes (including a synthetic joystick, which has no driver yet) via theinput.synthetic*Eventhelpers. Follow-up: the mouse driver attaches to the bus the way the keyboard does and decodes 3/4-byte packets into mouse events. - Drop-oldest under overflow is a defined loss; the 16-slot ring absorbs normal bursts. Real backpressure/flow-control is future work.
publishis unauthenticated — any process may publish, consistent with the current bring-up trust model (see driver-model.md). A source capability is future work.
Verifying it
The input case (python3 test/qemu_test.py input, in
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 — the synchronous rendezvous and the notification path
ipc_sendextends. - syscall.md — the system-call surface, including
ipc_send. - driver-model.md — class drivers, capability passing (M13), the trust model.