内存模型

此内存模型描述了所有实现提供的同步;但是,某些定义的同步需要实现支持额外的特性。请参阅 VkPhysicalDeviceVulkanMemoryModelFeatures

代理

操作是系统上执行的任何任务的通用术语。

根据定义,操作是已执行的内容。因此,如果由于控制流跳过指令,则不构成操作。

每个操作都由特定的代理执行。可能的代理包括每个着色器调用、每个主机线程和管线的每个固定功能阶段。

内存位置

内存位置标识用于 8 位数据的唯一存储。内存操作一次访问一个或多个内存位置组成的一组内存位置,例如,访问内存中 32 位整数的操作将读取/写入一组四个内存位置。访问整个聚合的内存操作可能会访问元素或成员之间的任何填充字节,但不会访问聚合末尾的填充字节。如果其内存位置集的交集为非空,则两组内存位置重叠。内存操作必须不影响其内存位置集之外的内存位置处的内存。

缓冲区和图像的内存位置在 VkDeviceMemory 对象中显式分配,并且在每个着色器调用中为 SPIR-V 变量隐式分配。

具有 Workgroup 存储类并指向块修饰类型的变量共享一组内存位置。

分配

新分配的内存位置中存储的值由 SPIR-V 变量的初始化器(如果存在)确定,否则为未定义。在创建分配时,尚未对任何其内存位置进行任何 内存操作。初始化不被视为内存操作。

对于细分控制着色器输出变量,初始化不被视为内存操作的结果是,某些实现可能需要在输出变量的初始化和对这些变量的任何读取之间插入障碍。

内存操作

对于操作 A 和内存位置 M

  • 如果且仅当 M 中存储的数据是 A 的输入时,A 读取 M。

  • 如果且仅当 A 的数据输出存储到 M 时,A 写入 M。

  • 如果且仅当它读取或写入(或两者兼有)M 时,A 访问 M。

其值与那些内存位置中已有的值相同的写入仍被视为写入,并具有所有相同的效果。

引用

引用是特定代理可以用来访问一组内存位置的对象。在主机上,引用是主机虚拟地址。在设备上,引用是

  • 变量绑定到的描述符,对于 Image、Uniform 或 StorageBuffer 存储类中的变量。如果变量是数组(或数组的数组等),则数组的每个元素可能是唯一的引用。

  • PhysicalStorageBuffer 存储类中缓冲区的地址范围,其中地址范围的基址使用 vkGetBufferDeviceAddress 查询,范围的长度是缓冲区的大小。

  • 对于所有具有 Workgroup 存储类并指向块修饰类型的变量的单个通用引用。

  • 对于 Workgroup 存储类中非块修饰类型的变量,变量本身。

  • 对于其他存储类中的变量,变量本身。

通过不同引用进行的两次内存访问可能需要 下面定义的可用性和可见性操作。

程序顺序

