从OpenGL再说离屏渲染

离屏渲染应该是所有iOS开发者绕不开的话题,关于离屏渲染的文章也有很多。objc.io 的文章绘制像素到屏幕上说过:
一般情况下,你需要避免离屏渲染,因为这是很大的消耗。直接将图层合成到帧的缓冲区中(在屏幕上)比先创建屏幕外缓冲区,然后渲染到纹理中,最后将结果渲染到帧的缓冲区中要廉价很多。因为这其中涉及两次昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)。
之前不懂GPU的工作原理,不懂OpenGL/Metal这些底层绘制API,对这一段话理解的非常模糊,后来学了下OpenGL和Metal,再结合之前看的文章和自己的理解,对离屏渲染(Offscreen Rendering)做一次梳理。有些地方是我自己的理解,不见得正确。先抛出下面的问题:
1、到底什么是离屏渲染?是在GPU上面还是CPU上面执行的?
2、为什么要有离屏渲染?什么情况下会产生离屏渲染?
2、帧缓冲区是什么?当前屏幕缓冲区和屏幕外缓冲区又是什么?
3、切换缓冲区是什么操作?真的比较耗时吗?

什么是离屏渲染?

2014年的WWDC Advanced Graphics and Animations for iOS Apps 对Core Animation的渲染机制做了详细解释:

图1

现今移动设备GPU都是采用Tile Based Rendering方式绘制,关于Tile Based Rendering在这篇文章有详细描述 Performance Tuning for
Tile-Based Architectures

Core Animation打包图层和动画信息到Render Server(这是一个单独的进程,所有app都会与这个进程通信以完成最终的绘制),由Render Server调用OpenGL/Metal指令,最终在GPU上面完成绘制。而在GPU上面的工作对于了解OpenGL的人比较熟悉了,顶点着色器(Vertex Shader)和GPU tiling一起构成Tiler操作,然后通过片元着色器(Pixel Shader)做Renderer操作,最后输出到渲染缓存(Render Buffer)中。这对应一个渲染管线的完整流程,实际上渲染管线还包括诸如光栅化,剔除,混合等操作,在上面的示意图中省略了。对于普通的屏幕内渲染,GPU只有一个Rendering Pass。
图2

对于离屏渲染,就存在多个Rendering Pass了。上面是Masking操作的示意图,一共有3步操作,对应3个Rendering Pass。最后的Compisiting pass输出到最后的帧缓存,是屏幕内渲染,而前面的pass1和pass2是绘制到texture供最后一个pass所用,即离屏渲染。

离屏渲染在GPU上面执行还是在CPU上面执行?

前面的图2很明确指出离屏渲染是在GPU上面执行的,但是有很多文章说CPU上面也会有离屏渲染,比如使用Core Graphics绘制的时候。Apple提供了检测离屏渲染的工具:Color Off-screen Rendered

图3

我重写UIView的drawRect方法(使用Core Graphics绘制),用Color Off-screen Rendered检测(iOS12模拟器)没有离屏渲染。因此严格来说CPU渲染不应该算作离屏渲染,离屏渲染发生在GPU上面。而且CPU渲染导致的卡顿和GPU的离屏渲染导致的卡顿原理完全不一样,在做性能优化的时候应该区别对待。
Core Graphics做绘制的时候,会有上下文Context,也有一个Bitmap画布,但是这个Bitmap画布是在CPU内存上面的,上下文Context也和上面说的环境转换:昂贵的环境转换(转换环境到屏幕外缓冲区,然后转换环境到帧缓冲区)不是一码概念。因此把CPU绘制归为离屏渲染个人感觉非常不妥。
UIKit 早期成员 Andy Matuschak,在一篇回复中有这样一段话:
In particular, a few (implementing drawRect and doing any CoreGraphics drawing, drawing with CoreText [which is just using CoreGraphics]) are indeed “offscreen drawing,” but they’re not what we usually mean when we say that. They’re very different from the rest of the list. When you implement drawRect or draw with CoreGraphics, you’re using the CPU to draw, and that drawing will happen synchronously within your application. You’re just calling some function which writes bits in a bitmap buffer, basically.

离屏渲染性能不好在哪里?

Advanced Graphics and Animations for iOS Apps 这个 session 以UIVisualEffectView为例描述GPU的处理逻辑,这里有五个Rendering Pass,上面蓝色为Tiler操作的时间分布,红色对应Renderer操作。实际上GPU时间大部分都花在Renderer操作上面,同样最后一个Rendering Pass是屏幕内渲染,那么UIVisualEffectView存在4个屏幕外的Rendering Pass。Rendering Pass之间还还存在黄色的Idle Time,这个就是环境转换(Context Switch)的时间,一个Context Switch大概占用0.1ms-0.2ms的时间,那么UIVisualEffectView的所有Rendering Pass会累积0.5-1.0ms的Idle Time,这个在16.67ms的帧时间内还是相当大的。因此离屏渲染性能不好在于:
1、更多的Rendering Pass,GPU运算量增大;
2、Rendering Pass之间的Context Switch导致的Idle Time。

图4

为什么要有离屏渲染?

