22、Camera与显示同步:VSYNC机制与Camera预览同步
各位好,我是老林。今天聊的话题,说白了就是让Camera拍到的画面,顺滑地显示在屏幕上。你可能会想,这有什么难的?拍到了就显示呗。嗯,真没那么简单。
我刚开始做Camera驱动那会儿,就踩过一个大坑。预览画面看着像“撕裂”了一样,上半截是上一帧,下半截是下一帧。用户反馈说“这手机拍照像得了帕金森”。后来我才明白,这是没处理好显示同步的问题。
这一章,我们就来彻底搞懂Camera和显示是怎么“握手”的。
VSYNC机制:显示系统的“心跳”
VSYNC,全称Vertical Synchronization,垂直同步。你可以把它想象成显示器的“心跳”。屏幕刷新不是一下子全刷新的,而是一行一行从上往下扫。扫到底部后,再跳回顶部重新开始。这个“跳回”的时刻,就是VSYNC信号发出的时机。
为什么要关心这个?因为如果你在屏幕正在扫描中间的时候,突然把Buffer里的数据换了,那画面就会撕裂——上半截是旧数据,下半截是新数据。
核心原则:Buffer的交换,必须发生在VSYNC信号到来时。也就是屏幕刚扫完一帧,准备扫下一帧的那个瞬间。
在Android系统中,VSYNC信号由SurfaceFlinger统一管理。它会定期向各个模块发送VSYNC事件。Camera HAL层需要做的就是:在VSYNC信号到来时,准备好新的图像数据。
SurfaceFlinger与Camera Buffer流转
Camera预览的Buffer流转,其实是一条流水线。我画了个图,你一看就明白:
流程其实不复杂:
- Camera HAL 从Gralloc分配器申请Buffer
- HAL把图像数据填进Buffer
- Buffer被queue到SurfaceFlinger的队列里
- SurfaceFlinger等到下一个VSYNC信号,才把Buffer交给显示硬件
- 显示硬件扫描屏幕,把画面呈现出来
我的经验:Buffer数量至少要有3个。一个正在被Camera填充,一个在SurfaceFlinger队列里等待,一个正在显示。这样才不会出现“等Buffer”的情况。我曾经只用了2个Buffer,结果帧率死活上不去,后来才发现是Buffer不够用。
显示刷新率与Camera帧率匹配
这里有个常见的误区:是不是Camera帧率越高越好?
不一定。如果你的屏幕刷新率是60Hz,Camera输出90fps,那多出来的30帧根本显示不出来,反而浪费功耗和带宽。反过来,如果屏幕是120Hz,Camera只输出30fps,那画面就会显得卡顿。
最佳实践是:Camera帧率 = 屏幕刷新率 或 屏幕刷新率的整数分之一。
| 屏幕刷新率 | 推荐Camera帧率 | 说明 |
|---|---|---|
| 60Hz | 30fps 或 60fps | 30fps省电,60fps流畅 |
| 90Hz | 30fps 或 45fps 或 90fps | 注意45fps需要特殊处理 |
| 120Hz | 30fps 或 60fps 或 120fps | 120fps对ISP压力大 |
注意:不要盲目追求高帧率。我曾经在一个项目里,把Camera调到120fps,结果手机发热到烫手,电池半小时就空了。后来发现用户根本不需要那么高的帧率,60fps完全够用。
低延迟预览实现:Preview Buffer直接送显
传统的预览路径是:Camera → 内存 → SurfaceFlinger → 显示。这中间至少经过两次内存拷贝,延迟在30-50ms左右。
对于普通预览,这个延迟可以接受。但如果你在做AR应用、无人机图传、或者游戏直播,这个延迟就太大了。用户会感觉“画面跟不上手”。
怎么优化?Preview Buffer直接送显。
思路是这样的:Camera HAL直接把Buffer的物理地址告诉显示硬件,跳过SurfaceFlinger的合成步骤。显示硬件直接从Camera的Buffer里读取数据。
// 伪代码示例:直接送显配置
// 在Camera HAL的configureStreams中
camera_stream_t previewStream;
previewStream.stream_type = CAMERA_STREAM_PREVIEW;
previewStream.width = 1920;
previewStream.height = 1080;
previewStream.format = HAL_PIXEL_FORMAT_IMPLEMENTATION_DEFINED;
previewStream.usage = GRALLOC_USAGE_HW_CAMERA_WRITE |
GRALLOC_USAGE_HW_COMPOSER | // 关键:直接给Composer用
GRALLOC_USAGE_HW_TEXTURE;
// 在processCaptureRequest中
// 直接使用显示驱动分配的Buffer,不做额外拷贝
buffer_handle_t buf = request->buffers[0].buffer;
// 填充YUV数据到buf
fill_yuv_data(buf, frame_data);
// 直接queue到显示队列
display_queue_buffer(buf);
这样做的好处是延迟可以降到10ms以内。但代价是:你失去了SurfaceFlinger的合成能力。如果界面上还有UI元素(比如拍照按钮),就需要用硬件Overlay来叠加。
核心要点:直接送显的关键是Buffer的Usage标记。必须同时包含GRALLOC_USAGE_HW_CAMERA_WRITE和GRALLOC_USAGE_HW_COMPOSER,这样Gralloc才会分配一个Camera和显示硬件都能直接访问的Buffer。
实战:实现60fps无撕裂预览
好了,理论说完了,我们来点实际的。怎么实现一个60fps无撕裂的预览?
我总结了几步:
- 确认硬件能力:ISP能否稳定输出60fps?传感器是否支持60fps?显示面板是否支持60Hz?
- 配置Stream:在Camera HAL中,把预览Stream的fpsRange设置为[60,60]
- Buffer管理:至少分配4个Buffer,形成流水线
- VSYNC对齐:Camera的帧中断要和VSYNC对齐。我习惯在HAL里监听VSYNC事件,然后调整传感器的曝光时序
- 直接送显:如果延迟要求高,就用上一节说的直接送显方式
- 验证:用dumpsys SurfaceFlinger查看帧率,用systrace看是否有掉帧
// 实际项目中的VSYNC对齐代码片段
// 在Camera HAL的processCaptureRequest中
static int64_t last_vsync_time = 0;
void on_vsync_event(int64_t timestamp_ns) {
last_vsync_time = timestamp_ns;
}
status_t processCaptureRequest(...) {
// 等待下一个VSYNC
int64_t current_vsync = last_vsync_time;
int64_t frame_duration_ns = 1000000000LL / 60; // 60fps对应16.67ms
// 计算下一个VSYNC时间
int64_t next_vsync = current_vsync + frame_duration_ns;
// 调整传感器曝光,使帧结束时间对齐到VSYNC
sensor_set_exposure_time(next_vsync - frame_duration_ns - 2000000); // 预留2ms
// 填充Buffer
fill_buffer(request);
return OK;
}
避坑指南:我曾经在某个平台上发现,即使代码写得再完美,预览还是偶尔会撕裂。查了三天,最后发现是显示驱动的一个Bug——它在VSYNC信号到来时没有正确等待Buffer更新完成。解决方案是给显示驱动打了一个补丁,在VSYNC处理函数里加了一个内存屏障。
最后,验证是否真的无撕裂,有个土办法:拍一个快速移动的物体,比如甩动的笔。如果画面是干净的,没有错位,那就说明同步成功了。如果看到画面像“撕开”了一样,那就回去检查VSYNC对齐吧。
嗯,这一章的内容就到这里。记住一句话:Camera和显示,就像两个人跳舞,步调必须一致。VSYNC就是那个节拍器。
公众号:蓝海资料掘金营,微信deep3321