Frame Pacing

news2024/11/27 17:57:49

Frame Pacing是每个游戏都要遇到的问题,这里面有很多细节值得探讨。

为什么需要做Frame Pacing?

从我们的游戏线程渲染一帧到最终屏幕上绘制出一帧不是一个概念,这种间会经历CPU,GPU,屏幕合成器等多个角色的协同工作,我们追求三个目标,在给定的渲染帧率下

1.  尽量保证每个游戏线程渲染的帧都能成功在屏幕上展示,否则就是发生了弃帧,弃帧相当于对功耗的白白浪费

2.  尽量保证屏幕上每一帧持续的持续的时间一致,否则就是发生了帧率不稳或者长短帧,这导致用户感受到不顺滑

3.  尽可能缩短从CPU提交到屏幕呈现的时间差,即减少延迟。

通常我们无法保证游戏过程中上述三个目标达到100%,但是需要尽量达到。由于CPU的逻辑线程,渲染提交线程,GPU,屏幕合成器都处于异步工作的状态,我们需要协调好他们的工作步骤,这也让pacing这个单词很形象,即让每个独立工作的人在一起又能踩到合适的点上。

渲染、提交、展示的工作流程

以Android系统为例子

这里面有四个角色在工作:

1. 在第N帧,CPU渲染一帧出来,将图像绘制到系统给的其中一个buffer上,又称为back buffer,(这里还没有考虑CPU上本身还有多线程渲染,这里假设CPU在单线程上通过逻辑计算,渲染,并提交出来一帧,最后的一个API调用如glswapbackandfrontbuffer或者VKqueuepresentkhr)。

2. SurfaceFlinger在垂直同步周期N+1开始后,如果检测到在垂直同步周期N或更早有一个有效的提交,就会启动帧合成工作,所谓帧合成工作是将当前提交的back buffer同其他系统界面合成到一个准备用于呈现给最终屏幕的 Buffer上(这个 Buffer用于再下个垂直同步周期开始时被刷到屏幕上,即设置为Front Buffer)

3.  GPU从CPU到提交开始就要工作,完成前面所有的buffer的绘制工作,通常要比CPU晚一个周期

4. 在垂直同步周期N+2开始的时候,如果这个Buffer被GPU处理完成,就可以被刷新到屏幕上。(我们知道系统会按照给定的频率刷新屏幕显示内容,这个刷新的间隔就是垂直同步时间,android系统可能同时支持多种刷新率,典型如60HZ,90HZ,120HZ,144HZ等,不能在屏幕使用front buffer刷新的时候去更改这个front buffer,surface flinger恰好为我们做了这个事情,在给定的垂直同步时间下,surface flinger会在固定的刷新时间点来临之前把某个完成的back buffer交换为front buffer,供屏幕刷新)

上图中,从CPU present到可以看到这一帧会经历2个垂直同步周期的延迟。

SurfaceFlinger的合成与弃帧

在上面这个流程中,有几个问题需要注意:

1. CPU必须要在垂直同步周期开始前至少t 纳秒前提交完成,才能被下个垂直同步的SurfaceFlinger合成,这个时间叫做PresentationDeadlineNanos,如下图,如果提交超过这个阈值,就得等到下下个周期才被合成。渲染上实际上就多延迟了一个同步周期

 

2. 如果在一个垂直同步周期内,SurfaceFlinger发现了大于1个back buffer被提交,那么他只会选择1个进行合成,而其他的被丢弃,因为合成更多的已经没有意义。并且Surface flinger合成的时机也是固定的,都是在每个垂直同步周期开始后的较短时间内。

如下图所示,Frame1实际上被丢弃,但是GPU还要处理Frame1,产生了功耗上的浪费。

back buffer的资源竞争和生命周期

关于Back Buffer的数量,系统的Back Buffer存在一定的数量限制,典型的系统有3个,也有2个,或更多。Buffer的生命周期如下

这是一个典型的生产者-消费者模式,应用程序APP生产(提交,如调用VKqueuepresentkhr)一个需要GPU填充的backbuffer。Surface Flinger发现了之后消费它,accquire这个buffer,调用它的present方法将它同其他系统界面合成到一个新的待呈现的buffer,cpu和gpu都使用完成后将其release,APP一侧下一帧通过dequeue buffer(如调用vkacquirenextimagekhr)找到一个released。

资源不足会导致某些工作受到阻塞:

最典型的例子是GPU瓶颈,导致CPU被阻塞,如下图

