diff options
Diffstat (limited to 'pkgs/applications/virtualization/cloud-hypervisor/0002-virtio-devices-add-a-GPU-device.patch')
-rw-r--r-- | pkgs/applications/virtualization/cloud-hypervisor/0002-virtio-devices-add-a-GPU-device.patch | 1344 |
1 files changed, 1344 insertions, 0 deletions
diff --git a/pkgs/applications/virtualization/cloud-hypervisor/0002-virtio-devices-add-a-GPU-device.patch b/pkgs/applications/virtualization/cloud-hypervisor/0002-virtio-devices-add-a-GPU-device.patch new file mode 100644 index 00000000000..c23caa94a67 --- /dev/null +++ b/pkgs/applications/virtualization/cloud-hypervisor/0002-virtio-devices-add-a-GPU-device.patch @@ -0,0 +1,1344 @@ +From 285370872d43dbc17ed017dbf67b1981df5c4b3b Mon Sep 17 00:00:00 2001 +From: Alyssa Ross <alyssa.ross@unikie.com> +Date: Wed, 7 Sep 2022 14:16:29 +0000 +Subject: [PATCH 2/2] virtio-devices: add a GPU device + +This adds support for exposing a virtio-gpu device to guest by +implementing a vhost-user frontend compatible with crosvm's GPU device +backend. + +Note that this is not the same as the "vhost-user-gpu" protocol +implemented by QEMU. + +Adding a GPU device from the command line looks like this: + + --gpu socket=/path/to/crosvm-gpu-vhost-user.sock + +Signed-off-by: Alyssa Ross <alyssa.ross@unikie.com> +Co-authored-by: Alyssa Ross <hi@alyssa.is> +Signed-off-by: Alyssa Ross <hi@alyssa.is> +--- + Cargo.lock | 1 + + src/main.rs | 12 + + virtio-devices/src/device.rs | 8 +- + virtio-devices/src/lib.rs | 4 +- + virtio-devices/src/seccomp_filters.rs | 16 + + virtio-devices/src/transport/pci_device.rs | 4 +- + virtio-devices/src/vhost_user/gpu.rs | 406 +++++++++++++++++++++ + virtio-devices/src/vhost_user/mod.rs | 2 + + vmm/Cargo.toml | 1 + + vmm/src/api/mod.rs | 10 +- + vmm/src/config.rs | 111 ++++++ + vmm/src/device_manager.rs | 140 ++++++- + vmm/src/lib.rs | 88 ++++- + vmm/src/vm.rs | 28 +- + vmm/src/vm_config.rs | 27 ++ + 15 files changed, 841 insertions(+), 17 deletions(-) + create mode 100644 virtio-devices/src/vhost_user/gpu.rs + +diff --git a/Cargo.lock b/Cargo.lock +index d591569b..e1a1ad02 100644 +--- a/Cargo.lock ++++ b/Cargo.lock +@@ -2370,6 +2370,7 @@ dependencies = [ + "versionize_derive", + "vfio-ioctls", + "vfio_user", ++ "virtio-bindings", + "virtio-devices", + "virtio-queue", + "vm-allocator", +diff --git a/src/main.rs b/src/main.rs +index 9521b7af..0dc315b7 100644 +--- a/src/main.rs ++++ b/src/main.rs +@@ -199,6 +199,10 @@ pub struct TopLevel { + /// tag=<tag_name>, socket=<socket_path>, num_queues=<number_of_queues>, queue_size=<size_of_each_queue>, id=<device_id>, pci_segment=<segment_id> + fs: Vec<String>, + ++ #[argh(option, long = "gpu")] ++ /// socket=<socket_path>,cache_size=<default 8GiB>,id=<device_id>,pci_segment=<segment_id> ++ gpu: Vec<String>, ++ + #[argh(option, long = "pmem")] + /// file=<backing_file_path>, size=<persistent_memory_size>, iommu=on|off, discard_writes=on|off, id=<device_id>, pci_segment=<segment_id> + pmem: Vec<String>, +@@ -333,6 +337,12 @@ impl TopLevel { + None + }; + ++ let gpu = if !self.gpu.is_empty() { ++ Some(self.gpu.iter().map(|x| x.as_str()).collect()) ++ } else { ++ None ++ }; ++ + let pmem = if !self.pmem.is_empty() { + Some(self.pmem.iter().map(|x| x.as_str()).collect()) + } else { +@@ -389,6 +399,7 @@ impl TopLevel { + rng, + balloon, + fs, ++ gpu, + pmem, + serial, + console, +@@ -803,6 +814,7 @@ mod unit_tests { + }, + balloon: None, + fs: None, ++ gpu: None, + pmem: None, + serial: ConsoleConfig { + file: None, +diff --git a/virtio-devices/src/device.rs b/virtio-devices/src/device.rs +index b70092f8..e091ddd6 100644 +--- a/virtio-devices/src/device.rs ++++ b/virtio-devices/src/device.rs +@@ -11,7 +11,7 @@ use crate::{ + VIRTIO_F_RING_INDIRECT_DESC, + }; + use libc::EFD_NONBLOCK; +-use std::collections::HashMap; ++use std::collections::{BTreeMap, HashMap}; + use std::io::Write; + use std::num::Wrapping; + use std::sync::{ +@@ -47,19 +47,19 @@ pub struct UserspaceMapping { + pub mergeable: bool, + } + +-#[derive(Clone)] ++#[derive(Clone, Debug)] + pub struct VirtioSharedMemory { + pub offset: u64, + pub len: u64, + } + +-#[derive(Clone)] ++#[derive(Clone, Debug)] + pub struct VirtioSharedMemoryList { + pub host_addr: u64, + pub mem_slot: u32, + pub addr: GuestAddress, + pub len: GuestUsize, +- pub region_list: Vec<VirtioSharedMemory>, ++ pub region_list: BTreeMap<u8, VirtioSharedMemory>, + } + + /// Trait for virtio devices to be driven by a virtio transport. +diff --git a/virtio-devices/src/lib.rs b/virtio-devices/src/lib.rs +index 680cbe29..55428cd8 100644 +--- a/virtio-devices/src/lib.rs ++++ b/virtio-devices/src/lib.rs +@@ -44,7 +44,7 @@ pub use self::block::{Block, BlockState}; + pub use self::console::{Console, ConsoleResizer, Endpoint}; + pub use self::device::{ + DmaRemapping, UserspaceMapping, VirtioCommon, VirtioDevice, VirtioInterrupt, +- VirtioInterruptType, VirtioSharedMemoryList, ++ VirtioInterruptType, VirtioSharedMemory, VirtioSharedMemoryList, + }; + pub use self::epoll_helper::{ + EpollHelper, EpollHelperError, EpollHelperHandler, EPOLL_HELPER_EVENT_LAST, +@@ -93,6 +93,8 @@ pub enum ActivateError { + VhostUserFsSetup(vhost_user::Error), + #[error("Failed to setup vhost-user daemon: {0}")] + VhostUserSetup(vhost_user::Error), ++ #[error("Failed to setup vhost-user-gpu daemon: {0}")] ++ VhostUserGpuSetup(vhost_user::Error), + #[error("Failed to create seccomp filter: {0}")] + CreateSeccompFilter(seccompiler::Error), + #[error("Failed to create rate limiter: {0}")] +diff --git a/virtio-devices/src/seccomp_filters.rs b/virtio-devices/src/seccomp_filters.rs +index 43d723b8..8620419f 100644 +--- a/virtio-devices/src/seccomp_filters.rs ++++ b/virtio-devices/src/seccomp_filters.rs +@@ -22,6 +22,7 @@ pub enum Thread { + VirtioRng, + VirtioVhostBlock, + VirtioVhostFs, ++ VirtioVhostGpu, + VirtioVhostNet, + VirtioVhostNetCtl, + VirtioVsock, +@@ -163,6 +164,20 @@ fn virtio_vhost_fs_thread_rules() -> Vec<(i64, Vec<SeccompRule>)> { + ] + } + ++fn virtio_vhost_gpu_thread_rules() -> Vec<(i64, Vec<SeccompRule>)> { ++ vec![ ++ (libc::SYS_clock_nanosleep, vec![]), ++ (libc::SYS_connect, vec![]), ++ (libc::SYS_getcwd, vec![]), ++ (libc::SYS_nanosleep, vec![]), ++ (libc::SYS_recvmsg, vec![]), ++ (libc::SYS_recvmsg, vec![]), ++ (libc::SYS_sendmsg, vec![]), ++ (libc::SYS_sendmsg, vec![]), ++ (libc::SYS_socket, vec![]), ++ ] ++} ++ + fn virtio_vhost_net_ctl_thread_rules() -> Vec<(i64, Vec<SeccompRule>)> { + vec![] + } +@@ -234,6 +249,7 @@ fn get_seccomp_rules(thread_type: Thread) -> Vec<(i64, Vec<SeccompRule>)> { + Thread::VirtioRng => virtio_rng_thread_rules(), + Thread::VirtioVhostBlock => virtio_vhost_block_thread_rules(), + Thread::VirtioVhostFs => virtio_vhost_fs_thread_rules(), ++ Thread::VirtioVhostGpu => virtio_vhost_gpu_thread_rules(), + Thread::VirtioVhostNet => virtio_vhost_net_thread_rules(), + Thread::VirtioVhostNetCtl => virtio_vhost_net_ctl_thread_rules(), + Thread::VirtioVsock => virtio_vsock_thread_rules(), +diff --git a/virtio-devices/src/transport/pci_device.rs b/virtio-devices/src/transport/pci_device.rs +index 60979573..0348b53a 100644 +--- a/virtio-devices/src/transport/pci_device.rs ++++ b/virtio-devices/src/transport/pci_device.rs +@@ -1054,11 +1054,11 @@ impl PciDevice for VirtioPciDevice { + PciDeviceError::IoRegistrationFailed(shm_list.addr.raw_value(), e) + })?; + +- for (idx, shm) in shm_list.region_list.iter().enumerate() { ++ for (shmid, shm) in shm_list.region_list.iter() { + let shm_cap = VirtioPciCap64::new( + PciCapabilityType::SharedMemory, + VIRTIO_SHM_BAR_INDEX as u8, +- idx as u8, ++ *shmid, + shm.offset, + shm.len, + ); +diff --git a/virtio-devices/src/vhost_user/gpu.rs b/virtio-devices/src/vhost_user/gpu.rs +new file mode 100644 +index 00000000..416d9629 +--- /dev/null ++++ b/virtio-devices/src/vhost_user/gpu.rs +@@ -0,0 +1,406 @@ ++// Copyright 2019 Intel Corporation. All Rights Reserved. ++// Copyright 2022 Unikie ++// Copyright 2023 Alyssa Ross <hi@alyssa.is> ++// SPDX-License-Identifier: Apache-2.0 ++ ++use super::vu_common_ctrl::VhostUserHandle; ++use super::{Error, Result}; ++use crate::seccomp_filters::Thread; ++use crate::thread_helper::spawn_virtio_thread; ++use crate::vhost_user::VhostUserCommon; ++use crate::{ ++ ActivateError, ActivateResult, UserspaceMapping, VirtioCommon, VirtioDevice, VirtioDeviceType, ++ VirtioInterrupt, VirtioSharedMemoryList, VIRTIO_F_IOMMU_PLATFORM, VIRTIO_F_VERSION_1, ++}; ++use crate::{GuestMemoryMmap, GuestRegionMmap, MmapRegion}; ++use seccompiler::SeccompAction; ++use std::io::{self, Write}; ++use std::os::unix::io::AsRawFd; ++use std::result; ++use std::sync::{Arc, Barrier, Mutex}; ++use std::thread; ++use vhost::vhost_user::message::{ ++ VhostUserConfigFlags, VhostUserProtocolFeatures, VhostUserShmemMapMsg, VhostUserShmemUnmapMsg, ++ VhostUserVirtioFeatures, ++}; ++use vhost::vhost_user::{ ++ HandlerResult, MasterReqHandler, VhostUserMaster, VhostUserMasterReqHandler, ++}; ++use virtio_bindings::virtio_gpu::{ ++ VIRTIO_GPU_F_CONTEXT_INIT, VIRTIO_GPU_F_RESOURCE_BLOB, VIRTIO_GPU_F_RESOURCE_UUID, ++ VIRTIO_GPU_F_VIRGL, ++}; ++use virtio_queue::Queue; ++use vm_memory::GuestMemoryAtomic; ++use vm_migration::{MigratableError, Pausable}; ++use vmm_sys_util::eventfd::EventFd; ++ ++const QUEUE_SIZES: &[u16] = &[256, 16]; ++const NUM_QUEUES: u16 = QUEUE_SIZES.len() as _; ++ ++struct SlaveReqHandler { ++ cache_size: u64, ++ mmap_cache_addr: u64, ++} ++ ++impl SlaveReqHandler { ++ // Make sure request is within cache range ++ fn is_req_valid(&self, offset: u64, len: u64) -> bool { ++ let end = match offset.checked_add(len) { ++ Some(n) => n, ++ None => return false, ++ }; ++ ++ !(offset >= self.cache_size || end > self.cache_size) ++ } ++} ++ ++impl VhostUserMasterReqHandler for SlaveReqHandler { ++ fn shmem_map(&self, req: &VhostUserShmemMapMsg, fd: &dyn AsRawFd) -> HandlerResult<u64> { ++ if !self.is_req_valid(req.shm_offset, req.len) { ++ return Err(io::Error::from_raw_os_error(libc::EINVAL)); ++ } ++ ++ let addr = self.mmap_cache_addr + req.shm_offset; ++ let ret = unsafe { ++ libc::mmap( ++ addr as *mut libc::c_void, ++ req.len as usize, ++ req.flags.bits() as i32, ++ // https://bugzilla.kernel.org/show_bug.cgi?id=217238 ++ if req.flags.bits() as i32 & libc::PROT_WRITE != 0 { ++ libc::MAP_SHARED ++ } else { ++ libc::MAP_PRIVATE ++ } | libc::MAP_FIXED, ++ fd.as_raw_fd(), ++ req.fd_offset as libc::off_t, ++ ) ++ }; ++ ++ if ret == libc::MAP_FAILED { ++ return Err(io::Error::last_os_error()); ++ } ++ ++ Ok(0) ++ } ++ ++ fn shmem_unmap(&self, req: &VhostUserShmemUnmapMsg) -> HandlerResult<u64> { ++ if !self.is_req_valid(req.shm_offset, req.len) { ++ return Err(io::Error::from_raw_os_error(libc::EINVAL)); ++ } ++ ++ let addr = self.mmap_cache_addr + req.shm_offset; ++ let ret = unsafe { ++ libc::mmap( ++ addr as *mut libc::c_void, ++ req.len as usize, ++ libc::PROT_NONE, ++ libc::MAP_ANONYMOUS | libc::MAP_PRIVATE | libc::MAP_FIXED, ++ -1, ++ 0, ++ ) ++ }; ++ if ret == libc::MAP_FAILED { ++ return Err(io::Error::last_os_error()); ++ } ++ ++ Ok(0) ++ } ++} ++ ++#[derive(Default)] ++#[repr(C, packed)] ++pub struct VirtioGpuConfig { ++ pub events_read: u32, ++ pub events_clear: u32, ++ pub num_scanouts: u32, ++ pub num_capsets: u32, ++} ++ ++pub struct Gpu { ++ common: VirtioCommon, ++ vu_common: VhostUserCommon, ++ id: String, ++ // Hold ownership of the memory that is allocated for the device ++ // which will be automatically dropped when the device is dropped ++ cache: Option<(VirtioSharedMemoryList, MmapRegion)>, ++ slave_req_support: bool, ++ seccomp_action: SeccompAction, ++ guest_memory: Option<GuestMemoryAtomic<GuestMemoryMmap>>, ++ epoll_thread: Option<thread::JoinHandle<()>>, ++ exit_evt: EventFd, ++ iommu: bool, ++} ++ ++impl Gpu { ++ /// Create a new virtio-gpu device. ++ pub fn new( ++ id: String, ++ path: &str, ++ cache: Option<(VirtioSharedMemoryList, MmapRegion)>, ++ seccomp_action: SeccompAction, ++ exit_evt: EventFd, ++ iommu: bool, ++ ) -> Result<Gpu> { ++ // Connect to the vhost-user socket. ++ let mut vu = VhostUserHandle::connect_vhost_user(false, path, NUM_QUEUES as u64, false)?; ++ ++ let avail_features = 1 << 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 ++ | VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits(); ++ ++ let avail_protocol_features = ++ VhostUserProtocolFeatures::CONFIG | VhostUserProtocolFeatures::SLAVE_REQ; ++ // The SHARED_MEMORY_REGIONS protocol feature is a way for the backend to indicate ++ // that it supports the GET_SHARED_MEMORY_REGIONS request. Since we don't use that ++ // request, we don't ack SHARED_MEMORY_REGIONS. ++ ++ let (acked_features, acked_protocol_features) = ++ vu.negotiate_features_vhost_user(avail_features, avail_protocol_features)?; ++ ++ Ok(Gpu { ++ common: VirtioCommon { ++ device_type: VirtioDeviceType::Gpu as u32, ++ avail_features: acked_features, ++ // If part of the available features that have been acked, the ++ // PROTOCOL_FEATURES bit must be already set through the VIRTIO ++ // acked features as we know the guest would never ack it, this ++ // the feature would be lost. ++ acked_features: acked_features & VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits(), ++ paused_sync: Some(Arc::new(Barrier::new(NUM_QUEUES as usize))), ++ queue_sizes: QUEUE_SIZES.to_vec(), ++ min_queues: NUM_QUEUES, ++ ..Default::default() ++ }, ++ vu_common: VhostUserCommon { ++ vu: Some(Arc::new(Mutex::new(vu))), ++ acked_protocol_features, ++ socket_path: path.to_string(), ++ vu_num_queues: NUM_QUEUES as usize, ++ ..Default::default() ++ }, ++ id, ++ cache, ++ slave_req_support: acked_protocol_features ++ & VhostUserProtocolFeatures::SLAVE_REQ.bits() ++ != 0, ++ seccomp_action, ++ guest_memory: None, ++ epoll_thread: None, ++ exit_evt, ++ iommu, ++ }) ++ } ++} ++ ++impl Drop for Gpu { ++ fn drop(&mut self) { ++ if let Some(kill_evt) = self.common.kill_evt.take() { ++ // Ignore the result because there is nothing we can do about it. ++ let _ = kill_evt.write(1); ++ } ++ } ++} ++ ++impl VirtioDevice for Gpu { ++ fn device_type(&self) -> u32 { ++ self.common.device_type ++ } ++ ++ fn queue_max_sizes(&self) -> &[u16] { ++ &self.common.queue_sizes ++ } ++ ++ fn features(&self) -> u64 { ++ let mut features = self.common.avail_features; ++ if self.iommu { ++ features |= 1u64 << VIRTIO_F_IOMMU_PLATFORM; ++ } ++ features ++ } ++ ++ fn ack_features(&mut self, value: u64) { ++ self.common.ack_features(value) ++ } ++ ++ fn read_config(&self, offset: u64, mut data: &mut [u8]) { ++ if let Some(vu) = &self.vu_common.vu { ++ if let Err(e) = vu ++ .lock() ++ .unwrap() ++ .socket_handle() ++ .get_config( ++ offset as u32, ++ data.len() as u32, ++ VhostUserConfigFlags::WRITABLE, ++ data, ++ ) ++ .map_err(|e| format!("{:?}", e)) ++ .and_then(|(_, config)| data.write_all(&config).map_err(|e| format!("{:?}", e))) ++ { ++ error!("Failed getting vhost-user-gpu configuration: {:?}", e); ++ } ++ } ++ } ++ ++ fn activate( ++ &mut self, ++ mem: GuestMemoryAtomic<GuestMemoryMmap>, ++ interrupt_cb: Arc<dyn VirtioInterrupt>, ++ queues: Vec<(usize, Queue, EventFd)>, ++ ) -> ActivateResult { ++ self.common.activate(&queues, &interrupt_cb)?; ++ self.guest_memory = Some(mem.clone()); ++ ++ // Initialize slave communication. ++ let slave_req_handler = if self.slave_req_support { ++ if let Some(cache) = self.cache.as_ref() { ++ let vu_master_req_handler = Arc::new(SlaveReqHandler { ++ cache_size: cache.0.len, ++ mmap_cache_addr: cache.0.host_addr, ++ }); ++ ++ let mut req_handler = ++ MasterReqHandler::new(vu_master_req_handler).map_err(|e| { ++ ActivateError::VhostUserGpuSetup(Error::MasterReqHandlerCreation(e)) ++ })?; ++ ++ if self.vu_common.acked_protocol_features ++ & VhostUserProtocolFeatures::REPLY_ACK.bits() ++ != 0 ++ { ++ req_handler.set_reply_ack_flag(true); ++ } ++ ++ Some(req_handler) ++ } else { ++ None ++ } ++ } else { ++ None ++ }; ++ ++ // Run a dedicated thread for handling potential reconnections with ++ // the backend. ++ let (kill_evt, pause_evt) = self.common.dup_eventfds(); ++ ++ let mut handler = self.vu_common.activate( ++ mem, ++ queues, ++ interrupt_cb, ++ self.common.acked_features, ++ slave_req_handler, ++ kill_evt, ++ pause_evt, ++ )?; ++ ++ let paused = self.common.paused.clone(); ++ let paused_sync = self.common.paused_sync.clone(); ++ ++ let mut epoll_threads = Vec::new(); ++ spawn_virtio_thread( ++ &self.id, ++ &self.seccomp_action, ++ Thread::VirtioVhostGpu, ++ &mut epoll_threads, ++ &self.exit_evt, ++ move || handler.run(paused, paused_sync.unwrap()), ++ )?; ++ self.epoll_thread = Some(epoll_threads.remove(0)); ++ ++ event!("virtio-device", "activated", "id", &self.id); ++ Ok(()) ++ } ++ ++ fn reset(&mut self) -> Option<Arc<dyn VirtioInterrupt>> { ++ // We first must resume the virtio thread if it was paused. ++ if self.common.pause_evt.take().is_some() { ++ self.common.resume().ok()?; ++ } ++ ++ if let Some(vu) = &self.vu_common.vu { ++ if let Err(e) = vu.lock().unwrap().reset_vhost_user() { ++ error!("Failed to reset vhost-user daemon: {:?}", e); ++ return None; ++ } ++ } ++ ++ if let Some(kill_evt) = self.common.kill_evt.take() { ++ // Ignore the result because there is nothing we can do about it. ++ let _ = kill_evt.write(1); ++ } ++ ++ event!("virtio-device", "reset", "id", &self.id); ++ ++ // Return the interrupt ++ Some(self.common.interrupt_cb.take().unwrap()) ++ } ++ ++ fn shutdown(&mut self) { ++ self.vu_common.shutdown() ++ } ++ ++ fn get_shm_regions(&self) -> Option<VirtioSharedMemoryList> { ++ // It would be possible to get the size of the region from the ++ // backend over vhost-user, but since we need to know the size ++ // up front in cloud-hypervisor to construct Self, it wouldn't ++ // help. The user is thereforce responsible for configuring ++ // the correct region size in VM configuration. ++ self.cache.as_ref().map(|cache| cache.0.clone()) ++ } ++ ++ fn set_shm_regions( ++ &mut self, ++ shm_regions: VirtioSharedMemoryList, ++ ) -> std::result::Result<(), crate::Error> { ++ if let Some(cache) = self.cache.as_mut() { ++ cache.0 = shm_regions; ++ Ok(()) ++ } else { ++ Err(crate::Error::SetShmRegionsNotSupported) ++ } ++ } ++ ++ fn add_memory_region( ++ &mut self, ++ region: &Arc<GuestRegionMmap>, ++ ) -> std::result::Result<(), crate::Error> { ++ self.vu_common.add_memory_region(&self.guest_memory, region) ++ } ++ ++ fn userspace_mappings(&self) -> Vec<UserspaceMapping> { ++ let mut mappings = Vec::new(); ++ if let Some(cache) = self.cache.as_ref() { ++ mappings.push(UserspaceMapping { ++ host_addr: cache.0.host_addr, ++ mem_slot: cache.0.mem_slot, ++ addr: cache.0.addr, ++ len: cache.0.len, ++ mergeable: false, ++ }) ++ } ++ ++ mappings ++ } ++} ++ ++impl Pausable for Gpu { ++ fn pause(&mut self) -> result::Result<(), MigratableError> { ++ self.vu_common.pause()?; ++ self.common.pause() ++ } ++ ++ fn resume(&mut self) -> result::Result<(), MigratableError> { ++ self.common.resume()?; ++ ++ if let Some(epoll_thread) = &self.epoll_thread { ++ epoll_thread.thread().unpark(); ++ } ++ ++ self.vu_common.resume() ++ } ++} +diff --git a/virtio-devices/src/vhost_user/mod.rs b/virtio-devices/src/vhost_user/mod.rs +index 2dee7324..d1b1c4e6 100644 +--- a/virtio-devices/src/vhost_user/mod.rs ++++ b/virtio-devices/src/vhost_user/mod.rs +@@ -31,11 +31,13 @@ use vu_common_ctrl::VhostUserHandle; + + pub mod blk; + pub mod fs; ++pub mod gpu; + pub mod net; + pub mod vu_common_ctrl; + + pub use self::blk::Blk; + pub use self::fs::*; ++pub use self::gpu::*; + pub use self::net::Net; + pub use self::vu_common_ctrl::VhostUserConfig; + +diff --git a/vmm/Cargo.toml b/vmm/Cargo.toml +index e256c90f..b3a374ab 100644 +--- a/vmm/Cargo.toml ++++ b/vmm/Cargo.toml +@@ -52,6 +52,7 @@ versionize = "0.1.10" + versionize_derive = "0.1.4" + vfio-ioctls = { git = "https://github.com/rust-vmm/vfio", branch = "main", default-features = false } + vfio_user = { git = "https://github.com/rust-vmm/vfio-user", branch = "main" } ++virtio-bindings = "0.2.0" + virtio-devices = { path = "../virtio-devices" } + virtio-queue = "0.9.0" + vm-allocator = { path = "../vm-allocator" } +diff --git a/vmm/src/api/mod.rs b/vmm/src/api/mod.rs +index 8aac5d3c..041a6c41 100644 +--- a/vmm/src/api/mod.rs ++++ b/vmm/src/api/mod.rs +@@ -38,8 +38,8 @@ pub use self::http::start_http_fd_thread; + pub use self::http::start_http_path_thread; + + use crate::config::{ +- DeviceConfig, DiskConfig, FsConfig, NetConfig, PmemConfig, RestoreConfig, UserDeviceConfig, +- VdpaConfig, VmConfig, VsockConfig, ++ DeviceConfig, DiskConfig, FsConfig, GpuConfig, NetConfig, PmemConfig, RestoreConfig, ++ UserDeviceConfig, VdpaConfig, VmConfig, VsockConfig, + }; + use crate::device_tree::DeviceTree; + use crate::vm::{Error as VmError, VmState}; +@@ -135,6 +135,9 @@ pub enum ApiError { + /// The fs could not be added to the VM. + VmAddFs(VmError), + ++ /// The gpu could not be added to the VM. ++ VmAddGpu(VmError), ++ + /// The pmem device could not be added to the VM. + VmAddPmem(VmError), + +@@ -305,6 +308,9 @@ pub enum ApiRequest { + /// Add a fs to the VM. + VmAddFs(Arc<FsConfig>, Sender<ApiResponse>), + ++ /// Add a gpu to the VM. ++ VmAddGpu(Arc<GpuConfig>, Sender<ApiResponse>), ++ + /// Add a pmem device to the VM. + VmAddPmem(Arc<PmemConfig>, Sender<ApiResponse>), + +diff --git a/vmm/src/config.rs b/vmm/src/config.rs +index a6836fc6..4fb0f543 100644 +--- a/vmm/src/config.rs ++++ b/vmm/src/config.rs +@@ -26,6 +26,8 @@ pub enum Error { + ParseFsTagMissing, + /// Filesystem socket is missing + ParseFsSockMissing, ++ /// GPU socket is missing ++ ParseGpuSockMissing, + /// Missing persistent memory file parameter. + ParsePmemFileMissing, + /// Missing vsock socket path parameter. +@@ -54,6 +56,8 @@ pub enum Error { + ParseBalloon(OptionParserError), + /// Error parsing filesystem parameters + ParseFileSystem(OptionParserError), ++ /// Error parsing GPU parameters ++ ParseGpu(OptionParserError), + /// Error parsing persistent memory parameters + ParsePersistentMemory(OptionParserError), + /// Failed parsing console +@@ -304,6 +308,8 @@ impl fmt::Display for Error { + ParseFileSystem(o) => write!(f, "Error parsing --fs: {o}"), + ParseFsSockMissing => write!(f, "Error parsing --fs: socket missing"), + ParseFsTagMissing => write!(f, "Error parsing --fs: tag missing"), ++ ParseGpu(o) => write!(f, "Error parsing --gpu: {o}"), ++ ParseGpuSockMissing => write!(f, "Error parsing --gpu: socket missing"), + ParsePersistentMemory(o) => write!(f, "Error parsing --pmem: {o}"), + ParsePmemFileMissing => write!(f, "Error parsing --pmem: file missing"), + ParseVsock(o) => write!(f, "Error parsing --vsock: {o}"), +@@ -368,6 +374,7 @@ pub struct VmParams<'a> { + pub rng: &'a str, + pub balloon: Option<&'a str>, + pub fs: Option<Vec<&'a str>>, ++ pub gpu: Option<Vec<&'a str>>, + pub pmem: Option<Vec<&'a str>>, + pub serial: &'a str, + pub console: &'a str, +@@ -1272,6 +1279,56 @@ impl FsConfig { + } + } + ++impl GpuConfig { ++ pub fn parse(gpu: &str) -> Result<Self> { ++ let mut parser = OptionParser::new(); ++ parser ++ .add("socket") ++ .add("cache_size") ++ .add("id") ++ .add("pci_segment"); ++ parser.parse(gpu).map_err(Error::ParseGpu)?; ++ ++ let socket = PathBuf::from(parser.get("socket").ok_or(Error::ParseGpuSockMissing)?); ++ let cache_size = parser ++ .convert::<ByteSized>("cache_size") ++ .map_err(Error::ParseGpu)? ++ .unwrap_or_else(|| ByteSized(default_gpuconfig_cache_size())) ++ .0; ++ let id = parser.get("id"); ++ ++ let pci_segment = parser ++ .convert("pci_segment") ++ .map_err(Error::ParseGpu)? ++ .unwrap_or_default(); ++ ++ Ok(GpuConfig { ++ socket, ++ cache_size, ++ id, ++ pci_segment, ++ }) ++ } ++ ++ pub fn validate(&self, vm_config: &VmConfig) -> ValidationResult<()> { ++ if let Some(platform_config) = vm_config.platform.as_ref() { ++ if self.pci_segment >= platform_config.num_pci_segments { ++ return Err(ValidationError::InvalidPciSegment(self.pci_segment)); ++ } ++ ++ if let Some(iommu_segments) = platform_config.iommu_segments.as_ref() { ++ if iommu_segments.contains(&self.pci_segment) { ++ return Err(ValidationError::IommuNotSupportedOnSegment( ++ self.pci_segment, ++ )); ++ } ++ } ++ } ++ ++ Ok(()) ++ } ++} ++ + impl PmemConfig { + pub fn parse(pmem: &str) -> Result<Self> { + let mut parser = OptionParser::new(); +@@ -1805,6 +1862,17 @@ impl VmConfig { + } + } + ++ if let Some(gpus) = &self.gpu { ++ if !gpus.is_empty() && !self.memory.shared { ++ return Err(ValidationError::VhostUserRequiresSharedMemory); ++ } ++ for gpu in gpus { ++ gpu.validate(self)?; ++ ++ Self::validate_identifier(&mut id_list, &gpu.id)?; ++ } ++ } ++ + if let Some(pmems) = &self.pmem { + for pmem in pmems { + pmem.validate(self)?; +@@ -1990,6 +2058,15 @@ impl VmConfig { + fs = Some(fs_config_list); + } + ++ let mut gpu: Option<Vec<GpuConfig>> = None; ++ if let Some(gpu_list) = &vm_params.gpu { ++ let mut gpu_config_list = Vec::new(); ++ for item in gpu_list.iter() { ++ gpu_config_list.push(GpuConfig::parse(item)?); ++ } ++ gpu = Some(gpu_config_list); ++ } ++ + let mut pmem: Option<Vec<PmemConfig>> = None; + if let Some(pmem_list) = &vm_params.pmem { + let mut pmem_config_list = Vec::new(); +@@ -2096,6 +2173,7 @@ impl VmConfig { + rng, + balloon, + fs, ++ gpu, + pmem, + serial, + console, +@@ -2150,6 +2228,13 @@ impl VmConfig { + removed |= fs.len() != len; + } + ++ // Remove if gpu device ++ if let Some(gpu) = self.gpu.as_mut() { ++ let len = gpu.len(); ++ gpu.retain(|dev| dev.id.as_ref().map(|id| id.as_ref()) != Some(id)); ++ removed |= gpu.len() != len; ++ } ++ + // Remove if net device + if let Some(net) = self.net.as_mut() { + let len = net.len(); +@@ -2219,6 +2304,7 @@ impl Clone for VmConfig { + rng: self.rng.clone(), + balloon: self.balloon.clone(), + fs: self.fs.clone(), ++ gpu: self.gpu.clone(), + pmem: self.pmem.clone(), + serial: self.serial.clone(), + console: self.console.clone(), +@@ -2613,6 +2699,21 @@ mod tests { + Ok(()) + } + ++ #[test] ++ fn test_parse_gpu() -> Result<()> { ++ // "socket" must be supplied ++ assert!(GpuConfig::parse("").is_err()); ++ assert_eq!( ++ GpuConfig::parse("socket=/tmp/sock")?, ++ GpuConfig { ++ socket: PathBuf::from("/tmp/sock"), ++ ..Default::default() ++ } ++ ); ++ ++ Ok(()) ++ } ++ + #[test] + fn test_pmem_parsing() -> Result<()> { + // Must always give a file and size +@@ -2847,6 +2948,7 @@ mod tests { + }, + balloon: None, + fs: None, ++ gpu: None, + pmem: None, + serial: ConsoleConfig { + file: None, +@@ -3012,6 +3114,15 @@ mod tests { + Err(ValidationError::VhostUserRequiresSharedMemory) + ); + ++ let mut invalid_config = valid_config.clone(); ++ invalid_config.gpu = Some(vec![GpuConfig { ++ ..Default::default() ++ }]); ++ assert_eq!( ++ invalid_config.validate(), ++ Err(ValidationError::VhostUserRequiresSharedMemory) ++ ); ++ + let mut still_valid_config = valid_config.clone(); + still_valid_config.memory.shared = true; + assert!(still_valid_config.validate().is_ok()); +diff --git a/vmm/src/device_manager.rs b/vmm/src/device_manager.rs +index 1f53390c..2d974480 100644 +--- a/vmm/src/device_manager.rs ++++ b/vmm/src/device_manager.rs +@@ -10,8 +10,8 @@ + // + + use crate::config::{ +- ConsoleOutputMode, DeviceConfig, DiskConfig, FsConfig, NetConfig, PmemConfig, UserDeviceConfig, +- VdpaConfig, VhostMode, VmConfig, VsockConfig, ++ ConsoleOutputMode, DeviceConfig, DiskConfig, FsConfig, GpuConfig, NetConfig, PmemConfig, ++ UserDeviceConfig, VdpaConfig, VhostMode, VmConfig, VsockConfig, + }; + use crate::cpu::{CpuManager, CPU_MANAGER_ACPI_SIZE}; + use crate::device_tree::{DeviceNode, DeviceTree}; +@@ -66,6 +66,7 @@ use serde::{Deserialize, Serialize}; + use std::collections::{BTreeSet, HashMap}; + use std::fs::{read_link, File, OpenOptions}; + use std::io::{self, stdout, Seek, SeekFrom}; ++use std::iter::once; + use std::mem::zeroed; + use std::num::Wrapping; + use std::os::unix::fs::OpenOptionsExt; +@@ -76,11 +77,13 @@ use std::sync::{Arc, Mutex}; + use std::time::Instant; + use tracer::trace_scoped; + use vfio_ioctls::{VfioContainer, VfioDevice, VfioDeviceFd}; ++use virtio_bindings::virtio_gpu::virtio_gpu_shm_id_VIRTIO_GPU_SHM_ID_HOST_VISIBLE as VIRTIO_GPU_SHM_ID_HOST_VISIBLE; + use virtio_devices::transport::VirtioTransport; + use virtio_devices::transport::{VirtioPciDevice, VirtioPciDeviceActivator}; + use virtio_devices::vhost_user::VhostUserConfig; + use virtio_devices::{ + AccessPlatformMapping, ActivateError, VdpaDmaMapping, VirtioMemMappingSource, ++ VirtioSharedMemory, VirtioSharedMemoryList, + }; + use virtio_devices::{Endpoint, IommuMapping}; + use vm_allocator::{AddressAllocator, SystemAllocator}; +@@ -122,6 +125,7 @@ const PVPANIC_DEVICE_NAME: &str = "__pvpanic"; + // identifiers if the user doesn't give one + const DISK_DEVICE_NAME_PREFIX: &str = "_disk"; + const FS_DEVICE_NAME_PREFIX: &str = "_fs"; ++const GPU_DEVICE_NAME_PREFIX: &str = "_gpu"; + const NET_DEVICE_NAME_PREFIX: &str = "_net"; + const PMEM_DEVICE_NAME_PREFIX: &str = "_pmem"; + const VDPA_DEVICE_NAME_PREFIX: &str = "_vdpa"; +@@ -158,9 +162,15 @@ pub enum DeviceManagerError { + /// Cannot create virtio-fs device + CreateVirtioFs(virtio_devices::vhost_user::Error), + ++ /// Cannot create virtio-gpu device ++ CreateVirtioGpu(virtio_devices::vhost_user::Error), ++ + /// Virtio-fs device was created without a socket. + NoVirtioFsSock, + ++ /// Virtio-gpu device was created without a socket. ++ NoVirtioGpuSock, ++ + /// Cannot create vhost-user-blk device + CreateVhostUserBlk(virtio_devices::vhost_user::Error), + +@@ -245,6 +255,9 @@ pub enum DeviceManagerError { + /// Cannot find a memory range for virtio-fs + FsRangeAllocation, + ++ /// Cannot find a memory range for virtio-gpu ++ GpuRangeAllocation, ++ + /// Error creating serial output file + SerialOutputFileOpen(io::Error), + +@@ -2140,6 +2153,9 @@ impl DeviceManager { + // Add virtio-fs if required + devices.append(&mut self.make_virtio_fs_devices()?); + ++ // Add virtio-gpu if required ++ devices.append(&mut self.make_virtio_gpu_devices()?); ++ + // Add virtio-pmem if required + devices.append(&mut self.make_virtio_pmem_devices()?); + +@@ -2655,6 +2671,118 @@ impl DeviceManager { + Ok(devices) + } + ++ fn make_virtio_gpu_device( ++ &mut self, ++ gpu_cfg: &mut GpuConfig, ++ ) -> DeviceManagerResult<MetaVirtioDevice> { ++ let id = if let Some(id) = &gpu_cfg.id { ++ id.clone() ++ } else { ++ let id = self.next_device_name(GPU_DEVICE_NAME_PREFIX)?; ++ gpu_cfg.id = Some(id.clone()); ++ id ++ }; ++ ++ info!("Creating virtio-gpu device: {:?}", gpu_cfg); ++ ++ let mut node = device_node!(id); ++ ++ if let Some(gpu_socket) = gpu_cfg.socket.to_str() { ++ let cache_size = gpu_cfg.cache_size; ++ // In crosvm, the 8 GiB bar is 8 GiB-aligned. ++ let cache_base = self.pci_segments[gpu_cfg.pci_segment as usize] ++ .allocator ++ .lock() ++ .unwrap() ++ .allocate(None, cache_size as GuestUsize, Some(cache_size)) ++ .ok_or(DeviceManagerError::GpuRangeAllocation)? ++ .raw_value(); ++ ++ // Update the node with correct resource information. ++ node.resources.push(Resource::MmioAddressRange { ++ base: cache_base, ++ size: cache_size, ++ }); ++ ++ let mmap_region = MmapRegion::build( ++ None, ++ cache_size as usize, ++ libc::PROT_NONE, ++ libc::MAP_ANONYMOUS | libc::MAP_PRIVATE, ++ ) ++ .map_err(DeviceManagerError::NewMmapRegion)?; ++ let host_addr: u64 = mmap_region.as_ptr() as u64; ++ ++ let mem_slot = self ++ .memory_manager ++ .lock() ++ .unwrap() ++ .create_userspace_mapping(cache_base, cache_size, host_addr, false, false, false) ++ .map_err(DeviceManagerError::MemoryManager)?; ++ ++ let region_list = once(( ++ VIRTIO_GPU_SHM_ID_HOST_VISIBLE as u8, ++ VirtioSharedMemory { ++ offset: 0, ++ len: cache_size, ++ }, ++ )) ++ .collect(); ++ ++ let cache = Some(( ++ VirtioSharedMemoryList { ++ host_addr, ++ mem_slot, ++ addr: GuestAddress(cache_base), ++ len: cache_size as GuestUsize, ++ region_list, ++ }, ++ mmap_region, ++ )); ++ ++ let virtio_gpu_device = Arc::new(Mutex::new( ++ virtio_devices::vhost_user::Gpu::new( ++ id.clone(), ++ gpu_socket, ++ cache, ++ self.seccomp_action.clone(), ++ self.exit_evt ++ .try_clone() ++ .map_err(DeviceManagerError::EventFd)?, ++ self.force_iommu, ++ ) ++ .map_err(DeviceManagerError::CreateVirtioGpu)?, ++ )); ++ ++ self.device_tree.lock().unwrap().insert(id.clone(), node); ++ ++ Ok(MetaVirtioDevice { ++ virtio_device: Arc::clone(&virtio_gpu_device) ++ as Arc<Mutex<dyn virtio_devices::VirtioDevice>>, ++ iommu: false, ++ id, ++ pci_segment: gpu_cfg.pci_segment, ++ dma_handler: None, ++ }) ++ } else { ++ Err(DeviceManagerError::NoVirtioGpuSock) ++ } ++ } ++ ++ fn make_virtio_gpu_devices(&mut self) -> DeviceManagerResult<Vec<MetaVirtioDevice>> { ++ let mut devices = Vec::new(); ++ ++ let mut gpu_devices = self.config.lock().unwrap().gpu.clone(); ++ if let Some(gpu_list_cfg) = &mut gpu_devices { ++ for gpu_cfg in gpu_list_cfg.iter_mut() { ++ devices.push(self.make_virtio_gpu_device(gpu_cfg)?); ++ } ++ } ++ self.config.lock().unwrap().gpu = gpu_devices; ++ ++ Ok(devices) ++ } ++ + fn make_virtio_pmem_device( + &mut self, + pmem_cfg: &mut PmemConfig, +@@ -3914,6 +4042,7 @@ impl DeviceManager { + | VirtioDeviceType::Block + | VirtioDeviceType::Pmem + | VirtioDeviceType::Fs ++ | VirtioDeviceType::Gpu + | VirtioDeviceType::Vsock => {} + _ => return Err(DeviceManagerError::RemovalNotAllowed(device_type)), + } +@@ -4182,6 +4311,13 @@ impl DeviceManager { + self.hotplug_virtio_pci_device(device) + } + ++ pub fn add_gpu(&mut self, gpu_cfg: &mut GpuConfig) -> DeviceManagerResult<PciDeviceInfo> { ++ self.validate_identifier(&gpu_cfg.id)?; ++ ++ let device = self.make_virtio_gpu_device(gpu_cfg)?; ++ self.hotplug_virtio_pci_device(device) ++ } ++ + pub fn add_pmem(&mut self, pmem_cfg: &mut PmemConfig) -> DeviceManagerResult<PciDeviceInfo> { + self.validate_identifier(&pmem_cfg.id)?; + +diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs +index 02ee347e..5abbdea5 100644 +--- a/vmm/src/lib.rs ++++ b/vmm/src/lib.rs +@@ -13,8 +13,8 @@ use crate::api::{ + VmSendMigrationData, VmmPingResponse, + }; + use crate::config::{ +- add_to_config, DeviceConfig, DiskConfig, FsConfig, NetConfig, PmemConfig, RestoreConfig, +- UserDeviceConfig, VdpaConfig, VmConfig, VsockConfig, ++ add_to_config, DeviceConfig, DiskConfig, FsConfig, GpuConfig, NetConfig, PmemConfig, ++ RestoreConfig, UserDeviceConfig, VdpaConfig, VmConfig, VsockConfig, + }; + #[cfg(all(target_arch = "x86_64", feature = "guest_debug"))] + use crate::coredump::GuestDebuggable; +@@ -25,8 +25,6 @@ use crate::migration::{recv_vm_config, recv_vm_state}; + use crate::seccomp_filters::{get_seccomp_filter, Thread}; + use crate::vm::{Error as VmError, Vm, VmState}; + use anyhow::anyhow; +-#[cfg(feature = "dbus_api")] +-use api::dbus::{DBusApiOptions, DBusApiShutdownChannels}; + use libc::{tcsetattr, termios, EFD_NONBLOCK, SIGINT, SIGTERM, TCSANOW}; + use memory_manager::MemoryManagerSnapshotData; + use pci::PciBdf; +@@ -1147,6 +1145,32 @@ impl Vmm { + } + } + ++ fn vm_add_gpu(&mut self, gpu_cfg: GpuConfig) -> result::Result<Option<Vec<u8>>, VmError> { ++ self.vm_config.as_ref().ok_or(VmError::VmNotCreated)?; ++ ++ { ++ // Validate the configuration change in a cloned configuration ++ let mut config = self.vm_config.as_ref().unwrap().lock().unwrap().clone(); ++ add_to_config(&mut config.gpu, gpu_cfg.clone()); ++ config.validate().map_err(VmError::ConfigValidation)?; ++ } ++ ++ if let Some(ref mut vm) = self.vm { ++ let info = vm.add_gpu(gpu_cfg).map_err(|e| { ++ error!("Error when adding new gpu to the VM: {:?}", e); ++ e ++ })?; ++ serde_json::to_vec(&info) ++ .map(Some) ++ .map_err(VmError::SerializeJson) ++ } else { ++ // Update VmConfig by adding the new device. ++ let mut config = self.vm_config.as_ref().unwrap().lock().unwrap(); ++ add_to_config(&mut config.gpu, gpu_cfg); ++ Ok(None) ++ } ++ } ++ + fn vm_add_pmem(&mut self, pmem_cfg: PmemConfig) -> result::Result<Option<Vec<u8>>, VmError> { + self.vm_config.as_ref().ok_or(VmError::VmNotCreated)?; + +@@ -2080,6 +2104,13 @@ impl Vmm { + .map(ApiResponsePayload::VmAction); + sender.send(response).map_err(Error::ApiResponseSend)?; + } ++ ApiRequest::VmAddGpu(add_gpu_data, sender) => { ++ let response = self ++ .vm_add_gpu(add_gpu_data.as_ref().clone()) ++ .map_err(ApiError::VmAddGpu) ++ .map(ApiResponsePayload::VmAction); ++ sender.send(response).map_err(Error::ApiResponseSend)?; ++ } + ApiRequest::VmAddPmem(add_pmem_data, sender) => { + let response = self + .vm_add_pmem(add_pmem_data.as_ref().clone()) +@@ -2245,6 +2276,7 @@ mod unit_tests { + }, + balloon: None, + fs: None, ++ gpu: None, + pmem: None, + serial: ConsoleConfig { + file: None, +@@ -2472,6 +2504,54 @@ mod unit_tests { + ); + } + ++ #[test] ++ fn test_vmm_vm_cold_add_gpu() { ++ let mut vmm = create_dummy_vmm(); ++ let gpu_config = GpuConfig::parse("socket=/tmp/sock").unwrap(); ++ ++ assert!(matches!( ++ vmm.vm_add_gpu(gpu_config.clone()), ++ Err(VmError::VmNotCreated) ++ )); ++ ++ let _ = vmm.vm_create(create_dummy_vm_config()); ++ assert!(vmm ++ .vm_config ++ .as_ref() ++ .unwrap() ++ .lock() ++ .unwrap() ++ .gpu ++ .is_none()); ++ ++ let result = vmm.vm_add_gpu(gpu_config.clone()); ++ assert!(result.is_ok()); ++ assert!(result.unwrap().is_none()); ++ assert_eq!( ++ vmm.vm_config ++ .as_ref() ++ .unwrap() ++ .lock() ++ .unwrap() ++ .gpu ++ .clone() ++ .unwrap() ++ .len(), ++ 1 ++ ); ++ assert_eq!( ++ vmm.vm_config ++ .as_ref() ++ .unwrap() ++ .lock() ++ .unwrap() ++ .gpu ++ .clone() ++ .unwrap()[0], ++ gpu_config ++ ); ++ } ++ + #[test] + fn test_vmm_vm_cold_add_pmem() { + let mut vmm = create_dummy_vmm(); +diff --git a/vmm/src/vm.rs b/vmm/src/vm.rs +index 2b829df5..ff5a6905 100644 +--- a/vmm/src/vm.rs ++++ b/vmm/src/vm.rs +@@ -12,8 +12,8 @@ + // + + use crate::config::{ +- add_to_config, DeviceConfig, DiskConfig, FsConfig, HotplugMethod, NetConfig, PmemConfig, +- UserDeviceConfig, ValidationError, VdpaConfig, VmConfig, VsockConfig, ++ add_to_config, DeviceConfig, DiskConfig, FsConfig, GpuConfig, HotplugMethod, NetConfig, ++ PmemConfig, UserDeviceConfig, ValidationError, VdpaConfig, VmConfig, VsockConfig, + }; + use crate::config::{NumaConfig, PayloadConfig}; + #[cfg(all(target_arch = "x86_64", feature = "guest_debug"))] +@@ -1488,6 +1488,30 @@ impl Vm { + Ok(pci_device_info) + } + ++ pub fn add_gpu(&mut self, mut gpu_cfg: GpuConfig) -> Result<PciDeviceInfo> { ++ let pci_device_info = self ++ .device_manager ++ .lock() ++ .unwrap() ++ .add_gpu(&mut gpu_cfg) ++ .map_err(Error::DeviceManager)?; ++ ++ // Update VmConfig by adding the new device. This is important to ++ // ensure the device would be created in case of a reboot. ++ { ++ let mut config = self.config.lock().unwrap(); ++ add_to_config(&mut config.gpu, gpu_cfg); ++ } ++ ++ self.device_manager ++ .lock() ++ .unwrap() ++ .notify_hotplug(AcpiNotificationFlags::PCI_DEVICES_CHANGED) ++ .map_err(Error::DeviceManager)?; ++ ++ Ok(pci_device_info) ++ } ++ + pub fn add_pmem(&mut self, mut pmem_cfg: PmemConfig) -> Result<PciDeviceInfo> { + let pci_device_info = self + .device_manager +diff --git a/vmm/src/vm_config.rs b/vmm/src/vm_config.rs +index 67ba0e12..79496a94 100644 +--- a/vmm/src/vm_config.rs ++++ b/vmm/src/vm_config.rs +@@ -419,6 +419,32 @@ impl Default for FsConfig { + } + } + ++#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] ++pub struct GpuConfig { ++ pub socket: PathBuf, ++ #[serde(default = "default_gpuconfig_cache_size")] ++ pub cache_size: u64, ++ #[serde(default)] ++ pub id: Option<String>, ++ #[serde(default)] ++ pub pci_segment: u16, ++} ++ ++impl Default for GpuConfig { ++ fn default() -> Self { ++ Self { ++ socket: PathBuf::new(), ++ cache_size: default_gpuconfig_cache_size(), ++ id: None, ++ pci_segment: 0, ++ } ++ } ++} ++ ++pub fn default_gpuconfig_cache_size() -> u64 { ++ 1 << 33 ++} ++ + #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] + pub struct PmemConfig { + pub file: PathBuf, +@@ -584,6 +610,7 @@ pub struct VmConfig { + pub rng: RngConfig, + pub balloon: Option<BalloonConfig>, + pub fs: Option<Vec<FsConfig>>, ++ pub gpu: Option<Vec<GpuConfig>>, + pub pmem: Option<Vec<PmemConfig>>, + #[serde(default = "default_serial")] + pub serial: ConsoleConfig, +-- +2.42.0 + |