DMA-Buf 解説:Linux ドライバ間のゼロコピーバッファ共有

すべての Linux GPU 関連 API は独自のバッファオブジェクトを 持つ——V4L2 バッファ、VA-API サーフェス、Vulkan VkDeviceMemory、GBM gbm_bo。それらの間でフレームを コピーすると帯域幅と CPU を浪費する。DMA-Buf は同じ物理 メモリをすべてに見せるカーネル側メカニズム:1つのドライバが 割り当てたバッファがプレーンなファイルディスクリプタとして エクスポートされ、他のドライバが mmap またはインポート できる。

モデル

dmabuf-flow.txt
プロデューサドライバ                  コンシューマドライバ
(V4L2 / VA-API / GPU)                (Vulkan / OpenCL / 他の GPU)
      │                                    │
      ▼                                    ▼
 バッファを割り当て ──export──► dma-buf fd ──import──► 同じページ、
                                                       ゼロコピー

dma-buf fd はメモリではない——バッキングページと簿記 (エクスポートしたドライバ、map/sync の ops テーブル)を 所有するカーネル struct dma_buf への参照で、fd が ハンドル、よって通常の fd ルールがすべて適用:dup()、 unix ソケット越しの SCM_RIGHTS 受け渡し、exec での FD_CLOEXEC など。

なぜポインタではなく fd か

ファイルディスクリプタはカーネルの既存ケイパビリティ メカニズム:

  • dup() が同じ dma-buf をプロセス間で共有
  • unix(7) ソケット越しの SCM_RIGHTS が無関係なプロセスに 渡す
  • fd への poll() がバッファ準備完了をシグナル(フェンス)
  • カーネル refcount が最後の fd が閉じるまでページをピン

エクスポーターは内部に DMA 予約オブジェクト(dma_resv)を 保持——それにアタッチされたフェンスでコンシューマが プロデューサの書き込み完了を待てる。

モディファイア問題——最大の落とし穴

裸の VkDeviceMemory サイズのブロブでは不十分:GPU は イメージを線形に格納しない。タイル化と圧縮を行い、 ベンダーごとにタイル化が異なる。だから dma-buf は DRM フォーマットモディファイア(drm_fourcc.h、 DRM_FORMAT_MOD_*)と対になる:

modifier.hint
DRM_FORMAT_MOD_LINEAR            = 0              — プレーン線形
I915_FORMAT_MOD_X_TILED          = 0x100000000000001
I915_FORMAT_MOD_Y_TILED          = 0x100000000000002  (Intel)
DRM_FORMAT_MOD_AMD_GFX9_GFX10    = ベンダータイル   (AMD)

モディファイアはバイトがどのように配置されるかを言う 64 ビット値。両側が一致しなければならない——Vulkan は VK_EXT_image_drm_format_modifier でサポートをクエリし、 エクスポーターにはコンシューマのモディファイアリストを 伝える必要。

失敗モードは静か。 モディファイアが一致しなければ、 インポートは成功するかも——ドライバは喜んで VkImage を 作成——そしてサンプルされたピクセルはゴミ(線形として 読まれたタイルバイト)。エラーなし、間違った色のみ。 コミット前に常にエクスポーターのモディファイアと vkGetPhysicalDeviceImageFormatProperties2 + VkPhysicalDeviceImageDrmFormatModifierInfoEXT の結果を 交差させる。

Vulkan 側

VK_EXT_external_memory_dma_buf が fd をインポート:

import.cpp
// 1. GPU がこのフォーマットで受け入れるモディファイアをクエリ:
vk::PhysicalDeviceImageFormatInfo2 fmtInfo{};
fmtInfo.setFormat(vk::Format::eR8Unorm)
       .setType(vk::ImageType::e2D)
       .setTiling(vk::ImageTiling::eDrmFormatModifierEXT)
       .setUsage(vk::ImageUsageFlagBits::eSampled);
// … pNext の DrmFormatModifier リスト → 対応モディファイアを列挙

// 2. ネゴシエートしたモディファイアでイメージを作成:
vk::ImageDrmFormatModifierExplicitCreateInfoEXT modInfo{};
modInfo.setDrmFormatModifier(chosenModifier)
       .setPlaneLayouts(planeLayouts);

vk::ImageCreateInfo ici{};
ici.setPNext(&modInfo)
   .setTiling(vk::ImageTiling::eDrmFormatModifierEXT)
   .setExternalMemoryHandleTypeFlags(
        vk::ExternalMemoryHandleTypeFlagBits::eDmaBufEXT);
// …

// 3. fd をインポート:
vk::ImportMemoryFdInfoKHR import{};
import.setHandleType(vk::ExternalMemoryHandleTypeFlagBits::eDmaBufEXT)
      .setFd(dmaBufFd)
      .setMemory(memory.get());
device.bindImageMemory2(bindInfos); // または vkBindImageMemory + ImportInfo

vkImportMemoryFdKHR は成功時に fd を消費——呼び出し側は その後 close() してはならない(二重クローズが番号を後の インポートに再利用 → ドライバが解放済み BO をサンプル → GPUVM フォルト)。

出現場所

  • V4L2 → Vulkan: VIDIOC_EXPBUF がキャプチャバッファを エクスポート;テクスチャとしてインポート——ゼロコピー カメラ取り込み。
  • VA-API → Vulkan: av_hwframe_map() が AVDRMFrameDescriptor(dma-buf fd + fourcc + モディファイア) を生成し、プレーンごとに VkImage としてインポート。
  • プロセス間 Vulkan → Vulkan: vkGetMemoryFdKHR で VkDeviceMemory をエクスポート——しかし fd は dma-buf なのでインポーターは eOpaqueFd ではなく eDmaBufEXT が必要。

各パスの動作例:

Example: VulkanExamples/p48_v4l2

Example: VulkanExamples/p49_vaapi

Example: VulkanExamples/p70_ipc

実践的なルール

  • 常にモディファイアをネゴシエート—— DRM_FORMAT_MOD_LINEAR を仮定しない。
  • fd は消費可能——所有権を慎重に追跡(成功したインポート後に -1)。
  • fd をインポートすべき子を exec する時 FD_CLOEXEC が 刺す——fork 前にフラグをクリア。
  • カーネルは dma_resv フェンスでアクセスを直列化—— DMA_BUF_IOCTL_SYNC または同等のドライバ内同期で プロデューサフェンスを尊重。

同じカテゴリの類似記事をチェック: Vulkan Linux