Files
RedBear-OS/local/recipes/drivers/virtio-inputd/source/src/virtio.rs
T
vasilito 19a9eecb54 virtio-inputd: implement Phase 5.1 virtio-input driver
Add a real, QEMU-targeted virtio-input driver as a new Red Bear recipe at
local/recipes/drivers/virtio-inputd/. The driver handles virtio-input-host-pci,
virtio-input-keyboard, virtio-input-mouse, and virtio-input-tablet devices and
closes Gap #19 of the v5.0 desktop plan.

The driver:

  * Walks the PCI capability list to find the modern virtio 1.0 capability
    block (common_cfg, notify_cfg, isr_cfg, device_cfg) using MmioRegion
    mappings via redox-driver-sys. Rejects legacy virtio-input (device 0x1052)
    which lacks the modern transport.
  * Negotiates VIRTIO_F_VERSION_1 only (the only required feature).
  * Allocates one event virtqueue (size up to 64) backed by four DMA buffers
    (desc, avail, used, event_buffers) and pre-fills the avail ring.
  * Polls the used ring at 60 Hz, drains completed events, decodes each
    virtio_input_event (8-byte type/code/value), and recycles drained buffers
    back to the avail ring.
  * Translates events to orbclient format and pushes them to inputd via
    ProducerHandle (Orbital path):
      - EV_KEY  -> KeyEvent  (with US-QWERTY character mapping)
      - EV_REL  -> MouseRelativeEvent (REL_X/REL_Y) or ScrollEvent (REL_WHEEL)
      - EV_SYN  -> dropped (inputd multiplexes)
      - Other   -> dropped (Phase 5.2 will add evdevd path)

Probe-time checks:

  * Vendor 0x1AF4, device_id >= 0x1042, revision >= 1
  * Caps include a device_cfg block with virtio type == 18 (virtio_input)

Configuration: a pcid-spawner fragment is added to config/redbear-full.toml
under /etc/pcid.d/virtio-inputd.toml matching class=0x09 vendor=0x1AF4 with
device_id_range 0x1042..=0x107F (and a separate 0x1052 entry that the driver
intentionally rejects).

Verification: cargo check produces 0 errors and 65 warnings, all of which are
unused input-event-codes.h constants reserved for the Phase 5.2 expansion.
Linking the binary requires the Redox cross-toolchain (relibc provides
redox_sys_call_v0); this is provided by the build system, not the host
toolchain.

Plan: this is Phase 5.1 of CONSOLE-TO-KDE-DESKTOP-PLAN.md v5.0. The plan is
updated to v5.1 with: (a) a 'What Changed Since v5.0' section, (b) Gap #19
marked DONE, (c) Phase 5 row marked DONE with sub-task status, (d) Gate E
updated, (e) Input pipeline section updated to reflect the (c) path is now
implemented. Phase 5.2 (evdevd producer path + virtio-snd) is documented as
the next planned work but not yet implemented.
2026-06-08 22:18:00 +03:00

510 lines
17 KiB
Rust

