快捷导航
查看: 347|回复: 0

王文斓 《VR游戏的性能分析和优化》

[复制链接]
大家好,我是来自intel的工程师,今天会讲的主题是VR游戏方面的性能优化,大家可能知道对于VR游戏来说,比起传统游戏它的性能是一个很大的瓶颈,因为大家知道可能是2k显示屏然后双目渲染90帧,然后我们今天的主题主要会从CPU的角度去看,CPU本身对VR性能他到底有什么影响,然后我们可以怎么样去分析和优化。
这是我今天会讲的大纲首先是介绍一下性能优化为什么对VR那么重要,然后会从CPU角度跟大家简单介绍一下CPU对VR性能是怎么样的影响,最后我们会举一个例子来分析一下,实际例子我们怎么样去看他的性能问题。然后我这边分别用到两个工具一个是GPUView一个是WPA,这个会给大家看一下分析的结果和优化结果然后最后可能有一个结论。
性能优化为什么对VR来说很重要?

今年属于VR的爆发期,各种各样的主流的或是一些第三方,一些非主流的VR的硬件都陆续出来了,VR本身它造成性能瓶颈的原因第一个是分辨率,第二个是它对延时的要求,第三个是高帧率,这些其实对硬件包括CPU,GPU要求都非常高,所以你看到包括Oculus或是HTC,要求的最低硬件也是i5的Haswell3GHz的CPU,GPU也是GTX970这样一个等级,那其中原因是在哪里,就是说它到底性能主要卡在什么部分?因为刚才说到主要是双目的部分,要做双目渲染的话,其实包括draw call,culling,Gbuffer这些东西都会双倍,所以就加了很多的计算量,另外VR做完渲染以后需要做一个镜头的形变色彩的校正,可能还要做Asynchronous Timewarp这些后处理,那这些加起来都使得VR跟传统应用或是游戏来说性能要求特别高。另外一个原因,VR跟传统的游戏不一样的就是它会造成眩晕感,如果性能达不到90帧的要求,突然掉帧了,这个时候很多用户就会感到明显的眩晕感,只要他在某一个场景下有眩晕感了,那之后就算性能达到了,这个眩晕感觉还在,所以就变成你只要在游戏里面出现了性能的一个落差,这个时候对很多用户来说,就是他的整个user experience,整个体验就被打破了,那之后他就会有一点晕,他会觉这个游戏或者应用,可能不是一个好游戏,因为晕,下次就不会再了,所以这也是另外一点性能优化对VR重要的原因,而且我们对VR的优化应该是在worst case,就是我们能确保一个VR游戏,它在各个场景下它都可以达到要求,所以我这边再讲一下在我们VR开发的过程中的话,我们怎么样去找出性能问题在哪里,然后我们怎么去优化。

CPU对VR来说为什么重要?

那我们先比较一下传统游戏的渲染流程跟VR游戏的渲染流程有什么区别传统游戏的渲染流程就是,比如说CPU先做一些draw call的准备啊,做一些constant buffer,memory allocation,做完之后把这些工作打包起来,通过API调给GPU,让GPU去做实际的渲染工作,所以传统游戏对延时的要求没那么高,每一次他可以buffer好几个帧,我渲染第N帧以后,我第N+1帧就立马可以渲染工作了,但是这个情况下对VR是不行的,如果我们以传统游戏pipeline去渲染VR的话,那我们看一下第N+2帧的时候,它的延时到底有多长,N+2帧CPU开始工作之前,先从头盔里面抓到惯性数据,知道玩家现在是面向哪个角度,然后再告诉CPU,CPU去渲染这个角度的一些东西,做一些处理和准备,然后再把它丢给GPU去渲染,所以看第N+2帧的例子的话,我们发现延时其实很大原因是中间GPU的工作被delay了,就是说可能N+2帧我处理完了,这个时候我希望GPU可以立马去工作,去处理第N+2帧,但是这个时候GPU还在算上一帧的东西,所以等第N+2帧的东西做完以后,整个延时就是我们的Motion-to-photonlatency会受到前面GPU工作还没算完的影响,所以VR游戏的渲染流程改成,CPU先做,做完以后就给GPU,GPU做完以后才去做我这一帧的准备,这样子end to end的延时就会大大缩减了,这也是为了把延时压在20毫秒以下,所以我们牺牲了一些性能,所以在这个情况下造成了一个问题,就是这里会产生CPU性能瓶颈,因为在传统游戏的话可以通过一个pipeline,就是把他pipeline起来,queue起来,这样就可以把CPU的性能问题隐藏起来,但是对于VR来说我们为了达到低延时要求,所以造成CPU和GPU之间没法很好的并行,可能很大一部分GPU是需要等CPU工作完成后才能算,所以这个时候就有CPU性能瓶颈的问题,因为这个原因,我们在做VR游戏的优化,其实我们必须要同时在CPU和GPU上两个角度去考虑,这样做我们才能尽可能的把这个游戏的性能提升到最高。

