Direct3D API 呼び出しの正確なプロファイリング (Direct3D 9)

Microsoft Direct3D アプリケーションが機能し、そのパフォーマンスを向上させる場合は、通常、既製のプロファイリング ツールまたはカスタム測定手法を使用して、1 つ以上のアプリケーション プログラミング インターフェイス (API) 呼び出しの実行にかかる時間を測定します。 これを行っても、レンダリング シーケンスによって異なるタイミング結果が得られている場合、または実際の実験結果まで保持されない仮説を立てている場合は、次の情報が理由を理解するのに役立つ場合があります。

ここで提供される情報は、以下に関する知識と経験があることを前提にしています。

  • C/C++ プログラミング
  • Direct3D API プログラミング
  • API のタイミングの測定
  • ビデオ カードとそのソフトウェア ドライバー
  • 以前のプロファイリング エクスペリエンスで説明できない可能性のある結果

Direct3D のプロファイリングを正確に行うのが難しい

プロファイラーは、各 API 呼び出しに費やされた時間について報告します。 これは、ホット スポットを見つけてチューニングすることでパフォーマンスを向上させるために行われます。 プロファイラーとプロファイリング手法にはさまざまな種類があります。

  • サンプリング プロファイラーは、実行されている関数をサンプリング (または記録) するために特定の間隔で起動し、アイドル状態になります。 各呼び出しに費やされた時間の割合を返します。 一般に、サンプリング プロファイラーはアプリケーションに対してあまり侵入的ではなく、アプリケーションのオーバーヘッドへの影響を最小限に抑えます。
  • インストルメント化プロファイラーは、呼び出しが返されるまでの実際の時間を測定します。 開始区切り記号と停止区切り記号をアプリケーションにコンパイルする必要があります。 インストルメント化プロファイラーは、サンプリング プロファイラーよりもアプリケーションに対して比較的侵襲的です。
  • また、高パフォーマンス タイマーを使用してカスタム プロファイリング手法を使用することもできます。 これにより、インストルメント化プロファイラーと非常によく似た結果が生成されます。

使用されるプロファイラーまたはプロファイリング手法の種類は、正確な測定を生成する課題の一部にすぎません。

プロファイリングでは、予算のパフォーマンスに役立つ回答が得られます。 たとえば、API 呼び出しで実行するクロック サイクルが平均 1,000 回であることがわかっているとします。 パフォーマンスに関して、次のような結論を出すことができます。

  • 2 GHz CPU (レンダリング時間の 50% を費やす) は、この API を 1 秒に 100 万回呼び出すことに制限されます。
  • 1 秒あたり 30 フレームを実現するには、この API をフレームあたり 33,000 回以上呼び出すことはできません。
  • レンダリングできるのは、フレームあたり 3.3K オブジェクトのみです (各オブジェクトのレンダリング シーケンスに対してこれらの API 呼び出しのうち 10 個を想定)。

つまり、API 呼び出しごとに十分な時間がある場合は、対話形式でレンダリングできるプリミティブの数などの予算作成の質問に答えることができます。 しかし、インストルメント化プロファイラーによって返される未加工の数値は、予算作成の質問に正確に答えるものではありません。 これは、グラフィックス パイプラインには、作業を行う必要があるコンポーネントの数、コンポーネント間の作業フローを制御するプロセッサの数、ランタイムとパイプラインをより効率的にするように設計されたドライバーに実装された最適化戦略など、複雑な設計上の問題があるためです。

各 API 呼び出しは、複数のコンポーネントを経由します

各呼び出しは、アプリケーションからビデオ カードへの途中で複数のコンポーネントによって処理されます。 たとえば、1 つの三角形を描画するための 2 つの呼び出しを含む次のレンダリング シーケンスについて考えてみましょう。

SetTexture(...);
DrawPrimitive(D3DPT_TRIANGLELIST, 0, 1);

次の概念図は、呼び出しが渡す必要があるさまざまなコンポーネントを示しています。

API 呼び出しが通過するグラフィックス コンポーネントの図

アプリケーションは Direct3D を呼び出し、シーンを制御し、ユーザーの操作を処理し、レンダリングの実行方法を決定します。 この作業はすべて、Direct3D API 呼び出しを使用してランタイムに送信されるレンダリング シーケンスで指定されます。 レンダリング シーケンスは実質的にハードウェアに依存しません (つまり、API 呼び出しはハードウェアに依存しませんが、アプリケーションにはビデオ カードがサポートする機能に関する知識があります)。

ランタイムは、これらの呼び出しをデバイスに依存しない形式に変換します。 ランタイムは、アプリケーションとドライバーの間のすべての通信を処理するため、(必要な機能に応じて) 複数の互換性のあるハードウェアでアプリケーションが実行されます。 関数呼び出しを測定する場合、インストルメント化プロファイラーは、関数で費やした時間と、関数が返される時間を測定します。 インストルメント化プロファイラーの 1 つの制限事項は、ドライバーが結果の作業をビデオ カードに送信するのにかかる時間や、ビデオ カードが作業を処理する時間を含まない場合があるということです。 言い換えると、既製のインストルメントプロファイラーは、各関数呼び出しに関連付けられているすべての作業を属性化できません。

ソフトウェア ドライバーは、ビデオ カードに関するハードウェア固有の知識を使用して、デバイスに依存しないコマンドを一連のビデオ カード コマンドに変換します。 ドライバーは、ビデオ カードに送信されるコマンドのシーケンスを最適化して、ビデオ カードでのレンダリングを効率的に行うこともできます。 これらの最適化により、プロファイリングの問題が発生する可能性があります。これは、実行された作業量が表示されないためです (最適化を考慮する必要がある場合があります)。 ドライバーは通常、ビデオ カードがすべてのコマンドの処理を完了する前に、ランタイムに制御を返します。

ビデオ カードでは、頂点バッファーとインデックス バッファー、テクスチャ、レンダリング状態情報、グラフィックス コマンドのデータを組み合わせることにより、レンダリングの大部分を実行します。 ビデオ カードのレンダリングが完了すると、レンダリング シーケンスから作成された作業が完了します。

