2018年的 Session 612 - Metal game performance optimization (原发表于《WWDC18 内参》 主要内容速览:
- Game Performance Template:新的游戏性能分析工具,全方面分析性能指标.
- Frame Pacing(帧率平顺性):避免总是出现一帧快,一帧慢,造成视觉不流畅.
- Thread Priorities(线程优先级):避免系统级功能在后台运行,影响游戏性能.
- Thermal States(热状态):小心功耗过高,造成手机高温,CPU降频引起卡顿.
- Dependency Viewer(依赖关系查看器):帮你找出不必要的任务.
Profiling Tools(分析工具)
- Instruments:包括Game Performance Template
- Xcode Metal Frame Debugger:包括下文讲到的Dependency Viewer
Game Performance Template其实是以前三个工具的整合,并添加了新的线程查看视图
打开后主要界面如下:
其中,黄色方块表示活跃的线程数比CPU物理核心数多,可能需要优化:
下图的线程图中,黄色表示被抢占(preempted),灰色表示被阻塞(blocked)
Frame Pacing(帧率平顺性)
Micro Stuttering(微型口吃)
<抱歉,实在找不到更合适的翻译了,WWDC官方中文也是这么翻译的>
就是帧率的不协调:
- 帧生成时间大于显示器刷新间隔
- 游戏逻辑时间错误
比如下面两个游戏画面,左边的试图以60帧运行,但实际只能达到40帧;右边的则持续稳定在30帧运行:
上图左边帧率高,反而看起来有一卡一顿的现象,这就是Micro Stuttering(微型口吃)导致的,其原理如下图:
这是个常见的三重缓冲,从左侧看,C缓冲在CPU上处理,B缓冲在GPU上处理,而A缓冲用于显示.
- 在
0--1
时间内,由于B缓冲在GPU上还没有处理好,不能交给Display显示,所以A缓冲只好显示了两帧时间0--2
. - 然后Display开始显示B缓冲内容,并将A交给CPU,但C缓冲在
3
之前准备好了,所以B缓冲只显示了一帧时间2--3
. - 接着
3--4
时间内,由于A在GPU上处理时间过长,没有在一帧内完成,所以C缓冲又显示了两帧3--5
. - 又一个循环,同第1步类似.
我们一般的代码就是,尽快向GPU提交生成的图形,这样的代码就可能造成Micro Stuttering(微型口吃):
// Render Scene
//...
// Get drawable and present
if let drawable = view.currentDrawable {
// Render Final Pass
//...
commandBuffer.present(drawable)
}
commandBuffer.commit()
注意:不要使用
usleep()
来让各个帧同步!!!!!
实际开发中的Micro Stuttering
实际开发中,Game Performance Template可以标识出丢帧现象,只要按住Option
拖动,就可看到详情:
这里显示的,和我们前面分析的一模一样:
最佳实践
应该明确指定帧率. 可使用的API(iOS 10.3+)有:
MTLDrawable addPresentedHandler
MTLCommandBuffer presentDrawable afterMinimumDuration
MTLCommandBuffer presentDrawable atTime
// Render Scene
//...
// Get drawable and present at 30 FPS
if let drawable = view.currentDrawable {
// Render Final Pass
//...
let duration = 33.0 / 1000.0 // Duration of 33 ms
commandBuffer.present(drawable, afterMinimumDuration: duration)
}
commandBuffer.commit()
Thread Priorities(线程优先级)
这是一种诡异的卡顿情况,一般是由于有系统任务在后台运行导致的,比如查收电子邮件,iCloud同步等
Thread Stalling(线程停滞)
渲染线程可能因为优先级较低,而被其他线程抢占:
- Priority decay(优先级衰减)
- Priority inversion(优先级反转)
下图灰色表示某些后台任务
实际开发中的Thread Stalling
实际开发中,Game Performance Template工具可以看到该现象:
你会发现GPU空闲,app的CPU各线程也空闲,但物理CPU却忙个不停,造成了卡顿.
点击左侧可以切换显示状态,发现一个线程被抢占了,选中后发现,该线程优先级只有26,于是被系统后台任务itunesstore任务给抢占了CPU时间:
最佳实践
配置渲染线程:
- Priority 45
- 退出监测服务Quality of Service
代码如下:
...
r = pthread_attr_init(&attr);
r = pthread_attr_setschedpolicy(&attr, SCHED_RR); // Opt out of Quality of Service
struct sched_param param = {.sched_priority = 45}; // Configure priority 45
r = pthread_attr_setschedparam(&attr, ¶m); // Set priority
r = pthread_create(&posixThreadID, &attr, &PosixThreadMainRoutine, NULL); r = pthread_attr_destroy(&attr);
...
Thermal States(发热状态)
要从设计开始,就考虑到性能的持续性,不要一开始疯狂高负载,几分钟后降频卡顿.
Thermal Throttling(热节流)
以下都会影响系统的性能:
- 设备温度过高
- 启用了低电量模式
最佳实践
可使用下面的API:
- (iOS11.0+)
NSProcessInfo thermalState
- (iOS 9.0+)
NSProcessInfo lowPowerModeEnabled
- (iOS 10.3+)
MTLCommandBuffer GPUStartTime/GPUEndTime
代码如下:
// Determine thermal state
switch ProcessInfo.processInfo.thermalState {
case .fair:
// Thermals are fair
// Consider taking proactive measures to prevent higher thermals
case .serious:
// Thermals are highly elevated
// Help the system by taking corrective action
case .critical:
// Thermals are extremely elevated
// Help the system by taking immediate corrective action
default:
// Thermals are okay
// Go about your business
// 正常情况
}
过热时调整负载
- 锁定到一个可持续的帧率
- 降低分辨率
- 简化阴影贴图
- 使用更小的纹理
- 降低几何体的细节级别(LOD:level of detail)
- 简化后置处理(post-processing)和特效.
Unnecessary GPU Work(不必要的GPU任务)
被浪费的GPU时间
- 大尺寸的资源
- 无用的GPU工作
最佳实践
对GPU进行性能分析:
- 理解每个渲染特性的花费
- 移除过度的任务
Metal System Trace
可以用Metal System Trace来查看时间消耗:
但是很多时候,你只看到时间很长,却不知道为什么时间长,于是今年苹果引入了别一个工具:Dependency Viewer
Dependency Viewer
它能让你以图表形式查看各个渲染通道里面,到底做了什么事情,而且是每个项目都带有详细的标签来说明:
Demo,找到隐藏的复杂度
你以为一个任务是这样,只有4个pass:
实际上,用Dependency Viewer可以看到有很多隐藏的渲染任务:
总结
- 尽可能早的,经常性的进行性能分析.
- 锁定一个可持续的帧率.
- 设置正确的线程优先级.
- 适应系统负载和热状态.
- 不要向GPU提交无用的任务
WWDC2018相关视频:
- Metal for Game Developers
- Metal for OpenGL Developers
- Metal for VR
- Metal Shader Debugging and Profiling
WWDC往年相关视频: