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、多维数据集和卷,因为锁定每个级别会产生额外的开销。 对于 mipmap,D3DLOCK_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();
}

使用动态顶点和索引缓冲区

当图形处理器使用缓冲区时锁定静态顶点缓冲区可能会产生显著的性能损失。 锁调用必须等待,直到图形处理器完成从缓冲区读取顶点或索引数据,然后才能返回到调用应用程序,这是一个重大延迟。 在每个帧中锁定然后从静态缓冲区进行多次呈现也会阻止图形处理器缓冲呈现命令,因为它必须在返回锁指针之前完成命令。 如果没有缓冲命令,图形处理器将保持空闲状态,直到应用程序完成填充顶点缓冲区或索引缓冲区并发出呈现命令。

理想情况下,顶点或索引数据永远不会更改,但这并不总是可能的。 在许多情况下,应用程序需要更改每个帧的顶点或索引数据,甚至每个帧多次。 对于这些情况,应使用D3DUSAGE_DYNAMIC创建顶点或索引缓冲区。 此使用标志会导致 Direct3D 优化频繁锁定操作。 仅当缓冲区频繁锁定时,D3DUSAGE_DYNAMIC才有用;保留常量的数据应放置在静态顶点或索引缓冲区中。

若要在使用动态顶点缓冲区时收到性能改进,应用程序必须使用相应的标志调用 IDirect3DVertexBuffer9::Lock 或 IDirect3DIndexBuffer9::Lock。 D3DLOCK_DISCARD指示应用程序不需要在缓冲区中保留旧的顶点或索引数据。 如果在使用 D3DLOCK_DISCARD 调用锁时图形处理器仍在使用缓冲区,则返回指向新内存区域的指针,而不是旧缓冲区数据。 这样,图形处理器就可以继续使用旧数据,而应用程序将数据放在新缓冲区中。 应用程序中不需要额外的内存管理;当图形处理器完成图形处理器时,旧缓冲区会自动重复使用或销毁。 请注意,锁定具有D3DLOCK_DISCARD的缓冲区始终丢弃整个缓冲区,指定非零偏移量或有限的大小字段不会在缓冲区的解锁区域中保留信息。

在某些情况下,应用程序需要存储每个锁的数据量很小,例如添加四个顶点来呈现子画面。 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选项对属性上的网格顶点和人脸进行分组。 完成此操作后,网格对象将计算应用程序可以通过调用 ID3DXBaseMesh::GetAttributeTable 获取的属性表。 如果未按属性对网格进行排序,则此调用将返回 0。 应用程序无法设置属性表,因为它由 ID3DXMesh::Optimize 方法生成。 属性排序是数据敏感的,因此,如果应用程序知道网格已排序,则仍需要调用 ID3DXMesh::Optimize 来生成属性表。

以下主题介绍网格的不同属性。

属性 ID

属性 ID 是一个值,该值将一组人脸与属性组相关联。 此 ID 描述应绘制哪些人脸 ID3DXBaseMesh::D rawSubset 子集。 为属性缓冲区中的人脸指定属性 ID。 属性 ID 的实际值可以是适合 32 位的任何值,但通常使用 0 到 n,其中 n 是属性数。

属性缓冲区

属性缓冲区是 DWORD 数组, (每张人脸) 一个,指定每个人脸所属的属性组。 此缓冲区在创建网格时初始化为零,但由加载例程填充,或者如果需要 ID 0 的多个属性,则必须由用户填充。 此缓冲区包含用于根据 ID3DXMesh::Optimize 中的属性对网格进行排序的信息。 如果没有属性表, ID3DXBaseMesh::D rawSubset 将扫描此缓冲区以选择要绘制的给定属性的人脸。

属性表

属性表是由网格拥有和维护的结构。 生成一个方法的唯一方法是调用 ID3DXMesh::Optimize ,并启用属性排序或更强优化。 特性表用于快速启动对 ID3DXBaseMesh::D rawSubset 的单一绘制基元调用。 唯一的另一个用途是,正在推进的网格也保持此结构,因此可以在当前的详细信息级别查看哪些人脸和顶点处于活动状态。

Z 缓冲区性能

通过确保按从前往后的顺序渲染场景,应用程序可以提高使用 z 缓冲和纹理时的性能。 以扫描线为基础,针对 z 缓冲区对纹理化的 z 缓冲的基元进行了预测试。 如果扫描线被之前渲染的多边形隐藏,系统会快速高效地拒绝。 Z 缓冲可以提高性能,但当场景超过一次绘制相同像素时,这一技术最为有用。 这难以精确计算,但通常可以进行近似计算。 如果相同像素的绘制次数少于两次,你可以通过关闭 z 缓冲和从后往前渲染场景实现最佳性能。

编程提示