Add input module: broadcast keyboard events over IPC

Programs can now subscribe to keyboard events (key_down/key_up/key_press)
and drivers can broadcast them, through a new user-space input service.

The delivery model is forced by danos IPC: a synchronous rendezvous holds
one pending reply, so a server cannot park N subscribers blocked in a
"wait for next event" call — delivery must be push. But a synchronous push
has no timeout and the kernel never wakes a sender parked on a dead peer's
endpoint, so one dying subscriber would hang all input. So this lands the
roadmap's planned asynchronous buffered send and builds the service on it:

- ipc_send (syscall 26): non-blocking post to an endpoint's bounded payload
  ring, delivered through reply_wait as a buffered message (notify_message_bit).
  A full ring drops the oldest. It can never hang on a dead/slow peer.
- input-protocol + runtime.input helpers (subscribe/next, connectSource/
  publish) — the first real consumer of M13 capability passing: a subscriber
  hands the service its own endpoint as a capability.
- input service (fan-out via ipc_send, dead-subscriber pruning), a synthetic
  input-source, and input-test; the ps2-bus keyboard driver publishes to it.
  Real IRQ1 scancode decoding (which must live in the bus, the PNP0303 owner)
  is a documented follow-up; the source is synthetic for now.
- build/init wiring, an `input` QEMU case, and docs/input.md.

Full QEMU suite 48/48, including the new input case and every IPC/endpoint
regression (ipc, ipc-call, ipc-cap, vfs, hpet, bus, irqfree).
This commit is contained in:
Daniel Samson
2026-07-11 15:03:24 +01:00
parent 2a583d55a8
commit 65244e3103
19 changed files with 765 additions and 5 deletions
+117
View File
@@ -0,0 +1,117 @@
//! 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.
//!
//! A **subscriber** does:
//! var listener = input.subscribe() orelse return;
//! while (true) { const event = listener.next() orelse continue; ... }
//!
//! A **source** (keyboard driver) does:
//! var source = input.connectSource() orelse return;
//! _ = source.publish(.{ .kind = ..., .keycode = ..., ... });
const std = @import("std");
const abi = @import("abi");
const ipc = @import("ipc.zig");
const system = @import("system.zig");
const protocol = @import("input-protocol");
pub const KeyEvent = protocol.KeyEvent;
pub const EventKind = protocol.EventKind;
pub const Keycode = protocol.Keycode;
/// 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.
fn lookupService() ?ipc.Handle {
var attempts: usize = 0;
while (attempts < 100) : (attempts += 1) {
if (ipc.lookup(.input)) |handle| return handle;
system.sleep(50);
}
return null;
}
/// A subscription to the input service: our own endpoint, which the service pushes events
/// to. Keep it and call `next` in a loop.
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).
endpoint: ipc.Handle,
receive: [protocol.event_size]u8 = undefined,
/// 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 {
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]);
}
};
/// 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 {
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 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;
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).
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 {
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;
}
};
/// Connect to the input service as an event source, waiting for it to come up. Returns a
/// `Publisher`, or null if the service never registered.
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 {
const Key = struct { code: Keycode, character: u32 };
const keys = [_]Key{
.{ .code = .a, .character = 'A' },
.{ .code = .b, .character = 'B' },
.{ .code = .c, .character = 'C' },
.{ .code = .d, .character = 'D' },
.{ .code = .e, .character = 'E' },
};
const key = keys[(step / 3) % keys.len];
return switch (step % 3) {
0 => .{ .kind = @intFromEnum(EventKind.key_down), .keycode = @intFromEnum(key.code), .character = 0, .modifiers = 0 },
1 => .{ .kind = @intFromEnum(EventKind.key_press), .keycode = @intFromEnum(key.code), .character = key.character, .modifiers = 0 },
else => .{ .kind = @intFromEnum(EventKind.key_up), .keycode = @intFromEnum(key.code), .character = 0, .modifiers = 0 },
};
}