3. 帧时间分析:用 PresentMon 定位卡顿帧

帧时间分析,说白了就是给每一帧“掐表”。

你想想看,双缓冲卡顿的本质是什么?是某一帧的耗时突然飙升,超过了 16.67ms(60fps)或 8.33ms(120fps)。但光知道“卡了”不够,我们得知道:这一帧到底卡在了哪里?

我个人习惯用 PresentMon 来做这件事。它轻量、免费,而且能直接告诉你每一帧的提交时间、显示时间,以及 CPU 和 GPU 各自干了多久。今天我就带你把它用透。

3.1 为什么非要用帧时间工具?

很多人排查卡顿,第一反应是看帧率(FPS)。但 FPS 是个平均值,它会把卡顿帧“稀释”掉。

举个例子:你跑了一分钟,平均 58fps,看起来还行。但实际上中间有 3 帧卡了 200ms,剩下的帧跑得飞快。平均帧率根本看不出来。

帧时间工具不一样。它记录的是每一帧的绝对耗时。卡顿帧会像一根根尖刺一样,直接戳在你眼前。

核心原则: 排查卡顿,永远看帧时间曲线,不要看平均帧率。平均帧率是骗人的。

3.2 PresentMon 基础用法

PresentMon 是微软开源的工具,原理是 hook 到 DXGI 的 Present 调用上。它不注入代码,只做旁观者,所以对性能影响极小。

3.2.1 快速上手

下载 PresentMon 后,我建议直接用命令行模式,最灵活。基本命令长这样:

PresentMon.exe -process_name YourGame.exe -output_file frametime.csv -captureall

参数说明:

  • -process_name:指定你要监控的进程名
  • -output_file:输出到 CSV 文件,方便后续分析
  • -captureall:捕获所有 Present 调用,包括垂直同步和窗口切换

跑起来之后,它会实时打印每一帧的数据。你让它跑个 30 秒到 1 分钟,模拟出卡顿场景,然后 Ctrl+C 停掉。

3.2.2 CSV 里到底有什么?

打开生成的 CSV 文件,你会看到一堆列。我挑几个最重要的说:

列名 含义 我的关注点
PresentID 帧序号 用来定位具体是哪一帧卡了
TimeInSeconds 该帧被提交的时间戳 看卡顿发生的时刻
MsBetweenPresents 两帧提交之间的间隔(毫秒) 这就是“提交卡顿”的直接指标
MsInPresentAPI Present 调用本身的耗时 如果这个值很大,说明 GPU 在排队
MsBetweenDisplayChange 两帧实际显示到屏幕上的间隔 这是“显示卡顿”的直接指标
Dropped 是否被丢弃(1 表示丢弃) 丢帧是卡顿的极端表现

我的小技巧: 我一般把 MsBetweenPresents 和 MsBetweenDisplayChange 这两列单独拉出来画折线图。前者看 CPU 提交节奏,后者看 GPU 实际显示节奏。两条线一对比,问题出在哪一目了然。

3.3 区分渲染卡顿与提交卡顿

这是本章最核心的内容。拿到帧时间曲线后,我们要回答一个问题:卡顿是渲染太慢导致的,还是提交太慢导致的?

3.3.1 渲染卡顿的特征

渲染卡顿,指的是CPU 或 GPU 在绘制这一帧的内容时花了太多时间。比如场景里突然刷出大量粒子、贴图流送卡住、或者 Shader 编译卡顿。

在 PresentMon 数据里,它的典型表现是:

  • MsBetweenPresents 突然飙升(比如从 8ms 跳到 80ms)
  • MsInPresentAPI 保持正常(因为 Present 调用本身没阻塞)
  • 卡顿帧前后,帧时间曲线呈现“单根尖刺”或“连续几根尖刺”

我曾经在优化一个开放世界项目时,发现每次进入新区域都会卡 200ms。一看 PresentMon,MsBetweenPresents 直接拉满,但 MsInPresentAPI 只有 0.5ms。这就排除了提交问题,最后定位到是地形 LOD 切换时触发了同步加载。

3.3.2 提交卡顿的特征

提交卡顿,指的是CPU 把命令提交给 GPU 的过程被阻塞了。常见原因有:驱动缓冲区满了、垂直同步导致的 Back Pressure、或者双缓冲交换链本身的设计问题。

它的典型表现是:

  • MsBetweenPresents 飙升
  • MsInPresentAPI 也跟着飙升(比如从 0.5ms 跳到 50ms)
  • 卡顿帧往往伴随着 Dropped 标记为 1

嗯,这里要注意:提交卡顿经常被误认为是渲染卡顿。因为从外部看,都是帧率掉下去了。但修复手段完全不同——渲染卡顿你要优化 Shader 或 LOD,提交卡顿你要调整交换链参数或驱动设置。

避坑指南: 我曾经遇到过一个案例,帧时间曲线看起来像渲染卡顿,但 MsInPresentAPI 始终很高。排查了两天才发现,是显卡驱动把 Present 调用当成了同步点,导致 CPU 在等 GPU 完成上一帧。这不是渲染问题,是驱动层的提交阻塞。所以,永远不要只看一条曲线。

3.4 实战:用 PresentMon 数据画卡顿分类图

光说理论不够,我给你画一张流程图,帮你快速定位卡顿类型。

帧时间卡顿分类流程图 帧时间卡顿分类决策流程 拿到帧时间曲线 MsBetweenPresents 是否飙升? 否 是 正常帧,无需处理 检查 MsInPresentAPI MsInPresentAPI 是否飙升? 否 是 渲染卡顿 提交卡顿 优化方向: Shader / LOD / 贴图流送 优化方向: 交换链参数 / 驱动设置 / 垂直同步

3.5 实操步骤:从 CSV 到结论

好了,理论讲完了,我给你一个可以直接上手的步骤清单:

  1. 采集数据:用 PresentMon 跑 30 秒,模拟出卡顿场景,保存 CSV。
  2. 画曲线:把 MsBetweenPresents 和 MsBetweenDisplayChange 两列导入 Excel 或 Python,画折线图。
  3. 找尖刺:找到帧时间超过 33ms(30fps)的帧,标记出来。
  4. 查 MsInPresentAPI:看这些卡顿帧的 MsInPresentAPI 是否也飙升。
  5. 下结论:
    • 如果 MsInPresentAPI 正常 → 渲染卡顿
    • 如果 MsInPresentAPI 飙升 → 提交卡顿
  6. 验证:针对结论做一次小改动(比如关掉垂直同步),再跑一次 PresentMon,看卡顿尖刺是否消失。

我的习惯: 我一般会在 CSV 里加一列公式:=IF(MsBetweenPresents>33, "卡顿", "正常"),然后筛选出所有卡顿帧,批量看它们的 MsInPresentAPI 分布。这样比肉眼扫图快得多。

3.6 常见误区

最后,我列几个我踩过的坑,你遇到了可以少走弯路:

  • 误区一:只看 CPU 帧时间。 很多人只盯着 MsBetweenPresents,忽略了 MsBetweenDisplayChange。实际上,有时候 CPU 提交很流畅,但 GPU 显示卡了,那是驱动或交换链的问题。
  • 误区二:把垂直同步的等待当成卡顿。 垂直同步开启时,如果帧率低于刷新率,MsBetweenPresents 会稳定在 16.67ms 的倍数上。这不是卡顿,是正常行为。卡顿指的是不规律的尖刺。
  • 误区三:一次采集就下结论。 卡顿可能是偶发的。我建议至少采集 3 次,每次跑不同的场景,取交集。如果同一个位置每次都卡,那才是真问题。