はじめに
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×1080 | 30 | 249 MB/s |
| 1920×1080 | 60 | 498 MB/s |
| 3840×2160 | 30 | 995 MB/s |
| 3840×2160 | 60 | 1,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/s | 249 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 / iOS | VideoToolbox | GPU (Metal テクスチャ) | wgpu に直接取り込む API がない |
| Android | MediaCodec | GPU (AHardwareBuffer) | wgpu の Vulkan バックエンドで import する手段が限定的 |
| Linux | VAAPI / VDPAU | GPU (Vulkan / GL テクスチャ) | 同上 |
| WASM (ブラウザ) | WebCodecs | GPU (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 割削れるので、先にそちらを検討する方が費用対効果が高い。