use ash::vk;
use std::ffi::CStr;
use std::os::fd::{AsRawFd, IntoRawFd, OwnedFd, RawFd};
use crate::{HostWgpuContext, InteropError, UnsupportedReason, VulkanExternalImage};
pub(crate) fn required_device_extensions() -> [&'static CStr; 4] {
[
ash::ext::external_memory_dma_buf::NAME,
ash::ext::image_drm_format_modifier::NAME,
ash::ext::queue_family_foreign::NAME,
ash::khr::external_memory_fd::NAME,
]
}
pub(crate) fn base_dmabuf_device_extensions() -> [&'static CStr; 3] {
[
ash::ext::external_memory_dma_buf::NAME,
ash::ext::image_drm_format_modifier::NAME,
ash::khr::external_memory_fd::NAME,
]
}
#[derive(Clone, Copy, Debug)]
pub struct VulkanDmaBufPlane {
pub buffer_index: usize,
pub offset: u64,
pub stride: u64,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum VulkanDmaBufQueueOwnership {
Foreign,
LocalUninitialized,
}
#[derive(Debug)]
pub struct VulkanDmaBufImport {
size: dpi::PhysicalSize<u32>,
format: wgpu::TextureFormat,
drm_format: u32,
drm_modifier: u64,
buffers: Vec<OwnedFd>,
planes: Vec<VulkanDmaBufPlane>,
queue_ownership: VulkanDmaBufQueueOwnership,
}
impl VulkanDmaBufImport {
pub fn new(
size: dpi::PhysicalSize<u32>,
format: wgpu::TextureFormat,
drm_format: u32,
drm_modifier: u64,
buffers: Vec<OwnedFd>,
planes: Vec<VulkanDmaBufPlane>,
queue_ownership: VulkanDmaBufQueueOwnership,
) -> Result<Self, InteropError> {
validate_plane_layout(&buffers, &planes)?;
Ok(Self {
size,
format,
drm_format,
drm_modifier,
buffers,
planes,
queue_ownership,
})
}
pub unsafe fn from_raw_owned_parts(
size: dpi::PhysicalSize<u32>,
format: wgpu::TextureFormat,
drm_format: u32,
drm_modifier: u64,
buffers: Vec<RawFd>,
planes: Vec<VulkanDmaBufPlane>,
queue_ownership: VulkanDmaBufQueueOwnership,
) -> Result<Self, InteropError> {
use std::os::fd::FromRawFd;
let buffers = buffers
.into_iter()
.map(|fd| unsafe { OwnedFd::from_raw_fd(fd) })
.collect();
Self::new(
size,
format,
drm_format,
drm_modifier,
buffers,
planes,
queue_ownership,
)
}
}
struct PlaneFdGuard(Vec<OwnedFd>);
impl PlaneFdGuard {
fn new(buffers: Vec<OwnedFd>) -> Self {
Self(buffers)
}
fn raw(&self, index: usize) -> RawFd {
self.0[index].as_raw_fd()
}
fn transfer_to_vulkan(&mut self, index: usize) {
let _ = self.0.swap_remove(index).into_raw_fd();
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct FdIdentity {
device: libc::dev_t,
inode: libc::ino_t,
}
fn fd_identity(fd: RawFd) -> Option<FdIdentity> {
let mut stat = std::mem::MaybeUninit::<libc::stat>::uninit();
if unsafe { libc::fstat(fd, stat.as_mut_ptr()) } != 0 {
return None;
}
let stat = unsafe { stat.assume_init() };
Some(FdIdentity {
device: stat.st_dev,
inode: stat.st_ino,
})
}
fn planes_share_kernel_object(buffers: &[OwnedFd], planes: &[VulkanDmaBufPlane]) -> bool {
if planes.len() <= 1 {
return true;
}
let Some(first) = fd_identity(buffers[planes[0].buffer_index].as_raw_fd()) else {
return false;
};
planes[1..]
.iter()
.all(|plane| fd_identity(buffers[plane.buffer_index].as_raw_fd()) == Some(first))
}
fn validate_plane_layout(
buffers: &[OwnedFd],
planes: &[VulkanDmaBufPlane],
) -> Result<(), InteropError> {
if planes.is_empty() {
return Err(InteropError::InvalidFrame("DMABUF has no planes"));
}
if buffers.is_empty() {
return Err(InteropError::InvalidFrame(
"DMABUF has no descriptor buffers",
));
}
if planes
.iter()
.any(|plane| plane.buffer_index >= buffers.len())
{
return Err(InteropError::InvalidFrame(
"DMABUF plane buffer index is out of range",
));
}
if !planes_share_kernel_object(buffers, planes) {
return Err(InteropError::Unsupported(
UnsupportedReason::VulkanDisjointDmabufNotSupported,
));
}
Ok(())
}
fn map_format(format: wgpu::TextureFormat) -> Result<vk::Format, InteropError> {
match format {
wgpu::TextureFormat::Rgba8Unorm => Ok(vk::Format::R8G8B8A8_UNORM),
wgpu::TextureFormat::Rgba8UnormSrgb => Ok(vk::Format::R8G8B8A8_SRGB),
wgpu::TextureFormat::Bgra8Unorm => Ok(vk::Format::B8G8R8A8_UNORM),
wgpu::TextureFormat::Bgra8UnormSrgb => Ok(vk::Format::B8G8R8A8_SRGB),
other => Err(InteropError::Vulkan(format!(
"DMABUF import does not support wgpu format {other:?}"
))),
}
}
fn map_drm_format(
drm_format: u32,
fallback: wgpu::TextureFormat,
) -> Result<vk::Format, InteropError> {
const DRM_FORMAT_ARGB8888: u32 = 0x3432_5241; const DRM_FORMAT_ABGR8888: u32 = 0x3432_4241; const DRM_FORMAT_XRGB8888: u32 = 0x3432_5258; const DRM_FORMAT_XBGR8888: u32 = 0x3432_4258;
match drm_format {
DRM_FORMAT_ARGB8888 | DRM_FORMAT_XRGB8888 => Ok(vk::Format::B8G8R8A8_UNORM),
DRM_FORMAT_ABGR8888 | DRM_FORMAT_XBGR8888 => Ok(vk::Format::R8G8B8A8_UNORM),
0 => map_format(fallback),
other => Err(InteropError::Vulkan(format!(
"DMABUF import does not support DRM fourcc {other:#010x}"
))),
}
}
pub fn import_dmabuf(
frame: VulkanDmaBufImport,
host: &HostWgpuContext,
) -> Result<wgpu::Texture, InteropError> {
let mut plane_fds = PlaneFdGuard::new(frame.buffers);
if host.backend != crate::InteropBackend::Vulkan {
return Err(InteropError::BackendMismatch {
expected: "Vulkan",
actual: "non-Vulkan",
});
}
if !host.dmabuf_support {
return Err(InteropError::Unsupported(
UnsupportedReason::VulkanDmabufExtensionNotEnabled,
));
}
if frame.queue_ownership == VulkanDmaBufQueueOwnership::Foreign
&& !host_has_foreign_queue_support(host)
{
return Err(InteropError::Unsupported(
UnsupportedReason::VulkanForeignQueueExtensionNotEnabled,
));
}
if frame.size.width == 0 || frame.size.height == 0 {
return Err(InteropError::InvalidFrame("DMABUF dimensions are zero"));
}
validate_plane_layout(&plane_fds.0, &frame.planes)?;
#[cfg(not(feature = "wgpu-30"))]
if frame.queue_ownership == VulkanDmaBufQueueOwnership::Foreign {
return Err(InteropError::Vulkan(
"foreign-queue DMABUF import requires wgpu 30 so the established \
RESOURCE state can be registered at the HAL boundary"
.into(),
));
}
let vk_format = map_drm_format(frame.drm_format, frame.format)?;
let width = frame.size.width;
let height = frame.size.height;
unsafe {
let hal_device = host.device.as_hal::<wgpu::wgc::api::Vulkan>().ok_or(
InteropError::BackendMismatch {
expected: "Vulkan",
actual: "non-Vulkan",
},
)?;
let vk_device = hal_device.raw_device().clone();
let vk_instance = hal_device.shared_instance().raw_instance().clone();
let physical_device = hal_device.raw_physical_device();
let plane_layouts: Vec<vk::SubresourceLayout> = frame
.planes
.iter()
.map(|plane| vk::SubresourceLayout {
offset: plane.offset,
size: 0,
row_pitch: plane.stride,
array_pitch: 0,
depth_pitch: 0,
})
.collect();
let mut drm_modifier_info = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default()
.drm_format_modifier(frame.drm_modifier)
.plane_layouts(&plane_layouts);
let mut external_memory_info = vk::ExternalMemoryImageCreateInfo::default()
.handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT);
let image_create_info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(vk_format)
.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::SAMPLED | vk::ImageUsageFlags::TRANSFER_SRC)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.initial_layout(vk::ImageLayout::UNDEFINED)
.push_next(&mut external_memory_info)
.push_next(&mut drm_modifier_info);
let vulkan_image = vk_device
.create_image(&image_create_info, None)
.map_err(|err| InteropError::Vulkan(format!("vkCreateImage (DMABUF): {err}")))?;
let memory = match allocate_and_bind_dmabuf_memory(
&vk_device,
&vk_instance,
physical_device,
vulkan_image,
plane_fds.raw(frame.planes[0].buffer_index),
frame.planes[0].buffer_index,
&mut plane_fds,
) {
Ok(memory) => memory,
Err(err) => {
vk_device.destroy_image(vulkan_image, None);
return Err(err);
}
};
let initial_state = match frame.queue_ownership {
VulkanDmaBufQueueOwnership::Foreign => {
if let Err(err) = acquire_from_foreign_queue(
&vk_device,
hal_device.raw_queue(),
hal_device.queue_family_index(),
vulkan_image,
) {
vk_device.free_memory(memory, None);
vk_device.destroy_image(vulkan_image, None);
return Err(err);
}
wgpu::TextureUses::RESOURCE
}
VulkanDmaBufQueueOwnership::LocalUninitialized => wgpu::TextureUses::UNINITIALIZED,
};
let hal_descriptor = wgpu_hal::TextureDescriptor {
label: Some("grafting-dmabuf-import"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: frame.format,
usage: wgpu::TextureUses::RESOURCE | wgpu::TextureUses::COPY_SRC,
memory_flags: wgpu_hal::MemoryFlags::empty(),
view_formats: Vec::new(),
};
let vk_device_for_drop = vk_device.clone();
let hal_texture = hal_device.texture_from_raw(
vulkan_image,
&hal_descriptor,
Some(Box::new(move || {
vk_device_for_drop.destroy_image(vulkan_image, None);
vk_device_for_drop.free_memory(memory, None);
})),
wgpu_hal::vulkan::TextureMemory::External,
);
Ok(crate::wgpu_compat::create_texture_from_hal::<
wgpu_hal::api::Vulkan,
>(
&host.device,
hal_texture,
&wgpu::TextureDescriptor {
label: Some("grafting-dmabuf-import"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
format: frame.format,
dimension: wgpu::TextureDimension::D2,
mip_level_count: 1,
sample_count: 1,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
},
initial_state,
))
}
}
fn host_has_foreign_queue_support(host: &HostWgpuContext) -> bool {
unsafe {
host.device
.as_hal::<wgpu::wgc::api::Vulkan>()
.is_some_and(|device| {
device
.enabled_device_extensions()
.contains(&ash::ext::queue_family_foreign::NAME)
})
}
}
unsafe fn allocate_and_bind_dmabuf_memory(
vk_device: &ash::Device,
vk_instance: &ash::Instance,
physical_device: vk::PhysicalDevice,
vulkan_image: vk::Image,
dmabuf_fd: RawFd,
dmabuf_buffer_index: usize,
plane_fds: &mut PlaneFdGuard,
) -> Result<vk::DeviceMemory, InteropError> {
let memory_requirements = unsafe { vk_device.get_image_memory_requirements(vulkan_image) };
let external_memory_fd_api = ash::khr::external_memory_fd::Device::new(vk_instance, vk_device);
let mut fd_properties = vk::MemoryFdPropertiesKHR::default();
unsafe {
external_memory_fd_api
.get_memory_fd_properties(
vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT,
dmabuf_fd,
&mut fd_properties,
)
.map_err(|err| InteropError::Vulkan(format!("vkGetMemoryFdPropertiesKHR: {err}")))?;
}
let allowed_memory_type_bits =
memory_requirements.memory_type_bits & fd_properties.memory_type_bits;
let memory_properties =
unsafe { vk_instance.get_physical_device_memory_properties(physical_device) };
let memory_type_index = memory_properties.memory_types
[..memory_properties.memory_type_count as usize]
.iter()
.enumerate()
.position(|(index, _)| allowed_memory_type_bits & (1 << index) != 0)
.ok_or_else(|| {
InteropError::Vulkan("image and imported DMABUF have no compatible memory type".into())
})? as u32;
let mut import_memory_info = vk::ImportMemoryFdInfoKHR::default()
.handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT)
.fd(dmabuf_fd);
let mut dedicated_allocate_info =
vk::MemoryDedicatedAllocateInfo::default().image(vulkan_image);
let memory = unsafe {
vk_device
.allocate_memory(
&vk::MemoryAllocateInfo::default()
.allocation_size(memory_requirements.size)
.memory_type_index(memory_type_index)
.push_next(&mut import_memory_info)
.push_next(&mut dedicated_allocate_info),
None,
)
.map_err(|err| InteropError::Vulkan(format!("vkAllocateMemory (DMABUF): {err}")))?
};
plane_fds.transfer_to_vulkan(dmabuf_buffer_index);
if let Err(err) = unsafe { vk_device.bind_image_memory(vulkan_image, memory, 0) } {
unsafe { vk_device.free_memory(memory, None) };
return Err(InteropError::Vulkan(format!(
"vkBindImageMemory (DMABUF): {err}"
)));
}
Ok(memory)
}
unsafe fn acquire_from_foreign_queue(
vk_device: &ash::Device,
queue: vk::Queue,
queue_family_index: u32,
image: vk::Image,
) -> Result<(), InteropError> {
let pool = unsafe {
vk_device
.create_command_pool(
&vk::CommandPoolCreateInfo::default()
.queue_family_index(queue_family_index)
.flags(vk::CommandPoolCreateFlags::TRANSIENT),
None,
)
.map_err(|err| {
InteropError::Vulkan(format!("vkCreateCommandPool (DMABUF acquire): {err}"))
})?
};
let result = unsafe {
let command_buffer = vk_device
.allocate_command_buffers(
&vk::CommandBufferAllocateInfo::default()
.command_pool(pool)
.level(vk::CommandBufferLevel::PRIMARY)
.command_buffer_count(1),
)
.map_err(|err| {
InteropError::Vulkan(format!("vkAllocateCommandBuffers (DMABUF acquire): {err}"))
})?[0];
vk_device
.begin_command_buffer(
command_buffer,
&vk::CommandBufferBeginInfo::default()
.flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
)
.map_err(|err| {
InteropError::Vulkan(format!("vkBeginCommandBuffer (DMABUF acquire): {err}"))
})?;
let barrier = vk::ImageMemoryBarrier::default()
.src_access_mask(vk::AccessFlags::empty())
.dst_access_mask(vk::AccessFlags::SHADER_READ | vk::AccessFlags::TRANSFER_READ)
.old_layout(vk::ImageLayout::UNDEFINED)
.new_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)
.src_queue_family_index(vk::QUEUE_FAMILY_FOREIGN_EXT)
.dst_queue_family_index(queue_family_index)
.image(image)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
});
vk_device.cmd_pipeline_barrier(
command_buffer,
vk::PipelineStageFlags::TOP_OF_PIPE,
vk::PipelineStageFlags::ALL_COMMANDS,
vk::DependencyFlags::empty(),
&[],
&[],
&[barrier],
);
vk_device
.end_command_buffer(command_buffer)
.map_err(|err| {
InteropError::Vulkan(format!("vkEndCommandBuffer (DMABUF acquire): {err}"))
})?;
let command_buffers = [command_buffer];
let submit = vk::SubmitInfo::default().command_buffers(&command_buffers);
vk_device
.queue_submit(queue, &[submit], vk::Fence::null())
.map_err(|err| {
InteropError::Vulkan(format!("vkQueueSubmit (DMABUF acquire): {err}"))
})?;
vk_device
.queue_wait_idle(queue)
.map_err(|err| InteropError::Vulkan(format!("vkQueueWaitIdle (DMABUF acquire): {err}")))
};
unsafe { vk_device.destroy_command_pool(pool, None) };
result
}
pub(crate) fn import_vulkan_external_image(
frame: VulkanExternalImage,
host: &HostWgpuContext,
) -> Result<wgpu::Texture, InteropError> {
let metadata = frame.metadata();
import_dmabuf(
VulkanDmaBufImport::new(
metadata.size,
metadata.format,
0,
frame.drm_modifier,
vec![frame.dmabuf_fd],
vec![VulkanDmaBufPlane {
buffer_index: 0,
offset: frame.dmabuf_offset,
stride: frame.dmabuf_stride,
}],
VulkanDmaBufQueueOwnership::LocalUninitialized,
)?,
host,
)
}
pub fn create_dmabuf_host_context(
adapter: &wgpu::Adapter,
desc: &wgpu::DeviceDescriptor<'_>,
) -> Result<HostWgpuContext, InteropError> {
use wgpu_hal::vulkan::CreateDeviceCallbackArgs;
let hal_adapter = unsafe {
adapter
.as_hal::<wgpu::wgc::api::Vulkan>()
.ok_or(InteropError::BackendMismatch {
expected: "Vulkan",
actual: "non-Vulkan",
})?
};
let capabilities = hal_adapter.physical_device_capabilities();
let missing: Vec<_> = required_device_extensions()
.into_iter()
.filter(|extension| !capabilities.supports_extension(extension))
.map(|extension| extension.to_string_lossy().into_owned())
.collect();
if !missing.is_empty() {
return Err(InteropError::Vulkan(format!(
"physical device does not advertise required DMABUF extensions: {missing:?}"
)));
}
let callback = Box::new(|args: CreateDeviceCallbackArgs<'_, '_, '_>| {
for extension in required_device_extensions() {
if !args.extensions.contains(&extension) {
args.extensions.push(extension);
}
}
});
#[cfg(any(feature = "wgpu-29", feature = "wgpu-30"))]
let open = unsafe {
hal_adapter
.open_with_callback(
desc.required_features,
&desc.required_limits,
&desc.memory_hints,
Some(callback),
)
.map_err(|err| InteropError::Vulkan(format!("open_with_callback: {err}")))?
};
#[cfg(not(any(feature = "wgpu-29", feature = "wgpu-30")))]
let open = unsafe {
hal_adapter
.open_with_callback(desc.required_features, &desc.memory_hints, Some(callback))
.map_err(|err| InteropError::Vulkan(format!("open_with_callback: {err}")))?
};
drop(hal_adapter);
let (device, queue) = unsafe {
adapter
.create_device_from_hal::<wgpu::wgc::api::Vulkan>(open, desc)
.map_err(|err| InteropError::Vulkan(format!("create_device_from_hal: {err}")))?
};
Ok(HostWgpuContext::new(device, queue))
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::fd::{AsRawFd, FromRawFd};
static FD_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn lock_fd_table() -> std::sync::MutexGuard<'static, ()> {
FD_TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn plane(buffer_index: usize) -> VulkanDmaBufPlane {
VulkanDmaBufPlane {
buffer_index,
offset: 0,
stride: 0,
}
}
fn open_pipe_read_fd() -> OwnedFd {
let mut fds = [0; 2];
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
unsafe { libc::close(fds[1]) };
unsafe { OwnedFd::from_raw_fd(fds[0]) }
}
#[test]
fn duplicated_plane_fds_are_one_kernel_object() {
let _fd_lock = lock_fd_table();
let first = open_pipe_read_fd();
let second = unsafe { libc::dup(first.as_raw_fd()) };
assert!(second >= 0);
let second = unsafe { OwnedFd::from_raw_fd(second) };
let buffers = vec![first, second];
assert!(planes_share_kernel_object(&buffers, &[plane(0), plane(1)]));
}
#[test]
fn repeated_descriptor_is_closed_once() {
let _fd_lock = lock_fd_table();
let fd = open_pipe_read_fd();
let raw = fd.as_raw_fd();
let guard = PlaneFdGuard::new(vec![fd]);
assert!(planes_share_kernel_object(&guard.0, &[plane(0), plane(0)]));
drop(guard);
assert_eq!(unsafe { libc::fcntl(raw, libc::F_GETFD) }, -1);
}
#[test]
fn independent_plane_fds_are_disjoint() {
let _fd_lock = lock_fd_table();
let first = open_pipe_read_fd();
let second = open_pipe_read_fd();
let buffers = vec![first, second];
assert!(!planes_share_kernel_object(&buffers, &[plane(0), plane(1)]));
}
#[test]
fn disjoint_plane_layout_has_a_specific_error_and_closes_every_fd() {
let _fd_lock = lock_fd_table();
let first = open_pipe_read_fd();
let second = open_pipe_read_fd();
let first_raw = first.as_raw_fd();
let second_raw = second.as_raw_fd();
let buffers = vec![first, second];
let planes = vec![plane(0), plane(1)];
assert!(matches!(
VulkanDmaBufImport::new(
dpi::PhysicalSize::new(1, 1),
wgpu::TextureFormat::Rgba8Unorm,
0,
0,
buffers,
planes,
VulkanDmaBufQueueOwnership::LocalUninitialized,
),
Err(InteropError::Unsupported(
UnsupportedReason::VulkanDisjointDmabufNotSupported
))
));
assert_eq!(unsafe { libc::fcntl(first_raw, libc::F_GETFD) }, -1);
assert_eq!(unsafe { libc::fcntl(second_raw, libc::F_GETFD) }, -1);
}
#[test]
fn invalid_plane_fd_is_conservatively_disjoint() {
let _fd_lock = lock_fd_table();
let first = open_pipe_read_fd();
let buffers = vec![first];
assert!(matches!(
validate_plane_layout(&buffers, &[plane(0), plane(1)]),
Err(InteropError::InvalidFrame(
"DMABUF plane buffer index is out of range"
))
));
}
}