问题非常经典,为什么应该尽量减少 Draw Call 是图形渲染优化中的核心问题之一。简单来说,Draw Call 的瓶颈并不仅仅是数据量,而是它对 CPU/GPU 协同工作 和 渲染流水线效率 的影响。以下我们详细分析 Draw Call 的瓶颈来源、合并 Draw Call 提高效率的原因。
1. 什么是 Draw Call?
Draw Call 是 CPU 向 GPU 发出的绘制指令(如绘制一个模型或一组顶点)。每次 Draw Call 都需要 CPU 和 GPU 之间进行通信和协调,涉及以下内容:
CPU 准备数据:CPU 需要计算和准备渲染所需的数据(如变换矩阵、材质参数等)。API 调用:CPU 调用图形 API(如 OpenGL、DirectX、Vulkan)将绘制命令传递给 GPU。GPU 执行绘制:GPU 接收到命令后开始绘制对应的图元。一个场景中可能包含数百甚至数千个物体,每个物体对应一次或多次 Draw Call。如果 Draw Call 过多,会导致 CPU 和 GPU 的工作效率大幅下降。
2. Draw Call 的瓶颈在哪里?
(1)CPU 的负担:Draw Call 是一种昂贵的操作
Draw Call 本身涉及大量的 CPU 操作,包括:
指令记录:每个 Draw Call 都需要 CPU 调用图形 API 函数(如 glDrawElements 或 ID3D12GraphicsCommandList::DrawIndexed)。状态切换(State Change):在发出 Draw Call 之前,CPU 通常需要设置 GPU 的状态(如绑定纹理、着色器、顶点缓冲区等),状态切换非常耗时。命令缓冲区管理:Draw Call 需要将指令写入 GPU 的命令缓冲区,这涉及线程同步和驱动程序开销。瓶颈:CPU 的执行速度远远低于 GPU,并且 Draw Call 是单线程执行的。过多的 Draw Call 会导致 CPU 成为性能瓶颈,GPU 无法高效工作。
(2)GPU 的流水线被频繁打断
GPU 的渲染流水线是一种高度并行的结构,但 Draw Call 过多时会频繁中断流水线的工作:
状态切换的影响:每次 Draw Call 可能需要切换 GPU 的状态(如绑定新的纹理、切换着色器、改变渲染目标等)。这些切换会导致流水线暂停,影响 GPU 的并行计算。命令缓冲提交:每次 Draw Call 都需要从 CPU 向 GPU 提交指令,增加了命令传输的延迟。瓶颈:GPU 的高效性依赖于连续的流水线工作,频繁的 Draw Call 会导致流水线无法保持满负荷运行。
(3)CPU/GPU 协同的延迟
现代图形渲染是 CPU 和 GPU 的协同工作,但 Draw Call 的高频率会导致两者失去平衡:
CPU 瓶颈:如果 CPU 花太多时间处理 Draw Call,GPU 会因为等待指令而闲置。GPU 瓶颈:如果 GPU 的流水线因频繁的状态切换而停顿,CPU 的指令发送速度也会受到影响。瓶颈:CPU 和 GPU 之间的同步开销(如驱动程序、命令提交)会进一步增加性能损失。
3. 合并 Draw Call 为何能提高效率?
将多个 Draw Call 合并为一个,可以显著减少 CPU 和 GPU 的负担,主要原因如下:
(1)减少 CPU 开销
指令调用次数减少:每次 Draw Call 都需要调用图形 API,而合并后只需要一次调用,显著减少 CPU 的调用开销。状态切换减少:合并后多个物体可以共享同样的状态(如材质、纹理、着色器等),减少了状态切换的次数。命令缓冲区操作减少:合并后,CPU 只需要将一组命令写入缓冲区,而不是多次写入。(2)减少 GPU 状态切换
流水线连续性更强:多个物体合并为一个 Draw Call 后,GPU 的流水线可以连续处理这些物体,而不需要频繁暂停。批量处理并行化:GPU 更擅长处理大批量的顶点和像素数据,合并 Draw Call 后可以充分利用 GPU 的并行计算能力。(3)减少 CPU/GPU 协同延迟
指令传输更高效:合并后,CPU 向 GPU 提交的命令更少,减少了 CPU 和 GPU 之间的通信延迟。更高的批处理效率:通过合并多个小任务为一个大任务,可以显著提升整个系统的效率。4. Draw Call 合并的常用方法
(1)实例化渲染(Instancing)
如果场景中有多个相同的物体(如草地、树木、敌人),可以使用实例化渲染技术(如 glDrawElementsInstanced)。实例化允许通过一个 Draw Call 渲染多个实例,显著减少指令数量。(2)合并网格(Mesh Merging)
将多个小的网格合并为一个大网格,并在着色器中通过索引或实例数据区分物体。例如,将一组相邻的物体(如静态建筑物)合并为一个网格。(3)纹理合并(Texture Atlas)
将多个纹理合并为一张大纹理(称为 Texture Atlas),减少纹理绑定次数。例如,将一组小的 UI 图标合并为一张大图。(4)批处理(Batching)
对具有相同材质、纹理和着色器的物体进行分组,并在同一个 Draw Call 中绘制这些物体。批处理通常由引擎自动完成,例如 Unity 的静态批处理(Static Batching)。5. 你的猜测是否正确?
1. 合并之后不会打断 GPU 的流水线
完全正确!
GPU 的效率依赖于流水线的连续性。Draw Call 合并后,GPU 不需要频繁暂停和切换状态,从而可以更高效地处理数据。
2. 减少 State Change 的次数
也正确!
状态切换(State Change,如纹理绑定、着色器切换)是 GPU 的高开销操作。Draw Call 合并后,多个物体可以共享相同的状态,显著减少了状态切换次数。
3. Draw Call 的主要瓶颈
Draw Call 的瓶颈主要来源于:
CPU 的高调用开销:每个 Draw Call 都需要 CPU 进行复杂的准备工作。GPU 的流水线中断:频繁的状态切换和命令提交会降低 GPU 的效率。CPU 和 GPU 的协同延迟:Draw Call 过多会导致两者的协同效率下降。6. 总结:为什么应该减少 Draw Call?
减少 CPU 开销:Draw Call 是一种昂贵的 CPU 操作,过多的 Draw Call 会导致 CPU 成为性能瓶颈。提高 GPU 效率:合并 Draw Call 可以减少状态切换,让 GPU 的流水线保持连续工作。减少同步延迟:CPU 和 GPU 的协同效率提高,避免两者因频繁通信而浪费时间。更好利用硬件资源:GPU 是高度并行的硬件,合并 Draw Call 后可以更充分地发挥其性能潜力。因此,减少 Draw Call 不是为了减少数据量,而是为了减少 CPU 和 GPU 的不必要开销,让渲染管线更加高效。