パフォーマンスの最適化

パフォーマンスの最適化

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

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

次に示す一般的なガイドラインに従って、アプリケーションのパフォーマンスを向上させるとよい。

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

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

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

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

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

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

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

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

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

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

  • アプリケーションで AGP (Accelerated Graphics Port) メモリ内の頂点バッファへのランダム アクセスが必要な場合は、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 
    

データベースおよびカリング

Microsoft® 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 のテクスチャが必要な場合を除いては、そのようなテクスチャを使用してはならない。

動的テクスチャの使い方

ドライバが動的テクスチャをサポートしているかどうかを確認するには、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 を指定した場合、またはフラグを指定しなかった場合は、常に頂点がグラフィックス プロセッサと適切に同期するが、フラグを指定せずにロックを実行すると、上記のとおりパフォーマンスの低下をもたらす可能性がある。BeginSceneEndScenePresent などのその他の 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% が実際にストリップを形成しているものとして検出し、それに応じて調整を行う。多くのドライバは、レガシー ハードウェアに対してもこのような処理を行う。

Direct3D エクステンション (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 バッファリングをオフにし、シーンを後から前にレンダリングすると、最高のパフォーマンスが得られる。