CPU的帧率可以达到120fps,而GPU只有60fps,假设系统有3个backbuffer,从第3个CPU帧开始,cpu就不能dequeue到一个可用的buffer了,因为1个buffer处于frame1 的 queued状态,1个buffer处于frame2 的acqured状态,另一个被用作front buffe。这会产生GPU对CPU的倒逼,将CPU也强行阻塞为60fps,在profile上我们将看到CPU的RHI线程大多数会卡顿在vkacquirenextimagekhr(对于gles来说,一般下一帧的acquire可以发生在前一帧的的present后,所以gles的app会看到卡顿在eglswapbuffer上)。无论是卡顿在vkacquirenextimagekhr或是eglswapbuffer都是典型的GPU瓶颈。

当然上面的这种倒逼情形是没有副作用的,因为它也满足我们最开始的两条目标,没有弃帧,帧呈现时间也是一致的,都是均匀的16.66ms。

渲染的提交时机和节奏

在Android系统上,Surface Flinger能够帮我们保证不出现因Front Buffer被同时读取刷新和修改而产生画面撕裂的问题,back buffer数量导致的倒逼机制可以使GPU瓶颈时CPU也不会过度提交,那我们是不是可以随意提交?显然不行。

1. 首先还是看上面的GPU瓶颈情形下,只要我们将初始提交发生一点时间上的位移,情况就会变得大不一样。这个从结果上看,刚好一半的帧被丢弃了,用户的实际接收帧率只有30fps,而不是预想的60fps。发生了什么?

我们不考虑第一帧,在第3,5,7帧都被丢弃了,第3帧被第3个垂直同步tick到进入surface flinger合成的时候,因为第二帧还在gpu处理中,所以不能进行,要推迟到下一个垂直同步,下一个垂直同步4开始时,又发现第4帧已经被提交了,那么这里有待合成的3和4,则帧3被丢弃掉了,开始合成frame4,依次类推下去。

这显然是个灾难,原因仅仅是因为我们提交的时机有所不同,出现这个问题的真实原因是我们不应该在前一帧的GPU还没处理完成的时候就进行下一帧的present。

所以提交时机很重要,如果我们能够保证等待前一阵GPU处理完才真正的提交,上面的情形就会被纠正过来,如图。可以看到除了前2帧有点问题外(第二帧持续了2个周期,弃掉了第一帧),从3帧开始就被纠正了过来。这个只是在最终发生prensent操作之前强行等待上一帧的GPU完成。这个例子表明CPU提交的时候要在乎gpu当前的状态。

 

2. 上面是典型的GPU瓶颈,如果是CPU瓶颈,又可能出现什么样的情况?假设在60hz下,GPU能跑满60,而CPU只有45fps,最终看上去的情况就可能是这个样子的,可以看到1,3帧在屏幕持续了1个周期,2,4..帧持续了两个周期,没有弃帧,但是帧呈现时间是不一致的,出现了long-shot frame现象,也就是说用户感受到的45帧实际是不平顺的45帧,这个问题的原因在于实际帧率不能被刷新率整除。

在这种情况下,对于很多游戏,可能平顺的30帧要比不平顺的45帧感受更好,并且节能,那不如将CPU的提交控制在30帧,当然对于平稳性和高帧率哪个更重要,是需要case-by-case的,还有一个折衷的方案是为游戏限制一个最小的阈值帧率如30帧,当帧率小于30时,帧率优先,档帧率高于30时,平稳优先。这个例子表明提交的时候还要在意实际帧率同刷新率的关系。

3. 如果是没有典型瓶颈,而CPU/GPU时间在给定垂直同步周期之内浮动情况(也就是我们在app侧认为游戏帧率能达到满帧)会有哪些情况发生呢?假设你的CPU和GPU帧率都能达到60,然后我们放任CPU随意提交。

下图是一种很有可能出现的情况,CPU\GPU都在60fps上下浮动,其中第4帧的CPU稍微高了一点点,它提交的时候超越了下帧垂直同步的deadline时刻,导致第4帧的surfaceflinger不能空闲,推迟到第5帧,直接导致了在最终用户屏幕上Frame3持续了2个周期,出现了长短帧。

又因为第4帧的影响导致第5帧提交也超出了dedaline,再加上第6帧的cpu跑的有点快,相对过短,这样6同5都在一个垂直同步周期内被提交,最终又导致frame 5被丢弃掉,没呈现到屏幕。

