Android图像处理系列:渲染性能优化——GL多线程的使用

Android图像处理系列:渲染性能优化——GL多线程的使用

2023年6月28日发(作者:)

Android图像处理系列:渲染性能优化——GL多线程的使⽤对于⼀个相机类应⽤来说,帧率或者流畅度是最关键的性能指标之⼀。⼀般来说,相机类应⽤因为要对图像数据做美颜、滤镜等处理,对每⼀帧数据的处理流程都⽐较长。天天P图的相机滤镜链处理过程也很长,中间包含了⾮常多步的计算和渲染,相机流处理流程中⼀帧要经过10~20个滤镜处理,即要调⽤OpenGL

draw call10~20次,还包括了很多CPU阻塞操作,包括CPU计算(⼈脸检测、⼿势检测等)、纹理上传和下载等,⽽且滤镜处理数据还在随着产品需求增加⽽不断增加。如何在保证产品效果需求不断增加渲染和计算的情况下保证⽤户使⽤相机的流畅度是我们需要持续优化的⼯作。本⽂从多线程的⾓度出发,介绍OpenGL多线程的适⽤场景和拆分原则,以及多线程带来的数据不同步的问题和解决⽅案,最后⽤对⽐性能数据说明多线程的优化效果。GL多线程的必要性和可⾏性仔细分析我们的相机处理流程,GPU上的渲染操作并不是连续的,中间会被CPU阻塞操作多次打断,⽐如⼈脸检测是CPU操作,在⼈脸检测时GPU空闲,等待⼈脸检测结果出来后才能继续渲染,再⽐如纹理数据的上传或者下载操作,虽然调⽤的是OpenGL API,但是这些API调⽤并不能⽴即返回,⽽是需要阻塞等待前⾯的OpenGL指令执⾏完,且数据传输完成后才能继续渲染,在这段时间内,GPU资源都是空闲的。这就有必要使⽤多线程来分担这部分CPU计算或阻塞操作,以便更充分地使⽤GPU资源。能调⽤OpenGL API的GL线程说⽩了其实就是⼀个包含了OpenGL上下⽂环境的普通线程。在默认情况下,⼀个GL线程是⽆法访问另⼀个GL线程的GL资源的,⽐如纹理、着⾊器和顶点缓冲区等。如果我们希望把纹理上传和下载等GPU阻塞操作放⼊另⼀个线程,就需要共享另⼀个线程的纹理等资源。OpenGL为了解决这个问题,提供了ShareContext的机制,在创建GL线程时可以引⽤另⼀个GL线程的上下⽂环境,以共享GL资源,这就为GL多线程提供了可⾏性。GL多线程的拆分原则这⾥我们⽤⼀个具体的场景为例介绍⼀下OpenGL多线程的拆分原则,如下图所⽰:假设⼀个渲染线程有5个渲染步骤,第⼀步渲染完后需要⾸先上传第⼆步渲染需要的纹理数据,然后渲染第⼆步;渲染第三步之前要⾸先上传需要的顶点Buffer,然后渲染第三步;渲染第四步之前需要⾸先编译渲染需要的shader,然后再渲染第四步;最后渲染第5步。在整个处理流程⾥,上传纹理、顶点Buffer和编译shader都是CPU阻塞操作,在渲染的第⼆、三、四步都需要等待这些操作完成后才能继续渲染,在这段时间内GPU资源空闲,并没有得到充分利⽤。我们可以把上传纹理、顶点Buffer和编译shader的操作放⼊另⼀个线程中,只要保证渲染第2步之前纹理资源已经上传好,渲染第3步之前顶点buffer数据已经上传好,渲染第四步之前shader已经编译好,主渲染线程就可以不⽤停顿保持持续渲染,异步线程和主渲染线程通过Share context机制共享GL资源。在这种情况下,主渲染线程只包含OpenGL渲染操作,耗时减少,帧率提⾼。这⾥有同学可能要问了,是否可以把部分渲染操作也放到异步线程中?这样主渲染线程包含的渲染操作更少,帧率可以进⼀步提⾼?这样做技术上是可⾏的,但实际上对帧率并不⼀定有优化。因为GPU驱动程序底层渲染接⼝同⼀时刻只能绑定⼀个OpenGL上下⽂环境,在GPU渲染处理是瓶颈的情况下,多个GL线程中同时做渲染操作并不能使总渲染耗时减少,反⽽可能因为切换Context导致总耗时增加,⽽且增加代码复杂度,这种做法也是不推荐的。这⾥需要提⼀下新⼀代渲染引擎Vulkan的多线程。Vulkan允许多个CPU线程向同⼀个命令缓冲区⾥提交GPU处理指令,在渲染操作⽐较简单,GPU处理快⽽CPU提交命令⽐较慢的情况下,GPU需要等待CPU提交渲染指令,在这种情况下,由多个线程来提交渲染指令能减少GPU等待时间,充分利⽤GPU资源,提⾼渲染性能。但是OpenGL并不⽀持多个GL线程向同⼀个命令缓冲区提交GPU处理指令,每个GL线程都有其⾃⼰的命令缓冲区和执⾏队列,所以OpenGL并不适合多个GL线程同时做渲染操作。另外,对天天P图类的相机流处理类应⽤来说,处理耗时主要在GPU渲染操作,CPU提交指令的速度并不是瓶颈,所以Vulkan的多线程场景也并不适⽤。天天P图⽬前的处理流程⾥就是把⼈脸检测、⼿势检测、⼈像分割、纹理上传和下载等CPU阻塞操作放到了另外⼏个GL线程做处理,通过ShareContext机制与主渲染线程传递GL资源。GL多线程引⼊的问题拆分成多个线程后,最⼤的问题在于⼈脸检测、⼿势检测、⼈像分割等处理返回的数据与渲染不同步,⼈像移动很快时分割mask和贴纸都会有追赶⼈脸的感觉,效果不好。如上图所⽰,以⼈像分割的处理流程为例,分割线程包含⾃⼰的分割mask存储区,相机纹理同时交由分割线程和渲染线程各⾃处理,分割线程处理完后更新⼈像分割mask,渲染线程在渲染时直接从分割线程获取已处理好的⼈像分割mask。因为分割线程和渲染线程是各⾃独⽴执⾏的,渲染线程此时得到的⼈像mask并不是当前渲染帧的mask⽽是之前某⼀帧的mask,⽤这样的mask和原图做渲染肯定⽆法做到紧密贴合。效果可以看⼀下这幅⽰意图,在⼈脸移动很快的时候mask追赶⼈脸的现象尤其明显。⼈脸检测和⼿势检测线程存在同样的问题。⽽这种做法我们在竞品⾥也可以看到,在低端机上他们会⽤这种异步处理来提⾼帧率。双Buffer实现分割和检测效果同步为了解决多线程下数据不同步问题,我们引⼊了双Buffer缓存⼀帧的策略来实现同步。每个线程都有两个buffer轮流切换来接收数据和输出数据,如下图所⽰:主渲染线程(Render GL Thread)、分割线程(Segment GL Thread)、检测线程(Detect GL Thread),每个线程都包含两个Buffer,⼀个Buffer是⼀个FBO及texture的绑定,其中包含了当前线程处理步骤中某⼀帧的处理结果。在第n帧数据过来时渲染到渲染线程的Buffer1上,同时交给分割线程和检测线程做处理,处理结果也将放到它们各⾃的Buffer1上。此时Buffer2上已经有了第n-1帧数据的原图和第n-1帧数据和处理结果,渲染线程直接拿三个Buffer2上的数据做后续的渲染。在第n+1帧过来时渲染到渲染线程的Buffer2上,同时交给分割线程和检测线程做处理, 处理结果放到它们各⾃的Buffer2上,此时分割和检测线程已经完成了第n帧数据的处理,Buffer1上的数据已经准备好了,渲染线程直接拿Buffer1⾥的原图和Buffer1的处理结果做后续的渲染,三个线程Buffer1⾥的数据是同步对应的。后续的渲染都是以这种轮流切换的⽅式,每⼀帧数据过来时,渲染线程不⽤等待当前帧的分割和检测结果,⽽是直接获取到上⼀帧的原图和上⼀帧的处理结果做后续的渲染,形成同步的处理效果,如下图所⽰:这⾥有两个需要注意的问题:1. 第1帧数据的处理。⼀开始所有的Buffer都是空的,第1帧数据送到分割和检测线程做处理后,渲染线程这时候拿不到Buffer2的数据做后续的渲染。这⾥我们对第1帧数据做了特殊处理,渲染线程不等待Buffer2的处理结果,直接⽤Buffer1⾥的原图和⼀个空的⼈脸分割mask和⼈脸⼿势数据做后续的渲染。第2帧过来时第1帧数据已经处理好了,渲染线程拿到Buffer1⾥的原图的处理结果做后续的渲染。也就是说⼀开始渲染到屏幕上的前两帧其实是同⼀帧,都是第1帧,只不过⼀帧没有动效效果,⼀帧有。后续的渲染就正常了。2. 如果第n+1帧过来的时候,第n帧还没处理好怎么办?为了实现同步的效果,渲染线程是需要等待分割线程或者检测线程处理完第n帧才能继续渲染的。这⾥同时也涉及⼀个问题,为什么要把分割、⼈脸检测和⼿势检测按照这样的⽅式拆分?下⾯我们就来分析⼀下双Buffer下⼀帧数据的处理耗时。双Buffer下GL多线程⼀帧处理耗时分析理想情况下,渲染线程的⼀帧处理耗时⼤于等于分割线程,也⼤于等于检测线程:以图⽰为例,假设分割线程处理⼀帧需要30ms,渲染线程需要50ms,检测线程需要10ms。按照双Buffer的处理流程,分割线程和检测线程处理当前帧,渲染线程处理上⼀帧。当渲染线程处理结束后,分割线程和检测线程已经完成了当前帧的处理,下⼀帧数据过来时渲染线程可以直接拿到当前帧的分割结果和检测结果进⾏渲染。这样⼀帧的处理耗时就是渲染线程的处理耗时50ms。⽽真实情况是分割线程的⼀帧处理耗时⼤于渲染线程,渲染线程⼤于检测线程:这样在第2帧数据过来时,分割线程还没有做完第1帧数据的分割,为了实现同步渲染效果,渲染线程需要等待20ms,等分割线程处理完第1帧后才能继续后⾯的渲染。所以⼀帧的处理耗时是分割线程的处理耗时50ms。因为双Buffer机制要保证同步的渲染效果,就不可避免地需要在某⼀时刻对三个线程的数据进⾏同步,⼀帧的处理耗时也就以处理时间最长的线程为准。回到我们刚才的问题,为什么分割处理单独放到⼀个线程,⼿势检测和⼈脸检测放到另⼀个线程?如果把⼈脸检测或⼿势检测和分割放在⼀个线程⾥,分割线程处理耗时会更长,⼀帧的处理耗时也会更长。⽽分割处理作为⼀个整体⼜是不可拆分的,所以把它单独放在⼀个线程⾥处理。在图⽰这种情况下,检测其实可以和渲染放在⼀个线程⾥,但是因为检测线程处理耗时不稳定,在多⼈脸情况下处理耗时会线性增长,所以我们把它们也单独放在⼀个线程处理。性能分析下⾯我们来看⼀下帧率对⽐数据。我们测试了Mi Note2⼿机mv_night这个素材在分割和检测都同步,分割异步,检测异步,分割和检测都异步这⼏种情况下的帧率,可以看到异步后帧率都有提升,分割和检测都异步后,单⼈脸情况下帧率由11fps提升到19fps,5⼈脸情况下由9fps提升到19fps,帧率提升⾮常明显。需要注意:GL多线程共享资源需要获取GL线程的Context,在Android端,这个接⼝只有在Android系统版本4.2及以上才有,在这⾥我们做了兼容性处理,低版本还是⽤单线程的⽅式。根据android官⽅的最新统计数据,4.2版本以下的⼿机⽬前只占到不到2%,绝⼤多数的机器都可以使⽤GL多线程的⽅式来提升帧率。总结GL多线程适⽤于GPU主渲染流程中夹杂了CPU阻塞操作的场景,在CPU阻塞期间GPU资源空闲,没有得到充分利⽤,可以基于OpenGLShareContext的机制⽤GL多线程的⽅式分担CPU阻塞操作的耗时,使主渲染线程保持持续渲染以提⾼帧率。

发布者:admin,转转请注明出处:http://www.yc00.com/news/1687954088a60483.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信