diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index 8465a3c7d..b6a29db2e 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -23,8 +23,8 @@ jobs: - name: Clippy (tdx) run: cargo clippy --locked --features tdx -- -D warnings - - name: Clippy (net+blk+gpu+snd) - run: cargo clippy --locked --features net,blk,gpu,snd -- -D warnings + - name: Clippy (net+blk+gpu+snd+input) + run: cargo clippy --locked --features net,blk,gpu,snd,input -- -D warnings code-quality-linux-aarch64: name: libkrun (Linux aarch64) @@ -41,8 +41,8 @@ jobs: - name: Clippy (default) run: cargo clippy --locked -- -D warnings - - name: Clippy (net+blk+gpu+snd) - run: cargo clippy --locked --features net,blk,gpu,snd -- -D warnings + - name: Clippy (net+blk+gpu+snd+input) + run: cargo clippy --locked --features net,blk,gpu,snd,input -- -D warnings code-quality-macos: name: libkrun (macOS aarch64) diff --git a/Cargo.lock b/Cargo.lock index d2af589f5..2fce0ef82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -431,6 +431,7 @@ dependencies = [ "hvf", "imago", "krun_display", + "krun_input", "kvm-bindings", "kvm-ioctls", "libc", @@ -850,6 +851,18 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "krun_input" +version = "0.1.0" +dependencies = [ + "bindgen 0.72.0", + "bitflags 2.9.1", + "libc", + "log", + "static_assertions", + "thiserror 2.0.12", +] + [[package]] name = "kvm-bindings" version = "0.12.0" @@ -898,6 +911,7 @@ dependencies = [ "env_logger", "hvf", "krun_display", + "krun_input", "kvm-bindings", "kvm-ioctls", "libc", @@ -1884,6 +1898,7 @@ dependencies = [ "kbs-types", "kernel", "krun_display", + "krun_input", "kvm-bindings", "kvm-ioctls", "libc", diff --git a/Cargo.toml b/Cargo.toml index a2ccd0335..3519338f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["src/libkrun"] +members = ["src/libkrun", "src/krun_input"] exclude = ["examples/gtk_display"] resolver = "2" diff --git a/Makefile b/Makefile index e1141625b..9b9eb80e0 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ LIBRARY_HEADER = include/libkrun.h LIBRARY_HEADER_DISPLAY = include/libkrun_display.h +LIBRARY_HEADER_INPUT = include/libkrun_input.h ABI_VERSION=1 FULL_VERSION=1.15.1 @@ -58,6 +59,9 @@ endif ifeq ($(SND),1) FEATURE_FLAGS += --features snd endif +ifeq ($(INPUT),1) + FEATURE_FLAGS += --features input +endif ifeq ($(NITRO),1) VARIANT = -nitro FEATURE_FLAGS := --features nitro @@ -147,6 +151,7 @@ install: libkrun.pc install -d $(DESTDIR)$(PREFIX)/include install -m 644 $(LIBRARY_HEADER) $(DESTDIR)$(PREFIX)/include install -m 644 $(LIBRARY_HEADER_DISPLAY) $(DESTDIR)$(PREFIX)/include + install -m 644 $(LIBRARY_HEADER_INPUT) $(DESTDIR)$(PREFIX)/include install -m 644 libkrun.pc $(DESTDIR)$(PREFIX)/$(LIBDIR_$(OS))/pkgconfig install -m 755 $(LIBRARY_RELEASE_$(OS)) $(DESTDIR)$(PREFIX)/$(LIBDIR_$(OS))/ cd $(DESTDIR)$(PREFIX)/$(LIBDIR_$(OS))/ ; ln -sf $(KRUN_BINARY_$(OS)) $(KRUN_SONAME_$(OS)) ; ln -sf $(KRUN_SONAME_$(OS)) $(KRUN_BASE_$(OS)) diff --git a/examples/Cargo.lock b/examples/Cargo.lock index 1fb08deb0..d8142cc16 100644 --- a/examples/Cargo.lock +++ b/examples/Cargo.lock @@ -618,6 +618,7 @@ dependencies = [ "crossbeam-channel", "gtk4", "krun_display", + "krun_input", "libc", "log", "utils", @@ -633,6 +634,7 @@ dependencies = [ "env_logger", "gtk_display", "krun-sys", + "krun_input", "log", "regex", ] @@ -717,6 +719,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "krun_input" +version = "0.1.0" +dependencies = [ + "bindgen 0.72.0", + "bitflags 2.9.1", + "libc", + "log", + "static_assertions", + "thiserror", +] + [[package]] name = "kvm-bindings" version = "0.13.0" diff --git a/examples/external_kernel b/examples/external_kernel new file mode 100755 index 000000000..846d3cd8f Binary files /dev/null and b/examples/external_kernel differ diff --git a/examples/gui_vm/Cargo.toml b/examples/gui_vm/Cargo.toml index 7e9d57a01..f2b33290d 100644 --- a/examples/gui_vm/Cargo.toml +++ b/examples/gui_vm/Cargo.toml @@ -6,6 +6,7 @@ edition = "2024" [dependencies] gtk_display = { path = "../krun_gtk_display" } krun-sys = { path = "../../krun-sys" } +krun_input = { path = "../../src/krun_input" } anyhow = "1.0.98" clap = "4.5.39" clap_derive = "4.5.32" diff --git a/examples/gui_vm/src/main.rs b/examples/gui_vm/src/main.rs index 4decbe222..660a2e21a 100644 --- a/examples/gui_vm/src/main.rs +++ b/examples/gui_vm/src/main.rs @@ -1,17 +1,29 @@ use clap::Parser; use clap_derive::Parser; -use gtk_display::DisplayBackendHandle; +use gtk_display::{ + Axis, DisplayBackendHandle, DisplayInputOptions, InputBackendHandle, TouchArea, + TouchScreenOptions, +}; + use krun_sys::{ + KRUN_LOG_LEVEL_TRACE, KRUN_LOG_LEVEL_WARN, KRUN_LOG_STYLE_ALWAYS, KRUN_LOG_TARGET_DEFAULT, VIRGLRENDERER_RENDER_SERVER, VIRGLRENDERER_THREAD_SYNC, VIRGLRENDERER_USE_ASYNC_FENCE_CB, - VIRGLRENDERER_USE_EGL, VIRGLRENDERER_VENUS, krun_add_display, krun_create_ctx, - krun_display_set_dpi, krun_display_set_physical_size, krun_display_set_refresh_rate, - krun_set_display_backend, krun_set_exec, krun_set_gpu_options, krun_set_log_level, - krun_set_root, krun_start_enter, + VIRGLRENDERER_USE_EGL, VIRGLRENDERER_VENUS, krun_add_display, krun_add_input_device, + krun_add_input_device_fd, krun_create_ctx, krun_display_set_dpi, + krun_display_set_physical_size, krun_display_set_refresh_rate, krun_init_log, + krun_set_display_backend, krun_set_exec, krun_set_gpu_options2, krun_set_root, + krun_set_vm_config, krun_start_enter, }; use log::LevelFilter; use regex::{Captures, Regex}; use std::ffi::{CString, c_void}; use std::fmt::Display; +use std::fs::{File, OpenOptions}; +use std::mem::size_of_val; + +use anyhow::Context; +use std::os::fd::IntoRawFd; +use std::path::PathBuf; use std::process::exit; use std::ptr::null; use std::str::FromStr; @@ -32,6 +44,7 @@ struct DisplayArg { height: u32, refresh_rate: Option, physical_size: Option, + touch: bool, } /// Parses a display settings string. @@ -39,12 +52,12 @@ struct DisplayArg { fn parse_display(display_string: &str) -> Result { static RE: LazyLock = LazyLock::new(|| { Regex::new( - r"^(?P\d+)x(?P\d+)(?:@(?P\d+))?(?::(?P\d+)dpi|:(?P\d+)x(?P\d+)mm)?$", + r"^(?P\d+)x(?P\d+)(?:@(?P\d+))?(?::(?P\d+)dpi|:(?P\d+)x(?P\d+)mm)?(?P\+touch(screen)?)?$", ).unwrap() }); let captures = RE.captures(display_string).ok_or_else(|| { - format!("Invalid display string '{display_string}' format. Examples of valid values:\n '1920x1080', '1920x1080@60', '1920x1080:162x91mm', '1920x1080:300dpi', '1920x1080@90:300dpi'") + format!("Invalid display string '{display_string}' format. Examples of valid values:\n '1920x1080', '1920x1080+touch','1920x1080@60', '1920x1080:162x91mm', '1920x1080:300dpi', '1920x1080@90:300dpi+touch'") })?; fn parse_group(captures: &Captures, name: &str) -> Result, String> @@ -78,48 +91,86 @@ fn parse_display(display_string: &str) -> Result { (None, None, None) => None, _ => unreachable!("regex bug"), }, + touch: captures.name("touch").is_some(), }) } #[derive(Parser, Debug)] struct Args { #[arg(long)] - root_dir: Option, + root_dir: CString, executable: Option, argv: Vec, + // Display specifications in the format WIDTHxHEIGHT[@FPS][:DPIdpi|:PHYSICAL_WIDTHxPHYSICAL_HEIGHTmm] #[clap(long, value_parser = parse_display)] display: Vec, + + /// Attach a virtual keyboard input device + #[arg(long)] + keyboard_input: bool, + + /// Pipe (or file) where to write log (with terminal color formatting) + #[arg(long)] + color_log: Option, + + /// Passthrough an input device (e.g. /dev/input/event0) + #[arg(long)] + input: Vec, } -fn krun_thread(args: &Args, display_backend_handle: DisplayBackendHandle) -> anyhow::Result<()> { +fn krun_thread( + args: &Args, + display_backend_handle: DisplayBackendHandle, + input_device_handles: Vec, +) -> anyhow::Result<()> { unsafe { - krun_call!(krun_set_log_level(3))?; + if let Some(path) = &args.color_log { + krun_call!(krun_init_log( + OpenOptions::new() + .write(true) + .open(path) + .context("Failed to open log output")? + .into_raw_fd(), + KRUN_LOG_LEVEL_TRACE, + KRUN_LOG_STYLE_ALWAYS, + 0 + ))?; + } else { + krun_call!(krun_init_log( + KRUN_LOG_TARGET_DEFAULT, + KRUN_LOG_LEVEL_WARN, + 0, + 0, + ))?; + } + let ctx = krun_call_u32!(krun_create_ctx())?; - krun_call!(krun_set_gpu_options( + krun_call!(krun_set_vm_config(ctx, 4, 4096))?; + + krun_call!(krun_set_gpu_options2( ctx, VIRGLRENDERER_USE_EGL | VIRGLRENDERER_VENUS | VIRGLRENDERER_RENDER_SERVER | VIRGLRENDERER_THREAD_SYNC - | VIRGLRENDERER_USE_ASYNC_FENCE_CB + | VIRGLRENDERER_USE_ASYNC_FENCE_CB, + 4096 ))?; - if let Some(root_dir) = &args.root_dir { - krun_call!(krun_set_root(ctx, root_dir.as_ptr()))?; - // Executable variable should be set if we have root_dir, this is verified by clap - let executable = args.executable.as_ref().unwrap().as_ptr(); - let argv: Vec<_> = args.argv.iter().map(|a| a.as_ptr()).collect(); - let argv_ptr = if argv.is_empty() { - null() - } else { - argv.as_ptr() - }; - let envp = [null()]; - krun_call!(krun_set_exec(ctx, executable, argv_ptr, envp.as_ptr()))?; - } + krun_call!(krun_set_root(ctx, args.root_dir.as_ptr()))?; + + let executable = args.executable.as_ref().unwrap().as_ptr(); + let argv: Vec<_> = args.argv.iter().map(|a| a.as_ptr()).collect(); + let argv_ptr = if argv.is_empty() { + null() + } else { + argv.as_ptr() + }; + let envp = [null()]; + krun_call!(krun_set_exec(ctx, executable, argv_ptr, envp.as_ptr()))?; for display in &args.display { let display_id = krun_call_u32!(krun_add_display(ctx, display.width, display.height))?; @@ -144,21 +195,76 @@ fn krun_thread(args: &Args, display_backend_handle: DisplayBackendHandle) -> any &raw const display_backend as *const c_void, size_of_val(&display_backend), ))?; + + for input in &args.input { + let fd = File::open(input) + .with_context(|| format!("Failed to open input device {input:?}"))? + .into_raw_fd(); + krun_call!(krun_add_input_device_fd(ctx, fd)) + .context("Failed to attach input device")?; + } + + // Configure all input devices + for handle in &input_device_handles { + let config_backend = handle.get_config(); + let event_provider_backend = handle.get_events(); + + krun_call!(krun_add_input_device( + ctx, + &raw const config_backend as *const c_void, + size_of_val(&config_backend), + &raw const event_provider_backend as *const c_void, + size_of_val(&event_provider_backend), + ))?; + } + krun_call!(krun_start_enter(ctx))?; }; Ok(()) } fn main() -> anyhow::Result<()> { - env_logger::builder().filter_level(LevelFilter::Info).init(); + env_logger::builder() + .filter_level(LevelFilter::Debug) + .init(); let args = Args::parse(); - let (display_backend, display_worker) = - gtk_display::crate_display("libkrun examples/gui_vm".to_string()); + let mut per_display_inputs = vec![vec![]; args.display.len()]; + for (idx, display) in args.display.iter().enumerate() { + if display.touch { + per_display_inputs[idx].push(DisplayInputOptions::TouchScreen(TouchScreenOptions { + // There is no specific reason for these axis sizes, just picked what my + // physical hardware had + area: TouchArea { + x: Axis { + max: 13764, + res: 40, + fuzz: 40, + ..Default::default() + }, + y: Axis { + max: 7740, + res: 40, + fuzz: 40, + ..Default::default() + }, + }, + emit_mt: true, + emit_non_mt: false, + triggered_by_mouse: true, + })); + } + } + + let (display_backend, input_backends, display_worker) = gtk_display::init( + "libkrun examples/gui_vm".to_string(), + args.keyboard_input, + per_display_inputs, + )?; thread::scope(|s| { s.spawn(|| { - if let Err(e) = krun_thread(&args, display_backend) { + if let Err(e) = krun_thread(&args, display_backend, input_backends) { eprintln!("{e}"); exit(1); } diff --git a/examples/krun_gtk_display/Cargo.toml b/examples/krun_gtk_display/Cargo.toml index eb924e5db..f6f90ae97 100644 --- a/examples/krun_gtk_display/Cargo.toml +++ b/examples/krun_gtk_display/Cargo.toml @@ -6,9 +6,10 @@ edition = "2024" [dependencies] utils = { path = "../../src/utils" } # Our version of rust vmm-sys-util -crossbeam-channel = "0.5.15" gtk = { version = "0.10", package = "gtk4", features = ["v4_16"] } krun_display = { path = "../../src/krun_display" } +krun_input = { path = "../../src/krun_input" } anyhow = "1.0.98" log = "0.4.27" libc = "0.2.174" +crossbeam-channel = "0.5.15" diff --git a/examples/krun_gtk_display/src/display_worker.rs b/examples/krun_gtk_display/src/display_worker.rs index f2cb0a4af..010d84d2e 100644 --- a/examples/krun_gtk_display/src/display_worker.rs +++ b/examples/krun_gtk_display/src/display_worker.rs @@ -1,24 +1,312 @@ +use super::scanout_paintable::ScanoutPaintable; +use crate::{Axis, DisplayEvent, DisplayInputOptions, TouchArea, TouchScreenOptions}; +use krun_display::Rect; +use krun_input::{InputEvent, InputEventType}; +use log::{debug, trace, warn}; use std::cell::RefCell; +use std::collections::HashSet; +use std::iter; use std::os::fd::AsRawFd; use std::rc::Rc; +use std::time::Duration; -use super::scanout_paintable::ScanoutPaintable; -use crate::DisplayEvent; -use krun_display::Rect; -use log::{debug, trace, warn}; -use utils::pollable_channel::PollableChannelReciever; +use utils::pollable_channel::{PollableChannelReciever, PollableChannelSender}; +use crate::input_backend::{MAX_FINGERS, gtk_keycode_to_linux}; +use crate::input_constants::{ + ABS_MT_POSITION_X, ABS_MT_POSITION_Y, ABS_MT_SLOT, ABS_MT_TRACKING_ID, ABS_X, ABS_Y, BTN_TOUCH, + SYN_REPORT, +}; use gtk::{ - AlertDialog, Align, Application, ApplicationWindow, Button, EventControllerMotion, HeaderBar, - Overlay, Picture, Revealer, RevealerTransitionType, Window, gdk, - gdk::MemoryFormat, + AlertDialog, Align, Application, ApplicationWindow, Button, EventControllerKey, + EventControllerLegacy, EventControllerMotion, HeaderBar, Overlay, Picture, Revealer, + RevealerTransitionType, Widget, Window, + gdk::{self, EventSequence, EventType, MemoryFormat, ModifierType, TouchEvent}, gio::ActionEntry, gio::Cancellable, - glib::{self, Bytes, ControlFlow, IOCondition, Propagation, unix_fd_add_local}, + glib::{ + self, Bytes, ControlFlow, IOCondition, Propagation, clone::Downgrade, + timeout_add_local_once, unix_fd_add_local, + }, + graphene::Point, prelude::*, }; use krun_display::MAX_DISPLAYS; +type EventSender = PollableChannelSender; + +#[derive(Debug)] +struct FingerState { + seq: Option, + tracking_id: Option, + pos: Option<(u32, u32)>, +} + +#[derive(Debug, Default)] +struct FingerTracker { + fingers: [Option; MAX_FINGERS], +} + +impl FingerTracker { + fn track(&mut self, seq: Option) -> (u16, &mut FingerState) { + let mut finger_idx = 0; + let mut found_empty_slot = false; + if let Some(seq) = &seq { + for (idx, f) in self.fingers.iter_mut().enumerate() { + match f { + Some(s) if s.seq.as_ref() == Some(seq) => { + finger_idx = idx; + break; + } + None if !found_empty_slot => { + finger_idx = idx; + found_empty_slot = true; + } + _ => continue, + } + } + } + + match self.fingers[finger_idx] { + Some(ref mut finger_state) => (finger_idx as u16, finger_state), + None => { + let finger_state = self.fingers[finger_idx].insert(FingerState { + seq: seq.clone(), + tracking_id: None, + pos: None, + }); + (finger_idx as u16, finger_state) + } + } + } + + fn get_by_id(&self, finger_idx: u16) -> Option<&FingerState> { + self.fingers[finger_idx as usize].as_ref() + } + + fn delete_by_idx(&mut self, finger_idx: u16) { + self.fingers[finger_idx as usize] = None; + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum TouchState { + Begin, + Update, + End, +} + +struct TouchEventSequencedSender { + fingers: FingerTracker, + synced_finger_zero_pos: (u32, u32), + last_tracking_id: u16, + active_finger_idx: u16, + options: TouchScreenOptions, + queue: Vec, + tx: EventSender, + requested_deferred_sync: bool, +} + +impl TouchEventSequencedSender { + fn new(tx: EventSender, options: TouchScreenOptions) -> Self { + Self { + fingers: Default::default(), + synced_finger_zero_pos: (0, 0), + last_tracking_id: 0, + active_finger_idx: u16::MAX, + options, + queue: Vec::new(), + tx, + requested_deferred_sync: true, + } + } + + fn sync(&mut self) { + if self.queue.is_empty() { + return; + } + self.requested_deferred_sync = false; + + let pending_events = [const { + InputEvent { + type_: 0, + code: 0, + value: 0, + } + }; 2]; + let mut pending_events_len = 0; + + if self.options.emit_non_mt + && let Some(finger_pos) = self.fingers.get_by_id(0).and_then(|f| f.pos) + { + if finger_pos.0 != self.synced_finger_zero_pos.0 { + self.queue.push(InputEvent { + type_: InputEventType::Abs as u16, + code: ABS_X, + value: finger_pos.0, + }); + pending_events_len += 1; + } + + if finger_pos.1 != self.synced_finger_zero_pos.1 { + self.queue.push(InputEvent { + type_: InputEventType::Abs as u16, + code: ABS_Y, + value: finger_pos.1, + }); + pending_events_len += 1; + } + self.synced_finger_zero_pos = finger_pos; + } + + let final_sync_event = iter::once(InputEvent { + type_: InputEventType::Syn as u16, + code: SYN_REPORT, + value: 0, + }); + + let input_events = self.queue.drain(..); + + let iter = pending_events[..pending_events_len].iter().copied(); + self.tx + .send_many(input_events.chain(final_sync_event).chain(iter)) + .unwrap(); + } + + // Map relative coordinates to the touchscreen axis + fn map_position( + TouchArea { + x: + Axis { + min: min_x, + max: max_x, + .. + }, + y: + Axis { + min: min_y, + max: max_y, + .. + }, + }: TouchArea, + (x, y): (f64, f64), + ) -> (u32, u32) { + let mapped_x = (x * (max_x - min_x) as f64) + min_x as f64; + let mapped_y = (y * (max_y - min_y) as f64) + min_y as f64; + + // Clamp the coordinates to be sure they cannot be slightly out of bounds due to rounding + let mapped_x = (mapped_x.round() as u32).clamp(min_x, max_x); + let mapped_y = (mapped_y.round() as u32).clamp(min_y, max_y); + (mapped_x, mapped_y) + } + + // None passed as EventSequence implicitly means finger `0` + // returns true fi a deferred sync should be scheduled + fn push_event( + &mut self, + seq: Option, + state: TouchState, + position: (f64, f64), + ) -> bool { + let (finger_idx, finger) = self.fingers.track(seq); + let (x, y) = Self::map_position(self.options.area, position); + + // Ignore other fingers if multitouch is disabled + if !self.options.emit_mt && finger_idx != 0 { + return false; + } + + let (old_x, old_y) = finger + .pos + .map(|(x, y)| (Some(x), Some(y))) + .unwrap_or((None, None)); + + if self.options.emit_mt { + if self.active_finger_idx != finger_idx { + self.queue.push(InputEvent { + type_: InputEventType::Abs as u16, + code: ABS_MT_SLOT, + value: finger_idx as u32, + }); + self.active_finger_idx = finger_idx; + } + + if finger.tracking_id.is_none() { + self.last_tracking_id = self.last_tracking_id.wrapping_add(1); + finger.tracking_id = Some(self.last_tracking_id); + self.queue.push(InputEvent { + type_: InputEventType::Abs as u16, + code: ABS_MT_TRACKING_ID, + value: self.last_tracking_id as u32, + }); + } + + if old_x.is_none_or(|old_x| old_x != x) { + self.queue.push(InputEvent { + type_: InputEventType::Abs as u16, + code: ABS_MT_POSITION_X, + value: x, + }); + } + + if old_y.is_none_or(|old_y| old_y != y) { + self.queue.push(InputEvent { + type_: InputEventType::Abs as u16, + code: ABS_MT_POSITION_Y, + value: y, + }); + } + } + finger.pos = Some((x, y)); + + match state { + TouchState::Begin => { + if self.options.emit_non_mt { + self.queue.push(InputEvent { + type_: InputEventType::Key as u16, + code: BTN_TOUCH, + value: 1, + }); + } + self.sync(); + false + } + TouchState::End => { + // Sync in case we have a position update to emit it separately + self.sync(); + + if self.options.emit_mt { + self.queue.push(InputEvent { + type_: InputEventType::Abs as u16, + code: ABS_MT_TRACKING_ID, + value: u32::MAX, + }); + } + + if self.options.emit_non_mt { + self.queue.push(InputEvent { + type_: InputEventType::Key as u16, + code: BTN_TOUCH, + value: 0, + }); + } + + self.fingers.delete_by_idx(finger_idx); + self.sync(); + false + } + TouchState::Update => { + if self.requested_deferred_sync { + false + } else { + self.requested_deferred_sync = true; + true + } + } + } + } +} + struct ScanoutWindow { window: ApplicationWindow, width: i32, @@ -28,6 +316,7 @@ struct ScanoutWindow { } impl ScanoutWindow { + #[allow(clippy::too_many_arguments)] pub fn new( app: &Application, title: &str, @@ -36,6 +325,8 @@ impl ScanoutWindow { width: i32, height: i32, format: MemoryFormat, + keyboard_event_tx: Option, + per_display_inputs: Vec<(EventSender, DisplayInputOptions)>, ) -> Self { let header_bar = HeaderBar::new(); let window = ApplicationWindow::builder() @@ -92,7 +383,6 @@ impl ScanoutWindow { let scanout_paintable = ScanoutPaintable::new(display_width, display_height); let picture = Picture::for_paintable(&scanout_paintable); - window.set_titlebar(Some(&header_bar)); header_bar.pack_end(&fullscreen_btn); @@ -101,6 +391,12 @@ impl ScanoutWindow { window.set_child(Some(&overlay)); window.set_visible(true); + if let Some(keyboard_event_tx) = keyboard_event_tx { + picture.set_focusable(true); + attach_keyboard(keyboard_event_tx, &picture); + } + attach_per_display_inputs(&picture, &overlay, per_display_inputs); + Self { window, width, @@ -128,6 +424,182 @@ impl Drop for ScanoutWindow { } } +fn attach_keyboard(keyboard_tx: EventSender, widget: &impl IsA) { + let key_controller = EventControllerKey::new(); + + // Handle key press events + let forwarder_press = keyboard_tx.clone(); + let pressed_keys = Rc::new(RefCell::new(HashSet::new())); + let pressed_keys_clone = pressed_keys.clone(); + key_controller.connect_key_pressed(move |_controller, key, keycode, _modifiers| { + let linux_keycode = gtk_keycode_to_linux(keycode); + if linux_keycode == 0 { + debug!("Unknown key GTK key={}, code={}", key, keycode); + return Propagation::Proceed; + } else { + debug!( + "Forwarding key press: GTK key={}, code={}, Linux code={}", + key, keycode, linux_keycode + ); + } + let is_first_keypress = pressed_keys_clone.borrow_mut().insert(linux_keycode); + let input_event = InputEvent { + type_: InputEventType::Key as u16, + code: linux_keycode, + value: if is_first_keypress { 1 } else { 2 }, + }; + forwarder_press.send(input_event).unwrap(); + let syn = InputEvent { + type_: InputEventType::Syn as u16, + code: SYN_REPORT, + value: 0, + }; + forwarder_press.send(syn).unwrap(); + Propagation::Proceed + }); + + // Handle key release events + let forwarder_release = keyboard_tx.clone(); + key_controller.connect_key_released(move |_controller, key, keycode, _modifiers| { + let linux_keycode = gtk_keycode_to_linux(keycode); + let input_event = InputEvent { + type_: InputEventType::Key as u16, + code: linux_keycode, + value: 0, // Key release + }; + debug!( + "Forwarding key release: GTK key={}, code={}, Linux code={}", + key, keycode, linux_keycode + ); + pressed_keys.borrow_mut().remove(&linux_keycode); + + forwarder_release.send(input_event).unwrap(); + let syn = InputEvent { + type_: InputEventType::Syn as u16, + code: SYN_REPORT, + value: 0, + }; + forwarder_release.send(syn).unwrap(); + }); + widget.add_controller(key_controller); +} + +/// Map a point (px, py in window coordinates) to the coordinates of a paintable inside a picture +/// The returned coordinates are normalized where (0..1) corresponds to coords within the paintable +fn compute_point_inside_paintable( + picture: &Picture, + container: &Overlay, + (x, y): (f64, f64), // window coords +) -> Option<(f64, f64)> { + let paintable = picture.paintable()?; + + let native = container.native().unwrap(); + let (x_offset, y_offset) = native.surface_transform(); + let p = native.compute_point( + picture, + &Point::new((x - x_offset) as f32, (y - y_offset) as f32), + )?; + let point_x = p.x() as f64; + let point_y = p.y() as f64; + + let img_w = picture.width() as f64; + let img_h = picture.height() as f64; + let paintable_w = paintable.intrinsic_width() as f64; + let paintable_h = paintable.intrinsic_height() as f64; + + let x_scale = img_w / paintable_w; + let y_scale = img_h / paintable_h; + let scale = f64::min(x_scale, y_scale); + + // Size of the empty area besides the paintable in the image (both left+right together) + let x_space = img_w - paintable_w * scale; + // Size of the empty area besides the paintable in the image (both top+bottom together) + let y_space = img_h - paintable_h * scale; + + let x_rel = (point_x - x_space / 2.0) / (img_w - x_space); + let y_rel = (point_y - y_space / 2.0) / (img_h - y_space); + + Some((x_rel.clamp(0.0, 1.0), y_rel.clamp(0.0, 1.0))) +} + +fn attach_per_display_inputs( + picture: &Picture, + overlay: &Overlay, + per_display_inputs: Vec<(EventSender, DisplayInputOptions)>, +) { + for (tx, options) in per_display_inputs { + match options { + DisplayInputOptions::TouchScreen(options) => { + let triggered_by_mouse = options.triggered_by_mouse; + let input_controller = EventControllerLegacy::new(); + let touch_sender = + Rc::new(RefCell::new(TouchEventSequencedSender::new(tx, options))); + + let picture_weak = Downgrade::downgrade(picture); + let overlay_weak = Downgrade::downgrade(overlay); + + input_controller.connect_event(move |_, event| { + let picture = picture_weak.upgrade().unwrap(); + let overlay = overlay_weak.upgrade().unwrap(); + + let (x, y); + let state; + let seq; + + if let Some(event) = event.downcast_ref::() { + (x, y) = event.position().unwrap(); + state = match event.event_type() { + EventType::TouchBegin => TouchState::Begin, + EventType::TouchUpdate => TouchState::Update, + EventType::TouchEnd | EventType::TouchCancel => TouchState::End, + _ => return Propagation::Proceed, + }; + seq = Some(event.event_sequence()); + } else if let Some(event) = event.downcast_ref::() + && triggered_by_mouse + && event.modifier_state().contains(ModifierType::BUTTON1_MASK) + { + (x, y) = event.position().unwrap(); + state = match event.event_type() { + EventType::ButtonPress => TouchState::Begin, + EventType::ButtonRelease => TouchState::End, + _ => return Propagation::Proceed, + }; + seq = None; + } else if let Some(event) = event.downcast_ref::() + && triggered_by_mouse + && event.modifier_state().contains(ModifierType::BUTTON1_MASK) + { + (x, y) = event.position().unwrap(); + state = TouchState::Update; + seq = None; + } else { + return Propagation::Proceed; + } + + let Some((x, y)) = compute_point_inside_paintable(&picture, &overlay, (x, y)) + else { + return Propagation::Proceed; + }; + + let requested_deferred_sync = + touch_sender.borrow_mut().push_event(seq, state, (x, y)); + + if requested_deferred_sync { + let touch_sender = touch_sender.clone(); + timeout_add_local_once(Duration::from_millis(0), move || { + touch_sender.borrow_mut().sync(); + }); + } + + Propagation::Stop + }); + overlay.add_controller(input_controller); + } + } + } +} + fn build_overlay(window: &Window) -> Overlay { let overlay_bar = HeaderBar::builder() .valign(Align::Start) @@ -185,6 +657,8 @@ pub struct DisplayWorker { app: Application, app_name: String, rx: PollableChannelReciever, + keyboard_event_tx: Option, + per_display_inputs: Vec, DisplayInputOptions)>>, scanouts: RefCell<[Option; MAX_DISPLAYS]>, } @@ -193,11 +667,15 @@ impl DisplayWorker { app: Application, app_name: String, rx: PollableChannelReciever, + keyboard_event_tx: Option, + per_display_inputs: Vec, DisplayInputOptions)>>, ) -> Self { Self { app, app_name, rx, + keyboard_event_tx, + per_display_inputs, scanouts: Default::default(), } } @@ -234,6 +712,11 @@ impl DisplayWorker { width as i32, height as i32, format, + self.keyboard_event_tx.clone(), + self.per_display_inputs + .get(scanout_id as usize) + .cloned() + .unwrap_or_default(), )); } } @@ -259,7 +742,12 @@ impl DisplayWorker { /// Run a GTK application in the current thread handling the krun_gtk_display events send over the channel. /// The events are produces by the `DisplayBackend` which is hooked up into libkrun. - pub fn run(app_name: String, rx: PollableChannelReciever) { + pub fn run( + app_name: String, + rx: PollableChannelReciever, + keyboard_tx: Option, + per_display_inputs: Vec, DisplayInputOptions)>>, + ) { let app = Application::builder().build(); // Hold the application so it doesn't close when we don't have any windows open. We hold the @@ -268,7 +756,13 @@ impl DisplayWorker { let _app_hold = app.hold(); let rx_fd = rx.as_raw_fd(); - let display_worker = Rc::new(DisplayWorker::new(app.clone(), app_name, rx)); + let display_worker = Rc::new(DisplayWorker::new( + app.clone(), + app_name, + rx, + keyboard_tx, + per_display_inputs, + )); app.connect_activate(move |_app| { let display_worker = display_worker.clone(); unix_fd_add_local(rx_fd, IOCondition::IN, move |_, _| { diff --git a/examples/krun_gtk_display/src/input_backend.rs b/examples/krun_gtk_display/src/input_backend.rs new file mode 100644 index 000000000..4d8278c93 --- /dev/null +++ b/examples/krun_gtk_display/src/input_backend.rs @@ -0,0 +1,254 @@ +use crate::input_constants::*; +use crate::{TouchScreenOptions, input_constants}; +use krun_input::{ + InputAbsInfo, InputBackendError, InputDeviceIds, InputEvent as KrunInputEvent, InputEventType, + InputEventsImpl, InputQueryConfig, ObjectNew, write_bitmap, +}; +use std::cmp::max; +use std::os::fd::{AsFd, BorrowedFd}; +use utils::pollable_channel::PollableChannelReciever; + +pub const KRUN_VENDOR_ID: u16 = u16::from_le_bytes(*b"RH"); +pub const KEYBOARD_DEVICE_NAME: &[u8] = b"libkrun Virtual Keyboard"; +pub const KEYBOARD_SERIAL_NAME: &[u8] = b"KRUN-KBD"; +pub const KEYBOARD_PRODUCT_ID: u16 = 0x0001; + +pub const TOUCHSCREEN_DEVICE_NAME: &[u8] = b"libkrun Touchscreen"; +pub const TOUCHSCREEN_SERIAL_NAME: &[u8] = b"KRUN-TOUCH"; +pub const TOUCHSCREEN_PRODUCT_ID: u16 = 0x0003; + +// GTK to Linux input key code mapping +pub const GTK_KEY_OFFSET: u32 = 8; + +/// Convert GTK key code to Linux input key code +/// Returns the Linux input key code or 0 if no mapping exists +pub fn gtk_keycode_to_linux(gtk_key: u32) -> u16 { + // GTK key codes are typically offset by 8 from Linux input key codes + if gtk_key >= GTK_KEY_OFFSET { + let linux_key = (gtk_key - GTK_KEY_OFFSET) as u16; + // Verify the key is in our supported set + if SUPPORTED_KEYBOARD_KEYS.contains(&linux_key) { + linux_key + } else { + 0 // Unsupported key + } + } else { + 0 // Invalid key + } +} + +pub struct GtkInputEventProvider { + rx: PollableChannelReciever, +} + +impl ObjectNew> for GtkInputEventProvider { + fn new(userdata: Option<&PollableChannelReciever>) -> Self { + Self { + rx: userdata.expect("GtkInputEvents requires receiver").clone(), + } + } +} + +impl InputEventsImpl for GtkInputEventProvider { + fn get_read_notify_fd(&self) -> Result, InputBackendError> { + Ok(self.rx.as_fd()) + } + + fn next_event(&mut self) -> Result, InputBackendError> { + match self.rx.try_recv() { + Ok(Some(event)) => Ok(Some(event)), + Ok(None) => Ok(None), + Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None), + Err(_) => Err(InputBackendError::InternalError), + } + } +} + +#[derive(Clone)] +pub struct GtkKeyboardConfig; + +impl ObjectNew<()> for GtkKeyboardConfig { + fn new(_userdata: Option<&()>) -> Self { + Self + } +} + +impl InputQueryConfig for GtkKeyboardConfig { + fn query_device_name(&self, name_buf: &mut [u8]) -> Result { + let copy_len = std::cmp::min(KEYBOARD_DEVICE_NAME.len(), name_buf.len()); + name_buf[..copy_len].copy_from_slice(&KEYBOARD_DEVICE_NAME[..copy_len]); + Ok(copy_len as u8) + } + + fn query_serial_name(&self, name_buf: &mut [u8]) -> Result { + let copy_len = std::cmp::min(KEYBOARD_SERIAL_NAME.len(), name_buf.len()); + name_buf[..copy_len].copy_from_slice(&KEYBOARD_SERIAL_NAME[..copy_len]); + Ok(copy_len as u8) + } + + fn query_device_ids(&self, ids: &mut InputDeviceIds) -> Result<(), InputBackendError> { + *ids = InputDeviceIds { + bustype: BUS_VIRTUAL, + vendor: KRUN_VENDOR_ID, + product: KEYBOARD_PRODUCT_ID, + version: 1, + }; + Ok(()) + } + + fn query_event_capabilities( + &self, + event_type: u8, + bitmap_buf: &mut [u8], + ) -> Result { + let event_type_enum = InputEventType::try_from(event_type as u16) + .map_err(|_| InputBackendError::InvalidParam)?; + match event_type_enum { + InputEventType::Syn => { + let key_events = write_bitmap(bitmap_buf, SUPPORTED_KEYBOARD_KEYS); + let rep_events = write_bitmap(bitmap_buf, &[/*REP_DELAY, REP_PERIOD*/]); + Ok(max(key_events, rep_events)) + } + InputEventType::Key => Ok(write_bitmap(bitmap_buf, SUPPORTED_KEYBOARD_KEYS)), + InputEventType::Rep => Ok(write_bitmap(bitmap_buf, &[/*REP_DELAY, REP_PERIOD*/])), + _ => Ok(0), + } + } + + fn query_abs_info( + &self, + _abs_axis: u8, + _abs_info: &mut InputAbsInfo, + ) -> Result<(), InputBackendError> { + Ok(()) + } + + fn query_properties(&self, bitmap: &mut [u8]) -> Result { + Ok(write_bitmap(bitmap, &[])) + } +} + +pub const MAX_FINGERS: usize = 16; + +#[derive(Clone)] +pub struct GtkTouchscreenConfig { + options: TouchScreenOptions, +} + +impl ObjectNew for GtkTouchscreenConfig { + fn new(userdata: Option<&TouchScreenOptions>) -> Self { + Self { + options: userdata.expect("Missing userdata").clone(), + } + } +} + +impl InputQueryConfig for GtkTouchscreenConfig { + fn query_device_name(&self, name_buf: &mut [u8]) -> Result { + let copy_len = std::cmp::min(TOUCHSCREEN_DEVICE_NAME.len(), name_buf.len()); + name_buf[..copy_len].copy_from_slice(&TOUCHSCREEN_DEVICE_NAME[..copy_len]); + Ok(copy_len as u8) + } + + fn query_serial_name(&self, name_buf: &mut [u8]) -> Result { + let copy_len = std::cmp::min(TOUCHSCREEN_SERIAL_NAME.len(), name_buf.len()); + name_buf[..copy_len].copy_from_slice(&TOUCHSCREEN_SERIAL_NAME[..copy_len]); + Ok(copy_len as u8) + } + + fn query_device_ids(&self, ids: &mut InputDeviceIds) -> Result<(), InputBackendError> { + *ids = InputDeviceIds { + bustype: BUS_VIRTUAL, + vendor: KRUN_VENDOR_ID, + product: TOUCHSCREEN_PRODUCT_ID, + version: 1, + }; + Ok(()) + } + + fn query_event_capabilities( + &self, + event_type: u8, + bitmap_buf: &mut [u8], + ) -> Result { + let event_type_enum = InputEventType::try_from(event_type as u16) + .map_err(|_| InputBackendError::InvalidParam)?; + + match event_type_enum { + InputEventType::Key if self.options.emit_non_mt => { + Ok(write_bitmap(bitmap_buf, &[BTN_TOUCH])) + } + InputEventType::Abs => { + let bitmap_len1 = if self.options.emit_non_mt { + write_bitmap(bitmap_buf, &[ABS_X, ABS_Y]) + } else { + 0 + }; + let bitmap_len2 = if self.options.emit_mt { + write_bitmap( + bitmap_buf, + &[ABS_MT_SLOT, ABS_MT_POSITION_X, ABS_MT_POSITION_Y], + ) + } else { + 0 + }; + Ok(max(bitmap_len1, bitmap_len2)) + } + _ => Ok(0), + } + } + + fn query_abs_info( + &self, + abs_axis: u8, + abs_info: &mut InputAbsInfo, + ) -> Result<(), InputBackendError> { + match abs_axis as u16 { + input_constants::ABS_MT_SLOT => { + *abs_info = InputAbsInfo { + min: 0, + max: MAX_FINGERS as u32, + fuzz: 0, + flat: 0, + res: 0, + }; + } + input_constants::ABS_MT_TOOL_TYPE => { + *abs_info = InputAbsInfo { + min: 0, + max: 2, + fuzz: 0, + flat: 0, + res: 0, + }; + } + input_constants::ABS_MT_POSITION_X if self.options.emit_mt => { + *abs_info = self.options.area.x.into(); + } + input_constants::ABS_MT_POSITION_Y if self.options.emit_mt => { + *abs_info = self.options.area.y.into(); + } + input_constants::ABS_X if self.options.emit_non_mt => { + *abs_info = self.options.area.x.into(); + } + input_constants::ABS_Y if self.options.emit_non_mt => { + *abs_info = self.options.area.y.into(); + } + input_constants::ABS_MT_TRACKING_ID => { + *abs_info = InputAbsInfo { + min: 0, + max: u16::MAX as u32, + fuzz: 0, + flat: 0, + res: 0, + }; + } + _ => (), + }; + Ok(()) + } + + fn query_properties(&self, properties: &mut [u8]) -> Result { + Ok(write_bitmap(properties, &[INPUT_PROP_DIRECT])) + } +} diff --git a/examples/krun_gtk_display/src/input_constants.rs b/examples/krun_gtk_display/src/input_constants.rs new file mode 100644 index 000000000..16dea83dd --- /dev/null +++ b/examples/krun_gtk_display/src/input_constants.rs @@ -0,0 +1,247 @@ +//! Linux input subsystem constants +//! These match the constants defined in + +// Bus types (from ) +pub const BUS_VIRTUAL: u16 = 0x00; + +//pub const REP_DELAY: u16 = 0x00; +//pub const REP_PERIOD: u16 = 0x01; + +// Input properties (from ) +pub const INPUT_PROP_DIRECT: u16 = 0x01; + +pub const KEY_ESC: u16 = 1; +pub const KEY_1: u16 = 2; +pub const KEY_2: u16 = 3; +pub const KEY_3: u16 = 4; +pub const KEY_4: u16 = 5; +pub const KEY_5: u16 = 6; +pub const KEY_6: u16 = 7; +pub const KEY_7: u16 = 8; +pub const KEY_8: u16 = 9; +pub const KEY_9: u16 = 10; +pub const KEY_0: u16 = 11; +pub const KEY_MINUS: u16 = 12; +pub const KEY_EQUAL: u16 = 13; +pub const KEY_BACKSPACE: u16 = 14; +pub const KEY_TAB: u16 = 15; +pub const KEY_Q: u16 = 16; +pub const KEY_W: u16 = 17; +pub const KEY_E: u16 = 18; +pub const KEY_R: u16 = 19; +pub const KEY_T: u16 = 20; +pub const KEY_Y: u16 = 21; +pub const KEY_U: u16 = 22; +pub const KEY_I: u16 = 23; +pub const KEY_O: u16 = 24; +pub const KEY_P: u16 = 25; +pub const KEY_LEFTBRACE: u16 = 26; +pub const KEY_RIGHTBRACE: u16 = 27; +pub const KEY_ENTER: u16 = 28; +pub const KEY_LEFTCTRL: u16 = 29; +pub const KEY_A: u16 = 30; +pub const KEY_S: u16 = 31; +pub const KEY_D: u16 = 32; +pub const KEY_F: u16 = 33; +pub const KEY_G: u16 = 34; +pub const KEY_H: u16 = 35; +pub const KEY_J: u16 = 36; +pub const KEY_K: u16 = 37; +pub const KEY_L: u16 = 38; +pub const KEY_SEMICOLON: u16 = 39; +pub const KEY_APOSTROPHE: u16 = 40; +pub const KEY_GRAVE: u16 = 41; +pub const KEY_LEFTSHIFT: u16 = 42; +pub const KEY_BACKSLASH: u16 = 43; +pub const KEY_Z: u16 = 44; +pub const KEY_X: u16 = 45; +pub const KEY_C: u16 = 46; +pub const KEY_V: u16 = 47; +pub const KEY_B: u16 = 48; +pub const KEY_N: u16 = 49; +pub const KEY_M: u16 = 50; +pub const KEY_COMMA: u16 = 51; +pub const KEY_DOT: u16 = 52; +pub const KEY_SLASH: u16 = 53; +pub const KEY_RIGHTSHIFT: u16 = 54; +pub const KEY_KPASTERISK: u16 = 55; +pub const KEY_LEFTALT: u16 = 56; +pub const KEY_SPACE: u16 = 57; +pub const KEY_CAPSLOCK: u16 = 58; +pub const KEY_F1: u16 = 59; +pub const KEY_F2: u16 = 60; +pub const KEY_F3: u16 = 61; +pub const KEY_F4: u16 = 62; +pub const KEY_F5: u16 = 63; +pub const KEY_F6: u16 = 64; +pub const KEY_F7: u16 = 65; +pub const KEY_F8: u16 = 66; +pub const KEY_F9: u16 = 67; +pub const KEY_F10: u16 = 68; +pub const KEY_NUMLOCK: u16 = 69; +pub const KEY_SCROLLLOCK: u16 = 70; +pub const KEY_KP7: u16 = 71; +pub const KEY_KP8: u16 = 72; +pub const KEY_KP9: u16 = 73; +pub const KEY_KPMINUS: u16 = 74; +pub const KEY_KP4: u16 = 75; +pub const KEY_KP5: u16 = 76; +pub const KEY_KP6: u16 = 77; +pub const KEY_KPPLUS: u16 = 78; +pub const KEY_KP1: u16 = 79; +pub const KEY_KP2: u16 = 80; +pub const KEY_KP3: u16 = 81; +pub const KEY_KP0: u16 = 82; +pub const KEY_KPDOT: u16 = 83; +pub const KEY_F11: u16 = 87; +pub const KEY_F12: u16 = 88; +pub const KEY_KPENTER: u16 = 96; +pub const KEY_RIGHTCTRL: u16 = 97; +pub const KEY_KPSLASH: u16 = 98; +pub const KEY_SYSRQ: u16 = 99; +pub const KEY_RIGHTALT: u16 = 100; +pub const KEY_HOME: u16 = 102; +pub const KEY_UP: u16 = 103; +pub const KEY_PAGEUP: u16 = 104; +pub const KEY_LEFT: u16 = 105; +pub const KEY_RIGHT: u16 = 106; +pub const KEY_END: u16 = 107; +pub const KEY_DOWN: u16 = 108; +pub const KEY_PAGEDOWN: u16 = 109; +pub const KEY_INSERT: u16 = 110; +pub const KEY_DELETE: u16 = 111; +pub const KEY_PAUSE: u16 = 119; +pub const KEY_MENU: u16 = 139; +pub const KEY_PRINT: u16 = 210; +pub const KEY_POWER: u16 = 116; +pub const KEY_HOMEPAGE: u16 = 172; +pub const KEY_MUTE: u16 = 113; +pub const KEY_VOLUMEDOWN: u16 = 114; +pub const KEY_VOLUMEUP: u16 = 115; +pub const KEY_BACK: u16 = 158; + +pub const ABS_X: u16 = 0x00; +pub const ABS_Y: u16 = 0x01; + +pub const BTN_TOUCH: u16 = 0x14a; + +pub const ABS_MT_SLOT: u16 = 0x2f; /* MT slot being modified */ +pub const ABS_MT_POSITION_X: u16 = 0x35; /* Center X touch position */ +pub const ABS_MT_POSITION_Y: u16 = 0x36; /* Center Y touch position */ +pub const ABS_MT_TOOL_TYPE: u16 = 0x37; /* Type of touching device */ +pub const ABS_MT_TRACKING_ID: u16 = 0x39; /* Unique ID of initiated contact */ + +pub const SYN_REPORT: u16 = 0x00; + +// Supported keyboard keys array +pub const SUPPORTED_KEYBOARD_KEYS: &[u16] = &[ + KEY_ESC, + KEY_1, + KEY_2, + KEY_3, + KEY_4, + KEY_5, + KEY_6, + KEY_7, + KEY_8, + KEY_9, + KEY_0, + KEY_MINUS, + KEY_EQUAL, + KEY_BACKSPACE, + KEY_TAB, + KEY_Q, + KEY_W, + KEY_E, + KEY_R, + KEY_T, + KEY_Y, + KEY_U, + KEY_I, + KEY_O, + KEY_P, + KEY_LEFTBRACE, + KEY_RIGHTBRACE, + KEY_ENTER, + KEY_LEFTCTRL, + KEY_A, + KEY_S, + KEY_D, + KEY_F, + KEY_G, + KEY_H, + KEY_J, + KEY_K, + KEY_L, + KEY_SEMICOLON, + KEY_APOSTROPHE, + KEY_GRAVE, + KEY_LEFTSHIFT, + KEY_BACKSLASH, + KEY_Z, + KEY_X, + KEY_C, + KEY_V, + KEY_B, + KEY_N, + KEY_M, + KEY_COMMA, + KEY_DOT, + KEY_SLASH, + KEY_RIGHTSHIFT, + KEY_KPASTERISK, + KEY_LEFTALT, + KEY_SPACE, + KEY_CAPSLOCK, + KEY_F1, + KEY_F2, + KEY_F3, + KEY_F4, + KEY_F5, + KEY_F6, + KEY_F7, + KEY_F8, + KEY_F9, + KEY_F10, + KEY_NUMLOCK, + KEY_SCROLLLOCK, + KEY_KP7, + KEY_KP8, + KEY_KP9, + KEY_KPMINUS, + KEY_KP4, + KEY_KP5, + KEY_KP6, + KEY_KPPLUS, + KEY_KP1, + KEY_KP2, + KEY_KP3, + KEY_KP0, + KEY_KPDOT, + KEY_F11, + KEY_F12, + KEY_KPENTER, + KEY_RIGHTCTRL, + KEY_KPSLASH, + KEY_SYSRQ, + KEY_RIGHTALT, + KEY_HOME, + KEY_UP, + KEY_PAGEUP, + KEY_LEFT, + KEY_RIGHT, + KEY_END, + KEY_DOWN, + KEY_PAGEDOWN, + KEY_INSERT, + KEY_DELETE, + KEY_PAUSE, + KEY_MENU, + KEY_PRINT, + KEY_POWER, + KEY_HOMEPAGE, + KEY_MUTE, + KEY_VOLUMEDOWN, + KEY_VOLUMEUP, + KEY_BACK, +]; diff --git a/examples/krun_gtk_display/src/lib.rs b/examples/krun_gtk_display/src/lib.rs index 65ae94869..d8c79b214 100644 --- a/examples/krun_gtk_display/src/lib.rs +++ b/examples/krun_gtk_display/src/lib.rs @@ -1,12 +1,17 @@ mod display_backend; mod display_worker; +mod input_backend; +mod input_constants; mod scanout_paintable; use crate::display_worker::DisplayWorker; +use crate::input_backend::{GtkInputEventProvider, GtkKeyboardConfig, GtkTouchscreenConfig}; use anyhow::Context; pub use display_backend::DisplayEvent; pub use display_backend::GtkDisplayBackend; use krun_display::{DisplayBackend, IntoDisplayBackend}; +use krun_input::{InputAbsInfo, InputConfigBackend, InputEventProviderBackend}; +use krun_input::{InputEvent, IntoInputConfig, IntoInputEvents}; use utils::pollable_channel::{PollableChannelReciever, PollableChannelSender, pollable_channel}; pub struct DisplayBackendHandle { @@ -19,25 +24,157 @@ impl DisplayBackendHandle { } } +pub enum InputBackendHandleConfig { + Keyboard, + TouchScreen(TouchScreenOptions), +} + +pub struct InputBackendHandle { + rx: PollableChannelReciever, + input_config: InputBackendHandleConfig, +} + +impl InputBackendHandle { + fn new(rx: PollableChannelReciever, device_type: InputBackendHandleConfig) -> Self { + Self { + rx, + input_config: device_type, + } + } + + pub fn get_events(&self) -> InputEventProviderBackend<'_> { + GtkInputEventProvider::into_input_events(Some(&self.rx)) + } + + pub fn get_config(&self) -> InputConfigBackend<'_> { + match self.input_config { + InputBackendHandleConfig::Keyboard => GtkKeyboardConfig::into_input_config(None), + InputBackendHandleConfig::TouchScreen(ref options) => { + GtkTouchscreenConfig::into_input_config(Some(options)) + } + } + } +} + pub struct DisplayBackendWorker { app_name: String, - rx: PollableChannelReciever, + display_rx: PollableChannelReciever, + keyboard_tx: Option>, + per_display_inputs: Vec, DisplayInputOptions)>>, } impl DisplayBackendWorker { /// NOTE: on macOS GTK has to run on the main thread of the application. pub fn run(self) { - DisplayWorker::run(self.app_name, self.rx) + DisplayWorker::run( + self.app_name, + self.display_rx, + self.keyboard_tx, + self.per_display_inputs, + ); + } +} + +#[derive(Copy, Clone, Debug, Default)] +pub struct Axis { + pub min: u32, + pub max: u32, + pub res: u32, + pub flat: u32, + pub fuzz: u32, +} + +impl From for InputAbsInfo { + fn from(val: Axis) -> Self { + InputAbsInfo { + min: val.min, + max: val.max, + fuzz: val.fuzz, + flat: val.flat, + res: val.res, + } } } -pub fn crate_display(app_name: String) -> (DisplayBackendHandle, DisplayBackendWorker) { - let (tx, rx) = pollable_channel() - .context("Failed to create channel") - .unwrap(); +#[derive(Copy, Clone, Debug)] +pub struct TouchArea { + pub x: Axis, + pub y: Axis, +} + +#[derive(Clone, Debug)] +pub struct TouchScreenOptions { + /// Touchscreen area into which to map the events + pub area: TouchArea, + /// Enable emitting multitouch events + pub emit_mt: bool, + /// Enable emitting non-multitouch ABS_X/ABS_Y events (in addition to the multitouch events) + pub emit_non_mt: bool, + /// Translate mouse click & drag into touch events + pub triggered_by_mouse: bool, +} + +#[derive(Clone, Debug)] +pub enum DisplayInputOptions { + TouchScreen(TouchScreenOptions), +} + +/// Create gtk display and input backends +/// `per_display_inputs` is an array indexed by display id. +/// It contains inputs associated with that specific scanout +pub fn init( + app_name: String, + keyboard_input: bool, + per_display_inputs: Vec>, +) -> anyhow::Result<( + DisplayBackendHandle, + Vec, + DisplayBackendWorker, +)> { + let mut input_backend_handles = + Vec::with_capacity(keyboard_input as usize + per_display_inputs.len()); + + let mut keyboard_tx = None; + if keyboard_input { + let (tx, rx) = pollable_channel().context("Failed to create keyboard events channel")?; + input_backend_handles.push(InputBackendHandle::new( + rx, + InputBackendHandleConfig::Keyboard, + )); + keyboard_tx = Some(tx); + } + + let mut per_display_event_tx = Vec::with_capacity(per_display_inputs.len()); + + for display_input_configs in per_display_inputs { + let mut inputs = Vec::with_capacity(display_input_configs.len()); + + for user_options in &display_input_configs { + match user_options { + DisplayInputOptions::TouchScreen(options) => { + let (tx, rx) = pollable_channel() + .context("Failed to create touchscreen events channel")?; + input_backend_handles.push(InputBackendHandle::new( + rx, + InputBackendHandleConfig::TouchScreen(options.clone()), + )); + inputs.push((tx, user_options.clone())) + } + } + } + per_display_event_tx.push(inputs); + } + + let (display_tx, display_rx) = + pollable_channel().context("Failed to create display events channel")?; + let display_backend = DisplayBackendHandle { tx: display_tx }; + + let worker = DisplayBackendWorker { + app_name, + display_rx, + keyboard_tx, + per_display_inputs: per_display_event_tx, + }; - ( - DisplayBackendHandle { tx }, - DisplayBackendWorker { app_name, rx }, - ) + Ok((display_backend, input_backend_handles, worker)) } diff --git a/include/libkrun.h b/include/libkrun.h index 5a55b3c64..cc48a7016 100644 --- a/include/libkrun.h +++ b/include/libkrun.h @@ -614,6 +614,36 @@ int32_t krun_display_set_refresh_rate(uint32_t ctx_id, uint32_t display_id, uint */ int32_t krun_set_display_backend(uint32_t ctx_id, const void *display_backend, size_t backend_size); + +/** + * Adds an input device with separate config and events objects. + * + * Arguments: + * "ctx_id" - the configuration context ID + * "config_backend" - Pointer to a krun_input_config struct + * "config_backend_size" - sizeof() the krun_input_config struct + * "events_backend" - Pointer to a krun_input_event_provider struct + * "events_backend_size" - sizeof() the krun_input_event_provider struct + * + * Returns: + * Zero on success or a negative error code otherwise. + */ +int krun_add_input_device(uint32_t ctx_id, const void *config_backend, size_t config_backend_size, + const void *events_backend, size_t events_backend_size); + +/** + * Creates a passthrough input device from a host /dev/input/* file descriptor. + * The device configuration will be automatically queried from the host device using ioctls. + * + * Arguments: + * "ctx_id" - The krun context + * "input_fd" - File descriptor to a /dev/input/* device on the host + * + * Returns: + * Zero on success or a negative error code otherwise. + */ +int krun_add_input_device_fd(uint32_t ctx_id, int input_fd); + /** * Enables or disables a virtio-snd device. * diff --git a/include/libkrun_input.h b/include/libkrun_input.h new file mode 120000 index 000000000..35d75d4d8 --- /dev/null +++ b/include/libkrun_input.h @@ -0,0 +1 @@ +../src/krun_input/libkrun_input.h \ No newline at end of file diff --git a/src/devices/Cargo.toml b/src/devices/Cargo.toml index cf69d3caa..9f65b4e2d 100644 --- a/src/devices/Cargo.toml +++ b/src/devices/Cargo.toml @@ -13,6 +13,7 @@ blk = [] efi = ["blk", "net"] gpu = ["rutabaga_gfx", "thiserror", "zerocopy", "krun_display"] snd = ["pw", "thiserror"] +input = ["zerocopy", "krun_input"] virgl_resource_map2 = [] nitro = [] test_utils = [] @@ -31,6 +32,7 @@ virtio-bindings = "0.2.0" vm-memory = { version = ">=0.13", features = ["backend-mmap"] } zerocopy = { version = "0.8.26", optional = true, features = ["derive"] } krun_display = { path = "../krun_display", optional = true, features = ["bindgen_clang_runtime"] } +krun_input = { path = "../krun_input", features = ["bindgen_clang_runtime"], optional = true } arch = { path = "../arch" } utils = { path = "../utils" } diff --git a/src/devices/src/virtio/input/device.rs b/src/devices/src/virtio/input/device.rs new file mode 100644 index 000000000..5d55b97e0 --- /dev/null +++ b/src/devices/src/virtio/input/device.rs @@ -0,0 +1,290 @@ +use std::cmp; +use std::io::Write; +use std::thread::JoinHandle; + +use log::{debug, error}; +use utils::eventfd::EventFd; +use vm_memory::GuestMemoryMmap; + +use super::super::{ActivateError, ActivateResult, DeviceState, Queue as VirtQueue, VirtioDevice}; +use super::worker::InputWorker; +use super::{defs, defs::uapi, InputError}; + +use crate::virtio::input::defs::config_select::VIRTIO_INPUT_CFG_UNSET; +use crate::virtio::input::defs::{config_select, EVENTQ_IDX, STATUSQ_IDX}; +use crate::virtio::InterruptTransport; +use krun_input::{ + InputAbsInfo, InputConfigBackend, InputConfigInstance, InputDeviceIds, + InputEventProviderBackend, InputQueryConfig, +}; + +#[derive(Clone, Copy)] +union InputConfig { + bytes: [u8; size_of::()], + repr: InputConfigRepr, +} + +impl InputConfig { + pub fn new() -> Self { + Self { + bytes: [0u8; size_of::()], + } + } + + pub fn select(&self) -> u8 { + unsafe { self.repr.select } + } + + pub fn subsel(&self) -> u8 { + unsafe { self.repr.subsel } + } + + pub fn bytes(&self) -> &[u8; size_of::()] { + unsafe { &self.bytes } + } + + pub fn invalidate(&mut self) { + self.repr.select = VIRTIO_INPUT_CFG_UNSET; + self.repr.subsel = 0; + self.repr.size = 0; + } + + fn update_select(&mut self, cfg: &InputConfigInstance, select: u8, subsel: u8) { + if select == self.select() && subsel == self.subsel() { + return; + } + + unsafe { + self.repr.payload.bytes.fill(0); + } + + let result = match select { + config_select::VIRTIO_INPUT_CFG_ID_NAME => { + cfg.query_device_name(unsafe { &mut self.repr.payload.bytes }) + } + config_select::VIRTIO_INPUT_CFG_ID_SERIAL => { + cfg.query_serial_name(unsafe { &mut self.repr.payload.bytes }) + } + config_select::VIRTIO_INPUT_CFG_ID_DEVIDS => cfg + .query_device_ids(unsafe { &mut self.repr.payload.ids }) + .map(|_| size_of::() as u8), + config_select::VIRTIO_INPUT_CFG_PROP_BITS => { + cfg.query_properties(unsafe { &mut self.repr.payload.bytes }) + } + config_select::VIRTIO_INPUT_CFG_EV_BITS => { + cfg.query_event_capabilities(subsel, unsafe { &mut self.repr.payload.bytes }) + } + config_select::VIRTIO_INPUT_CFG_ABS_INFO => cfg + .query_abs_info(subsel, unsafe { &mut self.repr.payload.abs }) + .map(|_| size_of::() as u8), + select => { + error!("Invalid config selection select = {select}"); + self.invalidate(); + return; + } + }; + + match result { + Ok(len) => { + self.repr.size = len; + self.repr.select = select; + self.repr.subsel = subsel; + } + Err(e) => { + error!("Failed to query config select={select}, subsel={subsel}: {e:?}"); + self.invalidate(); + } + }; + } +} + +#[derive(Clone, Copy)] +#[repr(C)] +pub struct InputConfigRepr { + select: u8, + subsel: u8, + size: u8, + reserved: [u8; 5], + payload: ConfigPayload, +} + +#[derive(Clone, Copy)] +#[repr(C)] +union ConfigPayload { + bytes: [u8; 128], + abs: InputAbsInfo, + ids: InputDeviceIds, +} + +/// VirtIO Input device state +pub struct Input { + queues: Vec, + queue_events: Vec, + avail_features: u64, + acked_features: u64, + device_state: DeviceState, + cfg: InputConfig, + config_instance: InputConfigInstance, + event_provider_backend: InputEventProviderBackend<'static>, + + worker_thread: Option>, + worker_stopfd: EventFd, +} + +impl Input { + pub(crate) fn with_queues( + queues: Vec, + config_backend: InputConfigBackend<'static>, + events_backend: InputEventProviderBackend<'static>, + ) -> super::Result { + debug!("input: with_queues"); + let mut queue_events = Vec::new(); + for _ in 0..queues.len() { + queue_events + .push(EventFd::new(utils::eventfd::EFD_NONBLOCK).map_err(InputError::EventFd)?); + } + + Ok(Input { + queues, + queue_events, + avail_features: AVAIL_FEATURES, + acked_features: 0, + event_provider_backend: events_backend, + config_instance: config_backend.create_instance().unwrap(), + device_state: DeviceState::Inactive, + cfg: InputConfig::new(), + worker_thread: None, + worker_stopfd: EventFd::new(libc::EFD_NONBLOCK).map_err(InputError::EventFd)?, + }) + } + + pub fn new( + config_backend: InputConfigBackend<'static>, + events_backend: InputEventProviderBackend<'static>, + ) -> super::Result { + let queues: Vec = defs::QUEUE_SIZES + .iter() + .map(|&max_size| VirtQueue::new(max_size)) + .collect(); + Self::with_queues(queues, config_backend, events_backend) + } + + pub fn id(&self) -> &str { + defs::INPUT_DEV_ID + } +} + +const AVAIL_FEATURES: u64 = 1 << uapi::VIRTIO_F_VERSION_1; + +impl VirtioDevice for Input { + fn avail_features(&self) -> u64 { + self.avail_features + } + + fn acked_features(&self) -> u64 { + self.acked_features + } + + fn set_acked_features(&mut self, acked_features: u64) { + self.acked_features = acked_features + } + + fn device_type(&self) -> u32 { + uapi::VIRTIO_ID_INPUT + } + + fn device_name(&self) -> &str { + "input" + } + + fn queues(&self) -> &[VirtQueue] { + &self.queues + } + + fn queues_mut(&mut self) -> &mut [VirtQueue] { + &mut self.queues + } + + fn queue_events(&self) -> &[EventFd] { + &self.queue_events + } + + fn read_config(&self, offset: u64, mut data: &mut [u8]) { + let cfg_slice = self.cfg.bytes(); + let cfg_len = cfg_slice.len() as u64; + + if offset >= cfg_len { + error!("Failed to read config space"); + return; + } + if let Some(end) = offset.checked_add(data.len() as u64) { + // This write can't fail, offset and end are checked against config_len. + data.write_all(&cfg_slice[offset as usize..cmp::min(end, cfg_len) as usize]) + .unwrap(); + } + } + + fn write_config(&mut self, offset: u64, data: &[u8]) { + let len = data.len() as u64; + + let mut select = self.cfg.select(); + let mut subsel = self.cfg.subsel(); + + if offset == 0 && len >= 1 { + select = data[0]; + if len >= 2 { + subsel = data[1] + } + } else if offset == 1 && len >= 1 { + subsel = data[0] + } + + self.cfg + .update_select(&self.config_instance, select, subsel); + } + + fn activate(&mut self, mem: GuestMemoryMmap, interrupt: InterruptTransport) -> ActivateResult { + if self.queues.len() != defs::NUM_QUEUES { + error!( + "Cannot perform activate. Expected {} queue(s), got {}", + defs::NUM_QUEUES, + self.queues.len() + ); + return Err(ActivateError::BadActivate); + } + + let worker = InputWorker::new( + self.queues[EVENTQ_IDX].clone(), + self.queue_events[EVENTQ_IDX].try_clone().unwrap(), + self.queues[STATUSQ_IDX].clone(), + self.queue_events[STATUSQ_IDX].try_clone().unwrap(), + interrupt.clone(), + mem.clone(), + self.event_provider_backend, + self.worker_stopfd.try_clone().unwrap(), + ); + + self.worker_thread = Some(worker.run()); + + self.device_state = DeviceState::Activated(mem, interrupt); + Ok(()) + } + + fn is_activated(&self) -> bool { + self.device_state.is_activated() + } + + fn reset(&mut self) -> bool { + if let Some(worker_thread) = self.worker_thread.take() { + self.worker_stopfd.write(1).unwrap(); + + match worker_thread.join() { + Ok(()) => debug!("Input worker thread stopped"), + Err(e) => { + error!("Failed to join worker thread: {e:?}"); + } + } + } + true + } +} diff --git a/src/devices/src/virtio/input/mod.rs b/src/devices/src/virtio/input/mod.rs new file mode 100644 index 000000000..1b1af3f06 --- /dev/null +++ b/src/devices/src/virtio/input/mod.rs @@ -0,0 +1,60 @@ +mod device; +pub mod passthrough; +mod worker; + +pub use self::defs::uapi::VIRTIO_ID_INPUT as TYPE_INPUT; +pub use self::device::Input; + +mod defs { + pub const INPUT_DEV_ID: &str = "virtio_input"; + pub const NUM_QUEUES: usize = 2; + pub const EVENTQ_IDX: usize = 0; // Event queue (device -> guest) + pub const STATUSQ_IDX: usize = 1; // Status queue (guest -> device) + pub const QUEUE_SIZES: &[u16] = &[256; NUM_QUEUES]; + + pub mod uapi { + pub const VIRTIO_F_VERSION_1: u32 = 32; + pub const VIRTIO_ID_INPUT: u32 = 18; + } + + pub mod config_select { + 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; + } +} + +#[derive(Debug)] +pub enum InputError { + /// Failed to create event fd. + EventFd(std::io::Error), + + /// Backend error + BackendError(String), + + SendNotificationFailed, + + EventFdError, + + HandleEventNotEpollIn, + + HandleEventUnknownEvent, + + UnexpectedConfig(u8), + + UnexpectedFetchEventError, + + UnexpectedDescriptorCount(usize), + + UnexpectedInputDeviceError, + + UnexpectedWriteDescriptorError, + + UnexpectedWriteVringError, +} + +type Result = std::result::Result; diff --git a/src/devices/src/virtio/input/passthrough.rs b/src/devices/src/virtio/input/passthrough.rs new file mode 100644 index 000000000..0f6b434da --- /dev/null +++ b/src/devices/src/virtio/input/passthrough.rs @@ -0,0 +1,211 @@ +use krun_input::{ + InputAbsInfo, InputBackendError, InputDeviceIds, InputEvent, InputEventsImpl, InputQueryConfig, + ObjectNew, +}; +use nix::fcntl::{fcntl, OFlag, F_GETFL, F_SETFL}; +use nix::{errno::Errno, ioctl_read, ioctl_read_buf, unistd}; +use std::mem; +use std::os::fd::{AsFd, AsRawFd, BorrowedFd, RawFd}; + +/// Internal passthrough input backend that forwards host /dev/input/* devices +pub struct PassthroughInputBackend { + fd: BorrowedFd<'static>, +} + +impl InputQueryConfig for PassthroughInputBackend { + fn query_serial_name(&self, serial_buf: &mut [u8]) -> Result { + match unsafe { eviocguniq(self.fd.as_raw_fd(), serial_buf) } { + Ok(len) => Ok(len as u8), + Err(e) => { + error!("Failed to get device serial (eviocguniq): {e}"); + Err(InputBackendError::InternalError) + } + } + } + + fn query_device_name(&self, name_buf: &mut [u8]) -> Result { + match unsafe { eviocgname(self.fd.as_raw_fd(), name_buf) } { + Ok(len) => Ok(len as u8), + Err(e) => { + error!("Failed to get device name (eviocgname): {e}"); + Err(InputBackendError::InternalError) + } + } + } + + fn query_device_ids(&self, ids: &mut InputDeviceIds) -> Result<(), InputBackendError> { + match unsafe { eviocgid(self.fd.as_raw_fd(), ids) } { + Ok(_) => Ok(()), + Err(e) => { + error!("Failed to get device information ids (eviocgid): {e}"); + Err(InputBackendError::InternalError) + } + } + } + + fn query_event_capabilities( + &self, + event_type: u8, + bitmap_buf: &mut [u8], + ) -> Result { + match unsafe { eviocgbit(self.fd.as_raw_fd(), event_type, bitmap_buf) } { + Ok(n) => { + let len = find_length(&bitmap_buf[..n as usize]) as u8; + debug!( + "eviocgbit: {event_type}, got {n} bytes (from n): {:#?}", + &bitmap_buf[..n as usize] + ); + Ok(len) + } + Err(e) => { + error!("Failed to get device event capabilities (eviocgbit): {e}"); + Err(InputBackendError::InternalError) + } + } + } + + fn query_abs_info( + &self, + abs_axis: u8, + abs_info: &mut InputAbsInfo, + ) -> Result<(), InputBackendError> { + let mut linux_abs_info = LinuxAbsInfo::default(); + match unsafe { eviocgabs(self.fd.as_raw_fd(), abs_axis, &mut linux_abs_info) } { + Ok(_) => { + *abs_info = InputAbsInfo { + min: linux_abs_info.minimum, + max: linux_abs_info.maximum, + fuzz: linux_abs_info.fuzz, + flat: linux_abs_info.flat, + res: linux_abs_info.resolution, + }; + Ok(()) + } + Err(e) => { + error!("Failed to get device abs_info (eviocgabs): {e}"); + Err(InputBackendError::InternalError) + } + } + } + + fn query_properties(&self, properties: &mut [u8]) -> Result { + match unsafe { eviocgprop(self.fd.as_raw_fd(), properties) } { + Ok(len) => Ok(len as u8), + Err(e) => { + error!("Failed to query device properties (eviocgprop): {e}"); + Err(InputBackendError::InternalError) + } + } + } +} + +impl ObjectNew> for PassthroughInputBackend { + fn new(userdata: Option<&BorrowedFd<'static>>) -> Self { + let fd = userdata + .copied() + .expect("Missing argument for PassthroughInputBackend::new"); + + make_non_blocking(&fd) + .expect("Cannot make device fd non-blocking (Invalid file descriptor?)"); + Self { fd } + } +} + +impl InputEventsImpl for PassthroughInputBackend { + fn get_read_notify_fd(&self) -> Result, InputBackendError> { + Ok(self.fd) + } + + fn next_event(&mut self) -> Result, InputBackendError> { + let mut linux_event = unsafe { std::mem::zeroed::() }; + let event_slice = unsafe { + std::slice::from_raw_parts_mut( + &mut linux_event as *mut _ as *mut u8, + size_of::(), + ) + }; + + match unistd::read(self.fd, event_slice) { + Ok(bytes_read) if bytes_read == size_of::() => { + trace!("Forwarding input: {linux_event:?}"); + Ok(Some(InputEvent { + type_: linux_event.type_, + code: linux_event.code, + value: linux_event.value, + })) + } + Ok(_bytes_read) => { + error!("Partial read from /dev/input was unexpected, not implemented!"); + Err(InputBackendError::InternalError) + } + Err(Errno::EAGAIN) => Ok(None), + Err(e) => { + error!("Failed to read event from input device: {e}"); + Err(InputBackendError::InternalError) + } + } + } +} + +#[repr(C)] +#[derive(Debug)] +struct LinuxInputEvent { + time: libc::timeval, + type_: u16, + code: u16, + value: u32, +} + +#[repr(C)] +#[derive(Debug, Default)] +struct LinuxAbsInfo { + value: u32, + minimum: u32, + maximum: u32, + fuzz: u32, + flat: u32, + resolution: u32, +} + +ioctl_read!(eviocgid, b'E', 0x02, InputDeviceIds); // Kernel uapi struct is the same as virtio +ioctl_read_buf!(eviocgname, b'E', 0x06, u8); +ioctl_read_buf!(eviocguniq, b'E', 0x08, u8); +ioctl_read_buf!(eviocgprop, b'E', 0x09, u8); + +unsafe fn eviocgbit(fd: RawFd, evt: u8, buf: &mut [u8]) -> Result { + let ioctl_num = nix::request_code_read!(b'E', 0x20 + evt, buf.len()); + + let n = libc::ioctl(fd, ioctl_num as _, buf.as_mut_ptr()); + if n < 0 { + return Err(Errno::last()); + } + Ok(n as u32) +} + +unsafe fn eviocgabs(fd: RawFd, axis: u8, abs_info: &mut LinuxAbsInfo) -> Result { + let ioctl_num = nix::request_code_read!(b'E', 0x40 + axis, size_of::()); + + let n = libc::ioctl(fd, ioctl_num as _, abs_info as *mut _); + if n < 0 { + return Err(Errno::last()); + } + Ok(mem::size_of::() as u32) +} + +fn make_non_blocking(fd: &impl AsFd) -> Result<(), nix::Error> { + let flags = fcntl(fd, F_GETFL)?; + fcntl( + fd, + F_SETFL(OFlag::from_bits_retain(flags) | OFlag::O_NONBLOCK), + )?; + + Ok(()) +} + +fn find_length(bytes: &[u8]) -> usize { + bytes + .iter() + .rposition(|b| *b != 0) + .map(|idx| idx + 1) + .unwrap_or(0) +} diff --git a/src/devices/src/virtio/input/worker.rs b/src/devices/src/virtio/input/worker.rs new file mode 100644 index 000000000..522e1a90e --- /dev/null +++ b/src/devices/src/virtio/input/worker.rs @@ -0,0 +1,285 @@ +use log::{debug, error}; +use std::io; +use std::io::Read; +use std::os::fd::AsRawFd; +use std::thread::{self, JoinHandle}; +use utils::epoll::{ControlOperation, Epoll, EpollEvent, EventSet}; +use utils::eventfd::EventFd; +use virtio_bindings::virtio_input; +use vm_memory::{ByteValued, GuestMemoryMmap}; + +use super::super::Queue; +use crate::virtio::descriptor_utils::{Reader, Writer}; +use crate::virtio::InterruptTransport; +use krun_input::{InputEventProviderBackend, InputEventProviderInstance, InputEventsImpl}; + +// Create a wrapper type to work around orphan rules +#[repr(C)] +#[derive(Copy, Clone, Debug)] +struct VirtioInputEvent { + type_: u16, + code: u16, + value: i32, +} + +unsafe impl ByteValued for VirtioInputEvent {} + +pub struct InputWorker { + event_queue: Queue, // Device -> Guest events + status_queue: Queue, // Guest -> Device events + interrupt: InterruptTransport, + mem: GuestMemoryMmap, + backend_wrapper: InputEventProviderBackend<'static>, + stop_fd: EventFd, + pub event_queue_efd: EventFd, + pub status_queue_efd: EventFd, +} + +impl InputWorker { + #[allow(clippy::too_many_arguments)] + pub fn new( + event_queue: Queue, + event_queue_efd: EventFd, + status_queue: Queue, + status_queue_efd: EventFd, + interrupt: InterruptTransport, + mem: GuestMemoryMmap, + backend: InputEventProviderBackend<'static>, + stop_fd: EventFd, + ) -> Self { + Self { + event_queue, + event_queue_efd, + status_queue, + status_queue_efd, + interrupt, + mem, + backend_wrapper: backend, + stop_fd, + } + } + + pub fn run(self) -> JoinHandle<()> { + thread::Builder::new() + .name("input worker".into()) + .spawn(|| self.work()) + .unwrap() + } + + fn work(mut self) { + debug!("input worker: starting"); + + // Create the events instance in this thread + let mut events_instance = match self.backend_wrapper.create_instance() { + Ok(instance) => instance, + Err(e) => { + error!("Failed to create events instance: {:?}", e); + return; + } + }; + + const EVENTQ: u64 = 1; + const STATUSQ: u64 = 2; + const EVENTQ_USER: u64 = 3; + const QUIT: u64 = 4; + // Set up epoll to wait for events + let epoll = Epoll::new().expect("Failed to create epoll"); + + let ready_fd = match events_instance.get_read_notify_fd() { + Ok(fd) => fd, + Err(e) => { + error!("Failed to get ready fd: {:?}", e); + return; + } + }; + + epoll + .ctl( + ControlOperation::Add, + ready_fd.as_raw_fd(), + &EpollEvent::new(EventSet::IN, EVENTQ_USER), + ) + .expect("Failed to add ready fd to epoll"); + epoll + .ctl( + ControlOperation::Add, + self.event_queue_efd.as_raw_fd(), + &EpollEvent::new(EventSet::IN, EVENTQ), + ) + .expect("Failed to add ready fd to epoll"); + epoll + .ctl( + ControlOperation::Add, + self.status_queue_efd.as_raw_fd(), + &EpollEvent::new(EventSet::IN, STATUSQ), + ) + .expect("Failed to add ready fd to epoll"); + epoll + .ctl( + ControlOperation::Add, + self.stop_fd.as_raw_fd(), + &EpollEvent::new(EventSet::IN, QUIT), + ) + .expect("Failed to add stop fd to epoll"); + + let mut events = vec![EpollEvent::default(); 16]; + + 'event_loop: loop { + let num_events = match epoll.wait(events.len(), 1000, &mut events) { + Ok(n) => n, + Err(e) => { + error!("Epoll wait failed: {:?}", e); + break; + } + }; + + let mut needs_interrupt = false; + + for event in &events[..num_events] { + match event.data() { + EVENTQ_USER => { + trace!("EVENTQ_USER"); + needs_interrupt |= self.process_event_queue(&mut events_instance); + } + EVENTQ => { + self.event_queue_efd.read().unwrap(); + trace!("EVENTQ"); + needs_interrupt |= self.process_event_queue(&mut events_instance); + } + STATUSQ => { + self.status_queue_efd.read().unwrap(); + needs_interrupt |= self.process_status_queue(); + } + QUIT => { + // Stop signal received + let _ = self.stop_fd.read(); + break 'event_loop; + } + x => { + error!("TODO: {x}") + } + } + if needs_interrupt { + self.interrupt.signal_used_queue(); + } + } + } + + debug!("input worker: stopping"); + } + + /// Fills a virtqueue with events from the source. Returns the number of bytes written. + fn fill_event_virtqueue( + &mut self, + events_instance: &mut InputEventProviderInstance, + writer: &mut Writer, + ) -> Result<(usize, bool), ()> { + let avail_bytes = writer.available_bytes(); + let mut eof = false; + while writer.bytes_written() + size_of::() <= avail_bytes { + match events_instance.next_event() { + Ok(Some(event)) => { + let virtio_event = VirtioInputEvent { + type_: event.type_, + code: event.code, + value: event.value as i32, + }; + debug!("Writing: {virtio_event:?}"); + writer + .write_obj(virtio_event) + .expect("Failed to write input event to virtqueue"); + } + // No more events available + Ok(None) => { + eof = true; + break; + } + Err(e) => { + error!("Error getting next event: {:?}", e); + eof = true; + break; + } + } + } + Ok((writer.bytes_written(), eof)) + } + + fn process_event_queue(&mut self, events_instance: &mut InputEventProviderInstance) -> bool { + let mut needs_interrupt = false; + let mem = self.mem.clone(); + + while let Some(desc_chain) = self.event_queue.pop(&mem) { + let mut writer = match Writer::new(&mem, desc_chain.clone()) { + Ok(w) => w, + Err(e) => { + error!("Failed to create writer: {:?}", e); + break; + } + }; + + let (bytes_written, eof) = self + .fill_event_virtqueue(events_instance, &mut writer) + .unwrap(); + + if bytes_written != 0 { + self.event_queue + .add_used(&mem, desc_chain.index, bytes_written as u32) + .expect("TODO"); + needs_interrupt = true; + } + + if bytes_written == 0 { + self.event_queue.undo_pop(); + break; + } + + if eof { + break; + } + } + needs_interrupt + } + + /// Reads events from guest and sends them to the event source (currently no-op) + fn read_status_virtqueue(&mut self, reader: &mut Reader) -> Result { + while reader.available_bytes() >= size_of::() { + let mut buffer: [u8; size_of::()] = + [0; size_of::()]; + reader.read_exact(&mut buffer)?; + debug!("Not implemented status queue request: {:?}", &buffer); + // For now, we don't send events back to the input source + // This would be used for things like setting LEDs on keyboards, haptic feedback, etc. + } + Ok(reader.bytes_read()) + } + + /// Process the status queue (guest -> device events) + fn process_status_queue(&mut self) -> bool { + let mut needs_interrupt = false; + let mem = self.mem.clone(); + + while let Some(desc_chain) = self.status_queue.pop(&mem) { + let mut reader = match Reader::new(&mem, desc_chain.clone()) { + Ok(r) => r, + Err(e) => { + error!("Failed to create reader for status queue: {e}"); + return false; + } + }; + match self.read_status_virtqueue(&mut reader) { + Ok(bytes_read) => { + self.status_queue + .add_used(&mem, desc_chain.index, bytes_read as u32) + .unwrap(); + } + Err(e) => { + error!("Input: failed to read events from virtqueue: {:?}", e); + } + } + + needs_interrupt = true; + } + + needs_interrupt + } +} diff --git a/src/devices/src/virtio/mod.rs b/src/devices/src/virtio/mod.rs index 791925700..4f9258383 100644 --- a/src/devices/src/virtio/mod.rs +++ b/src/devices/src/virtio/mod.rs @@ -25,6 +25,8 @@ pub mod file_traits; pub mod fs; #[cfg(feature = "gpu")] pub mod gpu; +#[cfg(feature = "input")] +pub mod input; pub mod linux_errno; mod mmio; #[cfg(feature = "net")] diff --git a/src/krun_input/Cargo.lock b/src/krun_input/Cargo.lock new file mode 100644 index 000000000..0523749be --- /dev/null +++ b/src/krun_input/Cargo.lock @@ -0,0 +1,311 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "bindgen" +version = "0.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a65b545ab31d687cff52899d4890855fec459eb6afe0da6417b8a18da87aa29" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "krun_input" +version = "0.1.0" +dependencies = [ + "bindgen", + "bitflags", + "log", + "static_assertions", + "thiserror", +] + +[[package]] +name = "libc" +version = "0.2.175" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "proc-macro2" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" diff --git a/src/krun_input/Cargo.toml b/src/krun_input/Cargo.toml new file mode 100644 index 000000000..ff41f462e --- /dev/null +++ b/src/krun_input/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "krun_input" +description = "Rust bindings for implementing input backends in Rust for libkrun" +version = "0.1.0" +edition = "2024" + +[dependencies] +thiserror = "2.0.12" +libc = "0.2" +log = "0.4.27" +bitflags = "2.9.1" +static_assertions = "1.1.0" + +[build-dependencies] +bindgen = { version = "0.72", default-features = false } + +[features] +bindgen_clang_runtime = ["bindgen/runtime"] diff --git a/src/krun_input/build.rs b/src/krun_input/build.rs new file mode 100644 index 000000000..061a127a5 --- /dev/null +++ b/src/krun_input/build.rs @@ -0,0 +1,16 @@ +use std::env; +use std::path::PathBuf; + +fn main() { + println!("cargo:rerun-if-changed=libkrun_input.h"); + + let bindings = bindgen::Builder::default() + .header("libkrun_input.h") + .generate() + .expect("Unable to generate bindings"); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("input_header.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/src/krun_input/libkrun_input.h b/src/krun_input/libkrun_input.h new file mode 100644 index 000000000..51059fb1e --- /dev/null +++ b/src/krun_input/libkrun_input.h @@ -0,0 +1,171 @@ +#ifndef _LIBKRUN_INPUT_H +#define _LIBKRUN_INPUT_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// The input backend encountered an internal error +#define KRUN_INPUT_ERR_INTERNAL -1 +#define KRUN_INPUT_ERR_EAGAIN -2 +#define KRUN_INPUT_ERR_METHOD_UNSUPPORTED -3 +#define KRUN_INPUT_ERR_INVALID_PARAM -4 + + +#define KRUN_INPUT_CONFIG_FEATURE_QUERY 1 +#define KRUN_INPUT_EVENT_PROVIDER_FEATURE_QUEUE 1 + +/** + * Represents an input event similar to Linux input events. + * This structure is compatible with virtio input events. + */ +struct krun_input_event { + uint16_t type; // Event type (EV_KEY, EV_REL, EV_ABS, etc.) + uint16_t code; // Event code (key code, relative axis, etc.) + uint32_t value; // Event value +}; + +/** + * Called to create an input backend instance. + * + * Arguments: + * "instance" - (Output) pointer to userdata which can be used to represent this/self argument. + * Implementation may set it to any value (even NULL) + * "userdata" - userdata specified in the `krun_input_backend` instance + * "reserved" - reserved/unused for now (arguments passed from libkrun to user) + * + * Returns: + * Zero on success or a negative error code (KRUN_INPUT_ERR_*) otherwise. + */ +typedef int32_t (*krun_input_create_fn)(void **instance, const void *userdata, const void *reserved); + +/** + * Called to destroy the input backend instance. + * + * Arguments: + * "instance" - userdata set by `krun_input_create`, represents this/self argument + * + * Returns: + * Zero on success or a negative error code (KRUN_INPUT_ERR_*) otherwise. + */ +typedef int32_t (*krun_input_destroy_fn)(void *instance); + +/** + * Gets a file descriptor that becomes ready for reading when input events are available. + * The implementation should return an eventfd or similar file descriptor that can be used + * with epoll/poll/select to wait for input events. + * + * Arguments: + * "instance" - userdata set by `krun_input_create`, represents this/self argument + * + * Returns: + * A valid file descriptor (>= 0) or a negative error code (KRUN_INPUT_ERR_*) otherwise. + */ +typedef int (*krun_input_get_ready_efd_fn)(void *instance); + +/** + * Fetches the next available input event from the backend. + * This function should not block. If no events are available, it should return 0. + * + * Arguments: + * "instance" - userdata set by `krun_input_create`, represents this/self argument + * "out_event" - (Output) pointer to where the event should be written + * + * Returns: + * 1 if an event was successfully retrieved and written to out_event + * 0 if no events are available + * negative error code (KRUN_INPUT_ERR_*) on error + */ +typedef int32_t (*krun_input_next_event_fn)(void *instance, struct krun_input_event *out_event); + +struct krun_input_event_provider_vtable { + krun_input_destroy_fn destroy; // (optional) + krun_input_get_ready_efd_fn get_ready_efd; // (required) + krun_input_next_event_fn next_event; // (required) +}; + +/** + * Device IDs structure for input devices + */ +struct krun_input_device_ids { + uint16_t bustype; + uint16_t vendor; + uint16_t product; + uint16_t version; +}; + +/** + * Absolute axis information structure + */ +struct krun_input_absinfo { + uint32_t min; + uint32_t max; + uint32_t fuzz; + uint32_t flat; + uint32_t res; +}; + +/** + * Called to create an instance of an object + * + * Arguments: + * "instance" - (Output) pointer to userdata which can be used to represent this/self argument. + * "userdata" - userdata specified in the config object + * "reserved" - reserved/unused for now + * + * Returns: + * Zero on success or a negative error code (KRUN_INPUT_ERR_*) otherwise. + */ +typedef int32_t (*krun_input_create_fn)(void **instance, const void *userdata, const void *reserved); + +/** + * Function pointer types for querying device configuration + */ +typedef int32_t (*krun_input_query_device_name_fn)(void *instance, uint8_t *name_buf, size_t name_buf_len); +typedef int32_t (*krun_input_query_serial_name_fn)(void *instance, uint8_t *name_buf, size_t name_buf_len); +typedef int32_t (*krun_input_query_device_ids_fn)(void *instance, struct krun_input_device_ids *ids); +typedef int32_t (*krun_input_query_event_capabilities_fn)(void *instance, uint8_t event_type, uint8_t *bitmap_buf, size_t bitmap_buf_len); +typedef int32_t (*krun_input_query_abs_info_fn)(void *instance, uint8_t abs_axis, struct krun_input_absinfo *abs_info); +typedef int32_t (*krun_input_query_properties_fn)(void *instance, uint8_t *bitmap_buf, size_t bitmap_buf_len); + +/** + * Config vtable structure + */ +struct krun_input_config_vtable { + krun_input_destroy_fn destroy; + krun_input_query_device_name_fn query_device_name; + krun_input_query_serial_name_fn query_serial_name; + krun_input_query_device_ids_fn query_device_ids; + krun_input_query_event_capabilities_fn query_event_capabilities; + krun_input_query_abs_info_fn query_abs_info; + krun_input_query_properties_fn query_properties; +}; + +/** + * Config object structure + */ +struct krun_input_config { + uint64_t features; + void *create_userdata; // (optional) + krun_input_create_fn create; // Creates the config object + struct krun_input_config_vtable vtable; +}; + +/** + * Events object structure + */ +struct krun_input_event_provider { + uint64_t features; + void *create_userdata; // (optional) + krun_input_create_fn create; // Creates the events object + struct krun_input_event_provider_vtable vtable; +}; + +#ifdef __cplusplus +} +#endif + +#endif // _LIBKRUN_INPUT_H \ No newline at end of file diff --git a/src/krun_input/src/c_to_rust.rs b/src/krun_input/src/c_to_rust.rs new file mode 100644 index 000000000..1f30c2132 --- /dev/null +++ b/src/krun_input/src/c_to_rust.rs @@ -0,0 +1,297 @@ +use crate::{ + ConfigFeatures, EventProviderFeatures, InputAbsInfo, InputBackendError, InputDeviceIds, + InputEvent, InputEventsImpl, InputQueryConfig, header, +}; +use log::{error, warn}; +use static_assertions::assert_not_impl_any; +use std::ffi::c_void; +use std::marker::PhantomData; +use std::os::fd::BorrowedFd; +use std::ptr::{null, null_mut}; + +#[macro_export] +macro_rules! into_rust_result { + ($expr:expr) => { + into_rust_result!($expr, + 0 => Ok(()), + code @ 0.. => { + log::warn!("{}: Unknown OK result code: {code}", stringify!($expr)); + Ok(()) + } + ) + }; + ($expr:expr, $($pat:pat $(if $pat_guard:expr)? => $pat_expr:expr),+ ) => { + match $expr { + $($pat $(if $pat_guard)? => $pat_expr,)+ + -1 => Err(InputBackendError::InternalError), + -2 => Err(InputBackendError::Again), + -3 => Err(InputBackendError::MethodNotSupported), + -4 => Err(InputBackendError::InvalidParam), + code @ i32::MIN.. => { + log::warn!("{}: Unknown error result code: {code}", stringify!($expr)); + Err(InputBackendError::InternalError) + } + } + }; +} + +macro_rules! method_call { + ($self:ident.$method:ident($($args:expr),*) ) => { + unsafe { + $self.vtable.$method + .ok_or(InputBackendError::MethodNotSupported)?( $self.instance, $($args),* ) + } + }; +} + +pub struct InputEventProviderInstance { + instance: *mut c_void, + vtable: header::krun_input_event_provider_vtable, +} +impl InputEventsImpl for InputEventProviderInstance { + /// Get the ready event file descriptor that becomes readable when input events are available + fn get_read_notify_fd(&self) -> Result, InputBackendError> { + let fd = method_call! { + self.get_ready_efd() + }; + + into_rust_result!(fd, + fd if fd >= 0 => Ok( + // SAFETY: We have checked the return code of the method, the so the fd should be valid + // The lifetime of the fd is the existence of this event provider. + unsafe { BorrowedFd::borrow_raw(fd) } + ) + ) + } + + /// Fetch the next available input event, returns None if no events are available + fn next_event(&mut self) -> Result, InputBackendError> { + let mut event = InputEvent { + type_: 0, + code: 0, + value: 0, + }; + + let result = method_call! { + self.next_event(&raw mut event) + }; + + into_rust_result!(result, + 1 => Ok(Some(event)), + 0 => Ok(None) + ) + } +} + +pub struct InputConfigInstance { + instance: *mut c_void, + vtable: header::krun_input_config_vtable, +} + +unsafe impl Send for InputConfigInstance {} +unsafe impl Sync for InputConfigInstance {} + +assert_not_impl_any!(InputEventProviderInstance: Sync, Send); + +impl Drop for InputEventProviderInstance { + fn drop(&mut self) { + let Some(destroy_fn) = self.vtable.destroy else { + return; + }; + + if let Err(e) = into_rust_result!(unsafe { destroy_fn(self.instance) }) { + error!("Failed to destroy krun input events instance: {e}"); + } + } +} + +impl Drop for InputConfigInstance { + fn drop(&mut self) { + let Some(destroy_fn) = self.vtable.destroy else { + return; + }; + + if let Err(e) = into_rust_result!(unsafe { destroy_fn(self.instance) }) { + error!("Failed to destroy krun input config instance: {e}"); + } + } +} + +// Remove the old InputConfigImpl methods as they're not needed + +impl InputQueryConfig for InputConfigInstance { + fn query_device_name(&self, name_buf: &mut [u8]) -> Result { + let result = method_call! { + self.query_device_name(name_buf.as_mut_ptr(), name_buf.len()) + }; + + into_rust_result!(result, + len if len >= 0 => Ok(len as u8) + ) + } + + fn query_serial_name(&self, name_buf: &mut [u8]) -> Result { + let result = method_call! { + self.query_serial_name(name_buf.as_mut_ptr(), name_buf.len()) + }; + + into_rust_result!(result, + len if len >= 0 => Ok(len as u8) + ) + } + + fn query_device_ids(&self, ids: &mut InputDeviceIds) -> Result<(), InputBackendError> { + let result = method_call! { + self.query_device_ids(ids as *mut InputDeviceIds) + }; + + into_rust_result!(result) + } + + fn query_event_capabilities( + &self, + event_type: u8, + bitmap_buf: &mut [u8], + ) -> Result { + let result = method_call! { + self.query_event_capabilities(event_type, bitmap_buf.as_mut_ptr(), bitmap_buf.len()) + }; + + into_rust_result!(result, + len if len >= 0 => Ok(len as u8) + ) + } + + fn query_abs_info( + &self, + abs_axis: u8, + abs_info: &mut InputAbsInfo, + ) -> Result<(), InputBackendError> { + let result = method_call! { + self.query_abs_info(abs_axis, abs_info as *mut InputAbsInfo) + }; + + into_rust_result!(result) + } + + fn query_properties(&self, properties: &mut [u8]) -> Result { + let result = method_call! { + self.query_properties(properties.as_mut_ptr(), properties.len()) + }; + + into_rust_result!(result, + len if len >= 0 => Ok(len as u8) + ) + } +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct InputConfigBackend<'userdata> { + pub features: u64, + pub create_userdata: *const c_void, + pub create_userdata_lifetime: PhantomData<&'userdata c_void>, + pub create_fn: header::krun_input_create_fn, + pub vtable: header::krun_input_config_vtable, +} +unsafe impl<'a> Send for InputConfigBackend<'a> {} +unsafe impl<'a> Sync for InputConfigBackend<'a> {} + +impl<'a> InputConfigBackend<'a> { + /// Create an InputConfigInstance for handling device configuration + pub fn create_instance(&self) -> Result { + let mut instance = null_mut(); + if let Some(create_fn) = self.create_fn { + into_rust_result!(unsafe { + create_fn(&raw mut instance, self.create_userdata, null()) + })?; + } + assert!(self.verify()); + + Ok(InputConfigInstance { + instance, + vtable: self.vtable, + }) + } + + pub fn verify(&self) -> bool { + let features = ConfigFeatures::from_bits_retain(self.features); + + // This requirement might change in the future when we add support for alternatives to this + if !features.contains(ConfigFeatures::QUERY) { + error!("This version of libkrun requires QUEUE feature"); + return false; + } + + for feature in features { + if feature.contains(ConfigFeatures::QUERY) { + if self.vtable.query_device_name.is_none() + || self.vtable.query_serial_name.is_none() + || self.vtable.query_device_ids.is_none() + || self.vtable.query_event_capabilities.is_none() + || self.vtable.query_abs_info.is_none() + || self.vtable.query_properties.is_none() + { + error!("Missing required methods for QUERY feature"); + return false; + } + } else { + warn!("Unknown features ({feature:x}) will be ignored") + } + } + true + } +} + +#[derive(Copy, Clone)] +#[repr(C)] +pub struct InputEventProviderBackend<'userdata> { + pub features: u64, + pub create_userdata: *const c_void, + pub create_userdata_lifetime: PhantomData<&'userdata c_void>, + pub create_fn: header::krun_input_create_fn, + pub vtable: header::krun_input_event_provider_vtable, +} + +unsafe impl<'a> Send for InputEventProviderBackend<'a> {} +unsafe impl<'a> Sync for InputEventProviderBackend<'a> {} + +impl<'a> InputEventProviderBackend<'a> { + /// Create an InputEventsInstance for handling input events + pub fn create_instance(&self) -> Result { + let mut instance = null_mut(); + if let Some(create_fn) = self.create_fn { + into_rust_result!(unsafe { + create_fn(&raw mut instance, self.create_userdata, null()) + })?; + } + assert!(self.verify()); + + Ok(InputEventProviderInstance { + instance, + vtable: self.vtable, + }) + } + + pub fn verify(&self) -> bool { + let features = EventProviderFeatures::from_bits_retain(self.features); + + // This requirement might change in the future when we add support for alternatives to this + if !features.contains(EventProviderFeatures::QUEUE) { + error!("This version of libkrun requires QUEUE feature"); + return false; + } + + for feature in features { + if feature.contains(EventProviderFeatures::QUEUE) { + if self.vtable.get_ready_efd.is_none() || self.vtable.get_ready_efd.is_none() { + error!("Missing required methods for BASIC_FRAMEBUFFER"); + return false; + } + } else { + warn!("Unknown features ({feature:x}) will be ignored") + } + } + true + } +} diff --git a/src/krun_input/src/lib.rs b/src/krun_input/src/lib.rs new file mode 100644 index 000000000..b95d1a6f6 --- /dev/null +++ b/src/krun_input/src/lib.rs @@ -0,0 +1,108 @@ +mod rust_to_c; + +use bitflags::bitflags; +pub use rust_to_c::*; +use std::cmp::max; + +mod c_to_rust; +pub use c_to_rust::{ + InputConfigBackend, InputConfigInstance, InputEventProviderBackend, InputEventProviderInstance, +}; + +use thiserror::Error; + +#[allow( + non_upper_case_globals, + non_snake_case, + non_camel_case_types, + dead_code, + unused_variables +)] +mod header { + include!(concat!(env!("OUT_DIR"), "/input_header.rs")); +} + +bitflags! { + pub struct ConfigFeatures: u64 { + const QUERY = header::KRUN_INPUT_EVENT_PROVIDER_FEATURE_QUEUE as u64; + } +} + +bitflags! { + pub struct EventProviderFeatures: u64 { + const QUEUE = header::KRUN_INPUT_EVENT_PROVIDER_FEATURE_QUEUE as u64; + } +} + +#[derive(Error, Debug)] +#[repr(i32)] +pub enum InputBackendError { + #[error("Backend implementation error")] + InternalError = header::KRUN_INPUT_ERR_INTERNAL, + #[error("Try again later")] + Again = header::KRUN_INPUT_ERR_EAGAIN, + #[error("Method not supported")] + MethodNotSupported = header::KRUN_INPUT_ERR_METHOD_UNSUPPORTED, + #[error("Invalid parameter")] + InvalidParam = header::KRUN_INPUT_ERR_INVALID_PARAM, +} + +/// Input event types matching Linux input event types +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum InputEventType { + Syn = 0x00, // EV_SYN + Key = 0x01, // EV_KEY + Rel = 0x02, // EV_REL + Abs = 0x03, // EV_ABS + Msc = 0x04, // EV_MSC + Sw = 0x05, // EV_SW + Led = 0x11, // EV_LED + Snd = 0x12, // EV_SND + Rep = 0x14, // EV_REP +} + +impl TryFrom for InputEventType { + type Error = (); + + fn try_from(value: u16) -> Result { + match value { + 0x00 => Ok(Self::Syn), + 0x01 => Ok(Self::Key), + 0x02 => Ok(Self::Rel), + 0x03 => Ok(Self::Abs), + 0x04 => Ok(Self::Msc), + 0x05 => Ok(Self::Sw), + 0x11 => Ok(Self::Led), + 0x12 => Ok(Self::Snd), + 0x14 => Ok(Self::Rep), + _ => Err(()), + } + } +} + +impl From for u16 { + fn from(val: InputEventType) -> Self { + val as u16 + } +} + +pub type InputEvent = header::krun_input_event; +pub type InputDeviceIds = header::krun_input_device_ids; +pub type InputAbsInfo = header::krun_input_absinfo; + +/// Writes the specific bits in bitmap, given the indices of the bits +/// Return the "length" of the newly constructed bitmap +pub fn write_bitmap(bitmap: &mut [u8], active_bits: &[u16]) -> u8 { + let mut max_byte: u8 = 0; + for idx in active_bits { + let byte_pos = (idx / 8).try_into().unwrap(); + let additional_bit = 1 << (idx % 8); + if byte_pos as usize > bitmap.len() { + panic!("Bit index {idx} out of bounds"); + } + bitmap[byte_pos as usize] |= additional_bit; + max_byte = max(max_byte, byte_pos); + } + max_byte.checked_add(1).unwrap() +} diff --git a/src/krun_input/src/rust_to_c.rs b/src/krun_input/src/rust_to_c.rs new file mode 100644 index 000000000..d70f3609f --- /dev/null +++ b/src/krun_input/src/rust_to_c.rs @@ -0,0 +1,266 @@ +use crate::header::{ + KRUN_INPUT_CONFIG_FEATURE_QUERY, KRUN_INPUT_EVENT_PROVIDER_FEATURE_QUEUE, + krun_input_config_vtable, krun_input_event_provider_vtable, +}; +use crate::{ + InputAbsInfo, InputBackendError, InputConfigBackend, InputDeviceIds, InputEvent, + InputEventProviderBackend, +}; +use std::ffi::c_void; +use std::marker::PhantomData; +use std::os::fd::{AsRawFd, BorrowedFd}; +use std::ptr; +use std::ptr::null; + +pub trait ObjectNew { + fn new(userdata: Option<&T>) -> Self; +} + +pub trait InputQueryConfig { + /// Query device name into provided buffer + fn query_device_name(&self, name_buf: &mut [u8]) -> Result; + + /// Query device name into provided buffer + fn query_serial_name(&self, name_buf: &mut [u8]) -> Result; + + /// Query device IDs into provided structure + fn query_device_ids(&self, ids: &mut InputDeviceIds) -> Result<(), InputBackendError>; + + /// Query event capabilities bitmap for specific event type into provided buffer + fn query_event_capabilities( + &self, + event_type: u8, + bitmap_buf: &mut [u8], + ) -> Result; + + /// Query absolute axis information into provided structure + fn query_abs_info( + &self, + abs_axis: u8, + abs_info: &mut InputAbsInfo, + ) -> Result<(), InputBackendError>; + + /// Query device properties into provided u32 + fn query_properties(&self, properties: &mut [u8]) -> Result; +} + +pub trait InputEventsImpl { + /// Get the file descriptor that becomes ready when input events are available + fn get_read_notify_fd(&self) -> Result, InputBackendError>; + + /// Fetch the next available input event, returns None if no events are available + fn next_event(&mut self) -> Result, InputBackendError>; +} + +pub trait IntoInputConfig { + fn into_input_config(userdata: Option<&T>) -> InputConfigBackend<'_>; +} + +impl IntoInputConfig for I +where + I: InputQueryConfig + ObjectNew, +{ + fn into_input_config(userdata: Option<&UserData>) -> InputConfigBackend<'_> { + extern "C" fn create_config_fn>( + instance: *mut *mut c_void, + userdata: *const c_void, + _reserved: *const c_void, + ) -> i32 { + let actual_userdata = if userdata.is_null() { + None + } else { + Some(unsafe { &*(userdata as *const T) }) + }; + + let config_obj = I::new(actual_userdata); + let boxed_config = Box::into_raw(Box::new(config_obj)); + unsafe { *instance = boxed_config as *mut c_void }; + 0 + } + + extern "C" fn config_destroy_fn(instance: *mut c_void) -> i32 { + if instance.is_null() { + return 0; + } + let _ = unsafe { Box::from_raw(instance as *mut I) }; + 0 + } + + extern "C" fn query_device_name_fn>( + instance: *mut c_void, + name_buf: *mut u8, + name_buf_len: usize, + ) -> i32 { + let config_obj = unsafe { &*(instance as *const I) }; + let name_buf_slice = unsafe { std::slice::from_raw_parts_mut(name_buf, name_buf_len) }; + + match config_obj.query_device_name(name_buf_slice) { + Ok(len) => len as i32, + Err(e) => e as i32, + } + } + + extern "C" fn query_serial_name_fn>( + instance: *mut c_void, + name_buf: *mut u8, + name_buf_len: usize, + ) -> i32 { + let config_obj = unsafe { &*(instance as *const I) }; + let name_buf_slice = unsafe { std::slice::from_raw_parts_mut(name_buf, name_buf_len) }; + + match config_obj.query_serial_name(name_buf_slice) { + Ok(len) => len as i32, + Err(e) => e as i32, + } + } + + extern "C" fn query_device_ids_fn>( + instance: *mut c_void, + ids: *mut InputDeviceIds, + ) -> i32 { + let config_obj = unsafe { &*(instance as *const I) }; + let ids = unsafe { &mut *ids }; + + match config_obj.query_device_ids(ids) { + Ok(()) => 0, + Err(e) => e as i32, + } + } + + extern "C" fn query_event_capabilities_fn>( + instance: *mut c_void, + event_type: u8, + bitmap_buf: *mut u8, + bitmap_buf_len: usize, + ) -> i32 { + let config_obj = unsafe { &*(instance as *const I) }; + let bitmap_buf_slice = + unsafe { std::slice::from_raw_parts_mut(bitmap_buf, bitmap_buf_len) }; + + match config_obj.query_event_capabilities(event_type, bitmap_buf_slice) { + Ok(len) => len as i32, + Err(e) => e as i32, + } + } + + extern "C" fn query_abs_info_fn>( + instance: *mut c_void, + abs_axis: u8, + abs_info: *mut InputAbsInfo, + ) -> i32 { + let config_obj = unsafe { &*(instance as *const I) }; + let abs_info = unsafe { &mut *abs_info }; + + match config_obj.query_abs_info(abs_axis, abs_info) { + Ok(()) => 0, + Err(e) => e as i32, + } + } + + extern "C" fn query_properties_fn>( + instance: *mut c_void, + bitmap_buf: *mut u8, + bitmap_buf_len: usize, + ) -> i32 { + let config_obj = unsafe { &*(instance as *const I) }; + let bitmap_buf_slice = + unsafe { std::slice::from_raw_parts_mut(bitmap_buf, bitmap_buf_len) }; + + match config_obj.query_properties(bitmap_buf_slice) { + Ok(len) => len as i32, + Err(e) => e as i32, + } + } + + let x = userdata.map_or(null(), |t| ptr::from_ref(t) as *const c_void); + + InputConfigBackend { + features: KRUN_INPUT_CONFIG_FEATURE_QUERY as u64, + create_userdata: x, + create_userdata_lifetime: PhantomData, + create_fn: Some(create_config_fn::), + vtable: krun_input_config_vtable { + destroy: Some(config_destroy_fn::), + query_device_name: Some(query_device_name_fn::), + query_serial_name: Some(query_serial_name_fn::), + query_device_ids: Some(query_device_ids_fn::), + query_event_capabilities: Some(query_event_capabilities_fn::), + query_abs_info: Some(query_abs_info_fn::), + query_properties: Some(query_properties_fn::), + }, + } + } +} + +pub trait IntoInputEvents { + fn into_input_events(userdata: Option<&T>) -> InputEventProviderBackend<'_>; +} + +impl IntoInputEvents for I +where + I: InputEventsImpl + ObjectNew, +{ + fn into_input_events(userdata: Option<&UserData>) -> InputEventProviderBackend<'_> { + extern "C" fn create_events_fn>( + instance: *mut *mut c_void, + userdata: *const c_void, + _reserved: *const c_void, + ) -> i32 { + let actual_userdata = if userdata.is_null() { + None + } else { + Some(unsafe { &*(userdata as *const T) }) + }; + + let events_obj = I::new(actual_userdata); + let boxed_events = Box::into_raw(Box::new(events_obj)); + unsafe { *instance = boxed_events as *mut c_void }; + 0 + } + + extern "C" fn events_destroy_fn(instance: *mut c_void) -> i32 { + if instance.is_null() { + return 0; + } + let _ = unsafe { Box::from_raw(instance as *mut I) }; + 0 + } + + extern "C" fn get_ready_efd_fn(instance: *mut c_void) -> i32 { + let events_obj = unsafe { &*(instance as *const I) }; + match events_obj.get_read_notify_fd() { + Ok(fd) => fd.as_raw_fd(), + Err(e) => e as i32, + } + } + + extern "C" fn next_event_fn( + instance: *mut c_void, + out_event: *mut crate::InputEvent, + ) -> i32 { + let events_obj = unsafe { &mut *(instance as *mut I) }; + let out_event = unsafe { &mut *out_event }; + + match events_obj.next_event() { + Ok(Some(event)) => { + *out_event = event; + 1 + } + Ok(None) => 0, + Err(e) => e as i32, + } + } + let x: *const c_void = userdata.map_or(null(), |t| ptr::from_ref(t) as *const c_void); + InputEventProviderBackend { + features: KRUN_INPUT_EVENT_PROVIDER_FEATURE_QUEUE as u64, + create_userdata: x, + create_userdata_lifetime: PhantomData, + create_fn: Some(create_events_fn::), + vtable: krun_input_event_provider_vtable { + destroy: Some(events_destroy_fn::), + get_ready_efd: Some(get_ready_efd_fn::), + next_event: Some(next_event_fn::), + }, + } + } +} diff --git a/src/libkrun/Cargo.toml b/src/libkrun/Cargo.toml index 8aa7929a6..a1b5e2fa1 100644 --- a/src/libkrun/Cargo.toml +++ b/src/libkrun/Cargo.toml @@ -14,6 +14,7 @@ blk = [] efi = [ "blk", "net" ] gpu = ["krun_display"] snd = [] +input = ["krun_input", "vmm/input", "devices/input"] virgl_resource_map2 = [] nitro = [ "dep:nitro", "dep:nitro-enclaves" ] @@ -25,6 +26,7 @@ libloading = "0.8" log = "0.4.0" once_cell = "1.4.1" krun_display = { path = "../krun_display", optional = true, features = ["bindgen_clang_runtime"] } +krun_input = { path = "../krun_input", optional = true, features = ["bindgen_clang_runtime"] } devices = { path = "../devices" } polly = { path = "../polly" } diff --git a/src/libkrun/src/lib.rs b/src/libkrun/src/lib.rs index c1740f13c..cbb49ab41 100644 --- a/src/libkrun/src/lib.rs +++ b/src/libkrun/src/lib.rs @@ -13,6 +13,7 @@ use devices::virtio::CacheType; use env_logger::{Env, Target}; #[cfg(feature = "gpu")] use krun_display::DisplayBackend; + use libc::c_char; #[cfg(feature = "net")] use libc::c_int; @@ -31,6 +32,8 @@ use std::ffi::{c_void, CStr}; use std::fs::File; #[cfg(target_os = "linux")] use std::os::fd::AsRawFd; +#[cfg(feature = "input")] +use std::os::fd::BorrowedFd; use std::os::fd::{FromRawFd, RawFd}; use std::path::PathBuf; use std::slice; @@ -61,6 +64,8 @@ use nitro::enclaves::NitroEnclave; #[cfg(feature = "gpu")] use devices::virtio::display::{DisplayInfoEdid, PhysicalSize, MAX_DISPLAYS}; +#[cfg(feature = "input")] +use krun_input::{InputConfigBackend, InputEventProviderBackend}; #[cfg(feature = "nitro")] use nitro_enclaves::launch::StartFlags; @@ -1396,6 +1401,91 @@ pub extern "C" fn krun_set_display_backend( KRUN_SUCCESS } +#[cfg(not(feature = "input"))] +#[allow(clippy::missing_safety_doc)] +#[no_mangle] +pub extern "C" fn krun_add_input_device( + _ctx_id: u32, + _config_backend: *const c_void, + _config_backend_size: size_t, + _event_provider_backend: *const c_void, + _event_provider_backend_size: size_t, +) -> i32 { + -libc::ENOTSUP +} + +#[cfg(feature = "input")] +#[allow(clippy::missing_safety_doc)] +#[no_mangle] +pub extern "C" fn krun_add_input_device_fd(ctx_id: u32, input_fd: i32) -> i32 { + use devices::virtio::input::passthrough::PassthroughInputBackend; + use krun_input::{IntoInputConfig, IntoInputEvents}; + + if input_fd < 0 { + return -libc::EINVAL; + } + // TODO: currently we let the fd (and it's Box allocation) live forever, we should eventually fix + // this + let input_fd = unsafe { + // SAFETY: The user provided fd should be valid. Its lifetime is 'static because it will + // exist until libkrun _exits the process + BorrowedFd::borrow_raw(input_fd) + }; + let borrowed_fd: &'static BorrowedFd<'static> = Box::leak(Box::new(input_fd)); + + let config_backend = PassthroughInputBackend::into_input_config(Some(borrowed_fd)); + let events_backend = PassthroughInputBackend::into_input_events(Some(borrowed_fd)); + + with_cfg(ctx_id, |cfg| { + cfg.vmr + .input_backends + .push((config_backend, events_backend)); + KRUN_SUCCESS + }) +} + +#[cfg(feature = "input")] +#[allow(clippy::missing_safety_doc)] +#[no_mangle] +pub unsafe extern "C" fn krun_add_input_device( + ctx_id: u32, + config_backend: *const InputConfigBackend<'static>, + config_backend_size: size_t, + event_provider_backend: *const InputEventProviderBackend<'static>, + event_provider_backend_size: size_t, +) -> i32 { + if config_backend.is_null() || event_provider_backend.is_null() { + return -libc::EINVAL; + } + + if config_backend_size < size_of::() + || event_provider_backend_size < size_of::() + { + return -libc::EINVAL; + } + + let config_backend = unsafe { *config_backend }; + let events_backend = unsafe { *event_provider_backend }; + + if !config_backend.verify() || !events_backend.verify() { + return -libc::EINVAL; + } + + with_cfg(ctx_id, |cfg| { + cfg.vmr + .input_backends + .push((config_backend, events_backend)); + KRUN_SUCCESS + }) +} + +#[cfg(not(feature = "input"))] +#[allow(clippy::missing_safety_doc)] +#[no_mangle] +pub unsafe extern "C" fn krun_add_input_device_fd(_ctx_id: u32, _input_fd: i32) -> i32 { + -libc::ENOTSUP +} + #[cfg(feature = "gpu")] #[allow(clippy::missing_safety_doc)] #[no_mangle] diff --git a/src/utils/src/pollable_channel.rs b/src/utils/src/pollable_channel.rs index 1d5cd21f4..aca76f4a4 100644 --- a/src/utils/src/pollable_channel.rs +++ b/src/utils/src/pollable_channel.rs @@ -2,7 +2,7 @@ use crate::eventfd::{EventFd, EFD_NONBLOCK, EFD_SEMAPHORE}; use std::collections::VecDeque; use std::io; use std::io::ErrorKind; -use std::os::fd::{AsRawFd, RawFd}; +use std::os::fd::{AsFd, AsRawFd, BorrowedFd, RawFd}; use std::sync::{Arc, Mutex}; /// A multiple producer single consumer channel that can be listened to by a file descriptor @@ -38,8 +38,19 @@ impl PollableChannelSender { self.inner.eventfd.write(1)?; Ok(()) } + + pub fn send_many>(&self, msg_iterator: I) -> io::Result<()> { + let msg_iterator = msg_iterator.into_iter(); + let mut data_lock = self.inner.queue.lock().unwrap(); + let old_len = data_lock.len(); + data_lock.extend(msg_iterator); + let num_added_items = data_lock.len() - old_len; + self.inner.eventfd.write(num_added_items as u64)?; + Ok(()) + } } +#[derive(Clone)] pub struct PollableChannelReciever { inner: Arc>, } @@ -53,7 +64,7 @@ impl PollableChannelReciever { Err(e) => return Err(e), } - Ok(data_lock.pop_back()) + Ok(data_lock.pop_front()) } pub fn len(&self) -> usize { @@ -70,3 +81,11 @@ impl AsRawFd for PollableChannelReciever { self.inner.eventfd.as_raw_fd() } } + +impl AsFd for PollableChannelReciever { + fn as_fd(&self) -> BorrowedFd<'_> { + // SAFETY: The lifetime of the fd is the same as the lifetime of self.inner.eventfd which + // is the same as the lifetime of self. + unsafe { BorrowedFd::borrow_raw(self.inner.eventfd.as_raw_fd()) } + } +} diff --git a/src/vmm/Cargo.toml b/src/vmm/Cargo.toml index b3158cd3c..2265a585d 100644 --- a/src/vmm/Cargo.toml +++ b/src/vmm/Cargo.toml @@ -13,6 +13,7 @@ blk = [] efi = [ "blk", "net" ] gpu = ["krun_display"] snd = [] +input = ["krun_input"] nitro = [] [dependencies] @@ -25,6 +26,7 @@ nix = { version = "0.30.1", features = ["fs", "term"] } vm-memory = { version = ">=0.13", features = ["backend-mmap"] } vmm-sys-util = ">=0.14" krun_display = { path = "../krun_display", optional = true, features = ["bindgen_clang_runtime"] } +krun_input = { path = "../krun_input", optional = true, features = ["bindgen_clang_runtime"] } arch = { path = "../arch" } arch_gen = { path = "../arch_gen" } diff --git a/src/vmm/src/builder.rs b/src/vmm/src/builder.rs index b6b92e6eb..1e7a52746 100644 --- a/src/vmm/src/builder.rs +++ b/src/vmm/src/builder.rs @@ -196,6 +196,8 @@ pub enum StartMicrovmError { RegisterFsSigwinch(kvm_ioctls::Error), /// Cannot initialize a MMIO Gpu device or add a device to the MMIO Bus. RegisterGpuDevice(device_manager::mmio::Error), + /// Cannot initialize a MMIO Input device or add a device to the MMIO Bus. + RegisterInputDevice(device_manager::mmio::Error), /// Cannot initialize a MMIO Network Device or add a device to the MMIO Bus. RegisterNetDevice(device_manager::mmio::Error), /// Cannot initialize a MMIO Rng device or add a device to the MMIO Bus. @@ -417,6 +419,14 @@ impl Display for StartMicrovmError { "Cannot initialize a MMIO Gpu Device or add a device to the MMIO Bus. {err_msg}" ) } + RegisterInputDevice(ref err) => { + let mut err_msg = format!("{err}"); + err_msg = err_msg.replace('\"', ""); + write!( + f, + "Cannot initialize a MMIO Input Device or add a device to the MMIO Bus. {err_msg}" + ) + } RegisterNetDevice(ref err) => { let mut err_msg = format!("{err}"); err_msg = err_msg.replace('\"', ""); @@ -985,6 +995,12 @@ pub fn build_microvm( _sender.clone(), )?; } + + #[cfg(feature = "input")] + if !vm_resources.input_backends.is_empty() { + attach_input_devices(&mut vmm, &vm_resources.input_backends, intc.clone())?; + } + #[cfg(not(any(feature = "tee", feature = "nitro")))] attach_fs_devices( &mut vmm, @@ -2140,6 +2156,29 @@ fn attach_gpu_device( Ok(()) } +#[cfg(feature = "input")] +fn attach_input_devices( + vmm: &mut Vmm, + input_backends: &[( + krun_input::InputConfigBackend<'static>, + krun_input::InputEventProviderBackend<'static>, + )], + intc: IrqChip, +) -> std::result::Result<(), StartMicrovmError> { + use self::StartMicrovmError::*; + + for (index, (config_backend, events_backend)) in input_backends.iter().enumerate() { + let input_device = Arc::new(Mutex::new( + devices::virtio::input::Input::new(*config_backend, *events_backend).unwrap(), + )); + + let id = format!("input{}", index); + attach_mmio_device(vmm, id, intc.clone(), input_device).map_err(RegisterInputDevice)?; + } + + Ok(()) +} + #[cfg(feature = "snd")] fn attach_snd_device(vmm: &mut Vmm, intc: IrqChip) -> std::result::Result<(), StartMicrovmError> { use self::StartMicrovmError::*; diff --git a/src/vmm/src/resources.rs b/src/vmm/src/resources.rs index 7f6652155..43ea451d9 100644 --- a/src/vmm/src/resources.rs +++ b/src/vmm/src/resources.rs @@ -138,6 +138,11 @@ pub struct VmResources { pub display_backend: Option>, #[cfg(feature = "gpu")] pub displays: Vec, + #[cfg(feature = "input")] + pub input_backends: Vec<( + krun_input::InputConfigBackend<'static>, + krun_input::InputEventProviderBackend<'static>, + )>, #[cfg(feature = "snd")] /// Enable the virtio-snd device. pub snd_device: bool, @@ -383,11 +388,13 @@ mod tests { gpu_virgl_flags: None, gpu_shm_size: None, #[cfg(feature = "gpu")] - display_backend: DisplayBackendConfig::Noop, + display_backend: None, #[cfg(feature = "gpu")] displays: Vec::new(), + #[cfg(feature = "input")] + input_backends: Vec::new(), #[cfg(feature = "snd")] - enable_snd: False, + snd_device: false, console_output: None, smbios_oem_strings: None, nested_enabled: false,