移植底特律:从Playstation 4变成PC的人类

介绍



在本系列文章中,我们将介绍如何将《底特律:成为人类》 从PlayStation 4移植到PC。



底特律:成为人类于2018年5月在PlayStation 4上发布。我们于2018年7月开始开发PC版本,并于2019年12月发布了该版本。这是一款具有三个可玩角色和许多故事情节的冒险游戏。它具有非常高质量的图形,并且大多数图形技术是由Quantic Dream自己开发的。



3D引擎具有出色的功能:



  • 人物的逼真渲染。
  • PBR照明。
  • 高质量的后期处理,例如景深(DOF),运动模糊等。
  • 临时抗锯齿。






底特律:成为人类



从一开始,游戏的3D引擎是专门为PlayStation设计的,我们不知道以后会支持其他平台。因此,PC版本对我们来说是一个挑战。



  • 3D引擎负责人Ronan Marshalot和3D引擎负责人Quantic Dream的Nicholas ViseriJonathan Siret将讨论移植游戏的渲染方面。他们将解释哪些优化可以从PlayStation 4无缝地转移到PC,以及由于平台之间的差异而面临的困难。
  • Lou KramerAMD的技术开发工程师她帮助我们优化了游戏,因此她将详细讨论PC尤其是AMD卡上的非统一资源索引。


选择图形API



我们已经有了该引擎的OpenGL版本,我们已经在开发工具中使用了该版本。



但是我们不想在OpenGL中发布游戏:



  • 我们有很多专有扩展,并非对所有GPU制造商都开放。
  • 尽管当然可以对其进行优化,但该引擎在OpenGL中的性能非常低。
  • 在OpenGL中,有很多方法可以实现不同方面,因此在所有平台上正确实现不同方面是一场噩梦。
  • OpenGL . , , .


由于大量使用无关资源,因此我们无法将游戏移植到DirectX11。它没有足够的资源插槽,如果我们不得不重做着色器以使用更少的资源,则很难获得不错的性能。



我们在DirectX 12和Vulkan之间进行选择,它们具有非常相似的功能集。 Vulkan将进一步使我们能够为Linux和手机提供支持,而DirectX 12将为Microsoft Xbox提供支持。我们知道最终我们将需要实现对这两个API的支持,但是对于端口而言,仅关注一个API更为有意义。



Vulkan支持Windows 7和Windows8。因为我们想让《底特律:成为人类》尽可能多的玩家都可以使用,这已经成为一个非常有力的论据。但是,移植只花了一年的时间,这种说法已经不重要了,因为Windows 10现在已经被广泛使用!



各种图形API概念



OpenGL和较旧的DirectX版本具有非常简单的GPU控制模型。这些API易于理解,非常适合学习。它们指示驱动程序执行很多对开发人员隐藏的工作。因此,在其中优化全功能的3D引擎将非常困难。



另一方面,PlayStation 4 API非常轻巧,非常接近硬件。



Vulkan在两者之间。它也具有抽象性,因为它运行在不同的GPU上,但是开发人员拥有更多的控制权。假设我们有一个任务来实现内存管理或着色器缓存。由于剩下的工作量较少,因此我们必须要做!但是,我们在PlayStation上开发了项目,因此,当我们可以控制所有内容时,它对我们来说更加方便。



难点



PlayStation 4 CPU是具有8核的AMD Jaguar。显然比新的PC硬件要慢;但是,PlayStation 4具有重要的优势,尤其是对硬件的快速访问。我们认为PlayStation 4图形API的效率远远高于PC上的所有API。他非常坦率,浪费很少的资源。这意味着我们可以实现每帧大量的绘制调用。我们知道,在较慢的PC上,高抽奖电话可能会成为问题。



另一个重要的优点是PlayStation 4上的所有着色器都可以预先编译,这意味着它们几乎可以立即加载。在PC上,驱动程序必须在引导时编译着色器:由于支持大量的GPU和驱动程序配置,因此无法提前完成此过程。



在《底特律:在PlayStation 4上成为人类》的开发过程中,艺术家能够为所有材质创建独特的着色器树。这产生了大量的顶点和像素着色器,因此我们从端口的一开始就知道这将是一个巨大的问题。



着色器管道



我们从OpenGL引擎知道,在计算机上编译着色器可能很耗时。在游戏制作期间,我们基于工作站的GPU模型生成了着色器缓存。为底特律生成完整的着色器缓存:“成为人类”花了一整夜!所有员工都可以在早上访问此着色器缓存。但是游戏仍然放慢了速度,因为驱动程序需要将此代码转换为GPU着色器的本机汇编代码。



事实证明,Vulkan处理此问题的能力比OpenGL好得多。



首先,Vulkan不直接使用像HLSL这样的高级着色器语言,而是使用一种称为SPIR-V的中间着色器语言。 SPIR-V加快了着色器编译速度,并使其易于针对驱动程序着色器编译器进行优化。实际上,就性能而言,它可与OpenGL着色器缓存系统媲美。



在Vulkan中,必须将着色器链接到form VkPipeline。例如,VkPipeline您可以从顶点和像素着色器创建。它还包含渲染状态信息(深度测试,模板,混合等)和渲染目标格式。此信息对驱动程序很重要,因此它可以尽可能高效地编译着色器。



