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

资讯详情

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

Vulkan同步2迁移实战:从旧屏障到新API的避坑指南

Vulkan同步2迁移实战:从旧屏障到新API的避坑指南 前阵子排查一个比较奇怪的渲染花屏问题出在Vulkan同步上。说奇怪是因为单张显卡上怎么跑都对换到另一张卡上就开始随机闪帧率还抖动。排查到凌晨基本确定不是着色器逻辑而是提交到队列里的一堆命令在读写同一个图像时缺少正确的同步关系。当时手里正好有VK_KHR_synchronization2这个扩展把管线屏障重写之后问题彻底消失。Vulkan同步是图形开发里面最绕的一部分VK_KHR_synchronization2的出现就是为了把这一块从“参数难传”变成“语义可查”。如果你正在做渲染器、计算管线或者刚从OpenGL迁移过来这篇内容会比较适合你。我会从老的vkCmdPipelineBarrier为什么难用讲起然后拆解新的屏障API设计与字段含义再用几个真实迁移例子演示改写过程最后把我踩过的坑全部摆出来。1. 老同步API让那么多人栽跟头的深层原因1.1 同步的本质其实只有两件事GPU里面所谓的同步归根到底解决两个问题第一执行顺序——命令A必须在命令B开始之前完成第二内存可见性——命令A写入的数据命令B真正能读得到。这两个问题经常被混在一起说但其实是两件独立的事。执行顺序比较好理解。比如我先做一次compute写入再做一次draw读入如果没保证顺序GPU很可能在两个shader并行执行时读到旧数据。内存可见性则更像缓存一致性就算命令A已经执行完它写进L2缓存的数据如果没有被“冲刷”到可见内存位置命令B看到的还是老内容。Vulkan的规范里把这两件事拆成了 execution dependency 和 memory dependency老API里这两者通过一堆stage mask和access mask混在一起表达非常容易写错。我打过一个比方执行依赖相当于“让第一个人必须离开房间第二个人才能进门”它保证的是先后内存依赖相当于“离开房间前必须把纸条贴到门口留言板上进门的人才能看见”它保证的是数据的可达性。很多时候光有先后是不够的因为GPU中不同单元看到的缓存视图并不相同。这也是不少新手写barrier时只盯着stage mask却把access mask写成了0最后得到一份能和验证层吵架的代码的原因。1.2 老API的六个具体槽点老的vkCmdPipelineBarrier从Vulkan 1.0一直用到今天接口长这样vkCmdPipelineBarrier( cmd, srcStageMask, dstStageMask, dependencyFlags, memoryBarrierCount, pMemoryBarriers, bufferMemoryBarrierCount, pBufferMemoryBarriers, imageMemoryBarrierCount, pImageMemoryBarriers);六个参数、三组数组。真正写的时候你会感觉到几个明确的问题所有barrier共享同一对 srcStageMask / dstStageMask。你想在一个调用里同时插入“计算着色器写完顶点输入读取”和“片元着色器写完下一轮帧缓冲读取”两组依赖做不到要么拆成两个调用要么把stage mask合并成更宽的范围。拆调用增加提交开销合并掩码则会让同步范围变大白白让GPU停顿。srcStageMask传0在老规范里属于非法用法。你有时候只是想表达“这次不做执行依赖只处理内存可见性”但API没有给一个合法的空掩码语义驱动之间的处理又不一样特别容易埋雷。老掩码是32位的粒度比较粗。像“着色器采样器读取”这种具体访问类型在没有扩展的老版本里只能用VK_ACCESS_SHADER_READ_BIT这种宽泛的位去覆盖一宽泛就把很多本来不相关的缓存访问也纳入了同步范围性能损失非常隐蔽。图像布局转换和访问同步被硬绑在一个结构体里。VkImageMemoryBarrier同时要负责 layout transition 和 data dependency你只想转换布局不想碰内存依赖时也得硬着头皮填访问掩码。一旦同一个帧里出现多个复杂依赖整段代码会变成一堵由结构体初始化和掩码组成的墙。我接手过一个项目光一帧的渲染主循环里就堆了12个VkImageMemoryBarrier共享两对stage mask维护起来极其痛苦后来重构成VK_KHR_synchronization2才重新做人。文档语义太糊涂。availability、visibility、first synchronization scope、second synchronization scope这些概念在不看spec的前提下基本不可能准确落地。出现数据竞争时验证层会报 SYNC-HAZARD 类错误但你的barrier到底缺了什么往往要对着规范翻半天。我记得一次印象很深的调试同一个渲染器在NVIDIA驱动上表现得很好换到AMD的卡上就开始闪烁排查后发现问题不是 shader 也不是布局而是barrier里dstStageMask包含了太多阶段导致某个tile-based GPU在特定路径上没有真正等对地方。这类问题在老API里非常难定位因为你很难从代码里看出来你真正创建的依赖是什么。2. 同步2到底改了什么从参数表到语义表2.1 每个barrier独立携带全套作用域VK_KHR_synchronization2最核心的变化是把每个barrier的同步作用域全部下沉到结构体内部。新的结构体长这样typedef struct VkMemoryBarrier2 { VkStructureType sType; const void* pNext; VkPipelineStageFlags2 srcStageMask; VkAccessFlags2 srcAccessMask; VkPipelineStageFlags2 dstStageMask; VkAccessFlags2 dstAccessMask; } VkMemoryBarrier2;还有VkBufferMemoryBarrier2和VkImageMemoryBarrier2它们都在原有字段基础上增加了自己的srcStageMask / srcAccessMask / dstStageMask / dstAccessMask。这带来的直接好处就是你一个vkCmdPipelineBarrier2调用里可以传多个member barrier每个barrier各自定义第一同步作用域和第二同步作用域互不干扰。旧API里“两组stage mask管所有barrier”的尴尬彻底消失了。这些barrier被统一装进一个VkDependencyInfo结构typedef struct VkDependencyInfo { VkStructureType sType; const void* pNext; VkDependencyFlags dependencyFlags; uint32_t memoryBarrierCount; const VkMemoryBarrier2* pMemoryBarriers; uint32_t bufferMemoryBarrierCount; const VkBufferMemoryBarrier2* pBufferMemoryBarriers; uint32_t imageMemoryBarrierCount; const VkImageMemoryBarrier2* pImageMemoryBarriers; } VkDependencyInfo;然后在命令缓冲里这样提交vkCmdPipelineBarrier2(cmd, depInfo);新API在函数设计上也做了减法不再有六个参数而是把一个依赖描述对象传进去。读代码的人只要看这一个对象就能清楚知道哪些资源的读写顺序被保护。我建议在新项目中直接使用这套写法。它并不是单纯的语法糖而是把之前混乱的语义表达变成了更接近“人话”的方式。你自己一眼能看懂code review的时候别人也能更快确认你有没有写错作用域。2.2 64位掩码和NONE的合法化同步2把VkPipelineStageFlags2和VkAccessFlags2都升级成了64位掩码。新增了很多细粒度的标志比如VK_PIPELINE_STAGE_2_COPY_BIT、VK_ACCESS_2_SHADER_SAMPLED_READ_BIT、VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT这些能够把同步范围缩到更精准的访问类型。这里最需要记住的是VK_PIPELINE_STAGE_2_NONE和VK_ACCESS_2_NONE。它们给“空作用域”提供了合法表达方式。老API里你想表示不关心source stage没有合适写法而现在可以直接给srcStageMask VK_PIPELINE_STAGE_2_NONE用来表达“这里不需要显式的执行依赖”。但我必须强调NONE不等于“随便”。如果你让srcStageMask NONE但srcAccessMask不是0规范并不会因此产生任何依赖。结果是整个barrier变成了空操作验证层也不会认为你有问题数据竞争反而更加隐蔽。真正想让依赖管住“所有阶段”时请用VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT别偷懒写成NONE。64位掩码对queue submit里的semaphore stage mask也很有用。老的VkSubmitInfo给 wait semaphore 指定dstStageMask时用的是32位掩码想表达“等整个命令缓冲全部执行完”通常会写VK_PIPELINE_STAGE_ALL_COMMANDS_BIT粒度也很粗。同步2的VkSemaphoreSubmitInfo里可以直接用64位掩码并且会自动配合更精确的访问类型。这给多队列协作项目带来的收益相当明显。2.3 新增或改写的命令清单同步2扩展对整个同步族都做了版本升级不只是vkCmdPipelineBarrier2。实际开发中会碰到的入口主要有vkCmdPipelineBarrier2替代旧的管线屏障。vkCmdSetEvent2/vkCmdResetEvent2/vkCmdWaitEvents2事件同步的新版本每个事件操作都带VkDependencyInfo。vkCmdWriteTimestamp2查询时间戳时改用64位stage掩码profiling更可靠。vkQueueSubmit2队列提交的新版本semaphore wait/signal信息用VkSemaphoreSubmitInfo表达。在Vulkan 1.3里VK_KHR_synchronization2被提升为核心特性只要设备支持1.3你就直接用不带KHR后缀的函数名。如果你的目标平台还在Vulkan 1.1或者1.2那就需要先启用扩展函数入口通常带KHR后缀比如vkCmdPipelineBarrier2KHR。我在示例代码里统一写的是核心版本函数名在低版本平台上换成带KHR后缀的即可。3. 从旧代码迁移的四个实际场景3.1 图像Layout过渡最常改写的屏障几乎所有渲染器里都有这个操作把渲染目标从颜色附件布局切换到采样纹理布局然后让后处理阶段去读它。旧的写法是VkImageMemoryBarrier barrier { .sType VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .srcAccessMask VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, .dstAccessMask VK_ACCESS_SHADER_READ_BIT, .oldLayout VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, .newLayout VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, .srcQueueFamilyIndex VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex VK_QUEUE_FAMILY_IGNORED, .image sceneColor, .subresourceRange { ... } }; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, NULL, 0, NULL, 1, barrier);改成同步2后VkImageMemoryBarrier2 barrier { .sType VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, .srcStageMask VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, .srcAccessMask VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, .dstStageMask VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, .dstAccessMask VK_ACCESS_2_SHADER_SAMPLED_READ_BIT, .oldLayout VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, .newLayout VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, .srcQueueFamilyIndex VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex VK_QUEUE_FAMILY_IGNORED, .image sceneColor, .subresourceRange { ... } }; VkDependencyInfo depInfo { .sType VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .imageMemoryBarrierCount 1, .pImageMemoryBarriers barrier, }; vkCmdPipelineBarrier2(cmd, depInfo);结构上看起来很相似但关键区别藏在细节里老的写法中srcStageMask和dstStageMask是从vkCmdPipelineBarrier参数传进来的所有barrier共用新的写法里每组stage和access都能独立指定。如果这个调用里还要同时同步另一张图从 compute 写入转到 vertex 读取旧API只能拆两个调用或者把stage mask合并成一个巨大的范围新API则可以在同一个VkDependencyInfo里塞两个 image barrier各自作用域互不干扰。3.2 计算写入给顶点输入跨阶段Buffer同步有类常见依赖是compute阶段写入一块缓冲区之后顶点着色器阶段把它当vertex attribute读取。旧的代码vkCmdDispatch(cmd, ...); VkBufferMemoryBarrier barrier { .sType VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, .srcAccessMask VK_ACCESS_SHADER_WRITE_BIT, .dstAccessMask VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT, .srcQueueFamilyIndex VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex VK_QUEUE_FAMILY_IGNORED, .buffer vertexBuf, .offset 0, .size VK_WHOLE_SIZE, }; vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, 0, 0, NULL, 1, barrier, 0, NULL);迁移后的版本VkBufferMemoryBarrier2 barrier { .sType VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2, .srcStageMask VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT, .srcAccessMask VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT, .dstStageMask VK_PIPELINE_STAGE_2_VERTEX_INPUT_BIT, .dstAccessMask VK_ACCESS_2_VERTEX_ATTRIBUTE_READ_BIT, .srcQueueFamilyIndex VK_QUEUE_FAMILY_IGNORED, .dstQueueFamilyIndex VK_QUEUE_FAMILY_IGNORED, .buffer vertexBuf, .offset 0, .size VK_WHOLE_SIZE, }; VkDependencyInfo depInfo { ... }; vkCmdPipelineBarrier2(cmd, depInfo);这里有个值得留意的点老的VK_ACCESS_SHADER_WRITE_BIT是一个比较宽泛的位它同时涵盖了storage写入和其他着色器写入类型。新API里循环粒度做细了就可以用VK_ACCESS_2_SHADER_STORAGE_WRITE_BIT只锁定storage buffer的写入。对于大尺寸buffer和频繁同步的管线这种缩小访问范围的操作能减少缓存刷新量。实际项目里加速效果不一定每次都能测出来但至少代码表达更加准确。更重要的是新API允许一个调用里同时存在两个访问掩码不同的buffer barrier这在旧API里也是做不到的。3.3 RenderPass的Subpass依赖也要升级同步2也提供了VkSubpassDependency2来升级render pass内部的subpass依赖。旧写法VkSubpassDependency dep { .srcSubpass 0, .dstSubpass 1, .srcStageMask VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, .dstStageMask VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, .srcAccessMask VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, .dstAccessMask VK_ACCESS_SHADER_READ_BIT, .dependencyFlags 0, };新写法多一个sTypeVkSubpassDependency2 dep { .sType VK_STRUCTURE_TYPE_SUBPASS_DEPENDENCY_2, .srcSubpass 0, .dstSubpass 1, .srcStageMask VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT, .dstStageMask VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT, .srcAccessMask VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT, .dstAccessMask VK_ACCESS_2_SHADER_SAMPLED_READ_BIT, .dependencyFlags 0, };如果你在用VkRenderPassCreateInfo2创建render pass把pDependencies指向VkSubpassDependency2数组即可。如果项目还在用旧的VkRenderPassCreateInfo我建议先把render pass创建也迁移到vkCreateRenderPass2再一起改否则两个子系统的结构体类型混着用很容易出错。subpass依赖本身不是为了多队列协作而是给tile-based GPU提供“片元与片元之间尽量在tile里完成读写”的优化空间值得认真维护。3.4 Event和队列提交队列层面的同步事件同步在新API里也换了用法。事件适合用在同一个队列里、不想触发全局barrier的情况。旧API要用vkCmdSetEvent和vkCmdWaitEvents每个都要传一对stage maskvkCmdSetEvent(cmd, event, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT); // ... 中间若干命令 ... vkCmdWaitEvents(cmd, 1, event, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, NULL, 0, NULL, 0, NULL);新API变成VkDependencyInfo setDepInfo { ... }; vkCmdSetEvent2(cmd, event, setDepInfo); VkDependencyInfo waitDepInfo { ... }; vkCmdWaitEvents2(cmd, 1, event, waitDepInfo);我个人在迁移事件同步时比较谨慎因为事件的语义不如barrier直观它涉及跨命令缓冲的状态和提交顺序。迁移时建议把验证层全开再配合多帧的渲染结果做对比确认没有出现提前等待或者永久等待。队列提交本身的同步也变了vkQueueSubmit2用VkSemaphoreSubmitInfo来指定每个semaphore和它关联的stage maskVkSemaphoreSubmitInfo waitSemInfo { .sType VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, .semaphore waitSem, .stageMask VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, }; VkCommandBufferSubmitInfo cmdInfo { .sType VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, .commandBuffer cmd, }; VkSemaphoreSubmitInfo signalSemInfo { .sType VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, .semaphore signalSem, .stageMask VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, }; VkSubmitInfo2 submit2 { .sType VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .waitSemaphoreInfoCount 1, .pWaitSemaphoreInfos waitSemInfo, .commandBufferInfoCount 1, .pCommandBufferInfos cmdInfo, .signalSemaphoreInfoCount 1, .pSignalSemaphoreInfos signalSemInfo, }; vkQueueSubmit2(queue, 1, submit2, fence);一个容易忽略的优化点semaphore wait上的stageMask应该尽量写成真正要消费数据的阶段而不是一律ALL_COMMANDS_BIT。如果下一个队列只是要做一次顶点读取就把stageMask写成VK_PIPELINE_STAGE_2_VERTEX_INPUT_BIT。那种“等整条管线的所有命令都执行完”的习惯会把多队列并行直接拍死。4. 迁移过程中我踩过的坑4.1 结构体字段顺序复制粘贴的安全隐患第一次迁移时最容易翻车的不是语义而是结构体的字段顺序。VkImageMemoryBarrier2的字段顺序和旧结构体不完全一样它把image相关的字段放到了偏后面VkImageMemoryBarrier2 { VkStructureType sType; const void* pNext; VkPipelineStageFlags2 srcStageMask; VkAccessFlags2 srcAccessMask; VkPipelineStageFlags2 dstStageMask; VkAccessFlags2 dstAccessMask; VkImageLayout oldLayout; VkImageLayout newLayout; uint32_t srcQueueFamilyIndex; uint32_t dstQueueFamilyIndex; VkImage image; VkImageSubresourceRange subresourceRange; }我看到过一个小伙伴把老的VkImageMemoryBarrier代码直接复制过来把image和subresourceRange挪到前面然后还用了C的聚合初始化按位置赋值。结果编译过了字段却错位在验证层里报了奇怪的布局错误。这种问题非常难查因为它不是崩溃而是数据错乱。我的建议是迁移时不要用旧的字段顺序硬套全部改成命名初始化器或者老老实实memset后逐个赋值。尤其是sType一定要填成VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2漏掉的后果同样很隐蔽。另外新版里VkDependencyInfo的sType是VK_STRUCTURE_TYPE_DEPENDENCY_INFO两个结构体的sType最容易搞混建议写代码时把sType和字段一次写对再想优化。4.2 把NONE当成“随便填一填”的惨痛教训有一次我重构一个后处理管线为了让barrier看起来更“高效”把一个buffer barrier的srcStageMask写成了VK_PIPELINE_STAGE_2_NONE以为这样等于“我不限制source stage”驱动可以更自由地调度。结果渲染结果开始间歇性闪烁。验证层报了一个 SYNC-HAZARD-WRITE-AFTER-READ但是因为我没有看日志就上线花了大半天才查回来。正确认知应该是NONE表示这个同步作用域是空的不会产生任何执行依赖也不会有内存可见性效果。如果你确实不关心之前的阶段但还需要让某个memory access变得可见必须写一个真实的stage mask比如VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT或者更具体的阶段掩码。access mask同理。想让内存依赖生效src和dst的access mask都不能是0。不然barrier就退化成纯执行顺序屏障数据该坏还是坏。这个坑几乎每个迁移者都会遇到一次。我的排查套路是怀疑哪个barrier就把它的srcStageMask临时改成ALL_COMMANDS_BIT如果闪烁消失就说明原来的scope写窄了如果还闪就继续查别的。配合验证层的同步验证可以最快定位。4.3 验证层和长稳测试仍然不可省同步验证是Vulkan验证层里最有价值的功能之一。我在迁移期间会把验证层强制打开并特意看每一帧的 SYNC-HAZARD 消息。迁移完成后我会在多帧循环里让GPU跑长一些时间再看有没有偶发的验证错误。有些同步问题要跑到特定帧数才会暴露因为GPU驱动的调度不是完全确定的。我还会在怀疑barrier写宽或写窄时跑一遍内存压力类的测试场景比如频繁分配和释放大块缓冲后马上进行读写这类负载最容易触发缓存一致性bug。如果你在社区里看到所谓的Vulkan memtest类工具它的意义也在这里——并不是替代验证层而是用长时间、高负载的运行把你的竞争条件逼出来。跑长稳时注意看验证层输出是否夹杂一些看起来毫无关联的报错。比如一个barrier写错了可能不会马上报错而是隔了几帧才在某个图像布局转换时报出READ-AFTER-WRITE。查这种问题要有耐心别急着改第一眼看到的位置。4.4 性能优化不要一上来就追求极限窄域把同步范围写得越窄理论上GPU停顿越少尤其对移动端的tile-based渲染有明显收益。但我还是强烈建议先把功能做正确所有barrier先按比较宽的范围写跑通后再逐步缩小。窄域同步的坑在于你可能对底层硬件单元的执行路径理解不够全面某个隐藏的预取或二级缓存行为正好被你忽略结果某张卡上稳定另一张卡上随机花屏。我自己的习惯是先保证正确性再分阶段优化。每缩小一次范围就开启验证层跑一轮多帧对比再跑到实际目标设备上验证一下。没有profiler数据支撑的“优化”很多时候只是在给未来的自己挖坑。5. 迁移策略与我的长期使用体会对新项目我不太建议再用老的那套vkCmdPipelineBarrier。Vulkan 1.3已经把它作为核心能力新开发的渲染器、计算管线、Vulkan多媒体应用直接使用同步2更合适。它的代码可读性和维护性都明显更好团队协作时也能减少因为掩码理解偏差导致的分歧。老项目迁移时可以渐进式推进。先把最核心的帧内屏障迁移到vkCmdPipelineBarrier2验证渲染结果一致后再把事件同步和队列提交逐步迁过去。不要试图一次改完所有同步代码那种改动规模会让你在排查问题时无从下手。有一个细节值得提前确认启用扩展时需要把VK_KHR_synchronization2加入VkDeviceCreateInfo的扩展列表并通过VkPhysicalDeviceSynchronization2FeaturesKHR把synchronization2特性打开。如果目标是Vulkan 1.3设备这些都可以省掉。函数入口方面Vulkan 1.3用vkCmdPipelineBarrier2低版本用带KHR后缀的版本代码里可以用宏统一封装一下。最后说点个人体会。同步2给我带来的最大提升不是帧率而是信心。以前写barrier总有一种“这次应该对了吧”的模糊感现在每个barrier的作用域和访问范围都在结构体里写得清清楚楚review代码的人也能直接看懂这个屏障到底在保护什么。对常年被Vulkan同步折磨的人来说这个改善比任何性能收益都实在。
返回列表