vhost-device-gpu: Add Initial Implementation

This program is a vhost-user backend daemon that provides
VIRTIO GPU device emulation as specified in the VIRTIO Spec v.1.2
https://docs.oasis-open.org/virtio/virtio/v1.2/csd01/virtio-v1.2-csd01.html
This crate utilizes the rutabaga crate from crosvm with some
minor modification to rutabaga crate to fix compilation.
This crate depends on this PR[rust-vmm/vhost#239]
that implements support for QEMU's vhost-user-gpu protocol.

This device uses the rutabaga_gfx crate to offer two rendering backends:
1. Virglrenderer:
   - Rutabaga translates OpenGL API and Vulkan calls to an intermediate
     representation and allows for OpenGL acceleration on the host.
2. Gfxstream:
   - GLES and Vulkan calls are forwarded to the host.

These backends can be used by simply changing the `--gpu-mode` command
line option.
This crate also includes some modifications from libkrun virtio-gpu device
https://github.com/containers/libkrun/tree/main/src/devices/src/virtio/gpu

Fixes: rust-vmm#598

Co-authored-by: Dorinda Bassey <dbassey@redhat.com>
Co-authored-by: Matej Hrica <mhrica@redhat.com>

Signed-off-by: Dorinda Bassey <dbassey@redhat.com>
Signed-off-by: Matej Hrica <mhrica@redhat.com>
This commit is contained in:
Dorinda Bassey 2024-10-31 15:19:36 +01:00 committed by Stefano Garzarella
parent b7321b7997
commit 391e32b82f
14 changed files with 4652 additions and 2 deletions

190
staging/Cargo.lock generated
View File

@ -108,6 +108,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
[[package]]
name = "clap"
version = "4.5.21"
@ -154,6 +160,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "downcast"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
[[package]]
name = "enumn"
version = "0.1.14"
@ -220,6 +232,18 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "fragile"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa"
[[package]]
name = "futures"
version = "0.3.31"
@ -397,6 +421,41 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "mockall"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2"
dependencies = [
"cfg-if",
"downcast",
"fragile",
"mockall_derive",
"predicates",
"predicates-tree",
]
[[package]]
name = "mockall_derive"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898"
dependencies = [
"cfg-if",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "nix"
version = "0.27.1"
@ -408,6 +467,19 @@ dependencies = [
"libc",
]
[[package]]
name = "nix"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"cfg_aliases",
"libc",
"memoffset",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
@ -457,6 +529,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
@ -466,6 +544,32 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "predicates"
version = "3.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97"
dependencies = [
"anstyle",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931"
[[package]]
name = "predicates-tree"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro-crate"
version = "3.2.0"
@ -484,6 +588,12 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "quick-error"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
[[package]]
name = "quote"
version = "1.0.37"
@ -558,6 +668,17 @@ version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "remain"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46aef80f842736de545ada6ec65b81ee91504efd6853f4b96de7414c42ae7443"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "rstest"
version = "0.23.0"
@ -610,6 +731,36 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rusty-fork"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f"
dependencies = [
"fnv",
"quick-error",
"tempfile",
"wait-timeout",
]
[[package]]
name = "rutabaga_gfx"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6628c6391bc654170f64fe8bfb7e5da3bf97f7ed3174c52c7a7349c012adec9c"
dependencies = [
"anyhow",
"cfg-if",
"libc",
"log",
"nix 0.28.0",
"pkg-config",
"remain",
"thiserror 1.0.69",
"winapi",
"zerocopy",
]
[[package]]
name = "semver"
version = "1.0.23"
@ -655,6 +806,12 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "termtree"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
[[package]]
name = "thiserror"
version = "1.0.69"
@ -755,7 +912,7 @@ dependencies = [
"bitflags 2.6.0",
"enumn",
"log",
"nix",
"nix 0.27.1",
"thiserror 1.0.69",
]
@ -772,6 +929,28 @@ dependencies = [
"vmm-sys-util",
]
[[package]]
name = "vhost-device-gpu"
version = "0.1.0"
dependencies = [
"assert_matches",
"clap",
"env_logger",
"libc",
"log",
"mockall",
"rusty-fork",
"rutabaga_gfx",
"tempfile",
"thiserror 2.0.3",
"vhost",
"vhost-user-backend",
"virtio-bindings",
"virtio-queue",
"vm-memory",
"vmm-sys-util",
]
[[package]]
name = "vhost-device-video"
version = "0.1.0"
@ -854,6 +1033,15 @@ dependencies = [
"libc",
]
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"

View File

@ -1,5 +1,6 @@
[workspace]
resolver = "2"
members = [
"vhost-device-gpu",
"vhost-device-video",
]

View File

@ -1,5 +1,5 @@
{
"coverage_score": 74.82,
"coverage_score": 79.80,
"exclude_path": "",
"crate_features": ""
}

View File

@ -0,0 +1,10 @@
# Changelog
## Unreleased
### Added
### Changed
### Fixed
### Deprecated

View File

@ -0,0 +1,39 @@
[package]
name = "vhost-device-gpu"
version = "0.1.0"
authors = ["Dorinda Bassey <dbassey@redhat.com>", "Matej Hrica <mhrica@redhat.com>"]
description = "A virtio-gpu device using the vhost-user protocol."
repository = "https://github.com/rust-vmm/vhost-device"
readme = "README.md"
keywords = ["gpu", "vhost", "vhost-user", "virtio"]
categories = ["multimedia::video", "virtualization"]
license = "Apache-2.0 OR BSD-3-Clause"
edition = "2021"
publish = false
[features]
xen = ["vm-memory/xen", "vhost/xen", "vhost-user-backend/xen"]
[dependencies]
clap = { version = "4.4", features = ["derive"] }
env_logger = "0.11.5"
libc = "0.2"
log = "0.4"
[target.'cfg(not(target_env = "musl"))'.dependencies]
rutabaga_gfx = { version = "0.1.4", features = ["gfxstream", "virgl_renderer"] }
thiserror = "2.0.3"
vhost = { version = "0.13.0", features = ["vhost-user-backend"] }
vhost-user-backend = "0.17"
virtio-bindings = "0.2.2"
virtio-queue = "0.14.0"
vm-memory = "0.16.1"
vmm-sys-util = "0.12.1"
[dev-dependencies]
assert_matches = "1.5"
tempfile = "3.13"
virtio-queue = { version = "0.14", features = ["test-utils"] }
vm-memory = { version = "0.16.1", features = ["backend-mmap", "backend-atomic"] }
mockall = "0.13.0"
rusty-fork = "0.3.0"

View File

@ -0,0 +1 @@
../../LICENSE-APACHE

View File

@ -0,0 +1 @@
../../LICENSE-BSD-3-Clause

View File

@ -0,0 +1,101 @@
# vhost-device-gpu - GPU emulation backend daemon
## Synopsis
```shell
vhost-device-gpu --socket-path <SOCKET>
```
## Description
A virtio-gpu device using the vhost-user protocol.
## Options
```text
-s, --socket-path <SOCKET>
vhost-user Unix domain socket path
-h, --help
Print help
-V, --version
Print version
```
## Limitations
We are currently only supporting sharing the display output to QEMU through a
socket using the transfer_read operation triggered by
VIRTIO_GPU_CMD_TRANSFER_FROM_HOST_3D to transfer data from and to virtio-gpu 3d
resources. It'll be nice to have support for directly sharing display output
resource using dmabuf.
This device does not yet support the VIRTIO_GPU_CMD_RESOURCE_CREATE_BLOB,
VIRTIO_GPU_CMD_SET_SCANOUT_BLOB and VIRTIO_GPU_CMD_RESOURCE_ASSIGN_UUID features.
Currently this crate requires some necessary bits in order to move the crate out of staging:
- Achieving a minimum of ~87% code coverage in the main vhost-device repository,
which requires some additional unit tests to increase code coverage.
- Addition of CLI arguments to specify the exact number of capsets and use
a default capset configuration when no capset is specified rather than using
hard-coded capset value.
## Features
The device leverages the [rutabaga_gfx](https://crates.io/crates/rutabaga_gfx) crate
to provide virglrenderer and gfxstream rendering. With Virglrenderer, Rutabaga
translates OpenGL API and Vulkan calls to an intermediate representation and allows
for OpenGL acceleration on the host. With the gfxstream rendering mode, GLES and
Vulkan calls are forwarded to the host with minimal modification.
## Examples
First start the daemon on the host machine using either of the 2 gpu modes:
1) virgl-renderer
2) gfxstream
```shell
host# vhost-device-gpu --socket-path /tmp/gpu.socket --gpu-mode virgl-renderer
```
With QEMU, there are two device front-ends you can use with this device.
You can either use `vhost-user-gpu-pci` or `vhost-user-vga`, which also
implements VGA, that allows you to see boot messages before the guest
initializes the GPU. You can also use different display outputs (for example
`gtk` or `dbus`).
By default, QEMU also adds another VGA output, use `-vga none` to make
sure it is disabled.
1) Using `vhost-user-gpu-pci`
Start QEMU with the following flags:
```text
-chardev socket,id=vgpu,path=/tmp/gpu.socket \
-device vhost-user-gpu-pci,chardev=vgpu,id=vgpu \
-object memory-backend-memfd,share=on,id=mem0,size=4G, \
-machine q35,memory-backend=mem0,accel=kvm \
-display gtk,gl=on,show-cursor=on \
-vga none
```
2) Using `vhost-user-vga`
Start QEMU with the following flags:
```text
-chardev socket,id=vgpu,path=/tmp/gpu.socket \
-device vhost-user-vga,chardev=vgpu,id=vgpu \
-object memory-backend-memfd,share=on,id=mem0,size=4G, \
-machine q35,memory-backend=mem0,accel=kvm \
-display gtk,gl=on,show-cursor=on \
-vga none
```
## License
This project is licensed under either of
- [Apache License](http://www.apache.org/licenses/LICENSE-2.0), Version 2.0
- [BSD-3-Clause License](https://opensource.org/licenses/BSD-3-Clause)

View File

@ -0,0 +1,7 @@
edition = "2018"
format_generated_files = false
format_code_in_doc_comments = true
format_strings = true
imports_granularity = "Crate"
group_imports = "StdExternalCrate"
wrap_comments = true

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,99 @@
// Copyright 2024 Red Hat Inc
//
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
#![deny(
clippy::undocumented_unsafe_blocks,
/* groups */
clippy::correctness,
clippy::suspicious,
clippy::complexity,
clippy::perf,
clippy::style,
clippy::nursery,
//* restriction */
clippy::dbg_macro,
clippy::rc_buffer,
clippy::as_underscore,
clippy::assertions_on_result_states,
//* pedantic */
clippy::cast_lossless,
clippy::cast_possible_wrap,
clippy::ptr_as_ptr,
clippy::bool_to_int_with_if,
clippy::borrow_as_ptr,
clippy::case_sensitive_file_extension_comparisons,
clippy::cast_lossless,
clippy::cast_ptr_alignment,
clippy::naive_bytecount
)]
#![allow(
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::must_use_candidate,
clippy::significant_drop_in_scrutinee,
clippy::significant_drop_tightening
)]
#[cfg(target_env = "gnu")]
pub mod device;
#[cfg(target_env = "gnu")]
pub mod protocol;
#[cfg(target_env = "gnu")]
pub mod virtio_gpu;
use std::path::{Path, PathBuf};
use clap::ValueEnum;
#[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum GpuMode {
VirglRenderer,
Gfxstream,
}
#[derive(Debug, Clone)]
/// This structure holds the internal configuration for the GPU backend,
/// derived from the command-line arguments provided through `GpuArgs`.
pub struct GpuConfig {
/// vhost-user Unix domain socket
socket_path: PathBuf,
gpu_mode: GpuMode,
}
impl GpuConfig {
/// Create a new instance of the `GpuConfig` struct, containing the
/// parameters to be fed into the gpu-backend server.
pub const fn new(socket_path: PathBuf, gpu_mode: GpuMode) -> Self {
Self {
socket_path,
gpu_mode,
}
}
/// Return the path of the unix domain socket which is listening to
/// requests from the guest.
pub fn socket_path(&self) -> &Path {
&self.socket_path
}
pub const fn gpu_mode(&self) -> GpuMode {
self.gpu_mode
}
}
#[cfg(test)]
mod tests {
use tempfile::tempdir;
use super::*;
#[test]
fn test_gpu_config() {
// Test the creation of `GpuConfig` struct
let test_dir = tempdir().expect("Could not create a temp test directory.");
let socket_path = test_dir.path().join("socket");
let gpu_config = GpuConfig::new(socket_path.clone(), GpuMode::VirglRenderer);
assert_eq!(gpu_config.socket_path(), socket_path);
}
}

