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:
Daniel Samson 2026-07-11 15:21:09 +01:00
parent 65244e3103
commit 1bf91115dd
No known key found for this signature in database
GPG Key ID: A9EB2589C60F7268
10 changed files with 457 additions and 160 deletions

View File

@ -52,9 +52,10 @@ rather than restate it. Roughly in the order things happen at runtime:
microkernel's `ps`/`kill`/SIGCHLD: enumerate as a table snapshot, the microkernel's `ps`/`kill`/SIGCHLD: enumerate as a table snapshot, the
supervision link as the kill authority, and child-exit notifications over the supervision link as the kill authority, and child-exit notifications over the
same endpoints IRQs arrive on. same endpoints IRQs arrive on.
16. **[input.md](input.md) — the input module.** Broadcasting keyboard events: why a 16. **[input.md](input.md) — the input module.** Broadcasting input events (keyboard,
synchronous rendezvous can't fan out to many listeners, the asynchronous `ipc_send` mouse, joystick): why a synchronous rendezvous can't fan out to many listeners, the
primitive built to fix it, and the subscribe/publish service layered on top. asynchronous `ipc_send` primitive built to fix it, and the per-device subscribe/publish
service layered on top.
17. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and 17. **[halting.md](halting.md) — halting.** Why a kernel can't just "exit", and
how `while (true) hlt` parks the CPU safely once there's nothing left to do. how `while (true) hlt` parks the CPU safely once there's nothing left to do.

View File

@ -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 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 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 (`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 it fans each event out to every interested subscriber. It is an ordinary ring-3 process
IPC, like the [VFS server](../system/services/vfs/vfs.zig) — no kernel knows what a key is. 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)): ## One service, several device classes
`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 The service carries three device classes today — **keyboard**, **mouse**, and
layout-independent `keycode` and a `modifiers` bitmask. **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 ## 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 ## How the pieces fit
``` ```
keyboard driver / input-source input service subscriber(s) keyboard/mouse driver, input-source input service subscriber(s)
-------------------------------- ------------- ------------- ----------------------------------- ------------- -------------
connectSource(); loop: replyWait: subscribe(): connectSource(); loop: replyWait: subscribeKeyboard()/…All:
publish(event) ── ipc_call ──▶ publish → broadcast: createIpcEndpoint() publishKeyboardEvent(k) ─ ipc_call ─▶ publish → broadcast: createIpcEndpoint()
for each sub: callCap(subscribe, publishMouseEvent(m) for each sub whose callCap(subscribe,
ipc_send(sub_ep) ──────────▶ send_cap = ep) publishJoystickEvent(j) mask matches event.device: send_cap = ep,
ipc_send(sub_ep) ──────▶ device_mask)
reply ok loop: next() reply ok loop: next()
subscribe → store sub_ep cap └─ replyWait(ep) subscribe → store {ep cap, └─ replyWait(ep)
(from the call's capability) → KeyEvent task id, device_mask} → InputEvent
``` ```
- A **subscriber** calls `input.subscribe()` - A **subscriber** calls `input.subscribe(mask)` — or a typed helper: `subscribeKeyboard()`,
([library/runtime/input.zig](../library/runtime/input.zig)): it creates its own endpoint `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 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 service is that feature's first real user), along with its `device_mask`. Then it loops on
is a `replyWait` on that endpoint returning each pushed `KeyEvent`. `next()`, a `replyWait` on that endpoint returning each pushed event.
- A **source** (a keyboard driver) calls `input.connectSource()` and - A **source** (a keyboard, mouse, or joystick driver) calls `input.connectSource()` and the
`Publisher.publish(event)`. Publishing is a short synchronous `ipc_call` the service method for its class: `publishKeyboardEvent`, `publishMouseEvent`, or
answers at once; the service's own fan-out is asynchronous, so publishing never blocks on `publishJoystickEvent`. Publishing is a short synchronous `ipc_call` the service answers at
a slow subscriber. once; the service's own fan-out is asynchronous, so publishing never blocks on a slow
- The **service** ([input.zig](../system/services/input/input.zig)) keeps a small subscriber.
subscriber table (endpoint handle + owning task id). On `publish` it `ipc_send`s the event - The **service** ([input.zig](../system/services/input/input.zig)) keeps a small subscriber
to every subscriber. On `subscribe` it stores the passed capability and, as housekeeping, table (endpoint handle + owning task id + `device_mask`). On `publish` it `ipc_send`s the
prunes any slot whose owning process has exited (checked against `process_enumerate`) — event to every subscriber whose mask includes the event's device class. On `subscribe` it
not for correctness (an async send to an orphaned endpoint is harmless) but to reclaim stores the passed capability and mask and, as housekeeping, prunes any slot whose owning
the slot. 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 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 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 ## Status and follow-ups
- **Synthetic source, for now.** The `ps2-bus` driver owns PNP0303, which carries *both* - **Synthetic sources, 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 the 0x60/0x64 ports and IRQ1, so reading real scancodes/packets has to live in the bus,
[keyboard.zig](../system/drivers/ps2-bus/keyboard.zig). Until that lands, the keyboard not in [keyboard.zig](../system/drivers/ps2-bus/keyboard.zig) /
driver (and the hardware-free `input-source` used by the test) publish a synthetic rolling [mouse.zig](../system/drivers/ps2-bus/mouse.zig). Until that lands, the keyboard driver
`A..E` stream via `input.syntheticEvent`. The fan-out path is real; only the bytes are publishes a synthetic key stream, the mouse driver a synthetic motion/click stream, and
placeholder. **Follow-up:** the bus binds IRQ1, reads port 0x60, and `ps2-library` the hardware-free `input-source` rotates through all three classes (including a synthetic
translates scan-set-1 → keycodes; the keyboard driver publishes decoded events. 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. - **Drop-oldest under overflow** is a defined loss; the 16-slot ring absorbs normal bursts.
Real backpressure/flow-control is future work. Real backpressure/flow-control is future work.
- **`publish` is unauthenticated** — any process may publish, consistent with the current - **`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 The `input` case (`python3 test/qemu_test.py input`, in
[tests.zig](../system/kernel/tests.zig) `inputTest`) boots the real kernel and spawns the [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 service, the synthetic source (which cycles keyboard, mouse, and joystick events), and a
heartbeats `input-test: ok` — proof that an event travelled source → service → subscriber subscriber that took all three classes. It passes only when the subscriber heartbeats
over IPC, exercising both `ipc_send` and capability-passing subscription. `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 ## See also

View File

@ -1,16 +1,25 @@
//! User-space input helpers: the client and publisher sides of the input service, so a //! User-space input helpers: the client and publisher sides of the input service, so a
//! program listening for keyboard events or a driver broadcasting them doesn't //! program listening for input events or a driver broadcasting them doesn't hand-roll
//! hand-roll the IPC. Layered over `ipc` (endpoints, capability passing, `send`) and the //! the IPC. Layered over `ipc` (endpoints, capability passing, `send`) and the shared
//! shared `input-protocol` wire format, the same way `device.zig` layers over the raw //! `input-protocol` wire format, the way `device.zig` layers over the raw `device_*` calls.
//! `device_*` calls. See system/services/input/input.zig. //! See system/services/input/input.zig.
//! //!
//! A **subscriber** does: //! The service carries several device classes (keyboard, mouse, joystick/gamepad). A
//! var listener = input.subscribe() orelse return; //! **source** publishes its class with the matching method:
//! while (true) { const event = listener.next() orelse continue; ... }
//!
//! A **source** (keyboard driver) does:
//! var source = input.connectSource() orelse return; //! var source = input.connectSource() orelse return;
//! _ = source.publish(.{ .kind = ..., .keycode = ..., ... }); //! _ = source.publishKeyboardEvent(.{ .kind = ..., .keycode = ..., ... });
//! _ = source.publishMouseEvent(.{ ... });
//! _ = source.publishJoystickEvent(.{ ... });
//!
//! A **subscriber** either takes one class with a typed helper
//! var keys = input.subscribeKeyboard() orelse return;
//! while (true) { const key = keys.next() orelse continue; ... }
//! or takes several at once and inspects the tagged envelope:
//! var listener = input.subscribeAll() orelse return;
//! while (true) {
//! const event = listener.next() orelse continue;
//! if (event.asKeyboard()) |k| { ... } else if (event.asMouse()) |m| { ... }
//! }
const std = @import("std"); const std = @import("std");
const abi = @import("abi"); const abi = @import("abi");
@ -18,10 +27,22 @@ const ipc = @import("ipc.zig");
const system = @import("system.zig"); const system = @import("system.zig");
const protocol = @import("input-protocol"); const protocol = @import("input-protocol");
pub const DeviceKind = protocol.DeviceKind;
pub const InputEvent = protocol.InputEvent;
pub const KeyEvent = protocol.KeyEvent; pub const KeyEvent = protocol.KeyEvent;
pub const MouseEvent = protocol.MouseEvent;
pub const JoystickEvent = protocol.JoystickEvent;
pub const EventKind = protocol.EventKind; pub const EventKind = protocol.EventKind;
pub const MouseEventKind = protocol.MouseEventKind;
pub const JoystickEventKind = protocol.JoystickEventKind;
pub const Keycode = protocol.Keycode; pub const Keycode = protocol.Keycode;
/// Interest masks re-exported so a caller can `subscribe(input.device_keyboard | input.device_mouse)`.
pub const device_keyboard = protocol.device_keyboard;
pub const device_mouse = protocol.device_mouse;
pub const device_joystick = protocol.device_joystick;
pub const device_all = protocol.device_all;
/// Look up the input service, retrying while it is still coming up. Both a subscriber and /// Look up the input service, retrying while it is still coming up. Both a subscriber and
/// a source race the service's registration at boot, so both wait for it here rather than /// a source race the service's registration at boot, so both wait for it here rather than
/// failing. Returns the service endpoint handle, or null if it never appears. /// failing. Returns the service endpoint handle, or null if it never appears.
@ -34,8 +55,12 @@ fn lookupService() ?ipc.Handle {
return null; return null;
} }
// --- subscribing ------------------------------------------------------------
/// A subscription to the input service: our own endpoint, which the service pushes events /// A subscription to the input service: our own endpoint, which the service pushes events
/// to. Keep it and call `next` in a loop. /// to. `next` returns each event as a tagged `InputEvent`; use `asKeyboard`/`asMouse`/
/// `asJoystick` to decode. Created with `subscribe`/`subscribeAll`; for a single device
/// class prefer the typed helpers (`subscribeKeyboard`, ...), which return decoded events.
pub const Subscriber = struct { pub const Subscriber = struct {
/// The endpoint the service delivers events to (created and owned by us; its handle /// The endpoint the service delivers events to (created and owned by us; its handle
/// was handed to the service as a capability at subscribe time). /// was handed to the service as a capability at subscribe time).
@ -44,47 +69,104 @@ pub const Subscriber = struct {
/// Block until the next event is pushed, and return it. Events arrive as asynchronous /// Block until the next event is pushed, and return it. Events arrive as asynchronous
/// buffered messages (`ipc_send` from the service), so nothing is owed in reply the /// buffered messages (`ipc_send` from the service), so nothing is owed in reply the
/// empty reply this issues is a harmless no-op (a pure subscriber holds no client). /// empty reply this issues is a harmless no-op. Returns null for any non-event wake-up
/// Returns null for any non-event wake-up (there should be none), so callers can loop. /// (there should be none), so callers can loop.
pub fn next(self: *Subscriber) ?KeyEvent { pub fn next(self: *Subscriber) ?InputEvent {
const got = ipc.replyWait(self.endpoint, &.{}, &self.receive, null); const got = ipc.replyWait(self.endpoint, &.{}, &self.receive, null);
if (!got.isMessage() or got.len < protocol.event_size) return null; if (!got.isMessage() or got.len < protocol.event_size) return null;
return std.mem.bytesToValue(KeyEvent, self.receive[0..protocol.event_size]); return std.mem.bytesToValue(InputEvent, self.receive[0..protocol.event_size]);
} }
}; };
/// Subscribe to keyboard events: create an endpoint for the service to push to, and hand /// Subscribe to the input classes named in `device_mask` (an OR of `device_*`, or
/// it over as a capability. Returns a `Subscriber` to loop `next` on, or null on failure /// `device_all`). Creates an endpoint for the service to push to and hands it over as a
/// (the service never came up, out of handles, or the subscribe call failed). /// capability. Returns a `Subscriber` to loop `next` on, or null on failure.
pub fn subscribe() ?Subscriber { pub fn subscribe(device_mask: u32) ?Subscriber {
const service = lookupService() orelse return null; const service = lookupService() orelse return null;
const endpoint = ipc.createIpcEndpoint() orelse return null; const endpoint = ipc.createIpcEndpoint() orelse return null;
var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.subscribe), .event = undefined }; var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.subscribe), .device_mask = device_mask };
var reply: [protocol.reply_size]u8 = undefined; var reply: [protocol.reply_size]u8 = undefined;
const result = ipc.callCap(service, std.mem.asBytes(&request), &reply, endpoint) catch return null; const result = ipc.callCap(service, std.mem.asBytes(&request), &reply, endpoint) catch return null;
if (result.len < protocol.reply_size) return null; if (result.len < protocol.reply_size) return null;
const header = std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]); if (std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status != 0) return null;
if (header.status != 0) return null;
return .{ .endpoint = endpoint }; return .{ .endpoint = endpoint };
} }
/// A connection to the input service for a source (a keyboard driver) that publishes /// Subscribe to every input class (keyboard, mouse, joystick) on one stream.
/// events. Cheap to hold; `publish` is a short synchronous call the service answers at pub fn subscribeAll() ?Subscriber {
/// once (its own fan-out to subscribers is asynchronous, so publishing never blocks on a return subscribe(device_all);
/// slow subscriber). }
/// A subscriber filtered to keyboard events, whose `next` returns a decoded `KeyEvent`.
pub const KeyboardSubscriber = struct {
inner: Subscriber,
pub fn next(self: *KeyboardSubscriber) ?KeyEvent {
return (self.inner.next() orelse return null).asKeyboard();
}
};
/// A subscriber filtered to mouse events, whose `next` returns a decoded `MouseEvent`.
pub const MouseSubscriber = struct {
inner: Subscriber,
pub fn next(self: *MouseSubscriber) ?MouseEvent {
return (self.inner.next() orelse return null).asMouse();
}
};
/// A subscriber filtered to joystick/gamepad events, whose `next` returns a decoded
/// `JoystickEvent`.
pub const JoystickSubscriber = struct {
inner: Subscriber,
pub fn next(self: *JoystickSubscriber) ?JoystickEvent {
return (self.inner.next() orelse return null).asJoystick();
}
};
/// Subscribe to keyboard events only; `next` returns decoded `KeyEvent`s.
pub fn subscribeKeyboard() ?KeyboardSubscriber {
return .{ .inner = subscribe(device_keyboard) orelse return null };
}
/// Subscribe to mouse events only; `next` returns decoded `MouseEvent`s.
pub fn subscribeMouse() ?MouseSubscriber {
return .{ .inner = subscribe(device_mouse) orelse return null };
}
/// Subscribe to joystick/gamepad events only; `next` returns decoded `JoystickEvent`s.
pub fn subscribeJoystick() ?JoystickSubscriber {
return .{ .inner = subscribe(device_joystick) orelse return null };
}
// --- publishing -------------------------------------------------------------
/// A connection to the input service for a source (a keyboard/mouse/joystick driver) that
/// publishes events. Each `publish*Event` is a short synchronous call the service answers
/// at once; its own fan-out to subscribers is asynchronous, so publishing never blocks on
/// a slow subscriber.
pub const Publisher = struct { pub const Publisher = struct {
service: ipc.Handle, service: ipc.Handle,
/// Broadcast one event to every subscriber. Returns false if the call to the service fn publish(self: Publisher, event: InputEvent) bool {
/// failed (e.g. the service is gone).
pub fn publish(self: Publisher, event: KeyEvent) bool {
var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.publish), .event = event }; var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.publish), .event = event };
var reply: [protocol.reply_size]u8 = undefined; var reply: [protocol.reply_size]u8 = undefined;
const len = ipc.call(self.service, std.mem.asBytes(&request), &reply) catch return false; const len = ipc.call(self.service, std.mem.asBytes(&request), &reply) catch return false;
if (len < protocol.reply_size) return false; if (len < protocol.reply_size) return false;
return std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status == 0; return std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status == 0;
} }
/// Broadcast a keyboard event to every subscriber that took keyboard events.
pub fn publishKeyboardEvent(self: Publisher, event: KeyEvent) bool {
return self.publish(InputEvent.fromKeyboard(event));
}
/// Broadcast a mouse event to every subscriber that took mouse events.
pub fn publishMouseEvent(self: Publisher, event: MouseEvent) bool {
return self.publish(InputEvent.fromMouse(event));
}
/// Broadcast a joystick/gamepad event to every subscriber that took joystick events.
pub fn publishJoystickEvent(self: Publisher, event: JoystickEvent) bool {
return self.publish(InputEvent.fromJoystick(event));
}
}; };
/// Connect to the input service as an event source, waiting for it to come up. Returns a /// Connect to the input service as an event source, waiting for it to come up. Returns a
@ -93,13 +175,13 @@ pub fn connectSource() ?Publisher {
return .{ .service = lookupService() orelse return null }; return .{ .service = lookupService() orelse return null };
} }
/// A tiny synthetic key-event generator, shared by the demo source and the keyboard // --- synthetic scaffolding --------------------------------------------------
/// driver's placeholder stream while real scancode decoding is still a follow-up. `step`
/// is a monotonically increasing tick; the result rolls through the keys A..E, emitting /// Synthetic key events, shared by the demo source and the keyboard driver's placeholder
/// for each one a `key_down`, then a `key_press` carrying the character, then a `key_up`. /// stream while real scancode decoding is still a follow-up. `step` rolls through A..E,
/// This is deliberately not wire protocol it is scaffolding, so it lives with the /// emitting for each key a `key_down`, then a `key_press` carrying the character, then a
/// helpers, not in `input-protocol`. /// `key_up`. Scaffolding, not wire protocol hence it lives with the helpers.
pub fn syntheticEvent(step: usize) KeyEvent { pub fn syntheticKeyEvent(step: usize) KeyEvent {
const Key = struct { code: Keycode, character: u32 }; const Key = struct { code: Keycode, character: u32 };
const keys = [_]Key{ const keys = [_]Key{
.{ .code = .a, .character = 'A' }, .{ .code = .a, .character = 'A' },
@ -115,3 +197,23 @@ pub fn syntheticEvent(step: usize) KeyEvent {
else => .{ .kind = @intFromEnum(EventKind.key_up), .keycode = @intFromEnum(key.code), .character = 0, .modifiers = 0 }, else => .{ .kind = @intFromEnum(EventKind.key_up), .keycode = @intFromEnum(key.code), .character = 0, .modifiers = 0 },
}; };
} }
/// Synthetic mouse events (placeholder until real PS/2 packet decoding). `step` alternates
/// a small diagonal motion with a left-button click.
pub fn syntheticMouseEvent(step: usize) MouseEvent {
return switch (step % 3) {
0 => .{ .kind = @intFromEnum(MouseEventKind.motion), .button = 0, .dx = 1, .dy = 1, .scroll_x = 0, .scroll_y = 0, .buttons = 0 },
1 => .{ .kind = @intFromEnum(MouseEventKind.button_down), .button = protocol.mouse_button_left, .dx = 0, .dy = 0, .scroll_x = 0, .scroll_y = 0, .buttons = protocol.mouse_button_left },
else => .{ .kind = @intFromEnum(MouseEventKind.button_up), .button = protocol.mouse_button_left, .dx = 0, .dy = 0, .scroll_x = 0, .scroll_y = 0, .buttons = 0 },
};
}
/// Synthetic joystick/gamepad events (placeholder until a real controller driver). `step`
/// sweeps axis 0 and toggles button 0.
pub fn syntheticJoystickEvent(step: usize) JoystickEvent {
return switch (step % 3) {
0 => .{ .kind = @intFromEnum(JoystickEventKind.axis), .control = 0, .value = 16384, .buttons = 0 },
1 => .{ .kind = @intFromEnum(JoystickEventKind.button_down), .control = 0, .value = 0, .buttons = 1 },
else => .{ .kind = @intFromEnum(JoystickEventKind.button_up), .control = 0, .value = 0, .buttons = 0 },
};
}

View File

@ -48,7 +48,7 @@ pub fn main() void {
var step: usize = 0; var step: usize = 0;
while (true) : (step +%= 1) { while (true) : (step +%= 1) {
_ = source.publish(runtime.input.syntheticEvent(step)); _ = source.publishKeyboardEvent(runtime.input.syntheticKeyEvent(step));
runtime.system.sleep(200); runtime.system.sleep(200);
} }
} }

View File

@ -40,8 +40,21 @@ pub fn main() void {
} }
writeLine("system/drivers/ps2-bus/mouse: claimed device for hid {s}\n", .{hid}); writeLine("system/drivers/ps2-bus/mouse: claimed device for hid {s}\n", .{hid});
// Broadcast mouse events through the input service so programs can listen for them
// (docs/input.md). As with the keyboard, decoding real PS/2 mouse packets is a
// follow-up; for now we publish the synthetic stand-in stream. The fan-out path is
// real, only the source of the movement is placeholder.
var source = runtime.input.connectSource() orelse {
_ = runtime.system.write("system/drivers/ps2-bus/mouse: input service unavailable\n");
return;
};
_ = runtime.system.write("system/drivers/ps2-bus/mouse: ok\n"); _ = runtime.system.write("system/drivers/ps2-bus/mouse: ok\n");
while (true) runtime.system.sleep(1000);
var step: usize = 0;
while (true) : (step +%= 1) {
_ = source.publishMouseEvent(runtime.input.syntheticMouseEvent(step));
runtime.system.sleep(200);
}
} }
pub const panic = runtime.panic; pub const panic = runtime.panic;

View File

@ -1587,13 +1587,14 @@ fn vfsTest(boot_information: *const BootInformation) void {
result(); result();
} }
/// The full input path: spawn the input service, a synthetic keyboard source, and a /// The full input path: spawn the input service, a synthetic source, and a subscriber from
/// subscriber from the initial_ramdisk. The source publishes key events; the service /// the initial_ramdisk. The source publishes keyboard, mouse, and joystick events in turn;
/// broadcasts them (with the asynchronous ipc_send); the subscriber receives them and /// the service routes them (with the asynchronous ipc_send) to the subscriber, which took
/// only once it has heartbeats "input-test: ok". Seeing that marker proves an event /// all three classes and only once it has received one heartbeats "input-test: ok".
/// travelled source -> service -> subscriber over IPC, exercising the async buffered-send /// Seeing that marker proves an event travelled source -> service -> subscriber over IPC,
/// primitive and capability-passing subscription. The source and service stay silent /// exercising the async buffered-send primitive, capability-passing subscription, and
/// after startup so the subscriber's line is the one left in the shared evidence buffer. /// per-device routing. The source and service stay silent after startup so the subscriber's
/// line is the one left in the shared evidence buffer.
fn inputTest(boot_information: *const BootInformation) void { fn inputTest(boot_information: *const BootInformation) void {
log("DANOS-TEST-BEGIN: input\n", .{}); log("DANOS-TEST-BEGIN: input\n", .{});
if (boot_information.initial_ramdisk_len == 0) { if (boot_information.initial_ramdisk_len == 0) {

View File

@ -1,13 +1,14 @@
//! system/services/input-source a hardware-free synthetic keyboard source, used to //! system/services/input-source a hardware-free synthetic input source, used to exercise
//! exercise the input service end to end without a real PS/2 controller (the `input` test //! the input service end to end without a real PS/2 controller (the `input` test case, and
//! case, and any bring-up where there is no keyboard). It stands in for a driver: it //! any bring-up where there is no hardware). It stands in for a driver: it connects to the
//! connects to the input service and `publish`es a rolling stream of key events, which the //! input service and publishes a rolling stream that cycles through all device classes
//! service broadcasts to every subscriber. //! keyboard, mouse, and joystick/gamepad which the service routes to interested
//! subscribers.
//! //!
//! It stays silent after startup (no per-event logging) so it can share the boot serial //! It stays silent after startup (no per-event logging) so it can share the boot serial
//! transcript with a subscriber whose output is the test's success marker. The real //! transcript with a subscriber whose output is the test's success marker. The real
//! keyboard driver publishes the same synthetic stream today; swapping in decoded //! keyboard and mouse drivers publish their own synthetic streams today; swapping in
//! scancodes is a follow-up (see docs/input.md). //! decoded hardware is a follow-up (see docs/input.md).
const runtime = @import("runtime"); const runtime = @import("runtime");
const input = runtime.input; const input = runtime.input;
@ -18,11 +19,17 @@ pub fn main() void {
_ = system.write("input-source: input service unavailable\n"); _ = system.write("input-source: input service unavailable\n");
return; return;
}; };
_ = system.write("input-source: publishing synthetic key events\n"); _ = system.write("input-source: publishing synthetic input events\n");
var step: usize = 0; var step: usize = 0;
while (true) : (step +%= 1) { while (true) : (step +%= 1) {
_ = source.publish(input.syntheticEvent(step)); // Rotate across the device classes so every publish path (and the service's
// per-device routing) is exercised.
switch (step % 3) {
0 => _ = source.publishKeyboardEvent(input.syntheticKeyEvent(step)),
1 => _ = source.publishMouseEvent(input.syntheticMouseEvent(step)),
else => _ = source.publishJoystickEvent(input.syntheticJoystickEvent(step)),
}
system.sleep(200); system.sleep(200);
} }
} }

View File

@ -1,9 +1,10 @@
//! system/services/input-test the input service's client and test oracle, the input //! system/services/input-test the input service's client and test oracle, the input
//! counterpart of vfs-test. It `subscribe`s to the input service, then loops receiving the //! counterpart of vfs-test. It subscribes to *all* device classes and loops receiving the
//! events a source broadcasts. Once it has received at least one event it heartbeats //! events a source broadcasts, logging each with its class. It emits the success marker
//! `"input-test: ok"` (repeatedly), which the in-kernel `input` test case watches for on //! `"input-test: ok"` only **after it has received at least one of each class** (keyboard,
//! the serial log: seeing it proves an event travelled source -> service -> subscriber //! mouse, and joystick), then heartbeats it. So the in-kernel `input` test case seeing that
//! over IPC and arrived intact. //! marker proves not just that IPC delivery works but that the service *routed* all three
//! device classes to one subscription source -> service -> subscriber, per device.
const std = @import("std"); const std = @import("std");
const runtime = @import("runtime"); const runtime = @import("runtime");
@ -16,20 +17,36 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
} }
pub fn main() void { pub fn main() void {
var listener = input.subscribe() orelse { var listener = input.subscribeAll() orelse {
_ = system.write("input-test: could not subscribe\n"); _ = system.write("input-test: could not subscribe\n");
return; return;
}; };
_ = system.write("input-test: subscribed\n"); _ = system.write("input-test: subscribed\n");
var received: usize = 0; var seen_keyboard = false;
var seen_mouse = false;
var seen_joystick = false;
while (true) { while (true) {
const event = listener.next() orelse continue; const event = listener.next() orelse continue;
received += 1; // Decode the class-specific payload from the tagged envelope and note the class.
// Report the round trip. The kernel test matches the "input-test: ok" prefix and if (event.asKeyboard()) |key| {
// requires it to recur, so the source staying up keeps this beating. seen_keyboard = true;
const kind: input.EventKind = @enumFromInt(event.kind); writeLine("input-test: got keyboard code={d} char={d}\n", .{ key.keycode, key.character });
writeLine("input-test: ok received {d} last kind={s} code={d} char={d}\n", .{ received, @tagName(kind), event.keycode, event.character }); } else if (event.asMouse()) |mouse| {
seen_mouse = true;
writeLine("input-test: got mouse dx={d} dy={d} buttons={d}\n", .{ mouse.dx, mouse.dy, mouse.buttons });
} else if (event.asJoystick()) |joystick| {
seen_joystick = true;
writeLine("input-test: got joystick control={d} value={d}\n", .{ joystick.control, joystick.value });
} else {
writeLine("input-test: got device={d}\n", .{event.device});
}
// The success marker: only once every class has been routed here does this appear,
// and then it heartbeats. Seeing "input-test: ok" proves per-device fan-out works.
if (seen_keyboard and seen_mouse and seen_joystick) {
_ = system.write("input-test: ok all classes received (keyboard, mouse, joystick)\n");
}
} }
} }

View File

@ -1,8 +1,9 @@
//! system/services/input the user-space input service. Shipped in the initial_ramdisk, //! system/services/input the user-space input service. Shipped in the initial_ramdisk,
//! spawned as a ring-3 process, and published under the well-known `input` service id. It //! spawned as a ring-3 process, and published under the well-known `input` service id. It
//! is the fan-out point between **sources** (keyboard drivers) and **subscribers** (any //! is the fan-out point between **sources** (keyboard, mouse, and joystick/gamepad drivers)
//! program that wants keyboard events): a source `publish`es a `KeyEvent`, and the service //! and **subscribers** (any program that wants input): a source `publish`es an
//! pushes it to every subscriber. //! `InputEvent`, and the service pushes it to every subscriber whose interest mask includes
//! that event's device class (keyboard / mouse / joystick).
//! //!
//! The delivery discipline is the whole design (see docs/input.md). The kernel's IPC is a //! The delivery discipline is the whole design (see docs/input.md). The kernel's IPC is a
//! synchronous rendezvous: a server holds one pending reply, so it cannot park N //! synchronous rendezvous: a server holds one pending reply, so it cannot park N
@ -31,6 +32,9 @@ const Subscriber = struct {
used: bool = false, used: bool = false,
endpoint: ipc.Handle = 0, endpoint: ipc.Handle = 0,
task_id: u32 = 0, task_id: u32 = 0,
/// Which device classes this subscriber wants (an OR of protocol.device_*). An event
/// is delivered only if its device's bit is set here.
device_mask: u32 = 0,
}; };
var subscribers = [_]Subscriber{.{}} ** 8; var subscribers = [_]Subscriber{.{}} ** 8;
@ -56,24 +60,25 @@ fn pruneDeadSubscribers() void {
} }
} }
/// Register `endpoint` (owned by task `task_id`) to receive events. Returns false if the /// Register `endpoint` (owned by task `task_id`) to receive the device classes in
/// subscriber table is full. /// `device_mask`. Returns false if the subscriber table is full.
fn addSubscriber(endpoint: ipc.Handle, task_id: u32) bool { fn addSubscriber(endpoint: ipc.Handle, task_id: u32, device_mask: u32) bool {
for (&subscribers) |*sub| { for (&subscribers) |*sub| {
if (!sub.used) { if (!sub.used) {
sub.* = .{ .used = true, .endpoint = endpoint, .task_id = task_id }; sub.* = .{ .used = true, .endpoint = endpoint, .task_id = task_id, .device_mask = device_mask };
return true; return true;
} }
} }
return false; return false;
} }
/// Push `event` to every registered subscriber. `ipc.send` never blocks, so a slow or /// Push `event` to every subscriber whose interest mask includes its device class.
/// dead subscriber cannot stall delivery to the others. /// `ipc.send` never blocks, so a slow or dead subscriber cannot stall delivery to others.
fn broadcast(event: protocol.KeyEvent) void { fn broadcast(event: protocol.InputEvent) void {
const bytes = std.mem.asBytes(&event); const bytes = std.mem.asBytes(&event);
const bit = protocol.deviceBit(event.device);
for (&subscribers) |*sub| { for (&subscribers) |*sub| {
if (sub.used) _ = ipc.send(sub.endpoint, bytes); if (sub.used and sub.device_mask & bit != 0) _ = ipc.send(sub.endpoint, bytes);
} }
} }
@ -95,8 +100,10 @@ fn handle(message: []const u8, got: ipc.Received, out: []u8) usize {
switch (@as(protocol.Operation, @enumFromInt(request.operation))) { switch (@as(protocol.Operation, @enumFromInt(request.operation))) {
.subscribe => { .subscribe => {
const endpoint = got.cap orelse return reply.write(out, -1); // no endpoint passed const endpoint = got.cap orelse return reply.write(out, -1); // no endpoint passed
// A zero mask means "everything" (a subscriber that named no class still wants input).
const mask = if (request.device_mask == 0) protocol.device_all else request.device_mask;
pruneDeadSubscribers(); pruneDeadSubscribers();
if (!addSubscriber(endpoint, @intCast(got.badge))) return reply.write(out, -1); // table full if (!addSubscriber(endpoint, @intCast(got.badge), mask)) return reply.write(out, -1); // table full
return reply.write(out, 0); return reply.write(out, 0);
}, },
.publish => { .publish => {

View File

@ -1,49 +1,80 @@
//! The input wire protocol the message format spoken between the user-space input //! The input wire protocol the message format spoken between the user-space input
//! service ([input.zig](input.zig)) and the two kinds of process that reach it: a //! service ([input.zig](input.zig)) and the two kinds of process that reach it: a
//! **source** (a keyboard driver) that `publish`es events, and a **subscriber** (any //! **source** (a keyboard, mouse, or joystick/gamepad driver) that publishes events, and a
//! program) that `subscribe`s and is then pushed each event. //! **subscriber** (any program) that subscribes and is then pushed each event.
//! //!
//! Two message shapes ride over one endpoint, tagged by `Operation`, exactly like the //! The service handles several device classes over one endpoint. Each class has its own
//! typed event (`KeyEvent`, `MouseEvent`, `JoystickEvent`); they all travel in a common
//! `InputEvent` envelope tagged with a `DeviceKind`, so the fan-out path is one code path
//! and a subscriber can take a mix of devices on a single stream. A subscriber declares
//! which classes it wants with a `device_mask`, and the service routes accordingly.
//!
//! Two message shapes ride over the endpoint, tagged by `Operation`, like the
//! [VFS protocol](../vfs/protocol.zig): //! [VFS protocol](../vfs/protocol.zig):
//! //!
//! - **subscribe / publish**: a synchronous `ipc_call` carrying a `Request`. `subscribe` //! - **subscribe / publish**: a synchronous `ipc_call` carrying a `Request`. `subscribe`
//! hands the service the subscriber's own endpoint as a capability (`send_cap`); //! hands the service the subscriber's own endpoint as a capability (`send_cap`) and a
//! `publish` carries a `KeyEvent`. The reply is a `Reply`. //! `device_mask`; `publish` carries an `InputEvent`. The reply is a `Reply`.
//! - **delivery**: the service pushes each `KeyEvent` to every subscriber with the //! - **delivery**: the service pushes each `InputEvent` to every interested subscriber with
//! asynchronous `ipc_send` no reply owed, and a dead subscriber can never stall the //! the asynchronous `ipc_send` no reply owed, and a dead subscriber can never stall the
//! broadcast (the reason the async primitive exists). The wire form is a bare //! broadcast. Received in the subscriber's buffer with `Received.isMessage()` set.
//! `KeyEvent`, received in the subscriber's buffer with `Received.isMessage()` set.
//! //!
//! This is a danos-native contract; shared by the input service, the `runtime.input` //! This is a danos-native contract, shared by the input service, the `runtime.input`
//! client helpers, and every source/subscriber. Everything fits one IPC message. //! client helpers, and every source/subscriber. Everything fits one IPC message.
/// What happened to a key. `key_down`/`key_up` are the physical make/break; `key_press` const std = @import("std");
/// is the higher-level "a character was produced" event a source emits alongside a
/// `key_down` for keys that map to a character (carrying it in `KeyEvent.character`). /// The classes of input device the service fans out. Each names a typed event and a bit in
pub const EventKind = enum(u32) { /// the subscription mask.
key_down = 0, // a key was pressed (make) pub const DeviceKind = enum(u32) {
key_up = 1, // a key was released (break) keyboard = 0,
key_press = 2, // a character-producing press; `character` is the Unicode scalar mouse = 1,
joystick = 2, // joysticks and gamepads/controllers
}; };
/// One keyboard event, as broadcast to subscribers. Fixed layout (`extern`) because it /// Subscription-interest bits (`Request.device_mask`) which device classes a subscriber
/// crosses the IPC boundary by memory copy. A hardware-independent `keycode` names the /// wants. OR them together, or use `device_all`.
/// physical key; `character` is the Unicode scalar for `key_press` (else 0); `modifiers` pub const device_keyboard: u32 = 1 << 0;
/// is a bitmask of the shift/ctrl/alt state (`modifier_*`), 0 until a source tracks it. pub const device_mouse: u32 = 1 << 1;
pub const device_joystick: u32 = 1 << 2;
pub const device_all: u32 = device_keyboard | device_mouse | device_joystick;
/// The subscription bit for a `DeviceKind` value (as it appears in `InputEvent.device`).
/// An unknown device maps to 0, so it matches no subscriber.
pub fn deviceBit(device: u32) u32 {
return switch (device) {
@intFromEnum(DeviceKind.keyboard) => device_keyboard,
@intFromEnum(DeviceKind.mouse) => device_mouse,
@intFromEnum(DeviceKind.joystick) => device_joystick,
else => 0,
};
}
// --- keyboard ---------------------------------------------------------------
/// What happened to a key. `key_down`/`key_up` are the physical make/break; `key_press`
/// is the higher-level "a character was produced", carrying it in `KeyEvent.character`.
pub const EventKind = enum(u32) {
key_down = 0,
key_up = 1,
key_press = 2,
};
/// One keyboard event. `keycode` names the physical key (layout-independent); `character`
/// is the Unicode scalar for `key_press` (else 0); `modifiers` is an OR of `modifier_*`.
pub const KeyEvent = extern struct { pub const KeyEvent = extern struct {
kind: u32, // an EventKind kind: u32, // an EventKind
keycode: u32, // a Keycode the physical key, layout-independent keycode: u32, // a Keycode
character: u32, // Unicode scalar for key_press, else 0 character: u32, // Unicode scalar for key_press, else 0
modifiers: u32, // OR of modifier_* bits modifiers: u32, // OR of modifier_*
}; };
/// Modifier bits for `KeyEvent.modifiers`.
pub const modifier_shift: u32 = 1 << 0; pub const modifier_shift: u32 = 1 << 0;
pub const modifier_control: u32 = 1 << 1; pub const modifier_control: u32 = 1 << 1;
pub const modifier_alt: u32 = 1 << 2; pub const modifier_alt: u32 = 1 << 2;
/// A minimal danos-native keycode namespace enough for the synthetic source and to /// A minimal danos-native keycode namespace enough for the synthetic source and to show
/// show the shape. A real set (USB HID usage-style) fills in with the scancode decoder. /// the shape. A real set (USB HID usage-style) fills in with the scancode decoder.
pub const Keycode = enum(u32) { pub const Keycode = enum(u32) {
unknown = 0, unknown = 0,
a = 4, // deliberately USB-HID-usage-aligned so a real decoder can extend this a = 4, // deliberately USB-HID-usage-aligned so a real decoder can extend this
@ -55,18 +86,111 @@ pub const Keycode = enum(u32) {
_, _,
}; };
/// Which side of a request this is. // --- mouse ------------------------------------------------------------------
pub const Operation = enum(u32) {
subscribe = 0, // register the caller's endpoint (passed as send_cap) to receive events /// What a mouse event reports. `motion` carries relative `dx`/`dy`; `button_down`/`up`
publish = 1, // a source submits `event` to broadcast to every subscriber /// name a button in `button`; `scroll` carries `scroll_x`/`scroll_y`.
pub const MouseEventKind = enum(u32) {
motion = 0,
button_down = 1,
button_up = 2,
scroll = 3,
}; };
/// Request header. For `subscribe`, `event` is ignored and the caller's receive endpoint pub const mouse_button_left: u32 = 1 << 0;
/// travels as the call's capability. For `publish`, `event` is the event to broadcast. pub const mouse_button_right: u32 = 1 << 1;
pub const mouse_button_middle: u32 = 1 << 2;
/// One mouse event. Relative motion (`dx`/`dy`) and wheel (`scroll_*`) are signed;
/// `buttons` is the current pressed-button bitmask (`mouse_button_*`).
pub const MouseEvent = extern struct {
kind: u32, // a MouseEventKind
button: u32, // the mouse_button_* bit for button_down/up, else 0
dx: i32, // relative X motion (.motion)
dy: i32, // relative Y motion (.motion)
scroll_x: i32, // horizontal wheel (.scroll)
scroll_y: i32, // vertical wheel (.scroll)
buttons: u32, // current pressed-button bitmask
};
// --- joystick / gamepad -----------------------------------------------------
/// What a joystick/gamepad event reports. `axis` carries a signed `value` on axis
/// `control`; `button_down`/`up` name a button index in `control`.
pub const JoystickEventKind = enum(u32) {
axis = 0,
button_down = 1,
button_up = 2,
};
/// One joystick/gamepad event. `control` is the axis index (`.axis`) or button index
/// (button events); `value` is the axis position (signed, e.g. -32768..32767) for `.axis`;
/// `buttons` is the current pressed-button bitmask.
pub const JoystickEvent = extern struct {
kind: u32, // a JoystickEventKind
control: u32, // axis index (.axis) or button index (button events)
value: i32, // axis value for .axis, else 0
buttons: u32, // current pressed-button bitmask
};
// --- the common envelope ----------------------------------------------------
/// The largest per-device event, so `InputEvent` can hold any of them inline.
pub const max_event_size: usize = @max(@sizeOf(KeyEvent), @max(@sizeOf(MouseEvent), @sizeOf(JoystickEvent)));
/// The tagged envelope broadcast to subscribers: a `DeviceKind` plus the raw bytes of the
/// matching per-device event. Decode it with `asKeyboard`/`asMouse`/`asJoystick` (each
/// returns null unless `device` matches), or build one with the `from*` constructors.
pub const InputEvent = extern struct {
device: u32, // a DeviceKind
_padding: u32 = 0,
data: [max_event_size]u8 = [_]u8{0} ** max_event_size,
pub fn asKeyboard(self: InputEvent) ?KeyEvent {
if (self.device != @intFromEnum(DeviceKind.keyboard)) return null;
return std.mem.bytesToValue(KeyEvent, self.data[0..@sizeOf(KeyEvent)]);
}
pub fn asMouse(self: InputEvent) ?MouseEvent {
if (self.device != @intFromEnum(DeviceKind.mouse)) return null;
return std.mem.bytesToValue(MouseEvent, self.data[0..@sizeOf(MouseEvent)]);
}
pub fn asJoystick(self: InputEvent) ?JoystickEvent {
if (self.device != @intFromEnum(DeviceKind.joystick)) return null;
return std.mem.bytesToValue(JoystickEvent, self.data[0..@sizeOf(JoystickEvent)]);
}
pub fn fromKeyboard(event: KeyEvent) InputEvent {
return pack(.keyboard, std.mem.asBytes(&event));
}
pub fn fromMouse(event: MouseEvent) InputEvent {
return pack(.mouse, std.mem.asBytes(&event));
}
pub fn fromJoystick(event: JoystickEvent) InputEvent {
return pack(.joystick, std.mem.asBytes(&event));
}
fn pack(device: DeviceKind, bytes: []const u8) InputEvent {
var self = InputEvent{ .device = @intFromEnum(device) };
@memcpy(self.data[0..bytes.len], bytes);
return self;
}
};
// --- request / reply --------------------------------------------------------
/// Which side of a request this is.
pub const Operation = enum(u32) {
subscribe = 0, // register the caller's endpoint (send_cap) for the classes in device_mask
publish = 1, // a source submits `event` to broadcast to interested subscribers
};
/// Request header. For `subscribe`, `device_mask` is the OR of `device_*` bits the caller
/// wants (0 means all) and the caller's receive endpoint travels as the call's capability;
/// `event` is ignored. For `publish`, `event` is the event to broadcast.
pub const Request = extern struct { pub const Request = extern struct {
operation: u32, // an Operation operation: u32, // an Operation
_padding: u32 = 0, device_mask: u32 = 0, // subscribe: interested device classes (0 => all)
event: KeyEvent, event: InputEvent = .{ .device = 0 },
}; };
/// Reply header. `status` is 0 on success or a negative errno. /// Reply header. `status` is 0 on success or a negative errno.
@ -77,11 +201,10 @@ pub const Reply = extern struct {
pub const request_size: usize = @sizeOf(Request); pub const request_size: usize = @sizeOf(Request);
pub const reply_size: usize = @sizeOf(Reply); pub const reply_size: usize = @sizeOf(Reply);
pub const event_size: usize = @sizeOf(KeyEvent); pub const event_size: usize = @sizeOf(InputEvent);
comptime { comptime {
// The delivery path posts a bare KeyEvent through ipc_send, so it must fit an // The delivery path posts a bare InputEvent through ipc_send, so it must fit an
// endpoint's async payload slot (abi has no dependency the other way, so the bound // endpoint's async payload slot (POST_MAXIMUM is 64).
// lives here where the wire form is defined: POST_MAXIMUM is 64). if (event_size > 64) @compileError("InputEvent must fit the ipc_send payload (POST_MAXIMUM)");
if (event_size > 64) @compileError("KeyEvent must fit the ipc_send payload (POST_MAXIMUM)");
} }