Introduction - はじめに

R&D部に所属しつつ、社内のPlaygroundエンジンを開発しているピコアです。 今回のテーマは最新描画APIのVulkanです。 Vulkanはグラフィックス処理APIトレンドの最先端にあるもので、ゲーム開発者が従来よりもGPUのパワーをきめ細かく制御できます。 そのかわりに開発者がさまざまな責任を負うことにもなります。

Vulkanの使い方や細かなAPIの説明などはネット上のあちこちで見つけられるので、今回はあまり触れません。 この記事の目的は、ゲームエンジンの開発者が実際にVulkanを利用した感想、学習中に得られた経験・苦労、そしてVulkanの良いところについての知見の共有です。

History of 3D Graphic APIs - 描画APIの歴史

まず、グラフィックス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となりました。

Evolution of APIs over time

1. 固定パイプライン時代

パイプラインとは、頂点の入力からピクセルがバッファに描画されるまでの処理です。
登場当初のDirectXの機能はOpenGLと大差なく、ともに固定パイプラインを採用していました。

固定パイプライン時代は名前の通り、描画機能はすべてハードウェアに実装されたパスで計算され、それ以外の描画オペレーションは選択できませんでした。 このため、光の計算や素材の描画の仕方などはすべて固定されていました。

しかし、それでも進化は激しいものでした。 固定パイプラインといえども、DirectX 7まで変更がありました。そこから、たくさんの変更もありましたが、詳細はリンクを読んで下さい。

対するOpenGLについては、OpenGLのデータパスのイメージ図などを参照してください。

OpenGL対DirectXという枠組みでは政治的な戦いやマーケットでの力関係もあって、性能面やそのあとに出た新ハードウェア機能の対応面などOpenGLが遅れていた部分がありました。 しかし、OpenGLのメリットはWindows環境以外でも動作することであり、生き残る事は出来ました。

2. Shader誕生

DirectX 8.0からシェーダーが追加されました。 ポリゴンの頂点毎に自由な計算(プログラム可能)をおこない、さらに計算結果をピクセル毎に受け取って自由に処理(プログラム可能)できるようになりました。 これらをVertex / Pixel Shaderと呼びます。

OpenGLの世界でも、OpenGL 1.4からシェーダーをExtensionとしてサポートし、2.0から本体サポートに含まれるようになりました(2004年ごろ)。

3. パイプライン内にプログラム可能な部分を追加

その後に、Geometry ShaderやTessellation Shaderも追加されました。 一方DirectX 9まで存在した擬似的な固定パイプラインAPIがDirectX 10で削除されました(OpenGLES 1.x -> OpenGLES 2.xでも同じく、固定パイプラインの機能がAPIから消えた)。 つまりここでプログラマの学習ハードルが高くなりました。

これ以降、画面にピクセルを描画するためには自前でVertex ShaderとPixel Shaderを組むことになります。

4. ステートを先に持っていく、先に情報をフォーマットし、使いまわしの効率をあげる

さらにCPUの負担をおさえるために、ゲームプログラマ側では描画ステートの変更タイミングを変更する取り組みがおこなわれました。 従来は描画実行タイミングでおこなっていた処理を生成/設定タイミングへと移動し、実行時負荷を低減するというものです。

ここで「作成時」と「利用時」を分けて考えた場合、仮に「作成時コスト」が大きくなったとしてもそれはプログラム実行中に一度だけ必要なコストです。 描画に利用するデータを「作成時」に事前計算することで、何度も発生する「利用時コスト」を減らせるならば、積極的にコストを「作成時」へ押し付けて可能な限り「利用時コスト」を0に近づけるのが合理的です。

この考え方とトレンドはのちにVulkanまで続いていきます。

5. より自由度の高いGPGPU(General-purpose computing on GPU)のモデルも追加

そしてComputeモデル(OpenCLまたはNVIDIAのCUDAに近い)も描画APIに追加されました。 つまり描画専用ではなく、もっと自由な形でGPUに計算させる仕組みが追加されました。
これによって例えば物理計算、音声データ処理なども出来るようになりました。 描画の面でも、ピクセルに直接関係しない描画処理にもGPUを利用できます(例:グローバル・イルミネーションの計算)。

ハードウェア性能を限界まで引き出すという点については、歴史的に専用ゲーム機のほうが先行してきました。 PlayStation / Xbox世代のゲーム機においても、粒度のこまかなAPIが開発者へ公開され、性能向上に活用できました。 しかしPCの世界ではマシンごとにハードウェア構成が異なるため、粒度のこまかなAPIを提供することは難しいとされてきました。

