Skip to content

Commit 7750db2

Browse files
committed
examples/gui_vm, gtk_display: Add support for input (keyboard+touch)
Signed-off-by: Matej Hrica <[email protected]>
1 parent cc71a0f commit 7750db2

File tree

9 files changed

+1307
-61
lines changed

9 files changed

+1307
-61
lines changed

examples/Cargo.lock

Lines changed: 14 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/gui_vm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ edition = "2024"
66
[dependencies]
77
gtk_display = { path = "../krun_gtk_display" }
88
krun-sys = { path = "../../krun-sys" }
9+
krun_input = { path = "../../src/krun_input" }
910
anyhow = "1.0.98"
1011
clap = "4.5.39"
1112
clap_derive = "4.5.32"

examples/gui_vm/src/main.rs

Lines changed: 157 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
use clap::Parser;
22
use clap_derive::Parser;
3-
use gtk_display::DisplayBackendHandle;
3+
use gtk_display::{
4+
Axis, DisplayBackendHandle, DisplayInputOptions, InputBackendHandle, TouchArea,
5+
TouchScreenOptions,
6+
};
7+
48
use krun_sys::{
5-
VIRGLRENDERER_RENDER_SERVER, VIRGLRENDERER_THREAD_SYNC, VIRGLRENDERER_USE_ASYNC_FENCE_CB,
6-
VIRGLRENDERER_USE_EGL, VIRGLRENDERER_VENUS, krun_add_display, krun_create_ctx,
9+
KRUN_DISK_FORMAT_QCOW2, KRUN_LOG_LEVEL_TRACE, KRUN_LOG_LEVEL_WARN, KRUN_LOG_STYLE_ALWAYS,
10+
KRUN_LOG_TARGET_DEFAULT, VIRGLRENDERER_RENDER_SERVER, VIRGLRENDERER_THREAD_SYNC,
11+
VIRGLRENDERER_USE_ASYNC_FENCE_CB, VIRGLRENDERER_USE_EGL, VIRGLRENDERER_VENUS, krun_add_disk2,
12+
krun_add_display, krun_add_input_device, krun_add_input_device_fd, krun_create_ctx,
713
krun_display_set_dpi, krun_display_set_physical_size, krun_display_set_refresh_rate,
8-
krun_set_display_backend, krun_set_exec, krun_set_gpu_options, krun_set_log_level,
9-
krun_set_root, krun_start_enter,
14+
krun_init_log, krun_set_display_backend, krun_set_exec, krun_set_gpu_options2,
15+
krun_set_passt_fd, krun_set_root, krun_set_root_disk_remount, krun_set_vm_config,
16+
krun_start_enter,
1017
};
1118
use log::LevelFilter;
1219
use regex::{Captures, Regex};
1320
use std::ffi::{CString, c_void};
1421
use std::fmt::Display;
22+
use std::fs::{File, OpenOptions};
23+
use std::mem::size_of_val;
24+
25+
use anyhow::Context;
26+
use std::os::fd::IntoRawFd;
27+
use std::os::unix::net::UnixStream;
28+
use std::path::PathBuf;
1529
use std::process::exit;
1630
use std::ptr::null;
1731
use std::str::FromStr;
@@ -32,19 +46,20 @@ struct DisplayArg {
3246
height: u32,
3347
refresh_rate: Option<u32>,
3448
physical_size: Option<PhysicalSize>,
49+
touch: bool,
3550
}
3651