在OpenGL中,编译着色器不知道使用着色器的上下文。驱动程序需要等待绘图调用才能生成GPU二进制文件,这就是为什么使用新着色器进行的第一次绘图调用会在CPU上花费很长时间的原因。



在Vulkan中,管道VkPipeline提供了使用的上下文,因此驱动程序具有生成GPU二进制文件所需的所有信息,并且第一个draw调用不会浪费任何资源。另外,我们可以VkPipelineCache在creation上进行更新VkPipeline



最初,我们尝试VkPipelines在第一次需要时创建它。这导致速度下降,类似于OpenGL驱动程序的情况。然后进行了VkPipelineCache更新,制动消失,直到下一次抽奖。



然后,我们预测可以VkPipelines在引导时创建,但是当它VkPipelineCache不相关时,它是如此之慢,以至于无法实施后台加载策略。



最终,我们决定VkPipeline在游戏首次发布时生成所有内容。这完全消除了制动问题,但是现在我们面临新的困难:一代人VkPipelineCache花了很长时间。



底特律:《成为人类》大约有99,500个VkPipeline!游戏使用前向渲染,因此材质着色器包含所有照明代码。因此,每个着色器的编译会花费很长时间。



我们提出了一些优化流程的想法:



  • , SPIR-V.
  • SPIR-V SPIR-V.
  • , CPU 100% VkPipeline.


此外,NVIDIA的Jeff Boltz提出了一项重要的优化建议,在我们的案例中,它证明是非常有效的。



许多VkPipeline非常相似。例如,有些VkPipeline可能具有相同的顶点和像素着色器,仅在少数渲染状态(例如模板参数)不同。在这种情况下,驱动程序可以将它们视为一条管道。但是,如果我们同时创建它们,则其中一个线程将只是空闲,等待另一个线程完成任务。从本质上讲,我们的流程会同时传输所有相似的内容VkPipeline。为了解决这个问题,我们只是更改了排序顺序VkPipeline。 “克隆”被放置在最后,结果,它们的创建开始花费更少的时间。



创作表现VkPipelines漂浮不定。特别是,它高度依赖于可用硬件线程的数量。在具有64个硬件线程的AMD Ryzen Threadripper上,只需花费两分钟。但是,不幸的是,在性能较弱的PC上,此过程可能会花费20多分钟。



后者对我们来说太长了。不幸的是,进一步减少此时间的唯一方法是减少着色器的数量。我们将需要改变创建材料的方式,以便尽可能多地共享它们。对于《底特律:成为人类》,这是不可能的,因为艺术家必须重做所有材料。我们计划在下一个游戏中实施适当的材质实例化,但是对于底特律来说,为时已晚:成为人类



索引描述符



为了优化PC上绘制调用的速度,我们使用了描述符的扩展索引VK_EXT_descriptor_indexing它的原理很简单:我们可以创建一组描述符,其中包含帧中使用的所有缓冲区和纹理。然后,我们可以通过索引访问缓冲区和纹理。这样的主要优点是,即使在多个绘制调用中使用,资源每帧仅绑定一次。这与在OpenGL中使用未绑定资源非常相似。



我们为使用的所有类型的资源创建资源数组:



  • 所有2D纹理的一个阵列。
  • 所有3D纹理的一个阵列。
  • 一个数组,用于所有立方纹理。
  • 一个用于所有物料缓冲器的阵列。


我们只有一个主缓冲区,该缓冲区在绘制调用之间实现(实现为循环缓冲区),该调用包含一个描述符索引,该描述符索引引用了所需的材质缓冲区和所需的矩阵。每个材质缓冲区均包含所使用纹理的索引。





由于采用了这种策略,我们能够保留所有绘制调用共有的少量描述符集,并包含绘制框架所需的所有信息。



优化描述符集更新



即使只有少量的描述符集,更新它们仍然是一个瓶颈。如果描述符集包含很多资源,则更新代价可能很高。例如,在一个《底特律:成为人类》框架中,可以有四千多个纹理。



我们对描述符集实施了增量更新,以跟踪在当前帧中变得可见和不可见的资源。此外,这限制了描述符数组的大小,因为它们具有足够的能力来处理当前时间的可见资源。跟踪可见性会浪费很少的资源,因为我们没有使用昂贵的算法来计算O(n.log(n))...取而代之的是,我们使用两个列表,一个用于当前帧,一个用于前一帧。将剩余的可见资源从一个列表移动到另一个列表,并检查第一个列表中的剩余资源,有助于确定哪些资源进入和从可见性金字塔中消失。



在这些计算过程中获得的增量将存储四帧-我们使用三重缓冲,并且要计算蒙皮对象的运动矢量,还需要一帧。描述符集必须至少保持四个帧不变,然后才能再次对其进行修改,因为它对GPU仍然有用。因此,我们将增量应用于四个帧的组。



最终,这种优化将描述符集的更新时间减少了一到两个数量级。



补足原语



使用描述符索引可以使我们在一次绘制调用中批量批处理多个基元vkCmdDrawIndexedIndirect我们用于gl_InstanceID访问主缓冲区中的所需索引。如果基元具有相同的描述符集,相同的着色器管道和相同的顶点缓冲区,则可以将它们分组。这是非常有效的,特别是在深度和阴影通过时。抽奖电话总数减少了60%。



到此结束文章系列的第一部分。在第2部分中,技术工程师Lou Kramer将特别讨论PC和AMD卡上的异构资源索引。



All Articles