virtio-inputd: v6.0 single-producer evdev; bump 0.1.0->0.2.3

This commit is contained in:
2026-06-09 02:58:19 +03:00
parent d6fda77672
commit 07dd9814ac
2 changed files with 69 additions and 260 deletions
@@ -1,8 +1,8 @@
[package]
name = "virtio-inputd"
version = "0.2.0"
version = "0.2.3"
edition = "2024"
description = "VirtIO input device driver for Red Bear OS (v6.0: writes Linux evdev events to /scheme/input/evdev). Handles QEMU virtio-input-host-pci, virtio-input-keyboard, virtio-input-mouse, virtio-input-tablet."
description = "virtio-input daemon v6.0 2026: reads virtio-input PCI events and writes Linux evdev events to /scheme/input/evdev"
[[bin]]
name = "virtio-inputd"
@@ -1,6 +1,6 @@
//! virtio-inputd — VirtIO input device driver for Red Bear OS
//! virtio-inputd v6.0 — VirtIO input device driver for Red Bear OS
//!
//! Handles the QEMU `virtio-input-*` paravirt input devices:
//! Drives QEMU `virtio-input-*` paravirt input devices:
//! - `-device virtio-input-host-pci` (host passthrough)
//! - `-device virtio-input-keyboard`
//! - `-device virtio-input-mouse`
@@ -8,27 +8,21 @@
//!
//! ## Pipeline
//!
//! 1. PCI probe for `vendor=0x1AF4 device=0x1052` (legacy virtio-input) or
//! `vendor=0x1AF4 device=0x1042+` (modern virtio 1.0, type=18). One daemon
//! per device — pcid-spawner launches us.
//! 2. Negotiate `VIRTIO_F_VERSION_1` (only feature we need).
//! 3. Set up one event virtqueue and pre-fill the available ring with 8 KiB
//! of DMA-allocated event buffers.
//! 4. Wait for `used_idx` to advance via IRQ. Drain used buffers, decode
//! virtio_input_event (type/code/value), translate to orbclient event,
//! write to inputd via ProducerHandle.
//! 5. Re-cycle drained buffers back to the avail ring and kick.
//! 1. PCI probe for modern virtio-input (vendor=0x1AF4, device=0x1042+).
//! pcid-spawner launches us with the PCI location as argument.
//! 2. Negotiate `VIRTIO_F_VERSION_1`.
//! 3. Set up one event virtqueue with 64 DMA-allocated event buffers.
//! 4. Drain used buffers, decode `VirtioInputEvent`, write Linux evdev events
//! to `/scheme/input/evdev` via `EvdevProducerHandle`.
//! 5. Recycle drained buffers to the avail ring and kick.
//!
//! ## Event Translation
//! ## Wire format
//!
//! virtio_input_event types map to orbclient events:
//! - EV_KEY (1) → KeyEvent (key press / release via value 0/1)
//! - EV_REL (2) → MouseRelativeEvent (dx, dy from REL_X, REL_Y)
//! - EV_SYN (0) → dropped (inputd multiplexes itself)
//! - EV_ABS / MSC / LED → dropped for now (Phase 5.2 expansion)
//!
//! Phase 5.2 (follow-up): add evdevd producer path in parallel with inputd.
//! See local/docs/CONSOLE-TO-KDE-DESKTOP-PLAN.md §5.
//! virtio-input sends `VirtioInputEvent { type: u8, code: u8, value: u32 }`.
//! We translate to Linux evdev `EvdevEvent { event_type: u16, code: u16, value: i32 }`:
//! - virtio `type` (u8) → evdev `event_type` (u16) — direct map, EV_SYN/EV_KEY/EV_REL/EV_ABS match
//! - virtio `code` (u8) → evdev `code` (u16) — zero-extend
//! - virtio `value` (u32) → evdev `value` (i32) — sign-extend for ABS, cast for others
#![forbid(unsafe_op_in_unsafe_fn)]
@@ -39,9 +33,8 @@ use std::sync::atomic::{Ordering, fence};
use std::thread;
use std::time::Duration;
use inputd::ProducerHandle;
use inputd::{EvdevProducerHandle, EV_ABS, EV_KEY, EV_REL, EV_SYN};
use log::{debug, error, info, warn};
use orbclient::{Event, KeyEvent, MouseRelativeEvent, ScrollEvent};
use redox_driver_sys::dma::DmaBuffer;
use redox_driver_sys::pcid_client::PcidClient;
use redox_driver_sys::pci::{PciDevice, PciDeviceInfo, PCI_CAP_ID_VNDR};
@@ -50,117 +43,12 @@ mod virtio;
use virtio::{
QueueConfig, VirtioInputEvent, VirtioModernPciTransport, VIRTIO_INPUT_EVENT_SIZE,
VIRTIO_INPUT_CFG_EV_BITS, VIRTIO_INPUT_CFG_ABS_INFO, VIRTIO_INPUT_CFG_ID_NAME,
VIRTIO_INPUT_CFG_ID_SERIAL, VIRTIO_INPUT_CFG_ID_DEVIDS, VIRTIO_INPUT_CFG_PROP_BITS,
VIRTIO_INPUT_CFG_ID_SERIAL,
VIRTIO_F_VERSION_1,
};
// Linux input-event-codes.h (subset we care about)
const EV_SYN: u16 = 0x00;
const EV_KEY: u16 = 0x01;
const EV_REL: u16 = 0x02;
const EV_ABS: u16 = 0x03;
const EV_MSC: u16 = 0x04;
const EV_SW: u16 = 0x05;
const EV_LED: u16 = 0x11;
const EV_SND: u16 = 0x12;
const EV_REP: u16 = 0x14;
const SYN_REPORT: u16 = 0;
const SYN_DROPPED: u16 = 3;
// REL_*
const REL_X: u16 = 0x00;
const REL_Y: u16 = 0x01;
const REL_WHEEL: u16 = 0x08;
const REL_HWHEEL: u16 = 0x06;
// KEY_*
const KEY_ESC: u16 = 1;
const KEY_1: u16 = 2;
const KEY_0: u16 = 11;
const KEY_Q: u16 = 16;
const KEY_P: u16 = 25;
const KEY_A: u16 = 30;
const KEY_L: u16 = 38;
const KEY_Z: u16 = 44;
const KEY_M: u16 = 50;
const KEY_F1: u16 = 59;
const KEY_F12: u16 = 68;
const KEY_LEFTCTRL: u16 = 29;
const KEY_LEFTALT: u16 = 56;
const KEY_LEFTSHIFT: u16 = 42;
const KEY_RIGHTSHIFT: u16 = 54;
const KEY_LEFTMETA: u16 = 91;
const KEY_RIGHTMETA: u16 = 92;
const KEY_KPENTER: u16 = 96;
const KEY_KPSLASH: u16 = 95;
const KEY_SPACE: u16 = 57;
const KEY_CAPSLOCK: u16 = 58;
const KEY_NUMLOCK: u16 = 69;
const KEY_SCROLLLOCK: u16 = 70;
const KEY_MINUS: u16 = 12;
const KEY_EQUAL: u16 = 13;
const KEY_TAB: u16 = 15;
const KEY_ENTER: u16 = 28;
const KEY_SEMICOLON: u16 = 39;
const KEY_APOSTROPHE: u16 = 40;
const KEY_GRAVE: u16 = 41;
const KEY_BACKSLASH: u16 = 43;
const KEY_COMMA: u16 = 51;
const KEY_DOT: u16 = 52;
const KEY_SLASH: u16 = 53;
const KEY_LEFTBRACE: u16 = 26;
const KEY_RIGHTBRACE: u16 = 27;
const KEY_BACKSPACE: u16 = 14;
const KEY_102ND: u16 = 86;
const KEY_RO: u16 = 89;
const KEY_KATAKANAHIRAGANA: u16 = 93;
const KEY_HENKAN: u16 = 92;
const KEY_MUHENKAN: u16 = 94;
const KEY_KPJPCOMMA: u16 = 95;
const KEY_KP7: u16 = 71;
const KEY_KP8: u16 = 72;
const KEY_KP9: u16 = 73;
const KEY_KPMINUS: u16 = 74;
const KEY_KP4: u16 = 75;
const KEY_KP5: u16 = 76;
const KEY_KP6: u16 = 77;
const KEY_KPPLUS: u16 = 78;
const KEY_KP1: u16 = 79;
const KEY_KP2: u16 = 80;
const KEY_KP3: u16 = 81;
const KEY_KP0: u16 = 82;
const KEY_KPDOT: u16 = 83;
const KEY_KPASTERISK: u16 = 55;
const KEY_KPEQUAL: u16 = 117;
const KEY_F2: u16 = 60;
const KEY_F3: u16 = 61;
const KEY_F4: u16 = 62;
const KEY_F5: u16 = 63;
const KEY_F6: u16 = 64;
const KEY_F7: u16 = 65;
const KEY_F8: u16 = 66;
const KEY_F9: u16 = 67;
const KEY_F10: u16 = 68;
const KEY_F11: u16 = 69;
const KEY_PRINT: u16 = 70;
const KEY_SCROLL: u16 = 70;
const KEY_PAUSE: u16 = 119;
const KEY_INSERT: u16 = 110;
const KEY_HOME: u16 = 102;
const KEY_PAGEUP: u16 = 104;
const KEY_DELETE: u16 = 111;
const KEY_END: u16 = 107;
const KEY_PAGEDOWN: u16 = 109;
const KEY_RIGHT: u16 = 106;
const KEY_LEFT: u16 = 105;
const KEY_DOWN: u16 = 108;
const KEY_UP: u16 = 103;
const VIRTQ_DESC_F_NEXT: u16 = 1;
const VIRTQ_DESC_F_WRITE: u16 = 2;
const VIRTQ_DESC_F_AVAIL: u16 = 4;
const VIRTQ_DESC_F_USED: u16 = 8;
#[derive(Debug)]
pub enum DriverError {
@@ -211,7 +99,7 @@ struct VirtqUsedElem {
///
/// virtio-input only needs to receive event buffers from the device, so this
/// implementation pre-allocates a ring of `size` buffers at startup. Each
/// buffer is exactly one `virtio_input_event` (8 bytes). Buffers are cycled
/// buffer is exactly one `VirtioInputEvent` (8 bytes). Buffers are cycled
/// back to the avail ring after the device has consumed them.
struct InputEventQueue {
index: u16,
@@ -272,10 +160,6 @@ impl InputEventQueue {
for i in 0..self.size {
self.push_avail(i);
}
// The device reads avail_ring[avail_idx % size] to discover new buffers.
// After writing all `size` ring slots, we MUST publish the new
// avail_idx = size, or the device will not see any of them.
// See virtio spec §2.8.6 "Publishing the used ring".
fence(Ordering::Release);
self.write_avail_idx(self.size);
}
@@ -322,8 +206,6 @@ impl InputEventQueue {
fence(Ordering::SeqCst);
let used_idx = self.read_used_idx();
// 64 is the maximum queue size we accept, so the stack array is
// always large enough for a single drain cycle.
let mut drained_ids: [u16; 64] = [0u16; 64];
let mut drained_count: usize = 0;
@@ -363,103 +245,50 @@ impl InputEventQueue {
}
}
/// Map a Linux evdev keycode to the closest character (US QWERTY layout).
/// Translate a virtio input event to evdev and write to the producer.
///
/// This is a very small subset — sufficient for QEMU virtio-input-keyboard
/// to produce useful KeyEvent::character values. Real evdev has a complex
/// keymap model; we accept the simplification that Phase 5.2 will replace
/// with the evdevd keymap bridge.
fn keycode_to_char(code: u16) -> char {
match code {
KEY_ESC => '\u{1B}',
KEY_1 => '1',
KEY_0 => '0',
KEY_Q => 'q',
KEY_P => 'p',
KEY_A => 'a',
KEY_L => 'l',
KEY_Z => 'z',
KEY_M => 'm',
KEY_MINUS => '-',
KEY_EQUAL => '=',
KEY_TAB => '\t',
KEY_SPACE => ' ',
KEY_LEFTBRACE => '[',
KEY_RIGHTBRACE => ']',
KEY_BACKSLASH => '\\',
KEY_SEMICOLON => ';',
KEY_APOSTROPHE => '\'',
KEY_GRAVE => '`',
KEY_COMMA => ',',
KEY_DOT => '.',
KEY_SLASH => '/',
KEY_ENTER => '\n',
KEY_BACKSPACE => '\u{08}',
KEY_KP0 => '0',
KEY_KP1 => '1',
KEY_KP2 => '2',
KEY_KP3 => '3',
KEY_KP4 => '4',
KEY_KP5 => '5',
KEY_KP6 => '6',
KEY_KP7 => '7',
KEY_KP8 => '8',
KEY_KP9 => '9',
KEY_KPMINUS => '-',
KEY_KPPLUS => '+',
KEY_KPDOT => '.',
KEY_KPASTERISK => '*',
KEY_KPSLASH => '/',
KEY_KPENTER => '\n',
KEY_KPEQUAL => '=',
_ => '\0',
}
}
/// virtio type (u8) → evdev event_type (u16): direct map, EV_SYN=0, EV_KEY=1, EV_REL=2, EV_ABS=3
/// virtio code (u8) → evdev code (u16): zero-extend
/// virtio value (u32) → evdev value (i32): cast (ABS sign-extends via i32)
fn write_evdev_event(producer: &mut EvdevProducerHandle, ev: &VirtioInputEvent) {
let event_type = ev.event_type as u16;
let code = ev.code as u16;
let value = ev.value as i32;
/// Translate a virtio_input_event into one or more orbclient events.
///
/// Multiple events are batched in the output vector so the caller can write
/// them all in one syscall.
fn translate_event(ev: &VirtioInputEvent) -> Vec<Event> {
match ev.event_type {
EV_SYN => Vec::new(),
EV_KEY => {
let pressed = ev.value != 0;
let character = keycode_to_char(ev.code);
vec![KeyEvent { character, scancode: ev.code as u8, pressed }.to_event()]
}
EV_REL => {
// REL_WHEEL: value is delta in 120ths of a notch; orbclient uses
// raw pixels. Clamp small positive/negative to +/-1.
match ev.code {
REL_X | REL_Y => {
vec![MouseRelativeEvent {
dx: if ev.code == REL_X { ev.value } else { 0 },
dy: if ev.code == REL_Y { ev.value } else { 0 },
}
.to_event()]
}
REL_WHEEL | REL_HWHEEL => {
let clicks = if ev.value == 0 {
0
} else if ev.value > 0 {
1
} else {
-1
};
vec![ScrollEvent {
x: if ev.code == REL_HWHEEL { clicks } else { 0 },
y: if ev.code == REL_WHEEL { clicks } else { 0 },
}
.to_event()]
}
_ => Vec::new(),
match event_type {
EV_SYN => {
if let Err(e) = producer.write_syn_report() {
warn!("virtio-inputd: write_syn_report failed: {e}");
}
}
// EV_ABS / EV_MSC / EV_LED / EV_REP / EV_SND / EV_SW are not yet
// translated — Phase 5.2 expansion. For now they are dropped, which
// is acceptable for QEMU keyboard + mouse + basic tablet.
_ => Vec::new(),
EV_KEY => {
if let Err(e) = producer.write_key(code, value != 0) {
warn!("virtio-inputd: write_key failed: {e}");
}
if let Err(e) = producer.write_syn_report() {
warn!("virtio-inputd: write_syn_report (after key) failed: {e}");
}
}
EV_REL => {
if let Err(e) = producer.write_rel(code, value) {
warn!("virtio-inputd: write_rel failed: {e}");
}
if let Err(e) = producer.write_syn_report() {
warn!("virtio-inputd: write_syn_report (after rel) failed: {e}");
}
}
EV_ABS => {
if let Err(e) = producer.write_abs(code, value) {
warn!("virtio-inputd: write_abs failed: {e}");
}
if let Err(e) = producer.write_syn_report() {
warn!("virtio-inputd: write_syn_report (after abs) failed: {e}");
}
}
// EV_MSC, EV_LED, EV_SND, EV_REP, EV_SW — dropped; not used by virtio-input devices
_ => {
debug!("virtio-inputd: dropped evdev event type={event_type} code={code} value={value}");
}
}
}
@@ -492,11 +321,8 @@ fn virtio_input_probe(pci: &mut PciDevice) -> Result<bool> {
let cap_id = pci.read_config_byte(offset as u64)?;
let cap_next = pci.read_config_byte(offset as u64 + 1)?;
if cap_id == PCI_CAP_ID_VNDR {
// cap.cfg_type is at offset 3 of the capability.
let cfg_type = pci.read_config_byte(offset as u64 + 3)?;
if cfg_type == 4 {
// cap.id is at offset 5 — for device_cfg, this is the
// virtio device type.
let dev_type = pci.read_config_byte(offset as u64 + 5)?;
if dev_type == 18 {
return Ok(true);
@@ -509,15 +335,12 @@ fn virtio_input_probe(pci: &mut PciDevice) -> Result<bool> {
}
fn run_device() -> Result<()> {
// Connect to pcid via the env var it provides. If absent, we cannot run.
if PcidClient::connect_default().is_none() {
return Err(DriverError::Initialization(
"virtio-inputd: not launched by pcid-spawner (PCID_CLIENT_CHANNEL unset)".into(),
));
}
// The pcid-spawner also passes the PCI location as a positional argument
// before PCID_CLIENT_CHANNEL. Format: "segment:bus:device.function".
let args: Vec<String> = env::args().skip(1).collect();
let loc_str = args.first().ok_or_else(|| {
DriverError::Initialization(
@@ -565,10 +388,6 @@ fn run_device() -> Result<()> {
let mut transport = VirtioModernPciTransport::new(&pci_info, &mut pci_device)?;
transport.initialize_device(VIRTIO_F_VERSION_1)?;
// Wrap remaining init in a closure so any error resets the device
// to a clean state. virtio 1.0 §2.1.6: a driver that fails to
// complete initialization MUST reset the device so a future
// attach (e.g. after driver-manager restart) starts cleanly.
let init_result = (|| -> Result<()> {
let num_queues = transport.num_queues();
if num_queues < 1 {
@@ -606,11 +425,9 @@ fn run_device() -> Result<()> {
if transport.config_read_size() != 0 {
let _ = transport.config_read_string(serial_buf.len(), &mut serial_buf);
}
let _ = std::str::from_utf8(&serial_buf[..]);
// Probe EV bits to log a summary
let mut ev_bits = [0u8; 16];
let mut abs_count = 0u8;
for ev_type in 0u8..16u8 {
transport.config_write_select(VIRTIO_INPUT_CFG_EV_BITS, ev_type);
let size = transport.config_read_size() as usize;
@@ -622,8 +439,8 @@ fn run_device() -> Result<()> {
debug!("virtio-inputd: device supports EV type {ev_type}");
}
}
// Probe ABS bits to count absolute axes. absinfo is 5*u32=20 bytes;
// a non-zero size response indicates the axis is supported.
// Probe ABS bits to count absolute axes.
let mut abs_count = 0u8;
for abs in 0u8..64u8 {
transport.config_write_select(VIRTIO_INPUT_CFG_ABS_INFO, abs);
if transport.config_read_size() >= 20 {
@@ -638,12 +455,12 @@ fn run_device() -> Result<()> {
device_name, event_qcfg.size, abs_count
);
// Open the inputd producer handle for event delivery.
let mut producer = match ProducerHandle::new() {
// Open the v6.0 evdev producer handle for event delivery.
let mut producer = match EvdevProducerHandle::new() {
Ok(p) => p,
Err(e) => {
warn!("virtio-inputd: failed to open /scheme/input/producer: {e} — events will be dropped");
return Err(DriverError::Io(format!("inputd producer unavailable: {e}")));
warn!("virtio-inputd: failed to open /scheme/input/evdev: {e} — events will be dropped");
return Err(DriverError::Io(format!("evdev producer unavailable: {e}")));
}
};
@@ -662,10 +479,9 @@ fn run_device() -> Result<()> {
fn run_event_loop(
transport: &mut VirtioModernPciTransport,
event_queue: &mut InputEventQueue,
producer: &mut ProducerHandle,
producer: &mut EvdevProducerHandle,
) {
let mut pending_events: Vec<VirtioInputEvent> = Vec::with_capacity(64);
let mut translated: Vec<Event> = Vec::with_capacity(16);
loop {
if transport.device_in_error_state() {
warn!("virtio-inputd: device entered FAILED or NEEDS_RESET state, exiting");
@@ -675,15 +491,8 @@ fn run_event_loop(
event_queue.drain(&mut pending_events);
if !pending_events.is_empty() {
for ev in &pending_events {
translated.clear();
translated.extend(translate_event(ev));
for event in &translated {
if let Err(e) = producer.write_event(*event) {
warn!("virtio-inputd: write_event failed: {e}");
}
}
write_evdev_event(producer, ev);
}
pending_events.clear();
if let Err(e) = transport.notify_queue(event_queue.index, event_queue.notify_off) {
warn!("virtio-inputd: notify_queue failed: {e}");
}
@@ -704,4 +513,4 @@ fn main() {
error!("virtio-inputd: fatal: {e:?}");
process::exit(1);
}
}
}