Direct3D API 呼び出しの正確なプロファイリング (Direct3D 9)
- Direct3D の正確なプロファイリングは困難
- Direct3D レンダリング シーケンスの正確なプロファイリング方法
- Direct3D ステートの変更のプロファイリング
- まとめ
- 付録
有益な Microsoft Direct3D アプリケーションを入手し、そのパフォーマンスを改善したい場合、通常は市販のプロファイリング ツールまたはあるカスタム測定テクニックを使用して 1 つ以上のアプリケーション プログラミング インターフェイス (API) の呼び出しにかかる時間を測定します。この呼び出しを行っても、1 つのレンダリング シーケンスから次のレンダリング シーケンスで実行時間が異なる場合、または実際の実験結果が正しいと限らないという仮説が成り立つ場合、その理由は次の情報でわかりやすくなります。
ここに提供されている情報は、読者が次に関する知識と経験を持つことを前提としています。
- C/C++ プログラミング
- Direct3D API プログラミング
- API タイミングの測定
- ビデオ カードとそのソフトウェア ドライバー
- 以前のプロファイリングで説明のつかないと思われる結果が得られた経験
Direct3D の正確なプロファイリングは困難
プロファイラーは、各 API コールにかかった時間を報告します。これは、ホット スポットを見つけてこれを回避することでパフォーマンスを改善するために実行されます。プロファイラーとプロファイリング テクニックにはいろいろな種類があります。
- サンプリング プロファイラーは、ほとんどの時間をアイドル状態ですごし、特定の間隔で呼び出されて、実行される機能をサンプリング (または記録) します。プロファイラーは、各呼び出しにかかった時間の割合を返します。一般に、サンプリング プロファイラーのアプリケーションへの影響はほとんどなく、アプリケーションのオーバーヘッドへの影響も最小限に抑えられます。
- インストルメント化プロファイラーは、呼び出しが戻るまでの実際の時間を測定します。開始および終了の区切り文字はアプリケーションにコンパイルする必要があります。インストルメント化プロファイラーは、サンプリング プロファイラーと比べるとアプリケーションへの影響があります。
- 高性能タイマーを使ったカスタムなプロファイリング テクニックを利用することもできます。このテクニックでは、インストルメント化プロファイラーと極めて似た結果が得られます。
正確な測定を生成するには、使用するプロファイラーまたはプロファイリング テクニックの種類だけが問題となるわけではありません。
パフォーマンスの割り当てに役立つ回答は、プロファイリングから得られます。たとえば、API 呼び出しの実行平均が 1000 クロック サイクルであることがわかっているとします。パフォーマンスに関して、次のような結論を下すことができます。
- 2 GHz の CPU (これは時間レンダリングの 50 % を費やします) では、この API を 1 秒間に 100 万回までしか呼び出すことができない。
- 1 秒間あたり 30 フレームを達成するために、この API をフレームあたり 3,3000 回より多く呼び出すことはできない。
- フレームあたり 3,3000 個のオブジェクトのみレンダリングできる (これらの API 呼び出しがオブジェクトのレンダリング シーケンスごとに 10 回とした場合)。
言い換えれば、API 呼び出しごとに十分な時間があれば、インタラクティブにレンダリングできるプリミティブの数など、割り当ての質問に答えることができます。ただし、インストルメント化プロファイラーによって返される RAW 数は、割り当て質問に対する正確な回答ではありません。これは、動作が必要な成分の数、成分間でのワーク フロー方法を制御するプロセッサーの数、パイプラインを効率化するためにラインタイムおよびドライバーに実装された最適化戦略など、複雑な設計上の問題がグラフィック パイプラインに存在するためです。
各 API 呼び出しは複数の成分を通過
各呼び出しは、アプリケーションからビデオ カードへの途中でいくつかの成分によって処理されます。たとえば、1 つの三角形を描画する 2 つの呼び出しを含む次のレンダリング シーケンスを考えてみましょう。
SetTexture(...); DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);
次の概念図は、呼び出しが通過しなければならないさまざまな成分を示しています。
アプリケーションは Direct3D を呼び出し、Direct3D はシーンを制御し、ユーザーの対話操作を処理し、レンダリングの実行方法を決定します。これらの処理はすべて、Direct3D API 呼び出しを使用してランタイムに送信されるレンダリング シーケンスで指定されます。レンダリング シーケンスは仮想的にはハードウェアから独立しています (つまり API 呼び出しはハードウェアから独立していますが、アプリケーションはビデオ カードがサポートしている機能を認識しています)。
ランタイムは、これらの呼び出しをデバイス非依存のフォーマットに変換します。ランタイムは、アプリケーションとドライバーとの間の通信をすべて処理します。そのためアプリケーションは複数の互換ハードウェア上で実行します (必要な機能によって異なります)。関数呼び出しの測定時には、インストルメント化プロファイラーは、関数にかかる時間、および関数が戻るのにかかる時間を測定します。インストルメント化プロファイラーには、ドライバーが作業結果をビデオ カードに送信するのにかかる時間も、ビデオ カードが作業を処理するのにかかる時間も含まれないという制限があります。言い換えれば、市販のインストルメント化プロファイラーは、各関数呼び出しに関連付けられたすべての作業の起因を探ることができません。
ソフトウェア ドライバーは、ビデオ カードに関するハードウェア固有の情報を利用してデバイス非依存のコマンドをビデオ カード コマンドのシーケンスに変換します。また、ドライバーはビデオ カードに送信されるコマンドのシーケンスも最適化できるため、ビデオ カードのレンダリングは効率的に実行されます。これらの最適化では、実行された作業量が表示される作業量と異なるため、プロファイリングの問題が生じることがあります (最適化を説明するには最適化を理解する必要があります)。通常、ドライバーは、ビデオ カードがすべてのコマンドの処理を終了する前に、ランタイムに制御を返します。
ビデオ カードは、頂点バッファーとインデックス バッファー、テクスチャー、レンダリング ステート情報、およびグラフィック コマンドからのデータを結合することでほとんどのレンダリングを実行します。ビデオ カードがレンダリングを終了すると、レンダリング シーケンスから作成される作業は完了します。
各 Direct3D API 呼び出しは、なんらかをレンダリングする成分 (ランタイム、ドライバー、ビデオ カード) ごとに処理する必要があります。
成分を制御するプロセッサーは複数存在する
これらの成分間の関係はさらに複雑です。これはアプリケーション、ランタイム、およびドライバーが 1 つのプロセッサーで制御され、ビデオ カードは別のプロセッサーで制御されるためです。次の図は、CPU (中央処理装置) と GPU (グラフィック プロセッシング ユニット) という 2 種類のプロセッサを示しています。
PC システムは、CPU と GPU を少なくとも 1 基ずつ搭載していますが、どちらかまたは両方を複数搭載することも可能です。CPU はマザーボードに配置され、GPU はマザーボードとビデオ カードのいずれかに配置されます。CPU の速度はマザーボード上の、あるクロック チップによって決定され、GPU の速度は別のクロップ チップによって決定されます。CPU クロックはアプリケーション、ランタイム、およびドライバーによって実行される作業の速度を制御します。アプリケーションはランタイムとドライバーを介して GPU に作業を送信します。
通常、CPU と GPU は互いに依存せず別の速度で実行します。GPU は作業が可能になるとすぐに作業に応答できます (GPU が前の作業の処理を終了している場合)。GPU の作業は、上の図の曲線で強調表示されているとおり、CPU の作業と平行して実行されます。通常、プロファイラーは GPU ではなく CPU のパフォーマンスを測定します。プロファイリングが難しいのはこのためです。インストルメント化プロファイラーが実行する測定は、CPU 時間を含んでも GPU 時間を含まないことがあるためです。
GPU の目的とは、グラフィック作業向けに特殊設計されたプロセッサーに CPU の処理を負担させることです。現代のビデオ カードでは、パイプラインのトランスフォームとライティング作業のほとんどを CPU に代わって GPU が実行します。これによって、CPU の負荷が大幅に軽減され、より多くの CPU サイクルを他の処理に使用できます。最大パフォーマンスに合わせてグラフィカル アプリケーションを調整するには、CPU と GPU の両方のパフォーマンスを測定し、2 種類のプロセッサー間の作業のバランスを調整する必要があります。
ここでは、GPU のパフォーマンスの測定、または CPU と GPU の作業のバランスの調整に関連するトピックについては説明しません。CPU (または特定のビデオ カード) のパフォーマンスについて詳しく知りたい場合は、ベンダーの Web サイトにアクセスして GPU パフォーマンスの詳細を検索してください。代わりに、ここでは、GPU の作業をわずかな量まで減らすことによって、ランタイムとドライバーが実行する作業がどうなるかについて焦点を当てます。これは、パフォーマンスに問題があるアプリケーションは一般に CPU による制約を受けているという経験に基づくものです。
ランタイムとドライバーの最適化によって API 測定のマスキングが可能に
ランタイムには、個々の呼び出しの測定に大きく影響する可能性のあるパフォーマンス最適化がビルトされています。次のシナリオ例はこの問題を示したものです。次のレンダリング シーケンスを考えてみましょう。
BeginScene(); ... SetTexture(...); DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1); ... EndScene(); Present();
例 1 : 単純なレンダリング シーケンス
レンダリング シーケンスの 2 つの呼び出しの結果を調べると、インストルメント化プロファイラーは次のような結果を返すことがあります。
Number of cycles for SetTexture : 100 Number of cycles for DrawPrimitive : 950,500
プロファイラーは各呼び出しに関連付けられている作業を処理するために必要な CPU サイクルの数を返します (GPU はこれらのコマンドでの作業をまだ開始していないため、GPU はこれらの数には含まれていません)IDirect3DDevice9::DrawPrimitive では、処理するためにほぼ 100 万サイクルを必要としたため、極めて効率的であると判断するかもしれません。ただし、すぐにこの結論が正しくないという理由、そして割り当てに利用できる結果を生成する方法がわかります。
ステート変更の測定では慎重なレンダリング シーケンスが必要
IDirect3DDevice9::DrawPrimitive、IDirect3DDevice9::DrawIndexedPrimitive、または IDirect3DDevice9::Clear (IDirect3DDevice9::SetTexture、IDirect3DDevice9::SetVertexDeclaration、IDirect3DDevice9::SetRenderState など) 以外のすべての呼び出しは、ステートの変更を生成します。各ステートの変更によって、レンダリングの実行方法を制御するパイプライン ステートが設定されます。
ランタイムおよび/またはドライバーの最適化は、必要な作業量を減らすことでレンダリングを高速化するように設計されています。プロファイルの平均を損なうことがある 2 つのステート変更の最適化は次のとおりです。
- ドライバー (またはランタイム) は、ステート変更をローカル ステートとして保存します。ドライバーは "遅延" アルゴリズム (絶対的に必要となるまで作業を遅延する) で動作するため、いくつかのステート変更に関連付けられた作業は延期されます。
- ランタイム (またはドライバー) は、最適化によってステート変更を削除することがあります。この例としては、ライティングが以前に無効化されているために、ライティングを無効にする冗長なステート変更を削除する場合などが挙げられます。
どのステート変更が dirty ビットを設定して作業を遅らせるか、または最適化のために単純に削除されるかをレンダリング シーケンスを調べて判断する確実な方法はありません。最適化されたステート変更を今日のランタイムまたはドライバーで特定できたとしても、明日のランタイムまたはドライバーは更新される可能性があります。また、前回のステートがどうであったかはすぐにはわからないため、冗長なステート変更を特定することは困難です。ステート変更のコストを確認する唯一の方法とは、ステート変更を含むレンダリング シーケンスを測定することです。
おわかりのとおり、複数のプロセッサー、複数の成分によって処理されるコマンド、および成分にビルトされる最適化を実装することで生まれる複雑さがプロファイリングの予測を困難にしています。次のセクションでは、これらのプロファイリングの課題それぞれについて説明します。サンプルの Direct3D レンダリング シーケンスを測定テクニックとともに示します。この知識があれば、呼び出しごとに正確で繰り返し可能な測定を生成できます。
Direct3D レンダリング シーケンスの正確なプロファイリング方法
プロファイリングの課題をいくつか挙げてきましたが、ここでは、割り当てに利用できるプロファイル測定の生成に役立つテクニックを示します。CPU によって生成される成分間の関係、およびランタイムとドライバーによって実装されるパフォーマンスの最適化を回避する方法を理解すれば、正確で繰り返し可能なプロファイリング測定が可能になります。
まず初めに、単一の API 呼び出しの実行時間を正確に測定できる必要があります。
QueryPerformanceCounter のような正確な測定ツールを選択する
Microsoft Windows オペレーティング システムには、高分解能の経過時間の測定に利用できる高分解能タイマーが搭載されています。そのようなタイマーの 1 つの現在値は、QueryPerformanceCounter を使用して返すことができます。QueryPerformanceCounter を呼び出して start 値と stop 値を返した後、QueryPerformanceFunction を使用して 2 つの値の違いを実際の経過時間 (秒単位) に変換できます。
QueryPerformanceCounter を使用することのメリットとは、これが Windows で使用できること、および使いやすいことです。単純に呼び出しを QueryPerformanceCounter 呼び出しで囲み、開始値と停止値を保存してください。したがって、ここでは、インストルメント化プロファイラーで測定する方法と同じように、QueryPerformanceCounter を使用して実行時間をプロファイルする方法について説明します。次の例は、QueryPerformanceCounter をソース コードに埋め込む方法を示しています。
BeginScene(); ... // Start profiling LARGE_INTEGER start, stop, freq; QueryPerformanceCounter(&start); SetTexture(...); DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1); QueryPerformanceCounter(&stop); stop.QuadPart -= start.QuadPart; QueryPerformanceFrequency(&freq); // Stop profiling ... EndScene(); Present();
例 2 : QPC によるカスタム プロファイリングの実装
start と stop は、高性能タイマーによって返された開始値と停止値を保持する 2 つの大きな整数です。QueryPerformanceCounter(&start) は、IDirect3DDevice9::SetTexture の直前に呼び出され、QueryPerformanceCounter(&stop) は IDirect3DDevice9::DrawPrimitive の直後に呼び出されます。stop 値を取得した後、QueryPerformanceFrequency が呼び出され、高分解能タイマーの周波数である freq が返されます。この仮説の例では、start、stop、freq について次の結果が得られたとします。
ローカル変数 | ティック数 |
---|---|
start | 1792998845094 |
stop | 1792998845102 |
freq | 3579545 |
これらの値は、次のような API 呼び出しを実行するためにかかるサイクル数に変換できます。
# ticks = (stop - start) = 1792998845102 - 1792998845094 = 8 ticks # cycles = CPU speed * number of ticks / QPF # 4568 = 2 GHz * 8 / 3,579,545
言い換えれば、この 2 GHz のマシンで IDirect3DDevice9::SetTexture と IDirect3DDevice9::DrawPrimitive を処理するには約 4568 クロック サイクルかかります。次のように、これらの値はすべての呼び出しを実行するためにかかる実際の時間に変換できます。
(stop - start)/ freq = elapsed time 8 ticks / 3,579,545 = 2.2E-6 seconds or between 2 and 3 microseconds.
QueryPerformanceCounter を使用する場合、開始測定と停止測定をレンダリング シーケンスに追加して、QueryPerformanceFrequency を使用して違い (ティックの数) を CPU サイクルの数、または実際の時間に変換する必要があります。カスタム プロファイリングの実装の開発は、まずは測定テクニックの特定から始めます。測定を開始する前に、ビデオ カードを扱う方法を知る必要があります。
CPU の測定について
既に説明したように、CPU と GPU は並列で動作して API 呼び出しによって生成された作業を処理します。現実の世界のアプリケーションでは、両方のタイプのプロセッサーをプロファイルしてアプリケーションが CPU による制約を受けるか、GPU による制約を受けるかを検出する必要があります。GPU パフォーマンスはベンダー固有であるため、ここでは、使用可能な各種ビデオ カードをカバーした結果を生成することは大変困難です。
代わりに、ランタイムとドライバーの作業を測定するカスタム テクニックを使用して、CPU によって実行される作業のプロファイリングのみ説明します。GPU の作業はわずかな量に削減されるため、CPU の結果がよりはっきりします。このアプローチの 1 つのメリットとは、付録に記載された結果がこのテクニックによって得られるとうことです。この結果をそれぞれの結果と相関させることができます。ビデオ カードで必要な作業をわずかな量に減らすために、単純にレンダリング作業をできるだけ少ない量に抑えてください。それには、描画呼び出しを単一の三角形のレンダリングに制限し、さらに各三角形がピクセルを 1 つだけ含むようにします。
CPU 作業の測定についてここで使用されている測定の単位は、実際の時間ではなく CPU クロック サイクルの数です。CPU クロック サイクルには、CPU 速度が異なるマシン間では実際の経過時間よりも移植性がある (CPU による制約を受けるアプリケーションの場合) という利点があります。これは、必要に応じて実際時間に簡単に変換できます。
ここでは、CPU と GPU との間の作業負荷分散に関するトピックは説明しません。このドキュメントの目的は、アプリケーションのパフォーマンス全体を測定することではなく、ランタイムとドライバーが API 呼び出しの処理にかける時間を正確に測定する方法を示すことです。これらを正確に測定することで、CPU の割り当ての作業を行い、特定のパフォーマンス シナリオを理解することができます。
ランタイムとドライバーの最適化の制御
特定された測定テクニック、および GPU 作業を減らす戦略を確認したら、次の手順はプロファイリング時に障害となるランタイムとドライバーの最適化を理解することです。
CPU の作業は、アプリケーションの作業、ランタイムの作業、およびドライバーの作業の 3 つに大別できます。アプリケーションの作業はプログラマーの管理下にあるため無視してください。アプリケーションの観点からすると、ランタイムとドライバーはブラック ボックスのようなものです。アプリケーションからはランタイムとドライバーに実装されているものを制御できないためです。重要なことは、ランタイムとドライバーに実装される最適化テクニックを理解することです。これらの最適化を理解しないと、プロファイル測定に基づいて CPU が実行している作業の量に関して間違った結論を下してしまいやすくなります。具体的には、コマンド バッファーと呼ばれるもの、そしてコマンド バッファーが何を実行することでプロファイリングが難しくなるのかという 2 つのトピックで取り上げます。これらのトピックは、次のとおりです。
- コマンド バッファーによるランタイムの最適化。コマンド バッファーとは、モードの移行の影響を抑えるランタイムの最適化です。モードの移行のタイミングを制御するには、「コマンド バッファーの制御」を参照してください。
- コマンド バッファーのタイミング エフェクトの無効化。モードの移行の経過時間は、プロファイリングの測定にかなり影響することがあります。これに対する戦略は、モードの移行と比較してレンダリング シーケンスを大きくするすることです。
コマンド バッファーの制御
アプリケーションが API 呼び出しを行うと、ランタイムは API 呼び出しをデバイス非依存のフォーマット (以下、コマンド) に変換し、これをコマンド バッファーに格納します。ブロック図に追加したコマンド バッファーは次のとおりです。
アプリケーションが別の API 呼び出しを行うたびに、ランタイムはこのシーケンスを繰り返して、別のコマンドをコマンド バッファーに追加します。ある時点で、ランタイムはバッファーを空にします (コマンドをドライバーに送信します)。Windows XP では、コマンド バッファーを空にすると、オペレーティング システムがランタイム (ユーザー モードで動作) からドライバー (カーネル モードで動作) に切り替わるときにモードの移行が発生します。
- ユーザー モード - アプリケーション コードを実行する特権のないプロセッサー。ユーザー モードのアプリケーションはシステム サービス以外からはシステム データにアクセスできません。
- カーネル モード - Windows ベースのエグゼクティブ コードが実行する特権のあるプロセッサー モード。カーネル モードで動作するドライバーまたはスレッドには、すべてのシステム メモリーへのアクセス権、ハードウェアへの直接アクセス権、ハードウェアで I/O を実行する CPU 命令があります。
移行は、CPU がユーザー モードからカーネル モード (またはその逆) に切り替わるたびに発生し、これが必要とするサイクル数は、個々の API 呼び出しと比べると多くなります。ランタイムが呼び出されたときに各 API 呼び出しをドライバーに送信した場合、API 呼び出しごとにモードの移行のコストが発生します。
一方で、コマンド バッファーはモードの移行の効率的なコスト削減を目的としたランタイム最適化です。コマンド バッファーは、単一のモードの移行として多くのドライバー コマンドをキューに入れます。ランタイムがコマンド バッファーにコマンドを追加すると、制御がアプリケーションに返されます。プロファイラーには、ドライバーにまだ送信されていない可能性のあるドライバー コマンドを知る方法がありません。結果として、市販のインストルメント化プロファイラーによって返された数は、ランタイム作業を測定しますが関連するドライバー作業は測定しないため、信頼性に欠けます。
モードの移行なしのプロファイル結果
ここでは、例 2 のレンダリング シーケンスを使用して、モードの移行の規模を示す一般的なタイミング測定をいくつか挙げます。IDirect3DDevice9::SetTexture 呼び出しと IDirect3DDevice9::DrawPrimitive 呼び出しがモードの移行発生させないと想定した場合、市販のインストルメント化プロファイラーは、次のような結果を返します。
Number of cycles for SetTexture : 100 Number of cycles for DrawPrimitive : 900
これらの数字はそれぞれ、ランタイムがこれらの呼び出しをコマンド バッファーに追加するのにかかる時間の量です。モードの移行はないため、ドライバーはまだどの作業も実行していません。プロファイラーの結果は正確ですが、レンダリング シーケンスが最終的に CPU を実行させることになる作業をすべては測定していません。
モードの移行ありのプロファイル結果
ここで、モードの移行が発生したときに同じサンプルで何が発生するかを見てましょう。今回、IDirect3DDevice9::SetTexture と IDirect3DDevice9::DrawPrimitive がモードの移行を発生させると想定します。市販のインストルメント化プロファイラーは、今回も次のような結果を返します。
Number of cycles for SetTexture : 98 Number of cycles for DrawPrimitive : 946,900
IDirect3DDevice9::SetTexture に測定された時間はほぼ同じですが、モードの移行により IDirect3DDevice9::DrawPrimitive にかかった時間は大幅に増加しています。次に、何が発生しているかについて説明します。
- レンダリング シーケンスを開始する前に、コマンド バッファーにはコマンド 1 つ分の余裕があるとします。
- IDirect3DDevice9::SetTexture はデバイス非依存のフォーマットに変換され、コマンド バッファーに追加されます。このシナリオでは、この呼び出しによってコマンド バッファーがいっぱいになります。
- ランタイムは IDirect3DDevice9::DrawPrimitive をコマンド バッファーに追加しようとしますが、バッファーがいっぱいであるためできません。代わりに、ランタイムはコマンド バッファーを空にします。これによってカーネルモードの移行が発生します。移行には約 5000 サイクルかかります。この時間は IDirect3DDevice9::DrawPrimitive にかかる時間に影響します。
- 次に、コマンド バッファーから空にされたすべてのコマンドに関連する作業をドライバーが処理します。コマンド バッファーをほぼいっぱいにしたコマンドを処理するドライバー時間は約 935,000 サイクルとします。また、IDirect3DDevice9::SetTexture に関連付けられたドライバーの作業は約 2750 サイクルとします。この時間は IDirect3DDevice9::DrawPrimitive にかかる時間に影響します。
- ドライバーがその作業を終了すると、ユーザーモードの移行によって、ランタイムに制御が返ります。これでコマンド バッファーは空になります。移行には約 5000 サイクルかかります。
- レンダリング シーケンスは、IDirect3DDevice9::DrawPrimitive を変換して、これをコマンド バッファーに追加することで完了します。これには約 900 サイクルかかるとします。この時間は IDirect3DDevice9::DrawPrimitive にかかる時間に影響します。
表示される結果はおおよそ次のとおりです。
DrawPrimitive = kernel-transition + driver work + user-transition + runtime work DrawPrimitive = 5000 + 935,000 + 2750 + 5000 + 900 DrawPrimitive = 947,950
モードの移行 (900 サイクル) のない IDirect3DDevice9::DrawPrimitive の測定と同様に、モードの移行 (947,950 サイクル) のある IDirect3DDevice9::DrawPrimitive の測定は正確ですが、CPU 作業の割り当てという観点からは役に立ちません。結果は、正確なランタイム作業、IDirect3DDevice9::SetTexture に対するドライバー作業、IDirect3DDevice9::SetTexture に先行したコマンドに対するドライバー作業、および 2 つのモードの移行を含みます。ただし、測定には IDirect3DDevice9::DrawPrimitive ドライバーの作業は含まれません。
モードの移行は、すべての呼び出しに対応して発生する可能性があります。これは、コマンド バッファー内に何があったかによって決まります。CPU 作業が (ランタイムとドライバー) がどれほど各呼び出しに関連付けられているかを理解するには、モードの移行を制御する必要があります。これには、コマンド バッファーとモードの移行のタイミングを制御するメカニズムが必要です。
クエリ メカニズム
Microsoft Direct3D のクエリ メカニズムは、ランタイムが GPU に処理について問い合わせることができ、GPU から特定のデータを返すことができるように設計されました。プロファイリング中には、GPU 作業は最小限度に抑えられるため、パフォーマンスへの影響はほとんどなく、GPU からのステータスを返してドライバーの作業の測定に役立てることができます。結局のところ、ドライバーの作業は、GPU がドライバーのコマンドを知った時点で完了します。さらに、クエリ メカニズムを上手に利用すると、コマンド バッファーがいつ空になるか、およびバッファーにどのくらいの作業があるかという、プロファイリングに重要なコマンド バッファーの 2 つの特性を制御できます。
クエリ メカニズムを使用した同じレンダリング シーケンスは次のとおりです。
// 1. Create an event query from the current device IDirect3DQuery9* pEvent; m_pD3DDevice->CreateQuery(D3DQUERYTYPE_EVENT, &pEvent); // 2. Add an end marker to the command buffer queue. pEvent->Issue(D3DISSUE_END); // 3. Empty the command buffer and wait until the GPU is idle. while(S_FALSE == pEvent->GetData( NULL, 0, D3DGETDATA_FLUSH )) ; // 4. Start profiling LARGE_INTEGER start, stop; QueryPerformanceCounter(&start); // 5. Invoke the API calls to be profiled. SetTexture(...); DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1); // 6. Add an end marker to the command buffer queue. pEvent->Issue(D3DISSUE_END); // 7. Force the driver to execute the commands from the command buffer. // Empty the command buffer and wait until the GPU is idle. while(S_FALSE == pEvent->GetData( NULL, 0, D3DGETDATA_FLUSH )) ; // 8. End profiling QueryPerformanceCounter(&stop);
例 3 : コマンド バッファーを制御するクエリの使用
これらのコード行それぞれの詳細な説明は次のとおりです。
- D3DQUERYTYPE_EVENT を使用してクエリ オブジェクトを作成することでイベント クエリを作成します。
- IDirect3DQuery9::Issue (D3DISSUE_END) を呼び出すことでコマンド バッファーにクエリ イベント マーカーを追加します。このマーカーは、どんなコマンドがマーカーに先行していても、GPU が実行をいつ完了するかを追跡するようにドライバーに伝えます。
- 最初の呼び出しでは、D3DGETDATA_FLUSH を指定した IDirect3DQuery9::GetData の呼び出しでコマンド バッファーが強制的に空にされるため、コマンド バッファーは空になります。後続の呼び出しでは、GPU をチェックしてすべてのコマンドバッファーの作業の処理がいつ終了するかを確認します。このループでは、GPU がアイドル状態となるまで S_OK を返しません。
- 開始時間をサンプリングします。
- プロファイルする API 呼び出しを呼び出します。
- 2 番目のクエリ イベント マーカーをコマンド バッファーに追加します。このマーカーは呼び出しの完了の追跡に使用されます。
- 最初の呼び出しでは、D3DGETDATA_FLUSH を指定した IDirect3DQuery9::GetData の呼び出しでコマンド バッファーが強制的に空にされるため、コマンド バッファーは空になります。GPU がすべてのコマンドバッファー作業の処理を完了すると、IDirect3DQuery9::GetData は S_OK を返し、GPU はアイドル状態であるためループは終了します。
- 停止時間をサンプリングします。
QueryPerformanceCounter と QueryPerformanceFrequency で測定された結果を次に示します。
ローカル変数 | ティック数 |
---|---|
start | 1792998845060 |
stop | 1792998845090 |
freq | 3579545 |
ティックを再びサイクルに変換 (2 GHz マシンで):
# ticks = (stop - start) = 1792998845090 - 1792998845060 = 30 ticks # cycles = CPU speed * number of ticks / QPF # 16,450 = 2 GHz * 30 / 3,579,545
呼び出しごとのサイクル数の詳細は次のとおりです。
Number of cycles for SetTexture : 100 Number of cycles for DrawPrimitive : 900 Number of cycles for Issue : 200 Number of cycles for GetData : 16,450
クエリ メカニズムによって、測定されるランタイムとドライバーの作業を制御できます。これらの各値を理解できるように、次の、API 呼び出しそれぞれへの応答時に何が発生するかを、予測されるタイミングとともに示します。
最初の呼び出しでは、D3DGETDATA_FLUSH を指定して IDirect3DQuery9::GetData を呼び出すことでコマンド バッファーを空にします。GPU がすべてのコマンドバッファー作業の処理を完了すると、IDirect3DQuery9::GetData は S_OK を返し、GPU はアイドル状態であるためループは終了します。
レンダリング シーケンスは、IDirect3DDevice9::SetTexture をデバイス非依存のフォーマットに変換して、これをコマンド バッファーに追加することで完了します。これには約 100 サイクルかかるとします。
IDirect3DDevice9::DrawPrimitive が変換され、コマンド バッファーに追加されます。これには約 900 サイクルかかるとします。
IDirect3DQuery9::Issue がクエリ マーカーをコマンド バッファーに追加します。これには約 200 サイクルかかるとします。
IDirect3DQuery9::GetData によってコマンド バッファーは空になります。これによってカーネルモードの移行が強制されます。これには約 5000 サイクルかかるとします。
次に、ドライバーは 4 つの呼び出しすべてに関連付けられた作業を処理します。IDirect3DDevice9::SetTexture を処理するドライバー時間は約 2964 サイクルで、IDirect3DDevice9::DrawPrimitive は約 3600 サイクル、IDirect3DQuery9::Issue は 約 200 サイクルとします。よって、4 つのコマンドすべての合計ドライバー時間は約 6450 サイクルです。
注 また、ドライバーは GPU のステータスを確認するのに若干時間がかかります。GPU 作業はわずかであるため、GPU はすでに実行済みです。IDirect3DQuery9::GetData は GPU が完了している可能性に基づいて S_OK を返します。
ドライバーがその作業を終了すると、ユーザーモードの移行によって、ランタイムに制御が返ります。これでコマンド バッファーは空になります。これには約 5000 サイクルかかるとします。
IDirect3DQuery9::GetData の主な数値は次のとおりです。
GetData = kernel-transition + driver work + user-transition GetData = 5000 + 6450 + 5000 GetData = 16,450 driver work = SetTexture + DrawPrimitive + Issue = driver work = 2964 + 3600 + 200 = 6450 cycles
QueryPerformanceCounter と組み合わせて使用されるクエリ メカニズムでは、CPU 作業がすべて測定されます。これは、クエリ マーカー、およびクエリ ステータス比較を組み合わせて実行されます。コマンド バッファーに追加される開始と停止のクエリ マーカーは、バッファー内の作業量を制御するために使用されます。正しいリターン コードが返されるまで待機することで、開始測定はクリーンなレンダリング シーケンスが開始される直前に実行されます。また、停止測定はコマンド バッファーの内容に関連付けられている作業をドライバーが完了した直後に実行されます。これは、ランタイム同様にドライバーによって実行される CPU 作業を効率的にキャプチャーします。
ここまでは、コマンド バッファー、およびそれがプロファイリングに持つことができるエフェクトについて説明しました。次に、ランタイムによってコマンド バッファーが空になることがあるその他の状態をいくつか説明します。これらはレンダリング シーケンスで確認する必要があります。これらの状態では、API 呼び出しに応答するものもあれば、ランタイムのリソース変更に応答するものもあります。以下の状態はいずれもモードの移行を発生させます。
- ロック メソッドの 1 つ (IDirect3DVertexBuffer9::Lock) が頂点バッファー、インデックス バッファー、またはテクスチャー (特定のフラグのある特定の条件下で) で呼び出される場合。
- デバイス、または頂点バッファー、インデックス バッファー、テクスチャーのいずれかが作成される場合。
- デバイス、または頂点バッファー、インデックス バッファー、テクスチャーのいずれかが最後の解放で破棄される場合。
- IDirect3DDevice9::ValidateDevice が呼び出される場合。
- IDirect3DDevice9::Present が呼び出される場合。
- コマンド バッファーがいっぱいになった場合。
- IDirect3DQuery9::GetData が D3DGETDATA_FLUSH を設定して呼び出される場合。
これらの状態をレンダリング シーケンスで慎重に確認してください。モードの移行が追加されるたびに、ドライバーの作業の 10,000 サイクルがプロファイリング測定に追加されます。さらに、コマンド バッファーは静的なサイズを持ちません。ランタイムはアプリケーションによって生成される作業量に応じてバッファーのサイズを変更することがあります。これは、レンダリング シーケンスに依存するもう 1 つの最適化です。
そのため、プロファイリング中にはモードの移行を慎重に制御してください。クエリ メカニズムは、モードの移行のタイミング、およびバッファーが含む作業量を制御できるように、コマンド バッファーを空にする堅牢な方法を提供します。ただし、モード移行時間を削減して測定結果に影響しないようにすることで、このテクニックはさらに改善できます。
モードの移行と比較してレンダリング シーケンスを大きくする
前の例では、カーネルモードのスイッチとユーザーモードのスイッチは、ランタイムとドライバーの作業と関係なく約 10,000 サイクルを消費します。モードの移行は、オペレーティング システムに組み込まれているため、ゼロに減らすことはできません。モードの移行の影響最小限にするには、レンダリング シーケンスを調整して、ドライバーとランタイム作業がモード スイッチよりも格段に大きくなるようにします。減算して移行を削除することはできますが、はるかに大きいレンダリング シーケンスのコストの償却はより信頼できます。
モードの移行が影響力をもたなくなるなるまで、モードの移行を減らすには、レンダリング シーケンスにループを追加すると効果的です。たとえば、レンダリング シーケンスを 1500 回繰り返すループが追加される場合のプロファイリング結果を見てみましょう。
// Initialize the array with two textures, same size, same format IDirect3DTexture* texArray[2]; CreateQuery(D3DQUERYTYPE_EVENT, pEvent); pEvent->Issue(D3DISSUE_END); while(S_FALSE == pEvent->GetData( NULL, 0, D3DGETDATA_FLUSH )) ; LARGE_INTEGER start, stop; // Now start counting because the video card is ready QueryPerformanceCounter(&start); // Add a loop to the render sequence for(int i = 0; i < 1500; i++) { SetTexture(taxArray[i%2]); DrawPrimitive(D3DPT_TRIANGLELIST, i*3, 1); } pEvent->Issue(D3DISSUE_END); while(S_FALSE == pEvent->GetData( NULL, 0, D3DGETDATA_FLUSH )) ; QueryPerformanceCounter(&stop);
例 4 : レンダリング シーケンスにループを追加
QueryPerformanceCounter と QueryPerformanceFrequency で測定された結果を次に示します。
ローカル変数 | ティック数 |
---|---|
start | 1792998845000 |
stop | 1792998847084 |
freq | 3579545 |
ここで QueryPerformanceCounter を使用して 2,840 テイックを測定します。既に説明しましたが、ティックは次のようにサイクルに変換します。
# ticks = (stop - start) = 1792998847084 - 1792998845000 = 2840 ticks # cycles = machine speed * number of ticks / QPF # 6,900,000 = 2 GHz * 2840 / 3,579,545
言い換えれば、レンダリング ループで 1500 の呼び出しを処理するには、この 2 GHz マシンで約 690 万サイクルかかります。690 万サイクルでは、モード移行の時間は、約 10,000 であるため、プロファイルの結果は IDirect3DDevice9::SetTexture と IDirect3DDevice9::DrawPrimitive で関連付けられたほぼすべての測定作業です。
このコード例では、2 つのクスチャの配列が必要です。IDirect3DDevice9::SetTexture が呼び出されるたびに同じテクスチャー ポインターを設定する場合、これを削除するランタイム最適化を避けるために、単純に 2 つのテクスチャーの配列を使用してください。これによって、ループを介するたびに、テクスチャー ポインターが変更され、IDirect3DDevice9::SetTexture に関連付けられた全作業が実行されます。テクスチャーによって他の状態が変わらないように、両方のテクスチャーのサイズとフォーマットが同じであることを確認してください。
Direct3D をプロファイリングするテクニックは以上です。このテクニックは、CPU の作業処理に必要なティック数を記録する高パフォーマンス カウンター (QueryPerformanceCounter) に依存します。クエリ メカニズムを使用して API 呼び出しに関連付けられたランタイムとドライバーの作業となるように作業は慎重に制御されます。クエリは 2 つの制御手段を提供します。1 番目はレンダリング シーケンスが開始される前にコマンド バッファーを空にすること、2 番目は、GPU 作業が完了したときに返すことです。
ここまでは、レンダリング シーケンスをプロファイリングする方法について説明しました。どのレンダリング シーケンスも 1 つの IDirect3DDevice9::DrawPrimitive 呼び出しと IDirect3DDevice9::SetTexture 呼び出しを含むものでいたって単純でした。単純なものを取り上げたのは、コマンド バッファー、およびそれを制御するクエリ メカニズムの使用に焦点を当てるためでした。次に、任意のレンダリング シーケンスをプロファイリングする方法をまとめます。
- QueryPerformanceCounter のような高パフォーマンス カウンターを使用して各 API 呼び出しの処理にかかる時間を測定します。QueryPerformanceFrequency および CPU クロック速度を使用してこれを API 呼び出しごとの CPU サイクルの数に変換します。
- 各三角形が 1 つのピクセルを含む、三角形のリストをレンダリングすることで GPU 作業の量を最小限に抑えます。
- レンダリング シーケンスの前にクエリ メカニズムを使用してコマンド バッファーを空にします。これによって、プロファイリングではレンダリング シーケンスに関連付けられたランタイムとドライバーの正しい作業量が確実にキャプチャーされます。
- クエリ イベント マーカーによってコマンド バッファーに追加された作業量を制御します。この同じクエリは、GPU がその作業をいつ終了するかを検出します。GPU 作業はわずかであるため、これは実質的にはドライバー作業の完了時を測定することに相当します。
これらのテクニックはすべてプロファイル ステートの変更に使用されます。コマンド バッファーを制御する方法を読んで理解し、IDirect3DDevice9::DrawPrimitive で完全なベースライン測定に成功すれば (この最適化の問題 - に注意してください)、レンダリング シーケンスにステート変更を追加できます。ステート変更をレンダリング シーケンスに追加する際には、プロファイリング上の問題がいくつかあります。レンダリング シーケンスにステート変更を追加する場合は、必ず次のセクションに進んでください。
Direct3D ステートの変更のプロファイリング
Direct3D は、多くのレンダリング ステートを使用してパイプラインのほとんどすべての側面を制御します。ステートの変更を発生させる API は、Draw*Primitive 呼び出し以外のあらゆる関数またはメソッドを含みます。
レンダリングなしではステートの変更のコストを確認できないことがあるため、ステートの変更は注意が必要です。これは、絶対に実行しなければならないときまで作業を遅らせるためにドライバーと GPU が使用する遅延アルゴリズムによるものです。一般に、1 つのステートの変更を測定するには、次の手順に従ってください。
- 最初に IDirect3DDevice9::DrawPrimitive をプロファイルします。
- 1 つのステートの変更をレンダリング シーケンスに追加して、新しいシーケンスをプロファイルします。
- 2 つのシーケンス間の差を減算してステートの変更のコストを取得します。
当然ながら、クエリ メカニズムの使用について、およびレンダリング シーケンスをループに入れてモード移行のコストをなくすことについてこれまで学んだことはすべて適用されます。
単純なステートの変更のプロファイリング
IDirect3DDevice9::DrawPrimitive を含むレンダリング シーケンスから始めます。次に、IDirect3DDevice9::SetTexture の追加コストの測定に関するコード シーケンスを示します。
// Get the start counter value as shown in Example 4 // Initialize a texture array as shown in Example 4 IDirect3DTexture* texArray[2]; // Render sequence loop for(int i = 0; i < 1500; i++) { SetTexture(0, texArray[i%2]; // Force the state change to propogate to the GPU DrawPrimitive(D3DPT_TRIANGLELIST, i*3, 1); } // Get the stop counter value as shown in Example 4
例 5 : 1 つのステート変更の API 呼び出しの測定
ループは IDirect3DDevice9::SetTexture と IDirect3DDevice9::DrawPrimitive の 2 つの呼び出しを含むことに注目してください。レンダリング シーケンスは 1500 回ループし、次のような結果を生成します。
ローカル変数 | ティック数 |
---|---|
start | 1792998860000 |
stop | 1792998870260 |
freq | 3579545 |
ティックを再びサイクルに変換すると、次の結果が得られます。
# ticks = (stop - start) = 1792998870260 - 1792998860000 = 10,260 ticks # cycles = machine speed * number of ticks / QPF 5,775,000 = 2 GHz * 10,260 / 3,579,545
ループ内の反復数を乗算すると、次の結果が得られます。
5,775,000 cycles / 1500 iterations = 3850 cycles for one iteration
ループの反復はそれぞれステートの変更と描画呼び出しを含みます。IDirect3DDevice9::DrawPrimitive レンダリング シーケンスの結果を減算すると、次が残ります。
3850 - 1100 = 2750 cycles for SetTexture
これは、IDirect3DDevice9::SetTexture をこのレンダリング シーケンスに追加する平均サイクル数です。これと同じテクニックを他のステートの変更にも適用できます。
IDirect3DDevice9::SetTexture が単純なステートの変更を呼び出すのはなぜでしょうか。設定されるステートには制約があるため、ステートが変更されるたびに、パイプラインは同じ量の作業を実行します。両方のテクスチャーを同じサイズとフォーマットに制限することで、IDirect3DDevice9::SetTexture 呼び出しごとに同じ量の作業が実行されます。
切り替えが必要なステート変更のプロファイリング
グラフィック パイプラインによって実行される作業量をレンダリング ループの反復ごとに変化させるステートの変更はほかにもあります。たとえば、Z テストが有効な場合、各ピクセル カラーは、新しいピクセルの Z 値が既存のピクセルの Z 値に対してテストされた後のみレンダー ターゲットを更新します。Z テストが無効になっている場合、このピクセル単位のテストは実行されず、出力はより高速に書き込まれます。Z テストのステートの有効化または無効化は、レンダリング中の作業量 (CPU と GPU による) を大幅に変更します。
IDirect3DDevice9::SetRenderState は、Z テストを有効または無効にするために特定のレンダリング ステートとステート値を必要とします。特定のステート値は実行時に評価され、どのくらいの作業が必要がが決定されます。レンダリング ループでこのステート変更を測定することは困難です。また、パイプライン ステートが切り替わるようにこれを Pre 条件で構成することも困難です。唯一の解決策は、レンダリング シーケンス中にステートの変更を切り替えることです。
たとえば、プロファイリング テクニックは次のように 2 回繰り返す必要があります。
- IDirect3DDevice9::DrawPrimitive レンダリング シーケンスをプロファイリングすることから開始します。このベースラインを呼び出します。
- ステートの変更を切り替える 2 番目のレンダリング シーケンスをプロファイルします。レンダリング シーケンスのループは次を含みます。
- ステートを "false" 状態に設定するステートの変更。
- 元のシーケンスと同じ IDirect3DDevice9::DrawPrimitive。
- ステートを "true" 状態に設定するステートの変更。
- 2 番目のステートの変更を強制的に実行する 2 番目の IDirect3DDevice9::DrawPrimitive。
- 2 つのレンダリング シーケンス間の違いを検索します。これは次の操作で行います。
- 新しいシーケンスには 2 つの IDirect3DDevice9::DrawPrimitive 呼び出しがあるため、ベースラインの IDirect3DDevice9::DrawPrimitive シーケンスを 2 で乗算します。
- 元のシーケンスから新しいシーケンスの結果を減算します。
- 結果を 2 で除算して、"false" と "true" ステートの両方の変更の平均コストを求めます。
レンダリング シーケンスで使用されたルーピング テクニックを使用する場合、変更パイプライン ステートのコストはレンダリング シーケンスの反復ごとに "true" から "false" へ (またはその逆に) 状態を切り替えることで測定する必要があります。ここでの "true" と "false" の意味は文字通りではなく、単に反対の状態に設定する必要のある状態を意味します。これによって、両方の状態変更がプロファイリング中に測定されます。もちろん、クエリ メカニズムの使用について、およびレンダリング シーケンスをループに入れてモードの移行のコストをなくすことに関してこれまで習得したことはすべて適用されます。
たとえば、Z テストをオンまたはオフに切り替えるコストを測定するコード シーケンスを以下に示します。
// Get the start counter value as shown in Example 4 // Add a loop to the render sequence for(int i = 0; i < 1500; i++) { // Precondition the pipeline state to the "false" condition SetRenderState(D3DRS_ZENABLE, FALSE); // Force the state change to propogate to the GPU DrawPrimitive(D3DPT_TRIANGLELIST, (2*i + 0)*3, 1); // Set the pipeline state to the "true" condition SetRenderState(D3DRS_ZENABLE, TRUE); // Force the state change to propogate to the GPU DrawPrimitive(D3DPT_TRIANGLELIST, (2*i + 1)*3, 1); } // Get the stop counter value as shown in Example 4
例 5 : ステート変更の切り替えの測定
ループは、2 つの IDirect3DDevice9::SetRenderState 呼び出しを実行することでステートを切り替えます。最初の IDirect3DDevice9::SetRenderState 呼び出しは Z テストを無効にして、2 つめの IDirect3DDevice9::SetRenderState は Z テストを有効にします。ステートの変更に関連付けられた作業がドライバーで dirty ビットを設定するのではなく、ドライバーによって処理されるように、IDirect3DDevice9::SetRenderState ごとに IDirect3DDevice9::DrawPrimitive を指定します。
このレンダリング シーケンスには次の値が適切です。
ローカル変数 | ティック数 |
---|---|
start | 1792998845000 |
stop | 1792998861740 |
freq | 3579545 |
ティックを再びサイクルに変換すると、次の結果が得られます。
# ticks = (stop - start) = 1792998861740 - 1792998845000 = 15,120 ticks # cycles = machine speed * number of ticks / QPF 9,300,000 = 2 GHz * 16,740 / 3,579,545
ループ内の反復数を乗算すると、次の結果が得られます。
9,300,000 cycles / 1500 iterations = 6200 cycles for one iteration
ループの反復はそれぞれ 2 つのステートの変更と 2 つの描画呼び出しを含みます。描画呼び出し (1100 サイクルとします) を減算すると、次が残ります。
6200 - 1100 - 1100 = 4000 cycles for both state changes
これは、両方の状態の変更の平均サイクル数であるため、各ステートの変更の平均時間は次のようになります。
4000 / 2 = 2000 cycles for each state change
したがって、Z テストを有効または無効にするサイクルの平均数は 2000 サイクルです。QueryPerformanceCounter が Z 有効ハーフ時間と Z 無効ハーフ時間を測定する意味はありません。このテクニックでは実際には両方のステートの変更の平均が測定されます。言い換えれば、ステートを切り替える時間を測定することになります。このテクニックを使用した場合、有効な時間と無効な時間の両方の平均を測定しているため、有効な時間と無効な時間が同じかどうかを知る方法はありません。ただし、これは切り替えステートをアプリケーションとして割り当てる場合には適切な値として使用できます。このステート変更を発生させるアプリケーションは、このステートを切り替えることによってのみこれを実行できるためです。
そのため、ここではこれらのテクニックを適用して、必要なすべてのステートの変更をプロファイルできると考えてよいでしょうか。そうではありません。やはり、実行が必要な作業量を減らすように設計されている最適化については慎重になる必要があります。レンダリング シーケンスを設計する場合には、認識しておくべき最適化は 2 種類あります。
ステートの変更の最適化に関する注意
前のセクションでは、反復ごとに同じ量の作業を生成するように制約された単純なステートの変更、および実行される作業量を大幅に変更するステート変更の切り替えという、2 種類のステートの変更をプロファイルする方法を示しました。前述のレンダリング シーケンスに別のステートの変更を追加すると、何が起きるでしょうか。たとえば、この例では、z>-enable レンダリング シーケンスをとり、z-func 比較をこれに追加します。
// Add a loop to the render sequence for(int i = 0; i < 1500; i++) { // Precondition the pipeline state to the opposite condition SetRenderState(D3DRS_ZFUNC, D3DCMP_NEVER); // Precondition the pipeline state to the opposite condition SetRenderState(D3DRS_ZENABLE, FALSE); // Force the state change to propogate to the GPU DrawPrimitive(D3DPT_TRIANGLELIST, (2*i + 0)*3, 1); // Now set the state change you want to measure SetRenderState(D3DRS_ZFUNC, D3DCMP_ALWAYS); // Now set the state change you want to measure SetRenderState(D3DRS_ZENABLE, TRUE); // Force the state change to propogate to the GPU DrawPrimitive(D3DPT_TRIANGLELIST, (2*i + 1)*3, 1); }
z-func ステートは、Z バッファーへの書き込み時に比較レベルを設定します (現在のピクセルの Z 値と、深度バッファーのピクセルの Z 値の間)。D3DCMP_ALWAYS が Z テストごとに実行されるように比較を設定していても、D3DCMP_NEVER は Z テスト比較をオフにします。
レンダリング シーケンスでのこれらのいずれかのステートの変更を IDirect3DDevice9::DrawPrimitive を指定してプロファイリングすると、次のような結果が生成されます。
単一のステートの変更 | サイクルの平均数 |
---|---|
D3DRS_ZENABLE のみ | 2000 |
または
単一のステートの変更 | サイクルの平均数 |
---|---|
D3DRS_ZFUNC のみ | 600 |
ただし、D3DRS_ZENABLE と D3DRS_ZFUNC の両方を同じレンダリング シーケンスでプロファイルした場合、次のような結果が生成されます。
両方のステージの変更 | サイクルの平均数 |
---|---|
D3DRS_ZENABLE + D3DRS_ZFUNC | 2000 |
ドライバーは両方のレンダリング ステートの設定に関連付けられたすべての作業を実行するため、結果は 2000 と 600 (または 2600) サイクルと予測できます。その場合、平均は 2000 サイクルです。
この結果は、ランタイム、ドライバー、または GPU に実装されているステートの変更の最適化を反映しています。この場合、ドライバーは最初の IDirect3DDevice9::SetRenderState を確認して、しばらく後に作業を延期する dirty ステートを設定できます。ドライバーが 2 番目の IDirect3DDevice9::SetRenderState を確認すると、同じ dirty ステートを冗長的に設定でき、同じ作業が再び延期されます。IDirect3DDevice9::DrawPrimitive が呼び出されると、dirty ステートに関連付けられた作業が最終的に処理されます。ドライバーは作業を一度だけ実行します。これは最初の 2 つのステートの変更がドライバーによって効率的に統合されることを意味します。同様に、2 番目の IDirect3DDevice9::DrawPrimitive が呼び出されると、3 番目と 4 番目のステートの変更がドライバーによって効率的に 1 つのステートの変更に統合されます。結果的に、ドライバーと GPU は描画呼び出しごとにステートの変更を 1 つ処理します。
これは、シーケンスに依存したドライバーの最適化の好例です。dirty ステートを設定することでドライバーは作業を 2 回延期し、dirty ステートをクリアするために作業を 1 度だけ実行しました。これは、作業を絶対に必要となるまで延期される場合に実行できる、ある種の効率的な改善の好例です。
どのステートの変更が内部的に dirty ステートを設定して、作業を延期しているかはどのようにわかりますか。レンダリング シーケンスをテストする (またはドライバーの記述者に聞く) よりありません。ドライバーは周期的に更新され、改良されるため、最適化のリストは固定ではありません。特定のハードウェア セットで所定のレンダリング シーケンスのステート変更コストがどのくらいかを確実に知る方法は 1 つしかありません。つまり、それを測定するというものです。
DrawPrimitive 最適化に関する注意
ステート変更の最適化に加えて、ランタイムはドライバーが処理しなければならない描画呼び出しの数を最適化しようとします。たとえば、これらを連続した描画呼び出しであると考えてみましょう。
DrawPrimitive(D3DPT_TRIANGLELIST, 0, 3); // Draw 3 primitives, vertices 0 - 8 DrawPrimitive(D3DPT_TRIANGLELIST, 9, 4); // Draw 4 primitives, vertices 9 - 20
例 5a:2 つの描画呼び出し
このシーケンスは、ランタイムが次のような 1 つの呼び出しに統合する 2 つの描画呼び出しを含みます。
DrawPrimitive(D3DPT_TRIANGLELIST, 0, 7); // Draw 7 primitives, vertices 0 - 20
例 5b:1 つの連結された描画呼び出し
ランタイムは、これらの特定の描画呼び出しの両方を 1 つの呼び出しに連結します。これによって、ドライバーは 1 つの描画呼び出しだけを処理すればよくなるため、ドライバーの作業が半分に削減されます。
一般に、次のような場合に、ランタイムは 2 つ以上の連続する IDirect3DDevice9::DrawPrimitive 呼び出しを連結します。
- プリミティブ タイプがトライアングル リストである (D3DPT_TRIANGLELIST)。
- 各連続する IDirect3DDevice9::DrawPrimitive 呼び出しが頂点バッファー内の連続する頂点を参照しなければならない。
同様に、2 つ以上の連続する IDirect3DDevice9::DrawIndexedPrimitive 呼び出しを連結するための適切な条件は次のとおりです。
- プリミティブ タイプがトライアングル リストである (D3DPT_TRIANGLELIST)。
- 各連続する IDirect3DDevice9::DrawIndexedPrimitive 呼び出しが頂点バッファー内の連続するインデックスを参照しなければならない。
- 各連続する IDirect3DDevice9::DrawIndexedPrimitive 呼び出しが BaseVertexIndex に同じ値を使用しなければならない。
プロファイリング中の連結を防ぐには、プリミティブ タイプがトライアングル リストではないようにレンダリング シーケンスを変更するか、連続する頂点 (またはインデックス) を使用する連続する描画呼び出しが存在しないようにレンダリング シーケンスを変更する必要があります。具体的には、ランタイムは次の両方の条件を満たす描画呼び出しも連結します。
- 前の呼び出しが IDirect3DDevice9::DrawPrimitive であるとき、次の描画呼び出しが以下の条件を満たす場合
- トライアングル リストを使用し、かつ
- StartVertex = previous StartVertex + previous PrimitiveCount * 3 を指定する
- IDirect3DDevice9::DrawIndexedPrimitive を使用しているとき、次の描画呼び出しが以下の条件を満たす場合
- トライアングル リストを使用し、かつ
- StartIndex = previous StartIndex + previous PrimitiveCount * 3 を指定し、かつ
- BaseVertexIndex = previous BaseVertexIndex を指定する
プロファイリング時に見落としやすい、より複雑な描画呼び出しの連結があります。レンダリング シーケンスが以下のように表示されるとします。
for(int i = 0; i < 1500; i++) { SetTexture(...); DrawPrimitive(D3DPT_TRIANGLELIST, i*3, 1); }
例 5c:1 つのステートの変更と 1 つの描画呼び出し
ループは、1500 個の三角形を反復し、三角形ごとにテクスチャーと描画を設定します。前のセクションで説明したとおり、このレンダリングでは、IDirect3DDevice9::SetTexture に対して約 2750 サイクル、IDirect3DDevice9::DrawPrimitive に対して 1100 サイクルを処理します。レンダリング ループ外に IDirect3DDevice9::SetTexture を移動することで、ドライバーの作業が 1500 * 1750 サイクル減少すると直感的に予測されるかもしれません。これは IDirect3DDevice9::SetTexture を 1500 回呼び出すことに関連する作業量です。コードは次のようになります。
SetTexture(...); // Set the state outside the loop for(int i = 0; i < 1500; i++) { // SetTexture(...); DrawPrimitive(D3DPT_TRIANGLELIST, i*3, 1); }
例 5d:ループ外へのステートの変更による例 5c
IDirect3DDevice9::SetTexture をレンダリング ループ外に移動すると、IDirect3DDevice9::SetTexture は 1500 回ではなく 1 度呼び出されるため、これに関連付けられた作業量が減ります。また、目立ちませんが、IDirect3DDevice9::DrawPrimitive の作業も 1500 回の呼び出しから 1 回の呼び出しに減るという副次的効果もあります。これは描画呼び出しを連結することの条件がすべて満たされるためです。レンダリング シーケンスが処理されるとき、ランタイムは 1500 回の呼び出しを 1 回のドライバー呼び出しで処理します。この 1 行のコードを移動することで、ドライバーの作業量は劇的に削減されています。
total work done = runtime + driver work Example 5c: with SetTexture in the loop: runtime work = 1500 SetTextures + 1500 DrawPrimitives driver work = 1500 SetTextures + 1500 DrawPrimitives Example 5d: with SetTexture outside of the loop: runtime work = 1 SetTexture + 1 DrawPrimitive + 1499 Concatenated DrawPrimitives driver work = 1 SetTexture + 1 DrawPrimitive
これらの結果は完全に正しいものですが、最初の質問に照らすと非常に紛らわしいものです。描画呼び出しの最適化によってドライバーの作業は劇的に削減されました。これは、カスタム プロファイリング時の一般的な問題です。レンダリング シーケンスから呼び出しを削除する場合には、描画呼び出しの連結を避けるように注意してください。実際に、このシナリオから、このランタイムの最適化によってドライバーのパフォーマンスがどのくらい改善されるかがよくわかります。
ステートの変更の測定方法は以上です。IDirect3DDevice9::DrawPrimitive のプロファイリングから始めてください。次に、各追加のステートの状態をシーケンスに追加し (1 つの呼び出しを追加する場合もあれば、2 つの呼び出しを追加することもあります)、2 つのシーケンス間の違いを測定します。この結果をティック、つまりサイクルまたは時間に変換できます。QueryPerformanceCounter を使用してレンダリング シーケンスを測定するのと同じように、個々のステートの変更の測定は、コマンド バッファーを制御するクエリ メカニズムに依存し、ステートの変更をループに入れることでモード移行の影響を最小限に抑えます。このテクニックでは、ステートの切り替えコストが測定されます。プロファイラーがステートの有効と無効の平均を返すためです。
この機能では、任意のレンダリング シーケンスの生成、および関連付けられているランタイムとドライバー作業の正確な測定を開始できます。次に測定値を使用して、CPU が制約されたシナリオを想定し、"適切なフレーム レートを保持しながら、レンダリング シーケンスでこれらの呼び出しをさらにいくつ行うことができますか" といった割り当ての質問に答えることができます。
まとめ
ここでは、個々の呼び出しを正確にプロファイルできるように、コマンド バッファーを制御する方法について説明しました。プロファイリングの値は、ティック、サイクル、または絶対時間単位で生成できます。これらは、各 API 呼び出しに関連付けられているランタイムとドライバーの作業量を表します。
レンダリング シーケンスで Draw*Primitive の呼び出しをプロファイルすることから開始します。次のことを忘れないでください。
- QueryPerformanceCounter を使用して API 呼び出しあたりのティック数を測定する。サイクルまたは時間に結果を変換したい場合は、QueryPerformanceFrequency を使用してください。
- 始める前に、クエリ メカニズムを使用してコマンド バッファーを空にする。
- モード移行の影響を最小限に抑えるためにレンダリング シーケンスをループに入れる。
- クエリ メカニズムを使用して GPU がその作業をいつ完了するかを測定する。
- 実行される作業量に大きく影響するランタイム連結に注意する。
これによって、フォームの構築に利用できる IDirect3DDevice9::DrawPrimitive のベースライン パフォーマンスが得られます。1 つのステートの変更をプロファイルするには、さらに以下のヒントに従ってください。
- ステートの変更を既知のレンダリング シーケンスに追加して、新しいシーケンスをプロファイルする。テストはループで実行されるため、反対の値 (たとえば有効または無効のように) にステートを 2 回設定する必要があります。
- 2 つのシーケンス間でのサイクル時間の差を比較する。
- パイプラインを大幅に変更するステートの変更については (IDirect3DDevice9::SetTexture のように)、2 つのシーケンス間の差を計算して、ステートの変更の時間を求める。
- パイプラインを大幅に変更する (従って IDirect3DDevice9::SetRenderState などのステート切り替えが必要) ステートについては、レンダリング シーケンス間の差を求め、2 で割る。これによってステートの変更ごとの平均サイクル時間が生成されます。
ただし、プロファイリング時に予期しない結果を発生させる最適化には十分に注意してください。ステートの変更の最適化によって、作業を遅延させる dirty 状態が設定されることがあります。これによって、期待に反してわかりにくいプロファイル結果が発生することがあります。描画呼び出しを連結すると、ドライバーの作業が大幅に削減され、誤った結論につながることがあります。ステートの変更と描画呼び出しの連結の発生を防ぐため、計画済みのレンダリング シーケンスの使用には注意してください。生成した数値が適切な割り当て数であるため、プロファイリング中に最適化が発生しないようにすることが秘訣です。
注 クエリ メカニズムを使用せずに、アプリケーションでこのプロファイリング戦略を複製することは、より困難です。Direct3D 9 の前に、コマンド バッファーを空にする唯一の予想可能な方法とは、アクティブなサーフェス (レンダー ターゲットなど) をロックして、GPU がアイドル状態となるまで待つことです。これは、GPU が完了するのを待機するのに加えて、ロックされる前にサーフェスを更新するレンダリング コマンドが、バッファー内に存在する場合には、サーフェスをロックすると、ランタイムがコマンド バッファーを空にするためです。このテクニックは有効ですが、Direct3D 9 に導入されているクエリ メカニズムを使用することの方が一般的です。
付録
この表の数値は、これらのステートの変更それぞれに関連付けられたランタイムとドライバー作業の量に対する近似の範囲です。近似値は、ここに示されたテクニックを使用してドライバーで行われた実際の測定に基づきます。これらの数値は Direct3D 9 を使用して生成され、ドライバーに依存しています。
このドキュメントのテクニックは、ランタイムとドライバーの作業を測定することを目的としています。一般に、すべてのアプリケーションで CPU と GPU のパフォーマンスが一致する結果を得ることは事実上不可能です。これは膨大なレンダリング シーケンスの包括的な配列が必要となるためです。さらに、GPU は、レンダリング シーケンスの前のパイプラインでのステート設定にかなり依存しているため、GPU のパフォーマンスをベンチマークすることはとりわけ困難です。たとえば、アルファ ブレンディングを有効にしても必要な CPU 作業量にはほとんど影響はありませんが、GPU によって処理される作業量にはかなり影響することがあります。したがって、ここでのテクニックでは、レンダリングが必要なデータ量を制限することで GPU の作業を最小限の量に制限します。つまり、表の数値は、CPU に制約のあるアプリケーション (GPU によって制限されたアプリケーションとは異なり) から得られた結果と最も近いことになります。
開発者それぞれに最も重要なシナリオと設定をカバーするテクニックを使用することをお勧めします。表の値は、生成した数値との比較に使用できます。ドライバーごとに異なるため、実際に表示される数値は、それぞれのシナリオを使ってプロファイリング結果を生成して確認するよりありません。
API の呼び出し | サイクルの平均数 |
---|---|
SetVertexDeclaration | 6500 - 11250 |
SetFVF | 6400 - 11200 |
SetVertexShader | 3000 - 12100 |
SetPixelShader | 6300 - 7000 |
SPECULARENABLE | 1900 - 11200 |
SetRenderTarget | 6000 - 6250 |
SetPixelShaderConstant (1 Constant) | 1500 - 9000 |
NORMALIZENORMALS | 2200 - 8100 |
LightEnable | 1300 - 9000 |
SetStreamSource | 3700 - 5800 |
LIGHTING | 1700 - 7500 |
DIFFUSEMATERIALSOURCE | 900 - 8300 |
AMBIENTMATERIALSOURCE | 900 - 8200 |
COLORVERTEX | 800 - 7800 |
SetLight | 2200 - 5100 |
SetTransform | 3200 - 3750 |
SetIndices | 900 - 5600 |
AMBIENT | 1150 - 4800 |
SetTexture | 2500 - 3100 |
SPECULARMATERIALSOURCE | 900 - 4600 |
EMISSIVEMATERIALSOURCE | 900 - 4500 |
SetMaterial | 1000 - 3700 |
ZENABLE | 700 - 3900 |
WRAP0 | 1600 - 2700 |
MINFILTER | 1700 - 2500 |
MAGFILTER | 1700 - 2400 |
SetVertexShaderConstant (1 Constant) | 1000 - 2700 |
COLOROP | 1500 - 2100 |
COLORARG2 | 1300 - 2000 |
COLORARG1 | 1300 - 1980 |
CULLMODE | 500 - 2570 |
CLIPPING | 500 - 2550 |
DrawIndexedPrimitive | 1200 - 1400 |
ADDRESSV | 1090 - 1500 |
ADDRESSU | 1070 - 1500 |
DrawPrimitive | 1050 - 1150 |
SRGBTEXTURE | 150 - 1500 |
STENCILMASK | 570 - 700 |
STENCILZFAIL | 500 - 800 |
STENCILREF | 550 - 700 |
ALPHABLENDENABLE | 550 - 700 |
STENCILFUNC | 560 - 680 |
STENCILWRITEMASK | 520 - 700 |
STENCILFAIL | 500 - 750 |
ZFUNC | 510 - 700 |
ZWRITEENABLE | 520 - 680 |
STENCILENABLE | 540 - 650 |
STENCILPASS | 560 - 630 |
SRCBLEND | 500 - 685 |
Two_Sided_StencilMODE | 450 - 590 |
ALPHATESTENABLE | 470 - 525 |
ALPHAREF | 460 - 530 |
ALPHAFUNC | 450 - 540 |
DESTBLEND | 475 - 510 |
COLORWRITEENABLE | 465 - 515 |
CCW_STENCILFAIL | 340 - 560 |
CCW_STENCILPASS | 340 - 545 |
CCW_STENCILZFAIL | 330 - 495 |
SCISSORTESTENABLE | 375 - 440 |
CCW_STENCILFUNC | 250 - 480 |
SetScissorRect | 150 - 340 |