这样看来,如果放任cpu随意提交,即使理论上我们认为他能跑满60帧,也会导致各种丢帧,长短帧的事情发生,用户感受上不平滑,并且实际没有60帧。

对于上面这种情形,我们稍微改变下提交节奏,事情就会变好。

我们把帧5 和帧6这两个快帧的后面稍微延迟一点时间,延迟到这帧接近垂直同步结束时再提交,就会使帧4的影响不会传递到后面去,即除了第3帧的图像在屏幕多存续一个周期外,后面的帧都很正常。

正确的节奏

上面的一些例子说明,提交的时机和节奏的确影响最终的帧率,我们计算一个游戏的帧率不是简单的用1000/max(cpu,gpu),而是看最终屏幕上给了多少帧,还要关注他们是否均匀给出。

要实现文章开头所说的3个目标,我们需要在正确的时机进行提交,垂直同步是一个无情的准时发生的机器,我们需要自己去找准这个时间,为了找准这个时间, 在android系统上我们需要关注的还有当前系统的刷新率,vsync的发生时间,surfaceflinger的工作时间(包括它present的deadline,上次present的时间),APP自己的cpu时间和gpu时间。

当我们知道了这些信息后,我们就可以制定一个较好的提交策略,考虑到上述的一些例子,这个策略至少包含一下几点:

1. CPU发生present的时候要保证前一帧的GPU完成,不然会导致即将到来的surface flinger不工作。(这需要我们知道GPU完成的fence时间)

2. 充分考虑高帧率和平滑性的关系,追求帧率平顺,就应该尽量让垂直同步周期为CPU提交间隔的整数倍,在60刷新率下渲染45帧一定是长短帧的,除非游戏更加追求高帧率。(这需要我们知道当前的刷新率,以及短期内的CPU,GPU时间)

3. 最重要的是,如果前一帧在第N个垂直同步周期内提交完成,那么CPU发生下一个present的时候,延迟到N+1个垂直同步开始之前的一个时间点T(n+1)时进行,这个T(n+1) = 第N+1帧的垂直同步开始时间-系统的PresentationDeadlineNanos。(这需要我们知道surfaceflinger的工作时间)

保证这一点非常重要。

为什么提交的时间点要是这个T?

如果提交过早发生,极有可能导致当前帧挤到上一帧的垂直同步周期内,而把提交时间推到这个时间点,恰好保证它刚刚可以被下一个垂直同步内的surface flinger消化(因为如果再晚就错过了deadline,要再延迟一帧了)

Choreographer和Swappy

为了让我们获取到surface flinger和vsync发生的精确时间点,android的choreographer库为我们提供了这些机制,它会基于准确的vsync发生的时机,给APP定期的回掉,让我们知道一个新的垂直同步周期到来,以决定我们需要wait到什么时候进行present操作。

正如choreographer到英文翻译“编舞者”一样,我们利用它编排CPU的渲染提交步调,让CPU,GPU,Surface Flinger,Vsync能够和谐起舞。

不过使用choreographer库有点繁琐,android的game sdk为我们提供了另外一个库Swappy高度封装了choreographer,swappy同时支持gles和vulkan。从使用者的角度,swappy为我们提供了很多种提交策略,包括:

1.自动帧率和固定帧率,自动帧率将由swappy根据刷新率,你的短期历史CPU GPU时间,你允许的最大帧率,计算一个可以被刷新率整除的帧率

2.  它会计算GPU完成的时间,避免CPU提交时上一帧GPU没有处理完

3.  我们不用关心何时present,它会自动将prenset的时机对准到前面所说的垂直同步开始前减去deadline的那个时间

4.  他还允许我们设置一个最小帧率,当fps过低时,放弃稳帧率措施和等待present时间点对齐,而直接提交。

IOS系统和WIndows系统的Frame Pacing

这篇文章到此谈论的都是android系统的frame pacing,android 系统上这个也是最复杂的。ios和windows都有更简单的机制。

对于IOS系统,直接通过系统提供的MtlDrawable上的(void)presentAfterMinimumDuration函数实现。

当一帧结束时,我们用这个函数做prensent,presentAfterMinimumDuration即指的是我们期待的帧率,他会在一些情况下自动为我们阻塞住当前的cpu,控制我们的提交节奏,例如等待前一帧GPU处理完,或者对其到合适的提交点。

至于windows系统上的DX,也是依靠

