パフォーマンスの最適化 (Direct3D 9)

3D グラフィックスを使用するリアルタイム アプリケーションの開発者にとって、パフォーマンスの最適化は関心事の 1 つです。ここでは、最高のパフォーマンスが得られるコードを記述するためのガイドラインについて説明します。

  • パフォーマンスに関する一般的なヒント
  • データベースとカリング
  • プリミティブのバッチ処理
  • ライティングに関するヒント
  • テクスチャー サイズ
  • 行列のトランスフォーム
  • 動的テクスチャーの使用
  • 動的な頂点バッファーおよびインデックス バッファーの使用
  • メッシュの使用
  • Z バッファーのパフォーマンス

パフォーマンスに関する一般的なヒント

  • クリア処理は必要なときにだけ行う。

  • ステート変更をできるだけ減らし、残ったステート変更をグループ化する。

  • できるだけ小さなテクスチャーを使用する。

  • シーン内のオブジェクトを前から後ろの順番に描画する。

  • 三角形リストや三角形ファンの代わりに三角形ストリップを使用する。頂点キャッシュのパフォーマンスを最適化するには、三角形の頂点を早めに再利用するようにストリップを配置します。

  • システム リソースの共有のバランスを崩すような特殊効果はなるべく使用しない。

  • アプリケーションのパフォーマンスを絶えずテストする。

  • 頂点バッファーの切り替えをできるだけ減らす。

  • 可能な場合は静的な頂点バッファーを使用する。

  • 静的なオブジェクトには、オブジェクトごとではなく、静的オブジェクト用の FVF ごとに 1 つの大きな静的頂点バッファーを使う。

  • アプリケーションで AGP メモリー内の頂点バッファーへのランダム アクセスが必要な場合は、32 バイトの倍数の頂点フォーマット サイズを選択する。その他の場合は、最も小さい適切なフォーマットを選択します。

  • インデックス付きプリミティブを使用して描画する。これによって、ハードウェア内の頂点キャッシュの効率が向上します。

  • 深度バッファー フォーマットにステンシル チャンネルが含まれる場合は、必ず深度およびステンシルのチャンネルを同時にクリアする。

  • 可能な場合はシェーダー命令とデータ出力を組み合わせる。次に例を示します。

    // Rather than doing a multiply and add, and then output the data with 
    //   two instructions:
    mad r2, r1, v0, c0
    mov oD0, r2
    
    // Combine both in a single instruction, because this eliminates an  
    //   additional register copy.
    mad oD0, r1, v0, c0 
    

データベースとカリング

Direct3D の優れたパフォーマンスを実現するための鍵となるのは、ワールド内のオブジェクトの信頼性あるデータベースを構築することです。これはラスター化やハードウェアの改善より重要です。

ポリゴンの個数は、管理できる範囲内で最小限に抑える必要があります。ポリゴンの個数を少なくするには、最初からその個数が少なくて済むようなモデルを構築します。ポリゴンの追加は、開発段階の後半で、パフォーマンスを犠牲にせずに追加できる場合に行います。最速のポリゴンは描画しないポリゴンであることを忘れないでください。

プリミティブのバッチ処理

実行時に最高のレンダリング パフォーマンスを得るには、プリミティブをバッチ処理し、レンダリング ステートの変更回数をできるだけ少なくします。たとえば、1 つのオブジェクトで 2 つのテクスチャーを使用している場合、最初のテクスチャーを使用する三角形をグループ化し、必要なレンダリング ステートをその三角形に付加してテクスチャーを変更します。次に、もう 1 つのテクスチャーを使用する三角形をすべてグループ化します。Direct3D の最も単純なハードウェア サポートは、ハードウェア アブストラクション レイヤー (HAL) を介して、レンダリング ステートのバッチおよびプリミティブのバッチによって呼び出されます。命令のバッチ処理を効率的に行えば、実行時の HAL の呼び出し回数が減少します。

ライティングに関するヒント