发现瓶颈——基本性能参数

那我这边举一个例子,实际我们分析的一个workload,看一下在实际的workload下是怎样的一个情况,这边测试的workload是基于Unreal4跟DX11的一个VR游戏,它最初的版本只支持Oculus Rift,那时CV1和HTCVive都还没发售,所以我们测试是基于DK2的,之后在后续版本里CV1和HTC Vive出来以后,我们把它porting到这两个新的硬件上面去。在优化前后它用的runtime也不一样,优化前我们只能在DK2上跑,那时我们是用那时最新的版本,0.8的runtime,Unreal4是4.10.2,然后优化后我们在Vive上用的是STEAM VR的runtime,用的版本是4.11.2,测试平台是i7的Skylake四核八线程的CPU,搭GTX980,我们先看一下这些优化前的基本数据,这个表格左边是系统闲置idle的时候,什么workload都没跑时候的数据,右边是游戏已经跑起来的一些数据。
从这个表里我们可以看到有几个点,首先就是这个游戏他有什么性能问题,从这里来说的话我们看得到,第一它GPU的利用率是非常低的,大概只有50%不到的利用率,而且帧率也很低,这个游戏只能跑大概36帧左右,大家知道如果是在DK2上的话他需要跑75帧,所以明显在帧率上来看这个游戏是不达标的,另外对context switch以及system call来说,他也非常高,就是从原本的idle,context switch 1000多上升到了10000多,然后system call也是6000多到27000多,那systemcall和context switch的升高对系统会有什么影响,第一会造成功耗较高,第二频繁的context switch切换对整个CPU的overhead也会比较高,另外看一下draw call,这个游戏draw call数目大概是4437,对VR来说是蛮高的,因为基于目前的硬件跟计算能力的要求,比如说配970加haswell i5的一个最低的硬件要求上面跑得动的话,我们建议draw call不要超过两千,如果优化做的比较好的话是可以超过这个数目的,最后看一下CPU,这个游戏CPU的占用只有30%多,CPU占用率很低,但是我们后面会仔细看一下,实际上CPU占用率高低对CPU的瓶颈问题是没有太大相关的。

深入分析——GPUView和WPA

