ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android图形栈核心:SurfaceFlinger与HWC硬件合成全解析

Android图形栈核心:SurfaceFlinger与HWC硬件合成全解析 做Android系统底层优化这几年我折腾最多的模块就是HWCHardware Composer硬件合成器。很多刚接触图形栈的同事会问我SurfaceFlinger不就是做合成的吗为什么还要单独搞一个HWC等真到了调显示性能、追掉帧、改平台适配的时候他们才发现这条链路远不是SurfaceFlinger合成一帧发给屏幕那么简单。这篇文章我把从SurfaceFlinger到显示驱动的完整流程重新梳理了一遍重点放在合成决策、HWC2.x接口交互、DRM/KMS驱动侧实现和调试手段上希望能帮到正在啃Android图形栈的伙伴。1. SurfaceFlinger、Layer、Display与HWC设备先理清谁在干什么1.1 SurfaceFlinger不是唯一的合成者SurfaceFlinger在Android系统里的地位类似所有窗口的总调度台。每个App往屏幕画内容时并不是直接写显存而是通过Surface/SurfaceControl向SurfaceFlinger注册一个Layer真正的内容由App侧的BufferQueue生产出来再交给SurfaceFlinger统一管理。这一层很多人熟悉但容易忽略一个事实SurfaceFlinger自身并不一定执行最终的合成操作它是一个图层组织和合成决策的中枢真正动手干活的有两拨——GPUOpenGLES/客户端合成和HWC硬件合成器。这一点是我带新人时最想让他们先建立的认知。SurfaceFlinger在Vsync到来时会收集当前所有可见Layer依据Z序、透明度、裁剪、变换等信息构建一个合成计划。这个计划可以是全GPU合成把所有Layer合帧到一块buffer里、全HWC合成直接把多块layer buffer交给显示控制器叠加也可以是混合的。至于走哪条路取决于硬件能力和性能策略。1.2 Layer、Display和HWC设备模型在SurfaceFlinger的世界里Layer不是一个display buffer那么简单它表示了一个窗口的呈现状态SourceCrop源裁剪、DisplayFrame目标位置、Transform旋转、BlendMode混合模式、Alpha、Z序等。HWC设备则对应真实的显示硬件或虚拟显示设备。SurfaceFlinger通过HWC2接口把我想在Display上显示这些Layer的诉求下发给HWC HALHWC HAL根据底层显示控制器的能力决定哪几个Layer我可以负责叠加哪几个我搞不定你得先用GPU合好再给我。一个容易混淆的点是Display的概念除了物理主屏还有副屏、HDMI/DP外接屏以及面向屏幕录制和无线投屏的VirtualDisplay。每路Display都会走一遍HWC的DPFDisplay Pipeline Flow。屏幕录制场景里VirtualDisplay往往由GPU或HWC的virtual display能力输出底层机制和物理屏不同这在做多屏投屏时是高频坑。1.3 一帧画面的完整旅程我把一帧典型画面的流程压缩成下面这条链App通过dequeueBuffer拿到GraphicBuffer绘制完成后queueBuffer。SurfaceFlinger在Vsync打断中醒来遍历每个Display上的Layer列表检查哪些Layer有新的buffer。SurfaceFlinger对Layer做可见性、遮挡、裁剪计算形成CompositionRequest。HWC HAL进入validate阶段对请求做能力检查返回每个Layer的合成方式建议。SurfaceFlinger把仍需client合成的Layer交给GPU完成后把结果buffer连同所有参数再提交给HWC。HWC commit最终调用DRM/KMS或私有显示驱动把内容送到显示器。这条链路里App到SurfaceFlinger这一段大家都写得比较多真正难懂也难调试的是后四步。下面我逐段拆开讲。2. 为什么不能全交给GPU硬件合成器的存在逻辑2.1 GPU合成是通吃方案但代价不小如果完全由GPU合成SurfaceFlinger会把所有可见Layer当作纹理通过OpenGLES画到一块与屏幕尺寸相同的buffer上再调用显示驱动整帧刷出去。这个方案逻辑上最简单几乎什么Layer都能处理——旋转、缩放、混色、圆角、阴影没有显示控制器挑剔的说法。但代价也非常直接。以一块1080p或2K屏为例GPU每帧都要对所有Layer执行一次全屏尺寸的纹理绘制像素填充率fillrate和内存带宽消耗非常夸张。屏幕上同时开着视频播放器、弹幕、底部导航栏的时候等于把三四个图层的内容全部重新tile一遍。CPU/GPU负载上去机器跟着发热续航肉眼可见地掉。我实测过一款中端平台在纯GPU合成下刷高刷屏GPU占用能飙到60%以上功耗比HWC路径多出1W到1.5W是很正常的。2.2 显示控制器天生就是多图层叠加器这里要先讲一个硬件常识现代显示控制器DPU/Display Controller本身就是一个多平面混合器。它内部有多条overlay plane也叫DMA pipe、图层平面每个plane可以独立抓取一块内存里的图像数据在扫描输出到屏幕时按顺序叠加plane之间支持alpha混合。也就是说多图层合成这件事显示硬件早就内置支持了根本不占GPU资源。HWC的核心匹配逻辑就在于把你应用程序的Layer和硬件overlay plane一一对应起来让显示器在扫描每一行像素时自动从几个plane同时取数据做叠加。GPU从头到尾只需要负责那些硬件plane吃不掉的层比如格式不支持、旋转角度太怪、layer数量超过plane数其余全走HWC直通。2.3 为合成分锅背后的性能账整个合成决策本质是一道优化题把所有Layer按平面资源和处理能力分配到HWC路径和GPU路径让总带宽和功耗最小。HWC HAL在validate时拿到每个Layer的buffer格式、变换、缩放、混合需求后会逐项和底层DPU的plane能力比对最终给出每个Layer的合成方式建议DEVICE还是CLIENT。我习惯把这条决策逻辑叫做自助餐模型GPU合成相当于中央厨房把所有配菜做成一份炖菜端出来客人只能吃同一盘HWC则像自助餐台每个餐位放一种菜屏幕这个食客经过时自己把喜欢的都夹上。自助餐要想成立前提是餐位overlay plane够用且每道菜layer buffer格式和摆放方式变换、缩放、混合都符合餐台的规格。超过餐位数量或规格不符的菜还是得退回中央厨房预处理。对普通用户而言最直观的体验就是开弹幕直播时弹幕层如果走了GPU合成整机功耗会明显上升弹幕滚动时帧率还可能不稳而弹幕层若能走HWCGPU几乎没动静滚动和画面稳定得多。3. HWC2.x接口逐段拆解从validate到present的握手细节3.1 为什么HWC2.x设计了validate这个预检步骤HWC1.5及更早年代的接口是无状态式的每帧调用一次perform把所有Layer属性和buffer一次性塞给HAL让HAL直接开干。HWC2.x改成了带状态的两段式交互SurfaceFlinger先把Layer创建出来并设置好全套属性然后调用validateDisplay。这一步不是白给的它给了HWC HAL一个静态检查和资源预分的机会HAL需要扫描所有Layer判断自身是否有足够的plane支持它们然后返回一个合成的建议。validate单独存在的一个深层原因是硬件资源分配不能轻率。DPU的overlay plane数量是有限的必须统筹全链路的plane占用情况如果HWC HAL不做预检等到真正commit时才发现有Layer无法支持SurfaceFlinger这一帧就已经白跑了。validate阶段返回的合成建议让SurfaceFlinger有机会做二次调整——比如把少量Layer先拉去GPU合并腾出plane给关键图层然后再commit。3.2 Layer状态与关键属性字段HWC2里Layer是一个有状态的对象SurfaceFlinger对它的设置非常讲究核心属性包括buffer实际图像数据通常是一块GraphicBuffer映射的dma-buf。compositionType请求的合成方式HWC2里默认值为DEVICE支持DEVICE/CLIENT/CURSOR/SIDEBAND四种。displayFrame该Layer在目标显示区域上的矩形位置。sourceCrop从源buffer里取哪一块矩形区域。transform旋转和翻转角度0/90/180/270HAL感应的ROT_0等。blendModeNONE/PREMULTIPLIED/COVERAGE决定alpha混合方式。planeAlpha整层透明度很多显示控制器原生支持走HWC非常划算。zOrder在Display上的层级序决定叠加关系。这些字段里最容易出坑的是transform。显示控制器能处理的旋转角度集合有限有的平台plane只支持ROT_0和ROT_90遇到ROT_270就要么整层回退GPU要么驱动自己再做一次交换。这一点后面我会讲踩坑。3.3 validate、commit、present三步走的完整时序我按实际调用顺序把SurfaceFlinger与HWC HAL的交互串一下SurfaceFlinger调用HWC2::Display::createLayer按当前图层列表创建匹配的Layer对象。逐个Layer调用setLayerBuffer、setLayerDisplayFrame、setLayerSourceCrop、setLayerTransform等接口填充状态。调用validateDisplayHWC HAL对照当前硬件与plane资源做一次分配检查。validateDisplay返回后SurfaceFlinger读取getChangedCompositionTypes拿到HWC建议每个Layer用DEVICE还是CLIENT。凡是建议CLIENT的LayerSurfaceFlinger会标出来准备交给GPU的client合成流程。如果有CLIENT层SurfaceFlinger会用GPU把这几层合帧到一块新buffer然后重新把这个合并结果作为DEVICE层设置回去。调用commitHWC HAL锁存全部Layer设置进行plane分配、buffer引用和fence登记。调用presentDisplayHWC HAL向显示硬件发起真正的刷新操作同时返回一个present fence。GPU或App侧通过releaseFence感知该buffer已被读取完成才能安全回收和复用。这套三步走的本质是把能不能这么合和就这么合两件事拆开了中间留一个纠正窗口。我调试时经常使用看线程名找阶段的办法SurfaceFlinger在主线程里做Layer整理和validate在做client合成时会在GPU线程执行draw调用最后的present其实很快如果present本身耗时异常问题往往出在驱动侧而不是纯SF侧。3.4 Fence流转合成链路的隐形血脉HWC链路里fence是最容易被忽略的。acquire fence告诉HWC这块buffer的GPU/编解码写入已经完成你可以拿来出图了release fence则告诉SurfaceFlinger显示硬件已经读完buffer数据可以回收了。很多显示花屏、帧率抖动的问题追溯到最后都是fence等待超时或者用错了fence。我举个典型场景视频播放器的解码输出buffer直接到SurfaceView如果release fence没有正确传给解码器解码器会以为buffer一直没被读完等待超时后才复用表现出来就是画面间歇卡顿每卡一次大约几毫秒到几十毫秒。排查这种问题优先看dumpsys SurfaceFlinger里layer的lastReleaseFence状态再配合systrace看fence signal是否延迟。4. 显示驱动侧的关键实现drm_hwcomposer与atomic commit4.1 DRM/KMS里plane、CRTC、Encoder、Connector的角色HWC HAL下面真正跟物理面板打交道的是显示驱动Android生态里最主流的标准接入层是drm_hwcomposer它实现了HWC2.x抽象底层走内核DRM/KMS。DRM/KMS把显示硬件抽象成四个角色CRTC显示控制器核心负责扫描输出和时序生成。Encoder信号编码器把CRTC输出的像素信号转换成DSI/eDP/HDMI/DP等物理协议。Connector物理连接器代表一块真实的屏幕或接口。Plane显存平面就是前面说的overlay plane。drm_hwcomposer把HWC的DEVICE层一一映射成DRM Plane。这里建议把DRM Plane的理解提升到硬件资源的级别它不止是一块内存背后绑定着DPU的抓取、缩放、混合、颜色转换通道。drm_hwcomposer在客户端HWC HAL侧维护一个plane资源池每个Layer过来时按照格式/颜色空间/缩放系数/旋转等条件筛选可用plane匹配成功后把layer buffer导入到该plane里。4.2 从GraphicBuffer到DMA-BUF再到drmModeAddFB2App侧交上来的GraphicBuffer是一块由Gralloc分配的图形内存。HWC这层要把它交给DRM plane使用需要经过两步通过Gralloc拿到buffer对应的dma-buf fd。在DRM侧调用drmModeAddFB2把dma-buf封装成一块可以在KMS层面上使用的framebuffer对象并附带格式、宽高、修饰符等元数据。这一步如果格式不匹配比如YUV格式不同、用了DRM不认识的tiling修饰符就会直接失败最终这个layer只能回退CLIENT合成。所以我在对接自研驱动时第一件事就是核对gralloc分配的内存格式与DRM plane支持的格式列表两者交集越大HWC能直通的图层比例越高。我记得到过一个具体案例某平台对NV12的plane支持只列了NV12线性格式而gralloc默认分配的是tiled压缩格式结果视频层永远到不了DRM plane勉强走GPU解码合成整机功耗居高不下。后来把gralloc的format modifier与DRM plane能力对齐视频层立刻走通了HWC路径。4.3 atomic commit一次提交多个plane状态drm_hwcomposer会把一个Display上所有plane的目标状态framebuffer fd、dst坐标、src坐标、rotation、alpha等打包进一个drmModeAtomicReq然后调用drmModeAtomicCommit一次性提交到内核。内核会校验atomic状态校验通过后触发一次page flip。这里的关键意义在于打包提交DPU在切换plane配置时各plane要尽量在同一帧边界更新避免出现撕裂或中间态。这个过程中还有一个很底层的概念——vblank。page flip完成时内核会生成vblank事件drm_hwcomposer把它作为vsync信号回灌给SurfaceFlinger驱动整个图形流水线的节奏。如果vblank事件频率与面板刷新率不对齐后面的合成节奏会乱。4.4 为什么Plan分配策略直接影响性能drm_hwcomposer的resource manager并不简单。我见过新手直接把所有Layer按顺序依次分配plane结果把支持旋转的高性能plane先给了静态图层等视频层需要旋转时没有合适plane整层退回GPU性能崩了。真正合理的做法需要做一次成本优先的匹配旋转/缩放需求量大的层优先匹配能力强的plane静态UI层可以退而求其次匹配基础plane。这个分配细节在图层数量接近plane数量上限时几乎决定了HWC路径是否还能继续保持。5. 显示问题三板斧dumpsys、HWC调试日志与systrace验证5.1 先看dumpsys SurfaceFlinger找到每个Layer的真实合成路径排查显示问题我很少直接改代码第一件事永远是连上设备执行dumpsys SurfaceFlinger。在输出里重点关注每个Layer的composition type信息下面是一段我经常在测试机上观察的示意输出// 节选实际字段因Android版本略有差异 Layer 0x1234 (com.android.systemui) composition type: DEVICE - 走HWC requested type: DEVICE buffer: 1080x2400 transform: ROT_0 blend: PREMULTIPLIED hasFence: true Layer 0x5678 (com.example.video) composition type: CLIENT - 走GPU合成 requested type: DEVICE buffer: 3840x2160 transform: ROT_0看到CLIENT比imagined多的情况就说明HWC能力没有被充分利用。常见的触发原因plane数量不够、layer格式不支持、旋转角度不被plane接受、alpha模式兼容性不足、或者是HAL里enablePlaneRatio参数配置太低。这一招能直接在布局层面缩小问题范围比闷头改代码高效得多。5.2 打开HWC调试日志与关键调试propdrm_hwcomposer和私有HWC实现都支持调试日志。通常可以通过设置System Property打开不同级别的日志比如关闭HWC强制让所有图层走GPUadb shell setprop debug.sf.disable_hwc 1 adb shell stop adb shell start这个开关在对比是不是HWC路径导致的显示问题时非常好用。如果关闭HWC后问题消失基本可以断定是HWC/驱动路径出的问题如果问题依旧就要往SurfaceFlinger或应用侧查。另外dumpsys SurfaceFlinger里的显示帧率、present统计、GL composition次数和systrace/perfetto里SurfaceFlinger、HWC线程的耗时都是定位帧率抖动的关键证据。5.3 实战排查弹幕层导致整屏回退GPU的案例我调过一个弹幕直播类App的发热问题现象是弹幕多时整机功耗异常弹幕稀少时功耗正常。用dumpsys SurfaceFlinger查看后发现那个App的弹幕层被标记为CLIENT原因是弹幕View是一个普通的SurfaceView但不带硬件平面属性SurfaceFlinger强制要求它走客户端合成。弹幕层一上屏视频播放层和弹幕层被GPU合帧等于每帧都做了一次全屏高清纹理绘制。定位后用两种方式解决在应用侧把弹幕层改为TextureView并允许硬件加速让系统识别为普通Layer或者在HWC HAL侧扩展一个独立类型的layer支持使弹幕层也能作为overlay plane直通。走通后dumpsys显示弹幕层变成DEVICEGPU占用直接掉了一半。这类问题的核心方法依旧是先用dumpsys找到合成路径再用systrace抓GPU负载数据验证优化效果。6. 四个我踩过的坑HWC实战中的典型翻车现场6.1 坑一修改Layer属性后没有触发revalidate画面纹丝不动有一次我为了验证某个图层走HWC是否稳定在SurfaceFlinger里改了Layer的alpha值但屏幕毫无变化。查了一圈发现HWC2.x下不是所有Layer属性变化都会自动触发重新validate很多属性需要在SurfaceFlinger侧通过setTransactionFlags设置LAYER_NEEDS_REVALIDATE后才会上报HWC。尤其是动态修改scale、transform这类非buffer字段非常容易被忽略。6.2 坑二DUMB buffer分配方式伪装成placeholder黑屏但CPU/GPU正常运行做芯片平台驱动适配时我遇到过一次显示黑屏但日志显示SurfaceFlinger、HWC都在正常干活图层也都标记为DEVICE。最后抓DRM侧发现framebuffer创建失败根源是drmModeAddFB2用了一个不支持的modifier驱动返回失败plane没有拿到有效buffer。这种错误type不会立刻崩只会以黑屏或画面残留出现。排查经验是只要全链路日志正常但屏幕异常第一时间到dmesg抓DRM报错。6.3 坑三present fence超时帧率被锁到原来的一半调高刷新率特性时我遇到过整机帧率稳定下跌到面板刷新率一半的情况。systrace里看presentDisplay耗时异常SurfaceFlinger主线程被阻塞等fence。最后追到是驱动在处理drmModeAtomicCommit时等待一个缓冲区的acquire fence超时而这个buffer之前是给GPU client合成用的HWC收的时候GPU尚未画完fence没有按时signal。解决办法是把client合成完成后的release fence正确传递到HWC的acquire fence保证HWC不会提前锁住一个未写完的buffer。6.4 坑四多屏与热插拔场景下plane资源分配被主屏占满做副屏/HDMI扩展显示时我发现副屏的画面基本全走CLIENT合成副屏一开整机GPU负载就升。原因是drm_hwcomposer的plane资源管理在热插拔事件发生后没有及时重分配大部分可用plane还挂在主屏的静态UI层上。修改资源管理策略在HOTPLUG事件里对全部display做一次plane重新规划后副屏也拿到了直通planeGPU负载降下来。这类问题在车载多屏和高端平板项目里很常见调试时一定要带着plane是有限资源的视角去看日志。最后再分享一条经验HWC这条链路每一层的接口看起来都不复杂但真出问题时故障可能发生在任何一层。我现在排查习惯是dumpsys先定性perfetto再定量最后才翻驱动代码。按这个顺序大多数显示问题都能在两轮内定位。
返回列表