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

3D グラフィックスを使用するリアルタイム アプリケーションを作成するすべての開発者は、パフォーマンスの最適化を懸念しています。 このセクションでは、コードから最高のパフォーマンスを得るためのガイドラインを示します。

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

  • 必要な場合にのみクリアします。
  • 状態の変更を最小限に抑え、残りの状態の変更をグループ化します。
  • 可能な場合は、小さなテクスチャを使用します。
  • シーン内のオブジェクトを前面から背面に描画します。
  • リストやファンの代わりに三角形のストリップを使用します。 頂点キャッシュのパフォーマンスを最適にするには、後でなく、三角形の頂点を再利用するようにストリップを配置します。
  • システム リソースの不均衡な共有を必要とする特殊効果を適切に低下させます。
  • アプリケーションのパフォーマンスを常にテストします。
  • 頂点バッファー スイッチを最小限に抑えます。
  • 可能な場合は静的頂点バッファーを使用します。
  • FVF ごとに 1 つの大きな静的頂点バッファーを、オブジェクトごとに 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 の優れたパフォーマンスの鍵となります。 ラスタライズやハードウェアの改善よりも重要です。

管理できる可能性のある最も低いポリゴン数を維持する必要があります。 最初から低ポリゴン モデルを構築して、低ポリゴン数を設計します。 開発プロセスの後半でパフォーマンスを犠牲にすることなく、これを行うことができる場合は、ポリゴンを追加します。 最も高速なポリゴンは描画しないポリゴンです。

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

実行中に最適なレンダリング パフォーマンスを得るには、バッチでプリミティブを操作し、レンダリング状態の変更の数を可能な限り少ないようにしてください。 たとえば、2 つのテクスチャを持つオブジェクトがある場合は、最初のテクスチャを使用する三角形をグループ化し、テクスチャを変更するために必要なレンダリング状態に従います。 次に、2 番目のテクスチャを使用するすべての三角形をグループ化します。 Direct3D の最も簡単なハードウェア サポートは、ハードウェア抽象化レイヤー (HAL) を介してレンダリング状態のバッチとプリミティブのバッチで呼び出されます。 命令をより効果的にバッチ処理すると、実行中に実行される HAL 呼び出しが少なくなります。

照明のヒント

ライトはレンダリングされた各フレームに頂点ごとのコストを追加するため、アプリケーションでの使用方法に注意することでパフォーマンスを大幅に向上させることができます。 次のヒントのほとんどは、「最も高速なコードは、決して呼び出されないコードです」という最大のヒントから派生しています。

  • できるだけ少ない光源を使用してください。 たとえば、全体的な照明レベルを上げるには、新しい光源を追加する代わりにアンビエント ライトを使用します。
  • ディレクショナル ライトは、ポイント ライトやスポットライトよりも効率的です。 ディレクショナル ライトの場合、ライトへの方向は固定され、頂点ごとに計算する必要はありません。
  • スポットライトは、ライトの円錐の外側の領域が迅速に計算されるため、ポイント ライトよりも効率的です。 スポットライトの効率が高いかどうかは、スポットライトによって照明されるシーンの量によって異なります。
  • 範囲パラメーターを使用して、照明する必要があるシーンの部分のみにライトを制限します。 すべてのライトタイプは、範囲外になるとかなり早く終了します。
  • 反射ハイライトは、ライトのコストのほぼ 2 倍になります。 必要な場合にのみ使用してください。 可能な限り、D3DRS_SPECULARENABLEレンダリング状態を既定値の 0 に設定します。 マテリアルを定義するときは、そのマテリアルの反射ハイライトをオフにするには、反射パワー値を 0 に設定する必要があります。反射色を 0,0,0 に設定するだけでは十分ではありません。

テクスチャ サイズ

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

  • テクスチャを小さくします。 テクスチャが小さいほど、メイン CPU のセカンダリ キャッシュで維持される可能性が高くなります。
  • プリミティブごとにテクスチャを変更しないでください。 使用するテクスチャの順序でポリゴンをグループ化し続けてください。
  • 可能な限り正方形のテクスチャを使用します。 サイズが 256 x 256 のテクスチャが最も高速です。 たとえば、アプリケーションで 4 つの 128 x 128 テクスチャを使用する場合は、同じパレットを使用し、すべてを 1 つの 256 x 256 テクスチャに配置してみてください。 この手法により、テクスチャスワップの量も減ります。 もちろん、256 x 256 テクスチャは、前述のようにテクスチャを可能な限り小さくする必要があるため、アプリケーションでそれほど多くのテクスチャを必要とする場合を除き、使用しないでください。

行列変換

