Frame Pacing

部署运行你感兴趣的模型镜像

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的数据,以及时找到游戏帧率不稳定的原因。

您可能感兴趣的与本文相关的镜像

ACE-Step

ACE-Step

音乐合成
ACE-Step

ACE-Step是由中国团队阶跃星辰(StepFun)与ACE Studio联手打造的开源音乐生成模型。 它拥有3.5B参数量,支持快速高质量生成、强可控性和易于拓展的特点。 最厉害的是,它可以生成多种语言的歌曲,包括但不限于中文、英文、日文等19种语言

### Frame Pack Usage in GUI Programming In graphical user interface (GUI) programming, `pack` is a geometry manager used to organize widgets within their container frame or window. The `pack()` method arranges the widget according to options specified by the programmer and automatically adjusts when resizing occurs[^1]. The following example demonstrates how to use the `Frame` widget with the `pack` geometry manager: ```python import tkinter as tk root = tk.Tk() root.title("Pack Example") # Create a frame inside root window frame = tk.Frame(root) # Add label into the frame label = tk.Label(frame, text="This is an example of using pack.") label.pack(side=tk.TOP, pady=10) # Configure button placement relative to the frame button1 = tk.Button(frame, text="Button 1") button1.pack(side=tk.LEFT, padx=5) button2 = tk.Button(frame, text="Button 2") button2.pack(side=tk.RIGHT, padx=5) # Place the frame itself into the main application window frame.pack(padx=20, pady=20) root.mainloop() ``` #### Key Parameters for `pack()` Several parameters control the behavior of packed elements: - **side**: Determines which side of the parent element the child will be placed on (`tk.TOP`, `tk.BOTTOM`, `tk.LEFT`, `tk.RIGHT`) [^2]. - **expand**: Indicates whether the widget should expand to fill any extra space allocated to it. - **fill**: Specifies how to fill the additional space around the widget; can take values like `none`, `x`, `y`, or `both`. - **padx/pady**: Adds padding horizontally (`padx`) or vertically (`pady`) between the widget's border and its content. When encountering issues related to packing frames, common problems include overlapping components due to incorrect configuration settings or unexpected layout behaviors caused by missing essential arguments such as 'side'. To resolve these challenges, ensure all necessary configurations are explicitly defined during each call to `pack`.
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值