性能优化 (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::LockIDirect3DIndexBuffer9::Lock。 D3DLOCK_DISCARD指示应用程序不需要在缓冲区中保留旧的顶点或索引数据。 如果在使用 D3DLOCK_DISCARD 调用锁时图形处理器仍在使用缓冲区,则会返回指向新内存区域的指针,而不是旧缓冲区数据。 这样,图形处理器就可以在应用程序在新缓冲区中放置数据时继续使用旧数据。 应用程序中不需要额外的内存管理;当图形处理器完成后,旧缓冲区会自动重复使用或销毁。 请注意,锁定具有D3DLOCK_DISCARD的缓冲区始终会丢弃整个缓冲区,指定非零偏移量或有限的大小字段不会保留缓冲区已解锁区域中的信息。

在某些情况下,应用程序需要存储每个锁的数据量很小,例如添加四个顶点来呈现子画面。 D3DLOCK_NOOVERWRITE指示应用程序不会覆盖动态缓冲区中已使用的数据。 锁定调用将返回指向旧数据的指针,允许应用程序在顶点或索引缓冲区的未使用区域中添加新数据。 应用程序不应修改绘图作中使用的顶点或索引,因为它们可能仍在由图形处理器使用。 然后,应用程序应在动态缓冲区满后使用D3DLOCK_DISCARD来接收新内存区域,在图形处理器完成后丢弃旧的顶点或索引数据。

异步查询机制可用于确定图形处理器是否仍在使用顶点。 在最后一次使用顶点的 DrawPrimitive 调用之后发出D3DQUERYTYPE_EVENT类型的查询。 当 IDirect3DQuery9::GetData 返回S_OK时,顶点不再使用。 锁定具有D3DLOCK_DISCARD或无标志的缓冲区将始终保证顶点与图形处理器正确同步,但是使用没有标志的锁会导致前面所述的性能损失。 其他 API 调用(例如 IDirect3DDevice9::BeginSceneIDirect3DDevice9::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 选项对属性的网格顶点和人脸进行分组。 完成此作后,网格对象将计算应用程序可以通过调用 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 缓冲并将场景从后向前呈现来实现最佳性能。

编程提示