Direct3D は、ユーザーが設定したワールド行列とビュー行列を使って、いくつかの内部データ構造を構成します。 新しいワールド行列またはビュー行列を設定するたびに、関連付けられた内部構造がシステムによって再計算されます。 これらのマトリックスを頻繁に設定する (フレームあたり数千回など) には、計算に時間がかかります。 必要な計算回数を最小限に抑えるには、ワールド行列とビュー行列を連結して 1 つのワールド ビュー行列を作成し、それをワールド行列として設定した後、ビュー行列を単位元に設定します。 このとき、必要に応じてワールド行列を変更、連結、リセットできるように、個々のワールド行列とビュー行列のキャッシュ コピーを保持しておくことをお勧めします。 このドキュメントをわかりやすくするために、Direct3D サンプルでは、この最適化を採用することはめったにありません。

動的テクスチャの使用

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

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

  • これらは管理できません。 たとえば、プールをD3DPOOL_MANAGEDすることはできません。
  • 動的テクスチャは、D3DPOOL_DEFAULTで作成された場合でもロックできます。
  • D3DLOCK_DISCARDは、動的テクスチャの有効なロック フラグです。

動的テクスチャは、形式ごとに 1 つだけ作成し、サイズごとに作成することをお勧めします。 動的ミップマップ、キューブ、ボリュームは、すべてのレベルをロックするオーバーヘッドが増えるので、推奨されません。 ミップマップの場合、D3DLOCK_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();
}

動的頂点バッファーとインデックス バッファーの使用

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

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

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

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

非同期クエリ メカニズムは、頂点がグラフィックス プロセッサでまだ使用されているかどうかを判断するのに役立ちます。 頂点を使用する最後の DrawPrimitive 呼び出しの後に、D3DQUERYTYPE_EVENT型のクエリを発行します。 IDirect3DQuery9::GetData がS_OKを返すとき、頂点は使用されなくなりました。 D3DLOCK_DISCARDまたはフラグなしのバッファーをロックすると、常に頂点がグラフィックス プロセッサと正しく同期されることを保証しますが、フラグなしのロックを使用すると、前に説明したパフォーマンスが低下します。 IDirect3DDevice9::BeginSceneIDirect3DDevice9::EndSceneIDirect3DDevice9::P resent などの他の 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 オプションを使用して、属性のメッシュ頂点と面をグループ化するオプションがあります。 これが完了すると、mesh オブジェクトは ID3DXBaseMesh::GetAttributeTable を呼び出すことによって、アプリケーションによって取得できる属性テーブルを計算します。 メッシュが属性で並べ替えされていない場合、この呼び出しは 0 を返します。 アプリケーションは ID3DXMesh::Optimize メソッドによって生成されるため、属性テーブルを設定する方法はありません。 属性の並べ替えはデータの機密性が高いので、メッシュが属性の並べ替えであることをアプリケーションで認識している場合でも、 ID3DXMesh::Optimize を呼び出して属性テーブルを生成する必要があります。

次のトピックでは、メッシュのさまざまな属性について説明します。

属性 ID

属性 ID は、顔のグループを属性グループに関連付ける値です。 この ID では、顔 ID3DXBaseMesh::D rawSubset のサブセットを描画する必要があります。 属性 ID は、属性バッファー内の顔に対して指定されます。 属性 ID の実際の値は、32 ビットに収まるものにすることができますが、n は属性の数である 0 ~ n を使用するのが一般的です。

属性バッファー

属性バッファーは、各顔が属する属性グループを指定する DWORD の配列 (顔ごとに 1 つ) です。 このバッファーは、メッシュの作成時にゼロに初期化されますが、ロード ルーチンによって塗りつぶされるか、ID 0 を持つ複数の属性が必要な場合はユーザーが入力する必要があります。 このバッファーには、 ID3DXMesh::Optimize の属性に基づいてメッシュを並べ替えるために使用される情報が含まれています。 属性テーブルが存在しない場合、 ID3DXBaseMesh::D rawSubset はこのバッファーをスキャンして、描画する特定の属性の顔を選択します。

属性テーブル

属性テーブルは、メッシュによって所有および維持される構造です。 生成する唯一の方法は、属性の並べ替えまたはより強力な最適化を有効にして ID3DXMesh::Optimize を呼び出すことです。 属性テーブルは、 ID3DXBaseMesh::D rawSubset への 1 回の描画プリミティブ呼び出しをすばやく開始するために使用されます。 もう 1 つの用途は、進行メッシュでもこの構造が維持されるため、現在の詳細レベルでアクティブになっている面と頂点を確認できることです。

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

アプリケーションでは、シーンが前面から順にレンダリングされるように処理することで、z バッファー処理を使用する際のパフォーマンスを向上させることができます。 テクスチャ処理された z バッファーのプリミティブは、スキャン ライン ベースで z バッファーについて事前にテストされます。 スキャン ラインが以前にレンダリングされた多角形によって隠れる場合、システムは迅速かつ効率的にそれを拒否します。 z バッファー処理によって、パフォーマンスを向上させることもできますが、この手法はシーンで同じピクセルを複数回描画するときに最も役に立ちます。 これを厳密に計算することは困難ですが、多くの場合、非常に近い結果を得ることができます。 同じピクセルの描画が 2 回未満である場合は、z バッファー処理を無効にして、背面から順にシーンをレンダリングすることによって、最適なパフォーマンスを実現できます。

プログラミングのヒント