use std::os::fd::{AsRawFd, IntoRawFd};
use crate::gbm::{Allocator, Scanout, ScanoutLayout, validate_plane_count};
use crate::kms::{Card, Output};
use ash::vk;
use drm::buffer::{DrmFourcc, DrmModifier};
use drm::control::Device as ControlDevice;
use super::Gpu;
pub struct Target {
pub(crate) image: vk::Image,
memory: vk::DeviceMemory,
pub(crate) view: vk::ImageView,
pub(crate) pool: vk::CommandPool,
pub(crate) cmd: vk::CommandBuffer,
pub(crate) fence: vk::Fence,
pub width: u32,
pub height: u32,
}
impl Target {
pub(crate) fn extent(&self) -> vk::Extent2D {
vk::Extent2D {
width: self.width,
height: self.height,
}
}
}
const RANGE: vk::ImageSubresourceRange = vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
};
fn import_layout(layout: &ScanoutLayout) -> Result<(u64, Vec<vk::SubresourceLayout>), String> {
validate_plane_count(layout.planes.len()).map_err(|e| e.to_string())?;
if layout.fourcc != drm::buffer::DrmFourcc::Xrgb8888 as u32
&& layout.fourcc != drm::buffer::DrmFourcc::Argb8888 as u32
{
return Err(format!(
"Vulkan import: unsupported scanout fourcc {:#x}; expected XRGB8888 or ARGB8888",
layout.fourcc
));
}
let modifier = layout.modifier.ok_or("Vulkan import: GBM returned DRM_FORMAT_MOD_INVALID (implicit layout); scanout requires an explicit supported modifier")?;
let planes = layout
.planes
.iter()
.map(|p| {
vk::SubresourceLayout::default()
.offset(p.offset as u64)
.row_pitch(p.pitch as u64)
})
.collect();
Ok((modifier, planes))
}
struct ImportResources<'a> {
device: &'a ash::Device,
image: vk::Image,
memory: vk::DeviceMemory,
view: vk::ImageView,
pool: vk::CommandPool,
armed: bool,
}
impl<'a> ImportResources<'a> {
fn new(device: &'a ash::Device) -> Self {
Self {
device,
image: vk::Image::null(),
memory: vk::DeviceMemory::null(),
view: vk::ImageView::null(),
pool: vk::CommandPool::null(),
armed: true,
}
}
}
impl Drop for ImportResources<'_> {
fn drop(&mut self) {
if self.armed {
unsafe {
self.device.destroy_command_pool(self.pool, None);
self.device.destroy_image_view(self.view, None);
self.device.destroy_image(self.image, None);
self.device.free_memory(self.memory, None);
}
}
}
}
fn modifier_candidates(
scanout: &[u64], rendering: &[vk::DrmFormatModifierPropertiesEXT],
) -> Vec<u64> {
let required = vk::FormatFeatureFlags::COLOR_ATTACHMENT | vk::FormatFeatureFlags::TRANSFER_DST;
let invalid = u64::from(DrmModifier::Invalid);
let mut candidates = Vec::new();
for entry in rendering {
let modifier = entry.drm_format_modifier;
if modifier != invalid && scanout.contains(&modifier)
&& entry.drm_format_modifier_tiling_features.contains(required)
&& (1..=4).contains(&entry.drm_format_modifier_plane_count)
&& !candidates.contains(&modifier)
{
candidates.push(modifier);
}
}
candidates.sort_by_key(|modifier| *modifier != 0);
candidates
}
fn try_modifiers<T>(
candidates: &[u64], mut allocate: impl FnMut(u64) -> Result<T, String>,
) -> Result<(T, u64), String> {
if candidates.is_empty() {
return Err("no shared explicit modifier for Vulkan rendering and the KMS plane".into());
}
let mut failures = Vec::new();
for &modifier in candidates {
match allocate(modifier) {
Ok(value) => return Ok((value, modifier)),
Err(error) => failures.push(format!("{modifier:#x}: {error}")),
}
}
Err(format!("no compatible explicit scanout allocation:\n {}", failures.join("\n ")))
}
impl Gpu {
pub fn create_scanout_target(
&self, alloc: &Allocator, card: &Card, output: &Output,
width: u32, height: u32, format: DrmFourcc,
) -> Result<(Scanout, Target), String> {
if !matches!(format, DrmFourcc::Xrgb8888 | DrmFourcc::Argb8888) {
return Err(format!("unsupported Vulkan scanout format {format:?}"));
}
let scanout = output.scanout_modifiers(card, format)
.map_err(|e| format!("KMS scanout modifiers: {e}"))?;
let rendering = self.scanout_modifier_properties();
let candidates = modifier_candidates(&scanout, &rendering);
let ((buffer, target), modifier) = try_modifiers(&candidates, |modifier| {
let buffer = alloc.alloc_with_modifiers(card, width, height, format, &[modifier])
.map_err(|e| format!("GBM allocation: {e}"))?;
match self.import(&buffer) {
Ok(target) => Ok((buffer, target)),
Err(error) => {
match card.destroy_framebuffer(buffer.fb) {
Ok(()) => Err(error),
Err(cleanup) => Err(format!("{error}; framebuffer cleanup: {cleanup}")),
}
}
}
})?;
println!("scanout: {format:?} {width}x{height} modifier={modifier:#x} planes={}", buffer.bo.plane_count());
Ok((buffer, target))
}
pub fn import(&self, scanout: &Scanout) -> Result<Target, String> {
let exported = scanout
.export()
.map_err(|e| format!("export scanout: {e}"))?;
let (modifier, layouts) = import_layout(&exported.layout)?;
if !exported
.shares_backing()
.map_err(|e| format!("dma-buf identity: {e}"))?
{
return Err("Vulkan import: scanout memory planes use separate dma-bufs; a shared allocation is required".into());
}
let (width, height) = (exported.layout.width, exported.layout.height);
self.validate_scanout_modifier(modifier, layouts.len() as u32, width, height)?;
let fd = exported
.planes
.into_iter()
.next()
.ok_or("scanout has no memory planes")?;
let dmabuf = vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT;
let mut ext_img = vk::ExternalMemoryImageCreateInfo::default().handle_types(dmabuf);
let mut drm = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default()
.drm_format_modifier(modifier)
.plane_layouts(&layouts);
let img_info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(vk::Format::B8G8R8A8_UNORM)
.extent(vk::Extent3D {
width,
height,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT)
.usage(vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::COLOR_ATTACHMENT)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.initial_layout(vk::ImageLayout::UNDEFINED)
.push_next(&mut ext_img)
.push_next(&mut drm);
let mut resources = ImportResources::new(&self.device);
let image = unsafe { self.device.create_image(&img_info, None) }
.map_err(|e| format!("image: {e}"))?;
resources.image = image;
let raw_fd = fd.as_raw_fd();
let type_bits = {
let ext = ash::khr::external_memory_fd::Device::new(&self.instance, &self.device);
let mut props = vk::MemoryFdPropertiesKHR::default();
unsafe {
(ext.fp().get_memory_fd_properties_khr)(
self.device.handle(),
dmabuf,
raw_fd,
&mut props,
)
}
.result()
.map_err(|e| format!("memory fd properties: {e}"))?;
let req = unsafe { self.device.get_image_memory_requirements(image) };
req.memory_type_bits & props.memory_type_bits
};
let mem_type = self
.find_memory_type(type_bits)
.ok_or("no importable memory type")?;
let req = unsafe { self.device.get_image_memory_requirements(image) };
let mut dedicated = vk::MemoryDedicatedAllocateInfo::default().image(image);
let mut import = vk::ImportMemoryFdInfoKHR::default()
.handle_type(dmabuf)
.fd(raw_fd);
let alloc = vk::MemoryAllocateInfo::default()
.allocation_size(req.size)
.memory_type_index(mem_type)
.push_next(&mut import)
.push_next(&mut dedicated);
let memory = unsafe { self.device.allocate_memory(&alloc, None) }
.map_err(|e| format!("import memory: {e}"))?;
let _ = fd.into_raw_fd();
resources.memory = memory;
unsafe { self.device.bind_image_memory(image, memory, 0) }
.map_err(|e| format!("bind: {e}"))?;
let view = unsafe {
self.device.create_image_view(
&vk::ImageViewCreateInfo::default()
.image(image)
.view_type(vk::ImageViewType::TYPE_2D)
.format(vk::Format::B8G8R8A8_UNORM)
.subresource_range(RANGE),
None,
)
}
.map_err(|e| format!("image view: {e}"))?;
resources.view = view;
let pool = unsafe {
self.device.create_command_pool(
&vk::CommandPoolCreateInfo::default().queue_family_index(self.queue_family),
None,
)
}
.map_err(|e| format!("command pool: {e}"))?;
resources.pool = pool;
let cmd = unsafe {
self.device.allocate_command_buffers(
&vk::CommandBufferAllocateInfo::default()
.command_pool(pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(1),
)
}
.map_err(|e| format!("command buffer: {e}"))?[0];
let fence = unsafe {
self.device
.create_fence(&vk::FenceCreateInfo::default(), None)
}
.map_err(|e| format!("fence: {e}"))?;
resources.armed = false;
Ok(Target {
image,
memory,
view,
pool,
cmd,
fence,
width,
height,
})
}
fn scanout_modifier_properties(&self) -> Vec<vk::DrmFormatModifierPropertiesEXT> {
let mut list = vk::DrmFormatModifierPropertiesListEXT::default();
let mut properties = vk::FormatProperties2::default().push_next(&mut list);
unsafe {
self.instance.get_physical_device_format_properties2(
self.physical,
vk::Format::B8G8R8A8_UNORM,
&mut properties,
)
};
let mut modifiers = vec![
vk::DrmFormatModifierPropertiesEXT::default();
list.drm_format_modifier_count as usize
];
let mut list = vk::DrmFormatModifierPropertiesListEXT::default()
.drm_format_modifier_properties(&mut modifiers);
let mut properties = vk::FormatProperties2::default().push_next(&mut list);
unsafe {
self.instance.get_physical_device_format_properties2(
self.physical,
vk::Format::B8G8R8A8_UNORM,
&mut properties,
)
};
let count = list.drm_format_modifier_count as usize;
modifiers.truncate(count);
modifiers
}
fn validate_scanout_modifier(
&self,
modifier: u64,
plane_count: u32,
width: u32,
height: u32,
) -> Result<(), String> {
let modifiers = self.scanout_modifier_properties();
let supported = modifiers
.iter()
.find(|m| m.drm_format_modifier == modifier)
.ok_or_else(|| {
format!(
"Vulkan import: DRM modifier {modifier:#x} is unsupported for B8G8R8A8_UNORM"
)
})?;
if supported.drm_format_modifier_plane_count != plane_count {
return Err(format!(
"Vulkan import: GBM reports {plane_count} memory planes for modifier {modifier:#x}; Vulkan requires {}",
supported.drm_format_modifier_plane_count
));
}
let mut drm = vk::PhysicalDeviceImageDrmFormatModifierInfoEXT::default()
.drm_format_modifier(modifier)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
let mut external = vk::PhysicalDeviceExternalImageFormatInfo::default()
.handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT);
let info = vk::PhysicalDeviceImageFormatInfo2::default()
.format(vk::Format::B8G8R8A8_UNORM)
.ty(vk::ImageType::TYPE_2D)
.tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT)
.usage(vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::COLOR_ATTACHMENT)
.push_next(&mut drm)
.push_next(&mut external);
let mut external_properties = vk::ExternalImageFormatProperties::default();
let mut properties =
vk::ImageFormatProperties2::default().push_next(&mut external_properties);
unsafe { self.instance.get_physical_device_image_format_properties2(self.physical, &info, &mut properties) }
.map_err(|e| format!("Vulkan import: modifier {modifier:#x} cannot be used for a renderable dma-buf image: {e}"))?;
let limits = properties.image_format_properties;
if width > limits.max_extent.width || height > limits.max_extent.height {
return Err(format!(
"Vulkan import: scanout {width}x{height} exceeds modifier image limits"
));
}
if !external_properties
.external_memory_properties
.external_memory_features
.contains(vk::ExternalMemoryFeatureFlags::IMPORTABLE)
{
return Err(format!(
"Vulkan import: modifier {modifier:#x} does not support dma-buf memory import"
));
}
Ok(())
}
pub fn destroy_target(&self, t: Target) {
unsafe {
self.device.destroy_fence(t.fence, None);
self.device.destroy_command_pool(t.pool, None);
self.device.destroy_image_view(t.view, None);
self.device.destroy_image(t.image, None);
self.device.free_memory(t.memory, None);
}
}
#[cfg(test)]
pub(crate) fn offscreen_target(&self, width: u32, height: u32) -> Result<Target, String> {
let d = &self.device;
let format = vk::Format::B8G8R8A8_UNORM;
let image = unsafe {
d.create_image(
&vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(format)
.extent(vk::Extent3D { width, height, depth: 1 })
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::OPTIMAL)
.usage(vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::TRANSFER_SRC),
None,
)
}
.map_err(|e| format!("offscreen image: {e}"))?;
let req = unsafe { d.get_image_memory_requirements(image) };
let mt = self
.find_memory_type_with(req.memory_type_bits, vk::MemoryPropertyFlags::DEVICE_LOCAL)
.ok_or("no device-local memory")?;
let memory = unsafe {
d.allocate_memory(&vk::MemoryAllocateInfo::default().allocation_size(req.size).memory_type_index(mt), None)
}
.map_err(|e| format!("offscreen mem: {e}"))?;
unsafe { d.bind_image_memory(image, memory, 0) }.map_err(|e| format!("bind: {e}"))?;
let view = unsafe {
d.create_image_view(
&vk::ImageViewCreateInfo::default().image(image).view_type(vk::ImageViewType::TYPE_2D).format(format).subresource_range(RANGE),
None,
)
}
.map_err(|e| format!("view: {e}"))?;
let pool = unsafe {
d.create_command_pool(&vk::CommandPoolCreateInfo::default().queue_family_index(self.queue_family), None)
}
.map_err(|e| format!("pool: {e}"))?;
let cmd = unsafe {
d.allocate_command_buffers(&vk::CommandBufferAllocateInfo::default().command_pool(pool).command_buffer_count(1))
}
.map_err(|e| format!("cmd: {e}"))?[0];
let fence = unsafe { d.create_fence(&vk::FenceCreateInfo::default(), None) }.map_err(|e| format!("fence: {e}"))?;
Ok(Target { image, memory, view, pool, cmd, fence, width, height })
}
#[cfg(test)]
pub(crate) fn read_target(&self, t: &Target) -> Result<Vec<u8>, String> {
let d = &self.device;
let size = (t.width * t.height * 4) as u64;
let buf = unsafe {
d.create_buffer(&vk::BufferCreateInfo::default().size(size).usage(vk::BufferUsageFlags::TRANSFER_DST), None)
}
.map_err(|e| format!("buffer: {e}"))?;
let req = unsafe { d.get_buffer_memory_requirements(buf) };
let mt = self
.find_memory_type_with(req.memory_type_bits, vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT)
.ok_or("no host memory")?;
let mem = unsafe { d.allocate_memory(&vk::MemoryAllocateInfo::default().allocation_size(req.size).memory_type_index(mt), None) }
.map_err(|e| format!("mem: {e}"))?;
unsafe {
d.bind_buffer_memory(buf, mem, 0).map_err(|e| format!("bind: {e}"))?;
d.reset_command_pool(t.pool, vk::CommandPoolResetFlags::empty()).map_err(|e| format!("reset: {e}"))?;
d.begin_command_buffer(t.cmd, &vk::CommandBufferBeginInfo::default()).map_err(|e| format!("begin: {e}"))?;
let region = vk::BufferImageCopy::default()
.image_subresource(vk::ImageSubresourceLayers::default().aspect_mask(vk::ImageAspectFlags::COLOR).layer_count(1))
.image_extent(vk::Extent3D { width: t.width, height: t.height, depth: 1 });
d.cmd_copy_image_to_buffer(t.cmd, t.image, vk::ImageLayout::GENERAL, buf, &[region]);
d.end_command_buffer(t.cmd).map_err(|e| format!("end: {e}"))?;
let cmds = [t.cmd];
d.queue_submit(self.queue, &[vk::SubmitInfo::default().command_buffers(&cmds)], t.fence).map_err(|e| format!("submit: {e}"))?;
d.wait_for_fences(&[t.fence], true, u64::MAX).map_err(|e| format!("wait: {e}"))?;
d.reset_fences(&[t.fence]).map_err(|e| format!("reset fence: {e}"))?;
let ptr = d.map_memory(mem, 0, size, vk::MemoryMapFlags::empty()).map_err(|e| format!("map: {e}"))? as *const u8;
let pixels = std::slice::from_raw_parts(ptr, size as usize).to_vec();
d.unmap_memory(mem);
d.destroy_buffer(buf, None);
d.free_memory(mem, None);
Ok(pixels)
}
}
fn find_memory_type(&self, type_bits: u32) -> Option<u32> {
let props = unsafe {
self.instance
.get_physical_device_memory_properties(self.physical)
};
(0..props.memory_type_count).find(|&i| type_bits & (1 << i) != 0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gbm::PlaneLayout;
#[test]
fn allocation_candidates_require_both_scanout_and_rendering_support() {
let required = vk::FormatFeatureFlags::COLOR_ATTACHMENT | vk::FormatFeatureFlags::TRANSFER_DST;
let tiled = 0x0200_0000_0000_0001;
let invalid = u64::from(DrmModifier::Invalid);
let entry = |modifier, planes, features| vk::DrmFormatModifierPropertiesEXT {
drm_format_modifier: modifier,
drm_format_modifier_plane_count: planes,
drm_format_modifier_tiling_features: features,
};
let rendering = [
entry(tiled, 3, required),
entry(0, 1, required),
entry(tiled, 3, required),
entry(100, 1, vk::FormatFeatureFlags::SAMPLED_IMAGE),
entry(101, 1, required),
entry(102, 0, required),
entry(103, 5, required),
entry(invalid, 1, required),
];
assert_eq!(
modifier_candidates(&[0, tiled, 100, 102, 103, invalid], &rendering),
[0, tiled],
);
}
#[test]
fn failed_linear_allocation_retries_tiled_modifier() {
let tiled = 0x0200_0000_0000_0001;
let mut attempted = Vec::new();
let (value, modifier) = try_modifiers(&[0, tiled, 99], |modifier| {
attempted.push(modifier);
if modifier == 0 { Err("linear rendering allocation unsupported".into()) }
else { Ok("allocated and imported") }
}).unwrap();
assert_eq!(attempted, [0, tiled]);
assert_eq!(modifier, tiled);
assert_eq!(value, "allocated and imported");
}
#[test]
fn exhausted_allocations_report_each_modifier_failure() {
let error = try_modifiers(&[0, 17], |modifier| {
Err::<(), _>(format!("allocation failure {modifier}"))
}).unwrap_err();
assert!(error.contains("0x0: allocation failure 0"));
assert!(error.contains("0x11: allocation failure 17"));
let error = try_modifiers::<()>(&[], |_| panic!("no modifier should be attempted"))
.unwrap_err();
assert!(error.contains("no shared explicit modifier"));
}
fn layout(modifier: Option<u64>) -> ScanoutLayout {
ScanoutLayout {
width: 1920,
height: 1080,
fourcc: drm::buffer::DrmFourcc::Xrgb8888 as u32,
modifier,
planes: vec![
PlaneLayout {
offset: 0,
pitch: 7680,
},
PlaneLayout {
offset: 8_388_608,
pitch: 512,
},
PlaneLayout {
offset: 8_650_752,
pitch: 256,
},
],
}
}
#[test]
fn explicit_import_keeps_all_modifier_memory_planes() {
let source = layout(Some(0x0200_0000_0000_0001));
let (modifier, planes) = import_layout(&source).unwrap();
assert_eq!(modifier, source.modifier.unwrap());
assert_eq!(planes.len(), 3);
for (plane, source) in planes.iter().zip(&source.planes) {
assert_eq!(plane.offset, source.offset as u64);
assert_eq!(plane.row_pitch, source.pitch as u64);
assert_eq!((plane.size, plane.array_pitch, plane.depth_pitch), (0, 0, 0));
}
}
#[test]
fn implicit_modifier_is_rejected_before_vulkan_calls() {
let error = import_layout(&layout(None)).unwrap_err();
assert!(error.contains("DRM_FORMAT_MOD_INVALID"));
assert!(error.contains("explicit supported modifier"));
assert_eq!(import_layout(&layout(Some(0))).unwrap().0, 0);
}
#[test]
fn xrgb_and_argb_use_the_same_bgra_vulkan_format() {
for fourcc in [
drm::buffer::DrmFourcc::Xrgb8888,
drm::buffer::DrmFourcc::Argb8888,
] {
let mut source = layout(Some(0));
source.fourcc = fourcc as u32;
assert!(import_layout(&source).is_ok());
}
}
#[test]
fn unsupported_pixel_format_is_rejected() {
let mut source = layout(Some(0));
source.fourcc = drm::buffer::DrmFourcc::Nv12 as u32;
assert!(
import_layout(&source)
.unwrap_err()
.contains("expected XRGB8888")
);
}
}