IDXGISwapChain::Present( UINT SyncInterval,UINT Flags) 这个方法完成的。

这里面的syncinterval是垂直同步间隔,flags则是一些策略。DX上这个syncinterval的概念同移动端不太一样。首先这个syncinterval可以为0,意味着不管垂直同步,即随时都能提交,并交换给frontbuffer,哪怕frontbuffer正在被读取刷新,屏幕可能撕裂,但是手机上不会出现,例如android上,因为surface flinger的存在时一定不会存在所谓的不开垂直同步的。如果syncinterval为非0的值n,意味着至少距上次间隔n次垂直同步将画面刷新上去。

利用Systrace 工具找到frame pacing问题

各种系统都提供了录制system trace的工具,我们可以利用它随时找到游戏的帧率不稳或者丢帧问题。例如android 的systrace,里面可以看到每帧的提交,合成情况,以及是否发生了丢帧等,如果使用了swappy,还能看到swappy为我们做的wait prensent时间点的措施,以及cpu,gpu的时间。

如下图是一个例子,可以看到最上面的是ue4 rhi线程的提交,即CPU一侧的提交,它的标志性函数是vkqueuepresent,但是因为集成了swappy,所以调用的是swappyvk的prenset,这里可以看到swappy调用了很多wait函数来推迟我们的提交时间点。

下面是surface flinger在固定的垂直同步周期时间点上进行帧合成,在下面还能看到swappy统计到的cpu和gpu时间,最下面一行时垂直同步间隔。

其中很重要等一行PrevFrameMissed则表征了弃帧的发生,我们要关注frame missed的数据,以及时找到游戏帧率不稳定的原因。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/590727.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【xv6操作系统】安装、运行与调试