6. OpenGLとDirectXに残された大きな弱点: マルチスレッド動作

さて、レンダリング機能が年々強化されていった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が実行しやすい形に変換するなどの処理です。

7. グラフィックスAPIパラダイムの転換前夜

DirectX 11リリース後、2012年末ごろまでの業界状況は次のようなものでした。

  • ハードウェアの進化ペースは低下しつつも続いていた(例:Conservative Rasterization)。

  • DirectXやOpenGLはプログラマーの開発負担を低減するために、さまざまな処理を裏側でおこなっていた。 場合により、ドライバーがいきなり重い作業をおこなったりと、開発者を悩ませることもあった(この負荷を避けるために、DirectXの癖を把握して、わざとある無駄なオペーレションを実行してタイミングをずらすハックもあったと聞きます)。

この頃のMicrosoft社のスタンスは「DirectXは11で最後」というものでした。 もちろん最後ではないはずですが、しばらくの間はもう抜本的な変更を加えた新しいAPIは出さないという意味でした。

いっぽうで、ゲーム開発者は昔から「たとえ記述が面倒だとしても、できるだけ完全なコントロールが欲しい」と要求してきました。 DirectXやOpenGLの提供する水準ではまだまだ足りないと感じ、専用ゲーム機並の自由なアクセスをしたいというものです。 AAAゲームを出しているゲーム会社の人間は最先端の技術を理解し、できるだけ質の高い物を作ろうとするため、当然の事です。 彼らはMicrosoftのスタンスに対して、別なルートを探していました。 もちろんKhronos Groupの中でもこの議論がおそらくあったと思います(携帯端末でのゲーム市場が伸びる中で、バッテリー消費をおさえつつ高品質なグラフィック処理を実現したいという流れもありました)。

8. Mantleの登場と各社の追従、Vulkanへ

そして、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とどう違うかを見ていきましょう。

Vulkan addition in more detail - Vulkanが追加する機能と概念

Everything in advance / ほぼすべての状態、設定を先にオブジェクトへビルドする

  • 1/ 全体の描画パイプラインをイメージするオブジェクトを先に作成可能(Input Streamの設定・Shader・Blend・Depth Buffer・Stencil・View Portのdefault値の設定など)
  • 2/ パイプラインにつながるテクスチャーやテクスチャーサンプリング方法、Uniform値を指定
  • 3/ 描画コマンドバッファで描画命令を貯められる。これらは再利用も可能で、どのスレッドからでも作成可能。GPUに送る事も出来る(出来上がったオブジェクトはある状態を表現するため、Immutable型で変更出来ません)。

この3点によって、従来のOpenGLでは描画時にかかっていた処理コストをすべて作成時のコストに移動できます。 事前処理によって内部でGPUの一番わかりやすい形(GPUでの利用時にオーバーヘッドが最も少ない形)のデータに変換することも可能です。 さらに描画命令を貯める事で処理差分のみをGPUに対して発行する最適化も可能で、これも処理負担減に貢献します。

つまり、以前にあった描画時にコミットのオーバーヘッドやステートの変更によってドライバーがやらないといけない重い仕事が消えます。 データはすべて事前に計算されているため、最低限の差分適用で前の処理から次の処理へ切り替えることもでき、GPUにとってもやさしい設計です。

描画コマンドも再利用可能なので、さらにコストダウンにつながります。 一回だけの作成コストで複雑な命令を組み合わせ、その後はずっと描画にコミットするだけです。

ここで重要なのは、描画コマンドのバッファを再利用する際に、画面が全く同じ見た目になる必要はないということです。 例えばカメラの角度を変えたり、キャラクターの場所や動きを変える事は簡単に出来ます。 例えば10万個の草の描画命令を簡単に再利用することが可能です。 コマンドバッファが利用するバッファの中身を差し替えることで、同じコマンドバッファを利用して異なる物を描画する事も可能です。

Reusability in Vulkan

  • Render Passes 利用するバッファ、書き込み用・読み込み用の依存グラフを定義できます。 これによって画面出力までのバッファの流れが整理され、Vulkanドライバがハードの制御を最適化できます。

  • Free Memory Management / 自由なメモリ管理 DirectXやOpenGLでは、メモリのアロケーション時にHandleが戻ってきます(テクスチャー, VBOなど、とにかくオブジェクトポインターやIDが戻ってくる)。

