From 44292f58f3e49f10184bf7c444edf5f57a63afa4 Mon Sep 17 00:00:00 2001 From: Chirantan Ekbote Date: Fri, 25 Jun 2021 18:31:41 +0900 Subject: [PATCH] Support out-of-tree vhost-user-gpu BUG=b:179755651 TEST=`glxinfo -B | grep virgl` inside a crostini vm Change-Id: I37b98fcccfb8e56af4d07a2afe828ca3b5087b1f Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crosvm/+/2987595 Tested-by: kokoro Reviewed-by: Gurchetan Singh Reviewed-by: Keiichi Watanabe Commit-Queue: Chirantan Ekbote --- devices/src/virtio/vhost/user/vmm/gpu.rs | 218 +++++++++++++++++++ devices/src/virtio/vhost/user/vmm/handler.rs | 27 ++- devices/src/virtio/vhost/user/vmm/mod.rs | 10 + src/crosvm.rs | 2 + src/error.rs | 4 + src/linux.rs | 58 ++++- src/main.rs | 4 + 7 files changed, 318 insertions(+), 5 deletions(-) create mode 100644 devices/src/virtio/vhost/user/vmm/gpu.rs diff --git a/devices/src/virtio/vhost/user/vmm/gpu.rs b/devices/src/virtio/vhost/user/vmm/gpu.rs new file mode 100644 index 0000000000..393c7a8ad8 --- /dev/null +++ b/devices/src/virtio/vhost/user/vmm/gpu.rs @@ -0,0 +1,218 @@ +// Copyright 2021 The Chromium OS Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +use std::{cell::RefCell, path::Path, thread}; + +use base::{error, Event, RawDescriptor, Tube}; +use cros_async::Executor; +use vm_memory::GuestMemory; +use vmm_vhost::vhost_user::message::{VhostUserProtocolFeatures, VhostUserVirtioFeatures}; + +use crate::{ + pci::{PciBarConfiguration, PciCapability}, + virtio::{ + gpu::QUEUE_SIZES, + vhost::user::vmm::{worker::Worker, Result, VhostUserHandler}, + virtio_gpu_config, Interrupt, PciCapabilityType, Queue, VirtioDevice, VirtioPciShmCap, + GPU_BAR_NUM, GPU_BAR_OFFSET, GPU_BAR_SIZE, TYPE_GPU, VIRTIO_GPU_F_CONTEXT_INIT, + VIRTIO_GPU_F_CREATE_GUEST_HANDLE, VIRTIO_GPU_F_RESOURCE_BLOB, VIRTIO_GPU_F_RESOURCE_SYNC, + VIRTIO_GPU_F_RESOURCE_UUID, VIRTIO_GPU_F_VIRGL, VIRTIO_GPU_SHM_ID_HOST_VISIBLE, + }, + PciAddress, +}; + +pub struct Gpu { + kill_evt: Option, + worker_thread: Option>, + handler: RefCell, + host_tube: Tube, + queue_sizes: Vec, +} + +impl Gpu { + pub fn new>( + base_features: u64, + socket_path: P, + host_tube: Tube, + device_tube: Tube, + ) -> Result { + let default_queue_size = QUEUE_SIZES.len(); + + let allow_features = 1u64 << crate::virtio::VIRTIO_F_VERSION_1 + | 1 << VIRTIO_GPU_F_VIRGL + | 1 << VIRTIO_GPU_F_RESOURCE_UUID + | 1 << VIRTIO_GPU_F_RESOURCE_BLOB + | 1 << VIRTIO_GPU_F_CONTEXT_INIT + | 1 << VIRTIO_GPU_F_RESOURCE_SYNC + | 1 << VIRTIO_GPU_F_CREATE_GUEST_HANDLE + | VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits(); + + let init_features = base_features | VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits(); + let allow_protocol_features = + VhostUserProtocolFeatures::CONFIG | VhostUserProtocolFeatures::SLAVE_REQ; + + let mut handler = VhostUserHandler::new_from_path( + socket_path, + default_queue_size as u64, + allow_features, + init_features, + allow_protocol_features, + )?; + handler.set_device_request_channel(device_tube)?; + + Ok(Gpu { + kill_evt: None, + worker_thread: None, + handler: RefCell::new(handler), + host_tube, + queue_sizes: QUEUE_SIZES[..].to_vec(), + }) + } +} + +impl VirtioDevice for Gpu { + fn keep_rds(&self) -> Vec { + Vec::new() + } + + fn device_type(&self) -> u32 { + TYPE_GPU + } + + fn queue_max_sizes(&self) -> &[u16] { + &self.queue_sizes + } + + fn features(&self) -> u64 { + self.handler.borrow().avail_features + } + + fn ack_features(&mut self, features: u64) { + if let Err(e) = self.handler.borrow_mut().ack_features(features) { + error!("failed to enable features 0x{:x}: {}", features, e); + } + } + + fn read_config(&self, offset: u64, data: &mut [u8]) { + if let Err(e) = self + .handler + .borrow_mut() + .read_config::(offset, data) + { + error!("failed to read gpu config: {}", e); + } + } + + fn write_config(&mut self, offset: u64, data: &[u8]) { + if let Err(e) = self + .handler + .borrow_mut() + .write_config::(offset, data) + { + error!("failed to write gpu config: {}", e); + } + } + + fn activate( + &mut self, + mem: GuestMemory, + interrupt: Interrupt, + queues: Vec, + queue_evts: Vec, + ) { + if let Err(e) = self + .handler + .borrow_mut() + .activate(&mem, &interrupt, &queues, &queue_evts) + { + error!("failed to activate queues: {}", e); + return; + } + + let (self_kill_evt, kill_evt) = match Event::new().and_then(|e| Ok((e.try_clone()?, e))) { + Ok(v) => v, + Err(e) => { + error!("failed creating kill Event pair: {}", e); + return; + } + }; + self.kill_evt = Some(self_kill_evt); + + let worker_result = thread::Builder::new() + .name("vhost_user_gpu".to_string()) + .spawn(move || { + let ex = Executor::new().expect("failed to create an executor"); + let mut worker = Worker { + queues, + mem, + kill_evt, + }; + + if let Err(e) = worker.run(&ex, interrupt) { + error!("failed to start a worker: {}", e); + } + worker + }); + + match worker_result { + Err(e) => { + error!("failed to spawn vhost_user_gpu worker: {}", e); + } + Ok(join_handle) => { + self.worker_thread = Some(join_handle); + } + } + } + + fn get_device_bars(&mut self, address: PciAddress) -> Vec { + if let Err(e) = self.host_tube.send(&address) { + error!("failed to send `PciAddress` to gpu device: {}", e); + return Vec::new(); + } + + match self.host_tube.recv() { + Ok(cfg) => cfg, + Err(e) => { + error!( + "failed to receive `PciBarConfiguration` from gpu device: {}", + e + ); + Vec::new() + } + } + } + + fn get_device_caps(&self) -> Vec> { + vec![Box::new(VirtioPciShmCap::new( + PciCapabilityType::SharedMemoryConfig, + GPU_BAR_NUM, + GPU_BAR_OFFSET, + GPU_BAR_SIZE, + VIRTIO_GPU_SHM_ID_HOST_VISIBLE, + ))] + } + + fn reset(&mut self) -> bool { + if let Err(e) = self.handler.borrow_mut().reset(self.queue_sizes.len()) { + error!("Failed to reset gpu device: {}", e); + false + } else { + true + } + } +} + +impl Drop for Gpu { + fn drop(&mut self) { + if let Some(kill_evt) = self.kill_evt.take() { + if let Some(worker_thread) = self.worker_thread.take() { + if let Err(e) = kill_evt.write(1) { + error!("failed to write to kill_evt: {}", e); + return; + } + let _ = worker_thread.join(); + } + } + } +} diff --git a/devices/src/virtio/vhost/user/vmm/handler.rs b/devices/src/virtio/vhost/user/vmm/handler.rs index 939dfd6b05..ba39d4c86e 100644 --- a/devices/src/virtio/vhost/user/vmm/handler.rs +++ b/devices/src/virtio/vhost/user/vmm/handler.rs @@ -6,7 +6,7 @@ use std::io::Write; use std::os::unix::net::UnixStream; use std::path::Path; -use base::{AsRawDescriptor, Event}; +use base::{AsRawDescriptor, Event, Tube}; use vm_memory::GuestMemory; use vmm_vhost::vhost_user::message::{ VhostUserConfigFlags, VhostUserProtocolFeatures, VhostUserVirtioFeatures, @@ -144,6 +144,31 @@ impl VhostUserHandler { .map_err(Error::CopyConfig) } + /// Writes `data` into the device configuration space at `offset`. + pub fn write_config(&mut self, offset: u64, data: &[u8]) -> Result<()> { + let config_len = std::mem::size_of::() as u64; + let data_len = data.len() as u64; + offset + .checked_add(data_len) + .and_then(|l| if l <= config_len { Some(()) } else { None }) + .ok_or(Error::InvalidConfigOffset { + data_len, + offset, + config_len, + })?; + + self.vu + .set_config(offset as u32, VhostUserConfigFlags::empty(), data) + .map_err(Error::SetConfig) + } + + /// Sets the channel for device-specific messages. + pub fn set_device_request_channel(&mut self, channel: Tube) -> Result<()> { + self.vu + .set_slave_request_fd(&channel.as_raw_descriptor()) + .map_err(Error::SetDeviceRequestChannel) + } + /// Sets the memory map regions so it can translate the vring addresses. pub fn set_mem_table(&mut self, mem: &GuestMemory) -> Result<()> { let mut regions: Vec = Vec::new(); diff --git a/devices/src/virtio/vhost/user/vmm/mod.rs b/devices/src/virtio/vhost/user/vmm/mod.rs index 03d28e76a0..fd495ea269 100644 --- a/devices/src/virtio/vhost/user/vmm/mod.rs +++ b/devices/src/virtio/vhost/user/vmm/mod.rs @@ -5,6 +5,8 @@ mod block; mod console; mod fs; +#[cfg(feature = "gpu")] +mod gpu; mod handler; mod mac80211_hwsim; mod net; @@ -14,6 +16,8 @@ mod worker; pub use self::block::*; pub use self::console::*; pub use self::fs::*; +#[cfg(feature = "gpu")] +pub use self::gpu::*; pub use self::handler::VhostUserHandler; pub use self::mac80211_hwsim::*; pub use self::net::*; @@ -67,6 +71,12 @@ pub enum Error { /// Failed to reset owner. #[error("failed to reset owner: {0}")] ResetOwner(VhostError), + /// Failed to set config. + #[error("failed to set config: {0}")] + SetConfig(VhostError), + /// Failed to set device request channel. + #[error("failed to set device request channel: {0}")] + SetDeviceRequestChannel(VhostError), /// Failed to set features. #[error("failed to set features: {0}")] SetFeatures(VhostError), diff --git a/src/crosvm.rs b/src/crosvm.rs index 10639dac54..b3bdde54ff 100644 --- a/src/crosvm.rs +++ b/src/crosvm.rs @@ -274,6 +274,7 @@ pub struct Config { pub vhost_user_blk: Vec, pub vhost_user_console: Vec, pub vhost_user_fs: Vec, + pub vhost_user_gpu: Vec, pub vhost_user_mac80211_hwsim: Option, pub vhost_user_net: Vec, pub vhost_user_wl: Vec, @@ -360,6 +361,7 @@ impl Default for Config { balloon_bias: 0, vhost_user_blk: Vec::new(), vhost_user_console: Vec::new(), + vhost_user_gpu: Vec::new(), vhost_user_fs: Vec::new(), vhost_user_mac80211_hwsim: None, vhost_user_net: Vec::new(), diff --git a/src/error.rs b/src/error.rs index 99efc1f5cb..93b8c0097d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -41,6 +41,7 @@ pub enum Error { BuildVm(::Error), ChownTpmStorage(base::Error), CloneEvent(base::Error), + CloneTube(base::TubeError), CloneVcpu(base::Error), ConfigureHotPlugDevice(::Error), ConfigureVcpu(::Error), @@ -143,6 +144,7 @@ pub enum Error { VhostUserBlockDeviceNew(VhostUserVmmError), VhostUserConsoleDeviceNew(VhostUserVmmError), VhostUserFsDeviceNew(VhostUserVmmError), + VhostUserGpuDeviceNew(VhostUserVmmError), VhostUserMac80211HwsimNew(VhostUserVmmError), VhostUserNetDeviceNew(VhostUserVmmError), VhostUserNetWithNetArgs, @@ -175,6 +177,7 @@ impl Display for Error { BuildVm(e) => write!(f, "The architecture failed to build the vm: {}", e), ChownTpmStorage(e) => write!(f, "failed to chown tpm storage: {}", e), CloneEvent(e) => write!(f, "failed to clone event: {}", e), + CloneTube(e) => write!(f, "failed to clone tube: {}", e), CloneVcpu(e) => write!(f, "failed to clone vcpu: {}", e), ConfigureHotPlugDevice(e) => write!(f, "Failed to configure pci hotplug device:{}", e), ConfigureVcpu(e) => write!(f, "failed to configure vcpu: {}", e), @@ -294,6 +297,7 @@ impl Display for Error { write!(f, "failed to set up vhost-user console device: {}", e) } VhostUserFsDeviceNew(e) => write!(f, "failed to set up vhost-user fs device: {}", e), + VhostUserGpuDeviceNew(e) => write!(f, "failed to set up vhost-user gpu device: {}", e), VhostUserMac80211HwsimNew(e) => { write!(f, "failed to set up vhost-user mac80211_hwsim device {}", e) } diff --git a/src/linux.rs b/src/linux.rs index 4c6e457a14..73ae11a838 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -30,17 +30,19 @@ use base::net::{UnixSeqpacket, UnixSeqpacketListener, UnlinkUnixSeqpacketListene use base::*; use devices::serial_device::{SerialHardware, SerialParameters}; use devices::vfio::{VfioCommonSetup, VfioCommonTrait}; -#[cfg(feature = "gpu")] -use devices::virtio::gpu::{DEFAULT_DISPLAY_HEIGHT, DEFAULT_DISPLAY_WIDTH}; #[cfg(feature = "audio_cras")] use devices::virtio::snd::cras_backend::Parameters as CrasSndParameters; use devices::virtio::vhost::user::vmm::{ Block as VhostUserBlock, Console as VhostUserConsole, Fs as VhostUserFs, Mac80211Hwsim as VhostUserMac80211Hwsim, Net as VhostUserNet, Wl as VhostUserWl, }; -#[cfg(feature = "gpu")] -use devices::virtio::EventDevice; use devices::virtio::{self, Console, VirtioDevice}; +#[cfg(feature = "gpu")] +use devices::virtio::{ + gpu::{DEFAULT_DISPLAY_HEIGHT, DEFAULT_DISPLAY_WIDTH}, + vhost::user::vmm::Gpu as VhostUserGpu, + EventDevice, +}; #[cfg(feature = "audio")] use devices::Ac97Dev; use devices::ProtectionType; @@ -655,6 +657,30 @@ fn create_vhost_user_wl_device(cfg: &Config, opt: &VhostUserWlOption) -> DeviceR }) } +#[cfg(feature = "gpu")] +fn create_vhost_user_gpu_device( + cfg: &Config, + opt: &VhostUserOption, + host_tube: Tube, + device_tube: Tube, +) -> DeviceResult { + // The crosvm gpu device expects us to connect the tube before it will accept a vhost-user + // connection. + let dev = VhostUserGpu::new( + virtio::base_features(cfg.protected_vm), + &opt.socket, + host_tube, + device_tube, + ) + .map_err(Error::VhostUserGpuDeviceNew)?; + + Ok(VirtioDeviceStub { + dev: Box::new(dev), + // no sandbox here because virtqueue handling is exported to a different process. + jail: None, + }) +} + #[cfg(feature = "gpu")] fn create_gpu_device( cfg: &Config, @@ -1210,6 +1236,7 @@ fn create_virtio_devices( _exit_evt: &Event, wayland_device_tube: Tube, gpu_device_tube: Tube, + vhost_user_gpu_tubes: Vec<(Tube, Tube)>, balloon_device_tube: Tube, disk_device_tubes: &mut Vec, pmem_device_tubes: &mut Vec, @@ -1328,6 +1355,16 @@ fn create_virtio_devices( devs.push(create_vhost_user_wl_device(cfg, opt)?); } + #[cfg(feature = "gpu")] + for (opt, (host_tube, device_tube)) in cfg.vhost_user_gpu.iter().zip(vhost_user_gpu_tubes) { + devs.push(create_vhost_user_gpu_device( + cfg, + opt, + host_tube, + device_tube, + )?); + } + #[cfg_attr(not(feature = "gpu"), allow(unused_mut))] let mut resource_bridges = Vec::::new(); @@ -1563,6 +1600,7 @@ fn create_devices( control_tubes: &mut Vec, wayland_device_tube: Tube, gpu_device_tube: Tube, + vhost_user_gpu_tubes: Vec<(Tube, Tube)>, balloon_device_tube: Tube, disk_device_tubes: &mut Vec, pmem_device_tubes: &mut Vec, @@ -1577,6 +1615,7 @@ fn create_devices( exit_evt, wayland_device_tube, gpu_device_tube, + vhost_user_gpu_tubes, balloon_device_tube, disk_device_tubes, pmem_device_tubes, @@ -2362,6 +2401,16 @@ where control_tubes.push(TaggedControlTube::VmMemory(wayland_host_tube)); } + let mut vhost_user_gpu_tubes = Vec::with_capacity(cfg.vhost_user_gpu.len()); + for _ in 0..cfg.vhost_user_gpu.len() { + let (host_tube, device_tube) = Tube::pair().map_err(Error::CreateTube)?; + vhost_user_gpu_tubes.push(( + host_tube.try_clone().map_err(Error::CloneTube)?, + device_tube, + )); + control_tubes.push(TaggedControlTube::VmMemory(host_tube)); + } + let (wayland_host_tube, wayland_device_tube) = Tube::pair().map_err(Error::CreateTube)?; control_tubes.push(TaggedControlTube::VmMemory(wayland_host_tube)); // Balloon gets a special socket so balloon requests can be forwarded from the main process. @@ -2463,6 +2512,7 @@ where &mut control_tubes, wayland_device_tube, gpu_device_tube, + vhost_user_gpu_tubes, balloon_device_tube, &mut disk_device_tubes, &mut pmem_device_tubes, diff --git a/src/main.rs b/src/main.rs index 7a1dba1f3c..e2344004a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1874,6 +1874,9 @@ fn set_argument(cfg: &mut Config, name: &str, value: Option<&str>) -> argument:: "vhost-user-console" => cfg.vhost_user_console.push(VhostUserOption { socket: PathBuf::from(value.unwrap()), }), + "vhost-user-gpu" => cfg.vhost_user_gpu.push(VhostUserOption { + socket: PathBuf::from(value.unwrap()), + }), "vhost-user-mac80211-hwsim" => { cfg.vhost_user_mac80211_hwsim = Some(VhostUserOption { socket: PathBuf::from(value.unwrap()), @@ -2228,6 +2231,7 @@ iommu=on|off - indicates whether to enable virtio IOMMU for this device"), Argument::value("balloon_bias_mib", "N", "Amount to bias balance of memory between host and guest as the balloon inflates, in MiB."), Argument::value("vhost-user-blk", "SOCKET_PATH", "Path to a socket for vhost-user block"), Argument::value("vhost-user-console", "SOCKET_PATH", "Path to a socket for vhost-user console"), + Argument::value("vhost-user-gpu", "SOCKET_PATH", "Paths to a vhost-user socket for gpu"), Argument::value("vhost-user-mac80211-hwsim", "SOCKET_PATH", "Path to a socket for vhost-user mac80211_hwsim"), Argument::value("vhost-user-net", "SOCKET_PATH", "Path to a socket for vhost-user net"), Argument::value("vhost-user-wl", "SOCKET_PATH:TUBE_PATH", "Paths to a vhost-user socket for wayland and a Tube socket for additional wayland-specific messages"),