ライトを適用した場合、レンダリングされる各フレームに頂点単位の負荷が加わります。したがって、アプリケーションでライトを慎重に使うことで、パフォーマンスを大幅に改善できます。次のヒントのほとんどは、"最速のコードは呼び出されないコードである" という考えに基づきます。

  • 光源は必要最小限で使用します。全体的なライティング レベルを増加させるには、たとえば、新しい光源を追加するのではなく、アンビエント ライトを使います。
  • ディレクショナル ライトは、ポイント ライトやスポットライトより効率的です。ディレクショナル ライトの場合、光の方向が固定されているため、頂点ごとに計算する必要がありません。
  • スポットライトは、ポイント ライトより効率的です。光のコーンの外側の領域の計算はすばやく行われるためです。スポットライトが効率的かどうかは、スポットライトでシーンを照らす量によって異なります。
  • 範囲パラメーターを使用して、シーンの必要な部分にだけライトを照らします。範囲外にあるすべての種類のライトはすぐに終了させます。
  • スペキュラ ハイライトは、ライトの 2 倍の負荷がかかります。必要なときだけこれを使用してください。D3DRS_SPECULARENABLE レンダリング ステートには、できるだけ既定値 0 を設定します。マテリアルを定義するときは、スペキュラ強度値をゼロに設定して、そのマテリアルに対するスペキュラ ハイライトをオフにします。単にスペキュラ色を 0,0,0 に設定するだけでは不十分です。

テクスチャー サイズ

テクスチャー マッピングのパフォーマンスは、メモリーの速度に大きく依存します。アプリケーションのテクスチャーをキャッシュする際のパフォーマンスを最大限に高める方法がいくつかあります。

  • テクスチャーを小さくする。テクスチャーが小さいほど、メイン CPU の 2 次キャッシュに格納される可能性が高くなります。
  • プリミティブごとにテクスチャーを変更しない。ポリゴンは使用するテクスチャーの順にグループ化します。
  • できるだけ正方形テクスチャーを使用します。ディメンジョンが 256 × 256 のテクスチャーが最速です。たとえば、アプリケーションで 128 × 128 の 4 つのテクスチャーを使用する場合、これらのテクスチャーで同じパレットを使い、すべてのテクスチャーを 256 × 256 の 1 つのテクスチャーに配置します。この方法を使用すると、テクスチャーのスワップ量も減少します。もちろん、上で説明したように、テクスチャーはできるだけ小さくする必要があるので、アプリケーションで 256 × 256 のテクスチャーが必要な場合を除いては、そのようなテクスチャーを使用しないでください。

行列のトランスフォーム

Direct3D では、ワールド行列およびビュー行列を設定して、いくつかの内部データ構造を構成します。新しいワールド行列またはビュー行列を設定すると、そのたびに関連付けられた内部構造が再計算されます。これらの行列を頻繁に設定すると (たとえばフレームあたり何千回も)、計算に時間がかかります。ワールド行列とビュー行列を、ワールド行列として設定するワールドビュー行列と組み合わせて、ビュー行列を単位に設定することで、必要な計算の数を最小限にすることができます。ワールド行列およびビュー行列の各キャッシュ コピーを保持しておくと、必要に応じてワールド行列を修正、連結、およびリセットできます。簡略化のため、このドキュメントでは Direct3D のサンプルはこの最適化を実行しません。

動的テクスチャーの使用

ドライバーが動的テクスチャーをサポートしているかどうかを確認するには、D3DCAPS9 構造体の D3DCAPS2_DYNAMICTEXTURES フラグを調べます。

動的テクスチャーを使用する場合は、常に次の点に注意してください。

  • 動的テクスチャーは管理できません。たとえば、動的テクスチャーのプールは D3DPOOOL_MANAGED になりません。
  • 動的テクスチャーは、D3DPOOL_DEFAULT で作成した場合でもロックできます。
  • D3DLOCK_DISCARD は、動的テクスチャーの有効なロック フラグです。

