Skip to content

Introduce a display subsystem for GPU output #319

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
404 changes: 376 additions & 28 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ endif
ifeq ($(GPU),1)
FEATURE_FLAGS += --features gpu
endif
ifeq ($(GTK_DISPLAY),1)
FEATURE_FLAGS += --features gtk_display
endif
ifeq ($(VIRGL_RESOURCE_MAP2),1)
FEATURE_FLAGS += --features virgl_resource_map2
endif
Expand Down
55 changes: 55 additions & 0 deletions examples/chroot_vm.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ static void print_help(char *const name)
" --net=NET_MODE Set network mode\n"
" --passt-socket=PATH Instead of starting passt, connect to passt socket at PATH"
"NET_MODE can be either TSI (default) or PASST\n"
" --display=DISPLAY Add a display to the vm (can be specified multiple times)\n"
"\n"
"DISPLAY: string in the form 'display_id:width:height' (e.g. '0:1920:1080')\n"
"NEWROOT: the root directory of the vm\n"
"COMMAND: the command you want to execute in the vm\n"
"COMMAND_ARGS: arguments of COMMAND\n",
Expand All @@ -49,17 +51,47 @@ static const struct option long_options[] = {
{ "help", no_argument, NULL, 'h' },
{ "net_mode", required_argument, NULL, 'N' },
{ "passt-socket", required_argument, NULL, 'P' },
{ "display", required_argument, NULL, 'D' },
{ NULL, 0, NULL, 0 }
};

struct display {
bool enabled;
uint32_t width;
uint32_t height;
};

struct cmdline {
bool show_help;
enum net_mode net_mode;
char const *passt_socket_path;
char const *new_root;
char *const *guest_argv;
bool enable_display_backend;
struct display displays[KRUN_MAX_DISPLAYS];
};

bool add_display(struct cmdline *cmdline, const char *arg) {
uint32_t index, width, height;

if (sscanf(arg, "%u:%u:%u", &index, &width, &height) != 3) {
fprintf(stderr, "Invalid value for --display\n", index);
return false;
}

if (index >= KRUN_MAX_DISPLAYS) {
fprintf(stderr, "Invalid display id: %u\n", index);
return false;
}

cmdline->enable_display_backend = true;
cmdline->displays[index].enabled = true;
cmdline->displays[index].width = width;
cmdline->displays[index].height = height;

return true;
}

bool parse_cmdline(int argc, char *const argv[], struct cmdline *cmdline)
{
assert(cmdline != NULL);
Expand All @@ -71,6 +103,8 @@ bool parse_cmdline(int argc, char *const argv[], struct cmdline *cmdline)
.passt_socket_path = NULL,
.new_root = NULL,
.guest_argv = NULL,
.enable_display_backend = false,
.displays = { 0 },
};

int option_index = 0;
Expand All @@ -94,6 +128,11 @@ bool parse_cmdline(int argc, char *const argv[], struct cmdline *cmdline)
case 'P':
cmdline->passt_socket_path = optarg;
break;
case 'D':
if (!add_display(cmdline, optarg)) {
return false;
}
break;
case '?':
return false;
default:
Expand Down Expand Up @@ -259,6 +298,22 @@ int main(int argc, char *const argv[])
return -1;
}

if (cmdline.enable_display_backend && (err = krun_set_display_backend_gtk(ctx_id))) {
errno = -err;
perror("Error enabling gtk display");
return -1;
}

for (int i = 0; i < KRUN_MAX_DISPLAYS; ++i) {
if (cmdline.displays[i].enabled) {
if (err = krun_set_display(ctx_id, i, cmdline.displays[i].width, cmdline.displays[i].height)) {
errno = -err;
perror("Error adding a display");
return -1;
}
}
}

