在试过quest 2之后,我发现VR存在的问题在于:
Meta显示系统负责人演讲:完美AR/VR头显的十大障碍 - 映维网资讯 (nweon.com)
我们知道MTP经典的定义就是:图左做出运动的时间,与图右在屏幕上看到的时间,二者的差值。如果差值过大(For VR systems, the M2P is typically below 20 milliseconds. For AR, the required latency is below 5 milliseconds. This is due to the fact that the user has the surrounding environment as a reference.),则会导致晕动症。但这样的定义存在迷惑,后面会解释。
既然VR的一个重要原则就是把这个延时做到最低,那我们看下,VR系统做了哪些努力。
A、绕过Android系统
虽然Meta一直想换掉整个Android系统,有团队一直在做更加实时的操作系统,最近看网上消息是解散了。
下面这张图是手机上Android系统的CS架构,大部分硬件都需要经过Android的Service才能与应用通信。有个例外便是GPU,为了提高渲染效率,GPU是不经过Binder的直接渲染模式。
但对于XR来说,则不一样了,一切以延时为第一要义,所有涉及可能导致延时的服务全部绕过android系统。像是IMU和SLAM则非常依赖于DSP的计算,也完全绕过了Android。
B、绕过 SufaceFlinger
之前手机上,GPU虽然不经过Android的service,但是:
对于VR应用,当然也不经过hwui,甚至SurfaceFlinger也会bypass,合成也不经过Android了。(对于Android的普通应用还是走原来的流程,即使在VR平台上)
C、绕过DRAM
直连:比如上图Sensors直接到DSP,需要全局时间戳。
D、绕过内存拷贝
共享内存:3Dof,6Dof, 以及Slam信息,GPU都是需要的,这就需要一个高效的共享内存机制;
如前面的“Android系统上的操控延时”一文讲过了,Android显示流水线的设计以及与触控延时的关系,详见
A、三级流水线
“绘制+合成+显示”这是手机上经典的三级流水线,在此不多说,显示阶段响应时间为2.5帧(一般我们选择屏幕显示中心位置,算是约定俗成,在此忽略touch事件的sensor等时间,只算显示)。
其中绘制流水线虽然只有1帧时间,但其实留给绘制时间非常长,远超一帧的时间,如下图所示,原因在于合成通常只在最后一刻处理。
B、四级流水线
标准UI经过Android的hwui,与游戏的SurfaceView自己控制渲染有所不同,在此分成两部分讨论。
标准UI: 随着刷新率的上升,渲染时间越来越不够用了,所以120HZ上就有了四级流水线(针对系统UI,对于游戏系统管不了),就是将渲染切成两部分(渲染是3D,通常是瓶颈,而合成是2D操作,速度快),如下图。
这样的缺点就是增加单帧的延迟 (延迟量为 number_of_pipeline_stages x longest_pipeline_stage),但由于120HZ的原因,即使增加一级流水,也比60HZ的延时也好很多。
游戏:可以针对渲染自行设计流水线(一般通过SurfaceView实现,不经过系统的默认DectorView),很多也是将渲染分为两部分,加上系统的两级,也是四级。
优点:渲染时间长,帧率稳定;
缺点:响应时间长,吞吐量和延时往往是相反的,总要有取舍,所以VR的设计方向与此相反。
C、如何设计多级流水线
其实非常简单,就是sleep,本来做完了,也要等待,强行拉长渲染时间,这也是响应时间长的原因。
就像是公交车一样,增加站点的等待时间(前提是上车的人很多,瓶颈在此处)吞吐量自然会上来。
PC上的游戏跑起来帧率都能到几百帧,而屏幕一般120HZ,多出的这些帧有什么用呢?
就在于响应延时,如果只看显示效果,还不如打开Vsync + 3buffer。
优点:响应快,电脑上鼠标要求显然是较高的,鼠标一滑整个画面可能就会有360度的转变,而触控是很难有这种效果的,而VR一扭头也会应有较大幅度的转变,这一点是符合的;
缺点:不考虑功耗,PC上对于耗电无感,所以浪费点就浪费点,但对于VR设备则不可能,续航和热都要考虑。
因为上屏显示这个时间是无法减小的(严格说也可以,就是FreeSync,GSync,QSync,参见Android上的FreeSync(下)实现方法 - 知乎 (zhihu.com)),所以只能将绘制+合成放在一个流水线里面,但这个流水线又不是严格的,它并不对齐,因为PC对于上屏这事完全不在意,撕裂就撕裂,默认就没有Vsync同步。
为了减小响应延时,需要流水线短、单buffer最好、一个T周期内完成、刷新率越高越好。
单屏OLED(最开始广泛使用,目前很少有人使用了)为例,为避免撕裂,左右眼分开(strip 渲染)。
为何VR可以使用single buffer而屏幕不撕裂?
因为左右眼内容不一样(与双buffer也差不多),写左眼与显示右眼并没有什么冲突。
这种也称为Strip rendering(Imagnation):
Strip rendering的想法就是,改变还没有显示出的那部分buffer,有2种不同的策略可供选择。
1. Beam Racing:改变下一步将显示的屏幕的那部分,因为我们想要跑在扫描线前更新还未被显示的那部分内存。对于优化延迟这个目的来说,beam racing策略会更好,但是同时也更难实现。GPU需要保证在很紧的时间窗口完成渲染。这非常难保证,尤其是在一个多线程的操作系统中,所有运作都发生在后台
2. Beam Chasing:在扫描线后更新画面。VR 应用取得显示的上一个Vsync 的时间,来调整自己以保证自己有渲染整个画面的时间。为了计算开始渲染的时间。我们需要条的尺寸。strip的尺寸,和使用多少strip,在执行时被定义。这取决于画面需要多久被显示,和我们需要多久去渲染这个画面。
在多数情况下,2条是最合适的。 在VR中,这还有个优点,就是一个眼睛一个strip。(我们可以从做渲染到右边)。这使得实现更容易一点。 (4个strip渲染当然也没问题,如下图4个GPU同时渲染4块,当然也可以使用1个GPU同时渲染4块,只要保持好同步)
比如4个strip渲染,延时更低,但一般不这么做。
重看“VR上的理想流水线”存在的问题:渲染时间太短(包含CPU处理,Pose计算),而渲染分辨率又大,以单眼2k 来算,也是手机像素的4倍了。合成时间也大大压缩了(合成的有些时间是可以留给绘制的),之前总和2T的时间压缩到0.5T,所以VR实际流水线如下,渲染流程明显拉长。
说明:“重绘L、R“就是ATW进程,后面会详细介绍这个单buffer渲染流程。
响应延时:1.75T 甚至比PC上 1.5T 还长
这说明提出MTP这个概念的主要目的并不是减小响应延时,而在于头部rotation导致的晕动症。
MTP不同于响应延时,按键事件的延时并不会产生晕动症,和大脑预期一致就不会,即使比较慢,比如手机上游戏触控延时在70ms了(算上服务器延时),但你大脑预期它就会慢,所以你也不会头晕。
ATW等减小延时的策略并不会减小响应延时,响应延时更多的和手柄测试链路相关。
既然MTP并不等同于响应时延,如果预计准确,预计位置就在显示L的中间位置,MTP的延时是不是0?
对于MTP,我们经常看到下面这张图的拆解,所以很多测评机构给出的都是20+ms。实际上根据下面这张图来计算,20ms是非常难以实现的(最好也就20ms),对于如今的fast LCD方案都要有30ms了。
同时,我们也看到其他MTP的测试机构,结果是下面这样的,延时及其短。
这个问题不是技术问题,而是语言问题。
重要的已经不是整个链路值,而是预测的准确性,预测时间要尽量短和尽量准确。
【MTP的测试方法】
1. MTP一般测试的是3dof(Yaw,Pitch,Roll),只应用于rotation(对于6dof用处不大,但最新也加入了运动向量和深度信息的预测,本文不涉及,具体测试方法,本文也不涉及,下次介绍)。
2. 任何MTP测试时,都会强调显示屏不同,则延时不同,到底差别在哪里?
对于VR来说,LCD首先排除,灰阶响应速度太慢,20ms的响应速度,而OLED的灰阶响应速度可以忽略,但是成本较高,不同于手机,VR是放大镜,对于坏点率要求非常高,另外做高PPI也比较难。所以目前VR的产品都是Fast LCD(左右分开的双屏,对于带宽也压力小一些),灰阶响应速度在4-5ms,如下图所示。
通过这个图可以看出,MTP的预测时间变长了很多,黑实线为OLED预测时长,而虚线为Fast LCD的预测时长。
(按理说,上半屏与下半屏同时预测,TW同时做也没问题,之所以TW还是分成两个时间段去处理,在于GPU资源的平均分配,也就是在于性能,而非功能上的原因)
所以未来还是Micro OLED更适合VR设备。
为了减小MTP时间,VR使用的SingleBuffer机制,是应用配置的吗?
OpenXR应用渲染方式分为MainThread(CPU)与RenderThread(GPU),这种方式一看上去似乎与Android上的典型渲染方式并没有太大差别,Android5.0之后就已是如此了。
再看下图,OpenXR应用也是3buffer(也有设置为2个的),对于应用渲染,看起来的确相差不大。
区别在于:Android上应用buffer则是ION分配的共享graphic buffer,XR上的应用buffer却是只有GPU认识的texture buffer,其他硬件都不认识,这3个buffer并不是single buffer(single buffer是ion分配的graphic buffer),如上图 ViewL + ViewR 加起来是一个single buffer,对应了上图6个texture buffer(eye buffer)。
这说明:SingleBuffer的使用是在TW(上面的Runtime)上的,而非应用渲染上的,应用有自己的buffers,buffer多了会引入延时,这种延时更多的是操控延时,而不是MTP,对于帧率反而是好事情,再次说明MTP与响应延时不同。
另外非VR的普通应用不使用single buffer,默认双buffer。
下面开始介绍渲染流程。
TW对于VR的渲染流程来说太重要了,必须要将仔细说下。
有个表述比较形象,在3DOF的VR体验中,一切观察的基点都来源于头部的视角,用户就像一个被装在电线杆上可以任意旋转的摄像头。TW就是根据预测位置值(3dof信息)重新调整当前画面,但是这样也有明显缺点,见下图,暂不讨论。
只单看如何实现这个TW功能,需要2点:
因此一个正常的TW流程如下:
为了解决上面提到的TW问题,比如translation则需要运动向量和深度信息估计,比如ASW,暂不在本文讨论。
在这里首先考虑如何解决丢帧问题,就是ATW(网上信息很多,最大的问题就是夸大ATW,似乎和TW差别很大一样,给人造成迷惑)。
ATW使用最后一个有效的帧加上时间扭曲来处理这种情况。如果渲染引擎没有及时完成(图像渲染的工作),ATW使用前一帧,并用时间扭曲处理它。
ATW实现方法:相比TW,改一下变量就好了,需要改动的代码不超过两行,叫它TW也没啥问题,(目前TW都是ATW了),下面就来介绍详细实现方法。
1)eyebuffer render thread:那就是VR的render出来的是texture(分配的eye buffer是GPU认识的texture),而不是graphic buffer(ion的共享内存),并不需要和其他硬件共享buffer,主要是3D操作,耗费时间。
此时作为GPU的输入端除了vectices,texture外,还有6dof送过来的Predicted pose。输出则是L和R的2个texture buffers (eye buffer)。
2)ATW thread: 既然VR的render出来的是texture,那么无法显示到屏幕上,所以还要有个将这个texture转换为graphic buffer的这么一个操作,这里便负责TW和distortion correction和CAC,主要是2D操作,时间短暂。
此时GPU的输入则是L和R的2个texture buffer,同时也有6dof送过来的Predicted pose,输出则是graphic buffer(这个进程的GPU优先级更高,可以抢占render线程)
可见Pose预计两次,第一次是绘制时,第二次是ATW时,误差更小,但对于按键事件没有影响;
细节拆分如下(以SXR为例,对于单oled屏幕):
1)SXR是高通的SDK,已经开源了,可以去看,是用opengles实现的,理解起来比较容易。但是未来高通也支持OpenXR了,所以SXR将不会再支持了,但是整体思路是不变的。
OpenXR有开源的mondo,是用Vulkan实现的,未来XR应用都推荐使用Vulkan,这样性能会更好些(当然如前所述,XR runtime和 app是两个组成部分,所以应用使用vulkan,并不影响runtime使用OpenGL)。
2)SXR的runtime,WarpThread是在同一个进程里面的高优先级进程,对用户是黑盒,也可以看出与屏幕做同步的是WarpThread,这与Openxr是不同的。
OpenXR runtime是围绕Out of Process Composition(OOPC)这一概念设计,从而有一个独立的进程:VR Compositor。它在后台运行,同时从所有客户端收集帧提交信息,然后进行合成和显示(附录有个更完整的图)。
启动帧:使用OpenXR时,PhaseSync始终处于启用状态,xrWaitFrame将负责帧同步和延迟优化,以便API可以阻塞调用线程。另外,开发者不需要调用特殊的API来获得predictedDisplayTime。这个值是从xrWaitFrame通过XrFrameState::predictedDisplayTime返回。
获取姿态:要获取追踪姿态,开发者可以调用xrLocateViews,它类似于vrapi_GetPredictedTracking2。
渲染:需要注意的是,OpenXR有专门的API来管理交换链;在将内容渲染到交换链之前,应调用xrAcquireSwapchainImage/xrWaitSwapchainImage。如果合成器尚未释放交换链图像,xrWaitSwapchainImage可以阻塞渲染线程。
提交帧:xrEndFrame负责帧提交,但与vrapi_SubmitFrame2不同,它不需要进行帧同步和交换链可用性检查,所以这个函数不会阻塞渲染线程。
以下是常见多线程OpenXR应用的示例:(runtime未介绍)
对于mondo的源码分析,可见 dogee.tech/2022/03/21/2022-03-21_Monado Android Sensor Data/
(对于其他基本概念的介绍,比如ASW、PTW、固定和动态注视点渲染、注视点传输、CAC,LDC,MulitView,STI、IPD、VAC、Duty time (display persistence)等解释放在下一次)
whitepaper-makingimmersivevirtualrealitypossibleinmobile.pdf (qualcomm.com)
Keeping the virtual world stable in VR | Qualcomm
Augmented Reality & Virtual Reality Head Mounted Display Testing
刘卫华 映维网:一张VR图像帧的完整生命周期:从帧生成到帧显示