「No Graphics API」の記事を読んだ
先月投稿されたブログ記事でだいぶ話題になっていた「No Graphics API」という記事について、遅ればせながら先日読みました。
勉強になる部分も多く興味深い記事だったので、その感想やメモ書きなどをまとめておきます。
記事の概要
まずは該当記事の概要について簡単にまとめます。 詳細については元記事を参照してください。
Introduction
近代的な low-level Graphics API(DX12 / Vulkan / Metal)は細かい制御ができる一方で非常に複雑になっている。 特に PSO (Pipeline State Object) の爆発が問題となっている。
この記事で書かれているハードウェアの詳細の情報源などについても触れられている。
Low-level graphics APIs change the industry / Modern APIs?
DX12 / Vulkan / Metal は、ドライバ任せだった最適化をアプリケーション側に移譲した。 CPU オーバーヘッド削減、マルチスレッド対応、明示的制御という点で業界に大きな影響を与えたが、一方でエンジン開発者の負担と複雑性を急激に増大させた。
現代的といいつつ、この API は既に 10 年の歴史があり、この API が作られたときのハードウェア制約による API の設計の決定があった。 今のハードウェアを前提にするなら必要ない API 設計上の妥協点も残されているので、それらを排除すれば大幅に API を簡略化できるはず。
The history of GPUs and APIs
GPU のハードウェアの歴史がまとめられている。 特にメモリ管理について、固定機能で特殊なことをやるのではなく汎用的なメモリ管理とバインドレスなアクセスに移行してきたことが述べられている。
Modern GPU memory management
現代的な GPU の仕組みでは統合型 GPU では UMA (Unified Memory Architecture) が使われ、ディスクリート GPU では PCIe 越しに PCIe Resizable BAR をサポートしている。 GPU ヒープ全体を CPU からマッピング可能となっている。 これによって可能になる gpuMalloc と gpuFree というようなメモリの割当やポインタを渡して一括でメモリをコピーする gpuMemCpy などの API について議論している。
Modern data
現代 GPU では、CUDA・Metal・OpenCL のような C/C++系シェーダー言語により、64 ビットポインタを用いたワイドロードと効率的なアンパックが可能で、高速なデータ処理が実現されている。 Vulkan の Buffer Device Address などで 64 ビットポインタは利用可能だが、HLSL/GLSL の言語的制約により実用性とデバッグ性は低い。 将来のグラフィックスシェーディング言語には、CUDA や Metal のようなネイティブポインタ対応とライブラリエコシステムが必要というような話をしている。
Root arguments
Root arguments というものを導入して、シェーダー起動時に必要なディスクリプタやバインディングという概念を完全に排除し、シェーダにポインタと即値だけを直接渡す形でシェーダーを起動できるようにするという提案。
現代的な GPU ではアクセスが uniform かどうかはバッファの種類ではなく実際のアクセスのされ方で決定している。 したがって、バッファの種類を区別する特別な API は不要なはず。 現代においてはバインディングやスロット管理は不要にできるはず。
Texture bindings
テクスチャのデスクリプタの実装方法について 2 通りあることと、それらはテクスチャデスクリプタのヒープ抽象化層で統合可能であるということ。 テクスチャヒープ内にインデックスでアクセスできるようにして、このテクスチャオブジェクトの情報を直接ヒープに書き込めれば、現在のテクスチャのデスクリプタ周りの複雑性は排除できる。 これによってすべての既存のテクスチャバインディング API は不要になる。
Shader pipelines
この章ではシェーダーのルートデータが 64bit ポインタでテクスチャがインデックスで管理される場合、テクスチャのバインド設定、バッファのバインド、バインドグループ(デスクリプタセットなど)、ルートシグネチャなどを排除して Shader Pipeline という概念を小さくできるということを述べている。
Static constants
WebGPU などでも使われている Pipeline Overridable Constants の仕組みについて。 現代的なエンジンはシェーダを多数の事前生成バリアントとして扱っており、シャドウのあるなしとかアルファクリップのあるなしとかそういう一部だけ処理が異なり大部分が処理が同じマテリアルのコードが爆発するようなことが多い。 それらについて static constants を使えば、シェーダのバリアント爆発を抑えられるだろうと述べている。
Barriers and fences
テクスチャのレイアウト変更まで含むメモリバリアが当時必要だった背景と、もはやそれらが現代的な GPU では不要になっていること。 リソースに紐づけることなく dispatch の間に flush する必要があるかのバリアが最低限必要なだけになるはず。 これはタイムラインセマフォと同じようにタイムラインベースのバリア同期で実装できるはずという提案。
Command buffers
コマンドバッファは使いまわしせずに one time なコマンドバッファだけで良いだろうとの話。
Graphics shaders
グラフィックスのシェーダーには、ピクセルシェーダー、頂点シェーダー、ジオメトリシェーダー、ハルシェーダー、ドメインシェーダー、パッチシェーダー、メッシュシェーダー、タスクシェーダー、そしてレイトレ用に追加でいくつかのシェーダーがある。
一方で CUDA は単一のエントリーポイントのカーネルを使っている。 CUDA では Tensor Core などの専用ハードウェアも組み込み関数として使える。
CUDA も見習いつつシェーダーの種類を簡素化できないかという問題提起。
Raster pipelines
頂点処理とピクセル処理、もしくはメッシュシェーダーとピクセル処理の組み合わせが使われている。 ジオメトリシェーダーやテッセレーションシェーダー周りは失敗作としてほとんど使われていない。
このメインで使われている方の組み合わせについて PSO の爆発を抑えるために PSO の状態を最小限に抑えたいという話の導入。
Graphics shader bindings
頂点の Input Assembly の入力ストリームは現代では必要ないはず。 現に Mesh Shader では頂点ストリームではなく自分でバッファにアクセスする形になっている。 この頂点のバインディングは PSO から排除できるのではないかという提案。
Rasterizer state
ラスタライザ状態(ブレンド・深度・ステンシル等)を単一の巨大な PSO 状態の一部として固定するのをやめ、役割ごとに分解し、必要最小限だけを固定が必要な状態として扱うことを提案している。 現行の API ではラスタライザの多くの状態が一体化された PSO の一部として扱われている。 しかしハードウェアの依存度は設定によって異なり、部分的に更新するとかも可能なプロパティまでまとめて PSO に固定で入ってしまっている。 それらを解体してより小さい固定機能用の状態として扱うことを提案している。
Indirect drawing
Indirect Draw / Dispatch を第一級として扱いたいという話。 これをバッファに書き込んだオブジェクトとインデックスではなく、ここでもポインタを使えばシンプルな API になるという提案。 これによって draw や dispatch についてもこの手の命令もメモリ上のデータとして扱えるという話をしている。
Render passes
ラスタライザハードウェアの準備としてレンダリングターゲットのバインドとクリア処理が必要。 このバインドに特殊な API を使わなくても上で定義した GPU テクスチャオブジェクトを渡せば、それでレンダリングパスに必要な情報は作れるはず。
Prototype API
ここまでの説明を元に提案する API が並べてあります。
既存のグラフィクス API の関数などの数に比べればだいぶスリムになっている。
Tooling
デバッグ可能性などについて述べています。 ポインタを使いまくるのにデバッグできるのかとかセキュリティ的に安全なのかという話をしています。
この記事の主張では、既存の C/C++に優秀なデバッガがあるしポインタを追いかけることもできるだろうということ、そして最近の GPU はメモリを仮想化していてプロセスごとにページテーブルを持っているのでセキュリティ的にも問題ないということを述べている。
Translation layers / Min spec hardware
この新しい API の上に既存 API のエミュレーションレイヤーを乗せることができること、そして現代的な GPU でもう既にこの API の実装はできるはずであることを述べている。 この API が遙か未来の提案ではなく、今すぐ実現できるはずであるということを主張している。
Conclusion
10 年前の API では CPU 手動のバインディングモデルを想定されていたが、現代においてはバインドレスをベースに統一すればより使いやすい API になるだろう。 CUDA のような直接のメモリアクセスや固定機能を関数で呼べる仕組みはグラフィックス API でも見習うところがあるだろう。
感想
以下にこの記事を読んで思い浮かんだ感想を雑多に列挙していきます。
既存のいらなくなったコア機能とその裏側についての感想
非常によく整理されており「そうだったのか」という感じ。
メモリバリアがかつてなぜ必要で、今は必要なくあたらしい拡張機能ができているかというあたりは勉強になりました。 Vulkan の VK_KHR_unified_image_layouts などの拡張機能について、なんでそんな拡張が出てきたのか理解していませんでしたがそういう理由だったんですね。
PSO についても Shader Object 拡張が何故出てきたのかという背景などがより深くわかりました。
APIのシンプルさへの感想
薄い API の思考実験としては面白いと感じました。 提案される API は実際に使ってみたくなるくらいよく整理されて薄くて良さそうです。 既存の API の面倒な手続きが必要なものに比べれば使い心地は遥かに良いでしょう。
これまでの Descriptor Set を作ったりする面倒さとかバリアを作ったりする大変さとか、実はもう必要ない抽象化だというのは結構衝撃に思います。
それらを廃止したクリーンな API を作れるということで提案された API は非常にシンプルで魅力的に思えました。 現代は Meshlet 時代であり GPU 駆動レンダリング時代になった今となっては Buffer Device Address なりインデックスでバッファ内を参照する形は普通に使われているわけで、それらを前提にして API を設計するのは理にかなっているように思えました。
この提案されるシンプルな API はぜひ使ってみたいなあと感じた次第です。
ポインタが乱舞するAPIへの感想
一方で気になるのは、やはりポインタが大前提の機能であること。
提案される API は「メモリへの自由なアクセスとポインタをくれ!あとは俺達がうまいことやるから!」というスタンスです。 C++のポインタとかが苦手すぎて Rust のようなメモリ安全な言語を使うようになった私としては、ポインタを使いまくる前提の API を何もこの時代に新しい API として作らなくてもなあという気がします。
現実的な話としては CPU アドレスと GPU アドレスがあったり、Rust のように ownership を使ってうまく整理するというのは難しいでしょうけど。 それでもこの記事の提案する raw pointer ベースの API はかなりデバッガビリティが低いようにみえます。
せめて fat pointer でいろいろな情報を載せておいてデバッグ情報が取れるが最適化モードではバイパスもできる、という形だったり、あるいはポインタそのものではなくハンドルベースになってほしいなあという気がします。
しかし、現状で Vulkan の Buffer Device Address 機能などはポインタの整数値を扱う API になってしまっているので、グラフィクス API の世界ではまだまだポインタベースが主流なのかもしれません。
Metal などはハンドルベースになっているという話を聞いたことがありますが、どんな感じなんですかね?私は Metal は触ったことがなく……。
プログラマが明示せず推論させるAPIへの感想
バッファの Uniform かどうかは推論できるから、バッファのバインディングで明示する必要はないというような主張がなされていた部分があったように思います。
バインディングとして固定化するかどうかは別にして、シェーダーコードのレベルではプログラマが明示できる API のほうが個人的には好ましいかなと思いました。 推論できるから明示する必要はなく、全部大統一するというのは必ずしも良いことばかりとは思えない気がします。 明示してそれに違反している場合はデバッグメッセージが出てくるとかそういう仕組みくらいはあってもよいかなあと思いました。
全部共通のメモリヒープへのポインタアクセスで何でもできるぜ!というのは付随情報をつけにくいので Validation Layer とかが出してくれる情報が貧弱にならないか気になります。
PSO爆発の対策にグラフィクスAPIの作り直しは本当に必要か?
Vulkan の Shader object 拡張などでこのブログの主張する 「PSO に固定で組み込まれている機能を減らして分離して扱えるようにするべき」というのは拡張機能で少しずつ実現できつつあるように思います。
他にもこのブログで提案される機能は Vulkan の拡張機能で追加される API などで段階的に実装できるものが多いように思えます。 現状のグラフィックス API の拡張性の限界にはまだ達してはいないのではないでしょうか。
新しくグラフィックス API を作り直すというのは非常に大変なことに思います。 GPU ベンダーから OS ベンダーは巻き込みますし、既存のゲームエンジンやレンダリングエンジンなども新しい API に対応するという必要があります。 ようやく OpenGL / DirectX 11 世代から Vulkan / DirectX 12 世代に移行できてきたというのに、また一から作り直すというのは非常に大変なことに思えます。
すでに拡張機能とかで既存の API について API を追加したり制限を緩和したりすることで、既存 API の制約を緩和して修復して行けているという事実があります。 例えば Vulkan を例に取ると、Dynamic Rendering では RenderPass という少なくとも DiscreteGPU では失敗作だった存在を是正できた例でしょう。 Dynamic State だったり Shader Object だったりは PSO の固定部分を少しずつ解体していく例のようにも思います。 また、メモリ周りについても Mesh Shader では頂点データの Input Assembly の代わりに自前でバッファにアクセするようになったり、Buffer Device Address でバッファ内の任意の位置にアクセスできるようになったりしています。
現状では段階的に既存 API の制約を緩和していくことができつつあるように思います。 まだ、API を全部作り直さないといけない状況かどうかは怪しい気がしました。
ハードウェアレイトレやWorkGraphなどの機能への言及がない
頂点とフラグメントのラスタライザ系のパイプラインについては、この記事の主張ではいくつかの固定機能を使うにせよ大部分はメモリを自由に使えるようにしてポインタベースで API を構築しても問題ないというお話でした。 私はハードウェアについては何も知らないのでこの記事の主張が正しいかは判断できませんが、この記事が述べている「既存のコア機能に入っていて既に必要なくなった機能」とその理由については説得力があるように見えました。
しかし、この記事には語られていない API として、レイトレのパイプラインや WorkGraph、さらに言えば ML 専用回路やビデオのエンコーダーデコーダーなども入ると思います。 そのようなハードウェア回路と密に結びついているような機能についての言及が欠落しているように思えました。
それらの機能については単純にメモリに配置して自在にポインタベースで実行できる、というモデルで完結させられるほどシンプルなものか個人的には疑問に感じました。 私はハードウェア側の都合について何も知らないので、もしかしたらこの機能も他の API のようにシンプルにできるということなのかもしれませんが、何にしろこの記事で議論されていないのは確かです。
例えば Work Graph を例にとって述べると、スケジューリングとかが必要で実行するデータが Warp の分だけ揃ったら実行可能になるとかの制約があるはずです。 この記事の言う dispatch 単位の flush について書き込みと利用のバリアを貼れば良いというメモリバリアが本当にそれでよいかは疑問に思っています。 Work Graph になると複数の dispatch が絡み合って溶け合って前の dispatch が途中でも新しい dispatch の warp に必要な情報が溜まったら実行する、というようなスケジューリングも可能なはず。 それらをこの提案 API で解決できるのかについての考察が特に書かれていないので気になりました。
提案 API はシンプルですが、それとはべつにハードウェア直結の特権的な機能用の専用 API が別に必要になるのであれば、結局この API がシンプルなのは機能を削っているからであって現行のグラフィクス API と同じだけの機能をもたせると複雑さが戻ってくるのではないかという気がします。 もちろん、既存 API で要らなくなった機能はパージできるのは確かですが、すべての複雑性は排除できず結局複雑な API も残る部分も多くありそうです。
そのようなハードウェアと結合した機能についての言及がなく、グラフィクス API 全体をシンプルにできるかのような示し方は、少し議論が足りていないように感じました。
Work Graph のパラダイムについて
現状、Work Graph はまだまだ出始めたばかりの目新しい技術で、バリバリ現役で使われる技術というわけでもないと思います。
Work Graph という新しいパラダイムの実験が、ドメインシェーダーやハルシェーダーやジオメトリシェーダーと同じ失敗作になるのか、それとも今後のグラフィックスプログラミングはすべてを Work Graph ベースにしていくほどの成功を収めるのかで、今後のグラフィックス API のあるべき方向も大きく変わるでしょう。
Work Graph が成功し、各社ベンダーの実装の足並みが揃い実装について合意が形成されたタイミングで、command buffer + queue の構造から DAG の Graph first な新しいグラフィクス API を考えても良い気もします。 そこまで大きなパラダイムシフトが起こる可能性も将来的にあることがわかっているなら、その Work Graph の結果がわからない今はまだ新しい API を設計する段階ではないような気がしています。
APIをシンプルにすることでデバッグツールを作りやすくなる話
これは確かに基盤 API をシンプルにする大きな利点ではあるとは思いました。
しかしこれも現状の API に内部の PSO とかデスクリプタの状態を問い合わせるような API があればデバッグツールとか RenderDoc とかは作っていけるはずだとも思います。 シンプルな API のほうがデバッグツールが作りやすいのは確かではありますが、デバッグツールを作るためには新しい API に移行しなければならない、というほど現状が何もできない状況というわけではないはずです。
新しい拡張追加 + ラッパーでまだいけるのでは?
API の使い勝手の良さはシンプルな API は大変評価できると思いました。 しかし、この表面的な API の話について言うならば、既存 API に被せるラッパーで十分使い勝手の向上は実現できるように思います。
Translation layers / Min spec hardware の章で述べていることは逆に、既存の API の上にラッパーを作ることもできそうということを示しているように思います。 いくつか既存 API に足りない機能を拡張として追加していけば、あとはラッパーで今回提案されるような API を実現できるのではないでしょうか。
まとめ
まとめとしては、提案される API は使いやすそうなので使ってみたい! しかし、それは既存 API を置き換えずともラッパーレベルで十分なことのように思えました。
このブログ記事は以下の点で有用に思います。
- 既存の要らなくなったコア機能の整理として非常に優れた記事
- 特にラスタ系の API の整理整頓としてよくできている
- RT / WorkGraph については不完全だが……
- より薄い API がどういう風になるかという思考実験としても興味深い
一方で、それらを根拠として主張される基盤グラフィクス API の作り直しについての主張は必ずしも納得できるほどの強い根拠が示されてはいないように思いました。
- 前者は既存 API の拡張機能で段階的に緩和していく
- 後者は既存 API の上にラッパーとして作る
というので十分なように見えており、個人的な感想ですが既存 API の作り直しという様々なベンダーや OS を巻き込む一大工事をやることを正当化するようには思えませんでした。 確かに提案されている API はきれいなんですけどね……。
この記事の提案する最終的なグラフィクス API の作り直しがベンダーや OS を作っている各社に受け入れられるかはわかりませんが(もしかしたらこの記事より前から既にもう Microsoft が DirectX 13 とかを考えているかもしれない)記事としては有用な情報が多く見どころのあるものでした。 既存のコア機能とその不要になった機能のまとめについては非常に勉強になるところがあり、薄い API の思考実験も非常に刺激的でためになるものでした。 非常によく調べられて整理されており、大変興味深い記事だったように思います。