rutabaga_gfx: cross-process Vulkan mappings

In multi-process mode, we currently rely on dma-buf mmap() to
map GPU buffers into the guest.  We usually have to fix the
Mesa driver, and maybe even the kernel to get to work.  That's
"kind of" fine for ChromeOS, which owns the entire stack.

For their Virtual Graphics Interface (VGI) initiative, Android
upstream has requested multi-process mode to work in a
cross-platform, generic way.  Using Vulkan is the only option
that meets the rigorous, uncompromising, strict, meticulous and
bone-crushing requirements of Android upstream.

This has possible two benefits:
  1) We can enable multi-process mode on Nvidia or other
     closed-source drivers, which is nice for Cuttlefish.

  2) On open-source drivers, dma-buf memory is pinned to the
     GTT (amdgpu), even when ideally it can be moved into faster
     vram regions.  This atleast gives the implementation the
     chance to do the smarter and faster option.

We shouldn't run into any SELinux issues since the main crosvm
process is not sandboxed.

Incidentals:
   * Changes vulkano_gralloc to consider integrated GPUs and dGPUs.
     Metadata query is preferred done on the integrated GPU.
   * Update vulkano_gralloc to match top of tree vulkano.

BUG=b:173630595
TEST=used Vulkano allocator and mapped memory into the guest

Change-Id: I78b069c7478d11b3201397894dcccd13bdc61f2c
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crosvm/+/2792042
Tested-by: Gurchetan Singh <gurchetansingh@chromium.org>
Tested-by: kokoro <noreply+kokoro@google.com>
Commit-Queue: Gurchetan Singh <gurchetansingh@chromium.org>
Reviewed-by: Zach Reizner <zachr@chromium.org>
This commit is contained in:
Gurchetan Singh 2021-02-08 16:09:31 -08:00 committed by Commit Bot
parent 6139da6efe
commit f860f50b8a
6 changed files with 348 additions and 62 deletions

View file

