背景
在Android系统中,图形绘制模块主要分为HWUI以及RenderEngine,我们所能在手机上看到的所有像素都有着这些模块或多或少的参与。而一个最简易的安卓渲染流程如图二所示,应用操作的所有view操作都转换为RenderNode的DisplayList;Displaylist存储着所有的绘制命令以及级联关系。
图一
图二
1.HWUI Skia介绍
HWUI、Skia的发展历程主要在Android1.0、3.0、4.0以及Q版本有着重大影响。目前View的绘制主要通过HWUI使用硬件加速的功能,Skia则是HWUI到openglES命令转换的中间件。最后通过OpenGLES操作GPU进行实际的绘制。
图三
当应用在View的实现中操作RenderNode的beginRecording时,会去返回一个Recordingcanvas用于记录Canvas的绘制命令,通过调用Canvas的drawRect,Canvas中的OP操作会以DisplaylistOp的形式记录到RecordingCanvas中的DisplayList里;在最后调用endRecording,结束该RenderNode的命令记录。
图四
RendeThread的绘制流程主要包含以下五个阶段,下面就这几个阶段分别进行介绍。
图五
1.1 同步View树
该阶段主要进行绘制环境的配置,纹理的上传以及View树的构建。在纹理上传中会去判断当前纹理是否在texture cache中存在,如果存在,则不进行上传。其中cache的键是通过纹理的宽、高、等一系列属性计算出来的hash值。在构建View树的过程中,会去递归的遍历所有子节点,并将View的DisplayList同步到RenderNode中。
1.2 计算当前Layout的Dirty区域
该阶段主要进行递归的计算所有的RenderNode,通过用下层的RenderNode和上层的renderNode进行交集计算最终找到需要绘制的区域。该绘制区域是用于送到GPU过程中,指定实际的渲染区域,可用于节省功耗。
图六
1.3 dequeueBuffer
最终会通过queryBufferAge调用GPU中的eglQuerysurface,GPU再调用对应surface的dequeueBuffer。
图七
1.4 计算该帧与前一帧的Dirty区域
在送到GPU之前再进行一次性能优化,通过计算当前帧与当前BufferQueue记录的dirty区域进行求并集,从而统计出当前帧实际需要绘制的区域。因为在Buffer在实现时,目前有两种渲染策略,一种是将当前帧所有的区域都进行更新,另一种只更新当前Buffer实际需要更新的区域。
图八
图九
1.5 Draw流程
在实际进行draw时,首先会去根据当前Surface创建一个帧缓存,即BackendRenderTraget,而后通过renderFrame去遍历DisplayList,进行帧的绘制操作。通过renderLayersImpl接口渲染自带离屏buffer的view(存在damage区域),可先将负责的view绘制到该离屏buffer上,可减轻renderFrameImpl的工作量。通过renderFrameImpl接口遍历renderNode进行帧的绘制操作,上面绘制完的离屏buffer可直接用,不用再绘制该renderNode。
图十
RenderNode是一个嵌套结构,RenderNode中嵌套displaylist,调用displaylist的draw操作,当op为drawable(RenderNodeDrawable),会去递归的调用drawContent, 直到op为具体的draw操作(非RenderNode),才会去进行实质的绘制操作。
图十一
当op为一个具体的draw操作时(如drawRect),将skia库中的skPaint转换为Gr库中的grPaint,并生成一些shader到fragmentProcessor,并将OP先与队列的最后一个op进行合并,如果可以则合并,最后将GrOp添加到GrOpChain中。
图十二
该阶段会去遍历所有的grOp并生成shader,转换为glOp并最终调用到GPU UMD里面的API。
图十三
1.6 queueBuffer
该阶段做的操作主要包含调用Surface::setSurfaceDamage设置damage区域;eglSwapBuffersWithDamageKHR会通过GPU调用对应surface的queueBuffer,同时触发onFrameAvailable回调。
图十四
1.7 使用Skia进行绘制Demo
图十五
2.OpenGL介绍
opengl渲染管线中可编程的操作管线主要包含Vertex Shader、Tessellation Shader、Geometry Shader以及Fragment Shader。通过这些管线的操作,我们可以实现各种复杂的模型。VS用于处理顶点数据、进行顶点变换;TS则通过外扩添加顶点的方式生成更多的三角形;GS则通过内插顶点的方式生成更多的三角形;FS则用于对像素进行着色,从而呈现出最终显示的图形。
图十六
3.GPU硬件介绍
目前硬件主流的渲染框架分为IMR、TBR和TBDR,下面就这三种框架进行简要介绍。
3.1 IMR
IMR,即Immediate Mode Rendering,是常用于PC端的渲染管线。它的架构设计比较简单清晰,整个管线是连续执行的,执行完上一个任务后,将立刻执行下一个任务,无需相互等待。当接收到一个绘制指令后,这一绘制会立即开始执行,并且将依次顺序经过如下步骤:
(1)顶点处理(Vertex Processing):从内存读取顶点索引,并根据索引查找相关顶点缓冲区,加载顶点数据。顶点着色器加载到SM中并执行。
(2)裁剪和剔除(Clip & Cull):在这一过程中,PE将剔除裁剪空间(clip space)外的三角形,并且进行背面剔除操作。
(3)光栅化(Raster):执行光栅化,从几何转化为像素,像素打包成warp,重新流入SM,并根据重心坐标插值顶点属性。
(4)提前可见性测试(Early Visibility Test):对于没有Alpha Test的像素,由ZROP执行early-Z test,通过后进入下一环节。
(5)纹理和着色(Texture & Shade):执行像素着色器。
(6)Alpha测试(Alpha Test)
(7)可见性测试(Late Visibility Test):对于有Alpha Test的像素,由ZROP执行late-Z test,并根据结果决定是否更新帧缓冲的颜色和深度。
(8)Alpha混合(Alpha Blend):对于通过测试的像素,CROP根据blend计算并更新颜色缓冲区。
主要使用设备:常用于PC、主机、笔记本等设备。最大的特点就是Primitive提交之后直接进行绘制。
内存访问:framebuffer一直存储在SystemMemory上。
优点:简单顺畅的流程保证了在单个Primitive渲染流程中没有额外的中间结果存储。
缺点:带宽消耗
图十七
3.2 TBR
核心思想是牺牲执行效率,优化带宽消耗,以更好地适配移动端硬件。
基于tile的TBR/TBDR,TBR/TBDR还有一个重要的特性,那就是它会将frameBuffer划分为多个tile,以tile为单位进行渲染,这也正是它名字的来源。当每个三角形都执行完vs阶段后,会进入binning pass阶段,此时framebuffer被划为多个tile,并会去计算每个三角形所关联的tile。最终,每个tile记录要渲染的三角形列表。
像素着色阶段,会以tile为单位依次进行绘制。根据primitive list判断当前tile包含哪些三角形以及对应的顶点属性,然后再绘制tile中每个三角形。绘制完成后,拷贝回framebuffer对应位置。
为什么说TBR/TDBR优化了带宽消耗
(1)批量读取/写入:对于IMR而言,依次连续执行指令,就类似于每次只读取一个数据,这样的操作执行n次;而对于TBR/TDBR而言,将所有指令执行完成后才进入下一阶段,就类似于一次性读取所有数据。这一设计是对带宽友好的。 (2)tile低带宽消耗 :读写深度缓冲/颜色缓冲是非常消耗带宽的操作,对于IMR架构而言,在做深度测试时,必须读FrameBuffer,必要时会写入FrameBuffer;在做Blending时,需要读写FrameBuffer。(FrameBuffer位于Systm Memory)使用TBR/TBDR后,因为渲染被切分为tile,而tile比较小,因此可以设计一种较快的内存,称为on chip memory。可以先将数据存储在tile上的on chip memory上,提升了读写性能。等所有操作完成后再写入FrameBuffer。
主要GPU型号:Mali、 Adreno系列GPU架构。
内存访问:test、blend等均在onchip memory上进行,只在最后这块tile渲染完毕,才会copy到fb上。
优点:减少带宽。
缺点:执行效率降低 流程:VS - Defer - RS - PS。
图十八
3.3 TBDR
不需要在软件层面对物体进行排序,HSR在硬件上实现了零Overdraw的优化。当一个像素通过了EarlyZ准备执行PS进行绘制前,先不画,只记录标记这个像素归哪个图元来画。等到这个Tile上所有的图元都处理完了,最后再真正的开始绘制每个图元中被标记上能绘制的像素点。这样每个像素上实际只执行了最后通过EarlyZ的那个PS 如前文所提,对于IMR而言,管线是连续执行的。而对于TBR/TBDR而言,它的整个过程是不连续的,这一不连续具体体现在:
(1)提交drawcall阶段。得到绘制指令后,不会立即开始绘制操作,而是将所有绘制指令缓存起来,到最后才进行绘制; (2)顶点着色阶段。对所有绘制指令执行vs,并将绘制结果保存起来;
(3)像素着色阶段。绘制所有图元后,再将结果拷贝到framebuffer对应位置。
概括而言:IMR就是单个指令依次连续执行,TBR/TDBR则是所有指令完成一个步骤后再进入下一步骤。
TBR和TBDR的区别在于,TBDR会等待所有绘制指令的光栅化执行完成后,再进入像素着色器执行;而TBR中每个指令的光栅化和像素着色器是连续执行的。
相当于:
TBR:drawcall - Wait - VS - Wait - RS - PS
TBDR : drawcall - Wait - VS - Wait - RS -Wait - PS
图十九
3.4 EarlyZ && HSR
HSR策略:hsr起作用的阶段是在光栅化生成所有fragment之后(这个跟ealry-z相似),但是这个hsr会打断后面的 着色流程(也就是说他会打断渲染管线,光栅化完成的图元不会马上进入fragment Shader阶段,而是会等待所有的 图元全部光栅化完成),这个时候所有的fragment已经生成,并且里面通过插值都已经得到了深度值,hsr可以从 里面筛选出实际有效的fragment,所以说hsr是可以彻底解决不透明物体的overdraw问题。
简单的说两个技术都是为了解决overdraw导致的渲染性能问题(当然这两个技术都只是针对不透明物体的绘制)。区别在于early-z只能部分缓解overdraw,hsr是可以彻底解决。之所以说early-z只是缓解这个问题,正如你所提到的,使用ealry-z需要我们应用程序上层在提交绘制之前,对绘制的几何物体做一个排序,问题也正是出在这个排序上面。因为这个排序只能做到大致上准确,做不到完全准确。比如你有两个三角形A、B交叉在一起,互有遮挡,那你不论怎么排序都会产生先完全绘制一个三角形,再绘制另外一个。(也就是说上层的这个排序粒度很粗,只能针对几何物体排序,而做不到针对组成几何图元的fragment排序) 理解了ealry-z的问题,那hsr就很好理解, hsr起作用的阶段是在光栅化生成所有fragment之后,(这个跟ealry-z相似),但是这个hsr会打断后面的着色流程(也就是说他会打断渲染管线,光栅化完成的图元不会马上进入fragmentShader阶段,而是会等待所有的图元全部光栅化完成),这个时候所有的fragment已经生成,并且里面通过插值都已经得到了深度值,hsr可以从里面筛选出实际有效的fragment,所以说hsr是可以彻底解决不透明物体的overdraw问题。ps:其实在hsr出来之前,利用early-z也是有一种方法来完全解决overdraw,就是先大致渲染一次场景,但是只写入Z值,然后有了这个z-buff再开始正式的渲染场景,这样利用early-z也是可以解决overdraw,但是很显然这里有一个预渲染z-buffer的开销。
图二十
3.5 使用OpenGL进行绘制Demo
图二十一
4.总结
本文通过三个章节分别介绍了HWUI基础知识以及HWUI与Skia之间的关系,OpenGL基础知识以及GPU硬件相关知识。通过该篇文章,读者可以简单了解图形的渲染引擎工作原理,了解如何通过Skia以及OpenGL绘制一个简易的三角形。后续读者可以通过该文章的书写逻辑阅读AOSP源码,对图形各模块进行深入研究。
参考文献
【1】 https://android.googlesource.com/
【2】 https://www.arm.com/zh-TW/products/silicon-ip-multimedia
Copyright © 2024 妖气游戏网 www.17u1u.com All Rights Reserved