【GDC 2019】GPU Driven Rendering and Virtual Texturing in Trials Rising

这篇文章是育碧在GDC2019上的一篇演讲,主要介绍《Trials Rising》中的GUP Driven Rendering技术。


这是一款UGC的游戏,游戏场景由大量的macro block组成,场景的几何复杂度很高。而且由于是UGC,所以不同的关卡的复杂度是不一样的,而且差别可能会比较大。

目前的引擎遇到的问题是,首先场景中存在大量的可见的instance,大概有2500左右。有两个CPU的核心在为rendering服务(包括收集可见信息和发起draw command),所以CPU存在巨大的瓶颈,而且在CPU平台没有Occlusion Culling。

因此需要GPU Driven Rendering, 在GDR下,将可见性测试移动到GPU上,而且GPU上可以直接使用Culling之后的结果,可以直接在GPU上进行Batch,将不同mesh的不同instance merge到一起。在GPU端可以感知整个scene的状态(而不只是知道某个culling状态的结果),最后GPU可以自己发起DrawCall。以上这些之前需要在CPU进行的操作,都可以在GPU端进行。

以上便是在GDR下,CPU和GPU的协作流程,CPU端负责更新Instance信息到GPU,并且准备Batch draw lists,在GPU端会筛选这些Draw lists(这里的filter是否可以理解为 culling的过程?),然后组装geometry数据,更新控制数据(也就是控制渲染管线的设置等),最后执行Draw Commands。

这是他们的第一个版本的成果和遇到的问题。第一版确实是带来了GPU和CPU整体性能上的提升,说明技术方向上是没问题的,但是并没有达到预期的帧率要求,主要问题是CPU在收集GPU需要的instance信息上出现了性能瓶颈,而且面临这CPU和GPU间传输大量数据的带宽压力,最终导致GPU的利用率不升反降。结论就是绘制数据的收集与合批处理是主要的性能瓶颈

这里介绍下GPU端的数据结构。使用两个GPU Buffer来保存几何数据,包括顶点数据和三角形数据,这里使用的是Pool memory,也就是所有geometry的空间都会从pool中来划分,然后使用一个大的GPU Buffer来保存所有的instance parameter,比如transform,material等信息,这个叫做Instance Data Pool


每个instance会有一个instance descriptor,类似于destriptor set, 描述该instance所有需要的信息,如上图所示,包括一些internal data,比如culling data,又或者是一些external data,比如vertex/index offset,transform offset等,这些数据需要通过instance descriptor里的index来从外部获取。使用Instance Descriptor就可以表示场景中的任何数据。


通过Instance Descriptor Table便可以表示整个scene,这个是整个渲染管线中最重要,也是使用频率最高的数据。在CPU端会有GPU端的Table的一个镜像copy,负责CPU到GPU端数据的同步。GPU等待CPU的同步信号完成之后再进行读取操作。第一个版本中,这部分的数据每帧都会重新生成并且同步,这会产生非常大的CPU和GPU的数据同步的性能开销,而且随着场景的复杂度越大,这种情况会更加严峻。

上图显示了PerInstanceData的内容,包括transform, skinning matrices和material data等,其中动画模型比静态模型要多skinning matrices的内容。对于每个Static Instance,占用224bytes的大小,这是可以接受的大小;但是对于每个动画模型,则会占用1248bytes的大小,这是比较大的。


这里使用了一个类似双缓冲的优化,由于这一帧的CurrentData就是下一帧的HistoryData,因此实际获取的时候通过address + indirection的方式来获取数据,每一帧记录一个当前的address mode,交替的更新和获取current/history数据,这样每次只需要更新其中一个内容即可,这样对于Static Instance,内容占用从224bytes降低到160bytes;Skinned Instance从1248bytes降低到736bytes。

优化的结果如上图所示,Static Instance总耗时从10.81ms降低到10.49ms,而Skinned Instance耗时从1.32ms降低到0.89ms,耗时减少的比例并没有达到实际的数据量减少的比例。测试下来是因为UpdateSubresource的实现是非常慢的,占用了约4ms。


实际测试中发现场景中大部分的静态物体实际上根本不会移动,也就是InstanceData中Transform这些实际上是不需要更新的,所以可以根据Instance的实际使用频率,或者说是更新频率来决定更新GPU数据的频率。游戏中将所有的instance按照update rate排序为四个category。每个category将会又不同的更新频率,比如skinning的更新频率要高于static,skinning每帧都会更新,而static则若干帧或者达到特定的条件才会触发更新。

GPU数据的更新会通过叫做Micro Patch的方式来进行,也就是bufferOffset + writeData的形式,其中bufferOffset是数据需要在哪写入,writeData是需要写入的数据。CPU端首先会收集所有的dirty Instance,然后通过InstanceTable来构建需要更新的micro patches,放入到patch pool中,GPU端会发起update任务来根据这些micro patch来更新GPU Buffer。为了提高并行,每一帧生成的micro patch数据会在下一帧被应用到GPU上。

这种micro patch的策略对于CPU端的性能提升是非常明显的。


另外不仅可以针对不同的Instance采用不同的更新频率,对于同一种类的instance,根据其重要性,也可以采用不同的Instance Data更新频率,比如对于Main Character这种重要角色,则采用60HZ的更新频率,对于物理模拟的instance,则可以降低更新频率到30HZ,甚至对于远距离的物理模型,可以进一步的降低到15HZ。这种Variable Synchronization Rate的策略可以更具不同的场景定义出不同的策略模型,自由度是非常高的,本质上都是为了尽可能的减少数据更新的性能开销。

总结下该文章的GPU Driven Rendering,采用了非常彻底的GPU Driven思路,将整个游戏场景的Instance,包括static和skinned,都放到了GPU上,所有的VB/IB/Instance Data都放到统一的大的buffer里,每个Instance维持一个Descriptor用于表示这个instance的所有状态,所有的culling和drawcommand发起等都在GPU端完成,极大的解放了CPU端的性能压力。为了减少GPU Driven带来的CPU和GPU的数据更新性能压力,采用了micro patch和VSR的优化策略,在保证游戏画面的前提下,尽可能的减少数据的传输,从而达到预期的性能目标。

参考文献

https://gdcvault.com/play/1026013/GPU-Driven-Rendering-and-Virtual