各 Direct3D API 呼び出しは、何かをレンダリングするために、各コンポーネント (ランタイム、ドライバー、ビデオ カード) によって処理される必要があります。

コンポーネントを制御するプロセッサが複数存在する

アプリケーション、ランタイム、ドライバーは 1 つのプロセッサによって制御され、ビデオ カードは別のプロセッサによって制御されるため、これらのコンポーネント間の関係はさらに複雑になります。 次の図は、中央処理装置 (CPU) とグラフィックス処理装置 (GPU) の 2 種類のプロセッサを示しています。

CPU と GPU とそのコンポーネントの図

PC システムには少なくとも 1 つの CPU と 1 つの GPU がありますが、どちらか一方または両方を複数持つことができます。 CPU はマザーボード上にあり、GPU はマザーボード上またはビデオ カード上にあります。 CPU の速度はマザーボード上のクロック チップによって決定され、GPU の速度は別のクロック チップによって決定されます。 CPU クロックは、アプリケーション、ランタイム、ドライバーによって実行される作業の速度を制御します。 アプリケーションは、ランタイムとドライバーを介して GPU に作業を送信します。

CPU と GPU は、通常、互いに関係なく、異なる速度で実行されます。 GPU は、作業が利用可能になるとすぐに作業に応答する場合があります (GPU が以前の作業の処理を完了したと仮定)。 GPU の作業は、上の図の曲線で強調表示されている CPU 作業と並行して行われます。 プロファイラーは通常、GPU ではなく CPU のパフォーマンスを測定します。 インストルメント化プロファイラーによって行われた測定には CPU 時間が含まれますが、GPU 時間が含まれていない可能性があるため、プロファイリングは困難になります。

GPU の目的は、グラフィックス作業用に特別に設計されたプロセッサに CPU から処理をオフロードすることです。 最新のビデオ カードでは、GPU は、CPU から GPU へのパイプラインでの変換と照明の作業の多くを置き換えます。 これにより、CPU ワークロードが大幅に削減され、他の処理に使用できる CPU サイクルが増えます。 ピーク パフォーマンスを得るためにグラフィカル アプリケーションを調整するには、CPU と GPU の両方のパフォーマンスを測定し、2 種類のプロセッサ間で作業のバランスを取る必要があります。

このドキュメントでは、GPU のパフォーマンスの測定や、CPU と GPU の間の作業の分散に関するトピックについては説明しません。 GPU (または特定のビデオ カード) のパフォーマンスについて理解を深めたい場合は、ベンダーの 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::D rawPrimitive では処理に約 100 万サイクルが必要なため、あまり効率的でないと結論付けることができます。 ただし、この結論が正しくない理由と、予算作成に使用できる結果を生成する方法がすぐにわかります。

状態の変更を測定するには、慎重なレンダリング シーケンスが必要です

IDirect3DDevice9::D rawPrimitiveDrawIndexedPrimitive、Clear (SetTextureSetVertexDeclarationSetRenderState など) 以外のすべての呼び出しでは、状態の変更が生成されます。 各状態変更は、レンダリングの実行方法を制御するパイプラインの状態を設定します。

ランタイムやドライバーの最適化は、必要な作業量を減らすことでレンダリングを高速化するように設計されています。 プロファイル平均を汚染する可能性があるいくつかの状態変更の最適化を次に示します。

  • ドライバー (またはランタイム) は、状態の変更をローカル状態として保存できます。 ドライバーは "遅延" アルゴリズムで動作する可能性があるため (絶対に必要になるまで作業を延期する)、一部の状態変更に関連する作業が遅れる可能性があります。
  • ランタイム (またはドライバー) は、最適化によって状態の変更を削除できます。 たとえば、照明が以前に無効にされているため、照明を無効にする冗長な状態変更を削除できます。

レンダリング シーケンスを見て、どの状態の変更がダーティビットを設定し、作業を延期するか、単に最適化によって削除されるかを結論付ける、確実な方法はありません。 今日のランタイムまたはドライバーで最適化された状態の変更を特定できたとしても、明日のランタイムまたはドライバーが更新される可能性があります。 また、以前の状態が何であったのかすぐにはわかりません。そのため、冗長な状態の変更を特定することは困難です。 状態変更のコストを確認する唯一の方法は、状態の変更を含むレンダリング シーケンスを測定することです。

ご覧のように、複数のプロセッサ、複数のコンポーネントによって処理されているコマンド、およびコンポーネントに組み込まれている最適化によって発生する複雑な問題により、プロファイリングの予測が困難になります。 次のセクションでは、これらの各プロファイリングの課題に対処します。 サンプルの Direct3D レンダリング シーケンスを、付属の測定手法と共に示します。 この知識があれば、個々の呼び出しで正確で反復可能な測定値を生成できます。

Direct3D レンダリング シーケンスを正確にプロファイリングする方法

プロファイリングの課題の一部が強調表示されたので、このセクションでは、予算作成に使用できるプロファイル測定を生成するのに役立つ手法について説明します。 CPU によって制御されるコンポーネント間の関係と、ランタイムとドライバーによって実装されるパフォーマンス最適化を回避する方法を理解している場合は、正確で反復可能なプロファイリング測定が可能です。

まず、1 つの API 呼び出しの実行時間を正確に測定できる必要があります。

QueryPerformanceCounter のような正確な測定ツールを選択する

Microsoft Windows オペレーティング システムには、高解像度の経過時間を測定するために使用できる高解像度タイマーが含まれています。 このようなタイマーの現在の値は 、QueryPerformanceCounter を使用して返すことができます。 QueryPerformanceCounter を呼び出して開始値と停止値を返した後、QueryPerformanceCounter を使用して、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 つの大きな整数です。 SetTexture の直前に QueryPerformanceCounter(&start) が呼び出され、DrawPrimitive の直後に QueryPerformanceCounter(&stop) が呼び出されます。 停止値を取得した後、QueryPerformanceFrequency が呼び出されて freq が返されます。これは、高解像度タイマーの頻度です。 この仮定の例では、start、stop、freq に対して次の結果が得られるとします。

