垂直同步(V-Sync):让画面不再撕裂

垂直同步,圈里人常叫它 V-Sync。这玩意儿说白了,就是让显示器的刷新和 GPU 的渲染步调一致。你想想看,如果两边各干各的,画面就容易出现撕裂——上半截是上一帧,下半截是下一帧,看着特别难受。

我最早接触 V-Sync 是在做车载仪表盘项目的时候。那时候客户投诉说导航画面偶尔会闪一下,查了半天才发现是垂直同步没开。嗯,从那以后我对 V-Sync 就特别上心。

V-Sync 的工作原理

显示器刷画面,是从左到右、从上到下逐行扫描的。扫完一整屏,叫一个垂直消隐期(VBlank)。V-Sync 的核心逻辑就是:只在 VBlank 期间交换缓冲区。

具体流程是这样的:

  1. GPU 在后缓冲区画完一帧
  2. 等待显示器发出垂直同步信号(V-Sync Pulse)
  3. 信号到来时,正好处于 VBlank 窗口
  4. 交换前后缓冲区指针
  5. 显示器开始读取新缓冲区的内容

关键点: V-Sync 本质上是一个同步闸门。它不让缓冲区交换发生在扫描过程中,从而避免画面撕裂。

我习惯用下面的图来理解这个流程:

垂直同步(V-Sync)工作原理流程图 V-Sync 工作原理流程图 GPU 渲染帧 渲染完成 等待 VBlank 信号 缓冲区交换 显示器读取帧 说明: • VBlank 是显示器从底部回到顶部的消隐期,时间很短(约 1-2ms) • 缓冲区交换只能在 VBlank 窗口内完成,否则画面撕裂 • 如果 GPU 没在 VBlank 前完成渲染,就只能等下一帧 • 这就是为什么开启 V-Sync 后帧率会被锁定在刷新率的整数倍

开启 V-Sync 的影响

开启 V-Sync,好处坏处都很明显。我列个表,你一看就明白:

方面 开启 V-Sync 关闭 V-Sync
画面撕裂 完全消除 可能出现
帧率上限 锁定为刷新率(如 60fps) 无上限,GPU 全力跑
输入延迟 增加 1-2 帧延迟 延迟最低
功耗 降低(GPU 不用空转) 升高(GPU 持续满载)
帧率抖动 帧率稳定,但可能卡顿 帧率波动大

注意: 开启 V-Sync 后,如果 GPU 渲染一帧的时间超过了显示器的刷新周期(比如 60Hz 显示器要求 16.67ms 内完成),帧率会直接腰斩到 30fps。这就是所谓的「卡顿感」来源。我在做游戏机模拟器时就踩过这个坑——画面不撕裂了,但操作手感变得黏糊糊的。

关闭 V-Sync 的影响

关闭 V-Sync,GPU 渲染完一帧就立刻交换缓冲区,不管显示器扫到哪了。好处是输入延迟最低,操作响应快。坏处嘛——画面撕裂。

撕裂的程度取决于帧率。帧率越高,撕裂的区域越窄,但出现频率也越高。我见过一些项目为了追求极低延迟,硬是关了 V-Sync,结果用户反馈说「画面像被刀切了一样」。后来我们加了一个撕裂检测算法,在撕裂严重时自动开启 V-Sync,才算解决问题。

关闭 V-Sync 的典型场景:

  • 竞技类游戏(需要最低输入延迟)
  • 实时控制系统(操作响应优先)
  • 性能测试(测量 GPU 原始帧率)

V-Sync 与双缓冲的关系

双缓冲是 V-Sync 的基础设施。没有双缓冲,V-Sync 根本没法工作。为什么?

单缓冲模式下,GPU 直接往显存里写数据,显示器同时从显存里读数据。你写一半我读一半,画面必然撕裂。双缓冲给了我们一个「安全区」——GPU 写后缓冲区,显示器读前缓冲区,互不干扰。

V-Sync 就是那个「什么时候交换」的决策者。它告诉双缓冲系统:别急着换,等显示器扫完这一屏再说。

我的经验: 在嵌入式系统里,双缓冲 + V-Sync 是标配。但要注意,有些低端 MCU 的显示控制器不支持 VBlank 中断,这时候只能用定时器模拟 V-Sync。精度差一些,但总比没有强。我曾经在一个 Cortex-M4 项目上这么干过,效果还行。

实际配置示例

以 STM32 的 LTDC 控制器为例,配置 V-Sync 的代码大概是这样:

/* 启用 LTDC 全局中断 */
HAL_LTDC_EnableIT(&hltdc, LTDC_IT_LI);

/* 配置行中断位置(VBlank 起始) */
hltdc.Instance->LIPCR = (panel_height - 1);

/* 中断服务函数中处理缓冲区交换 */
void LTDC_LineInterruptHandler(void)
{
    if (__HAL_LTDC_GET_FLAG(&hltdc, LTDC_FLAG_LI))
    {
        __HAL_LTDC_CLEAR_FLAG(&hltdc, LTDC_FLAG_LI);
        
        /* VBlank 到达,安全交换缓冲区 */
        swap_buffers();
    }
}

这段代码的核心就是:等到显示器扫到最后一行时触发中断,然后在中断里交换缓冲区。这样能保证交换操作发生在 VBlank 窗口内。

避坑指南

做 V-Sync 配置时,有几个坑我反复踩过:

  • 中断优先级: VBlank 中断的优先级要设高一点,否则被其他中断拖住,缓冲区交换就错过了 VBlank 窗口
  • 缓冲区对齐: 有些显示控制器要求缓冲区地址按 64 字节对齐,不对齐会导致 DMA 传输异常
  • 帧率测量: 别用延时函数算帧率,要用 VBlank 中断计数。延时函数不准,还占用 CPU

我曾经踩过的坑: 在一个项目里,我把 VBlank 中断优先级设成了最低,结果被一个 SPI 中断频繁打断。缓冲区交换经常延迟,导致画面每隔几秒就闪一下。查了两天才找到原因。后来我把 VBlank 中断优先级提到第二高(最高留给系统时钟),问题就解决了。

V-Sync 不是银弹。它解决撕裂问题,但引入了延迟和帧率锁定。具体开还是关,得看你的应用场景。做嵌入式系统,没有绝对的对错,只有合适的取舍。