From 37082de03a5dd55d2feef6cecceb323a6ab928f3 Mon Sep 17 00:00:00 2001 From: Matej Hrica Date: Mon, 11 Nov 2024 18:50:00 +0100 Subject: [PATCH] gpu: add CLI arguments for configuring Rutabaga builder Add command line arguments for configuring which capsets and features are enabled when configuring Rutabaga. Since we now specify the capsets explicitly we can drop the MAX_NUM_CAPSETS constant and fix the TODO. The curently exposed capsets for virglrenderer are: virgl and virgl2. For gfxstream they are: gfxstream-vulkan and gfxstream-gles. Signed-off-by: Matej Hrica --- staging/Cargo.lock | 1 + staging/vhost-device-gpu/Cargo.toml | 1 + staging/vhost-device-gpu/README.md | 52 +++- staging/vhost-device-gpu/src/device.rs | 42 +-- staging/vhost-device-gpu/src/lib.rs | 305 ++++++++++++++++++--- staging/vhost-device-gpu/src/main.rs | 183 +++++++++++-- staging/vhost-device-gpu/src/virtio_gpu.rs | 37 +-- 7 files changed, 519 insertions(+), 102 deletions(-) diff --git a/staging/Cargo.lock b/staging/Cargo.lock index 474f82e..af8afc3 100644 --- a/staging/Cargo.lock +++ b/staging/Cargo.lock @@ -934,6 +934,7 @@ name = "vhost-device-gpu" version = "0.1.0" dependencies = [ "assert_matches", + "bitflags 2.6.0", "clap", "env_logger", "libc", diff --git a/staging/vhost-device-gpu/Cargo.toml b/staging/vhost-device-gpu/Cargo.toml index 554ad24..d7c4e81 100644 --- a/staging/vhost-device-gpu/Cargo.toml +++ b/staging/vhost-device-gpu/Cargo.toml @@ -34,6 +34,7 @@ virtio-bindings = "0.2.2" virtio-queue = "0.14.0" vm-memory = "0.16.1" vmm-sys-util = "0.12.1" +bitflags = "2.6.0" [dev-dependencies] assert_matches = "1.5" diff --git a/staging/vhost-device-gpu/README.md b/staging/vhost-device-gpu/README.md index 3d12eee..07accde 100644 --- a/staging/vhost-device-gpu/README.md +++ b/staging/vhost-device-gpu/README.md @@ -13,10 +13,54 @@ A virtio-gpu device using the vhost-user protocol. ## Options ```text - -s, --socket-path vhost-user Unix domain socket - -g, --gpu-mode [possible values: virglrenderer, gfxstream] - -h, --help Print help - -V, --version Print version + -s, --socket-path + vhost-user Unix domain socket + + -g, --gpu-mode + The mode specifies which backend implementation to use + + [possible values: virglrenderer, gfxstream] + + -c, --capset + Comma separated list of enabled capsets + + Possible values: + - virgl: [virglrenderer] OpenGL implementation, superseded by Virgl2 + - virgl2: [virglrenderer] OpenGL implementation + - gfxstream-vulkan: [gfxstream] Vulkan implementation (partial support only) + NOTE: Can only be used for 2D display output for now, there is no hardware acceleration yet + - gfxstream-gles: [gfxstream] OpenGL ES implementation (partial support only) + NOTE: Can only be used for 2D display output for now, there is no hardware acceleration yet + + --use-egl + Enable backend to use EGL + + [default: true] + [possible values: true, false] + + --use-glx + Enable backend to use GLX + + [default: false] + [possible values: true, false] + + --use-gles + Enable backend to use GLES + + [default: true] + [possible values: true, false] + + --use-surfaceless + Enable surfaceless backend option + + [default: true] + [possible values: true, false] + + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version ``` _NOTE_: Option `-g, --gpu-mode` can only accept the `gfxstream` value if the diff --git a/staging/vhost-device-gpu/src/device.rs b/staging/vhost-device-gpu/src/device.rs index 07a605d..eabd3b7 100644 --- a/staging/vhost-device-gpu/src/device.rs +++ b/staging/vhost-device-gpu/src/device.rs @@ -11,7 +11,7 @@ use std::{ sync::{self, Arc, Mutex}, }; -use log::{debug, error, trace, warn}; +use log::{debug, error, info, trace, warn}; use rutabaga_gfx::{ ResourceCreate3D, RutabagaFence, Transfer3D, RUTABAGA_PIPE_BIND_RENDER_TARGET, RUTABAGA_PIPE_TEXTURE_2D, @@ -50,7 +50,7 @@ use crate::{ VIRTIO_GPU_FLAG_FENCE, VIRTIO_GPU_FLAG_INFO_RING_IDX, VIRTIO_GPU_MAX_SCANOUTS, }, virtio_gpu::{RutabagaVirtioGpu, VirtioGpu, VirtioGpuRing}, - GpuConfig, GpuMode, + GpuConfig, }; type Result = std::result::Result; @@ -101,7 +101,7 @@ struct VhostUserGpuBackendInner { gpu_backend: Option, pub exit_event: EventFd, mem: Option>, - gpu_mode: GpuMode, + gpu_config: GpuConfig, } pub struct VhostUserGpuBackend { @@ -112,20 +112,26 @@ pub struct VhostUserGpuBackend { } impl VhostUserGpuBackend { - pub fn new(gpu_config: &GpuConfig) -> Result> { - log::trace!("VhostUserGpuBackend::new(config = {:?})", &gpu_config); + pub fn new(gpu_config: GpuConfig) -> Result> { + info!( + "GpuBackend using mode {} (capsets: '{}'), flags: {:?}", + gpu_config.gpu_mode(), + gpu_config.capsets(), + gpu_config.flags() + ); + let inner = VhostUserGpuBackendInner { virtio_cfg: VirtioGpuConfig { events_read: 0.into(), events_clear: 0.into(), num_scanouts: Le32::from(VIRTIO_GPU_MAX_SCANOUTS), - num_capsets: RutabagaVirtioGpu::MAX_NUMBER_OF_CAPSETS.into(), + num_capsets: Le32::from(gpu_config.capsets().num_capsets()), }, event_idx_enabled: false, gpu_backend: None, exit_event: EventFd::new(EFD_NONBLOCK).map_err(|_| Error::EventFdFailed)?, mem: None, - gpu_mode: gpu_config.gpu_mode(), + gpu_config, }; Ok(Arc::new(Self { @@ -575,7 +581,7 @@ impl VhostUserGpuBackendInner { // so if somehow another thread accidentally wants to create another gpu here, // it will panic anyway let virtio_gpu = - RutabagaVirtioGpu::new(control_vring, self.gpu_mode, gpu_backend); + RutabagaVirtioGpu::new(control_vring, &self.gpu_config, gpu_backend); event_poll_fd = virtio_gpu.get_event_poll_fd(); maybe_virtio_gpu.insert(virtio_gpu) @@ -715,7 +721,6 @@ mod tests { use assert_matches::assert_matches; use mockall::predicate; use rusty_fork::rusty_fork_test; - use tempfile::tempdir; use vhost::vhost_user::gpu_message::{VhostUserGpuScanout, VhostUserGpuUpdate}; use vhost_user_backend::{VhostUserDaemon, VringRwLock, VringT}; use virtio_bindings::virtio_ring::{VRING_DESC_F_NEXT, VRING_DESC_F_WRITE}; @@ -742,9 +747,9 @@ mod tests { VIRTIO_GPU_RESP_OK_NODATA, }, virtio_gpu::MockVirtioGpu, + GpuCapset, GpuFlags, GpuMode, }; - const SOCKET_PATH: &str = "vgpu.socket"; const MEM_SIZE: usize = 2 * 1024 * 1024; // 2MiB const CURSOR_QUEUE_ADDR: GuestAddress = GuestAddress(0x0); @@ -755,10 +760,13 @@ mod tests { const CONTROL_QUEUE_SIZE: u16 = 1024; fn init() -> (Arc, GuestMemoryAtomic) { - let test_dir = tempdir().expect("Could not create a temp test directory."); - let socket_path = test_dir.path().join(SOCKET_PATH); - let backend = - VhostUserGpuBackend::new(&GpuConfig::new(socket_path, GpuMode::VirglRenderer)).unwrap(); + let config = GpuConfig::new( + GpuMode::VirglRenderer, + Some(GpuCapset::VIRGL | GpuCapset::VIRGL2), + GpuFlags::default(), + ) + .unwrap(); + let backend = VhostUserGpuBackend::new(config).unwrap(); let mem = GuestMemoryAtomic::new( GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), MEM_SIZE)]).unwrap(), ); @@ -1340,10 +1348,8 @@ mod tests { rusty_fork_test! { #[test] fn test_verify_backend() { - let test_dir = tempdir().expect("Could not create a temp test directory."); - let socket_path = test_dir.path().join(SOCKET_PATH); - let gpu_config = GpuConfig::new(socket_path, GpuMode::VirglRenderer); - let backend = VhostUserGpuBackend::new(&gpu_config).unwrap(); + let gpu_config = GpuConfig::new(GpuMode::VirglRenderer, None, GpuFlags::default()).unwrap(); + let backend = VhostUserGpuBackend::new(gpu_config).unwrap(); assert_eq!(backend.num_queues(), NUM_QUEUES); assert_eq!(backend.max_queue_size(), QUEUE_SIZE); diff --git a/staging/vhost-device-gpu/src/lib.rs b/staging/vhost-device-gpu/src/lib.rs index 085ba02..484f8c1 100644 --- a/staging/vhost-device-gpu/src/lib.rs +++ b/staging/vhost-device-gpu/src/lib.rs @@ -39,28 +39,23 @@ pub mod device; pub mod protocol; pub mod virtio_gpu; -use std::path::{Path, PathBuf}; +use std::{ + fmt::{Display, Formatter}, + path::Path, +}; +use bitflags::bitflags; use clap::ValueEnum; use log::info; +#[cfg(feature = "gfxstream")] +use rutabaga_gfx::{RUTABAGA_CAPSET_GFXSTREAM_GLES, RUTABAGA_CAPSET_GFXSTREAM_VULKAN}; +use rutabaga_gfx::{RUTABAGA_CAPSET_VIRGL, RUTABAGA_CAPSET_VIRGL2}; use thiserror::Error as ThisError; use vhost_user_backend::VhostUserDaemon; use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap}; use crate::device::VhostUserGpuBackend; -type Result = std::result::Result; - -#[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(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] pub enum GpuMode { #[value(name = "virglrenderer", alias("virgl-renderer"))] @@ -69,51 +64,194 @@ pub enum GpuMode { Gfxstream, } +impl Display for GpuMode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::VirglRenderer => write!(f, "virglrenderer"), + #[cfg(feature = "gfxstream")] + Self::Gfxstream => write!(f, "gfxstream"), + } + } +} + +bitflags! { + /// A bitmask for representing supported gpu capability sets. + #[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub struct GpuCapset: u64 { + const VIRGL = 1 << RUTABAGA_CAPSET_VIRGL as u64; + const VIRGL2 = 1 << RUTABAGA_CAPSET_VIRGL2 as u64; + const ALL_VIRGLRENDERER_CAPSETS = Self::VIRGL.bits() | Self::VIRGL2.bits(); + + #[cfg(feature = "gfxstream")] + const GFXSTREAM_VULKAN = 1 << RUTABAGA_CAPSET_GFXSTREAM_VULKAN as u64; + #[cfg(feature = "gfxstream")] + const GFXSTREAM_GLES = 1 << RUTABAGA_CAPSET_GFXSTREAM_GLES as u64; + #[cfg(feature = "gfxstream")] + const ALL_GFXSTREAM_CAPSETS = Self::GFXSTREAM_VULKAN.bits() | Self::GFXSTREAM_GLES.bits(); + } +} + +impl Display for GpuCapset { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let mut first = true; + for capset in self.iter() { + if !first { + write!(f, ", ")?; + } + first = false; + + match capset { + Self::VIRGL => write!(f, "virgl"), + Self::VIRGL2 => write!(f, "virgl2"), + #[cfg(feature = "gfxstream")] + Self::GFXSTREAM_VULKAN => write!(f, "gfxstream-vulkan"), + #[cfg(feature = "gfxstream")] + Self::GFXSTREAM_GLES => write!(f, "gfxstream-gles"), + _ => panic!("Unknown capset {:#x}", self.bits()), + }?; + } + + Ok(()) + } +} + +impl GpuCapset { + /// Return the number of enabled capsets + pub const fn num_capsets(self) -> u32 { + self.bits().count_ones() + } +} + #[derive(Debug, Clone)] -/// This structure holds the internal configuration for the GPU backend, -/// derived from the command-line arguments provided through `GpuArgs`. +/// This structure holds the configuration for the GPU backend pub struct GpuConfig { - /// vhost-user Unix domain socket - socket_path: PathBuf, gpu_mode: GpuMode, + capset: GpuCapset, + flags: GpuFlags, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GpuFlags { + pub use_egl: bool, + pub use_glx: bool, + pub use_gles: bool, + pub use_surfaceless: bool, +} + +impl GpuFlags { + // `const` version of `default()` + pub const fn new_default() -> Self { + Self { + use_egl: true, + use_glx: false, + use_gles: true, + use_surfaceless: true, + } + } +} + +impl Default for GpuFlags { + fn default() -> Self { + Self::new_default() + } +} + +#[derive(Debug, ThisError)] +pub enum GpuConfigError { + #[error("The mode {0} does not support {1} capset")] + CapsetUnsuportedByMode(GpuMode, GpuCapset), + #[error("Requested gfxstream-gles capset, but gles is disabled")] + GlesRequiredByGfxstream, } 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, + pub const DEFAULT_VIRGLRENDER_CAPSET_MASK: GpuCapset = GpuCapset::ALL_VIRGLRENDERER_CAPSETS; + + #[cfg(feature = "gfxstream")] + pub const DEFAULT_GFXSTREAM_CAPSET_MASK: GpuCapset = GpuCapset::ALL_GFXSTREAM_CAPSETS; + + pub const fn get_default_capset_for_mode(gpu_mode: GpuMode) -> GpuCapset { + match gpu_mode { + GpuMode::VirglRenderer => Self::DEFAULT_VIRGLRENDER_CAPSET_MASK, + #[cfg(feature = "gfxstream")] + GpuMode::Gfxstream => Self::DEFAULT_GFXSTREAM_CAPSET_MASK, } } - /// 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 + fn validate_capset(gpu_mode: GpuMode, capset: GpuCapset) -> Result<(), GpuConfigError> { + let supported_capset_mask = match gpu_mode { + GpuMode::VirglRenderer => GpuCapset::ALL_VIRGLRENDERER_CAPSETS, + #[cfg(feature = "gfxstream")] + GpuMode::Gfxstream => GpuCapset::ALL_GFXSTREAM_CAPSETS, + }; + for capset in capset.iter() { + if !supported_capset_mask.contains(capset) { + return Err(GpuConfigError::CapsetUnsuportedByMode(gpu_mode, capset)); + } + } + + Ok(()) + } + + /// Create a new instance of the `GpuConfig` struct, containing the + /// parameters to be fed into the gpu-backend server. + pub fn new( + gpu_mode: GpuMode, + capset: Option, + flags: GpuFlags, + ) -> Result { + let capset = capset.unwrap_or_else(|| Self::get_default_capset_for_mode(gpu_mode)); + Self::validate_capset(gpu_mode, capset)?; + + #[cfg(feature = "gfxstream")] + if capset.contains(GpuCapset::GFXSTREAM_GLES) && !flags.use_gles { + return Err(GpuConfigError::GlesRequiredByGfxstream); + } + + Ok(Self { + gpu_mode, + capset, + flags, + }) } pub const fn gpu_mode(&self) -> GpuMode { self.gpu_mode } + + pub const fn capsets(&self) -> GpuCapset { + self.capset + } + + pub const fn flags(&self) -> &GpuFlags { + &self.flags + } } -pub fn start_backend(config: &GpuConfig) -> Result<()> { +#[derive(Debug, ThisError)] +pub enum StartError { + #[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), +} + +pub fn start_backend(socket_path: &Path, config: GpuConfig) -> Result<(), StartError> { info!("Starting backend"); - let socket = config.socket_path(); - let backend = VhostUserGpuBackend::new(config).map_err(Error::CouldNotCreateBackend)?; + let backend = VhostUserGpuBackend::new(config).map_err(StartError::CouldNotCreateBackend)?; let mut daemon = VhostUserDaemon::new( "vhost-device-gpu-backend".to_string(), backend.clone(), GuestMemoryAtomic::new(GuestMemoryMmap::new()), ) - .map_err(Error::CouldNotCreateDaemon)?; + .map_err(StartError::CouldNotCreateDaemon)?; backend.set_epoll_handler(&daemon.get_epoll_handlers()); - daemon.serve(socket).map_err(Error::ServeFailed)?; + daemon.serve(socket_path).map_err(StartError::ServeFailed)?; Ok(()) } @@ -121,26 +259,111 @@ pub fn start_backend(config: &GpuConfig) -> Result<()> { mod tests { use std::path::Path; + #[cfg(feature = "gfxstream")] use assert_matches::assert_matches; - use tempfile::tempdir; + use clap::ValueEnum; 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); + fn test_gpu_config_create_default_virglrenderer() { + let config = GpuConfig::new(GpuMode::VirglRenderer, None, GpuFlags::new_default()).unwrap(); + assert_eq!(config.gpu_mode(), GpuMode::VirglRenderer); + assert_eq!(config.capsets(), GpuConfig::DEFAULT_VIRGLRENDER_CAPSET_MASK); + } + + #[test] + #[cfg(feature = "gfxstream")] + fn test_gpu_config_create_default_gfxstream() { + let config = GpuConfig::new(GpuMode::Gfxstream, None, GpuFlags::default()).unwrap(); + assert_eq!(config.gpu_mode(), GpuMode::Gfxstream); + assert_eq!(config.capsets(), GpuConfig::DEFAULT_GFXSTREAM_CAPSET_MASK); + } + + #[cfg(feature = "gfxstream")] + fn assert_invalid_gpu_config(mode: GpuMode, capset: GpuCapset, expected_capset: GpuCapset) { + let result = GpuConfig::new(mode, Some(capset), GpuFlags::new_default()); + assert_matches!( + result, + Err(GpuConfigError::CapsetUnsuportedByMode( + requested_mode, + unsupported_capset + )) if unsupported_capset == expected_capset && requested_mode == mode + ); + } + + #[test] + fn test_gpu_config_valid_combination() { + let config = GpuConfig::new( + GpuMode::VirglRenderer, + Some(GpuCapset::VIRGL2), + GpuFlags::default(), + ) + .unwrap(); + assert_eq!(config.gpu_mode(), GpuMode::VirglRenderer); + } + + #[test] + #[cfg(feature = "gfxstream")] + fn test_gpu_config_invalid_combinations() { + assert_invalid_gpu_config( + GpuMode::VirglRenderer, + GpuCapset::VIRGL2 | GpuCapset::GFXSTREAM_VULKAN, + GpuCapset::GFXSTREAM_VULKAN, + ); + + assert_invalid_gpu_config( + GpuMode::Gfxstream, + GpuCapset::VIRGL2 | GpuCapset::GFXSTREAM_VULKAN, + GpuCapset::VIRGL2, + ); + } + + #[test] + #[cfg(feature = "gfxstream")] + fn test_gles_required_by_gfxstream() { + let capset = GpuCapset::GFXSTREAM_VULKAN | GpuCapset::GFXSTREAM_GLES; + let flags = GpuFlags { + use_gles: false, + ..GpuFlags::new_default() + }; + let result = GpuConfig::new(GpuMode::Gfxstream, Some(capset), flags); + assert_matches!(result, Err(GpuConfigError::GlesRequiredByGfxstream)); + } + + #[test] + fn test_default_num_capsets() { + assert_eq!(GpuConfig::DEFAULT_VIRGLRENDER_CAPSET_MASK.num_capsets(), 2); + #[cfg(feature = "gfxstream")] + assert_eq!(GpuConfig::DEFAULT_GFXSTREAM_CAPSET_MASK.num_capsets(), 2); + } + + #[test] + fn test_capset_display_multiple() { + let capset = GpuCapset::VIRGL | GpuCapset::VIRGL2; + let output = capset.to_string(); + assert_eq!(output, "virgl, virgl2") + } + + /// Check if display name of GpuMode is the same as the name in the CLI arg + #[test] + fn test_gpu_mode_display_eq_arg_name() { + for mode in GpuMode::value_variants() { + let mode_str = mode.to_string(); + let mode_from_str = GpuMode::from_str(&mode_str, false); + assert_eq!(*mode, mode_from_str.unwrap()); + } } #[test] fn test_fail_listener() { // This will fail the listeners and thread will panic. let socket_name = Path::new("/proc/-1/nonexistent"); - let config = GpuConfig::new(socket_name.into(), GpuMode::VirglRenderer); + let config = GpuConfig::new(GpuMode::VirglRenderer, None, GpuFlags::default()).unwrap(); - assert_matches!(start_backend(&config).unwrap_err(), Error::ServeFailed(_)); + assert_matches!( + start_backend(socket_name, config).unwrap_err(), + StartError::ServeFailed(_) + ); } } diff --git a/staging/vhost-device-gpu/src/main.rs b/staging/vhost-device-gpu/src/main.rs index 5d7677a..0d00b11 100644 --- a/staging/vhost-device-gpu/src/main.rs +++ b/staging/vhost-device-gpu/src/main.rs @@ -6,9 +6,45 @@ use std::{path::PathBuf, process::exit}; -use clap::Parser; +use clap::{ArgAction, Parser, ValueEnum}; use log::error; -use vhost_device_gpu::{start_backend, GpuConfig, GpuMode}; +use vhost_device_gpu::{start_backend, GpuCapset, GpuConfig, GpuConfigError, GpuFlags, GpuMode}; + +#[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)] +#[repr(u64)] +pub enum CapsetName { + /// [virglrenderer] OpenGL implementation, superseded by Virgl2 + Virgl = GpuCapset::VIRGL.bits(), + + /// [virglrenderer] OpenGL implementation + Virgl2 = GpuCapset::VIRGL2.bits(), + + /// [gfxstream] Vulkan implementation (partial support only){n} + /// NOTE: Can only be used for 2D display output for now, there is no + /// hardware acceleration yet + #[cfg(feature = "gfxstream")] + GfxstreamVulkan = GpuCapset::GFXSTREAM_VULKAN.bits(), + + /// [gfxstream] OpenGL ES implementation (partial support only){n} + /// NOTE: Can only be used for 2D display output for now, there is no + /// hardware acceleration yet + #[cfg(feature = "gfxstream")] + GfxstreamGles = GpuCapset::GFXSTREAM_GLES.bits(), +} + +impl From for GpuCapset { + fn from(capset_name: CapsetName) -> GpuCapset { + GpuCapset::from_bits(capset_name as u64) + .expect("Internal error: CapsetName enum is incorrectly defined") + } +} + +pub fn capset_names_into_capset(capset_names: impl IntoIterator) -> GpuCapset { + capset_names + .into_iter() + .map(CapsetName::into) + .fold(GpuCapset::empty(), GpuCapset::union) +} #[derive(Parser, Debug)] #[clap(author, version, about, long_about = None)] @@ -16,23 +52,87 @@ pub struct GpuArgs { /// vhost-user Unix domain socket. #[clap(short, long, value_name = "SOCKET")] pub socket_path: PathBuf, + + /// The mode specifies which backend implementation to use #[clap(short, long, value_enum)] pub gpu_mode: GpuMode, + + /// Comma separated list of enabled capsets + #[clap(short, long, value_delimiter = ',')] + pub capset: Option>, + + #[clap(flatten)] + pub flags: GpuFlagsArgs, } -impl From for GpuConfig { - fn from(args: GpuArgs) -> Self { - let socket_path = args.socket_path; - let gpu_mode: GpuMode = args.gpu_mode; +#[derive(Parser, Debug)] +#[allow(clippy::struct_excessive_bools)] +pub struct GpuFlagsArgs { + /// Enable backend to use EGL + #[clap( + long, + action = ArgAction::Set, + default_value_t = GpuFlags::new_default().use_egl + )] + pub use_egl: bool, - GpuConfig::new(socket_path, gpu_mode) + /// Enable backend to use GLX + #[clap( + long, + action = ArgAction::Set, + default_value_t = GpuFlags::new_default().use_glx + )] + pub use_glx: bool, + + /// Enable backend to use GLES + #[clap( + long, + action = ArgAction::Set, + default_value_t = GpuFlags::new_default().use_gles + )] + pub use_gles: bool, + + /// Enable surfaceless backend option + #[clap( + long, + action = ArgAction::Set, + default_value_t = GpuFlags::new_default().use_surfaceless + )] + pub use_surfaceless: bool, +} + +impl From for GpuFlags { + fn from(args: GpuFlagsArgs) -> Self { + GpuFlags { + use_egl: args.use_egl, + use_glx: args.use_glx, + use_gles: args.use_gles, + use_surfaceless: args.use_surfaceless, + } } } -fn main() { +pub fn config_from_args(args: GpuArgs) -> Result<(PathBuf, GpuConfig), GpuConfigError> { + let flags = GpuFlags::from(args.flags); + let capset = args.capset.map(capset_names_into_capset); + let config = GpuConfig::new(args.gpu_mode, capset, flags)?; + Ok((args.socket_path, config)) +} + +pub fn main() { env_logger::init(); - if let Err(e) = start_backend(&GpuConfig::from(GpuArgs::parse())) { + let args = GpuArgs::parse(); + + let (socket_path, config) = match config_from_args(args) { + Ok(config) => config, + Err(e) => { + error!("{e}"); + exit(1); + } + }; + + if let Err(e) = start_backend(&socket_path, config) { error!("{e}"); exit(1); } @@ -42,28 +142,65 @@ fn main() { mod tests { use std::path::Path; - use tempfile::tempdir; - use vhost_device_gpu::{GpuConfig, GpuMode}; + use clap::{Parser, ValueEnum}; + use vhost_device_gpu::{GpuCapset, GpuFlags, GpuMode}; use super::*; - impl GpuArgs { - pub(crate) fn from_args(path: &Path) -> Self { - Self { - socket_path: path.to_path_buf(), - gpu_mode: GpuMode::Gfxstream, - } + #[test] + fn test_capset_enum_in_sync_with_capset_bitset() { + // Convert each GpuCapset into CapsetName + for capset in GpuCapset::all().iter() { + let display_name = capset.to_string(); + let capset_name = CapsetName::from_str(&display_name, false).unwrap(); + let resulting_capset: GpuCapset = capset_name.into(); + assert_eq!(resulting_capset, capset); + } + + // Convert each CapsetName into GpuCapset + for capset_name in CapsetName::value_variants().iter().cloned() { + let resulting_capset: GpuCapset = capset_name.into(); // Would panic! if the definition is incorrect + assert_eq!(resulting_capset.bits(), capset_name as u64) } } #[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"); + fn test_default_cli_flags() { + // The default CLI flags should match GpuFlags::default() + let args: &[&str] = &[]; + let flag_args = GpuFlagsArgs::parse_from(args); + let flags: GpuFlags = flag_args.into(); + assert_eq!(flags, GpuFlags::default()); + } - let cmd_args = GpuArgs::from_args(socket_path.as_path()); - let config = GpuConfig::from(cmd_args); + #[test] + fn test_config_from_args() { + let expected_path = Path::new("/some/test/path"); + let args = GpuArgs { + socket_path: expected_path.into(), + gpu_mode: GpuMode::VirglRenderer, + capset: Some(vec![CapsetName::Virgl, CapsetName::Virgl2]), + flags: GpuFlagsArgs { + use_egl: false, + use_glx: true, + use_gles: false, + use_surfaceless: false, + }, + }; - assert_eq!(config.socket_path(), socket_path); + let (socket_path, config) = config_from_args(args).unwrap(); + + assert_eq!(socket_path, expected_path); + assert_eq!( + *config.flags(), + GpuFlags { + use_egl: false, + use_glx: true, + use_gles: false, + use_surfaceless: false, + } + ); + assert_eq!(config.gpu_mode(), GpuMode::VirglRenderer); + assert_eq!(config.capsets(), GpuCapset::VIRGL | GpuCapset::VIRGL2) } } diff --git a/staging/vhost-device-gpu/src/virtio_gpu.rs b/staging/vhost-device-gpu/src/virtio_gpu.rs index caff88d..ec15480 100644 --- a/staging/vhost-device-gpu/src/virtio_gpu.rs +++ b/staging/vhost-device-gpu/src/virtio_gpu.rs @@ -39,7 +39,7 @@ use crate::{ GpuResponsePlaneInfo, VirtioGpuResult, VIRTIO_GPU_FLAG_INFO_RING_IDX, VIRTIO_GPU_MAX_SCANOUTS, }, - GpuMode, + GpuConfig, GpuMode, }; fn sglist_to_rutabaga_iovecs( @@ -331,10 +331,6 @@ pub struct RutabagaVirtioGpu { const READ_RESOURCE_BYTES_PER_PIXEL: u32 = 4; impl RutabagaVirtioGpu { - // TODO: this depends on Rutabaga builder, so this will need to be handled at - // runtime eventually - pub const MAX_NUMBER_OF_CAPSETS: u32 = 3; - fn create_fence_handler( queue_ctl: VringRwLock, fence_state: Arc>, @@ -387,24 +383,27 @@ impl RutabagaVirtioGpu { }) } - fn configure_rutabaga_builder(gpu_mode: GpuMode) -> RutabagaBuilder { - let component = match gpu_mode { + fn configure_rutabaga_builder(gpu_config: &GpuConfig) -> RutabagaBuilder { + let component = match gpu_config.gpu_mode() { GpuMode::VirglRenderer => RutabagaComponentType::VirglRenderer, #[cfg(feature = "gfxstream")] GpuMode::Gfxstream => RutabagaComponentType::Gfxstream, }; - RutabagaBuilder::new(component, 0) - .set_use_egl(true) - .set_use_gles(true) - .set_use_glx(true) - .set_use_surfaceless(true) + + RutabagaBuilder::new(component, gpu_config.capsets().bits()) + .set_use_egl(gpu_config.flags().use_egl) + .set_use_glx(gpu_config.flags().use_glx) + .set_use_gles(gpu_config.flags().use_gles) + .set_use_surfaceless(gpu_config.flags().use_surfaceless) + // Since vhost-user-gpu is out-of-process this is the only type of blob resource that + // could work, so this is always enabled .set_use_external_blob(true) } - pub fn new(queue_ctl: &VringRwLock, gpu_mode: GpuMode, gpu_backend: GpuBackend) -> Self { + pub fn new(queue_ctl: &VringRwLock, gpu_config: &GpuConfig, gpu_backend: GpuBackend) -> Self { let fence_state = Arc::new(Mutex::new(FenceState::default())); let fence = Self::create_fence_handler(queue_ctl.clone(), fence_state.clone()); - let rutabaga = Self::configure_rutabaga_builder(gpu_mode) + let rutabaga = Self::configure_rutabaga_builder(gpu_config) .build(fence, None) .expect("Rutabaga initialization failed!"); @@ -907,7 +906,7 @@ mod tests { }; use super::*; - use crate::protocol::VIRTIO_GPU_FORMAT_R8G8B8A8_UNORM; + use crate::{protocol::VIRTIO_GPU_FORMAT_R8G8B8A8_UNORM, GpuCapset, GpuFlags}; const CREATE_RESOURCE_2D_720P: ResourceCreate3D = ResourceCreate3D { target: RUTABAGA_PIPE_TEXTURE_2D, @@ -941,7 +940,13 @@ mod tests { } fn new_gpu() -> RutabagaVirtioGpu { - let builder = RutabagaVirtioGpu::configure_rutabaga_builder(GpuMode::VirglRenderer); + let config = GpuConfig::new( + GpuMode::VirglRenderer, + Some(GpuCapset::VIRGL | GpuCapset::VIRGL2), + GpuFlags::default(), + ) + .unwrap(); + let builder = RutabagaVirtioGpu::configure_rutabaga_builder(&config); let rutabaga = builder.build(RutabagaHandler::new(|_| {}), None).unwrap(); RutabagaVirtioGpu { rutabaga,