ローカル変数 ティック数
start 1792998845094
stop 1792998845102
周波数 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 マシンで SetTextureDrawPrimitive を処理するには、約 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 つの三角形をレンダリングすることで実現でき、各三角形に 1 ピクセルしか含めないようにさらに制約できます。

このペーパーで CPU の作業時間を測定するために使用される測定単位は、実際の時間ではなく CPU クロック サイクルの数になります。 CPU クロック サイクルには、CPU 速度が異なるマシン間の実際の経過時間よりも移植性が高い (CPU 制限付きアプリケーションの場合) という利点があります。 これは、必要に応じて実際の時刻に簡単に変換できます。

このドキュメントでは、CPU と GPU の間の作業負荷の分散に関連するトピックについては説明しません。 このペーパーの目的は、アプリケーションの全体的なパフォーマンスを測定するのではなく、ランタイムとドライバーが API 呼び出しを処理するのにかかる時間を正確に測定する方法を示す点に注意してください。 これらの正確な測定値を使用すると、特定のパフォーマンス シナリオを理解するために CPU の予算を設定するタスクを実行できます。

ランタイムとドライバーの最適化の制御

測定手法を特定し、GPU の作業を削減するための戦略を使用して、次の手順は、プロファイリング時に邪魔になるランタイムとドライバーの最適化を理解することです。

CPU の作業は、アプリケーションの作業、ランタイム作業、ドライバーの動作の 3 つのバケットに分けることができます。 これはプログラマの制御下にあるため、アプリケーションの動作を無視します。 アプリケーションの観点から見ると、ランタイムとドライバーはブラック ボックスに似ています。これは、アプリケーションがそれらの中に実装されているものを制御できないのでです。 重要なのは、ランタイムとドライバーに実装できる最適化手法を理解することです。 これらの最適化を理解していない場合は、プロファイルの測定値に基づいて CPU が実行している作業の量について間違った結論にジャンプするのは非常に簡単です。 特に、コマンド バッファーと呼ばれるものと、プロファイリングを難読化するためにできることに関連する 2 つのトピックがあります。 各トピックは以下のとおりです。

コマンド バッファーの制御

アプリケーションが API 呼び出しを行うと、ランタイムは API 呼び出しをデバイスに依存しない形式 (コマンドを呼び出します) に変換し、コマンド バッファーに格納します。 コマンド バッファーを次の図に追加します。

コマンド バッファーを含む CPU コンポーネントの図

アプリケーションが別の API 呼び出しを行うたびに、ランタイムはこのシーケンスを繰り返し、コマンド バッファーに別のコマンドを追加します。 ある時点で、ランタイムはバッファーを空にします (ドライバーにコマンドを送信します)。 Windows XP では、次の図に示すように、コマンド バッファーを空にすると、オペレーティング システムがランタイム (ユーザー モードで実行) からドライバー (カーネル モードで実行) に切り替わると、モードが切り替わります。

  • user mode - アプリケーション コードを実行する非特権プロセッサ モード。 ユーザー モード アプリケーションは、システム サービスを介した場合を除き、システム データにアクセスできません。
  • カーネル モード - Windows ベースのエグゼクティブ コードを実行する特権プロセッサ モード。 カーネル モードで実行されているドライバーまたはスレッドは、すべてのシステム メモリ、ハードウェアへの直接アクセス、およびハードウェアで I/O を実行するための CPU 命令にアクセスできます。

ユーザー モードとカーネル モードの切り替えの図

この切り替えは、CPU がユーザーからカーネル モード (およびその逆) に切り替わるたびに行われ、必要なサイクル数は個々の API 呼び出しと比較して大きくなります。 ランタイムが呼び出されたときに各 API 呼び出しをドライバーに送信した場合、すべての API 呼び出しでモード切り替えのコストが発生します。

代わりに、コマンド バッファーは、モード遷移の効果的なコストを削減するように設計されたランタイム最適化です。 コマンド バッファーは、1 つのモード遷移に備えて、多数のドライバー コマンドをキューに入れます。 ランタイムがコマンド バッファーにコマンドを追加すると、コントロールがアプリケーションに返されます。 プロファイラーには、ドライバー コマンドがまだドライバーに送信されていない可能性があることを知る方法はありません。 その結果、既製のインストルメンティング プロファイラーによって返される数値は、ランタイム作業を測定しますが、関連付けられているドライバーの作業は測定しないため、誤解を招く可能性があります。

モード切り替えなしのプロファイル結果

例 2 のレンダリング シーケンスを使用して、モード遷移の大きさを示す一般的なタイミング測定をいくつか次に示します。 SetTexture 呼び出しと DrawPrimitive 呼び出しでモード遷移が発生しないと仮定すると、既製のインストルメント化プロファイラーは次のような結果を返す可能性があります。

Number of cycles for SetTexture           : 100
Number of cycles for DrawPrimitive        : 900

これらの各番号は、ランタイムがこれらの呼び出しをコマンド バッファーに追加するのにかかる時間です。 モード切り替えがないため、ドライバーはまだ作業を行っていません。 プロファイラーの結果は正確ですが、レンダリング シーケンスによって最終的に CPU が実行されるすべての作業を測定するわけではありません。

モード遷移を使用したプロファイル結果

次に、モード遷移が発生したときに同じ例で何が起こるかを見てみましょう。 今回は、 SetTextureDrawPrimitive が モード遷移を引き起こすと仮定します。 もう一度、既製のインストルメントプロファイラーは、次のような結果を返す可能性があります。

Number of cycles for SetTexture           : 98 
Number of cycles for DrawPrimitive        : 946,900

