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:
parent
65244e3103
commit
1bf91115dd
|
|
@ -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
|
||||
supervision link as the kill authority, and child-exit notifications over the
|
||||
same endpoints IRQs arrive on.
|
||||
16. **[input.md](input.md) — the input module.** Broadcasting keyboard events: why a
|
||||
synchronous rendezvous can't fan out to many listeners, the asynchronous `ipc_send`
|
||||
primitive built to fix it, and the subscribe/publish service layered on top.
|
||||
16. **[input.md](input.md) — the input module.** Broadcasting input events (keyboard,
|
||||
mouse, joystick): why a synchronous rendezvous can't fan out to many listeners, the
|
||||
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
|
||||
how `while (true) hlt` parks the CPU safely once there's nothing left to do.
|
||||
|
||||
|
|
|
|||
106
docs/input.md
106
docs/input.md
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,25 @@
|
|||
//! 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
|
||||
//! hand-roll the IPC. Layered over `ipc` (endpoints, capability passing, `send`) and the
|
||||
//! shared `input-protocol` wire format, the same way `device.zig` layers over the raw
|
||||
//! `device_*` calls. See system/services/input/input.zig.
|
||||
//! program listening for input events — or a driver broadcasting them — doesn't hand-roll
|
||||
//! the IPC. Layered over `ipc` (endpoints, capability passing, `send`) and the shared
|
||||
//! `input-protocol` wire format, the way `device.zig` layers over the raw `device_*` calls.
|
||||
//! See system/services/input/input.zig.
|
||||
//!
|
||||
//! A **subscriber** does:
|
||||
//! var listener = input.subscribe() orelse return;
|
||||
//! while (true) { const event = listener.next() orelse continue; ... }
|
||||
//!
|
||||
//! A **source** (keyboard driver) does:
|
||||
//! The service carries several device classes (keyboard, mouse, joystick/gamepad). A
|
||||
//! **source** publishes its class with the matching method:
|
||||
//! 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 abi = @import("abi");
|
||||
|
|
@ -18,10 +27,22 @@ const ipc = @import("ipc.zig");
|
|||
const system = @import("system.zig");
|
||||
const protocol = @import("input-protocol");
|
||||
|
||||
pub const DeviceKind = protocol.DeviceKind;
|
||||
pub const InputEvent = protocol.InputEvent;
|
||||
pub const KeyEvent = protocol.KeyEvent;
|
||||
pub const MouseEvent = protocol.MouseEvent;
|
||||
pub const JoystickEvent = protocol.JoystickEvent;
|
||||
pub const EventKind = protocol.EventKind;
|
||||
pub const MouseEventKind = protocol.MouseEventKind;
|
||||
pub const JoystickEventKind = protocol.JoystickEventKind;
|
||||
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
|
||||
/// 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.
|
||||
|
|
@ -34,8 +55,12 @@ fn lookupService() ?ipc.Handle {
|
|||
return null;
|
||||
}
|
||||
|
||||
// --- subscribing ------------------------------------------------------------
|
||||
|
||||
/// 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 {
|
||||
/// 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).
|
||||
|
|
@ -44,47 +69,104 @@ pub const Subscriber = struct {
|
|||
|
||||
/// 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
|
||||
/// empty reply this issues is a harmless no-op (a pure subscriber holds no client).
|
||||
/// Returns null for any non-event wake-up (there should be none), so callers can loop.
|
||||
pub fn next(self: *Subscriber) ?KeyEvent {
|
||||
/// empty reply this issues is a harmless no-op. Returns null for any non-event wake-up
|
||||
/// (there should be none), so callers can loop.
|
||||
pub fn next(self: *Subscriber) ?InputEvent {
|
||||
const got = ipc.replyWait(self.endpoint, &.{}, &self.receive, 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
|
||||
/// it over as a capability. Returns a `Subscriber` to loop `next` on, or null on failure
|
||||
/// (the service never came up, out of handles, or the subscribe call failed).
|
||||
pub fn subscribe() ?Subscriber {
|
||||
/// Subscribe to the input classes named in `device_mask` (an OR of `device_*`, or
|
||||
/// `device_all`). Creates an endpoint for the service to push to and hands it over as a
|
||||
/// capability. Returns a `Subscriber` to loop `next` on, or null on failure.
|
||||
pub fn subscribe(device_mask: u32) ?Subscriber {
|
||||
const service = lookupService() 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;
|
||||
const result = ipc.callCap(service, std.mem.asBytes(&request), &reply, endpoint) catch return null;
|
||||
if (result.len < protocol.reply_size) return null;
|
||||
const header = std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]);
|
||||
if (header.status != 0) return null;
|
||||
if (std.mem.bytesToValue(protocol.Reply, reply[0..protocol.reply_size]).status != 0) return null;
|
||||
return .{ .endpoint = endpoint };
|
||||
}
|
||||
|
||||
/// A connection to the input service for a source (a keyboard driver) that publishes
|
||||
/// events. Cheap to hold; `publish` 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).
|
||||
/// Subscribe to every input class (keyboard, mouse, joystick) on one stream.
|
||||
pub fn subscribeAll() ?Subscriber {
|
||||
return subscribe(device_all);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
service: ipc.Handle,
|
||||
|
||||
/// Broadcast one event to every subscriber. Returns false if the call to the service
|
||||
/// failed (e.g. the service is gone).
|
||||
pub fn publish(self: Publisher, event: KeyEvent) bool {
|
||||
fn publish(self: Publisher, event: InputEvent) bool {
|
||||
var request = protocol.Request{ .operation = @intFromEnum(protocol.Operation.publish), .event = event };
|
||||
var reply: [protocol.reply_size]u8 = undefined;
|
||||
const len = ipc.call(self.service, std.mem.asBytes(&request), &reply) catch return false;
|
||||
if (len < protocol.reply_size) return false;
|
||||
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
|
||||
|
|
@ -93,13 +175,13 @@ pub fn connectSource() ?Publisher {
|
|||
return .{ .service = lookupService() orelse return null };
|
||||
}
|
||||
|
||||
/// A tiny synthetic key-event generator, shared by the demo source and the keyboard
|
||||
/// 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
|
||||
/// for each one a `key_down`, then a `key_press` carrying the character, then a `key_up`.
|
||||
/// This is deliberately not wire protocol — it is scaffolding, so it lives with the
|
||||
/// helpers, not in `input-protocol`.
|
||||
pub fn syntheticEvent(step: usize) KeyEvent {
|
||||
// --- synthetic scaffolding --------------------------------------------------
|
||||
|
||||
/// Synthetic key events, shared by the demo source and the keyboard driver's placeholder
|
||||
/// stream while real scancode decoding is still a follow-up. `step` rolls through A..E,
|
||||
/// emitting for each key a `key_down`, then a `key_press` carrying the character, then a
|
||||
/// `key_up`. Scaffolding, not wire protocol — hence it lives with the helpers.
|
||||
pub fn syntheticKeyEvent(step: usize) KeyEvent {
|
||||
const Key = struct { code: Keycode, character: u32 };
|
||||
const keys = [_]Key{
|
||||
.{ .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 },
|
||||
};
|
||||
}
|
||||
|
||||
/// 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 },
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ pub fn main() void {
|
|||
|
||||
var step: usize = 0;
|
||||
while (true) : (step +%= 1) {
|
||||
_ = source.publish(runtime.input.syntheticEvent(step));
|
||||
_ = source.publishKeyboardEvent(runtime.input.syntheticKeyEvent(step));
|
||||
runtime.system.sleep(200);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,8 +40,21 @@ pub fn main() void {
|
|||
}
|
||||
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");
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1587,13 +1587,14 @@ fn vfsTest(boot_information: *const BootInformation) void {
|
|||
result();
|
||||
}
|
||||
|
||||
/// The full input path: spawn the input service, a synthetic keyboard source, and a
|
||||
/// subscriber from the initial_ramdisk. The source publishes key events; the service
|
||||
/// broadcasts them (with the asynchronous ipc_send); the subscriber receives them and —
|
||||
/// only once it has — heartbeats "input-test: ok". Seeing that marker proves an event
|
||||
/// travelled source -> service -> subscriber over IPC, exercising the async buffered-send
|
||||
/// primitive and capability-passing subscription. The source and service stay silent
|
||||
/// after startup so the subscriber's line is the one left in the shared evidence buffer.
|
||||
/// The full input path: spawn the input service, a synthetic source, and a subscriber from
|
||||
/// the initial_ramdisk. The source publishes keyboard, mouse, and joystick events in turn;
|
||||
/// the service routes them (with the asynchronous ipc_send) to the subscriber, which took
|
||||
/// all three classes and — only once it has received one — heartbeats "input-test: ok".
|
||||
/// Seeing that marker proves an event travelled source -> service -> subscriber over IPC,
|
||||
/// exercising the async buffered-send primitive, capability-passing subscription, and
|
||||
/// 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 {
|
||||
log("DANOS-TEST-BEGIN: input\n", .{});
|
||||
if (boot_information.initial_ramdisk_len == 0) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
//! system/services/input-source — a hardware-free synthetic keyboard source, used to
|
||||
//! exercise the input service end to end without a real PS/2 controller (the `input` test
|
||||
//! case, and any bring-up where there is no keyboard). It stands in for a driver: it
|
||||
//! connects to the input service and `publish`es a rolling stream of key events, which the
|
||||
//! service broadcasts to every subscriber.
|
||||
//! system/services/input-source — a hardware-free synthetic input source, used to exercise
|
||||
//! the input service end to end without a real PS/2 controller (the `input` test case, and
|
||||
//! any bring-up where there is no hardware). It stands in for a driver: it connects to the
|
||||
//! input service and publishes a rolling stream that cycles through all device classes —
|
||||
//! 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
|
||||
//! 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
|
||||
//! scancodes is a follow-up (see docs/input.md).
|
||||
//! keyboard and mouse drivers publish their own synthetic streams today; swapping in
|
||||
//! decoded hardware is a follow-up (see docs/input.md).
|
||||
|
||||
const runtime = @import("runtime");
|
||||
const input = runtime.input;
|
||||
|
|
@ -18,11 +19,17 @@ pub fn main() void {
|
|||
_ = system.write("input-source: input service unavailable\n");
|
||||
return;
|
||||
};
|
||||
_ = system.write("input-source: publishing synthetic key events\n");
|
||||
_ = system.write("input-source: publishing synthetic input events\n");
|
||||
|
||||
var step: usize = 0;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
//! 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
|
||||
//! events a source broadcasts. Once it has received at least one event it heartbeats
|
||||
//! `"input-test: ok"` (repeatedly), which the in-kernel `input` test case watches for on
|
||||
//! the serial log: seeing it proves an event travelled source -> service -> subscriber
|
||||
//! over IPC and arrived intact.
|
||||
//! counterpart of vfs-test. It subscribes to *all* device classes and loops receiving the
|
||||
//! events a source broadcasts, logging each with its class. It emits the success marker
|
||||
//! `"input-test: ok"` only **after it has received at least one of each class** (keyboard,
|
||||
//! mouse, and joystick), then heartbeats it. So the in-kernel `input` test case seeing that
|
||||
//! 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 runtime = @import("runtime");
|
||||
|
|
@ -16,20 +17,36 @@ fn writeLine(comptime fmt: []const u8, arguments: anytype) void {
|
|||
}
|
||||
|
||||
pub fn main() void {
|
||||
var listener = input.subscribe() orelse {
|
||||
var listener = input.subscribeAll() orelse {
|
||||
_ = system.write("input-test: could not subscribe\n");
|
||||
return;
|
||||
};
|
||||
_ = system.write("input-test: subscribed\n");
|
||||
|
||||
var received: usize = 0;
|
||||
var seen_keyboard = false;
|
||||
var seen_mouse = false;
|
||||
var seen_joystick = false;
|
||||
while (true) {
|
||||
const event = listener.next() orelse continue;
|
||||
received += 1;
|
||||
// Report the round trip. The kernel test matches the "input-test: ok" prefix and
|
||||
// requires it to recur, so the source staying up keeps this beating.
|
||||
const kind: input.EventKind = @enumFromInt(event.kind);
|
||||
writeLine("input-test: ok received {d} last kind={s} code={d} char={d}\n", .{ received, @tagName(kind), event.keycode, event.character });
|
||||
// Decode the class-specific payload from the tagged envelope and note the class.
|
||||
if (event.asKeyboard()) |key| {
|
||||
seen_keyboard = true;
|
||||
writeLine("input-test: got keyboard code={d} char={d}\n", .{ key.keycode, key.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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
//! 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
|
||||
//! is the fan-out point between **sources** (keyboard drivers) and **subscribers** (any
|
||||
//! program that wants keyboard events): a source `publish`es a `KeyEvent`, and the service
|
||||
//! pushes it to every subscriber.
|
||||
//! is the fan-out point between **sources** (keyboard, mouse, and joystick/gamepad drivers)
|
||||
//! and **subscribers** (any program that wants input): a source `publish`es an
|
||||
//! `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
|
||||
//! synchronous rendezvous: a server holds one pending reply, so it cannot park N
|
||||
|
|
@ -31,6 +32,9 @@ const Subscriber = struct {
|
|||
used: bool = false,
|
||||
endpoint: ipc.Handle = 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;
|
||||
|
|
@ -56,24 +60,25 @@ fn pruneDeadSubscribers() void {
|
|||
}
|
||||
}
|
||||
|
||||
/// Register `endpoint` (owned by task `task_id`) to receive events. Returns false if the
|
||||
/// subscriber table is full.
|
||||
fn addSubscriber(endpoint: ipc.Handle, task_id: u32) bool {
|
||||
/// Register `endpoint` (owned by task `task_id`) to receive the device classes in
|
||||
/// `device_mask`. Returns false if the subscriber table is full.
|
||||
fn addSubscriber(endpoint: ipc.Handle, task_id: u32, device_mask: u32) bool {
|
||||
for (&subscribers) |*sub| {
|
||||
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 false;
|
||||
}
|
||||
|
||||
/// Push `event` to every registered subscriber. `ipc.send` never blocks, so a slow or
|
||||
/// dead subscriber cannot stall delivery to the others.
|
||||
fn broadcast(event: protocol.KeyEvent) void {
|
||||
/// Push `event` to every subscriber whose interest mask includes its device class.
|
||||
/// `ipc.send` never blocks, so a slow or dead subscriber cannot stall delivery to others.
|
||||
fn broadcast(event: protocol.InputEvent) void {
|
||||
const bytes = std.mem.asBytes(&event);
|
||||
const bit = protocol.deviceBit(event.device);
|
||||
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))) {
|
||||
.subscribe => {
|
||||
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();
|
||||
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);
|
||||
},
|
||||
.publish => {
|
||||
|
|
|
|||
|
|
@ -1,49 +1,80 @@
|
|||
//! 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
|
||||
//! **source** (a keyboard driver) that `publish`es events, and a **subscriber** (any
|
||||
//! program) that `subscribe`s and is then pushed each event.
|
||||
//! **source** (a keyboard, mouse, or joystick/gamepad driver) that publishes events, and a
|
||||
//! **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):
|
||||
//!
|
||||
//! - **subscribe / publish**: a synchronous `ipc_call` carrying a `Request`. `subscribe`
|
||||
//! hands the service the subscriber's own endpoint as a capability (`send_cap`);
|
||||
//! `publish` carries a `KeyEvent`. The reply is a `Reply`.
|
||||
//! - **delivery**: the service pushes each `KeyEvent` to every subscriber with 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
|
||||
//! `KeyEvent`, received in the subscriber's buffer with `Received.isMessage()` set.
|
||||
//! hands the service the subscriber's own endpoint as a capability (`send_cap`) and a
|
||||
//! `device_mask`; `publish` carries an `InputEvent`. The reply is a `Reply`.
|
||||
//! - **delivery**: the service pushes each `InputEvent` to every interested subscriber with
|
||||
//! the asynchronous `ipc_send` — no reply owed, and a dead subscriber can never stall the
|
||||
//! broadcast. 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.
|
||||
|
||||
/// What happened to a key. `key_down`/`key_up` are the physical make/break; `key_press`
|
||||
/// 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`).
|
||||
pub const EventKind = enum(u32) {
|
||||
key_down = 0, // a key was pressed (make)
|
||||
key_up = 1, // a key was released (break)
|
||||
key_press = 2, // a character-producing press; `character` is the Unicode scalar
|
||||
const std = @import("std");
|
||||
|
||||
/// The classes of input device the service fans out. Each names a typed event and a bit in
|
||||
/// the subscription mask.
|
||||
pub const DeviceKind = enum(u32) {
|
||||
keyboard = 0,
|
||||
mouse = 1,
|
||||
joystick = 2, // joysticks and gamepads/controllers
|
||||
};
|
||||
|
||||
/// One keyboard event, as broadcast to subscribers. Fixed layout (`extern`) because it
|
||||
/// crosses the IPC boundary by memory copy. A hardware-independent `keycode` names the
|
||||
/// physical key; `character` is the Unicode scalar for `key_press` (else 0); `modifiers`
|
||||
/// is a bitmask of the shift/ctrl/alt state (`modifier_*`), 0 until a source tracks it.
|
||||
/// Subscription-interest bits (`Request.device_mask`) — which device classes a subscriber
|
||||
/// wants. OR them together, or use `device_all`.
|
||||
pub const device_keyboard: u32 = 1 << 0;
|
||||
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 {
|
||||
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
|
||||
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_control: u32 = 1 << 1;
|
||||
pub const modifier_alt: u32 = 1 << 2;
|
||||
|
||||
/// A minimal danos-native keycode namespace — enough for the synthetic source and to
|
||||
/// show the shape. A real set (USB HID usage-style) fills in with the scancode decoder.
|
||||
/// A minimal danos-native keycode namespace — enough for the synthetic source and to show
|
||||
/// the shape. A real set (USB HID usage-style) fills in with the scancode decoder.
|
||||
pub const Keycode = enum(u32) {
|
||||
unknown = 0,
|
||||
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.
|
||||
pub const Operation = enum(u32) {
|
||||
subscribe = 0, // register the caller's endpoint (passed as send_cap) to receive events
|
||||
publish = 1, // a source submits `event` to broadcast to every subscriber
|
||||
// --- mouse ------------------------------------------------------------------
|
||||
|
||||
/// What a mouse event reports. `motion` carries relative `dx`/`dy`; `button_down`/`up`
|
||||
/// 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
|
||||
/// travels as the call's capability. For `publish`, `event` is the event to broadcast.
|
||||
pub const mouse_button_left: u32 = 1 << 0;
|
||||
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 {
|
||||
operation: u32, // an Operation
|
||||
_padding: u32 = 0,
|
||||
event: KeyEvent,
|
||||
device_mask: u32 = 0, // subscribe: interested device classes (0 => all)
|
||||
event: InputEvent = .{ .device = 0 },
|
||||
};
|
||||
|
||||
/// 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 reply_size: usize = @sizeOf(Reply);
|
||||
pub const event_size: usize = @sizeOf(KeyEvent);
|
||||
pub const event_size: usize = @sizeOf(InputEvent);
|
||||
|
||||
comptime {
|
||||
// The delivery path posts a bare KeyEvent through ipc_send, so it must fit an
|
||||
// endpoint's async payload slot (abi has no dependency the other way, so the bound
|
||||
// lives here where the wire form is defined: POST_MAXIMUM is 64).
|
||||
if (event_size > 64) @compileError("KeyEvent must fit the ipc_send payload (POST_MAXIMUM)");
|
||||
// The delivery path posts a bare InputEvent through ipc_send, so it must fit an
|
||||
// endpoint's async payload slot (POST_MAXIMUM is 64).
|
||||
if (event_size > 64) @compileError("InputEvent must fit the ipc_send payload (POST_MAXIMUM)");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue