和学习其他API一样,学习Vulkan API中有一个重要部分:了解Vulkan API定义了拿下类型,以及这些类型之间的关系。为了帮助理解这些类型,接下来会绘制一幅关系图,表现它们之间的关系,尤其是创建依赖关系。
每个Vulkan类型都有一个特定的前缀Vk。这些前缀在下面的关系图中将会被省略。每个Vulkan函数都有一个特定前缀vk。举个例子:关系图中的Sampler,其实表示VkSampler。这些Vulkan类型不能被视为指针或者数字,把它理解成句柄是一个不错的方向,它是一个不透明的句柄。绿色背景对象的原始类型是uint32_t。
带箭头的实线表示创建顺序。举个例子,创建DescriptorSet之前,必选先创建DescriptorPool。
带实心菱形的实线表示包含关系,也就是说,该对象无需创建,直接从它的父对象中获取。举个例子:PhysicalDevice对象并不是创建的,而是从Instance对象中枚举的。
虚线表示其他关系,比如:提交各种命令到CommandBUffer对象。

Instance:Vulkan应用第一个创建的对象。它用于连接上层应用与Vulkan运行时驱动,因此,在上层应用中应该只创建一个。同时,它也用于存储Vulkan程序相关状态的软件结构,因此,所有上层应用想要使能的验证层或特性扩展,都应该在创建Instance时被指定。
PhysicalDevice:表示GPU硬件的抽象。上层应用可以从Instance对象中枚举得到物理设备,之所以是枚举的原因在于可以存在多个物理设备。同时,可以从PhysicalDevice对象中查询VendorId,DeviceId和所有支持的特性,以及相关属性和限制。
PhysicalDevice对象可以枚举得到所有可用的Queue Families。最主要的就是图形队列,其次还有计算队列或传输队列。
PhysicalDevice对象还可以枚举得到支持Memory Heaps和Memory Types。Memory Heap表示特定的RAM池,它能抽象母板上的RAM、或GPU片上的RAM、或任何其他Host-Or-Device内存。在分配内存时,上层应用必须指定Memory Type,它承载了堆内存的特定需求,比如:Host-Visible、Coherent(CPU and GPU Visible)。根据不同的供应商,这些类型会有不同的组合。
Device:它是基于物理设备创建的逻辑设备。它是Vulkan API中最基本的对象,几乎所有对象的创建都会依赖它。在创建逻辑设备时,需要指定期望使能的设备特性,比如:各向异性纹理过滤。同时,上层应用也必须指明将会使用的队列,包括其索引和Queue Families。
Queue:它接收指令,并将指令提交到GPU上去执行。所有GPU执行任务,都会填充到CommandBuffers中,然后提交到Queues,使用Vulkan API函数vkQueueSubmit。如果,上层应用分别制定了图形队列和计算队列,则可以将不同的CommandBuffers提交对应的队列。
CommandPool:它是一个简单的对象,唯一的功能就是分配CommandBuffer。它也需要指定Queue Family。
CommandBuffer:它是从CommandPool对象中分配的。它表示在逻辑设备上执行各种指令的缓冲。在这个指令缓冲上面,上层应用可以填充各种各样的指令,所有指令都有相同的前缀vkCmd。
Sampler:它不会被绑定到任何特定的Image上。它更像是一组状态参数,比如:滤波模式(nearest or linear),或寻址模式(repeat, clamp-to-edge, clamp-to-border)。
Buffer和Image是两种占用设备内存的资源类型。
Buffer是相对简单的那种,它是任意的二进制数据的容器,且以字节(Byte)为单位的长度。
Image则表示像素集合。在其他API中,被称之为纹理(textture)。上层应用创建一个Image时,有很多参数需要指定。比如:类型上可以分为1D,2D,3D;像素格式也有很多种(比如:R8G8B8A8_UNORM or R32_SFLOAT);也可以是一组离散图像,用于mipmap;Image在不同的驱动实现下,可以有两种内部组成格式(tiling或layout)。
创建一个Buffer或者Image,驱动并不会自动为之分配内存。上层应用需要分成3步去创建:
- Allocate DeviceMemory
- Create Buffer or Image
- Bing them together using function
vkBindBufferMemoryorvkBindImageMemory
这就是为什么上层应用必须创建DeviceMemory对象,它代表了一块内存,按照指定的内存类型和按照字节为单位指定大小。同时,上层应用不应该为每一个Buffer或Image分别创建一个DeviceMemory。取而代之的是,上层应用应该批发一大块DeviceMemory用于众多Buffer或Image。这是因为,分配DeviceMemory是一个开销较大的操作,同时,驱动也限制了DeviceMemory分配的数量。这个限制可以在PhysicalDevice中查询。
这里有一个例外,SwapChain中的Image则不需要上层应用主动分配并绑定DeviceMemory。
Buffer和Image被创建、绑定DeviceMemory,在渲染过程中也不能被直接使用。
BufferView:它必须依赖已经创建好的Buffer对象,才能被创建。同时,创建的时候必须传递偏移和范围,来表示该BufferView仅仅只是访问其中一部分。
ImageView:它必须依赖已经创建好的Image对象,才能被创建。同时,也必须传递一组参数,来限制该ImageView的访问范围和方式。
着色器访问这些资源(Buffer、Image和Sampler)的方式是描述符。但是,描述符本身并不存在,它们总是被被描述符集管理。在创建描述符集之前,应用程序必须先创建一个DescriptorSetLayout,它是用于定义描述符集的行为模板。举个例子:假设,应用程序的某个着色器需要如下资源:
| Binding slot | Resource |
|---|---|
| 0 | One uniform buffer(called constant buffer in DirectX) available to the vertex shader stage. |
| 1 | Another uniform buffer available to the fragment shader stage |
| 2 | A sampled image |
| 3 | A sampler, also available to the fragment shader stage |
在创建描述符集之前,应用程序需要创建一个DescriptorPool,它专用于分配DescriptorSet。在创建DescriptorPool之前,应用程序必须指定将来需要使用的描述符集的类型以及最大可分配数量。
最终,应用程序分配DescriptorSet所需要的前置对象包括DescriptorPool 和DescriptorSetLayout 。
DescriptorSet表示保存实际描述符集的内存,可以对其进行配置,以便描述符集指向特定的Buffer、BufferView、Image或Sampler。应用程序可以通过掉用函数vkUpdateDescriptorSets。
可以通过函数vkCmdBindDescriptorSets将CommandBuffer中需要使用的描述符集绑定。这个函数还需要PipelineLayout对象。它表示渲染管线的配置,指定描述符集的类型将会在CommandBuffer中使用。
FrameBuffer:应用程序可以创建一个FrameBuffer对象,必须指定RenderPass和ImageView数组。同时,它的数量和格式必须匹配RenderPass。
Pipeline下次再看:
Semaphore:它的创建无需参数,用于Queue之间的同步。
Event:它的创建无需参数,专用于GPU和CPU之间的同步,调用的函数是vkCmdSetEvent、vkCmdResetEvent和vkCmdWaitEvent。同样,也可以在CPU的其他线程中调用函数vkGetEventStatus获取Event的状态。
参考:https://gpuopen.com/learn/understanding-vulkan-objects/
参考:https://docs.vulkan.org/spec/latest/chapters/pipelines.html