typedef uint32_t VkBool32;
本章介绍一些基础的概念,包括Vulkan架构和执行模型、API语法、队列、管线、配置、 数值表示、状态和状态查询,还有不同类型的对象和着色器。 在本规范文档剩余部分中它提供了一个对更加精细描述命令和行为做解释的框架。
Vulkan和其API为符合以下特征的CPU、GPU和其他硬件加速架构所设计和实现:
运行时库支持8位、16位、32位和64位有符号和无符号整形,都可以通过该类型 数据的粒度的大小来寻址到。
运行时库支持满足 Floating Point Computation 节 的范围和精度的32位和64位浮点类型。
这些类型的表示和大小端必须满足主机端和设备端一致。
注意
因为Vulkan中很多数据类型和结构可能在主机端和设备端内存来回的映射,主机端和设备端 架构必须能够高效的访问到数据,以便很方便的写高性能、可移植的应用程序。 |
在支持Vulkan的特定平台上此规范对影响ABI()的选项开放,这些选项通常是平台提供商用来 向前兼容的。 一些选项,比如函数调用惯例,可能在 vk_platform.h 头文件中的不同部分。
注意
例如,Android ABI由Google定义,Linux ABI是通过一系列的GCC默认项、发行版提供商和诸如Linux标准库 的外部标准一同定义的。 |
本节描绘了Vulkan执行系统执行模型的主框架。
Vulkan对外暴露一个或多个 设备,每一个对外暴露一个或多个 队列,队列之间异步的处理工作。 一个设备支持的一个集合的队列被分到 族_里。每一个族都支持一个或多个类型的功能,并可能包含多个拥有相近特性的队列。 在同一个族中的队列被认为是互相 _兼容的,同一个族中的队列需要完成的任务可以被任何一个队列执行。 这份规范定义了队列可能支持的四种功能:图形,计算,转移,稀疏内存管理。
注意
单个设备可能报告有多个相近的队列族,而非报告含有这些队列中一个或者多个成员。 这表明多个族的有相近功能的成员之间可能并不兼容。 |
设备内存是由应用程序显式的管理的。每一个设备可能宣称有一个或多个堆,表示内存的不同区域。 内存堆可能是在主机端或者是设备端,但是,都能被设备所见。 关于内存堆的更多细节是通过该堆上获取的内存类型对外暴露的。 在一个Vulkan实现上可用的内存区域的例子包含如下:
device local 设物理连接到设备的内存。
device local, host visible 是设备端内存,对主机端可见。 the host.
host local, host visible 是主机端内存,对设备和主机都可见。
在其他的一些架构上,也许只有个堆,可以用作任何用途。
一个Vulkan应用程序通过提交记录了Vulkan库调用激发的设备命令的命令缓冲区来控制多个物理设备。 命令缓冲区的内容是通过硬件指定的,对应用程序不可见。 一个命令缓冲区一旦被构造出来,就可以马上一次或多次提交到一个设备上以备执行。 多个命令缓冲区可能在应用程序里多线程中并行的被构建。
提交到不同的队列的命令缓冲区可能并行的执行或者乱序执行。 提交到同一个队列的命令缓冲区遵循submission order, 这在synchronization chapter中深入描述。 命令缓冲区在设备上的执行和主机端的执行也是异步的。 一旦命令缓冲区被提交到一个队列,控制权马上返回到应用程序。 在主机端和设备之间的同步,和不同队列之间的同步是应用程序的职责。
Vulkan队列提供了设备执行引擎的接口。执行引擎的命令需要在执行之前被记录到命令缓冲区。 这些命令缓冲区然后被一个 _queue submission_提交到队列以供一个或多个批次执行。 一旦提交到队列,这些命令将开始并完成执行,不受应用程序的干扰,虽然执行的顺序会受到 implicit and explicit ordering constraints的一些限制。
任务通常被一些队列提交命令提交到队列,这些命令一般形如vkQueue
* (比如 vkQueueSubmit
, vkQueueBindSparse
),且可能接受一些等待任务开始的信号量和一些任务完成才激发的信号量。 任务本身,以及激发和等待信号量都是 队列操作。
在不同队列上的队列操作并没有隐式的顺序限制,可能以任何顺序执行。 不同队列间显式的顺序限制可以通过semaphores 和fences表述。
提交到单个队列的命令缓冲区遵循submission order 和其他 implicit ordering guarantees,否则可能重叠或者乱序执行。 对于单一队列上的批次和队列提交的其他类型,和其他队列或批次提交之间并没有隐式的顺序限制。 在不同队列和各自的批次之间的附加显式的顺序限制可以通过semaphores 和 fences表述。
在栅栏或信号量被激发之前,可以确定的是之前被提交的队列操作已经完成了,且这些队列操作的内存写入对未来的队列操作 可见。 等待一个被激发的信号量或者栅栏保证之前的可用的写入对后续的命令是可见的。
在相同或不同的批次或者提交,还有主和次命令缓冲区之间的命令缓冲区边界,不会有任何附加的顺序限制。 也就是,在任何信号量或栅栏操作之间提交多个命令缓冲区(包含执行次级命令缓冲区)执行被记录的命令,就如同他们被记录进入 单个主命令缓冲区一样,除了每一个边界当前的状态都被重置 。显式顺序限制可以通过 explicit synchronization primitives表示。
在一个命令缓冲区内多个命令之间有一些隐式顺序保证,但是只包含 一部分执行子集。附加的显式顺序限制可以通过多种显式同步原语来表示。
注意
Vulkan实现对提交到一个队列的任务之间的重叠执行有极大的自由度,这是由 Vulkan设备里深度的管线和并行机制导致的。 |
被记录在命令缓冲区的命令,要么执行操作(绘制、分发、清除、复制、查询/时间戳操作、开始/结束subpass操作), 设置状态(绑定管线、描述符集、缓冲区、设置动态状态、推送常量、设置render pass/subpass状态), 要们执行同步(设置/等待时间、管线屏障、renderpass/subpass依赖)。一些命令执行不止一个上述任务。状态设置命令 更新命令缓冲区的 当前状态。一些命令执行操作(如绘制/分发)基于从命令缓冲区开始累积到当前状态集。 执行操作的命令内的任务是可以重叠或者重新记录的,但是必须禁止改动每一个操作命令使用的状态。 通常,操作命令是那些更改帧缓冲区附件、读写缓冲区或者图像内存、想查询池写入的命令,
同步命令在两个操作命令集合之间引入显式的execution and memory dependencies ,这里 第二个命令集合依赖于第一个命令集合。 这些依赖强制保证在后面的集合中的某些管线阶段的执行发生在源集合中某些阶段的执行之后, 且某些管线阶段执行的内存访问的影响结果顺序发生并对彼此可见。 当没有显式的依赖或隐式的顺序保证,操作命令也许重叠执行或者乱序执行,而且看不到 彼此的内存访问的影响结果。
设备执行队列操作和主机端是异步的。当命令缓冲区被提交到队列后控制流马上就退回到应用程序了。 应用程序必须按需求在主机端和设备端同步任务。
设备、队列和Vulkan中其他的的实体都是通过Vulkan对象表示的。 在API层,所有的对象都通过handle来引用。有两种类型的handle:可分发的与不可分发的。 可分发的 handle是不可见类型数据的指针。这个指针可被layers使用,被当作拦截API命令的一部分, 每一个API命令头接受一个可分发类型的handle作为第一个参数。 每一个不可分发类型的对象必须在其生命周期内有唯一一个handle值。
_不可分发的_handle类型是64位整型类型,其含义是Vulkan实现决定的,能把对象信息直接包含到handle里,而非通过指向一个数据结构。 不可分发类型的对象,不一定只有一个唯一的handle值。 如果其他类型的handle值变得无效了,那么销毁这样的一个handle必须不能导致此对象其他类型的handle失效,如果一个handle值被创建的次数多与被销毁的次数 ,则必须不能导致同种类型的等价的handle变得无效。
所有通过VkDevice
(比如 with a VkDevice
作为第一个参数)的命令创建的对象都是该设备私有的,必须不能被其他设备使用。
对象都是通过形如vkCreate
* and vkAllocate
* 这样的命令创建或者分配的。 一旦一个对象被创建或者分配,它的结构就被认为是不变的,即使某个对象类型的内容仍然是可以被自由的改动。 对象都是通过形如vkDestroy
* and vkFree
* 的命令来销毁或者释放的。
被分配(而不是创建)的对象从一个已存在的池子对象或者内存堆中获取资源,当被释放时把资源归还给该池子或者堆。但 对象的创建和销毁在运行时通常是低频操作, 分配或者释放对象可能是高频的。对象池帮助调节分配和释放的性能提升。
应用程序有责任跟踪Vulkan对象的生命周期,且在对象正在被使用时不能销毁它们。
应用程序所拥有的内存被内存传递所到的命令迅速使用。在使用这些内存的命令返回后,应用程序可以立刻更改或者释放这些内存。
以下对象类型被传入Vulkan命令是被使用,此后并不被用它们来创建的对象所访问。它们在传入所到的API命令执行期间被能被销毁:
VkShaderModule
VkPipelineCache
一个 VkPipelineLayout
对象在被任何使用它的命令缓冲区记录状态时被销毁。
VkDescriptorSetLayout
对象可以被操作使用其布局的描述符集合的命令访问,在描述符集合布局被销毁后这些描述符集合必须不能被vkUpdateDescriptorSets
更新。否则的话,描述符集合布局可在它们不被Vulkan API命令使用的任何时刻被销毁。
在设备(如从过命令缓冲区执行)已经完成使用Vulkan对象之前,应用程序必须不能销毁这些任何类型的Vulkan对象。
如下类型的Vulkan对象在被命令缓冲区使用或者暂停执行时不能被销毁:
VkEvent
VkQueryPool
VkBuffer
VkBufferView
VkImage
VkImageView
VkPipeline
VkSampler
VkDescriptorPool
VkFramebuffer
VkRenderPass
VkCommandPool
VkDeviceMemory
VkDescriptorSet
一下Vulkan对象在队列执行使用到这些对象的命令时不能被销毁:
VkFence
VkSemaphore
VkCommandBuffer
VkCommandPool
通常,对象可以按照任意顺序销毁或者释放 ,即使被释放的对象可能使用到另外一个对象(如在视图中使用一个资源, 在描述符集合中使用视图,在命令缓冲区中使用对象,绑定分配的内存到资源),只要使用被释放了的对象的对象不被 再次使用,除了被销毁或者重置这样的导致对象不再使用另外一个对象的情况(比如重置了命令缓冲区)。 如果对象被重置,那么它可以被使用,如同从来没有用过被释放的对象一样。 一个例外是对象之间存在父子关系时。在这种情况下,在子对象被销毁前应用程序必须不能销毁父对象,除非父对象被释放 时被定义显式的释放它的子对象(比如下面定义的对象池)。
VkCommandPool
对象是 VkCommandBuffer
的父对象。 VkDescriptorPool
对象是 VkDescriptorSet
的父对象。VkDevice
对象是很多对象类型(所有接受VkDevice
作为参数来创建)的父对象。
下面的Vulkan对象在被销毁时有特定的限制:
VkQueue
不能被显式的销毁。当它们所在的VkDevice
对象被销毁时才被隐式的销毁。
销毁一个池对象隐式的释放从它分配出来的所有对象。特别是,销毁VkCommandPool
就会释放所有从之分配出来 的VkCommandBuffer
对象,销毁VkDescriptorPool
会释放从之分配而来的VkDescriptorSet
对象。
当所有从VkDevice
获取到的VkQueue
对象处于空闲状态时,VkDevice
对象可以被销毁, 所有依VkQueue
而创建的对象也被销毁了。这包括如下对象:
VkFence
VkSemaphore
VkEvent
VkQueryPool
VkBuffer
VkBufferView
VkImage
VkImageView
VkShaderModule
VkPipelineCache
VkPipeline
VkPipelineLayout
VkSampler
VkDescriptorSetLayout
VkDescriptorPool
VkFramebuffer
VkRenderPass
VkCommandPool
VkCommandBuffer
VkDeviceMemory
VkPhysicalDevice
不能被显式的销毁。相反,在所有从值获取的VkInstance
对象被销毁后被隐式的销毁。
当所有从VkPhysicalDevice
中创建的 VkDevice
被销毁后, VkInstance
对象才被销毁。
这份规范描述Vulkan命令为C99语法的函数或者过程。其他语言,如C++和JavaScript的绑定允许更严格的参数传递,或者面向对象接口。 Vulkan使用标准的C类型作为标量参数的基础类型(比如stdint.h中的类型),例外的情况有如下或者本文档中任何合适的地方:
VkBool32
表示boolean类型的`True` and `False`值,因为C并没有可移植的内置boolean类型:
typedef uint32_t VkBool32;
VK_TRUE
表示 boolean True (整型 1) 值, VK_FALSE
表示一个boolean False (整型 0) 值。
从Vulkan实现返回的VkBool32
类型的值要么是VK_TRUE
要么是 VK_FALSE
。
当Vulkan实现期望VkBool32
类型参数时,应用程序不能传入 VK_TRUE
或 VK_FALSE
以外的值。
VkDeviceSize
表示设备内存大小和偏移量:
typedef uint64_t VkDeviceSize;
创建Vulkan对象的命令都形如vkCreate
*,接受形如Vk
*CreateInfo的参数。这些Vulkan对象通过形如vkDestroy
* 的命令来销毁。
每一个用来创建或者销毁Vulkan对象的命令的最后一个传入参数是pAllocator
。pAllocator
参数可以被置为非NULL值,此时, 对给定对象的分配任务就代理给应用程序提供的回调函数了;请参考Memory Allocation章节以得到更多细节。
从池对象中分配Vulkan对象的命令都形如vkAllocate
*,接受形如 Vk
*AllocateInfo 的结构为参数。 这些Vulkan对象都通过形如vkFree
*的命令来释放。这些对象并不接受内存分配器;如果需要主机端内存,它们将使用缓存池被创建时 指定分配器。
通过调用形如vkCmd
*的API命令把命令记录到命令缓冲区。每一个命令可能有不同的限制条件:在和/或此命令缓冲区,在一个 renderpass内部和/或外部,在一个或者多个受支持队列类型中。这些限制条件都在每一个命令的定义处给出。
Vulkan命令的 _duration_是指调用命令到它返回的时间段。
通过形如vkGet
* and vkEnumerate
*的命令从Vulkan实现中获取这些信息。
除非针对一个特定的命令,结果都是 不变的 。亦即,只要参数保持有效,用相同的参数调用这些命令的结果将会保持不变。
在多线程CPU主机上,Vulkan提供线性拓展能力。所有的命令都支持多线程,但是一些参数或者参数成员需要在外部保持同步。这意味着 调用者必须保证同一时刻只能有一个线程在使用这个参数。
更准确的来说,Vulkan命令使用简单的存储来更新表示Vulkan对象的数据结构。在主机端执行命令时,被声明为外部同步的参数的内容可能 就被更新了。如果两个命令操作同一个对象,至少有一个命令生命这个对象需要被外部同步保护,然而调用者保证命令不会同时执行, 但是如果有必要,两个命令可以通过内存屏障被分离。
注意
内存屏障在ARM CPU架构上非常重要,因为很多开发者所熟悉的X86/x64平台编程比相对无序一些。幸运的是,大多数高层同步原语 (像pthread库)是内存屏障表现为互斥的一种,所以通过这些原语实现Vulkan对象互斥会有预期的效果。 |
很多对象类型是不可改变内容的,意味着对象一旦被创建就不能被改变。这些类型不需要外部同步,除了在销毁的时候不能在另外 一个线程中使用。在一些特定的场合下,可改变内容的对象参数在其内部同步,所以无需在外部保持同步。这样的一个例子是在vkCreateGraphicsPipelines
和vkCreateComputePipelines
中VkPipelineCach
的使用,这里给这样一个重型的 命令做外部同步不切实际。在这个例子中,Vulkan实现必须在cache内部保持同步。还有,某些和命令参数相关的对象(比如命令池和描述符池)可能 被命令影响,它们也必须在外部保持同步。这些隐式的参数在下面文档给出。
需要在外部保持同步的命令的参数列举如下。
也有一些接受内容是在外部保持同步的参数的用户分配列表的命令的例子。这些情况下,调用者必须保证某时刻最多只有一个线程正在使用 列表中指定的元素。这些参数列举如下。
另外,有一些隐式参数需要在外部保持同步。比如所有需要在外部保持同步的的commandBuffer
参数表明当创建 该命令缓冲区时被传入的commandPool
也需要在外部保持同步。这个隐式的参数和它关联的对象列举如下。