Share via


Direct3D 9 (效能優化)

建立使用 3D 圖形的即時應用程式的每個開發人員都會擔心效能優化。 本節提供從程式碼取得最佳效能的指導方針。

一般效能秘訣

  • 只有當您必須清除時才清除。
  • 將狀態變更最小化,並將其餘狀態變更分組。
  • 如果可以這麼做,請使用較小的紋理。
  • 從前到後繪製場景中的物件。
  • 使用三角形帶,而不是清單和風扇。 為了獲得最佳頂點快取效能,請排列等量以更快重複使用三角形頂點,而不是更新版本。
  • 正常降級需要不相等的系統資源分享的特殊效果。
  • 持續測試應用程式的效能。
  • 將頂點緩衝區交換器最小化。
  • 盡可能使用靜態頂點緩衝區。
  • 針對靜態物件使用每個 FVF 一個大型靜態頂點緩衝區,而不是每個物件一個。
  • 如果您的應用程式需要隨機存取 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 中建置物件的可靠資料庫是絕佳效能的關鍵。 比點陣化或硬體的改善更為重要。

您應該維持您可以管理的最低多邊形計數。 從頭開始建置低多邊形模型,以設計低多邊形計數。 如果您可以這麼做,但稍後在開發程式中不犧牲效能,請新增多邊形。 請記住,最快的多邊形是您不會繪製的多邊形。

批次處理基本類型

若要在執行期間獲得最佳轉譯效能,請嘗試在批次中使用基本類型,並盡可能讓轉譯狀態變更的數目保持最低。 例如,如果您有具有兩個紋理的物件,請將使用第一個紋理的三角形分組,並遵循必要的轉譯狀態來變更紋理。 然後將使用第二個紋理的所有三角形分組。 Direct3D 的最簡單硬體支援是透過硬體抽象層 (HAL) ,透過轉譯狀態批次和基本類型批次來呼叫。 更有效率地批處理指示,在執行期間會執行較少的 HAL 呼叫。

光源提示

因為光線會將每個頂點成本新增至每個轉譯的畫面格,所以您可以仔細瞭解如何在應用程式中使用這些畫面,以大幅改善效能。 下列大部分秘訣都衍生自最大值:「最快的程式碼是永遠不會呼叫的程式碼」。

  • 盡可能使用較少的光源。 例如,若要增加整體光源等級,請使用環境光線,而不是新增新的光源。
  • 方向燈比點燈或焦點更有效率。 對於方向燈,光線的方向是固定的,不需要根據每個頂點計算。
  • 焦點比點燈更有效率,因為快速計算光線圓錐之外的區域。 焦點是否更有效率,取決於焦點所點選的場景數量。
  • 使用 range 參數,將光線限制為只有您需要光源的場景部分。 所有光線類型在超出範圍時會相當早結束。
  • 反射醒目提示幾乎會將光線的成本加倍。 只有當您必須使用它們時才使用它們。 盡可能將D3DRS_SPECULARENABLE轉譯狀態設定為 0,預設值。 定義材質時,您必須將反射電源值設定為零,才能關閉該材質的反射醒目提示;只要將反射色彩設定為 0,0,0 就不夠。

紋理大小

紋理對應效能高度相依于記憶體的速度。 有數種方式可將應用程式紋理的快取效能最大化。

  • 讓紋理保持小。 紋理越小,在主要 CPU 的次要快取中維護紋理的機會就越好。
  • 請勿依基本類型變更紋理。 請嘗試依使用之紋理的順序,將多邊形分組。
  • 盡可能使用方形紋理。 維度為 256x256 的紋理是最快的。 例如,如果您的應用程式使用四個 128x128 紋理,請嘗試確保它們使用相同的調色盤,並將其全部放在一個 256x256 紋理中。 這項技術也會減少紋理交換的數量。 當然,除非您的應用程式需要大量紋理,否則您不應該使用 256x256 紋理,因為如前所述,紋理應盡可能保持較小。

矩陣轉換

Direct3D 使用世界和檢視矩陣,讓您設定數個內部資料結構。 每當您設定世界或檢視矩陣,系統會重新計算相關內部結構。 經常設定這些矩陣-例如,每個畫面的數千次 - 是計算耗時的。 您可以串連世界和檢視矩陣成為世界檢視矩陣 (您將其設為世界矩陣),然後將檢視矩陣設為單位矩陣,將所需的計算次數最小化。 保留個別世界和檢視矩陣的快取複本,讓您可以視需要修改、串連並重設世界矩陣。 為了清楚瞭解本檔,Direct3D 範例很少採用此優化。

使用動態紋理

若要瞭解驅動程式是否支援動態紋理,請檢查 D3DCAPS9 結構的D3DCAPS2_DYNAMICTEXTURES旗標。

使用動態紋理時,請記住下列事項。

  • 無法管理它們。 例如,其集區不能D3DPOOL_MANAGED。
  • 即使動態紋理是在D3DPOOL_DEFAULT中建立,仍可鎖定。
  • D3DLOCK_DISCARD是動態紋理的有效鎖定旗標。