View File

@ -0,0 +1,132 @@
// VIRTIO GPU Emulation via vhost-user
//
// Copyright 2024 Red Hat Inc
//
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
// Rust vmm container (https://github.com/rust-vmm/rust-vmm-container) doesn't
// have tools to do a musl build at the moment, and adding that support is
// tricky as well to the container. Skip musl builds until the time pre-built
// rutabaga library is available for musl.
#[cfg(target_env = "gnu")]
pub mod gnu_main {
use std::{path::PathBuf, process::exit};
use clap::Parser;
use log::{error, info};
use thiserror::Error as ThisError;
use vhost_device_gpu::{
device::{self, VhostUserGpuBackend},
GpuConfig, GpuMode,
};
use vhost_user_backend::VhostUserDaemon;
use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap};
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, ThisError)]
pub enum Error {
#[error("Could not create backend: {0}")]
CouldNotCreateBackend(device::Error),
#[error("Could not create daemon: {0}")]
CouldNotCreateDaemon(vhost_user_backend::Error),
#[error("Fatal error: {0}")]
ServeFailed(vhost_user_backend::Error),
}
#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
pub struct GpuArgs {
/// vhost-user Unix domain socket.
#[clap(short, long, value_name = "SOCKET")]
pub socket_path: PathBuf,
#[clap(short, long, value_enum)]
pub gpu_mode: GpuMode,
}
impl From<GpuArgs> for GpuConfig {
fn from(args: GpuArgs) -> Self {
let socket_path = args.socket_path;
let gpu_mode: GpuMode = args.gpu_mode;
GpuConfig::new(socket_path, gpu_mode)
}
}
pub fn start_backend(config: &GpuConfig) -> Result<()> {
info!("Starting backend");
let socket = config.socket_path();
let backend = VhostUserGpuBackend::new(config).map_err(Error::CouldNotCreateBackend)?;
let mut daemon = VhostUserDaemon::new(
"vhost-device-gpu-backend".to_string(),
backend.clone(),
GuestMemoryAtomic::new(GuestMemoryMmap::new()),
)
.map_err(Error::CouldNotCreateDaemon)?;
backend.set_epoll_handler(&daemon.get_epoll_handlers());
daemon.serve(socket).map_err(Error::ServeFailed)?;
Ok(())
}
pub fn main() {
env_logger::init();
if let Err(e) = start_backend(&GpuConfig::from(GpuArgs::parse())) {
error!("{e}");
exit(1);
}
}
}
#[cfg(target_env = "gnu")]
fn main() {
gnu_main::main();
}
#[cfg(target_env = "musl")]
fn main() {}
#[cfg(target_env = "gnu")]
#[cfg(test)]
mod tests {
use std::path::Path;
use assert_matches::assert_matches;
use tempfile::tempdir;
use vhost_device_gpu::{GpuConfig, GpuMode};
use super::gnu_main::{start_backend, Error, GpuArgs};
impl GpuArgs {
pub(crate) fn from_args(path: &Path) -> Self {
Self {
socket_path: path.to_path_buf(),
gpu_mode: GpuMode::Gfxstream,
}
}
}
#[test]
fn test_parse_successful() {
let test_dir = tempdir().expect("Could not create a temp test directory.");
let socket_path = test_dir.path().join("vgpu.sock");
let cmd_args = GpuArgs::from_args(socket_path.as_path());
let config = GpuConfig::from(cmd_args);
assert_eq!(config.socket_path(), socket_path);
}
#[test]
fn test_fail_listener() {
// This will fail the listeners and thread will panic.
let socket_name = Path::new("/proc/-1/nonexistent");
let cmd_args = GpuArgs::from_args(socket_name);
let config = GpuConfig::from(cmd_args);
assert_matches!(start_backend(&config).unwrap_err(), Error::ServeFailed(_));
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff