显示器为什么是画面撕裂:遇到wait for vsync怎么处理?
什么叫水平同步?什么叫垂直同步?


Unity3D中新建一个场景空的时候,帧速率(FPS总是很低),大概在60~70之间。一直不太明白是怎么回事,现在基本上明白了。我在这里解释一下原因,如有错误,欢迎指正。在Unity3D中当运行场景打开Profiler的时候,我们会看到VSync]

]       什么叫水平同步?什么叫垂直同步?
]       为什么关闭垂直同步信号会影响游戏中的FPS数值?
]如果我们选择等待垂直同步信号(也就是我们平时所说的垂直同步打开),那么在游戏中或许强劲的显卡迅速的绘制完一屏的图像,但是没有垂直同步信号的到达,显卡无法绘制下一屏,只有等85单位的信号到达,才可以绘制。这样FPS自然要受到操作系统刷新率运行值的制约。
]

WaitForTargetFPS,是关于帧数限制的,你可能开了垂直同步,其实是防止撕裂。
先说撕裂,在显示器的帧缓存会被不同步的显卡的帧缓存给替换掉,导致显示器显示到一半的时候,内存被换掉,你看到上频是上一针的画面,下频是下一针的画面,游戏刷新的频率越快,撕裂就越严重,所以就会撕裂了。
开了垂直同步,是指将游戏帧数锁定到和你的显示器刷新频率一样的,因为显卡和显示器的刷新频率不一样。简单的说,WaitForTargetFPS消耗越高,说明被压缩的帧数越多,能用的资源越多,等待的可用的进程越多。只要关闭垂直同步就可以了。
比如说你项目能跑到60帧,但是你限定最高是30帧,那么他就会强制压到30帧,这中间可能会出现差值,所以需要等待。




Gfx.WaitForPresent && Graphics.PresentAndSync

这两个参数在Profiler中经常出现CPU占用较高的情况,且仅在发布版本中可以看到。究其原因,其实是CPU和GPU之间的垂直同步(VSync)导致的,之所以会有两种参数,主要是与项目是否开启多线程渲染有关。当项目开启多线程渲染时,你看到的则是Gfx.WaitForPresent;当项目未开启多线程渲染时,看到的则是Graphics.PresentAndSync。

Graphics.PresentAndSync 是指主线程进行Present时的等待时间和等待垂直同步的时间。Gfx.WaitForPresent其字面意思同样也是进行Present时需要等待的时间,但这里其实省略了很多的内容。其真实的意思应该是为了在渲染子线程(Rendering Thread)中进行Present,当前主线程(MainThread)需要等待的时间



同时,对于开启垂直同步的项目而言,Gfx.WaitForPresent 和 Graphics.PresentAndSync也会出现CPU占用较高的情况。在解释这种问题之前,我们先以“大家乘坐地铁”来举个例子。一般来说,地铁到达每一站的时间均是平均且一定的,假设每10分钟一班接走一批乘客。但是几乎没有多少乘客可以按点到达,如果提前两分钟到达,则只需要等待两分钟即可乘上地铁,但是,如果你错过了,哪怕只差了一分钟,那么你也不得不再等待九分钟才能乘上地铁。

上述的情况我们经常会遇到。在GPU的渲染流水线中,其转换front buffer和back buffer的工作原理和“乘坐地铁”其实是一致的。大家可以把GPU的流水线简单地想象成为一列地铁。对于移动设备来说,GPU的帧率一般为30帧/秒或60帧/秒,即VSync每33ms或每16.6ms“到站一次”,CPU的Present即为“乘客乘上地铁”,然后前往各自的目的地。与乘客的早到和晚到一样,CPU的Present也会出现类似的情况。


VSYNC在显示周期内同步一些确定的事件,APP在VSYNC结束的时间点绘制画面,也是在这个时间点SurfaceFlinger进行画面合成。这种机制消除了卡顿,提高了图形的视觉表现。硬件合成器(HWC)引用了VSYNC的实现函数


int (waitForVsync*) (int64_t *timestamp)

在VSYNC发生之前这个函数是阻塞的,函数会返回真实的VSYNC时间戳,每次VSYNC同步时总会有一个消息发出。客户端在指定的时间间隔会收到VSYNC时间戳,你必须用最大1毫秒的延迟来实现垂直同步(推荐0.5毫秒或更少);


