use std::{
ffi::CStr,
os::fd::{AsRawFd, BorrowedFd, IntoRawFd},
};
use ash::vk;
use bevy::{
prelude::*,
render::{
RenderPlugin,
renderer::{
RenderDevice,
raw_vulkan_init::{AdditionalVulkanFeatures, RawVulkanInitSettings},
},
},
};
use tracing::{error, info, warn};
use wgpu::hal::api::Vulkan;
pub use ash;
pub use bevy::render::renderer::raw_vulkan_init;
pub const DRM_FORMAT_MOD_LINEAR: u64 = 0;
pub struct DmabufImportEnabled;
const REQUIRED_EXTENSIONS: [&CStr; 3] = [
ash::khr::external_memory_fd::NAME,
ash::ext::external_memory_dma_buf::NAME,
ash::ext::image_drm_format_modifier::NAME,
];
const OPTIONAL_EXTENSIONS: [&CStr; 1] = [ash::ext::queue_family_foreign::NAME];
pub struct DmabufTexturePlugin;
impl Plugin for DmabufTexturePlugin {
fn build(&self, app: &mut App) {
if app.is_plugin_added::<RenderPlugin>() {
error!(
"DmabufTexturePlugin was added after RenderPlugin; the Vulkan device is already \
configured and DMA-BUF import will be unavailable. Add it before DefaultPlugins."
);
return;
}
let mut settings = app
.world_mut()
.get_resource_or_insert_with(RawVulkanInitSettings::default);
unsafe {
settings.add_create_device_callback(|args, adapter, features| {
let caps = adapter.physical_device_capabilities();
let missing: Vec<_> = REQUIRED_EXTENSIONS
.iter()
.filter(|e| !caps.supports_extension(e))
.collect();
if !missing.is_empty() {
warn!("DMA-BUF import unavailable, device lacks {missing:?}");
return;
}
let api_version = {
let instance = adapter.shared_instance().raw_instance();
let props =
instance.get_physical_device_properties(adapter.raw_physical_device());
props.api_version
};
let mut wanted: Vec<&'static CStr> = REQUIRED_EXTENSIONS.to_vec();
wanted.extend(
OPTIONAL_EXTENSIONS
.iter()
.filter(|e| caps.supports_extension(e)),
);
if api_version < vk::API_VERSION_1_2 {
for dep in [
ash::khr::image_format_list::NAME,
ash::khr::bind_memory2::NAME,
ash::khr::sampler_ycbcr_conversion::NAME,
ash::khr::maintenance1::NAME,
ash::khr::get_memory_requirements2::NAME,
] {
if caps.supports_extension(dep) {
wanted.push(dep);
}
}
}
for ext in wanted {
if !args.extensions.contains(&ext) {
args.extensions.push(ext);
}
}
features.insert::<DmabufImportEnabled>();
info!("Vulkan device created with DMA-BUF import extensions");
});
}
}
}
pub fn import_supported(
features: Option<&AdditionalVulkanFeatures>,
device: &RenderDevice,
) -> bool {
if features.is_some_and(|f| f.has::<DmabufImportEnabled>()) {
return true;
}
unsafe { device.wgpu_device().as_hal::<Vulkan>() }.is_some_and(|d| {
REQUIRED_EXTENSIONS
.iter()
.all(|e| d.enabled_device_extensions().contains(e))
})
}
#[derive(Debug, thiserror::Error)]
pub enum DmabufImportError {
#[error("render backend is not Vulkan")]
NotVulkan,
#[error(
"Vulkan device was created without {0:?}; add DmabufTexturePlugin before DefaultPlugins"
)]
MissingExtension(&'static CStr),
#[error("texture format {0:?} has no DMA-BUF mapping in this crate")]
UnsupportedFormat(wgpu::TextureFormat),
#[error("driver does not list modifier {modifier:#x} for {format:?}")]
ModifierUnsupported {
format: wgpu::TextureFormat,
modifier: u64,
},
#[error("driver reports {format:?} with modifier {modifier:#x} is not importable from DMA-BUF")]
NotImportable {
format: wgpu::TextureFormat,
modifier: u64,
},
#[error("no memory type satisfies both the image and the DMA-BUF")]
NoMemoryType,
#[error("failed to duplicate DMA-BUF fd: {0}")]
Dup(std::io::Error),
#[error("{op}: {result:?}")]
Vulkan {
op: &'static str,
result: vk::Result,
},
}
fn vk_err(op: &'static str) -> impl FnOnce(vk::Result) -> DmabufImportError {
move |result| DmabufImportError::Vulkan { op, result }
}
pub struct DmabufPlane<'a> {
pub fd: BorrowedFd<'a>,
pub offset: u64,
pub stride: u32,
}
pub struct DmabufImageDesc<'a> {
pub label: Option<&'a str>,
pub width: u32,
pub height: u32,
pub format: wgpu::TextureFormat,
pub modifier: u64,
pub plane: DmabufPlane<'a>,
pub usage: wgpu::TextureUsages,
}
fn vk_format(format: wgpu::TextureFormat) -> Option<vk::Format> {
use wgpu::TextureFormat as F;
Some(match format {
F::R8Unorm => vk::Format::R8_UNORM,
F::Rg8Unorm => vk::Format::R8G8_UNORM,
F::Rgba8Unorm => vk::Format::R8G8B8A8_UNORM,
F::Rgba8UnormSrgb => vk::Format::R8G8B8A8_SRGB,
F::Bgra8Unorm => vk::Format::B8G8R8A8_UNORM,
F::Bgra8UnormSrgb => vk::Format::B8G8R8A8_SRGB,
F::R16Unorm => vk::Format::R16_UNORM,
F::Rg16Unorm => vk::Format::R16G16_UNORM,
F::Rgba16Unorm => vk::Format::R16G16B16A16_UNORM,
F::Rgb10a2Unorm => vk::Format::A2B10G10R10_UNORM_PACK32,
_ => return None,
})
}
fn vk_usage(usage: wgpu::TextureUsages) -> vk::ImageUsageFlags {
let mut flags = vk::ImageUsageFlags::empty();
if usage.contains(wgpu::TextureUsages::COPY_SRC) {
flags |= vk::ImageUsageFlags::TRANSFER_SRC;
}
if usage.contains(wgpu::TextureUsages::COPY_DST) {
flags |= vk::ImageUsageFlags::TRANSFER_DST;
}
if usage.contains(wgpu::TextureUsages::TEXTURE_BINDING) {
flags |= vk::ImageUsageFlags::SAMPLED;
}
if usage.contains(wgpu::TextureUsages::STORAGE_BINDING) {
flags |= vk::ImageUsageFlags::STORAGE;
}
if usage.contains(wgpu::TextureUsages::RENDER_ATTACHMENT) {
flags |= vk::ImageUsageFlags::COLOR_ATTACHMENT;
}
flags
}
fn hal_uses(usage: wgpu::TextureUsages) -> wgpu::TextureUses {
let mut uses = wgpu::TextureUses::empty();
if usage.contains(wgpu::TextureUsages::COPY_SRC) {
uses |= wgpu::TextureUses::COPY_SRC;
}
if usage.contains(wgpu::TextureUsages::COPY_DST) {
uses |= wgpu::TextureUses::COPY_DST;
}
if usage.contains(wgpu::TextureUsages::TEXTURE_BINDING) {
uses |= wgpu::TextureUses::RESOURCE;
}
if usage.contains(wgpu::TextureUsages::STORAGE_BINDING) {
uses |= wgpu::TextureUses::STORAGE_READ_WRITE;
}
if usage.contains(wgpu::TextureUsages::RENDER_ATTACHMENT) {
uses |= wgpu::TextureUses::COLOR_TARGET;
}
uses
}
pub fn import_dmabuf_texture(
device: &wgpu::Device,
desc: &DmabufImageDesc<'_>,
) -> Result<wgpu::Texture, DmabufImportError> {
let format = vk_format(desc.format).ok_or(DmabufImportError::UnsupportedFormat(desc.format))?;
let usage = vk_usage(desc.usage);
let handle_type = vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT;
let hal_texture = unsafe {
let hal_device = device
.as_hal::<Vulkan>()
.ok_or(DmabufImportError::NotVulkan)?;
for ext in REQUIRED_EXTENSIONS {
if !hal_device.enabled_device_extensions().contains(&ext) {
return Err(DmabufImportError::MissingExtension(ext));
}
}
let raw: ash::Device = hal_device.raw_device().clone();
let instance: ash::Instance = hal_device.shared_instance().raw_instance().clone();
let phd = hal_device.raw_physical_device();
check_modifier_supported(&instance, phd, desc.format, format, desc.modifier)?;
check_importable(
&instance,
phd,
desc.format,
format,
desc.modifier,
usage,
handle_type,
)?;
let plane_layouts = [vk::SubresourceLayout {
offset: desc.plane.offset,
size: 0,
row_pitch: desc.plane.stride as u64,
array_pitch: 0,
depth_pitch: 0,
}];
let mut modifier_info = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default()
.drm_format_modifier(desc.modifier)
.plane_layouts(&plane_layouts);
let mut external_info =
vk::ExternalMemoryImageCreateInfo::default().handle_types(handle_type);
let image_info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.format(format)
.extent(vk::Extent3D {
width: desc.width,
height: desc.height,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.samples(vk::SampleCountFlags::TYPE_1)
.tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT)
.usage(usage)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.initial_layout(vk::ImageLayout::UNDEFINED)
.push_next(&mut external_info)
.push_next(&mut modifier_info);
let image = raw
.create_image(&image_info, None)
.map_err(vk_err("vkCreateImage"))?;
let bind = (|| -> Result<vk::DeviceMemory, DmabufImportError> {
let mut dedicated_req = vk::MemoryDedicatedRequirements::default();
let mut req = vk::MemoryRequirements2::default().push_next(&mut dedicated_req);
raw.get_image_memory_requirements2(
&vk::ImageMemoryRequirementsInfo2::default().image(image),
&mut req,
);
let size = req.memory_requirements.size;
let image_type_bits = req.memory_requirements.memory_type_bits;
let fd_ext = ash::khr::external_memory_fd::Device::new(&instance, &raw);
let mut fd_props = vk::MemoryFdPropertiesKHR::default();
fd_ext
.get_memory_fd_properties(handle_type, desc.plane.fd.as_raw_fd(), &mut fd_props)
.map_err(vk_err("vkGetMemoryFdPropertiesKHR"))?;
let type_bits = image_type_bits & fd_props.memory_type_bits;
if type_bits == 0 {
return Err(DmabufImportError::NoMemoryType);
}
let memory_type_index = type_bits.trailing_zeros();
let dup = desc
.plane
.fd
.try_clone_to_owned()
.map_err(DmabufImportError::Dup)?;
let dup_raw = dup.into_raw_fd();
let mut import_info = vk::ImportMemoryFdInfoKHR::default()
.handle_type(handle_type)
.fd(dup_raw);
let mut dedicated_info = vk::MemoryDedicatedAllocateInfo::default().image(image);
let alloc_info = vk::MemoryAllocateInfo::default()
.allocation_size(size)
.memory_type_index(memory_type_index)
.push_next(&mut import_info)
.push_next(&mut dedicated_info);
let memory = match raw.allocate_memory(&alloc_info, None) {
Ok(m) => m,
Err(e) => {
libc_close(dup_raw);
return Err(vk_err("vkAllocateMemory (import)")(e));
}
};
if let Err(e) = raw.bind_image_memory(image, memory, 0) {
raw.free_memory(memory, None);
return Err(vk_err("vkBindImageMemory")(e));
}
Ok(memory)
})();
let memory = match bind {
Ok(m) => m,
Err(e) => {
raw.destroy_image(image, None);
return Err(e);
}
};
let hal_desc = wgpu::hal::TextureDescriptor {
label: desc.label,
size: wgpu::Extent3d {
width: desc.width,
height: desc.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: desc.format,
usage: hal_uses(desc.usage),
memory_flags: wgpu::hal::MemoryFlags::empty(),
view_formats: vec![],
};
let raw_for_drop = raw.clone();
let drop_callback: wgpu::hal::DropCallback = Box::new(move || {
raw_for_drop.destroy_image(image, None);
raw_for_drop.free_memory(memory, None);
});
hal_device.texture_from_raw(
image,
&hal_desc,
Some(drop_callback),
wgpu::hal::vulkan::TextureMemory::External,
)
};
let texture = unsafe {
device.create_texture_from_hal::<Vulkan>(
hal_texture,
&wgpu::TextureDescriptor {
label: desc.label,
size: wgpu::Extent3d {
width: desc.width,
height: desc.height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: desc.format,
usage: desc.usage,
view_formats: &[],
},
wgpu::TextureUses::UNINITIALIZED,
)
};
Ok(texture)
}
fn libc_close(fd: i32) {
unsafe {
drop(<std::os::fd::OwnedFd as std::os::fd::FromRawFd>::from_raw_fd(fd));
}
}
unsafe fn check_modifier_supported(
instance: &ash::Instance,
phd: vk::PhysicalDevice,
wgpu_format: wgpu::TextureFormat,
format: vk::Format,
modifier: u64,
) -> Result<(), DmabufImportError> {
let mut list = vk::DrmFormatModifierPropertiesListEXT::default();
let mut props = vk::FormatProperties2::default().push_next(&mut list);
unsafe { instance.get_physical_device_format_properties2(phd, format, &mut props) };
let count = list.drm_format_modifier_count as usize;
let mut entries = vec![vk::DrmFormatModifierPropertiesEXT::default(); count];
let mut list = vk::DrmFormatModifierPropertiesListEXT::default()
.drm_format_modifier_properties(&mut entries);
let mut props = vk::FormatProperties2::default().push_next(&mut list);
unsafe { instance.get_physical_device_format_properties2(phd, format, &mut props) };
let reported = list.drm_format_modifier_count as usize;
let found = entries.iter().take(reported).any(|e| {
e.drm_format_modifier == modifier
&& e.drm_format_modifier_plane_count == 1
&& e.drm_format_modifier_tiling_features
.contains(vk::FormatFeatureFlags::TRANSFER_SRC)
});
if found {
Ok(())
} else {
Err(DmabufImportError::ModifierUnsupported {
format: wgpu_format,
modifier,
})
}
}
unsafe fn check_importable(
instance: &ash::Instance,
phd: vk::PhysicalDevice,
wgpu_format: wgpu::TextureFormat,
format: vk::Format,
modifier: u64,
usage: vk::ImageUsageFlags,
handle_type: vk::ExternalMemoryHandleTypeFlags,
) -> Result<(), DmabufImportError> {
let mut external_info =
vk::PhysicalDeviceExternalImageFormatInfo::default().handle_type(handle_type);
let mut modifier_info = vk::PhysicalDeviceImageDrmFormatModifierInfoEXT::default()
.drm_format_modifier(modifier)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
let info = vk::PhysicalDeviceImageFormatInfo2::default()
.format(format)
.ty(vk::ImageType::TYPE_2D)
.tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT)
.usage(usage)
.push_next(&mut external_info)
.push_next(&mut modifier_info);
let mut external_props = vk::ExternalImageFormatProperties::default();
let mut props = vk::ImageFormatProperties2::default().push_next(&mut external_props);
unsafe { instance.get_physical_device_image_format_properties2(phd, &info, &mut props) }
.map_err(|_| DmabufImportError::NotImportable {
format: wgpu_format,
modifier,
})?;
if external_props
.external_memory_properties
.external_memory_features
.contains(vk::ExternalMemoryFeatureFlags::IMPORTABLE)
{
Ok(())
} else {
Err(DmabufImportError::NotImportable {
format: wgpu_format,
modifier,
})
}
}
pub fn foreign_acquire(
device: &wgpu::Device,
texture: &wgpu::Texture,
layout: vk::ImageLayout,
) -> Option<wgpu::CommandBuffer> {
ownership_barrier(device, texture, layout, true)
}
pub fn foreign_release(
device: &wgpu::Device,
texture: &wgpu::Texture,
layout: vk::ImageLayout,
) -> Option<wgpu::CommandBuffer> {
ownership_barrier(device, texture, layout, false)
}
fn ownership_barrier(
device: &wgpu::Device,
texture: &wgpu::Texture,
layout: vk::ImageLayout,
acquire: bool,
) -> Option<wgpu::CommandBuffer> {
unsafe {
let hal_device = device.as_hal::<Vulkan>()?;
let hal_texture = texture.as_hal::<Vulkan>()?;
let image = hal_texture.raw_handle();
let family = hal_device.queue_family_index();
let raw = hal_device.raw_device().clone();
let has_foreign = hal_device
.enabled_device_extensions()
.contains(&ash::ext::queue_family_foreign::NAME);
drop(hal_texture);
drop(hal_device);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some(if acquire {
"dmabuf foreign acquire"
} else {
"dmabuf foreign release"
}),
});
let recorded = encoder.as_hal_mut::<Vulkan, _, _>(|hal_encoder| {
let Some(hal_encoder) = hal_encoder else {
return false;
};
let cb = hal_encoder.raw_handle();
let read_access = vk::AccessFlags::TRANSFER_READ | vk::AccessFlags::SHADER_READ;
let read_stages = vk::PipelineStageFlags::TRANSFER
| vk::PipelineStageFlags::FRAGMENT_SHADER
| vk::PipelineStageFlags::COMPUTE_SHADER;
let subresource = vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
};
if acquire {
let host_visible = vk::MemoryBarrier::default()
.src_access_mask(vk::AccessFlags::HOST_WRITE)
.dst_access_mask(read_access);
raw.cmd_pipeline_barrier(
cb,
vk::PipelineStageFlags::HOST,
read_stages,
vk::DependencyFlags::empty(),
&[host_visible],
&[],
&[],
);
if has_foreign {
let take = vk::ImageMemoryBarrier::default()
.src_access_mask(vk::AccessFlags::empty())
.dst_access_mask(read_access)
.old_layout(layout)
.new_layout(layout)
.src_queue_family_index(vk::QUEUE_FAMILY_FOREIGN_EXT)
.dst_queue_family_index(family)
.image(image)
.subresource_range(subresource);
raw.cmd_pipeline_barrier(
cb,
vk::PipelineStageFlags::TOP_OF_PIPE,
read_stages,
vk::DependencyFlags::empty(),
&[],
&[],
&[take],
);
}
} else if has_foreign {
let give = vk::ImageMemoryBarrier::default()
.src_access_mask(read_access)
.dst_access_mask(vk::AccessFlags::empty())
.old_layout(layout)
.new_layout(layout)
.src_queue_family_index(family)
.dst_queue_family_index(vk::QUEUE_FAMILY_FOREIGN_EXT)
.image(image)
.subresource_range(subresource);
raw.cmd_pipeline_barrier(
cb,
read_stages,
vk::PipelineStageFlags::BOTTOM_OF_PIPE,
vk::DependencyFlags::empty(),
&[],
&[],
&[give],
);
}
true
});
recorded.then(|| encoder.finish())
}
}