Turn a keycode + modifiers into a character. The input module delivers HID
usage keycodes but nothing mapped them to characters; rather than hand-maintain
layout tables, vendor the X11 xkeyboard-config database and compile it to native
Zig at build time (no X11 runtime), the way make-initial-ramdisk.py packs the
ramdisk.
- tools/make-xkeyboard-config.py: `fetch` downloads the pinned xkeyboard-config
release (2.44, sha256-verified), resolves the include graph for the configured
layouts, and vendors only the reached symbols files + keysymdef.h + COPYING +
PROVENANCE into library/xkeyboard-config/vendor/. `generate` parses that
(keycodes via a HID->xkb-name table, symbols with include/augment/override and
per-key type, keysymdef for keysym->Unicode) and emits generated/layouts.zig
deterministically.
- library/xkeyboard-config/xkeyboard-config.zig: the API over the generated data
— map(layout, hid_usage, mods) -> { keysym, character }, byName, and the
level-selection semantics (the generated tables stay pure data). Host tests
assert US letters/digits with Shift/Caps, GB £ vs US # on Shift+3, and French
AZERTY q-where-US-has-a — the end-to-end proof of the parse->emit->lookup path.
- build.zig: `xkeyboard-config` + `layouts` modules, the test wired into
`zig build test`, and a `zig build gen-xkeyboard-config` convenience step.
- Layouts: us, gb, de, fr, es, dvorak. Scope (documented): group 1, no dead-key
composition, curated key types. Standalone library; wiring it into the input
path to fill KeyEvent.character is a documented follow-up.
zig build test green (incl. the new keymap tests); regeneration is byte-identical;
full QEMU suite 48/48 (unaffected — no kernel/runtime/service change).
148 lines
9.1 KiB
Markdown
148 lines
9.1 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 [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.
|