ginokent Blog
RSS EN

wgpu で動画を再生するときの CPU→GPU 転送コストと、ゼロコピーへの道

はじめに

wgpu ベースの UI フレームワークで動画再生を実装したので、そのパイプラインと帯域コストについて書き残す。


結論から言うと、CPU デコード → write_texture() 方式は 1080p30fps で約 249 MB/s の帯域を使う。実用上は問題ないが、ゼロコピーでもっと効率化できる余地はある。ただしゼロコピーは wgpu HAL の不安定さとプラットフォーム固有コードの量を考えると、今すぐ手を出すものではない。

動画再生パイプライン

素直な実装はこうなる。

動画ファイル


┌─────────────────────┐
│  FFmpeg デコーダ     │  CPU 上で動作
│  (libavcodec)       │
└──────────┬──────────┘
           │ YUV420P フレーム

┌─────────────────────┐
│  swscale            │  CPU 上でピクセルフォーマット変換
│  (YUV → RGBA)       │
└──────────┬──────────┘
           │ RGBA バッファ (8.3 MB @ 1080p)

┌─────────────────────┐
│  write_texture()    │  CPU → GPU 転送
│  (wgpu)             │
└──────────┬──────────┘
           │ GPU テクスチャ

┌─────────────────────┐
│  レンダーパス       │  GPU 上でテクスチャを描画
│  (wgpu)             │
└─────────────────────┘

全ステップが毎フレーム走る。デコードと色変換は CPU、テクスチャアップロードが CPU→GPU のバス、描画が GPU。

帯域コストの計算

1080p RGBA の 1 フレームあたりのデータ量:

1920 × 1080 × 4 bytes (RGBA) = 8,294,400 bytes ≈ 8.3 MB/frame

フレームレート別の帯域:

解像度fps帯域
1920×108030249 MB/s
1920×108060498 MB/s
3840×216030995 MB/s
3840×2160601,990 MB/s

1080p30fps で 249 MB/s。PCIe 3.0 x16 の帯域は約 16 GB/s あるので余裕だが、モバイルのメモリバスだと 4K60fps は厳しくなる。

SVG ラスタライズとの比較

前の記事 で書いた Stencil-then-Cover 方式の SVG アニメーションと比較すると、転送量の桁が違う。

SVG アニメーション (Stencil-then-Cover)動画再生
GPU に送るものパスの制御点 (頂点データ)ピクセル全量 (RGBA)
転送量/frame数百 B 〜 数十 KB約 8.3 MB (1080p)
30fps 時の帯域数 KB/s 〜 数百 KB/s249 MB/s
GPU の仕事ラスタライズ (頂点 → ピクセル)テクスチャサンプリングのみ
CPU の仕事SMIL 評価 (軽い)デコード + swscale (重い)

SVG は「形の定義」を送って GPU にラスタライズさせる。動画は「ピクセルそのもの」を送る。帯域コストが 3〜4 桁違う。

なぜ動画では帯域コストが不可避か

SVG のような幾何的データは制御点だけ送れば GPU がラスタライズしてくれる。一方で動画はそもそもピクセルデータがコンテンツそのもの。

SVG:  定義 (数 KB) → GPU がラスタライズ → ピクセル
動画: ピクセル (数 MB) → GPU にそのまま渡す → ピクセル

動画のフレームは写真の連続。ジオメトリから GPU で生成する代替がない。圧縮コーデック (H.264, VP9, AV1) はストレージ・ネットワーク上では効くが、GPU に渡す時点では展開済みのピクセルデータが必要になる。


つまり CPU デコード方式を取る限り、CPU→GPU の帯域コストは構造的に避けられない。

削減の余地

帯域コストをゼロにはできないが、減らす手段はある。

YUV テクスチャの直接アップロード

swscale で RGBA に変換する代わりに、YUV420P のまま GPU に送ってシェーダーで色変換する方式。

YUV420P: Y(1920×1080) + U(960×540) + V(960×540) = 3,110,400 bytes ≈ 3.0 MB
RGBA:    1920×1080×4                              = 8,294,400 bytes ≈ 8.3 MB