在进行深入分析之前我们先看一下刚才讲要用到的两个工具,GPUView和WPA,这两个工具其实都在windows的ADK里面,那windowsADK的话是从win7到win8到win10都支持的一个开发包,这个工包里面我们其实可以做一些性能分析用的,那GPUView本身它可以看到什么,它可以看到在时间轴上面显示CPU和GPU的一个运行的情况然后我们可以通过GPUView知道这个程序到底是受限于CPU还是GPU还是两边都是,那WPA的话是另外一个ADK里面的另外一个工具,它主要是用来追踪跟记录一些windows事件,可以用图表的形式去显示call stack、function ID或是通过module去排序的工具,它主要用来分析CPU,CPU上的问题。这两个工具其实都是用来分析WPR记录的结果,WPR是一个Windows性能记录器,比如说游戏在跑的时候可以去记录其中的30秒或是一分钟或一段时间,那记录下来这个文件我们可以用GPUView和WPA打开,可以用不同的角度看同样一个文件。
两个工具的好处是什么?第一这两个工具是GPUView和WPA是非常直观的,从图表上来看,很容易看到性能问题在哪里。另外它们俩工具都是通用的分析工具。就是不管用什么显示头盔、游戏引擎,这套工具都可以用来去分析,像Unreal公司,Unity它们其实也有自己的分析工具,比如Unreal也有frontend, Unity有它自己的一个profiler 。但是你不用它们引擎的话,就没法用它的引擎去分析了。
那好,我们先看一下GPUView的结果
横轴X轴就是时间,纵轴就是Queue,然后最上面的蓝色那条是GPU的运作情况,中间稍微浅绿的那一块是Oculus的一个runtime的情况,最下面是游戏本身的情况。然后我们最先从上面看,GPU的运用情况。它上面那个GPU运用情况,我们发现有一些地方是空的,代表是说从这个时候起GPU是不做事情的,但如果说有个方块的话,就是我们GPU是有做事情的。那么可以看得出大概它的比例是1:1,所以这个GPU的利用率仅是50%左右。那这也是跟我们之前用的GPU-Z计算出来的结果是一样的。另外从GPU上面我们看到,这次大概是两个帧,这两个是差不多一样的一个workload,所以它就是说每一帧它的时间我们把它计算出来,然后把它导数,我们知道它的平均帧率大概在37,这也跟前面统计出结果是吻合的。GPU部分再看一下,浅绿色代表是Oculus runtime做GPU的工作,我们发现其实它会在每一帧最后的部分会做一些变形跟色彩校正。另外就是游戏本身来说,基本上queue的形状跟上面GPU是一样的,因为这个时候我们平台只有这个游戏在跑,因为把CPU工作丢到queue以后,实际上在通过GPU去运行。下面这些长条形的就是CPU的线程,第一可以看到这个workload本身有两个问题,就是CPU性能,跟GPU性能的问题。那CPU性能问题就是所谓的CPU bound是怎么看的,我们看一下上面,它这里就是有一个GPU空闲的时候,这个部分,我们叫GPU bubble就是GPU空闲的时候,为什么GPU空闲?第一有一些CPU工作还没做完,我们没法把它submit给GPU去做处理。就是因为CPU工作没做完导致没法提交给GPU,所以GPU闲着,所以这时CPU加GPU完成一帧渲染的时间,如果要求75FPS的话就达不到13.3毫秒这样的要求,这样你会有drop frame。另外它这个workload,其实也有GPU bound,那GPU bound是怎么看呢?我们就假设如果CPU bound问题没有了,就GPU这些bubble的地方都没有了,都是可以跑满那这个时候它跑一帧的话,这些有一个个GPU方块的地方加起来,它一帧需要处理多长时间。我们发现它大概是14.7毫秒。其实代表它达不到75FPS底下13.3毫秒要求。所以就算我CPU bound全部解决了,这个workload也受GPU bound影响,没法跑到75,所以我们就可以看到很明显,这个workload,CPU跟GPU都需要去优化的

优化建议及结果

那这里简单列一下,就说我们推荐的一些对于CPU bound问题的优化的一些方法。这是一个比较general角度的一些方法。首先既然它是受制于CPU bound,那有些CPU工作如果不是那么紧急是不是可以把它延后处理,是不是就相当于说减低这一段CPU bound时间的CPU负载,所以说有些工作,比如说像物理、AI这些东西其实它是可以延后处理的,尽量让CPU去做一些渲染的准备,包括draw call、occlusion culling,内存分配这种涉及到渲染线程上的一些工作。另外,第二对渲染线程本身是不是可以提早处理?CPU bound有些工作是不是能够稍微提前两个毫秒或是三个毫秒,如果能够提前处理的话,bound时间也会减低的,这样的坏处就是可能额外增加一点latency,比如说往前移三个毫秒,就多增加三个毫秒的latency,当然这主要是性能跟延迟之间的trade-off。另外可以考虑用一些多线程的技术,比如说像英特尔的TBB,或是Open MP这种,其实都可以用来做多线程优化,去优化像物理、骨骼、粒子系统,其实都是CPU的一些负载,我们可以把这些工作去做一些多线程处理,尽量去用到多核的性能。另外CPU工作是需要优化的,因为CPU bound,所以比如drawcall、dynamic shadowing 、物理 、粒子、AI这种东西为其实尽可能要去优化。然后因为它是DX11,大家可能知道DX11对drawcalloverhead是比较高的,所以这时太多drawcall调用的话,其实会对渲染线程有很大影响,所以如果是情况允许可以考虑改用DX12,DX12是对多线程是比较友好的。

