【Graphic】Android5.0 Graphic(1) VSync与Project Butter

来源:互联网 发布:java返回string 编辑:程序博客网 时间:2024/06/06 06:50


VSync的概念

 

典型的现实系统中,Frame Buffer代表了显示一帧画面的缓冲数据。GPU/CPU绘图过程与屏幕刷新使用的是同一块缓冲,它们的速度不同的时候,就会出现画面“割裂”的显示。假如屏幕的刷新率为66HZ,而CPU/GPU的绘图能力有100HZ,它们分别处理完成一帧数据需要0.015秒和0.01秒。


Fromhttps://en.wikipedia.org/wiki/File:Tearing_%28simulated%29.jpg

 

以时间为横坐标来描述接下来会发生的事情,如下图:


 Screen Tearing产生过程分析

 

上半部分的方框表示在不同的时间点时显示屏的内容(加深的部分),下半部分则是同一时间点时frame buffer中的数据状态,编号表示第几个frame,不考虑清屏。

·0.01

由于两者速率相差不小,此时buffer中已经准备好了第1帧数据,显示器只显示了第1帧画面的2/3

·0.015

1帧画面完整地显示出来了,此时buffer中有1/3的部分已经被填充上第2帧数据了。

·0.02

Buffer中已经准备好第2帧数据,而显示屏出现了screentearing,有三分之一是第2帧内容,其余的则属于第1帧画面。

 

在单缓冲区的情况下,这个问题很难规避。采用双缓冲技术,基本原理就是采用两块buffer。一块back buffer用于CPU/GPU后台绘制,另一块framebuffer则用于显示,当back buffer准备就绪后,它们才进行交换。不可否认,doublebuffering可以在很大程度上降低screen tearing错误,但也不是万能的。这就是后来GoogleUI这块进行了优化,引入了VSynctriple buffering的实现。

 

一个需要考虑的问题是我们什么时候进行两个缓冲区的交换呢?假如是back buffer准备完成一帧数据以后就进行,那么如果此时屏幕还没有完整显示上一帧内容的话,肯定是会出问题的。看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。

 

典型的显示器有两个重要特性,即“行频和场频”。行频(Horizontal Scanning Frequency)又称为“水平扫描频率”,是屏幕每秒钟从左至右扫描的次数;场频(Vertical Scanning Frequency)又称为“垂直扫描频率”,是每秒钟整个屏幕刷新的次数。所以有如下的关系,行频=场频×纵坐标分辨率。

 

当扫描完一个屏幕之后,设备需要回到第一行以进入下一轮的循环,此时有一段时间的空隙,称为Vertical Blanking IntervalVBI)。对于使用双缓冲区的情况下,这个时间点就是需要缓冲区交换的最佳时间。此时屏幕没有在刷新,避免了交换过程中出现Screen Tearing的现象。VSync(垂直同步)是Vertical Synchronization,利用VBI时期出现的VerticalSync Pluse来保证双缓冲能在最佳时间点进行交换。举例来说,如果屏幕的刷新率为60HZ,那么生成帧的速度就应该被固定在1/60秒。

 

Project Butter

 

直译过来,就是“黄油计划”,为什么叫这个名字呢?这个Project的目的是为了改善用户抱怨最多的Android几大缺陷之一,即UI响应速度——Google希望这一新计划可以让Android系统摆脱UI交互上给人带来的“滞后”感,而能像黄油一般“顺滑”。Google2012年的I/O大会上宣布了这一计划,并在Android 4.1中正式搭载了实现机制。

 

Project ButterAndroidDisplay系统进行了重构,引入了三个核心元素,即VSYNCTriple BufferChoreographer

 

前面讨论的情况基于的假设是绘图速度大于显示速度,那么如果反过来呢?


绘图过程没有采用VSync同步的情况

