极术小姐姐 · 2021年03月22日

移动设备渲染架构以及GPU优化技巧

前言

目前所有的移动设备基本采用Tile-Based Rendering(基于图块的GPU架构,简称为TBR)渲染主流的渲染架构。本文主要介绍介绍TBR的优缺点。它还将Arm Mali基于图块的GPU架构设计与通常在台式机或控制台中发现的更传统的即时模式GPU进行了比较。
Mali GPU使用基于图块的渲染架构。这意味着GPU将输出帧缓冲区渲染为几个不同的较小子区域,称为图块。然后,它会在完成后将每个图块写出到内存中。使用Mali GPU,这些图块很小,每个图块仅16x16像素。


一、常用的两种GPU渲染架构

目前常用的两种GPU渲染架构是Tile-Based Rendering(基于图块的GPU架构),主要用于移动设备,还有一种是Immediate Mode GPUs(简称IMR, 即时模式架构)传统的台式机GPU架构。

二、Immediate Mode Rendering

1.说明

简称IMR, 也就是Full Screen,因为它不去分Tile,传统的台式机GPU架构通常称为即时模式架构。即时模式GPU将渲染处理为严格的命令流,在每个绘图调用中的每个图元上依次执行顶点和片段着色器。

伪代码如下:

for draw in renderPass:
    for primitive in draw:
        for vertex in primitive:
            execute_vertex_shader(vertex)
        if primitive not culled:
            for fragment in primitive:
                execute_fragment_shader(fragment)

2.优点

下图显示了硬件数据流和内存交互:

图上可以看出,顶点着色器以及其他与几何相关的着色器的输出可以保留在GPU内部的芯片,着色器的输出可以存储在FIFO缓冲区中,直到流水线的下一个阶段准备使用数据为止,GPU很少使用外部存储器带宽来存储和检索中间几何结果。
(备注:DDR为数据流, FIFO:First In First Out队列)

IMR的优势分析图:

IMR的优势是每个primitive直接提交渲染,pipeline没有中断,渲染速度快,pipeline并行起来时,每个Raster core只要负责render分给它的primitive即可,无需其他控制逻辑,只需在pixel shader后对Raster出的pixel做个排序。
(备注:raster为光栅化)

3.缺点

1)如果有很大的图形(主要是三角形)需要被渲染,那framebuffer就会很大,比如对于整个屏幕的颜色渲染或者深度渲染就会消耗很多存储资源,但是片上是没有这么多资源的,因此就要频繁读取DDR。很多和当前frame有关的操作( 比如blending, depth testing 或者stencil testing)都需要读取这个working set,存储器上的带宽负载可能会非常高,并且这样能耗也很高,对于移动设备来说,这种方式很不利于设备运行;
2)z test跟blending都要频繁从framebuffer里读数据,毕竟framebuffer是位于Memory上,带宽压力和功耗自然高;
3)Overdraw的问题,比如Application在一帧里先画了棵树,然后画了面墙刚好遮住了树,在IMR下树仍然要在Pixel Shader里Sample texture,而Texture也是放在Memory,访存功耗大。

三、Tile-Based Rendering

1.说明

基于图块渲染也称基于瓦片渲染或基于小方块渲染,它是一种在光学空间中通过规则的网格细分计算机图形图像并分别渲染网格(grid)或图块(tile)各部分的过程。这种设计的优点在于,与立即绘制整个帧的立即模式渲染系统相比,它减少了对内存和带宽的消耗。这使图块渲染系统的使用特别常见于低功耗硬件设备。图块渲染有时也被称为中置排序(sort middle)架构,因为它在绘图流水线中间而不是接近结束时进行几何排序。

2.以Mali GPU为例

Mali GPU采用不同的方法来处理渲染过程,这就是所谓的基于图块的渲染方法。此方法旨在最大程度地减少片段着色期间GPU需要访问的外部存储器的数量。
基于图块的渲染将屏幕分成小块,并对每个小图块进行着色着色,直到将其写出到内存中为止。为了使这项工作有效,GPU必须预先知道哪些几何图形有助于每个图块。因此,基于图块的渲染器将每个渲染过程分为两个处理过程:
1):第一遍执行所有与几何相关的处理,并生成图块列表数据结构,该结构指示哪些图元对每个屏幕图块起作用。
2):第二遍将逐块执行所有片段处理,并在完成后将切片写回到内存中。请注意,Mali GPU渲染16x16的图块。

