简介
概述
本文档主要指导开发者如何在马良(Maleoon)GPU上达到最佳的性能表现,只针对Maleoon GPU的优化实践,帮助开发者高效完成渲染任务,如果想要达成该目标,首先需要通过[Graphics Profiler]等GPU分析工具,找到当前的能效瓶颈点,并遵循以下两个基本优化原则进行性能调优。本文所有优化建议,都将围绕这两个基本优化原则展开。
- 结合Maleoon GPU的软硬件架构,让驱动和硬件各模块并发执行,使芯片能力充分释放出来。
- 利用硬件能力高效执行任务,从而降低芯片负载。
图形渲染的基本流程如下图所示。
读者对象
本文档主要适用于以下开发者:
熟悉图形标准API([Vulkan]或[OpenGL ES])、了解shader编码,有一定GPU性能优化基础。
CPU优化
Memory
1. Device Memory分配与释放
vkAllocateMemory为了避免分配的内存没有真正被渲染线程使用,造成内存浪费,采用了延迟分配(protected memory除外)的处理。在调用vkBindImageMemory/vkBindBufferMemory/vkMapMemory(如果内存支持VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT属性)时会真正分配内存,但是需要在此之前调用vkAllocateMemory分配device memory对象。
【推荐】
根据资源需求,选择最为匹配的内存类型进行内存分配,相同类型的memory按照用户实际需求一次分配大块size用于不同类型的资源(比如index buffer、vertex buffer及uniform buffer),可以提升内存申请的效率。
- vkFreeMemory一定要与vkAllocateMemory成对使用,避免内存泄露。
- 绑定的memory资源尽量分时复用。
- 如果存在CPU访问内存,建议使用VK_MEMORY_PROPERTY_HOST_CACHED_BIT进行申请。
【不推荐】
频繁使用vkAllocateMemory。设备内存申请次数支持的最大数量可以通过maxMemoryAllocationCount limits属性获取。
2. D evice Memory访问
按照spec描述的内存类型范围支持4种类型:
- VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT
- VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_CACHED_BIT
- VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT
- VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_PROTECTED_BIT
对于不同类型的memory资源,有不同的使用方法:
-
DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT类型:属于CPU non-cache的内存,最好是用于CPU只写的资源。
-
DEVICE_LOCAL | HOST_VISIBLE | HOST_CACHED类型:HOST_CACHED内存的访问要考虑一致性问题,该类型内存写操作后要使用如下接口进行cache同步操作:
- vkFlushMappedMemoryRanges:CPU修改对GPU可见,CPU到GPU的同步。
- vkInvalidateMappedMemoryRanges:GPU对memory的更新对CPU可见,GPU到CPU的同步。
以上这两个操作对性能都有一定的消耗。
-
DEVICE_LOCAL | LAZILY_ALLOCATED类型:此种类型的内存允许只分配虚拟地址空间而不分配物理页,如果存在内存访问,则按需分配更多的物理页,支持内存增长。
-
DEVICE_LOCAL | PROTECTED类型:Protected memory将内存分成了Protected device memory和Unprotected device memory两种。
- Protected device memory只对device(即GPU)可见,对host(即CPU)不可见。
- Unprotected device memory对device可见,也可以对host可见,取决于host visiable属性。
【推荐】
- DEVICE_LOCAL | HOST_VISIBLE | HOST_COHERENT类型的内存用于CPU只写的资源。
- DEVICE_LOCAL | HOST_VISIBLE | HOST_CACHED类型的内存用于CPU回读的资源。
- DEVICE_LOCAL | LAZILY_ALLOCATED类型的内存只能用于TRANSIENT_ATTACHMENT用途的image。
【不推荐】
从CPU non-cache的内存中频繁回读数据,会影响性能。
Pipeline
Pipeline是整个渲染过程的状态集合,包括可编程的shader和不可编程的fixed function state,其参数配置影响整个渲染过程的执行效率。
1. Shader Stage
【推荐】
- 在shader中如果需要用同一个sampler对同一个纹理多次采样的结果进行加权处理(比如模糊处理)并且权值和坐标是都mediump,建议可以使用base采样坐标加多组偏移的方式进行采样,并且用一层for循环进行多次采样加权操作。base采样坐标、偏移值和加权值需要是uniform,例如编译时常量或者来自uniform buffer(循环次数为编译时常量,最大为64)。
vec2 baseCoord = ve2(0.0);
vec2 offset[4] = {...};
float weight[4] = {...};
vec4 color = vec4(0.0);
for (int i = 0; i < 4; i++) {
color += weight[i] * texture(texSampler, baseCoord + offset[i]);
}
-
shader中如果访问小批量的常量数据,推荐使用push constant。
-
shader中访问uniform buffer array时,索引下标使用编译时常量。
-
shader中访问uniform buffer时,访问的位置使用编译时常量。
-
compute shader的一个workgroup size的三个维度的乘积为32的整数倍。
-
fragment shader中,使用input attribute作为采样坐标去采样的次数小于等于8次,且所有采样用的texture和sampler都放到同一个descriptor set中。
【不推荐】
- 使用tessellation shader和geometry shader。
- create VkDevice时使能VkPhysicalDeviceProtectedMemoryFeatures::protectedMemory(或者VkPhysicalDeviceVulkan11Features::protectedMemory)。
- 需要使能depth test时,fragment shader中使用discard指令。
- 需要使能depth test时,fragment shader中写gl_FragDepth。
- 需要使能depth test时,fragment shader中写了storage image或者storage buffer。
- 需要使能depth test时,fragment shader中写了gl_SampleMask。
- fragment shader中使用highp。
- 需要使能blend时,fragment shader的输出color值与color attachment format的精度不匹配。例如color format是VK_FORMAT_B10G11R11_UFLOAT_PACK32,针对R通道,预期颜色在0.0~1.0f精度范围内时11bit的float可以表示较为精准,但是数值越大能够表示的精度越差。两个相邻像素本来预期的颜色假如是1024.0f和1024.1f,但是11bit可以精确表示1024.0f,但是无法精确表示1024.1f,所以1024.1f被存储成1040.0f,导致视觉效果上产生突变。
2. Fixed State
【推荐】
- 不同的vertex attribute在vertex buffer内连续排布。
- 不同的vertex attribute其所属的vertex buffer的binding号从0开始连续编排。比如有2个vertex buffer,其binding号配置为0和1性能较友好,配置为1和3性能不友好。
- App如果需要提前使用vkCreateGraphicsPipelines进行场景预热,建议create info中(主要是renderpass、pipeline layout、blend state、multisample count、alpha to coverage)要填充真实场景所使用的create info,因为这些info会影响shader的编译,如果预热时填充的create info与真实场景不一致,pipeline cache起不到作用,达不到预热的目的。
【不推荐】
使能depth test时,开启alpha to coverage。
【影响】
使能multisample,消耗多倍GPU运行资源,降低执行效率。
Shader编译
Shader的首次编译(glCompileShader,glLinkProgram)耗时长,若在应用运行时进行实时编译,容易引起卡顿和丢帧,影响用户体验。
【推荐】
建议将Shader编译提前至应用启动时进行。
【不推荐】
在应用运行时实时编译Shader。
Command Buffer
Command buffer usage flags会影响command buffer的执行性能。当使用了SIMULTANEOUS_USE_BIT时,会降低command buffer的执行性能。如果在renderpass内使用secondary command buffer,flags不会有影响。
【推荐】
使用ONE_TIME_SUBMIT_BIT flag创建Command Buffer。
【不推荐】
除了用在renderpass内的secondary command buffer,其他类型的command buffer使用SIMULTANEOUS_USE_BIT。
Draw Call Batching
游戏下发的draw call数量越多,带来的CPU开销越多,整体的性能越差,建议合并draw call来降低CPU侧的开销。
【推荐】
一些典型的Draw call Batching场景:
-
使用shader相同,资源不同
所使用的vertex buffers或者texture等资源变化,这些draw可以通过合并vertex buffer或使用texture arrays等方式来合并。
-
使用shader和Mesh相同
通常绘制石头、树、灌木丛等情况,游戏会使用多个相同几何形状的instance去绘制,这种情况建议使用instance draw来完成。
- 使用Multi-Draw indirect功能
【不推荐】
- 使用大量顶点数少的draw call。
- 频繁切换管线状态,例如program、uniform等,对应相同管线状态的draw call尽量集中处理。
GPU优化
Vertex shading
1. 精度
为了规避Vertex shader计算位置信息的偏差导致后续shader stage误差放大,Maleoon GPU上vertex shader精度统一按照highp实现。
【推荐】
建议使用highp设置。
2. InstanceID
InstanceID经常会参与uniform buffer索引值的计算,此种情况下,Maleoon GPU会根据是否能有效减小load mem的次数从而开启Single InstanceID优化。此优化可以保证每组任务运行时InstanceID一致,从而每组任务load mem只用执行first thread一次load即可拿到整组对应数据。进一步,如果此shader所有uniform不超过1024 bytes大小,此uniform buffer可以完全放在constant register里面,即可通过Maleoon GPU特有的relative constant register代替此load mem操作,性能最优。
【推荐】
- InstanceID参与uniform buffer索引值计算时,uniform buffer中尽量精简只保留有效数据。
- 条件允许下,InstanceID每个实例可以多画一些点,性能收益更大。
- InstanceID参与uniform buffer索引值计算时,尽量不要在复杂的嵌套之内。
【示例】
原始shader:
struct unity_ Type {
vec4 vecArrA[4];
vec4 vecArrB[4];
};
layout(std140) uniform UnityInstancing_PerDraw0 {
unity_Type unity_Array[32];
};
void main() {
u_xlati0 = gl_InstanceID + unity_BaseInstanceID;
u_xlati0 = u_xlati0 << 2;
u_xlat1.yw = (-_mhyWorldOffset.zx) + unity_Array[u_xlati0 / 4].vecArrA[3].zx;
}
推荐shader:
struct unity_ Type {
vec4 vecArrA[4];
};
layout(std140) uniform UnityInstancing_PerDraw0 {
unity_Type unity_Array[32];
};
void main() {
u_xlati0 = gl_InstanceID + unity_BaseInstanceID;
u_xlati0 = u_xlati0 << 2;
u_xlat1.yw = (-_mhyWorldOffset.zx) + unity_Array[u_xlati0 / 4].vecArrA[3].zx;
}
3. 顶点排布
Maleoon GPU是Tile-Based架构的GPU,对于Tile-Based架构的GPU,有一个单独的pass(binning pass)仅用于计算顶点着色器的顶点位置。因此,如果输入的顶点位置相关属性与所有其他属性存储在同一个buffer中,则binning pass获取vertex shader输入时,由于memory的连续读取粒度较大,将会导致实际读取到较多对该shader无效的输入数据,内存带宽将会增加很多。
【推荐】
将顶点相关的属性存储在独立buffer中。
Per-Fragment Test
Overdraw
Overdraw作为影响GPU性能的核心问题之一,开发者可以对于不透明的primitive进行排序,让后面的primitive可以被剔除,GPU也在硬件层次上支持不依赖primitive排序的剔除方案。
Maleoon GPU内部有一套针对Depth Tes