当前位置: 首页 > news >正文

哪个网站可以做问卷调查做网站最多的行业

哪个网站可以做问卷调查,做网站最多的行业,崇州网站制作,网站推广与营销知识本文出自GPU Zen 2。 Vulkan 是一个新的显式跨平台图形 API。它引入了许多新概念#xff0c;即使是经验丰富的图形程序员也可能不熟悉。Vulkan 的主要目标是性能——然而#xff0c;获得良好的性能需要深入了解这些概念及其高效应用方法#xff0c;以及特定驱动程序实现的实…本文出自GPU Zen 2。 Vulkan 是一个新的显式跨平台图形 API。它引入了许多新概念即使是经验丰富的图形程序员也可能不熟悉。Vulkan 的主要目标是性能——然而获得良好的性能需要深入了解这些概念及其高效应用方法以及特定驱动程序实现的实现方式。本文将探讨内存分配、描述符集管理、命令缓冲区记录、管道障碍、渲染通道等主题并讨论如何优化当前桌面/移动 Vulkan 渲染器的 CPU 和 GPU 性能同时展望未来的 Vulkan 渲染器可以进行哪些不同的改进。 现代渲染器变得越来越复杂必须支持许多不同的图形 API它们具有不同程度的硬件抽象和不相交的概念集。这有时使得支持所有平台达到相同的效率水平具有挑战性。幸运的是对于大多数任务Vulkan 提供了多种选择可以简单地重新实现其他 API 的概念以更高的效率实现具体取决于将代码专门针对渲染器需求或重新设计大型系统以使其最优于 Vulkan。我们将在适用时尝试覆盖这两个极端——最终这是一个在 Vulkan 兼容系统上的最大效率与每个引擎需要仔细选择的实现和维护成本之间的权衡。此外效率往往取决于应用程序——本文中的指导是通用的,最终最佳性能是通过在目标平台上对目标应用程序进行分析并基于结果做出明智的实现决策来实现的。 本文假定读者已经熟悉 Vulkan API 的基础知识并希望更好地理解它们和/或学习如何高效使用该 API。 4.1 内存管理 内存管理一直是一个极其复杂的话题而在 Vulkan 中由于不同硬件的堆配置多样性它变得更加复杂。早期的 API 采用了资源为中心的概念——程序员并不直接管理图形内存而是管理图形资源并且不同的驱动程序可以根据 API 使用标志和一系列启发式方法来自行管理资源内存。而 Vulkan 强制要求在前期就考虑内存管理因为你必须手动分配内存来创建资源。 一个合理的第一步是集成 VulkanMemoryAllocator以下简称 VMA它是 AMD 开发的开源库能够通过在 Vulkan 函数之上提供通用资源分配器来解决一些内存管理的细节。即使你使用了这个库仍然有多个性能考虑因素适用本节的其余部分将讨论内存注意事项而不假定你使用 VMA所有的指导对 VMA 同样适用。 4.1.1 内存堆选择 在 Vulkan 中创建资源时你需要选择一个堆来分配内存。Vulkan 设备暴露了一组内存类型每种内存类型都有定义该内存行为的标志和可用大小的堆索引。大多数 Vulkan 实现暴露以下两种或三种标志组合 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 这通常指的是 GPU 内存该内存从 CPU 端不可直接访问GPU 访问此内存的速度最快应该用来存储所有渲染目标、仅限 GPU 资源如计算用的缓冲区以及所有静态资源如纹理和几何缓冲区。 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 在 AMD 硬件上这种内存类型指的是 CPU 可以直接写入的高达 256 MB 的显存非常适合分配由 CPU 每帧写入的数据如统一缓冲区或动态顶点/索引缓冲区。   VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT 这指的是 GPU 可以直接访问的 CPU 内存对这类内存的读取通过 PCI Express 总线进行。如果没有前一种内存类型一般情况下这类内存应该是统一缓冲区或动态顶点/索引缓冲区的首选还应该用于存储阶段缓冲区这些缓冲区用于将数据填充到使用 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 分配的静态资源中。   VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT 这指的是在平铺架构上用于渲染目标的 GPU 内存可能永远不需要分配。建议使用延迟分配的内存来节省物理内存用于从未存储的大型渲染目标如 MSAA 图像或深度图像。 对于集成 GPU没有 GPU 和 CPU 内存的区别——这些设备通常暴露 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 通过这些可以分配所有静态资源。 在处理动态资源时通常在非设备本地的主机可见内存中分配效果良好——这简化了应用程序管理并且由于 GPU 端缓存只读数据而高效。然而对于高随机访问度的资源如动态纹理最好在 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 中分配它们并使用在 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 内存中分配的阶段缓冲区上传数据——类似于处理静态纹理的方式。在某些情况下你可能也需要为缓冲区这样做——虽然统一缓冲区通常不会受到影响但在一些应用程序中使用大存储缓冲区且具有高度随机访问模式除非你先将缓冲区复制到 GPU否则可能会产生过多的 PCIe 事务此外从 GPU 侧访问主机内存的延迟较高可能会影响许多小绘制调用的性能。 当从 VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT 分配资源时如果出现 VRAM 过度订阅的情况你可能会耗尽内存此时你应退回到在非设备本地 VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT 内存中分配资源。当然你应该首先确保大且频繁使用的资源如渲染目标被优先分配。还有一些其他方法可以在过度订阅事件中处理如将较不常用的资源从 GPU 内存迁移到 CPU 内存——这超出了本文的讨论范围此外在某些操作系统如 Windows 10上正确处理过度订阅需要当前 Vulkan 中尚未提供的 API。 4.1.2 内存子分配 与其他一些允许为每个资源执行一次内存分配的 API 不同在 Vulkan 中对于大型应用程序而言这种做法并不实际——驱动程序仅需支持最多 4096 个独立的分配。除了总数量有限外分配的执行速度可能会很慢由于假设了最坏情况下的对齐要求还可能会浪费内存并且在命令缓冲区提交时需要额外的开销来确保内存驻留。因此子分配是必要的。Vulkan 的典型工作模式涉及使用 vkAllocateMemory 进行大规模分配例如16 MB 到 256 MB具体取决于内存需求的动态性并在此内存中对对象进行子分配有效地自行管理这些内存。至关重要的是应用程序需要正确处理内存请求的对齐问题以及限制缓冲区和图像有效配置的 bufferImageGranularity 限制。 简而言之bufferImageGranularity 限制了在同一分配中缓冲区和图像资源的相对位置要求在各个分配之间进行额外的填充。处理这种情况有几种方法 始终超对齐图像资源因为它们通常有更大的对齐要求通过 bufferImageGranularity实际上使用最大所需对齐和 bufferImageGranularity 进行地址和大小对齐。         跟踪每个分配的资源类型如果前一个或后一个资源是不同类型的则让分配器添加所需的填充。这需要一个稍微复杂一些的分配算法。         将图像和缓冲区分别分配在不同的 Vulkan 分配中从而避免整个问题。这减少了由于较小对齐填充带来的内部碎片但如果后备分配过大例如 256 MB可能会浪费更多内存。 在许多 GPU 上图像资源所需的对齐要求比缓冲区要大得多这使得最后一个选项具有吸引力——除了由于缺少缓冲区和图像之间额外填充而减少浪费外它还减少了图像对齐造成的内部碎片当图像跟随缓冲区资源时。VMA 提供了第二种默认和第三种选项的实现参见 VMA_POOL_CREATE_IGNORE_BUFFER_IMAGE_GRANULARITY_BIT。 4.1.3 专用分配 尽管Vulkan提供的内存管理模型意味着应用程序执行大规模分配并使用子分配将多个资源放置在一个分配中但在某些GPU上将某些资源作为专用分配进行分配更为高效。这样驱动程序可以在特殊情况下在更快的内存中分配这些资源。 为此Vulkan提供了一个扩展在1.1版本中为核心功能来执行专用分配——在分配内存时您可以指定要为这个单独的资源分配内存而不是作为一个不透明的块。要知道这是否值得您可以通过vkGetImageMemoryRequirements2KHR或vkGetBufferMemoryRequirements2KHR查询扩展内存需求结果结构VkMemoryDedicatedRequirementsKHR将包含requiresDedicatedAllocation如果所分配的资源需要与其他进程共享则可能会设置此标志和prefersDedicatedAllocation标志。 一般来说根据硬件和驱动程序应用程序可能会在需要大量读/写带宽的大型渲染目标上看到专用分配带来的性能提升。 4.1.4 内存映射 Vulkan提供了两种选项来映射内存以获取可见于CPU的指针 在CPU需要向分配写入数据之前执行此操作并在写入完成后取消映射。在主机可见内存被分配后立即执行此操作并且永远不取消映射内存。 第二种选项通常称为持久映射通常是一个更好的权衡——它最小化了获取可写指针所需的时间vkMapMemory在某些驱动程序上并不是特别便宜消除了处理来自同一内存对象的多个资源需要同时写入的情况对已经被映射且未取消映射的分配调用vkMapMemory是无效的并简化了代码。 唯一的缺点是这种技术使得在“内存堆选择”中描述的AMD GPU上的256 MB主机可见且设备本地的显存块变得不那么有用——在使用Windows 7和AMD GPU的系统上在此内存上使用持久映射可能会迫使WDDM将分配迁移到系统内存。如果这种组合是您用户的重要性能目标那么根据需要映射和取消映射内存可能更合适。 4.2 描述符集 与早期采用基于插槽绑定模型的API不同在Vulkan中应用程序在将资源传递给着色器时有更多自由。资源被分组到描述符集中这些描述符集具有应用程序指定的布局每个着色器可以使用多个描述符集这些描述符集可以单独绑定。应用程序负责管理描述符集以确保CPU不会更新GPU正在使用的描述符集并提供具有CPU侧更新成本和GPU侧访问成本之间最佳平衡的描述符布局。此外由于不同渲染API使用不同的资源绑定模型并且没有一个模型完全匹配Vulkan模型因此以高效和跨平台方式使用该API变得具有挑战性。我们将概述几种可能的方法来处理Vulkan描述符集这些方法在可用性和性能之间取得不同平衡。 4.2.1 Mental Model 在处理Vulkan描述符集时拥有一种 Mental Model 以了解它们如何映射到硬件是有益的。一种这样的可能性——也是预期设计——是描述符集映射到包含描述符的一块GPU内存中——这些是不透明的数据块其大小为16-64字节具体取决于资源它们完全指定了着色器访问资源数据所需的所有资源参数。在调度着色器工作时CPU可以指定有限数量指向描述符集的指针这些指针将在着色器线程启动时对着色器可用。 考虑到这一点Vulkan API可以或多或少直接映射到该模型——创建一个描述符集池将分配一块足够大的GPU内存以容纳最大指定数量的描述符。从描述符池中分配一个集合可以像通过VkDescriptorSetLayout确定已分配描述符累积大小那样简单请注意这样实现不支持从池中释放单个描述符时进行内存回收vkResetDescriptorPool将指针重置回池内存开始位置并使整个池再次可用于分配。最后vkCmdBindDescriptorSets将发出命令缓冲区命令以设置与描述符集指针对应的GPU寄存器。 请注意此模型忽略了一些复杂性例如动态缓冲区偏移、限制数量硬件资源用于描述符集等。此外这只是一个可能实现——一些GPU具有较少通用的描述符模型需要驱动程序在将描述符集绑定到管道时执行额外处理。然而这是规划描述符集分配/使用时有用的一种模型。 4.2.2 动态描述符集管理 基于上述Mental Model您可以将描述符集视为可见于GPU的内存——应用程序负责将描述符集合并成池并保留它们直到GPU完成读取它们。 一种有效的方法是使用空闲列表来管理描述符池每当您需要一个描述符池时从空闲列表中分配一个并用于当前帧当前线程中的后续描述符集分配。一旦当前池中的描述符耗尽就会申请新的池。在给定帧中使用过的任何池都需要保留一旦帧完成渲染由相关围栏对象确定则可以通过vkResetDescriptorPool重置这些描述符池并返回空闲列表。虽然可以通过VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT从池中释放单个描述符但这会使驱动端记忆管理变得复杂不推荐使用。 当创建一个描述符池时应用程序指定从中分配的最大数量以及每种类型可以从中分配的最大数量。在Vulkan 1.1版本中应用程序不必处理这些限制——它只需调用vkAllocateDescriptorSets并通过切换到新的描述符池来处理该调用产生的错误。不幸的是在没有任何扩展名的Vulkan 1.0版本中如果池没有可用空间则调用vkAllocateDescriptorSets会出错因此应用程序必须跟踪每种类型集合和描绘数以提前知道何时切换到不同池。 不同管道对象可能使用不同数量的描绘这就引出了池配置的问题。一种简单的方法是创建所有配置相同、针对每种类型使用最坏情况数量描绘配置。例如如果每个集合最多可以使用16个纹理和8个缓冲区描绘则可以以maxSets 1024、纹理描绘16 × 1024和缓冲区描绘8 × 1024配置所有池。这种方法可以工作但实际上可能导致对于具有不同描绘计数着色器产生非常显著的内存浪费——您不能从上述配置中的任何池中申请超过1024个描绘集合因此如果您的大多数管道对象只需4个纹理则您将在纹理描绘内存上浪费75%。 提供更好平衡以优化内存利用率有两个替代方案 测量特征场景下每种类型在着色器管道中的平均描绘数并相应地配置池大小。例如如果在给定场景中我们需要3000个描绘集合、13400个纹理描绘和1700个缓冲区描绘则每个集合平均需要4.47个纹理向上舍入至5和0.57个缓冲区向上舍入至1因此合理配置为maxSets 1024、5 × 1024纹理描绘、1024缓冲区描绘。当某个类型出现耗尽时我们就申请新的。这一方案保证能工作并且平均效率应该合理。将着色器管道对象按大小类别进行归类以近似常见模式并选择适当大小类别下获得合适配置。这是一种扩展上述方案以涵盖多个大小类别的方法。例如在场景中通常会有大量阴影/深度预通道绘制调用以及大量常规绘制调用但这两个组所需描绘数不同其中阴影绘制调用通常每组要求0到1个纹理和0到1个缓冲区当动态缓冲区偏移被使用时。为了优化内存利用率更合适的是分别为阴影/深度和其他绘制调用单独申请描绘集合。类似于通用目的分配器可以根据给定应用程序优化特定大小类别这仍然能够在较低级别管理层进行管理只要提前配置好特定于应用程序用途。 4.2.3 选择合适的描述符类型 对于每种资源类型Vulkan提供了几种在着色器中访问这些资源的选项应用程序负责选择最佳的描述符类型。 对于缓冲区应用程序必须在统一缓冲区和存储缓冲区之间进行选择并决定是否使用动态偏移。统一缓冲区对最大可寻址大小有一个限制——在桌面硬件上您可以获得最多64 KB的数据但在移动硬件上有些GPU仅提供16 KB的数据这也是规范所保证的最小值。缓冲资源可以大于此限制但着色器只能通过一个描述符访问这么多数据。 在某些硬件上统一缓冲区和存储缓冲区之间的访问速度没有区别然而对于其他硬件根据访问模式统一缓冲区可能显著更快。对于小到中等大小的数据特别是当访问模式固定时例如用于材质或场景常量的缓冲区优先使用统一缓冲区。当您需要比统一缓冲区限制更大的数组并且在着色器中动态索引时存储缓冲区更为合适。 对于纹理如果需要过滤则可以选择组合图像/采样器描述符类似于OpenGL其中描述符同时指定纹理数据源和过滤/寻址属性、分开的图像和采样器描述符更好地映射到Direct3D 11模型以及带有不可变采样器描述符的图像描述符其中采样器属性必须在创建管道对象时指定。 这些方法的相对性能高度依赖于使用模式然而一般来说不可变描述符更符合其他新API如Direct3D 12中推荐的使用模型并给驱动程序更多自由来优化着色器。这确实在一定程度上改变了渲染器设计使得实现某些动态部分的采样器状态成为必要例如在流式传输过程中用于纹理淡入的每个纹理LOD偏差使用着色器ALU指令。 4.2.4 基于插槽的绑定 Vulkan绑定模型的一种简单替代方案是Metal/Direct3D11模型其中应用程序可以将资源绑定到插槽而运行时/驱动程序管理描述符内存和描述符集参数。该模型可以建立在Vulkan描述符集之上虽然不提供最优结果但通常是移植现有渲染器时一个不错的起点并且通过仔细实现它可能会出奇地高效。 为了使这个模型工作应用程序需要决定有多少资源命名空间以及它们如何映射到Vulkan集/插槽索引。例如在Metal中每个阶段VS、FS、CS都有三个资源命名空间——纹理、缓冲区、采样器——没有区别例如统一缓冲区和存储缓冲区。在Direct3D 11中命名空间更加复杂因为只读结构化缓冲区与纹理属于同一命名空间但与无序访问一起使用的纹理和缓冲区则位于一个单独的命名空间。 Vulkan规范仅保证整个管道跨所有阶段至少有4个可访问的描述符集因此最方便的映射选项是让资源绑定在所有阶段之间匹配——例如无论从哪个阶段访问纹理插槽3都应包含相同的纹理资源——并为不同类型使用不同的描述符集例如将0集用于缓冲区将1集用于纹理将2集用于采样器。或者应用程序可以为每个阶段使用一个描述符集并执行静态索引重映射例如插槽0-16将用于纹理插槽17-24将用于统一缓冲区等——然而这可能会使用更多描述符集内存因此不推荐。最后可以为每个着色器阶段实现优化紧凑型动态插槽重映射例如如果顶点着色器使用纹理插槽0、4、5则它们映射到集合0中的Vulkan描述符索引0、1、2在运行时应用程序通过此重映射表提取相关纹理信息。 在所有这些情况下将纹理设置到给定插槽的一般实现不会运行任何Vulkan命令而只是更新阴影状态在绘制调用或调度之前需要从适当池分配一个新的描述符集用新描述符更新它然后使用vkCmdBindDescriptorSets绑定所有描述符集。请注意如果一个描述符集中有5个资源而自上次绘制调用以来只有其中一个发生了变化则仍需分配一个新的包含5个资源的描述符集并更新它们全部。 要通过这种方法达到良好的性能需要遵循几个准则 如果集合中没有任何内容发生变化则不要分配或更新描述符集。在不同阶段之间共享插槽模型下这意味着如果两个绘制调用之间没有设置任何纹理则不需要分配/更新带有纹理描述符的描述符集。如果可能请批量调用vkAllocateDescriptorSets——在某些驱动程序上每次调用都有可测量的开销因此如果需要更新多个集合将两者一起分配可能会更快。要更新描述符集可以使用带有写入数组的vkUpdateDescriptorSets或从Vulkan 1.1开始使用vkUpdateDescriptorSetWithTemplate。虽然利用vkUpdateDescriptorSets复制大多数来自先前分配数组中的描绘是诱人的但这可能会在从写合并内存中分配描绘时变得很慢。 描述符模板可以减少应用程序进行更新所需工作的数量——因为在此方案中需要从由应用程序维护的阴影状态中读取描述符信息因此通过告诉驱动程序阴影状态布局使得某些驱动上的更新速度显著加快。最后更倾向于动态统一缓冲区而不是更新统一缓冲区描绘。动态统一缓冲区允许通过vkCmdBindDescriptorSets中的pDynamicOffsets参数指定对缓冲对象的偏移而无需分配和更新新的描绘。这与动态常量管理很好地结合其中绘制调用常量从大型统一缓冲区中分配从而显著降低CPU开销并且可能对GPU更高效。虽然在某些GPU上为避免驱动中的额外开销需要保持动态缓冲区数量较少但一两个动态统一缓冲区应能很好地适应此方案在所有架构上均如此。 一般而言上述方法可以非常高效地提升性能——尽管不如下面所述具有更多静态描绘集合的方法那样高效但如果仔细实施它仍然能够超越旧API。不幸的是在某些驱动程序上分配和更新路径并不是非常优化——在某些移动硬件上如果可以在帧内重复使用那么根据包含的描绘缓存描绘集合可能是合理的。 4.2.5 基于频率的描述符集 虽然基于插槽的资源绑定模型简单且熟悉但并未产生最佳性能。一些移动硬件可能不支持多个描绘集合然而总体而言Vulkan API和驱动期望应用程序根据变化频率管理描绘集合。 一种更以Vulkan为中心的渲染器将根据变化频率将着色器所需的数据组织成组并为各个频率使用单独集合其中set 0表示变化最少而set 3表示变化最多。例如一个典型配置将包括 Set 0 描述符集合包含具有全局、每帧或每视图数据的统一缓冲区以及全球可用纹理如阴影贴图纹理数组/图集Set 1 描述符集合包含材料数据所需统一缓冲区和纹理描绘如反照率贴图、菲涅尔系数等Set 2 描述符集合包含具有逐绘制数据如世界变换数组的动态统一缓冲区 对于set 0我们期望其每帧仅改变几次因此可以采用类似上一节所述的方法进行动态分配方案。 对于set 1我们期望大多数对象材料数据在帧间保持不变因此只需在游戏代码更改材料数据时进行分配和更新。 对于set 2该数据将完全动态由于使用了动态统一缓冲区我们很少需要分配和更新此描绘集合——假设动态常量上传到一系列大型逐帧缓存对于大多数绘制我们需要用常量数据更新缓存并调用vkCmdBindDescriptorSets以新偏移量进行绑定。 请注意由于管道对象之间兼容性规则在大多数情况下只需每当材料发生变化时绑定sets 1和2当材料与上一次绘制调用相同则仅绑定set 2。这导致每次绘制调用只需一次vkCmdBindDescriptorSets调用。 对于复杂渲染器不同着色器可能需要使用不同布局——例如并非所有着色器都需要就材料数据达成一致。在极少数情况下根据帧结构也可能合理地使用超过3个集合。此外考虑到Vulkan灵活性不严格要求对场景中的所有绘制调用采用相同资源绑定系统。例如后处理绘制调用链通常高度动态各个绘制调用之间纹理/常量数据完全改变。一些渲染器最初实现了前一节中的动态基于插槽绑定模型然后继续额外实现基于频率的数据以进行世界渲染以尽量减少集合管理带来的性能损失同时仍保持基于插槽模型对渲染管道较为动态部分简单性。 上述方案假设在大多数情况下每次绘制的数据大小超过了通过推送常量有效设置大小。推送常量可以无需更新或重新绑定描绘集合即可设置由于每次绘制调用保证限制为128字节因此很容易将其用于逐画数据例如对象的一组4x3变换矩阵。然而在一些架构上可推送常量实际数量快速取决于着色器所用描绘设置更接近12字节左右。超过此限制可能迫使驱动将推送常量溢出到由驱动管理环形缓存这最终可能比将此数据移动到应用侧动态统一缓存更昂贵。虽然有限使用推送常量对某些设计仍然是好主意但在下一节中全面无绑定方案下更合适地使用它们。 4.2.6 无绑定描绘设计 基于频率的描绘集合减少了描绘集合绑定开销然而对于每个绘制调用仍然需要绑定一两个描绘集合。维护材料描绘集合需要管理层该层需要随时更新GPU可见描绘集合以便材料参数发生变化此外由于材质数据中缓存了纹理描绘这使得全局纹理流系统难以处理——无论何时某些mipmap级别被流入或流出都需要更新所有引用该纹理材料。这要求材质系统与纹理流系统之间复杂交互并引入额外开销每当调整文本时都会出现这种情况这部分抵消了基于频率方案带来的好处。最后由于每次绘制调用都需要设置描绘集合因此很难将上述任何方案适应GPU基础剔除或命令提交。 可以设计一种无绑定方案其中世界渲染所需设置绑定调用数量保持不变从而解耦材质与纹理描写使得实现全局流系统更加容易并促进GPU基础提交。如前面的方案一样可以结合小型场景部分采用动态临时说明书更新当那些地方抽象出较少数量时灵活性重要例如后处理。 为了充分利用无绑定功能仅靠核心Vulkan可能不足够一些无绑定实现要求在更新后无需重新绑定即可更新描写集合而这在核心Vulkan 1.0或1.1中不可用但可以通过VK_EXT_descriptor_indexing扩展来实现。然而如下所述基本设计可以无需扩展而工作只要设定足够高的信息限度。这要求双重缓存上述所述文本说明书数组以便不断被GPU访问并能及时更新单独说明书。 类似于基于频率设计我们将着色器数据拆分为全局统一变量和纹理set 0、材质数据以及逐画数据。全局统一变量和纹理可以通过前面章节相同方式指定说明书。 对于逐材质数据我们将把文本说明书移动到大型文本说明书数组中注意这与文本数组概念不同——文本数组只用一个说明书并强迫所有文本具有相同大小和格式说明书数组没有这一限制可以作为数组元素包含任意文本说明书包括文本数组说明书。材料数据中的每种材料将在此数组中拥有一个索引而不是文本说明书该索引将成为材料数据的一部分还会包含其他材料常数。 场景中所有材料的大型存储缓存将在这里存在虽然支持多种材质类型是可能但为了简单起见我们假设所有材料都能用相同的数据指定。以下是材质数据结构示例 struct MaterialData {vec4 albedoTint;float tilingX;float tilingY;float reflectance;float unused0; // pad to vec4uint albedoTexture;uint normalTexture;uint roughnessTexture;uint unused1; // pad to vec4 }; 类似地场景中所有对象的每次绘制常量可以驻留在另一个大的存储缓冲区中为简单起见我们假设所有的 per-draw 常量具有相同的结构。为了在这样的方案中支持蒙皮对象我们将把transform 数据提取到一个单独的第三个存储缓冲区中 struct TransformData {vec4 transform[3]; }; 我们迄今为止忽略的一个方面是顶点数据规范。虽然Vulkan通过调用vkCmdBindVertexBuffers提供了一种一流的方法来指定顶点数据但在每次绘制时绑定顶点缓冲区对于完全无绑定设计来说并不可行。此外一些硬件不支持将顶点缓冲区作为一类实体驱动程序必须模拟顶点缓冲区绑定这在使用vkCmdBindVertexBuffers时会导致一些CPU端的性能下降。在完全无绑定设计中我们需要假设所有顶点缓冲区都在一个大型缓冲区中进行子分配并使用每次绘制的顶点偏移vkCmdDrawIndexed的firstVertex参数让硬件从中获取数据或者在每次绘制调用中将此缓冲区中的偏移量传递给着色器并在着色器中从缓冲区获取数据。这两种方法都可以很好地工作具体效率可能因GPU而异在这里我们假设顶点着色器将执行手动顶点获取。 因此对于每个绘制调用我们需要向着色器指定三个整数 材质索引用于从材质存储缓冲区查找材质数据。然后可以使用材质数据中的索引和描述符数组访问纹理。变换数据索引用于从变换存储缓冲区查找变换数据。顶点数据偏移用于从顶点存储缓冲区查找顶点属性。 如果需要我们可以通过绘制数据指定这些索引和附加数据 struct DrawData {uint materialIndex;uint transformOffset;uint vertexOffset;uint unused0; // vec4 padding// ... extra gameplay data goes here }; 着色器需要访问包含MaterialData、TransformData、DrawData的存储缓冲区以及包含顶点数据的存储缓冲区。这些可以通过全局描述符集绑定到着色器唯一剩下的信息是绘制数据索引可以通过推送常量传递。 通过这种方案我们需要在每帧更新材料和绘制调用使用的存储缓冲区并使用我们的全局描述符集绑定它们一次此外我们还需要绑定索引数据——假设像顶点数据一样索引数据分配在一个大型索引缓冲区中我们只需使用vkCmdBindIndexBuffer绑定一次。 完成全局设置后对于每个绘制调用如果着色器发生变化我们需要调用vkCmdBindPipeline然后是vkCmdPushConstants以指定绘制数据缓冲区中的索引最后是vkCmdDrawIndexed。 在以GPU为中心的设计中我们可以使用vkCmdDrawIndirect或vkCmdDrawIndirectCountKHR由KHR_draw_indirect_count扩展提供并使用gl_DrawIDARB由KHR_shader_draw_parameters扩展提供作为索引来获取每次绘制常量而不是推送常量。唯一的注意事项是对于基于GPU的提交我们需要根据管道对象对CPU上的绘制调用进行分组因为否则不支持切换管道对象。 这样变换顶点的顶点着色器代码可能如下所示 DrawData dd drawData[gl_DrawIDARB]; TransformData td transformData[dd.transformOffset]; vec4 positionLocal vec4(positionData[gl_VertexIndex dd.vertexOffset], 1.0); vec3 positionWorld mat4x3(td.transform[0], td.transform[1], td.transform[2]) * positionLocal; 采样材质纹理的片段着色器代码可能如下所示 DrawData dd drawData[drawId]; MaterialData md materialData[dd.materialIndex]; vec4 albedo texture(sampler2D(materialTextures[md.albedoTexture], albedoSampler), uv * vec2(md.tilingX, md.tilingY)); 该方案最小化了CPU端开销。当然从根本上说这是多个因素之间的平衡 虽然该方案可以扩展到多种格式的材质、绘制和顶点数据但管理起来会更加困难。在某些架构上仅使用存储缓冲区而不是统一缓冲区可能会增加GPU时间。从由材质索引的数据数组中获取纹理描述符相较于某些替代设计可能会增加GPU上的额外间接访问。在某些硬件上各种描述符集限制可能使得该技术难以实施为了能够从着色器动态索引任意纹理maxPerStageDescriptorSampledImages应该足够大以容纳所有材质纹理——虽然许多桌面驱动程序在这里暴露了较大的限制但规范仅保证16个因此在某些其他支持Vulkan的硬件上无绑定仍然无法实现。 随着渲染器变得越来越复杂无绑定设计将变得更加复杂并最终允许将更大部分渲染管道移动到GPU由于硬件限制这种设计并不适用于每一个兼容Vulkan的设备但在为未来硬件设计新的渲染路径时绝对值得考虑。 4.3 命令缓冲记录与提交 在旧API中GPU命令有一个单一时间线CPU执行的命令按顺序执行到GPU因为通常只有一个线程记录它们没有精确控制CPU何时向GPU提交命令而驱动程序被期望优化管理命令流所用内存以及提交点。 相比之下在Vulkan中应用程序负责管理命令缓冲内存在多个线程中记录命令到多个命令缓冲并以适当粒度提交它们以供执行。尽管经过仔细编写代码后单核Vulkan渲染器可以显著快于旧API但通过利用系统中的多个核心进行命令记录可以获得峰值效率和最小延迟这需要仔细管理内存。 4.3.1 Mental Model 类似于描述符集命令缓冲是从命令池中分配的理解驱动程序如何实现这一点对于推测成本和使用影响非常重要。 命令池必须管理将被CPU填充命令并随后由GPU命令处理器读取的内存。命令占用内存量不能静态确定池的一般实现因此涉及固定大小页面的空闲列表。命令缓冲将包含实际命令页面列表以及特殊跳转指令将控制从每个页面转移到下一个页面以便GPU能够按顺序执行所有这些指令。每当需要从命令缓冲分配指令时它将编码到当前页面如果当前页面没有空间驱动程序将使用关联池中的空闲列表分配下一个页面将跳转编码到当前页面并切换到下一个页面以进行后续指令记录。 每个命令池只能由一个线程同时使用因此上述操作不需要线程安全。使用vkFreeCommandBuffers释放命令缓冲可能会通过将其添加到空闲列表来返回被该命令缓冲占用的页面。当重置命令池时可以将所有被所有命令缓冲占用的页面放入池空闲列表当VK_COMMAND_POOL_RESET_RELEASE_RESOURCES_BIT被使用时这些页面可以返回给系统以便其他池可以重用它们。 请注意没有保证vkFreeCommandBuffers实际上会将内存返回给池替代设计可能涉及多个命令缓冲在较大页面内分配块这使得vkFreeCommandBuffers很难回收内存。实际上在某个移动厂商上当默认设置下没有VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT时需要vkResetCommandPool才能重新利用未来的指令记录所需内存。 4.3.2 多线程命令记录 Vulkan对命令池使用有两个关键限制 从一个池分配的命令缓存不得被多个线程同时记录GPU仍在执行相关指令时不得释放或重置命令缓存和池 因此典型线程设置要求一组命令缓存池。该集合必须包含F T*个池其中F是帧队列长度——F通常为2一帧由CPU记录而另一帧正在由GPU执行或3T是可以同时记录指令的线程数这个数可以高达系统上的核心数量。当从线程记录指示时该线程需要使用与当前帧和线程关联的池分配一个指示缓存并将指示记录到其中。假设指示缓存不会跨帧边界进行记录并且在帧边界处通过等待队列中的最后一帧完成执行来强制实施帧队列长度那么我们就可以释放为该帧分配的所有指示缓存并重置所有关联命令池。 此外在调用vkResetCommandPool之后可以重用指示缓存而不是释放它们——这意味着不必再次分配指示缓存。虽然理论上分配指示缓存可能很便宜但一些驱动程序实现与指示缓存分配相关联有可测量开销。这也确保驱动程序永远不需要将指示内存返回给系统从而使这些缓存提交更便宜。 请注意根据帧结构上述设置可能导致跨线程的不平衡内存消耗例如阴影绘制调用通常需要更少设置和更少命令内存。当结合许多作业调度程序产生有效随机工作负载分布时这可能导致所有命名空间都根据最坏情况消耗进行调整。如果应用程序受限于内存且这成为问题则可以限制每个单独通行证并根据录制通行证选择相应的指示缓存/池以限制浪费。 这要求向指示缓存管理器引入大小类别概念。通过为每个线程创建一个命令池并手动重用已分配指示缓存如上所述可以为每个大小类别保持空闲列表根据绘制调用数量例如“100”、“100–400”等和/或单个绘制调用复杂性仅深度、gbuffer定义大小类别。根据预期用途选择缓存可导致更稳定的内存消耗。此外对于过小通行证在录制这些通行证时减少并行性也是值得考虑。例如如果通行证有100个绘制调用则相比于在4核系统上将其拆分为4个录制作业将其作为一个作业录制可能更高效因为这样可以减少对指示内存管理和提交开销。 4.3.3 命令缓存提交 虽然为了提高效率在多个线程上记录多个指示缓存很重要但由于状态不会跨越不同缓存重复利用且存在其他调度限制因此为确保GPU不会在处理期间处于空闲状态每个提交必须合理大。此外每次提交都会产生一定开销包括CPU侧和GPU侧。一般而言一个Vulkan应用应针对每帧10次提交每次提交占据0.5毫秒或更多GPU工作负载以及100个每帧每个指示缓存占据0.1毫秒或更多GPU工作负载。这可能需要根据特定通行证调整并发限制例如如果特定光源阴影通行证具有100个绘制调用则可能有必要限制此通行证录制过程中的并发性至仅一个线程。此外对于更短通行证将其与相邻通行证组合成一个指示缓存也变得有利可图。最后每帧提交次数越少越好——不过这必须与早期为框架提交足够工作以增加CPU和GPU并行性相平衡例如在录入框架其他部分之前有意义的是先提交所有阴影渲染相关联缚内容。 关键的是此处提及提交次数是针对所有vkQueueSubmit调用中总共提交VkSubmitInfo结构体数量而不是针对单独vkQueueSubmit调用次数。例如当提交10个指示缓存时与逐一处理10条VkSubmitInfo结构体相比更有效地使用一个VkSubmitInfo来一次性提交10条即使两者情况下仅执行一次vkQueueSubmit调用。本质上VkSubmitInfo是同步/调度单位因为它有自己的一组围栏/信号量。 4.3.4 次级命名空间 当应用程序中的某一渲染通道包含大量绘制调用例如gbuffer通道为了提高CPU提交流程效率将绘制调用拆分成多个组并在多个线程上记录非常重要。有两种方法来做到这一点 记录主指挥块将绘图块渲染到同一帧图像中使用vkCmdBeginRenderPass和vkCmdEndRenderPass; 使用vkQueueSubmit执行生成结果记录次级指挥块将图形块渲染传递给 vkBeginCommandBuffer 以及 VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT; 在主控制块中使用 vkCmdBeginRenderPass 和 VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS 然后再跟随 vkCmdExecuteCommands 执行所有已记录次级控制块 虽然立即模式 GPU 的第一种方法可行而且就 CPU 的同步点而言它稍微容易管理但对于采用切片渲染技术 GPU 而言第二种方法至关重要。在切片模式 GPU 上采用第一种方式则要求各切片内容需刷新至内存然后再加载回内容这对性能来说是灾难性的。 4.3.5 命名空间重用 根据上述关于提交控制块指导原则在大多数情况下多次提交流程后重新利用单一控制块变得不切实际。一般来说为场景某部分预先录入控制块的方法反而适得其反因为它们可能因保持控制块负载庞大而导致过多 GPU 负载同时还触发某些切片渲染器上的低效代码路径因此应用程序应该专注于改善 CPU 上线程及绘图提交流程成本。因此应采用VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT以确保驱动程序有自由生成无需重复播放多次的新任务。 这一规则偶尔会存在例外。例如对于 VR 渲染应用程序可能希望仅首次录入左眼与右眼之间合成视锥体对应控制块。如果逐眼数据显示来自单一统一控制块则此统一控制块可随后通过 vkCmdUpdateBuffer 更新再跟随 vkCmdExecuteCommands 如果采用次级控制块的话否则则为 vkQueueSubmit。不过要指出的是对于 VR 来说如果可用的话探索 VK_KHR_multiview 扩展也是值得考虑因为它应允许驱动进行类似优化。 4.4 管道障碍 管道障碍仍然是 Vulkan 代码最具挑战性的部分之一。在旧API中当发生诸如片段着色器读取之前已经呈现过纹理等危害情况时运行时和驱动负责确保适当特定硬件同步。这要求仔细跟踪每项资源绑定从而导致不得不付出过高 CPU 开销去执行有时过多 GPU 同步例如Direct3D 11 驱动通常会插入障碍以防任何两个连续计算调度共享相同UAV即使根据应用逻辑这些危害本身不存在。因为快速且最佳地插入障碍通常要求了解应用如何利用资源所以 Vulkan 要求应用自行处理这一过程。 为了实现最佳渲染管道障碍设置必须完美。如果缺失障碍风险应用遇到未测试或更糟糕的是尚未存在体系结构上的时间依赖错误那么最坏情况下甚至会导致 GPU 崩溃。不必要障碍则会降低 GPU 利用率因为这减少了潜在平行执行机会——甚至更糟糕的是会触发非常昂贵解压缩操作等问题。更糟糕的是目前虽然工具如Radeon Graphics Profiler能够可视化过多障碍成本但缺失障碍一般不会被验证工具检测出来。因此了解障碍行为、过度指定它们带来的后果以及如何与之协作至关重要。 4.4.1 Mental Model 规范以执行依赖关系和管道阶段之间内存可见性来描述障碍例如一个资源之前被计算着色器阶段写入并将在传输阶段读取以及图像布局变化例如一个资源之前处于通过颜色附加输出写入最佳格式应转换为通过着色器读取最佳格式。然而从其后果来看思考障碍也许更容易——即当使用障碍时 GPU 会发生什么情况。请注意 GPU 行为当然依赖于具体供应商及体系结构但帮助映射抽象方式指定之障碍至更具体构造能理解其性能影响非常重要。 障碍能造成三种不同结果 阻塞特定阶段直到另一个阶段完成当前所有工作。例如如果渲染通道向纹理输出数据而随后的渲染通道则利用顶点着色器读取此输出则 GPU 必须等待所有待处理片段着色器及ROP工作完成然后才能启动随后的阶段进行顶点工作。大多数阻塞操作将在某些阶段导致执行阻塞。刷新或失效内部 GPU 缓存并等待事务完成以确保另一个阶段能读取结果工作。例如在某些架构下 ROP 写入可能经过 L2 纹理缓存而传输阶段则直接操作内存。如果纹理已在渲染过程中呈现则后续传输操作读出陈旧的数据除非先刷新该缓存。同样如果纹理阶段需读取通过传输阶段复制得到图像则 L2 纹理缓存需失效以确保不含陈旧数据。不一定所有阻塞操作都需如此处理。转换资源所储藏格式一般来说就是解压缩资源储藏。例如在某些架构下 MSAA 纹理以压缩形式保存每像素都有样本掩码显示此像素包含多少独特颜色以及样本数据另做保存。如果传输阶段或着色器阶段无法直接读取压缩纹理则阻塞转换自VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL至VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL或VK_IMAGE_USAGE_TRANSFER_SRC_BIT就需解压缩纹理将所有样本写入内存。但大多数阻塞操作不必如此处理但那些确实如此处理成本极高。 考虑到这一点让我们尝试理解关于阻塞运用指导原则。 4.4.2 性能指导原则 当生成针对各自阻塞操作之独立事务时驱动仅对该阻塞持局部视角对过去与未来事务均无了解。因此第一个重要规则就是要尽量批量化阻塞事务。如果存在意味着等待片段阶段为空闲状态及L2纹理高速缓存刷新的阻隔那么驱动将在您呼叫 vkCmdPipelineBarrier 时忠实生成该事务。如果您指定多个资源于单条 vkCmdPipelineBarrier 调用之中只要其中任意转换必要则驱动仅生成一次L2高速缓存刷新事务从而降低成本。 为了确保阻隔成本不会高于必要仅需包括相关阶段。例如其中一种最常见类型就是把资源状态转变自VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL至VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL。当指定该阻隔的时候应明确哪些真实读取此资源之着色器阶段 via dstStageMask 。尽管诱人地想把stage mask指定为VK_PIPELINE_STAGE_ALL_COMMANDS_BIT以支持计算着色器或顶点着色器读取。然而这样做意味着后续绘图请求中的顶点着色工作不能启动这是有问题 在立即模式渲染者里稍微降低了各请求间平行性使得所有片段线程必须完成才能开始任何顶点线程这导致最终经过这个过程之后 GPU 利用率降至0然后逐渐提升至希望达到100%在切片模式渲染者里对于某些设计期望后续请求中的所有顶点工作完成才能开始片段工作因此等待片段工作结束才能开始任何其它请求完全消除了各自间平行性这是许多天真移植 Vulkan 标题遇见最大潜力性能问题之一。 请注意即使正确地指定了阻隔——假设我们从片段阶段读取贴图此dstStageMask应设定为VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT——但仍然存在执行依赖关系这仍然会导致降低 GPU 利用率。这种情况包括计算场景其中若想从由另计算场景生成的数据读取就需表达CS与CS间依赖关系但规定管道阻隔则保证完全排干 GPU 的计算任务然后再慢慢填充回来。因此不妨考虑透过所谓“拆分”方式规定依赖关系不是简单地运用 vkCmdPipelineBarrier 而是在写入操作完成之后呼叫 vkCmdSetEvent 然后再在读操作开始前呼叫 vkCmdWaitEvents。当然如果紧接 vkCmdSetEvent 就呼叫 vkCmdWaitEvents 则事倍功半且速度甚至比 vkCmdPipelineBarrier 更慢而应尝试重构算法确保 Set 与 Wait 间存在足够任务从而等待处理请求时该事件已基本信号无损效率。 另外有时候算法能够改组减少同步节点数量同时仍然运用管道阻隔使开销显得不那么显著。例如一个基于 GPU 的粒子模拟可能对每粒子效果运行两个计算调度第一个用于发射新粒子而第二个用于模拟粒子。这两个调度之间要求有管道阻隔以同步执行如果粒子系统依顺序模拟则则需逐粒子系统施加管道阻隔。一种更优实现首先提交通知发射粒子的全部调度彼此间无依赖接下来再发送用于同步发射与模拟调度之管道阻隔然后再发送全部模拟粒子的调度——这就能让 GPU 长时间保持良好利用率。从那之后运用拆分式屏蔽能够帮助彻底隐藏同步成本。 关于资源解压缩很难给出普遍建议——某些架构下这种情况根本不存在而另一些架构下确实如此不过取决于算法也许无法避免。在理解解压缩对画面的性能影响方面通过供应商专属工具如Radeon Graphics Profiler 是至关重要在某些情况下也许能够调整算法使其根本无需解压缩例如通过把任务移动到不同阶段。当然需要指出的是当完全没有必要发生资源解压缩且由于过度规定屏蔽造成这种情况发生例如如果您呈现目标框架其中包含深度目标且未来永远不会读取深度内容应保持深度目标处于VK_IMAGE_LAYOUT_DEPTH_STENCIL_OPTIMAL布局而非徒劳地转换成VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL这样反而触发解压缩记住驱动无法知道您未来是否要读取该资源。 4.4.3 简化屏蔽规范 面对规定屏蔽所涉及复杂性有助于拥有常见需求屏蔽实例作为参考。有幸的是Khronos Group 提供了许多有效且最佳屏蔽实例用于各种类型同步作为Vulkan文档库的一部分可以访问GitHub: Synchronization Examples · KhronosGroup/Vulkan-Docs Wiki · GitHub 这些实例能够帮助提升对一般屏蔽行为理解也能直接用于发布应用程序。同时对于未覆盖这些例子的情况以及一般而言为简化规范代码使其更加正确可切换至一种简易模型其中不仅全面规定访问掩码、各阶段及图像布局只需了解有关资源概念即封装可利用该资源之各阶及最常见访问类型之状态即可。那么所有转换都涉及把资源从状态A转变至状态B这样理解容易得多。因此Khronos Group成员及Vulkan规范共同作者Tobias Hector撰写了一款开源库simple_vulkan_synchronization该库将资源状态否则称作访问类型转换成 Vulkan 阻挡规范。这个库小巧简单同时支持拆分式屏蔽以及完整管线屏蔽功能。 4.4.4 用渲染图预测未来 上一节概述性能指导原则很难实际遵循尤其是在传统立即模式渲染架构下。在确保各阶及图像布局转换未过度规定方面很重要的一步就是了解未来如何运用该资源——如果想要呈现完结后的管线屏蔽没有这些信息通常只能强迫发出带有目标布局及目的阶掩码全部内容的信息。而解决此问题相比之后读出资源前发出屏蔽信息似乎颇具吸引力因为那时候就能知道如何书写此资源。然而这又使批量处理屏蔽信息变得困难。例如在具有三个渲染通路A、B、C 的框架中其中C分别读取A输出与B输出两条独立请求为尽量减少贴图高速缓存刷新的数量及其他屏蔽工作的数量一般而言最好是在C之前准确指定正确转换A、B输出的信息。而实际情况却是在C 的每条请求前都有一道屏蔽信息。在一些情况下拆分式屏蔽能够减少相关费用但一般来说即时屏蔽代价太高。同时运用即时屏蔽要求追踪资源状态以了解前一次布局这是很难做到正确多线程系统因为最终GPU上的执行顺序只有等全部任务录入线性化后才可知晓。 由于上述问题许多现代渲染器开始尝试使用渲染图作为一种声明性地指定帧资源之间所有依赖关系的方法。基于生成的有向无环图DAG结构可以建立正确的障碍包括在多个队列之间同步工作的障碍并以最小的物理内存使用量分配瞬态资源。关于渲染图系统的完整描述超出了本文的范围但感兴趣的读者可以参考以下演讲和文章 FrameGraph: Extensible Rendering Architecture in Frostbite, Yuriy O’Donnell, GDC 2017。Advanced Graphics Tech: Moving to DirectX 12: Lessons Learned, Tiago Rodrigues, GDC 2017。Render graphs and Vulkan—a deep dive, Hans-Kristian Arntzen。 不同的引擎选择不同的解决方案参数例如Frostbite渲染图由应用程序使用最终执行顺序指定本文作者发现这种方式更可预测且更可取而另外两个演示则基于某些启发式方法对图进行线性化以尝试找到更优的执行顺序。无论如何重要的是必须提前声明通道之间的依赖关系以确保可以适当地发出障碍。值得注意的是帧图系统在数量有限且占据所需障碍大部分的瞬态资源方面表现良好虽然可以将资源上传和类似流式工作的所需障碍作为同一系统的一部分进行指定但这可能使图变得过于复杂处理时间过长因此通常最好在帧图系统之外处理这些。 4.5 渲染通道 与旧API和新的显式API相比Vulkan中相对独特的一个概念是渲染通道。渲染通道允许应用程序将其渲染帧的大部分指定为一类对象将工作负载拆分为单独的子通道并明确列出子通道之间的依赖关系以便驱动程序能够调度工作并放置适当的同步命令。从这个意义上说渲染通道类似于上述描述的渲染图并且可以用来实现这些功能但存在一些限制例如当前渲染通道只能表达光栅化工作负载这意味着如果需要支持计算工作负载则应使用多个渲染通道。然而本节将重点关注更简单、更实用地集成到现有渲染器中的渲染通道使用同时仍提供性能收益。 4.5.1 加载和存储操作 渲染通道最重要的特性之一是能够指定加载和存储操作。通过这些操作应用程序可以选择每个帧缓冲附件的初始内容是否需要清除、从内存加载或保持未指定并未被应用程序使用以及在渲染通道完成后附件是否需要存储到内存中。这些操作至关重要——在切片架构上使用冗余加载或存储操作会导致带宽浪费从而降低性能并增加功耗。在非切片架构上驱动程序仍然可以利用这些操作为后续渲染执行某些优化——例如如果附件之前内容不相关但有相关压缩元数据则驱动程序可能会清除这些元数据以提高后续渲染效率。 为了最大限度地给予驱动程序自由非常重要的一点是指定所需最弱加载/存储操作——例如当向写入所有像素的附件绘制全屏四边形时在切片GPU上VK_ATTACHMENT_LOAD_OP_CLEAR可能比VK_ATTACHMENT_LOAD_OP_LOAD更快而在立即模式GPU上LOAD可能更快——指定VK_ATTACHMENT_LOAD_OP_DONT_CARE非常重要以便驱动程序能够做出最佳选择。在某些情况下VK_ATTACHMENT_LOAD_OP_DONT_CARE可能比LOAD或CLEAR更好因为它允许驱动程序避免对图像内容进行昂贵的清除操作但仍然清除图像元数据以加速后续渲染。 同样如果应用程序不期望读取绘制到附件的数据则应使用VK_ATTACHMENT_STORE_OP_DONT_CARE——这通常适用于深度缓冲区和MSAA目标。 4.5.2 快速 MSAA 解决 在向 MSAA 纹理绘制数据后通常会将其解析为非 MSAA 纹理以进行进一步处理。如果固定功能解析功能足够有两种方法可以在 Vulkan 中实现这一点 对于 MSAA 纹理使用 VK_ATTACHMENT_STORE_OP_STORE并在渲染通道结束后调用 vkCmdResolveImage。对于 MSAA 纹理使用 VK_ATTACHMENT_STORE_OP_DONT_CARE并通过 VkSubpassDescription 的 pResolveAttachments 成员指定解析目标。 在后一种情况下驱动程序将在子通道/渲染通道结束时执行必要工作以解析 MSAA 内容。 第二种方法可能显著更高效。在切片架构上采用第一种方法需要将整个 MSAA 纹理存储到主内存中然后再从内存读取并解析到目标第二种方法可以以最有效方式执行切片内部解析。在立即模式架构上一些实现可能不支持通过传输阶段读取压缩 MSAA 纹理——API 在调用 vkCmdResolveImage 前需要转换为 VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL 布局这可能导致 MSAA 纹理解压缩从而浪费带宽和性能。通过 pResolveAttachments驱动程序可以以最大性能执行解析操作而不考虑架构。 在某些情况下固定功能 MSAA 解析是不足够的。在这种情况下需要将纹理转换为 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL并在单独的渲染通道中进行解析。在切片架构上这与 vkCmdResolveImage 固定功能方法具有相同效率问题在立即模式架构上其效率取决于 GPU 和驱动。一个可能的替代方案是使用额外子通道通过输入附件读取 MSAA 纹理。 为了使其正常工作第一个向 MSAA 纹理绘制的子通道必须通过 pColorAttachments 指定 MSAA 纹理同时将 VK_ATTACHMENT_STORE_OP_DONT_CARE 设置为存储操作。执行解析的第二个子通道需要通过 pInputAttachments 指定 MSAA 纹理并通过 pColorAttachments 指定解析目标然后该子通道需要用着色器绘制全屏四边形或三角形该着色器使用 subpassInputMS 资源读取 MSAA 数据。此外应用程序需要指定两个子通道之间依赖关系这表明阶段/访问掩码与管线障碍类似以及依赖标志 VK_DEPENDENCY_BY_REGION_BIT。有了这些信息驱动应该有足够的信息安排执行以便在切片 GPU 上MSAA 内容永远不会离开切片内存而是在切片内部解决将结果写入主内存。请注意这是否发生取决于驱动并且不太可能导致立即模式 GPU 上显著节省。 4.6 管线对象 旧API通常根据功能单元将GPU状态拆分为块——例如在Direct3D 11中可以使用各种阶段VS、PS、GS、HS、DS的着色器对象集合以及状态对象集光栅化、混合、深度模板、输入汇编配置输入布局、基元拓扑以及一些其他隐式位如输出呈现目标格式来描述GPU完整状态模组资源绑定。然后API用户可以分别设置状态单独位而无需考虑底层硬件设计或复杂性。不幸的是这一模型并不符合硬件通常使用模型因此会出现几种性能陷阱 虽然单个状态对象应模拟GPU状态的一部分并且可以直接转移到设置GPU状态命令中但在某些GPU上所需的数据来自多个不同状态块。因此驱动通常必须保留所有状态副本并在Draw/DrawIndexed时将状态转换为实际GPU命令。随着光栅化管线变得更加复杂并获得更多可编程阶段一些GPU没有直接映射它们到硬件阶段这意味着着色器微代码可能依赖于其他着色器阶段是否处于活动状态在某些情况下还依赖于其他阶段特定微代码这意味着驱动可能必须根据只能在Draw/DrawIndexed时发现的数据编译新的着色器微代码。同样在某些GPU上从API描述中固定功能单元作为其中一个着色器阶段的一部分实现——改变顶点输入格式、混合设置或呈现目标格式都可能影响着色器微代码。由于状态仅在Draw/DrawIndexed时已知因此最终微代码必须重新编译。 虽然第一个问题较轻微但第二个和第三个问题会导致显著延迟因为由于现代着色器和着色器编译管线复杂性着色器编译根据硬件情况可能花费数十到数百毫秒。为了解决这个问题Vulkan和其他新API引入了管线对象概念——它封装了大多数GPU状态包括顶点输入格式、呈现目标格式、所有阶段状态以及所有阶段着色器模块。期望是在每个支持GPU上此状态足以构建最终着色器微代码和设置所需GPU命令因此驱动无需在绘制时编译微代码并尽可能优化管线对象设置。 然而该模型在Vulkan之上实现渲染器时面临挑战。有多种方法解决此问题各自涉及不同复杂性、效率与设计权衡。 4.6.1 即时编译 支持Vulkan最简单的方法是对管线对象进行即时编译。在许多引擎中由于缺乏与Vulkan匹配的一类概念因此呈现后端必须根据各种状态设置调用收集有关管线状态各部分的信息与Direct3D 11驱动所做类似。然后就在绘制/调度之前当完整状态已知时将所有单独位聚集起来并查找哈希表如果缓存中已有管线状态对象则可直接使用否则创建新对象。这一方案能使应用运行起来但存在两个性能陷阱。 一个小问题是需要哈希组合起来的状态潜力较大对于每次绘制调用这样做当缓存已包含所有相关对象时会非常耗时。这可以通过将状态分组到对象中并哈希指针来减轻一般来说从高级API视角简化状态规范也能有所帮助。 然而一个主要问题是对于必须创建任何管线状态对象而言驱动可能需要编译多个着色器至最终GPU微代码。这一过程耗时较长此外它无法与即时编译模型最佳线程化——如果应用仅用一个线程提交命令则该线程通常也会同时编译管线状态对象即使有多个线程也常常会有多个线程请求相同管线对象从而串行化编译又或者一个线程需要多个新管线对象这增加了整体提交延迟因为其他线程先完成却没有任何工作可做。 对于多线程提交即使缓存已满也会导致核心间争用访问缓存。不过幸运的是可以通过如下双级缓存方案解决此问题 缓存将有两个部分不变部分与变换部分。在执行管线缓存查找时我们首先检查不变缓存是否包含该对象——此过程无需任何同步。如果发生缓存未命中的情况我们锁定关键区域并检查变换缓存是否包含该对象如果没有则解锁关键区域、创建管线对象然后再次锁定并将其插入缓存有可能替换掉另一个对象如果两个线程请求相同对象仅发起一次编译请求则需额外同步。帧结束时将所有变换缓存中的对象添加至不变缓存并清空变换缓存以便下一帧访问这些对象可自由线程化。 4.6.2 管线缓存与缓存预热 虽然即时编译能够工作但会导致游戏过程中显著卡顿。当具有新一组着色器/状态之物体进入帧时我们最终不得不为其编译管线对象而这过程缓慢。这与Direct3D 11标题面临的问题相似不过Direct3D 11 驱动后台做了大量工作以隐藏编译延迟更早预先编译一些着色器并实施自定义方案动态修补字节码无需完全重新编译。而在Vulkan中希望应用手工智能地处理管线对象创建因此幼稚的方法效果不佳。 为了使即时编译更加实用非常重要的一点是利用Vulkan管线缓存在运行之间序列化它并从多个线程预热前面章节描述之内存中的缓存。 Vulkan提供了一种管线缓存对象VkPipelineCache可以存储特定于驱动程序的一组位及着色器微代码从而改善管线对象之编译时间。例如如果应用创建两个设置完全相同但剔除模式不同之管线对象则着色器微代码通常相同。为了确保驱动仅需一次完成该物体生成应让应用传递相同VkPipelineCache实例给vkCreateGraphicsPipelines两次此情况下第一次调用会完成着色器微代码生成而第二次则能复用它。如果这些调用同时发生于不同线程则因数据仅当其中一次调用完成后才添加至缓存所以驱动仍然可能重复生成两次该着色器。 创建所有管线对象时务必使用相同VkPipelineCache物体并利用vkGetPipelineCacheData及VkPipelineCacheCreateInfo中的pInitialData成员序列化至磁盘。这确保已生成物体将在运行间复用从而减少随后的应用运行期间框架波动时间。 遗憾的是在第一次游戏过程中由于尚未包含所有组合因此仍然会出现着色器编译波峰。此外即便管线缓存包含必要微代码但vkCreateGraphicsPipelines不是免费的因此新建管线物体之编译仍然会增加框架时间方差。要解决这一点可以选择预热内存中的缓存和/或vkPipelineCache以提升加载速度。 这里一种可行方案是在游戏过程结束后让渲染者保存内存中的管线缓存数据——哪些着色器与哪些状态一起被用过——至数据库。然后在QA测试过程中该数据库可通过多次测试填充来自不同图形设置等的数据——有效地收集出实际游戏过程中很可能被用到的一系列状态。 这个数据库随后可以随游戏一起发布游戏启动时可利用来自该数据库的数据预填充内存中的缓冲区或者根据当前图形设置下各类组合数量此预热阶段也可仅限当前设定下各类组合。此过程应当在线程间进行以降低加载时间影响首次运行仍会有较长加载时间这可通过Steam预缓冲等特性进一步减少但因即时创建造成框架波峰基本可以避免。 如果QA测试过程中未发现特定组合集该系统仍然能正常运作只不过代价是一定程度上的卡顿。这一结果方案基本普遍且实用但需要付出潜力巨大的努力去经历足够关卡及不同设定以捕捉大多数现实工作负载使其管理起来稍显困难。 4.6.3 提前编译 “完美”解决方案即Vulkan设计初衷就是去掉即时编译缓存及预热而让每个潜在管线物体提前就绪。 这通常要求改变渲染者设计将管线路径概念整合进材质系统使得材质能够完全指定其路径。在这里存在多种设计选项本节将概述其中一种但重要的是理解这一一般原则。 通常一个物体与材质关联该材质规定了呈现该物体所需之图形路径及资源绑定。在这种情况下将资源绑定与图形路径分开非常重要因为目标是能够提前枚举出所有组合路径。我们称这一集合为“技术”这一术语故意与Direct3D Effect Framework中的术语相似不过在那里路径被保存在pass中。技术随后可被归类成效果而材质则引用效果以及某种键值来指定效果中的技术。 效果集合及每个效果中的技术集合都是静态效果集合也是静态。尽管效果不是能够预先编译出管线路径的重要因素但它们能作为技术有效语义分组。例如经常材料创建期间分配给材料效应但技术则依据物体被呈现位置例如阴影传递、gbuffer传递、反射传递或活跃游戏效果变化例如高亮而变化。 关键的是每项技术必须静态提前规定创建管线路径所需全部路径——通常作为某文本文件定义的一部分无论是在类似D3DFX DSL 的形式下还是 JSON/XML 文件中。其中必须包括全部着色器、混合态、剔除态、顶点格式、呈现目标格式及深度态。 这儿给出一个示例 technique gbuffer {vertex_shader gbuffer_vsfragment_shader gbuffer_fs #ifdef DECALdepth_state less_equal falseblend_state src_alpha one_minus_src_alpha #elsedepth_state less_equal trueblend_state disabled #endifrender_target 0 rgba16frender_target 1 rgba8_unormrender_target 2 rgba8_unormvertex_layout gbuffer_vertex_struct } 假设包括用于后处理等目的之所有绘制调用均运用效应系统来制定呈现路径同时假设效应与技术集合静态那么就很简单地能预先创建所有管线路径——每项技术只需一次即可——利用多线程方式加载时间在运行时期则能运用非常高效代码无需内存缓冲或框架波峰概率。 实际上在现代渲染者中实现此系统是一项复杂性管理练习。经常运用复杂shader或state排列组合例如为双面呈现通常需要改变剔除态也许还要改变shader以实现双面照明。而对于骨骼动画呈现需要改变顶点格式并添加一些代码到顶点shader中用骨骼矩阵转变属性。在某些图形设置下也许决定呈现目标格式需要浮点R10G11B10而不是RGBA16F以节省带宽。这些组合都会成倍增加需要您能够简洁高效地表示它们同时准确意识到不断增长组合数量以及适当重构/简化它们。一些效果稀少到甚至可单独传递而不会增加排列数量。有些计算简单到始终运算它们比增加排列数量要划算。而一些呈现技术提供了更好的解耦与关注分离也能减少排列数量。 不过值得注意的是将state排列组合加入其中使得问题更加困难却不会使其有所区别——许多renderer无论如何都要解决大量shader排列组合的问题一旦您把所有render state纳入shader/技术规范同时专注于减少技术排列数量相同复杂性管理解决方案也适用于这两者的问题。 实施这样的系统好处就是对所有所需组合拥有完美知识相比依赖脆弱排列发现系统极佳性能且随后的帧间方差最低包括首次加载以及强迫机制保持呈现代码复杂性受控。 4.7 总结 Vulkan API 将大量责任从驱动开发人员转移到了应用开发人员身上。当众多实现选项可供选择时各种渲染功能间导航变得更加棘手写出正确Vulkan renderer已经足够具有挑战性但性能和内存消耗至关重要。本文试图讨论处理Vulkan中特定问题时的重要考虑因素展示提供不同复杂性、易用性和性能权衡多种实现方法以及涵盖从移植现有renderer 到围绕Vulkan重新设计renderer 范围的问题。 最终很难给出适用于所有供应商和renderer 的普遍建议。因此在目标平台/供应商上分析生成代码至关重要——对于Vulkan而言非常重要的是监控计划发布游戏上的各供应商性能因为应用做出的选择尤为重要而且在某些情况下如固定功能顶点缓冲绑定是一个供应商快速路径但另一个却慢速路径。 除了利用验证层确保代码正确性以及诸如AMD Radeon Graphics Profiler 或 NVidia Nsight Graphics 等特定供应商分析工具外还有许多开源库可以帮助优化您的Vulkan renderer VulkanMemoryAllocator. GitHub - GPUOpen-LibrariesAndSDKs/VulkanMemoryAllocator: Easy to integrate Vulkan memory allocation library 提供方便且高效的 Vulkan 内存分配函数以及其他如碎片整理等相关算法。volk. GitHub - zeux/volk: Meta loader for Vulkan API 提供一种简单的方法直接从驱动获取 Vulkan 条目函数可减少函数调用开销。simple_vulkan_synchronization. GitHub - Tobski/simple_vulkan_synchronization: A single-header library with a simplified interface for Vulkan synchronization 提供了一种基于简化访问类型模型指定 Vulkan 障碍的方法有助于平衡正确性与性能。Fossilize. GitHub - Themaister/Fossilize: This repository is discontinued, see https://github.com/ValveSoftware/Fossilize 提供对各种 Vulkan 对象序列化支持其中最显著的是用于实现pipeline cache 的pipeline state 创建信息序列化支持。perfdoc. GitHub - ARM-software/perfdoc: A cross-platform Vulkan layer which checks Vulkan applications for best practices on Arm Mali devices. 提供类似验证层那样分析绘制指令流并识别ARM GPU 潜在性能问题层支持 最后一些供应商开发了Linux 的开源 Vulkan 驱动研究其源代码有助于深入了解某些 Vulkan 构造性能 AMD. GPUOpen Drivers · GitHub. 包含xgl其中含有 Vulkan 驱动源以及PAL它是xgl 使用的一款库许多 Vulkan 函数调用最终都会经过xgl 和PAL 两者。AMD. https://github.com/mesa3d/mesa/tree/master/src/amd/vulkan 包含社区开发开源radv 驱动源文件Intel. https://github.com/mesa3d/mesa/tree/master/src/intel/vulkan 包含Anvil 驱动源文件 致谢 作者感谢Alex Smith (Feral Interactive)、Daniel Rákos (AMD)、Hans-Kristian Arntzen (ARM)、Matthäus Chajdas (AMD)、Wessam Bahnassi (INFramez Technology Corp) 和Wolfgang Engel (CONFETTI) 审阅本文草稿并帮助改进内容质量。
http://www.w-s-a.com/news/512818/

