外观
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 NCPU 调用图形 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
→ 指定绘制范围
→ 调用图形 APIRenderer 合批的目标,是让多个兼容对象共享上述状态和缓冲,一次提交更多几何。
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() 验证,不要只看文件名。
实验记录表
| 状态 | 节点数 | DrawCall | Frame Time | 备注 |
|---|---|---|---|---|
| A | 200 | 实测 | 实测 | 同纹理连续 |
| B | 200 | 实测 | 实测 | 纹理交替 |
| C | 200 | 实测 | 实测 | 状态打断 |
实验目标是解释变化,不是追求某个“标准 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 当前版本、平台和渲染后端为准。
- DrawCall 为什么有 CPU 成本? CPU/驱动需要验证状态、绑定资源、准备命令并提交。
- DrawCall 低为什么仍可能 GPU 慢? 单批可以有大量顶点、复杂 Shader、大纹理带宽和高 Overdraw。
- 为什么 Renderer 不能把所有同 Texture Sprite 排到一起? 透明 2D 的绘制顺序影响最终像素,任意重排会破坏视觉。
- 优化 DrawCall 后应验证什么? Frame Time 分解、CPU/GPU、内存、视觉正确性和目标设备稳定结果。
能力验收
你应该能说出一次提交前 CPU 要准备的资源与状态,构造“高 DrawCall、低片元”和“低 DrawCall、高片元”两个反例,并能在优化后同时检查 CPU、GPU、视觉和内存,而不是只汇报 DrawCall。
本课总结
text
DrawCall 是一次绘制提交。
CPU 负责准备状态和发出命令。
GPU 负责执行顶点和片段计算。
DrawCall 是重要指标,但不是唯一指标。十一、下一课预告
text
Lesson049|两个 Sprite 在什么条件下可以合批?