Skip to content

Lesson 048|DrawCall 是什么?CPU 在提交什么? ​

Tags: #Creator2.x #Stage04 #DrawCall #Batch #CPUDifficulty: ⭐⭐⭐☆☆


真实问题:DrawCall 从 80 降到 20,帧率为什么没变 ​

团队合并图集,把页面 DrawCall 从 80 降到 20,却发现低端机仍然只有 30 FPS。

进一步测量发现,瓶颈是全屏半透明动画造成的 Overdraw,而不是 CPU 提交。优化确实减少了 DrawCall,但没有击中当前最慢环节。

DrawCall 是一次绘制提交,不是整体性能分数。

🎯 本课目标 ​

本课把常见的 DrawCall 从“一个数字”还原成一次提交行为:

CPU 告诉 GPU 用什么状态、什么数据、画多少东西。


二、一次 DrawCall 的抽象内容 ​

一次绘制提交通常需要准备:

text
Vertex Buffer
Index Buffer
Material / Shader
Texture / Sampler
Uniform
Render State
绘制数量和范围

CPU 或引擎侧把这些状态绑定并发出绘制命令,GPU 再执行。

“提交 600 个索引”并没有告诉 GPU这些索引属于哪个对象。GPU 只看到当前绑定的资源和一段绘制范围:

text
当前 Shader / Pass
当前 Texture / Sampler
当前 Uniform
当前 Vertex / Index Buffer
当前 Blend / Depth / Stencil
从索引 N 开始画 M 个

业务层的 Sprite 名字、Prefab 层级和脚本组件不会跟着 DrawCall 一起交给 GPU。Renderer 的工作就是把高级场景对象翻译成这组低层命令。


三、DrawCall 为什么有 CPU 成本 ​

每次提交都可能涉及:

  • 检查和设置 GPU 状态。
  • 绑定缓冲和纹理。
  • 更新 Uniform。
  • 调用图形 API。
  • 跨 JS、Native 或渲染线程边界。

因此在低端设备或大量小对象场景中,DrawCall 数量可能成为 CPU 瓶颈。

CPU 提交和 GPU 完成是异步流水:

text
CPU:准备 Frame N+1 的命令
GPU:可能仍在执行 Frame N

CPU 调用图形 API 返回,往往只代表命令已经进入队列,不代表像素已经写完。CPU 太慢时 GPU 等命令;GPU 太慢时队列积压,CPU 最终可能在同步点等待。

所以 DrawCall 同时连接 CPU 和 GPU,但“DrawCall 多”主要是一个待验证的提交成本假设,不是自动成立的卡顿结论。


四、DrawCall 不是三角形数量 ​

text
DrawCall:提交次数
顶点数:提交的数据量
三角形数:几何复杂度
Fragment 数:像素计算量

一个 DrawCall 可以包含很多顶点;一个小 DrawCall 也可能覆盖整屏并产生大量 Fragment。


五、为什么 Renderer 需要排序 ​

为了减少状态切换,Renderer 可能按某些规则组织对象:

text
渲染层
→ 材质
→ 纹理
→ 深度和透明规则

但排序不能破坏正确的前后关系。透明对象尤其需要在“正确显示”和“减少切换”之间取平衡。


六、DrawCall 统计应该怎么用 ​

统计值适合回答:

text
这个场景提交了多少批绘制?
某次改动是否让提交次数暴涨?
哪个页面或设备超出预算?

它不适合单独回答:

text
为什么卡?
GPU 是否空闲?
CPU 是否已经超时?

需要结合 CPU Frame Time、GPU 时间、Overdraw 和内存一起看。

最有价值的用法是做差异对照:

text
改动前:80 DrawCall,CPU 12 ms,GPU 5 ms
改动后:20 DrawCall,CPU 7 ms,GPU 5 ms
→ 提交优化有效,CPU 获益

改动前:80 DrawCall,CPU 4 ms,GPU 18 ms
改动后:20 DrawCall,CPU 3 ms,GPU 18 ms
→ DrawCall 降了,但 GPU 瓶颈仍在

只报告“从 80 降到 20”缺少帧时间和瓶颈证据,无法判断产品体验是否改善。


七、案例:100 个 Sprite 的两种情况 ​

情况 A:兼容状态 ​

text
100 个 Sprite
共享材质和纹理
状态相容
→ 可能合并为少量 DrawCall

情况 B:每个对象独立状态 ​

text
100 个 Sprite
不同材质、纹理或 Blend
→ 可能接近 100 次提交

真正结果要看 Renderer 的批处理规则,不应只根据对象数量猜测。


八、常见误区 ​

误区一:DrawCall 越少一定越快 ​

过度合批可能带来更大的顶点或状态管理成本,GPU 像素压力也可能更高。

误区二:DrawCall 就是 GPU 执行时间 ​

它是提交次数,不等于完整 GPU 时间。

误区三:一个 Sprite 一定对应一个 DrawCall ​

合批、材质、Mask 和 Renderer 类型都会改变结果。

误区四:只看编辑器统计就代表真机结果 ​

平台 GPU、分辨率和驱动差异可能很大。



九、深入推导:CPU 一次提交需要准备什么 ​

高层抽象:

text
选择 Pipeline/Shader/Pass
→ 绑定 Render State
→ 绑定 Texture/Sampler
→ 设置 Uniform/Descriptor
→ 绑定 Vertex/Index Buffer
→ 指定绘制范围
→ 调用图形 API

