本文目录一览:
- 1、Android 图形系统(2)---- Android vsync 机制与 project buffer
- 2、Android 图形系统(3)---- DRM 显示框架初步
- 3、Android图形系统系统篇之HWC
- 4、Android图形系统(十一)-Choreographer
Android 图形系统(2)---- Android vsync 机制与 project buffer
一个典型的图形显示系统包含 CPU,GPU,Display 三个部分,CPU负责计算需要渲染的数据,把计算好的数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到buffer(图像缓冲区)里存起来,然后Display(屏幕或显示器)负责把buffer里的数据呈现到屏幕上。
CPU 和 GPU 的计算是可以并行执行的。
屏幕刷新频率 一秒内屏幕刷新的次数(一秒内显示了多少帧的图像),单位 Hz(赫兹),如常见的 60 Hz。 刷新频率取决于硬件的固定参数 (不会变的)。
帧率 (Frame Rate) 表示 GPU 在一秒内绘制操作的帧数 ,单位 fps。例如在电影界采用 24 帧的速度足够使画面运行的非常流畅。而 Android 系统则采用更加流程的 60 fps,即每秒钟GPU最多绘制 60 帧画面。帧率是动态变化的,例如当画面静止时,GPU 是没有绘制操作的,屏幕刷新的还是buffer中的数据,即GPU最后操作的帧数据。
屏幕刷新频是固定的,比如每16.6ms从buffer取数据显示完一帧,理想情况下帧率和刷新频率保持一致,即每绘制完成一帧,显示器显示一帧。但是CPU/GPU写数据是不可控的,所以会出现buffer里有些数据根本没显示出来就被重写了,即buffer里的数据可能是来自不同的帧的, 当屏幕刷新时,此时它并不知道buffer的状态,因此从buffer抓取的帧并不是完整的一帧画面,即出现画面撕裂(tearing)。
简单来说就是 边画边显示 造成的,正在显示的buffer 里的内容被篡改,导致显示画面里的内容不是来自同一帧数据。
double buffe双缓存,让绘制和显示器拥有各自的buffer:GPU 始终将完成的一帧图像数据写入到 Back Buffer,而显示器使用 Front Buffer,当屏幕刷新时,Frame Buffer 并不会发生变化,当Back buffer准备就绪后,它们才进行交换。如下图:
Android4.1之前,屏幕刷新也遵循 上面介绍的双缓存+VSync 机制;但是会存在下面的缺陷。
上层的有更新画面的需求时,才会去重新绘制和显示画面;
上层更新画面的时机是不确定的。
问题:
第2个Diplay VSync来时,由于第2帧数据还没有准备就绪,缓存没有交换,显示的还是第1帧。这种情况被Android开发组命名为“Jank”,即发生了 丢帧 。
为了优化显示性能,Google在Android 4.1系统中对Android Display系统进行了重构,实现了Project Butter(黄油工程):只有系统在收到Display VSync后,才会马上开始下一帧的渲染。即一旦收到VSync通知(16ms触发一次),CPU和GPU 才立刻开始计算然后把数据写入buffer
总结起来就是只有在vsync 到来之后,才会开始进行帧的渲染和绘制。
VSync同步使得CPU/GPU充分利用了16.6ms时间,减少jank。
这里再考虑CPU和 GPU处理时间超出一个Vsync 的情形
如果在double buffer 基础上增加一个,使用triple buffer:
第一个Jank,是不可避免的。但是在第二个 16ms 时间段,CPU/GPU 使用 第三个 Buffer 完成C帧的计算,虽然还是会多显示一次 A 帧,但后续显示就比较顺畅了,有效避免 Jank 的进一步加剧。
Android 图形系统(3)---- DRM 显示框架初步
DRM 是目前主流的图形显示框架,Linux 内核中已经有Framebuffer 驱动用于管理显示设备的 Framebuffer, Framebuffer 框架也可以实现Linux 系统的显示功能,但是缺点如下:
下面是Linux graphic system 的框架,基于Wayland的Windowing system,在DRI框架下,通过两条路径(DRM和KMS),分别实现Rendering和送显两个显示步骤,注意观察 DRM 框架在其中的作用
DRM 框架的基本流程框图如下:
软件角度框图:
DRM框架涉及到的元素很多,大致如下:
KMS: CRTC,ENCODER,CONNECTOR,PLANE,FB,VBLANK,property
GEM: DUMB、PRIME、Fence
下面一一介绍
libdrm:
应用层的一个动态链接库,对底层接口进行封装,向上层提供通用的API接口,主要是对各种IOCTL接口进行封装。
KMS:
Kernel mode setting 简而言之做两件事:更新画面 + 设置显示参数
更新画面: 显示buffer 的切换,多图层之间的合成方式,每个图层的显示位置
设置显示参数:包含 刷新率,分辨率,电源状态 休眠唤醒等等。
GEM:
Graphic Execution Manager,主要负责显示buffer的分配和释放,也是GPU唯一用到DRM的地方。
学习 drm 就是学习上面这些概念的用法和实现。
Android图形系统系统篇之HWC
HWC (hwcomposer)是Android中进行窗口( Layer )合成和显示的HAL层模块,其实现是特定于设备的,而且通常由显示设备制造商 (OEM)完成,为 SurfaceFlinger 服务提供硬件支持。
SurfaceFlinger 可以使用 OpenGL ES 合成 Layer ,这需要占用并消耗GPU资源。大多数GPU都没有针对图层合成进行优化,当 SurfaceFlinger 通过GPU合成图层时,应用程序无法使用GPU进行自己的渲染。而 HWC 通过硬件设备进行图层合成,可以减轻GPU的合成压力。
显示设备的能力千差万别,很难直接用API表示硬件设备支持合成的 Layer 数量, Layer 是否可以进行旋转和混合模式操作,以及对图层定位和硬件合成的限制等。因此HWC描述上述信息的流程是这样的:
虽然每个显示设备的能力不同,但是官方要求每个 HWC 硬件模块都应该支持以下能力:
但是并非所有情况下 HWC 都比GPU更高效,例如:当屏幕上没有任何变化时,尤其是叠加层有透明像素并且需要进行图层透明像素混合时。在这种情况下, HWC 可以要求部分或者全部叠加层都进行GPU合成,然后 HWC 持有合成的结果Buffer,如果 SurfaceFlinger 要求合成相同的叠加图层列表, HWC 可以直接显示之前合成的结果Buffer,这有助于提高待机设备的电池寿命。
HWC 也提供了 VSync 事件,用于管理渲染和图层合成时机,后续文章会进行介绍。
Android7.0提供了HWC和HWC2两个版本,默认使用HWC,但是手机厂商也可以选择HWC2,如下所示:
SurfaceFlinger 通过 HWComposer 使用 HWC 硬件能力, HWComposer 构造函数通过 loadHwcModule 方法加载HWC模块,并封装成 HWC2::Device 结构,如下所示:
上述通过 hw_get_module 方法(hardware\libhardware\hardware.c)加载 hwcomposer 模块,此模块由硬件厂商提供实现,例如:hardware\libhardware\modules\hwcomposer\hwcomposer.cpp是 hwcomposer 模块基于HWC1的default实现,对应的共享库是 hwcomposer.default.so ;hardware\qcom\display\msm8994\libhwcomposer\hwc.cpp是高通MSM8994基于HWC1的实现,对应的共享库是 hwcomposer.msm8994.so 。
如果是基于HWC2协议实现,则需要实现hwcomposer2.h中定义的 hwc2_device_t 接口,例如: class VendorComposer : public hwc2_device_t 。Android7.0的 hwcomposer 模块默认都是基于HWC1协议实现的。
每个HAL层模块实现都要定义一个 HAL_MODULE_INFO_SYM 数据结构,并且该结构的第一个字段必须是 hw_module_t ,下面是高通MSM8994 hwcomposer 模块的定义:
frameworks\native\services\surfaceflinger\DisplayHardware\HWC2.h主要定义了以下三个结构体:
它们是对 HWC 硬件模块的进一步封装,方便进行调用。 HWC2::Device 持有一个 hwc2_device_t ,用于连接硬件设备,它包含了很多HWC2_PFN开头的函数指针变量,这些函数指针定义在 hwcomposer2.h 。
在 HWC2::Device 的构造函数中,会通过 Device::loadFunctionPointers - loadFunctionPointer(FunctionDescriptor desc, PFN outPFN) - hwc2_device_t::getFunction 的调用链从硬件设备中获取具体的函数指针实现。关键模板函数如下所示:
这些函数指针主要分为三类:
通过上述函数指针可以与 hwc2_device_t 表示的硬件合成模块进行交互。三类指针分别选取了一个示例:
可以通过类图,直观感受下引用关系。
HWC2::Device 构造函数除了完成获取函数指针实现以外,还会通过 Device::registerCallbacks 向硬件设备注册三个 Display 的回调:热插拔,刷新和VSync信号,如下所示:
总结一下, HWC2::Device 构造函数向硬件设备注册三个 Display 回调:热插拔,刷新和VSync信号。当 HWC2::Device 收到这些回调时,会通过监听器向外回调到对应的HWComposer函数: HWComposer::hotplug / HWComposer::invalidate / HWComposer::vsync 。HWComposer再通过这些信息驱动对应工作,后续文章进行介绍。
上文提到 HWC2::Device 中的函数指针是hardware\libhardware\include\hardware\hwcomposer2.h中定义的,除此之外,该头文件还定义了一些重要的结构体,这里介绍两个比较重要的:
DisplayType 表示显示屏类型,上面注释已经介绍,重点看下Layer合成类型:
那么一个 Layer 的合成方式是怎么确定的那?大致流程如下所示:
本篇文章只是简单介绍了HWC模块的相关类: HWComposer 、 HWC2::Device 、 HWC2::Display 和 HWC2::Layer ,以及它们的关系。此外,还着重介绍了Layer的合成方式和合成流程。后续文章会更加全面的介绍 SurfaceFlinger 是如何通过HWC模块完成Layer合成和上屏的(虚拟屏幕是到离屏缓冲区)。
Android图形系统(十一)-Choreographer
在Android4.1之后增加了Choreographer机制,用于同Vsync机制配合,统一动画、输入和绘制时机。本文以绘制为例来简单学习下Choreographer。
ViewRootImpl的requestLayout开启绘制流程:
这里主要关注两点:
postSyncBarrier : Handler 的同步屏障。它的作用是可以拦截 Looper 对同步消息的获取和分发,加入同步屏障之后,Looper 只会获取和处理异步消息,如果没有异步消息那么就会进入阻塞状态。也就是说,对View绘制渲染的处理操作可以优先处理(设置为异步消息)。
Choreographer : 编舞者。统一动画、输入和绘制时机。也是这章需要重点分析的内容。
frameworks\base\core\java\android\view\Choreographer.java
每一个Looper线程都有自己的Choreographer,其他线程发送的回调只能运行在对应Choreographer所属的Looper线程上
Choreographer类中有一个Looper和一个FrameHandler变量。变量USE_VSYNC用于表示系统是否是用了Vsync同步机制,该值是通过读取系统属性debug.choreographer.vsync来获取的。如果系统使用了Vsync同步机制,则创建一个FrameDisplayEventReceiver对象用于请求并接收Vsync事件,最后Choreographer创建了一个大小为3的CallbackQueue队列数组,用于保存不同类型的Callback。
这里,不同类型的Callback包括如下4种:
CallbackQueue是一个容量为4的数组,每一个元素作为头指针,引出对应类型的链表,4种事件就是通过这4个链表来维护的。
而FrameHandler中主要处理三类消息:
Choreographer提供了两类添加回调的方式:postCallback 与 postFrameCallback,当然对应类型也包含delay的方法,算上其实有4个方法。
postCallback对应的:
postFrameCallback对应的:
相比之下postCallback更灵活一点。两者最终都会调到:postCallbackDelayedInternal
mCallbackQueues先把对应的callback添加到链表上来,然后判断是否有延迟,如果没有则会马上执行scheduleFrameLocked,如果有,则发送一个what为MSG_DO_SCHEDULE_CALLBACK类型的定时消息,等时间到了再处理,其最终处理也是执行scheduleFrameLocked(long now)方法。
这里首先判断USE_VSYNC,如果使用了VSYNC:走scheduleVsyncLocked,即请求VSYNC信号,最终调用doFrame,如果没使用VSYNC,则通过消息执行doFrame。
那么我们先简单了解下请求VSYNC信号的流程:
mDisplayEventReceiver 对应的是FrameDisplayEventReceiver,它继承自 DisplayEventReceiver , 主要是用来接收同步脉冲信号 VSYNC。scheduleVsync()方法通过底层nativeScheduleVsync()向SurfaceFlinger 服务注册,即在下一次脉冲接收后会调用 DisplayEventReceiver的dispatchVsync()方法。这里类似于订阅者模式,但是每次调用nativeScheduleVsync()方法都有且只有一次dispatchVsync()方法回调。
然后再看看接收VSYNC信号:
底层向应用层发送VSYNC信号,java层通过dispatchVsync()接收,最后回调在FrameDisplayEventReceiver的onVsync
可见onVsync()过程是通过FrameHandler向主线程Looper发送了一个自带callback的消息 callback为FrameDisplayEventReceiver。 当主线程Looper执行到该消息时,则调用FrameDisplayEventReceiver.run()方法,紧接着便是调用doFrame。
当Vsync事件到来时,顺序执行4种事件对应CallbackQueue队列中注册的回调。
按时间顺序先后执行CallbackRecord对应的run方法
接开篇讲的
mTraversalRunnable对应:
run方法被执行,所以doTraversal()被执行,开启View的绘制流程。
所以整个绘制过程总的流程如下所示:
简单总结:
参考
发布于 2023-03-21 00:06:37 回复
发布于 2023-03-21 06:57:44 回复
发布于 2023-03-20 23:36:40 回复