最后讲到GPU部分,其实因为说到GPU bound有这个的问题,所以这个时候我必须要优化GPU工作,那才能避免丢帧。我发现GPU bubble 一帧,GPU bubble在前面的时候大概需要7.37个毫秒,CPU的工作CPU bound引起的,那我们就看一下怎么去看CPU bound这个时候到底CPU在干什么,它是因为做什么事情而导致我的性能被影响了,那这里的话我们就要讲到另外一个工具,就是WPA,,刚才说到其实WPA和GPUView一样,都可以用来分析WPR产生的ETL数据,所以这时我们就可以用WPA打开同样的文件,还有用GPUView打开的结果,这个时候我们可以看到它的时间,在GPUView里面我们可以看到CPU bound的时间是从第几秒到第几秒,每一个CPU线程ID是多少。
然后我们根据这个数据,用WPA打开这个同样ETL文件找出同样的时间段。
这里红色mark起来的时间段,就是刚才说的7.37秒的bound的时间段,然后按照GPUView里面看到的线程ID,去map到这样一个WPA里面看到的线程ID,因为WPA里面,其实我们可以按照CPU利用率做排序,我们可以知道每个线程它到底是什么函数,是最占CPU最耗CPU的,我们用这两个工具结合起来去分析CPU实际bound在什么地方。
接下来我们可以逐一的分析CPU的线程分别受限什么地方,那我们按照CPU占用率最高的一个线程就是渲染线程,去看渲染线程到底是在bound的什么地方,我们从WPA按照call stack一看,然后按照右边这个CPU利用率排序来看的话,我们发现他渲染线程其实最大的前三个瓶颈是什么,第一个就是他static mesh的bass pass rendering,第二个就是一个dynamic shadow的就是阴影的一些初始化,然后第三个就是它的viewvisibility的计算我们发现主要问题他其实是在太多的staticmesh,这也是为什么之前我们看到它的draw call数目其实是比较高的,有4000多draw call,所以他有太多draw call造成他static mesh的bass pass rendering有太多的计算,所以这个时候我们在优化上我们可以考虑如果用Unity你可以用Unity的batching或者Unreal的actor merging去减少static mesh的绘制,然后尽可能另外我们也需要去用到的就是说像LOD,Level of Details或是用使用比较少的材质去做优化,减少staticmesh的数目,那第二点就是说用Unity可以用DoubleWide Rendering或者Unreal的InstancedStereoRendering这两个工具都可以把draw call数目减半,因为传统的话我们都是要先渲染左眼再渲染右眼,那draw call数目就会double了,但是利用这两个feature我们就可以同时画两个眼睛,所以它可以通过这些工具去把draw call数目减半,另外动态阴影的初始化也是蛮大的一个瓶颈,所以说可以去尽量不使用和减少用动态阴影,去降低CPU bound,还有像多次渲染的一些反射、逐像素、透明或是多材质的东西在VR里面也尽可能去减少,如果你VR有性能问题的话。
第二个逻辑线程,那逻辑线程是除了渲染线程外的第二个重要的一个线程,那在逻辑线程里面它其实,前三个瓶颈,主要第一是在那个骨骼动画的一个计算前置上面然后后面还有View port或是鼠标处理事件,对View port或鼠标处理事件可能就需要对这个程序本身,这游戏本身有这两个问题,所以要我们来检查一下这两个地方是不是有可优化。如果是对骨骼动画的话,因为,在这个workload里面,它其实是骨骼动画先做一个背景处理的前置,之后把它分到两个线程去做,每一个线程其实处理的工作量是非常少,所以如果是这个情况的话,其实我们可以考虑单线程处理,因为本身工作量不大,那如果你把它分到多线程的话,你可能就会产生的overhead,可能反而会导致性能更差。
接下来就是工作线程那工作线程本身就是包括后衣服模拟、物理、粒子模拟,都在工作线程中。那在工作线程里面,在这游戏里面,它其实是分成3个线程去做,那这些东西其实就是刚才说到,其实它是可以去延后去处理的,那我们就尽可能把这个东西延后去处理,那这个时候就减少了CPU bound 的这一段对那个渲染线程造成影响。
那好这一项我们看一下优化后的结果
就是说在优化这边我们实现了几个优化,第一个是LOD、Instanced Stereo Rendering, 还有移除了一些动态阴影,另外对CPU逻辑线程跟工作线程都延后到后面去处理。做了这些优化以后,帧率从当初在DK2上面的36帧提升到Vive上面的71帧,大家知道这两个头盔的分辨率是不一样的,Vive是2K,Oculus只有1080P,所以它的性能提升大概是2点多倍左右,帧率提升大概是2点多倍,另外因为CPU瓶颈降低了,所以GPU利用率也从原本的只有50%左右提升高到70%,所以从这两个GPU利用率来看,它的GPU bubble在优化后其实更少了,就等于GPU利用率提升了。因为我们游戏在VR的线程优化上希望尽可能利用我们的CPU和GPU资源,所以如果GPU利用率很低,比如你是1080的显卡,但是利用率只有一半,那其实一半性能是浪费掉的,所以这时其实我们可以看出如果它是一个CPU bound的话,我们可以通过优化CPU性能提升,也把GPU利用率提升
这边是一个优化后的一个结果,但优化后我们发现,红框的部分其实也是一个CPU bound的部分,但是它CPU bound从原本的7.37毫秒减到两点多个毫秒。然后我们发现在这里尤其是这两个线程bound,最上面就是渲染线程,最下面是驱动线程,那中间那些线程到哪里去了?就是那些逻辑线程、工作线程,发现移到后面去了,所以也就是说我们把这个这些工作移到后面去处理,如果紧急的话移到后面去处理,这个时候我们可以减低里面的一些CPU bound的负荷。那这里是做完优化以后我们再对刚才瓶颈部分做分析,我们发现它接下来瓶颈是哪一个,就是Call Stack,就是它驱动的瓶颈在什么地方,后面是render thread线程的瓶颈在什么地方,我们可以用刚才说到的方法就重复在优化后,再看线程现在的瓶颈在什么地方
OK,那后面这边就是大概的一个总结,就是优化前跟优化后,它在CPU线程上,它的一些瓶颈到什么地方。
然后可以发现就是,优化前就是这些瓶颈,到优化后的话其实逻辑线程跟物理都没有了,因为它都已经被延后,然后渲染线程其实还有在 base pass这边占比比较大,因为它是Unreal的游戏,所以我们可以进一步用Actor Merging把邻近的一些mesh合并,可以进一步的降低 base pass渲染的瓶颈。假设我们能进一步的去优化,去降低它CPU瓶颈,目前优化后这个比是2.62毫秒,如果我们可以进一步把它降低,假设可以降到0的话,那GPU处理单帧时间就可以减到11.38,相对来说帧率是87.8,这时我们只要再做一点GPU方面的优化,就可以达到90帧的要求。
这是最后统计的结果,就是左边是idle情况,中间是刚才说过的在优化前的一个结果,最右边是Vive上面的结果,我们可以看到,draw call数目,从原本4000多减到800多了,就是因为做了LOD、InstancedStereo Rendering以后,draw call数会降低非常多,帧率也从30几提升到70几了,GPU利用率也从50%左右提高到78%接近于80,所以这是我们优化后的一个数据,可以看到,通过这样一个优化,我们可以达到性能蛮大的一个提升

