GPU性能抉择:多绘制调用独立顶点缓冲区VS单绘制调用共享顶点缓冲区
问题解答
性能对比
两种方案的性能瓶颈完全不同,没有绝对的优劣,取决于场景中物体的数量、更新频率和数据量:
- 方案一(独立顶点缓冲区):瓶颈在CPU的绘制调用开销。当场景物体数量达到数千甚至上万级时,CPU会被大量DrawCall的提交逻辑占满,GPU反而可能处于空闲状态。但该方案带宽占用低,仅传输变化物体的数据,维护逻辑简单,适合变化频繁但总数量较少的场景。
- 方案二(共享大型顶点缓冲区):瓶颈在内存带宽。每帧传输整个缓冲区会浪费大量带宽(尤其是未变化数据占比高时),可能拖慢GPU数据接收效率。虽然DrawCall数量极低,但三角数可变的物体需要频繁调整缓冲区布局,维护复杂度高,仅适合所有物体都频繁变化的极端场景。
业界推荐实践
业界普遍采用按物体属性分组的混合策略,而非一刀切选择单一方案:
- 极少变化、三角数固定的物体:打包到静态顶点缓冲区,一次性上传至GPU显存(Metal用
MTLResourceStorageModePrivate,DirectX用D3D12_HEAP_TYPE_DEFAULT),后续不再更新。通过实例化绘制或批次绘制减少DrawCall。 - 变化频繁、三角数固定的物体:使用动态顶点缓冲区,仅更新每帧变化的区域;或复用顶点数据,将物体变换参数单独存入实例缓冲区/常量缓冲区,只传输变换数据。
- 三角数可变的物体:采用独立小型动态缓冲区,或用间接绘制处理,避免CPU端反复计算绘制参数。
折中方案(兼顾低带宽与少DrawCall)
核心思路是分组批次绘制+动态局部更新,既减少DrawCall数量,又避免传输未变化数据,同时适配Metal和DirectX:
Metal 实现细节
- 分组缓冲区管理
- 静态物体组:创建
MTLBuffer时设置storageMode = MTLResourceStorageModePrivate,一次性上传所有顶点数据,通过drawPrimitives做批次绘制,或用实例化绘制(drawPrimitives:instanceCount:),将变换参数存入实例缓冲区。 - 频繁变化固定三角数物体组:创建
MTLBuffer时设置storageMode = MTLResourceStorageModeManaged,预留足够空间。每帧用replaceBytes:inRange:withBytes:更新变化物体对应的局部区域,再通过一次drawPrimitives绘制整个组(通过顶点起始位置/索引缓冲区分组)。 - 三角数可变物体组:使用间接命令缓冲区(
MTLIndirectCommandBuffer),CPU每帧更新绘制参数(顶点数、实例数等),GPU执行间接绘制,降低CPU提交开销。
- 静态物体组:创建
- 实例化优化
对于顶点数据相同、仅变换不同的物体(如大量相同立方体),顶点缓冲区仅存一份基础顶点数据,将每个物体的模型矩阵存入实例缓冲区,每帧仅更新变化的矩阵,通过一次实例化绘制完成所有同类型物体渲染。
DirectX 实现细节
- 分组缓冲区管理
- 静态物体:用
D3D12_HEAP_TYPE_DEFAULT创建缓冲区,上传后不再修改,通过DrawIndexedInstanced做实例化批次绘制。 - 频繁变化固定三角数物体组:用
D3D12_HEAP_TYPE_UPLOAD创建动态缓冲区,通过Map/Unmap更新局部区域,再做批次绘制;或用结构化缓冲区存储变换数据,复用顶点数据做实例化绘制。 - 三角数可变物体组:使用
DrawIndexedInstancedIndirect间接绘制,CPU端更新D3D12_DRAW_INDEXED_INSTANCED_ARGUMENTS结构体到间接缓冲区,由GPU执行绘制。
- 静态物体:用
- 可见性剔除配合
结合视锥体剔除、遮挡剔除,仅绘制可见物体,减少需要处理的物体数量;对可见的同组物体合并批次,进一步降低DrawCall。
总结
最优方案是针对不同类型物体采用对应策略的混合模式:
- 静态物体:静态缓冲区+实例化/批次绘制,一次上传终身使用,DrawCall极少。
- 频繁变化固定三角数物体:动态缓冲区局部更新+批次绘制,平衡带宽与DrawCall开销。
- 三角数可变物体:独立动态缓冲区+间接绘制,灵活处理顶点数变化,降低CPU负担。
内容的提问来源于stack exchange,提问作者user1548418
相关产品推荐
相关产品推荐