相关文章:

  • 基金项目实验室信息网站建设wordpress文章新窗口打开
  • php网站开发就业前景做网站推荐源创网络
  • wordpress 8211西安网站优化维护
  • 泰安招聘网站有哪些wordpress 回复提醒
  • 网站服务器不稳定怎么打开网页企业营销策划心得体会
  • 自己做视频网站会不会追究版权企业商城网站建设方案
  • 烟台网站制作计划网站做seo的好处
  • 网站首页轮播官方网站下载拼多多
  • 罗庄区建设局网站自己做网站推广产品
  • 优秀flash网站欣赏苏州吴中区建设局网站
  • 网站添加wordpress博客网上商城购物系统论文
  • 上海市建设安全协会网站王夑晟企业网站建设需要做些什么
  • 网站app 开发辽宁建设工程信息网官网新网站是哪个
  • 厦门建设企业网站建设wordpress添加形式
  • 建立什么网站可以赚钱室内设计效果图qq群
  • 厦门网站设计大概多少钱服务公司沈傲芳
  • 如何起手做网站项目百度站长app
  • dede 购物网站wordpress主题超限
  • 做啊录音网站WordPress音乐悬浮插件
  • 安徽建设厅证书查询网网站网站策划与运营课程认知
  • wordpress证书关闭重庆seo优化效果好
  • 直播网站建设模板网站活动怎么做的
  • 医院网站建设网站网站开发工资高嘛
  • 个人网站备案内容写什么做网站是不是涉及很多语言职
  • 怎么用手机做一个网站门户网站建设工作的自查报告
  • 网站搭建怎么收费浙江建设集团网站
  • 建网站怎么赚钱免费引流软件下载
  • 自建网站服务器备案做基础销量的网站
  • 淘宝 网站建设 发货音乐网站首页设计
  • 丽水做网站杭州建电商网站多少钱