動的テクスチャーを作成するのは、フォーマットごとに 1 つだけ、また可能であればサイズごとに 1 つだけにすることをお勧めします。すべてのレベルをロックすることによってオーバーヘッドが増大するので、動的なミップマップ、キューブ、およびボリュームはお勧めできません。ミップマップの場合、最上位レベルでのみ LOCK_DISCARD を使用できます。最上位レベルをロックするだけですべてのレベルが破棄されます。この動作は、ボリュームおよびキューブでも同様です。キューブの場合、最上位レベルおよび面 0 がロックされます。

次の擬似コードは、動的テクスチャーの使用例を示しています。

DrawProceduralTexture(pTex)
{
    // pTex should not be very small because overhead of 
    //   calling driver every D3DLOCK_DISCARD will not 
    //   justify the performance gain. Experimentation is encouraged.
    pTex->Lock(D3DLOCK_DISCARD);
    <Overwrite *entire* texture>
    pTex->Unlock();
    pDev->SetTexture();
    pDev->DrawPrimitive();
}

動的な頂点バッファーおよびインデックス バッファーの使用

グラフィック プロセッサによる使用中に静的な頂点バッファーをロックすると、パフォーマンスが大幅に低下する可能性があります。ロック呼び出しは、呼び出し元のアプリケーションに戻る前に、グラフィック プロセッサがバッファーから頂点データまたはインデックス データの読み取りを完了するまで待機する必要があり、大幅な遅延が生じます。グラフィック プロセッサはロック ポインターに戻る前にコマンドを完了する必要があるため、静的バッファーに対してロックとレンダリングを 1 フレームあたり複数回実行する場合も、グラフィック プロセッサはレンダリング コマンドをバッファーに入れることができません。バッファーに格納されたコマンドがないと、アプリケーションが頂点バッファーまたはインデックス バッファーへの格納を完了してレンダリング コマンドを発行するまで、グラフィック プロセッサはアイドル状態のままです。

理想は頂点データまたはインデックス データがまったく変化しないことですが、これは必ずしも可能ではありません。アプリケーションがフレームごとに頂点データまたはインデックス データを変更する場合は多くあり、フレームあたり複数回変更することさえあります。このような場合は、D3DUSAGE_DYNAMIC を使って頂点バッファーまたはインデックス バッファーを作成する必要があります。この利用フラグによって Direct3D を頻繁なロック処理用に最適化します。D3DUSAGE_DYNAMIC はバッファーを頻繁にロックする場合にのみ有用であり、変化しないデータは静的な頂点バッファーまたはインデックス バッファーに格納する必要があります。

アプリケーションで動的な頂点バッファー使用時のパフォーマンスを向上させるには、適切なフラグを指定して IDirect3DVertexBuffer9::Lock または IDirect3DIndexBuffer9::Lock を呼び出す必要があります。D3DLOCK_DISCARD は、アプリケーションが古い頂点データまたはインデックス データをバッファー内に保持する必要がないことを示します。D3DLOCK_DISCARD でロックを呼び出した時点でグラフィック プロセッサがまだバッファーを使用中である場合は、古いバッファー データの代わりにメモリーの新しい領域へのポインターが返されます。これにより、アプリケーションが新しいバッファーにデータを格納するまで、グラフィック プロセッサは古いデータを使い続けることができます。アプリケーションによる追加のメモリー管理は必要ありません。グラフィック プロセッサが古いバッファーを使い終わると、古いバッファーは自動的に再利用または破棄されます。D3DLOCK_DISCARD を使ってバッファーをロックすると常にバッファー全体が破棄されること、非ゼロのオフセットまたは制限付きのサイズ フィールドを指定すると、ロックしていないバッファー領域の情報は保持されないことに注意してください。

