20:12

Wednesday, April 20, 2022 (GMT+8)

Time in Guangdong Province, China


简介

某次调图形性能的时候(启动后台录屏,下(或)称case)发现Android SurfaceFlinger Vsync机制并没有以前想的这么简单粗糙,特别是这次调图形性能发现一些跟Vsync有关联,因此做个总结详解。

跟不上旋律节奏的VSYNC

一份追踪报告,发现Vsync信号非常不规律,于是从这里入手分析、总结Vsync。

如下图,As we can see,VSYNC-sf出现严重的漂移(见图,第二行的VSYNC-sf残次不齐、跟不上规律、难看且混乱),这导致了丢帧。(但VSYNC-sf的失控仅表示与丢帧的相关性,并不直接表明因果性。)

从VSYNC信号看,后台录屏RUNNING的情况下,带来的额外工作负载直接压垮了SurfaceFlinger,导致其提交屏幕刷新的VSYNC-sf都出现了严重缺失、错位、延期等——总之是非常不正常的工况

VSYNC信号从全局来看就是触发屏幕刷新的信号。事实上,图形层(SurfaceFlinger)的处理比应用层(App)更加复杂一步——应用层只会看到一个16ms触发(60Hz)的信号,而在图形层,由于图形栈的实现事实上是生产者-消费者模型的流水线,且图形缓冲区大小是编程固定的。这就意味着,VSYNC-sf与VSYNC-app应该是交替出现、规律且稳定的。正常情况下,它们的节拍应该是由App首先完成绘图,然后图形栈(主要是SurfaceFlinger那块)提交到Framebuffer,因此VSYNC-sf和VSYNC-app交替出现,且间隔、每次持续时间都是稳定的。当其中一个信号大幅漂移时,显然有难受的情况发生了。

  • VSYNC-app
    这里简单解释一下VSYNC-sf和VSYNC-app,如下图。As we all known,VSYNC信号由硬件产生(或软件模拟),经过SurfaceFlinger与编舞者(Choreographer)的机制,回调到ViewRootImpl,由ViewRootImpl从根View开始逐层绘制,实现应用层绘图。但实际上,回调Choreographer的VSYNC并不是直接来自底层,而是经过图形层处理后,虚拟的一个VSYNC,标称为VSYNC-app

  • 两个独立的VSYNC
    VSYNC-sf和VSYNC-app是相互独立、互不影响的。系统内并没有限制VSYNC-sf或VSYNC-app之间需要以间隔运行的方式交替发出。其中两条VSYNC都分别有各自固定的节拍——即时间间隔,这个节拍是VSYNC-app和VSYNC-sf都各自拥有一个独立的,可调的时间间隔。VSYNC信号(其中的VSYNC-sf)被设计用于保障图形栈与底层Framebuffer的同步,而另一个VSYNC-app则是用于触发应用绘图(触发渲染线程工作)并与SurfaceFlinger同步。从流水线模型角度看,VSYNC-app和VSYNC-sf机制让应用首先完成绘图,然后由SurfaceFlinger合成、渲染并提交显示。

如下图,Hardware VSync被虚拟分出两个VSYNC,它们有各自独立的节拍(图中用两个Phase offset表示)。

  • VSYNC垂直同步框架
    DispSyncThread将Hardware vsync(HW_VSYNC_0)虚拟地分为VSYNC-app(VSYNC)和VSYNC-sf(SF-VSYNC),分别由两个对应地EventThread处理。其中处理VSYNC-app的EventThread会按照编程的间隔时间回调App进行绘制和渲染(在开动硬件加速的情况下,绘制在主线程亦即UI Thread发生,渲染在Render Thread通过Display List调用GPU发生),而另一个处理VSYNC-sf的EventThread以相同的原理(但是不是取同一个时间间隔对象,虽然这个对象数值上可以等于VSYNC-app的间隔)触发SurfaceFlinger进行合成。合成完成后,图像就被送往Framebuffer或其Flip buffer/OffScreen Buffer,即将显示到屏幕。

VSYNC_EVENT_PHASE_OFFSET_NSSF_VSYNC_EVENT_PHASE_OFFSET_NS可以分别设置VSYNC-app和VSYNC-sf的间隔。

