VK_EXT_external_memory_dma_buf explained: zero-copy DMA-Buf to VkImage

The previous post covered how the pixels are laid out (DRM modifiers). This is the mechanism that gets them into Vulkan: a DMA-Buf is a kernel-owned fd that references GPU memory allocated by another driver (VAAPI, V4L2, GBM, a camera). VK_EXT_external_memory_dma_buf lets you claim that fd as VkDeviceMemory and bind it to a VkImage — the decode result is a texture, no copy.

The pipeline

flow.txt
decoder (VAAPI) ──► DMA-Buf fd ──► vkGetMemoryFdPropertiesKHR
                                   (validate: is it a dma-buf, which
                                    memory types accept it)
                                ──► vkAllocateMemory(ImportMemoryFdInfoKHR)
                                ──► vkBindImageMemory → VkImage
                                ──► sampled in shader

One fd → one VkDeviceMemory → one VkImage. The kernel keeps the backing pages; Vulkan just maps the same physical memory.

The code

import.cpp
// 1. validate the fd is importable + get allowed memory types:
vk::MemoryFdPropertiesKHR fdProps =
    dev.getMemoryFdPropertiesKHR(
        vk::ExternalMemoryHandleTypeFlagBits::eDmaBufEXT, fd);
// fdProps.memoryTypeBits — which memory types can host it

// 2. allocate with the fd chained in:
vk::ImportMemoryFdInfoKHR imp{};
imp.handleType = vk::ExternalMemoryHandleTypeFlagBits::eDmaBufEXT;
imp.fd         = dup(fd);   // Vulkan takes ownership of its own fd

vk::MemoryAllocateInfo mai{};
mai.pNext          = &imp;
mai.allocationSize = size;      // from the image's mem requirements
mai.memoryTypeIndex = pickType(fdProps.memoryTypeBits,
                               eDeviceLocal);
auto mem = dev.allocateMemory(mai);
dev.bindImageMemory(img, mem, 0);

The dup(fd) matters — vkAllocateMemory consumes the fd; you keep your original for the DMA-Buf’s other users.

Multi-plane images

planes.txt
NV12 DMA-Buf = one fd, two planes (Y, UV):
  → VkImage per plane OR VkImage with multiple planes
    (eG8B8R82Plane420Unorm + VK_IMAGE_CREATE_DISJOINT +
     vkBindImageMemory2 per plane — the disjoint path)
  → OR: two separate VkImages (R8 + R8G8), one fd per plane —
    VAAPI often splits it this way per-layer

Handle both layouts: one multi-plane layer or a per-plane fd — the AVDRMFrameDescriptor tells you which.

Ownership and lifetime

  • The fd is the lifetime — when the decoder reuses the buffer the fd may be recycled; keep the VkImage alive until the decoder signals the buffer is free.
  • VK_EXT_queue_family_foreign is the sync half — the DMA-Buf’s data is written by a foreign queue (the decoder’s); a queue-family transfer makes the writes visible.
  • Modifiers decide the tiling — the DMA-Buf import doesn’t set the layout; eDrmFormatModifierEXT (previous post) does.

Practical rules

  • Always dup() the fd — vkAllocateMemory takes ownership of the passed fd; a shared original lets the exporter keep the buffer.
  • Check memoryTypeBits from getMemoryFdPropertiesKHR — an imported DMA-Buf may only land in DEVICE_LOCAL types on some drivers.
  • VkImage creation must declare externalMemory usage + eDrmFormatModifierEXT tiling — the combination signals “foreign memory, modifier layout”.
  • The fd’s offset/stride in AVDRMFrameDescriptor ≠ the image’s internal offsets — VkImageDrmFormatModifierExplicitCreateInfoEXT pins them if needed; the simple pDrmFormatModifiers list lets the driver pick.
  • On a failure path, fall back to av_hwframe_transfer_data + CPU staging — the import is the fast path, not the only path.

Check out similar posts by category: Vulkan C++ Linux