显式同步


显示同步是必须的,它提供了一个以同步方式持有和释放Gralloc buffers的方式。显示同步允许graphics buffers的生产者和消费者在处理完一个buffer的时候发送一个信号。这种机制允许安卓进入队列的buffers可以被异步读写,

此时这些buffers不会被其余的第三者消费者或生产者需要。


这个显示同步有很多优点比如:设备之间的行为变化更少,更好的调试支持,改进的测试指标。例如,同步框架更容易找出问题根源,并在系统正常流事件发生时集中表现surfaceflinger时间戳。


这种通信得益于synchronization fences的使用,当请求一个buffer用来消费或者生产的时候synchronization fences是会被使用的。同步框架有三个主要模块:sync_timeline,sync_pt,和sync_fence。


sync_timeline


一个sync_timeline是一个单调递增的时间线,这个时间线会被每一个驱动实例使用,比如,一个GL context, display controller, or 2D blitter.本质上这是一个要提交到内核特定部位的一系列工作。它保证操作顺序和硬件可以实现。


这个sync_timeline是提供一个CPU-only的参考实现,这个实现被称作sw_sync(software sync)。如果可能的话,用它代替sync_timeline以便节省资源和避免复杂性。如果你不使用硬件资源,sw_sync应该足够了。


如果你要实现一个sync_timeline,使用sw_sync驱动。遵循这些准则:


  • 为所有drivers, timelines, 和 fences提供有用的名称。这简化了调试。


  • 在你的timelines实现timeline_value_str和pt_value_str操作让调试信息更加可读。


  • 如果你想让你的用户空间库(如GL库)来访问你的timelines的私有数据,那么你可以填充driver_data算子。这可以让你获得不会改变的sync_fence和sync_pts对象,建立基于命令行的命令。



当实现一个sync_timeline请不要:


  • 建立在任何真正的时间轴,比如墙上的挂钟。最好是创建一个你可以控制抽象的时间轴。


  • 允许用户显式地创建或发送fence。这会导致一个用户管道产生拒绝服务攻击从而导致停止掉了所有的功能。这是因为用户不能对内核的稳定负责。


  • 不要去访问sync_timeline,sync_pt或者sync_fence元素,需要的话请使用相关API.



sync_pt


一个sync_pt是sync_timeline的一个点,它有三个状态:active, signaled, and error. 它开始于active状态过度到signaled或error状态.例如,当一个buffer不再被一个image consumer需要,这个点会发出信号让image producers知道再一次写入这个buffer是可以的.


sync_fence


一个sync_fence是一个sync_pts的集合,这个集合通常有不同的sync_timeline父亲(比如display controller and GPU),这些是主要的驱动程序和用户空间通信原始的依赖关系。fence是内核保证能按时完成一个进入队列的一个工作的承诺。


它允许多个消费者或生产者发出信号说明他们正在使用一个buffer ,并允许这些信息与一个函数的参数进行通信。Fences由文件描述符支持,可以从内核空间传递到用户空间。比如说,一个fence可以包含两个sync_points,这表示两个单独的image consumers正在读取同一个buffer。当fence发出信号时,image producers知道消费者都在消费。


Fences,像sync_pts,开始的状态是active随后sync_pts根据自己的点的状态来进行调整状态。如果所有的sync_pts变成signaled状态,那么sync_fence也会变成signaled状态。如果有一个sync_pt变成error状态,整个sync_fence也会变成error状态。


sync_fence的成员在fence创建完成后是不可以改变的。一个sync_pt只可在一个fence里面,作为一个副本。即使两个points有相同的值,那么也将有两份sync_pt在fence里面。要在fence中获得一个以上的point,就要进行合并操作,从两个不同的fences把points添加到第三个fence。如果原始的fence里面有一个是在signaled状态,另一个则不是,被合成的第三个fence也不会处于signaled状态。