SetTexture の測定時間は約同じですが、DrawPrimitive で費やされる時間の大幅な増加はモードの切り替えによるものです。 何が起こっているのかを次に示します。

  1. レンダリング シーケンスが開始される前に、コマンド バッファーに 1 つのコマンド用のスペースがあるとします。
  2. SetTexture はデバイスに依存しない形式に変換され、コマンド バッファーに追加されます。 このシナリオでは、この呼び出しによってコマンド バッファーがいっぱいになります。
  3. ランタイムは DrawPrimitive をコマンド バッファーに追加しようとしますが、完全であるため追加できません。 代わりに、ランタイムはコマンド バッファーを空にします。 これにより、カーネル モードの遷移が発生します。 移行に約 5,000 サイクルかかるとします。 この時間は DrawPrimitive で費やされた時間に貢献します。
  4. ドライバーは、コマンド バッファーから空にされたすべてのコマンドに関連付けられている作業を処理します。 ドライバーがコマンド バッファーをほぼ満たしたコマンドを処理する時間が約 935,000 サイクルであると仮定します。 SetTexture に関連付けられているドライバーの作業が約 2750 サイクルであると仮定します。 この時間は DrawPrimitive で費やされた時間に貢献します。
  5. ドライバーの作業が完了すると、ユーザー モードの切り替えは、ランタイムに制御を返します。 コマンド バッファーが空になりました。 移行に約 5,000 サイクルかかるとします。
  6. レンダリング シーケンスは 、DrawPrimitive を変換してコマンド バッファーに追加することで終了します。 これには約 900 サイクルかかるとします。 この時間は DrawPrimitive で費やされた時間に貢献します。

結果を要約すると、次のように表示されます。

DrawPrimitive = kernel-transition + driver work    + user-transition + runtime work
DrawPrimitive = 5000              + 935,000 + 2750 + 5000            + 900
DrawPrimitive = 947,950  

モード遷移 (900 サイクル) を使用しない DrawPrimitive の測定と同様に、モード遷移 (947,950 サイクル) を使用した DrawPrimitive の測定は正確ですが、CPU 作業の予算化の点では役に立ちません。 結果には、正しいランタイム作業、 SetTexture のドライバーの動作、 SetTexture より前のすべてのコマンドのドライバーの動作、および 2 つのモード遷移が含まれます。 ただし、測定に DrawPrimitive ドライバーの作業がありません。

モード遷移は、任意の呼び出しに応答して発生する可能性があります。 これは、コマンド バッファー内の以前の内容によって異なります。 各呼び出しに関連付けられている CPU 作業 (ランタイムとドライバー) の量を理解するには、モードの切り替えを制御する必要があります。 そのためには、コマンド バッファーとモード遷移のタイミングを制御するためのメカニズムが必要です。

クエリ メカニズム

Microsoft Direct3D 9 のクエリ メカニズムは、ランタイムが 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: クエリを使用してコマンド バッファーを制御する

これらの各コード行の詳細な説明を次に示します。

  1. D3DQUERYTYPE_EVENTを使用してクエリ オブジェクトを作成して、イベント クエリを作成します。
  2. Issue(D3DISSUE_END) を呼び出して、コマンド バッファーにクエリ イベント マーカー追加します。 このマーカーは、GPU がマーカーの前にあるコマンドの実行をいつ完了するか追跡するようにドライバーに指示します。
  3. D3DGETDATA_FLUSH で GetData を 呼び出すとコマンド バッファー が強制的に 空になるため、最初の呼び出しではコマンド バッファーが空になります。 後続の各呼び出しでは、GPU がチェックされ、すべてのコマンド バッファー作業の処理がいつ終了されるかが確認されます。 このループは、GPU がアイドル状態になるまでS_OKを返しません。
  4. 開始時刻をサンプリングします。
  5. プロファイリング対象の API 呼び出しを呼び出します。
  6. コマンド バッファーに 2 つ目のクエリ イベント マーカーを追加します。 このマーカーは、呼び出しの完了を追跡するために使用されます。
  7. D3DGETDATA_FLUSH で GetData を 呼び出すとコマンド バッファー が強制的に 空になるため、最初の呼び出しではコマンド バッファーが空になります。 GPU がすべてのコマンド バッファー作業の処理を完了すると、 GetData はS_OKを返し、GPU がアイドル状態であるためループが終了します。
  8. 停止時間をサンプリングします。

QueryPerformanceCounter と QueryPerformanceFrequency で測定された結果を次に示します。

ローカル変数 ティック数
start 1792998845060
stop 1792998845090
周波数 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 呼び出しに応答して発生している内容と、推定されるタイミングを次に示します。

  1. 最初の呼び出しでは、D3DGETDATA_FLUSHを使用して GetData を呼び出すことで、コマンド バッファーを空にします。 GPU がすべてのコマンド バッファー作業の処理を完了すると、 GetData はS_OKを返し、GPU がアイドル状態であるためループが終了します。

  2. レンダー シーケンスは、まず SetTexture をデバイスに依存しない形式に変換し、コマンド バッファーに追加します。 これには約 100 サイクルかかるとします。

  3. DrawPrimitive が変換され、コマンド バッファーに追加されます。 これには約 900 サイクルかかるとします。

  4. 問題 により、コマンド バッファーにクエリ マーカーが追加されます。 これには約 200 サイクルかかるとします。

  5. GetData を使用すると、コマンド バッファーが空になり、カーネル モードの遷移が強制されます。 これには約 5,000 サイクルかかるとします。

  6. ドライバーは、4 つの呼び出しすべてに関連付けられている作業を処理します。 SetTexture を処理するドライバーの時間は約 2964 サイクル、DrawPrimitive は約 3600 サイクル、問題は約 200 サイクルであるとします。 したがって、4 つのすべてのコマンドのドライバー時間の合計は約 6450 サイクルです。

    注意

    また、ドライバーは GPU の状態を確認するのに少し時間がかかります。 GPU の作業は簡単なので、GPU は既に行う必要があります。 GetData は、GPU が完了した可能性に基づいてS_OKを返します。

     

  7. ドライバーの作業が完了すると、ユーザー モードの切り替えは、ランタイムに制御を返します。 コマンド バッファーが空になりました。 これには約 5,000 サイクルかかるとします。

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 作業が効果的にキャプチャされます。

