DrawCall (ドローコール) って何? 描画パフォーマンスを考える一つの指標を見る

初めまして、ドリコムクライアントエンジニアの増野です。
ドリコムでは、スマートフォンを中心として“ゲーム制作”に関する技術のキャッチアップを行っています。

その中で今回は描画に関する部分、特に GPU と DrawCall (ドローコール) の話について触れてみたいと思います。

画面の描画はどのように行われているか?

ゲームにおける「描画」とは極端に言ってしまえば、表示用のメモリ領域をピクセル情報で埋める作業のことを指します。

例えば、下記のようなドリコムのロゴがあったとします。

これを拡大していくと画像のようなピクセル情報の集まりだという事が見えてきます。

ピクセル一点一点につき、メモリにはそのピクセルが赤なのか青なのかと言った色情報が保存されており、その色情報のメモリをディスプレイに転送することで初めて目に見える形で表示が行われます。なお、画面全体の色情報が記録されたメモリ領域のことをフレームバッファと呼んだりします。

極端な例になりますが 2×2 px のフレームバッファがあった場合(画像は3600%拡大しています)下記のような値が書き込まれます。

少し分かりづらいですが、ABGR の順で16進数での色情報が入っています。

を指しています。

※ なぜ一般的な RGBA ではなく ABGR の順で記述されているのかは別の話になるためここでは割愛させて頂きます。興味がある方は「リトルエンディアン」で調べて見て下さい。

描画とは、システムから見れば上記のような 00 FF 00 FF と言った色情報(ピクセル)をメモリに書き込む工程を指します。

CPU と GPU の違い

簡単にですが描画とは何かを説明してきました。

さて、最近ではフルHD(1920x1080px)の画面解像度を持ったスマートフォン端末が増えてきています。1920x1080px だと、ピクセル数で言えば縦横合計でおよそ 207万px あります。

またリアルタイム性が求められるゲームでは 60FPS(1秒間に60回画面が更新される)での実行速度が求められることが多く、その場合 207万ピクセル x 60回 = 約1億2441万ピクセルを1秒間に更新を行う必要があります。

たった1秒ゲームが実行されるだけで、さきほど説明した 00 FF 00 FF のような色情報が1億回以上メモリに書き込まれています。私たちが普段何気なく遊んでいるゲームも、膨大な処理によって成り立っていることが分かります。

この1億回のメモリの書き換え(ピクセルの更新)ですが、当然 CPU を使用して行うことも出来ますが、幾ら CPU が早くなったとは言え、毎回毎回この膨大なピクセル情報をメモリに書き込み続けるのはあまり効率的ではありません。

そのため、フレームバッファのメモリを書き換える専用のプロセッサが必要とされ、それが GPU として搭載されています。

最近では計算目的に活用されることが多くなった GPU ですが、今でもフレームバッファのメモリを埋める(厳密に言えば、ピクセル更新に必要な一連の演算処理を行う)のが主な役割です。

グラフィックス API

GPU はフレームバッファの内容を書き換え、描画結果の生成を行います。
そこで、GPU を使用したプログラミングを考えてみたいと思います。

GPU は複数のベンダーからデスクトップ用途で使われるものや、モバイル向けに低消費電力化された物など幾つかの製品が存在します。こう言った外部のハードウェア(デバイス)には必ず、そのハードウェアを制御するための専用のソフトウェア「ドライバ」が存在します。アプリケーションはドライバを通してハードウェアにアクセスすることが出来ます。

しかし大きな問題があります。
ドライバの実装内容は製品によって異なるため、アプリケーションから GPU を使おうとした場合、A社の GPU と B社の GPU ではプログラマは別のコードを書かなくてはなりません。対応する GPU が増えるごとに動作させるコードを追加していくのはめっちゃ大変です。

そこで共通の API を策定して統一的に GPU を利用できるようにした方が効率的だと考え、登場したのが OpenGL / DirectX の グラフィックスAPI です。アプリケーションから見れば、OpenGL / DirectX の関数を呼び出せば、後はドライバが良しなに GPU を駆動させて描画してくれます。

アプリケーションはこのグラフィックスAPIを呼び出して、初めて GPU を用いた描画が行えるのです。

DrawCall (ドローコール) とは何か?

ここまで長くなりましたが本題です。

グラフィックスAPI を使用して、画面に描画を行う際に呼び出す命令の事を DrawCall (ドローコール) と呼びます。

具体的には下記のような関数が該当します。(ここで並べているのは一部です)

これらの関数を呼び出すことで初めて GPU は駆動し、フレームバッファのメモリ情報が書き換わることになります。

当然、Unity なども内部で glDrawElements()DrawIndexed() を呼び出して描画を行っており、この Draw関数の一度の呼び出しが、1 DrawCall になります。

CPU から GPUへ 描画時には何が行われるのか?

グラフィックスAPI の呼び出しによって、モデルやステートの情報は描画コマンドとしてメインメモリ上のコマンドバッファに蓄積されます。そのコマンドはグラフィクスAPI の Present や Flush の呼び出しなどによってGPUに転送され処理されます。

Direct3D のステート設定、プレゼント、描画のコマンドがアプリケーションによって呼び出されると、これらのコマンドは内部コマンド バッファーのキューに格納されます。Flush はこれらのコマンドを GPU に送信して処理されるようにします。通常これらのコマンドは、コマンド バッファーが満杯の場合や、リソースのマッピング時など、Direct3D によって必要だと判定された場合に自動的に GPU に送信されます。Flush は、コマンドを手動で送信します。

ID3D11DeviceContext::Flush

簡単にまとめると、以下のような形になると思います。

実際には GPU に対してコマンドが転送される前に、描画コマンドの正当性チェック(バリデーション)なども行われているはずです。

コンシューマ機であればグラフィックスAPI が直接ネイティブなGPUコマンドを発行しますが、DirectX ではグラフィックスAPIが蓄積したコマンドをドライバがGPUコマンドへと変換する処理が入るため、これが余計なオーバーヘッドにもなっています。

まとめ

描画コマンドの処理は、DrawCall を一つの基準としてドライバが処理するため、DrawCall の増加が CPU の負荷となります。

これが DrawCall による CPU の オーバーヘッドになり、これを減らす事がアプリケーションの最適化へと繋がります。

なお、よく誤解されるのですが、上述の通り DrawCall は CPU 側の負荷です。アプリケーションにおいて GPU がボトルネックになっている場合は、DrawCall を減らしても最適化には繋がりません。

DrawCall のオーバーヘッドの削減へ

近年、Low-Overhead, Low-Level グラフィックスAPI が登場しています。
2013年の AMD Mantle 以降、その思想は DirectX 12 へと受け継がれ、Apple Metal や Khronos Vulkan など新しいグラフィックスAPIが生み出されてきました。

従来、開発者からは不透明であったリソースのメモリ管理であったり、コマンドバッファの構築など、全てアプリケーション側で責任を持つ必要があります。今までブラックボックスだった多くの部分が開発者に公開されるようになりました。その反面、グラフィックスAPIは更に高度化、複雑化の道を辿りつつあります。

最近のグラフィックスAPIを考える上で大切なことは、また別の機会に解説出来たらと思っています。