伪代码如下:

# Pass one
for draw in renderPass:
    for primitive in draw:
        for vertex in primitive:
            execute_vertex_shader(vertex)
        if primitive not culled:
            append_tile_list(primitive)

# Pass two
for tile in renderPass:
    for primitive in tile:
        for fragment in primitive:
            execute_fragment_shader(fragment)

下图显示了硬件数据流以及与内存的交互:

优势:解决了传统模型的带宽问题,因为fragment shader每次都是读取一个小块放在片上,不需要频繁读取内存,直到最后操作完成,再写入内存。甚至还能够通过压缩tile的方法进一步减少对于内存的读写。另外在图像有一些区域固定不动的时候,通过调用函数判断tile是否相同,减少重复的渲染。

3.优势

下图显示了TBR的优势:

对于IMR所有read z/framebuffer,到了TBR通通不需要。TBR只需render完tile后把on-chip的pixel写到frame buffer(不需要写z,因为下一帧不需要用到前一帧的z和color)。这个好处在于TBR将Screen Tiling。这样,每次render的区域变小,小到可以把z/framebuffer搬到on-chip,快,省电。

另外两点优势是:
1):TBR给消除Overdraw提供了机会,PowerVR用了HSR技术,Mali用了Forward Pixel Killing技术,目标一样,就是要最大限度减少被遮挡pixel的texturing和shading。
2):TBR主要是 cached friendly, 在cache里头的速度要比全局内存的速度快的多,以及有可能降低render rate的代价,降低带宽,省电

3.缺点

1)这个操作需要在vertex阶段之后,将输出的几何数据写入到DDR,然后才被fragment shader读取。这之间也就是tile写入DDR的开销和fragment shader渲染读取DDR开销的平衡。另外还有一些操作(比如tessellation)也不适用于TBR;
2)如果某些三角形叠加在数个图块(Overdraw),则需要绘制数次。这意味着总渲染时间将高于即时渲染模式。

四、两种渲染架构对比

下图显示了两种渲染架构的对比:

说明:
1):IMR的pipeline畅通无干扰,sorting简单,TBR的sorting较复杂,但也给低功耗优化提供了灵活的选择;
2):Geometry的transform和场景的tiling,然后往memory里写入Geometry的数据和每个tile所要rendering的Geometry,相对来说多了内存消耗;
3):PC屏幕大,PC game场景复杂,对Tile list压力大,另外PC追求frame rate,所以很少用TBR,即使用了,遇到复杂游戏场景估计会切换到IMR。

五、移动端GPU优化建议

对于对于GPU优化通常会有两个方向,一个是带宽,另一个是填充率。
1)像素填充率,可以简单的理解为图形处理单元每秒渲染的像素数量,对移动端GPU来说,这块是优化的最重要点。如何分析像素填充率?可以通过缩放分辨率去验证。这块也是比较难优化的点,因为填充率很容易成为瓶颈。我们可以通过下面几种方式来进行:

【1】分析游戏并记录GPU时间。关注半透明物体overdraw的问题,比如特效的Overdraw,通常来说特效会有比较严重的overdraw问题,所以对于特效来说shader算法效率应当更重视,能采用简单的方式实现就用简单的。后处理也是重点关注的,比如bloom效果,抗锯齿效果都会导致填充率瓶颈,通过分析每个后处理的GPU耗时进行对应的优化。(备注:要以60fps的帧率进行平滑的渲染,每一帧所占用的时间需要少于16ms(长期维持在16ms也会导致GPU无休息时间而手机发热,但通常比较难避免,超过16ms后容易掉帧【持续一段时间超过16ms后会导致发热后降频了】,因为GPU长期处于高负荷状态),理想情况下是12ms,30fps应少于33ms,理想情况下是18ms)