コマンド バッファーと、それがプロファイリングに与える影響について理解したので、ランタイムがコマンド バッファーを空にする原因となる可能性がある他のいくつかの条件があることを理解しておく必要があります。 これらはレンダー シーケンスでwatchする必要があります。 これらの条件の一部は API 呼び出しに応答し、他の条件はランタイムでのリソースの変更に応答しています。 次の条件のいずれかが発生すると、モードが切り替わります。

  • いずれかのロック メソッド (Lock) が頂点バッファー、インデックス バッファー、またはテクスチャ (特定のフラグを持つ特定の条件下) で呼び出された場合。
  • デバイスまたは頂点バッファー、インデックス バッファー、またはテクスチャが作成されたとき。
  • デバイスまたは頂点バッファー、インデックス バッファー、またはテクスチャが最後のリリースによって破棄された場合。
  • ValidateDevice が呼び出されたとき。
  • Present が呼び出されたとき。
  • コマンド バッファーがいっぱいになったとき。
  • getData が D3DGETDATA_FLUSH で呼び出されたとき。

レンダー シーケンスでこれらの条件をwatchするように注意してください。 モード切り替えが追加されるたびに、プロファイリング測定に 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 で測定された結果を次に示します。

ローカル変数 Tics の数
start 1792998845000
stop 1792998847084
周波数 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

言い換えると、この 2 GHz コンピューターでは、レンダー ループ内の 1500 回の呼び出しを処理するのに約 690 万サイクルかかります。 690 万サイクルのうち、モード遷移の時間は約 10,000 時間であるため、プロファイルの結果は 、SetTextureDrawPrimitive に関連する作業をほぼ完全に測定しています。

コード サンプルには、2 つのテクスチャの配列が必要であることに注意してください。 呼び出されるたびに同じテクスチャ ポインターを設定した場合に SetTexture を削除するランタイム最適化を回避するには、単に 2 つのテクスチャの配列を使用します。 これにより、ループのたびにテクスチャ ポインターが変更され、 SetTexture に関連付けられている完全な作業が実行されます。 両方のテクスチャのサイズと形式が同じであることを確認して、テクスチャの場合に他の状態が変更されないようにします。

これで、Direct3D をプロファイリングする手法が作成されました。 高パフォーマンス カウンター (QueryPerformanceCounter) に依存して、CPU が作業を処理するために必要なティック数を記録します。 作業は、クエリ メカニズムを使用して API 呼び出しに関連付けられているランタイムとドライバーの作業として慎重に制御されます。 クエリは、2 つの制御手段を提供します。最初に、レンダリング シーケンスが開始される前にコマンド バッファーを空にし、次に GPU の作業が完了したときに を返します。

ここまでで、このペーパーではレンダリング シーケンスをプロファイリングする方法を示しました。 各レンダー シーケンスはかなり単純で、1 つの DrawPrimitive 呼び出しと SetTexture 呼び出しが含まれています。 これは、コマンド バッファーと、それを制御するためのクエリ メカニズムの使用に焦点を当てる目的で行われました。 任意のレンダリング シーケンスをプロファイリングする方法の簡単な概要を次に示します。

  • QueryPerformanceCounter などのハイ パフォーマンス カウンターを使用して、各 API 呼び出しの処理にかかる時間を測定します。 QueryPerformanceFrequency と CPU クロック レートを使用して、これを API 呼び出しあたりの CPU サイクル数に変換します。
  • 各三角形に 1 ピクセルが含まれる三角形リストをレンダリングして、GPU の作業量を最小限に抑えます。
  • クエリ メカニズムを使用して、レンダリング シーケンスの前にコマンド バッファーを空にします。 これにより、プロファイリングによって、レンダリング シーケンスに関連付けられている適切な量のランタイムとドライバーの作業がキャプチャされます。
  • クエリ イベント マーカーを使用して、コマンド バッファーに追加される作業量を制御します。 この同じクエリは、GPU が作業を完了したときに検出します。 GPU の作業は簡単であるため、これはドライバーの作業が完了したタイミングを測定することと実質的に同等です。

これらの手法はすべて、状態の変更をプロファイリングするために使用されます。 コマンド バッファーの制御方法を読んで理解し、 DrawPrimitive でベースライン測定を正常に完了したと仮定すると、レンダリング シーケンスに状態変更を追加する準備が整います。 レンダリング シーケンスに状態変更を追加する場合、いくつかのプロファイリングの課題があります。 レンダリング シーケンスに状態変更を追加する場合は、次のセクションに進んでください。

Direct3D 状態の変更のプロファイリング

Direct3D では、多くのレンダリング状態を使用して、パイプラインのほぼすべての側面を制御します。 状態の変更を引き起こす API には、Draw*Primitive 呼び出し以外の関数またはメソッドが含まれます。

状態の変更は、レンダリングなしで状態変更のコストを確認できない可能性があるため、複雑です。 これは、ドライバーと GPU が絶対に実行する必要があるまで作業を延期するために使用する遅延アルゴリズムの結果です。 一般に、1 つの状態変更を測定するには、次の手順に従う必要があります。

  1. 最初 に DrawPrimitive をプロファイルします。
  2. レンダリング シーケンスに 1 つの状態変更を追加し、新しいシーケンスをプロファイリングします。
  3. 2 つのシーケンス間の差を減算して、状態の変化のコストを取得します。

当然ながら、クエリ メカニズムの使用と、モード切り替えのコストを否定するループ内のレンダリング シーケンスの配置について学んだことはすべて引き続き適用されます。

単純な状態変更のプロファイリング

DrawPrimitive を含むレンダー シーケンス以降では、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 propagate to the GPU
  DrawPrimitive(D3DPT_TRIANGLELIST, i*3, 1);
}

// Get the stop counter value as shown in Example 4 

例 5: 1 つの状態変更 API 呼び出しの測定

