BlogKLabは、多くのスマートフォン向けゲームを開発・提供しています。スマートフォン向けゲームの開発と運用は、Webソーシャルゲームと比べて格段に複雑で、またコンシューマゲーム開発とも異なったノウハウが求められる領域もあります。このブログでは、KLabのゲーム開発・運用の中で培われた様々な技術や挑戦とそのノウハウについて、広く紹介していきます。はじめにR&D部に所属しつつ、社内のPlaygroundエンジンを開発しているピコアです。今回のテーマは最新描画APIのVulkanです。Vulkanはグラフィックス処理APIトレンドの最先端にあるもので、ゲーム開発者が従来よりもGPUのパワーをきめ細かく制御できます。そのかわりに開発者がさまざまな責任を負うことにもなります。Vulkanの使い方や細かなAPIの説明などはネット上のあちこちで見つけられるので、今回はあまり触れません。この記事の目的は、ゲームエンジンの開発者が実際にVulkanを利用した感想、学習中に得られた経験・苦労、そしてVulkanの良いところについての知見の共有です。描画APIの歴史まず、グラフィックスAPIの歴史と変遷をおさらいします。内容は設計思想の流れと時代に応じた変化を理解するための概要に留めます(例えば3DfxのGlideは無視する)。1992年1月に公開されました。当時のGPUの設計は現在とかなり違っていて、決まった固定パイプラインで描画するAPIでした。制限もありましたが、OpenGLの良いところはそれ以前の仕組みと違って、ハードウェアに依存しないAPIだったことです。関数ポインターを利用して拡張機能を簡単に追加できる仕組みがあります。そのおかげで、登場から25年近くが経った現在でもOpenGLにはまだ未来があります。時代と共に技術の進歩へ追従してきた、現代のハードウェアでもまともに動くAPIです。Windowsの世界ではDirectXが1995年にMicrosoft社から発表されました。MicrosoftはDOSからWindowsへの移行にあたり、WinGやDirectDrawのAPIを公開したのちに、3D描画のAPIを公開しました。DirectXの枠組みは音声、コントローラー入力など多くの仕様を含みますが、今回はDirect3Dの話です。Programming)モデルです。ハードウェアの進化にあわせて、徐々に新しいバージョンのDirectXをリリースするという方針です。OS上には以前のDirectXもインストールされており、古いアプリケーションがそのまま動作します。APIを維持して前のバージョンとの互換性を保つという設計思想ではないことに注意が必要です。Revisions)。バージョンごとにAPI設計を大きく変更することで、OpenGLよりも理解しやすく時代にあわせて洗練されたAPI体系を保ってきました。OSがゲーム環境として成長したことで、マーケットをリードするAPIとなりました。固定パイプライン時代パイプラインとは、頂点の入力からピクセルがバッファに描画されるまでの処理です。登場当初のDirectXの機能はOpenGLと大差なく、ともに固定パイプラインを採用していました。固定パイプライン時代は名前の通り、描画機能はすべてハードウェアに実装されたパスで計算され、それ以外の描画オペレーションは選択できませんでした。このため、光の計算や素材の描画の仕方などはすべて固定されていました。それでも進化は激しいものでした。7まで変更がありました。そこから、たくさんの変更もありましたが、詳細はリンクを読んで下さい。対するOpenGLについては、OpenGLのデータパスのイメージ図などを参照してください。OpenGL対DirectXという枠組みでは政治的な戦いやマーケットでの力関係もあって、性能面やそのあとに出た新ハードウェア機能の対応面などOpenGLが遅れていた部分がありました。OpenGLのメリットはWindows環境以外でも動作することであり、生き残る事は出来ました。8.0からシェーダーが追加されました。ポリゴンの頂点毎に自由な計算(プログラム可能)をおこない、さらに計算結果をピクセル毎に受け取って自由に処理(プログラム可能)できるようになりました。Shaderと呼びます。1.4からシェーダーをExtensionとしてサポートし、2.0から本体サポートに含まれるようになりました(2004年ごろ)。Shaderも追加されました。2.xでも同じく、固定パイプラインの機能がAPIから消えた)。ここでプログラマの学習ハードルが高くなりました。Shaderを組むことになります。ステートを先に持っていく、先に情報をフォーマットし、使いまわしの効率をあげるさらにCPUの負担をおさえるために、ゲームプログラマ側では描画ステートの変更タイミングを変更する取り組みがおこなわれました。従来は描画実行タイミングでおこなっていた処理を生成/設定タイミングへと移動し、実行時負荷を低減するというものです。ここで「作成時」と「利用時」を分けて考えた場合、仮に「作成時コスト」が大きくなったとしてもそれはプログラム実行中に一度だけ必要なコストです。描画に利用するデータを「作成時」に事前計算することで、何度も発生する「利用時コスト」を減らせるならば、積極的にコストを「作成時」へ押し付けて可能な限り「利用時コスト」を0に近づけるのが合理的です。この考え方とトレンドはのちにVulkanまで続いていきます。GPU)のモデルも追加そしてComputeモデル(OpenCLまたはNVIDIAのCUDAに近い)も描画APIに追加されました。描画専用ではなく、もっと自由な形でGPUに計算させる仕組みが追加されました。これによって例えば物理計算、音声データ処理なども出来るようになりました。描画の面でも、ピクセルに直接関係しない描画処理にもGPUを利用できます(例:グローバル・イルミネーションの計算)。ハードウェア性能を限界まで引き出すという点については、歴史的に専用ゲーム機のほうが先行してきました。Xbox世代のゲーム機においても、粒度のこまかなAPIが開発者へ公開され、性能向上に活用できました。PCの世界ではマシンごとにハードウェア構成が異なるため、粒度のこまかなAPIを提供することは難しいとされてきました。マルチスレッド動作さて、レンダリング機能が年々強化されていったOpenGLとDirectXですが、ともに処理モデル上の大きな弱点を持っていました。複数のスレッドを利用して(マルチスレッドで)描画したいという開発者のニーズを満たすAPIの不在です。OpenGLにはマルチスレッドモデルがまったく存在しないに等しいです。スレッドごとにContextを持つことはできますが、描画命令を実行できるOpenGLコンテキストはアプリケーション毎に1個しかありません。マルチスレッドを活用しようとしても、2個目以降は非同期のテクスチャアップロードにしか使えないのです。コンテキスト間でのテクスチャ共有には同期の面で注意を払う必要があります。11からマルチスレッド描画がサポートされました。10までの世代とOpenGLをターゲットとするゲームエンジンのなかには、複数のCPUコアを有効利用するために自前の処理レイヤを構築しているものもありました。具体的には、グラフィックスAPIを擬似的にマルチスレッド化するラッパーを作り、複数スレッドから描画コマンド列を作成する仕組みです。記録した描画コマンド列はメインスレッドがまとめてGPUへ転送します。この仕組みはゲームエンジン側のコードに新たなオーバーヘッド(データ構造をパースしてグラフィックAPI関数の呼び出しに必要な描画ステート、テクスチャ、モデル情報を検索するコスト)を生むため、非効率的と感じるかもしれません。このコストを払ってもなお、自前でグラフィックAPI呼び出しに相当するコマンドキューを作成してデータ列を詰め込み、レンダリング用のスレッドがそれを取り出して利用するという仕組みには処理効率上のメリットがありました。11でマルチスレッド描画がサポートされたと述べました。このなかでモデルを変更してコマンド列を貯める仕組みが追加されましたが、貯めたバッファのコミットは依然としてメインスレッドの役目でした。11の方式はゲームエンジン開発者が自前でコマンド列を貯めて発行していた方式と基本的に変わりません。唯一の違いはこれらの処理をDirectX自体がおこなうことです。このため、コマンド列を貯めるのと並行してさまざまな準備処理を先行させられます。コマンド間の差分のみを含むセットへと変換したり、GPUが実行しやすい形に変換するなどの処理です。11リリース後、2012年末ごろまでの業界状況は次のようなものでした。Rasterization)。DirectXやOpenGLはプログラマーの開発負担を低減するために、さまざまな処理を裏側でおこなっていた。場合により、ドライバーがいきなり重い作業をおこなったりと、開発者を悩ませることもあった(この負荷を避けるために、DirectXの癖を把握して、わざとある無駄なオペーレションを実行してタイミングをずらすハックもあったと聞きます)。この頃のMicrosoft社のスタンスは「DirectXは11で最後」というものでした。最後ではないはずですが、しばらくの間はもう抜本的な変更を加えた新しいAPIは出さないという意味でした。いっぽうで、ゲーム開発者は昔から「たとえ記述が面倒だとしても、できるだけ完全なコントロールが欲しい」と要求してきました。DirectXやOpenGLの提供する水準ではまだまだ足りないと感じ、専用ゲーム機並の自由なアクセスをしたいというものです。AAAゲームを出しているゲーム会社の人間は最先端の技術を理解し、できるだけ質の高い物を作ろうとするため、当然の事です。彼らはMicrosoftのスタンスに対して、別なルートを探していました。Groupの中でもこの議論がおそらくあったと思います(携帯端末でのゲーム市場が伸びる中で、バッテリー消費をおさえつつ高品質なグラフィック処理を実現したいという流れもありました)。Mantleの登場と各社の追従、Vulkanへそして、Mantleが登場しました。DICE(ゲーム会社)が組んで開発したAPIです。APIをサポートしたいならば、自由にやって良いという条件のもとで公開されました。3エンジンでの成果がかなり良く、業界に革命ともいえる大きなショックを与えました。優れたAPIであっても標準仕様ではない独自APIが成功する例は少なく、MantleがメインストリームAPIになる可能性は低いものでした。MantleはPC向けの独自APIでモバイル環境を考慮していなかったため、たとえばRenderPassの概念が存在しませんでした。寝ていた巨人が起きました。12を発表したのです。GroupがMantleをベースにVulkanの仕様を仕上げました。この間にApple社がMetalという描画APIを公開しました。細かくみていませんが、イメージ的にVulkanより扱いやすいと思います。残念な事にAppleがVulkanをサポートする気配は今のところありません。Metalの上で実行出来るVulkanのライブラリ(名前はMolten)が存在しますが、どこまで機能が動くのかはわかりません。12とも違う特徴を持つことです。VulkanにはMantleにはなかった物も入っています。12の概念はとても近いですが、Vulkanははじめからマルチプラットフォームを想定しています。OSもLinuxもサポートします。OneもAMDのチップを使っているので、Vulkan対応が可能だと考えられますが、政治的な意味でVulkan対応の可能性は低そうです。今後の新しい環境、ゲーム機などがサポートする可能性は十分あると考えられます。つづいて、Vulkanが従来のAPIとどう違うかを見ていきましょう。描画コマンドバッファで描画命令を貯められる。これらは再利用も可能で、どのスレッドからでも作成可能。GPUに送る事も出来る(出来上がったオブジェクトはある状態を表現するため、Immutable型で変更出来ません)。この3点によって、従来のOpenGLでは描画時にかかっていた処理コストをすべて作成時のコストに移動できます。事前処理によって内部でGPUの一番わかりやすい形(GPUでの利用時にオーバーヘッドが最も少ない形)のデータに変換することも可能です。描画命令を貯める事で処理差分のみをGPUに対して発行する最適化も可能で、これも処理負担減に貢献します。以前にあった描画時にコミットのオーバーヘッドやステートの変更によってドライバーがやらないといけない重い仕事が消えます。データはすべて事前に計算されているため、最低限の差分適用で前の処理から次の処理へ切り替えることもでき、GPUにとってもやさしい設計です。描画コマンドも再利用可能なので、さらにコストダウンにつながります。一回だけの作成コストで複雑な命令を組み合わせ、その後はずっと描画にコミットするだけです。ここで重要なのは、描画コマンドのバッファを再利用する際に、画面が全く同じ見た目になる必要はないということです。カメラの角度を変えたり、キャラクターの場所や動きを変える事は簡単に出来ます。10万個の草の描画命令を簡単に再利用することが可能です。コマンドバッファが利用するバッファの中身を差し替えることで、同じコマンドバッファを利用して異なる物を描画する事も可能です。利用するバッファ、書き込み用・読み込み用の依存グラフを定義できます。これによって画面出力までのバッファの流れが整理され、Vulkanドライバがハードの制御を最適化できます。VBOなど、とにかくオブジェクトポインターやIDが戻ってくる)。APIを利用するアプリケーションでも、Vulkanに対してメモリのアロケーションを要求します。このときに対象メモリの用途がテクスチャーなのかバッファなのか、その他のユースケースを明示します。要求したメモリはOpenGLやDirectXと同じく、Handleとして戻ってきます。端末のプロフィール情報から、どれぐらいのメモリがあるのか、それらの種類は何か、なども知る事が出来ます。この情報を利用して、メモリのアロケーション時にどの種類のメモリをどれだけ利用するか設定できます。割り当てられたメモリを利用する時に必ずすべてのAPIにHandleのオフセットを設定します。これによって、自然な形ですべてのメモリブロックに対する自前のアロケーションの仕組みを作成できます。Offsetのお陰でポインタを持っているのと同じ事が可能です。これはすごく頭の良い仕組みだと思いました。対象がCPUから見えるメモリであれば、Handleからアドレスにマッピング出来ます(GPUへのメモリ転送によく利用される)。基本的にスレッド毎にコマンドバッファで描画コマンドを貯めるのは変わりません。かわるのはどのスレッドでもコミットが可能という点です。同時に2つ以上のスレッドが同じQueueにアクセスしてはなりません。同時アクセスを防ぐための制御はユーザの責任でおこないます。すべてのデータの種類に関する、細かなルールは以下のページにあります。ここでVulkan用にインストールされているレイヤー(APIのラッパーの理解で良い、目的によって、いろいろなレイヤーが存在する)を列挙します。この段階で拡張関数の取得も可能です。Vulkanのインスタンスを作成します。実際にコンピュター内にVulkan対応の物理デバイス(つまりグラフィックカード)のリストを取得します。デバイスごとにサポートされている機能、機能の制限、メモリの種類と制限、拡張の情報を取得できます。デバイスが対応しているQueue一覧(Queueは命令の配列を実行する物)を取得できます。APIでインスタンス化します。物理デバイスのインスタンスから、論理デバイスを作成します。論理デバイスでリソース、Queueなどを取得します(作業はほぼすべて論理デバイスで行う)。表示用の拡張APIを取得します。Surfaceを作成します。Surfaceが対応しているフォーマットや機能を確認します。表示の仕方(モデル)を取る、選択し、設定します。スワップチェインを作成します。スワップチェイン内からのイメージ一覧を取得します。Bufferを作成します。メモリバリアを作成します。イメージ毎にViewを作成します。2つのセマフォを作成します。