离屏渲染既然不好,为什么它还存在?这要从OpenGL/Metal和GPU说起,GPU有少量的逻辑处理单元和大量的核心,CPU则相反。CPU适合做逻辑运算,复杂的运算,而GPU适合做简单运算,大量重复运算。对于Tiler中的大量顶点运算,Renderer中的着色混合等都适合在GPU上面并行运算。但是GPU不适合做逻辑运算,所以一次只能绘制简单的图元(Primitives),对应到OpenGL中就是GL_POINTS、GL_LINES、GL_LINE_STRIP、GL_TRIANGLES、GL_TRIANGLES_STRIP,比如一个CALayer就由两个三角形(GL_TRIANGLES)组成,绘制普通Layer的时候GPU只需要一个Render Pass即可。而mask效果是将一个层作为“形状”来绘制另一个层,而这个“形状”是无法通过点,线,三角形这些基本图元来描述的,因此mask效果无法用GPU一步绘制出来,但是可以多步组合绘制出来,如上图2描述的三个Rendering Pass组合绘制mask效果。
除此之外,我们知道CALayer的shouldRasterize属性可以强制离屏渲染。Advanced Graphics and Animations for iOS Apps 这样描述:

图5

Rasterization会使用GPU将多个Layer绘制到一个image(Texture)中,并且这个image是会缓存的,以便后续直接使用缓存进行渲染。在Rendering阶段,存在一个颜色混合(https://learnopengl-cn.readthedocs.io/zh/latest/04%20Advanced%20OpenGL/03%20Blending/)的操作,即对应到每一个像素点,在绘制的时候都会取rendertBuffer中的原有颜色与当前颜色按照指定公式计算得到颜色值。对应到OpenGL就是:

glEnable(GL_BLEND);
glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

这个操作在GPU上面是比较耗时的(因为在移动平台GPU上,取rendertBuffer中取颜色是消耗操作)。如果CALayer树结构比较复杂,图层众多,GPU每一帧都得混合所有的层,导致GPU消耗巨大。Rasterization的作用在于可以指定用GPU混合一些复杂的CALayer成Texture,后续直接使用,从而避免GPU的混合消耗。可见离屏渲染不一定降低性能,有时候还可以优化性能。注意图4指明Rasterization会增加内存消耗,同时只适合在图层内容变化不频繁的场景。

帧缓冲区是什么?

帧缓冲区(Frame Buffer)在OpenGL和Metal里面是最基础的概念,可以理解为一块内存画布,类似于Core Graphics的画布一样。对于屏幕内渲染,会将画布的内容输出到屏幕上,指定目标为Render Buffer,在OpenGL中用glFramebufferRenderbuffer来指定:

glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, sampleColorRenderbuffer);
glDrawArrays 或者 glDrawElements

对于离屏渲染,画布的内容输出到Texture上,使用glFramebufferTexture2D指定,比如:

// rendering pass1
glGenFramebuffers(1, &framebuffer1);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer1);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture1, 0);
glDrawArrays 或者 glDrawElements

// rendering pass2
glGenFramebuffers(1, &framebuffer2);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer2);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture2, 0);
glDrawArrays 或者 glDrawElements

...

// final rendering pass
glBindFramebuffer(GL_FRAMEBUFFER, framebufferFinal);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, sampleColorRenderbuffer);
// 

glGenFramebuffers为开辟帧缓冲区,glBindFramebuffer为切换帧缓冲区,对于离屏渲染会有更多的内存分配和切换操作。从上面分析离屏消耗消耗主要在于:
1、glGenFramebuffers导致更多的内存消耗;
2、更多的glDrawArrays和glDrawElements导致GPU有更多绘制操作;
3、切换缓冲区带来的消耗:实际上glBindFramebuffer命令并不会有过多的消耗,根据Andy Matuschak,在回复中的原话:It’s expensive for the GPU to switch contexts from on-screen to off-screen drawing (it must flush its pipelines and barrier),意思是离屏渲染的Context切换会导致渲染管线的flush操作,众所周知OpenGL中的glFlush操作是非常昂贵的,所以我理解这才是Context切换昂贵的最终原因。

为什么Context切换会导致flush

先从表面看图2,mask效果需要3个renderIng pass,只有最后一个rendering pass是输出到屏幕显示,而前面两个rendering pass都是渲染出Texture作为最后一个的输入,而最后一个rendering pass要想获得正确的结果,前面的rendering pass必须先完成。GPU是多核并行计算,而这种依赖关系导致rendering pass无法真正并行执行。
实际上,只要是将Frame Buffer渲染到Texture,Texture又用于后续的渲染,那么后续的渲染都会等待前面的Texture渲染完成,即glFlush操作,以保证最终结果的正确。Performance Tuning for
Tile-Based Architectures
这篇文章也指明使用render to texture的结果会导致flush。

参考

https://objccn.io/issue-3-1/
https://www.seas.upenn.edu/~pcozzi/OpenGLInsights/OpenGLInsights-TileBasedArchitectures.pdf
https://github.com/seedante/iOS-Note/wiki/Mastering-Offscreen-Render
https://developer.apple.com/videos/play/wwdc2014/419/
https://learnopengl-cn.readthedocs.io/zh/latest/04%20Advanced%20OpenGL/03%20Blending/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 143,809评论 1 304
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 61,651评论 1 257
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 95,178评论 0 213
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 41,241评论 0 181
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 49,047评论 1 259
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 38,899评论 1 178
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,503评论 2 274
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,249评论 0 168
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 29,125评论 6 235
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 32,605评论 0 213
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,368评论 2 215
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 30,723评论 1 232
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 24,285评论 0 32
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,190评论 2 214
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 31,634评论 3 209
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,651评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,052评论 0 167
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 33,638评论 2 232
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 33,760评论 2 237

推荐阅读更多精彩内容