Android的显示流水线(Render Pipeline)由应用层绘图、SurfaceFlinger合成、处理屏幕显示的硬件混合渲染器(HWC)三大流程/组织组成的(HWC合成HWC_OVERLAY,SF合成HWC_FRAMEBUFFER)。这三part分别由VSYNC-app、VSYNC-sf和HW_VSYNC_0控制。图形层虚拟出来的VSYNC编程上是从HW_VSYNC中经过偏移后产生的,相关机制下文简述。

  • VSYNC如何实现?为什么需要虚拟出两个VSYNC信号?
    下图从应用层的角度展示了最简单的垂直同步机制——也就是仅考虑HW_VSYNC的情景(忽略虚拟化的VSYNC-app和VSYNC-sf,装作应用层绘图和SurfaceFlinger合成的触发信号)。它将整个流水线(Pipeline)简化为:VSync刷新屏幕(HW_VSYNC),而应用层绘制(performTraversal)(蓝色块,CPU行,UI Thread使用CPU调度绘图)和硬件加速(绿色块,GPU行,硬件加速情况下会创建Render Thread,它实际使用GPU完成绘图,Display List -> OpenGL -> GPU)、SurfaceFlinger合成(绿色块,GPU行)这两块的虚拟VSYNC触发信号被省略不表了。Besides,现代Android普遍采用比双重缓冲更高的缓冲级别,如下图为三重缓冲(部分vendor可能采用四重缓冲)

下图中,ABC表示时间顺序上的三个图像帧,当蓝块+绿块未能在HW_VSYNC之前完成,即SurfaceFlinger没能完成合成->HWC未能完成合成并提交到offscreen buffer(off-fb),那么在A帧显示完成,需要刷新显示B帧时,没有就绪的buffer,导致原本被替换为B帧的A帧继续显示——掉帧,B帧被“丢掉”了(**==注意,掉帧并不意味着帧真的被丢掉了(不再渲染了)==——它只不过没来得及在DeadDeadline之前提交而已,实际上由于流水线的生产者-消费者模型,它最终还是能够按照既定的顺序上屏显示的**。如下图,第一个VSync发生掉帧,原因是SF合成没有完成,因此HWC不得不暂时不刷新屏幕,让Display继续显示A帧。在接下来的新的一个VSync到来前,SF已经完成工作并提交,此时B帧被显示——它错过了本来要上屏的VSync(掉帧),但在完成工作后上屏了(不会丢失))。

这是非常简化的垂直同步机制,也是应用视角最基本的抽象模型。它能阐述图形层如何保证屏幕刷新以及实现三重缓冲、SurfceFlinger与Display的同步,抽象地忽略了应用层绘图与图形栈的同步、应用层的两帧缓冲。

  • 流水线两帧缓冲:图形栈在合成和渲染阶段会提供两帧缓冲,表现是,Display显示画面N时(号码为N的帧),图形层已经在合成N+1帧了(这个N+1帧是上一个周期中应用层完成的绘图),应用层同步被调度进行N+2帧的绘图。这个流水线保证,上游比下游提前1帧绘制,应用层、图形栈、Display(HWC提交)各自分别提前各自下游1帧的工作。如上图,Display显示帧A(号码N)时,SurfaceFlinger已经在合成N+1了,应用层正在或即将绘制N+2。

虽然应用每帧绘图需要时间n(60Hz下,经过计算,n为16.66ms),但是显示这帧画面需要2n的时间(~33ms,因为n大于16.6ms)。

双重/三重缓冲是指Linux Framebuffer具有多重缓冲:包括正在显示的fb和缓存下一帧画面的off-screen fb。两帧缓冲是指流水线上游比下游提前绘制一帧,应用层的即时绘图是Display接下来会显示的第二帧画面。

一般来说三级流水线配合三个buffer效果最好,四级流水线对应配合四个buffer。如在120Hz的刷新率下,Android提供四级流水线与四重buffer。目前各系统2、3、4级流水线都设计了对应的2、3、4重缓冲搭配实现更好的效果。

  • 查看Android系统中应用的缓冲区
    dumpsys SurfaceFlinger命令输出的最后,根据包名/活动名可以看到对应的buffer。除SurfaceView、非原生图形引擎如Flutter、Cocos2d、Unity、RN、Weex外,常规原生应用都会有三个buffer,对应底层三重缓冲。应用可以通过SurfaceView自行绘制图形,当然也可以通过SurfaceView自行设计实现多重缓冲

接下来简述两个虚拟化出来的VSYNC是如何工作的,实际上它是对上图抽象模型的具象解释,将上图中蓝色块和绿色块调度原理——VSYNC-app和VSYNC-sf进行阐述。

