
同济大学 计算机系统结构硕士
107 人赞同了该文章
当我们点击“知乎”这个应用后,它是怎么在屏幕上显示出来的?
这个问题困扰我很久了,当我刚接触显示的时候,大约是十年前的事情了,当时我连Framebuffer都不了解。尽管与显示芯片接触的越来越久,但萦绕在我心头的疑惑也并没有减少,此时大部分时间都与硬件交互,对上层的理解仍是糊里糊涂。
我当时就挺希望有人能从头到尾的介绍一下整个显示流程,可惜网上并没有这样的教程,实际接触到的同事基本分管单个模块,从上而下也很难说清。感谢曾经在联芯的日子,那时候空闲时间多了不少。大约2017年左右,使我有时间整理了一份60页的文档,这份文档如今也留在了原公司,当然本系列文章会按照PPT的形式介绍大概流程。
从事Android显示相关领域已经很多年了,如今越来越偏离这个领域,总心有不忍,不愿完全离开这个方向,所以一直也比较关注这个领域的发展。
背景知识
为了更好的了解整个框架,我们先了解一下背景知识;因为很多人并不太了解Graphics与Display的区别,主要原因还在于PC时代的影响。
PC显卡与移动端GPU的区别

上面是ARM给出的视频流显示过程,主要介绍AFBC的带宽压缩技术。但我们给出这张图的主要目的是为了说明PC上的显卡与移动端GPU的区别。在PC上这整个流程都在显卡内完成。
PC的显卡 = 移动端GPU + Display Processor + Video Processor
PC上Linux系统显示流程在2012年就已全部采用drm框架了,如下图,从框架中我们能看出drm中renderX节点就是服务于GPU(应用有权限,内容端),而ControlDX是服务于DisplayProcessor(系统权限,控制端)。

Android在2018年左右也采用了这个框架,但移动端这个区分在硬件上早就是存在的。后面我们会常用到内容端与控制端这2个概念。
说句题外话,游戏对于Graphics(GPU)要求比较高,而除了Adreno、Mali少数的几个能提供GPU平台的公司(它们也同时提供DP芯片),更多的手机公司基本都是从事Display的工作。
Android系统特点

我们知道Android是采用的Client-Server架构,而不是微内核架构。如上图,应用与硬件交互都需要经过binder委托给系统服务。但是有个例外——
GPU是用户进程唯一可以直接操作的硬件

这主要是为了性能考虑,毕竟绘图的99%的工作都由GPU完成。
在了解了背景知识后,我们从上至下了解整个框架。如“Android上的FreeSync——显示延时”那一篇文章提到的,Android的显示是三段式的。

关于这个系列我们将分为上中下三篇——上篇只介绍绘制部分,也就是内容端;中篇是承上启下的SurfaceFlinger、Hwcomposer(合成), 下篇主要讲解硬件相关的DisplayProcessor以及显示接口如LCD(显示)。
显示流程
下图是显示一帧所经过的流程,可以看出来经过5个主要模块——

Application--Renderer
Frameworks/SurfaceFlinger
Hwcomposer
Display Driver
Display Device
为了避免概念分歧,本文主要讲解绘制,也就是内容端,当然Frameworks/SurfaceFlinger属于承上启下,不可避免也会涉及到。
一、Application/Activity/View
关于这个概念网上已经有了很多,我们只简单介绍一下。
Application包括4大组件:Activity、Service、Broadcast、ContentProvider
Activity委托View负责显示:
TextView、EditView、Button、Custom View
Custom main view:setContentView(R.layout.main_activity)
View的3个重要方法
Measure:系统会先根据xml布局文件和代码中对控件属性的设置,来获取或者计算出每个View和ViewGrop的尺寸,并将这些尺寸保存下来。
显示大小,应用可调,由服务端WMS最终计算确定(控制端)
2. Layout:根据测量出的结果以及对应的参数,来确定每一个控件应该显示的位置。
显示位置,应用可调,由服务端WMS最终计算确定(控制端)
3. Draw:确定好位置后,就将这些控件绘制到屏幕上。
显示内容,应用决定,此时才会用到GPU(内容端)
所以,我们也可以理解,为什么onCreate里面的setContentView的时候,实际是没有分配Buffer的,真正分配Buffer的时候是始于Draw的时候。
最重要的View——DectorView
Android窗口主要分为两种:
应用窗口——PhoneWindow
一个activity有一个主窗口,弹出的对话框也有一个窗口,Menu菜单也是一个窗口。在同一个activity中,主窗口、对话框、Menu窗口之间通过该activity关联起来。
和应用相关的窗口表示类是PhoneWindow,其继承于Window,针对手机屏幕做了一些优化工作,里面核心的是mDecorView这个变量,mDecorView是一个顶层的View,窗口的添加就是通过WindowManager.addView()把该mDecorView添加到WindowManager中。

2. 公共界面的窗口
如最近运行对话框、关机对话框、状态栏下拉栏、锁屏界面等。这些窗口都是系统级别的窗口,不从属于任何应用,和activity没有任何关系。这种窗口没有任何窗口类来封装,也是直接调用WindowManager.addView()来把一个view添加到WindowManager中。
至此流程如下(具体细节不再展开,图片来自之前一个同事,借用一下)

二、Framework的控制端与内容端
控制端
WindowManagerService (服务端,与绘制无关)
窗口的状态、属性,如大小,位置;(WindowState,与上面的DectorView一一对应)
View增加、删除、更新
窗口顺序
Input Event 消息收集和处理等(与绘制无关,可不用关心)
SurfaceFlinger(服务端,与绘制无关)
Layer的大小、位置(Layer与上面的WindowState一一对应)
Layer的增加、删除、更新;
Layer的zorder顺序
内容端——绘制
framework/base: Canvas
SoftwareCanvas (skia/CPU)
HardwareCanvas (hwui/GPU)
framework/base: Surface
区别于WMS的Surface概念,与Canvas一一对应,内容生产者

framework/native:
Surface: 负责分配buffer与填充(由上面的Surface传下来)
SurfaceFlinger
Layer数据已填充好,与上面提到的Surface同样是一一对应
可见Layer这个概念即是控制端又是内容端
当然,SF更重要的是合成,后文会继续讲解
至此流程如下(具体细节不再展开)

三、概念澄清
在这里各种Surface,Window,View、Layer实在是太乱了,下面就区分一下这些概念。
Window -> DecorView-> ViewRootImpl -> WindowState -> Surface -> Layer 是一一对应的。
一般的Activity包括的多个View会组成View hierachy的树形结构,只有最顶层的DecorView,也就是根结点视图,才是对WMS可见的,即有对应的Window和WindowState。
一个应用程序窗口分别位于应用程序进程和WMS服务中的 两个Surface对象有什么区别呢?
虽然它们都是用来操作位于SurfaceFlinger服务中的同一个Layer对象的,不过,它们的操作方式却不一样。具体来说,就是位于应用程序进程这一侧的Surface对象负责绘制应用程序窗口的UI,即往应用程序窗口的图形缓冲区填充UI数据,而位于WMS服务这一侧的Surface对象负责设置应用程序窗口的属性,例如位置、大小等属性。
这两种不同的操作方式分别是通过C++层的Surface对象和SurfaceControl对象来完成的,因此,位于应用程序进程和WMS服务中的两个Surface对象的用法是有区别的。之所以会有这样的区别,是因为 绘制应用程序窗口是独立的,由应用程序进程来完即可,而设置应用程序窗口的属性却需要全局考虑,即需要由WMS服务来统筹安排。
四、软件绘制
软件绘制就是使用CPU去绘制,Skia之前就是一个CPU实现的2D库,当然现在也用GPU去实现了。
硬件加速就是使用GPU去绘制,这看起来似乎天经地义,其实Android并不是一开始就支持的,而是在Android 3.0之后才开启了默认硬件加速,如果你的应用target API < 14,还是默认使用软件绘制。
但在Android 3.0之前,GPU也是存在的,其意义何在?
1)虽然UI是默认软件绘制的,但是并不是说所有的应用都不是硬件绘制的,即使在Android3.0之前,BootAnimation也是一个典型的GPU应用;Camera的后处理特效;还有大部分游戏都是硬件加速的。
2)SurfaceFlinger是用来合成Layer的,而且是用GPU合成Layer的,这也需要GPU的功能,只需2D模块就够了。其实在早期的GPU上,比如imagination,vivant都是有这个2D模块的,尽管现在都已经取消了,甚至这2个GPU已经消失在大众的视野了。
3)在Android3.0之前,你如果没有GPU硬件,那么SF是否就无法启动呢?遇到BootAnimation是否就挂掉了呢?这到也不是,当时Android给了一个模拟GPU的libGLES_android库,但是只支持固定管线Opengles 1.1,主要用于调试用。合成Layer是没有什么问题,BootAnimation也没啥问题。有问题的是复杂的游戏等,所以GPU还是得标配。
软件绘制的典型场景
1. 鼠标---直接调用Surface->lock
2. Mali对于小layer的旋转
3. Target API < 14等应用强制使用软件绘制的情形
软件绘制的Buffer管理

软件绘制流程比较简单
在Android应用程序进程这一侧,每一个窗口都关联有一个Surface。每当窗口需要绘制UI时,就会调用其关联的Surface的成员函数lock获得一个Canvas,其本质上是向SurfaceFlinger服务dequeue一个Graphic Buffer。
Canvas封装了由Skia提供的2D UI绘制接口,并且都是在前面获得的Graphic Buffer上面进行绘制的,这个Canvas的底层是一个bitmap,也就是说,绘制都发生在这个Bitmap上。绘制完成之后,Android应用程序进程再调用前面获得的Canvas的成员函数unlockAndPost请求显示在屏幕中,其本质上是向SurfaceFlinger服务queue一个Graphic Buffer,以便SurfaceFlinger服务可以对Graphic Buffer的内容进行合成,以及显示到屏幕上去。

如何确定是否软件绘制
CPU负载会相对高;
Systrace无RenderThreader线程;
Systrace中无eglSwapBuffer等opengl api (opengl在Android尚无无软件实现)
Dumpsys gfxinfo pid 得不到相应的绘制信息
如何强制使用软件或硬件加速
其实我们真正有可能用到的是关闭或开启硬件加速,比如在调试的过程中看下是否是GPU的兼容性引起的问题。
1. 针对应用
在Android中,可以在四个不同层次上打开或关闭硬件加速:
Application :< application android:hardwareAccelerated=“false”>
Activity:< activity android:hardwareAccelerated=" false ">
Window:getWindow().setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED); //此为打开,
View:view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
在这四个层次中, 应用和Activity是可以选择的,Window只能打开,View只能关闭。
设置targetSDKVersion
在apk的AndroidManifest中,如果指定了minSDKVersion&targetSDKVersion=7,会使得应用无法使用硬件加速进行绘图。如果targetSDKVersion<14或者不设置都是默认软件绘图。
2. 针对整个系统
关闭硬件加速:
BoardConfigVendor.mk file :Set the USE_OPENGL_RENDERER attribute to False.
使能硬件加速:
Settings→Developer options→Force GPU rendering: This forces the apps to use the GPU for 2D drawing.
五、硬件绘制
硬件绘制相对比较复杂,在这里只简单带过。
硬件buffer管理与软件绘制基本是一致的。

硬件加速渲染和软件渲染一样,在开始渲染之前,都是要先向SurfaceFlinger服务dequeue一个Graphic Buffer。不过对硬件加速渲染来说,这个Graphic Buffer会被封装成一个ANativeWindow,并且传递给Open GL进行硬件加速渲染环境初始化。
在Android系统中,ANativeWindow和Surface可以是认为等价的,只不过是ANativeWindow常用于Native层中,而Surface常用于Java层中。 Open GL获得了一个ANativeWindow,并且进行了硬件加速渲染环境初始化工作之后,Android应用程序就可以调用Open GL提供的API进行UI绘制了,绘制出来内容就保存在前面获得的Graphic Buffer中。
当绘制完毕,Android应用程序再调用libegl库(一般由第三方提供)的eglSwapBuffer接口请求将绘制好的UI显示到屏幕中,其本质上与软件渲染过程是一样的。
Android自带的libGLES_android如下,但实际使用的GPU驱动mali或者adreno稍有不同,mali的是先dequeueBuffer再queueBuffer。adreno完全闭源。
EGLBoolean egl_window_surface_v2_t::swapBuffers() {
…………
nativeWindow->queueBuffer(nativeWindow, buffer, -1);//此处也有等待fence
buffer = 0;
// dequeue a new buffer
int fenceFd = -1;
if (nativeWindow->dequeueBuffer(nativeWindow, &buffer, &fenceFd) == NO_ERROR) {
sp<Fence> fence(new Fence(fenceFd));
if (fence->wait(Fence::TIMEOUT_NEVER)) {
nativeWindow->cancelBuffer(nativeWindow, buffer, fenceFd);
return setError(EGL_BAD_ALLOC, EGL_FALSE);
}
…………
}
整个流程如下(首帧):

MainThread和RenderThread的分离
在Android 5.0之前,Android应用程序的Main Thread不仅负责用户输入,同时也是一个OpenGL线程,也负责渲染UI。通过引进Render Thread,我们就可以将UI渲染工作从Main Thread释放出来,交由Render Thread来处理,从而也使得Main Thread可以更专注高效地处理用户输入,这样使得在提高UI绘制效率的同时,也使得UI具有更高的响应性。

对于上层应用而言,UI thread仍然是Main Thread,它并不清楚Render Thread的存在,而对于SurfaceView,UI thread不是Main Thread,而是重新启用了一个新的线程。

在Android应用程序窗口中,每一个View都抽象为一个Render Node,而且如果一个View设置有Background,这个Background也被抽象为一个Render Node。这是由于在OpenGLRenderer库中,并没有View的概念,所有的一切可绘制的元素都抽象为一个Render Node。
每一个Render Node都关联有一个Display List Renderer。这里又涉及到另外一个概念—— Display List。Display List是一个绘制命令缓冲区。也就是说,当View的成员函数onDraw被调用时,我们调用通过参数传递进来的Canvas的drawXXX成员函数绘制图形时,我们实际上只是将对应的绘制命令以及参数保存在一个Display List中。接下来再通过Display List Renderer执行这个Display List的命令,这个过程称为Display List Replay。
引进Display List的概念有什么好处呢?主要是两个好处。
第一个好处是在下一帧绘制中,如果一个View的内容不需要更新,那么就不用重建它的Display List,也就是不需要调用它的onDraw成员函数。
第二个好处是在下一帧中,如果一个View仅仅是一些简单的属性发生变化,例如位置和Alpha值发生变化,那么也无需要重建它的Display List,只需要在上一次建立的Display List中修改一下对应的属性就可以了,这也意味着不需要调用它的onDraw成员函数。这两个好处使用在绘制应用程序窗口的一帧时,省去很多应用程序代码的执行,也就是大大地节省了CPU的执行时间。
虽然从Android3.0开始到Android8.0,每一次都会对硬件绘制升级,也显示了硬件绘制的重要地位,但基本原理没有变,还是利用了DisplayList。
简单的TextView的DisplayList命令是这样的
Save 3
DrawPatch
Save 3
ClipRect 20.00, 4.00, 99.00, 44.00, 1
Translate 20.00, 12.00
DrawText 9, 18, 9, 0.00, 19.00, 0x17e898
Restore
RestoreToCount 0
DisplayList最后的GPU命令如下,如果是游戏通常直接调用下面类似的命令,这样效率更高。
glUniformMatrix4fv(location = 2, count = 1, transpose = false, value = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0])
glBindBuffer(target = GL_ARRAY_BUFFER, buffer = 0)
glGenBuffers(n = 1, buffers = [3])
glBindBuffer(target = GL_ELEMENT_ARRAY_BUFFER, buffer = 3)
glBufferData(target = GL_ELEMENT_ARRAY_BUFFER, size = 24576, data = [ 24576 bytes ], usage = GL_STATIC_DRAW)
glVertexAttribPointer(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0xbefdcf18)
glVertexAttribPointer(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0xbefdcf20)
glVertexAttribPointerData(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x??, minIndex = 0, maxIndex = 48)
glVertexAttribPointerData(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x??, minIndex = 0, maxIndex = 48)
glDrawElements(mode = GL_MAP_INVALIDATE_RANGE_BIT, count = 72, type = GL_UNSIGNED_SHORT, indices = 0x0)
glBindBuffer(target = GL_ARRAY_BUFFER, buffer = 2)
glBufferSubData(target = GL_ARRAY_BUFFER, offset = 768, size = 576, data = [ 576 bytes ])
glDisable(cap = GL_BLEND)
glUniformMatrix4fv(location = 2, count = 1, transpose = false, value = [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 33.0, 0.0, 1.0])
glVertexAttribPointer(indx = 0, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x300)
glVertexAttribPointer(indx = 1, size = 2, type = GL_FLOAT, normalized = false, stride = 16, ptr = 0x308)
glDrawElements(mode = GL_MAP_INVALIDATE_RANGE_BIT, count = 54, type = GL_UNSIGNED_SHORT, indices = 0x0)
eglSwapBuffers()
总结
GPU是应用绘制的主力, 我们称之为内容端,也就是真正的Buffer;而控制端则由DisplayProcessor完成;无论是Frameworks中的WMS还是SF都是在解耦这两个功能。
但是你可能会觉得奇怪,SF中用GPU的地方也很多啊,这属于内容端还是控制端呢?SF中GPU是绘制还是合成?
显示系统中还有另外一个让人混淆的地方就是谁来绘制,谁来合成,谁来显示的问题。这与上面的内容端与控制端又属于2个维度,其中我们可以认为绘制属于内容端,而显示属于控制端,而合成则介于两者之间。
如上文所述,Android显示流水线大约分为3部分,上文只有讲解绘制(客户端,对应于DRM),本文主要讲解一下合成部分(服务端,对应于KMS),合成部分是承上启下,也是理解显示框架的核心部分,我们知道Linux系统上目前的窗口系统Wayland最重要的模块便是Wayland Composer(Weston),后面会专门讲解一下linux系统的窗口管理系统与Android的区别。
前提概要
1. 按照显示流水线来看,合成阶段发生在SurfaceFlinger 和 Hwcomposer