【2】对中高低进行分辨率的适配来调整,比如对于低档机子,分辨率(以高为基准)720p的方式,中档机为720p * 1.2,高档机型为1080(720p_*_ 1. 5)的方式来调整, 这个比例根据项目实际情况自行调整。

【3】控制好Shader的复杂度(顶点着色器VS和片元着色器PS。片段着色器是着色器代码的各个部分,它们告诉GPU如何绘制单个像素。此代码由GPU针对必须绘制的每个像素执行,因此,如果代码效率低下,则很容易堆积性能问题。对于如何分析shader的复杂度,可以通过对shader代码的分析,比如函数的指令级别等进行优化,参考文档:

Shader指令集优化

不过最终的性能分析,还是需要借助工具来实现,推荐使用Renderdoc,ios可以直接用XCode,分析每个PS(片段着色器)的耗时,至于如何使用Renderdoc,后续会发文章来介绍,也可以用Snapdragon profiler有条件的还可以使用WeTest, UWA进行分析。单独测试笔者比较推荐使用RenderDoc,高通和Mail的GPU可以直接使用,RenderDoc还可以帮助定位一些图形显示问题,比较方便的。填充率可以写个工具去统计,比如通常填充率为4倍左右就开始发热了,8倍就掉帧了,这个数据读者可自行验证,不同的GPU处理器可能不同的结果,可以分别跑下高通(晓龙),Mail(麒麟),苹果的,做对比。(正式开发中读者一定要有能够测试GPU性能的工具,这样好实时观察性能指标,做好后续的优化策略

其他关于shader部分的优化也有一些,比如:

A. 单独独立出来说确实是Alpha Blending会比AlphaTest性能要好一些,因为移动平台的GPU会有EarlyZ的优化,而AlphaTest会使Early Z无效。但Alpha Blend会导致Overdraw的问题,最明显的就是特效,填充率比较高的情况(达到瓶颈了)下,alpha Blending并没有效率提升,反而alpha test会好一些, 但特效大部分使用blending,这样效果会好些,我们还是根据实际情况去选择。(补充:对于深度读写关闭的,Alpha Test在移动设备上就足够快了,因为它不会写入DepthBuffer,所以没有管道中断(由ARM人员确认这个原理了))下面有个链接说得还可以:

flashyiyi:再议移动平台的AlphaTest效率问题​图标

B. 避免寄存器溢出, 通常寄存器溢出的原因是一是因为Uniform/Varying变量和临时变量过多,而是因为变量精度过高(移动端half基本够用了,除非是一些需要float精度的变量)。为何需要避免这个问题呢?因为寄存器溢出会使GPU在内存中读取Uniform,会影响Load/Store的性能,增加带宽,降低cache 命中率。

(备注:当Overdraw过高的时候,即使shader性能再好,也会导致填充率过高的问题,这时候需要同时考虑的是去优化overdraw,比如特效,因为填充率一旦超过瓶颈(读者可根据实际情况自己去测试,一般超过4倍就有些严重了),GPU耗时高手机发热等问题无法避免了, 这种情况下性能再好的shader也无法发挥作用了)

2)显存带宽,这块很多文章都有介绍,通常大部分图片不要超过1024即可,但场景有些贴图是可以2048的,但要注意是否2048的图是否空出来太多,还有normal贴图,emission图,AORM图等大小控制,这些贴图通常都是小于等于1024,这块需要约定好一个标准。至于压缩格式,Android和IOS通常使用ASTC\_RGBA\_6x6或者ASTC\_RGBA\_8x8,不带Alpha通道为ASTC\_RGB\_6x6 or ASTC\_RGB\_8x8,因为目前大部分的Android平台都是支持ASTC,但对于比较老的机型,比如15年,ES3.0,可能会不支持ASTC,目前这块的用户量其实比较少,看项目需求是否坚持采用ASTC,不然就ETC2格式,或者做个兼容。
下图显示了以unity为例如何通过调整贴图设置去打手机包分析是否带宽瓶颈的一种方式:

unity的Quality设置选项里可以设置纹理的缩小倍数,它会限制GPU使用的纹理大小(但不会减少内存占用),这样能成倍减少贴图的带宽使用量,但对读屏幕那一部分没有任何影响。如果调低它帧数上升了,就可以说明确实遇到了带宽瓶颈,但没变化,也有可能是因为优化幅度太低了看不出来。(备注:如果移动端上使用的是延迟渲染,带宽很可能成为瓶颈。)

还有一种方式可以减少带宽就是 mipmap,其同样也具备增加缓存命中率,减少摩尔纹等特性,不过这块会多占用33%的内存大小。如果目前内存使用已经很多了,需要注意这块的使用,通常来说一般会对场景和角色的贴图做mipmap,其他的不做mipmap(会糊掉),做mipmap还是根据实际情况分析是否需要做这块。对于一些自研引擎或者是UE4,会有一种贴图加载的mipmap,比如一开始在包体上有一种贴图就1024,512, 256,1 28的大小的,通过mipmap偏移去加载对应大小的贴图带显存上,这种方式可以优化内存的大小的。

补充:因为移动GPU基本上都采用了TBR的架构,渲染时GPU会有一个叫片上内存的东西,它所有的渲染结果实际上是直接对片上内存进行操作,而不是直接对显存进行操作,就能够减少频繁读显存所带来的带宽开销。如果我们有些操作可以不将内存加载到片上内存,自然而然这个操作是没有任何的带宽开销的,比如unity中的RenderBuffer上的LoadStore操作可以对这块进行优化。更详细的读者可以查看下面这篇文章,其中也介绍了unity的减少Blit操作可以优化带宽等:

https://mp.weixin.qq.com/s/qjhWQ1rX8KZGIQaoX4hRSA​mp.weixin.qq.com

大量的RT切换,相当于硬件FBO的切换,在Tile GPU上会造成额外的内存的回读,性能大量地消耗,全屏的blit操作性能消耗会更大些。

六、补充

1)计算DrawCall