Vulkan APIを利用するアプリケーションでも、Vulkanに対してメモリのアロケーションを要求します。 しかし、このときに対象メモリの用途がテクスチャーなのかバッファなのか、その他のユースケースを明示します。 要求したメモリはOpenGLやDirectXと同じく、Handleとして戻ってきます。

端末のプロフィール情報から、どれぐらいのメモリがあるのか、それらの種類は何か、なども知る事が出来ます。 この情報を利用して、メモリのアロケーション時にどの種類のメモリをどれだけ利用するか設定できます。 そして、割り当てられたメモリを利用する時に必ずすべてのAPIにHandleのオフセットを設定します。

これによって、自然な形ですべてのメモリブロックに対する自前のアロケーションの仕組みを作成できます。 メモリのポインタが直接見えないにも関わらず、Handle + Offsetのお陰でポインタを持っているのと同じ事が可能です。 これはすごく頭の良い仕組みだと思いました。 そして、対象がCPUから見えるメモリであれば、Handleからアドレスにマッピング出来ます(GPUへのメモリ転送によく利用される)。

  • Threading Modelの特徴と制約 基本的にスレッド毎にコマンドバッファで描画コマンドを貯めるのは変わりません。 かわるのはどのスレッドでもコミットが可能という点です。 ただし、同時に2つ以上のスレッドが同じQueueにアクセスしてはなりません。 同時アクセスを防ぐための制御はユーザの責任でおこないます。

すべてのデータの種類に関する、細かなルールは以下のページにあります。

https://www.khronos.org/registry/vulkan/specs/1.0/xhtml/vkspec.html#fundamentals-threadingbehavior

Vulkan Basic - Vulkanの基本

First Setup

初期設定

  • Enumerate Layers (=API Wrappers) and Extensions. ここでVulkan用にインストールされているレイヤー(APIのラッパーの理解で良い、目的によって、いろいろなレイヤーが存在する)を列挙します。 この段階で拡張関数の取得も可能です。

  • Create Instance of Vulkan. Vulkanのインスタンスを作成します。

  • List all physical GPU devices supporting Vulkan. 実際にコンピュター内にVulkan対応の物理デバイス(つまりグラフィックカード)のリストを取得します。
    • Can list / access all the features / properties / memory properties / extensions of device. デバイスごとにサポートされている機能、機能の制限、メモリの種類と制限、拡張の情報を取得できます。
    • 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 / 設定完了


Swap Chain Setup

Swap Chain / 描画用のスワップチェイン

  • 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 command buffer for post/pre presentation for each images イメージ毎に描画前・後の表示用のCommand Bufferを作成します。
    • Related Image Memory Barrier メモリバリアを作成します。
    • Create View for each image. イメージ毎にViewを作成します。

  • Use 2 semaphores : 2つのセマフォを作成します。
    • One for presentation complete. 表示完了用です。
    • One for rendering complete. 描画完了用です。

そして、

  • Get format properties supported, create Depth/Stencil Buffer,Image and view. 対応できるフォーマットを取得し、Depth/Stencilバッファ、イメージ、ビューを作成します。
    • 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.