スプライトをレンダリングするために 4 つの頂点を追加する場合のように、アプリケーションが 1 回のロックで格納するデータの量が少ない場合があります。D3DLOCK_NOOVERWRITE は、既に使用中である動的バッファー内のデータをアプリケーションが上書きしないことを示します。ロック呼び出しは古いデータへのポインターを返します。これにより、アプリケーションは頂点バッファーまたはインデックス バッファー内の未使用領域に新しいデータを追加できます。描画処理で使った頂点またはインデックスは、グラフィック プロセッサによってまだ使用中である可能性があるため、アプリケーションで変更してはなりません。動的バッファーがいっぱいになってメモリーの新しい領域を受け取れなくなったら、アプリケーションは D3DLOCK_DISCARD を使って、グラフィック プロセッサが使い終わった後の古い頂点データまたはインデックス データを破棄します。

グラフィック プロセッサが頂点をまだ使用中かどうかの確認には、非同期問い合わせメカニズムが役に立ちます。頂点を使う最後の DrawPrimitive 呼び出しの後、D3DQUERYTYPE_EVENT タイプの問い合わせを発行します。IDirect3DQuery9::GetData が S_OK を返した場合、頂点は既に使用中ではありません。バッファーをロックするときに D3DLOCK_DISCARD を指定した場合、またはフラグを指定しなかった場合は、常に頂点がグラフィック プロセッサと適切に同期しますが、フラグを指定せずにロックを実行すると、上記のとおりパフォーマンスの低下をもたらす可能性があります。IDirect3DDevice9::BeginSceneIDirect3DDevice9::EndScene、および IDirect3DDevice9::Present などのその他の API 呼び出しでは、グラフィック プロセッサが頂点を使い終わったことは保証されません。

動的バッファーと適切なロック フラグの使い方は次のとおりです。

    // USAGE STYLE 1
    // Discard the entire vertex buffer and refill with thousands of vertices.
    // Might contain multiple objects and/or require multiple DrawPrimitive 
    //   calls separated by state changes, etc.
 
    // Determine the size of data to be moved into the vertex buffer.
    UINT nSizeOfData = nNumberOfVertices * m_nVertexStride;
 
    // Discard and refill the used portion of the vertex buffer.
    CONST DWORD dwLockFlags = D3DLOCK_DISCARD;
    
    // Lock the vertex buffer.
    BYTE* pBytes;
    if( FAILED( m_pVertexBuffer->Lock( 0, 0, &pBytes, dwLockFlags ) ) )
        return false;
    
    // Copy the vertices into the vertex buffer.
    memcpy( pBytes, pVertices, nSizeOfData );
    m_pVertexBuffer->Unlock();
 
    // Render the primitives.
    m_pDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0, nNumberOfVertices/3)
    // USAGE STYLE 2
    // Reusing one vertex buffer for multiple objects
 
    // Determine the size of data to be moved into the vertex buffer.
    UINT nSizeOfData = nNumberOfVertices * m_nVertexStride;
 
    // No overwrite will be used if the vertices can fit into 
    //   the space remaining in the vertex buffer.
    DWORD dwLockFlags = D3DLOCK_NOOVERWRITE;
    
    // Check to see if the entire vertex buffer has been used up yet.
    if( m_nNextVertexData > m_nSizeOfVB - nSizeOfData )
    {
        // No space remains. Start over from the beginning 
        //   of the vertex buffer.
        dwLockFlags = D3DLOCK_DISCARD;
        m_nNextVertexData = 0;
    }
    
    // Lock the vertex buffer.
    BYTE* pBytes;
    if( FAILED( m_pVertexBuffer->Lock( (UINT)m_nNextVertexData, nSizeOfData, 
               &pBytes, dwLockFlags ) ) )
        return false;
    
    // Copy the vertices into the vertex buffer.
    memcpy( pBytes, pVertices, nSizeOfData );
    m_pVertexBuffer->Unlock();
 
    // Render the primitives.
    m_pDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 
               m_nNextVertexData/m_nVertexStride, nNumberOfVertices/3)
 
    // Advance to the next position in the vertex buffer.
    m_nNextVertexData += nSizeOfData;