结论

不同的性能分析工具看到问题的角度跟方向都不一样,那对性能分析来说其实,最好是先做一个大概的overview,大概看一下运行情况,比如说我们通过GPUView去看到它CPU、GPU的并行,这时我们知道它bound在CPU还是GPU 还是都有,之后我们可以用再深入的比如说Unreal的工具,深入去看GPU瓶颈是在overdraw,还是在哪个带宽,还是在哪里,CPU的话也可以看它是不是在Static Meshing的渲染上面,还是Memory Allocation上的瓶颈。GPUView跟WPA是比较好的切入必要性能分析的一个点,因为它可以大概看到一个overview,那我们知道说,我们把精力花在对性能提升最大的一些部分的优化,其实对整个开发节省时间是蛮大的,蛮有帮助的。
另外,CPU对VR来说它其实也是有很大影响,因为刚才已经给大家看过了,我们要达到一个好的用户体验必须要对CPU跟GPU都去做优化。如果是是情况允许的话可以从DX11换到DX12,如果真的是性能不够的话,需要用一个比较好的CPU或是做CPU端优化,那CPU端优化呢刚才也提到很多,比如说draw call 减少,Dynamic Shadowing减少,纹理合併。另外尽量用Light Map去烘焙静态光,不要用Dynamic Lighting,这些都可以对优化起到帮助。另外英特尔最新推出了 Turbo Boost Max Technology 3.0,就是所谓的TurboBoost 3.0,比如对68系列跟69系列的CPU来说,它可以对其中一个核去做频率的提升,如果说游戏是跑在这个CPU的话,你可以把渲染线程allocate在一个最快的核心上面,因为主要瓶颈在渲染线程上,所以我们把最快的这个核心allocate在渲染线程,这时我可以通过Turbo Boost 3.0把这个CPU最快核心性能提升15%左右。最后如果使用到一些第三方引擎,比如Unity、Unreal的话其实尽可能要用到最新版本,因为这样你可以用到他们最新的技术,引擎的更新以及对VR的支持是非常快的,所以如果可能是说比较旧的版本的话可能就是很多VR的部分是没有良好的优化,所以这个时候性能就会有问题,所以尽可能要用到最新的一个版本。大概是这样,谢谢。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