尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Simple Automatic Resource Synchronization Method for Vulkan Applications

Simple Automatic Resource Synchronization Method for Vulkan Applications 17.1 引言较早期的图形应用程序接口如OpenGL在内存操作顺序方面提供了诸多隐式保证。例如将图像作为某个绘制调用的颜色附件使用后在后续绘制调用中继续将同一图像作为纹理进行采样这种操作能按直觉预期正常工作。对API使用者而言这种方式相当简单便捷然而现代图形处理器并非以如此简单的方式运作它们是具有复杂内存层次结构的大规模并行机器。这意味着举例来说某个绘制调用的特定阶段可能与其他绘制调用的其他阶段并行执行或者阶段A可能需要刷新缓存才能看到阶段B执行的某些操作产生的内存效应。若未进行恰当协调内存操作可能相互冲突写入操作可能彼此竞争读取操作可能返回部分数据或过时数据等。传统API实现抽象化了内存访问同步的诸多细节因此用户通常无需关注这些问题。相比之下Vulkan API将确保资源内存访问之间不存在冲突的责任交由应用程序开发者承担。为此Vulkan提供了一系列工具——内存屏障、事件、信号量等——API使用者需要利用这些工具来显式管理访问同步。这种显式的同步方法理论上使得在 GPU 上实现比传统 API 允许的更高级别的并行度成为可能。然而承担管理同步的责任并非易事——它很可能被认为是 Vulkan 最复杂和令人困惑的方面之一。为了在保持正确性和可移植性的同时实现可接受的性能应用程序需要一种系统的方法来处理显式同步的复杂性。一种流行的处理同步方法通常被称为渲染图或帧图 [ODonnell 17]。它已在许多已发布的应用中成功使用。通过了解给定帧中将使用哪些资源以及应用于这些资源的操作的性质和顺序渲染图可以推断出在帧内同步命令的最佳方式从而在不会引入任何冲突的情况下实现最大程度的并行性。除此之外渲染图还可用于在帧的不同时间点自动为不同资源复用相同的后备内存——这种技术称为资源别名有助于减少 GPU 内存使用量。尽管有诸多优点但渲染图未必是适合所有人的理想解决方案。其实施和维护成本可能很高并且可能迫使渲染器的代码采用原本可能不会选择的模式。某些应用程序可能更倾向于用渲染图的某些好处来换取更简单、更灵活的渲染代码编写方式。在本文中我们提出了一种相对简单的替代方法来解决同步问题。它在一定程度上牺牲了使用渲染图可以实现的部分并行性以换取实现、维护和使用的简单性。据了解业界以前也曾使用过类似的方法因此作者并不声称其具有新颖性。本文假设读者至少具备 Vulkan 的基础知识。本文描述的具体实现方法是为 nicegraf [nicebyte 24] 开发的这是一个开源的图形 API 抽象层。nicegraf 可以最好地描述为渲染硬件接口RHI——一个与现有 GPU API 非常相似的接口并为每个支持的平台提供特定实现。就 RHI 而言nicegraf 的目标是处于中等抽象级别。为此nicegraf 不要求用户手动管理资源内存或同步。nicegraf 的 Vulkan 后端会自动推断并插入必要的内存屏障且这些屏障不会过度限制性能。17.1.1 先前工作帧图/渲染图已被多个游戏引擎广泛使用并且其使用有详尽的文档记录 [ODonnell 17, Arntzen 17, Gaule 24]。本文提出的方法在理念上更接近那些没有显式同步机制的API驱动程序如何处理风险跟踪。这里讨论的具体实现首次在 Vulkanised 2024 会议上提出 [Dzhavadyan 24]尽管实现细节较少。类似的方法已用于 DawnWebGPU API 的一个实现 [Dawn 24, Bernhardsson 24]的 Vulkan 后端以及 NVIDIA 渲染硬件接口NVRHI[NVIDIA 24]。它也在某种程度上类似于 LunarG Vulkan SDK [Zulauf 24] 中的同步验证所采用的方法都会补发缺失的内存屏障而非仅作报告但显著区别在于验证层必须考虑用户发出的同步命令所产生的影响。17.1.2 概述以下是本文其余部分各节的简要路线图在第 17.2 节中我们提供了该技术的总体概述。我们阐述了在同时记录多个命令缓冲区的情况下朴素的自动同步方法难以维持的问题并提出了一个解决方案。第 17.3 节引入了同步请求和资源同步状态的概念。我们详细描述了这两种结构并深入讨论了如何发出和处理同步请求。第 17.4 节讨论了如何以尽量减少对vkCmdPipelineBarrier调用次数的方式将同步请求转换为实际的 Vulkan 同步命令。第 17.5 节描述了当资源需要被命令读取或写入时会发生什么从而展示了前面章节的概念如何协同工作以在单个命令缓冲区内实现正确的同步。第 17.6 节讨论了不同命令缓冲区之间的同步问题。最后我们通过讨论所提出方法的局限性并检视在实践环境中应用该方法的效果来结束本文。17.2 技术17.2.1 命令缓冲区带来的问题Vulkan 与其他一些 GPU API 类似引入了命令缓冲区的概念。命令缓冲区为应用程序在如何发出 GPU 命令方面提供了很大的灵活性。然而命令缓冲区也使得自动化资源访问同步的任务更加困难。为了理解原因让我们首先考虑一个简单的情况即应用程序一次只允许记录一个命令缓冲区。在该场景下请考虑以下示例见图 17.1我们有两个资源一个图像 A 和一个缓冲区 B。一个渲染通道将 A 用作颜色附件。一个计算通道将一些数据写入缓冲区 B。最后另一个渲染通道对图像 A 进行采样并从缓冲区 B 读取数据。与这些操作对应的命令按上述顺序记录到我们唯一的命令缓冲区中。只要我们跟踪哪些资源被哪些操作接触过在我们记录命令到命令缓冲区时推断并发出正确的内存屏障并不太难。实际上在我们必须开始执行记录示例中最后一个渲染通道的命令时我们已经拥有所有必要的上下文我们知道一个渲染通道已经写入了 A。我们知道一个计算分发已经写入了 B。我们拥有这些上下文因为在我们即将开始记录最后一个渲染通道时我们已经看到了我们唯一命令缓冲区中之前的整个命令历史。我们不可能错过某个需要同步的命令所有命令都按明确定义的顺序进入同一个命令缓冲区。然而一旦我们引入可能在任何给定时间同时记录多个命令缓冲区的情况问题就变得更具挑战性。考虑一个类似于上面的例子不同之处在于写入 A 和 B 的命令被记录到命令缓冲区 X 中而从 A 和 B 读取的命令则被记录到另一个命令缓冲区 Y 中见图 17.2。在这种情况下我们在记录命令缓冲区时不再拥有发出内存屏障所需的必要上下文。例如在记录命令缓冲区 Y 中的渲染通道命令时我们不知道命令缓冲区 X 是在 Y 之前还是之后提交。实际上在我们需要将命令记录到 Y 中的那一刻修改 A 和 B 的命令可能甚至还没有被记录到 X 中。同时存在多个活动命令缓冲区对象的根本问题在于没有一个单一的、有序的命令流可供分析。在单个命令缓冲区的情况下我们可以访问提交顺序中完整的命令历史。但在允许多个命令缓冲区的情况下我们无法做到这一点它们的提交顺序是先验未知的。17.2.2 中间屏障解决上述问题的方案包含两部分在记录单个命令缓冲区时对同步做出简化的假设。在稍后的时间点重新审视这些假设并插入任何可能被遗漏的内存屏障。我们引入资源同步状态的概念可以将其视为用于跟踪不同流水线阶段对资源访问的一块数据。每个资源都有一个单一的全局同步状态。此外每个资源在每个使用它的命令缓冲区中都有一个本地同步状态见图 17.3。在记录单个命令缓冲区时我们将对是否需要同步做出某些简化的假设。更确切地说我们将假设在给定的命令缓冲区内流水线阶段对资源的首次访问不需要同步。对于同一流水线阶段的后续访问将使用资源的本地同步状态作为算法的输入该算法决定给定访问是否需要同步我们稍后将详细描述该算法。对于访问资源的每个流水线阶段我们将跟踪其对资源的首次访问以及直到第一个需要同步的访问为止的所有后续访问。我们将这组阶段-访问对统称为该资源对于给定命令缓冲区的期望同步状态。期望同步状态有效地指明了在执行命令缓冲区之前哪些流水线阶段内的哪些访问应该不需要针对该资源进行同步。只要每个参与资源的期望同步状态所表达的需求得到满足我们记录的每个单独的命令缓冲区就不会引入任何风险。解决方案的第二部分涉及在帧内所有命令缓冲区的内容和提交顺序已知后插入缺失的同步。回想一下在前一步骤中我们假设所有对资源的首次访问都不需要同步。现在当给定帧的所有命令及其提交顺序都可用时可以重新审视这些假设如果证明是错误的则通过修补缺失的内存屏障来纠正。一个帧的所有命令缓冲区都在同一个线程上提交形成一个有序的时间线。对于每个提交的命令缓冲区我们将其内使用到的资源的期望同步状态与它们各自的全局同步状态进行比较。通过这种比较可以直接推断出满足即将执行的命令缓冲区的期望同步状态所需的内存屏障从而维护一个良好同步的命令流。推断出的屏障被记录到一个辅助命令缓冲区中该缓冲区在即将执行的主命令缓冲区之前提交随后更新所有参与资源的全局同步状态。17.3 同步状态与同步请求在本节中我们将更详细地描述同步状态数据结构。我们还将介绍同步请求的概念。最后我们将描述在给定上下文中决定是否需要内存屏障的过程。17.3.1 同步状态清单 17.1 包含了 nicegraf 中使用的同步状态数据结构的定义。typedef struct ngfvk_sync_state { // 1. 最后一次写入的信息 ngfvk_sync_barrier_masks last_writer_masks; // 2. 哪些读取已经同步过了 uint32_t per_stage_readers_map; // 3. 图像当前布局仅对图像有效 VkImageLayout layout; } ngfvk_sync_state; typedef struct ngfvk_sync_barrier_masks { VkAccessFlags access_mask; // 什么类型的访问读/写 VkPipelineStageFlags stage_mask; // 在哪个流水线阶段 } ngfvk_sync_barrier_masks; // 清单17.1 同步状态数据结构同步状态包含三个主要部分字段last_writer_masks指定哪个流水线阶段中的哪种访问最后修改了资源。在清单 17.1 中它被命名为last_writer_masks。字段per_stage_readers_map指定哪些流水线阶段中的哪些访问已经通过内存屏障使最后一次写入的结果对它们可用和可见。在清单 17.1 中它被命名为per_stage_readers_map。该字段将一个阶段-访问对映射到一个二进制值指示给定阶段中的给定访问是否已经观察到最后一次写入操作的结果。在 nicegraf 中这种映射是通过位图实现的见图 17.4因为不到 32 位就足以处理我们必须应对的所有阶段和访问的组合。该字段内的每 3 位一组对应于一个特定的流水线阶段而 3 位组内的每个位要么对应于其阶段内的特定访问要么未被使用。最后字段layout指定资源的当前布局如果资源是图像在清单 17.1 中命名为layout。17.3.2 同步请求同步请求或同步请求描述访问资源的意图。它包含两部分指定请求访问的流水线阶段以及请求的访问类型的位掩码访问期间资源必须处于的布局仅适用于图像。以下是 nicegraf 中使用的相应ngfvk_sync_req结构的定义typedef struct ngfvk_sync_req { ngfvk_sync_barrier_masks stage_and_access_masks ; VkImageLayout layout ; } ngfvk_sync_req ;在即将记录可能导致资源被读取或修改的命令时会发出同步请求。在 nicegraf 中在以下情况下会发出同步请求绑定属性或索引缓冲区发生这种情况时会为正在绑定的缓冲区发出同步请求。请求的流水线阶段掩码设置为VK_PIPELINE_STAGE_VERTEX_INPUT_BIT访问掩码设置为VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT如果绑定的是属性缓冲区或VK_ACCESS_INDEX_READ_BIT如果绑定的是索引缓冲区。启动传输操作例如缓冲区到缓冲区、缓冲区到图像、图像到缓冲区。在这种情况下会发出两个同步请求一个用于源资源一个用于目标资源。两个请求的流水线阶段掩码都设置为VK_PIPELINE_STAGE_TRANSFER_BIT。对应于源资源的请求的访问掩码设置为VK_ACCESS_TRANSFER_READ_BIT而对应于目标资源的请求的访问掩码设置为VK_ACCESS_TRANSFER_WRITE_BIT。如果源资源是图像其对应的同步请求将期望布局设置为VK_LAYOUT_TRANSFER_SRC_OPTIMAL。如果目标资源是图像其对应的同步请求将期望布局设置为VK_LAYOUT_TRANSFER_DST_OPTIMAL。开始渲染通道发生这种情况时我们会为渲染通道将要写入的每个附件发出同步请求。对于每个颜色附件同步请求的流水线阶段掩码设置为VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT同时将VK_ACCESS_COLOR_ATTACHMENT_READ_BIT和VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT都添加到访问掩码中。为颜色附件同步请求指定的布局是VK_LAYOUT_COLOR_ATTACHMENT_OPTIMAL。对于深度/模板附件同步请求的流水线阶段掩码设置为包括VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT和VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT访问掩码设置为包括VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT和VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT。深度/模板附件的布局设置为VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL。绑定资源以在着色器中使用在这种情况下会为正在绑定的资源发出同步请求。流水线阶段掩码根据实际使用该资源所绑定的描述符的着色器阶段来设置。这些阶段可以通过使用 SPIRV-Reflect 库分析当前绑定的管线状态对象中使用的着色器模块来确定此分析仅在管线状态对象创建期间执行一次。描述符的类型决定了访问掩码VK_ACCESS_SHADER_READ_BIT用于只读纹理、只读存储缓冲区和 texel 缓冲区VK_ACCESS_UNIFORM_READ_BIT用于 uniform 缓冲区VK_ACCESS_SHADER_READ_BIT|VK_ACCESS_SHADER_WRITE_BIT用于读写存储图像和存储缓冲区。请注意我们区分了只读和读写存储缓冲区这允许生成的内存屏障限制更少。同步请求可能会也可能不会导致发出内存屏障这个决定由一个特殊的过程做出该过程将同步请求和发出同步请求的资源的当前同步状态作为输入。17.3.3 处理同步请求的规则决定特定同步请求是否需要内存屏障的过程必须遵循一套特定的规则以提供所需的正确性和性能保证。以下是这些规则并发读取几乎总是应该被允许的。这是能够并行运行更多事情的先决条件。一次写入不能与对同一内存位置的任何其他写入或读取并发。这是避免冲突的先决条件。这意味着任何请求写入访问的流水线阶段都将获得对资源的独占访问。理想情况下独占访问将仅限于受写入操作影响的资源子区域。然而nicegraf 的实现在这里并不最优因为我们是将限制应用于整个资源。这限制了 GPU 操作理论上可实现的并行性但使同步状态跟踪更容易。总的来说状态跟踪的粒度与它引起的 CPU 开销之间存在权衡更细粒度的跟踪可能导致 GPU 内存屏障行为更佳但代价是消耗更多的 CPU 周期。一旦先前对资源的写入的结果已通过内存屏障对某个流水线阶段内的访问可用且可见那么在该资源被再次修改之前该访问不需要针对该资源进行进一步的同步。这是避免多余同步的先决条件。图像布局转换是读-修改-写操作并且出于屏障发射的目的它们被视为写入。它们是第一条规则的唯一显著例外如果两个对同一图像的读取需要源图像使用不同的布局则它们不能并发。例如考虑一个将像素从图像复制到缓冲区的传输操作随后是一个对同一图像进行采样的计算分发。除非图像使用VK_IMAGE_LAYOUT_GENERAL布局否则这两个只读操作不能并发因为它们之间需要进行布局转换。17.3.4 处理同步请求的算法我们现在将描述 nicegraf 用来确定同步请求是否需要内存屏障的过程给定发出请求的资源的同步状态。同步请求可以分为修改性或非修改性。如果同步请求指定了任何暗示写入资源的访问例如VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT、VK_ACCESS_SHADER_WRITE_BIT等则该请求被归类为修改性。此外如果同步请求暗示了图像布局转换则即使请求的访问是只读的它也被视为修改性。我们通过将同步请求的布局字段与给定同步状态中指定的布局进行比较来确定是否暗示了布局转换。如果没有暗示布局转换并且请求的访问是只读的则该请求被视为非修改性。如果同步请求是非修改性的我们首先检查资源是否经历过先前的写入。这是通过简单地将当前同步状态的last_writer_masks与零进行比较来完成的。如果资源没有经历过任何先前的写入则不需要内存屏障。但是我们仍然需要更新与同步请求中指定的阶段和访问相对应的per_stage_readers_map字段的位。这样做是为了能够将这些读取与任何未来的读取或修改操作同步。如果资源经历过先前的写入我们需要检查与同步请求中指定的访问和流水线阶段相对应的per_stage_readers_map的位以确定这些访问是否已经赶上了先前的写入。如果任何相关的位为 0则需要发出相应的内存屏障以避免读后写冲突之后需要更新这些位。否则不需要采取任何行动。如果同步请求是修改性的并且资源经历过任何先前的读取或写入操作则需要发出相关的内存屏障以避免写后读和写后写冲突。随后同步状态的last_writer_masks字段需要被赋值为请求中指定的访问和流水线阶段并且per_stage_readers_map字段需要被清零。如果正在执行布局转换则需要相应地更新同步状态的layout字段并且需要在per_stage_readers_map中设置与请求的访问和阶段相对应的位因为根据 Vulkan 规范布局转换的结果会自动对它们可用且可见。17.4 发出屏障17.4.1 最小化对 vkCmdPipelineBarrier2 的调用次数出于性能原因在 Vulkan 中尽量减少对vkCmdPipelineBarrier2的调用次数倾向于尽可能通过单次调用发出多个屏障这被认为是一种良好实践。为了实现这一点nicegraf 批量处理同步请求当发出请求时其处理被推迟到最后一刻。当那个时刻到来时我们一次性处理所有待处理的同步请求批按照它们发出的顺序。先前描述的同步请求处理算法被应用于批次中的每个请求以确定是否需要内存屏障。所有由此产生的内存屏障都通过单次调用vkCmdPipelineBarrier2发出。我们尽可能使用VK_KHR_synchronization2扩展。通常没有理由不使用此扩展因为有一个 SDK 层可以在没有原生支持的平台上模拟它。倾向于使用VK_KHR_synchronization2而不是传统同步 API 的原因之一是传统的vkCmdPipelineBarrier函数需要将所有内存屏障的源和目标流水线阶段合并到两个位掩码中而vkCmdPipelineBarrier2允许单个屏障指定它们自己的阶段掩码。这可能会创建虚假的依赖关系从而导致同步效率降低。17.4.2 处理同步请求批次对于计算分发所有待处理的同步请求在处理后屏障会在记录分发命令之前立即发出。对于绘制调用情况稍微复杂一些。问题在于Vulkan 规范将在渲染通道实例内发出的屏障的同步范围限制为仅当前子通道。这意味着如果我们需要将渲染通道实例内的某个命令与渲染通道实例外的某个事物同步我们也需要在渲染通道实例外发出相应的内存屏障。然而为了知道我们可能想要与什么同步我们需要已经看到渲染通道内的所有命令。这意味着我们不能在遇到渲染通道命令时立即记录它们。有几种可能的方法可以解决这种情况根本不使用渲染通道转而依赖VK_KHR_dynamic_rendering。然后同步请求可以在每次绘制调用之前处理类似于计算分发的方式。如果无法使用VK_KHR_dynamic_rendering另一种方法是将屏障命令记录到一个二级命令缓冲区中主命令缓冲区应在开始渲染通道实例之前调用它。另一种可能的方法是缓冲渲染通道命令将它们的实际记录延迟到用户结束渲染通道之后。避免二级命令缓冲区可以节省潜在的微小间接成本但更重要的是这种方法可以被视为针对任何潜在实现问题的对冲因为二级命令缓冲区是 Vulkan 中一个使用率略低的功能。这是 nicegraf 选择的方法。17.5 单个命令缓冲区中的同步17.5.1 命令缓冲区资源表在 nicegraf 中所有命令缓冲区对象都有一个关联的表其中包含命令缓冲区内使用的每个资源的对应条目。它被实现为一个简单的平坦哈希表使用开放寻址法和 murmur3 哈希函数。表中的每个条目除其他信息外还包含相应资源的本地同步状态及其期望同步状态typedef struct ngfvk_sync_res_data { // 最新的同步状态。 ngfvk_sync_state local_sync_state ; // 期望的同步状态。 ngfvk_sync_req expected_sync_state ; // ... } ngfvk_sync_res_data ;资源的期望同步状态传达了在执行命令缓冲区之前哪些流水线阶段内的哪些访问应该不需要针对该资源进行同步以及对于图像资源应处于何种布局。这恰好对应于同步请求中包含的信息这就是为什么期望同步状态被表示为一个ngfvk_sync_req。本地同步状态是一个ngfvk_sync_state实例已在清单 17.1 中描述。当资源在命令缓冲区中首次使用时其本地同步状态被视为空白状态假定没有先前的读取器或写入器。这意味着last_writer_masks和per_stage_readers_map都设置为零。17.5.2 在命令缓冲区中使用资源每当我们记录一个可能导致特定资源被读取或修改的命令时就会针对该资源的当前本地同步状态发出一个同步请求该请求对应于该命令的流水线阶段和访问。由于本地同步状态最初假定没有先前的写入或读取第一个同步请求不会导致任何屏障被发出。我们将简单地在资源的期望同步状态中设置相应的访问和流水线阶段位。甚至可能为资源发出的第二个、第三个等同步请求也不会导致发出内存屏障。例如在第一次访问是读取且第二次访问也是可以与第一次并发的读取的情况下就可能发生这种情况。只要迄今为止为该资源发出的所有同步请求都没有导致屏障我们就需要继续将它们对应的访问和阶段位添加到资源的期望同步状态中以便跟踪在包含该资源的命令缓冲区执行之前可能需要的屏障。只有在当前命令缓冲区的记录过程中某个同步请求首次导致屏障之后我们才可以停止更新期望同步状态。17.6 跨命令缓冲区的同步应用迄今为止描述的方法我们应该能够获得各自内部良好同步的命令缓冲区。内部良好同步意味着只要所有参与资源的期望同步状态所表达的需求得到满足该命令缓冲区在提交时就不会导致任何同步风险。与特定主命令缓冲区相对应的需求可以通过在主命令缓冲区本身提交之前提交一个包含适当内存屏障操作的辅助命令缓冲区来满足。这个操作可以在提交时执行因为此时主命令缓冲区的提交顺序是已知的这使得推断必要的内存屏障的任务相当直接。在 nicegraf 中所有命令缓冲区都由单个线程提交并且该线程是唯一允许读取或写入资源全局同步状态的线程。现在我们将介绍当一组内部良好同步的命令缓冲区被提交执行时会发生什么。对于每个即将提交到 GPU 队列执行的主命令缓冲区执行以下过程对于参与即将执行的主命令缓冲区的每个资源我们针对该资源的全局同步状态生成一个同步请求。请求中的阶段、访问和布局由资源的期望同步状态决定。我们批量处理前一步骤中生成的同步请求并将由此产生的内存屏障记录到一个辅助命令缓冲区中。辅助命令缓冲区被入队等待执行。仅当在前一步骤中生成了任何内存屏障时才执行此操作。即将执行的主命令缓冲区被入队等待执行。对于最后入队的命令缓冲区中使用的每个资源我们更新该资源的全局同步状态。无论在前面的步骤中是否必须提交带有中间屏障的辅助命令缓冲区都需要执行此操作。这个过程并非完全直接并且取决于最后入队的命令缓冲区是否有任何写入资源的操作请记住布局转换也算作写入。如果资源被最后入队的命令缓冲区写入其对应的全局同步状态可以直接用最后入队命令缓冲区中最后已知的本地同步状态覆盖。否则只应将最后入队命令缓冲区中资源本地同步状态的per_stage_readers_mask合并到资源全局同步状态的per_stage_readers_mask中即需要将两个掩码按位 OR 在一起并将结果记录到全局的per_stage_readers_mask中。采取这些措施是必要的因为对资源的最后一次写入可能是由某个较早的命令缓冲区完成的。最后入队命令缓冲区中的本地同步状态不包含先前命令缓冲区所做的写入信息。用该本地同步状态覆盖全局同步状态会使我们丢失关于哪个流水线阶段内的哪种访问最后修改了资源的信息。17.7 局限性在本节中我们将讨论所描述方法的根本局限性以及其在 nicegraf 中特定实现的局限性。同步粒度差如前所述nicegraf 对修改同一资源的操作施加了严格的限制即使触及资源完全不相交的部分即允许它们并发实际上不会导致冲突它们也不能并发。我们将在第 17.9 节中讨论解决此问题的潜在方法。不支持无绑定nicegraf 目前不支持无绑定资源的概念。自动化跟踪依赖于每次绑定都必须使用 CPU 记录的命令来执行。尝试确定可能巨大的描述符表中的哪些描述符在给定着色器中实际被使用是不切实际的因此需要一种更显式的方法并在一定程度上需要用户配合才能支持无绑定。无拆分屏障所提出的方法固有的一个事实是内存屏障在最后一刻才被发出。这使得无法使用 VkEvent 实现拆分屏障例如可以先启动布局转换并在实际需要使用新布局的图像之前将其与一些其他有用工作重叠。巧合的是这是使用渲染图可以实现的事情。无资源别名nicegraf 不向用户暴露内存管理因此资源别名本身就没有意义。如果需要资源别名渲染图将更好地服务于该用例。仅单队列nicegraf 的特定实现被设计为仅处理单个 GPU 命令队列。话虽如此所提出的方法没有根本性原因不能扩展以支持多队列。不支持顶点和片段着色器中的存储和原子操作nicegraf 的特定实现目前不处理顶点和片段着色器中的存储和原子操作。这不是所提出方法的根本限制如果需要可以通过相当直接的方式添加。17.8 结果在本节中我们将回顾将上述自动同步算法应用于代表高质量独立游戏渲染工作负载的结果。我们将检查该算法产生的 GPU 屏障并考察该算法的 CPU 端开销。在撰写本文时nicegraf 正被一家工作室用作其内部游戏引擎的 RHI用于一款即将推出的游戏。该渲染器具有多项功能包括基于计算的角色蒙皮和可变形网格、实时软阴影、全局光照和时间抗锯齿。这意味着有多个通道——图形和计算通道——协同工作相互传递数据以构建单帧图像。为此设置手动编写同步是可能的但相当繁琐。渲染器的具体算法细节超出了本文的范围但在图 17.5 中我们提供了构建帧所涉及的所有通道的示意图。通道之间的数据流用箭头表示从生产者指向消费者。鉴于这种设置nicegraf 的同步自动化为每个通道生成一次vkCmdPipelineBarrier调用每个调用包括该通道中涉及的资源输入和输出。例如图 17.6 中的帧涉及 18 次对vkCmdPipelineBarrier的调用这与所涉及的单独通道的数量相匹配。请注意引擎开发人员不必显式指定任何图节点或其输入或输出他们只需以类似于 OpenGL 或 Metal 的风格编写渲染代码无需显式同步。生成的屏障本身具有精确的阶段和访问掩码这意味着它们不会引入额外不必要的限制。例如在将时间抗锯齿TAA输出移交给第一个下采样通道之前生成的屏障具有以下阶段掩码设置源阶段掩码VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT目标阶段掩码VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT。多亏了 SPIR-V 自省我们知道 TAA 输出纹理所绑定的描述符仅由下采样着色器的片段阶段访问。这有助于我们将目标阶段掩码中设置的位保持为最小并避免设置像VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_VERTEX_SHADER_BIT这样的过宽掩码以防万一。在某些基于图块的延迟渲染TBDR风格的 GPU 上添加额外的阶段位可能会损害性能为了提升性能它们依赖将下一帧的顶点处理与当前帧的片段处理重叠但是在内存屏障的目标阶段掩码中设置顶点着色器位会妨碍这种优化。这表明了精确设置阶段/访问掩码的重要性。在衡量对 CPU 性能的影响时发现大约 5% 的分析器样本最终落入了与自动同步跟踪相关的函数中。这远非引擎的瓶颈——在撰写本文时CPU 时间实际上主要由处理游戏玩法相关的更新所占据。17.9 未来工作本文提出的方法表现出的同步粒度差问题是可能解决的例如通过单独跟踪图像 mip 级别或要求 API 用户指定缓冲区的预定义不相交区域以单独跟踪。然而所有这些解决方案都伴随着权衡由于跟踪的子资源数量增加风险跟踪的开销也会增加。一个可能有所帮助的观察是并非所有资源都需要一直被跟踪。例如用作纹理的图像通常只被写入一次在纹理上传期间之后该图像就只被读取。可以添加一个将资源标记为不可变的 API。被标记为不可变后资源不能再被写入或经历布局转换。这样的资源可以从跟踪中排除从而减少 CPU 开销。作为另一个未来改进点所提出的方法可以扩展以处理提交到多个 GPU 队列的命令。为此需要引入队列本地的资源同步状态回想一下我们已经有了全局和命令缓冲区本地的资源同步状态。跨队列的风险跟踪将使用与我们描述的跟踪提交到同一队列的命令缓冲区之间的风险类似的方法必要屏障的发出需要在队列提交边界进行。17.10 结论在本文中我们提出了一种在 Vulkan 应用中自动同步资源访问的方法。与帧图方法相比我们的方法放弃了对客户端代码的大部分期望使其在风格上更接近于为 WebGPU、Metal 或 Direct3D 11 等 API 编写的代码。增加灵活性的代价是 GPU 并行性有所降低。我们考察了在一个实际环境中应用我们方法的一个案例——一个具有复杂渲染管线的独立游戏——并发现 GPU 和 CPU 性能都完全在所涉项目的可接受范围内。同时开发人员能够快速迭代他们的渲染器而完全不涉及资源同步的细节。最后我们描述了所提出方法的缺点并展示了其中一些缺点将来如何可能得到解决。总的来说这种同步方法提供了不错的投入产出比对于任何不一定关心利用 GPU 最后一点并行性的应用来说这将是一个很好的起点。参考文献[Arntzen 17] Hans-Kristian Arntzen. Render Graphs and Vulkan—A Deep Dive. Maisters Graphics Adventures, August 15, 2017. https://themaister.net/blog/2017/08/15/render-graphs-and-vulkan-a-deep-dive/.[Bernhardsson 24] Albin Bernhardsson. Vulkan Synchronization for WebGPU. Presented at Vulkanised Conference, 2024. https://vulkan.org/user/pages/09.events/vulkanised-2024/vulkanised-2024-albin-bernhardsson-arm.pdf.[Dawn 24] Dawn. Dawn WebGPU Implementation. Dawn Git Repository, 2024. https://dawn.googlesource.com/dawn//refs/heads/main/src/dawn/native/vulkan/.[Dzhavadyan 24] Grigory Dzhavadyan. Vulkan Synchronization Made Easy. Presented at Vulkanised Conference, 2024. https://vulkan.org/user/pages/09.events/vulkanised-2024/vulkanised-2024-grigory-dzhavadyan.pdf.[Gaule 24] Akio Gaule. Mastering Chaos: Navigating Vulkan Synchronization on O3DE. Presented at Vulkanised Conference, 2024. https://vulkan.org/user/pages/09.events/vulkanised-2024/vulkanised-2024-akio-gaule-o3de.pdf.[nicebyte 24] nicebyte. nicegraf. GitHub, 2024. http://nice.graphics/.[NVIDIA 24] NVIDIA. NVRHI. NVIDIA GameWorks, GitHub, 2024. https://github.com/NVIDIAGameWorks/nvrhi.[ODonnell 17] Yuriy ODonnell. FrameGraph: Extensible Rendering Architecture in Frostbite. Presented at Game Developers Conference, 2017. https://www.gdcvault.com/play/1024612/FrameGraph-Extensible-Rendering-Architecture-in.[Zulauf 24] John Zulauf. Guide to Vulkan Synchronization Validation. LunarG Whitepaper, 2024. https://www.lunarg.com/wp-content/uploads/2024/01/Guide-to-Vulkan-Synchronization-Validation-FINAL-01-18-2024.pdf.[Arntzen 17]- “Render Graphs and Vulkan—A Deep Dive.”解决的问题在Vulkan等现代API中手动处理资源状态转换和同步Pipeline Barrier极其复杂且容易出错导致代码难以维护和扩展。提出的重要思想/方法详细阐述了在个人引擎Granite中实现的渲染图Render Graph架构。该思想受[O‘Donnell 17]的Frostbite FrameGraph演讲启发旨在通过全局视角来管理帧的渲染流程。具体实现方式引擎各模块向中央渲染图注册渲染Pass声明其输入/输出资源。渲染图在“烘焙Bake”阶段通过遍历依赖图、重排序Pass以优化重叠执行、自动推导资源状态转换和插入同步屏障最终生成一个可高效执行的命令缓冲区记录序列。[Bernhardsson 24]- “Vulkan Synchronization for WebGPU.”解决的问题WebGPU作为新兴的Web图形标准其API设计目标是更安全、易用。如何在其底层实现如使用Vulkan后端时优雅地处理复杂的Vulkan同步以提供上层简洁的抽象。提出的重要思想/方法分享了在实现Dawn(WebGPU实现)的Vulkan后端时如何封装和自动化Vulkan的同步机制。核心思想是将WebGPU的高级概念如命令编码器、渲染通道映射到Vulkan的同步原语。具体实现方式演讲可能深入探讨了Dawn如何利用Vulkan的Render Pass、Subpass依赖、Pipeline Barrier以及队列所有权转移等特性来确保WebGPU API所承诺的正确执行顺序和资源可见性同时将底层的复杂性隐藏起来。[Dawn 24]- “Dawn WebGPU Implementation.”解决的问题提供Dawn项目中Vulkan后端实现的具体源代码。Dawn是Google主导的WebGPU实现之一。提出的重要思想/方法该代码仓库展示了如何将WebGPU的跨平台API翻译成特定平台的底层API这里是Vulkan。它是[Bernhardsson 24]演讲中讨论思想的实际落地。具体实现方式通过阅读src/dawn/native/vulkan/下的源代码如Pipeline.cpp,CommandBuffer.cpp,Sync.cpp可以学习到如何为WebGPU的渲染管线、计算管线和资源管理对象创建对应的Vulkan对象并自动管理和优化所需的同步操作。[Dzhavadyan 24]- “Vulkan Synchronization Made Easy.”解决的问题Vulkan的同步机制Barriers, Events, Semaphores, Fences复杂且容易出错导致开发者生产力低下和难以调试的Bug。提出的重要思想/方法介绍了一套旨在简化Vulkan同步的策略、模式或高级工具。可能包含对[Zulauf 24]中描述的同步验证层的实际应用经验或更高级的同步抽象。具体实现方式演讲可能结合了代码示例演示如何通过遵循特定模式如使用时间线Semaphore、结构化Barriers、依赖渲染图等来避免常见的同步陷阱并展示如何利用验证层的同步检查来确保代码的正确性。[Gaule 24]- “Mastering Chaos: Navigating Vulkan Synchronization on O3DE.”解决的问题在大型、复杂的开源3D引擎O3DEOpen 3D Engine中如何设计和实现一套可扩展且健壮的Vulkan同步架构以支撑其各种渲染特性和工作负载。提出的重要思想/方法分享了O3DE团队应对Vulkan同步挑战的实战经验特别是在一个功能丰富的引擎中实现高效、正确同步的架构决策。具体实现方式演讲可能剖析了O3DE的渲染后端如何组织命令、管理资源状态如使用渲染图变体并与[Zulauf 24]的同步验证层配合来诊断和修复复杂的同步Bug最终“驯服”Vulkan的同步复杂性。[nicebyte 24]- “nicegraf.”解决的问题提供一个轻量级、跨平台的图形抽象层封装Vulkan、D3D12等现代API的复杂性特别是同步和资源管理以提高开发效率。提出的重要思想/方法nicegraf库本身的设计和实现。它可能借鉴了[Arntzen 17]和[O‘Donnell 17]的渲染图思想但打包成一个更易于集成和使用的库。具体实现方式通过其API设计如ngf_graph,ngf_pass,ngf_attachment等和GitHub上的源码可以研究它如何抽象资源创建、Pass声明、以及在底层自动生成Vulkan命令缓冲区和同步指令从而让上层应用无需直接处理Pipeline Barriers等细节。[NVIDIA 24]- “NVRHI.”解决的问题NVIDIA内部以及提供给合作开发者一个高性能、可移植的渲染抽象接口RHI用于封装不同底层图形APIVulkan, D3D11, D3D12的细节特别是处理它们之间的特性差异和同步模型。提出的重要思想/方法NVRHI库的设计哲学和架构。它旨在为需要最高性能和灵活性的应用如游戏引擎、专业可视化工具提供一个“薄”且高效的抽象层。具体实现方式通过阅读其GitHub源码可以学习到它如何定义统一的接口如Buffer,Texture,CommandList并在Vulkan后端中高效地实现这些接口包括自动处理资源屏障、队列提交和同步原语使得上层代码可以跨平台运行而无需修改。[O‘Donnell 17]- “FrameGraph: Extensible Rendering Architecture in Frostbite.”解决的问题在大型商业引擎Frostbite中如何构建一个灵活、可扩展且高性能的渲染架构以应对不同游戏和硬件平台的需求同时管理复杂的资源依赖和GPU同步。提出的重要思想/方法提出了FrameGraph这一开创性概念。它是[Arntzen 17]等后续许多渲染图实现的思想源头。核心是将每一帧的渲染过程建模为一个有向无环图DAG节点是渲染Pass边是资源依赖。具体实现方式演讲详细介绍了FrameGraph如何通过声明式API来构建Pass然后通过编译Compilation步骤进行资源别名分析、剔除不可见Pass、自动插入屏障和优化内存分配最终生成高效的GPU命令列表。[Zulauf 24]- “Guide to Vulkan Synchronization Validation.”解决的问题帮助Vulkan开发者理解和有效使用Vulkan Validation Layer中的同步验证Synchronization Validation功能以自动检测代码中的数据竞争Hazards如RAW, WAR, WAW。提出的重要思想/方法提供了关于同步验证层的详细技术指南包括其工作原理、如何启用、如何解读其错误消息以及如何利用它来调试和优化同步代码。具体实现方式该白皮书深入解释了同步验证层如何跟踪每个资源的“最近访问”信息包括访问类型、Stage、Command等并在每次新操作记录时检查是否存在未受屏障保护的冲突。它提供了大量调试技巧例如如何插入“全屏障”来隔离问题以及如何通过命名对象来获得一致的调试输出。
返回列表