178 lines
12 KiB
Markdown
178 lines
12 KiB
Markdown
# 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.
|