(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 是不够的。

纹理大小

纹理映射性能在很大程度上取决于内存的速度。 有多种方法可以最大程度地提高应用程序纹理的缓存性能。

  • 使纹理保持较小。 纹理越小,在main 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::LockIDirect3DIndexBuffer9::Lock 。 D3DLOCK_DISCARD指示应用程序不需要在缓冲区中保留旧的顶点或索引数据。 如果使用 D3DLOCK_DISCARD 调用 lock 时图形处理器仍在使用缓冲区,则返回指向内存新区域的指针,而不是旧缓冲区数据。 这样,当应用程序将数据放入新缓冲区时,图形处理器就可以继续使用旧数据。 应用程序中不需要额外的内存管理;当图形处理器完成旧缓冲区时,会自动重复使用或销毁旧缓冲区。 请注意,使用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 缓冲和从后往前渲染场景实现最佳性能。

编程提示