@ -638,15 +638,28 @@ impl VirtioGpu {
.ok_or(ErrInvalidResourceId)?;
let map_info = self.rutabaga.map_info(resource_id).map_err(|_| ErrUnspec)?;
let vulkan_info_opt = self.rutabaga.vulkan_info(resource_id).ok();
let export = self.rutabaga.export_blob(resource_id);
let request = match export {
Ok(export) => VmMemoryRequest::RegisterFdAtPciBarOffset(
self.pci_bar,
export.os_handle,
resource.size as usize,
offset,
),
Ok(export) => match vulkan_info_opt {
Some(vulkan_info) => VmMemoryRequest::RegisterVulkanMemoryAtPciBarOffset {
alloc: self.pci_bar,
descriptor: export.os_handle,
handle_type: export.handle_type,
memory_idx: vulkan_info.memory_idx,
physical_device_idx: vulkan_info.physical_device_idx,
offset,
size: resource.size,
},
None => VmMemoryRequest::RegisterFdAtPciBarOffset(
self.pci_bar,
export.os_handle,
resource.size as usize,
offset,
),
},
Err(_) => {
if self.external_blob {
return Err(ErrUnspec);

View file

@ -505,8 +505,18 @@ impl Rutabaga {
.get(&resource_id)
.ok_or(RutabagaError::InvalidResourceId)?;
let map_info = resource.map_info.ok_or(RutabagaError::SpecViolation)?;
Ok(map_info)
resource.map_info.ok_or(RutabagaError::SpecViolation)
}
/// Returns the `vulkan_info` of the blob resource, which consists of the physical device
/// index and memory index associated with the resource.
pub fn vulkan_info(&self, resource_id: u32) -> RutabagaResult<VulkanInfo> {
let resource = self
.resources
.get(&resource_id)
.ok_or(RutabagaError::InvalidResourceId)?;
resource.vulkan_info.ok_or(RutabagaError::Unsupported)
}
/// Returns the 3D info associated with the resource, if any.
@ -516,8 +526,7 @@ impl Rutabaga {
.get(&resource_id)
.ok_or(RutabagaError::InvalidResourceId)?;
let info_3d = resource.info_3d.ok_or(RutabagaError::Unsupported)?;
Ok(info_3d)
resource.info_3d.ok_or(RutabagaError::Unsupported)
}
/// Exports a blob resource. See virtio-gpu spec for blob flag use flags.

View file

@ -7,7 +7,7 @@
use std::collections::BTreeMap as Map;
use base::round_up_to_page_size;
use base::{round_up_to_page_size, MappedRegion};
use crate::rutabaga_gralloc::formats::*;
use crate::rutabaga_gralloc::system_gralloc::SystemGralloc;
@ -89,6 +89,26 @@ impl RutabagaGrallocFlags {
}
}
/// Sets the SW write flag's presence.
#[inline(always)]
pub fn use_sw_write(self, e: bool) -> RutabagaGrallocFlags {
if e {
RutabagaGrallocFlags(self.0 | RUTABAGA_GRALLOC_USE_SW_WRITE_OFTEN)
} else {
RutabagaGrallocFlags(self.0 & !RUTABAGA_GRALLOC_USE_SW_WRITE_OFTEN)
}
}
/// Sets the SW read flag's presence.
#[inline(always)]
pub fn use_sw_read(self, e: bool) -> RutabagaGrallocFlags {
if e {
RutabagaGrallocFlags(self.0 | RUTABAGA_GRALLOC_USE_SW_READ_OFTEN)
} else {
RutabagaGrallocFlags(self.0 & !RUTABAGA_GRALLOC_USE_SW_READ_OFTEN)
}
}
/// Returns true if the texturing flag is set.
#[inline(always)]
pub fn uses_texturing(self) -> bool {
@ -167,6 +187,17 @@ pub trait Gralloc {
/// Implementations must allocate memory given the requirements and return a RutabagaHandle
/// upon success.
fn allocate_memory(&mut self, reqs: ImageMemoryRequirements) -> RutabagaResult<RutabagaHandle>;
/// Implementations must import the given `handle` and return a mapping, suitable for use with
/// KVM and other hypervisors. This is optional and only works with the Vulkano backend.
fn import_and_map(
&mut self,
_handle: RutabagaHandle,
_vulkan_info: VulkanInfo,
_size: u64,
) -> RutabagaResult<Box<dyn MappedRegion>> {
Err(RutabagaError::Unsupported)
}
}
/// Enumeration of possible allocation backends.
@ -293,6 +324,22 @@ impl RutabagaGralloc {
gralloc.allocate_memory(reqs)
}
/// Imports the `handle` using the given `vulkan_info`. Returns a mapping using Vulkano upon
/// success. Should not be used with minigbm or system gralloc backends.
pub fn import_and_map(
&mut self,
handle: RutabagaHandle,
vulkan_info: VulkanInfo,
size: u64,
) -> RutabagaResult<Box<dyn MappedRegion>> {
let gralloc = self
.grallocs
.get_mut(&GrallocBackend::Vulkano)
.ok_or(RutabagaError::Unsupported)?;
gralloc.import_and_map(handle, vulkan_info, size)
}
}
#[cfg(test)]
@ -363,4 +410,44 @@ mod tests {
// Reallocate with same requirements
let _handle2 = gralloc.allocate_memory(reqs).unwrap();
}
#[test]
fn export_and_map() {
let gralloc_result = RutabagaGralloc::new();
if gralloc_result.is_err() {
return;
}
let mut gralloc = gralloc_result.unwrap();
let info = ImageAllocationInfo {
width: 512,
height: 1024,
drm_format: DrmFormat::new(b'X', b'R', b'2', b'4'),
flags: RutabagaGrallocFlags::empty()
.use_linear(true)
.use_sw_write(true)
.use_sw_read(true),
};
let mut reqs = gralloc.get_image_memory_requirements(info).unwrap();
// Anything else can use the mmap(..) system call.
if reqs.vulkan_info.is_none() {
return;
}
let handle = gralloc.allocate_memory(reqs).unwrap();
let vulkan_info = reqs.vulkan_info.take().unwrap();
let mapping = gralloc
.import_and_map(handle, vulkan_info, reqs.size)
.unwrap();
let addr = mapping.as_ptr();
let size = mapping.size();
assert_eq!(size as u64, reqs.size);
assert_ne!(addr as *const u8, std::ptr::null());
}
}

View file

@ -9,22 +9,27 @@
#![cfg(feature = "vulkano")]
use std::collections::BTreeMap as Map;
use std::convert::TryInto;
use std::iter::Empty;
use std::sync::Arc;
use base::MappedRegion;
use crate::rutabaga_gralloc::gralloc::{Gralloc, ImageAllocationInfo, ImageMemoryRequirements};
use crate::rutabaga_utils::*;
use vulkano::device::{Device, DeviceCreationError, DeviceExtensions};
use vulkano::image::{sys, ImageCreationError, ImageDimensions, ImageUsage};
use vulkano::image::{sys, ImageCreateFlags, ImageCreationError, ImageDimensions, ImageUsage};
use vulkano::instance::{
Instance, InstanceCreationError, InstanceExtensions, MemoryType, PhysicalDevice,
PhysicalDeviceType,
};
use vulkano::memory::{
DedicatedAlloc, DeviceMemoryAllocError, DeviceMemoryBuilder, ExternalMemoryHandleType,
MemoryRequirements,
DedicatedAlloc, DeviceMemoryAllocError, DeviceMemoryBuilder, DeviceMemoryMapping,
ExternalMemoryHandleType, MemoryRequirements,
};
use vulkano::memory::pool::AllocFromRequirementsFilter;
@ -32,7 +37,32 @@ use vulkano::sync::Sharing;
/// A gralloc implementation capable of allocation `VkDeviceMemory`.
pub struct VulkanoGralloc {
device: Arc<Device>,
devices: Map<PhysicalDeviceType, Arc<Device>>,
has_integrated_gpu: bool,
}
struct VulkanoMapping {
mapping: DeviceMemoryMapping,
size: usize,
}
impl VulkanoMapping {
pub fn new(mapping: DeviceMemoryMapping, size: usize) -> VulkanoMapping {
VulkanoMapping { mapping, size }
}
}
unsafe impl MappedRegion for VulkanoMapping {
/// Used for passing this region for hypervisor memory mappings. We trust crosvm to use this
/// safely.
fn as_ptr(&self) -> *mut u8 {
unsafe { self.mapping.as_ptr() }
}
/// Returns the size of the memory region in bytes.
fn size(&self) -> usize {
self.size
}
}
impl VulkanoGralloc {
@ -42,39 +72,52 @@ impl VulkanoGralloc {
// explanation of VK initialization.
let instance = Instance::new(None, &InstanceExtensions::none(), None)?;
// We should really check for integrated GPU versus dGPU.
let physical = PhysicalDevice::enumerate(&instance)
.next()
.ok_or(RutabagaError::Unsupported)?;
let mut devices: Map<PhysicalDeviceType, Arc<Device>> = Default::default();
let mut has_integrated_gpu = false;
let queue_family = physical
.queue_families()
.find(|&q| {
// We take the first queue family that supports graphics.
q.supports_graphics()
})
.ok_or(RutabagaError::Unsupported)?;
for physical in PhysicalDevice::enumerate(&instance) {
let queue_family = physical
.queue_families()
.find(|&q| {
// We take the first queue family that supports graphics.
q.supports_graphics()
})
.ok_or(RutabagaError::Unsupported)?;
let supported_extensions = DeviceExtensions::supported_by_device(physical);
let desired_extensions = DeviceExtensions {
khr_dedicated_allocation: true,
khr_get_memory_requirements2: true,
khr_external_memory: true,
khr_external_memory_fd: true,
ext_external_memory_dmabuf: true,
..DeviceExtensions::none()
};
let supported_extensions = DeviceExtensions::supported_by_device(physical);
let intersection = supported_extensions.intersection(&desired_extensions);
let desired_extensions = DeviceExtensions {
khr_dedicated_allocation: true,
khr_get_memory_requirements2: true,
khr_external_memory: true,
khr_external_memory_fd: true,
ext_external_memory_dmabuf: true,
..DeviceExtensions::none()
};
let (device, mut _queues) = Device::new(
physical,
physical.supported_features(),
&intersection,
[(queue_family, 0.5)].iter().cloned(),
)?;
let intersection = supported_extensions.intersection(&desired_extensions);
Ok(Box::new(VulkanoGralloc { device }))
let (device, mut _queues) = Device::new(
physical,
physical.supported_features(),
&intersection,
[(queue_family, 0.5)].iter().cloned(),
)?;
if device.physical_device().ty() == PhysicalDeviceType::IntegratedGpu {
has_integrated_gpu = true
}
// If we have two devices of the same type (two integrated GPUs), the old value is
// dropped. Vulkano is verbose enough such that a keener selection algorithm may
// be used, but the need for such complexity does not seem to exist now.
devices.insert(device.physical_device().ty(), device);
}
Ok(Box::new(VulkanoGralloc {
devices,
has_integrated_gpu,
}))
}
// This function is used safely in this module because gralloc does not:
@ -88,6 +131,16 @@ impl VulkanoGralloc {
&mut self,
info: ImageAllocationInfo,
) -> RutabagaResult<(sys::UnsafeImage, MemoryRequirements)> {
let device = if self.has_integrated_gpu {
self.devices
.get(&PhysicalDeviceType::IntegratedGpu)
.ok_or(RutabagaError::Unsupported)?
} else {
self.devices
.get(&PhysicalDeviceType::DiscreteGpu)
.ok_or(RutabagaError::Unsupported)?
};
let usage = match info.flags.uses_rendering() {
true => ImageUsage {
color_attachment: true,
@ -111,14 +164,14 @@ impl VulkanoGralloc {
let vulkan_format = info.drm_format.vulkan_format()?;
let (unsafe_image, memory_requirements) = sys::UnsafeImage::new(
self.device.clone(),
device.clone(),
usage,
vulkan_format,
ImageCreateFlags::none(),
ImageDimensions::Dim2d {
width: info.width,
height: info.height,
array_layers: 1,
cubemap_compatible: false,
},
1, /* number of samples */
1, /* mipmap count */
@ -133,11 +186,23 @@ impl VulkanoGralloc {
impl Gralloc for VulkanoGralloc {
fn supports_external_gpu_memory(&self) -> bool {
self.device.loaded_extensions().khr_external_memory
for device in self.devices.values() {
if !device.loaded_extensions().khr_external_memory {
return false;
}
}
true
}
fn supports_dmabuf(&self) -> bool {
self.device.loaded_extensions().ext_external_memory_dmabuf
for device in self.devices.values() {
if !device.loaded_extensions().ext_external_memory_dmabuf {
return false;
}
}
true
}
fn get_image_memory_requirements(
@ -148,6 +213,16 @@ impl Gralloc for VulkanoGralloc {
let (unsafe_image, memory_requirements) = unsafe { self.create_image(info)? };
let device = if self.has_integrated_gpu {
self.devices
.get(&PhysicalDeviceType::IntegratedGpu)
.ok_or(RutabagaError::Unsupported)?
} else {
self.devices
.get(&PhysicalDeviceType::DiscreteGpu)
.ok_or(RutabagaError::Unsupported)?
};
let planar_layout = info.drm_format.planar_layout()?;
// Safe because we created the image with the linear bit set and verified the format is
@ -188,13 +263,11 @@ impl Gralloc for VulkanoGralloc {
AllocFromRequirementsFilter::Allowed
};
let first_loop = self
.device
let first_loop = device
.physical_device()
.memory_types()
.map(|t| (t, AllocFromRequirementsFilter::Preferred));
let second_loop = self
.device
let second_loop = device
.physical_device()
.memory_types()
.map(|t| (t, AllocFromRequirementsFilter::Allowed));
@ -219,7 +292,7 @@ impl Gralloc for VulkanoGralloc {
reqs.vulkan_info = Some(VulkanInfo {
memory_idx: memory_type.id() as u32,
physical_device_idx: self.device.physical_device().index() as u32,
physical_device_idx: device.physical_device().index() as u32,
});
Ok(reqs)
@ -227,15 +300,26 @@ impl Gralloc for VulkanoGralloc {
fn allocate_memory(&mut self, reqs: ImageMemoryRequirements) -> RutabagaResult<RutabagaHandle> {
let (unsafe_image, memory_requirements) = unsafe { self.create_image(reqs.info)? };
let vulkan_info = reqs.vulkan_info.ok_or(RutabagaError::SpecViolation)?;
let memory_type = self
.device
let device = if self.has_integrated_gpu {
self.devices
.get(&PhysicalDeviceType::IntegratedGpu)
.ok_or(RutabagaError::Unsupported)?
} else {
self.devices
.get(&PhysicalDeviceType::DiscreteGpu)
.ok_or(RutabagaError::Unsupported)?
};
let memory_type = device
.physical_device()
.memory_type_by_id(vulkan_info.memory_idx)
.ok_or(RutabagaError::SpecViolation)?;
let (handle_type, rutabaga_type) =
match self.device.loaded_extensions().ext_external_memory_dmabuf {
match device.loaded_extensions().ext_external_memory_dmabuf {
true => (
ExternalMemoryHandleType {
dma_buf: true,
@ -252,7 +336,7 @@ impl Gralloc for VulkanoGralloc {
),
};
let dedicated = match self.device.loaded_extensions().khr_dedicated_allocation {
let dedicated = match device.loaded_extensions().khr_dedicated_allocation {
true => {
if memory_requirements.prefer_dedicated {
DedicatedAlloc::Image(&unsafe_image)
@ -264,7 +348,7 @@ impl Gralloc for VulkanoGralloc {
};
let device_memory =
DeviceMemoryBuilder::new(self.device.clone(), memory_type, reqs.size as usize)
DeviceMemoryBuilder::new(device.clone(), memory_type.id(), reqs.size as usize)
.dedicated_info(dedicated)
.export_info(handle_type)
.build()?;
@ -276,6 +360,43 @@ impl Gralloc for VulkanoGralloc {
handle_type: rutabaga_type,
})
}
/// Implementations must map the memory associated with the `resource_id` upon success.
fn import_and_map(
&mut self,
handle: RutabagaHandle,
vulkan_info: VulkanInfo,
size: u64,
) -> RutabagaResult<Box<dyn MappedRegion>> {
let device = self
.devices
.values()
.find(|device| {
device.physical_device().index() as u32 == vulkan_info.physical_device_idx
})
.ok_or(RutabagaError::Unsupported)?;
let handle_type = match handle.handle_type {
RUTABAGA_MEM_HANDLE_TYPE_DMABUF => ExternalMemoryHandleType {
dma_buf: true,
..ExternalMemoryHandleType::none()
},
RUTABAGA_MEM_HANDLE_TYPE_OPAQUE_FD => ExternalMemoryHandleType {
opaque_fd: true,
..ExternalMemoryHandleType::none()
},
_ => return Err(RutabagaError::Unsupported),
};
let valid_size: usize = size.try_into()?;
let device_memory =
DeviceMemoryBuilder::new(device.clone(), vulkan_info.memory_idx, valid_size)
.import_info(handle.os_handle.into(), handle_type)
.build()?;
let mapping = DeviceMemoryMapping::new(device.clone(), device_memory.clone(), 0, size, 0)?;
Ok(Box::new(VulkanoMapping::new(mapping, valid_size)))
}
}
// Vulkano should really define an universal type that wraps all these errors, say

View file

@ -6,6 +6,7 @@
use std::fmt::{self, Display};
use std::io::Error as IoError;
use std::num::TryFromIntError;
use std::os::raw::c_void;
use std::path::PathBuf;
use std::str::Utf8Error;
@ -154,6 +155,8 @@ pub enum RutabagaError {
SpecViolation,
/// System error returned as a result of rutabaga library operation.
SysError(SysError),
/// An attempted integer conversion failed.
TryFromIntError(TryFromIntError),
/// The command is unsupported.
Unsupported,
/// Utf8 error.
@ -209,6 +212,7 @@ impl Display for RutabagaError {
ComponentError(ret) => write!(f, "rutabaga component failed with error {}", ret),
SpecViolation => write!(f, "violation of the rutabaga spec"),
SysError(e) => write!(f, "rutabaga received a system error: {}", e),
TryFromIntError(e) => write!(f, "int conversion failed: {}", e),
Unsupported => write!(f, "feature or function unsupported"),
Utf8Error(e) => write!(f, "an utf8 error occured: {}", e),
VolatileMemoryError(e) => write!(f, "noticed a volatile memory error {}", e),
@ -238,6 +242,12 @@ impl From<SysError> for RutabagaError {
}
}
impl From<TryFromIntError> for RutabagaError {
fn from(e: TryFromIntError) -> RutabagaError {
RutabagaError::TryFromIntError(e)
}
}
impl From<Utf8Error> for RutabagaError {
fn from(e: Utf8Error) -> RutabagaError {
RutabagaError::Utf8Error(e)

View file

@ -32,7 +32,10 @@ use base::{
};
use hypervisor::{IrqRoute, IrqSource, Vm};
use resources::{Alloc, MmioType, SystemAllocator};
use rutabaga_gfx::{DrmFormat, ImageAllocationInfo, RutabagaGralloc, RutabagaGrallocFlags};
use rutabaga_gfx::{
DrmFormat, ImageAllocationInfo, RutabagaGralloc, RutabagaGrallocFlags, RutabagaHandle,
VulkanInfo,
};
use sync::Mutex;
use vm_memory::GuestAddress;
@ -263,6 +266,17 @@ pub enum VmMemoryRequest {
RegisterFdAtPciBarOffset(Alloc, SafeDescriptor, usize, u64),
/// Similar to RegisterFdAtPciBarOffset, but is for buffers in the current address space.
RegisterHostPointerAtPciBarOffset(Alloc, u64),
/// Similiar to `RegisterFdAtPciBarOffset`, but uses Vulkano to map the resource instead of
/// the mmap system call.
RegisterVulkanMemoryAtPciBarOffset {
alloc: Alloc,
descriptor: SafeDescriptor,
handle_type: u32,
memory_idx: u32,
physical_device_idx: u32,
offset: u64,
size: u64,
},
/// Unregister the given memory slot that was previously registered with `RegisterMemory*`.
UnregisterMemory(MemSlot),
/// Allocate GPU buffer of a given size/format and register the memory into guest address space.
@ -292,14 +306,14 @@ impl VmMemoryRequest {
/// `VmMemoryResponse` with the intended purpose of sending the response back over the socket
/// that received this `VmMemoryResponse`.
pub fn execute(
&self,
self,
vm: &mut impl Vm,
sys_allocator: &mut SystemAllocator,
map_request: Arc<Mutex<Option<ExternalMapping>>>,
gralloc: &mut RutabagaGralloc,
) -> VmMemoryResponse {
use self::VmMemoryRequest::*;
match *self {
match self {
RegisterMemory(ref shm) => {
match register_memory(vm, sys_allocator, shm, shm.size() as usize, None) {
Ok((pfn, slot)) => VmMemoryResponse::RegisterMemory { pfn, slot },
@ -323,7 +337,39 @@ impl VmMemoryRequest {
.ok_or_else(|| VmMemoryResponse::Err(SysError::new(EINVAL)))
.unwrap();
match register_memory_hva(vm, sys_allocator, Box::new(mem), (alloc, offset)) {
match register_host_pointer(vm, sys_allocator, Box::new(mem), (alloc, offset)) {
Ok((pfn, slot)) => VmMemoryResponse::RegisterMemory { pfn, slot },
Err(e) => VmMemoryResponse::Err(e),
}
}
RegisterVulkanMemoryAtPciBarOffset {
alloc,
descriptor,
handle_type,
memory_idx,
physical_device_idx,
offset,
size,
} => {
let mapped_region = match gralloc.import_and_map(
RutabagaHandle {
os_handle: descriptor,
handle_type,
},
VulkanInfo {
memory_idx,
physical_device_idx,
},
size,
) {
Ok(mapped_region) => mapped_region,
Err(e) => {
error!("gralloc failed to import and map: {}", e);
return VmMemoryResponse::Err(SysError::new(EINVAL));
}
};
match register_host_pointer(vm, sys_allocator, mapped_region, (alloc, offset)) {
Ok((pfn, slot)) => VmMemoryResponse::RegisterMemory { pfn, slot },
Err(e) => VmMemoryResponse::Err(e),
}
@ -908,7 +954,7 @@ fn register_memory(
Ok((addr >> 12, slot))
}
fn register_memory_hva(
fn register_host_pointer(
vm: &mut impl Vm,
allocator: &mut SystemAllocator,
mem: Box<dyn MappedRegion>,