若要实现显式同步,请提供以下文件:


  • 实现特定硬件的同步时间轴的内核空间驱动器。需要fence-aware的驱动程序通常是为了与Hardware Composer进行访问或通信。关键文件包括:




  • 核心文件:



    • kernel/common/include/linux/sync.h


    • kernel/common/drivers/base/sync.c


  • sw_sync:



    • kernel/common/include/linux/sw_sync.h


    • kernel/common/drivers/base/sw_sync.c


  • 文档在 kernel/common//Documentation/sync.txt.


  • 和kernel-space通信的库在 platform/system/core/libsync.




  • Hardware Composer HAL模块(v1.3或更高),支持新的同步功能。你必须提供synchronization fences作为HAL里面set()和prepare()函数的参数。


  • 两个fence-related GL扩展(EGL_ANDROID_native_fence_sync 和 EGL_ANDROID_wait_sync)并且你的图形驱动程序支持fence。



例如,使用支持同步功能的API,可以开发具有显示buffer功能的显示驱动程序。在同步框架存在之前,这个函数会接收dma-bufs,把这些buffers显示,如果buffer是可见的那么这个函数处于block状态。例如:

/*
* assumes buf is ready to be displayed.  returns when buffer is no longer on
* screen.
*/

void display_buffer(struct dma_buf *buf);

使用同步框架,API调用稍微复杂一些。在将buffer放在显示器上时,您可以将它与buffer准备就绪时的fence关联起来。在清除fence之后你可以把任务放入队列。


以这种方式,你不会阻塞任何东西。您立即返回您自己的fence,这是保证buffer从显示器消失。当buffers进入队列时,内核将列出同步框架的依赖项:


/*
* will display buf when fence is signaled.  returns immediately with a fence
* that will signal when buf is no longer displayed.
*/

struct sync_fence* display_buffer(struct dma_buf *buf, struct sync_fence
*fence);

Sync 集成

本节介绍如何将Android不同部分的低级别同步框架和必须相互通信的驱动程序集成在一起。

整合规范

对于图形的Android HAL接口遵循一致的约定,所以当文件描述符在HAL接口上传递时,文件描述符的所有权总是被转移的。这意味着:


  • 如果从同步框架接收到一个fence文件描述符,则必须关闭它。


  • 如果将一个fence文件描述符返回到同步框架,框架将关闭它。


  • 要继续使用fence文件描述符,必须复制描述符。



每一次,一个fence传递给BufferQueue(如一个窗口通过传递fence给BufferQueue来说明自己新的内容已经准备好)fence对象会被重命名。内核支持fences用字符串表示名称,同步框架使用的窗口名称和正在进入队列的buffer index命名这个fence(比如,SurfaceView:0)。如果这个名字出现在/d/sync形式的log里或bugreports,那么对定位死锁问题的原因非常重要。


ANativeWindow 集成

ANativeWindow是fence一体化,即dequeuebuffer,queuebuffer,和cancelbuffer 包含fence的引用。


OpenGL ES的集成

OpenGL ES同步整合依赖于两个EGL扩展:


  • EGL_ANDROID_native_fence_sync.提供了一种以EGLSyncKHR对象的方式来创建原生的Android fence文件描述符。


  • EGL_ANDROID_wait_sync.允许GPU而不是CPU等待,让GPU等待一个EGLSyncKHR。本质上是和EGL_KHR_wait_sync扩展是相同的(指规范细节)。



这些扩展可以独立使用,被在libgui里面的编译标志位控制。要使用它们,首先实现EGL_ANDROID_native_fence_sync扩展来获得内核的相关支持。下一步,添加一个对于fences支持的ANativeWindow驱动程序,然后打开libgui的编译标志位来确保使用EGL_ANDROID_native_fence_sync扩展。


第二步,打开EGL_ANDROID_wait_sync扩展并把它分开。EGL_ANDROID_native_fence_sync扩展由不同的原生fence EGLSync对象类型组成,因此正在应用于EGLSync对象的类型不需要应用于EGL_ANDROID_native_fence对象类型以避免不必要的相互影响。


EGL_ANDROID_native_fence_sync扩展采用相应的原生fence文件描述符属性。这种属性只能设置在创建时不能直接查询到一个现有的同步对象。此属性可以设置为两种模式之一:


  • 有效的fence文件描述符。把原生Android fence 文件描述符封装为EGLSyncKHR 对象。


  • - 1.从EGLSyncKHR 对象创建一个原生的Android fence文件描述符.