SurfaceFlinger 顾名思义:Surface + Flinger, 即将Surface组合一起;
Hwcomposer 顾名思义:HW + composer:硬件合成器
所以,本文重点即是讲解发送在SF和HWC阶段的合成,但讲解顺序为先HWC后SF,主要原因在于HWC更为基础,是理解合成阶段的关键;对于SF,我们会结合Buffer管理讲解,其中包括Gralloc和Fence等概念。(在中下篇)
2. 按照显示模块关系来看,SF的位置承上启下,如下图(标注橙色模块为OEM厂商负责)

OpenGLES/Vulkan(GPU) : 主要发生在绘制阶段,上文已讲解(合成也需要,主要2D功能)
Gralloc(ION):分配和使用Buffer;
Hwcomposer(KMS):决定合成策略;
3. 是否能将显示与合成放在一个流水线里面,这理论上是更理想的流水线方式,如下图对比,延时会明显降低,这是个思考题,最后我们会讨论一下。

Hwcomposer
这是Android发展到2.0之后才出现的概念,算是SF的附属子模块,之前是运行在SF主线程的,如今改成单独的Service,这是Android的设计理念,效率其实是下降了,两个进程的交互是相当频繁的。

如上图rebuildLayerStack、prepare、doComposition就是SF进程调用HWC模块
1 功能

其中合成策略的整体方法如下:
A scene from SurfaceFlinger would be composited via a combination of putting some layers directly on overlay planes and flattening the rest with the GL compositor (and putting the result on a hardware plane).

下面有个更直观的图,决定layer列表是由GPU合成还是overlay planes合成:其中3个小图层使用了overlay planes,而另外几个图层由GPU合成后,又分配了一个overlay plane。

对于上面这张图,我们给出一个对应的DPU设计图(高通称MDP,就是移动显示处理器),如下,(显示下篇会主要详细介绍DPU),如下每个overlay就按照顺序对应着上面图中的每个layer buffer(如Status Bar等4个)

2 HWC的历史
2.1 Overlay模块
前身:overlay模块,大约在Android2.0之前
用途:假如没有这样一个模块,所有合成任务都将有GPU完成,则会带来性能与功耗的负担,GPU主要用于绘制,参与合成则会造成资源的浪费,特别是在10年前,GPU参与合成,性能会急剧下降——对于如今来说,并没有那么大影响了,基本顺手捎带着就完成了,如今的考虑主要在于功耗,GPU合成的效率太低了。
我们来看GPU合成的操作是不需要3D渲染的,所做的工作无非是:
格式转换;
缩放操作;
拷贝操作;
这种操作也称为Blit2D,最早的GPU会拆分一个模块去做这件事情,比如vivante、Imagenation,但后来这些接口都废弃不用了,承担这一责任的就是DPU了,效率显然更高。(GPU也不再提供专门的2D模块,毕竟2D是3D的子集)
DPU:Display Processor Unit
DPU的能力有强有弱,至少要存在1个通道,称为Graphic Pipe,如果只有1个通道,则一般由GPU合成输出。如果DPU要有合成功能(layer mixer),则至少要有2个通道。
最早的DPU增加的overlay plane还主要用于视频播放,支持的格式也只是YUV;当然这也与Android最早的Overlay模块的默认使用的是V4L2功能,主要处理视频输出。其分为2部分——数据与控制,这与Android系统上的SurfaceFlinger的数据与控制端是类似的。但HWC中则与应用Client(数据端)解耦了,只与Server有关了。
——我最早接触的DPU,都没有alpha blending的功能,视频只能支持全屏输出,带UI的就无能无力了,苟且的方案是colorkey,但这个效果太差,是不能在项目上落地的。
2.2 HWC 1.0
随着DPU能力的增强,支持的overlay通道越来越多,而且overlay的能力也越来越强,另外此时也支持多个显示设备了,所以Hwc 1.0应运而生,这个版本持续时间还比较长,大约一直持续到Android M。
接口非常简单,可以说只有2个函数:prepare() set()

Prepare:先验证下是否能用overlay通道合成,能的设置为HWC_OVERLAY,其余的则标记为HWC_FRAMEBUFFER,统一由GPU合成;
Set:将最终的Buffer列表传给DPU,等vsync到来显示出来;
但其实比如VSync事件处理、HDMI插拔事件处理、还有些显示后处理的操作基本也会被OEM厂商实现了,也就是越接近硬件的功能都会放在这里实现。
如果来看数据流程图,也比较清晰(Fence会专门放在后面讲解,这是理解显示流程的关键);

2.3 FB到ADF的转变

ADF是短暂的过渡,高通没有使用该框架;还是坚持FB的框架(显示下篇我们会介绍经典的FB框架是如何实现的),但ARM是使用了ADF框架的(ADF是兼容FB的,在内核中简单实现了接口)。
ADF代码写的很不错,框架是非常清晰的:
1. 定义了drm buffer、overlay planes、显示设备CRTC;所有这些和Linux的DRI框架KMS是类似的(如下drm框架)。

2. 显示框架中明确加入了Fence的流程(之前这是留给OEM厂商去实现的)
3. 明确支持了vsync与hdmi插拔事件(之前也是留给OEM厂商去实现的)
2.4 HWC 2.0

虽然HWC版本升级了,但基本的工作还是没变,只是将之前很多OEM厂商自己要做的事情,由HWC模块规范化了,这也吸收了ADF的一些特性;
以drm_hwcomposer为例,图中d0代表显示设备display0,dn代表显示设备displayn

伴随着HWC2的是从FB/ADF到DRM(KMS)的转变,这是最大的变动,见下节。
2.5 FB/ADF到DRM(KMS)的转变
如前所述,ADF框架高通并未采用,也许高通知道谷歌的规划是将采用Linux系统下的DRM框架,这在上一篇文章中已经讲过了。

