Vulkan1.2の概要

fadis氏:2020年の頭にVulkan1.2が発表されました。Vulkan1.2では1要素当たり8bitでバッファに詰め込まれた整数の配列を、シェーダから読めるようになります。

なんで今さらそんな短い整数のサポートを追加するのかというと、ニューラルネットワークは個々の重みの精度より個数のほうが性能に効くことが知られていて、GPUでニューラルネットワークの評価をする時に、floatを1個置くメモリがあったら8bit整数を4個置きたいという事情があるからです。

Vulkan1.2で追加されたBuffer device addressを使うと、GPUのメモリ上に置かれたバッファのGPU内での先頭アドレスを取得できます。この機能の用途の1つはデバッグ情報にアドレスを載せることですが、もっと遊べるのはこのアドレスをバッファのデータに書いた場合です。

この場合、GLSLのbuffer_reference拡張を使って指定されたアドレスのデータをdereferenceすることができます。つまり、GPU上でlinked listのような構造を作れるようになります。

シェーダが複雑になって使うリソースが増えてくると、このようなつらいbindingの列挙が生じます。Vulkan1.2で追加されたDescriptor Indexingは、ディスクリプタの配列を作れるようにするものです。

それに合わせてディスクリプタセットの扱い方の制限が緩和されます。まずディスクリプタセットのディスクリプタのうち、シェーダが触らないものについてはなにも結びつけない状態でシェーダを実行してもよくなります。さらにコマンドバッファの記録中であっても、今GPUが触っていないディスクリプタは更新してもよくなります。

この3つの変更によってとりあえず巨大なディスクリプタセットを作り、シェーダ側は配列でそれを受け取り、必要に応じて空いているディスクリプタにリソースを割り当てて使うという運用が可能になります。

グラフィックスパイプラインの出力先としてフレームバッファが必要で、フレームバッファは作成時にイメージビューを指定しなければなりませんでした。これはフレームバッファより先にイメージを作っておく必要があるということです。

Vulkan1.2では、フレームバッファ作成時に、「イメージは後で用意する」という指定ができるようになりました。これは「Imageless framebuffer」と呼ばれます。

Imageless framebufferを使ってレンダリングを行う場合は、レンダーバスをキューに投げる段階でイメージビューを割り当てます。

Vulkanでは、深度とステンシルは同じイメージの異なるチャネルに記録されます。これは一般的な深度が16から24bit、ステンシルは8bitで十分で、両方くっつけると扱いやすいからです。しかし、深度値しか必要なくても深度とステンシルが入ったイメージについてバリアを置くことになるので、実際には依存がないデータへの依存関係を生じさせます。

Vulkan1.2ではこの問題に対処するために、深度とステンシルのイメージのうち、どちらか一方にしか用がないことを明示できるSeparate Depth Stencil Layoutsが追加されました。さらに、シェーダの中で使えるAtomic演算も加わりました。GPUがサポートをする場合、64bit整数に対するAtomicCasやAtomicAddといった定番のAtomic演算を使うことができます。

Vulkan1.1では16bit整数や浮動小数点数をバッファから読み書きできるようになりましたが、計算はあくまで32bitで行っていました。しかし、Vulkan1.2ではGPUがサポートしている場合、計算を16bitで行うよう指示できるようになりました。8bit整数の場合も同様です。

8bit整数として計算することを要求された場合、例えば8bitベクタ型の4個の値を1度に計算するような命令を使って性能が向上するか、というのはGPU次第になります。

異なるキューに送られているコマンドバッファ同士で同期を取るにはセマフォを使いますが、1箇所の同期に1個のセマフォが必要なので、同期を要する箇所が大量にある場合、セマフォの管理が面倒でした。

Vulkan1.2で追加されたTimeline Semaphoreは、1bitの完了通知ではなく整数値を持っています。コマンドバッファ完了時にはセマフォの値を加算し、セマフォの値が特定の値を上回った時にコマンドバッファの実行を開始できます。こんな感じで、従来のセマフォでは組めなかった複雑な条件でコマンドの実行を開始させられます。

Vulkanの標準に入っていないベンダー拡張

Vulkanには標準に入っていないさまざまなベンダー拡張があります。ビデオキューは、GPUのハードウェア動画のエンコーダ・デコーダを使って動画をエンコード・デコードするコマンドに対応したキューを追加する拡張です。

動画の1つのストリームをVkBufferに入れてデコード要求をキューに流すと、指定したイメージの列にデコードした動画のフレームが書き込まれます。でき上がったイメージは、GPUのメモリに転がっている状態になるので、ここからイメージレイアウトを変換してレンダリング素材として使うのも簡単です。

インタラクティブな3Dグラフィックスは、間接照明を無視します。間接照明は、光源からの光が直接物体に届くのではなく、他の面に1回以上反射して物体に届く状況のことです。

なぜ間接照明を無視するのかというと、高速に計算する方法がないからです。点Pの位置に届く間接照明を計算しようとすると、点Pからある方向へ伸びる線分Vが、点Qの位置で他の面とぶつかることを発見しなければなりません。

3Dグラフィックスの物体の形状は頂点配列で与えられます。これはたくさんの三角形の頂点の座標が順番に列挙されたものです。ある線分Vがこれらの三角形のどれと交差するかを知るには、すべての三角形を順番に調べるしかありません。これは「リアルタイムでやって」と言われても無理な話です。

頂点配列をより効率よく交差判定できる木構造に事前に変換することで、高速に線分Vとぶつかる面を見つけられます。しかし、この変換には時間がかかるため、3Dのキャラクターのような変形する物体の木構造を毎フレーム生成し直すことはできません。