下图。黄线蓝线分别是app和sf的VSYNC相对于HW_VSYNC_0的偏移(延时),当这些线从左往右结束时,出现的黄色块和蓝色块表示app或SF开始工作。

  • HW_VSYNC_0:屏幕刷新信号,Display显示新的一帧画面(N)
  • VSYNC-APP:经过HW_VSYNC_0加上VSYNC-app偏移指定的间隔时间后(下图,HW_VSYNC + App offset = 黄色块开始,应用绘图),应用绘图生成下一帧(N + 2)
  • VSYNC-SF:官方文档标称SF_VSYNC。与VSYNC-APP一致,只不过HW_VSYNC_0加上的偏移量不一样(由VSYNC-sf指定),经过偏移后的时间间隔,SF开始合成下一帧(N + 1)

偏移量是相对HW_VSYNC的相位偏移,可以简单理解为相对于HW_VSYNC_0的延时(可以是负值)。

由于VSYNC-app和VSYNC-sf的存在,以及它们对HW_VSYNC_0的固有的、编程的延时,应用层绘图和SF合成事实上就不是和屏幕刷新同步的,且由于延时的存在,应用绘图和SF合成工时(最大可用工时,超出工时就是掉帧(连续超出工时导致流水线缓存的帧耗尽))事实上小于HW_VSYNC_0间隔

  • 那么,VSYNC偏移的意义是什么?
    VSYNC偏移的意义是,减少画面延时。画面延时不是丢帧,它是指,原本应用的画面经过流水线处理,在N+2进行显示,而画面延时则导致该帧在N+3才显示。

减少画面延时还有一个绝招,就是使用SurfaceView,自行控制绘制节奏。

首先VSYNC-sf很好理解,因为它的流水线上游(应用)首先完成绘图,然后才到SF合成,所以SF当然完全没必要紧跟着HW_VSYNC_0VSYNC-sf的偏移,即上图中SF开始合成时相对于HW_VSYNC_0的延时Aphase-sf的作用是,**在延时期内,等待、确保应用完成绘图,==一旦VSYNC-sf到达,SF开始合成图形,对于没有完成画面更新的app,将继续使用old passed画面帧(上一帧,已合成/显示过的帧)进行合成——这意味着,SF按照自己的节奏工作,保证自己及时(下一个HW_VSYNC_0到来之前)提交合成的画面。对于一部分没来得及完成绘制的应用,从它们单个应用的角度来看,它们发生了掉帧,但是SF完成了提交,所以系统全局并没有掉帧==**。

没有VSYNC-sf或VSYNC-sf很短
应用来不及完成绘图,在绘图完成前SF就触发合成了,此时该应用仍以上一帧画面参与合成,展示到屏幕上时画面仍然是上一帧。对于SF来说它及时提交了,然而对于应用来说它掉了1帧。而这个“掉”的一帧,最快(如果应用完成绘制的话)会在下一个VSYNC-sf才合成、下一个HW_VSYNC_0才上屏,这意味着画面延时了。

对于VSYNC-app,这个延时是为了给App留下一点处理时间,在绘制之前App可能想要更新UI控件、调整绘制细节。

  1. 应用的 UI 线程处理输入事件,调用应用的回调,并更新视图层次结构中记录的绘图命令列表(DisplayLists)
  2. 应用的 RenderThread 将记录的命令发送到 GPU (GPU硬件加速)
  3. GPU 绘制这一帧(是指应用对应窗口的一帧,还不是SF合成的全屏幕的帧)
  4. SF合成各个Layer的画面,并将画面提交到HAL;在下一个HW_VSYNC_0中画面上屏

在俺的某个设备中,VSYNC-sf、VSYNC-app都配置为~8.3ms

  • VSYNC-sf为什么会出现偏差?
    出于功耗的考虑,VSYNC-sf合VSYNC-app并不是一定会触发的。如果app或sf并没有更新画面的需求,那么死板固定地调度它们进行绘制和合成是不必的。编程上,负责触发VSYNC-sf和VSYNC-app的两个EventThread会在requestNextVsync调用后才会将下一个VSYNC-sf或VSYNC-app发出。因此,当requestNextVsync没有调用时,VSYNC-app和VSYNC-sf也就出现漂移。BufferQueueLayer::onFrameAvailable会在应用提交后调用,该方法通过调用SF的signalLayerUpdate触发产生下一个VSYNC-sf

到此,VSYNC-sf虽然出现了偏差,但是目前看它与卡顿问题仅有相关性,并非因果关系。猜测是其他卡顿问题导致了SF延缓了对VSYNC的request,导致其信号出现漂移。

参考

  1. Vsync机制
  2. 显示流水线VSYNC垂直同步