VK_EXT_queue_family_foreign 解説:インポートしたバッファの所有権を取得する

DMA-Buf をインポートした(external_memory_dma_buf の記事)。しかし デコーダーは自分のキューで、異なるドライバー内でそれに書き込んだ。 Vulkan のメモリモデルでは、書き込みは所有権を移転しない限り、 書き込みを行ったキューファミリにのみ可視だ。 VK_EXT_queue_family_foreign は、センチネルのファミリインデックス VK_QUEUE_FAMILY_FOREIGN_EXT —「最後に触れた側」のキュー — と、 バッファを自分のキューファミリに移すセマフォ/バリアの仕組みを提供する。

問題

problem.txt
デコーダーキューが DMA-Buf に書き込む → 自分のグラフィックスキューがサンプルする
                                 ↑  Vulkan のメモリモデルでは異なる
                                    「キューファミリ」 —
                                    書き込みの可視性は保証されない

転送なしのサンプリングは未定義動作だ。運よく動くこともあれば、 古いデータを返すこともある。

所有権の転送

transfer.cpp
// デコーダー側(概念的 — エクスポーターがセマフォをシグナルするか、
// 完了を知る方法がある):
//   DMA-Buf は VK_QUEUE_FAMILY_FOREIGN_EXT に「所有」されている

// 自分のキュー — release/acquire ペア、または両フィールドを持つバリア:
vk::ImageMemoryBarrier imb{};
imb.srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT;  // 以前:foreign
imb.dstQueueFamilyIndex = myGraphicsFamily;             // 以後:自分
imb.image    = importedImg;
imb.oldLayout = vk::ImageLayout::eGeneral;
imb.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
// srcAccess = foreign 側が行ったもの — 多くは同期を運ぶ
//   セマフォ wait 経由の eMemoryWrite
cmd.pipelineBarrier(eTopOfPipe, eFragmentShader, {}, {}, {}, {imb});

srcQueueFamilyIndex = VK_QUEUE_FAMILY_FOREIGN_EXT がポイントだ。 これは「どのファミリが書き込んだか分からないので、取得する」という 意味だ。エクスポーターの完了を表すセマフォ wait と組み合わせれば、 書き込みが可視になる。

セマフォの経路

sem.cpp
// デコーダーがシグナル。こちらは wait + acquire:
vk::SubmitInfo si{};
si.waitSemaphoreCount   = 1;
si.pWaitSemaphores      = &decoderSem;    // VA-API/V4L2 のフェンスから
si.pWaitDstStageMask    = &fragmentStage;
// … 上記のバリアは同じコマンドバッファに入れる …
queue.submit(si);

実際には VAAPI/V4L2 は sync fd 経由で完了をシグナルし、それが VkSemaphore になる。vkGetSemaphoreFdKHR/vkImportSemaphoreFdKHR で取り込む。

全体の流れ

flow.txt
FFmpeg のデコード完了 → AVDRMFrameDescriptor がプレーンの fd を持つ
→ DMA-Buf パスで VkImage としてインポート
→ バリア:FOREIGN_EXT → グラフィックスキュー、eGeneral → eShaderReadOnly
→ グリッドシェーダーでサンプリング
→ フレーム退役時:返却(dst=FOREIGN_EXT)するか、VkImage をそのまま
  破棄 — DMA-Buf はデコーダーのプールに戻る

実践的なルール

  • VK_QUEUE_FAMILY_FOREIGN_EXT は擬似ファミリだ。 queueFamilyProperties には決して現れない。 srcQueueFamilyIndex/dstQueueFamilyIndex フィールドにだけ使う。
  • バリア単体では順序付けは運ばれない。セマフォ wait と組み合わせて、 自分の読み取りが始まる前に foreign 側の書き込みが完了している ことを保証する。
  • 使い終わったら release する。dst=FOREIGN_EXT のバリアはバッファを 返す。DMA-Buf が再利用ではなく破棄される場合に限り省略できる。
  • ドライバーとハードウェアが実際に同じキューファミリを共有する同一 デバイスのインポートでは、バリアは no-op だ。ただし仕様上は宣言が 必要だ。
  • これを怠ると、症状は断続的な古いフレームになる。典型的な 「最初のフレームは緑、以降のフレームは正常」という動画インポートの バグだ。

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