R&D部に所属しつつ、社内のPlaygroundエンジンを開発しているピコアです。 今回のテーマは最新描画APIのVulkanです。 Vulkanはグラフィックス処理APIトレンドの最先端にあるもので、ゲーム開発者が従来よりもGPUのパワーをきめ細かく制御できます。 そのかわりに開発者がさまざまな責任を負うことにもなります。
Vulkanの使い方や細かなAPIの説明などはネット上のあちこちで見つけられるので、今回はあまり触れません。 この記事の目的は、ゲームエンジンの開発者が実際にVulkanを利用した感想、学習中に得られた経験・苦労、そしてVulkanの良いところについての知見の共有です。
まず、グラフィックスAPIの歴史と変遷をおさらいします。 内容は設計思想の流れと時代に応じた変化を理解するための概要に留めます(例えば3DfxのGlideは無視する)。
OpenGL : ご先祖です。1992年1月に公開されました。 当時のGPUの設計は現在とかなり違っていて、決まった固定パイプラインで描画するAPIでした。 制限もありましたが、OpenGLの良いところはそれ以前の仕組みと違って、ハードウェアに依存しないAPIだったことです。 そして関数ポインターを利用して拡張機能を簡単に追加できる仕組みがあります。 そのおかげで、登場から25年近くが経った現在でもOpenGLにはまだ未来があります。 時代と共に技術の進歩へ追従してきた、現代のハードウェアでもまともに動くAPIです。
Windowsの世界ではDirectXが1995年にMicrosoft社から発表されました。
MicrosoftはDOSからWindowsへの移行にあたり、WinGやDirectDrawのAPIを公開したのちに、3D描画のAPIを公開しました。
DirectXの枠組みは音声、コントローラー入力など多くの仕様を含みますが、今回はDirect3Dの話です。
Microsoft社が設計したDirectXの大前提は、COMを使ったOOP(Object Oriented Programming)モデルです。
そして、ハードウェアの進化にあわせて、徐々に新しいバージョンのDirectXをリリースするという方針です。
OS上には以前のDirectXもインストールされており、古いアプリケーションがそのまま動作します。
APIを維持して前のバージョンとの互換性を保つという設計思想ではないことに注意が必要です。
DirectXは1995年~2003年の間に9個ものバージョンがリリースされました(DirectX 1.0 -> 9.0bまで全体的に24 Revisions)。
バージョンごとにAPI設計を大きく変更することで、OpenGLよりも理解しやすく時代にあわせて洗練されたAPI体系を保ってきました。
ドキュメントもOpenGLより詳細に書かれており、またWindows OSがゲーム環境として成長したことで、マーケットをリードするAPIとなりました。
パイプラインとは、頂点の入力からピクセルがバッファに描画されるまでの処理です。
登場当初のDirectXの機能はOpenGLと大差なく、ともに固定パイプラインを採用していました。
固定パイプライン時代は名前の通り、描画機能はすべてハードウェアに実装されたパスで計算され、それ以外の描画オペレーションは選択できませんでした。 このため、光の計算や素材の描画の仕方などはすべて固定されていました。
しかし、それでも進化は激しいものでした。 固定パイプラインといえども、DirectX 7まで変更がありました。そこから、たくさんの変更もありましたが、詳細はリンクを読んで下さい。
対するOpenGLについては、OpenGLのデータパスのイメージ図などを参照してください。
OpenGL対DirectXという枠組みでは政治的な戦いやマーケットでの力関係もあって、性能面やそのあとに出た新ハードウェア機能の対応面などOpenGLが遅れていた部分がありました。
しかし、OpenGLのメリットはWindows環境以外でも動作することであり、生き残る事は出来ました。
DirectX 8.0からシェーダーが追加されました。 ポリゴンの頂点毎に自由な計算(プログラム可能)をおこない、さらに計算結果をピクセル毎に受け取って自由に処理(プログラム可能)できるようになりました。 これらをVertex / Pixel Shaderと呼びます。
OpenGLの世界でも、OpenGL 1.4からシェーダーをExtensionとしてサポートし、2.0から本体サポートに含まれるようになりました(2004年ごろ)。
その後に、Geometry ShaderやTessellation Shaderも追加されました。 一方DirectX 9まで存在した擬似的な固定パイプラインAPIがDirectX 10で削除されました(OpenGLES 1.x -> OpenGLES 2.xでも同じく、固定パイプラインの機能がAPIから消えた)。 つまりここでプログラマの学習ハードルが高くなりました。
これ以降、画面にピクセルを描画するためには自前でVertex ShaderとPixel Shaderを組むことになります。
さらにCPUの負担をおさえるために、ゲームプログラマ側では描画ステートの変更タイミングを変更する取り組みがおこなわれました。 従来は描画実行タイミングでおこなっていた処理を生成/設定タイミングへと移動し、実行時負荷を低減するというものです。
ここで「作成時」と「利用時」を分けて考えた場合、仮に「作成時コスト」が大きくなったとしてもそれはプログラム実行中に一度だけ必要なコストです。 描画に利用するデータを「作成時」に事前計算することで、何度も発生する「利用時コスト」を減らせるならば、積極的にコストを「作成時」へ押し付けて可能な限り「利用時コスト」を0に近づけるのが合理的です。
この考え方とトレンドはのちにVulkanまで続いていきます。
そしてComputeモデル(OpenCLまたはNVIDIAのCUDAに近い)も描画APIに追加されました。
つまり描画専用ではなく、もっと自由な形でGPUに計算させる仕組みが追加されました。
これによって例えば物理計算、音声データ処理なども出来るようになりました。
描画の面でも、ピクセルに直接関係しない描画処理にもGPUを利用できます(例:グローバル・イルミネーションの計算)。
ハードウェア性能を限界まで引き出すという点については、歴史的に専用ゲーム機のほうが先行してきました。 PlayStation / Xbox世代のゲーム機においても、粒度のこまかなAPIが開発者へ公開され、性能向上に活用できました。 しかしPCの世界ではマシンごとにハードウェア構成が異なるため、粒度のこまかなAPIを提供することは難しいとされてきました。
さて、レンダリング機能が年々強化されていったOpenGLとDirectXですが、ともに処理モデル上の大きな弱点を持っていました。 複数のスレッドを利用して(マルチスレッドで)描画したいという開発者のニーズを満たすAPIの不在です。
まず、OpenGLにはマルチスレッドモデルがまったく存在しないに等しいです。 スレッドごとにContextを持つことはできますが、描画命令を実行できるOpenGLコンテキストはアプリケーション毎に1個しかありません。 マルチスレッドを活用しようとしても、2個目以降は非同期のテクスチャアップロードにしか使えないのです。 さらに、コンテキスト間でのテクスチャ共有には同期の面で注意を払う必要があります。
DirectXでは、DirectX 11からマルチスレッド描画がサポートされました。
DirectX 10までの世代とOpenGLをターゲットとするゲームエンジンのなかには、複数のCPUコアを有効利用するために自前の処理レイヤを構築しているものもありました。 具体的には、グラフィックスAPIを擬似的にマルチスレッド化するラッパーを作り、複数スレッドから描画コマンド列を作成する仕組みです。 記録した描画コマンド列はメインスレッドがまとめてGPUへ転送します。
この仕組みはゲームエンジン側のコードに新たなオーバーヘッド(データ構造をパースしてグラフィックAPI関数の呼び出しに必要な描画ステート、テクスチャ、モデル情報を検索するコスト)を生むため、非効率的と感じるかもしれません。 しかしこのコストを払ってもなお、自前でグラフィックAPI呼び出しに相当するコマンドキューを作成してデータ列を詰め込み、レンダリング用のスレッドがそれを取り出して利用するという仕組みには処理効率上のメリットがありました。
さて、先にDirectX 11でマルチスレッド描画がサポートされたと述べました。 このなかでモデルを変更してコマンド列を貯める仕組みが追加されましたが、貯めたバッファのコミットは依然としてメインスレッドの役目でした。 つまり、DirectX 11の方式はゲームエンジン開発者が自前でコマンド列を貯めて発行していた方式と基本的に変わりません。 唯一の違いはこれらの処理をDirectX自体がおこなうことです。 このため、コマンド列を貯めるのと並行してさまざまな準備処理を先行させられます。 たとえば、コマンド間の差分のみを含むセットへと変換したり、GPUが実行しやすい形に変換するなどの処理です。
DirectX 11リリース後、2012年末ごろまでの業界状況は次のようなものでした。
ハードウェアの進化ペースは低下しつつも続いていた(例:Conservative Rasterization)。
DirectXやOpenGLはプログラマーの開発負担を低減するために、さまざまな処理を裏側でおこなっていた。 場合により、ドライバーがいきなり重い作業をおこなったりと、開発者を悩ませることもあった(この負荷を避けるために、DirectXの癖を把握して、わざとある無駄なオペーレションを実行してタイミングをずらすハックもあったと聞きます)。
この頃のMicrosoft社のスタンスは「DirectXは11で最後」というものでした。 もちろん最後ではないはずですが、しばらくの間はもう抜本的な変更を加えた新しいAPIは出さないという意味でした。
いっぽうで、ゲーム開発者は昔から「たとえ記述が面倒だとしても、できるだけ完全なコントロールが欲しい」と要求してきました。 DirectXやOpenGLの提供する水準ではまだまだ足りないと感じ、専用ゲーム機並の自由なアクセスをしたいというものです。 AAAゲームを出しているゲーム会社の人間は最先端の技術を理解し、できるだけ質の高い物を作ろうとするため、当然の事です。 彼らはMicrosoftのスタンスに対して、別なルートを探していました。 もちろんKhronos Groupの中でもこの議論がおそらくあったと思います(携帯端末でのゲーム市場が伸びる中で、バッテリー消費をおさえつつ高品質なグラフィック処理を実現したいという流れもありました)。
そして、Mantleが登場しました。 MantleとはAMD社(GPUメーカー), EA DICE(ゲーム会社)が組んで開発したAPIです。 Mantleの仕様は、他のGPUメーカーがMantle APIをサポートしたいならば、自由にやって良いという条件のもとで公開されました。 そしてEA DICEのFrostbite 3エンジンでの成果がかなり良く、業界に革命ともいえる大きなショックを与えました。
もちろん、いくら優れたAPIであっても標準仕様ではない独自APIが成功する例は少なく、MantleがメインストリームAPIになる可能性は低いものでした。 さらにMantleはPC向けの独自APIでモバイル環境を考慮していなかったため、たとえばRenderPassの概念が存在しませんでした。
しかし、まずは寝ていた巨人が起きました。 Microsoftが1年のずれでDirectX 12を発表したのです。
そして、Khronos GroupがMantleをベースにVulkanの仕様を仕上げました。
さて、この間にApple社がMetalという描画APIを公開しました。 これはVulkanやDirectX 12に近い感じです。 細かくみていませんが、イメージ的にVulkanより扱いやすいと思います。 残念な事にAppleがVulkanをサポートする気配は今のところありません。 Metalの上で実行出来るVulkanのライブラリ(名前はMolten)が存在しますが、どこまで機能が動くのかはわかりません。
重要なのは、VulkanがMantleのコピーではなく、DirectX 12とも違う特徴を持つことです。 VulkanにはMantleにはなかった物も入っています。 VulkanとDirectX 12の概念はとても近いですが、Vulkanははじめからマルチプラットフォームを想定しています。 モバイルも、Windows OSもLinuxもサポートします。 PS4やXbox OneもAMDのチップを使っているので、Vulkan対応が可能だと考えられますが、政治的な意味でVulkan対応の可能性は低そうです。 しかし今後の新しい環境、ゲーム機などがサポートする可能性は十分あると考えられます。
つづいて、Vulkanが従来のAPIとどう違うかを見ていきましょう。
この3点によって、従来のOpenGLでは描画時にかかっていた処理コストをすべて作成時のコストに移動できます。 事前処理によって内部でGPUの一番わかりやすい形(GPUでの利用時にオーバーヘッドが最も少ない形)のデータに変換することも可能です。 さらに描画命令を貯める事で処理差分のみをGPUに対して発行する最適化も可能で、これも処理負担減に貢献します。
つまり、以前にあった描画時にコミットのオーバーヘッドやステートの変更によってドライバーがやらないといけない重い仕事が消えます。 データはすべて事前に計算されているため、最低限の差分適用で前の処理から次の処理へ切り替えることもでき、GPUにとってもやさしい設計です。
描画コマンドも再利用可能なので、さらにコストダウンにつながります。 一回だけの作成コストで複雑な命令を組み合わせ、その後はずっと描画にコミットするだけです。
ここで重要なのは、描画コマンドのバッファを再利用する際に、画面が全く同じ見た目になる必要はないということです。 例えばカメラの角度を変えたり、キャラクターの場所や動きを変える事は簡単に出来ます。 例えば10万個の草の描画命令を簡単に再利用することが可能です。 コマンドバッファが利用するバッファの中身を差し替えることで、同じコマンドバッファを利用して異なる物を描画する事も可能です。
Render Passes 利用するバッファ、書き込み用・読み込み用の依存グラフを定義できます。 これによって画面出力までのバッファの流れが整理され、Vulkanドライバがハードの制御を最適化できます。
Free Memory Management / 自由なメモリ管理 DirectXやOpenGLでは、メモリのアロケーション時にHandleが戻ってきます(テクスチャー, VBOなど、とにかくオブジェクトポインターやIDが戻ってくる)。
Vulkan APIを利用するアプリケーションでも、Vulkanに対してメモリのアロケーションを要求します。 しかし、このときに対象メモリの用途がテクスチャーなのかバッファなのか、その他のユースケースを明示します。 要求したメモリはOpenGLやDirectXと同じく、Handleとして戻ってきます。
端末のプロフィール情報から、どれぐらいのメモリがあるのか、それらの種類は何か、なども知る事が出来ます。 この情報を利用して、メモリのアロケーション時にどの種類のメモリをどれだけ利用するか設定できます。 そして、割り当てられたメモリを利用する時に必ずすべてのAPIにHandleのオフセットを設定します。
これによって、自然な形ですべてのメモリブロックに対する自前のアロケーションの仕組みを作成できます。 メモリのポインタが直接見えないにも関わらず、Handle + Offsetのお陰でポインタを持っているのと同じ事が可能です。 これはすごく頭の良い仕組みだと思いました。 そして、対象がCPUから見えるメモリであれば、Handleからアドレスにマッピング出来ます(GPUへのメモリ転送によく利用される)。
すべてのデータの種類に関する、細かなルールは以下のページにあります。
https://www.khronos.org/registry/vulkan/specs/1.0/xhtml/vkspec.html#fundamentals-threadingbehavior
Enumerate Layers (=API Wrappers) and Extensions. ここでVulkan用にインストールされているレイヤー(APIのラッパーの理解で良い、目的によって、いろいろなレイヤーが存在する)を列挙します。 この段階で拡張関数の取得も可能です。
Create Instance of Vulkan. Vulkanのインスタンスを作成します。
Can list all the queues supported by the physical device. そしてデバイスが対応しているQueue一覧(Queueは命令の配列を実行する物)を取得できます。
Create a physical instance. 物理デバイスのプロファイルをVulkan APIでインスタンス化します。
Create a logical instance from physical device. そして、物理デバイスのインスタンスから、論理デバイスを作成します。
Get Queue and all ressources from logical device. 論理デバイスでリソース、Queueなどを取得します(作業はほぼすべて論理デバイスで行う)。
Job Done / 設定完了
Get Extensions for presentation. 表示用の拡張APIを取得します。
Create Surface Surfaceを作成します。
Get surface support and possible format. Surfaceが対応しているフォーマットや機能を確認します。
Get,select and set presentation mode. 表示の仕方(モデル)を取る、選択し、設定します。
Create Swap Chain スワップチェインを作成します。
Get images from swap chain. スワップチェイン内からのイメージ一覧を取得します。
Create View for each image. イメージ毎にViewを作成します。
One for rendering complete. 描画完了用です。
そして、
Allocate necessary memory and bind both. 必要なメモリを割り当ててこれらを紐付けます。
For each views of all images from the swap chain, create a framebuffer linking the depth buffer view and the image views.
[### エンジンの描画コマンド ###] は自分が描画したい物です。 もちろん、ここで挙げたのは単純なケースで、1つのターゲットバッファしか使わないパターンです。
Not For :
For :
つまり、描画APIの勉強以外、ほとんどOpenGL系でも十分です。 しかも、OpenGL AZDO Extensionを利用すれば、Vulkanとほぼ同じ事を楽にできます。
すなわち、Vulkanは仕事上で専門に描画APIを叩く方に向いている仕組みです。 個人的には、勉強・趣味とゲームエンジン/3D CAD類の開発者以外はあまり調べなくても困らないのではないかと思います。
今回の記事を書くにあたって、Vulkanのすべての機能を使ったわけではありません。 また仕様を細かいところまで完全にマスターして、理解したわけではありません。 あくまでも、使った分の個人的な感想です。
このフラグの組み合わせで本当にいいのか、という確証を得られない場合がありました。 このため、あるフラグを設定しない場合はどうなるのかなどの疑問を持ちながら開発を進めました。 特にメモリのタイプ・転送について、またViewの切り替えについて、不明確な部分が多かったです。 例えばRender Targetのreadとwriteの状態を切り替える概念を理解しても、実装・設定に関して、不安なところがありました。
APIの説明はいいのですが、ユースケースやパターンに関する説明が不足しています。 APIの説明を読んでも、見えていないところもあります。
新しいAPIであるため一定は仕方ないことですが、これによってDirectXやOpenGLでの経験がないプログラマがVulkanを理解するのが難しくなっていると思います。 また、DirectXやOpenGLのAPIをたくさん叩いた経験があったとしても、内部で起きている事・GPUの設計を理解していなければ理解しにくい部分があります。 暗黙的なノウハウがかなり多いと感じています。
Vulkanを利用した開発をおこなう際にヒントとなりそうなものをいくつか紹介します。
C++ラッパーはオーバーヘッドがなく、しょうもないミスを防ぎやすく、メンテナンスしやすいVulkanコードを組めます。 使わない理由はないと思います。
型安全なEnum指定によって定数指定ミスを防げますし、IDEで自動的に関数やEnumを補完できるため、開発がかなり楽になります。 今回は勉強のために、作業途中でC++版の自前ラッパーを作ったのですが、本記事の執筆時点でNVIDIAがC++ヘッダの自動生成ツールを提供しています。 Vulkanコードを真剣に組むなら、使うべきだと思います。
なお、NVIDIAのC++ヘッダ生成ツールはKhronos Groupからオフィシャルに認められて、2016年7月末からKhronos Groupによってメンテナンスされています。
大変役に立ちます。 もちろん、もうちょっとわかりやすいエラーを出せばいいのにと感じることもあります。 それでも無言でcrashするより、大変素晴らしいです。 そして、エラーではないが推奨されない指定についてWarningを出してくれるケースもありました。 これによって、コピペによるフラグの設定ミスなどを修正出来ました。
Vulkan Validation Layersの使い方を説明したサンプルコードはこちらです。 http://gpuopen.com/using-the-vulkan-validation-layers/
当社で開発している2Dゲームエンジン"Playground"でVulkanを実験的にサポートするために行った対応を紹介します。 エンジン固有の事情も含まれるため、前提を補足しつつ説明します。
Playgroundではもともと描画用の頂点データを実行時にCPU側で計算しているため、GPU側のバッファを一切使っていません。 OpenGLのドライバーと同じ働きをし、命令コマンドを貯めながら、転送する頂点のデータも貯める構造です。 そして、描画命令を実行する前に先にメモリの転送を行います。
エンジンの実行時にOpenGLのステート切り替えをしていたため、エンジンのRefactoringはおこなわずに同じ動作を再現しました。 フラグとステートの差分を管理し、使うPipelineを切り替えるようにしました。 (Supported states : Viewport, scissor, depth read/write, depth function.)
エンジンではシェーダーやターゲットバッファの切り替えをサポートしていますが、今回は最小限の作業で可能な範囲の対応をおこなったため、シェーダーの切り替えやターゲットの切り替え部分は未実装です。
Vulkanでは二つの方法が存在すると認識しているのですが、試したのは片方だけです。 OpenGLやDirectXのようなダイナミックな切り替えはVulkanの場合、PipelineにつなぐDescriptor Set Layoutをテクスチャー毎に作成(OpenGL系でいうと、Uniform値の塊を設定するオブジェクト)することで実現します。 もう一つの方法は、複数のDescriptor Set Layoutを作成せずに、すべてのテクスチャーを配列化して、indexでアクセスする方法です(試していないし、実装方法を調べていません)。
今回対応したものはこんな感じです。
とにかくVulkanを学んでみたいという方のために、Vulkan SDKとネット上に存在するVulkan関連のサンプルコードを追いかけて学習するためのおすすめフローを簡単に紹介します。
まずはKhronos Groupのホームページから始めましょう。 SDKへのリンク、仕様書、ドライバーなど、すべてそろっています。 https://www.khronos.org/vulkan/
個人的にNVIDIAのカードを持っているため、NVIDIAのドライバーをインストールし、LunarGのSDKもインストールしました。 LunarGのSDKをインストールすると、Debug/Validationレイヤーが追加されるので役に立つと思います。
今回の記事で扱ったのは感想や全体的な話がメインでした。 サンプルコードをもとにして具体的にVulkanを試したい場合は、次の順でみていくと良いでしょう。
Sascha Willemsのサンプルが一番機能が揃っていると同時にやる事が多く、フレームワーク化されています。 このため、生のAPIコールを連続的に綺麗に見ていきたい場合は、少々苦労するかもしれません(LunarGのAPI Dumpレイヤーを使ってよいかもしれませんが)。
モダンなAPI(Vulkan, DirectX 12, Metal)はGPUの内部構造にフィットしやすい・計算モデルを近づけているため、今後のハードウェアの進化にあたって、APIも進化すると個人的に思います。 今までハードルだった古いAPIの体系が消えたことで、今後のAPI拡張はハードの性能を十分に引き出しながら進められると思います。 例えば、Raytracingの拡張、Conservative Rasterizationの拡張、などなど、すでに進化は始まっています。 例: http://gpuopen.com/gcn-shader-extensions-for-direct3d-and-vulkan/
Vulkanの調査とPlaygroundエンジンへの組み込み作業をおこなったのは2016年6月でした。 そしてこの記事を書いたのは7月でしたが、記事公開までの間の7月にSIGGRAPH 2016が開催されました。 このなかでVulkanに関する良い資料が公開されていたので紹介します。
https://www.khronos.org/assets/uploads/developers/library/2016-siggraph/3D-BOF-SIGGRAPH_Jul16.pdf
Vulkanは低レベルAPIであると言われることが多いですが、個人的にはそうは思いません。 たしかに気をつけるべき事、やる事はかなり増えますが、今までの描画APIの流れで考えると自然なものが多いです。
つまり、Vulkanは完全に新しい概念というわけではなく、これまでの歴史の流れと追加されてきた概念の続きを担うAPIです。 具体的には、可能な限り事前処理できるものを増やす、データの再利用生を高める、描画時の負担を減らす、メモリアクセスをより柔軟にコントロールできるようにする、というトレンドに沿うものです。 これらを実現しやすくなるかわりに、GPUや描画パイプラインの細かいところまですべて理解する必要があります(覚える事は多いですが、昔からやっている描画エンジニアなら特に問題ありません)。
現時点のVulkanが抱える問題は、まだ丁寧なドキュメントがないことです。 ある事を実現したい時に複数の方法を考えつくことが多いのですが、何が「正しい」かという答えが出ない場合もあります(そもそも答えは存在しないかもしれません)。 つまり、ユースケースごとのわかりやすい例がありません。 おそらく何年間もゲーム機やPCでのグラフィック描画で苦労した経験者は悩む事が少ないでしょう。 私は描画APIをバリバリ触ってゲーム機向けの開発をおこなったことがないため、正当な評価をするための能力や経験が乏しい部分もありますが、より暗黙的なノウハウに依存しないマニュアル作りとさまざまな全体図/例の提示がほしいと感じました。
まずは、今後のGraham Sellers(AMD)のVulkan本に期待したいと思います。 https://www.amazon.com/Vulkan-Programming-Guide-Official-Learning/dp/0134464540
KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。
合わせて読みたい
KLabのゲーム開発・運用で培われた技術や挑戦とそのノウハウを発信します。