From 1460f1be1b80dac79856e5403b8f36e6a9cbeca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dav=C3=AD=C3=B0=20Steinn=20Geirsson?= Date: Sun, 22 Mar 2026 22:27:42 +0000 Subject: [PATCH] gpu: add Wayland cross-domain support via unified rutabaga backend Replace the separate virglrenderer and gfxstream backends with a single rutabaga-based backend that supports cross-domain, virgl, and venus capsets simultaneously. This enables guest Wayland applications to composite on the host Wayland compositor via the virtio-gpu cross-domain protocol. Key changes: - New backend/rutabaga.rs implementing the Renderer trait via rutabaga_gfx - New --gpu-mode rutabaga (replaces virglrenderer/gfxstream modes) - New --capset cross-domain and --wayland-socket CLI args - Ungated rutabaga type conversions and capset constants - Implement get_shmem_config() for blob resource mapping (8 GiB region) - Old virgl.rs and gfxstream.rs kept as dead code for upstream merge compatibility Co-Authored-By: Claude Opus 4.6 (1M context) --- vhost-device-gpu/Cargo.toml | 4 +- vhost-device-gpu/src/backend/mod.rs | 2 +- vhost-device-gpu/src/backend/rutabaga.rs | 845 +++++++++++++++++++++++ vhost-device-gpu/src/backend/virgl.rs | 1 - vhost-device-gpu/src/device.rs | 58 +- vhost-device-gpu/src/gpu_types.rs | 11 +- vhost-device-gpu/src/lib.rs | 160 ++++- vhost-device-gpu/src/main.rs | 47 +- 8 files changed, 1047 insertions(+), 81 deletions(-) create mode 100644 vhost-device-gpu/src/backend/rutabaga.rs diff --git a/vhost-device-gpu/Cargo.toml b/vhost-device-gpu/Cargo.toml index bf8c94d..2e8a8a9 100644 --- a/vhost-device-gpu/Cargo.toml +++ b/vhost-device-gpu/Cargo.toml @@ -14,7 +14,7 @@ edition = "2021" resolver = "2" [features] -default = ["backend-virgl", "backend-gfxstream"] +default = [] xen = ["vm-memory/xen", "vhost/xen", "vhost-user-backend/xen"] backend-gfxstream = ["rutabaga_gfx/gfxstream"] backend-virgl = ["dep:virglrenderer"] @@ -26,7 +26,7 @@ libc = "0.2" log = "0.4" [target.'cfg(not(target_env = "musl"))'.dependencies] -rutabaga_gfx = "0.1.75" +rutabaga_gfx = { version = "0.1.75", features = ["virgl_renderer"] } thiserror = "2.0.18" virglrenderer = { git = "https://gitlab.freedesktop.org/mtjhrc/virglrenderer-rs.git", branch = "map_info", optional = true } vhost = { git = "https://git.dsg.is/dsg/vhost.git", features = ["vhost-user-backend"] } diff --git a/vhost-device-gpu/src/backend/mod.rs b/vhost-device-gpu/src/backend/mod.rs index 1927fd1..cba881a 100644 --- a/vhost-device-gpu/src/backend/mod.rs +++ b/vhost-device-gpu/src/backend/mod.rs @@ -2,10 +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; +pub mod rutabaga; #[cfg(feature = "backend-virgl")] pub mod virgl; diff --git a/vhost-device-gpu/src/backend/rutabaga.rs b/vhost-device-gpu/src/backend/rutabaga.rs new file mode 100644 index 0000000..1e629f6 --- /dev/null +++ b/vhost-device-gpu/src/backend/rutabaga.rs @@ -0,0 +1,845 @@ +// Unified rutabaga backend device +// Copyright 2019 The ChromiumOS Authors +// Copyright 2026 Red Hat Inc +// +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause + +use std::{ + cell::RefCell, + collections::BTreeMap, + io::IoSliceMut, + os::{ + fd::{AsFd, FromRawFd}, + raw::c_void, + }, + sync::{Arc, Mutex}, +}; + +use log::{debug, error, warn}; +use rutabaga_gfx::{ + ResourceCreate3D, Rutabaga, RutabagaBuilder, RutabagaFence, RutabagaFenceHandler, + RutabagaHandle, RutabagaIntoRawDescriptor, RutabagaIovec, RutabagaPath, Transfer3D, + RUTABAGA_HANDLE_TYPE_MEM_OPAQUE_FD, RUTABAGA_MAP_ACCESS_MASK, RUTABAGA_MAP_ACCESS_READ, + RUTABAGA_MAP_ACCESS_RW, RUTABAGA_MAP_ACCESS_WRITE, RUTABAGA_MAP_CACHE_MASK, + RUTABAGA_PATH_TYPE_WAYLAND, +}; +use vhost::vhost_user::{ + gpu_message::{ + VhostUserGpuCursorPos, VhostUserGpuEdidRequest, VhostUserGpuScanout, VhostUserGpuUpdate, + }, + message::VhostUserMMapFlags, + Backend, GpuBackend, +}; +use vhost_user_backend::{VringRwLock, VringT}; +use virtio_bindings::virtio_gpu::VIRTIO_GPU_BLOB_MEM_HOST3D; +use vm_memory::{GuestAddress, GuestMemory, GuestMemoryMmap, VolatileSlice}; +use vmm_sys_util::eventfd::EventFd; + +use crate::{ + backend::{ + common, + common::{ + common_map_blob, common_set_scanout_disable, common_unmap_blob, AssociatedScanouts, + CursorConfig, VirtioGpuScanout, + }, + }, + device::Error, + gpu_types::{FenceState, ResourceCreate3d, ResourceCreateBlob, Transfer3DDesc, VirtioGpuRing}, + protocol::{ + virtio_gpu_rect, GpuResponse, + GpuResponse::{ + ErrInvalidParameter, ErrInvalidResourceId, ErrUnspec, OkCapset, OkCapsetInfo, + OkMapInfo, OkNoData, OkResourcePlaneInfo, + }, + GpuResponsePlaneInfo, VirtioGpuResult, VIRTIO_GPU_BLOB_FLAG_CREATE_GUEST_HANDLE, + VIRTIO_GPU_FLAG_INFO_RING_IDX, VIRTIO_GPU_MAX_SCANOUTS, + }, + renderer::Renderer, + GpuConfig, +}; + +// Number of bytes per pixel for reading 2D resources (assuming RGBA8 format) +const READ_RESOURCE_BYTES_PER_PIXEL: u32 = 4; + +// A local resource struct for the Rutabaga backend +#[derive(Default, Clone)] +pub struct RutabagaBackendResource { + pub id: u32, + pub width: u32, + pub height: u32, + scanouts: common::AssociatedScanouts, + pub info_3d: Option, + pub handle: Option>, + pub blob_size: u64, + pub blob_shmem_offset: Option, +} + +impl RutabagaBackendResource { + fn calculate_size(&self) -> Result { + let width = self.width as usize; + let height = self.height as usize; + let size = width + .checked_mul(height) + .ok_or("Multiplication of width and height overflowed")? + .checked_mul(READ_RESOURCE_BYTES_PER_PIXEL as usize) + .ok_or("Multiplication of result and bytes_per_pixel overflowed")?; + + Ok(size) + } +} + +impl RutabagaBackendResource { + /// Creates a new `RutabagaBackendResource` with 2D/3D metadata + pub fn new(resource_id: u32, width: u32, height: u32) -> Self { + Self { + id: resource_id, + width, + height, + scanouts: AssociatedScanouts::default(), + info_3d: None, + handle: None, + blob_size: 0, + blob_shmem_offset: None, + } + } +} + +// Thread-local storage for the Rutabaga instance. +// This allows each worker thread to have its own, non-shared instance. +thread_local! { + static TLS_RUTABAGA: RefCell> = const { RefCell::new(None) }; +} + +pub struct RutabagaAdapter { + #[allow(dead_code)] + backend: Backend, + gpu_backend: Option, + resources: BTreeMap, + fence_state: Arc>, + scanouts: [Option; VIRTIO_GPU_MAX_SCANOUTS as usize], +} + +impl RutabagaAdapter { + pub fn new( + queue_ctl: &VringRwLock, + backend: Backend, + gpu_config: &GpuConfig, + gpu_backend: Option, + ) -> Self { + debug!("RutabagaAdapter::new() starting"); + let fence_state = Arc::new(Mutex::new(FenceState::default())); + let fence = Self::create_fence_handler(queue_ctl.clone(), fence_state.clone()); + + // Lazily initialize Rutabaga for the thread + debug!("Initializing Rutabaga TLS"); + TLS_RUTABAGA.with(|slot| { + if slot.borrow().is_none() { + debug!("Building Rutabaga (capsets: {}, wayland_socket: {:?})", + gpu_config.capsets(), gpu_config.wayland_socket()); + let builder = Self::configure_rutabaga_builder(gpu_config, fence); + debug!("RutabagaBuilder configured, calling build()..."); + let rb = match builder.build() { + Ok(rb) => { + debug!("Rutabaga built successfully"); + rb + } + Err(e) => { + error!("Failed to build Rutabaga: {e:?}"); + panic!("Failed to build Rutabaga: {e:?}"); + } + }; + *slot.borrow_mut() = Some(rb); + debug!("Rutabaga stored in TLS"); + } + }); + + debug!("RutabagaAdapter::new() complete"); + Self { + backend, + gpu_backend, + fence_state, + resources: BTreeMap::new(), + scanouts: Default::default(), + } + } + + fn create_fence_handler( + queue_ctl: VringRwLock, + fence_state: Arc>, + ) -> RutabagaFenceHandler { + RutabagaFenceHandler::new(move |completed_fence: RutabagaFence| { + debug!( + "XXX - fence called: id={}, ring_idx={}", + completed_fence.fence_id, completed_fence.ring_idx + ); + + let mut fence_state = fence_state.lock().unwrap(); + let mut i = 0; + + let ring = match completed_fence.flags & VIRTIO_GPU_FLAG_INFO_RING_IDX { + 0 => VirtioGpuRing::Global, + _ => VirtioGpuRing::ContextSpecific { + ctx_id: completed_fence.ctx_id, + ring_idx: completed_fence.ring_idx, + }, + }; + + while i < fence_state.descs.len() { + debug!("XXX - fence_id: {}", fence_state.descs[i].fence_id); + if fence_state.descs[i].ring == ring + && fence_state.descs[i].fence_id <= completed_fence.fence_id + { + let completed_desc = fence_state.descs.remove(i); + debug!( + "XXX - found fence: desc_index={}", + completed_desc.desc_index + ); + + queue_ctl + .add_used(completed_desc.desc_index, completed_desc.len) + .unwrap(); + + queue_ctl + .signal_used_queue() + .map_err(Error::NotificationFailed) + .unwrap(); + debug!("Notification sent"); + } else { + i += 1; + } + } + + // Update the last completed fence for this context + fence_state + .completed_fences + .insert(ring, completed_fence.fence_id); + }) + } + + fn configure_rutabaga_builder( + gpu_config: &GpuConfig, + fence: RutabagaFenceHandler, + ) -> RutabagaBuilder { + let mut builder = RutabagaBuilder::new(gpu_config.capsets().bits(), fence) + .set_use_egl(gpu_config.flags().use_egl) + .set_use_gles(gpu_config.flags().use_gles) + .set_use_surfaceless(gpu_config.flags().use_surfaceless) + .set_use_external_blob(true); + + if let Some(wayland_socket) = gpu_config.wayland_socket() { + let paths = vec![RutabagaPath { + path: wayland_socket.to_path_buf(), + path_type: RUTABAGA_PATH_TYPE_WAYLAND, + }]; + builder = builder.set_rutabaga_paths(Some(paths)); + } + + builder + } + + fn sglist_to_rutabaga_iovecs( + vecs: &[(GuestAddress, usize)], + mem: &GuestMemoryMmap, + ) -> std::result::Result, ()> { + if vecs + .iter() + .any(|&(addr, len)| mem.get_slice(addr, len).is_err()) + { + return Err(()); + } + + let mut rutabaga_iovecs: Vec = Vec::new(); + for &(addr, len) in vecs { + let slice = mem.get_slice(addr, len).unwrap(); + let iov = RutabagaIovec { + base: slice.ptr_guard_mut().as_ptr().cast::(), + len, + }; + rutabaga_iovecs.push(iov); + } + Ok(rutabaga_iovecs) + } + + fn with_rutabaga T>(f: F) -> T { + TLS_RUTABAGA.with(|slot| { + let mut opt = slot.borrow_mut(); + let rb = opt.as_mut().expect("Rutabaga not initialized"); + f(rb) + }) + } + + fn read_2d_resource( + resource: &RutabagaBackendResource, + output: &mut [u8], + ) -> Result<(), String> { + let minimal_buffer_size = resource.calculate_size()?; + assert!(output.len() >= minimal_buffer_size); + + let transfer = Transfer3D { + x: 0, + y: 0, + z: 0, + w: resource.width, + h: resource.height, + d: 1, + level: 0, + stride: resource.width * READ_RESOURCE_BYTES_PER_PIXEL, + layer_stride: 0, + offset: 0, + }; + Self::with_rutabaga(|rutabaga| { + rutabaga.transfer_read(0, resource.id, transfer, Some(IoSliceMut::new(output))) + }) + .map_err(|e| format!("{e}"))?; + + Ok(()) + } + + fn result_from_query(resource_id: u32) -> GpuResponse { + let Ok(query) = Self::with_rutabaga(|rutabaga| rutabaga.resource3d_info(resource_id)) + else { + return OkNoData; + }; + let mut plane_info = Vec::with_capacity(4); + for plane_index in 0..4 { + plane_info.push(GpuResponsePlaneInfo { + stride: query.strides[plane_index], + offset: query.offsets[plane_index], + }); + } + let format_modifier = query.modifier; + OkResourcePlaneInfo { + format_modifier, + plane_info, + } + } +} + +impl Renderer for RutabagaAdapter { + fn resource_create_3d(&mut self, resource_id: u32, args: ResourceCreate3d) -> VirtioGpuResult { + let rutabaga_args: ResourceCreate3D = args.into(); + Self::with_rutabaga(|rutabaga| rutabaga.resource_create_3d(resource_id, rutabaga_args))?; + + let resource = RutabagaBackendResource { + id: resource_id, + width: rutabaga_args.width, + height: rutabaga_args.height, + scanouts: AssociatedScanouts::default(), + info_3d: None, + handle: None, + blob_size: 0, + blob_shmem_offset: None, + }; + debug_assert!( + !self.resources.contains_key(&resource_id), + "Resource ID {resource_id} already exists in the resources map." + ); + + // Rely on rutabaga to check for duplicate resource ids. + self.resources.insert(resource_id, resource); + Ok(Self::result_from_query(resource_id)) + } + + fn unref_resource(&mut self, resource_id: u32) -> VirtioGpuResult { + let resource = self.resources.remove(&resource_id); + match resource { + None => return Err(ErrInvalidResourceId), + // The spec doesn't say anything about this situation and this doesn't actually seem + // to happen in practise but let's be careful and refuse to disable the resource. + // This keeps the internal state of the gpu device and the fronted consistent. + Some(resource) if resource.scanouts.has_any_enabled() => { + warn!( + "The driver requested unref_resource, but resource {resource_id} has \ + associated scanouts, refusing to delete the resource." + ); + return Err(ErrUnspec); + } + _ => (), + } + Self::with_rutabaga(|rutabaga| rutabaga.unref_resource(resource_id))?; + + Ok(OkNoData) + } + + fn transfer_write( + &mut self, + ctx_id: u32, + resource_id: u32, + transfer: Transfer3DDesc, + ) -> VirtioGpuResult { + Self::with_rutabaga(|rutabaga| { + rutabaga.transfer_write(ctx_id, resource_id, transfer.into(), None) + })?; + Ok(OkNoData) + } + + fn transfer_write_2d( + &mut self, + ctx_id: u32, + resource_id: u32, + transfer: Transfer3DDesc, + ) -> VirtioGpuResult { + Self::with_rutabaga(|rutabaga| { + rutabaga.transfer_write(ctx_id, resource_id, transfer.into(), None) + })?; + Ok(OkNoData) + } + + fn transfer_read( + &mut self, + ctx_id: u32, + resource_id: u32, + transfer: Transfer3DDesc, + buf: Option, + ) -> VirtioGpuResult { + let buf = buf.map(|vs| { + IoSliceMut::new( + // SAFETY: trivially safe + unsafe { std::slice::from_raw_parts_mut(vs.ptr_guard_mut().as_ptr(), vs.len()) }, + ) + }); + + Self::with_rutabaga(|rutabaga| { + rutabaga.transfer_read(ctx_id, resource_id, transfer.into(), buf) + })?; + Ok(OkNoData) + } + + fn attach_backing( + &mut self, + resource_id: u32, + mem: &GuestMemoryMmap, + vecs: Vec<(GuestAddress, usize)>, + ) -> VirtioGpuResult { + let rutabaga_iovecs = + Self::sglist_to_rutabaga_iovecs(&vecs[..], mem).map_err(|()| GpuResponse::ErrUnspec)?; + Self::with_rutabaga(|rutabaga| rutabaga.attach_backing(resource_id, rutabaga_iovecs))?; + Ok(OkNoData) + } + + fn detach_backing(&mut self, resource_id: u32) -> VirtioGpuResult { + Self::with_rutabaga(|rutabaga| rutabaga.detach_backing(resource_id))?; + Ok(OkNoData) + } + + fn update_cursor( + &mut self, + resource_id: u32, + cursor_pos: VhostUserGpuCursorPos, + hot_x: u32, + hot_y: u32, + ) -> VirtioGpuResult { + if self.gpu_backend.is_none() { + return Ok(OkNoData); + } + + let config = CursorConfig { + width: 64, + height: 64, + }; + + let cursor_resource = self + .resources + .get(&resource_id) + .ok_or(ErrInvalidResourceId)?; + + if cursor_resource.width != config.width || cursor_resource.height != config.height { + error!("Cursor resource has invalid dimensions"); + return Err(ErrInvalidParameter); + } + + let data = common::common_read_cursor_resource(self, resource_id, config)?; + + common::common_update_cursor( + // The existence of gpu_backend has been already checked above. + self.gpu_backend.as_ref().unwrap(), + cursor_pos, + hot_x, + hot_y, + &data, + config, + ) + } + + fn move_cursor(&mut self, resource_id: u32, cursor: VhostUserGpuCursorPos) -> VirtioGpuResult { + self.gpu_backend + .as_ref() + .map_or(Ok(OkNoData), |gpu_backend| { + common::common_move_cursor(gpu_backend, resource_id, cursor) + }) + } + + fn resource_assign_uuid(&self, _resource_id: u32) -> VirtioGpuResult { + error!("Not implemented: resource_assign_uuid"); + Err(ErrUnspec) + } + + fn get_capset_info(&self, index: u32) -> VirtioGpuResult { + debug!("get_capset_info index={index}"); + let (capset_id, version, size) = + Self::with_rutabaga(|rutabaga| rutabaga.get_capset_info(index))?; + Ok(OkCapsetInfo { + capset_id, + version, + size, + }) + } + + fn get_capset(&self, capset_id: u32, version: u32) -> VirtioGpuResult { + let capset = Self::with_rutabaga(|rutabaga| rutabaga.get_capset(capset_id, version))?; + Ok(OkCapset(capset)) + } + + fn create_context( + &mut self, + ctx_id: u32, + context_init: u32, + context_name: Option<&str>, + ) -> VirtioGpuResult { + Self::with_rutabaga(|rutabaga| { + rutabaga.create_context(ctx_id, context_init, context_name) + })?; + Ok(OkNoData) + } + + fn destroy_context(&mut self, ctx_id: u32) -> VirtioGpuResult { + Self::with_rutabaga(|rutabaga| rutabaga.destroy_context(ctx_id))?; + Ok(OkNoData) + } + + fn context_attach_resource(&mut self, ctx_id: u32, resource_id: u32) -> VirtioGpuResult { + Self::with_rutabaga(|rutabaga| rutabaga.context_attach_resource(ctx_id, resource_id))?; + Ok(OkNoData) + } + + fn context_detach_resource(&mut self, ctx_id: u32, resource_id: u32) -> VirtioGpuResult { + Self::with_rutabaga(|rutabaga| rutabaga.context_detach_resource(ctx_id, resource_id))?; + Ok(OkNoData) + } + + fn submit_command( + &mut self, + ctx_id: u32, + commands: &mut [u8], + fence_ids: &[u64], + ) -> VirtioGpuResult { + Self::with_rutabaga(|rutabaga| rutabaga.submit_command(ctx_id, commands, fence_ids))?; + Ok(OkNoData) + } + + fn create_fence(&mut self, rutabaga_fence: RutabagaFence) -> VirtioGpuResult { + Self::with_rutabaga(|rutabaga| rutabaga.create_fence(rutabaga_fence))?; + Ok(OkNoData) + } + + fn process_fence( + &mut self, + ring: VirtioGpuRing, + fence_id: u64, + desc_index: u16, + len: u32, + ) -> bool { + common::common_process_fence(&self.fence_state, ring, fence_id, desc_index, len) + } + + fn get_event_poll_fd(&self) -> Option { + let result = Self::with_rutabaga(|rutabaga| { + rutabaga.poll_descriptor().map(|fd| { + // SAFETY: Safe, the fd should be valid, because Rutabaga guarantees it. + // into_raw_descriptor() returns a RawFd and makes sure SafeDescriptor::drop + // doesn't run. + unsafe { EventFd::from_raw_fd(fd.into_raw_descriptor()) } + }) + }); + debug!("get_event_poll_fd: {}", if result.is_some() { "Some(fd)" } else { "None" }); + result + } + + fn event_poll(&self) { + Self::with_rutabaga(|rutabaga| { + rutabaga.event_poll(); + }); + } + + fn force_ctx_0(&self) { + Self::with_rutabaga(|rutabaga| { + rutabaga.force_ctx_0(); + }); + } + + fn display_info(&self) -> VirtioGpuResult { + self.gpu_backend + .as_ref() + .map_or(Ok(GpuResponse::OkDisplayInfo(Vec::new())), |gpu_backend| { + common::common_display_info(gpu_backend) + }) + } + + fn get_edid(&self, edid_req: VhostUserGpuEdidRequest) -> VirtioGpuResult { + self.gpu_backend.as_ref().map_or( + Ok(GpuResponse::OkEdid { + blob: Box::new([0u8; 0]), + }), + |gpu_backend| common::common_get_edid(gpu_backend, edid_req), + ) + } + + fn set_scanout( + &mut self, + scanout_id: u32, + resource_id: u32, + rect: virtio_gpu_rect, + ) -> VirtioGpuResult { + let Some(gpu_backend) = self.gpu_backend.as_ref() else { + return Ok(OkNoData); + }; + + let scanout_idx = scanout_id as usize; + if resource_id == 0 { + common_set_scanout_disable(&mut self.scanouts, scanout_idx); + + gpu_backend + .set_scanout(&VhostUserGpuScanout { + scanout_id, + width: 0, + height: 0, + }) + .map_err(|e| { + error!("Failed to disable scanout: {e:?}"); + ErrUnspec + })?; + + return Ok(OkNoData); + } + + // If there was a different resource previously associated with this scanout, + // disable the scanout on that old resource + if let Some(old_scanout) = &self.scanouts[scanout_idx] { + let old_resource_id = old_scanout.resource_id; + if old_resource_id != resource_id { + if let Some(old_resource) = self.resources.get_mut(&old_resource_id) { + old_resource.scanouts.disable(scanout_id); + } + } + } + + let resource = self + .resources + .get_mut(&resource_id) + .ok_or(ErrInvalidResourceId)?; + + debug!( + "Enabling legacy scanout scanout_id={scanout_id}, resource_id={resource_id}: {rect:?}" + ); + + gpu_backend + .set_scanout(&VhostUserGpuScanout { + scanout_id, + width: rect.width.into(), + height: rect.height.into(), + }) + .map_err(|e| { + error!("Failed to legacy set_scanout: {e:?}"); + ErrUnspec + })?; + + resource.scanouts.enable(scanout_id); + self.scanouts[scanout_idx] = Some(VirtioGpuScanout { resource_id }); + + // Send initial framebuffer update to QEMU + // This ensures the display is properly initialized + let resource_size = resource.calculate_size().map_err(|e| { + error!("Invalid resource size for scanout: {e:?}"); + ErrUnspec + })?; + + let mut data = vec![0; resource_size]; + + if let Err(e) = Self::read_2d_resource(resource, &mut data) { + error!("Failed to read resource {resource_id} for initial scanout {scanout_id}: {e}"); + } else { + // Send the initial framebuffer data to QEMU + gpu_backend + .update_scanout( + &VhostUserGpuUpdate { + scanout_id, + x: 0, + y: 0, + width: resource.width, + height: resource.height, + }, + &data, + ) + .map_err(|e| { + error!("Failed to send initial framebuffer update: {e:?}"); + ErrUnspec + })?; + } + + Ok(OkNoData) + } + + fn flush_resource(&mut self, resource_id: u32, _rect: virtio_gpu_rect) -> VirtioGpuResult { + let Some(gpu_backend) = self.gpu_backend.as_ref() else { + return Ok(OkNoData); + }; + + if resource_id == 0 { + return Ok(OkNoData); + } + + let resource = self + .resources + .get(&resource_id) + .ok_or(ErrInvalidResourceId)? + .clone(); + + for scanout_id in resource.scanouts.iter_enabled() { + let resource_size = resource.calculate_size().map_err(|e| { + error!("Invalid resource size for flushing: {e:?}"); + ErrUnspec + })?; + + let mut data = vec![0; resource_size]; + + if let Err(e) = Self::read_2d_resource(&resource, &mut data) { + error!("Failed to read resource {resource_id} for scanout {scanout_id}: {e}"); + continue; + } + + gpu_backend + .update_scanout( + &VhostUserGpuUpdate { + scanout_id, + x: 0, + y: 0, + width: resource.width, + height: resource.height, + }, + &data, + ) + .map_err(|e| { + error!("Failed to update_scanout: {e:?}"); + ErrUnspec + })?; + } + Ok(OkNoData) + } + + fn resource_create_blob( + &mut self, + ctx_id: u32, + resource_create_blob: ResourceCreateBlob, + vecs: Vec<(vm_memory::GuestAddress, usize)>, + mem: &vm_memory::GuestMemoryMmap, + ) -> VirtioGpuResult { + let mut rutabaga_iovecs = None; + + if resource_create_blob.blob_flags & VIRTIO_GPU_BLOB_FLAG_CREATE_GUEST_HANDLE != 0 { + panic!("GUEST_HANDLE unimplemented"); + } else if resource_create_blob.blob_mem != VIRTIO_GPU_BLOB_MEM_HOST3D { + rutabaga_iovecs = + Some(Self::sglist_to_rutabaga_iovecs(&vecs[..], mem).map_err(|_| ErrUnspec)?); + } + + Self::with_rutabaga(|rutabaga| { + rutabaga.resource_create_blob( + ctx_id, + resource_create_blob.resource_id, + resource_create_blob.into(), + rutabaga_iovecs, + None, + ) + })?; + + let resource = RutabagaBackendResource { + id: resource_create_blob.resource_id, + width: 0, + height: 0, + scanouts: AssociatedScanouts::default(), + info_3d: None, + handle: None, + blob_size: resource_create_blob.size, + blob_shmem_offset: None, + }; + + debug_assert!( + !self + .resources + .contains_key(&resource_create_blob.resource_id), + "Resource ID {} already exists in the resources map.", + resource_create_blob.resource_id + ); + + // Rely on rutabaga to check for duplicate resource ids. + self.resources + .insert(resource_create_blob.resource_id, resource); + Ok(Self::result_from_query(resource_create_blob.resource_id)) + } + + fn resource_map_blob(&mut self, resource_id: u32, offset: u64) -> VirtioGpuResult { + let resource = self + .resources + .get_mut(&resource_id) + .ok_or(ErrInvalidResourceId)?; + + let map_info = Self::with_rutabaga(|rutabaga| rutabaga.map_info(resource_id)) + .map_err(|_| ErrUnspec)?; + + let export = Self::with_rutabaga(|rutabaga| rutabaga.export_blob(resource_id)) + .map_err(|_| ErrUnspec)?; + + // Check handle type - we don't support OPAQUE_FD mapping + if export.handle_type == RUTABAGA_HANDLE_TYPE_MEM_OPAQUE_FD { + return Err(ErrUnspec); + } + + // Convert map_info access flags to VhostUserMMapFlags + let flags = match map_info & RUTABAGA_MAP_ACCESS_MASK { + RUTABAGA_MAP_ACCESS_READ => VhostUserMMapFlags::default(), + RUTABAGA_MAP_ACCESS_WRITE => VhostUserMMapFlags::WRITABLE, + RUTABAGA_MAP_ACCESS_RW => VhostUserMMapFlags::WRITABLE, + _ => { + error!("Invalid access mask for blob resource, map_info: {map_info}"); + return Err(ErrUnspec); + } + }; + + common_map_blob( + &self.backend, + flags, + &export.os_handle.as_fd(), + resource.blob_size, + offset, + resource_id, + )?; + + resource.blob_shmem_offset = Some(offset); + + // Return cache flags only (access flags not part of virtio-gpu spec) + Ok(OkMapInfo { + map_info: map_info & RUTABAGA_MAP_CACHE_MASK, + }) + } + + fn resource_unmap_blob(&mut self, resource_id: u32) -> VirtioGpuResult { + let resource = self + .resources + .get_mut(&resource_id) + .ok_or(ErrInvalidResourceId)?; + + let Some(offset) = resource.blob_shmem_offset else { + log::warn!( + "Guest tried to unmap blob resource with resource_id={resource_id}, but it is not \ + mapped!" + ); + return Err(ErrInvalidParameter); + }; + + common_unmap_blob(&self.backend, resource.blob_size, offset)?; + + resource.blob_shmem_offset = None; + + Ok(OkNoData) + } +} diff --git a/vhost-device-gpu/src/backend/virgl.rs b/vhost-device-gpu/src/backend/virgl.rs index 540d37e..12eda4b 100644 --- a/vhost-device-gpu/src/backend/virgl.rs +++ b/vhost-device-gpu/src/backend/virgl.rs @@ -183,7 +183,6 @@ impl VirglRendererAdapter { .use_render_server(venus_enabled) .use_egl(config.flags().use_egl) .use_gles(config.flags().use_gles) - .use_glx(config.flags().use_glx) .use_surfaceless(config.flags().use_surfaceless) .use_external_blob(true) .use_async_fence_cb(true) diff --git a/vhost-device-gpu/src/device.rs b/vhost-device-gpu/src/device.rs index 5577e99..579f877 100644 --- a/vhost-device-gpu/src/device.rs +++ b/vhost-device-gpu/src/device.rs @@ -52,7 +52,7 @@ use rutabaga_gfx::RutabagaFence; use thiserror::Error as ThisError; use vhost::vhost_user::{ gpu_message::{VhostUserGpuCursorPos, VhostUserGpuEdidRequest}, - message::{VhostUserProtocolFeatures, VhostUserVirtioFeatures}, + message::{VhostUserProtocolFeatures, VhostUserShMemConfig, VhostUserVirtioFeatures}, Backend, GpuBackend, }; use vhost_user_backend::{VhostUserBackend, VringEpollHandler, VringRwLock, VringT}; @@ -76,6 +76,7 @@ use vmm_sys_util::{ #[cfg(feature = "backend-gfxstream")] use crate::backend::gfxstream::GfxstreamAdapter; +use crate::backend::rutabaga::RutabagaAdapter; #[cfg(feature = "backend-virgl")] use crate::backend::virgl::VirglRendererAdapter; use crate::{ @@ -663,6 +664,22 @@ impl VhostUserGpuBackendInner { vrings ), + GpuMode::Rutabaga => handle_adapter!( + RutabagaAdapter, + TLS_RUTABAGA_ADAPTER, + |control_vring, backend, gpu_backend| -> io::Result { + Ok(RutabagaAdapter::new( + control_vring, + backend, + &self.gpu_config, + gpu_backend, + )) + }, + self, + device_event, + vrings + ), + GpuMode::Null => handle_adapter!( NullAdapter, TLS_NULL, @@ -738,6 +755,17 @@ impl VhostUserBackend for VhostUserGpuBackend { | VhostUserProtocolFeatures::SHMEM } + fn get_shmem_config(&self) -> IoResult { + // Shared memory region for mapping blob resources (dmabufs) into guest + // address space. ID 1 per virtio-gpu spec, size matches crosvm default. + const VIRTIO_GPU_SHM_ID_HOST_VISIBLE: u8 = 1; + const SHMEM_SIZE: u64 = 1 << 33; // 8 GiB + Ok(VhostUserShMemConfig::new(&[( + VIRTIO_GPU_SHM_ID_HOST_VISIBLE, + SHMEM_SIZE, + )])) + } + fn set_event_idx(&self, enabled: bool) { self.inner.lock().unwrap().event_idx_enabled = enabled; debug!("Event idx set to: {enabled}"); @@ -939,8 +967,7 @@ mod tests { fn init() -> (Arc, GuestMemoryAtomic) { let config = GpuConfigBuilder::default() - .set_gpu_mode(GpuMode::VirglRenderer) - .set_capset(GpuCapset::VIRGL | GpuCapset::VIRGL2) + .set_gpu_mode(GpuMode::Null) .set_flags(GpuFlags::default()) .build() .unwrap(); @@ -1455,7 +1482,7 @@ mod tests { #[test] fn test_verify_backend() { let gpu_config = GpuConfigBuilder::default() - .set_gpu_mode(GpuMode::VirglRenderer) + .set_gpu_mode(GpuMode::Null) .set_flags(GpuFlags::default()) .build() .unwrap(); @@ -1522,6 +1549,7 @@ mod tests { } } + #[cfg(feature = "backend-virgl")] mod test_image { use super::*; const GREEN_PIXEL: u32 = 0x00FF_00FF; @@ -1542,6 +1570,7 @@ mod tests { } } + #[cfg(feature = "backend-virgl")] fn split_into_mem_entries( addr: GuestAddress, len: u32, @@ -1572,6 +1601,7 @@ mod tests { entries } + #[cfg(feature = "backend-virgl")] fn new_hdr(type_: u32) -> virtio_gpu_ctrl_hdr { virtio_gpu_ctrl_hdr { type_: type_.into(), @@ -1579,6 +1609,24 @@ mod tests { } } + #[cfg(feature = "backend-virgl")] + fn init_virgl() -> (Arc, GuestMemoryAtomic) { + let config = GpuConfigBuilder::default() + .set_gpu_mode(GpuMode::VirglRenderer) + .set_capset(GpuCapset::VIRGL | GpuCapset::VIRGL2) + .set_flags(GpuFlags::default()) + .build() + .unwrap(); + let backend = VhostUserGpuBackend::new(config).unwrap(); + let mem = GuestMemoryAtomic::new( + GuestMemoryMmap::<()>::from_ranges(&[(GuestAddress(0), MEM_SIZE)]).unwrap(), + ); + + backend.update_memory(mem.clone()).unwrap(); + (backend, mem) + } + + #[cfg(feature = "backend-virgl")] rusty_fork_test! { /// This test uses multiple gpu commands, it crates a resource, writes a test image into it and /// then present the display output. @@ -1604,7 +1652,7 @@ mod tests { fd_drm_fourcc: 875_708_993, // This is a placeholder; actual value depends on the backend. }; - let (backend, mem) = init(); + let (backend, mem) = init_virgl(); let (mut gpu_frontend, gpu_backend) = gpu_backend_pair(); gpu_frontend .set_read_timeout(Some(Duration::from_secs(10))) diff --git a/vhost-device-gpu/src/gpu_types.rs b/vhost-device-gpu/src/gpu_types.rs index 8380106..db9381c 100644 --- a/vhost-device-gpu/src/gpu_types.rs +++ b/vhost-device-gpu/src/gpu_types.rs @@ -4,7 +4,6 @@ /// 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 { @@ -26,7 +25,6 @@ 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 +46,6 @@ macro_rules! impl_from_resource_create3d { }; } -#[cfg(any(feature = "backend-virgl", feature = "backend-gfxstream"))] macro_rules! impl_from_resource_create_blob { ($target:ty) => { impl From for $target { @@ -66,7 +63,6 @@ macro_rules! impl_from_resource_create_blob { 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; @@ -105,9 +101,8 @@ impl Transfer3DDesc { } } } -// Invoke the macro for both targets +// Invoke the macro for all targets // rutabaga_gfx::Transfer3D -#[cfg(feature = "backend-gfxstream")] impl_transfer3d_from_desc!(Transfer3D); // virglrenderer::Transfer3D #[cfg(feature = "backend-virgl")] @@ -157,13 +152,11 @@ pub struct ResourceCreate3d { pub flags: u32, } -// Invoke the macro for both targets -#[cfg(feature = "backend-gfxstream")] +// Invoke the macro for all targets impl_from_resource_create3d!(rutabaga_gfx::ResourceCreate3D); #[cfg(feature = "backend-virgl")] impl_from_resource_create3d!(virglrenderer::ResourceCreate3D); -#[cfg(feature = "backend-gfxstream")] impl_from_resource_create_blob!(rutabaga_gfx::ResourceCreateBlob); #[cfg(feature = "backend-virgl")] impl_from_resource_create_blob!(virglrenderer::ResourceCreateBlob); diff --git a/vhost-device-gpu/src/lib.rs b/vhost-device-gpu/src/lib.rs index ee2932d..e4d3d05 100644 --- a/vhost-device-gpu/src/lib.rs +++ b/vhost-device-gpu/src/lib.rs @@ -19,9 +19,7 @@ pub mod renderer; #[cfg(test)] pub(crate) mod testutils; -#[cfg(feature = "backend-virgl")] use std::fs::File; -#[cfg(feature = "backend-virgl")] use std::os::fd::OwnedFd; use std::{ fmt::{Display, Formatter}, @@ -31,22 +29,26 @@ use std::{ use bitflags::bitflags; use clap::ValueEnum; use log::info; +use rutabaga_gfx::{ + RUTABAGA_CAPSET_CROSS_DOMAIN, RUTABAGA_CAPSET_VENUS, RUTABAGA_CAPSET_VIRGL, + RUTABAGA_CAPSET_VIRGL2, +}; #[cfg(feature = "backend-gfxstream")] use rutabaga_gfx::{RUTABAGA_CAPSET_GFXSTREAM_GLES, RUTABAGA_CAPSET_GFXSTREAM_VULKAN}; -#[cfg(feature = "backend-virgl")] -use rutabaga_gfx::{RUTABAGA_CAPSET_VENUS, 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; -#[cfg(feature = "backend-virgl")] pub const DEFAULT_VIRGLRENDER_CAPSET_MASK: GpuCapset = GpuCapset::ALL_VIRGLRENDERER_CAPSETS; #[cfg(feature = "backend-gfxstream")] pub const DEFAULT_GFXSTREAM_CAPSET_MASK: GpuCapset = GpuCapset::ALL_GFXSTREAM_CAPSETS; +pub const DEFAULT_RUTABAGA_CAPSET_MASK: GpuCapset = + GpuCapset::VIRGL2.union(GpuCapset::CROSS_DOMAIN); + #[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)] pub enum GpuMode { #[value(name = "virglrenderer", alias("virgl-renderer"))] @@ -54,6 +56,8 @@ pub enum GpuMode { VirglRenderer, #[cfg(feature = "backend-gfxstream")] Gfxstream, + #[value(name = "rutabaga")] + Rutabaga, Null, } @@ -64,6 +68,7 @@ impl Display for GpuMode { Self::VirglRenderer => write!(f, "virglrenderer"), #[cfg(feature = "backend-gfxstream")] Self::Gfxstream => write!(f, "gfxstream"), + Self::Rutabaga => write!(f, "rutabaga"), Self::Null => write!(f, "null"), } } @@ -73,13 +78,10 @@ bitflags! { /// A bitmask for representing supported gpu capability sets. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct GpuCapset: u64 { - #[cfg(feature = "backend-virgl")] const VIRGL = 1 << RUTABAGA_CAPSET_VIRGL as u64; - #[cfg(feature = "backend-virgl")] const VIRGL2 = 1 << RUTABAGA_CAPSET_VIRGL2 as u64; - #[cfg(feature = "backend-virgl")] const VENUS = 1 << RUTABAGA_CAPSET_VENUS as u64; - #[cfg(feature = "backend-virgl")] + const CROSS_DOMAIN = 1 << RUTABAGA_CAPSET_CROSS_DOMAIN as u64; const ALL_VIRGLRENDERER_CAPSETS = Self::VIRGL.bits() | Self::VIRGL2.bits() | Self::VENUS.bits(); #[cfg(feature = "backend-gfxstream")] @@ -106,12 +108,10 @@ impl Display for GpuCapset { first = false; match capset { - #[cfg(feature = "backend-virgl")] Self::VIRGL => write!(f, "virgl")?, - #[cfg(feature = "backend-virgl")] Self::VIRGL2 => write!(f, "virgl2")?, - #[cfg(feature = "backend-virgl")] Self::VENUS => write!(f, "venus")?, + Self::CROSS_DOMAIN => write!(f, "cross-domain")?, #[cfg(feature = "backend-gfxstream")] Self::GFXSTREAM_VULKAN => write!(f, "gfxstream-vulkan")?, #[cfg(feature = "backend-gfxstream")] @@ -137,8 +137,8 @@ pub struct GpuConfig { gpu_mode: GpuMode, capset: GpuCapset, flags: GpuFlags, - #[cfg(feature = "backend-virgl")] render_server_fd: Option, + wayland_socket: Option, } #[derive(Debug, Default)] @@ -146,8 +146,8 @@ pub struct GpuConfigBuilder { gpu_mode: Option, capset: Option, flags: Option, - #[cfg(feature = "backend-virgl")] gpu_device: Option, + wayland_socket: Option, } impl GpuConfigBuilder { @@ -166,18 +166,32 @@ impl GpuConfigBuilder { self } - #[cfg(feature = "backend-virgl")] pub fn set_gpu_device(mut self, gpu_device: PathBuf) -> Self { self.gpu_device = Some(gpu_device); self } + pub fn set_wayland_socket(mut self, path: PathBuf) -> Self { + self.wayland_socket = Some(path); + self + } + + fn gpu_mode_supports_gpu_device(gpu_mode: GpuMode) -> bool { + match gpu_mode { + #[cfg(feature = "backend-virgl")] + GpuMode::VirglRenderer => true, + GpuMode::Rutabaga => true, + _ => false, + } + } + fn validate_capset(gpu_mode: GpuMode, capset: GpuCapset) -> Result<(), GpuConfigError> { let supported_capset_mask = match gpu_mode { #[cfg(feature = "backend-virgl")] GpuMode::VirglRenderer => GpuCapset::ALL_VIRGLRENDERER_CAPSETS, #[cfg(feature = "backend-gfxstream")] GpuMode::Gfxstream => GpuCapset::ALL_GFXSTREAM_CAPSETS, + GpuMode::Rutabaga => GpuCapset::ALL_VIRGLRENDERER_CAPSETS | GpuCapset::CROSS_DOMAIN, GpuMode::Null => GpuCapset::empty(), }; for capset in capset.iter() { @@ -197,6 +211,7 @@ impl GpuConfigBuilder { GpuMode::VirglRenderer => DEFAULT_VIRGLRENDER_CAPSET_MASK, #[cfg(feature = "backend-gfxstream")] GpuMode::Gfxstream => DEFAULT_GFXSTREAM_CAPSET_MASK, + GpuMode::Rutabaga => DEFAULT_RUTABAGA_CAPSET_MASK, GpuMode::Null => GpuCapset::empty(), }); @@ -207,12 +222,23 @@ impl GpuConfigBuilder { return Err(GpuConfigError::GlesRequiredByGfxstream); } - #[cfg(feature = "backend-virgl")] - if self.gpu_device.is_some() && !matches!(gpu_mode, GpuMode::VirglRenderer) { + if self.gpu_device.is_some() && !Self::gpu_mode_supports_gpu_device(gpu_mode) { return Err(GpuConfigError::GpuDeviceNotSupportedByMode); } - #[cfg(feature = "backend-virgl")] + // Cross-domain / wayland socket validation + if capset.contains(GpuCapset::CROSS_DOMAIN) && self.wayland_socket.is_none() { + return Err(GpuConfigError::WaylandSocketRequired); + } + if !capset.contains(GpuCapset::CROSS_DOMAIN) && self.wayland_socket.is_some() { + return Err(GpuConfigError::WaylandSocketWithoutCrossDomain); + } + + // VIRGL without VIRGL2 validation + if capset.contains(GpuCapset::VIRGL) && !capset.contains(GpuCapset::VIRGL2) { + return Err(GpuConfigError::VirglWithoutVirgl2); + } + let render_server_fd = if let Some(gpu_device) = self.gpu_device { let fd = File::open(&gpu_device) .map(Into::into) @@ -226,8 +252,8 @@ impl GpuConfigBuilder { gpu_mode, capset, flags, - #[cfg(feature = "backend-virgl")] render_server_fd, + wayland_socket: self.wayland_socket, }) } } @@ -235,7 +261,6 @@ impl GpuConfigBuilder { #[derive(Debug, PartialEq, Eq)] pub struct GpuFlags { pub use_egl: bool, - pub use_glx: bool, pub use_gles: bool, pub use_surfaceless: bool, pub headless: bool, @@ -246,7 +271,6 @@ impl GpuFlags { pub const fn new_default() -> Self { Self { use_egl: true, - use_glx: false, use_gles: true, use_surfaceless: true, headless: false, @@ -272,6 +296,12 @@ pub enum GpuConfigError { GpuDeviceNotSupportedByMode, #[error("Failed to open GPU device '{0}': {1}")] InvalidGpuDevice(PathBuf, std::io::Error), + #[error("--wayland-socket is required when cross-domain capset is enabled")] + WaylandSocketRequired, + #[error("--wayland-socket should only be specified with cross-domain capset")] + WaylandSocketWithoutCrossDomain, + #[error("virgl capset specified without virgl2 — virglrenderer will not initialize. Add virgl2 to the capset list")] + VirglWithoutVirgl2, } impl GpuConfig { @@ -287,10 +317,13 @@ impl GpuConfig { &self.flags } - #[cfg(feature = "backend-virgl")] pub fn render_server_fd(&self) -> Option<&OwnedFd> { self.render_server_fd.as_ref() } + + pub fn wayland_socket(&self) -> Option<&Path> { + self.wayland_socket.as_deref() + } } #[derive(Debug, ThisError)] @@ -322,16 +355,13 @@ pub fn start_backend(socket_path: &Path, config: GpuConfig) -> Result<(), StartE #[cfg(test)] mod tests { - use std::path::Path; + use std::path::{Path, PathBuf}; use assert_matches::assert_matches; use super::*; - #[cfg(feature = "backend-virgl")] fn assert_gpu_device_fails_for_mode(mode: GpuMode) { - use std::path::PathBuf; - let result = GpuConfigBuilder::default() .set_gpu_mode(mode) .set_gpu_device(PathBuf::from("/dev/dri/renderD128")) @@ -425,8 +455,7 @@ mod tests { } #[test] - #[cfg(feature = "backend-virgl")] - fn test_gpu_device_only_with_virglrenderer() { + fn test_gpu_device_only_with_supported_modes() { assert_gpu_device_fails_for_mode(GpuMode::Null); #[cfg(feature = "backend-gfxstream")] @@ -435,14 +464,13 @@ mod tests { #[test] fn test_default_num_capsets() { - #[cfg(feature = "backend-virgl")] assert_eq!(DEFAULT_VIRGLRENDER_CAPSET_MASK.num_capsets(), 3); #[cfg(feature = "backend-gfxstream")] assert_eq!(DEFAULT_GFXSTREAM_CAPSET_MASK.num_capsets(), 2); + assert_eq!(DEFAULT_RUTABAGA_CAPSET_MASK.num_capsets(), 2); } #[test] - #[cfg(feature = "backend-virgl")] fn test_capset_display_multiple() { let capset = GpuCapset::VIRGL | GpuCapset::VIRGL2; let output = capset.to_string(); @@ -475,4 +503,76 @@ mod tests { StartError::ServeFailed(_) ); } + + #[test] + fn test_gpu_config_rutabaga_default_capset() { + let config = GpuConfigBuilder::default() + .set_gpu_mode(GpuMode::Rutabaga) + .set_flags(GpuFlags::default()) + .set_wayland_socket(PathBuf::from("/tmp/wayland-0")) + .build() + .unwrap(); + assert_eq!(config.gpu_mode(), GpuMode::Rutabaga); + assert!(config.capsets().contains(GpuCapset::VIRGL2)); + assert!(config.capsets().contains(GpuCapset::CROSS_DOMAIN)); + } + + #[test] + fn test_gpu_config_cross_domain_requires_wayland_socket() { + let result = GpuConfigBuilder::default() + .set_gpu_mode(GpuMode::Rutabaga) + .set_capset(GpuCapset::CROSS_DOMAIN) + .set_flags(GpuFlags::default()) + .build(); + assert_matches!(result, Err(GpuConfigError::WaylandSocketRequired)); + } + + #[test] + fn test_gpu_config_wayland_socket_without_cross_domain() { + let result = GpuConfigBuilder::default() + .set_gpu_mode(GpuMode::Rutabaga) + .set_capset(GpuCapset::VIRGL2) + .set_flags(GpuFlags::default()) + .set_wayland_socket(PathBuf::from("/tmp/wayland-0")) + .build(); + assert_matches!(result, Err(GpuConfigError::WaylandSocketWithoutCrossDomain)); + } + + #[test] + fn test_gpu_config_virgl_without_virgl2() { + let result = GpuConfigBuilder::default() + .set_gpu_mode(GpuMode::Rutabaga) + .set_capset(GpuCapset::VIRGL | GpuCapset::CROSS_DOMAIN) + .set_flags(GpuFlags::default()) + .set_wayland_socket(PathBuf::from("/tmp/wayland-0")) + .build(); + assert_matches!(result, Err(GpuConfigError::VirglWithoutVirgl2)); + } + + #[test] + fn test_gpu_config_cross_domain_alone() { + let config = GpuConfigBuilder::default() + .set_gpu_mode(GpuMode::Rutabaga) + .set_capset(GpuCapset::CROSS_DOMAIN) + .set_flags(GpuFlags::default()) + .set_wayland_socket(PathBuf::from("/tmp/wayland-0")) + .build() + .unwrap(); + assert_eq!(config.capsets(), GpuCapset::CROSS_DOMAIN); + assert_eq!(config.wayland_socket(), Some(Path::new("/tmp/wayland-0"))); + } + + #[test] + fn test_gpu_config_cross_domain_plus_virgl() { + let config = GpuConfigBuilder::default() + .set_gpu_mode(GpuMode::Rutabaga) + .set_capset(GpuCapset::VIRGL | GpuCapset::VIRGL2 | GpuCapset::CROSS_DOMAIN) + .set_flags(GpuFlags::default()) + .set_wayland_socket(PathBuf::from("/tmp/wayland-0")) + .build() + .unwrap(); + assert!(config.capsets().contains(GpuCapset::VIRGL)); + assert!(config.capsets().contains(GpuCapset::VIRGL2)); + assert!(config.capsets().contains(GpuCapset::CROSS_DOMAIN)); + } } diff --git a/vhost-device-gpu/src/main.rs b/vhost-device-gpu/src/main.rs index 97cf2dc..784905f 100644 --- a/vhost-device-gpu/src/main.rs +++ b/vhost-device-gpu/src/main.rs @@ -14,22 +14,19 @@ use vhost_device_gpu::{ #[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")] Virgl = GpuCapset::VIRGL.bits(), /// [virglrenderer] OpenGL implementation - #[cfg(feature = "backend-virgl")] Virgl2 = GpuCapset::VIRGL2.bits(), /// [virglrenderer] Venus (Vulkan) implementation - #[cfg(feature = "backend-virgl")] Venus = GpuCapset::VENUS.bits(), + /// Cross-domain support for Wayland forwarding + CrossDomain = GpuCapset::CROSS_DOMAIN.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 @@ -41,20 +38,10 @@ 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") } @@ -82,6 +69,10 @@ pub struct GpuArgs { #[clap(short, long, value_delimiter = ',')] pub capset: Option>, + /// Wayland socket path for cross-domain (e.g., /run/user/1000/wayland-0) + #[clap(long, value_name = "PATH")] + pub wayland_socket: Option, + #[clap(flatten)] pub flags: GpuFlagsArgs, } @@ -97,14 +88,6 @@ pub struct GpuFlagsArgs { )] pub use_egl: bool, - /// 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, @@ -127,7 +110,6 @@ pub struct GpuFlagsArgs { /// GPU device path (e.g., /dev/dri/renderD128) #[clap(long, value_name = "PATH")] - #[cfg(feature = "backend-virgl")] pub gpu_device: Option, } @@ -135,7 +117,6 @@ 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, headless: args.headless, @@ -144,7 +125,6 @@ impl From for GpuFlags { } pub fn config_from_args(args: GpuArgs) -> Result<(PathBuf, GpuConfig), GpuConfigError> { - #[cfg(feature = "backend-virgl")] let gpu_device = args.flags.gpu_device.clone(); let flags = GpuFlags::from(args.flags); @@ -158,11 +138,14 @@ pub fn config_from_args(args: GpuArgs) -> Result<(PathBuf, GpuConfig), GpuConfig builder = builder.set_capset(capset); } - #[cfg(feature = "backend-virgl")] if let Some(gpu_device) = gpu_device { builder = builder.set_gpu_device(gpu_device); } + if let Some(wayland_socket) = args.wayland_socket { + builder = builder.set_wayland_socket(wayland_socket); + } + let config = builder.build()?; Ok((args.socket_path, config)) } @@ -222,19 +205,19 @@ mod tests { } #[test] + #[cfg(feature = "backend-virgl")] 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]), + wayland_socket: None, flags: GpuFlagsArgs { use_egl: false, - use_glx: true, use_gles: false, use_surfaceless: false, headless: false, - #[cfg(feature = "backend-virgl")] gpu_device: None, }, }; @@ -246,7 +229,6 @@ mod tests { *config.flags(), GpuFlags { use_egl: false, - use_glx: true, use_gles: false, use_surfaceless: false, headless: false, @@ -256,16 +238,15 @@ mod tests { assert_eq!(config.capsets(), GpuCapset::VIRGL | GpuCapset::VIRGL2); // Test with invalid GPU device - #[cfg(feature = "backend-virgl")] { let invalid_gpu_device = Path::new("/nonexistent/gpu/device"); let invalid_args = GpuArgs { socket_path: expected_path.into(), gpu_mode: GpuMode::VirglRenderer, capset: Some(vec![CapsetName::Virgl]), + wayland_socket: None, flags: GpuFlagsArgs { use_egl: true, - use_glx: false, use_gles: true, use_surfaceless: true, headless: false,