(引用自Google2012 I/O,作者Chet HaaseRomain GuyAndroidUI Toolkit Engineers

 

这个图中有三个元素,Display是显示屏幕,GPUCPU负责渲染帧数据,每个帧以方框表示,并以数字进行编号,如012等等。VSync用于指导双缓冲区的交换。

 

以时间的顺序来看下将会发生的异常:

Step1. Display显示第0帧数据,此时CPUGPU渲染第1帧画面,而且赶在Display显示下一帧前完成。

Step2.因为渲染及时,Display在第0帧显示完成后,也就是第1VSync后,正常显示第1帧。

Step3.由于某些原因,比如CPU资源被占用,系统没有及时地开始处理第2帧,直到第2VSync快来前才开始处理。

Step4.2VSync来时,由于第2帧数据还没有准备就绪,显示的还是第1帧。这种情况被Android开发组命名为“Jank”。

Step5.当第2帧数据准备完成后,它并不会马上被显示,而是要等待下一个VSync

 

所以总的来说,就是屏幕平白无故地多显示了一次第1帧。原因大家应该都看到了,就是CPU没有及时地开始着手处理第2帧的渲染工作,以致“延误军机”。 Android系统中一直存在着这个问题,即便是上一版本的Ice Cream Sandwich

 

Android 4.1Jelly Bean开始,VSync得到了进一步的应用。系统在收到VSync pulse后,将马上开始下一帧的渲染。


整个显示系统都以VSync进行同步

 

如上图所示,一旦VSync出现后,CPU不再犹豫,紧接着就开始执行buffer的准备工作。大部分的Android显示设备刷新率是60Hz,这也就意味着每一帧最多只能有1/60=16ms左右的准备时间。假如CPU/GPUFPS(FramesPer Second)高于这个值,那么这个方案是完美的,显示效果将很好。

 

可是我们没有办法保证所有设备的硬件配置都能达到要求。假如CPU/GPU的性能无法满足上图的条件,又是什么情况呢?

 

在分析这一问题之前,我们先来看下正常情况下,采用双缓冲区的系统的运行情况。


双缓冲展示

 

这个图采用了双缓冲,以及前面介绍的VSync,可以看到整个过程还是相当不错的,虽然CPU/GPU处理所用的时间时短时长,但总的来说都在16ms以内,因而不影响显示效果。AB分别代表两个缓冲区,它们不断地交换来正确显示画面。

 

现在我们可以继续分析FPS低于屏幕刷新率的情况。

如下图所示:


FPS低于屏幕刷新率的情况

 

CPU/GPU的处理时间超过16ms时,第一个VSync到来时,缓冲区B中的数据还没有准备好,于是只能继续显示之前A缓冲区中的内容。而B完成后,又因为缺乏VSync pulse信号,它只能等待下一个signal的来临。于是在这一过程中,有一大段时间是被浪费的。当下一个VSync出现时,CPU/GPU马上执行操作,此时它可操作的bufferA,相应的显示屏对应的就是B。这时看起来就是正常的。只不过由于执行时间仍然超过16ms,导致下一次应该执行的缓冲区交换又被推迟了——如此循环反复,便出现了越来越多的“Jank”。

 

那么有没有规避的办法呢?

 

很显然,第一次的Jank看起来是没有办法的,除非升级硬件配置来加快FPS。我们关注的重点是被CPU/GPU浪费的时间段,怎么才能充分利用起来呢?分析上述的过程,造成CPU/GPU无事可做的假象是因为当前已经没有可用的buffer了。换句话说,如果增加一个buffer,情况会不会好转呢?


 Triple Buffering

 

Triple BufferingMultipleBuffering的一种,指的是系统使用3个缓冲区用于显示工作。我们来逐步分析下这个新机制是否有效。首先和预料中的一致,第一次“Jank”无可厚非。不过让人欣慰的是,当第一次VSync发生后,CPU不用再等待了,它会使用第三个buffer C来进行下一帧数据的准备工作。虽然对缓冲区C的处理所需时间同样超过了16ms,但这并不影响显示屏——第2VSync到来后,它选择buffer B进行显示;而第3VSync时,它会接着采用C,而不是像double buffering中所看到的情况一样只能再显示一遍B了。这样子就有效地降低了系统显示错误的机率。

 

底层的BufferQueue中最多有32BufferSlot,不过在实际使用时具体值是可以设置的。

 

·TARGET_DISABLE_TRIPLE_BUFFERING

这个宏用于disable triple buffering。如果宏打开的话,Layer.cpponFirstRef有如下操作:

 

#ifdefTARGET_DISABLE_TRIPLE_BUFFERING

#warning"disabling triplebuffering"

 mSurfaceTexture->setBufferCountServer(2);

#else

 mSurfaceTexture->setBufferCountServer(3);

#endif

也就是将mSurfaceTexture(BufferQueue)中的mServerBufferCount设为2,否则就是3

 

·对于应用程序来说,它也可以通过ISurfaceTexture::setBufferCount来告诉BufferQueue它希望的Slot值,对应的则是mClientBufferCount。默认情况下这个变量是0,表示应用端不关心到底有多少buffer可用。

 

·BufferQueue中还有另一个变量mBufferCount,默认值是MIN_ASYNC_BUFFER_SLOTS。在具体的实现中,以上这三个变量都是要考虑到的,BufferQueue会通过权衡各个值来选择最佳的解决方式。


参考文档:《深入理解Android内核设计思想》

0 0
原创粉丝点击