内网的一篇好文 征得大佬同意拿来分享一下。硬核介绍Android显示系统,深入每一个细节,从硬件到软件,带你体会不一样的风景。
- Android渲染系列(1)之原理概述篇
- Android渲染系列(2)之如何渲染UI
- Android渲染系列(3)之Choreographer
- Android渲染系列(4)之Surface与SurfaceFlinger
- Android渲染系列(5)之Window、Activity、View之间关系
- Android渲染系列(6)之Vsync
🔥面试官的小抄 面试进阶一网打尽,可能是东半球最好的面试资料
硬件篇
一 硬件显示模块
无论软件架构多么的高端大气上档次,都离不开硬件的支持,软件架构是构建的硬件的运行原理之上的,显示模块更是如此。
1 常见的显示设备:
- LCD(liquid crystal display)液晶屏
液晶是一种材料,液晶这种材料具有一种特点:可以在电信号的驱动下液晶分子进行旋转,旋转时会影响透光性,因此我们可以在整个液晶面板后面用白光照(称为背光),可以通过不同电信号让液晶分子进行选择性的透光,此时在液晶面板前面看到的就是各种各样不同的颜色,这就是 LCD 显示画面的原理。有些显示器(譬如 LED 显示器、CRT 显示器)自己本身会发光称为主动发光,有些(LCD)本身不会发光只会透光,需要背光的协助才能看起来是发光的,称为被动发光。
- CRT(cathode ray tube)阴极射线管显示器
主要有五部分组成:电子枪(Electron Gun)、偏转线圈(Deflection coils)、荫罩(Shadow mask)、高压石墨电极和荧光粉涂层(Phosphor)及玻璃外壳。CRT显示器靠电子束激发屏幕内表面的荧光粉来显示图像的,由于荧光粉被点亮后很快会熄灭,所以电子枪必须循环地不断激发这些点。
- OLED(organic light-emitting diode)有机发光二极管
有机半导体材料和发光材料在电场驱动下,通过载流子注入和复合导致发光的现象,很容易制作,而且只需要低的驱动电压,这些主要的特征使得OLED在满足平面显示器的应用上显得非常突出。OLED显示屏比LCD更轻薄、亮度高、功耗低、响应快、清晰度高、柔性好、发光效率高,能满足消费者对显示技术的新需求。
- LED( light-emitting diode ) 发光二极管
由一个个小的LED模块面板组成,用来显示文字、图像、视频等各种信息的设备。 LED电子显示屏集微电子技术、计算机技术、信息处理于一体,具有色彩鲜艳、动态范围广、亮度高、寿命长、工作稳定可靠等优点。
2 视频显示接口
- 对外显示器的视频接口
设备与设备之间的视频传输接口。根据出现的时间以及性能,可以分为五代:
- 第一代:亮色混合,视频接口代表:CVBS、AV
- 第二代:亮色分离,视频接口代表:S-端子、色差信号
- 第三代:模数共存,视频接口代表:VGA
- 第四代:纯数字,视频接口代表:DVI(DVI-D、DVI-A、DVI-I)
- 第五代:更强性能,视频接口代表:HDMI、MHL、DP、thunderbolt
- CVBS(RF)
- C端子(RCA/AV)
- S端子
- YPbPr
- VGA、DVI、HDMI、DP
- SDI
- HDBASE-T
- 对内显示器视频接口
对内的显示器视频传输接口指的是设备内部,板与板之间或板上的器件与器件之间的视频传输接口,目前常见的对内显示器视频接口主要有以下几种:
- DVP(ITU BT601/656/1120)
- LVDS(区别于Serdes)
- MIPI(DSI/CSI)
- eDP
- DVP digital video parallel
- LVDS low voltage differential signaling
4/5/10 lane差分信号
- MIPI mobile indurstry processor interface
2/3/4/5 lane差分信号
- eDP embeded DP
1/2/3/4 lane 差分信号,无clk,基于displayport架构和协议的一种全数字化接口
- LCD时序
一个典型的 Android 显示系统中,一般包括 SOC、DDIC、Panel 三个部分,
- SOC 负责绘画与多图层的合成,把合成好的数据通过硬件接口按某种协议传输给 DDIC,
- 显示驱动芯片DDIC(Display Driver IC)负责把 buffer 里的数据呈现到 Panel 上。
DDIC 是面板的主要控制元件之一。 DDIC 通过电信号的形式向显示面板发送驱动信号和数据,继而实现对屏幕亮度和色彩的控制,使得诸如字母、图片等图像信息得以在屏幕上显现。
如上图所示首先 CPU 或 GPU 负责绘画,画出的多个 layer 交由 MDP 进行合成,合成的数据通过 mipi 协议和 DSI 总线传输给 DDIC, DDIC 将数据存到 GRAM 内(非 video 屏), Panel 不断 scanGRAM 来显示内容。
- 并口时序信号
- 多帧画面输出时序
多帧画面依次输出到屏幕的时候我们就可以看到运动的画面了,通常这个速度到达每秒 60 帧时人眼就已经感觉画面很流畅了。如上图所示,在消隐区结束(或开始)时 DDIC 会向 SOC 发出一个中断信号,这个信号称为 TE 信号, SOC 这边就是通过该中断信号来判断上一帧数据是否已被 DDIC 读走,进而决定是否可以将 buffer 写入下一帧数据。
- LCD上的画面更新流程
如上图所示,首先 SOC 准备画面 A, DDIC 上一帧画面更新完毕进入消隐区,同时向 SOC 侧发送 TE 信号,SOC 收到 TE 信号后,A 画面的数据开始通过 DSI 总线向 DDIC 传输(DSI Write), 当消隐区时间结束时开始这一帧数据从数据变为像素点颜色的过程 (Disp Scan), Disp Scan 是以行为单位将 GRAM 内一行的数据内容通过改变电流电压等方式改变 Panel 上像素点的颜色。进而实现一行画面的更新,按下来 Disp Scan 将以一定速度逐行读取 GRAM 的内容,而于此同时 DSI Write 也还在进行中,由于 DSI Write 较 Disp Scan 早了一个 Vporch 的时间,所以 Disp Scan 扫描到的数据都是 A 画面的数据。那么人眼会看到画面“逐渐” 出现到显示屏上,当 A 画面的所有行都经 Disp Scan 到达屏幕后,下一个 Vporch 开始,DDIC 再次向 SOC 发出 TE 信号, 下帧 B 画面的数据开始经过 DSI 总线传输到 DDIC, 如此循环往复可以将连续的 A, B, C 画面更新到屏幕上。 Vporch消隐区时序保证了画面更新的时序性,如上图所示,写是在进入 vporch 时就开始了,而读的动作是离开 vproch 时,所以在读和写的这场 “百米跑” 竞赛中总是写跑在前面,这样保证读始终读到的是同一帧画面的数据。
二 显示模块驱动及系统接口
DRM,英文全称 Direct Rendering Manager, 即直接渲染管理器。DRM是linux内核的一个子系统,它提供一组 API,用户空间程序可以通过它发送画面数据到 GPU 或者专用图形处理硬件(如高通的MDP/海思的VDP/瑞芯微的VOP),也可以使用它执行诸如配置分辨率,刷新率之类的设置操作。原本是设计提供给 PC 使用来支持复杂的图形设备,后来也用于嵌入式系统上。目前在各大平台Android系统上的显示系统也是使用的这组API来完成画面的渲染更新。 在 DRM 之前 Linux 内核已经有一个叫 FBDEV 的 API,用于管理图形适配器的帧缓存区,但不能用于满足基于 3D 加速的现代基于 GPU 的视频硬件的需求,FBDEV 社区维护者也较少; 且无法提供 overlay hw cursor 这样的 features; 开发者们本身就鼓励以后迁移到DRM 上。 如上图所示,展示了DRM子系统框图,主要分为3个部分:
1 KMS kernel mode setting
CRTC 对显示buffer进行扫描,并产生时序信号的硬件模块,通常指Display Controller ENCODER 将CRTC输出的timing时序转换成外部设备所需要的信号,如HDMI转换器或DSI Controller CONNECTOR 连接物理显示设备的连接器,如HDMI、DisplayPort、DSI总线,通常和Encoder驱动绑定 PLANE 硬件图层,有的Display硬件支持多层合成显示,但所有的Display Controller至少要有1个plane Bridge 桥接设备,一般用于注册 encoder 后面另外再接的转换芯片,如 DSI2HDMI 转换芯片; Panel:泛指屏,各种 LCD、HDMI 等显示设备的抽象; FB Framebuffer,单个图层的显示内容,唯一一个和硬件无关的基本元素 VBLANK 软件和硬件的同步机制,RGB时序中的垂直消影区,软件通常使用硬件VSYNC来实现 PROPERTY 任何想设置的参数,都可做成property,是DRM驱动中最灵活、最方便的Mode setting机制
2 GEM graphic execution manager
DUMB 只支持连续物理内存,基于kernel中通用CMA API实现,多用于小分辨率简单场景 PRIME 连续、非连续物理内存都支持,基于DMA-BUF机制,可以实现buffer共享,用于大内存复杂场景 FENCE buffer同步机制,基于内核dma_fence机制实现,用于防止显示内容出现异步问题
3 libdrm
DRM是Linux下的图形渲染架构,用来管理显示输出和分配buffer。应用程序可以直接操纵DRM的ioctl或者是用framebuffer提供的接口进行显示相关操作。libdrm库封装了这些接口,让用户可以更加方便的进行显示控制。 各模块的souce code位置如下(rockchip android12 平台):
libdrm | external/libdrm |
---|---|
hwcomposer | hardware/rockchip/hwcomposer/drmhwc2 |
drm driver | kernel-5.18/drivers/gpu/drm |
三 用户空间的帧数据流
在 Android 系统上应用要绘制一个画面,首先要向 SurfaceFlinger 申请一个画布,这个画布所使用的 buffer 是 SurfaceFlinger 通过 allocator service(android.hardware.graphics.allocator@4.0-service)来分配出来的,allocator service 是通过 ION 从 kernel 开辟出来的一块共享内存,这里申请的都是每个图层所拥有独立 buffer, 这个 buffer 会共享到 HWC Service 里,由 SurfaceFlinger 来作为中枢控制这块 buffer 的所有权,其所有权会随状态不同在 App, SurfaceFlinger, HWC Service 间来回流转。 HWC Service正是那个使用 libdrm 和 kernel 打交道的人 ,HWC Service负责把 SurfaceFlinger 交来的图层做合成,将合成后的画画提交给 DRM 去显示。
1 App到Allocator Service
Android 12版本,APP首先通过 Surface 的接口向 Allocator申请3块 buffer,并通过importBuffer 把内存映射到应用的进程空间里,这种提前分配buffer的模式可以避免在渲染时分配buffer带来的delay,该过程可以在perfetto上观察到: 以前的版本,App通过 Surface 的接口向Surfaceflinger申请 buffer, 在应用第一次要绘制画面时 dequeueBuffer 会让 SurfaceFlinger 去 alloc buffer, 在应用侧会通过 importBuffer 把这块内存映射到应用的进程空间来。
2 App到SurfaceFlinger
App通过dequeueBuffer拿到画布,通过queueBuffer来提交绘制好的数据,这个过程可以在perfetto上看到: 如上图所式,由于app在进程内分配了graphicBuffer并进行了importBuffer操作,不需要通过binder和Surfaceflinger之间产生通信,直接通过dequeueBuffer和requestBuffer拿到对应的buffer地址。之后app通过gpu完成绘制之后执行eglSwapBuffer,进而调用queueBuffer提交数据,最后通过binder知会surfaceflinger侧执行importBuffer完成数据映射及后续处理。而老版本的android需要通过binder在surfaceflinger端执行queueBuffer系列操作。
3 SurfaceFlinger到HWC service
HWC Service 负责将 SurfaceFlinger 送来的图层做合成,形成最终的画面,然后通过 DRM 的接口更新到屏幕上去。 如上图所示,surfaceflinger任务由一个个onMessageInvalidate组成,在此过程中,通过binder实现和上层app及下层的hwcomposer的信息对通,不断的完成画面的获取、合成和上屏操作,循环往复。下面详细分析onMessageInvalidate干了那些事情: 如上图所示,onMessageInvalidate函数的具体流程大致分为11步:
- handleMessageTransaction->setBuffer App完成了绘制之后,会调用setBuffer设置对应的transactionflag,并最终通知到Surfaceflinger。
- handleMessageInvalidate->handlePageFlip->latchBuffer SurfaceFlinger获知App提交了绘制完成的buffer,会调用handlePageFlip,并进一步调用latchBuffer锁定acquire这个buffer,以便接下来进行画面的合成刷新。
- updateInputFlinger 该函数用于处理画面合成过程的input事件。
- preComposition 预合成函数会检测是否还有其他图元需要消费,并调用signalLayerUpdate进行下一轮的invalidate消费。
- prepare 该函数调用rebuildLayerStacks构建Layer栈,重新计算所有需要绘制的Layer的脏区域。
- updateCompositionState 重新计算ouputlayer的几何状态,计算DisplayFrame、SourceCrop等参数
- writeCompositionState 将outputLayer属性设置给hwcomposer
- prepareFrame 确定layer的合成方式,gpu合成或者hwc合成,这一步需要和hwc service进行binder通信,hwc service调用了ValidateDisplay进行相关处理。
- finishFrame 对于需要gpu进行绘制的layer,进行合成绘制操作。
- postFramebuffer 提交frame进入显示流程,并通过binder消息通知hwc service调用了PresentDisplay,该函数会调用drm相关接口操作内核drm驱动。完成frame的显示处理
- postComposition 合成送显结束之后的buffer fence释放,vsync的同步等善后工作。
软件篇
四 BufferQueue机制
1 BufferQueue要解决什么问题
APP 绘画的画布是由Allocator Service提供的,而画布是一块共享内存,APP 向 Allocator Service申请到画布,是将共享内存的地址映射到自身进程空间。 App负责在画布上作画,画完的作品提交给 SurfaceFlinger,这个提交操作并不是把内存复制一份给 SurfaceFlinger,而是把共享内存的控制权交还给 SurfaceFlinger。
SurfaceFlinger 把拿来的多个应用的共享内存再送给HWC Service去合成,HWC Service 把合成的数据交给 DRM 去输出完成 app 画面显示到屏幕上的过程。为了更有效地利用时间这样的共享内存不止一份,可能有两份或三份,即常说的 double buffering, triple buffering。那么我们就需要设计一个机制可以管理 buffer 的控制权,这个就是BufferQueue。BufferQueue 要解决的是生产者和消费者的同步问题,
应用程序生产画面,SurfaceFlinger 消费画面;SurfaceFlinger 生产画面而 HWC Service 消费画面。用来存储这些画面的存储区我们称其为帧缓冲区 buffer。
2 Buffer State的切换
在 BufferQueue 的设计中,一个 buffer 的状态有以下几种:
- FREE :表示该 buffer 可以给到应用程序,由应用程序来绘画
- DEQUEUED: 表示该 buffer 的控制权已经给到应用程序侧,这个状态下应用程序可以在上面绘画了
- QUEUED: 表示该 buffer 已经由应用程序绘画完成,buffer 的控制权已经回到 SurfaceFlinger 手上了
- ACQUIRED: 表示该 buffer 已经交由 HWC Service 去合成了,这时控制权已给到 HWC Service 了
状态切换如上图所示,Buffer 的初始状态为 FREE, 当生产者通过 dequeueBuffer 来申请 buffer 成功时,buffer 状态变为了 DEQUEUED 状态, 应用画图完成后通过 queueBuffer 把 buffer 状态改到 QUEUED 状态,当 SurfaceFlinger 通过 acquireBuffer 操作把 buffer 拿去给 HWC Service 合成, 这时 buffer 状态变为 ACQUIRED 状态,合成完成后通过 releaseBuffer 把 buffer 状态重新改为 FREE 状态。 从时间轴上来看一个 buffer 的状态总是这样循环变化: FREE->DEQUEUED->QUEUED->ACQUIRED->FREE 应用程序在 DEQUEUED 状态下绘画,而 HWC Service 在状态为 ACQUIRED 状态下做合成。
3 BufferSlot管理
**每一个应用程序的图层在 SurfaceFlinger 里称为一个 Layer, 而每个 Layer 都拥有一个独立的 BufferQueue, **每个 BufferQueue 都有多个 Buffer,Android 系统上目前支持每个Layer最多64个buffer,每个 buffer 用一个结构体 BufferSlot 来代表。
struct BufferSlot{
BufferState mBufferState;// Buffer的状态 FREE/DEQUEUED/QUEUED/ACQUIRED
sp<GraphicBuffer> mGraphicBuffer;//真正的buffer的存储空间
uint64_t mFrameNumber;//表⽰这个slot被queued的编号
sp<Fence> mFence;// gpu和cpu之间的信号同步
}
BufferSlot 可以分成两个部分,Used Slots 和 Unused Slots, 而 Used Slots 又可以分为 Active Slots 和 UnActive Slots, 处在 DEQUEUED, QUEUED, ACQUIRED 状态的被称为 Active Slots, 剩下 FREE 状态的称为 UnActive Slots, 所以所有 Active Slots 都是正在有人使用中的 slot, 使用者可能是生产者也可能是消费者。而 FREE 状态的 Slot 根据是否已经为其分配过内存来分成两个部分,一是已经分配过内存的, 在 Android 源码中称为 mFreeBuffers, 没有分配过内存的称为 mFreeSlots, 所以如果我们在代码中看到是从 mFreeSlots 里拿出一个 BufferSlot 那说明这个 BufferSlot 是还没有配置 GraphicBuffer 的,这个 slot 可能是第一次被使用到。其分类如下图所示: 应用上帧时,SurfaceFlinger 正是通过BufferSlot管理分配这些 Slot 的,应用侧对图层 buffer 的操作接口在这个文件:frameworks/native/libs/gui/Surface.cpp。 应用第一次 dequeueBuffer 前会通过 connect 接口和 SurfaceFlinger 建立 “连接”: 应用在第一次 dequeueBuffer 时会先调用 requestBuffer,如下图所示,函数内部先尝试去dequeueBuffer,由于此时surfaceflinger对应layer的slot还没有分配buffer,此时surfaceflinger回复的flag会有BUFFER_NEEDS_REALLOCATION标识,然后接下来的流程检测到有该标志位就会发出一次requestBuffer: 在 SurfaceFlinger 这端,第一次收到 dequeueBuffer 时发现分配出来的 slot 没有 GraphicBuffer, 这时会去申请对应的 buffer,如下图所示: APP侧收到带有 BUFFER_NEEDS_REALLOCATION 标记的返回结果后就会调 requestBuffer 来获取对应 buffer 的信息: 应用侧在 requestBuffer 后会拿到 GraphicBuffer 的信息,然后会通过 importBuffer 在本进程内通过 binder 传过来的 parcel 包把 GraphicBuffer 重建出来: 以上是Surfaceflinger通过BufferSlot管理buffer的经典流程,这部分在Android12上为了性能提升得到了优化,不过基本流程和原理是一致的,详细原理上述章节已经详细阐述。
从App侧看,前三帧都会有requestBuffer, 都会有importBuffer,在第4帧时就没有requestBuffer/importBuffer了,因为我们当前系统一共使用了三个buffer,在Android12上表现为App侧allocate并import了3个buffer。 当一个surface被创建出来开始上帧时其流程如下图所示,应用所使用的画布是在前三帧被分配出来的,从第四帧开始进入稳定上帧期,这时会重复循环利用前三次分配的buffer。
4 Buffer管理
上边提到每个图层Layer都有最多64个BufferSlot,每个BufferSlot都会记录自身的状态BufferState,以及自己的GraphicBuffer指针mGraphicBuffer。 但不是每个Layer都能使用到那么多,一般2到3个,default是2个,在Surface创建时初始化: 接下来分析BufferSlot管理的场景,如下图所示:
- Time1: 在上图中,初始状态下,有 0, 1, 2 这三个 BufferSlot, 由于它们都没有分配过 GraphicBuffer, 所以它们都位于 mFreeSlots 队列里,当应用来 dequeueBuffer 时,SurfaceFlinger 会先检查在 mFreeBuffers 队列中是否有 Slot, 如果有则直接分配该 Slot 给应用。显然此时 mFreeBuffers 里是空的,这时 Surfaceflinger 会去 mFreeSlots 里去找出第一个 Slot, 这时就找到了 0 号 Slot, dequeueBuffer 结束时应用就拿到了 0 号 Slot 的使用权,于此同时 SurfaceFlinger 也会为 0 号 Slot 分配 GraphicBuffer, 之后应用将通过 requestBuffer 和 importBuffer 来获取到该 Slot 的实际内存空间。应用 dequeueBuffer 之后 0 号 Slot 切换到 DEQUEUED 状态,并被放入 mActiveBuffers 列表。
- Time2: 应用完成绘制后通过 queueBuffer 来提交绘制好的画面,完成后 0 号 Slot 状态变为 QUEUED 状态,放入 mQueue 队列,此时 1,2 号 Slot 还停留在 mFreeSlots 队列中。
- Time3: 上面这个状态会持续到下一个 Vsync-sf 信号到来,当 Vsync-sf 信号到来时,SurfaceFlinger 主线程会检查 mQueue 队列中是否有 Slot, 有就意味着有应用上帧,这时它会把该 Slot 从 mQueue 中取出放入 mActiveBuffers 队列,并将 Slot 的状态切换到 ACQUIRED, 代表这个 Slot 已被拿去做画面合成。那么这之后 0 号 Slot 被从 mQueue 队列拿出放入 mActivieBuffers 里
- Time4: 接下来应用继续调用 dequeueBuffer 申请 buffer, 此时 0 号 Slot 在 mActiveBuffers 里,1,2 号在 mFreeSlots 里,SurfaceFlinger 仍然是先检查 mFreeBuffers 里有没有 Slot, 发现还是没有,再检查 mFreeSlots 里是否有,于是取出了 1 号 Slot 给到应用侧,同时 1 号 Slot 状态切换到 DEQUEUED 状态, 放入 mActiveBuffers
- Time5:1 号 Slot 应用绘画完毕,通过 queueBuffer 提交上来,这时 1 号 Slot 状态由 DEQUEUED 状态切换到了 QUEUED 状态,进入 mQueue 队列,之后将维持该状态直到下一个 Vsync-sf 信号到来。
- Time6: 此时 Vsync-sf 信号到来,发现 mQueue 中有个 Slot 1, 这时 SurfaceFlinger 主线程会把它取出,把状态切换到 ACQUIRED, 并放入 mActiveBuffers 里。
- Time7: 这时 0 号 Slot HWC Service 使用完毕,通过 releaseBuffer 还了回来,0 号 Slot 的状态将从 ACQUIRED 切换回 FREE, Surfaceflinger 会把它从 mActivieBuffers 里拿出来放入 mFreeBuffers 里。注意这时放入的是 mFreeBuffers 里而不是 mFreeSlots 里,因为此时 0 号 Slot 是有 GraphicBuffer 的。
- Time11: 当下的状态是 0,1 两个 Slot 都在 mFreeBuffers 里,2 号 Slot 在 mActiveBuffers 里,这时应用来 dequeueBuffer
- Time12: SurfaceFlinger 仍然会先查看 mFreeBuffers 列表看是否有可用的 Slot, 发现 0 号可用,于是 0 号 Slot 状态由 FREE 切换到 DEQUEUED 状态,并被放入 mActiveBuffers 里
- Time13: 应用对 0 号 Slot 的绘图完成后提交上来,这时状态从 DEQUEUED 切换到 QUEUED 状态,0 号 Slot 被放入 mQueue 队列,之后会维持该状态直到下一下 Vsync-sf 信号到来
- Time14: 这时 Vsync-sf 信号到来,SurfaceFlinger 主线程中检查 mQueue 队列中是否有 Slot, 发现 0 号 Slot, 于是通过 aquireBuffer 操作把 0 号 Slot 状态切换到 ACQUIRED
- Time 23: 当前状态 mQueue 里有两个 buffer
- Time 24:Vsync-sf 信号到达,从 mQueue 队列里取走了 0 号 Slot,
- Time 25: 再一次 Vsync-sf 到来,这时 SurfaceFlinger 会先查看 mQueue 队列是否有 buffer,发现有 2 号 Slot, 会先取走 2 号 Slot
- Time 26: 此时 0 号 Slot 已经被 HWC Service 使用完毕,需要把 Slot 还回来,0 号 Slot 在此刻进入 mFreeBuffers 队列。
所以如果应用上帧速度较慢,比如其上帧周期时长大于两倍屏幕刷新周期时,每次应用来 dequeueBuffer 时前一次 queueBuffer 的 BufferSlot 都已经被 release 回来了,这时总会在 mFreeBuffers 里找到可用的,那么就不需要三个 Slot 都分配出 GraphicBuffer。 其中有两个时序需要注意:
- 每次 Vsync-sf 信号到来时总是先查看 mQueue 队列看是否有 Layer 上帧,然后才会走到 releaseBuffer 把 HWC Service 使用的 Slot 回收回来
- 本次 Vsync-sf 被 aquireBuffer 取走的 Slot 总是会在下一个 Vsync-sf 时才会被 release 回来
5上帧流程
- 通过surfaceview上帧观测完整流程:
App通过 dequeueBuffer 拿到了 BufferSlot 0, 开始第 1 步绘图,绘图完成后通过 queueBuffer 将 Slot 0 提交到 SurfaceFlinger, 下一个 Vsync-sf 信号到达后,开始第 2 步图层处理,这时 SurfaceFlinger 通过 aquireBuffer 把 Slot 0 拿去给到 HWC Service,与此同时进入第 3 步 HWC Service 开始把多个图层做合成,合成完成后通过 libdrm 提供的接口通知 DRM 模块通过 DSI 传输给 DDIC, Panel 通过 Disp Scan Gram 把图像显示到屏幕。
- Tripple Buffer连续上帧过程
App在每个 Vsync 信号到来后都会通过 dequeueBuffer/queueBuffer 来申请 buffer 和提交绘图数据,Surfaceflinger 都会在下一个 vsync 信号到来时取走 buffer 去做合成和显示,并在下一下个 vsync 时将 buffer 还回来,再次循环。
五 Fence机制
Fenc解决了什么问题
一般凡是共享的资源都要建立一个同步机制来管理,比如在多线程编程中对临界资源的通过加锁实现互斥访问,再比如 BufferQueue 中 Surfaceflinger 和应用对共享内存(帧缓冲)的访问中有 bufferstate 来标识共享内存控制权的方法来做同步。没有同步机制的无序访问极可能造成数据混乱。
上面图中的 BufferState 的方式只是解决了在 CPU 管理之下,当下共享内存的控制权归属问题,但当共享资源是在两个硬件之中时,情况就不同了,比如当一个帧缓冲区共享内存给到 GPU 时,GPU 并不清楚 CPU 还有没有在使用它,同样地,当 GPU 在使用共享内存时,CPU 也不清楚 GPU 是否已使用完毕。 如上图所示,CPU 调用 OpenGL 函数绘图过程的一个简化版流程如上图所示,首先 CPU 侧调用 glClear 清空画布,再调用 glXXX()来画各种各样的画面,对于 CPU 来讲在 glXXX() 执行完毕后,它的绘图工作已经完成了。但其实 glXXX()的具体工作是由 GPU 来完成的,CPU 侧的 glXXX() 只是在向 GPU 传达任务而已,任务传达完并不意味着任务已经完成了。真正任务做完是在 GPU 把 glXXX() 所对应的工作做完才是真正的任务完成了。从 CPU 下达完任务到 GPU 完成任务间存在时差,而且这个时差受 GPU 工作频率影响并不是一个定值。在 OpenGL 的语境中 CPU 可以通过 glFilish() 来等待 GPU 完成所有工作,但这显然浪费了 CPU 本可以并行工作的时间,这段时间 CPU 没有用来做别的事情。 Fence 提供了一种方式来处理不同硬件对共享资源的访问控制,解决上述提到的CPU和GPU异步访问资源的问题。 如上图所示,Fence 是一个内核 driver, 对一个 Fence 对象有两种操作, signal 和 wait, 当生产者(App)向 GPU 下达了很多绘图指令(drawCall)后 GPU 开始工作,这里 CPU 就认为绘图工作已经完成了,之后把创建的 Fence 对象通过 binder 通知给消费者(SurfaceFlinger),SurfaceFlinger 收到通知后,此时 SurfaceFlinger 并不知道 GPU 是否已经绘图完毕,即 GPU 是否已对共享资源访问完毕,消费者先通过 Fence 对象的 wait 方法等待,如果 GPU 绘图完成会调用 Fence 的 signal, 这时消费者就会从 Fence 对象的 wait 方法中跳出。即 wait 方法结束时就是 GPU 工作完成时。这个 signal 由 kernel driver 来完成。有了 Fence 的情况下,CPU 在完成自已的工作后就可以继续做别的事情,到了真正要使用共享资源时再通过 Fence wait 来和 GPU 同步,尽最大可能做到了让不同硬件并行工作。
2 Fence与BufferQueue的协作方式
如上图所示,首先 App 通过 dequeueBuffer 获得某一 Slot 的使用权,这时 Slot 的状态切换到 DEQUEUED 状态,随着 dequeueBuffer 函数返回的还有一个 releaseFence 对象,这时因为 releaseFence 还没有 signaled, 这意味着虽然在 CPU 这边已经拿到了 buffer 的使用权,但别的硬件还在使用这个 buffer, 这时的 GPU 还不能直接在上面绘画,它要等 releaseFence signaled 后才能绘画。 接下来我们先假设 GPU 的工作花费的时间较长,在它完成之前 CPU 侧 APP 已经完成了 queueBuffer 动作,这时 Slot 的状态已切换为 QUEUED 状态,或者 vsync 已经到来状态变为 ACQUIRED 状态, 这在 CPU 侧代表该 buffer 给 HWC 去合成了,但这时 HWC 的硬件 VOP 还不能去读里面的数据,它还需要等待 acauireFence 的 signaled 信号,只有等到了 acquireFence 的 signaled 信号才代表 GPU 的绘画工作真正做完了,GPU 已经完成了对帧缓冲区的访问,这时 HWC 的硬件才能去读帧缓冲区的数据,完成图层合成的工作。 同样地,当 SurfaceFlinger 执行到 releaseBuffer 时,并不能代表 HWC 已经完全完成合成工作了,很有可能它还在读取缓冲区的内容做合成, 但不妨碍 releaseBuffer 的流程执行,虽然 HWC 还在使用缓冲区做合成,但帧缓冲区的 Slot 有可能被应用申请走变成 DEQUEUED 状态,虽然 Slot 是 DEQUEUED 状态这时 GPU 并不能直接存取它,它要等代表着 HWC 使用完毕的 releaseFence 的 signaled 信号。 如上图所示,App申请buffer的同时,会获取一个fence对象,用于等待HWC使用完毕这个buffer,此后GPU可以操作这个buffer。 同理,应用侧上帧时要创建一个 fence 来代表 GPU 的功能还在进行中,提交 buffer 的同时把 fence 对象传给 SurfaceFlinger,SurfaceFlinger 侧用于等待GPU绘制完成。此后CPU可以操作这个buffer。 如上图所示,是一个因 GPU 工作时间太长,从而让 DRM 工作线程卡在等 Fence 的情况,complete_commit 函数执行时前面有一段时间是陷于等待状态了,从图中所示我们可以看出它在等下 73026 号 fence 的 signal 信号。这种情况说明 drm 内部的 dma 要去读这 miHoYo.yuanshen 这个应用的 buffer 时发现应用的 GPU 还没有把画面画完,它不得不等待它画完才能开始读取,但既然都已经送到 crtc_commit 了,在 CPU 这侧,该 Slot 的 BufferState 已经是 ACQUIRED 状态。
六 画面显示流程
上述章节分析了Userspace、Kernel、App、Surfaceflinger、Hwcomposer的关系,帧流动的关键节点、同步问题处理等各种技术细节,那么App所绘制的画面是如何一步步进入Android设计的那些流程之中的呢?
1 App启动流程图
如上图所示为AMS完成App的Activity Thread创建之后的主要流程,ActivityThread类中的handleLaunchActivity,performLaunchActivity,handleResumeActivity这3个主要的方法完成了Activity的创建到启动工作,完成了Activity的onCreate、onStart、onResume这三个生命周期的执行。
- handleLaunchActivity: 创建Activity实例,并调用其attach方法创建了PhoneWindow对象(Window的实现类),并为该Window对象设置了WindowManager(Windows的管理方法)。
- performLaunchActivity: 调用onCreate方法,通过setContentView加载定义的布局文件。初始化DecorView,将设置的布局文件解析为View树,添加到mContentParent中,并调用onStart函数进行其他初始化操作。
- handleResumeActivity: 调用onResume方法,获取Activity的WindowManager、Window、DecorView对象,调用WindowManager.addView方法初始化ViewRootImpl,进而调用ViewRootImpl.setView为Window添加DecorView,同时setView内部调用了requestLayout,该函数内部又调用了scheduleTraversals,该方法完成View三大流程<measure、layout、draw>
Android中的视图是以View树的形式组织的,而View树必须依附在Window上才能工作,一个Window对应着一个View树,Activity内部可以有多个Window。由于View的测量、布局、绘制只是在View树内进行的,因此一个Window内View的改动不会影响到另一个Window。
- Activity 像个控制器,不负责视图部分,它只是控制生命周期和处理事件。
- Window 像个承载器,装着内部视图,并真正控制视图。
- ViewRootImpl 像个连接器,负责沟通,通过硬件的感知(分发事件,绘制View内容)来通知视图,进行用户之间的交互。
- DecorView 顶层视图,是所有View的最外层布局。
2 画布的申请
在Android系统中每个 Activity 都有一个独立的画布(在应用侧称为 Surface, 在 SurfaceFlinger 侧称为 Layer),无论这个 Activity 安排了多么复杂的 view 结构,它们最终都是被画在了所属 Activity 的这块画布上,当然也有一个例外,SurfaceView 是有自已独立的画布的. 如上文分析,每个应用都会创建有自已的 Activity, 同时 Android 会为每个应用的Activity 创建一个 ViewRootImpl,并在ViewRootImpl里向Choreographer注册一个回调,每当有Vsync信号来临就会执行mTraversalRunnable开启绘图流程,这里边会调用到ViewRootImpl的 performTraversals 这个函数: 该函数内部又调用了relayoutWindow函数: relayoutWindow又通过binder调用了WMS远端的relayoutwindow函数: relayoutWindow内部调用了createSurfaceControl: createSurfaceLocked又进一步调用了如下函数: 最后调用native的接口: 至此,完成了从一个App的Activity创建到Surfaceflinger内部为它创建了一个Layer来对应。 通过上图我们看到 java 层 Surface 的 lock 方法最终它有去调用到 GraphicBufferProducer 的 dequeueBuffer 函数,dequeueBuffer 在获取一个 Slot 后,如果 Slot 没有分配 GraphicBuffer 会在这时给它分配 GraphicBuffer, 然后会返回一个带有 BUFFER_NEEDS_REALLOCATION 标记的 flag, 应用侧看到这个 flag 后会通过 requestBuffer 和 importBuffer 接口把 GraphicBuffer 映射到自已的进程空间。到此应用拿到了它绘制界面所需的“画布”。 上述代码过程的总结如上图所示,这个过程中涉及到三个进程,APP,system_server, surfaceflinger, 先从应用调用 performTraversals 中调用 relayoutWindow 这个函数开始,它跨进程调用到了 system_server 进程中的 WMS 模块,这个模块的 relayoutWindow 又经一系列过程创建一个 SurfaceContorl, 在 SurfaceControl 创建过程中会跨进程调用 SurfaceFlinger 让它创建一个 Layer 出来。 之后 SurfaceControll 对象会跨进程通过参数回传给应用,应用根据 SurfaceControl 创建出应用侧的 Surface 对象,而 Surface 对象通过一些 api 封装向上层提供拿画布(dequeueBuffer)和提交画布(queueBuffer)的操作接口。这样应用完成了对画布的申请操作。
3 帧数据的绘制
App拿到画布之后,就可以进入绘制流程了,分析如下: performTraversals函数中获取了surface之后,初始化ThreadRender时会把Surface对象传过去。 如上图所示,ThreadRender的初始化中调用了setSurface,该函数通过jni调用到native层,并通过RenderProxy向RenderThread的消息队列post了一个消息,在消息处理中会调用Context的setSurface,既是CanvasContext::setSurface,并在该函数内部初始化pipelineSurface,最终调用到VulkanSurface的ConnectAndSetWindowDefaults函数,进行了一系列的window初始化操作。其过程如下图所示: 该过程也可以在perfetto上观察到,如下图: 接下来App主线程会遍历ViewTree(根View->DecorView)对所有的View完成measure、layout、draw的工作: performDraw中最终调用到ThreadedRenderer.java的draw,如下图: 如上图所示,draw函数最终调用到DecorView的基类函数updateDisplayListIfDirty,该函数中又调用了如下函数:
在 Android 的设计里 View 会对应一个RenderNode, RenderNode里的一个重要数据结构是DisplayList, 每个DisplayList都会包含一系列 DisplayListData。这些 DisplayList 也会同样以树形结构组织在一起。上图中的RecordingCanvas相当于一个绘图指令记录员,将这个View及其子View通过draw函数绘制的指令以DisplayList的形式记录下来。 DisplayListData定义在framework/base/libs/hwui/RecordingCanvas.h中,主要可以分为3部分:
- draw开头的函数,基本的绘图指令
- push模版函数
- fBytes 存储区
以DisplayListData::drawRect 方法为例,函数调用流程如下: 由上图可知,drawRect函数没有将App设计的View转换成像素点数据,而是把画这个 Rect 的方法和参数存入了 fBytes 这块内存中,那么最后fBytes这段内存空间就放置了一条条的绘制指令。 如上图所示,完成了对view的指令翻译之后,正式进入绘图流程: 如上图所示,最终来到CavasContext::draw函数,关键步骤分为3步:
- mRenderPipeline->getFrame
函数内部最终通过调用dequeueBuffer获取buffer。
- mRenderPipeline->draw
renderFrameImpl中会把在UI线程中记录的DisplayList重新“绘制”到skSurface中,然后通过SkCanvas将其转化为gl指令,vulkanManager().finishFrame这句是将指令发送给GPU执行。
- mRenderPipeline->swapBuffers
最终通过queueBuffer将绘制的Buffer提交到SurfaceFlinger做进一步的合成输出。 如上图所示,总结一下应用通过 android 的 View 系统画出第一帧的流程:首先是UI线程进行measure,layout,然后开始 draw, 在draw的过程中会建立displaylist树,将每个view应该怎么画记录下来,然后通过RenderProxy把后续任务下达给RenderThread, RenderThread主要完成三个动作,先通过 Surface 接口向 Surfaceflinger申请buffer, 然后通过 SkiaOpenGLPipline 的 draw 方法把 displaylist翻译成GPU 指令,指挥 GPU 把指令变成像素点数据,最后通过 swapBuffer 把数据提交给 SurfaceFlinger, 完成一帧数据的绘制和提交。该过程可以通过perfetto观察到:
4 帧数据的提交
Android12中queueBuffer流程和之前有较大不同,提交流程如下: App的dequeueBuffer,会在native层调用BufferQueueProducer.cpp的queueBuffer函数,进而通过调用frameAvailableListener->onFrameAvailable通知消息监听对象ConsumerBase。 CosumerBase通过listener->onFrameAvailable通知监听对象BLASTBufferQueue: BLASTBufferQueue::onFrameAvailable进一步调用了processNextBufferLocked: 在processNextBufferLocked内部,最终调用了acquireBuffer函数去获取App提交的buffer,如下图所示: 同时,在processNextBufferLocked函数内部,最后会通过transaction机制触发Surfaceflinger侧进入onMessageInvalidate流程: 进入SurfaceFlinger::onMessageInvalidate流程之后就开始了画面送显流程,详细过程在第三部分第三节已经做过介绍,下面对代码段进行一些具体分析: 如上图所示onMessageInvalidate做了一些frame的逻辑判断和处理之后,开始处理Transaction消息,对App提交的layer进行了handlepageflip、latchbuffer等处理,并对refreshNeeded进行的赋值,触发surfaceflinger进入onMessageRefresh逻辑。 如上图所示,这些关键函数在前边章节都涉及过,perfetto上也可以看到其函数调用的过程,到此SurfaceFlinger侧的处理流程结束。总体流程如下图所示:
七 总结
整个流程下来,APP 的画面要显示到屏幕上大致上要经过如下图所示系统组件的处理:
- 首先 App 向 SurfaceFlinger 申请画布 (Android12在内部申请),SurfaceFlinger 内部有一个 BufferQueue 的管理实体,它会分配一个 GraphicBuffer 给到 APP, App 拿到 buffer 后调用图形库向这块 buffer 内绘画。
- APP 绘画完成后使用向 SurfaceFlinger 提交绘制完成的 buffer(通过 queueBuffer 接口), 当然这时候的绘制完成只是说在 CPU 侧绘制完成,此时 GPU 可能还在该 buffer 上作画,所以这时向 SurfaceFlinger 提交数据的同时还会带上一个 acquireFence,使用接下来使用该 buffer 的人能知道什么时候 buffer 使用完毕了。
- SurfaceFlinger 收到应用提交的帧缓冲区 buffer 后是在下一个 vsync-sf 信号来时做处理,首先遍历所有的 Layer, 找到哪些 Layer 有上帧, 通过 latchBuffer 把 Buffer 拿出来,通知给 HWC Service 去参与合成, 最后调用 HWC Service 的 presentDisplay 接口来告知 HWC Service SurfaceFlinger 的工作已完成。
- HWC Service 收到合成任务后开始合成数据,在 SurfaceFlinger 调用 presetDisplay 时会去调用 DRM 接口 DRMAtomicReq::Commit 通知 kernel 可以向 DDIC 发送数据了.
- 如果有 TE 信号来提示已进入消隐区,这时 DRM 驱动会马上开始通过 DSI 总线向 DDIC 传输数据,与此同时 Panel 的 Disp Scan 也在进行中,传输完成后这帧画面就完整地显示到了屏幕上。
至此,一帧画面的更新过程就完成了。
本期就到这里了,更多精彩内容点击查看