Oculus详述『延迟』问题及对应『帧渲染』解决方案
查看引用和消息源请点击:映维网
理解CPU和GPU是如何同步渲染帧是实现最佳性能的关键
(映维网 2019年04月12日)对于延迟,不同的人有着不同的衡量方法。例如,从按下按钮到解码器予以识别的时间;从系统渲染一阵到屏幕进行显示的时间等等。Oculus的测量方法则是,从游戏逻辑采样预测追踪到利用这一游戏状态渲染的一帧呈现在屏幕中的时间。
对于传统的游戏帧,一开始都是采样输入,执行所有逻辑更新,将所有对象渲染到帧;然后,用前置缓存交换后置缓存,从而在屏幕显示全新的一阵。对于为传统显示器设计的游戏,它们可能会尝试维持稳定的帧速率,如30fps或60fps。但丢失的一帧通常会被忽略,因为游戏中的camera位置和旋转与真实世界的显示器位置和旋转隔离。对于VR,丢帧会对用户的舒适度产生严重影响,因为只要渲染世界与现实世界不匹配,沉浸幻觉就会被打破。所以,Oculus提供了一个名为异步时间扭曲(Asynchronous TimeWarp)的系统,利用最近渲染的一帧,并在屏幕显示之前对其进行修改,从而令眼睛视图尽可能接近相应的真实世界方向。
这意味着Oculus的渲染管道存在略微的不同。帧更新循环的第一部分仍然相同:查询输入,更新游戏逻辑,然后渲染场景。但接下来,系统不再是交换缓存,而是在渲染时将帧,以及视图姿态提交给异步时间扭曲,这样系统就可以在最后一刻进行修改以匹配更新的视图姿态。时间扭曲的巧妙之处在于丢帧发生时的情形。时间扭曲并不只是将显示器锁定在最后渲染的内容,它会利用前一帧,但执行与更新视图姿态相同的逻辑,这样即便世界的时间状态已经发生改变,你的视图都能匹配真实世界。
1. VSync & Virtual VSync
垂直同步(VSyncl;Vertical Sync)又可以称为“帧同步(Frame Sync)。这种系统已经出现多年时间,而游戏引擎主要是用它来匹配物理显示器的刷新率。对于VR,由于显示器的实际绘制交给了异步时间扭曲,所以它同时负责VSync。每一阵都需要固定的时间量,所以如果从这一点开始计算,我们可以定义所谓的Virtual VSync(V-VSync),亦即所有游戏处理都可以围绕它进行。
请参阅上面的时间表,它暂时忽略了游戏过程。你可以看到,对于每一帧时间扭曲都需要少量的CPU时间,然后在运行VSync时需要一段GPU时间。因此,在运行V-VSync时游戏必须确保帧可供使用,以便时间扭曲能够处理它们。这当然只是一个简化的模型,目的是为了介绍时间扭曲所需的处理。
2. Simplest Game Loop
最简单的游戏循环都有一个执行游戏逻辑的CPU线程:将渲染命令发送到GPU,然后调用SubmitFrame,亦即等待下一个V-VSync。类似下图:
如你所见,游戏逻辑和渲染发生在一帧长度之内,而且时间扭曲能够立即使用渲染帧。从游戏角度来看,这将涉及最低的延迟。如果你的GPU没有及时完成渲染,时间扭曲将不得不使用最后一帧,并导致渲染帧被丢弃。因为下一帧的CPU工作可以在当前帧的GPU工作仍在运行时运行,所以最终你可能是以全帧速率运行,但会出现多个过时帧。因此,与实际帧速率相比,过时帧的数量是更重要的监控度量。
更糟糕的事情是,GPU渲染时间超过下一个V-VSync。前一帧需要重复使用两次,而下一帧的SubmitFrame调用会被阻止,直至当前帧完成渲染。这为GPU赶上CPU提供了时间,但同时意味当N+1帧最终显示时,这将出现一整帧的延迟。
事实证明,在一个VSync的正常范围内执行全帧渲染是非常难以实现的目标,因为CPU时间、GPU时间加起来需要不到一帧(如72hz时是13.89ms,60Hz时是16.67ms)。实际上,几乎每款游戏都需要更多的时间。因此,Oculus API支持一种名为“Extra Latency Mode(额外延迟模式)”的功能。额外延迟模式令错过这一小窗口变成预期的行为,并始终使用为前一帧提交的帧。所述模式的图例如下所示:
这样做的最大优势是,你可以为CPU和GPU利用完整一帧,所以你可以接近于实现100%的利用率。当然,缺点是丢失一帧延迟。Oculus认为,这样的权衡折中非常值得,乃至于Unity或Unreal 4都默认开启额外延迟模式.
如果一切都按时运行,结果当然是显而易见,但如果CPU或GPU需要更长的时间才能完整帧的渲染呢?实际上,GPU的情况与这样一种情况非常类似:当一帧需要两个以上的V-VSyncs完成渲染时,额外延迟模式没有启用。迟到的一帧将导致下一帧的SubmitFrame调用被阻止。正如在关闭额外延迟模式时的情况一样,当回到预期的帧周期时,你将呈现至少3个高延迟帧(前一个重复帧,当前帧和下一帧)。所以,避免GPU运行过长时间对游戏的流畅度而言至关重要。
CPU的情况没有那么多问题。在启用额外延迟模式时,在V-Vsync返回后立即调用SubmitFrame(假设前一帧已经准备就绪)。例如:
如你所见,果CPU花费的时间继续超过帧时间,GPU最终将花费过长的时间,而SubmitFrame会被阻止。但如果CPU时间减少,游戏将恢复,应用程序将永远不会丢失帧。
3. 多线程应用
尽管单线程应用程序最为简单,但运行Oculus软件的移动设备(Gear VR,Oculus Go和Oculus Quest)都拥有具有多个CPU内核的芯片组。因此,你需要多线程应用来利用这些内核。Unity和UE4都提供了多线程渲染模式。
对于这一模式,主线程执行游戏逻辑,渲染逻辑则由另一个线程执行。所述线程由渲染线程调用SubmitFrame进行同步,因此要等待V-VSync。当V-VSync触发帧开始时,渲染线程向游戏线程发送信号,以便在操作当前帧时可以开始执行下一帧的逻辑。最终效果是,在游戏逻辑和屏幕呈现渲染帧之间发生一帧的额外延迟。这是一个例子:
类似地,如果渲染线程迟到,则不会发送信号以通知游戏线程开始下一帧:
4. UE4 and RHIThread
Ureal 4最近推出了一种名为RHIThread的功能。它将图形API调用(对于Oculus Mobile,这是OpenGLES或Vulkan)的实际提交与Render Thread完成的其他工作(如剔除和排序等等)分开。对于某些应用程序而言,这可以提高性能,因为渲染逻辑从单帧时间拆分为两个。但是,这需要付出一个额外延迟帧的代价。除非必要,否则大多数应用程序都应该避免启用RHIThread,因为总延迟有可能远远超过50ms。
5. 总结
理解CPU和GPU是如何同步渲染帧是实现最佳性能的关键。如果你的游戏开始丢帧,解决问题的第一步是判断哪个线程是瓶颈所在。或者如果你有相反的问题,亦即游戏飞速运行,但运动到光子延迟非常高,你可以通过降低线程复杂性来改善延迟。