mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-20 01:35:19 +03:00
A Vulkan implicit layer that captures frames from inside the workload's own process and encodes them on the GPU they were drawn on. Fourth and last of this batch, imported as a tree from `nestrilabs/nescapture` on the same terms. Filed under `apps/` rather than `crates/` despite building a cdylib. The rule here is what a thing *is*, not what it compiles to: this is a finished artefact that gets installed into an image beside its layer manifest, not a library another crate in this tree depends on. `crates/` is for the latter, and putting this there would make the distinction useless the first time someone looked. Wired to the workspace, `nesprotocol` by path. Its description named the transport component; that reads better as what it actually is — where the frames go — so it says that instead. Whole workspace builds and tests: 21 across four members. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
243 lines
9.2 KiB
Rust
243 lines
9.2 KiB
Rust
//! DMA-BUF import support for zero-copy video encoding.
|
|
//!
|
|
//! This module provides the ability to import Linux DMA-BUF file descriptors as
|
|
//! Vulkan images for direct video encoding without CPU-side copies.
|
|
//!
|
|
//! `DmaBufImporter` caches imported Vulkan resources per compositor buffer index
|
|
//! so that pre-allocated GBM buffers are imported only once. Subsequent frames
|
|
//! from the same buffer reuse the cached `VkImage` and `VkDeviceMemory`,
|
|
//! eliminating per-frame Vulkan object creation and layout transitions.
|
|
|
|
use anyhow::Result;
|
|
use ash::vk;
|
|
use log::debug;
|
|
use pixelforge::VideoContext;
|
|
use std::os::fd::RawFd;
|
|
use std::os::unix::io::{BorrowedFd, IntoRawFd};
|
|
|
|
/// Information about a single DMA-BUF plane.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct DmaBufPlane {
|
|
/// File descriptor for the DMA-BUF.
|
|
pub fd: RawFd,
|
|
/// Offset within the DMA-BUF to the start of this plane.
|
|
pub offset: u32,
|
|
/// Row stride in bytes.
|
|
pub stride: u32,
|
|
/// DRM format modifier.
|
|
pub modifier: u64,
|
|
}
|
|
|
|
/// Cached Vulkan resources for a single compositor buffer slot.
|
|
struct CachedImport {
|
|
image: vk::Image,
|
|
memory: vk::DeviceMemory,
|
|
}
|
|
|
|
/// Importer for DMA-BUF file descriptors into Vulkan images.
|
|
///
|
|
/// Owns a per-buffer-index cache of `VkImage` + `VkDeviceMemory`.
|
|
/// Layout transitions are deferred to the consumer (e.g. `ColorConverter`)
|
|
/// to avoid a separate GPU submission per first-time import.
|
|
pub struct DmaBufImporter {
|
|
context: VideoContext,
|
|
external_memory_fd: ash::khr::external_memory_fd::Device,
|
|
/// Per-buffer-index cache. Index corresponds to `ExportedFrame::buffer_index`.
|
|
cached_imports: Vec<Option<CachedImport>>,
|
|
}
|
|
|
|
impl DmaBufImporter {
|
|
/// Create a new DMA-BUF importer.
|
|
pub fn new(context: VideoContext) -> Result<Self> {
|
|
let external_memory_fd =
|
|
ash::khr::external_memory_fd::Device::load(context.instance(), context.device());
|
|
|
|
Ok(Self {
|
|
context,
|
|
external_memory_fd,
|
|
cached_imports: Vec::new(),
|
|
})
|
|
}
|
|
|
|
/// Import a DMA-BUF as a Vulkan image, reusing a cached import when
|
|
/// the same `buffer_index` has been seen before.
|
|
///
|
|
/// The `format` parameter specifies the Vulkan format matching the DMA-BUF
|
|
/// pixel format (e.g. `B8G8R8A8_UNORM` for SDR, `A2B10G10R10_UNORM_PACK32`
|
|
/// for 10-bit HDR, `R16G16B16A16_SFLOAT` for FP16 HDR).
|
|
///
|
|
/// Returns `(image, needs_transition)` where `needs_transition` is `true`
|
|
/// for first-time imports whose image is still in `UNDEFINED` layout.
|
|
/// The caller is responsible for transitioning the image (e.g. by passing
|
|
/// the appropriate `src_layout` to `ColorConverter::convert`).
|
|
pub fn import_or_reuse(
|
|
&mut self,
|
|
buffer_index: usize,
|
|
width: u32,
|
|
height: u32,
|
|
format: vk::Format,
|
|
planes: &[DmaBufPlane],
|
|
) -> Result<(vk::Image, bool)> {
|
|
// Grow the cache vector if needed.
|
|
if self.cached_imports.len() <= buffer_index {
|
|
self.cached_imports.resize_with(buffer_index + 1, || None);
|
|
}
|
|
|
|
if let Some(cached) = &self.cached_imports[buffer_index] {
|
|
return Ok((cached.image, false));
|
|
}
|
|
|
|
// First time seeing this buffer — full import.
|
|
debug!(
|
|
"First import for buffer {buffer_index}: {}x{}, format={:?}, fd={}, stride={}, modifier={:#x}",
|
|
width, height, format, planes[0].fd, planes[0].stride, planes[0].modifier
|
|
);
|
|
|
|
let (image, memory) = self.import_internal(width, height, format, planes)?;
|
|
|
|
self.cached_imports[buffer_index] = Some(CachedImport { image, memory });
|
|
Ok((image, true))
|
|
}
|
|
|
|
/// Perform the raw Vulkan import of a DMA-BUF with the specified format.
|
|
///
|
|
/// Returns the `(VkImage, VkDeviceMemory)` pair. The image is in
|
|
/// `UNDEFINED` layout; the caller must transition it.
|
|
fn import_internal(
|
|
&self,
|
|
width: u32,
|
|
height: u32,
|
|
format: vk::Format,
|
|
planes: &[DmaBufPlane],
|
|
) -> Result<(vk::Image, vk::DeviceMemory)> {
|
|
if planes.is_empty() {
|
|
return Err(anyhow::anyhow!("At least one DMA-BUF plane is required"));
|
|
}
|
|
|
|
let device = self.context.device();
|
|
|
|
// Build DRM format modifier plane layouts for all planes.
|
|
// AMD modifiers (e.g. tiled/DCC) may require multiple planes;
|
|
// the layout count must match the modifier's expected plane count.
|
|
let plane_layouts: Vec<vk::SubresourceLayout> = planes
|
|
.iter()
|
|
.map(|p| {
|
|
vk::SubresourceLayout::default()
|
|
.offset(p.offset as u64)
|
|
.row_pitch(p.stride as u64)
|
|
})
|
|
.collect();
|
|
|
|
let modifier = planes[0].modifier;
|
|
let mut drm_format_modifier_info =
|
|
vk::ImageDrmFormatModifierExplicitCreateInfoEXT::default()
|
|
.drm_format_modifier(modifier)
|
|
.plane_layouts(&plane_layouts);
|
|
|
|
let mut external_memory_info = vk::ExternalMemoryImageCreateInfo::default()
|
|
.handle_types(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT);
|
|
external_memory_info.p_next = &mut drm_format_modifier_info
|
|
as *mut vk::ImageDrmFormatModifierExplicitCreateInfoEXT
|
|
as *mut _;
|
|
|
|
let mut image_create_info = 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::DRM_FORMAT_MODIFIER_EXT)
|
|
.usage(vk::ImageUsageFlags::TRANSFER_SRC | vk::ImageUsageFlags::SAMPLED)
|
|
.sharing_mode(vk::SharingMode::EXCLUSIVE)
|
|
.initial_layout(vk::ImageLayout::UNDEFINED);
|
|
image_create_info.p_next =
|
|
&mut external_memory_info as *mut vk::ExternalMemoryImageCreateInfo as *mut _;
|
|
|
|
let image = unsafe { device.create_image(&image_create_info, None) }
|
|
.map_err(|e| anyhow::anyhow!("DMA-BUF image creation: {e}"))?;
|
|
|
|
// Memory requirements.
|
|
let mem_requirements = unsafe { device.get_image_memory_requirements(image) };
|
|
|
|
// FD memory properties.
|
|
let mut memory_fd_properties = vk::MemoryFdPropertiesKHR::default();
|
|
unsafe {
|
|
self.external_memory_fd.get_memory_fd_properties(
|
|
vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT,
|
|
planes[0].fd,
|
|
&mut memory_fd_properties,
|
|
)
|
|
}
|
|
.map_err(|e| anyhow::anyhow!("Failed to get memory FD properties: {e}"))?;
|
|
|
|
// Duplicate the FD — vkAllocateMemory consumes it.
|
|
let fd = unsafe { BorrowedFd::borrow_raw(planes[0].fd) }
|
|
.try_clone_to_owned()
|
|
.map_err(|e| anyhow::anyhow!("Failed to duplicate DMA-BUF FD: {e}"))?
|
|
.into_raw_fd();
|
|
|
|
let mut import_memory_fd_info = vk::ImportMemoryFdInfoKHR::default()
|
|
.handle_type(vk::ExternalMemoryHandleTypeFlags::DMA_BUF_EXT)
|
|
.fd(fd);
|
|
|
|
let memory_type_bits =
|
|
mem_requirements.memory_type_bits & memory_fd_properties.memory_type_bits;
|
|
|
|
debug!(
|
|
"Memory allocation: size={}, image_type_bits={:#x}, fd_type_bits={:#x}, combined={:#x}",
|
|
mem_requirements.size,
|
|
mem_requirements.memory_type_bits,
|
|
memory_fd_properties.memory_type_bits,
|
|
memory_type_bits
|
|
);
|
|
|
|
let memory_type_index = self
|
|
.context
|
|
.find_memory_type(memory_type_bits, vk::MemoryPropertyFlags::empty())
|
|
.ok_or_else(|| anyhow::anyhow!("No suitable memory type for DMA-BUF import"))?;
|
|
|
|
// Dedicated allocation (required by many drivers for external memory).
|
|
let mut dedicated_alloc_info = vk::MemoryDedicatedAllocateInfo::default().image(image);
|
|
import_memory_fd_info.p_next =
|
|
&mut dedicated_alloc_info as *mut vk::MemoryDedicatedAllocateInfo as *mut _;
|
|
|
|
let mut alloc_info = vk::MemoryAllocateInfo::default()
|
|
.allocation_size(mem_requirements.size)
|
|
.memory_type_index(memory_type_index);
|
|
alloc_info.p_next = &mut import_memory_fd_info as *mut vk::ImportMemoryFdInfoKHR as *mut _;
|
|
|
|
let memory = unsafe { device.allocate_memory(&alloc_info, None) }.map_err(|e| {
|
|
unsafe { device.destroy_image(image, None) };
|
|
anyhow::anyhow!("DMA-BUF memory import: {e}")
|
|
})?;
|
|
|
|
if let Err(e) = unsafe { device.bind_image_memory(image, memory, 0) } {
|
|
unsafe {
|
|
device.free_memory(memory, None);
|
|
device.destroy_image(image, None);
|
|
}
|
|
return Err(anyhow::anyhow!("DMA-BUF memory bind: {e}"));
|
|
}
|
|
|
|
Ok((image, memory))
|
|
}
|
|
}
|
|
|
|
impl Drop for DmaBufImporter {
|
|
fn drop(&mut self) {
|
|
let device = self.context.device();
|
|
unsafe {
|
|
// Clean up cached imports.
|
|
for cached in self.cached_imports.drain(..).flatten() {
|
|
device.destroy_image(cached.image, None);
|
|
device.free_memory(cached.memory, None);
|
|
}
|
|
}
|
|
}
|
|
}
|