1. Direct rendering manager (DRM), is introduced to deal with graphic cards embedding GPUs
2. Kernel mode setting (KMS) is a subpart of the DRM API, sets mode on the given display
3. Rendering and mode setting are split into two different APIs and are accessible through /dev/dri/renderX and/dev/dri/controlDX
4. KMS provides a way to configure the display pipeline of a graphic card (or an embedded system)
5. KMS is an alternative to frame buffer dev (FBDEV)
FB框架到DRM框架是整个框架的改变,终于和Linux实现了兼容,FB框架(在显示下篇会介绍)基本上被废弃了,对于上层用户来说,’/dev/fb0’是找不到了,但在drm框架下兼容FB接口还是非常简单的,主要看OEM厂商的意愿了。
还需要兼容这些接口吗?那就看谁在使用FB/ADF/DRM的接口呢?
其一,Android启动后,hwcomposer就是唯一的使用者;
其二,Android启动前,大约有下面3个情况:
Recovery——显示调用比较简单,可以通过这个了解drm的基本显示流程;
关机充电;
开机从kernel到android的启动前这段时间;基本不会主动绘制(通常是一个logo,由bootloader传过来),这是在kernel完成的
可以看出来使用这些接口都是系统完成的,由谷歌自己完成了升级,第三方是没有权限调这样的接口的,兼容是没必要了。
相比FB,所有的hwcomposer的接口也随之改变,开源的drm_hwcomposer给了很好的范例,也增加了很多代码。
Plane objects managed through properties and atomic mode set commit;
Features exposed through plane properties;
Default allocation in driver. User mode can query and override, virtualization is taken care in CRTC
2.6 composer 作为单独的服务
Anroid 为了将vendor与system分隔开,composer被拿出来作为单独的服务,效率肯定是变差了。我们以使用开源的drm_hwcomposer为例,需要下面这样的配置:
+ <hal format="hidl">
+ <name>android.hardware.graphics.composer</name>
+ <transport>hwbinder</transport>
+ <version>2.3</version>
+ <interface>
+ <name>IComposer</name>
+ <instance>default</instance>
+ </interface>
</hal>
+PRODUCT_PACKAGES += android.hardware.graphics.composer@2.3-service \
+ libhwc2on1adapter \
+ libhwc2onfbadapter
+PRODUCT_PACKAGES += hwcomposer.drm
+PRODUCT_PROPERTY_OVERRIDES += \
+ ro.hardware.hwcomposer=drm \
+ hwc.drm.device=/dev/dri/card0
3 合成策略
3.1 基本策略
1)当前buffer格式是否overlay所支持的,目前基本上能支持几乎所有格式;
2)当前buffer输入大小是否overlay所支持的,一般有最大最小限制;
3)当前buffer输出大小是否overlay所支持的,一般有最大最小限制;
4)当前buffer缩放比例(输出/输入)是overlay所支持,一般0.25-4倍;
5)当前buffer旋转方向是否overlay所支持的,一般90,180,270是可以的;
主要是针对视频,因为视频是VPU解码出来的,未经过GPU。而经过GPU绘制的全部预旋转过了(这在SurfaceFlinger是可以配置的),也就是说全部是0度,即使是你旋转屏幕。说到底这也是为了DPU方便处理;
6)当前buffer是否带有alpha值,overlay通道中有些是不支持的;
——其实在调试DPU的时候,我们遇到最大的问题是DPU的带宽不够(比如缩放比例过小,比如旋转),这会导致屏幕的闪烁——但目前DPU设计的带宽都足够了,但减小系统带宽一直是芯片厂商努力的方向,因为可以这可以减小功耗,比如Arm的AFBC,比如高通的UBWC,都是减小从GPU/VPU到DPU的带宽。
如果说上面的6条措施是硬性规定,并没有什么策略性问题,但当前buffer list是否超过overlay个数,我们不得不面对这样的决策——我们该选择哪几个layer用于GPU合成?这里的原则也是减小带宽。
通常用于GPU合成的layer是:
1) 首先是不怎么变化的layer;
2) 其次再看Layer比较小的(较大的留给overlay);
3)个别情况还需要计算,哪几个layer总的pixel值最小;
4)屏幕几乎静止的时刻,选择方案也会很有趣,此时会采用GPU合成——原因在于关闭DPU几个通道此时对功耗更有利。
目前GPU合成情况少之又少
随着DPU发展迅速,现在已经支持到16个layer了,用到GPU的情况是越来越小了。而且有些DPU可以通过4个1080P组成4K的输出,对于4K的视频也不需要GPU参与合成了。
对于3D的支持也都是目前DPU标配,之前这也是通过GPU来辅助处理的;
3.2 多显示设备
对于多个显示设备,考虑的情形则更多:
1. 主显示设备——LCD;
2. 辅助显示设备——HDMI;
3. 虚拟显示设备——WFD,没有真正的物理设备;
对于LCD + HDMI这样的组合,比如我们可以有2个overlay赋值给LCD,2个overlay赋值给HDMI,也可以3+1,也可以是1+3等;我们之前为了减小DP的带宽,将HDMI的输出格式改为RGB565(两个通道的配置是完全可分开的)。
对于虚拟显示设备,为了减小带宽,则使用了Writeback功能,这也是DPU的一个功能,也就是将DPU的数据直接给到虚拟显示设备的output buffer中去,但是对于宽高以及比例都一定的要求。
3.3 drm_hwcomposer中的改进方案(一个蛮有意思的点)

先说结论,这个方法减小了功耗,但是ARM,高通都不支持,所以现在没人用,又改回去了,如下图。

但这个思路值得研究,来龙去脉在此:

基于硬件优化的GL composer是没有fallback 到SF,而在HWC完成合成,期间无需blending。


这种方法缩短了显示流程,降低了功耗,但带来的优势还是非常小的——因为fall back到GPU的过程本身就是极小概率(16个layer),而且DPU通常也设计出overlay专门用于no blending hardware,功耗足够低。
4 事件处理
除了上面这种合成所做的事情,还会处理一些其他事件,也都是和显示相关的。
VSync(包括对于freesync处理);
HDMI插拔;
分辨率切换;
局部刷新;
5 显示后处理
事实上,在现代DPU的设计中,后处理的功能越来越多,早已超过了仅仅合成的功能,在显示下篇我们会详细介绍这一部分,在这里只是简短说一下。
1. 在我之前写过的“DC调光”那篇文章中提到的,蒙版dither的方法,添加一个dim layer,其逻辑实现就是放在HWC中,这就是一种显示后处理的方法。
2. Pixelworks 的 “独显芯片”中的后处理逻辑实现;
3. 支持HDR这样的Tone mapper方法;
4. 通过内容调节背光的逻辑也在于此;
6 遗留问题回答
对于最开始的问题,能否将绘制与合成2个流水线缩减为1个流水线,其实另一篇文章已经有讲述。
沧浪之水:Android系统上的操控延时24 赞同 · 2 评论文章

——我再复述一下结论,首先缩减流水线的目的是为了减小操控延时,但这样对于帧率稳定性是不利的,事实上,Android不仅没有这样做,反而在高刷的机器上将显示加到了4级流水线。
7 最后
本文主要讲解了hwcompser这个模块,但里面有个关键的概念Fence却没有涉及,是专门留给了下一篇文章,而Fence是理解显示流程中Buffer运转的关键点,既然是Buffer,我们也会结合Gralloc模块。
Graphic Buffer的共享、分配、同步
我们继续讨论显示的3个流水线:绘制——合成——显示中的Buffer流转过程:分为3部分:Graphic Buffer的共享、分配、同步。
1. Buffer共享
1.1 进程间共享
如果我们按照进程模型来说,绘制为Client端(APP),而合成为Server端(SF),最后的显示与合成关系更近,也算是Server端。

