首页 / 资讯中心 / 文章详情

Android P HWC2硬件合成深度解析:SurfaceFlinger显示性能优化实战

Android P HWC2硬件合成深度解析:SurfaceFlinger显示性能优化实战 ★ FEATURED ARTICLE
开机第一帧出来之前整个系统其实都在忙一件事把一堆图层叠成一张图。而这件事做得快不快、稳不稳直接决定了你滑动列表跟不跟手也决定了外卖App里那张图片滚起来是不是一卡一卡的。Android P上这套流程的核心就是HAL层的HWC2硬件合成器。这篇文章我想把HWC2从设计思路到实际调用流程拿出来聊聊重点筛出那些源码和文档里讲得绕、但实际开发中绕不开的东西。这个系列是图形显示方向的第一篇我们先聚焦硬件合成。适合做系统开发、Framework移植、显示驱动调试的朋友阅读尤其是你在Android 8.0以后遇到过HWC初始化失败、合成策略不对导致GPU负载飙升、外接屏/虚拟屏显示异常这些问题这篇应该能帮你理清头绪。1. HWC2在整个显示体系里处于什么位置1.1 SurfaceFlinger的“最后一道搬运工”先抛开代码想想显示链路。App画好了一张图交给SurfaceFlinger统一管理但SurfaceFlinger并不认识屏幕它只认识Buffer。真正把Buffer变成屏幕上像素的人是Kernel空间的Display Driver而连接SurfaceFlinger和Display Driver之间的桥梁就是Hardware Composer HAL。HWC2就是这套HAL在Android 8.0之后的新版本。它干的事很纯粹SurfaceFlinger把一堆Layer丢给它说“你看怎么合成性价比最高”HWC2根据硬件能力来决定——哪些Layer可以叠在一起让Display硬件去合成哪些Layer因为格式、旋转、透明关系等原因必须交给GPU合成。这个决策是整个合成性能的核心。别小看这个“决定”它直接关系到你跟手度。SurfaceFlinger如果自己埋头让GPU把所有图层都合成一遍一来GPU负载高二来多一次Buffer拷贝延迟就上来了。HWC存在的意义就是尽量让“合成”这件事发生在Display Controller里代价最小路径最短。1.2 HWC1.x和HWC2差在哪很多老工程师是从HWC 1.x时代过来的那时接口长这样一个hwc_composer_device_1结构体里面一对回调函数SurfaceFlinger调prepare、set然后就没然后了。1.x最大的问题在于Display和Layer的建模太弱扩展虚拟显示、多Display时大家都靠hack。HWC2的改动是伤筋动骨的。它不是打补丁而是把整个模型重写了。HWC2引进了一整套HIDL接口跑在binderized HAL的框架下不再像1.x那样通过shared library直接链接进SurfaceFlinger进程。这带来的好处很明显进程隔离厂商实现crash不至于把system_server带走HAL可以独立重启整个接口用HIDL描述后前后向兼容性也更好管了。对比起来HWC2最大的标签是三个逻辑模型Device、Display、Layer。这三个模型不直接对应硬件而是抽象层。一个Device代表一块物理GPU/DPU一个Display代表一个屏幕输出一个Layer就是SurfaceFlinger送下来的一个图层。我见过不少同事第一次看HWC2代码时懵的就是因为它突然冒出好多回调什么Hotplug、Vsync、Refresh还有一堆CommandBuffer风格的调用。这些都是SurfaceFlinger和HWC之间交互的协议搞懂了它们整个框架就串起来了。2. HWC2的核心设计Device、Display和Layer三层模型2.1 Device不是“设备”是“能力集合”先澄清一个容易绕晕的名词。HWC2中的Device英文注释叫“a physical device that can generate frames”但它并不是你理解的那块屏幕也不是主板上某个芯片物理单元。它是一个逻辑概念描述一个能接收Layer并输出Frame的硬件通路。在Android P的代码里Device对应着HWC2::Device这个C封装类内部持有spIComposer通过HIDL跟vendor实现打交道。厂商侧实现的是android.hardware.graphics.composer2.1::IComposer它内部管理多个Display。Device有三个模式Device、FrameBuffer、Virtual。Device模式就是正常输出到屏幕FrameBuffer模式是把合成结果写到内存Buffer里Virtual模式则是输出到虚拟显示比如投屏、录屏。很多虚拟显示问题查不到原因其实就是没搞懂这三个模式的差异。2.2 Display是输出的“画布”Display在HWC2里对应一个可以被点亮、有分辨率、能输出图像的目标。物理屏幕是一个Display虚拟显示也是一个Display。区别在于物理Display的Mode有限面板时序由驱动和屏参决定虚拟Display没有固定ModeBuffer来了就能写。Android P里SurfaceFlinger通过spHWC2::Display管理这些Display。每个Display都有自己的Id在HWC1.x时代根本没有这个Id概念导致多屏时代大家靠猜。HWC2用DisplayId把多屏管理正规化了。每个Display又关联了一堆属性比如宽高、Vsync周期、SupportedModes、ColorMode。你在SurfaceFlinger的dumpsys里看到一堆“Display 0”的块就是这些属性的运行时快照。2.3 Layer合成的最小决策单元Layer这个概念最直观就是SurfaceFlinger传下来的一个图层。但它有个极容易混淆的点这个Layer跟WindowManager那边说的Window不是一回事跟SurfaceFlinger内部的Layer也不是同一个对象。HWC2里的Layer是HWC侧的“视图层”只包含合成需要的信息Buffer、Transform、Blend模式、Z序、DisplayFrame、SourceCrop等等。SurfaceFlinger那边的Layer经过一层映射把状态拷出来组成了一个HWC2::Layer送到HWC去校验。这个“校验”就是HWC2的精髓。厂商拿到Layer说“我能合成”SurfaceFlinger才放心地把合成任务交出去如果厂商说“这个我合不了”SurfaceFlinger就得灰溜溜地把Layer拿来自己用GPU合。关键点是Layer带了个Composition状态机。刚开始是Client意思是“默认我GPU合”HWC如果能接手就改成Device改不了的维持Client。每次Frame经过一次validate/present循环这个状态才真正确定。2.4 合成策略为什么“谁合成”是个大问题合成策略是HWC的灵魂直接决定性能。Display Controller能做的合成叫硬件合成Device CompositionGPU做的合成是客户端合成Client Composition还有一种叫Display合成Display Composition常见于Exynos平台的DECON它是在Display扫描到物理屏幕前最后一刻把多个Layer直接叠加。从原理上讲硬件合成的效率远高于GPU合成。GPU合成需要把多个Buffer读进GPU算好再写回一块新Buffer再交扫描。硬件合成则是Display Controller在扫描输出时直接读取多个Buffer在输出端叠加。少了读写和内存拷贝延迟和功耗都下来了。厂商的HWC实现里合成策略一般是一个决策函数。它会逐个Layer检查Buffer格式是不是硬件支持是不是旋转了有没有Alpha通道是不是算过颜色转换任何一项不符就打回Client。我个人的经验是Debug合成策略问题最好的突破口就是dumpsys SurfaceFlinger里那行composition它会显示每个Layer最终用的是哪种合成方式。如果发现大面积跑Client说明HWC判断太保守或者Layer属性有什么东西让HWC不敢接。3. 从SurfaceFlinger到Composer HAL的完整调用链3.1 关键类与HIDL接口的对应关系Android P的代码里SurfaceFlinger是通过HWComposer这个类去和HWC2打交道的。这个类位于services/surfaceflinger/DisplayHardware/HWComposer.cpp内部持有一堆HWC2::Device、HWC2::Display、HWC2::Layer的智能指针。Classes的关系大致是HWComposerSurfaceFlinger的专用门面负责创建、注册回调、管理Display。HWC2::Device封装IComposer HAL接口所有Display操作都是通过它往下走的。HWC2::Display封装IComposerClient的Display接口。HWC2::Layer封装Layer级操作对应HIDL里acceptDisplayChanges、setLayerXXX那一堆。HIDL侧的核心接口是IComposer和IComposerClient。IComposer管HAL初始化IComposerClient管Display和Layer操作。Android 9上这些接口在2.1版本对比8.0的2.0增加了setPowerMode_2_1和若干显示模式控制能力。真正的HAL实现一般在/hardware/interfaces/graphics/composer/2.1/default/或厂商的vendor/xxx/hardware/display/里。大多数厂商不会直接用default实现而是基于自己的DPU驱动封装一层。3.2 SurfaceFlinger初始化HWC的过程看代码顺序SurfaceFlinger启动时会走HWComposer::init。这个init做几件事通过getComposer()拿到IComposer HAL服务。通过createDisplay创建物理Display。注册回调客户端。设置Vsync权限。其中容易出问题的点是createDisplay。物理Display的Id是HWComposer从HAL侧动态拿到的不一定是0。如果你在厂商实现里写死DisplayId0后果是SurfaceFlinger这边动态发现的时候跟你对不上导致合成时找不到Display。正常情况下SurfaceFlinger创建的是HWC_DISPLAY_PRIMARY主屏它内部按逻辑DisplayId查表映射到HWC2的DisplayId。HWC1.x时代这个映射是固定的HWC2之后变成了查表所以初始化阶段的顺序特别重要。3.3 Present主流程从validate到presentDisplay每次刷新SurfaceFlinger在doComposition里会走一个标准的HWC2流程。拆开看是这样SurfaceFlinger把各Layer状态同步到HWC2::Layer。调用validateDisplay。HWC解析所有Layer内部判断合成策略。SurfaceFlinger调acceptDisplayChanges接受结果。调用presentDisplay让HWC把合成结果送出去。present返回后处理ReleaseFence通知各BufferProducer可以回收Buffer。这套流程里最有意思的是validate和present分成两步。为什么不能一步到位因为HWC合成前需要先把Layer的Buffer“锁住”防止SurfaceFlinger还在写。validate的目的就是让厂商有机会说“这些Layer我合不了”SurfaceFlinger听到后先把GPU合成的部分处理掉再重新走一轮validate最后才present。Android P上一个Frame最多允许两次validate。第一次全Client第二次HWC尽可能接手。如果两次过后还有Layer没被接受就只能维持Client合成性能会受影响。我调试时见过有些厂商的HWC实现在validate里不做任何判断全部返回Device合成结果是一些奇怪的格式比如P010、RGBA1010102跑得乱七八糟。这种问题一查一个准validate里断点打上看stopLayerRequest。3.4 HWC2回调Hotplug、Vsync、RefreshSurfaceFlinger跟HWC2之间不是一个单纯的“你调用我”的模式HWC2还可以主动通知。三个重要的回调Hotplug回调处理屏幕热插拔。手机上的副屏、Type-C转HDMI、电视盒子的HDMI接入都靠这个。回调带了DisplayId和连接状态SurfaceFlinger收到后动态添加或移除Display。Vsync回调是刷新的心跳。HWC实现里一般是DPU的TE信号触发的。Android P上回调里会带一个timestampSurfaceFlinger用它来推算帧时间。Vsync回调没对齐就会导致动画掉帧或GPU渲染和显示错拍。Refresh回调的语义是“你给我赶紧再刷一帧”一般出现在Mode切换、ColorTransform变化等场景。如果HWC实现里忘记发Refresh外接屏切换分辨率后屏幕上会一直是花屏或者黑屏直到下一次手动触发刷新。回调线程是独立在HAL实现里的你在调试时经常能看到两个线程交替打日志一个是SurfaceFlinger主循环在调用一个是vender回调在报事件。这两个线程如果同步没做好最容易出问题的是Display power时序和Vsync回调同时到来造成SurfaceFlinger访问一个已经释放的Display。4. Android P上HWC2的适配关键点与源码视角4.1 从HWC1.x迁移到HWC2厂商要改什么这节对做平台移植的兄弟有点价值。从HWC1.x迁到HWC2并不是改改函数签名那么简单。底层数据结构全变了底层调用方式从直接调用变成HIDL RPC。在HIDL架构下vendor进程和SurfaceFlinger进程分开跑。厂商HAL实现在自己进程里初始化DPU、注册中断、管理Fence。SurfaceFlinger那边的HWComposer对象只是个proxy。通信走binder传输LayerBuffer时通过共享内存传递BufferHandle。所以迁移的技术点首先是写HIDL服务。可以参考AOSP提供的Composer haldefault实现但真正的难点在你的DPU驱动怎么通过HIDL暴露出来。其次是BufferHandle的传递。HWC1.x里buffer_handle_t直接当一个指针传。HWC2里这个handle要通过HIDL的native_handle打包Binder传输大Buffer时还可能触发Fd映射。这个过程中最容易出现文件描述符泄漏导致内存逐步涨上去最后整机卡死。另外还涉及Fence的传递。HWC2里每个Buffer都带 acquire fence 和 release fenceHIDL层的Fence是int类型的文件描述符。一方要负责close一方只要dup。这里最忌的是多写一份或者少写一份。少closefd泄漏多close其他进程拿着失效的fd去wait直接炸。4.2 ReleaseFence和PresentFence的正确处理姿势Fence问题值得单独说因为它是显示系统里最难排查的bug类别之一。每个Layer在HWC合成前都需要等它的acquire fence信号。acquire fence一般来自ProducerCPU/GPU渲染完成。HWC拿到后要sync_wait或者 async wait。sync_wait是阻塞的HWC线程等fence会阻塞后续Display导致整个合成流程拉长。更合理的是async wait注册一个callbackfence signaled后再继续。很多显示卡顿问题其实是HWC实现在fence等待上用了阻塞等待一旦某一层GPU渲染慢整个显示管线都卡住。present之后的PresentFence则用于标记这一帧真正被扫描到屏幕上的时间。SurfaceFlinger在advanceFrame时读这个时间戳用于计算帧率、jank统计。可以说这个fence打不准你在Perfetto上看到的帧时间就会骗人。我推荐的做法是能在HAL层用硬件支持的时间戳比如DPU的tear interrupt时间就用硬件时间戳不要拿CPU时间去凑。CPU时间跟屏上实际扫描时刻有几十毫秒误差平时无感一旦做A/B帧率对比就会被坑到。4.3 CompositionStrategy的调整经验有些场景下HWC的合成策略和实际画面要求不匹配需要上层干预。最常见的是Video播放留黑边。视频层的Transform特殊有的DPU支持旋转但没做反交错。HWC判断不出来视频是否支持会倾向交给GPU但GPU合成会掉帧。此时可以通过在Framework层给Video层加一个特殊LayerFlag或者修改HWC的进程白名单来强制走Device合成。还有一种是低内存机器GPU非常弱。Client合成一多GPU带宽直接占满整机发热、掉电。这种场景就需要HWC尽量让所有Layer都走Device合成即使性能风险也不给GPU添负担。AOSP的MaxClientCompositionCacheSize等参数就是用来调这类行为的。P上还有一个坑有些屏幕是single-display但VirtualDisplay一直在后台跑录屏此时HWC有的实现会把所有Layer都丢给Client导致正常屏幕也开始卡。这种多半是HWC没区分好物理Display和VirtualDisplay的能力边界。5. 稳定的排障路径怎么定位HWC2相关的问题5.1 先用dumpsys建立全局认知遇到显示异常我第一步永远是adb shell dumpsys SurfaceFlinger先看当前合成链路状态。重点看几个字段mHwcDisplayId是不是正确映射到了物理屏composition每一层的合成方式mClientComposition有没有异常增长presentFence是否持续输出hasFrame的状态如果dumpsys里能看到每个Display的supportedModes说明HWC初始化正常如果这块是空的多半HAL服务没起来或者SurfaceFlinger和HWC的HIDL连接断了。dumpsys SurfaceFlinger --list可以看所有Layer配合--latency拿帧时间。这套组合拳基本能覆盖80%的画面卡顿、刷新率异常问题。5.2 实战问题一外接屏无显示典型的症状是HDMI插上后外接屏黑屏dumpsys里能看到ExternalDisplay存在但mode是0x0。排查顺序是先看Hotplug有没有来再判断mode设置有没有生效最后看buffer有没有送过去。热点问题往往是Hotplug到了但mode没设置HWC认为外部屏不支持当前分辨率。另一个隐蔽点是外接屏一般走的HWC_DISPLAY_EXTERNAL但有些厂商的Display索引不是0或1而是动态分配的。如果在HAL层写死了外部屏的DisplayId换屏后就会新旧DisplayId不一致显示永远不对。5.3 实战问题二画面撕裂和掉帧画面撕裂本质是multiple Buffer扫描时Buffer切换点没对齐Vsync。HWC层看是present调用落在了Vsync周期中间。排查时先确认Vsync的精度。dumpsys SurfaceFlinger里的Vsync时间戳是不是规律抖动。如果抖动大多半是HWC的Vsync timestamp取错了源或者取的是CPU时间不是硬件TE时间。如果Vsync正常但依旧撕裂就要看PresentFence跟Vsync的相位。有的HWC实现里present返回太晚导致Display已经扫到屏幕中部才切换Buffer。这个在代码上怎么查拉两张连续Frame的present时间戳看差值是否稳定在Vsync周期倍数。5.4 实战问题三SurfaceFlinger和HWC通信异常常见的HIDL错误是TransportError和DeadObject。前者多半是传输的BufferHandle过大或者Binder fd数量到极限。后者是HAL服务崩溃了SystemServer可能把SurfaceFlinger也一起重启。这种问题在HAL侧要看logcat有没有libc Fatal有时候vendor端的crash不会直接打到logcat需要抓tombstone。Android P上检查/data/tombstones/里的crash顺手把vendor进程的backtrace捞出来。通信异常还有一种情况HAL服务的SELinux权限被收紧SurfaceFlinger serve客户端访问不了vendor composer服务。这时logcat里会出现avc: denied关键字。这种问题通常是在CTS测试后暴露出来往往是因为之前的te规则写得太宽。5.5 常见问题排查速查表症状首选排查命令常见根因外接屏黑屏dumpsys SurfaceFlinger 看ExternalDisplay状态Hotplug/mode配置未生效画面撕裂dumpsys SurfaceFlinger --latencyPresentFence相位错位/Vsync抖动滑动掉帧Perfetto抓kSwapBuffersClient合成占比过高部分界面花屏dumpsys SurfaceFlinger 看composition图层格式/旋转导致合成策略反转HAL服务反复崩溃tombstone logcatFence/Handle泄漏SELinux拒绝logcat搜avcte规则缺失虚拟显示录屏黑帧SurfaceFlinger dumpsys readbackVirtualDisplay的FrameBuffer模式未实现6. 实操中容易踩的坑和一点个人建议这块是我自己的血泪经验。HWC2的问题不像上层应用bug那么好复现往往要挂机跑很久才出现一次。第一个坑Fence传递路径中忘了对fd重复使用做防护。很多Buffer是带cache的同一份handle会被多个Display引用。如果HWC的releaseFence处理时把fd给close了另一个Display再引用时就会拿到一个无效的fd然后在sync_wait时直接抛异常。这个问题排查起来很痛苦因为不是每次都crash和内存重用的时序强相关。第二个坑HAL服务进程不要用上文讲的sync_wait阻塞方式等所有fence。Android P之后SurfaceFlinger对HWC的响应时间有了更严格的要求如果HWC阻塞超过某个阈值SF会放弃该帧并打Choreographer掉帧。要改成异步fence callback或者至少把阻塞放在专用线程而不是调用线程上。第三个坑权限问题。HWC2 HIDL服务要用独立SELinux domain不能跟着SurfaceFlinger的domain走。domain隔离不做好HAL服务起来后没权限访问DPU寄存器日志却可能只有一条打印。第四个坑要尽早引入Perfetto抓trace的习惯。HWC2相关的debugger在P上已经有了专门categorygfx.hwc、gfx.sf。把这些category打开后Perfetto能直接看到HWC的validate/present耗时分布以及fence wait的耗时。最后给个我个人的模式每次setPowerMode切换后HWC2的Display状态机常会残留上一轮mode的历史状态导致下次present时用旧mode参数。稳妥做法是在setPowerMode里不要只更新电源状态还要把当前mode重新下发一次顺便更新modeId。这个方法在我调过的好几个平台上都有奇效。HWC2这套东西说难也难模型抽象多、调用路径长、还涉及进程间通信但理解了Device/Display/Layer这一层逻辑结构之后再去看SurfaceFlinger和HWC的代码基本就能顺着逻辑走下来了。下一篇我打算拆一下P的BufferQueue和GraphicBuffer分配写写Buffer从App到SurfaceFlinger再到HWC的完整生命周期到时候把Fence这块再往深挖一挖。
阅读完成 · 觉得有帮助?
咨询建站