和学习其他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
vkBindBufferMemory
orvkBindImageMemory
这就是为什么上层应用必须创建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