// Map port 18000 in the host to 8000 in the guest (if networking uses TSI)
if (cmdline.net_mode == NET_MODE_TSI) {
if (err = krun_set_port_map(ctx_id, &port_map[0])) {
Expand Down
32 changes: 32 additions & 0 deletions include/libkrun.h
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,38 @@ int32_t krun_set_gpu_options2(uint32_t ctx_id,
uint32_t virgl_flags,
uint64_t shm_size);

/* Maximum number of displays. Same as VIRTIO_GPU_MAX_SCANOUTS defined in the virtio-gpu spec */
#define KRUN_MAX_DISPLAYS 16

/**
* Configure and enable a display output for the VM.
*
* Some display backend must be set using krun_set_display_backend_*, and the GPU
* device must be enabled.
*
* Arguments:
* "ctx_id" - the configuration context ID.
* "display_id" - the ID of the display (range: 0 to KRUN_MAX_DISPLAYS - 1)
* "width" - the width of the window/display
* "height" - the height of the window/display
*
* Returns:
* Zero on success or a negative error number on failure.
*/
int32_t krun_set_display(uint32_t ctx_id, uint32_t display_id, uint32_t width, uint32_t height);

/**
* Enable the Gtk display backend. This allows you to attach virtual displays to the GPU
* for graphical output.
*
* Arguments:
* "ctx_id" - the configuration context ID
*
* Returns:
* Zero on success or a negative error number on failure.
*/
int32_t krun_set_display_backend_gtk(uint32_t ctx_id);

/**
* Enables or disables a virtio-snd device.
*
Expand Down
3 changes: 3 additions & 0 deletions src/devices/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ blk = []
efi = ["blk", "net"]
gpu = ["rutabaga_gfx", "thiserror", "zerocopy", "zerocopy-derive"]
snd = ["pw", "thiserror"]
gtk_display = ["gpu", "gtk4", "glib"]
virgl_resource_map2 = []

[dependencies]
Expand All @@ -29,6 +30,8 @@ virtio-bindings = "0.2.0"
vm-memory = { version = ">=0.13", features = ["backend-mmap"] }
zerocopy = { version = "0.6.3", optional = true }
zerocopy-derive = { version = "0.6.3", optional = true }
gtk4 = { version = "0.9.6", features = ["v4_14"], optional = true }
glib = { version = "0.20.9", optional = true }

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is already pulled by gtk4, no need for a specific dep.


arch = { path = "../arch" }
utils = { path = "../utils" }
Expand Down
116 changes: 116 additions & 0 deletions src/devices/src/display/gtk/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
mod worker;

use crate::display::gtk::worker::gtk_display_main_loop;
use crate::display::{check_scanout_id, DisplayBackend, DisplayBackendError, DisplayInfoList};
use crate::virtio::GpuResourceFormat;
use gtk4::gdk;
use std::thread;
use utils::pollable_channel::{pollable_channel, PollableChannelSender};

enum DisplayEvent {
ConfigureScanout {
scanout_id: u32,
width: i32,
height: i32,
format: gdk::MemoryFormat,
},
DisableScanout {
scanout_id: u32,
},
UpdateScanout {
scanout_id: u32,
data: Vec<u8>,
/// stride/pitch of row specified in bytes
stride: u32,
},
}

pub struct DisplayBackendGtk {
tx: PollableChannelSender<DisplayEvent>,
displays: DisplayInfoList,
}

fn resource_format_into_gdk(format: GpuResourceFormat) -> gdk::MemoryFormat {
match format {
GpuResourceFormat::BGRA => gdk::MemoryFormat::B8g8r8a8,
GpuResourceFormat::BGRX => gdk::MemoryFormat::B8g8r8x8,
GpuResourceFormat::ARGB => gdk::MemoryFormat::A8r8g8b8,
GpuResourceFormat::XRGB => gdk::MemoryFormat::X8r8g8b8,
GpuResourceFormat::RGBA => gdk::MemoryFormat::R8g8b8a8,
GpuResourceFormat::XBGR => gdk::MemoryFormat::X8b8g8r8,
GpuResourceFormat::ABGR => gdk::MemoryFormat::A8b8g8r8,
GpuResourceFormat::RGBX => gdk::MemoryFormat::R8g8b8x8,
}
}

impl DisplayBackendGtk {
pub fn new(displays: DisplayInfoList) -> DisplayBackendGtk {
let (tx, rx) = pollable_channel().unwrap();
let displays_clone = displays.clone();
thread::Builder::new()
.name("gtk display".to_string())
.spawn(move || {
gtk_display_main_loop(rx, displays_clone);
})
.unwrap();

Self { displays, tx }
}
}

impl DisplayBackend for DisplayBackendGtk {
fn displays(&self) -> &DisplayInfoList {
&self.displays
}

fn configure_scanout(
&self,
scanout_id: u32,
width: u32,
height: u32,
format: GpuResourceFormat,
) -> Result<(), DisplayBackendError> {
check_scanout_id(self, scanout_id)?;
let Ok(width) = width.try_into() else {
warn!("Display width out of range");
return Err(DisplayBackendError::InvalidParameter);
};

let Ok(height) = height.try_into() else {
warn!("Display width out of range");
return Err(DisplayBackendError::InvalidParameter);
};

let format = resource_format_into_gdk(format);

self.tx.send(DisplayEvent::ConfigureScanout {
scanout_id,
width,
height,
format,
})?;
Ok(())
}

fn disable_scanout(&self, scanout_id: u32) -> Result<(), DisplayBackendError> {
check_scanout_id(self, scanout_id)?;
self.tx.send(DisplayEvent::DisableScanout { scanout_id })?;
Ok(())
}

fn update_scanout(
&self,
scanout_id: u32,
data: Vec<u8>,
stride: u32,
) -> Result<(), DisplayBackendError> {
check_scanout_id(self, scanout_id)?;

self.tx.send(DisplayEvent::UpdateScanout {
scanout_id,
data,
stride,
})?;
Ok(())
}
}
Loading
Loading