图1 Graphic buffer进程间共享
Graphic Buffer能被Client与Server端同时认识,则需要共享机制,在Android上采用的是Binder机制;在Linux上则是采用Socket。
1.2 硬件共享
如果按照硬件来说,Graphic Buffer会被GPU,CPU,DPU访问(其中CPU除了非常特殊情况下,是不会访问这个Buffer,更多的是状态的改变)
PC独立显卡的显存就是GPU与DPU共享的。
Splitting DRM and KMS device nodes
While most devices of the 3 major x86 desktop GPU-providers have GPU and display-controllers merged on a single card, recent development (especially on ARM) shows that rendering (via GPU) and mode-setting (via display-controller or called DPU) are not necessarily bound to the same device. To better support such devices, several changes are being worked on for DRM.
1) Render-nodes
2) Mode-setting nodes
Rendering and mode setting are split into two different APIs and are accessible through /dev/dri/renderX and /dev/dri/controlDX
但是对于嵌入式设备,GPU与DPU不属于同一个硬件。
如果多个硬件设备能访问同一个Buffer,就需要一个共享机制,而这种跨硬件的共享方式就是Prime fd(dma buffer)。而Linux系统2012年引入的原因正是为了解决集成显卡与独立显卡的Buffer共享问题(英伟达的Optimus技术)

在Android HAL层中是ION方式实现,也就是ion fd;
CPU与GPU是有较为频繁的交互,但并不是针对Graphic Buffer,主要是绘制命令和纹理贴图数据。只有Graphic Buffer才采用ion,在Android上通过EGL mem体现,而其他buffer则通过GL mem体现,后者才是大头。
对于移动设备,这个Graphic Buffer地址CPU也是能快速访问的(统一内存架构),对于PC上独立显卡则需要经过PCI-e传输而慢了很多,但CPU通常并不会访问它。
1.2.1 CPU如何访问Graphic Buffer
图1是有年代感的图了,会有个误导:就是mmap函数(与后面提到的GraphicBufferMapper还不一样),这主要是用于CPU的共享方式,或用于CPU与GPU共享,但其实Graphic Buffer通常是在GPU、DPU之间的共享,所以这个mmap函数几乎是很少使用的,我们也能从Gralloc的实现演进看出来。
mmap的时候很少,此时CPU与GPU访问一块地址( 对于移动设备,GPU没有独立显存时,这个函数很快,但如果独显,则需要PCIE传输,就要考虑这个延时);
CPU何时使用GraphicBuffer?
surface-> lock()就是map这块地址了, 这时我们需要这块地址addr,此时是CPU来访问了。但几乎不会用到。 Android图形缓冲区映射过程源码分析_深入剖析Android系统-CSDN博客 中还是 比较久远的gralloc 1.0,其中register也使用了map函数,但目前register的实现也不会如此。
1.2.2 Graphic Buffer常用的共享方式:ion fd
正常的时候,GPU与DPU直接进行交互,只需要传递Fd即可,DPU压根不关心这个具体地址。通常Gralloc中的是这样使用的,如下只是fd就够了。(importBuffer是关键函数)
initWithHandle --> mBufferMapper. importBuffer( handle, width, height, 206 layerCount, format, usage, stride, & importedHandle);
2. Buffer分配
2.1 Gralloc
顾名思义:graphic alloc;但其实包含了:分配Buffer + 使用Buffer;
不像PC,独立显卡有独立显存,Graphic Buffer自然只能使用这样的显存(效率才高)。最近发布的Mac使用统一内存架构,而这个早已是手机上通用的架构了,GPU与CPU是通用RAM的,包括Graphic Buffer都是在RAM上分配的。(比如高通Adreno,高速显存有1-3M左右,但不用于Graphic Buffer)
问题:Gralloc分配的Buffer是物理地址连续的吗?
对于普通的GraphicBuffer(drm_framebuffer)是不需要的, 因为GPU、DPU都有MMU功能,无需物理地址连续,即分配system heap就可以;
对于视频、Camera可能会有要求,要看VPU,ISP是否支持MMU,或者其他要求;
2.1.1 Server端:Allocate
GraphicBufferAllocator :分配Buffer,由 SurfaceFlinger负责(也可以用单独的服务,这个可以配置,后面默认就用SF);只有两个接口:alloc与free

老式方式-Server端分配(SF分配)
2.1.2 Android S blastQueue
为了减小SF的负载,Android S开始强制 Client端分配buffer,而linux上早已如此处理。Android 12 Google将BufferQueue(简称BQ)组件从SF端移动到了客户端,BQ组件的初始化也放在BlastBQ的初始化中。
Android S 版本之前
应用绘制缓冲区仅能通过 BufferQueue IGBP(IGraphicBufferProducer) 提交;
应用窗口Geometry的改变仅能通过事务(Transaction)提交;
通过合并事务(Transaction.merge())或延迟事务来更改应用窗口间的Geometry;
多进程缓冲区之间无法做到同步。
Android S 或更高版本
应用绘制缓冲区可以通过事务Transaction.setBuffer()进行提交;
应用窗口Geometry的改变可以通过BlastBufferQueue进行提交;
应用绘制的缓冲区和应用窗口Geometry可以进行同步;
多应用绘制的缓冲区之间可以进行同步。
作者:大天使之剑
链接: https://www.jianshu.com/p/50a30fa6952e
来源:简书

能看出来,这个新的架构,混合了数据端与控制端,之前bufferqueue只负责数据端,而SurfaceControl负责控制端,如今可以混合使用了,BufferQueue也可以控制大小,宽高,而SurfaceControl可以更新buffer。
所以说,假如buffer与gemetry同时更新,则在一个调用里面就可以完成。
2.1.3 Client端:Mapper
GraphicBufferMapper :应用使用Buffer,由GPU填东西了;接口主要为:importBuffer(之前是registerBuffer),lock,导入当前进程地址空间。

Android 系统中,真正会分配图形缓冲区的进程仅有 Surfaceflinger( 如今也可以配置由client端负责,Android S上已经强制如此),尽管可能会创建 BufferQueue 的进程有多个。
GraphicBufferAllocator 和 GraphicBufferMapper 在对象创建的时候,都会通过 Gralloc1::Loader 加载 HAL gralloc。只有需要分配图形缓冲区的进程才需要创建 GraphicBufferAllocator,只有需要访问图形缓冲区的进程,才需要创建 GraphicBufferMapper 对象。
从 Android 的日志分析,可以看到,只有 Surfaceflinger 进程中,同时发生了为创建 GraphicBufferAllocator 和 GraphicBufferMapper 而加载 HAL gralloc。而为创建 GraphicBufferMapper而加载 HAL gralloc 则发生在 zygote、bootanimation 等多个进程。可见能够访问图形缓冲区的进程包括 Android Java 应用等多个进程。
Zygote在preload会加载GraphicBufferMapper,因为zygote是所有应用的父进程,也就是说每个应用也都会调用这个mapper,这样的话,应用才能正常使用这个buffer,拿来绘制。Zygote Preload的资源还有就是常用texture 和 常用shader。
下图为谷歌设计的显示栈,可以看到gralloc.minigbm在整个架构中的位置(高通还是按照ION框架实现,虽然底层也是drm buffer)

Google DRM for Android:Android on an Upstream Stack
2.2 BufferQueue
GraphicBuffer 如上图所示,是GraphicBufferAllocator 与GraphicBufferMapper 结合体,接口包含分配,也包含使用。
GraphicBuffer的队列放在缓冲队列BufferQueue中(Client和Producer在这里是同义的)。
BufferQueue对App端的接口为IGraphicBufferProducer,实现类为Surface,对SurfaceFlinger端的接口为IGraphicBufferConsumer,实现类为SurfaceFlingerConsumer(最新版本改名了,但不影响讨论,android S 已经基本取消了bufferQueueLayer,默认都使用BufferStateLayer)
BufferQueue中对每一个GraphiBuffer都有BufferState标记着它的状态,
比如new Surface是不会真正分配的,只有在 dequeuBuffer的时候才会请求分配,此时会调用new GraphicBuffer则会真正分配。
在状态分配时,对于Client端有dequeueBuffer(请求), queueBuffer(绘制结束,发送至服务端) ;
除了上面提到的dequeue/queue/acquire/release这些基本操作函数外,BufferQueue还为我们提供了一些特殊函数接口,方便调用者在一些非常规流程中使用(用于视频、Camera,正常UI不调用attach与detach)。
Producer:
attachBuffer 不涉及到buffer的分配动作
dequeueBuffer 可能会涉及到buffer的分配动作
detachBuffer 释放buffer,slot —> mFreeSlots
cancelBuffer 不释放buffer,slot —> mFreeBuffers
Consumer:
attachBuffer 直接从FREE —> ACQUIRED
acquireBuffer 必须是 QUEUED —> ACQUIRED
detachBuffer 释放buffer,slot —> mFreeSlots
releaseBuffer 不释放buffer,slot —> mFreeBuffers
原文链接: BufferQueue 学习总结(内附动态图)
一个显示流程需要几个BufferQueue呢?
如上面所述,正常情况一个BufferQueue就够了,APP就是生产端,SurfaceFlinger就是消费端。
2. 对于Camera来说,则涉及三个缓冲区队列:
App - 该应用使用 SurfaceTexture 实例从相机接收帧,并将其转换为外部 GLES 纹理。
SurfaceFlinger - 应用声明用来显示帧的 SurfaceView 实例。
MediaServer - 您可以使用输入 Surface 配置 MediaCodec 编码器,以创建视频(与显示关系不大)。
在上图中,箭头指示相机的数据传输路径。 BufferQueue 实例则用颜色标示(生产方为青色,使用方为绿色)。