3752
/// Parses a display settings string.
3853
/// The expected format is "WIDTHxHEIGHT[@FPS][:DPIdpi|:PHYSICAL_WIDTHxPHYSICAL_HEIGHTmm]".
3954
fn parse_display(display_string: &str) -> Result<DisplayArg, String> {
4055
static RE: LazyLock<Regex> = LazyLock::new(|| {
4156
Regex::new(
42-
r"^(?P<width>\d+)x(?P<height>\d+)(?:@(?P<refresh_rate>\d+))?(?::(?P<dpi>\d+)dpi|:(?P<width_mm>\d+)x(?P<height_mm>\d+)mm)?$",
57+
r"^(?P<width>\d+)x(?P<height>\d+)(?:@(?P<refresh_rate>\d+))?(?::(?P<dpi>\d+)dpi|:(?P<width_mm>\d+)x(?P<height_mm>\d+)mm)?(?P<touch>\+touch(screen)?)?$",
4358
).unwrap()
4459
});
4560

4661
let captures = RE.captures(display_string).ok_or_else(|| {
47-
format!("Invalid display string '{display_string}' format. Examples of valid values:\n '1920x1080', '1920x1080@60', '1920x1080:162x91mm', '1920x1080:300dpi', '1920x1080@90:300dpi'")
62+
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'")
4863
})?;
4964

5065
fn parse_group<T: FromStr>(captures: &Captures, name: &str) -> Result<Option<T>, String>
@@ -78,6 +93,7 @@ fn parse_display(display_string: &str) -> Result<DisplayArg, String> {
7893
(None, None, None) => None,
7994
_ => unreachable!("regex bug"),
8095
},
96+
touch: captures.name("touch").is_some(),
8197
})
8298
}
8399

@@ -86,41 +102,103 @@ struct Args {
86102
#[arg(long)]
87103
root_dir: Option<CString>,
88104

105+
// Set disk to mount after boot
106+
#[arg(long)]
107+
remount_disk: Option<CString>,
108+
89109
executable: Option<CString>,
90110
argv: Vec<CString>,
91111
// Display specifications in the format WIDTHxHEIGHT[@FPS][:DPIdpi|:PHYSICAL_WIDTHxPHYSICAL_HEIGHTmm]
92112
#[clap(long, value_parser = parse_display)]
93113
display: Vec<DisplayArg>,
114+
115+
/// Attach a virtual keyboard input device
116+
#[arg(long)]
117+
keyboard_input: bool,
118+
119+
/// Pipe (or file) where to write log (with terminal color formatting)
120+
#[arg(long)]
121+
color_log: Option<PathBuf>,
122+
123+
/// Passthrough an input device (e.g. /dev/input/event0)
124+
#[arg(long)]
125+
input: Vec<PathBuf>,
94126
}
95127