DupNativeFenceFD函数的作用是从原生Android fence文件描述符提取EGLSyncKHR对象。这与查询被设置的属性有相同的结果,但遵守接收者关闭fence的约定(会重复操作)。最后,销毁EGLSync对象时应该关闭内部fence属性。


Hardware Composer集成

The Hardware Composer 处理下面三种类型的sync fences:


  • Acquire fence.每一个层,在调用 HWC::set 之前进行设置。当Hardware Composer读取buffer的时候它变为signals 模式。


  • Release fence.每一个层,在HWC::set调用后被填满。当Hardware Composer读取buffer完毕,它会变成signals 状态,以便framework可以给特定的层再次使用该buffer。


  • Retire fence. 对于每一个完整的层框架,HWC::set 每次被调用都会被填满。所有层的HWC::set 操作全部完成后它会变成signals状态并通知framework 。在下一次HWC::set操作的结果显示在屏幕上的时候,retire fence将变为signals状态。




垂直同步偏移(VSYNC offset)


APP和SurfaceFlinger循环渲染过程应该和hardware VSYNC同步。hardware VSYNC事件发生时顺序是display显示frame N,SurfaceFlinger合成frame N+1,app生成frame N+2。


Synchronizing with VSYNC 发出一致的延时。这样可以减少APP和SurfaceFlinger和display彼此的误差。假定APP和SurfaceFlinger的每帧时间损耗比较固定,那么即使这样,延时至少也是两帧。


为了解决这个问题,你可以采用VSYNC offsets减少input-to-display的延时通过让APP和composition signal与hardware VSYNC进行比较。这是可能的,因为app加composition的过程通常需要少于33ms。


VSYNC offset的结果是三个信号同一段时间偏移相同:


  • HW_VSYNC_0. Display开始显示下一个frame.


  • VSYNC. App读取输入开始展示下一个frame.


  • SF VSYNC. SurfaceFlinger开始合成下一个frame.



VSYNC offset,SurfaceFlinger接收buffer、合成frame,APP处理事件渲染frame,都发生在一个frame的时间周期内。

注意:VSYNC offsets减少了APP和composition的可利用时间从而减少了错误的发生。


DispSync

DispSync保持了一个定期的hardware-based VSYNC显示模型。这个模型保证了执行来自hardware VSYNC的定期回调函数。


DispSync是一个software phase lock loop (PLL) 。它生成Choreographer使用的VSYNC signals 和SurfaceFlinger使用的SF VSYNC信号也包括hardware VSYNC没有偏移(offset )的情况。


图一.DispSync 流程


DispSync具有下面的特点:

Reference. HW_VSYNC_0.

Output. VSYNC and SF VSYNC.

Feedback. Retire fence signal timestamps from Hardware Composer.


VSYNC/Retire offset

retire fences signal 时间戳需要匹配HW VSYNC即使设备没有使用

offset phase。否则会出现严重错误。


三角模型里面Retire fence是direct memory access (DMA)显存显示的最后阶段,但实际display switch and HW VSYNC有时候会在后面。


PRESENT_TIME_OFFSET_FROM_VSYNC_NS宏被设置在设备的BoardConfig.mk文件里。它是基于显示控制器和面板特性。这个值代表的是retire fence和HW VSYNC signal的纳秒级别的时间差。


VSYNC and SF_VSYNC offsets

VSYNC_EVENT_PHASE_OFFSET_NS和SF_VSYNC_EVENT_PHASE_OFFSET_NS高负荷的使用情况下的临界值,如部分GPU在窗口平移或者滚动页面的情況下。这些offsets允许更长的应用程序渲染时间和更长的GPU合成时间。


超过1ms或者两个延迟是明显的。我们建议减少延迟,而不是增加错误次数。

注意:这两个offsets也被配置在了设备的BoardConfig.mk文件里面。表示和

HW_VSYNC_0时间戳的差单位是纳秒,如果这两个值不被赋值那么默认是0,也可以被设置为负数。



大家在玩游戏的时候应该注意到了“垂直同步”这个名词。现在很多游戏特效设置了里边都有这个选项,那么到底是打开还是关闭垂直同步?垂直同步是什么意思,有什么作用呢?

      提示:如何打开或关闭垂直同步