3. 上面这个过程实际上还有可能增加一个BufferQueue:也就是GPU合成的时候

此时SurfaceFlinger则又增加一个BufferQueue,此时有4个BufferQueue了(关乎显示流程的是3个)。
3 Buffer 同步:Fence
BufferQueue里面的QUEUED,DEQUEUE等状态一定程度上说明了该GraphicBuffer的归属,但仅仅指示了CPU里的状态,而GraphicBuffer的真正使用者是GPU和DPU。也就是说,当生产者把一个GraphicBuffer放入BufferQueue时,仅仅是在CPU层面完毕了归属的转移。但GPU说不定还在用,假设还在用的话消费者是不能拿去合成的。这时候GraphicBuffer和生产消费者的关系就比較暧昧了。消费者对GraphicBuffer具有拥有权。但无使用权,它须要等一个信号,告诉它GPU用完了,消费者才真正拥有使用权。
这就需要一种不仅是跨进程的,也是跨硬件的同步机制: Fence 机制
Android中的GraphicBuffer同步机制-Fence - brucemengbm - 博客园
GPU编程和纯CPU编程一个非常大的不同是它是异步的。也就是说当我们调用GL command返回时这条命令并不一定完毕了。仅仅是把这个命令放在本地的command buffer里。详细什么时候这条GL command被真正运行完毕CPU是不知道的,除非CPU使用glFinish()等待这些命令运行完,第二种方法就是基于同步对象的Fence机制。
一个说明:
我们这里的同步基本到合成结束,因为显示这一部分:我们默认buffer早已经是完整的,且完全更新过了(尽管可以在最后一行刷新的时候准备好最后一行就好了,就像VR那样,但手机上我们不这么做)
同步也就是在GPU、DPU、CPU之间,其中CPU只处理Buffer状态,并不触及内容,真正的内容同步是GPU、DPU之间的,这是通过Fence完成的。如下图,可以看出有3个不同的Client与Server路径。
(从下图,能看出一个有意思的现象,为何3Buffer机制中FB确使用了2个buffer呢?后面路径2会解释)

原图

注释路径
3.1 最可能的路径1:(绘制——合成——显示)
在Client与Server端同步一般会遇到2个问题:
1)在client端GPU还在绘制的时候,Server端DPU是不能拿来用的,否则就是撕裂的画面;(此时是等待acquireFence,具体的流程可参加最后一节Fence代码流程,直接引用参考文献)

如上,Client的Renderer发送Buffer的时候是顺便带着一个Fence的,这个fence经过SF、HWC传到DisplayDriver,此时会wait(acquire_fences()),然后在vsync的时候传给Display。
2)在Server端还在显示的时候,这个Buffer同样是不能被Client端GPU拿来使用的,否则显示也会撕裂;(此时是ReleaseFence)如上图,Client的Renderer在dequeueBuffer的时候也会等待。
大部分情况下,正是走的这条路径,这样的话2个流水线是能走完的,那剩下的时间都干嘛去了?如下图:

可见真正的绘制是很长的,可以有接近2个vsync周期;看起来是有些浪费(主要是时间的浪费,GPU资源并没有浪费,流水线是能充分利用资源),如果我们将流水线设置为2段(绘制+合成作为一个),这与PC上是类似的;
3.1.1 三级流水线的必要性
按照上面这样的描述,似乎使用2个流水线(绘制——显示)就能搞定,这也是Fence引入时所希望的,但为什么我们并没有这么做?(上一节并没有讲的那么清楚)。

问题的回答:
但是如前所述,如果我们采用VSYNC,则需要3 Buffer(PC游戏上3 Buffer与Vsync也是相辅相成)
谷歌文档中,给出了这样的解释:假如缓存区B的处理需要较长时间,并且A正在使用中显示当前帧。此时引入第三个缓存区,则CPU与GPU不会空等,而是系统创建C缓冲区,可以避免下一次Jank,从而让系统变得流程,通俗来讲就是减小红绿灯的等待。

但这个图是有误导作用的,首先对于Buffer A、B、C来说,CPU是不会触碰到的,只有GPU与Display才是使用者,对于CPU只是处理状态,这样的蓝色B,C,A就是对于当前业务逻辑的处理,而且这样的业务逻辑是随机的,所以就会出现上面这样的情况;如下图所示

依赖于GPU的地方在于,flush_commands 和 eglSwapBuffer,如下图,CPU占比还是比较重的,也就是帧率稳定性会受到较大的波动(出现谷歌文档出现的现象)

(从以上可以看出,CPU的处理过程占了很大的比重,对于游戏来说,通常来说CPU的处理是恒常的,但如转场动画、触控事件都会导致不确定性;但假如游戏设计的很好,双Buffer不是不可行的,但是这需要Android重新设计流水线了,同时也不要等待Vsync调节了)
3.2 路径2:(仅包含 绘制——合成)
但显然还有另外一种情况,那就是如果我们不得不Fallback to GL composer时,该如何呢?
1)在client端GPU还在绘制的时候,Server端(此时是GPU了,GPU是支持多实例的)不能拿来合成至FrameBufferSurface,否则合成后便是撕裂的画面;
2)在Server端(同样是GPU)在合成至FrameBufferSurface(doing),这个Buffer同样是不能被Client端GPU拿来使用的。
FB之所以使用2个Buffer,则是因为使用2个流水线,这个FB buffer,绘制阶段是用不到的。
3.3 路径3:(仅包含 合成——显示)
但其实路径2还没结束
此时又是一个Cleint端与Server端的流程(尽管此时Client与Server端都分别对应SF与composer)
1)在client端GPU还在合成的时候,Server端DPU是不能拿来用的,否则就是撕裂的画面;
2)在Server端还在显示的时候,这个Buffer同样是不能被Client端GPU拿来使用的,否则显示也会撕裂;
4 Fence代码分析(虽然代码较老,但框架没变)
全部援引参考:Android中的GraphicBuffer同步机制-Fence - brucemengbm - 博客园
4.1 acquireFence: (见下图 :acquireFence流程)