公式:DrawCall\_Num = 25K * CPU\_Frame * CPU\_Percentage / FPS

DrawCall\_Num : DrawCall数量(最大支持)

CPU\_Frame : CPU 工作频率(GHZ单位)

CPU\_Percentage:CPU 分配在DrawCall这件事情上的时间率 (百分比) ​ FPS:希望的游戏帧率

2)计算带宽

以最低分辨率1280x720计算,一个屏幕缓冲区加上深度缓冲检测,每个像素需要6byte,然后以60帧/s的帧率要求,乘起来的结果是:绘制一屏幕数据(1 overdraw),需要的带宽至少是331776000bytes,也就是0.316Gbytes。不过,由于透明物体需要和原屏幕像素进行混合,所以还需要回读一次屏幕缓冲区的数据,会增加接近一倍,0.527Gbytes。

如果游戏的特效峰值overdraw达到了10(测一下的话,会发现全屏特效特别容易到达高值,乱做单特效都能直接到5),也就是10个屏幕的话,那就是5.27Gbytes。以前在一些老机器上,诸如三星S3,总内存带宽其实也就8G,目前的手机好了许多,有20多G,但现在手机屏幕会比较大,基本是1080P,对带宽要求就要乘2.25。

总结

目前通常移动设备采用的架构都是TBR的,对于GPU优化通常会有两个方向,一个是带宽,另一个是填充率。对于TBR架构来说,影响比较大还是填充率,可以通过调整分辨率,Shader的复杂度(顶点和片元着色器的复杂度)等来控制。而带宽的话,通常大部分图片不要超过1024即可,但场景有些贴图是可以2048的,但要注意是否2048的图是否空出来太多,还有normal贴图,emission图等大小控制,这块需要约定好一个标准。

参考

Graphics and Gaming Development | Tile-Based Rendering – Arm Developer​developer.arm.com

基于图块渲染​zh.wikipedia.org

Jiangxh1992:【Metal2研发笔录(二):传统延迟渲染和TBDR】​

作者:汤姆猫X
阅读直达链接:https://zhuanlan.zhihu.com/p/265151933
欢迎大家点赞留言,更多Arm技术文章动态请关注极术社区Arm技术专栏
推荐阅读
关注数
110
内容数
18
Arm Mali GPU系列相关技术干货
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息