VK_KHR_video_decode_queue と VK_KHR_video_decode_h264:Vulkan ネイティブの H.264 デコード

VAAPI でデコードしてその結果をインポートするパスは機能するが、Vulkan にはネイティブな動画デコード API がある。VK_KHR_video_decode_queue と VK_KHR_video_decode_h264(および h265、av1)だ。セッション/DPB/ vkCmdDecodeVideoKHR のパターンは AV1 の記事と同じで、H.264 ではコーデック 関連の構造体が AV1* の代わりに VkVideoDecodeH264* になる。これにより VAAPI レイヤーが完全に不要になる。VA-API コンテキストも DMA-Buf エクスポートも不要で、デコードされたピクチャはすでに VkImage だ。

なぜ VAAPI+インポートではなくネイティブなのか

compare.txt
VAAPI パス:   FFmpeg hwaccel → VASurface → av_hwframe_map → DMA-Buf
              → vkImportMemoryFdKHR → VkImage  (API が2つ、デバイス
              コンテキストが2つ、modifier/foreign-queue の一連の処理)

Vulkan パス:  avcodec がビットストリームを自分のコードに渡す →
              vkCmdDecodeVideoKHR に供給 → 出力 VkImage(API は1つ、
              フレームはすでに Vulkan 内 — インポートは一切不要)

VAAPI パスが存在するのは FFmpeg がすでにそれを実装しているからだ。 デコードループを自分で制御できるなら、ネイティブパスの方がクリーンだ。

セッション

session.cpp
vk::VideoDecodeH264ProfileInfoKHR h264{};
h264.stdProfileIdc   = STD_VIDEO_H264_PROFILE_IDC_HIGH;
h264.pictureLayout   = vk::VideoDecodeH264PictureLayoutFlagBitsKHR
                       ::eProgressive;

vk::VideoProfileInfoKHR prof{};
prof.pNext = &h264;
prof.videoCodecOperation =
    vk::VideoCodecOperationFlagBitsKHR::eDecodeH264;
prof.chromaSubsampling = vk::VideoChromaSubsamplingFlagBitsKHR::e420;

// AV1 の記事と同様:eVideoDecodeKHR 対応のキューファミリで
//   vkCreateVideoSessionKHR を呼ぶ

デコードコマンド

decode.cpp
vk::VideoDecodeH264PictureInfoKHR pic{};
pic.pStdPictureInfo        = &stdPic;    // StdVideoH264PictureInfo
pic.sliceCount             = nSlices;
pic.pSliceOffsets          = sliceOffsets;  // srcBuffer へのバイトオフセット

vk::VideoDecodeInfoKHR dec{};
dec.pNext = &pic;
dec.srcBuffer        = bitstreamBuf;   // 生の NAL/フレームデータ
dec.dstPictureResource = {dstView, eVideoDecodeDstKHR};
dec.referenceSlotCount = nRefs;
dec.pReferenceSlots    = refs;

cmd.beginVideoCodingKHR(begin);
cmd.decodeVideoKHR(dec);
cmd.endVideoCodingKHR({});

自分でパースする必要があるもの

parse.txt
CPU 側で自分で用意する必要があるもの:
  StdVideoH264SequenceParameterSet   (SPS)
  StdVideoH264PictureParameterSet    (PPS)
  StdVideoH264PictureInfo            (フレームごとのヘッダー)
  srcBuffer へのスライスごとのオフセット

FFmpeg/libavcodec の h264 パーサーはこれらをすべて生成する。
実践的なパターンは「FFmpeg に demux とパースを任せ、パース済みの
ヘッダーと NAL を Vulkan デコーダーに渡す」というものだ。

AV1 との DPB の違い

  • H.264 の参照モデルはスライディングウィンドウ / MMCO — 参照 スロットはフレームごとに変わり、AV1 の 8 スロットモデルと同様に VkVideoReferenceSlotInfoKHR で管理する。
  • field_pic_flag/インターレースの扱い — H.264 にはインターレース モードがある(eFieldInterlaced レイアウト)。AV1 はプログレッシブのみだ。
  • H.264 の maxDpbSlots は典型的には 16 — AV1 の 17+8 より小さい。

実践的なルール

  • まずキューのケイパビリティを確認する。QueueFlagBitsKHR::eVideoDecodeKHR と eDecodeH264 を指定した vkGetPhysicalDeviceVideoCapabilitiesKHR だ。 すべての GPU がデコードキューを公開しているわけではない (VAAPI フォールバックでカバーできる)。
  • デコードセッションには SPS/PPS を保持する VkVideoSessionParametersKHR が必要になる。ストリームのパラメータセットが変わったら更新する。
  • 出力は自分で確保した VkImage に書き込まれる。pictureFormat(例: eG8B8R82Plane420Unorm)は DPB フォーマットと一致させ、そのイメージを dstPictureResource として直接バインドする。デコード済みフレームは そのままテクスチャになっている。
  • インターレース H.264 が厄介な点だ。pictureLayout フラグはストリームと 一致させる必要がある。プログレッシブのみのストリームなら簡単だ。
  • VAAPI との比較:ネイティブ Vulkan デコードでは DMA-Buf エクスポートと foreign-queue 転送が不要になる。トレードオフとして、FFmpeg が処理して いた DPB とパラメータのパースを自分で引き受けることになる。

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