Generalize input module to mouse and joystick/gamepad events
Extend the input service beyond the keyboard so mouse and joystick/gamepad drivers can broadcast too, with per-device publish and subscribe methods. - protocol: KeyEvent joins MouseEvent (motion/buttons/scroll) and JoystickEvent (axes/buttons), all carried in a common InputEvent envelope tagged with a DeviceKind. A subscribe request carries a device_mask, so a subscriber names the classes it wants and the service routes each event only to interested subscribers (a mouse-only listener never wakes for keystrokes). - runtime: per-device publish methods (publishKeyboardEvent/publishMouseEvent/ publishJoystickEvent) and subscribe helpers (subscribeKeyboard/Mouse/Joystick, each typed, plus subscribe(mask)/subscribeAll returning the tagged envelope). - service: subscriber table gains a device_mask; broadcast routes by the event's device class. - mouse driver now publishes (synthetic) mouse events like the keyboard driver; input-source cycles all three classes; input-test subscribes to all and only emits its "ok" marker once it has received one of each class — so the passing test proves per-device routing, not just delivery. Real HID decoding stays a follow-up. No kernel changes: ipc_send is generic and the 36-byte InputEvent fits its 64-byte payload. Full QEMU suite 48/48; serial log confirms keyboard, mouse, and joystick all reach one subscription.
This commit is contained in:
+66
-40
@@ -1,16 +1,33 @@
|
||||
# The input module: broadcasting keyboard events
|
||||
# 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 driver and the listeners sits the **input service**
|
||||
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 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.
|
||||
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.
|
||||
|
||||
Three event kinds cross the wire ([protocol.zig](../system/services/input/protocol.zig)):
|
||||
`key_down` and `key_up` are the physical make/break; `key_press` is the higher-level
|
||||
"a character was produced", carrying the Unicode scalar. A `KeyEvent` also has a
|
||||
layout-independent `keycode` and a `modifiers` bitmask.
|
||||
## 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
|
||||
|
||||
@@ -54,32 +71,36 @@ This is the async counterpart of `ipc_call`, and the input service is its first
|
||||
## How the pieces fit
|
||||
|
||||
```
|
||||
keyboard driver / input-source input service subscriber(s)
|
||||
-------------------------------- ------------- -------------
|
||||
connectSource(); loop: replyWait: subscribe():
|
||||
publish(event) ── ipc_call ──▶ publish → broadcast: createIpcEndpoint()
|
||||
for each sub: callCap(subscribe,
|
||||
ipc_send(sub_ep) ──────────▶ send_cap = ep)
|
||||
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 sub_ep cap └─ replyWait(ep)
|
||||
(from the call's capability) → KeyEvent
|
||||
subscribe → store {ep cap, └─ replyWait(ep)
|
||||
task id, device_mask} → InputEvent
|
||||
```
|
||||
|
||||
- A **subscriber** calls `input.subscribe()`
|
||||
([library/runtime/input.zig](../library/runtime/input.zig)): it creates its own endpoint
|
||||
- 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). Then it loops on `Subscriber.next()`, which
|
||||
is a `replyWait` on that endpoint returning each pushed `KeyEvent`.
|
||||
- A **source** (a keyboard driver) calls `input.connectSource()` and
|
||||
`Publisher.publish(event)`. 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). On `publish` it `ipc_send`s the event
|
||||
to every subscriber. On `subscribe` it stores the passed capability 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.
|
||||
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
|
||||
@@ -87,13 +108,16 @@ the service delivers to its endpoint, which only the same thread could receive).
|
||||
|
||||
## Status and follow-ups
|
||||
|
||||
- **Synthetic source, for now.** The `ps2-bus` driver owns PNP0303, which carries *both*
|
||||
the 0x60/0x64 ports and IRQ1, so reading real scancodes has to live in the bus, not in
|
||||
[keyboard.zig](../system/drivers/ps2-bus/keyboard.zig). Until that lands, the keyboard
|
||||
driver (and the hardware-free `input-source` used by the test) publish a synthetic rolling
|
||||
`A..E` stream via `input.syntheticEvent`. The fan-out path is real; only the bytes are
|
||||
placeholder. **Follow-up:** the bus binds IRQ1, reads port 0x60, and `ps2-library`
|
||||
translates scan-set-1 → keycodes; the keyboard driver publishes decoded events.
|
||||
- **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.
|
||||
- **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
|
||||
@@ -104,9 +128,11 @@ the service delivers to its endpoint, which only the same thread could receive).
|
||||
|
||||
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, and a subscriber. It passes only when the subscriber
|
||||
heartbeats `input-test: ok` — proof that an event travelled source → service → subscriber
|
||||
over IPC, exercising both `ipc_send` and capability-passing subscription.
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user