ループには 、SetTextureDrawPrimitive の 2 つの呼び出しが含まれていることに注意してください。 レンダー シーケンスは 1500 回ループし、次のような結果を生成します。

ローカル変数 Tics の数
start 1792998860000
stop 1792998870260
周波数 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

ループの各イテレーションには、状態の変更と描画呼び出しが含まれます。 DrawPrimitive レンダリング シーケンスの結果を減算すると、次のようになります。

3850 - 1100 = 2750 cycles for SetTexture

これは、このレンダリング シーケンスに SetTexture を追加する平均サイクル数です。 この同じ手法は、他の状態の変更にも適用できます。

SetTexture が単純な状態変更と呼ばれるのはなぜですか? 設定されている状態は、状態が変更されるたびにパイプラインが同じ量の作業を実行するように制約されるためです。 両方のテクスチャを同じサイズと形式に制限すると、 SetTexture 呼び出しごとに同じ量の作業が保証されます。

切り替える必要がある状態変更のプロファイリング

グラフィックス パイプラインによって実行される作業量が、レンダー ループの反復ごとに変化するその他の状態の変更があります。 たとえば、z テストが有効になっている場合、各ピクセルカラーは、新しいピクセルの z 値が既存のピクセルの z 値に対してテストされた後にのみレンダー ターゲットを更新します。 z テストが無効になっている場合、このピクセル単位のテストは行われず、出力ははるかに高速に書き込まれます。 z テスト状態を有効または無効にすると、レンダリング中に実行される作業量 (CPU と GPU によって) が劇的に変わります。

SetRenderState では、z テストを有効または無効にするために、特定のレンダリング状態と状態値が必要です。 特定の状態値は実行時に評価され、必要な作業量が決定されます。 レンダー ループでこの状態の変化を測定し、切り替えるようにパイプラインの状態を事前に調整することは困難です。 唯一の解決策は、レンダリング シーケンス中の状態の変更を切り替える方法です。

たとえば、プロファイリング手法を次のように 2 回繰り返す必要があります。

  1. まず、 DrawPrimitive レンダリング シーケンスをプロファイリングします。 これをベースラインと呼びます。
  2. 状態の変化を切り替える 2 番目のレンダリング シーケンスをプロファイリングします。 レンダー シーケンス ループには、次のものが含まれます。
    • 状態を "false" 状態に設定する状態の変更。
    • DrawPrimitive は、元のシーケンスと同じです。
    • 状態を "true" 条件に設定する状態の変更。
    • 2 番目の状態変更を強制的に実現する 2 番目の DrawPrimitive
  3. 2 つのレンダリング シーケンスの違いを見つけます。 このためには、次のことを行います。
    • 新しいシーケンスには 2 つの DrawPrimitive 呼び出しがあるため、ベースライン 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 propagate 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 propagate to the GPU
  DrawPrimitive(D3DPT_TRIANGLELIST, (2*i + 1)*3, 1); 
}

// Get the stop counter value as shown in Example 4 

例 5: 切り替え状態の変化を測定する

ループは、2 つの SetRenderState 呼び出しを実行して状態を切り替えます。 最初の SetRenderState 呼び出しでは z テストが無効になり、2 番目の SetRenderState によって z テストが有効になります。 各 SetRenderState の後に DrawPrimitive が続き、ドライバーでダーティ ビットのみを設定する代わりに、状態変更に関連付けられた作業がドライバーによって処理されます。

このレンダリング シーケンスでは、次の数値が妥当です。

ローカル変数 ティック数
start 1792998845000
stop 1792998861740
周波数 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 検定を有効または無効にする平均サイクル数は 2,000 サイクルです。 QueryPerformanceCounter が z-enable の半分の時間と z-disable の半分の時間を測定していることは注目に値します。 この手法は、実際には両方の状態変化の平均を測定します。 つまり、状態を切り替える時間を測定しています。 この手法を使用すると、両方の平均を測定しているため、有効時間と無効化時間が同等であるかどうかを知る方法はありません。 ただし、この状態の変更を引き起こすアプリケーションとして切り替え状態を予算化する場合は、この状態を切り替えるだけで適切な数値を使用できます。

これで、これらの手法を適用し、必要なすべての状態変更をプロファイリングできます。 そうとも言えません。 それでも、実行する必要がある作業の量を減らすために設計された最適化に注意する必要があります。 レンダリング シーケンスの設計時に注意する必要がある最適化には、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 propagate 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 propagate to the GPU
  DrawPrimitive(D3DPT_TRIANGLELIST, (2*i + 1)*3, 1); 
}

z-func 状態は、z バッファーに書き込むときに比較レベルを設定します (現在のピクセルの z 値と深度バッファー内のピクセルの z 値の間)。 D3DCMP_NEVERは z テストの比較をオフにし、D3DCMP_ALWAYSは z テストが行われるたびに比較が行われるよう設定します。

DrawPrimitive を使用してレンダリング シーケンスでこれらの状態の変更のいずれかをプロファイリングすると、次のような結果が生成されます。

単一状態の変更 平均サイクル数
D3DRS_ZENABLEのみ 2000

 

または

単一状態の変更 平均サイクル数
D3DRS_ZFUNCのみ 600

 

ただし、D3DRS_ZENABLEとD3DRS_ZFUNCの両方を同じレンダリング シーケンスでプロファイリングすると、次のような結果が表示される可能性があります。

両方の状態の変更 平均サイクル数
D3DRS_ZENABLE + D3DRS_ZFUNC 2000

 

ドライバーが両方のレンダリング状態の設定に関連するすべての作業を行っているため、結果は 2000 サイクルと 600 サイクル (または 2600) サイクルの合計になると予想できます。 代わりに、平均は 2000 サイクルです。

