Highlight
macOS Monterey 引入 Adaptive-Sync 显示器支持,Metal 游戏可通过
presentAfterMinimumDuration实现自适应帧率;iPad Pro 的 ProMotion 显示器配合CADisplayLink的targetTimestamp可在帧率变化时保持动画平滑。
核心内容
你的游戏在 120Hz 显示器上运行,大部分时候能稳定在 120fps。但偶尔场景复杂度飙升,某一帧花了 9ms 才渲染完。在固定刷新率显示器上,这帧会被延迟到下一个 16.6ms 的刷新周期,用户感受到明显的卡顿。在 Adaptive-Sync 显示器上,这帧只延迟 1ms,几乎无感知。
这就是可变刷新率显示器的价值。但要用好它,需要改变传统的帧率控制思路。
详细内容
Adaptive-Sync 显示器在 macOS 上的工作原理
(01:27)
固定刷新率显示器(60Hz 或 120Hz)每 16.6ms 或 8.3ms 刷新一次。如果 GPU 没准备好新帧,就重复显示上一帧。
Adaptive-Sync 显示器的工作方式不同。每帧在屏幕上停留的时间是一个区间,而不是固定值。比如一台 40-120Hz 的 Adaptive-Sync 显示器,每帧可以在屏幕上停留 8ms 到 25ms。
这意味着:
- 如果你的帧在 9ms 完成,它会在 9ms 后立刻显示,不需要等到 16.6ms
- 卡顿从 8ms 降到了 1ms,用户几乎感知不到
(02:44)
关键点:
- Adaptive-Sync 显示器的刷新区间是动态的
- 帧完成后立即显示,不需要等待下一个固定刷新点
- 超出最大停留时间后,显示器必须刷新,会短暂不可用
Adaptive-Sync 最佳实践
(04:18)
在固定刷新率显示器上,如果 GPU 工作持续超过刷新间隔,Apple 建议把帧率降到下一个因数(比如从 60fps 降到 30fps)。
在 Adaptive-Sync 显示器上,这个建议变了:你应该以应用能稳定达到的最高均匀帧率呈现帧。
// 检测 Adaptive-Sync 是否可用
let screen = window.screen
let isAdaptiveSync = screen.minimumRefreshInterval != screen.maximumRefreshInterval
let isFullScreen = window.styleMask.contains(.fullScreen)
if isAdaptiveSync && isFullScreen {
// 启用 Adaptive-Sync 帧率控制
}
(05:51)
关键点:
minimumRefreshInterval和maximumRefreshInterval是 NSScreen 的新属性- 两个值不相等表示 Adaptive-Sync 模式
- 必须同时检查窗口是否全屏
- 只在全屏模式下才能使用 Adaptive-Sync 调度
用 presentAfterMinimumDuration 控制帧率
(06:51)
最简单的方式是用 presentAfterMinimumDuration 固定帧率:
let desiredInterval: CFTimeInterval = 1.0 / 78.0 // 目标 78Hz
// 在渲染循环中
drawable.present(afterMinimumDuration: desiredInterval)
(07:11)
关键点:
presentAfterMinimumDuration确保帧在指定间隔后才呈现- 适合实现用户可调节的 FPS 滑块
- 帧率稳定但可能不是最优
更高级的做法是动态计算帧率:
var averageGPUTime: CFTimeInterval = 1.0 / 120.0 // 初始乐观估计
// 第一帧:用乐观估计
let firstDrawable = getDrawable()
let commandBuffer = encodeWork()
commandBuffer.addCompletedHandler { _ in
let gpuTime = CACurrentMediaTime() - frameStartTime
// 滚动平均
averageGPUTime = 0.9 * averageGPUTime + 0.1 * gpuTime
}
commandBuffer.commit()
firstDrawable.present(afterMinimumDuration: averageGPUTime)
(08:44)
关键点:
- 用
CommandBuffer的 completed handler 测量 GPU 时间 - 滚动平均平滑帧率变化
- 同一套代码在不同性能的 Mac 上自动适配帧率
- 在弱性能 Mac 上可能跑到 48Hz,在强性能 Mac 上跑到 78Hz
ProMotion 和 CADisplayLink 最佳实践
(10:22)
iPad Pro 的 ProMotion 显示器支持最高 120Hz,但以下情况可能不可用:
- 用户开启了低电量模式(iPadOS 15 新增)
- 用户在辅助功能中设置了限制帧率(上限 60Hz)
- 设备过热,系统限制 120Hz
CADisplayLink 是驱动自定义绘制的推荐工具。它会在每次 vsync 时唤醒,比普通 NSTimer 更精确。
// 设置 CADisplayLink
displayLink = CADisplayLink(target: self, selector: #selector(draw))
displayLink.preferredFramesPerSecond = 40
displayLink.add(to: .current, forMode: .common)
// 在回调中
@objc func draw() {
let interval = displayLink.targetTimestamp - displayLink.timestamp
// interval 是实际可用的渲染时间
}
(15:37)
关键点:
preferredFramesPerSecond只是建议,实际帧率由系统决定- 在 60Hz 显示器上请求 40Hz,系统会自动调整到 30Hz
duration属性反映当前实际帧间隔- 用
targetTimestamp而不是timestamp准备动画
为什么用 targetTimestamp 而不是 timestamp
(17:46)
当帧率从 120Hz 降到 60Hz 时,如果用 timestamp 采样动画进度,会出现跳变:
// 用 timestamp:帧率切换时动画进度跳变
// 帧1 (120Hz): timestamp=0, progress=0
// 帧2 (120Hz): timestamp=8ms, progress=0.05
// 帧3 (60Hz): timestamp=16ms, progress=0.10 <- 这里跳变!
用 targetTimestamp 则平滑:
// 用 targetTimestamp:帧率切换时动画平滑
// 帧1 (120Hz): targetTimestamp=8ms, progress=0.05
// 帧2 (120Hz): targetTimestamp=16ms, progress=0.10
// 帧3 (60Hz): targetTimestamp=32ms, progress=0.20 <- 平滑过渡
(19:26)
关键点:
timestamp是回调被调用的时间targetTimestamp是下一帧将被合成的时间- 用
targetTimestamp采样动画,帧率变化时不会出现跳变 - 代码改动很简单:把
timestamp替换为targetTimestamp
处理错过的回调
(20:16)
高优先级线程或繁忙的 runloop 可能导致 CADisplayLink 回调被延迟或跳过。
@objc func draw() {
let currentTime = CACurrentMediaTime()
let timeRemaining = displayLink.targetTimestamp - currentTime
if timeRemaining < minimumWorkTime {
// 时间不够,减少工作量或跳过这帧
return
}
// 用上一帧的 targetTimestamp 计算正确的时间增量
let delta = displayLink.targetTimestamp - previousTargetTimestamp
previousTargetTimestamp = displayLink.targetTimestamp
// 用 delta 更新动画状态
updateAnimation(delta: delta)
}
(21:48)
关键点:
- 用
CACurrentMediaTime()检查剩余时间 - 保留
previousTargetTimestamp计算正确的时间增量 - 回调被跳过时,delta 自动变为两倍的帧间隔
- 动画不会减速或加速
核心启发
-
为 Metal 游戏添加 Adaptive-Sync 支持。检测
minimumRefreshInterval != maximumRefreshInterval且窗口全屏时,用滚动平均 GPU 时间动态控制帧率。入口 API:NSScreen.minimumRefreshInterval+MTLDrawable.present(afterMinimumDuration:)。 -
把 CADisplayLink 的 timestamp 全部替换为 targetTimestamp。这是避免帧率切换时动画跳变的唯一正确做法。入口 API:
CADisplayLink.targetTimestamp。 -
为自定义动画添加帧率降级逻辑。当
timeRemaining < minimumWorkTime时减少工作量或跳过帧,保证不丢帧。入口 API:CACurrentMediaTime()+displayLink.targetTimestamp。 -
在 iPad 应用中检测 ProMotion 可用性。用
CADisplayLink.duration获取实际帧间隔,不要用UIScreen.maximumFramesPerSecond(它始终返回 120)。入口 API:CADisplayLink.duration。 -
为游戏添加用户可调节的 FPS 上限。用
presentAfterMinimumDuration固定帧率,降低 GPU 功耗和发热。入口 API:MTLDrawable.present(afterMinimumDuration:)。
关联 Session
- Optimize high-end games for Apple GPUs — Apple GPU TBDR 架构游戏优化
- Enhance your app with Metal ray tracing — Metal 光线追踪 API
- Create image processing apps powered by Apple silicon — Apple Silicon 图像处理优化
- Discover Metal debugging, profiling, and asset creation tools — Xcode 13 Metal 调试和性能分析工具
评论
GitHub Issues · utterances