Renderer 合批的目标,是让多个兼容对象共享上述状态和缓冲,一次提交更多几何。

DrawCall 的 CPU 成本随平台、图形 API、驱动和状态变化不同,不能给出跨项目固定毫秒值。

还要区分“提交命令”和“GPU 已经画完”:

text
CPU 构建并提交命令
→ 命令进入驱动或图形队列
→ GPU 按顺序执行
→ 最终写入 RenderTarget

正常情况下 CPU 与 GPU 可以流水并行。CPU 提交完成只表示命令已经交出去,不表示像素已经完成;当 CPU 产生命令太慢时 GPU 可能等 CPU,当 GPU 执行太慢或队列积压时 CPU 又可能在同步点等待 GPU。

这就是为什么“DrawCall 是 CPU 问题”和“DrawCall 最终让 GPU 工作”可以同时成立。真正要判断的是当前帧哪一端先耗尽预算。

十、DrawCall、三角形和片元是三个轴 ​

text
案例 A:1000 个独立小批次,每批 2 三角形
→ DrawCall 高,三角形少

案例 B:1 个巨大网格,数十万三角形
→ DrawCall 低,顶点/GPU 成本高

还要加入第三轴:片元覆盖。一个两三角形全屏特效也可能很贵。

十一、Creator 2.4.x 实验:相同节点数,不同提交次数 ​

场景 ​

创建 200 个 Sprite,布局相同,准备三个状态:

text
State A:全部同 Texture、同材质、连续顺序
State B:两张 Texture 交替排列
State C:同 Texture,但每隔一个插入 Mask/不同材质

通过 Creator Profiler 记录 DrawCall 与 Frame Time。

DrawCallProbe.ts ​

ts
const { ccclass, property } = cc._decorator;

@ccclass
export default class DrawCallProbe extends cc.Component {
    @property([cc.SpriteFrame])
    frames: cc.SpriteFrame[] = [];

    @property(cc.Node)
    container: cc.Node = null;

    useSingleTexture() {
        for (const child of this.container.children) {
            child.getComponent(cc.Sprite).spriteFrame = this.frames[0];
        }
    }

    alternateTextures() {
        for (let i = 0; i < this.container.childrenCount; i++) {
            const sprite = this.container.children[i]
                .getComponent(cc.Sprite);
            sprite.spriteFrame = this.frames[i % this.frames.length];
        }
    }
}

确保 frames 是否来自同一 Texture 要通过 getTexture() 验证,不要只看文件名。

实验记录表 ​

状态节点数DrawCallFrame Time备注
A200实测实测同纹理连续
B200实测实测纹理交替
C200实测实测状态打断

实验目标是解释变化,不是追求某个“标准 DrawCall”。

十二、Renderer 为什么不能任意排序 ​

为了把相同状态放一起,理论上可以排序;但 2D 透明 UI 的顺序影响最终颜色:

text
Background
→ Character
→ DamageOverlay
→ Foreground

若把所有同纹理对象跨层移动到一起,可能改变遮挡与混合结果。因此 Renderer 只能在不破坏视觉语义的范围内合批。

这也是“资源在同一 Atlas 里却没有完全合批”的常见原因。

十三、真实项目案例:红点节点打断列表批次 ​

结构 ​

text
Item 1:Background → Icon → Label → RedDot
Item 2:Background → Icon → Label → RedDot
...

即使所有 Background 同图集、所有 Icon 同图集,按 Item 交错的渲染顺序会形成反复状态切换。

优化选择:

  • 调整层级,让可交换顺序的同类组件连续;
  • 合并静态装饰;
  • 使用同一 Atlas/材质;
  • 评估 Label 和 RedDot 的状态;
  • 不能牺牲正确遮挡与组件复用盲目重排。

十四、DrawCall 诊断顺序 ​

text
确认统计范围和 Camera
→ 找出批次边界附近对象
→ 对比 Texture 身份
→ 对比 Material/Pass
→ 对比 Blend/Depth/Stencil
→ 检查 Mask/Graphics/Spine/Particle
→ 检查渲染顺序是否允许重排
→ 改一个变量后复测

不要看到数值高就先做大 Atlas。状态不同或顺序交错时,Atlas 可能无效。

十五、练习、推导答案与版本边界 ​

Profiler 的 DrawCall 口径和合批实现以 Creator 2.4.x 当前版本、平台和渲染后端为准。

  1. DrawCall 为什么有 CPU 成本? CPU/驱动需要验证状态、绑定资源、准备命令并提交。
  2. DrawCall 低为什么仍可能 GPU 慢? 单批可以有大量顶点、复杂 Shader、大纹理带宽和高 Overdraw。
  3. 为什么 Renderer 不能把所有同 Texture Sprite 排到一起? 透明 2D 的绘制顺序影响最终像素,任意重排会破坏视觉。
  4. 优化 DrawCall 后应验证什么? Frame Time 分解、CPU/GPU、内存、视觉正确性和目标设备稳定结果。

能力验收 ​

你应该能说出一次提交前 CPU 要准备的资源与状态,构造“高 DrawCall、低片元”和“低 DrawCall、高片元”两个反例,并能在优化后同时检查 CPU、GPU、视觉和内存,而不是只汇报 DrawCall。

本课总结 ​

text
DrawCall 是一次绘制提交。
CPU 负责准备状态和发出命令。
GPU 负责执行顶点和片段计算。
DrawCall 是重要指标,但不是唯一指标。

十一、下一课预告 ​

text
Lesson049|两个 Sprite 在什么条件下可以合批?