最好只為每個格式建立一個動態紋理,而且可能每個大小。 不建議動態 mipmap、Cube 和磁片區,因為鎖定每個層級的額外負荷。 若為 mipmap,D3DLOCK_DISCARD只允許在最上層。 所有層級只要鎖定最上層,即可捨棄所有層級。 磁片區和 Cube 的行為相同。 針對 Cube,最上層和臉部 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::LockIDirect3DIndexBuffer9::Lock 。 D3DLOCK_DISCARD表示應用程式不需要在緩衝區中保留舊的頂點或索引資料。 如果使用 D3DLOCK_DISCARD 呼叫鎖定時,圖形處理器仍在使用緩衝區,則會傳回新記憶體區域的指標,而不是舊的緩衝區資料。 這可讓圖形處理器在應用程式將資料放在新的緩衝區時繼續使用舊資料。 應用程式中不需要額外的記憶體管理;當圖形處理器完成圖形處理器時,舊的緩衝區會自動重複使用或終結。 請注意,鎖定具有D3DLOCK_DISCARD的緩衝區一律會捨棄整個緩衝區,指定非零位移或有限的大小欄位並不會保留緩衝區解除鎖定區域中的資訊。

在某些情況下,應用程式需要儲存每個鎖定的資料量很小,例如新增四個頂點來轉譯 Sprite。 D3DLOCK_NOOVERWRITE表示應用程式不會覆寫已在動態緩衝區中使用的資料。 鎖定呼叫會傳回舊資料的指標,讓應用程式能夠在頂點或索引緩衝區的未使用區域中新增資料。 應用程式不應該修改繪製作業中使用的頂點或索引,因為圖形處理器可能仍在使用中。 然後,應用程式應該在動態緩衝區已滿之後使用D3DLOCK_DISCARD來接收新的記憶體區域,在圖形處理器完成後捨棄舊的頂點或索引資料。

非同步查詢機制有助於判斷頂點是否仍在圖形處理器使用中。 在最後一個使用頂點的 DrawPrimitive 呼叫之後,發出類型為 D3DQUERYTYPE_EVENT 的查詢。 當 IDirect3DQuery9::GetData傳回S_OK時,不再使用頂點。 鎖定具有D3DLOCK_DISCARD或沒有任何旗標的緩衝區一律會保證頂點與圖形處理器正確同步處理,不過使用沒有旗標的鎖定將會產生先前所述的效能負面影響。 其他 API 呼叫,例如 IDirect3DDevice9::BeginSceneIDirect3DDevice9::EndSceneIDirect3DDevice9::P resent ,不保證圖形處理器使用頂點完成。

以下是使用動態緩衝區和適當鎖定旗標的方式。

    // 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 來產生屬性資料表。

下列主題描述網格的不同屬性。

屬性識別碼

屬性識別碼是一個值,可將臉部群組與屬性群組產生關聯。 此識別碼描述應該繪製哪些臉部 ID3DXBaseMesh::D rawSubset 子集。 屬性識別碼是針對屬性緩衝區中的臉部所指定。 屬性識別碼的實際值可以是符合 32 位的任何值,但通常會使用 0 到 n,其中 n 是屬性的數目。

屬性緩衝區

屬性緩衝區是 DWORD 的陣列,每個臉部) (一個,指定每個臉部所屬的屬性群組。 建立網格時,此緩衝區會初始化為零,但會由負載常式填滿,或者如果需要識別碼為 0 的多個屬性,則必須由使用者填入。 此緩衝區包含用來根據 ID3DXMesh::Optimize中的屬性來排序網格的資訊。 如果沒有屬性資料表存在, ID3DXBaseMesh::D rawSubset 會掃描此緩衝區,以選取要繪製之指定屬性的臉部。

屬性資料表

屬性資料表是由網格所擁有和維護的結構。 唯一要產生的方法,就是呼叫 ID3DXMesh::Optimize 並啟用屬性排序或更強大的優化。 屬性資料表可用來快速起始 ID3DXBaseMesh::D rawSubset的單一繪製基本類型呼叫。 唯一的用途是進度網格也會維護此結構,因此可以看到目前詳細資料層級作用中的臉部和頂點。

Z 緩衝區效能

使用 z 緩衝和紋理處理時,應用程式可提高效能,藉由確保場景從前到後轉譯。 有紋理的 z 緩衝基本類型會針對 z 緩衝區預先測試,以掃描列為基礎。 如果掃描列被掀錢轉譯的多邊形隱藏,系統會快速且有效率地拒絕它。 Z 緩衝可以提升效能,但此技術在場景多次繪製相同像素時最實用。 這很難精確計算,但是您通常可以算出近似值。 如果相同像素繪製少於兩次,將 z 緩衝關閉並從後到前轉譯場景,將可達到最佳效能。

程式設計提示