//! VirtIO input device protocol definitions and modern PCI transport.
//!
//! Reference: Linux 7.1 drivers/virtio/virtio_input.c
//! Linux 7.1 include/uapi/linux/virtio_input.h
//!
//! virtio-input is a paravirt input device used by QEMU. QEMU options:
//! -device virtio-input-host-pci (passthrough host input)
//! -device virtio-input-keyboard
//! -device virtio-input-mouse
//! -device virtio-input-tablet
//!
//! The device uses a single event virtqueue (no status queue) and config-space
//! introspection to advertise supported event types and absolute axis ranges.
use log::{debug, info};
use redox_driver_sys::memory::{CacheType, MmioProt, MmioRegion};
use redox_driver_sys::pci::{PciDevice, PciDeviceInfo, PCI_CAP_ID_VNDR};
use crate::DriverError;
use crate::Result;
const VIRTIO_PCI_CAP_COMMON_CFG: u8 = 1;
const VIRTIO_PCI_CAP_NOTIFY_CFG: u8 = 2;
const VIRTIO_PCI_CAP_ISR_CFG: u8 = 3;
const VIRTIO_PCI_CAP_DEVICE_CFG: u8 = 4;
const DEVICE_STATUS_ACKNOWLEDGE: u8 = 0x01;
const DEVICE_STATUS_DRIVER: u8 = 0x02;
const DEVICE_STATUS_DRIVER_OK: u8 = 0x04;
const DEVICE_STATUS_FEATURES_OK: u8 = 0x08;
const DEVICE_STATUS_FAILED: u8 = 0x80;
const COMMON_DEVICE_FEATURE_SELECT: usize = 0x00;
const COMMON_DEVICE_FEATURE: usize = 0x04;
const COMMON_DRIVER_FEATURE_SELECT: usize = 0x08;
const COMMON_DRIVER_FEATURE: usize = 0x0C;
const COMMON_MSIX_CONFIG: usize = 0x10;
const COMMON_NUM_QUEUES: usize = 0x12;
const COMMON_DEVICE_STATUS: usize = 0x14;
const COMMON_QUEUE_SELECT: usize = 0x16;
const COMMON_QUEUE_SIZE: usize = 0x18;
const COMMON_QUEUE_MSIX_VECTOR: usize = 0x1A;
const COMMON_QUEUE_ENABLE: usize = 0x1C;
const COMMON_QUEUE_NOTIFY_OFF: usize = 0x1E;
const COMMON_QUEUE_DESC_LO: usize = 0x20;
const COMMON_QUEUE_DESC_HI: usize = 0x24;
const COMMON_QUEUE_AVAIL_LO: usize = 0x28;
const COMMON_QUEUE_AVAIL_HI: usize = 0x2C;
const COMMON_QUEUE_USED_LO: usize = 0x30;
const COMMON_QUEUE_USED_HI: usize = 0x34;
const COMMON_CFG_REQUIRED_BYTES: usize = COMMON_QUEUE_USED_HI + core::mem::size_of::<u32>();
const ISR_STATUS_OFFSET: usize = 0;
const ISR_CFG_REQUIRED_BYTES: usize = ISR_STATUS_OFFSET + core::mem::size_of::<u8>();
const NOTIFY_CFG_REQUIRED_BYTES: usize = core::mem::size_of::<u16>();
// virtio_input.h enums
pub const VIRTIO_INPUT_CFG_UNSET: u8 = 0x00;
pub const VIRTIO_INPUT_CFG_ID_NAME: u8 = 0x01;
pub const VIRTIO_INPUT_CFG_ID_SERIAL: u8 = 0x02;
pub const VIRTIO_INPUT_CFG_ID_DEVIDS: u8 = 0x03;
pub const VIRTIO_INPUT_CFG_PROP_BITS: u8 = 0x10;
pub const VIRTIO_INPUT_CFG_EV_BITS: u8 = 0x11;
pub const VIRTIO_INPUT_CFG_ABS_INFO: u8 = 0x12;
// virtio_input_event is 8 bytes (type: u16, code: u16, value: u32)
pub const VIRTIO_INPUT_EVENT_SIZE: usize = 8;
pub const VIRTIO_INPUT_CONFIG_SIZE: usize = 40; // select(1) + subsel(1) + size(1) + reserved(5) + payload(32) = 40
/// Required feature bit: VIRTIO_F_VERSION_1 (bit 32).
pub const VIRTIO_F_VERSION_1: u64 = 1u64 << 32;
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct VirtioPciCap {
cap_vndr: u8,
cap_next: u8,
cap_len: u8,
cfg_type: u8,
bar: u8,
id: u8,
padding: [u8; 2],
offset: u32,
length: u32,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Default)]
struct VirtioPciNotifyCap {
cap: VirtioPciCap,
notify_off_multiplier: u32,
}
fn pci_error(e: redox_driver_sys::DriverError) -> DriverError {
DriverError::Pci(format!("{e}"))
}
fn read_pci_cap(pci: &mut PciDevice, offset: u8) -> Result<VirtioPciCap> {
let mut raw = [0u8; 16];
for (i, byte) in raw.iter_mut().enumerate() {
*byte = pci.read_config_byte(offset as u64 + i as u64)?;
}
Ok(VirtioPciCap {
cap_vndr: raw[0],
cap_next: raw[1],
cap_len: raw[2],
cfg_type: raw[3],
bar: raw[4],
id: raw[5],
padding: [raw[6], raw[7]],
offset: u32::from_le_bytes([raw[8], raw[9], raw[10], raw[11]]),
length: u32::from_le_bytes([raw[12], raw[13], raw[14], raw[15]]),
})
}
fn read_notify_cap(pci: &mut PciDevice, offset: u8) -> Result<VirtioPciNotifyCap> {
let mut raw = [0u8; 20];
for (i, byte) in raw.iter_mut().enumerate() {
*byte = pci.read_config_byte(offset as u64 + i as u64)?;
}
let cap = VirtioPciCap {
cap_vndr: raw[0],
cap_next: raw[1],
cap_len: raw[2],
cfg_type: raw[3],
bar: raw[4],
id: raw[5],
padding: [raw[6], raw[7]],
offset: u32::from_le_bytes([raw[8], raw[9], raw[10], raw[11]]),
length: u32::from_le_bytes([raw[12], raw[13], raw[14], raw[15]]),
};
let notify_off_multiplier = u32::from_le_bytes([raw[16], raw[17], raw[18], raw[19]]);
Ok(VirtioPciNotifyCap { cap, notify_off_multiplier })
}
fn map_cap_region(
info: &PciDeviceInfo,
cap: &VirtioPciCap,
label: &'static str,
min_bytes: usize,
) -> Result<MmioRegion> {
if cap.length < min_bytes as u32 {
return Err(DriverError::Initialization(format!(
"VirtIO input {label} cap length {min_bytes} required, got {}",
cap.length
)));
}
let bar = info.bars.get(cap.bar as usize).ok_or_else(|| {
DriverError::Pci(format!(
"VirtIO input {label}: BAR index {} out of range",
cap.bar
))
})?;
let (phys_addr, bar_size) = bar
.memory_info()
.ok_or_else(|| DriverError::Pci(format!("VirtIO input {label}: BAR not memory")))?;
MmioRegion::map(
phys_addr + cap.offset as u64,
cap.length as usize,
CacheType::Uncacheable,
MmioProt::READ_WRITE,
)
.map_err(|e| DriverError::Mmio(format!("virtio-inputd: failed to map {label}: {e}")))
}
#[derive(Debug)]
pub struct QueueConfig {
pub index: u16,
pub size: u16,
pub notify_off: u16,
}
pub struct VirtioModernPciTransport {
common_cfg: MmioRegion,
notify_cfg: MmioRegion,
isr_cfg: MmioRegion,
device_cfg: MmioRegion,
notify_off_multiplier: u32,
}
impl VirtioModernPciTransport {
pub fn new(info: &PciDeviceInfo, pci: &mut PciDevice) -> Result<Self> {
let mut common_cap = None;
let mut notify_cap = None;
let mut isr_cap = None;
let mut device_cap = None;
let cap_ptr = pci.read_config_byte(0x34)?;
if cap_ptr == 0 {
return Err(DriverError::Initialization(
"VirtIO input has no PCI capabilities".into(),
));
}
let mut offset = cap_ptr;
let mut visited = 0u8;
const MAX_CAPS: u8 = 48;
while offset != 0 && visited < MAX_CAPS {
visited += 1;
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 {
let raw = read_pci_cap(pci, offset)?;
match raw.cfg_type {
VIRTIO_PCI_CAP_COMMON_CFG => common_cap = Some(raw),
VIRTIO_PCI_CAP_NOTIFY_CFG => notify_cap = Some(read_notify_cap(pci, offset)?),
VIRTIO_PCI_CAP_ISR_CFG => isr_cap = Some(raw),
VIRTIO_PCI_CAP_DEVICE_CFG => device_cap = Some(raw),
_ => {}
}
}
offset = cap_next;
}
info!(
"virtio-inputd: VirtIO PCI capability scan found {} caps, common={} notify={} isr={} device={}",
visited,
common_cap.is_some(),
notify_cap.is_some(),
isr_cap.is_some(),
device_cap.is_some(),
);
let common_cap = common_cap
.ok_or_else(|| DriverError::Initialization("VirtIO input missing common_cfg".into()))?;
let notify_cap = notify_cap
.ok_or_else(|| DriverError::Initialization("VirtIO input missing notify_cfg".into()))?;
let isr_cap = isr_cap
.ok_or_else(|| DriverError::Initialization("VirtIO input missing isr_cfg".into()))?;
let device_cap = device_cap
.ok_or_else(|| DriverError::Initialization("VirtIO input missing device_cfg".into()))?;
let common_cfg = map_cap_region(info, &common_cap, "common_cfg", COMMON_CFG_REQUIRED_BYTES)?;
let notify_cfg = map_cap_region(
info,
&notify_cap.cap,
"notify_cfg",
NOTIFY_CFG_REQUIRED_BYTES,
)?;
let isr_cfg = map_cap_region(info, &isr_cap, "isr_cfg", ISR_CFG_REQUIRED_BYTES)?;
let device_cfg = map_cap_region(info, &device_cap, "device_cfg", VIRTIO_INPUT_CONFIG_SIZE)?;
info!(
"virtio-inputd: VirtIO PCI transport mapped for {} (notify multiplier {})",
info.location, notify_cap.notify_off_multiplier
);
Ok(Self {
common_cfg,
notify_cfg,
isr_cfg,
device_cfg,
notify_off_multiplier: notify_cap.notify_off_multiplier,
})
}
pub fn initialize_device(&mut self, requested_features: u64) -> Result<u64> {
debug!("virtio-inputd: VirtIO reset device");
self.write_device_status(0);
self.write_device_status(DEVICE_STATUS_ACKNOWLEDGE);
self.write_device_status(DEVICE_STATUS_ACKNOWLEDGE | DEVICE_STATUS_DRIVER);
let available = self.read_device_features();
if (available & requested_features) & VIRTIO_F_VERSION_1 == 0 {
self.fail(format!(
"VirtIO input missing VIRTIO_F_VERSION_1 (device features={available:#x})"
))?;
}
let negotiated = available & requested_features;
self.write_driver_features(negotiated);
let mut status = self.device_status();
status |= DEVICE_STATUS_FEATURES_OK;
self.write_device_status(status);
if self.device_status() & DEVICE_STATUS_FEATURES_OK == 0 {
self.fail("VirtIO input rejected FEATURES_OK during negotiation".into())?;
}
info!("virtio-inputd: VirtIO negotiated features device={available:#x} driver={negotiated:#x}");
Ok(negotiated)
}
pub fn finalize_device(&mut self) {
let status = self.device_status() | DEVICE_STATUS_DRIVER_OK;
self.write_device_status(status);
}
pub fn device_status(&self) -> u8 {
self.common_cfg.read8(COMMON_DEVICE_STATUS)
}
pub fn read_isr_status(&mut self) -> u8 {
self.isr_cfg.read8(ISR_STATUS_OFFSET)
}
pub fn num_queues(&self) -> u16 {
self.common_cfg.read16(COMMON_NUM_QUEUES)
}
pub fn prepare_queue(&self, index: u16, requested_size: u16) -> Result<QueueConfig> {
self.select_queue(index);
let device_size = self.common_cfg.read16(COMMON_QUEUE_SIZE);
if device_size == 0 {
return Err(DriverError::Initialization(format!(
"VirtIO input queue {index} reports size 0"
)));
}
let size = device_size.min(requested_size);
let notify_off = self.common_cfg.read16(COMMON_QUEUE_NOTIFY_OFF);
Ok(QueueConfig {
index,
size,
notify_off,
})
}
pub fn activate_queue(
&self,
index: u16,
size: u16,
desc_addr: u64,
avail_addr: u64,
used_addr: u64,
msix_vector: Option<u16>,
) -> Result<()> {
self.select_queue(index);
self.common_cfg.write16(COMMON_QUEUE_SIZE, size);
self.common_cfg
.write16(COMMON_QUEUE_MSIX_VECTOR, msix_vector.unwrap_or(u16::MAX));
self.write_u64_pair(COMMON_QUEUE_DESC_LO, COMMON_QUEUE_DESC_HI, desc_addr);
self.write_u64_pair(COMMON_QUEUE_AVAIL_LO, COMMON_QUEUE_AVAIL_HI, avail_addr);
self.write_u64_pair(COMMON_QUEUE_USED_LO, COMMON_QUEUE_USED_HI, used_addr);
self.common_cfg.write16(COMMON_QUEUE_ENABLE, 1);
if self.common_cfg.read16(COMMON_QUEUE_ENABLE) != 1 {
return Err(DriverError::Initialization(format!(
"VirtIO input queue {index} refused queue_enable"
)));
}
Ok(())
}
pub fn set_config_msix_vector(&self, vector: Option<u16>) {
self.common_cfg
.write16(COMMON_MSIX_CONFIG, vector.unwrap_or(u16::MAX));
}
pub fn notify_queue(&self, queue_index: u16, notify_off: u16) -> Result<()> {
let byte_offset = usize::from(notify_off)
.checked_mul(self.notify_off_multiplier as usize)
.ok_or_else(|| DriverError::Mmio("VirtIO notify offset overflow".into()))?;
let end = byte_offset
.checked_add(core::mem::size_of::<u16>())
.ok_or_else(|| DriverError::Mmio("VirtIO notify MMIO overflow".into()))?;
if end > self.notify_cfg.size() {
return Err(DriverError::Mmio(format!(
"VirtIO input queue notify outside notify_cfg window: end={end:#x} size={:#x}",
self.notify_cfg.size()
)));
}
self.notify_cfg.write16(byte_offset, queue_index);
Ok(())
}
// Config-space read helpers (used to enumerate device capabilities)
pub fn config_write_select(&mut self, select: u8, subsel: u8) {
self.device_cfg.write8(0, select);
self.device_cfg.write8(1, subsel);
}
pub fn config_read_size(&self) -> u8 {
self.device_cfg.read8(2)
}
pub fn config_read_string(&mut self, max_len: usize, out: &mut [u8]) -> usize {
// Force a size re-read after the select write above
let _ = self.config_read_size();
let cap = self.device_cfg.size().min(out.len());
for i in 0..cap.min(max_len) {
out[i] = self.device_cfg.read8(8 + i);
}
cap
}
pub fn config_read_bitmap(&mut self, max_len: usize, out: &mut [u8]) -> usize {
let cap = self.device_cfg.size().min(out.len());
for i in 0..cap.min(max_len) {
out[i] = self.device_cfg.read8(8 + i);
}
cap
}
pub fn config_read_absinfo(&mut self, abs_code: u8) -> AbsInfo {
self.config_write_select(VIRTIO_INPUT_CFG_ABS_INFO, abs_code);
let min = read_le32(&mut self.device_cfg, 8);
let max = read_le32(&mut self.device_cfg, 12);
let fuzz = read_le32(&mut self.device_cfg, 16);
let flat = read_le32(&mut self.device_cfg, 20);
let res = read_le32(&mut self.device_cfg, 24);
AbsInfo { min, max, fuzz, flat, res }
}
pub fn config_read_devids(&mut self) -> Option<DevIds> {
self.config_write_select(VIRTIO_INPUT_CFG_ID_DEVIDS, 0);
if self.config_read_size() < 8 {
return None;
}
let bustype = read_le16(&mut self.device_cfg, 8);
let vendor = read_le16(&mut self.device_cfg, 10);
let product = read_le16(&mut self.device_cfg, 12);
let version = read_le16(&mut self.device_cfg, 14);
Some(DevIds { bustype, vendor, product, version })
}
fn fail<T>(&mut self, reason: String) -> Result<T> {
let status = self.device_status() | DEVICE_STATUS_FAILED;
self.write_device_status(status);
Err(DriverError::Initialization(reason))
}
fn read_device_features(&self) -> u64 {
self.common_cfg.write32(COMMON_DEVICE_FEATURE_SELECT, 0);
let low = self.common_cfg.read32(COMMON_DEVICE_FEATURE) as u64;
self.common_cfg.write32(COMMON_DEVICE_FEATURE_SELECT, 1);
let high = self.common_cfg.read32(COMMON_DEVICE_FEATURE) as u64;
low | (high << 32)
}
fn write_driver_features(&self, features: u64) {
self.common_cfg.write32(COMMON_DRIVER_FEATURE_SELECT, 0);
self.common_cfg
.write32(COMMON_DRIVER_FEATURE, features as u32);
self.common_cfg.write32(COMMON_DRIVER_FEATURE_SELECT, 1);
self.common_cfg
.write32(COMMON_DRIVER_FEATURE, (features >> 32) as u32);
}
fn write_device_status(&mut self, status: u8) {
self.common_cfg.write8(COMMON_DEVICE_STATUS, status);
}
fn select_queue(&self, index: u16) {
self.common_cfg.write16(COMMON_QUEUE_SELECT, index);
}
fn write_u64_pair(&self, lo: usize, hi: usize, value: u64) {
self.common_cfg.write32(lo, value as u32);
self.common_cfg.write32(hi, (value >> 32) as u32);
}
}
/// virtio_input_absinfo (Linux include/uapi/linux/virtio_input.h)
#[derive(Clone, Copy, Debug, Default)]
pub struct AbsInfo {
pub min: u32,
pub max: u32,
pub fuzz: u32,
pub flat: u32,
pub res: u32,
}
/// virtio_input_devids
#[derive(Clone, Copy, Debug, Default)]
pub struct DevIds {
pub bustype: u16,
pub vendor: u16,
pub product: u16,
pub version: u16,
}
/// A decoded virtio_input_event (8 bytes from the wire).
///
/// Wire layout per Linux include/uapi/linux/virtio_input.h:
/// struct virtio_input_event {
/// __le16 type;
/// __le16 code;
/// __le32 value;
/// };
#[derive(Clone, Copy, Debug, Default)]
pub struct VirtioInputEvent {
pub event_type: u16,
pub code: u16,
pub value: i32,
}
impl VirtioInputEvent {
pub fn read_le(buf: &[u8; VIRTIO_INPUT_EVENT_SIZE]) -> Self {
Self {
event_type: u16::from_le_bytes([buf[0], buf[1]]),
code: u16::from_le_bytes([buf[2], buf[3]]),
value: i32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
}
}
}
fn read_le16(mmio: &mut MmioRegion, offset: usize) -> u16 {
let b0 = mmio.read8(offset);
let b1 = mmio.read8(offset + 1);
u16::from_le_bytes([b0, b1])
}
fn read_le32(mmio: &mut MmioRegion, offset: usize) -> u32 {
let b0 = mmio.read8(offset);
let b1 = mmio.read8(offset + 1);
let b2 = mmio.read8(offset + 2);
let b3 = mmio.read8(offset + 3);
u32::from_le_bytes([b0, b1, b2, b3])
}