メッシュの使用

メッシュを最適化するには、Direct3D のインデックス付き三角形ストリップではなく、インデックス付き三角形を使用します。ハードウェアは、連続する三角形の 95% が実際にストリップを形成しているものとして検出し、それに応じて調整を行います。多くのドライバーは、レガシー ハードウェアに対してもこのような処理を行います。

D3DX メッシュ オブジェクトは、各三角形、つまり面に DWORD でタグ付けすることができます。このタグは、その面の属性と呼ばれます。DWORD のセマンティクスはユーザーが定義します。D3DX はこれらを使ってメッシュをサブセットに分類します。アプリケーションは、ID3DXMesh::LockAttributeBuffer の呼び出しを使って各面の属性を設定します。非同期 ID3DXMesh::Optimize メソッドでは、D3DXMESHOPT_ATTRSORT オプションを使って、メッシュの頂点および面を属性によってグループ化できます。これを行うと、メッシュ オブジェクトによって属性テーブルが計算されます。アプリケーションは ID3DXBaseMesh::GetAttributeTable を呼び出して、このテーブルを取得できます。メッシュが属性によって分類されていない場合、この呼び出しは 0 を返します。属性テーブルは ID3DXMesh::Optimize メソッドが生成するので、アプリケーションがこのテーブルを設定することはできません。属性による分類はデータの影響を受けます。したがって、アプリケーションは、メッシュが属性に基づいて分類されていることがわかっている場合でも、ID3DXMesh::Optimize を呼び出して属性テーブルを生成する必要があります。

以下に、メッシュのさまざまな属性について示します。

属性 ID

属性識別子 (ID) は、面のグループを属性グループに関連付ける値です。この ID は、ID3DXBaseMesh::DrawSubset で描画する面のサブセットを記述します。属性 ID は、属性バッファー内の面に対して指定します。属性 ID の実際の値は、32 ビットに収まればどのような値でもかまいませんが、一般には 0 ~ n (n は属性の数) を使います。

属性バッファー

属性バッファーは、各面が属する属性グループを指定する DWORD (各面に 1 つ) の配列です。このバッファーは、メッシュの作成時にゼロに初期化されますが、ロード ルーチンによって値を指定するか、ID 0 の属性が複数個必要な場合にはユーザーによって指定するかのいずれかの必要があります。このバッファーには、ID3DXMesh::Optimize で属性に基づいてメッシュをソートするために使う情報が格納されます。属性テーブルが提示されていない場合、ID3DXBaseMesh::DrawSubset はこのバッファーを調べて、描画する指定の属性の面を選択します。

属性テーブル

属性テーブルは、メッシュによって所有および保持される構造体です。これを生成するには、属性ソート機能または最適化の強化機能を有効にして ID3DXMesh::Optimize を呼び出す必要があります。属性テーブルを使うと、プリミティブを描画する ID3DXBaseMesh::DrawSubset の単一の呼び出しをすばやく行うことができます。また、プログレッシブ メッシュもこの構造体を保持するので、現在の詳細レベルでアクティブな面と頂点を表示できます。

Z バッファーのパフォーマンス

シーンが前から後にレンダリングされるように、z バッファーリングを使用してテクスチャー処理を行うと、アプリケーションのパフォーマンスを向上させることができます。テクスチャーを適用し、z バッファーを使用したプリミティブは、走査線単位で z バッファーについて事前にテストされます。前にレンダリングされたポリゴンによって走査線が隠れている場合は、そのポリゴンがシステムによってすばやく効率的に除去されます。z バッファーリングによってパフォーマンスは向上しますが、このテクニックはシーンで同じピクセルを複数回描画するときに最も有効です。これを正確に計算するの困難ですが、近似値を求めることはできます。同じピクセルを 1 回しか描画しない場合は、z バッファーリングをオフにし、シーンを後から前にレンダリングすると、最高のパフォーマンスが得られます。