当App端通过queueBuffer()向BufferQueue插入GraphicBuffer时,会顺带一个Fence,这个Fence指示这个GraphicBuffer是否已被生产者用好。之后该GraphicBuffer被消费者通过acquireBuffer()拿走,同一时候也会取出这个acquireFence。之后消费者(也就是SurfaceFlinger)要把它拿来渲染时,须要等待Fence被触发。假设该层是通过GPU渲染的,那么使用它的地方是Layer::onDraw()。当中会通过bindTextureImage()绑定纹理:
status_terr=mSurfaceFlingerConsumer->bindTextureImage();
该函数最后会调用doGLFenceWaitLocked()等待acquireFence触发。由于再接下来就是要拿来画了。假设这儿不等待直接往下走,那渲染出来的就是错误的内容。
假设该层是HWC渲染的Overlay层,那么不须要经过GPU,那就须要把这些层相应的acquireFence传到HWC中。这样。HWC在合成前就能确认这个buffer是否已被生产者使用完,因此一个正常点的HWC须要等这些个acquireFence全被触发才干去绘制。这个设置的工作是在SurfaceFlinger::doComposeSurfaces()中完毕的。该函数会调用每一个层的layer::setAcquireFence()函数:
428 if (layer.getCompositionType() == HWC_OVERLAY) {
429 sp<Fence> fence = mSurfaceFlingerConsumer->getCurrentFence();
...
431 fenceFd = fence->dup();
...
437 layer.setAcquireFenceFd(fenceFd);
能够看到当中忽略了非Overlay的层,由于HWC不须要直接和非Overlay层同步,它仅仅要和这些非Overlay层合成的结果FramebufferTarget同步就能够了。GPU渲染完非Overlay的层后,通过queueBuffer()将GraphicBuffer放入FramebufferSurface相应的BufferQueue。然后FramebufferSurface::onFrameAvailable()被调用。它先会通过nextBuffer()->acquireBufferLocked()从BufferQueue中拿一个GraphicBuffer,附带拿到它的acquireFence。
接着调用HWComposer::fbPost()->setFramebufferTarget(),当中会把刚才acquire的GraphicBuffer连带acquireFence设到HWC的Layer list中的FramebufferTarget slot中:
580 acquireFenceFd = acquireFence->dup();
...
586 disp.framebufferTarget->acquireFenceFd = acquireFenceFd;
综上,HWC进行最后处理的前提是Overlay层的acquireFence及FramebufferTarget的acquireFence都被触发。
4.2 releaseFence: (见下图 :releaseFence流程)

前面提到合成的过程先是GPU工作,在doComposition()函数中合成非Overlay的层,结果放在framebuffer中。然后SurfaceFlinger会调用postFramebuffer()让HWC開始工作。
postFramebuffer()中最主要是调用HWC的set()接口通知HWC进行合成显示,然后会将HWC中产生的releaseFence(如有)同步到SurfaceFlingerConsumer中。实现位于Layer的onLayerDisplayed()函数中:
mSurfaceFlingerConsumer->setReleaseFence(layer->getAndResetReleaseFence());
上面主要是针对Overlay的层,那对于GPU绘制的层呢?在收到INVALIDATE消息时,SurfaceFlinger会依次调用handleMessageInvalidate()->handlePageFlip()->Layer::latchBuffer()->SurfaceFlingerConsumer::updateTexImage() ,当中会调用该层相应Consumer的GLConsumer::updateAndReleaseLocked() 函数。
该函数会释放老的GraphicBuffer,释放前会通过syncForReleaseLocked()函数插入releaseFence,代表假设触发时该GraphicBuffer消费者已经使用完成。然后调用releaseBufferLocked()还给BufferQueue,当然还带着这个releaseFence。
这样。当这个GraphicBuffer被生产者再次通过dequeueBuffer()拿出时。就能够通过这个releaseFence来推断消费者是否仍然在使用。
还有一方面,HWC合成完成后,SurfaceFlinger会依次调用DisplayDevice::onSwapBuffersCompleted() -> FramebufferSurface::onFrameCommitted()。onFrameCommitted()核心代码例如以下:
148 sp<Fence> fence = mHwc.getAndResetReleaseFence(mDisplayType);
...
151 status_t err = addReleaseFence(mCurrentBufferSlot,
152 mCurrentBuffer, fence);
此处拿到HWC生成的FramebufferTarget的releaseFence,设到FramebufferSurface中相应的GraphicBuffer Slot中。这样FramebufferSurface相应的GraphicBuffer也能够被释放回BufferQueue了。当将来EGL从中拿到这个buffer时,照例也要先等待这个releaseFence触发才干使用。
Display Processor的设计
本文DPU代表Display Processor Unit,并不是机器学习的Deep-LeaningPU
PC上DPU是嵌入在显卡上,不管是独立显卡还是集成显卡都是如此。由于GPU能力越来越强,DPU目前基本是附赠的功能,但从历史来看,GPU才是后有的新鲜之物,最早的只有DPU,从最早的Framebuffer机制就能看出,DRM框架中最早版本中也是不存在GPU的代码。
DPU最简单的功能便是将Frambuffer数据输出到显示设备上去,而Framebuffer的来源也都是来自于CPU的软件绘制,而非GPU绘制。

原始DPU
上图没有给我们很大启发,因为这离我们现代的DPU设计差别太远。
1. DPU与GPU的耦合是历史产物,完全可以独立出来
【DPU用于控制端,GPU用于内容端】

通过Linux的dri显示框架,也能看出KMS的相对独立性,对应于系统侧的composer,而drm则在于内容相关的应用侧。对于Android系统也是一样的,GPU对应于drm(不过高通与mali并没有遵循这个开源drm框架)是用来绘制的,属于应用端的进程;而DPU对应于KMS,运行于服务端,可以认为在SurfaceFlinger(composer)中,开机就会初始化,然后保持不变,两者的分离更加彻底。
PC上Linux与移动端Android的不同
PC上耦合还是非常强的,DPU与GPU共享显存,代码也放在一个文件里,Buffer管理(GEM/TTM)自然是互通的,linux中默认代码是合并一块的,这是历史遗留问题——Andriod则不同,天生就是分离的,而ION是Android分配buffer的标准。
Linux平台:我们拿高通adreno的Linux开源代码来看,系统将DPU与GPU合并在一个文件夹下: drivers/gpu/drm/msm,功能基本也大体是分开的,比如GPU相关的为:adreno、msm_gpu.c,msm_ringbuffer.c,比如DPU相关的为disp,edp,hdmi等。但仍然有一部分代码是耦合在一起的,比如msm_gem.c, mem_drv.c。GPU命令还是使用drm标准的或定制的命令。
对于GPU来说,UMD使用的是mesa(高通并没有官方linux的支持)
Android平台:高通官方代码则在两个完全不同的仓库,不存在任何代码的共享,GPU放在drivers/gpu/msm,配置的是KGSL,DPU则是不开源的私有库(OEM厂商可以拿到)。这也说明两者逻辑上并不存在那么紧密的联系,也就是传个framebuffer。
对于GPU来说,UMD是libGLES_xx.so(包含GL和EGL),并没有GEM和DRM那套东西,完全闭源,OEM也拿不到源码。
GPU与DPU完全可以采用不同的厂商,但通常也是一家的,原因何在呢?
Buffer共享更高效:虽然buffer共享是通过ion,但是为了节省DDR带宽,通常会将共享的buffer压缩,比如Arm的AFBC,高通的UBWC。
如果使用不同的厂家,其实也能做到这一点,比如对于ARM,如今mali gpu还是广泛被使用,但mali dpu已经少有人用了,那就附赠一个AFBC Decode模块,如下图。(高通并没有放开这个限制)

DPU的基本功能应该有哪些呢?
DPU的设计相比GPU来说还是简单的,在于其功能的固定性,不可编程,其基本功能大约有2个。
1)2D加速(缩放,合成)
最早的linux代码还能看出痕迹,一开始2D加速功能都是使用CPU;后面2D加速开始使用GPU来实现。到Android系统后,则由GPU专门的2D模块来实现(甚至会配置为双GPU,其中一个GPU只做2D加速),然后专门的DPU出现代替了GPU的2D模块(后面GPU再没有专门的2D模块,因为2D本来就是3D的子集,虽然专门设计的2D模块效率会高一点,但也没有DPU效果高,所以逐渐淘汰)。
2)vout的管理(连接LCD,HDMI等设备)。
下面给出DPU的一个基本设计原型,这包含4个部分。
2. DPU的原型设计
2.1【DPU的四大组成部分】
这是2013年的DPU设计图,当年Android发布了升级最大的4.4(也许是最成功的一代)。从下图可以看出DPU的设计大体分为四部分:

DPU Design
1)Source Surface Pipes(Pipe也称overlay,后面不再区分): 支持4个overlay通道(V1-V4),支持RGBX,YUV等多个格式,缩放比例(1/4 - 4),且每一个layer都支持alpha通道,
C1、C2是鼠标层,对于PC来说很重要,但对于手机来说基本没人使用。
当时还不支持旋转;
支持4个layer的alpha blending,在当时还是比较奢侈的,比如监控就没必要这样设计了,更离谱的有的设计了16个layer看着很唬人,但支持alpha 只有1个,也没有任何用处,对于Android系统来说alpha的layer特别多。
2)Blender: 支持2个Blender,对应于2个Path(除了LCD外,对应于DP或HDMI投屏);
3)Destination surface post-processor:支持dither,gamma调整;目前的趋势是这部分越来越重要。
4)Display Interface:支持最多2路同时的输出设备(物理显示设备,虚拟显示设备不需要实际的输出设备);支持LVDS,DSI,CVBS,HDMI等显示设备;
DPU更细节的图如下:

