[转载]为DrawPrimitiveUP(DrawUserPrimitive)洗冤

在做D3D画线时,处理顶点缓冲发现的问题!所以转一下

最初只因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剁几下就行了。另外注意测试的时候一定要用硬件模式测,软件模式的结果是完全不同的。

发表评论

电子邮件地址不会被公开。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>