大多数浏览器和
Developer App 均支持流媒体播放。
-
针对各种刷新速率显示器进行优化
了解如何在支持动态显示时序的 Apple 平台上实现流畅屏幕更新。学习在 macOS 的自适应同步显示屏上调整全屏游戏更新速度的技巧,了解低功率模式和其他系统状态如何影响 ProMotion 显示屏上的帧速率可用状态。我们还将分享利用显示链接 API 促进定制绘图的最佳实践。
资源
相关视频
WWDC22
WWDC19
-
下载
嗨 我叫凯尔萨拿 我来自图形处理器软件工程小组 我会和我的同事艾利克斯李一起谈谈 如何在你的应用程序中 达到最佳的帧步调 在各种刷新率的展示中皆然 我们会聚焦在一些将更新于 macOS的新显示科技技术 如自适应同步 以及如何驱动自定义绘图 不论何种情境 能在iPad Pro上顺畅执行 我们首先会快速概览显示有哪些类别 现在已经获得Apple平台的支持
我们将要向你介绍Mac上 自适应同步技术的显示 还有macOS Monterey中的新工具 你可以用它们来做出 全屏应用程序及游戏中 这些显示流畅的帧速率
接着我们会深入探讨 iPad Pro上的ProMotion 然后参考一些CADisplayLink的 行业最佳作法 能协助你的应用程序 维持正确的帧步调 帧速率相异时亦同
我们先回顾一下 Apple设备能够支持的显示类别
大部在Apple系统上运行的显示 都是固定刷新率 意思是 它们维持一定的刷新速率 只要有接上电源 有个例外 就是我们 在iPad上的ProMotion显示 还有现在在macOS上的 自适应同步显示 我们来探讨一下Mac上 进行自适应同步显示的全新功能
我们先谈什么是自适应同步显示 再看它们如何在Mac上运行 但首先我们先很快看一下 固定刷新率的显示如何运作 这边这个图表展示了 帧如何被传送到60赫兹显示上 每一帧都被呈现在显示上 会停留16毫秒 直到显示刷新 如果你的Mac取用了帧缓存里 预备好的新的一帧 接着新的那一帧就会呈现出来 如果没有的话 前一帧就会再度显示
看看120赫兹的显示是怎么样 你可以看到虽然我们 把刷新率加倍了 而因此减半间隔时间 也就是每一帧停留在画面的时间 它表现的方式是一样的 只是更快了
再来看看这个自适应同步显示 它的长度并非完全不变 每一帧都有一个时间范围 可以在屏幕上不定时停留 这个范围会随着使用的显示器变化 这个显示可以在40到120赫兹间运作 意味着一帧可以在屏幕上 停留8到25毫秒
注意 只要超过最大时限 系统就需要刷新面板 而显示会短暂地 无法作用 因为要花上短短一瞬等待新的更新 好的 那么你的游戏跟应用程序 会得到什么好处 如果使用自适应同步显示呢? 如果是大部分时间运行 都使用最高刷新率显示的应用程序 自适应同步显示 无偿提供了很大的好处 我们先看看这个情境 当你的应用程序大多有办法显示 八毫秒以下的新帧时 代表你运行120赫兹时堪称稳定 但由于画面复杂度暂时上升 已经完成的帧来到了帧缓存中 却已是前一帧显示后的九毫秒 在固定帧速率的显示中 前一帧 就会显示16毫秒 而不是你希望的八毫秒 这在你的应用程序中 就造成了可以察觉的顿点
在自适应同步显示中 你的帧传送给显示器的时间 是完成后的瞬间 所以你的应用程序 只会有一毫秒的损失 这么小的顿点一般不会被使用者察觉 没办法达到显示器上 最大帧速率的工作量 你可以让帧滑顺且平整 只要做 一些微小的调整 改变你的应用程序 呈现绘图的方式就行了 想想看这个情境 一个游戏正在执行一个复杂的场景 可以以差不多90赫兹的频率更新 然而间断的效果让复杂度出现跳位 但又如此不一致 导致突然下滑到66赫兹 通过监测你应用程序的图形处理器 你可以对下滑做出反应 处理它的复杂度 手段是刻意晚点呈现你的帧 直到你的场景复杂度 稳定回到较低的状态 现在我们来谈谈 自适应同步的一些最佳作法 显示为固定速率时 当你的应用程序 图形处理器运作 一直都超过 显示器屏幕上的间隔时间时 我们先前建议你放慢你的算图速度 来达到显示器最快刷新速率的 下个因子
一般来说 这表示把你的目标 每秒帧数从60降到30 就像这边这个范例一样
然而 当要呈现到 一个自适应同步显示时 我们也改变了导引 你应该反而试着去让帧 以最高的速率呈现 你的应用程序 可以流畅地达成这个目标 除了可以流畅地呈现每个帧 请记得如果你的帧 呈现的速率低于最低速率 而显示器无法支持的话 显示可能会 无法让新的帧运作 可能会导致你的应用程序剧烈晃动 但只要你在支持的范围里 你可以随意选择 最适合你应用程序的速率 既然你已经相当了解 Mac今年带来的全新显示支持 我们就来谈谈如何在你的游戏里 启用自适应同步 首先你需要一台支持的Mac 任何拥有Apple 芯片图形处理器的 Mac都会有不错的效果 我们也支持大部分近期 使用英特尔芯片的Mac 再来 你需要一台 支持自适应同步的显示器 用以启用自适应同步的模式 可以通过选择全新的 变动刷新率来达成 在显示器的系统偏好中可以找到 最后 你们应用程序 需要以全屏模式运行 我们来看看你可以在你的应用程序 呼叫哪些应用程序接口 以侦测 是否它应该执行自适应同步的排程 首先 你需要搞清楚 你选用的显示器是否 有办法执行自适应同步的排程 NSScreen今年为此加入了全新的性质 也就是minimumRefreshInterval 以及maximumRefreshInterval 这些值告诉你 有效的屏幕时间范围是多少 以呈现在这个显示器上面的帧 在固定帧数的显示器上 这些值都会是相同的 所以一个简单但不完全相符的比较 会告诉你是否这个屏幕 开启了自适应同步模式 接下来你会需要知道 你的窗口现在是否是全屏模式 检查窗口里的styleMask就能查到了
记住 你需要结合这两个检查 来确保你的应用程序 有办法利用到自适应同步排程 很好 那么现在你已经会处理 自适应同步显示以及macOS提供的 全新应用程序接口以侦测到它们 我们来看看我们要如何调整 现有的Metal呈现技术 来流畅地呈现在自适应同步显示上
你可以使用我们的MetalDrawable 应用程序接口 内建了帧步调 像是PresentAfterMinimumDuration 或是presentAtTime 对于自适应同步的显示都很有效 或者是你也可以呼叫现在呈现 来加入你的解决方案 以及你的自定义定时器 我们来看一些不一样的建置 是如何运作的
我们以一个简单的例子开始 我们要在这边获取一个绘图 设置好图形处理器的作业 并把它呈现在屏幕上 我们依赖一个绘图对象的后压可用 来设定我们的帧速率 在固定速率的显示上 我们知道这不是最好的主意 因为这不代表你的图形处理器作业 一定会符合显示器的刷新率
但你可以看到这个自适应 同步显示完成的Instruments撷取 当我们的场景是前后一致时 看起来运作正常无碍 问题是这个场景执行的时候 碰到了间断发生的顿点 顿点就会变成用户不断看到的停顿 我们试着用固定平均的速率呈现 来修复这个问题 这个技术也可以使用在你想要建置的 用户可调的帧每秒浮动块上 提供给你的游戏玩家 我们在这里把频率设定为78赫兹 不使用单纯的呈现呼叫 而是呈现afterMinimumDuration 给这个绘图并且规定 我们上面定义过的间隔时间 我们可以看到这里的帧顺畅地呈现 而且依照我们要求的速率 我们没有像上个例子一样快速呈现 但是你的使用者 反而几乎不会碰到停顿 而你的应用程序会花更少时间 使用中央处理器与图形处理器 好的 这里就有趣了 我们来试一个方法 会制作步调一致的帧 而不需要设定单一固定的速率 其中一个达成方法 是计算图形处理器工作的滚动平均 要多少才能制作每一个帧 然后回送给我们 presentDrawable的呼叫 从第一帧开始我们要加载一个起始值 给averageGPUTime 我选择乐观一些 设最快的速率为目标 达到显示器可以支持的上限 对于我们的平均这只是一个开始而已 所以我们做的任何合理推测都可以 现在我们加上CommandBuffer的 完成处置在这里 来估算图形处理器要花多少时间 给这一帧算图 然后把这个时间值 加到我们的滚动平均上 首先我们要取得图形处理器 花了多少时间完成我们的工作 之后我们会把新的时间 整合到滚动平均里 我们呈现下一帧时会用上它 这边就是结果 你可以看到我们呈现的速率 跟上个例子很相似 但这个限制是靠着 我们产出的上一帧来决定的 会产生同样的帧速率 在Mac图形处理器上的一个范围内 我们可以在这边看到同一个程序 以48赫兹的速度顺畅地执行 在一台性能较差的Mac上 而不需要对代码多做更动
好的 你现在知道 一些新的工具与技术 可以用来在自适应同步显示时 优化你的应用程序 如果你想更了解 macOS上的自适应同步显示 可以参考Apple Developer的网站上 全新的Metal范例计划 来了解如何使用Metal 传达高效能的体验 请参考WWDC前几年的谈话内容 现在我要把你交给艾利克斯 你将能学到更多 iPad Pro上帧步调的知识 谢谢你 凯尔 接着 我们来谈谈ProMotion 自2017年开始 每台iPad Pro 都配备了ProMotion的显示技术 能够提供高达120赫兹的刷新率 然而120赫兹在某些情况下 可能无法使用 包括用户调成低电量模式的状态 这个功能今年才在iPadOS 15上 提供给iPad使用 适当的帧步调能允许你的应用程序 呈现动态内容 正确且又顺畅 不论显示特性 用户偏好、系统状态为何 我们将要看看ProMotion 以及固定速率显示的差异 以及某些帧速率不可用的情形 接着我们会讨论何谓显示链接 以及你的应用程序如何使用它 来驱动自定义绘图 最后我们会提供一些 显示链接的最佳作法 我们就直接进入正题吧 如同刚刚凯尔有稍微提到的一样 固定在60赫兹的显示 每16毫秒会刷新 固定进行校正 它支持了顺畅的内容呈现 其帧速率是60的因子 比方说60赫兹、30赫兹、20赫兹等等 然而当内容比显示刷新率还慢的时候 像是只有30赫兹的话 显示自身仍然要用 同样的校正进行刷新 因此 其他每一帧都是前一帧的重复 这样会耗损到一些电源 相对地 ProMotion 就提供了绝佳的反应能力 刷新率可以高达120赫兹 它也能根据屏幕上的内容进行调整 并减少电源的消耗 我们来看看它是怎么运作的 刷新频率若是最高的120赫兹 显示当然就会每八毫秒刷新一次
因为120是60的倍数 ProMotion支持所有可选用的帧速率 它不仅能提供120赫兹的选择 也可以支持中间的帧速率 给你的应用程序 此外 ProMotion可以动态调整 它的刷新率 所以内容若只需60赫兹即能流畅运作 可以仅16毫秒才刷新一次 而不需要重复 而它的显示 本来需要固定在120赫兹上 就算频率降低到24赫兹也一样
现在有个问题 这些帧速率不会一直都可用 用户可以打开限制帧速率的开关 在辅助功能的设定中 会把最大帧速率限为60赫兹 还有 当装置变烫的时候 系统可能会 应用限制到120赫兹的可用性上 通过iPadOS 15 我们也能在 低电量模式执行60赫兹的最大限度 这些情境如何影响你的应用程序呢? 好消息是大部分应用程序的 运行状态都不会有任何改变 但如果你的应用程序运行 一帧一帧的自定义绘图 你可能就需要注意 这些帧速率的变化了 我们会向你展示该如何做 自定义绘图推荐的工具便是显示链接 其实就是一个定时器 与显示刷新率同步而已 它会帮助你的应用程序驱动 任何自定义动画或是算图循环 这里有两个显示链接 一个是macOS上的CVDisplayLink 来自Core Video 及CADisplayLink 来自CoreAnimation兼容其他平台 还有macOS的Catalyst 每个都有些微不同的特性及行为 今天我们只会讨论CADisplayLink 但在更高的层次上 这些概念都会应用到两个上面 CADisplayLink每碰到vsync 就会唤醒并引动回呼 这提供给应用程序整整八微秒 来完成自己的工作
一个规律的定时器 像是NSTimer 不太可能 完完全全跟显示同步 会产生异相或是漂浮 所以有时应用程序可能没有 足够时间完成工作 所以会导致失帧 你已经看到CADisplayLink 如何提供稳定的时间点 这里还有它的其他附加好处 它可以在比显示刷新率 更低的情况下运行 要这么做的话 你的应用程序利用 preferredFramesPerSecond提供提示 我们会替你选择最靠近的可用帧速率 当帧速率的可用性改变 也就是我们上面讨论的状况时 CADisplayLink会自动偷偷调整速率 当然 这提供给你的应用程序 必要的时间点信息 所以你的自定义绘图 会察觉到这些改变 我们不会深究如何写出自定义动画 或是自定义算图循环 但我们会提供给你四个行业最佳作法 来协助你的自定义绘图 与显示的时间点同步 避免常常会出现的圈套
首先 通过运行时来查询显示刷新率 是很重要的 而不是硬写下去 再来 你通常应该使用 CADisplayLink自己的帧速率 接着 使用targetTimestamp 来预备绘图能帮助减少顿点 最后 替意外预作预备总是好事 作法是不断计算时间差值 我们一个一个来看 最大的显示刷新率 可通过UIScreen查询 ProMotion显示器上的回复 一定是120赫兹 就算低电源模式开启 之类的状况下也是 此外 CADisplayLink其实会 提供每帧之间的最短间隔时间 通过长度的性质 它会参照目前装置的状态 去作动态更新 但绝大多数的时间 你都应该使用目前的帧信息 直接从CADisplayLink提取 因为显示链接可能会比 最大的显示刷新率跑得还慢 还有 帧速率的可用性取决于硬件 实际的帧速率可能会 由于链接本身产生动态改变 来响应系统状态的变化 我们来看一个例子 假设我们请求了 一个40赫兹的显示链接 如你所见 在ProMotion的显示器上 40赫兹是支持的 然而 在一个60赫兹的显示器上 或当ProMotion被限制在60赫兹时 显示链接会自动把自己调整成30赫兹 这确保了良好的校正 因为每个唤醒都是靠着某个vsync 它尝试给每一帧一样的时长 如果我们要使用40赫兹的NSTimer 它不会侦测帧速率的话 它的唤醒可能刚好就在vsync的 间隔时间中间 我们当然无法呈现任何帧 所以你很有可能会看到 你的自定义绘图中发生顿点 那么代码看起来是什么样子呢? 这里可以看到 你通常如何设定显示链接 首先 你要提供一个目标 跟一个选择器 它就是引动的回呼 接着提示偏好的帧速率40赫兹 使用preferredFramesPerSecond 接着你添加显示链接 到目前的运行回路上 回呼便会由它所引动 所以在回呼中 你可以得到预期的间隔时间 在显示链接唤醒之间 把timestamp 从targetTimestampby减掉就可以了 间隔时间不会永远是一秒40次 因为显示链接本身 可以在不同的频率下运作 接下来我们来谈谈这些timestamps CADisplayLink主要有两种timestamps Timestamp是指当回呼已经排程好 要被引动的时间 而targetTimestamp 则是CoreAnimation 何时会复合下一帧 我们会一步步看一个例子 它会告诉你为什么应该用 targetTimestamp来预备你绘图 这边有个动画 它的时域已经常态化成0到1 假设我们要以最高的帧速率为目标 目前这个数字是120赫兹 CADisplayLink会唤醒 而如果我们要预备 使用timestamp来进行帧的呈现 我们会直接在这抽样 在下个vsync就会呈现 如这里所见
同样的过程继续下去 我们会看到它的校正相当良好 每个120赫兹的帧 都会让我们的动画进度增加0.05 现在假设温度的状况变化 120赫兹不再可用 显示链接现在就会再度唤醒 应用程序预备动画 而进度数值是0.4 下次vsync就会在这里呈现
同样的样态会持续下去 有的时候转换在这边看起来不太对 我们看到进度增加了0.05 但有一个超过了八毫秒 另一个则超过16毫秒 非常显而易见 如果我们测定进度对上时间 我们就会在转换时看到跳了一下 这对用户来说就是一个可察觉的顿点 那不是我们想要的
现在我们来试试targetTimestamp CADisplayLink在这里唤醒 进度在targetTimestamp取样 数值是0.15 同样的样态持续下去 我们又看到了不错的校正 在这个帧速率转换点 显示链接唤醒了 在targetTimestamp取样 得到的数值是0.50 持续以同样的方式下去 如果我们画出同一个 进度对上时间的图表 你会看到一直线 因此它能够提供顺畅的内容 就算帧速率改变也一样 所以应该要使用targetTimestamp 而不是timestamp来预备你的绘图 在你的代码里 一般该是简单的 跟你使用targetTimestamp 来取代timestamp的使用一样 最后我们来讨论一下 动态计算时间的差值 targetTimestamp跟timestamp的差异 在显示链接回呼之间 给了你期望的时间长度 但实际的时间长度可不一定 高优先的线程 可能排程在中央处理器上 或是运行回路正忙于处理其他东西 极端的例子中 回呼可能会完全跳过 所以在这种情况下 有件特别重要的事 那就是 依然保存正确的时间点在你的 自定义绘图里 以带来最佳用户体验 当CADisplayLink的回呼引动时 应用程序就执行它的工作 来准备好更新或是必要的算图 给下一帧 通常回呼引动的时间点 都刚好是排程的唤醒点 但也不是毫无例外 我们预期下一个回呼会在这里引动 然而显示链接没有机会运行 直到vsync间隔时间的几毫秒之后 因此你不会得到完整的八毫秒 在这个例子里你可以查询 CACurrentMediaTime 把它跟targetTimestamp比较 来得知有多少时间可用
现在假设这一帧的工作花了太长时间 下一个回呼不会被引动 直到运行回路畅通为止 因为这一个产生延误 下一个回呼就被跳过了 当你准备要前推 你自定义绘图的进度时 在这个回呼里要小心 你该使用的时间差值不是八毫秒 而是16毫秒 如果你要追踪上一个timestamp的话 这个数值是 你自定义绘图状态更新的数值 因此如果你的应用程序 使用了时间差值 来提前你自定义绘图的状态 这就会让你的自定义绘图减慢一帧 每一次你的回呼被跳过的时候 你可以反其道追踪上一个 targetTimestamp 你就可以把状态正确地前推了 如果你的自定义绘图工作量很大 你可以查看targetTimestamp 来尽可能减少工作量 以达成期限的需求
总结一下最佳作法 别光猜显示刷新率 请总是在运行时间查询 你的自定义绘图应该要 在支持的帧速率保有弹性 并准备好适应不同的速率 使用targetTimestamp来确保 没有顿点的帧速率转换 要小心注意突发状况 像是缺少显示链接回呼 我们整理一下 这一节的前半段我们讨论了 如何优化你应用程序的 帧步调 当你在macOS上的 自适应同步显示器运行时 第二部分我们解释了一些最佳作法 帮助你的应用程序驱动自定义绘图 并维持顺畅的帧步调 不论是何种情形之下 在iPad Pro的ProMotion显示器上 随着显示科技持续进步 我们希望这一节内容提供你 不只是想法的东西 还有工具及最佳作法 来支持变化越来越大的显示时间点 感谢你共襄盛举 祝你收看WWDC其他内容愉快 [音乐]
-
-
5:51 - Is Adaptive-Sync scheduling enabled
// Detecting an Adaptive-Sync display - (BOOL) isAdaptiveSyncSupported:(NSScreen *)screen { NSTimeInterval minInterval = screen.minimumRefreshInterval; NSTimeInterval maxInterval = screen.maximumRefreshInterval; return minInterval != maxInterval; } // Detecting full-screen - (BOOL) isWindowFullscreen:(NSWindow *)window { return ([window styleMask] &= NSFullScreenWindowMask) == NSFullScreenWindowMask; } // Tying it all together - (BOOL) isAdaptiveSyncSchedulingEnabled:(NSScreen *)window { NSScreen* windowScreen = [window screen]; return [self isWindowFullscreen:window] && [self isAdaptiveSyncSupported:windowScreen]; }
-
6:49 - Leverage Drawable present calls
// Drawable present APIs with frame-pacing [commandBuffer presentDrawable:drawable afterMinimumDuration:interval]; [commandBuffer presentDrawable:drawable atTime:t]; // Drawable present API without frame-pacing [commandBuffer presentDrawable:drawable];
-
7:11 - A simple example
id<CAMetalDrawable> currentDrawable = [metalLayer nextDrawable]; // Your encoder and command buffers here [commandBuffer presentDrawable:currentDrawable];
-
7:55 - Adaptive-Sync in your app 1
id<CAMetalDrawable> currentDrawable = [metalLayer nextDrawable]; NSTimeInterval userFramerateCap = 78.0; NSTimeInterval userInterval = 1.0 / userFramerateCap; // Your encoders and command buffers are still here [commandBuffer presentDrawable:currentDrawable afterMinimumDuration:userInterval];
-
8:43 - Adaptive-Sync in your app 2
id<CAMetalDrawable> currentDrawable = [metalLayer nextDrawable]; // Your encoders and command buffers are still available! NSTimeInterval averageGPUTime = screen.minimumRefreshInterval; [commandBuffer presentDrawable:currentDrawable afterMinimumDuration:averageGPUTime]; [commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> buffer) { const NSTimeInterval GPUTime = buffer.GPUEndTime - buffer.GPUStartTime; // Use an exponential moving average const double alpha = .25; averageGPUTime = (GPUTime * alpha) + (averageGPUTime * (1.0 - alpha)); }];
-
15:36 - Query the display refresh rate at runtime
// Maximum frame rate from UIKit NSInteger maxRate = [[UIScreen mainScreen] maximumFramesPerSecond]; // Current maximum frame rate from CoreAnimation NSInteger currentMaxRate = round(1 / link.duration);
-
17:06 - Use the actual frame rate of the CADisplayLink
CADisplayLink *link = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)]; [link setPreferredFramesPerSecond:40]; [link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; - (void)displayLinkCallback:(CADisplayLink *)link { CFTimeInterval interval = link.targetTimestamp - link.timestamp; //... }
-
21:47 - Dynamically compute the time delta 1
- (void)displayLinkCallback:(CADisplayLink *)link { progress += link.targetTimestamp - link.timestamp; [self renderAnimationWithProgress:progress]; }
-
21:57 - Dynamically compute the time delta 2
- (void)displayLinkCallback:(CADisplayLink *)link { progress += link.targetTimestamp - previousTargetTimestamp; previousTargetTimestamp = link.targetTimestamp; [self renderAnimationWithProgress:progress]; }
-
22:08 - Dynamically compute the time delta 3
- (void)displayLinkCallback:(CADisplayLink *)link { progress += link.targetTimestamp - previousTargetTimestamp; previousTargetTimestamp = link.targetTimestamp; [self renderAnimationWithProgress:progress withDeadline:link.targetTimestamp]; }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。