进入游戏后在游戏的画面设置里边一般都会有垂直同步的开关选项的,如图:



      垂直同步是什么意思?

       垂直同步又称场同步(Vertical Hold),从CRT显示器的显示原理来看,单个象素组成了水平扫描线,水平扫描线在垂直方向的堆积形成了完整的画面。显示器的刷新率受显卡DAC控制,显卡DAC完成一帧的扫描后就会产生一个垂直同步信号。我们平时所说的打开垂直同步指的是将该信号送入显卡3D图形处理部分,从而让显卡在生成3D图形时受垂直同步信号的制约。

       垂直同步的作用:

      简单来说垂直同步的作用是防止画面撕裂。因为画面的渲染不是整个画面一起渲染的,是逐行或者逐列渲染。如果关闭垂直同步,而电脑配置不够,则画面在高速移动中会出现这一画面还没渲染完成就开始下一画面的撕裂情况。

       打开与关闭垂直同步的主要区别在于那些高速运行的游戏,比如实况,FPS游戏,打开后能防止游戏画面高速移动时画面撕裂现象,当然打开后如果你的游戏画面FPS数能达到或超过你显示器的刷新率,这时你的游戏画面FPS数被限制为你显示器的刷新率。你会觉得原来移动时的游戏画面是如此舒服,如果达不到会出现不同程度的跳帧现象,FPS与刷新率差距越大跳帧越严重。关闭后除高速运动的游戏外其他游戏基本看不出画面撕裂现象。关闭此选项画面流畅程度会有一定的提高。

       为什么是否关闭垂直同步信号会影响我们CS中的fps数值?道理一点都不复杂,首先我们平时运行操作系统一般屏幕刷新率是多少?大概一般都是在85上下吧,那么显卡就会每按照85的频率时间来发送一个垂直同步信号,信号和信号的时间间隔是85的分辨率所写一屏图像时间。

      如果我们选择“等待垂直同步信号”(也就是我们平时所说的“垂直同步打开”),那么在游戏中,或许强劲的显卡迅速的绘制完一屏的图像,但是没有垂直同步信号的到达,显卡无法绘制下一屏,只有等85单位的信号到达,才可以绘制。这样fps自然要受到操作系统刷新率运行值的制约。

      而如果我们选择“不等待垂直同步信号”(也就是我们平时所说“关闭垂直同步”),那么游戏中作完一屏画面,显卡和显示器无需等待垂直同步信号,就可以开始下一屏图像的绘制,自然可以完全发挥显卡的实力。

      但是,不要忘记,正是因为垂直同步的存在,才能使得游戏进程和显示器刷新率同步,使得画面平滑,使得画面稳定。取消了垂直同步信号,固然可以换来更快的速度,但是在图像的连续性上,性能势必打折扣。这也正是很多朋友抱怨关闭垂直后发现画面不连续的理论原因!


      什么是画面撕裂:

      画面撕裂可以简单的理解为,这一帧的数据打进显示器,上一帧的还没消去。结果就是画面上面和下面显示不同步。

      其实图像显示是一张一张的图片高速的覆盖,形成影像。而通常说的帧数,就是指每秒刷新图像的次数。计算机算出图片结果就要渲染在屏幕上,而渲染和算出的花费时间是不一样的,
通常算的会非常快,而渲染要慢的多。开了垂直同步,计算机就会等待上一张图片渲染完成后才会发出开始下一张渲染的命令;不开垂直同步,那么计算机就急不可待的想把算好的结果渲染在屏幕上,这样就容易出现上一张还没有渲染完,又来新的图来覆盖,时间差大到一定程度就会出现屏幕撕裂。

      下边是一张关于画面撕裂的例子(实际情况,没有图中这么夸张的)


      什么情况下打开或关闭垂直同步:

      在玩fps类游戏时,如果帧数过高画面有撕裂的话就会感觉头痛头晕。此时就需要打开垂直同步,以保持画面输出的稳定。一般游戏建议开垂直,保护显卡,降低功耗,而普通的60帧与在高的帧数已经无法用肉眼直接看出区别了,不影响游戏体验。除非你的显卡性能非常低,不开垂直远远不能达到屏幕刷新率的帧数,这种情况下关闭垂直同步,可以一定程度上减小显卡的负担,让画面更加流畅一些。