96-
fn krun_thread(args: &Args, display_backend_handle: DisplayBackendHandle) -> anyhow::Result<()> {
128+
fn krun_thread(
129+
args: &Args,
130+
display_backend_handle: DisplayBackendHandle,
131+
input_device_handles: Vec<InputBackendHandle>,
132+
) -> anyhow::Result<()> {
97133
unsafe {
98-
krun_call!(krun_set_log_level(3))?;
134+
if let Some(path) = &args.color_log {
135+
krun_call!(krun_init_log(
136+
OpenOptions::new()
137+
.write(true)
138+
.open(path)
139+
.context("Failed to open log output")?
140+
.into_raw_fd(),
141+
KRUN_LOG_LEVEL_TRACE,
142+
KRUN_LOG_STYLE_ALWAYS,
143+
0
144+
))?;
145+
} else {
146+
krun_call!(krun_init_log(
147+
KRUN_LOG_TARGET_DEFAULT,
148+
KRUN_LOG_LEVEL_WARN,
149+
0,
150+
0,
151+
))?;
152+
}
153+
99154
let ctx = krun_call_u32!(krun_create_ctx())?;
100155

101-
krun_call!(krun_set_gpu_options(
156+
krun_call!(krun_set_vm_config(ctx, 4, 4096))?;
157+
158+
krun_call!(krun_set_passt_fd(
159+
ctx,
160+
UnixStream::connect("/tmp/passt_1.socket")
161+
.unwrap()
162+
.into_raw_fd()
163+
))?;
164+
krun_call!(krun_set_gpu_options2(
102165
ctx,
103166
VIRGLRENDERER_USE_EGL
104167
| VIRGLRENDERER_VENUS
105168
| VIRGLRENDERER_RENDER_SERVER
106169
| VIRGLRENDERER_THREAD_SYNC
107-
| VIRGLRENDERER_USE_ASYNC_FENCE_CB
170+
| VIRGLRENDERER_USE_ASYNC_FENCE_CB,
171+
4096
108172
))?;
109173

110-
if let Some(root_dir) = &args.root_dir {
111-
krun_call!(krun_set_root(ctx, root_dir.as_ptr()))?;
112-
// Executable variable should be set if we have root_dir, this is verified by clap
113-
let executable = args.executable.as_ref().unwrap().as_ptr();
114-
let argv: Vec<_> = args.argv.iter().map(|a| a.as_ptr()).collect();
115-
let argv_ptr = if argv.is_empty() {
174+
if let Some(remount_disk) = &args.remount_disk {
175+
krun_call!(krun_add_disk2(
176+
ctx,
177+
c"/dev/vda".as_ptr(),
178+
remount_disk.as_ptr(),
179+
KRUN_DISK_FORMAT_QCOW2,
180+
false
181+
))?;
182+
krun_call!(krun_set_root_disk_remount(
183+
ctx,
184+
c"/dev/vda".as_ptr(),
185+
c"btrfs".as_ptr(),
116186
null()
117-
} else {
118-
argv.as_ptr()
119-
};
120-
let envp = [null()];
121-
krun_call!(krun_set_exec(ctx, executable, argv_ptr, envp.as_ptr()))?;
187+
))?;
188+
} else if let Some(root_dir) = &args.root_dir {
189+
krun_call!(krun_set_root(ctx, root_dir.as_ptr()))?;
122190
}
123191

192+
let executable = args.executable.as_ref().unwrap().as_ptr();
193+
let argv: Vec<_> = args.argv.iter().map(|a| a.as_ptr()).collect();
194+
let argv_ptr = if argv.is_empty() {
195+
null()
196+
} else {
197+
argv.as_ptr()
198+
};
199+
let envp = [null()];
200+
krun_call!(krun_set_exec(ctx, executable, argv_ptr, envp.as_ptr()))?;
201+
124202
for display in &args.display {
125203
let display_id = krun_call_u32!(krun_add_display(ctx, display.width, display.height))?;
126204
if let Some(refresh_rate) = display.refresh_rate {
@@ -144,21 +222,74 @@ fn krun_thread(args: &Args, display_backend_handle: DisplayBackendHandle) -> any
144222
&raw const display_backend as *const c_void,
145223
size_of_val(&display_backend),
146224
))?;
225+
226+
for input in &args.input {
227+
let fd = File::open(input)
228+
.context("Failed to open input device: {input}")?
229+
.into_raw_fd();
230+
krun_call!(krun_add_input_device_fd(ctx, fd))
231+
.context("Failed to attach input device")?;
232+
}
233+
234+
// Configure all input devices
235+
for handle in &input_device_handles {
236+
let config_backend = handle.get_config();
237+
let event_provider_backend = handle.get_events();
238+
239+
krun_call!(krun_add_input_device(
240+
ctx,
241+
&raw const config_backend as *const c_void,
242+
size_of_val(&config_backend),
243+
&raw const event_provider_backend as *const c_void,
244+
size_of_val(&event_provider_backend),
245+
))?;
246+
}
247+
147248
krun_call!(krun_start_enter(ctx))?;
148249
};
149250
Ok(())
150251
}
151252

152253
fn main() -> anyhow::Result<()> {
153-
env_logger::builder().filter_level(LevelFilter::Info).init();
254+
env_logger::builder()
255+
.filter_level(LevelFilter::Debug)
256+
.init();
154257
let args = Args::parse();
155258

156-
let (display_backend, display_worker) =
157-
gtk_display::crate_display("libkrun examples/gui_vm".to_string());
259+
let mut per_display_inputs = vec![vec![]; args.display.len()];
260+
for (idx, display) in args.display.iter().enumerate() {
261+
if display.touch {
262+
per_display_inputs[idx].push(DisplayInputOptions::TouchScreen(TouchScreenOptions {
263+
// There is no specific reason for these axis sizes, just picked what my
264+
// physical hardware had
265+
area: TouchArea {
266+
x: Axis {
267+
max: 13764,
268+
res: 40,
269+
..Default::default()
270+
},
271+
y: Axis {
272+
max: 7740,
273+
res: 40,
274+
..Default::default()
275+
},
276+
},
277+
emit_mt: true,
278+
emit_non_mt: false,
279+
triggered_by_mouse: true,
280+
}));
281+
}
282+
}
283+
284+
let (display_backend, input_backends, display_worker) = gtk_display::init(
285+
"libkrun examples/gui_vm".to_string(),
286+
args.keyboard_input,
287+
per_display_inputs,
288+
)?;
158289

159290
thread::scope(|s| {
160291
s.spawn(|| {
161-
if let Err(e) = krun_thread(&args, display_backend) {
292+
if let Err(e) = krun_thread(&args, display_backend, input_backends) {
162293
eprintln!("{e}");
163294
exit(1);
164295
}

examples/krun_gtk_display/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ edition = "2024"
66

77
[dependencies]
88
utils = { path = "../../src/utils" } # Our version of rust vmm-sys-util
9-
crossbeam-channel = "0.5.15"
109
gtk = { version = "0.10", package = "gtk4", features = ["v4_16"] }
1110
krun_display = { path = "../../src/krun_display" }
11+
krun_input = { path = "../../src/krun_input" }
1212
anyhow = "1.0.98"
1313
log = "0.4.27"
1414
libc = "0.2.174"

examples/krun_gtk_display/src/display_backend.rs

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
1-
use crossbeam_channel::{Receiver, Sender, TrySendError, bounded};
21
use gtk::{gdk::MemoryFormat, glib::Bytes};
32
use krun_display::{
43
DisplayBackendBasicFramebuffer, DisplayBackendError, DisplayBackendNew, MAX_DISPLAYS, Rect,
54
ResourceFormat,
65
};
7-
use log::error;
86
use std::mem;
7+
use std::sync::mpsc::{Receiver, SyncSender, sync_channel};
98
use utils::pollable_channel::PollableChannelSender;
109

1110
// We try to push the maximum amount of data to the GTK thread. Currently, we want the display thread
1211
// deal with dropping the frames if they are coming too quickly to render. If we set this to a lower
1312
// number could slow down the libkrun thread, by making it wait for the display thread when calling
1413
// DisplayBackendBasicFramebuffer::alloc_frame.
15-
const MAX_DISPLAY_BUFFERS: usize = 4;
14+
const MAX_DISPLAY_BUFFERS: usize = 32;
1615
const _: () = {
1716
if MAX_DISPLAY_BUFFERS < 2 {
1817
panic!("At least 2 buffers are required")
@@ -74,7 +73,7 @@ impl DisplayBackendBasicFramebuffer for GtkDisplayBackend {
7473
if let Some(ref mut scanout) = self.scanouts[scanout_id as usize] {
7574
scanout.required_buffer_size = required_buffer_size;
7675
} else {
77-
let (buffer_tx, buffer_rx) = bounded(MAX_DISPLAY_BUFFERS);
76+
let (buffer_tx, buffer_rx) = sync_channel(MAX_DISPLAY_BUFFERS);
7877

7978
for _ in 0..MAX_DISPLAY_BUFFERS {
8079
// We initialize the buffers as empty in case we don't end up using them, the buffers
@@ -171,7 +170,7 @@ fn resource_format_into_gdk(format: ResourceFormat) -> MemoryFormat {
171170
}
172171

173172
struct Scanout {
174-
buffer_tx: Sender<Vec<u8>>,
173+
buffer_tx: SyncSender<Vec<u8>>,
175174
buffer_rx: Receiver<Vec<u8>>,
176175
required_buffer_size: usize,
177176
current_buffer: Vec<u8>,
@@ -187,7 +186,7 @@ impl Scanout {
187186
}
188187

189188
struct BufferReturner {
190-
return_tx: Sender<Vec<u8>>,
189+
return_tx: SyncSender<Vec<u8>>,
191190
buf: Vec<u8>,
192191
}
193192

@@ -199,14 +198,11 @@ impl AsRef<[u8]> for BufferReturner {
199198

200199
impl Drop for BufferReturner {
201200
fn drop(&mut self) {
202-
match self.return_tx.try_send(mem::take(&mut self.buf)) {
201+
match self.return_tx.send(mem::take(&mut self.buf)) {
203202
Ok(_) => (),
204203
// We can just drop the buffer if the other party doesn't exist anymore.
205-
Err(TrySendError::Disconnected(_)) => (),
206-
Err(TrySendError::Full(_)) => {
207-
error!(
208-
"Either the channel is too small or we have more than MAX_DISPLAY_BUFFERS buffers!?"
209-
);
204+
Err(_) => {
205+
// If send fails, it's because the receiver is gone, so just drop the buffer
210206
}
211207
}
212208
}

0 commit comments

Comments
 (0)