この結果には、ランタイム、ドライバー、または GPU に実装された状態変更の最適化が反映されます。 この場合、ドライバーは最初の SetRenderState を確認し、後で作業を延期するダーティ状態を設定できます。 ドライバーが 2 番目の SetRenderState を確認すると、同じダーティ状態を冗長に設定でき、同じ作業が再度延期されます。 DrawPrimitive が呼び出されると、ダーティ状態に関連付けられている作業が最終的に処理されます。 ドライバーは 1 回限り作業を実行します。つまり、最初の 2 つの状態の変更は、ドライバーによって効果的に統合されます。 同様に、3 番目と 4 番目の状態の変更は、2 番目の DrawPrimitive が呼び出されると、ドライバーによって 1 つの状態変更に効果的に統合されます。 結果として、ドライバーと GPU は描画呼び出しごとに 1 つの状態変更を処理します。

これは、シーケンスに依存するドライバーの最適化の良い例です。 ドライバーは、ダーティ状態を設定して 2 回作業を延期した後、ダーティ状態をクリアするために 1 回作業を実行しました。 これは、作業が絶対に必要になるまで延期されるときに実行できる効率向上の種類の良い例です。

内部的にダーティ状態を設定し、後で作業を延期する状態の変更を知る方法 レンダー シーケンスをテストする (またはドライバー ライターと通信する) 場合のみ。 ドライバーは定期的に更新され、改善されるため、最適化の一覧は静的ではありません。 特定のハードウェア セットで、特定のレンダリング シーケンスで状態変更のコストを絶対に把握する方法は 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 つの描画呼び出し

このシーケンスには 2 つの描画呼び出しが含まれています。この呼び出しは、ランタイムによって次と同等の 1 つの呼び出しに統合されます。

DrawPrimitive(D3DPT_TRIANGLELIST, 0, 7); // Draw 7 primitives, vertices 0 - 20

例 5b: 1 つの連結された描画呼び出し

ランタイムは、これらの特定の描画呼び出しの両方を 1 つの呼び出しに連結します。これにより、ドライバーが描画呼び出しを 1 回処理するだけで済むため、ドライバーの作業が 50% 減少します。

一般に、ランタイムは、次の場合に 2 つ以上のバックツーバック DrawPrimitive 呼び出しを連結します。

  1. プリミティブ型は三角形リスト (D3DPT_TRIANGLELIST)。
  2. 連続する DrawPrimitive 呼び出しごとに、頂点バッファー内の連続する頂点を参照する必要があります。

同様に、2 つ以上のバックツーバック DrawIndexedPrimitive 呼び出しを連結するための適切な条件は次のとおりです。

  1. プリミティブ型は三角形リスト (D3DPT_TRIANGLELIST)。
  2. 連続する 各 DrawIndexedPrimitive 呼び出しでは、インデックス バッファー内で連続するインデックスを連続して参照する必要があります。
  3. 連続 する DrawIndexedPrimitive 呼び出しでは、BaseVertexIndex に同じ値を使用する必要があります。

プロファイリング中に連結されないようにするには、プリミティブ型が三角形リストにならないようにレンダリング シーケンスを変更するか、連続する頂点 (またはインデックス) を使用するバックツーバック描画呼び出しがないようにレンダリング シーケンスを変更します。 具体的には、ランタイムでは、次の両方の条件を満たす描画呼び出しも連結されます。

  • 前の呼び出しが DrawPrimitive の場合、次の描画呼び出しの場合:
    • は三角形リストを使用し、AND
    • StartVertex = 以前の StartVertex + 以前の PrimitiveCount * 3 を指定します
  • DrawIndexedPrimitive を使用する場合、次の描画呼び出しの場合:
    • は三角形リストを使用し、AND
    • StartIndex = 前の StartIndex + 以前の PrimitiveCount * 3、AND を指定します
    • BaseVertexIndex = 以前の BaseVertexIndex を指定します

プロファイリング時に見落としやすい描画呼び出し連結のより微妙な例を次に示します。 レンダリング シーケンスは次のようになります。

  for(int i = 0; i < 1500; i++)
  {
    SetTexture(...);
    DrawPrimitive(D3DPT_TRIANGLELIST, i*3, 1);
  }

例 5c: 1 つの状態変更と 1 つの描画呼び出し

ループは 1500 個の三角形を反復処理し、テクスチャを設定し、各三角形を描画します。 このレンダリング ループは、前のセクションで示したように 、SetTexture の場合は約 2750 サイクル、 DrawPrimitive の場合は約 1100 サイクルかかります。 レンダリング ループの外側に SetTexture を 移動すると、ドライバーが実行する作業の量を 1500 * 2750 サイクル減らす必要があります。これは 、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

SetTexture をレンダー ループの外側に移動すると、SetTexture が 1500 回ではなく 1 回呼び出されるため、SetTexture に関連付けられている作業量が減ります。 あまり明らかな二次効果は、 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 

これらの結果は完全に正しいですが、元の質問のコンテキストでは非常に誤解を招きます。 描画呼び出しの最適化により、ドライバーの作業量が大幅に削減されました。 これは、カスタム プロファイリングを実行する際の一般的な問題です。 レンダリング シーケンスからの呼び出しを排除する場合は、呼び出しの連結を描画しないように注意してください。 実際、このシナリオは、このランタイムの最適化によって可能なドライバーのパフォーマンスの向上量の強力な例です。

これで、状態の変化を測定する方法がわかります。 最初に DrawPrimitive をプロファイリングします。 次に、シーケンスに各追加の状態変更を追加し (場合によっては 1 つの呼び出しを追加し、他の場合は 2 つの呼び出しを追加します)、2 つのシーケンスの違いを測定します。 結果をティックまたはサイクルまたは時間に変換できます。 QueryPerformanceCounter を使用してレンダー シーケンスを測定するのと同様に、個々の状態の変化を測定するには、クエリ メカニズムを使用してコマンド バッファーを制御し、状態の変更をループに入れてモード遷移の影響を最小限に抑えます。 プロファイラーは状態の有効化と無効化の平均を返すので、この手法は状態を切り替えるコストを測定します。