SPIR-V 中定义了指令的动态实例 (https://registry.khronos.org/spir-v/specs/unified1/SPIRV.html#DynamicInstance),作为引用静态指令特定执行的一种方法。程序顺序是单个着色器调用执行的指令动态实例的排序

  • (基本块):如果指令 A 和 B 在同一个基本块中,并且 A 在模块中列在 B 之前,则 A 的第 n 个动态实例在 B 的第 n 个动态实例之前按程序顺序排序。

  • (分支):分支或开关指令的动态实例按程序顺序排在它将控制转移到的 OpLabel 指令的动态实例之前。

  • (调用入口):OpFunctionCall 指令的动态实例按程序顺序排在 OpFunctionParameter 指令的动态实例和被调用函数的主体之前。

  • (调用退出):OpFunctionCall 指令之后的指令的动态实例,在被调用函数执行的返回指令的动态实例之后按程序顺序排序。

  • (传递闭包):如果任何指令的动态实例 A 在任何指令的动态实例 B 之前按程序顺序排序,并且 B 在任何指令的动态实例 C 之前按程序顺序排序,则 A 在 C 之前按程序顺序排序。

  • (完整定义):没有其他动态实例按程序顺序排序。

对于在主机上执行的指令,源语言定义了程序顺序关系(例如,作为“sequenced-before”)。

着色器调用相关是在调用上定义的等价关系,定义为以下内容的对称和传递闭包:

  • 如果 A 是由 B 执行的着色器调用指令创建的,则 A 与 B 着色器调用相关。

着色器调用顺序

着色器调用顺序是在着色器调用相关的调用执行的指令的动态实例上的偏序。

  • (程序顺序):如果动态实例 A 在 B 之前按程序顺序排序,则 A 在 B 之前按着色器调用顺序排序。

  • (着色器调用入口):如果 A 是着色器调用指令的动态实例,而 B 是由 A 创建的调用执行的动态实例,则 A 在 B 之前按着色器调用顺序排序。

  • (着色器调用退出):如果 A 是着色器调用指令的动态实例,B 是同一调用执行的下一个动态实例,而 C 是由 A 创建的调用执行的动态实例,则 C 在 B 之前按着色器调用顺序排序。

  • (传递闭包):如果 A 在 B 之前按着色器调用顺序排序,并且 B 在 C 之前按着色器调用顺序排序,则 A 在 C 之前按着色器调用顺序排序。

  • (完整定义):没有其他动态实例按着色器调用顺序排序。

作用域

原子指令和屏障指令包括作用域,这些作用域标识着色器调用的集合,这些着色器调用必须遵守以下定义的请求的操作的排序和原子性规则。

各种作用域在着色器章节中详细描述。

原子操作

设备上的原子操作是任何名称以 OpAtomic 开头的 SPIR-V 操作。主机上的原子操作是使用 std::atomic 类型对象执行的任何操作。

每个原子操作都有一个内存作用域和一个语义。非正式地说,作用域确定它相对于哪些其他代理是原子的,而语义约束其与其他内存访问的排序。设备原子操作具有显式的作用域和语义。每个主机原子操作都隐式使用 CrossDevice 作用域,并使用等效于 C++ std::memory_order 值(relaxed、acquire、release、acq_rel 或 seq_cst)的内存语义。

当且仅当以下所有条件都为真时,两个原子操作 A 和 B 才是潜在互序的

  • 它们访问同一组内存位置。

  • 它们使用相同的引用。

  • A 在 B 的内存作用域的实例中。

  • B 在 A 的内存作用域的实例中。

  • A 和 B 不是相同的操作(非自反)。

当且仅当它们是潜在互序的,并且以下任何一个条件为真时,两个原子操作 A 和 B 才是互序的

  • A 和 B 都是设备操作。

  • A 和 B 都是主机操作。

如果两个原子操作不是互序的,并且它们的内存位置集合重叠,则每个原子操作必须像它们是非原子操作一样与其他操作同步。

作用域修改顺序

对于给定的原子写入 A,所有与 A 互序的原子写入都以称为 A 的作用域修改顺序的顺序发生。A 的作用域修改顺序与其他操作无关。

在 A 的内存作用域实例之外的调用可能以与作用域修改顺序不一致的顺序观察到 A 的内存位置集合中的值变得对其可见。

在同一组内存位置上具有不同作用域实例中的非原子操作或原子操作是有效的,只要它们像它们是非原子操作一样相互同步(如果不是,则将其视为数据竞争)。这意味着 A 的作用域修改顺序的定义可能包括在非原子操作之后很久才发生的原子操作。这有点不直观,但这有助于保持此定义简单且非循环。

内存语义

默认情况下,非原子内存操作可能被一个代理以不同于另一个代理写入的顺序观察到。

原子操作和一些同步操作包括内存语义,这些是约束同一代理执行的其他内存访问(包括非原子内存访问和可用性和可见性操作)的顺序的标志,这些内存访问可以被其他代理观察到,或者可以观察到其他代理的访问。

包括语义的设备指令是 OpAtomic*OpControlBarrierOpMemoryBarrierOpMemoryNamedBarrier。包括语义的主机指令是一些 std::atomic 方法和内存栅栏。

SPIR-V 支持以下内存语义

  • Relaxed:对其他内存访问的顺序没有约束。

  • Acquire:具有此语义的内存读取执行获取操作。具有此语义的内存屏障是获取屏障

  • Release:具有此语义的内存写入执行释放操作。具有此语义的内存屏障是释放屏障

  • AcquireRelease:具有此语义的内存读取-修改-写入操作执行获取操作和释放操作,并继承来自这两个操作的排序限制。具有此语义的内存屏障既是释放屏障又是获取屏障。

SPIR-V 不支持设备上的“consume”语义。

内存语义操作数还包括存储类语义,指示哪些存储类受同步约束。SPIR-V 存储类语义包括

  • UniformMemory

  • WorkgroupMemory

  • ImageMemory

  • OutputMemory

每个 SPIR-V 内存操作访问单个存储类。同步操作中的语义可以包括存储类的组合。

UniformMemory 存储类语义适用于对 PhysicalStorageBuffer、ShaderRecordBufferKHR、Uniform 和 StorageBuffer 存储类中的内存的访问。WorkgroupMemory 存储类语义适用于对 Workgroup 存储类中的内存的访问。ImageMemory 存储类语义适用于对 Image 存储类中的内存的访问。OutputMemory 存储类语义适用于对 Output 存储类中的内存的访问。

非正式地说,这些约束限制了内存操作的重新排序方式,并且这些限制不仅适用于在执行指令的代理中执行访问的顺序,还适用于写入的效果在同一指令的内存作用域的实例中对所有其他代理变为可见的顺序。

不同线程中的释放和获取操作可以充当同步操作,以保证在释放之前发生的写入在获取之后是可见的。(这不是正式定义,只是一个信息性的前向引用。)

OutputMemory 存储类语义仅在细分控制着色器中才有用,这是输出变量在调用之间共享的唯一执行模型。

内存语义操作数可以还包括可用性和可见性标志,这些标志应用可用性和可见性中描述的可用性和可见性操作。可用性/可见性标志是

  • MakeAvailable: 语义必须为 Release 或 AcquireRelease。在释放操作或屏障之前执行可用性操作。

  • MakeVisible: 语义必须为 Acquire 或 AcquireRelease。在获取操作或屏障之后执行可见性操作。

这些操作的具体细节在可用性和可见性语义中定义。

主机原子操作可能支持不同的内存语义和同步操作列表,具体取决于主机架构和源语言。

释放序列

在原子操作 A 对一组内存位置 M 执行释放操作之后,以 A 开头的释放序列是 A 的作用域修改顺序中最长的连续子序列,它由以下组成:

  • 原子操作 A 作为其第一个元素

  • 任何代理对 M 执行的原子读取-修改-写入操作

最后一个要点的原子操作必须通过位于 A 的作用域修改顺序中而与 A 相互排序。

这有意省略了“同一代理执行的对 M 的原子写入”,它存在于相应的 C++ 定义中。

同步于

同步于是操作之间的关系,其中每个操作要么是原子操作,要么是内存屏障(也称为主机上的栅栏)。

如果 A 和 B 是原子操作,则 A 与 B 同步当且仅当以下所有条件都为真:

  • A 执行释放操作

  • B 执行获取操作

  • A 和 B 相互排序

  • B 读取由 A 或由以 A 开头的释放序列中的操作写入的值

OpControlBarrierOpMemoryBarrierOpMemoryNamedBarrier 是 SPIR-V 中的内存屏障指令。

如果 A 是释放屏障,而 B 是执行获取操作的原子操作,则 A 与 B 同步当且仅当以下所有条件都为真:

  • 存在一个原子写入 X(具有任何内存语义)

  • A 在程序顺序上先于 X

  • X 和 B 相互排序

  • B 读取由 X 或由以 X 开头的释放序列中的操作写入的值

    • 如果 X 是宽松的,它仍然被认为为此规则引导一个假设的释放序列

  • A 和 B 位于彼此的内存作用域的实例中

  • X 的存储类在 A 的语义中。

如果 A 是执行释放操作的原子操作,而 B 是获取屏障,则 A 与 B 同步当且仅当以下所有条件都为真:

  • 存在一个原子读取 X(具有任何内存语义)

  • X 在程序顺序上先于 B

  • X 和 A 相互排序

  • X 读取由 A 或由以 A 开头的释放序列中的操作写入的值

  • A 和 B 位于彼此的内存作用域的实例中

  • X 的存储类在 B 的语义中。

如果 A 是释放屏障,而 B 是获取屏障,则 A 与 B 同步当且仅当以下所有条件都为真:

  • 存在一个原子写入 X(具有任何内存语义)

  • A 在程序顺序上先于 X

  • 存在一个原子读取 Y(具有任何内存语义)

  • Y 在程序顺序上先于 B

  • X 和 Y 相互排序

  • Y 读取由 X 或由以 X 开头的释放序列中的操作写入的值

    • 如果 X 是宽松的,它仍然被认为为此规则引导一个假设的释放序列

  • A 和 B 位于彼此的内存作用域的实例中

  • X 和 Y 的存储类都在 A 和 B 的语义中。

    • 注意:X 和 Y 必须具有相同的存储类,因为它们是相互排序的。

如果 A 是释放屏障,B 是获取屏障,而 C 是控制屏障(其中 A 可以等于 C,而 B 可以等于 C),则 A 与 B 同步当且仅当以下所有条件都为真:

  • A 在程序顺序上先于(或等于)C

  • C 在程序顺序上先于(或等于)B

  • A 和 B 位于彼此的内存作用域的实例中

  • A 和 B 位于 C 的执行作用域的实例中

这类似于上面的屏障-屏障同步,但控制屏障填充了宽松原子操作的角色。

设 F 为片段着色器调用的排序,使得调用 F1 在调用 F2 之前排序当且仅当 F1 和 F2片段着色器互锁中所述重叠,并且 F1 在 F2 之前执行互锁代码。

如果 A 是 OpEndInvocationInterlockEXT 指令,而 B 是 OpBeginInvocationInterlockEXT 指令,则如果执行 A 的代理在 F 中在执行 B 的代理之前排序,则 A 与 B 同步。A 和 B 都被认为具有 FragmentInterlock 内存作用域和 UniformMemory 和 ImageMemory 的语义,A 被认为具有 Release 语义,而 B 被认为具有 Acquire 语义。

OpBeginInvocationInterlockEXTOpBeginInvocationInterlockEXT 不执行隐式可用性或可见性操作。通常,使用片段着色器互锁的着色器会将相关资源声明为 coherent,以获得隐式按指令的可用性和可见性操作

如果 A 是释放屏障,而 B 是获取屏障,则 A 与 B 同步当且仅当以下所有条件都为真:

  • A 在着色器调用顺序上先于 B

  • A 和 B 位于彼此的内存作用域的实例中

没有其他释放和获取屏障彼此同步。

系统同步于

系统同步于是设备或主机上任意操作之间的关系。某些操作彼此系统同步,这非正式地意味着第一个操作发生在第二个操作之前,并且同步是在不使用应用程序可见的内存访问的情况下执行的。

如果在两个操作 A 和 B 之间存在执行依赖关系,则第一个同步范围内的操作与第二个同步范围内的操作系统同步。

这涵盖了所有 Vulkan 同步原语,包括在同步原语发出信号之前执行的设备操作、在后续设备操作之前发生的等待操作、在等待它们的主机操作之前发生的信号操作,以及在 vkQueueSubmit 之前发生的主机操作。该列表分布在整个同步章节中,此处不再重复。

系统同步于隐式包括所有存储类语义,并具有 CrossDevice 作用域。

如果 A 系统同步于 B,我们也可以说 A 在 B 之前系统同步,而 B 在 A 之后系统同步

私有 vs. 非私有

默认情况下,非原子内存操作被视为私有,这意味着此类内存操作不打算用于与其他代理通信。设置了 NonPrivatePointer/NonPrivateTexel 位的内存操作被视为非私有,并且旨在用于与其他代理通信。

更准确地说,要在不同的代理之间将私有内存操作进行位置排序,需要使用系统同步于而不是基于着色器的同步。私有内存操作仍然遵守程序顺序。

原子操作始终被视为非私有。

线程间先于发生

设 SC 为存储类语义的非空集合。然后(使用模板语法),如果以下任何一项为真,则操作 A 线程间先于发生<SC>操作 B:

  • A 系统同步于 B

  • A 同步于 B,并且 A 和 B 的语义中都包含 SC 的所有内容

  • A 是对 SC 中的存储类中的内存或语义中包含 SC 所有内容的内存执行的操作,B 是语义中包含 SC 所有内容的释放屏障或释放原子操作,并且 A 在程序顺序上先于 B

  • A 是语义中包含 SC 所有内容的获取屏障或获取原子操作,B 是对 SC 中的存储类中的内存或语义中包含 SC 所有内容的内存执行的操作,并且 A 在程序顺序上先于 B

  • A 和 B 都是主机操作,并且 A 按照主机语言规范中定义的在线程间先于发生于 B

  • A 线程间先于发生<SC> 某个 X,而 X 线程间先于发生<SC> B

先发生关系

当且仅当以下任何一种情况为真时,操作 A 先发生于 操作 B

  • A 在程序顺序上先于 B

  • 对于某些存储类 SC,A 线程间先发生<SC> B

后发生 的定义类似。

与 C++ 不同,先发生关系并不总是足以保证写入对读取可见。写入操作要对其他内存访问 可见,可能需要额外的可用性和可见性操作。

先发生关系不具备传递性,但程序顺序和线程间先发生<SC> 都具有传递性。可以将它们分别视为涵盖“单线程”情况和“多线程”情况,并且在这两者之间建立链式关系是不必要的(也是无效的)。

可用性和可见性

可用性可见性 是写入操作的状态,它们(非正式地)跟踪写入在系统中传播的程度,即哪些代理和引用能够观察到写入。可用性状态是每个内存域的属性。可见性状态是每个 (代理,引用) 对的属性。可用性和可见性状态是每次写入操作的每个内存位置的属性。

内存域根据使用该域的内存访问的代理命名。着色器调用使用的域按层次结构组织成多个较小的内存域,这些域对应于不同的作用域。每个内存域都被认为是作用域的对偶,反之亦然。Vulkan 中定义的内存域包括

  • 主机 - 可由主机代理访问

  • 设备 - 可由特定设备的所有设备代理访问

  • 着色器 - 可由特定设备的着色器代理访问,对应于 Device 作用域

  • 队列族实例 - 可由单个队列族中的着色器代理访问,对应于 QueueFamily 作用域。

  • 片段互锁实例 - 可由重叠的片段着色器代理访问,对应于 FragmentInterlock 作用域。

  • 着色器调用实例 - 可由着色器调用相关的着色器代理访问,对应于 ShaderCallKHR 作用域。

  • 工作组实例 - 可由同一工作组中的着色器代理访问,对应于 Workgroup 作用域。

  • 子组实例 - 可由同一子组中的着色器代理访问,对应于 Subgroup 作用域。

内存域按上面列出的顺序嵌套,但着色器调用实例域除外,列表中靠后的内存域嵌套在列表中靠前的内存域中。着色器调用实例域在列表中的位置取决于具体实现,并根据该位置进行嵌套。着色器调用实例域的范围不大于队列族实例域。

内存域不对应于存储类或设备本地和主机本地的VkDeviceMemory 分配,而是指示写入是否只能对同一子组、同一工作组、重叠的片段着色器调用、着色器调用相关的光线追踪调用、任何着色器调用、设备上的任何位置或主机中的代理可见。着色器、队列族实例、片段互锁实例、着色器调用实例、工作组实例和子组实例域仅用于基于着色器的可用性/可见性操作,在其他情况下,可以通过设备域使写入从/对着色器可见。

可用性操作可见性操作内存域操作 会更改先于它们发生且包含在其源作用域中的写入操作的状态,使其对其目标作用域可用或可见。

  • 对于可用性操作,源作用域是一组 (代理,引用,内存位置) 元组,目标作用域是一组内存域。

  • 对于内存域操作,源作用域是一个内存域,目标作用域是一个内存域。

  • 对于可见性操作,源作用域是一组内存域,目标作用域是一组 (代理,引用,内存位置) 元组。

如何确定作用域取决于具体的操作。可用性和内存域操作会扩展写入可用的内存域集合。可见性操作会扩展写入可见的 (代理,引用,内存位置) 元组集合。

回想一下,可用性和可见性状态是每个内存位置的属性,并设 W 是代理 A 通过引用 R 执行的对一个或多个位置的写入操作。设 L 是写入的位置之一。(W,L)(对 L 的写入 W)最初对任何内存域都不可用,并且仅对 (A,R,L) 可见。一个发生于 W 之后的可用性操作 AV 并且其源作用域中包含 (A,R,L) 使 (W,L) 对其目标作用域中的内存域可用

一个发生于 AV 之后,并且对于其源作用域中 (W,L) 可用的内存域操作 DOM 使 (W,L) 在目标内存域中可用。

一个发生于 AV(或 DOM)之后,并且对于其源作用域中的任何域中 (W,L) 都可用的可见性操作 VIS 使 (W,L) 对其目标作用域中包含的所有 (代理,引用,L) 元组可见

如果写入 W2 发生于 W 之后,并且它们的内存位置集合重叠,则对于那些重叠的内存位置,W 将不会对所有代理/引用可用/可见(并且未来的 AV/DOM/VIS 操作无法恢复 W 对这些位置的写入)。

可用性、内存域和可见性操作被视为其他非原子内存访问,用于内存语义,这意味着它们可以通过 release-acquire 序列或内存屏障排序。

一个可用性链是一系列对越来越广泛的内存域的可用性操作,其中链的元素 N+1 在元素 N 的目标内存域的双重作用域实例中执行,并且元素 N 先于元素 N+1 发生。例如,一个目标作用域为工作组实例域的可用性操作先于由同一工作组中的调用执行的对着色器域的可用性操作。一个发生于 W 之后,并且其源作用域中包含 (A,R,L) 的可用性链 AVC 使 (W,L) 对其最终目标作用域中的内存域可用。单元素的可用性链只是可用性操作。

类似地,一个可见性链是一系列来自越来越窄的内存域的可见性操作,其中链的元素 N 在元素 N+1 的源内存域的双重作用域实例中执行,并且元素 N 先于元素 N+1 发生。例如,一个源作用域为着色器域的可见性操作先于由同一工作组中的调用执行的源作用域为工作组实例域的可见性操作。一个发生于 AVC(或 DOM)之后,并且对于其源作用域中的任何域中 (W,L) 都可用的可见性链 VISC 使 (W,L) 对其最终目标作用域中包含的所有 (代理,引用,L) 元组可见。单元素的可见性链只是可见性操作。

可用性、可见性和域操作

以下操作会生成可用性、可见性和域操作。当描述多个可用性/可见性/域操作时,它们会按照列出的顺序彼此进行系统同步。

执行内存依赖的操作会生成

  • 如果源访问掩码包含 VK_ACCESS_HOST_WRITE_BIT,则依赖关系包括从主机域到设备域的内存域操作。

  • 一个可用性操作,其源范围是依赖关系的第一个访问范围中的所有写入,目标范围是设备域。

  • 一个可见性操作,其源范围是设备域,目标范围是依赖关系的第二个访问范围。

  • 如果目标访问掩码包含 VK_ACCESS_HOST_READ_BITVK_ACCESS_HOST_WRITE_BIT,则依赖关系包括从设备域到主机域的内存域操作。

vkFlushMappedMemoryRanges 执行一个可用性操作,其源范围是(代理,引用)=(所有主机线程,传递给命令的所有映射内存范围),目标范围是主机域。

vkInvalidateMappedMemoryRanges 执行一个可见性操作,其源范围是主机域,目标范围是(代理,引用)=(所有主机线程,传递给命令的所有映射内存范围)。

vkQueueSubmit 执行从主机到设备的内存域操作,以及一个可见性操作,其源范围是设备域,目标范围是设备上的所有代理和引用。

可用性和可见性语义

通过代理 A 进行的内存屏障或原子操作,如果其语义中包含 MakeAvailable,则执行一个可用性操作,其源范围包括代理 A 和该指令存储类语义中的所有存储类中的所有引用以及所有内存位置,其目标范围是如下所述选择的一组内存域。隐式可用性操作在屏障或原子操作与所有在屏障或原子操作之前按程序排序的其他操作之间按程序排序。

通过代理 A 进行的内存屏障或原子操作,如果其语义中包含 MakeVisible,则执行一个可见性操作,其源范围是如下所述选择的一组内存域,其目标范围包括代理 A 和该指令存储类语义中的所有存储类中的所有引用以及所有内存位置。隐式可见性操作在屏障或原子操作与所有在屏障或原子操作之后按程序排序的其他操作之间按程序排序。

内存域的选择基于指令的内存范围,如下所示

  • Device 范围使用着色器域

  • QueueFamily 范围使用队列族实例域

  • FragmentInterlock 范围使用片段互锁实例域

  • ShaderCallKHR 范围使用着色器调用实例域

  • Workgroup 范围使用工作组实例域

  • Subgroup 使用子组实例域

  • Invocation 不执行可用性/可见性操作。

当代理 A 执行的可用性操作的目标范围中包含内存域 D 时,其中 D 对应于范围实例 S,它也包括对应于每个较小的范围实例 S' 的内存域,S' 是 S 的子集且包含 A。可见性操作也是如此。

每个指令的可用性和可见性语义

包含 MakePointerAvailable 的内存写入指令或包含 MakeTexelAvailable 的图像写入指令,执行一个可用性操作,其源范围包括用于执行写入的代理和引用以及指令写入的内存位置,其目标范围是由 可用性和可见性语义 中指定的 Scope 操作数选择的一组内存域。隐式可用性操作在写入和所有在写入之后按程序排序的其他操作之间按程序排序。

包含 MakePointerVisible 的内存读取指令或包含 MakeTexelVisible 的图像读取指令,执行一个可见性操作,其源范围是由 可用性和可见性语义 中指定的 Scope 操作数选择的一组内存域,其目标范围包括用于执行读取的代理和引用以及指令读取的内存位置。隐式可见性操作在读取和所有在读取之前按程序排序的其他操作之间按程序排序。

尽管具有每个指令可见性的读取仅执行来自着色器或片段互锁实例或着色器调用实例或工作组实例或子组实例域的可见性操作,它们也将看到通过设备域可见的写入,即先前由非着色器代理执行并通过 API 命令使其可见的写入。

预计子组中的所有调用都在同一处理器上执行,并使用相同的内存访问路径,因此具有子组范围的可用性和可见性操作预计是“免费的”。

位置排序

设 X 和 Y 是对重叠内存位置集 M 的内存访问,其中 X != Y。设 (AX,RX) 是用于 X 的代理和引用,(AY,RY) 是用于 Y 的代理和引用。现在,让 “→” 表示先于发生,“→rcpo” 表示程序排序之前的自反闭包。

如果 D1 和 D2 是不同的内存域,则让 DOM(D1,D2) 表示从 D1 到 D2 的内存域操作。否则,让 DOM(D,D) 为占位符,使得 X→DOM(D,D)→Y 当且仅当 X→Y。

如果满足以下任一条件,则 X 在 M 中的位置 L 上位置排序在 Y 之前

  • AX == AY 并且 RX == RY 并且 X→Y

    • 注意:这种情况意味着当它是相同的(代理,引用)时,不需要可用性/可见性操作。

  • X 是一个读取,X 和 Y 都是非私有的,并且 X→Y

  • X 是一个读取,并且 X(传递地)与 Y 系统同步

  • 如果 RX == RY 并且 AX 和 AY 访问一个公共内存域 D(例如,如果 D 是工作组实例域,则位于同一工作组实例中),并且 X 和 Y 都是非私有的

    • X 是一个写入,Y 是一个写入,AVC(AX,RX,D,L) 是一个可用性链,使 (X,L) 对域 D 可用,并且 X→rcpoAVC(AX,RX,D,L)→Y

    • X 是一个写入,Y 是一个读取,AVC(AX,RX,D,L) 是一个可用性链,使 (X,L) 对域 D 可用,VISC(AY,RY,D,L) 是一个可见性链,使在域 D 中对 L 的写入对 Y 可见,并且 X→rcpoAVC(AX,RX,D,L)→VISC(AY,RY,D,L)→rcpoY

    • 如果 VkPhysicalDeviceVulkanMemoryModelFeatures::vulkanMemoryModelAvailabilityVisibilityChainsVK_FALSE,则在上面的每个子条目中,AVC 和 VISC 必须 各自在链中只有一个元素。

  • 让 DX 和 DY 分别是设备域或主机域,具体取决于 AX 和 AY 是在设备上还是在主机上执行

    • X 是一个写入,Y 是一个写入,并且 X→AV(AX,RX,DX,L)→DOM(DX,DY)→Y

    • X 是一个写入,Y 是一个读取,并且 X→AV(AX,RX,DX,L)→DOM(DX,DY)→VIS(AY,RY,DY,L)→Y

最后一个条目(通过设备/主机域同步)需要 API 级别的同步操作,因为设备/主机域无法通过着色器指令访问。“设备域” 不应与 “设备范围” 混淆,后者通过 “着色器域” 同步。

数据竞争

设 X 和 Y 是访问重叠内存位置集合 M 的操作,其中 X != Y,且 X 和 Y 中至少有一个是写操作,并且 X 和 Y 不是互斥有序的原子操作。如果对于 M 中的每个位置,X 和 Y 之间不存在位置有序关系,则存在数据竞争

应用程序必须确保在其应用程序执行期间不会发生数据竞争。

数据竞争只能由于实际执行的指令而发生。例如,由于控制流而跳过的指令不得导致数据竞争。

可见性 (Visible-To)

设 X 为写操作,Y 为读操作,它们的内存位置集合重叠,且 M 为重叠的内存位置集合。设 M2 为 M 的非空子集。当且仅当以下所有条件都成立时,对于内存位置 M2,X 对 Y 是可见的

  • 对于 M2 中的每个位置 L,X 在位置上先于 Y。

  • 不存在对 M2 中任何位置 L 的另一个写操作 Z,使得 X 在位置上先于位置 L 的 Z,且 Z 在位置上先于位置 L 的 Y。

如果 X 对 Y 可见,则 Y 读取 X 对于位置 M2 写入的值。

在 X 和 Y 之间可能存在写操作,该操作会覆盖一部分内存位置,但剩余的内存位置(M2)仍将对 Y 可见。

无环性 (Acyclicity)

Reads-from 是操作之间的一种关系,其中第一个操作是写操作,第二个操作是读操作,并且第二个操作读取第一个操作写入的值。From-reads 是操作之间的一种关系,其中第一个操作是读操作,第二个操作是写操作,并且第一个操作读取的值早于第二个操作在其作用域修改顺序或位置顺序中的写入(或者第一个操作从初始值读取,第二个操作是对相同位置的任何写入)。

然后,实现必须保证以下关系的并集中不存在环:

  • 位置有序 (location-ordered)

  • 作用域修改顺序(对于所有原子写操作)

  • 读取自 (reads-from)

  • 来自读取 (from-reads)

这是一个“一致性”公理,它非正式地保证操作序列不会违反因果关系。

作用域修改顺序一致性 (Scoped Modification Order Coherence)

设 A 和 B 是互斥有序的原子操作,其中 A 在位置上先于 B。那么,以下规则是无环性的结果:

  • 如果 A 和 B 都是读取操作,且 A 没有读取初始值,那么 A 取值的写入操作必须在其自身的作用域修改顺序中早于(或与)B 取值的写入操作(位置顺序、读取自和来自读取之间没有环)。

  • 如果 A 是读取操作,B 是写入操作,且 A 没有读取初始值,那么 A 必须从 B 的作用域修改顺序中早于 B 的写入操作中取值(位置顺序、作用域修改顺序和读取自之间没有环)。

  • 如果 A 是写入操作,B 是读取操作,那么 B 必须从 A 或 A 的作用域修改顺序中晚于 A 的写入操作中取值(位置顺序、作用域修改顺序和来自读取之间没有环)。

  • 如果 A 和 B 都是写入操作,那么 A 必须在其自身的作用域修改顺序中早于 B(位置顺序和作用域修改顺序之间没有环)。

  • 如果 A 是写入操作,B 是读取-修改-写入操作,且 B 读取 A 写入的值,那么 B 紧随 A 在 A 的作用域修改顺序之后(作用域修改顺序和来自读取之间没有环)。

着色器 I/O (Shader I/O)

如果着色器阶段(而非 Vertex)中的着色器调用 A 从存储类 CallableDataKHRIncomingCallableDataKHRRayPayloadKHRHitAttributeKHRIncomingRayPayloadKHRInput 中的对象执行内存读取操作 X,则 X 在系统同步之后发生,在生成调用 A 的着色器调用中,所有对相应的 CallableDataKHRIncomingCallableDataKHRRayPayloadKHRHitAttributeKHRIncomingRayPayloadKHROutput 存储变量的写入操作之后,并且这些写入操作对 X 都是可见的。

上游着色器调用不需要完成执行,它们只需要生成正在读取的输出即可。

取消分配 (Deallocation)

vkFreeMemory 的调用必须发生在对该 VkDeviceMemory 对象中所有内存位置的所有内存操作之后。

通常,给定队列中的设备内存操作与 vkFreeMemory 同步,方法是让主机线程等待该队列发出信号的栅栏,并且等待发生在主机上调用 vkFreeMemory 之前。

SPIR-V 变量的取消分配由系统管理,并且发生在对这些变量的所有操作之后。

描述(信息性)(Descriptions (Informative))

本小节为应用/编译器开发人员提供了更易于理解的内存模型后果。

设 SC 为释放或获取操作或屏障指定的存储类。

  • 具有释放语义的原子写入不得针对 SC 的任何读取或写入操作重新排序,该读取或写入操作在程序顺序中位于该原子写入操作之前(无论原子操作位于哪个存储类中)。

  • 具有获取语义的原子读取不得针对 SC 的任何读取或写入操作重新排序,该读取或写入操作在程序顺序中位于该原子读取操作之后(无论原子操作位于哪个存储类中)。

  • 在释放屏障之后按程序顺序对 SC 的任何写入操作不得针对该屏障之前按程序顺序对 SC 的任何读取或写入操作重新排序。

  • 在获取屏障之前按程序顺序从 SC 的任何读取操作不得针对该屏障之后按程序顺序对 SC 的任何读取或写入操作重新排序。

控制屏障(即使它没有内存语义)也不得针对任何内存屏障重新排序。

此内存模型允许在相同的内存位置上执行具有和不具有可用性和可见性操作的内存访问,以及原子操作。这对于能够推理以多种方式重用的内存至关重要,例如在不同着色器调用或绘制调用的生命周期中。虽然 GLSL(和旧版 SPIR-V)出于历史原因将“一致性”修饰符应用于变量,但此模型将每个内存访问指令视为具有可选的隐式可用性/可见性操作。GLSL 到 SPIR-V 的编译器应将一致性变量上的所有(非原子)操作映射到此模型中的 Make{Pointer,Texel}{Available}{Visible} 标志。

原子操作隐式具有可用性/可见性操作,并且这些操作的范围取自原子操作的范围。

细分输出排序 (Tessellation Output Ordering)

对于使用 Vulkan 内存模型的 SPIR-V,OutputMemory 存储类用于同步对细分控制输出变量的访问。对于没有通过 OpMemoryModel 启用 Vulkan 内存模型的旧版 SPIR-V,细分输出可以使用没有特定内存范围或语义的控制屏障进行排序,如下所述。

令 X 和 Y 是着色器调用 AX 和 AY 执行的内存操作。当且仅当以下所有条件都为真时,操作 X 在操作 Y 之前是细分输出排序的

  • 存在一个 OpControlBarrier 指令 C 的动态实例,使得 X 在 AX 中在 C 之前按程序排序,并且 C 在 AY 中在 Y 之前按程序排序。

  • AX 和 AY 位于 C 的执行范围的同一实例中。

如果 TessellationControl 执行模型中的着色器调用 AX 和 AY 分别在 Output 存储类上执行内存操作 X 和 Y,并且 X 在 Workgroup 范围内在 Y 之前按细分输出排序,则 X 在 Y 之前按位置排序,如果 X 是写入且 Y 是读取,则 X 对 Y 可见。

协作矩阵内存访问

对于协作矩阵加载指令(OpCooperativeMatrixLoadKHR , OpCooperativeMatrixLoadNV , OpCooperativeMatrixLoadTensorNV)的每个动态实例,矩阵范围内的某些实现相关的调用会从被定义为由该指令访问的每个内存位置执行非原子加载。

对于协作矩阵存储指令(OpCooperativeMatrixStoreKHR , OpCooperativeMatrixStoreNV , OpCooperativeMatrixStoreTensorNV)的每个动态实例访问的每个内存位置,矩阵范围内的单个实现相关的调用会对该内存位置执行非原子存储。