拙訳
Vulkan --- The Beginnings
- OpenGLやDX10以前では、データをアップロードして、ステートを切り替えて、描画コマンドを発行する
- ドライバは巨大なステートマシンである
- レンダリング処理はシングルスレッドでおこなわなければならない
- コマンドのサブミッションは時間のかかる処理である
- ドライバはフレームの終わりやコマンドバッファが満杯でないとサブミットしない
- 毎フレームごとに異なるコマンドをサブミットする可能性がある
- ドライバはあらかじめにコマンドバッファをベイクしたりできない
- ドライバは巨大なステートマシンである
- OpenGLやDX10時代のワークアラウンド
- ストリーミングデータのアップロードに複数のレンダリングスレッドを使う
- できるだけGPUにデータを生成する
- ステートの構成によってドローコールをバッチする
- 一部のハードウェアでは並列計算のためにコンピュートシェーダを使う
- GPUバブルを減らすためにできるだけ多くのフレームを前もってレンダリングする
- これらのワークアラウンドは最大の問題を解決するものではない
- GPUは高度に非同期化されている
- 数多くの種類のタスクを並列に処理するよう設計されている
- 計算
- DMA転送
- ラスタライゼーション
- その他(例えば、画像データ変換の高速化)
- APIの立場からだと、
- GPUへはひとつのレンダリングスレッドからのみ 処理チャンク を実行するよう要求できる
- アプリケーションは信用されていない --- API呼び出しの検証にCPU時間が使われる
- 実際の懸念事項
- 段々とCPUに縛られたアプリケーションが市場に現れてきている
- ドライバスレッドがCPU時間を消費している
- アプリケーションの複雑さが増加している
- クロスプラットフォームな方法でこれらに対処するのは簡単ではない
- Tilers(モバイルのTiled Rendering)はOpenGL ESでフルパワーを活用できていない
- ベンダ固有の解決法のみが存在する(例えば、Pixel Local Storage)
- 段々とCPUに縛られたアプリケーションが市場に現れてきている
- 以下のユースケースには言及しない
- マルチGPUのサポート
- VR
Vulkan --- Do I Need It?
- Vulkanは先程述べた問題のすべてに対処する
- コマンドキューファミリーの集まりとしてGPUを開示する
- コマンドバッファを複数のスレッドからキューにサブミットすることができる
- アプリケーションは以下の責任を持つ
- 正しいコマンドキューに処理チャンクをサブミットすること
- GPUジョブの実行の同期を取ること
- メモリヒープの集まりとして利用可能なGPUメモリを開示する
- アプリケーションは フラッシュ 、 無効化 、管理に対する責任を持つ
- アプリケーションは動いているGPUの能力に合わせる必要がある
- 作法を間違えると、GPUがハングする
- Vulkanを必要なケース
- CPUに縛られたアプリケーション:
- 情報の大多数がコンピュートやレンダリングで要求される --- ロード時に事前にベイクする
- たった2つのコマンドでフレームがレンダリングできる!
- ドライバ側の検証なし = 本当に重要なことにより多くのCPU時間を割ける
- GPUに縛られたアプリケーション:
- 以下によりGPU使用率が改善する
- 関連するキューファミリーにコンピュート及びグラフィクスジョブをサブミットする
- トランスファーキューでVRAM-VRAM間やRAM-VRAM間のコピー処理を行う
- 突然のパフォーマンス低下やスパイクがなくなる
- 予測可能な時間で、アプリケーションが指定した情報に従い、すべてのGPU側のキャッシュはフラッシュされる
- ドライバは 当てずっぽう をしなくて良くなる
- 以下によりGPU使用率が改善する
- CPUに縛られたアプリケーション:
- Vulkanを必要とするかもしれないケース
- 既存のGL4.xやDX11以下のアプリケーション:
- Vulkanに移行すれば、パフォーマンス的な利益がもたらされるかもしれないし、されないかもしれない
- かかるCPUパワーが減る可能性は高い
- 既存のGL4.xやDX11以下のアプリケーション:
- Vulkanを必要としないケース
- 迅速な開発期間を要求するプロトタイプアプリケーション:
- 検証レイヤはまだ仕様のすべてを網羅していない
- 多くの間違ったユースケースは依然として発見されていない
- 急勾配な学習曲線
- CPUにもGPUにも縛られていないような単純なアプリケーション:
- 学ぶことを目的とする場合を除いて、Vulkanから利益を得る可能性は低い
- 迅速な開発期間を要求するプロトタイプアプリケーション:
Vulkan --- Problematic Areas: Introduction
- 我々(AMD)のドライバは世に出て数カ月1経った程度
- トップレベル観察:
- Vulkanを使うのは、アプリ側でも時間的にも、負担が大きい
- アプリケーションがAというGPUで動作しても、BというGPUで動作するとは限らない
- 基本的な落とし穴:
- バリア
- 正しいデータのアップロード
- イメージ遷移
- レンダパス
- ISVは一般的には検証レイヤを使うことに 消極的 である
- 使ってください。多くの時間を節約してくれます
Vulkan --- Problematic Areas: Command Queues
- CPU側:
- Vulkanにレンダリングスレッドはない
- 複数のスレッドからGPU側のコマンドキューへ処理チャンクをサブミットできる
- GPU側:
- コマンドキューは実行できるコマンドのタイプでグループ化される
- 問題点:
- コマンドキューの数はハードウェア依存!
- キューファミリーの数はハードウェア依存!
- これの何が問題か?
- 効率的にGPUタスクを配ることは今やVulkanアプリケーションの責任である
- 解決法はデバイスの能力に応じてアップスケールやダウンスケールできなければならない
- オープンソースな解決法はまだない
- Vulkan 1.0ではひとつのコンピュート+グラフィクスのキューファミリーのみ保証されている
- 単純なアプリケーションはユニバーサルキューの存在ひとつ に頼る可能性が高いだろう
- ただし、パフォーマンス最優先で書かれたVulkanアプリケーションではない
- 解決法:
- さまざまなVulkan実装であなたのレンダリングエンジンをテストする
Vulkan --- Problematic Areas: Command Buffers
- Vulkanでは、コマンドバッファは
- GPU側で実行されるコマンドを保持している
- アプリケーションによって別途明示しない限り、再利用可能である
- 問題点:
- アプリケーションはしばしばフレームごとにコマンドバッファを記録し直す
- これの何が問題か?
- 多くのCPU時間を無駄にする
- 多くの場合では必要ない
- 解決法:
- レンダリングロジックに影響を与えるすべてのパラメータをイメージ、ストレージバッファ、ユニフォームバッファに移動する
- 必要であれば、各スワップチェーンイメージごとに一度だけすべてのコマンドバッファを事前にベイクする
- コマンドバッファの再利用性を改善するなら、インダイレクトディスパッチやインダイレクト描画のコマンドを使う
Vulkan --- Problematic Areas: Memory Management
- メモリ管理もまたVulkanアプリケーションの責任である:
- 物理デバイスは1以上のメモリヒープを報告する
- 各メモリヒープは:
- プラットフォーム固有のサイズを持つ
- device-localであるかもしれないが、必ずしも必要ではない
- メモリヒープ --- アプリケーションが直接アクセス可能ではない
- 代わりに、ドライバはハードウェア固有の”メモリタイプ”の配列を開示している
- GPUメモリを割り当てるとき、Vulkanアプリケーションはメモリタイプのインデックスを指定する
- 辛い所は?
- Vulkanとアプリケーションとの契約はとても薄い
- 以下が保証されている
- 少なくとも1つのメモリタイプはhost-visibleかつhost-coherentである
- 少なくとも1つのメモリタイプはdevice-localである
- バッファメモリとイメージメモリの割り当てはドライバ固有のメモリタイプ由来でなければならない
- そのタイプは以下に依存して変化するかもしれない
- オブジェクトの特性
- オブジェクトのタイプ
- 最も辛い所は?
- ISVは
maxMemoryAllocationCountの制限を無視する傾向にある - 同時に生存できる割り当ての限界の最大最小数は4096である
- 複雑なアプリケーションなら達するのは非常に容易い
- デスクトップのVulkan実装で報告される普通の値である
- ISVは
- 解決法:
- 利用可能なGPUメモリをアプリケーション側で事前に割り当てて管理する
- 小さなメモリ割り当てを避けて、大きなものから副割り当てを行う
Vulkan --- Problematic Areas: Descriptor Pools
- 大多数のシェーダは外部データにアクセスする
- Vulkanでは:
- デスクリプタを経由して開示される
- デスクリプタは直接生成できない
- 代わりに、アプリケーションによってインスタンス化されたデスクリプタプールから取り出される
- 問題点:
maxSetsがISVが期待しているようには働かない
- 時折見られる誤解:
- デスクリプタは”
maxSets * {poolSizeCount * pPoolSizes}個割り当てられるよね” - “できないの?アンタんとこのドライバはクソだ。Xのドライバではできてるのに!”
- デスクリプタは”
- 正確な理解:
- 最大
maxSetsのデスクリプタセットに最大Nの事前割り当てされたデスクリプタを配布する
- 最大
Vulkan --- Problematic Areas: Sparse Descriptor Bindings
- デスクリプタは後の使用のためにデスクリプタセットにグループ化される
- デスクリプタタイプとバインディングの関係はデスクリプタセットレイアウトで定義される
- GPUが消費する実際のバッファやイメージはコマンドバッファにバインドされる
- デスクリプタセットレイアウトは
vkCreateDescriptorSetLayoutで生成される
- 問題点:
- デスクリプタセットレイアウトが以下のデスクリプタセットをどのようにして探すすべきか
- バインディング0: ストレージバッファ
- バインディング2: ストレージイメージ
- バインディング1に対して
VkDescriptorSetLayoutBindingのアイテムを含める必要があるか否か?
- デスクリプタセットレイアウトが以下のデスクリプタセットをどのようにして探すすべきか
- 解決法:
- このアプリケーションは非効率であり、ダミーバインディングはパフォーマンスに悪影響を及ぼす
- ただし、本当にそのバインディングが必要であるならやってもいい
- 使わないバインディングの
descriptorCountを0に設定していることを確かめる
Vulkan --- Problematic Areas: Images
- Vulkanでは、
- テクスチャステートはイメージオブジェクトに格納される
- テクスチャデータはメモリオブジェクトに格納され、イメージオブジェクトにバインドされる
- イメージオブジェクトはイメージデータの指定の特性により生成される:
- 以下のようなビットや数値:
- タイプ(1D、2D、3D)
- ベースのMIPMAPサイズ
- MIPMAP数
- タイリングタイプ
- 使い方フラグ
- その他
- 以下のようなビットや数値:
Vulkan --- Problematic Areas: Image Usage Flags
- 生成時に前もってのイメージの使い方の宣言を要求する
- 使い方は1つ以上のビットの組み合わせである
- ドライバはあるイメージの使い方に対するフォーマットのサポートを提供しなくても良い
- そのときは、使い方設定は以下に制限する:
- サポートされるメモリタイプ
- 最大イメージ解像度、サンプル数、など
- 共通の問題点:
- アプリケーションは不正確なイメージの使い方を指定する
- 例:
VK_IMAGE_USAGE_TRANSFER_DST_BITを伴ってイメージを生成することを検討する- イメージはカラーアタッチメントとして使用してはならない
- アプリケーションが気にしない
- 結果:
- 未定義動作
- 解決法:
- 検証が有効化されていれば、この種の問題は簡単に検出できる
Vulkan --- Problematic Areas: Image Tiling
- タイリング設定はGPUによって使われるイメージデータレイアウトを決定する:
- Linear: イメージは行優先 の配置で、各行は潜在的にパッディングされる
- Optimal: プラットフォーム固有のデータ配置で、速度に最適化される
- 線形タイル化イメージの特性:
- 最適タイル化イメージに提供される機能のサブセットをサポートする
- パフォーマンス的には劣る
- なぜ線形イメージに悩まされるのか?
- GPUでレンダリングされたイメージデータを 読み戻す のに必要である場合に極めて重要
- 共通の問題点:
- ISVはデータを最適タイル化イメージに直接コピーする
- 典型的なシナリオ:
- イメージAは
VK_IMAGE_TILING_OPTIMALのタイリング設定で生成される - アプリケーションはイメージAに対して
vkGetImageSubresourceLayoutを呼ぶ - アプリケーションは”報告された”特徴を用いてデータをアップロードすることを試みる
- イメージAは
- 解決法:
- 最適タイル化イメージにデータをコピーするためにステージングバッファを使う:
- バッファオブジェクトを生成して、メモリ領域をバインドする
- データで埋める
- イメージを
GENERALかTRANSFER_DST_OPTIMALのレイアウトに遷移する vkCmdCopyBufferToImageの呼び出しによりコピー処理をスケージューリングする- コマンドバッファをサブミットして、実行が完了するまで待つ
- 一時的バッファオブジェクトを解放する
- 覚えておくこと: バッファからイメージへのコピー処理はマルチサンプリングイメージでは働かない
- そこにデータをアップロードするためには、実際のディスパッチや描画の呼び出しを使う必要があるだろう
- 最適タイル化イメージにデータをコピーするためにステージングバッファを使う:
Vulkan --- Problematic Areas: Image Layout Transitions
- GPUは 実行中に データを圧縮・展開したり再配置したりするかもしれない
- 帯域幅のプレッシャーが小さくなるので、パフォーマンスがよくなる
- DX11以下やOpenGL: 透明でヒューリスティック駆動な処理
- Vulkan: イメージレイアウトの遷移時に起こる
- 例: DCC(Delta Color Compression)
- ハードウェアレベルの最適化:
- ハードウェアのアーキテクチャとハードウェアの世代は異なる
- 一般にベンダ固有である
- Vulkanでは:
- イメージは使用前に正しいレイアウトへ移動されなければならない
- これは以下によって要求することができる
- コマンドバッファにイメージバリアを注入する
- 正しいレンダパスとサブパスの構成
- 間違うと、視覚的な破損が起こるかもしれない
(画像)
- 一般的な問題:
- イメージを不正なレイアウトに遷移する
- 例:シェーダで読みたいのに、前のバリアが
TRANSFER_DST_OPTIMALである
- 例:シェーダで読みたいのに、前のバリアが
- イメージを不正なレイアウトに遷移する
(画像)
- 一般的な問題:
2. イメージバリアで定義される前のレイアウトが不正確である。
- 例:前のバリアを
SHADER_READ_ONLY_OPTIMALにしたのに、後ろのバリアをTRANSFER_DST_OPTIMALのままになっている
(画像)
- 一般的な問題: 3. “なあ、AMDさんよ、俺のアプリケーションはY社のドライバでは動くんだが、お前んとこのドライバではダメなんだよなぁ!” - いくつかのベンダはイメージバリアを無視しているけど、我々(AMD)はそれをしていない - すると、間違っているのは誰のドライバでしょうか?:)
- 解決法:
- 検証レイヤは絶えず改善している --- 使おう!
- 色んなVulkan実装でソフトウェアをテストしよう
Vulkan --- Problematic Areas: Image Layout Transitions & Renderpasses
- 一般的な問題: 4. ISVはレンダパスがとうやってイメージのサブリソースを遷移するかを誤解している - レンダパスはVulkanにおける 新しく複雑な 概念である - ドライバに”時間旅行”させ、先んじて知らせておくために導入される - どのカラーまたは深度ステンシルのアタッチメントがラスタライズされ、アクセスされるか(いつ?どうやって?) - どのイメージのサブリソース範囲は同期される必要があるか(いつ?どうやって?) - どのレイアウトへイメージのサブリソースは遷移されるべきか、そしてそれはいつか - これは間違えてしまうほどに情報が膨大である。手動で記述するときは特に :)
(レンダパス: 各”レンダリングパス”はユーザ指定のサプバスで記述される)
(レンダパス: 実行順はユーザ指定の依存関係から推定される)
(レンダパス: イメージ遷移はサブパスとレンダパスの構成から推定される)
Vulkan --- Problematic Areas: GPU-Side Synchronization
- 超一般的なVulkanのGPU側コマンド実行のルール:
- コマンドキューはそれぞれが独立に実行する
- キューAにサブミットしたとき、コマンドバッファは特定の順序で実行する
- 順序はバリア、レンダパス、同期プリミティブによって強要されない限り、
- サブミットされたコマンドは並列に実行されるかもしれない
- サブミットされたコマンドは 順序に関係なく実行 されるかもしれない
- 以下の同期オブジェクトが使える:
- イベント(**キュー内**同期)
- セマフォ(**キュー間**同期)
- フェンス(サブミットしたジョブチャンクが実行し終わるまで、CPUスレッドをブロックする)
- 問題:
- ISVは時々毎フレームに同期オブジェクトを生成する
- 解決法:
- すべてのコストを避けること!
- 以下を覚えておく
- イベントはCPU側とGPU側をリセットさせることができる
- フェンスはCPU側をリセットさせることができる
- セマフォは正常に待った後で自動的にリセットさせることができる
- さらに実行可能であるならば、先んじてスワップチェーン毎のイメージに同期オブジェクト一式をベイクする
Footnotes
-
訳注:この文書がまとめられたのは2016年3月頃 ↩