この機能を使用すると、任意のレンダリング シーケンスの生成を開始し、関連するランタイムとドライバーの作業を正確に測定できます。 その後、CPU が制限されたシナリオを想定して、適切なフレーム レートを維持しながら、レンダリング シーケンスで "これらの呼び出しの数" などの予算作成の質問に回答するために使用できます。

まとめ

このペーパーでは、個々の呼び出しを正確にプロファイリングできるようにコマンド バッファーを制御する方法を示します。 プロファイル番号は、ティック、サイクル、または絶対時間で生成できます。 これらは、各 API 呼び出しに関連付けられているランタイムとドライバーの作業の量を表します。

まず、レンダー シーケンスで Draw*Primitive 呼び出しをプロファイリングします。 次の点を忘れないでください。

  1. QueryPerformanceCounter を使用して、API 呼び出しあたりのティック数を測定します。 QueryPerformanceFrequency を使用して、必要にじて結果をサイクルまたは時間に変換します。
  2. 開始する前に、クエリ メカニズムを使用してコマンド バッファーを空にします。
  3. モード遷移の影響を最小限に抑えるために、レンダリング シーケンスをループに含めます。
  4. GPU の作業が完了したタイミングを測定するには、クエリ メカニズムを使用します。
  5. 実行された作業量に大きな影響を与えるランタイム連結に注意してください。

これにより、ビルド元として使用できる DrawPrimitive のベースライン パフォーマンスが得られます。 1 つの状態変更をプロファイリングするには、次の追加のヒントに従います。

  1. 状態の変更を、新しいシーケンスの既知のレンダリング シーケンス プロファイルに追加します。 テストはループで行われるので、状態を反対の値に 2 回設定する必要があります (たとえば、有効化や無効化など)。
  2. 2 つのシーケンス間のサイクル時間の差を比較します。
  3. パイプラインを大幅に変更する状態の変更 ( SetTexture など) の場合は、2 つのシーケンス間の差を減算して、状態変更の時間を取得します。
  4. パイプラインを大幅に変更する状態の変更 (したがって SetRenderState などの状態の切り替えが必要) の場合は、レンダリング シーケンスの差を減算し、2 で除算します。 これにより、各状態の変化の平均サイクル数が生成されます。

ただし、プロファイリング時に予期しない結果を引き起こす最適化には注意してください。 状態変更の最適化では、ダーティ状態が設定され、作業が遅延する可能性があります。 これにより、プロファイルの結果が予期したほど直感的ではない可能性があります。 連結された呼び出しを描画すると、ドライバーの作業が大幅に削減され、誤解を招く可能性があります。 慎重に計画されたレンダリング シーケンスを使用して、状態の変更と描画呼び出しの連結が発生しないようにします。 このトリックは、生成する数値が妥当な予算作成数値になるように、プロファイリング中に最適化が行われないようにする方法です。

注意

クエリ メカニズムを使用せずにアプリケーションでこのプロファイリング戦略を複製することは、より困難です。 Direct3D 9 より前では、コマンド バッファーを空にする唯一の予測可能な方法は、アクティブなサーフェス (レンダー ターゲットなど) をロックして、GPU がアイドル状態になるまで待機することです。 これは、サーフェスをロックすると、GPU の終了を待機するだけでなく、サーフェスがロックされる前にサーフェスを更新する必要があるレンダリング コマンドがバッファー内に存在する場合に備えて、ランタイムがコマンド バッファーを強制的に空にするためです。 この手法は機能しますが、Direct3D 9 で導入されたクエリ メカニズムを使用する方が目立っています。

 

付録

この表の数値は、これらの各状態変更に関連付けられているランタイムとドライバーの作業量の近似値の範囲です。 近似値は、論文に示されている手法を使用してドライバーに対して行われた実際の測定値に基づいています。 これらの数値は、Direct3D 9 ランタイムを使用して生成され、ドライバーに依存します。

このペーパーの手法は、ランタイムとドライバーの作業を測定するように設計されています。 一般に、すべてのアプリケーションで CPU と GPU のパフォーマンスに一致する結果を提供することは実用的ではありません。これは、レンダリング シーケンスの完全な配列を必要とするためです。 さらに、GPU のパフォーマンスをベンチマークすることは特に困難です。これは、レンダリング シーケンスの前のパイプラインでの状態の設定に大きく依存するためです。 たとえば、アルファ ブレンドを有効にすると、必要な CPU 作業の量にほとんど影響しませんが、GPU によって実行される作業の量に大きな影響を与える可能性があります。 したがって、このペーパーの手法では、レンダリングする必要があるデータの量を制限することで、GPU の作業を可能な限り最小限に制限します。 つまり、テーブル内の数値は、(GPU によって制限されているアプリケーションではなく) CPU が制限されているアプリケーションから得られた結果と最も密接に一致します。

提示されている手法を使用して、最も重要なシナリオと構成をカバーすることをお勧めします。 テーブル内の値を使用して、生成した数値と比較できます。 各ドライバーは異なるため、実際に表示される数値を生成する唯一の方法は、シナリオを使用してプロファイリング結果を生成することです。

API 呼び出し サイクルの平均数
SetVertexDeclaration 6500 - 11250
SetFVF 6400 - 11200
SetVertexShader 3000 - 12100
SetPixelShader 6300 - 7000
反射可能 1900 - 11200
SetRenderTarget 6000 - 6250
SetPixelShaderConstant (1 定数) 1500 - 9000
NORMALIZENORMALS 2200 - 8100
LightEnable 1300 - 9000
SetStreamSource 3700 - 5800
照明 1700 - 7500
DIFFUSEMATERIALSOURCE 900 - 8300
AMBIENTMATERIALSOURCE 900 - 8200
COLORVERTEX 800 - 7800
SetLight 2200 - 5100
SetTransform 3200 - 3750
SetIndices 900 - 5600
周囲 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 定数) 1000 - 2700
COLOROP 1500 - 2100
COLORARG2 1300 - 2000
COLORARG1 1300 - 1980
CULLMODE 500 - 2570
クリッピング 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

 

高度なトピック