一、构建、装入过程 1.编写“启动代码主体代码”(在下载的xv6的原始代码上进行修改) 2.源代码进行编译、链接生成系统镜像(elf格式的目标文件) 3.将系统镜像保存起来(如保存到磁盘、flash或者网络服务器上&#xff…

上海斯歌K2 x 赛博威 | 战略合作深度交流暨签约仪式

2月16日,上海斯歌K2与赛博威进行了战略合作深度交流,并在赛博威广州科学城办公室举办战略合作签约仪式。 为满足客户在数智化建设过程中的多元化需求,上海斯歌K2与赛博威曾多次产生交集。凭借双方多年合作的良好基础,自2022年始&a…

【C++】右值引用和移动语义(详细解析)

文章目录 1.左值引用和右值引用左值引用右值引用 2.左值引用和右值引用的比较左值引用总结右值引用总结 3.右值引用的使用场景和意义知识点1知识点2知识点3知识点4总结 4.完美转发万能引用见识完美转发的使用完美转发的使用场景 1.左值引用和右值引用 传统的C语法中就有引用的…

【C++】引用 - 基本语法,注意事项,函数参数,函数返回值,本质

文章目录 1. 引用的基本语法2. 引用的注意事项3. 引用做函数参数4. 引用做函数返回值5. 引用的本质6. 常量引用 1. 引用的基本语法 作用是:给变量起别名 语法:数据类型 &别名 原名 2. 引用的注意事项 引用必须初始化引用在初始化后,不…

量子计算:基本概念

选了课程 《量子计算与量子信息》,没学过量子力学的博主实在是听不懂啊 (ㄒoㄒ) 简略整理了下 可能大概也许 明白一二都没有 的课程最开始两节的内容,如有错误欢迎指出 ~ ~ ~ 文章目录 矩阵论复空间中的矩阵矩阵上的运算 量子力学量子态基本假设 量子计算…

阿里云的内容识别技术可以实现哪些场景下的智能化应用?

阿里云的内容识别技术可以实现哪些场景下的智能化应用? [本文由阿里云代理商[聚搜云]撰写]   随着人工智能技术的快速发展,阿里云借助自身的技术和资源优势,开发了一种名为“内容识别”的技术。这项技术能够高效、准确地识别出图片、视频、…

有个规划文档,会让软件开发更有效

有个规划文档,会让软件开发更有效 中小企业,业务部门不太清楚软件生产过程 软件生产有一定的抽象和复杂性 要形成一个共识 趣讲大白话:要有点整体观 【趣讲信息科技181期】 **************************** 2019年整理出了一个目录框架 用在很多…

windows的cmd命令窗口介绍

1.打开cmd 1.1.方式一 左下角搜索:“运行” -> 打开 输入"cmd" -> 确定 1.2.方式二 直接使用快捷键 windows r 即可打开 然后输入cmd,点击确认 1.3.方式三 打开文件管理器,输入cmd,回车 即可在该文件路径下…

统计软件与数据分析Lesson16----pytorch基本知识及模型构建

统计软件与数据分析Lesson16----pytorch基本知识及模型构建 0.上节回顾0.1 一元线性回归数据生成数据处理初始数据可视化 0.2 梯度下降Gradient DescentStep 0: 随机初始化 Random InitializationStep 1: 计算模型预测值 Compute Models PredictionsStep 2: 计算损失 Compute t…

让进程能够“相互沟通”的高级方式一:匿名管道

代码运行及测试环境:linux centos7.6 在阅读这篇文章时,需要掌握OS对文件管理的基础知识(文件打开表、文件描述符、索引结点…) 前言 我们都知道进程是具有独立性的,意味着进程之间无法相互通信。但在一些情况下&…

当我们谈笔记的时候,我们在谈什么

文章具体内容如图,感谢妙友分享好文🎉 本篇内容来源于网站Untag Minja 上传的内容《当我们谈笔记的时候,我们在谈什么》 如有侵权请联系删除!

如何搭建与使用FTP服务器

一、概述 目前用作搭建FTP服务器端的软件有很多,比如 Vsftpd、ProFTPD、PureFTPd、Wuftpd、ServerU、Filezilla Server等,这里使用Vsftpd进行搭建。 vsftpd 是“very secure ftp daemon”的首字母缩写,它是一款在Linux发行版中最受推崇的免…

应用到vscode

随着ChatGPT的热度持续上升,我们也不得不深刻认识到,这已经势不可挡了。我们必须去接受它,甚至是拥抱它。 私信我可以获取最新包 今天呢,我们要介绍的是vscode的一款插件,叫做ChatGPT: 使用方式 安装完成…

Xshell安装使用教程

简介 Xshell 是一个强大的安全终端模拟软件,它支持SSH1, SSH2, 以及Microsoft Windows 平台的TELNET 协议。Xshell 通过互联网到远程主机的安全连接以及它创新性的设计和特色帮助用户在复杂的网络环境中享受他们的工作。 Xshell可以在Windows界面下用来访问远端不…

【C#图解教程】第三章 C#编程概述 笔记总结

程序实例 命名空间一组类型声明 using system;表示使用类型库中的system中的所有类 namespace Myspace{}则会创建一个新的命名空间,这个空间中可以声明该空间的类 SimpleProgram中使用了两个命名空间。先是使用了System命名空间中定义的console类&…

【Linux】项目自动化构建工具-make和Makefile 的使用和进度条的实现

文章目录 一、什么是make/makefile二、如何编写makefile三、make 的工作原理1.make的使用2.make的依赖性3.项目清理4..PHONY伪目标 四、Linux第一个小程序 -- 进度条1.\r&&\n2.行缓冲区概念3.进度条process.hprocesstest.cmakefile 一、什么是make/makefile 什么是mak…

【c语言习题】使用链表解决约瑟夫问题

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; &#x1f525;c语言系列专栏&#xff1a;c语言之路重点知识整合 &#x…

在 Shell 脚本中使用 `exec` 命令的方法和用途

在 Shell 脚本中&#xff0c;exec 是一个非常有用的命令&#xff0c;用于执行命令或替换当前进程。它允许您在脚本中启动新的进程&#xff0c;并将控制权完全转移到新进程。本文将详细介绍在 Shell 脚本中使用 exec 命令的方法和用途。 什么是 Exec 命令&#xff1f; exec 是一…

python基础----02-----字面量、变量、数据类型及其转换、标识符以及字符串、数据输入(input语句)

一 字面量 1 字面量 字面量&#xff1a;在代码中&#xff0c;被写下来的的固定的值称之为字面量。类似C/C的字符串常量。 1.1 常见的python值类型 Python中常见的值类型&#xff1a; 实际上在C/C字面量和这里的类型还是有区别的&#xff0c;体现在内存存储中&#xff0c;字…

JavaScript正则表达式

1.介绍 2.语法 3.元字符 4.修饰符 目标&#xff1a;学习正则表达式概念及语法&#xff0c;编写简单的正则表达式实现字符的查找或检测。 一、介绍 1.什么是正则表达式 正则表达式&#xff08;Regular Expression&#xff09;是用于匹配字符串中字符组合的模式。&#xff08;…