最近のNVIDIAのGPUなどには、この問題を乗り越えるために頂点配列から木構造を爆速で作り、そのまま線分との交差判定をする専用のハードウェアが載っています。

GPUが生成する木構造のことを、Vulkanでは「Acceleration Structure」と呼びます。この木構造の詳細なフォーマットは、GPUが最適化をがんばりたいところなので、VkImageと同じように隠蔽されます。GPUのメモリを確保してVkAcceleration_structureにバインドすることで、そのメモリを、木構造を保持するための領域として使えるようになります。

VkCmdBuildAccelerationStructuresをキューに流すことで、指定したAcceleration Structureに対して、指定したアドレスにある頂点配列の内容が書き込まれます。

三角形の面ではなくAxis Aligned Bounding Boxを木構造に入れることもできます。これは木構造をさらに階層的にする時によく用いられます。

Acceleration Structureの用意ができたら、rayQueryを使って指定した線分と交差する三角形やAxis Aligned Bounding Boxがないかを調べます。

交差判定を1回やって終わりならこれだけでもよいのですが、真面目にレイトレーシングを行う場合、交差した位置からさらにrayQueryを行って次にぶつかる物体を探します。 物体の表面が完全な鏡面でない限り光はさまざまな方向に散らばっていくため、物体の表面に1度ぶつかるたびに探索する方向が増えていきます。つまり、データの並列度が徐々に上がっていきます。

このような特徴を持つ処理を既存のパイプラインの中でうまく扱うのは無理そうだったので、新しいパイプライン、ray_tracing_pipelineが生えました。

レンダリング結果を画面に出すには、コンポジタからサーフェスをもらいますが、コンポジタを経由して描画するオーバーヘッドが我慢できないということがよくあります。

Windowsでは全画面表示でVulkanを使う場合、一時的にアプリケーションにディスプレイへの出力を直接触らせるVK_EXT_full_screen_exclusiveを使うことで、このオーバーヘッドを回避できます。

一方Linuxの場合、XサーバーやWayland Compositorが動いていない状態であれば、VK_KHR_disprayを使うとVulkanアプリケーションが直接ディスプレイを操作できます。 この拡張はLinuxのKernel Mode Settingに対する薄いラッパーになっていて、解像度やリフレッシュレート、ピクセルフォーマットを選んで直接描画するためのスワップチェーンを作れます。

VK_EXT_fragment_density_mapを使ってGPUの負荷を抑える

レンダリングを行っていると、メッシュの境界部分以外の場所では近傍のピクセルがほとんど同じ色になるということがよく起こります。

事前に境界がどこにくるかわかっている場合、境界部分だけ細かくレンダリングして、それ以外の部分は広い範囲で1回だけフラグメントシェーダーを実行することで、GPUの負荷を抑えたいところです。

VK_EXT_fragment_density_mapは、どの部分にどの程度のフラグメントシェーダーの密度が必要かを表すイメージをアタッチできるようにします。

この拡張にはもう1つ役割があります。人間の視覚は中心視野では細かな模様を捉えることができますが、周辺視野ではおおざっぱな色しか見ていないことが知られています。したがって、視線追跡機能付きのVRヘッドセットでは、視線が向いている位置の周囲だけを細かくレンダリングし、それ以外の部分はおおざっぱにすることで、GPUの負荷を劇的に下げることができます。

MSAAやSupersamplingは、アンチエイリアスのために1ピクセルに対して複数のフラグメントシェーダーの実行結果を持ちます。これは滑らかな境界線を描くには大変効果的ですが、境界部分以外ではただのメモリと待機の無駄遣いです。

VK_KHR_fragment_shading_rateでは、Fragment Density Mapと同じように、どのピクセルにどの程度のフラグメントシェーダーの実行が必要かをイメージで指示できるようにします。

Transform_feedbackは、グラフィックスパイプラインの処理をジオメトリシェーダーまでで切り上げ、ラスタライズせずにジオメトリシェーダーが入った頂点の列をバッファに書きます。

これはフルセットのOpenGLには標準で備わっていて、10年以上前から活用されていた機能です。しかし、OpenGL ESの後継でもあるVulkanでは標準に取り込むわけにはいかず、拡張に転がっています。

レンダーパスはモバイルGPUの性能を引き出すのに欠かせない仕組みですが、モバイルでないGPUではあまり必要のない機能なので、アプリケーションによってはレンダーパスはまったく活用されず、パイプラインが1つだけのレンダーパスが大量に作られます。

このような状況では無意味なレンダーパスを大量に作らないといけないのが面倒になります。そこでDynamic Renderingでは、グラフィックスパイプラインをレンダーパスに結び付けなくてもよくします。

レンダーパスに結びついていないパイプラインは、BeginRenderingで即席のレンダーパスを作るとそのままコマンドバッファに積むことができます。

最後に宣伝ですが、Vulkanには今日紹介しきれなかった機能や拡張がまだまだあります。それらも含む最新のVulkanの話を盛り込んだ『3DグラフィクスAPI Vulkanを出来るだけやさしく解説する本 Version 3.0』を次の技術書典でリリースする予定です。お楽しみに。

まとめに入りましょう。GPUはたくさんのプロセッサーが載った計算機です。Vulkanを使えばGPUの一通りの操作ができます。VulkanはGPUの進化に合わせて今も活発に改良が続けられているAPIです。ご清聴ありがとうございました。