//! Import a tiled VAAPI NV12 DMA-BUF as two wgpu textures (Y = R8, UV = RG8), aliasing //! the one imported `VkDeviceMemory` at the plane offsets. Two single-format images are //! used instead of one multi-planar image so each is an ordinary wgpu render target. use crate::vaapi::MappedSurface; use crate::vk_device::DrmDevice; use ash::vk; /// Plane layout for a single-object NV12/P010 DMA-BUF (the common VAAPI case). #[derive(Clone, Copy)] pub struct Nv12DmaBuf { pub fd: i32, pub size: u64, pub modifier: u64, pub width: u32, pub height: u32, pub y_offset: u64, pub y_pitch: u64, pub uv_offset: u64, pub uv_pitch: u64, /// True for 10/12/16-bit content (P010 etc.): planes are 16-bit (R16/Rg16) rather than 8-bit /// (R8/Rg8). The sampled float is normalized either way, so the consumer needs no change. pub ten_bit: bool, } /// Frees the shared imported `VkDeviceMemory` once both plane images are gone. Held by /// both textures' drop callbacks (via `Arc`); the last one to run frees the memory — /// after wgpu has destroyed the images, in its wait-idle'd deferred-destruction pass. struct MemoryGuard { device: ash::Device, memory: vk::DeviceMemory, } impl Drop for MemoryGuard { fn drop(&mut self) { unsafe { self.device.free_memory(self.memory, None) }; } } /// Frees the duplicated dma-buf fd and any partially-created Vulkan objects when an import /// errors out before ownership has been handed to wgpu/`MemoryGuard`. `commit()` disarms it on /// the success path; `fd_consumed()` is called once `vkAllocateMemory` has taken the fd. struct ImportGuard { device: ash::Device, fd: libc::c_int, img_y: vk::Image, img_uv: vk::Image, memory: vk::DeviceMemory, armed: bool, } impl ImportGuard { fn fd_consumed(&mut self) { self.fd = -1; // vkAllocateMemory owns the fd now; don't close it ourselves } fn commit(&mut self) { self.armed = false; } } impl Drop for ImportGuard { fn drop(&mut self) { if !self.armed { return; } unsafe { if self.img_uv != vk::Image::null() { self.device.destroy_image(self.img_uv, None); } if self.img_y != vk::Image::null() { self.device.destroy_image(self.img_y, None); } if self.memory != vk::DeviceMemory::null() { self.device.free_memory(self.memory, None); } if self.fd >= 0 { libc::close(self.fd); } } } } /// A VAAPI surface imported as two wgpu plane textures. The underlying Vulkan image/ /// memory are destroyed by wgpu (via drop callbacks) when these textures drop. pub struct ImportedNv12 { y: wgpu::Texture, uv: wgpu::Texture, } impl ImportedNv12 { pub fn y(&self) -> &wgpu::Texture { &self.y } pub fn uv(&self) -> &wgpu::Texture { &self.uv } /// Consume into the `(Y, UV)` plane textures (for handing to a longer-lived owner). pub fn into_planes(self) -> (wgpu::Texture, wgpu::Texture) { (self.y, self.uv) } } /// Convenience: map a freshly-allocated `MappedSurface` and import it onto `drm`. pub fn import(drm: &DrmDevice, surf: &MappedSurface) -> Result { import_raw( &drm.device, &drm.adapter, &Nv12DmaBuf { fd: surf.fd, size: surf.size, modifier: surf.modifier, width: surf.width, height: surf.height, y_offset: surf.y_offset, y_pitch: surf.y_pitch, uv_offset: surf.uv_offset, uv_pitch: surf.uv_pitch, ten_bit: false, }, ) } /// Import an NV12 DMA-BUF (described by `buf`) as two wgpu plane textures **on `device`**. The raw /// Vulkan handles are extracted from `device`/`adapter` via `as_hal`, so this works with any /// DMA-BUF-import-capable wgpu device — the encoder/decoder's own `DrmDevice` *or* the editor's /// shared device. The fd is duplicated, so the caller keeps ownership of theirs. pub fn import_raw( device: &wgpu::Device, adapter: &wgpu::Adapter, buf: &Nv12DmaBuf, ) -> Result { use wgpu_hal::vulkan::Api as Vk; unsafe { let hal_device = device .as_hal::() .ok_or("device is not Vulkan")?; let raw_device = hal_device.raw_device().clone(); let raw_instance = adapter .as_hal::() .ok_or("adapter is not Vulkan")? .shared_instance() .raw_instance() .clone(); let instance = &raw_instance; let dup_fd = libc::dup(buf.fd); if dup_fd < 0 { return Err("dup(dma-buf fd) failed".into()); } // Owns the fd + any Vk objects created below until ownership transfers to wgpu; on any // early `?`/return before that, its Drop frees them (was leaking on every failed import). let mut guard = ImportGuard { device: raw_device.clone(), fd: dup_fd, img_y: vk::Image::null(), img_uv: vk::Image::null(), memory: vk::DeviceMemory::null(), armed: true, }; // 16-bit-norm plane formats (P010) are NOT renderable, so the import is sample-only for // those (decode path). 8-bit planes keep COLOR_ATTACHMENT for the encoder's RGBA→NV12 write. let vk_usage = if buf.ten_bit { vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_SRC | vk::ImageUsageFlags::TRANSFER_DST } else { vk::ImageUsageFlags::COLOR_ATTACHMENT | vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_SRC | vk::ImageUsageFlags::TRANSFER_DST }; let make_image = |format: vk::Format, w: u32, h: u32, pitch: u64| -> Result { let mut ext = vk::ExternalMemoryImageCreateInfo::default() .handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT); let plane_layouts = [vk::SubresourceLayout::default().offset(0).row_pitch(pitch)]; let mut drm_info = vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default() .drm_format_modifier(buf.modifier) .plane_layouts(&plane_layouts); let info = vk::ImageCreateInfo::default() .image_type(vk::ImageType::TYPE_2D) .format(format) .extent(vk::Extent3D { width: w, height: h, depth: 1 }) .mip_levels(1) .array_layers(1) .samples(vk::SampleCountFlags::TYPE_1) .tiling(vk::ImageTiling::DRM_FORMAT_MODIFIER_EXT) .usage(vk_usage) .sharing_mode(vk::SharingMode::EXCLUSIVE) .initial_layout(vk::ImageLayout::UNDEFINED) .push_next(&mut ext) .push_next(&mut drm_info); raw_device .create_image(&info, None) .map_err(|e| format!("vkCreateImage(modifier) failed: {e:?}")) }; // 8-bit NV12 → R8/Rg8 planes; 10/12/16-bit P010-style → R16/Rg16 (sampled value is // normalized either way, so the NV12→RGB consumer is unchanged). let (vk_y, vk_uv) = if buf.ten_bit { (vk::Format::R16_UNORM, vk::Format::R16G16_UNORM) } else { (vk::Format::R8_UNORM, vk::Format::R8G8_UNORM) }; let (wgpu_y, wgpu_uv) = if buf.ten_bit { (wgpu::TextureFormat::R16Unorm, wgpu::TextureFormat::Rg16Unorm) } else { (wgpu::TextureFormat::R8Unorm, wgpu::TextureFormat::Rg8Unorm) }; let img_y = make_image(vk_y, buf.width, buf.height, buf.y_pitch)?; guard.img_y = img_y; let img_uv = make_image(vk_uv, buf.width / 2, buf.height / 2, buf.uv_pitch)?; guard.img_uv = img_uv; let fd_dev = ash::khr::external_memory_fd::Device::new(instance, &raw_device); let mut fd_props = vk::MemoryFdPropertiesKHR::default(); fd_dev .get_memory_fd_properties(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT, dup_fd, &mut fd_props) .map_err(|e| format!("vkGetMemoryFdPropertiesKHR failed: {e:?}"))?; let req_y = raw_device.get_image_memory_requirements(img_y); let req_uv = raw_device.get_image_memory_requirements(img_uv); let type_bits = fd_props.memory_type_bits & req_y.memory_type_bits & req_uv.memory_type_bits; if type_bits == 0 { return Err("no memory type compatible with dma-buf + both plane images".into()); } let mem_type = type_bits.trailing_zeros(); let mut import_info = vk::ImportMemoryFdInfoKHR::default() .handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT) .fd(dup_fd); let alloc = vk::MemoryAllocateInfo::default() .allocation_size(buf.size) .memory_type_index(mem_type) .push_next(&mut import_info); let memory = raw_device .allocate_memory(&alloc, None) .map_err(|e| format!("vkAllocateMemory(import dma-buf) failed: {e:?}"))?; guard.fd_consumed(); // the import transferred fd ownership to Vulkan guard.memory = memory; raw_device .bind_image_memory(img_y, memory, buf.y_offset) .map_err(|e| format!("bind Y plane: {e:?}"))?; raw_device .bind_image_memory(img_uv, memory, buf.uv_offset) .map_err(|e| format!("bind UV plane: {e:?}"))?; // Shared guard: frees `memory` once both images' drop callbacks have run. let mem_guard = std::sync::Arc::new(MemoryGuard { device: raw_device.clone(), memory }); // Match the Vulkan usage: 16-bit-norm planes (P010) are sample-only (not renderable). let (hal_usage, wgpu_usage) = if buf.ten_bit { ( wgpu_types::TextureUses::RESOURCE | wgpu_types::TextureUses::COPY_SRC, wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC, ) } else { ( wgpu_types::TextureUses::COLOR_TARGET | wgpu_types::TextureUses::RESOURCE | wgpu_types::TextureUses::COPY_SRC, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_SRC, ) }; let wrap = |img: vk::Image, format: wgpu::TextureFormat, w: u32, h: u32| -> wgpu::Texture { // wgpu destroys the image (after wait-idle) when the texture drops; the // captured Arc frees the shared memory once both have run. let dev = raw_device.clone(); let guard = mem_guard.clone(); let cb: wgpu_hal::DropCallback = Box::new(move || { dev.destroy_image(img, None); drop(guard); }); let hal_desc = wgpu_hal::TextureDescriptor { label: Some("vaapi-plane"), size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format, usage: hal_usage, memory_flags: wgpu_hal::MemoryFlags::empty(), view_formats: vec![], }; let hal_tex = hal_device.texture_from_raw(img, &hal_desc, Some(cb)); device.create_texture_from_hal::( hal_tex, &wgpu::TextureDescriptor { label: Some("vaapi-plane"), size: wgpu::Extent3d { width: w, height: h, depth_or_array_layers: 1 }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format, usage: wgpu_usage, view_formats: &[], }, ) }; // Ownership of the images (→ texture drop callbacks) and memory (→ MemoryGuard) has now // transferred to wgpu; disarm the cleanup guard so it doesn't double-free them. guard.commit(); let y = wrap(img_y, wgpu_y, buf.width, buf.height); let uv = wrap(img_uv, wgpu_uv, buf.width / 2, buf.height / 2); drop(hal_device); Ok(ImportedNv12 { y, uv }) } }