Rendering / 描画

  • Pre Rendering / 描画前
    • Get Next Image ID. (=Current) / 次の描画バッファのIDを取得します。
    • Post Present Buffer / 描画後コマンドバッファ
      • Begin Command/コマンドの開始
      • Push Image Memory Barrier with Current Image / 現在のイメージオブジェクトにメモリバリアを設定
      • End Command / コマンドの完了
      • Send to Queue / Queueに転送
    • Wait for Queue to complete. (Queue idle wait) / Queueの完了待ち
  • Rendering itself / 描画本体
    • Submit current frame command queues [### エンジンの描画コマンド ###] with following condition / 現在のイメージオブジェクトに合っているコマンドバッファに[### エンジンの描画コマンド ###]を次の条件付きでコミット
      • Wait for "present complete semaphore" / 表示完了セマフォ待ちを指定
      • Set for "render complete semaphore" / 描画完了セマフォ待ちを指定
      • Send to Queue.
  • Post Rendering / 描画後
    • Pre Present Buffer
      • Begin Command / コマンドの開始
      • Push Image Memory Barrier with Current Image / 現在のイメージオブジェクトにメモリバリアを設定
      • End Command / コマンドの完了
      • Send to Queue / Queueに転送
    • Swap Chain Queue Present API with Current Image with following condition :
      • Wait "render complete semaphore" / 描画完了セマフォ待ち
    • Wait for Queue Idle. / Queueの完了待ち

[### エンジンの描画コマンド ###] は自分が描画したい物です。 もちろん、ここで挙げたのは単純なケースで、1つのターゲットバッファしか使わないパターンです。

What Vulkan is for - Vulkanは何のため

Not For :

  • 単純なテスト・単純な物で描画したい場合。基本的にVulkanは通常のアプリケーション本体に書く物ではありません。描画フレームワーク内から呼び出す物です。
  • CPUが描画以外の処理に忙しい場合。Vulkanの性質を活かすためには、描画用の前処理にCPU時間を割く必要があるためです。
  • GPUの細かな事(内部構造)を知らない方
  • OpenGL(ES) / DirectXの仕様を読んで悩んでいる方。
  • Single Thread描画。これはちょっと微妙な意見です。実際Single Threadの場合でもVulkanをうまく使うことで効果は出せます。しかし、性能を最も引き出せるのは複数のスレッドで描画した場合だと思います。

For :

  • CPUバウンド描画。今までOpenGLやDirectXを使って複数のコアで作業していても、描画による処理でCPUが足りなかったパターン。
  • たくさんのdraw callが必要なアプリケーション。
  • 安定した描画時間が必要な場合。ドライバの勝手な裏作業をなくすことで、描画にかかる時間を見積もりやすくなります。

つまり、描画APIの勉強以外、ほとんどOpenGL系でも十分です。 しかも、OpenGL AZDO Extensionを利用すれば、Vulkanとほぼ同じ事を楽にできます。

すなわち、Vulkanは仕事上で専門に描画APIを叩く方に向いている仕組みです。 個人的には、勉強・趣味とゲームエンジン/3D CAD類の開発者以外はあまり調べなくても困らないのではないかと思います。

What I found difficult - 難しいと思ったところ

今回の記事を書くにあたって、Vulkanのすべての機能を使ったわけではありません。 また仕様を細かいところまで完全にマスターして、理解したわけではありません。 あくまでも、使った分の個人的な感想です。

  • ユースケースごとの説明が足りない

このフラグの組み合わせで本当にいいのか、という確証を得られない場合がありました。 このため、あるフラグを設定しない場合はどうなるのかなどの疑問を持ちながら開発を進めました。 特にメモリのタイプ・転送について、またViewの切り替えについて、不明確な部分が多かったです。 例えばRender Targetのreadとwriteの状態を切り替える概念を理解しても、実装・設定に関して、不安なところがありました。

  • 同じくRender passについて、説明が物足りない

APIの説明はいいのですが、ユースケースやパターンに関する説明が不足しています。 APIの説明を読んでも、見えていないところもあります。

  • API仕様書ではなく、概念やユースケースを説明する本が存在しない

新しいAPIであるため一定は仕方ないことですが、これによってDirectXやOpenGLでの経験がないプログラマがVulkanを理解するのが難しくなっていると思います。 また、DirectXやOpenGLのAPIをたくさん叩いた経験があったとしても、内部で起きている事・GPUの設計を理解していなければ理解しにくい部分があります。 暗黙的なノウハウがかなり多いと感じています。

Hints - ヒント

Vulkanを利用した開発をおこなう際にヒントとなりそうなものをいくつか紹介します。

C++ラッパーはオーバーヘッドがなく、しょうもないミスを防ぎやすく、メンテナンスしやすいVulkanコードを組めます。 使わない理由はないと思います。

型安全なEnum指定によって定数指定ミスを防げますし、IDEで自動的に関数やEnumを補完できるため、開発がかなり楽になります。 今回は勉強のために、作業途中でC++版の自前ラッパーを作ったのですが、本記事の執筆時点でNVIDIAがC++ヘッダの自動生成ツールを提供しています。 Vulkanコードを真剣に組むなら、使うべきだと思います。

なお、NVIDIAのC++ヘッダ生成ツールはKhronos Groupからオフィシャルに認められて、2016年7月末からKhronos Groupによってメンテナンスされています

  • Debug Layerを使ったバリデーションを活用しましょう。

大変役に立ちます。 もちろん、もうちょっとわかりやすいエラーを出せばいいのにと感じることもあります。 それでも無言でcrashするより、大変素晴らしいです。 そして、エラーではないが推奨されない指定についてWarningを出してくれるケースもありました。 これによって、コピペによるフラグの設定ミスなどを修正出来ました。

Vulkan Validation Layersの使い方を説明したサンプルコードはこちらです。 http://gpuopen.com/using-the-vulkan-validation-layers/

In the case of Playground

当社で開発している2Dゲームエンジン"Playground"でVulkanを実験的にサポートするために行った対応を紹介します。 エンジン固有の事情も含まれるため、前提を補足しつつ説明します。

  • CPU Batching and dynamic buffer binding.

Playgroundではもともと描画用の頂点データを実行時にCPU側で計算しているため、GPU側のバッファを一切使っていません。 OpenGLのドライバーと同じ働きをし、命令コマンドを貯めながら、転送する頂点のデータも貯める構造です。 そして、描画命令を実行する前に先にメモリの転送を行います。

  • OpenGL like wrapper in engine (not modern stateless)

エンジンの実行時にOpenGLのステート切り替えをしていたため、エンジンのRefactoringはおこなわずに同じ動作を再現しました。 フラグとステートの差分を管理し、使うPipelineを切り替えるようにしました。 (Supported states : Viewport, scissor, depth read/write, depth function.)

  • Dynamic Shaders, target buffers : not supported.

エンジンではシェーダーやターゲットバッファの切り替えをサポートしていますが、今回は最小限の作業で可能な範囲の対応をおこなったため、シェーダーの切り替えやターゲットの切り替え部分は未実装です。

  • Texture Switch. テクスチャーの切り替え

Vulkanでは二つの方法が存在すると認識しているのですが、試したのは片方だけです。 OpenGLやDirectXのようなダイナミックな切り替えはVulkanの場合、PipelineにつなぐDescriptor Set Layoutをテクスチャー毎に作成(OpenGL系でいうと、Uniform値の塊を設定するオブジェクト)することで実現します。 もう一つの方法は、複数のDescriptor Set Layoutを作成せずに、すべてのテクスチャーを配列化して、indexでアクセスする方法です(試していないし、実装方法を調べていません)。

今回対応したものはこんな感じです。

Draw Command In Playground

Vulkan SDK, Sample Code of vulkan over the net :

とにかく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/

Update :

Vulkanの調査とPlaygroundエンジンへの組み込み作業をおこなったのは2016年6月でした。 そしてこの記事を書いたのは7月でしたが、記事公開までの間の7月にSIGGRAPH 2016が開催されました。 このなかでVulkanに関する良い資料が公開されていたので紹介します。

https://www.khronos.org/assets/uploads/developers/library/2016-siggraph/3D-BOF-SIGGRAPH_Jul16.pdf

Conclusion :

Vulkanは低レベルAPIであると言われることが多いですが、個人的にはそうは思いません。 たしかに気をつけるべき事、やる事はかなり増えますが、今までの描画APIの流れで考えると自然なものが多いです。

つまり、Vulkanは完全に新しい概念というわけではなく、これまでの歴史の流れと追加されてきた概念の続きを担うAPIです。 具体的には、可能な限り事前処理できるものを増やす、データの再利用生を高める、描画時の負担を減らす、メモリアクセスをより柔軟にコントロールできるようにする、というトレンドに沿うものです。 これらを実現しやすくなるかわりに、GPUや描画パイプラインの細かいところまですべて理解する必要があります(覚える事は多いですが、昔からやっている描画エンジニアなら特に問題ありません)。

現時点のVulkanが抱える問題は、まだ丁寧なドキュメントがないことです。 ある事を実現したい時に複数の方法を考えつくことが多いのですが、何が「正しい」かという答えが出ない場合もあります(そもそも答えは存在しないかもしれません)。 つまり、ユースケースごとのわかりやすい例がありません。 おそらく何年間もゲーム機やPCでのグラフィック描画で苦労した経験者は悩む事が少ないでしょう。 私は描画APIをバリバリ触ってゲーム機向けの開発をおこなったことがないため、正当な評価をするための能力や経験が乏しい部分もありますが、より暗黙的なノウハウに依存しないマニュアル作りとさまざまな全体図/例の提示がほしいと感じました。

まずは、今後のGraham Sellers(AMD)のVulkan本に期待したいと思います。 https://www.amazon.com/Vulkan-Programming-Guide-Official-Learning/dp/0134464540