FFmpeg hwcontext_vulkan で GPU ビデオデコードとゼロコピー Vulkan 相互運用を行う方法

FFmpeg 7+ には第一級の Vulkan パスがある:-hwaccel vulkan は VK_KHR_video_decode_queue 経由で H.264/HEVC/AV1 をデコードし、 フレームは FFmpeg 自身の AVHWDeviceContext 内で VkImage として存在する。VA-API なし、DMA-Buf エクスポートの手間なし。 アプリがすでに VkDevice を所有しているなら、それを共有する (または2つの間でフレームをマップする)ことができる。

コマンドラインでの証明

cli.sh
# 完全に GPU 上でデコード——出力はデバイスに留まる:
ffmpeg -init_hw_device vulkan=vk0 -hwaccel vulkan \
       -hwaccel_output_format vulkan \
       -i input.mp4 -f null -

# デコード → vulkan フィルタ → h264_vulkan に再エンコード:
ffmpeg -init_hw_device vulkan=vk0 -hwaccel vulkan -i in.mp4 \
       -vf "scale_vulkan=w=1280:h=720" \
       -c:v h264_vulkan -y out.mp4

scale_vulkan、avgblur_vulkan、chromakey_vulkan、hflip_vulkan、 overlay_vulkan は実際の vkCmdDispatch/vkCmdDraw として動作 する。ホストコピーなし。エンコーダ h264_vulkan/hevc_vulkan (FFmpeg 7.1+/8)は VK_KHR_video_encode_queue を使う。

API 側

device.cpp
// デバイスを作成(または av_hwdevice_ctx_create_derived で自分のを再利用):
AVBufferRef* hwDev = nullptr;
av_hwdevice_ctx_create(&hwDev, AV_HWDEVICE_TYPE_VULKAN,
                       /*device*/ nullptr, /*opts*/ nullptr, 0);
// → FFmpeg がデフォルトアダプタを選ぶ。固定するには "vulkan=0" /device アドレスを渡す

// デコードでフレームは AV_PIX_FMT_VULKAN として届く:
AVFrame* f = av_frame_alloc();
avcodec_receive_frame(dec, f);
if (f->format == AV_PIX_FMT_VULKAN) {
    AVVkFrame* vkF = (AVVkFrame*)f->data[0];
    VkImage   img   = vkF->img[0];         // プレーンごとの VkImage
    VkSemaphore sem = vkF->sem[0];         // デコード完了時
    VkSemaphore sem2= vkF->sem[1];         // アプリが待つべき時
    VkImageLayout layout = vkF->layout[0];
    // Y/U/V 用プレーン 0..2 — 同じ VkImage かプレーンごとに分離
}

AVVkFrame が鍵となる型:VkImage、レイアウト、2つのセマフォを 所有する。sem[0] は「デコード完了」をシグナルし、sem[1] は アプリの読み取りが終わったときにあなたがシグナルし、FFmpeg が フレームスロットを再利用できるようにする。

レンダラーとの共有

interop.txt
FFmpeg vk デバイス ─┐
                    ├─ 同じ VkDevice(派生コンテキスト)→ イメージ直接使用
自分の VkDevice   ──┘   ↓ eShaderReadOnlyOptimal へのレイアウト遷移
                    または av_hwframe_map → AV_PIX_FMT_DRM_PRIME → dmabuf
                    → 自分のレンダラーで vkImportMemoryFdKHR(VA-API パス)

2つの統合レベル:

  • 派生デバイス——自分の VkDevice から av_hwdevice_ctx_create_derived(FFmpeg 7.1+)→ フレームは あなたのデバイスメモリ空間にある。eShaderReadOnlyOptimal に遷移し、バインドしてサンプルする。ゼロコピー。
  • マップパス——av_hwframe_map(VULKAN → DRM_PRIME) が DMA-Buf fd をエクスポート → レンダラーにインポート。 デバイス/プロセス間で動作する。
map.cpp
AVFrame* drm = av_frame_alloc();
av_hwframe_map(drm, vkFrame, AV_HWFRAME_MAP_READ);
// drm->data[0] = AVDRMFrameDescriptor* → VA-API 投稿と同じパス

機能プローブ

probe.cpp
// ドライバーはビデオキューを公開しなければならない:
auto qf = dev.getQueueFamilyProperties();
bool hasDec = std::ranges::any_of(qf, [](auto& q){
    return bool(q.queueFlags & vk::QueueFlagBitsKHR::eVideoDecodeKHR);});
// コーデックレベル:vkGetPhysicalDeviceVideoCapabilitiesKHR と
//   VkVideoProfileInfoKHR{videoCodecOperation = eDecodeH264/…}

実践的なルール

  • hwaccel_output_format vulkan が全体の要点だ。それなしでは FFmpeg は hwdownload にフォールバックし、とにかく CPU コピーの代金を払う。
  • AVVkFrame セマフォはデコーダと共有される。読み取りが 本当に終わったときだけ sem[1] をシグナルする。早いシグナル = 破れたフレーム。
  • レイアウトはデコーダが残したもの(通常 eVideoDecodeDstKHR)。 サンプリング前に遷移する。eGeneral を仮定しない。
  • h264_vulkan/hevc_vulkan でのエンコードには VK_KHR_video_encode_queue が必要。サポートを確認する。 デコードより稀だ。
  • 弱い/モバイルドライバーでは vulkan hwaccel が VA-API より 遅いことがある。実績のあるデコードパスをゼロコピー末端と 交換する。両方を測定する。

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