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) <noreply@anthropic.com>
This commit is contained in:
Davíð Steinn Geirsson 2026-03-22 22:27:42 +00:00
parent 1c021aaea6
commit 1460f1be1b
8 changed files with 1047 additions and 81 deletions

View file

@ -14,7 +14,7 @@ edition = "2021"
resolver = "2" resolver = "2"
[features] [features]
default = ["backend-virgl", "backend-gfxstream"] default = []
xen = ["vm-memory/xen", "vhost/xen", "vhost-user-backend/xen"] xen = ["vm-memory/xen", "vhost/xen", "vhost-user-backend/xen"]
backend-gfxstream = ["rutabaga_gfx/gfxstream"] backend-gfxstream = ["rutabaga_gfx/gfxstream"]
backend-virgl = ["dep:virglrenderer"] backend-virgl = ["dep:virglrenderer"]
@ -26,7 +26,7 @@ libc = "0.2"
log = "0.4" log = "0.4"
[target.'cfg(not(target_env = "musl"))'.dependencies] [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" thiserror = "2.0.18"
virglrenderer = { git = "https://gitlab.freedesktop.org/mtjhrc/virglrenderer-rs.git", branch = "map_info", optional = true } 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"] } vhost = { git = "https://git.dsg.is/dsg/vhost.git", features = ["vhost-user-backend"] }

View file

@ -2,10 +2,10 @@
// //
// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause // SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause
#[cfg(any(feature = "backend-virgl", feature = "backend-gfxstream"))]
mod common; mod common;
#[cfg(feature = "backend-gfxstream")] #[cfg(feature = "backend-gfxstream")]
pub mod gfxstream; pub mod gfxstream;
pub mod null; pub mod null;
pub mod rutabaga;
#[cfg(feature = "backend-virgl")] #[cfg(feature = "backend-virgl")]
pub mod virgl; pub mod virgl;

View file

@ -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<rutabaga_gfx::Resource3DInfo>,
pub handle: Option<Arc<RutabagaHandle>>,
pub blob_size: u64,
pub blob_shmem_offset: Option<u64>,
}
impl RutabagaBackendResource {
fn calculate_size(&self) -> Result<usize, &str> {
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<Option<Rutabaga>> = const { RefCell::new(None) };
}
pub struct RutabagaAdapter {
#[allow(dead_code)]
backend: Backend,
gpu_backend: Option<GpuBackend>,
resources: BTreeMap<u32, RutabagaBackendResource>,
fence_state: Arc<Mutex<FenceState>>,
scanouts: [Option<VirtioGpuScanout>; VIRTIO_GPU_MAX_SCANOUTS as usize],
}
impl RutabagaAdapter {
pub fn new(
queue_ctl: &VringRwLock,
backend: Backend,
gpu_config: &GpuConfig,
gpu_backend: Option<GpuBackend>,
) -> 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<Mutex<FenceState>>,
) -> 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<Vec<RutabagaIovec>, ()> {
if vecs
.iter()
.any(|&(addr, len)| mem.get_slice(addr, len).is_err())
{
return Err(());
}
let mut rutabaga_iovecs: Vec<RutabagaIovec> = 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::<c_void>(),
len,
};
rutabaga_iovecs.push(iov);
}
Ok(rutabaga_iovecs)
}
fn with_rutabaga<T, F: FnOnce(&mut 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<VolatileSlice>,
) -> 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<EventFd> {
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)
}
}

View file

