爲DrawPrimitiveUP(DrawUserPrimitive)洗冤

 


最初只因DXSDK文檔裏說了句推薦用Vertex Buffer而不要用DrawPrimitiveUP(C#裏叫DrawUserPrimitive),DrawPrimitiveUP很快被描繪成傳說中的瘟疫,人人都在警告不要接近它。估計有人會想過,既然DrawPrimitiveUP這麼不好,爲什麼還要提供它,難道只是爲了顯示DX也可以像OpenGL一樣簡單地畫三角形?

記得當年我就是抱着這種想法在網上狂搜,功夫不負有心人,還真找到了。不過看了很不好意思,人家上來就批判不實際測試、以訛傳訛的問題,我也比較懶,沒動手測一下。那麼爲了和我一樣的懶人,我把問題用中文解釋一遍。

首先,DrawPrimitiveUP內部其實就是一個dynamic vertex buffer(動態頂點緩衝),和我們自己實現一個動態頂點緩衝沒區別。一般情況下,DrawPrimitiveUP和用動態頂點緩衝的效率也沒多大區別。也就是說DrawPrimitiveUP其實很高效的,而且簡單易用。Irrlicht引擎幾乎所有繪製都用的DrawPrimitiveUP,也很快的。

那爲什麼不推薦用?

原因一:DX8發佈時顯存容量已經有了很大提高,靜態頂點緩衝可以緩存在顯存或AGP內存裏,從而節省帶寬佔用。所以推薦能用靜態頂點緩衝的一定要用靜態的。靜態的可以比DrawPrimitiveUP和動態頂點緩衝都快很多。

原因二:DrawPrimitiveUP相對動態頂點緩衝而言,需要將用戶內存裏的頂點數據複製到內部動態頂點緩衝,即多了一次複製,如果頂點數量較大,複製開銷也會加大。但很多程序裏的動態緩衝設計並不太好,爲了抽象或方便使用,也會複製一次數據。所以便喪失了這條優勢。

原因三:這個比較複雜,我們知道一幀內Batch(批)的數量直接影響CPU的佔用率,1G處理器30FPS下每幀700Batch左右就會佔用100%CPU。每個設置設備狀態到發出繪製命令的轉換都將產生一個Batch。動態頂點緩衝的推薦使用模式是一個可以合併Batch的模式,即不斷地填充頂點數據,但不立刻繪製,在緩衝填滿時才提交繪製一次,當然能合併的前提是各個batch都使用相同的設備狀態,即紋理、材質、RenderStates、變換矩陣等。

原因四:DrawPrimitiveUP只支持一個頂點流。這其實是個不算是原因的原因。當然是只用一個頂點流時才用它。

綜上所述,這其實是個優化問題,用動態頂點緩衝有可能做更多的優化,但如果做得不好,會比DrawPrimitiveUP差。如果正確使用了,但沒有進一步的優化或者引擎的用法不具備可優化的特性,那麼也就和DrawPrimitiveUP效率相當。

但事實上用動態頂點緩衝做錯了的也很多,最常見的就是沒有正確使用Lock標誌位,用鎖定靜態緩衝的方法鎖定,根本得不到動態緩衝的效果。另外用C#和MDX的,如果用返回數組的Lock方法重載,也完全沒有意義,因爲在內部整個緩衝被複制到數組,Unlock時再複製回去。即使用GraphicsStream寫頂點數據也很慢,因爲會導致大量的Boxing,只有直接用指針寫數據才能發揮動態緩衝的優勢。

怎麼纔算做得好,DXSDK裏有明確的樣例,爲了懶人,我帖出來:
    // 用法 1
    // 每次繪製拋棄整個頂點緩衝內容並重新填充幾千個頂點
    // 可能包含多個物體,有可能需要按設備狀態分幾次DrawPrimitive
 
    // 計算需要填充的字節數
    UINT nSizeOfData = nNumberOfVertices * m_nVertexStride;
 
    // 拋棄並重新填充
    CONST DWORD dwLockFlags = D3DLOCK_DISCARD;
   
    // 鎖定頂點緩衝內存
    BYTE* pBytes;
    if( FAILED( m_pVertexBuffer->Lock( 0, 0, &pBytes, dwLockFlags ) ) )
        return false;
   
    // 將頂點數據複製到頂點緩衝
    memcpy( pBytes, pVertices, nSizeOfData );
    m_pVertexBuffer->Unlock();
 
    // 繪製
    m_pDevice->DrawPrimitive( D3DPT_TRIANGLELIST, 0, nNumberOfVertices/3)

    // 用法 2
    // 對多個物體複用一個頂點緩衝
 
    // 計算需要填充的字節數
    UINT nSizeOfData = nNumberOfVertices * m_nVertexStride;
 
    // 如果頂點緩衝內的剩餘空間可以容納要填充的頂點數量,則指定不覆蓋原有數據
    DWORD dwLockFlags = D3DLOCK_NOOVERWRITE;
   
    // 檢查頂點緩衝空間是否用光
    if( m_nNextVertexData > m_nSizeOfVB - nSizeOfData )
    {
        // 沒有足夠的空間,拋棄原有數據重新開始
        dwLockFlags = D3DLOCK_DISCARD;
        m_nNextVertexData = 0;
    }
   
    // 鎖定頂點緩衝內存
    BYTE* pBytes;
    if( FAILED( m_pVertexBuffer->Lock( (UINT)m_nNextVertexData, nSizeOfData,
               &pBytes, dwLockFlags ) ) )
        return false;
   
    // 將頂點數據複製到頂點緩衝
    memcpy( pBytes, pVertices, nSizeOfData );
    m_pVertexBuffer->Unlock();
 
    // 繪製
    m_pDevice->DrawPrimitive( D3DPT_TRIANGLELIST,
               m_nNextVertexData/m_nVertexStride, nNumberOfVertices/3)
 
    // 計算下一次的寫入位置
    m_nNextVertexData += nSizeOfData;

當然,這只是個正確用法樣例,優化起來可還是會面目全非的。如果你的D3D功夫不夠劍豪劍聖級別,大可安心地用DrawPrimitiveUP,對付一些雜碎三角面,也大可不必殺雞用牛刀,用DrawPrimitiveUP剁幾下就行了。另外注意測試的時候一定要用硬件模式測,軟件模式的結果是完全不同的。

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/Nightmare/archive/2007/07/10/1684490.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章