效能優化 (Direct3D 9)
每個建立使用 3D 圖形之即時應用程式的開發人員都擔心效能優化。 本節提供從程序代碼取得最佳效能的指導方針。
- 一般效能秘訣
- 資料庫和 Culling
- 批處理基本類型
- 光源提示
- 紋理大小
- 矩陣轉換
- 使用動態紋理
- 使用動態頂點和索引緩衝區
- 使用網格
- Z 緩衝區效能
一般效能秘訣
- 請只在您必須時清除。
- 將狀態變更最小化,並將其餘狀態變更分組。
- 如果可以這麼做,請使用較小的紋理。
- 從正面到後面繪製場景中的物件。
- 使用三角形條紋,而不是清單和風扇。 為了獲得最佳頂點快取效能,請排列等量以更快重複使用三角形頂點,而不是更新版本。
- 適當地降低需要不成比例的系統資源分享的特殊效果。
- 持續測試應用程式的效能。
- 最小化頂點緩衝區參數。
- 盡可能使用靜態頂點緩衝區。
- 針對靜態物件使用每個 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 效能的關鍵。 這比點陣化或硬體的改善更重要。
您應該維持您可以管理的最低多邊形計數。 從一開始就建置低多邊形模型,以設計低多邊形計數。 如果您可以這麼做,而不犧牲開發程式中的效能,請新增多邊形。 請記住,最快的多邊形是您不繪製的多邊形。
批處理基本類型
若要在執行期間獲得最佳轉譯效能,請嘗試在批次中使用基本類型,並盡可能降低轉譯狀態變更的數目。 例如,如果您有具有兩個紋理的物件,請將使用第一個紋理的三角形分組,並遵循必要的轉譯狀態來變更紋理。 然後將所有使用第二個紋理的三角形分組。 Direct3D 最簡單的硬體支援是透過硬體抽象層 (HAL) 以批次的轉譯狀態和基本類型批次來呼叫。 更有效地批處理指令,執行期間會執行較少的 HAL 呼叫。
光源提示
因為燈光會將每個頂點成本新增至每個轉譯的畫面,因此您可以仔細考慮如何在應用程式中使用這些畫面來大幅提升效能。 下列大部分秘訣都衍生自 maxim:「最快的程式代碼是永遠不會呼叫的程序代碼」。
- 盡可能使用最少的光源。 例如,若要增加整體光源等級,請使用環境光線,而不是新增新的光源。
- 方向燈比點燈或聚光燈更有效率。 針對方向燈,光線的方向是固定的,不需要根據每個頂點計算。
- 聚光燈比點燈更有效率,因為快速計算光錐外的區域。 聚光燈是否更有效率取決於聚光燈照亮您的場景數量。
- 使用 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::Lock 或 IDirect3DIndexBuffer9::Lock。 D3DLOCK_DISCARD表示應用程式不需要將舊的頂點或索引數據保留在緩衝區中。 如果在使用 D3DLOCK_DISCARD呼叫lock時,圖形處理器仍在使用緩衝區,則會傳回記憶體新區域的指標,而不是舊的緩衝區數據。 這可讓圖形處理器在應用程式將數據放在新的緩衝區時繼續使用舊數據。 應用程式中不需要額外的記憶體管理;當圖形處理器完成時,舊的緩衝區會自動重複使用或終結。 請注意,鎖定具有D3DLOCK_DISCARD的緩衝區一律會捨棄整個緩衝區,指定非零位移或有限大小欄位並不會保留緩衝區解除鎖定區域中的資訊。
在某些情況下,應用程式需要儲存每個鎖定的數據量很小,例如新增四個頂點來轉譯 Sprite。 D3DLOCK_NOOVERWRITE表示應用程式不會覆寫已在動態緩衝區中使用的數據。 鎖定呼叫會傳回舊數據的指標,讓應用程式在頂點或索引緩衝區的未使用區域中新增數據。 應用程式不應該修改繪圖作業中使用的頂點或索引,因為圖形處理器可能仍在使用中。 然後,應用程式應該在動態緩衝區滿後使用D3DLOCK_DISCARD來接收新的記憶體區域,在圖形處理器完成之後捨棄舊的頂點或索引數據。
異步查詢機制有助於判斷圖形處理器是否仍在使用頂點。 在最後一次使用頂點的 DrawPrimitive 呼叫之後,發出類型為 D3DQUERYTYPE_EVENT的查詢。 IDirect3DQuery9::GetData 傳回S_OK時,頂點不再使用中。 鎖定具有D3DLOCK_DISCARD或沒有旗標的緩衝區一律會保證頂點與圖形處理器正確同步處理,不過使用沒有旗標的鎖定將會產生先前所述的效能損失。 其他 API 呼叫,例如 IDirect3DDevice9::BeginScene、IDirect3DDevice9::EndScene,以及 IDirect3DDevice9::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 緩衝,並將場景從背面轉譯為正面,以達到最佳效能。
相關主題