YUV420P は RGBA の約 36% のサイズ。転送量が 6 割減る。CPU 上の swscale も不要になるので CPU 負荷も下がる。


ただしシェーダーで YUV→RGB 変換を書く必要がある。変換行列は BT.601 / BT.709 で違うので、コーデックのメタデータを見て切り替える実装がいる。

テクスチャの再利用

毎フレーム create_texture() するのではなく、テクスチャを事前に確保して write_texture() で中身だけ書き換える。テクスチャの確保・破棄コストをゼロにできる。ダブルバッファリングにすれば GPU が前フレームをサンプリング中に次フレームを書き込める。

ゼロコピーの理想と外部テクスチャ API

究極の最適化はゼロコピー。CPU→GPU の転送自体をなくす。

【現状: CPU デコード方式】

 動画ファイル → CPU デコード → CPU メモリ (RGBA) → write_texture() → GPU テクスチャ → 描画
                                                     ~~~~~~~~~~~~~~~
                                                     この転送を消したい

【理想: ゼロコピー方式】

 動画ファイル → HW デコーダ (GPU 上) → GPU テクスチャ → 描画

                                       デコーダが直接 GPU メモリに書く
                                       CPU→GPU 転送がゼロ

ハードウェアデコーダ (VideoToolbox, MediaCodec, VAAPI など) は GPU 上にテクスチャを生成する。このテクスチャを wgpu に取り込めればゼロコピーが実現する。


ただし wgpu には「外部で作られたテクスチャを取り込む」標準 API がない。やるなら HAL レイヤーに降りて create_texture_from_hal を使う必要がある。

HW デコーダ

  │ プラットフォーム固有のテクスチャハンドル
  │ (Metal: MTLTexture, Vulkan: VkImage, etc.)

wgpu HAL レイヤー

  │ create_texture_from_hal()
  │ ※ unsafe, バックエンド固有

wgpu::Texture

  │ 通常の wgpu API で使える

レンダーパスで描画

create_texture_from_hal は wgpu の内部 API 扱いで、安定性の保証がない。バックエンド (Metal / Vulkan / DX12) ごとに実装が要る。

プラットフォーム別の現実

主要なプラットフォームにはハードウェアデコーダがあるが、現状のよくある実装では GPU→CPU→GPU の往復が発生している。

【ありがちなパターン】

 HW デコーダ (GPU) → CPU メモリにコピー → write_texture() → GPU テクスチャ → 描画
                     ~~~~~~~~~~~~~~~~~~   ~~~~~~~~~~~~~~~
                     GPU→CPU             CPU→GPU
                     往復してしまっている
プラットフォームHW デコーダデコード先CPU に引き戻す理由
macOS / iOSVideoToolboxGPU (Metal テクスチャ)wgpu に直接取り込む API がない
AndroidMediaCodecGPU (AHardwareBuffer)wgpu の Vulkan バックエンドで import する手段が限定的
LinuxVAAPI / VDPAUGPU (Vulkan / GL テクスチャ)同上
WASM (ブラウザ)WebCodecsGPU (VideoFrame)copyTo() で CPU に引き戻す必要がある

どのプラットフォームも「デコード自体は GPU で高速にやってくれるのに、wgpu に渡すために CPU メモリを経由する」という状況になりがち。


WebCodecs の VideoFrame を WebGPU の importExternalTexture() で直接取り込む方法はあるが、これは wgpu (native) ではなくブラウザの WebGPU API の話。Rust の wgpu クレートから使えるわけではない。

まとめ

現時点では CPU デコード → write_texture() 方式で実用上十分。1080p30fps で 249 MB/s の帯域は PCIe 環境なら問題にならない。


ゼロコピーを実現するには wgpu HAL の create_texture_from_hal を使ってプラットフォーム固有のテクスチャを取り込む必要があるが:

  • HAL API が不安定 (wgpu のバージョンアップで壊れる可能性)
  • Metal / Vulkan / DX12 ごとに実装が要る
  • HW デコーダとの連携コードもプラットフォームごとに異なる

パフォーマンス問題が顕在化するまでは見送りが妥当。YUV 直接アップロードとテクスチャ再利用で帯域を 6 割削れるので、先にそちらを検討する方が費用対効果が高い。