如果放在Android系统中,我们来看一个HDR视频的播放流程的话,则能更好的看出这4个部分。

2.2【KSM与DPU】
其实这张图也和我们常见的DRM的KSM框架图非常契合,也就是说KSM与DPU功能几乎等同:

此图DRM Framebuffer应该是DRM Framebuffer list
Source Surface Pipes:每个overlay对应一个Plane,每个ovelay中都有一个DRM Framebuffer;在dumpsys SurfaceFlinger的时候,每个Layer就是一个overlay,一个DRM Framebuffer。
-----------------------------------------------------------------------------------------------------------------------------------------------
Layer name
Z | Window Type | Layer Class | Comp Type | Transform | Disp Frame (LTRB) | Source Crop (LTRB) | Frame Rate (Explicit) [Focused]
-----------------------------------------------------------------------------------------------------------------------------------------------
com.android.systemui.ImageWallpaper#0
rel 0 | 2013 | 0 | DEVICE | 0 | 0 0 1080 2400 | 0.0 0.0 1080.0 2400.0 | [ ]
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
com.miui.home/com.miui.home.launcher.Launcher#0
rel 0 | 1 | 0 | DEVICE | 0 | 0 0 1080 2400 | 0.0 0.0 1080.0 2400.0 | [*]
CRTC:对应于一个Path,每个Path至少有一个Blender,DSPP也在此处(如下图);所有的layer都会做alpha blending,输出到一个显示通路,通常格式为RGB24和RGB30(10bit),也就是显示在屏幕上的内容。
Display Interface:Encoder,Connector是与显示设备相关的;最终这个RGB24的数据会通过MIPI、DP传给显示设备,这些协议相关的实现也在DPU模块内完成。

3. DPU的最新设计
3.1【Source Suface Pipes or Overlays】
1)Pipes(也有叫overlays)一般分为两种:(这也不能叫趋势,高通从一开始就有这两种)
一种支持缩放、旋转,锐化等复杂功能的Video overlay(当然,video overlay可以适用于任何layer,video只是一个称呼,更适用于游戏和视频);这里的缩放,锐化是单layer的,与后面的整个屏幕的缩放不同。
一种简单功能的Graphic overlay;支持格式转换,也支持alpha;
2)支持输入的分辨率更大,比如支持4K的输入,这需要更高的DPU频率;
3)随着XR(AR、VR)设备的出现,目前单眼4k已经出现(DPU就要支持8K的输入),这样带宽压力太大,所以目前的做法通常并不是直接4K的输入,而是切分成2个2k(当然这样可用的layer就会减小一半),这就是Split功能;(这也不是新功能,因为很多年前4k视频就出现了)。
4)支持旋转,主要用于视频播放,其他场景基本用不上,GPU会预旋转。(mali dp 650支持这个就不好,主要带宽影响太大,从kernel开源代码看,dp650后,mali似乎便没有更新)
5)pipe越来愈多,比如8个,16个(基本也不会比这更多了)
对于手机至少需要6个:1. Main activity (2 layers); 2. status and navi bar (2 layers) 3. round cornor(2 layers,高通针对round cornor这种永远不会变化的区域也有优化);
对于应用于电视的Box则要考虑缩放,每个layer都会被缩放(所以需要一个dest的缩放,而非source)
6)支持压缩格式(UBWC或AFBC);减小内存带宽,特别是与GPU的交互带宽。

小结:这些技术都出现很多年了,也看不出未来变化的趋势,除了第三点,因为XR对于分辨率的追求仍没有到头,单眼8K也会到来,这样DPU要支持16K的输入,这个带宽压力太大了(特别是在scale down的时候),即使切成2个8K,压力仍然很大,所以未来是不是搞2个DPU出来也未可知。
3.2【Blender】
1)合成layer越来愈多,比如支持10个layer的合成(大部分layer其实不会互相叠加);
2)合成path越来愈多,比如支持4个(同时使用3个的场景已经非常罕见)
WFD(虚拟显示设备)也算是一个path,对于XR来说每个2D应用都是通过wfd来实现的,而WFD是DPU的writeback功能实现的,而writeback功能一般也只支持一个path,如果有多个wfd,则只能借助GPU来实现了。
如果未来有发展,便在于是否增加Writeback的path,如果这样不合算,则需要考虑只采用一个虚拟显示设备,所有的2D应用都放在此处。
3)支持3D功能;(可以区分左右眼,因为3D功能是很多年前便普及的了,所以不是新技术)
4)Dim layer:Android上的常用场景,作为渐变色,只有灰度值的变化,其他不变;
如果大家对于Oled屏幕上的DC调光有了解,便会知道,Oppo最初的方案便是增加一个dim layer,然后调整这个灰度值去让屏幕显得没有那么亮,从而避免PWM调光。
5)Background color:对于单色图片,也有一些优化方案。
小结:对于4和5,完全是根据应用场景增加的优化方案,为了节省功耗,也算是一点点抠了。未来XR的发展,可能会针对Writeback功能做进一步优化;
3.3【Destination surface post-processor】
最开始后处理还只是dither、gamma校正、还有亮度、对比度、饱和度调整这些功能,在四个模块中并不重要,但却是近几年发展最快的一个模块了。现在旗舰手机很多用上了独立显示芯片PixelWorks(后面简称PW),宣传的功能便是:MEMC、HDR、阳光屏(CABL)、护眼模式、Demura、超分;这些功能高通都有,全放在自己的后处理中。
1) 超分与锐化
这里的超分指的是Destination Scaler,是对整个屏幕数据做的,与前面的Source pipe的针对layer的超分是不同的,虽然算法是一样的。
目前平台几乎都不再使用简单的双线性插值,而是自己的算法,但目前仍是基于单帧的技术,虽然MTK宣称已经支持AI超分,但效果并没有让大家觉得特别亮眼。
PC上的有英伟达的AI超分DLSS、AMD的传统超分FSR,在网上反映都还比较不错,但放在手机上要么功耗高,要么在手机上这种高PPI的应用场景,超分的优势就没那么大了。(在PC上表现良好的FSR超分算法在手机上效果真的是不好)
随着XR对于分辨率越来越高,所以这个需求还会继续发展,也是未来的一个发展方向。
2)支持HDR,SDR to HDR,都是基本操作。
3)亮度调整:区别于Android根据环境光调整,主要是基于内容的背光调整算法。可以区分为indoor和outdoor。Indoor光线不强,主要由CABL和FOSS,其中分别针对LCD和OLED屏幕;outdoor则使用ARM的阳光屏技术,当然高通在后面采用了自己的Local Tone Mapper策略(既可以用于indoor,也可以用于outdoor)替换了ARM的阳光屏技术,主要拉升图像暗处细节,也不能让高亮的地方出现过饱和。
4)MEMC
电视上的标配,目前手机上也都是放在视频上,是PW最开始引入手机上最重要的原因,通过插帧实现30帧都60帧视频的流畅。
5)demura:oled上的必备流程
小结:同样工作放在DPU中处理功耗也会低一点,PW是放在后处理后的interface模块,所以PW去做功耗则会高一点;如果DDIC去做,则功耗会更高一点,越靠前则功耗更低。不仅在于流程,还在于制程,所以PW存在的价值在于其算法能力,是否能超过高通或MTK。
DPU –> PW -> DDIC
3.4【Display Interface】
东西很多,不再一一列举(后面专门讲下mipi),可见未来的发展还在于XR。

4. 总结
DPU分为4部分,功能已经比较稳定:其中显示后处理是以后升级的重点(其中超分与锐化又是优化的重点),同样的功能,相比独立显示芯片PW或DDIC去做有更好的功耗;
XR会极大左右DPU的发展:无论是分辨率带来的带宽压力,还是最新的注视点传输这样的技术,都需要DPU做出较大改变。