diff --git a/vhost-device-gpu/README.md b/vhost-device-gpu/README.md index a049b30..72e925a 100644 --- a/vhost-device-gpu/README.md +++ b/vhost-device-gpu/README.md @@ -18,8 +18,8 @@ A virtio-gpu device using the vhost-user protocol. -g, --gpu-mode The mode specifies which backend implementation to use - - [possible values: virglrenderer, gfxstream] + + [possible values: virglrenderer, gfxstream, null] -c, --capset Comma separated list of enabled capsets @@ -63,8 +63,9 @@ A virtio-gpu device using the vhost-user protocol. Print version ``` -_NOTE_: Option `-g, --gpu-mode` can only accept the `gfxstream` value if the -crate has been built with the `backend-gfxstream` feature, which is the default. +_NOTE_: Option `-g, --gpu-mode` can only accept the `virglrenderer` or `gfxstream` +values if the crate has been built with the `backend-virgl` or `backend-gfxstream` +features respectively (both are enabled by default). The `null` mode is always available. ## Limitations @@ -87,7 +88,7 @@ Because blob resources are not yet supported, some capsets are limited: - gfxstream-vulkan and gfxstream-gles support are exposed, but can practically only be used for display output, there is no hardware acceleration yet. ## Features -This crate supports two GPU backends: gfxstream (default) and virglrenderer. +This crate supports three GPU backends: virglrenderer, gfxstream (both enabled by default), and null. The **virglrenderer** backend uses the [virglrenderer-rs](https://crates.io/crates/virglrenderer-rs) crate, which provides Rust bindings to the native virglrenderer library. It translates @@ -98,24 +99,36 @@ The **gfxstream** backend leverages the [rutabaga_gfx](https://crates.io/crates/ crate. With gfxstream rendering mode, GLES and Vulkan calls are forwarded to the host with minimal modification. +The **null** backend is a no-op implementation that accepts all GPU commands but performs +no actual rendering. This backend is primarily intended for testing and CI purposes. + Install the development packages for your distro, then build with: ```session $ cargo build ``` -gfxstream support is compiled by default, it can be disabled by not building with the `backend-gfxstream` feature flag, for example: +Both virglrenderer and gfxstream support are compiled by default. The null backend is +always available. To build with specific backends: ```session +# Build with only virglrenderer (and null) +$ cargo build --no-default-features --features backend-virgl + +# Build with only gfxstream (and null) +$ cargo build --no-default-features --features backend-gfxstream + +# Build with null backend only (for testing) $ cargo build --no-default-features ``` ## Examples -First start the daemon on the host machine using either of the 2 gpu modes: +First start the daemon on the host machine using one of the available gpu modes: 1) `virglrenderer` (if the crate has been compiled with the feature `backend-virgl`) 2) `gfxstream` (if the crate has been compiled with the feature `backend-gfxstream`) +3) `null` (always available, for testing) ```shell host# vhost-device-gpu --socket-path /tmp/gpu.socket --gpu-mode virglrenderer diff --git a/vhost-device-gpu/src/backend/mod.rs b/vhost-device-gpu/src/backend/mod.rs index c3fe127..1927fd1 100644 --- a/vhost-device-gpu/src/backend/mod.rs +++ b/vhost-device-gpu/src/backend/mod.rs @@ -2,8 +2,10 @@ // // SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause +#[cfg(any(feature = "backend-virgl", feature = "backend-gfxstream"))] mod common; #[cfg(feature = "backend-gfxstream")] pub mod gfxstream; +pub mod null; #[cfg(feature = "backend-virgl")] pub mod virgl; diff --git a/vhost-device-gpu/src/backend/null.rs b/vhost-device-gpu/src/backend/null.rs new file mode 100644 index 0000000..6be1b3e --- /dev/null +++ b/vhost-device-gpu/src/backend/null.rs @@ -0,0 +1,463 @@ +// Null backend +// Copyright 2025 Red Hat Inc +// +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause + +use log::trace; +use rutabaga_gfx::RutabagaFence; +use vhost::vhost_user::{ + gpu_message::{VhostUserGpuCursorPos, VhostUserGpuEdidRequest}, + GpuBackend, +}; +use vm_memory::{GuestAddress, GuestMemoryMmap, VolatileSlice}; +use vmm_sys_util::eventfd::EventFd; + +use crate::{ + gpu_types::{ResourceCreate3d, Transfer3DDesc, VirtioGpuRing}, + protocol::{virtio_gpu_rect, GpuResponse, VirtioGpuResult}, + renderer::Renderer, + GpuConfig, +}; + +pub struct NullAdapter { + _gpu_backend: GpuBackend, +} + +impl NullAdapter { + pub fn new( + _queue_ctl: &vhost_user_backend::VringRwLock, + _config: &GpuConfig, + gpu_backend: GpuBackend, + ) -> Self { + trace!("NullAdapter created"); + Self { + _gpu_backend: gpu_backend, + } + } +} + +impl Renderer for NullAdapter { + fn resource_create_3d( + &mut self, + _resource_id: u32, + _args: ResourceCreate3d, + ) -> VirtioGpuResult { + trace!("NullAdapter::resource_create_3d - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn unref_resource(&mut self, _resource_id: u32) -> VirtioGpuResult { + trace!("NullAdapter::unref_resource - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn transfer_write( + &mut self, + _ctx_id: u32, + _resource_id: u32, + _transfer: Transfer3DDesc, + ) -> VirtioGpuResult { + trace!("NullAdapter::transfer_write - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn transfer_write_2d( + &mut self, + _ctx_id: u32, + _resource_id: u32, + _transfer: Transfer3DDesc, + ) -> VirtioGpuResult { + trace!("NullAdapter::transfer_write_2d - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn transfer_read( + &mut self, + _ctx_id: u32, + _resource_id: u32, + _transfer: Transfer3DDesc, + _buf: Option, + ) -> VirtioGpuResult { + trace!("NullAdapter::transfer_read - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn attach_backing( + &mut self, + _resource_id: u32, + _mem: &GuestMemoryMmap, + _vecs: Vec<(GuestAddress, usize)>, + ) -> VirtioGpuResult { + trace!("NullAdapter::attach_backing - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn detach_backing(&mut self, _resource_id: u32) -> VirtioGpuResult { + trace!("NullAdapter::detach_backing - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn update_cursor( + &mut self, + _resource_id: u32, + _cursor_pos: VhostUserGpuCursorPos, + _hot_x: u32, + _hot_y: u32, + ) -> VirtioGpuResult { + trace!("NullAdapter::update_cursor - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn move_cursor( + &mut self, + _resource_id: u32, + _cursor: VhostUserGpuCursorPos, + ) -> VirtioGpuResult { + trace!("NullAdapter::move_cursor - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn resource_assign_uuid(&self, _resource_id: u32) -> VirtioGpuResult { + trace!("NullAdapter::resource_assign_uuid - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn get_capset_info(&self, _capset_index: u32) -> VirtioGpuResult { + trace!("NullAdapter::get_capset_info - no capsets"); + Ok(GpuResponse::OkNoData) + } + + fn get_capset(&self, _capset_id: u32, _capset_version: u32) -> VirtioGpuResult { + trace!("NullAdapter::get_capset - no capsets"); + Ok(GpuResponse::OkNoData) + } + + fn create_context( + &mut self, + _ctx_id: u32, + _context_init: u32, + _context_name: Option<&str>, + ) -> VirtioGpuResult { + trace!("NullAdapter::create_context - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn destroy_context(&mut self, _ctx_id: u32) -> VirtioGpuResult { + trace!("NullAdapter::destroy_context - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn context_attach_resource(&mut self, _ctx_id: u32, _resource_id: u32) -> VirtioGpuResult { + trace!("NullAdapter::context_attach_resource - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn context_detach_resource(&mut self, _ctx_id: u32, _resource_id: u32) -> VirtioGpuResult { + trace!("NullAdapter::context_detach_resource - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn submit_command( + &mut self, + _ctx_id: u32, + _commands: &mut [u8], + _fence_ids: &[u64], + ) -> VirtioGpuResult { + trace!("NullAdapter::submit_command - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn create_fence(&mut self, _rutabaga_fence: RutabagaFence) -> VirtioGpuResult { + trace!("NullAdapter::create_fence - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn process_fence( + &mut self, + _ring: VirtioGpuRing, + _fence_id: u64, + _desc_index: u16, + _len: u32, + ) -> bool { + trace!("NullAdapter::process_fence - no-op"); + true + } + + fn get_event_poll_fd(&self) -> Option { + trace!("NullAdapter::get_event_poll_fd - no-op"); + None + } + + fn event_poll(&self) { + trace!("NullAdapter::event_poll - no-op"); + } + + fn force_ctx_0(&self) { + trace!("NullAdapter::force_ctx_0 - no-op"); + } + + fn display_info(&self) -> VirtioGpuResult { + trace!("NullAdapter::display_info - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn get_edid(&self, _edid_req: VhostUserGpuEdidRequest) -> VirtioGpuResult { + trace!("NullAdapter::get_edid - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn set_scanout( + &mut self, + _scanout_id: u32, + _resource_id: u32, + _rect: virtio_gpu_rect, + ) -> VirtioGpuResult { + trace!("NullAdapter::set_scanout - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn flush_resource(&mut self, _resource_id: u32, _rect: virtio_gpu_rect) -> VirtioGpuResult { + trace!("NullAdapter::flush_resource - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn resource_create_blob( + &mut self, + _ctx_id: u32, + _resource_id: u32, + _blob_id: u64, + _size: u64, + _blob_mem: u32, + _blob_flags: u32, + ) -> VirtioGpuResult { + trace!("NullAdapter::resource_create_blob - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn resource_map_blob(&mut self, _resource_id: u32, _offset: u64) -> VirtioGpuResult { + trace!("NullAdapter::resource_map_blob - no-op"); + Ok(GpuResponse::OkNoData) + } + + fn resource_unmap_blob(&mut self, _resource_id: u32) -> VirtioGpuResult { + trace!("NullAdapter::resource_unmap_blob - no-op"); + Ok(GpuResponse::OkNoData) + } +} + +#[cfg(test)] +mod tests { + use std::os::unix::net::UnixStream; + + use vhost_user_backend::{VringRwLock, VringT}; + use vm_memory::{GuestAddress, GuestMemoryAtomic, GuestMemoryMmap}; + + use super::*; + use crate::{GpuFlags, GpuMode}; + + fn create_null_adapter() -> NullAdapter { + let (_, backend) = UnixStream::pair().unwrap(); + let gpu_backend = GpuBackend::from_stream(backend); + let mem = GuestMemoryAtomic::new( + GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(), + ); + let vring = VringRwLock::new(mem, 0x100).unwrap(); + let config = GpuConfig::new(GpuMode::Null, None, GpuFlags::default()).unwrap(); + + NullAdapter::new(&vring, &config, gpu_backend) + } + + #[test] + fn test_null_adapter_creation() { + // Verify that NullAdapter can be successfully created + let _adapter = create_null_adapter(); + } + + #[test] + fn test_null_adapter_resource_operations() { + let mut adapter = create_null_adapter(); + + // Verify resource creation returns success without doing anything + let resource_create = ResourceCreate3d { + target: 2, + format: 1, + bind: 1, + width: 640, + height: 480, + depth: 1, + array_size: 1, + last_level: 0, + nr_samples: 0, + flags: 0, + }; + let result = adapter.resource_create_3d(1, resource_create); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify unreferencing a resource succeeds + let result = adapter.unref_resource(1); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify attaching and detaching backing memory succeeds + let mem = GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), 0x1000)]).unwrap(); + let result = adapter.attach_backing(1, &mem, vec![]); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + let result = adapter.detach_backing(1); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + } + + #[test] + fn test_null_adapter_transfer_operations() { + let mut adapter = create_null_adapter(); + let transfer = Transfer3DDesc::new_2d(0, 0, 640, 480, 0); + + // Verify 3D transfer write succeeds + let result = adapter.transfer_write(0, 1, transfer); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify 2D transfer write succeeds + let result = adapter.transfer_write_2d(0, 1, transfer); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify transfer read succeeds + let result = adapter.transfer_read(0, 1, transfer, None); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + } + + #[test] + fn test_null_adapter_context_operations() { + let mut adapter = create_null_adapter(); + + // Verify context creation succeeds + let result = adapter.create_context(1, 0, Some("test")); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify attaching a resource to a context succeeds + let result = adapter.context_attach_resource(1, 1); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify detaching a resource from a context succeeds + let result = adapter.context_detach_resource(1, 1); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify context destruction succeeds + let result = adapter.destroy_context(1); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + } + + #[test] + fn test_null_adapter_display_operations() { + let mut adapter = create_null_adapter(); + + // Verify getting display info succeeds + let result = adapter.display_info(); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify getting EDID info succeeds + let result = adapter.get_edid(VhostUserGpuEdidRequest { scanout_id: 0 }); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify setting scanout succeeds + let result = adapter.set_scanout(0, 1, virtio_gpu_rect::default()); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify flushing a resource succeeds + let result = adapter.flush_resource(1, virtio_gpu_rect::default()); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + } + + #[test] + fn test_null_adapter_cursor_operations() { + let mut adapter = create_null_adapter(); + let cursor_pos = VhostUserGpuCursorPos { + scanout_id: 0, + x: 0, + y: 0, + }; + + // Verify updating cursor succeeds + let result = adapter.update_cursor(1, cursor_pos, 0, 0); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify moving cursor succeeds + let result = adapter.move_cursor(1, cursor_pos); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + } + + #[test] + fn test_null_adapter_capset_operations() { + let adapter = create_null_adapter(); + + // Verify getting capset info returns success (null backend has no capsets) + let result = adapter.get_capset_info(0); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify getting capset returns success (null backend has no capsets) + let result = adapter.get_capset(0, 0); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + } + + #[test] + fn test_null_adapter_command_operations() { + let mut adapter = create_null_adapter(); + let mut commands = vec![0u8; 64]; + + // Verify submitting commands succeeds + let result = adapter.submit_command(1, &mut commands, &[]); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + } + + #[test] + fn test_null_adapter_fence_operations() { + let mut adapter = create_null_adapter(); + let fence = RutabagaFence { + flags: 0, + fence_id: 1, + ctx_id: 0, + ring_idx: 0, + }; + + // Verify creating a fence succeeds + let result = adapter.create_fence(fence); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify processing fence returns true (fence is immediately ready in null + // backend) + let ready = adapter.process_fence(VirtioGpuRing::Global, 1, 0, 0); + assert!(ready); + } + + #[test] + fn test_null_adapter_blob_operations() { + let mut adapter = create_null_adapter(); + + // Verify blob resource creation succeeds + let result = adapter.resource_create_blob(0, 1, 1, 4096, 0, 0); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify mapping blob resource succeeds + let result = adapter.resource_map_blob(1, 0); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify unmapping blob resource succeeds + let result = adapter.resource_unmap_blob(1); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + } + + #[test] + fn test_null_adapter_misc_operations() { + let adapter = create_null_adapter(); + + // Verify assigning UUID to resource succeeds + let result = adapter.resource_assign_uuid(1); + assert!(matches!(result, Ok(GpuResponse::OkNoData))); + + // Verify no event poll fd is provided (null backend has no events) + let event_fd = adapter.get_event_poll_fd(); + assert!(event_fd.is_none()); + + // Verify event polling and force_ctx_0 don't panic (they're no-ops) + adapter.event_poll(); + adapter.force_ctx_0(); + } +} diff --git a/vhost-device-gpu/src/device.rs b/vhost-device-gpu/src/device.rs index 2064aac..1847310 100644 --- a/vhost-device-gpu/src/device.rs +++ b/vhost-device-gpu/src/device.rs @@ -77,6 +77,7 @@ use crate::backend::gfxstream::GfxstreamAdapter; #[cfg(feature = "backend-virgl")] use crate::backend::virgl::VirglRendererAdapter; use crate::{ + backend::null::NullAdapter, gpu_types::{ResourceCreate3d, Transfer3DDesc, VirtioGpuRing}, protocol::{ virtio_gpu_ctrl_hdr, virtio_gpu_ctx_create, virtio_gpu_get_edid, @@ -630,6 +631,17 @@ impl VhostUserGpuBackendInner { device_event, vrings ), + + GpuMode::Null => handle_adapter!( + NullAdapter, + TLS_NULL, + |control_vring, gpu_backend| { + NullAdapter::new(control_vring, &self.gpu_config, gpu_backend) + }, + self, + device_event, + vrings + ), } } diff --git a/vhost-device-gpu/src/gpu_types.rs b/vhost-device-gpu/src/gpu_types.rs index 0caf067..6c9df9e 100644 --- a/vhost-device-gpu/src/gpu_types.rs +++ b/vhost-device-gpu/src/gpu_types.rs @@ -4,6 +4,7 @@ /// Generates an implementation of `From` for any compatible /// target struct. +#[cfg(any(feature = "backend-virgl", feature = "backend-gfxstream"))] macro_rules! impl_transfer3d_from_desc { ($target:path) => { impl From for $target { @@ -25,6 +26,7 @@ macro_rules! impl_transfer3d_from_desc { }; } +#[cfg(any(feature = "backend-virgl", feature = "backend-gfxstream"))] macro_rules! impl_from_resource_create3d { ($target:ty) => { impl From for $target { @@ -48,7 +50,9 @@ macro_rules! impl_from_resource_create3d { use std::{collections::BTreeMap, os::raw::c_void}; +#[cfg(feature = "backend-gfxstream")] use rutabaga_gfx::Transfer3D; +#[cfg(feature = "backend-virgl")] use virglrenderer::Transfer3D as VirglTransfer3D; use crate::protocol::virtio_gpu_rect; @@ -87,8 +91,10 @@ impl Transfer3DDesc { } // Invoke the macro for both targets // rutabaga_gfx::Transfer3D +#[cfg(feature = "backend-gfxstream")] impl_transfer3d_from_desc!(Transfer3D); // virglrenderer::Transfer3D +#[cfg(feature = "backend-virgl")] impl_transfer3d_from_desc!(VirglTransfer3D); // These are neutral types that can be used by all backends @@ -136,7 +142,9 @@ pub struct ResourceCreate3d { } // Invoke the macro for both targets +#[cfg(feature = "backend-gfxstream")] impl_from_resource_create3d!(rutabaga_gfx::ResourceCreate3D); +#[cfg(feature = "backend-virgl")] impl_from_resource_create3d!(virglrenderer::ResourceCreate3D); #[derive(Debug, Clone, Copy)] diff --git a/vhost-device-gpu/src/lib.rs b/vhost-device-gpu/src/lib.rs index b710f68..a50ca42 100644 --- a/vhost-device-gpu/src/lib.rs +++ b/vhost-device-gpu/src/lib.rs @@ -44,6 +44,7 @@ pub enum GpuMode { VirglRenderer, #[cfg(feature = "backend-gfxstream")] Gfxstream, + Null, } impl Display for GpuMode { @@ -53,6 +54,7 @@ impl Display for GpuMode { Self::VirglRenderer => write!(f, "virglrenderer"), #[cfg(feature = "backend-gfxstream")] Self::Gfxstream => write!(f, "gfxstream"), + Self::Null => write!(f, "null"), } } } @@ -79,7 +81,12 @@ bitflags! { impl Display for GpuCapset { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.is_empty() { + return write!(f, "none"); + } + let mut first = true; + #[allow(unused_assignments)] for capset in self.iter() { if !first { write!(f, ", ")?; @@ -88,15 +95,15 @@ impl Display for GpuCapset { match capset { #[cfg(feature = "backend-virgl")] - Self::VIRGL => write!(f, "virgl"), + Self::VIRGL => write!(f, "virgl")?, #[cfg(feature = "backend-virgl")] - Self::VIRGL2 => write!(f, "virgl2"), + Self::VIRGL2 => write!(f, "virgl2")?, #[cfg(feature = "backend-gfxstream")] - Self::GFXSTREAM_VULKAN => write!(f, "gfxstream-vulkan"), + Self::GFXSTREAM_VULKAN => write!(f, "gfxstream-vulkan")?, #[cfg(feature = "backend-gfxstream")] - Self::GFXSTREAM_GLES => write!(f, "gfxstream-gles"), + Self::GFXSTREAM_GLES => write!(f, "gfxstream-gles")?, _ => panic!("Unknown capset {:#x}", self.bits()), - }?; + } } Ok(()) @@ -165,6 +172,7 @@ impl GpuConfig { GpuMode::VirglRenderer => Self::DEFAULT_VIRGLRENDER_CAPSET_MASK, #[cfg(feature = "backend-gfxstream")] GpuMode::Gfxstream => Self::DEFAULT_GFXSTREAM_CAPSET_MASK, + GpuMode::Null => GpuCapset::empty(), } } @@ -174,6 +182,7 @@ impl GpuConfig { GpuMode::VirglRenderer => GpuCapset::ALL_VIRGLRENDERER_CAPSETS, #[cfg(feature = "backend-gfxstream")] GpuMode::Gfxstream => GpuCapset::ALL_GFXSTREAM_CAPSETS, + GpuMode::Null => GpuCapset::empty(), }; for capset in capset.iter() { if !supported_capset_mask.contains(capset) { diff --git a/vhost-device-gpu/src/main.rs b/vhost-device-gpu/src/main.rs index 55ef8dd..90019c0 100644 --- a/vhost-device-gpu/src/main.rs +++ b/vhost-device-gpu/src/main.rs @@ -12,6 +12,9 @@ use vhost_device_gpu::{start_backend, GpuCapset, GpuConfig, GpuConfigError, GpuF #[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)] #[repr(u64)] +// __Null is a placeholder to prevent a zero-variant enum when building with +// --no-default-features, not an implementation of the non-exhaustive pattern +#[allow(clippy::manual_non_exhaustive)] pub enum CapsetName { /// [virglrenderer] OpenGL implementation, superseded by Virgl2 #[cfg(feature = "backend-virgl")] @@ -32,10 +35,20 @@ pub enum CapsetName { /// hardware acceleration yet #[cfg(feature = "backend-gfxstream")] GfxstreamGles = GpuCapset::GFXSTREAM_GLES.bits(), + + /// Placeholder variant to prevent zero-variant enum when no backend + /// features are enabled. The null backend doesn't use capsets, so this + /// maps to GpuCapset::empty(). + #[doc(hidden)] + __Null = 0, } impl From for GpuCapset { fn from(capset_name: CapsetName) -> GpuCapset { + if matches!(capset_name, CapsetName::__Null) { + return GpuCapset::empty(); + } + GpuCapset::from_bits(capset_name as u64) .expect("Internal error: CapsetName enum is incorrectly defined") }