1. 从一次黑屏问题说起:为什么需要理解HWC
做Android系统开发的朋友,几乎都遇到过这样的场景:App层明明已经把画面提交上来了,SurfaceFlinger也调用了,但屏幕就是黑的、花屏的,或者滑动列表时明显掉帧。查半天,log里翻来覆去就那几行,最后发现问题出在硬件合成器HWC(Hardware Composer)上。
我接触HWC是在做系统性能优化的时候。当时被测设备在低亮度下播放视频,功耗比竞品高了快10%,CPU占用率倒是正常,但GPU负载一直下不来。翻了一周代码,定位到合成链路:SurfaceFlinger把所有的图层都交给GPU做GPU合成(GLES Composition),完全绕过了HWC的硬件合成能力。这就像你家里明明有洗碗机,却坚持手洗所有碗筷,费时费力还费水。搞懂HWC在整个显示链路里的位置和作用,是解决这类问题的第一步。
这篇文章我会从SurfaceFlinger的合成决策开始,一路讲到显示驱动层的DRM/KMS接口,把HWC硬件合成器的完整流程拆开揉碎。内容包括:HWC1与HWC2的架构差异、Layer与Composition的决策逻辑、BufferQueue与Fence同步机制、真正的HAL层接口实现,以及怎么用dumpsys SurfaceFlinger验证合成是否真的走了硬件。同时会分享一些我在实际调试中踩过的坑——比如crop坐标计算错误、fence超时导致的显示卡顿、HWC回退到GPU合成的原因排查。
这篇内容适合正在做Android系统移植与优化的工程师、想深入理解显示子系统的应用层开发者,以及准备面试系统开发岗位的同学。不需要你有显示驱动开发经验,但对Android基本架构要有概念。
2. HWC在整个显示链路中的位置与核心职责
2.1 SurfaceFlinger到显示驱动的完整调用链
先看一条完整链路。一个App要显示一帧动画,从产生到屏幕点亮,大概要经过以下环节:
App调用ANativeWindow的dequeueBuffer获取一块可写的图形缓冲区,然后通过Canvas或OpenGL ES把内容渲染上去,再调用queueBuffer把这块缓冲区提交到BufferQueue。BufferQueue的生产者端持有这些缓冲区,消费者端通常就是SurfaceFlinger。SurfaceFlinger通过CompositionEngine汇总所有可见Surface的缓冲区,决定哪些图层可以交给HWC做硬件合成,哪些需要自己用GPU先合成一次,最终输出到显示设备。
SurfaceFlinger的下游就是HAL层。HWC(Hardware Composer)是Android定义的一套标准HAL接口,硬件厂商根据自家GPU/DPU(Display Processing Unit)的能力实现这套接口。在HWC2架构里,SurfaceFlinger通过IComposerClient与HWC服务通信,HWC服务再通过IDevice、IDisplay、ILayer这些接口操作具体的显示设备。
再往下,HWC的实现通常基于DRM(Direct Rendering Manager)子系统。以高通平台为例,HWC会通过libdrm调用drmModeSetPlane、drmModePageFlip等接口,把图层交由KMS(Kernel Mode Setting)配置到显示控制器。显示控制器负责把内存中的像素数据按时序输出到HDMI、DSI、DP等物理接口上。
2.2 HWC解决的核心问题:功耗与带宽
很多人问,SurfaceFlinger自己也能做合成,为什么非要硬件合成器?
核心原因有两个:带宽和功耗。
假设屏幕分辨率是1080p,刷新率60Hz,像素格式RGBA8888,一帧原始数据大约是192010804字节,约8.3MB。60Hz下一秒钟的数据量接近500MB。如果所有图层都让GPU先合成到一块临时缓冲区,再由显示器控制器读走,内存带宽消耗会非常可观。
但如果把多个图层直接交给显示控制器硬件去叠加,显示控制器本身就支持多个plane(硬件图层),它可以在读取每一行的像素时实时叠加多层数据,不需要先写入一块合成后的缓冲区。这就省掉了一次全屏读写的带宽开销。
功耗上的差异更明显。GPU做合成时,需要把整个屏幕的像素处理一遍,GPU频率会被拉高,耗电自然增加。HWC硬件合成时,图层仅仅是硬件叠加,GPU可以进入低功耗状态。尤其是在视频播放、相机预览这类持续全屏显示的场景里,HWC是否生效直接决定设备的续航表现。
提示:在低刷新率或静态画面场景,HWC省电效果更明显。如果只是滑动列表这种UI持续变化的场景,GPU合成与HWC的功耗差距反而不大。评测HWC效果时要注意场景选择。
3. HWC2架构详解:从接口设计到核心概念
3.1 HWC1到HWC2的演进逻辑
Android 8.0之前使用的是HWC1接口,SurfaceFlinger需要把全套Layer信息传给HWC,HWC返回每个Layer的合成方式。HWC1的接口设计偏向固定功能硬件,很多处理逻辑被硬编码在HAL里。
Android 8.0开始引入HWC2,设计思路做了很大调整。HWC2把决策权更多地收回到SurfaceFlinger侧,HAL层主要提供能力声明和图层配置能力。HWC2定义了三种合成类型:
- DEVICE:图层由HWC硬件合成,SurfaceFlinger不需要参与
- CLIENT:图层需要SurfaceFlinger用GPU合成到一块目标缓冲区
- CURSOR:专门为光标图层优化,硬件支持的话可以不占用正常Layer资源
HWC2还有一个重要的变化:支持多Display。HWC1时代主要面向内置屏幕,HWC2把外部显示(HDMI、DP、无线投屏)也纳入统一管理,每个Display有独立的合成配置。这对现代Android设备的双屏、多屏交互场景至关重要。
3.2 HWC2的四大核心接口族
HWC2的HAL定义在hardware/interfaces/graphics/composer/2.x目录下,核心接口可以分为四组:
- IComposerClient:HWC服务的入口,负责创建Display、创建Layer、设置全局属性
- IDevice:代表一个物理显示控制器,操作包括创建Layer、设置Power Mode、验证合成方案
- IDisplay:具体的显示输出,1024号Display通常是主屏(Internal Display),外接HDMI等设备会有独立的Display ID
- ILayer:指一个可配置的显示图层,包含Buffer、SourceCrop、DisplayFrame、Transform、BlendMode、PlaneAlpha等属性
实际开发中接触最多的接口是IDevice的validateDisplay和presentDisplay,以及ILayer的setLayerBuffer、setLayerSourceCrop、setLayerDisplayFrame。
一个典型的HWC2配置顺序是:SurfaceFlinger先调用setLayerBuffer等接口配置好所有Layer的属性,然后调用validateDisplay让HWC检查这套配置是否可行,HWC返回结果并标记每个图层应该走DEVICE还是CLIENT。SurfaceFlinger根据返回值做处理,如果所有图层都走DEVICE,就直接调用presentDisplay提交显示;如果有CLIENT图层,SurfaceFlinger先用GPU合成这些图层,再重新validate,直到所有图层都被HWC接受。
3.3 Layer、Plane与合成方式的核心概念
Layer是SurfaceFlinger传给HWC的图层抽象,每个Layer包含一个Buffer、显式的位置信息、裁剪信息、透明度等。Plane是显示控制器物理上支持的图层叠加单元,数量有限,常见的是4到8个。
可以这样理解:Plane是硬件资源,Layer是软件对象。HWC要做的事就是把Layer映射到Plane上。如果Layer数量超过硬件Plane数量,或者某些Layer属性硬件不支持,就超出的部分就要靠GPU合成来合并。
合成方式的选择有几个关键判断条件:
- Layer是否带有透明度或者复杂的混合模式
- Layer是否做了缩放、旋转等变换
- Layer是否使用了硬件不支持的像素格式
- Layer数量是否超出Plane能力
实际调优中,最常见的HWC回退原因是Transform不被支持。比如SurfaceFlinger把旋转角度设置成Transform::ROT_90,但显示控制器的plane不支持90度旋转,HWC只能把这部分Layer标记为CLIENT。
注意:查看HWC合成决策时,优先关注HWC的getCapabilities能力集。不同芯片方案的HWC能力差异很大,同样的Layer配置,在A平台走DEVICE,在B平台可能走CLIENT,这不一定是bug,跟硬件能力直接相关。
4. 实战:一次视频播放HWC合成全流程拆解
4.1 环境与工具准备
我这次实战使用的设备是高通骁龙平台的Android 12系统,内核版本5.10,HWC实现基于DRM。调试工具包括:
- adb shell dumpsys SurfaceFlinger:查看合成决策、Layer信息、帧率统计
- adb shell dumpsys display:查看显示设备状态
- adb shell cat /sys/kernel/debug/dri/0/state:查看DRM状态
- adb shell dumpsys gfxinfo:查看App渲染性能
- 自定义的HWC trace点,通过systrace抓取
提示:如果设备没有root权限,DRM调试节点可能无法访问。可以先用adb shell dumpsys SurfaceFlinger --list来确认SurfaceFlinger版本,再决定后续调试方式。
4.2 SurfaceFlinger合成决策日志解读
启动一个视频播放App,让它全屏播放视频。播放过程中执行:
adb shell dumpsys SurfaceFlinger关键输出如下:
Display 0: HWC 2.x Layers: | Z | Comp Type | Disp Frame | Source Crop | Surface | ... |----|------------|--------------|---------------------|-----------| | 0 | DEVICE | [ 0, 0] | [ 0, 0] | videoDec | | 1 | DEVICE | [ 0, 0] | [ 0, 0] 1280x720| videoLayer | | 2 | DEVICE | [ 0, 0] | [ 0, 0] 1080x1920| statusbar | | 3 | DEVICE | [ 0, 0] | [ 0, 0] 1080x1920| navigationbar |这里的Comp Type是我们最关心的字段。DEVICE表示该图层由HWC硬件合成,CLIENT表示需要GPU参与。
一个常见的疑问是:状态栏和导航栏也是独立的图层,为什么它们也能走DEVICE?原因是大多数显示控制器支持至少4个plane,系统UI层虽然透明混合,但如果HWC声明支持BLEND_PREMULTIPLIED,就可以直接硬件叠加。
4.3 DRM驱动侧的实际状态验证
SurfaceFlinger给出的结论是“我请求了DEVICE合成”,但真正显示出来的效果,还得看DRM层是否按预期配置了Plane。通过DRM状态节点验证:
cat /sys/kernel/debug/dri/0/state输出节选:
plane[31]: crtc=24 fb=256 src=(0,0) 1280x720 dst=(0,0) 1024x768 rotation=0 plane[32]: crtc=24 fb=257 src=(0,0) 1080x1920 dst=(0,0) 1080x1920 rotation=0每个有内容的fb对应一块framebuffer,src是源裁剪区域,dst是目标显示区域。如果src和dst与SurfaceFlinger配置的Layer信息一致,说明HWC确实把配置下发到了DRM。
4.4 帧率与延迟的实测数据
用systrace抓取一次视频播放的合成过程。重点看三个时间段:
- App的queueBuffer到SurfaceFlinger消费该Buffer的间隔
- SurfaceFlinger调用HWC的validateDisplay到presentDisplay之间的间隔
- presentDisplay返回到Vsync下一次到来的间隔
在纯DEVICE合成场景,SurfaceFlinger的处理时间通常在1到2ms以内,其中大部分耗时消耗在Binder IPC和BufferQueue的acquire/release上。GPU合成场景下,这个时间会拉长到5到8ms,同时也伴随着GPU频率上升。
一个值得关注的数字是合成耗时占比。如果在一段时间内,合成耗时持续超过帧间隔的50%,就说明显示链路压力很大,需要考虑减少图层数或者优化合成策略。
5. HWC关键参数与BufferQueue、Fence同步机制
5.1 BufferQueue在合成流程中的角色
要理解HWC的工作方式,必须理解BufferQueue。它是Android图形系统的数据管道,由生产者(App)和消费者(SurfaceFlinger)通过共享内存的方式传递图形缓冲区。
HWC并不直接消费BufferQueue的缓冲区,它只负责把SurfaceFlinger已经acquire到的Buffer配置到硬件Plane上。SurfaceFlinger在把Layer信息传给HWC之前,需要保证对应的Buffer状态正确。dequeueBuffer和queueBuffer的过程中,Buffer有四种状态:FREE、DEQUEUED、QUEUED、ACQUIRED。
合成流程中,SurfaceFlinger作为消费者从BufferQueue acquire一块Buffer后,会把Buffer的handle传给HWC。HWC在使用这个Buffer之前,必须等它的ReleaseFence信号——也就是说,生产者(App的GPU渲染)真正完成了写入,HWC才能读取。
5.2 Fence同步机制:跨进程的“完成信号”
Fence是Android图形系统里的同步原语,本质上是一个文件描述符,对应内核的sync_fence机制。它解决的核心问题是:多个硬件单元(GPU、DPU、Display Controller)之间的依赖关系。
举一个场景。视频解码器输出一帧视频数据到Buffer A,App的SurfaceFlinger想把这帧数据显示到屏幕。解码器写Buffer A的操作可能还没完成,SurfaceFlinger不能贸然把Buffer交给显示控制器。这就是AcquireFence的作用:SurfaceFlinger在queueBuffer时传入AcquireFence,HWC在配置Plane前会等待这个fence。
同样,SurfaceFlinger在GPU合成CLIENT图层时,GPU可能在渲染目标缓冲区,HWC不能立即读取。SurfaceFlinger会为合成结果传入ReleaseFence,HWC等待ReleaseFence之后再做Display输出。
Fence超时是最常见的显示问题来源之一。如果某个硬件单元一直没有signal fence,显示就会卡住,最终触发SurfaceFlinger的FenceTimeout机制,导致掉帧甚至屏幕黑掉。排查这类问题时,dumpsys SurfaceFlinger的Fence状态和内核的/sys/kernel/debug/sync信息非常有用。
提示:如果用adb shell dumpsys SurfaceFlinger看到某一帧长时间卡在acquireFence状态,优先检查GPU是否hang住。在部分平台,GPU hang会导致fence一直不signal,进而阻塞整个合成流程。
5.3 HWC与Vsync的配合
显示系统还有一个核心概念是Vsync(垂直同步)。屏幕的刷新是一个持续过程,从左上角扫描到右下角,每扫完一帧就触发一个Vsync信号。HWC和SurfaceFlinger都依赖Vsync来推进渲染节奏。
SurfaceFlinger在Vsync到来时开始处理合成任务,HWC在Vsync间隔内完成Plane配置。如果HWC合成耗时超过Vsync间隔,就会出现掉帧。这也是为什么在设计合成方案时,硬件Plane的数量和配置速度至关重要。
6. 显示驱动侧的核心实现:DRM/KMS接口解析
6.1 DRM与KMS的基本概念
DRM(Direct Rendering Manager)是Linux内核的图形子系统,负责管理GPU、显示控制器、输出接口等。KMS(Kernel Mode Setting)是DRM里的显示管理部分,负责配置显示模式、分辨率、帧率以及Plane的布局。
Android的HWC HAL实现基于DRM/KMS的情况下,HWC通过libdrm用户态库操作DRM设备节点(/dev/dri/card0)。关键的DRM对象有:
- CRTC(Cathode Ray Tube Controller):显示控制器,负责把内存中的画面按特定时序输出
- Encoder:编码器,把CRTC输出的信号转换成物理接口需要的格式
- Connector:物理连接器,比如HDMI、DP、DSI
- Plane:硬件图层,可以理解为显示控制器里的独立图像叠加层
6.2 HWC如何与DRM/KMS交互
一个完整的HWC到DRM的调用链路是:
HWC的validate阶段,会把一组Layer提交给DRM的Atomic Test接口,查询硬件是否支持这套配置。drmModeAtomicCommit的flag如果包含DRM_MODE_ATOMIC_TEST_ONLY,则表示只做校验不真正生效。如果校验失败,HWC会把部分Layer标记为CLIENT,要求SurfaceFlinger先用GPU合成。
校验通过后,HWC进入commit阶段,调用drmModeAtomicCommit正式提交配置,KMS会更新硬件Plane的寄存器,在下一个Vsync开始按新配置输出画面。
Atomic API的设计让HWC可以一次性提交多个属性更新,保证显示的原子性。传统Legacy API是一次一个属性设置,应用层在配置过程中可能出现撕裂。Android的HWComposer要保证画面切换不产生撕裂(tearing),Atomic API是关键机制。
很多显示问题与DRM/KMS配置错误有关。举一个典型的案例:某设备外接4K显示器时,HWC配置了4K分辨率的Plane,但CRTC仍停留在1080p模式,结果只显示了左上角四分之一画面。定位后发现是HWC的Display配置和DRM的Connector配置未同步,重新设置CRTC的模式后解决。
6.3 显示驱动的调试技巧
遇到显示异常时,我常用的排查顺序是:
第一步,看DRM state节点确认Plane配置是否正确。如果配置正确,说明HWC与DRM的交互正常,问题可能出在硬件或面板本身。第二步,看内核dmesg有没有DRM或DPU报错。第三步,看SurfaceFlinger的日志,判断是合成决策异常还是Buffer处理异常。
内核对DRM的错误处理通常有完整日志。比如drm_atomic_check_failed就表示Atomic校验失败,后面会带上具体失败的原因,诸如plane does not support rotation等。这类日志对定位HWC能力声明错误非常有帮助。
7. 实战调优:让合成真正走HWC的配置策略
7.1 确认当前合成方式
在做任何优化之前,先要确认设备当前的真实合成路径。上面讲过dumpsys SurfaceFlinger可以查看Comp Type,但更快速的方式是看SF的log:
adb logcat -s SurfaceFlinger搜索Composition或validate关键字的输出,可以看到类似这样的日志:
SurfaceFlinger: Composition DISPLAY_0: DEVICE(3) CLIENT(0)DEVICE后面的数字表示有多少个图层走了硬件合成,CLIENT后面的数字表示多少个图层走了GPU合成。如果CLIENT数量大于0,说明至少有一部分图层没有走HWC。
7.2 常见的HWC回退原因及对策
我在实际操作中总结了几种高频的HWC回退场景:
第一种,Layer数量超过Plane数量。常见于多窗口模式或分屏模式。SurfaceFlinger会把超过Plane能力的Layer合并,或者部分Layer回退到CLIENT。对策是检查HWC的getMaxLayerCount声明,同时注意Surface的过度分层,避免不必要的图层叠加。
第二种,Transform旋转导致回退。硬件Plane通常只支持ROT_0,部分平台支持ROT_90等。如果App层设置了Transform,HWC校验失败就会回退。对策是让App层尽量避免使用无法硬件加速的旋转,或者检查HWC是否正确声明了ROTATE能力。
第三种,像素格式不支持。比如RGB_888与RGBA_8888的Plane支持情况不同。某些平台对YUV格式的硬件叠加能力有限。对策是检查显示控制器的格式支持表,必要的时候让App规范像素格式的使用。
第四种,Buffer维度超过硬件限制。显示控制器对Plane的宽高有限制,如果Surface过大或过小,超出硬件能力范围,也会回退。这类问题通常伴随硬件规格文档限制,需要详细比对。
7.3 优化Layer以提升合成效率
在实际开发中改变App的UI层级往往不现实,所以HWC优化更多是系统侧的配置工作:
一是减少Surface数量。不必要的Surface会增加Layer数,超出Plane能力后就必然有回退。对于纯系统UI,可以考虑合并Surface或者在低端设备上减少分屏的图层。
二是合理使用SetLayerBuffer的时机。SurfaceFlinger频繁把Buffer交给HWC,HWC内部有一整套Buffer管理和缓存逻辑。如果配置不当,可能导致每次合成都要重新设置Buffer,增加IPC开销。
三是利用好隐藏显示(Display Off)策略。在屏幕熄灭状态,HWC可以停止合成以省电。MR(Mipi Read)模式下屏幕内容可以自刷新,HWC只需要在内容变化时更新。这个功能在黑屏待机场景下功耗优化效果明显。
7.4 用systrace定位掉帧根因
通过systrace抓取一次卡顿现场的合成耗时。执行以下步骤:
# 启动systrace,抓取gfx、view、sf、hwc相关tag python systrace.py -a 你的应用包名 -b 16384 gfx view wm am sf hwc idle sched freq抓取后,在浏览器打开trace文件,重点观察SurfaceFlinger线程的执行时间线。如果发现SF单次合成耗时超过5ms,进一步下钻到这个时间段内SF调用了哪些函数,比较HWC validate与HWC present的耗时占比。
如果validate阶段耗时偏高,往往是Layer属性配置复杂导致HWC内部校验时间长;如果present阶段耗时偏高,可能是Buffer的AcquireFence等待太久,也就是生产者端跟不上节奏。
实操心得:present耗时偏高,但GPU并不繁忙时,优先检查BufferQueue的dequeueBuffer超时,这通常意味着Buffer数量配置不足。SurfaceFlinger默认BufferQueue会分配3块Buffer,在某些高帧率场景可以尝试增加Buffer数量,但要权衡内存占用。
8. 文档之外的进阶认知与学习路线
8.1 HWC学习资源推荐
我日常工作里最常用的几份资料:
- AOSP源码的
hardware/interfaces/graphics/composer/2.x目录:HWC2的接口定义,必须通读 - 各芯片厂商的HWC实现源码,比如高通平台的
sdm(Snapdragon Display Manager),代码量大,但能学到完整的显示控制器管理逻辑 - Linux内核的
drivers/gpu/drm目录:DRM原子接口的实际实现 - 官方文档《Android Graphics Architecture》:AOSP源码的docs目录下可以找到,是理解整个图形架构的入门材料
如果做的是MTK平台,则重点看vendor/mediatek/proprietary/packages/modules/Gallery和vendor/mediatek/proprietary/hardware/hwc相关部分;展锐平台的HWC则主要在sprd相关目录,思路大同小异,但细节差异不小。
8.2 建议的动手路线
如果从头开始学,我建议的顺序是:
第一步,先跑通SurfaceFlinger的dumpsys分析,理解合成决策的输出字段。第二步,修改HWC的配置文件,人为让某个图层回退到CLIENT合成,感受合成方式变化带来的性能差异。第三步,在HWC HAL里加日志或trace点,跟踪validate和present的调用细节。第四步,进入DRM驱动层面,尝试调整Plane的数量或者修改CRTC模式,观察显示输出的变化。
这套路线从用户态到内核态,循序渐进。每一步都会加深你对“一帧画面是如何显示的”这个问题的理解。
8.3 我对HWC调试的一点心得
调试显示问题需要同时具备三层知识:Android框架层的合成决策逻辑、HAL层的接口实现、Linux内核DRM的硬件管理能力。多数工程师精通其中一层,遇到问题就容易被卡住。
我自己的经验是,先把dumpsys SurfaceFlinger的输出读透。它能告诉你最终发生了什么,然后顺着线索向下找原因。大多数显示问题,80%的信息都藏在这份dumpsys的Layer列表和Frame统计里,只有少数情况需要深入到驱动层用示波器去测波形。
在真正动手写HWC代码之前,先在现有的HWC实现上做增量修改,会比从零开始更高效。比如尝试调整一个Layer的BlendMode,看看dumpsys里的Comp Type有什么变化。这种小步迭代的方式,能快速建立起对合成器行为的直觉。
这个领域还有一个容易被忽略的点是功耗调优。很多人遇到功耗问题喜欢先查CPU和GPU频率,但真正的问题可能在显示链路——HWC没有生效导致GPU持续高频工作。学会看功耗数据在gfxinfo和battery historian里的表现,再对照合成方式,往往能快速定位问题。