@ -183,7 +183,6 @@ impl VirglRendererAdapter {
.use_render_server(venus_enabled) .use_render_server(venus_enabled)
.use_egl(config.flags().use_egl) .use_egl(config.flags().use_egl)
.use_gles(config.flags().use_gles) .use_gles(config.flags().use_gles)
.use_glx(config.flags().use_glx)
.use_surfaceless(config.flags().use_surfaceless) .use_surfaceless(config.flags().use_surfaceless)
.use_external_blob(true) .use_external_blob(true)
.use_async_fence_cb(true) .use_async_fence_cb(true)

View file

@ -52,7 +52,7 @@ use rutabaga_gfx::RutabagaFence;
use thiserror::Error as ThisError; use thiserror::Error as ThisError;
use vhost::vhost_user::{ use vhost::vhost_user::{
gpu_message::{VhostUserGpuCursorPos, VhostUserGpuEdidRequest}, gpu_message::{VhostUserGpuCursorPos, VhostUserGpuEdidRequest},
message::{VhostUserProtocolFeatures, VhostUserVirtioFeatures}, message::{VhostUserProtocolFeatures, VhostUserShMemConfig, VhostUserVirtioFeatures},
Backend, GpuBackend, Backend, GpuBackend,
}; };
use vhost_user_backend::{VhostUserBackend, VringEpollHandler, VringRwLock, VringT}; use vhost_user_backend::{VhostUserBackend, VringEpollHandler, VringRwLock, VringT};
@ -76,6 +76,7 @@ use vmm_sys_util::{
#[cfg(feature = "backend-gfxstream")] #[cfg(feature = "backend-gfxstream")]
use crate::backend::gfxstream::GfxstreamAdapter; use crate::backend::gfxstream::GfxstreamAdapter;
use crate::backend::rutabaga::RutabagaAdapter;
#[cfg(feature = "backend-virgl")] #[cfg(feature = "backend-virgl")]
use crate::backend::virgl::VirglRendererAdapter; use crate::backend::virgl::VirglRendererAdapter;
use crate::{ use crate::{
@ -663,6 +664,22 @@ impl VhostUserGpuBackendInner {
vrings vrings
), ),
GpuMode::Rutabaga => handle_adapter!(
RutabagaAdapter,
TLS_RUTABAGA_ADAPTER,
|control_vring, backend, gpu_backend| -> io::Result<RutabagaAdapter> {
Ok(RutabagaAdapter::new(
control_vring,
backend,
&self.gpu_config,
gpu_backend,
))
},
self,
device_event,
vrings
),
GpuMode::Null => handle_adapter!( GpuMode::Null => handle_adapter!(
NullAdapter, NullAdapter,
TLS_NULL, TLS_NULL,
@ -738,6 +755,17 @@ impl VhostUserBackend for VhostUserGpuBackend {
| VhostUserProtocolFeatures::SHMEM | VhostUserProtocolFeatures::SHMEM
} }
fn get_shmem_config(&self) -> IoResult<VhostUserShMemConfig> {
// 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) { fn set_event_idx(&self, enabled: bool) {
self.inner.lock().unwrap().event_idx_enabled = enabled; self.inner.lock().unwrap().event_idx_enabled = enabled;
debug!("Event idx set to: {enabled}"); debug!("Event idx set to: {enabled}");
@ -939,8 +967,7 @@ mod tests {
fn init() -> (Arc<VhostUserGpuBackend>, GuestMemoryAtomic<GuestMemoryMmap>) { fn init() -> (Arc<VhostUserGpuBackend>, GuestMemoryAtomic<GuestMemoryMmap>) {
let config = GpuConfigBuilder::default() let config = GpuConfigBuilder::default()
.set_gpu_mode(GpuMode::VirglRenderer) .set_gpu_mode(GpuMode::Null)
.set_capset(GpuCapset::VIRGL | GpuCapset::VIRGL2)
.set_flags(GpuFlags::default()) .set_flags(GpuFlags::default())
.build() .build()
.unwrap(); .unwrap();
@ -1455,7 +1482,7 @@ mod tests {
#[test] #[test]
fn test_verify_backend() { fn test_verify_backend() {
let gpu_config = GpuConfigBuilder::default() let gpu_config = GpuConfigBuilder::default()
.set_gpu_mode(GpuMode::VirglRenderer) .set_gpu_mode(GpuMode::Null)
.set_flags(GpuFlags::default()) .set_flags(GpuFlags::default())
.build() .build()
.unwrap(); .unwrap();
@ -1522,6 +1549,7 @@ mod tests {
} }
} }
#[cfg(feature = "backend-virgl")]
mod test_image { mod test_image {
use super::*; use super::*;
const GREEN_PIXEL: u32 = 0x00FF_00FF; const GREEN_PIXEL: u32 = 0x00FF_00FF;
@ -1542,6 +1570,7 @@ mod tests {
} }
} }
#[cfg(feature = "backend-virgl")]
fn split_into_mem_entries( fn split_into_mem_entries(
addr: GuestAddress, addr: GuestAddress,
len: u32, len: u32,
@ -1572,6 +1601,7 @@ mod tests {
entries entries
} }
#[cfg(feature = "backend-virgl")]
fn new_hdr(type_: u32) -> virtio_gpu_ctrl_hdr { fn new_hdr(type_: u32) -> virtio_gpu_ctrl_hdr {
virtio_gpu_ctrl_hdr { virtio_gpu_ctrl_hdr {
type_: type_.into(), type_: type_.into(),
@ -1579,6 +1609,24 @@ mod tests {
} }
} }
#[cfg(feature = "backend-virgl")]
fn init_virgl() -> (Arc<VhostUserGpuBackend>, GuestMemoryAtomic<GuestMemoryMmap>) {
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! { rusty_fork_test! {
/// This test uses multiple gpu commands, it crates a resource, writes a test image into it and /// This test uses multiple gpu commands, it crates a resource, writes a test image into it and
/// then present the display output. /// 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. 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(); let (mut gpu_frontend, gpu_backend) = gpu_backend_pair();
gpu_frontend gpu_frontend
.set_read_timeout(Some(Duration::from_secs(10))) .set_read_timeout(Some(Duration::from_secs(10)))

View file

@ -4,7 +4,6 @@
/// Generates an implementation of `From<Transfer3DDesc>` for any compatible /// Generates an implementation of `From<Transfer3DDesc>` for any compatible
/// target struct. /// target struct.
#[cfg(any(feature = "backend-virgl", feature = "backend-gfxstream"))]
macro_rules! impl_transfer3d_from_desc { macro_rules! impl_transfer3d_from_desc {
($target:path) => { ($target:path) => {
impl From<Transfer3DDesc> for $target { impl From<Transfer3DDesc> 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 { macro_rules! impl_from_resource_create3d {
($target:ty) => { ($target:ty) => {
impl From<ResourceCreate3d> for $target { impl From<ResourceCreate3d> 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 { macro_rules! impl_from_resource_create_blob {
($target:ty) => { ($target:ty) => {
impl From<ResourceCreateBlob> for $target { impl From<ResourceCreateBlob> for $target {
@ -66,7 +63,6 @@ macro_rules! impl_from_resource_create_blob {
use std::{collections::BTreeMap, os::raw::c_void}; use std::{collections::BTreeMap, os::raw::c_void};
#[cfg(feature = "backend-gfxstream")]
use rutabaga_gfx::Transfer3D; use rutabaga_gfx::Transfer3D;
#[cfg(feature = "backend-virgl")] #[cfg(feature = "backend-virgl")]
use virglrenderer::Transfer3D as VirglTransfer3D; 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 // rutabaga_gfx::Transfer3D
#[cfg(feature = "backend-gfxstream")]
impl_transfer3d_from_desc!(Transfer3D); impl_transfer3d_from_desc!(Transfer3D);
// virglrenderer::Transfer3D // virglrenderer::Transfer3D
#[cfg(feature = "backend-virgl")] #[cfg(feature = "backend-virgl")]
@ -157,13 +152,11 @@ pub struct ResourceCreate3d {
pub flags: u32, pub flags: u32,
} }
// Invoke the macro for both targets // Invoke the macro for all targets
#[cfg(feature = "backend-gfxstream")]
impl_from_resource_create3d!(rutabaga_gfx::ResourceCreate3D); impl_from_resource_create3d!(rutabaga_gfx::ResourceCreate3D);
#[cfg(feature = "backend-virgl")] #[cfg(feature = "backend-virgl")]
impl_from_resource_create3d!(virglrenderer::ResourceCreate3D); impl_from_resource_create3d!(virglrenderer::ResourceCreate3D);
#[cfg(feature = "backend-gfxstream")]
impl_from_resource_create_blob!(rutabaga_gfx::ResourceCreateBlob); impl_from_resource_create_blob!(rutabaga_gfx::ResourceCreateBlob);
#[cfg(feature = "backend-virgl")] #[cfg(feature = "backend-virgl")]
impl_from_resource_create_blob!(virglrenderer::ResourceCreateBlob); impl_from_resource_create_blob!(virglrenderer::ResourceCreateBlob);

View file

@ -19,9 +19,7 @@ pub mod renderer;
#[cfg(test)] #[cfg(test)]
pub(crate) mod testutils; pub(crate) mod testutils;
#[cfg(feature = "backend-virgl")]
use std::fs::File; use std::fs::File;
#[cfg(feature = "backend-virgl")]
use std::os::fd::OwnedFd; use std::os::fd::OwnedFd;
use std::{ use std::{
fmt::{Display, Formatter}, fmt::{Display, Formatter},
@ -31,22 +29,26 @@ use std::{
use bitflags::bitflags; use bitflags::bitflags;
use clap::ValueEnum; use clap::ValueEnum;
use log::info; use log::info;
use rutabaga_gfx::{
RUTABAGA_CAPSET_CROSS_DOMAIN, RUTABAGA_CAPSET_VENUS, RUTABAGA_CAPSET_VIRGL,
RUTABAGA_CAPSET_VIRGL2,
};
#[cfg(feature = "backend-gfxstream")] #[cfg(feature = "backend-gfxstream")]
use rutabaga_gfx::{RUTABAGA_CAPSET_GFXSTREAM_GLES, RUTABAGA_CAPSET_GFXSTREAM_VULKAN}; 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 thiserror::Error as ThisError;
use vhost_user_backend::VhostUserDaemon; use vhost_user_backend::VhostUserDaemon;
use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap}; use vm_memory::{GuestMemoryAtomic, GuestMemoryMmap};
use crate::device::VhostUserGpuBackend; use crate::device::VhostUserGpuBackend;
#[cfg(feature = "backend-virgl")]
pub const DEFAULT_VIRGLRENDER_CAPSET_MASK: GpuCapset = GpuCapset::ALL_VIRGLRENDERER_CAPSETS; pub const DEFAULT_VIRGLRENDER_CAPSET_MASK: GpuCapset = GpuCapset::ALL_VIRGLRENDERER_CAPSETS;
#[cfg(feature = "backend-gfxstream")] #[cfg(feature = "backend-gfxstream")]
pub const DEFAULT_GFXSTREAM_CAPSET_MASK: GpuCapset = GpuCapset::ALL_GFXSTREAM_CAPSETS; 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)] #[derive(Clone, Copy, Debug, PartialEq, Eq, ValueEnum)]
pub enum GpuMode { pub enum GpuMode {
#[value(name = "virglrenderer", alias("virgl-renderer"))] #[value(name = "virglrenderer", alias("virgl-renderer"))]
@ -54,6 +56,8 @@ pub enum GpuMode {
VirglRenderer, VirglRenderer,
#[cfg(feature = "backend-gfxstream")] #[cfg(feature = "backend-gfxstream")]
Gfxstream, Gfxstream,
#[value(name = "rutabaga")]
Rutabaga,
Null, Null,
} }
@ -64,6 +68,7 @@ impl Display for GpuMode {
Self::VirglRenderer => write!(f, "virglrenderer"), Self::VirglRenderer => write!(f, "virglrenderer"),
#[cfg(feature = "backend-gfxstream")] #[cfg(feature = "backend-gfxstream")]
Self::Gfxstream => write!(f, "gfxstream"), Self::Gfxstream => write!(f, "gfxstream"),
Self::Rutabaga => write!(f, "rutabaga"),
Self::Null => write!(f, "null"), Self::Null => write!(f, "null"),
} }
} }
@ -73,13 +78,10 @@ bitflags! {
/// A bitmask for representing supported gpu capability sets. /// A bitmask for representing supported gpu capability sets.
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct GpuCapset: u64 { pub struct GpuCapset: u64 {
#[cfg(feature = "backend-virgl")]
const VIRGL = 1 << RUTABAGA_CAPSET_VIRGL as u64; const VIRGL = 1 << RUTABAGA_CAPSET_VIRGL as u64;
#[cfg(feature = "backend-virgl")]
const VIRGL2 = 1 << RUTABAGA_CAPSET_VIRGL2 as u64; const VIRGL2 = 1 << RUTABAGA_CAPSET_VIRGL2 as u64;
#[cfg(feature = "backend-virgl")]
const VENUS = 1 << RUTABAGA_CAPSET_VENUS as u64; 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(); const ALL_VIRGLRENDERER_CAPSETS = Self::VIRGL.bits() | Self::VIRGL2.bits() | Self::VENUS.bits();
#[cfg(feature = "backend-gfxstream")] #[cfg(feature = "backend-gfxstream")]
@ -106,12 +108,10 @@ impl Display for GpuCapset {
first = false; first = false;
match capset { 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-virgl")]
Self::VENUS => write!(f, "venus")?, Self::VENUS => write!(f, "venus")?,
Self::CROSS_DOMAIN => write!(f, "cross-domain")?,
#[cfg(feature = "backend-gfxstream")] #[cfg(feature = "backend-gfxstream")]
Self::GFXSTREAM_VULKAN => write!(f, "gfxstream-vulkan")?, Self::GFXSTREAM_VULKAN => write!(f, "gfxstream-vulkan")?,
#[cfg(feature = "backend-gfxstream")] #[cfg(feature = "backend-gfxstream")]
@ -137,8 +137,8 @@ pub struct GpuConfig {
gpu_mode: GpuMode, gpu_mode: GpuMode,
capset: GpuCapset, capset: GpuCapset,
flags: GpuFlags, flags: GpuFlags,
#[cfg(feature = "backend-virgl")]
render_server_fd: Option<OwnedFd>, render_server_fd: Option<OwnedFd>,
wayland_socket: Option<PathBuf>,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
@ -146,8 +146,8 @@ pub struct GpuConfigBuilder {
gpu_mode: Option<GpuMode>, gpu_mode: Option<GpuMode>,
capset: Option<GpuCapset>, capset: Option<GpuCapset>,
flags: Option<GpuFlags>, flags: Option<GpuFlags>,
#[cfg(feature = "backend-virgl")]
gpu_device: Option<PathBuf>, gpu_device: Option<PathBuf>,
wayland_socket: Option<PathBuf>,
} }
impl GpuConfigBuilder { impl GpuConfigBuilder {
@ -166,18 +166,32 @@ impl GpuConfigBuilder {
self self
} }
#[cfg(feature = "backend-virgl")]
pub fn set_gpu_device(mut self, gpu_device: PathBuf) -> Self { pub fn set_gpu_device(mut self, gpu_device: PathBuf) -> Self {
self.gpu_device = Some(gpu_device); self.gpu_device = Some(gpu_device);
self 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> { fn validate_capset(gpu_mode: GpuMode, capset: GpuCapset) -> Result<(), GpuConfigError> {
let supported_capset_mask = match gpu_mode { let supported_capset_mask = match gpu_mode {
#[cfg(feature = "backend-virgl")] #[cfg(feature = "backend-virgl")]
GpuMode::VirglRenderer => GpuCapset::ALL_VIRGLRENDERER_CAPSETS, GpuMode::VirglRenderer => GpuCapset::ALL_VIRGLRENDERER_CAPSETS,
#[cfg(feature = "backend-gfxstream")] #[cfg(feature = "backend-gfxstream")]
GpuMode::Gfxstream => GpuCapset::ALL_GFXSTREAM_CAPSETS, GpuMode::Gfxstream => GpuCapset::ALL_GFXSTREAM_CAPSETS,
GpuMode::Rutabaga => GpuCapset::ALL_VIRGLRENDERER_CAPSETS | GpuCapset::CROSS_DOMAIN,
GpuMode::Null => GpuCapset::empty(), GpuMode::Null => GpuCapset::empty(),
}; };
for capset in capset.iter() { for capset in capset.iter() {
@ -197,6 +211,7 @@ impl GpuConfigBuilder {
GpuMode::VirglRenderer => DEFAULT_VIRGLRENDER_CAPSET_MASK, GpuMode::VirglRenderer => DEFAULT_VIRGLRENDER_CAPSET_MASK,
#[cfg(feature = "backend-gfxstream")] #[cfg(feature = "backend-gfxstream")]
GpuMode::Gfxstream => DEFAULT_GFXSTREAM_CAPSET_MASK, GpuMode::Gfxstream => DEFAULT_GFXSTREAM_CAPSET_MASK,
GpuMode::Rutabaga => DEFAULT_RUTABAGA_CAPSET_MASK,
GpuMode::Null => GpuCapset::empty(), GpuMode::Null => GpuCapset::empty(),
}); });
@ -207,12 +222,23 @@ impl GpuConfigBuilder {
return Err(GpuConfigError::GlesRequiredByGfxstream); return Err(GpuConfigError::GlesRequiredByGfxstream);
} }
#[cfg(feature = "backend-virgl")] if self.gpu_device.is_some() && !Self::gpu_mode_supports_gpu_device(gpu_mode) {
if self.gpu_device.is_some() && !matches!(gpu_mode, GpuMode::VirglRenderer) {
return Err(GpuConfigError::GpuDeviceNotSupportedByMode); 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 render_server_fd = if let Some(gpu_device) = self.gpu_device {
let fd = File::open(&gpu_device) let fd = File::open(&gpu_device)
.map(Into::into) .map(Into::into)
@ -226,8 +252,8 @@ impl GpuConfigBuilder {
gpu_mode, gpu_mode,
capset, capset,
flags, flags,
#[cfg(feature = "backend-virgl")]
render_server_fd, render_server_fd,
wayland_socket: self.wayland_socket,
}) })
} }
} }
@ -235,7 +261,6 @@ impl GpuConfigBuilder {
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub struct GpuFlags { pub struct GpuFlags {
pub use_egl: bool, pub use_egl: bool,
pub use_glx: bool,
pub use_gles: bool, pub use_gles: bool,
pub use_surfaceless: bool, pub use_surfaceless: bool,
pub headless: bool, pub headless: bool,
@ -246,7 +271,6 @@ impl GpuFlags {
pub const fn new_default() -> Self { pub const fn new_default() -> Self {
Self { Self {
use_egl: true, use_egl: true,
use_glx: false,
use_gles: true, use_gles: true,
use_surfaceless: true, use_surfaceless: true,
headless: false, headless: false,
@ -272,6 +296,12 @@ pub enum GpuConfigError {
GpuDeviceNotSupportedByMode, GpuDeviceNotSupportedByMode,
#[error("Failed to open GPU device '{0}': {1}")] #[error("Failed to open GPU device '{0}': {1}")]
InvalidGpuDevice(PathBuf, std::io::Error), 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 { impl GpuConfig {
@ -287,10 +317,13 @@ impl GpuConfig {
&self.flags &self.flags
} }
#[cfg(feature = "backend-virgl")]
pub fn render_server_fd(&self) -> Option<&OwnedFd> { pub fn render_server_fd(&self) -> Option<&OwnedFd> {
self.render_server_fd.as_ref() self.render_server_fd.as_ref()
} }
pub fn wayland_socket(&self) -> Option<&Path> {
self.wayland_socket.as_deref()
}
} }
#[derive(Debug, ThisError)] #[derive(Debug, ThisError)]
@ -322,16 +355,13 @@ pub fn start_backend(socket_path: &Path, config: GpuConfig) -> Result<(), StartE
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::path::Path; use std::path::{Path, PathBuf};
use assert_matches::assert_matches; use assert_matches::assert_matches;
use super::*; use super::*;
#[cfg(feature = "backend-virgl")]
fn assert_gpu_device_fails_for_mode(mode: GpuMode) { fn assert_gpu_device_fails_for_mode(mode: GpuMode) {
use std::path::PathBuf;
let result = GpuConfigBuilder::default() let result = GpuConfigBuilder::default()
.set_gpu_mode(mode) .set_gpu_mode(mode)
.set_gpu_device(PathBuf::from("/dev/dri/renderD128")) .set_gpu_device(PathBuf::from("/dev/dri/renderD128"))
@ -425,8 +455,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(feature = "backend-virgl")] fn test_gpu_device_only_with_supported_modes() {
fn test_gpu_device_only_with_virglrenderer() {
assert_gpu_device_fails_for_mode(GpuMode::Null); assert_gpu_device_fails_for_mode(GpuMode::Null);
#[cfg(feature = "backend-gfxstream")] #[cfg(feature = "backend-gfxstream")]
@ -435,14 +464,13 @@ mod tests {
#[test] #[test]
fn test_default_num_capsets() { fn test_default_num_capsets() {
#[cfg(feature = "backend-virgl")]
assert_eq!(DEFAULT_VIRGLRENDER_CAPSET_MASK.num_capsets(), 3); assert_eq!(DEFAULT_VIRGLRENDER_CAPSET_MASK.num_capsets(), 3);
#[cfg(feature = "backend-gfxstream")] #[cfg(feature = "backend-gfxstream")]
assert_eq!(DEFAULT_GFXSTREAM_CAPSET_MASK.num_capsets(), 2); assert_eq!(DEFAULT_GFXSTREAM_CAPSET_MASK.num_capsets(), 2);
assert_eq!(DEFAULT_RUTABAGA_CAPSET_MASK.num_capsets(), 2);
} }
#[test] #[test]
#[cfg(feature = "backend-virgl")]
fn test_capset_display_multiple() { fn test_capset_display_multiple() {
let capset = GpuCapset::VIRGL | GpuCapset::VIRGL2; let capset = GpuCapset::VIRGL | GpuCapset::VIRGL2;
let output = capset.to_string(); let output = capset.to_string();
@ -475,4 +503,76 @@ mod tests {
StartError::ServeFailed(_) 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));
}
} }

View file

@ -14,22 +14,19 @@ use vhost_device_gpu::{
#[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)] #[derive(ValueEnum, Debug, Copy, Clone, Eq, PartialEq)]
#[repr(u64)] #[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 { pub enum CapsetName {
/// [virglrenderer] OpenGL implementation, superseded by Virgl2 /// [virglrenderer] OpenGL implementation, superseded by Virgl2
#[cfg(feature = "backend-virgl")]
Virgl = GpuCapset::VIRGL.bits(), Virgl = GpuCapset::VIRGL.bits(),
/// [virglrenderer] OpenGL implementation /// [virglrenderer] OpenGL implementation
#[cfg(feature = "backend-virgl")]
Virgl2 = GpuCapset::VIRGL2.bits(), Virgl2 = GpuCapset::VIRGL2.bits(),
/// [virglrenderer] Venus (Vulkan) implementation /// [virglrenderer] Venus (Vulkan) implementation
#[cfg(feature = "backend-virgl")]
Venus = GpuCapset::VENUS.bits(), Venus = GpuCapset::VENUS.bits(),
/// Cross-domain support for Wayland forwarding
CrossDomain = GpuCapset::CROSS_DOMAIN.bits(),
/// [gfxstream] Vulkan implementation (partial support only){n} /// [gfxstream] Vulkan implementation (partial support only){n}
/// NOTE: Can only be used for 2D display output for now, there is no /// NOTE: Can only be used for 2D display output for now, there is no
/// hardware acceleration yet /// hardware acceleration yet
@ -41,20 +38,10 @@ pub enum CapsetName {
/// hardware acceleration yet /// hardware acceleration yet
#[cfg(feature = "backend-gfxstream")] #[cfg(feature = "backend-gfxstream")]
GfxstreamGles = GpuCapset::GFXSTREAM_GLES.bits(), 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<CapsetName> for GpuCapset { impl From<CapsetName> for GpuCapset {
fn from(capset_name: CapsetName) -> GpuCapset { fn from(capset_name: CapsetName) -> GpuCapset {
if matches!(capset_name, CapsetName::__Null) {
return GpuCapset::empty();
}
GpuCapset::from_bits(capset_name as u64) GpuCapset::from_bits(capset_name as u64)
.expect("Internal error: CapsetName enum is incorrectly defined") .expect("Internal error: CapsetName enum is incorrectly defined")
} }
@ -82,6 +69,10 @@ pub struct GpuArgs {
#[clap(short, long, value_delimiter = ',')] #[clap(short, long, value_delimiter = ',')]
pub capset: Option<Vec<CapsetName>>, pub capset: Option<Vec<CapsetName>>,
/// Wayland socket path for cross-domain (e.g., /run/user/1000/wayland-0)
#[clap(long, value_name = "PATH")]
pub wayland_socket: Option<PathBuf>,
#[clap(flatten)] #[clap(flatten)]
pub flags: GpuFlagsArgs, pub flags: GpuFlagsArgs,
} }
@ -97,14 +88,6 @@ pub struct GpuFlagsArgs {
)] )]
pub use_egl: bool, 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 /// Enable backend to use GLES
#[clap( #[clap(
long, long,
@ -127,7 +110,6 @@ pub struct GpuFlagsArgs {
/// GPU device path (e.g., /dev/dri/renderD128) /// GPU device path (e.g., /dev/dri/renderD128)
#[clap(long, value_name = "PATH")] #[clap(long, value_name = "PATH")]
#[cfg(feature = "backend-virgl")]
pub gpu_device: Option<PathBuf>, pub gpu_device: Option<PathBuf>,
} }
@ -135,7 +117,6 @@ impl From<GpuFlagsArgs> for GpuFlags {
fn from(args: GpuFlagsArgs) -> Self { fn from(args: GpuFlagsArgs) -> Self {
GpuFlags { GpuFlags {
use_egl: args.use_egl, use_egl: args.use_egl,
use_glx: args.use_glx,
use_gles: args.use_gles, use_gles: args.use_gles,
use_surfaceless: args.use_surfaceless, use_surfaceless: args.use_surfaceless,
headless: args.headless, headless: args.headless,
@ -144,7 +125,6 @@ impl From<GpuFlagsArgs> for GpuFlags {
} }
pub fn config_from_args(args: GpuArgs) -> Result<(PathBuf, GpuConfig), GpuConfigError> { pub fn config_from_args(args: GpuArgs) -> Result<(PathBuf, GpuConfig), GpuConfigError> {
#[cfg(feature = "backend-virgl")]
let gpu_device = args.flags.gpu_device.clone(); let gpu_device = args.flags.gpu_device.clone();
let flags = GpuFlags::from(args.flags); 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); builder = builder.set_capset(capset);
} }
#[cfg(feature = "backend-virgl")]
if let Some(gpu_device) = gpu_device { if let Some(gpu_device) = gpu_device {
builder = builder.set_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()?; let config = builder.build()?;
Ok((args.socket_path, config)) Ok((args.socket_path, config))
} }
@ -222,19 +205,19 @@ mod tests {
} }
#[test] #[test]
#[cfg(feature = "backend-virgl")]
fn test_config_from_args() { fn test_config_from_args() {
let expected_path = Path::new("/some/test/path"); let expected_path = Path::new("/some/test/path");
let args = GpuArgs { let args = GpuArgs {
socket_path: expected_path.into(), socket_path: expected_path.into(),
gpu_mode: GpuMode::VirglRenderer, gpu_mode: GpuMode::VirglRenderer,
capset: Some(vec![CapsetName::Virgl, CapsetName::Virgl2]), capset: Some(vec![CapsetName::Virgl, CapsetName::Virgl2]),
wayland_socket: None,
flags: GpuFlagsArgs { flags: GpuFlagsArgs {
use_egl: false, use_egl: false,
use_glx: true,
use_gles: false, use_gles: false,
use_surfaceless: false, use_surfaceless: false,
headless: false, headless: false,
#[cfg(feature = "backend-virgl")]
gpu_device: None, gpu_device: None,
}, },
}; };
@ -246,7 +229,6 @@ mod tests {
*config.flags(), *config.flags(),
GpuFlags { GpuFlags {
use_egl: false, use_egl: false,
use_glx: true,
use_gles: false, use_gles: false,
use_surfaceless: false, use_surfaceless: false,
headless: false, headless: false,
@ -256,16 +238,15 @@ mod tests {
assert_eq!(config.capsets(), GpuCapset::VIRGL | GpuCapset::VIRGL2); assert_eq!(config.capsets(), GpuCapset::VIRGL | GpuCapset::VIRGL2);
// Test with invalid GPU device // Test with invalid GPU device
#[cfg(feature = "backend-virgl")]
{ {
let invalid_gpu_device = Path::new("/nonexistent/gpu/device"); let invalid_gpu_device = Path::new("/nonexistent/gpu/device");
let invalid_args = GpuArgs { let invalid_args = GpuArgs {
socket_path: expected_path.into(), socket_path: expected_path.into(),
gpu_mode: GpuMode::VirglRenderer, gpu_mode: GpuMode::VirglRenderer,
capset: Some(vec![CapsetName::Virgl]), capset: Some(vec![CapsetName::Virgl]),
wayland_socket: None,
flags: GpuFlagsArgs { flags: GpuFlagsArgs {
use_egl: true, use_egl: true,
use_glx: false,
use_gles: true, use_gles: true,
use_surfaceless: true, use_surfaceless: true,
headless: false, headless: false,