第 6 章. 分配图像资源并使用 WSI 构建 Swapchain

第 6 章. 分配图像资源并使用 WSI 构建 Swapchain

在前一章中,我们介绍了与内存管理和命令缓冲相关的概念。 我们明白了主机内存和设备内存以及在 Vulkan API 中分配的方式。 我们还介绍了命令缓冲区,实现了命令缓冲区记录 API 的调用并将它们提交给队列进行处理。

在本章中,我们将通过对命令缓冲区和内存分配的知识实现交换链和深度图。 交换链提供了一种机制,通过这种机制,我们可以将绘制图元渲染为交换链中的彩色图像,然后将其传递到展示层以便在窗口中显示图元。 图像是交换缓冲区创建的先决条件;因此,本章将帮助您深入了解图像资源及其在 Vulkan 应用程序中的使用。

我们将涵盖以下主题:

图像资源入门了解图像资源内存分配以及绑定映像资源交换链介绍创建深度图总结应用程序的流程

图像资源入门

Vulkan 资源只是包含数据的内存视图的一种表示。 Vulkan 主要有两种类型的资源:缓冲区和图像。 在本章中,我们只讨论图像资源的概念,将用于实现交换链。 有关缓冲区资源类型的更多信息,请参阅第 7 章,“缓冲区资源”,“渲染通道”,“帧缓冲区”以及“使用 SPIR-V 的着色器”。 为了对此进行概述,您可能需要重温第 2 章中“你的第一个 Vulkan 伪代码程序”中的“资源对象 – 管理图像和缓冲区”部分。

Vulkan 图像以 1D / 2D / 3D 形式表示连续的纹理数据。 这些图像主要用作附件或纹理:

附件 Attachment:图像可以被附加到管线,用于帧缓冲区的颜色附件或深度附件,也可以用作辅助表面,用于多通道处理目的。纹理 Texture:图像用作描述符的接口,并以采样器的形式在着色器阶段(片段着色器)共享。

注意

如果您有使用过 OpenGL 的背景,请注意在 Vulkan 中使用图像与在 OpenGL 中使用图像完全不同。 在 Vulkan 中,通过指定一些指示图像使用类型的按位字段来创建图像,例如颜色附件、深度附件、模板附件,着色器中的采样图像,图像加载和存储等。 另外,您需要指定图像的平铺信息(线性或最优)。 这会为内存中的图像数据指定平铺或混合布局。

Vulkan 中纹理的概念主要使用图像,图像布局和图像视图来解释:

图像 Image:图像代表 Vulkan 中的纹理对象, 其中包含用于计算内存需求的元数据。 收集的内存需求在内存分配期间很有用。 图像可以表示其他以及众多类型的信息,例如格式,大小和类型(稀疏映射,立方体映射等)。 单个图像可能包含多个子资源,例如基于 mipmap 级别或一系列层的多个图像。 每个图像或图像子资源都使用图像布局进行指定。图像布局 Image layout:图像布局是在图像内存存储空间中,以网格坐标表示形式存储图像纹理信息的一种特定实现方法 。 存储在图像内存中的图像非常依赖具体的实现;每个图像都有特定的用法,例如,颜色附件,着色器中的采样图像,图像加载、存储或大图像的稀疏纹理。 对于这些特殊用途,实现提供了专门用于图像内存使用的图像布局,用来提供最佳的性能。

注意

每个图像布局都是特定的, 每个可能只提供某些功能。 例如,使用 VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL 图像布局指定的图像可以用作彩色图像附件以获得最佳性能;但是,它不能用于传输目的。

图像视图 Image view:图像不能直接通过 API 调用或管线着色器用于读写目的;相反,可以直接使用图像视图。 它不仅表现的像图像对象的接口那样工作,而且还提供元数据,用于表示连续范围的子图像资源。

图像创建概述

在本节中,我们会对图像创建过程以一种分步操作的方式快速浏览一遍。 这有助于获得图像、图像视图以及相关的内存分配的整体脉络。 紧接着本节的另外两节会介绍图像(理解图像资源)及其内存分配(内存分配和绑定图像资源)以及相应 API 的详细说明。

以下是关于如何使用 Vulkan API 创建图像资源的步骤说明:

首先,创建图像对象:使用 vkCreateImage()API 创建图像对象(VkImage)。 该 API 提供了一个 VkImageCreateInfo 结构的数组,该数组指定了创建一个或多个图像对象的重要图像特征。 在这个时刻,该图像对象在设备上还没有进行物理存储的分配;但是,它携带了下一步中分配存储空间的逻辑内存信息。 此内存信息来自 VkImageCreateInfo 对象,其中包含格式,图像大小,创建标志等。然后,分配图像内存:获取内存要求:在我们分配所需的图像内存块之前,我们需要计算要分配内存的适当尺寸。 这是使用 vkGetImageMemoryRequirements()API 完成的。 它可以根据图像属性自动计算图像的适当尺寸,使用了我们在前面步骤中描述的 VkCreateImageInfo 对象作为参数。确定内存类型:接下来,从可用内存类型中获取适当的内存类型。 类型可用后,就查找与用户需要的属性匹配的类型。分配设备内存:使用 vkAllocateMemory()API 分配设备内存(VkDeviceMemory)。绑定分配的内存:使用 vkBindImageMemory()API 把分配的设备内存(VkDeviceMemory)绑定到图像对象(VkImage)。设置图像布局:根据应用要求设置正确的图像布局;通过 vkCmdPipelineBarrier()并利用管线图像内存屏障执行此操作。创建图像视图:图像只能通过图像视图进行访问。 这是我们使用 vkCreateImageView()创建图像视图的最后一步。 图像现在终于可以被 API 调用或管线着色器使用了。

理解图像资源

本节将介绍用于创建图像资源的 Vulkan API。 在这里,我们将详细研究数据图像,图像视图和图像布局的概念。

创建图像

Vulkan 中的图像资源使用 VkImage 对象来表示。 该对象支持最多三维数据数组的多维图像。 图像是使用 vkCreateImage()API 创建的。 这是执行此操作的语法:

VkResult vkCreateImage( VkDevice device, const VkImageCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkImage* pImage);

下表描述了 VkCommandPoolCreateInfo 的各个参数:

device : 这是指负责创建图像的逻辑设备。

pCreateInfo :这指的是一个 VkImageCreateInfo 指针。

pAllocator: 这控制着主机内存分配的过程。

pImage :该参数指的是在创建 VkImage 后返回它的指针。

vkCreateImage()采用 VkImageCreateInfo 作为第二个参数,这个控制结构的定义如下所示:

typedef struct VkImageCreateInfo { VkStructureType sType; const void* pNext; VkImageCreateFlags flags; VkImageType imageType; VkFormat format; VkExtent3D extent; uint32_t mipLevels; uint32_t arrayLayers; VkSampleCountFlagBits samples; VkImageTiling tiling; VkImageUsageFlags usage; VkSharingMode sharingMode; uint32_t queueFamilyIndexCount; const uint32_t* pQueueFamilyIndices; VkImageLayout initialLayout; } VkImageCreateInfo;

下表介绍了 VkImageCreateInfo 的各个字段:

sType :这是这个控制结构的类型信息。必须将其指定为 VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO。

pNext :这可能是一个指向特定于扩展结构的有效指针或 NULL。

flags :这是指 VkImageCreateFlagBits 位字段标志。 有关这方面的更多信息将在本节后面提供。

imageType :这使用 VkImageType 枚举指定图像的 1D / 2D / 3D 维度。 它必须是下列之一:VK_IMAGE_TYPE_1D,VK_IMAGE_TYPE_2D 或 VK_IMAGE_TYPE_3D。

format:这是指在 VkFormat 类型中指定的图像格式,描述了将被包含在图像中的数据元素的格式和类型。

extent :这描述了基本级别每个维度中的元素数目。

mipLevels :这是指缩小的采样图像中可用的不同细节级别。

arrayLayers :这指定了图像中层的数量。

samples :这指定了图像中子数据元素样本的数量,定义在 VkSampleCountFlagBits 中。 tilings:这指定了内存中图像的平铺信息。 它应该是 VkImageTiling 类型,并且必须是以下两个枚举值之一:VK_IMAGE_TILING_OPTIMAL 或 VK_IMAGE_TILING_LINEAR。

usage :这个字段的表示的是 VkImageUsageFlagBits 指定描述图像预期用途的位字段。 有关这方面的更多信息将在本节后面提供。

sharingMode :这指定了图像的共享模式,当它会被多个队列族所访问时。 这必须是以下值之一:VkSharingMode 枚举中的 VK_SHARING_MODE_EXCLUSIVE 或 VK_SHARING_MODE_CONCURRENT。

queueFamilyIndexCount :这表示 queueFamilyIndices 数组中的条目数。 queueFamilyIndices | 这是要访问图像的队列族的一个数组。 共享模式必须是 VK_SHARING_MODE_CONCURRENT; 否则,就会忽略它。

initialLayout:这定义了图像所有子资源的初始 VkImageLayout 状态。 这必须是 VK_IMAGE_LAYOUT_UNDEFINED 或 VK_IMAGE_LAYOUT_PREINITIALIZED。 有关图像布局的更多信息,请参阅本章的“了解图像布局”部分。

使用 VkImageUsageFlagBits 枚举标志描述 VkImageCreateInfo 控制结构的、图像的 usage 标志。 以下是这个枚举的语法,以及每个字段类型的说明:

typedef enum VkImageUsageFlagBits { VK_IMAGE_USAGE_TRANSFER_SRC_BIT = 0x00000001, VK_IMAGE_USAGE_TRANSFER_DST_BIT = 0x00000002, VK_IMAGE_USAGE_SAMPLED_BIT = 0x00000004, VK_IMAGE_USAGE_STORAGE_BIT = 0x00000008, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT = 0x00000010, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT= 0x00000020, VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT = 0x00000040, VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT = 0x00000080, } VkImageUsageFlagBits;

我们来仔细看看这些按位字段,并理解它们的含义:

VK_IMAGE_USAGE_TRANSFER_SRC_BIT :图像由传输命令(复制命令)源使用。

VK_IMAGE_USAGE_TRANSFER_DST_BIT :图像由传输命令(复制命令)目的地使用。

VK_IMAGE_USAGE_SAMPLED_BIT :此图像类型通过图像视图类型在着色阶段用作采样器,其中关联的描述符集槽(VkDescriptorSet)类型可以是 VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE 或 VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER。着色器中的采样图像用于地址计算,控制过滤行为和其他属性。

VK_IMAGE_USAGE_STORAGE_BIT :使用此图像类型在图像内存上进行加载,存储和原子操作。 图像视图与类型 VK_DESCRIPTOR_TYPE_STORAGE_IMAGE 的描述符类型槽相关联。

VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT :从这种类型的图像资源创建的图像视图适用于颜色附件或与帧缓冲区对象(VkFrameBuffer)关联的解析附件。

VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT :从这种类型的图像资源创建的图像视图适用于深度 / 模板附件,或与帧缓冲区对象(VkFrameBuffer)关联的解析附件。

VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT :使用这个标志表示的图像类型是惰性分配的。 这种内存类型必须指定为 VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT。 请注意,如果指定了此标志,则 VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT 和 VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT 不得使用。 VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT :从这种类型的图像资源创建的图像视图适用于着色器阶段和帧缓冲区中的输入附件。 该图像视图必须与 VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT 类型的描述符集槽(VkDescriptorSet)关联。

注意

使用 VK_MEMORY_PROPERTY_LAZILY_-ALLOCATED_BIT 位标志分配的内存没有按照请求的大小立刻进行存储空间的分配,但可以以一种单调的方式分配,其中内存随应用程序需求逐渐增加。

VkImageCreateInfo 枚举中的 flag 字段提示底层应用程序如何使用 VkImageCreateFlagBits 枚举来管理各种图像资源,如内存,格式和属性。 以下是每种类型的语法:

typedef enum VkImageCreateFlagBits { VK_IMAGE_CREATE_SPARSE_BINDING_BIT = 0x00000001, VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT = 0x00000002, VK_IMAGE_CREATE_SPARSE_ALIASED_BIT = 0x00000004, VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT = 0x00000008, VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT = 0x00000010, VK_IMAGE_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF } VkImageCreateFlagBits; typedef VkFlags VkImageCreateFlags;

现在我们来了解一下各个标记的定义:

VK_IMAGE_CREATE_SPARSE_BINDING_BIT :图像使用稀疏内存绑定进行完全存储。

VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT :图像可以使用稀疏内存绑定进行部分存储。 为了使用此字段,图像必须具有 VK_IMAGE_CREATE_SPARSE_BINDING_BIT 标志。

VK_IMAGE_CREATE_SPARSE_ALIASED_BIT :在这种类型的标志中,图像存储在稀疏内存中;它也可以将相同图像的多个部分保存在相同的存储区域中。 必须使用 VK_IMAGE_CREATE_SPARSE_BINDING_BIT 标志创建图像。

VK_IMAGE_CREATE_MUTABLE_FORMAT_BIT :此格式在图像视图(VkImageView)格式与创建的图像对象格式本身(VkImage)不同的情况下非常有用。

VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT :这种格式用于立方体映射。 在这种情况下,VkImageView 必须是 VK_IMAGE_VIEW_TYPE_CUBE 或 VK_IMAGE_VIEW_TYPE_CUBE_ARRAY 类型。

销毁创建的图像

当图像不再需要时,可以使用 vkDestroyImage()销毁图像。 这是执行此操作的语法:

void vkDestroyImage( VkDevice device, VkImage image, const VkAllocationCallbacks* pAllocator);

该 API 接受三个参数,如下表所述:

device :这是要销毁图像的逻辑设备。

image :这是需要销毁的 VkImage 对象。

pAllocator :这个参数控制了主机内存释放的过程。 请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”部分。

理解图像布局

让我们来看看 Vulkan 规范中可用的各种图像布局。 它们由 VkImageLayout 枚举值表示,如以下列表中所述:

VK_IMAGE_LAYOUT_UNDEFINED:此布局不支持设备对访问。 这在图像变换过程中最适合用于初始化布局,比如 intialLayout 或 oldLayout。 这种过渡布局对它持有的内存数据不提供任何保证,这一点在实践中一定要注意,避免出现意外的结果。VK_IMAGE_LAYOUT_GENERAL:此布局支持所有类型的设备访问。VK_IMAGE_LAYOUT_PREINITIALIZED:此布局也不支持设备访问,最适合图像转换中的 intialLayout 或 oldLayout。 转换时会保留布局内存中持有的内容。 这种类型的布局在初始化时数据容易获得的情况下非常有用。 这样,数据就可以直接存储在设备内存中,无需额外的步骤执行布局转换。VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:此布局非常适合彩色图像。 因此,它只能用于 VkFrameBuffer 的颜色和解析附件。 为了使用此布局,图像必须将 usage 位设置为 VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT。

注意

图像的各种子资源没有单独指定的 usage 位 – 它们仅针对整个图像指定。

VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL:此布局只能用于 VkFrameBuffer 的深度、模板附件。 为了使用此布局,图像必须将 usage 位设置为 VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT。VK_IMAGE_LAYOUT_DEPTH_STENCIL_READ_ONLY_OPTIMAL:与 VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL 类似,只是它在着色器中用作只读 VkFrameBuffer 附件或只读图像,此处必须将其作为采样图像,组合图像、采样器或输入附件进行读取。 必须使用设置为 VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT 的 usage 位创建图像。VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:这个必须用作只读的着色器图像,例如采样图像,组合图像、采样器或输入附件。 必须使用设置为 VK_IMAGE_USAGE_SAMPLED_BIT 或 VK_IMAGE_USAGE_INPUT_ATTACHMENT_BIT 的 usage 位创建图像的子资源。VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL:这个必须将其用作传输命令(使用传输管线)的源图像,并且它的 usage 仅在图像子资源的 usage 位设置为 VK_IMAGE_USAGE_TRANSFER_SRC_BIT 时才有效。VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:这个必须用作传输命令(使用传输管线)的目标图像,并且它的使用仅在图像子资源的 usage 位设置为 VK_IMAGE_USAGE_TRANSFER_DST_BIT 时才有效。

创建图像视图

图像视图是使用 vkCreateImageView()创建的。 其语法如下所示:

VkResult vkCreateImageView( VkDevice device, const VkImageViewCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkImageView* pView);

下表介绍了 VkCommandPoolCreateInfo 的各个参数:

device :这是创建图像视图的逻辑设备的句柄。 pCreateInfo | 这是指向 VkCreateImageViewInfo 的指针;控制着 VkImageView 的创建。

pAllocator :这个控制着主机内存的分配过程。 有关更多信息,请参阅中第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“主机内存”部分。

pView:这个会返回创建的 VkImageView 对象的句柄。

VkCreateImageViewInfo 数据结构包含 vkCreateImageView()API 用来创建图像视图的视图特定的属性。 以下是每个字段的语法:

typedef struct VkImageViewCreateInfo { VkStructureType sType; const void* pNext; VkImageViewCreateFlags flags; VkImage image; VkImageViewType viewType; VkFormat format; VkComponentMapping components; VkImageSubresourceRange subresourceRange; } VkImageViewCreateInfo;

下表介绍了 VkImageViewCreateInfo 的各个字段:

sType :这是该结构的类型信息;它必须是 VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO。

pNext :这是一个指针,指向扩展特定的结构。 该参数也可以为 NULL。

flags : 该参数为 NULL; 被保留供将来使用。

image :这是 VkImage 的句柄。

viewType :这表示使用枚举 VkImageViewType 的图像视图类型。 它必须是以下任一标志值:VK_IMAGE_VIEW_TYPE_1D,VK_IMAGE_VIEW_TYPE_2D,VK_IMAGE_VIEW_TYPE_3D,VK_IMAGE_VIEW_TYPE_CUBE,VK_IMAGE_VIEW_TYPE_1D_ARRAY,VK_IMAGE_VIEW_TYPE-_2D_ARRAY 或 VK_IMAGE_VIEW_TYPE_CUBE_ARRAY。

format : 这指定了图像的格式(VkFormat)。

components:这个用于在颜色、深度、模板被转换成颜色分量后进行重新映射。

subresourceRange:这个用于选择 mipmap levels 以及 array layers 的范围,可通过视图对其进行访问。

销毁图像视图

使用 vkCreateImageView()API 销毁图像视图。 该 API 采用三个参数。 第一个参数(设备)指定负责销毁图像视图(imageView)的逻辑设备,具体的图像视图由第二个参数指示。 最后一个参数 pAllocator 控制主机内存的分配过程。 下面是它的语法:

void vkDestroyImageView( VkDevice device, VkImageView imageView, VkAllocationCallbacks* pAllocator);

内存分配以及绑定图像资源

当创建一个图像资源对象(VkImage)时,其中包含一种逻辑分配。 该图像在此时与设备内存还没有物理关联。 实际的存储空间支持会在后期单独提供。 物理分配是非常依赖于类型的;图像可以分为稀疏和非稀疏。 稀疏资源使用稀疏创建标志(VkImageCreateInfo 中的 VkImageCreateFlagBits)指定;但是,如果未指定该标志,则它就是一个非稀疏的图像资源。 本章仅将非稀疏内存作为参考。 有关稀疏资源分配的更多信息,请参阅官方的 Vulkan 1.0 规范。

图像与内存的关联的过程需要三个步骤:为图像的分配收集内存分配需求,在设备内存上分配物理块,将分配的内存绑定到图像资源。 我们来仔细看看。

收集内存分配需求

非稀疏图像资源的内存需求可以使用 vkGetImageMemoryRequirements()API 进行查询。 这里是它的语法:

void vkGetImageMemoryRequirements( VkDevice device, VkImage image, VkMemoryRequirements* pMemoryRequirements);

以下是 vkGetImageMemoryRequirements()API 的不同参数:

device :这是指拥有图像的设备。

image :这是指 VkImage 对象。

pMemoryRequirements | 这个是返回的 VkMemoryRequirements 结构对象。

VkMemoryRequirements 结构对象包含我们传递给 vkGetImageMemoryRequirements()的图像对象相关的内存需求。 它的语法如下所示:

typedef struct VkMemoryRequirements { VkDeviceSize size; VkDeviceSize alignment; uint32_t memoryTypeBits; } VkMemoryRequirements;

该结构的字段及其各自的描述如下:

size :这指定了所需的图像资源的尺寸(以字节为单位)。

alignment :这是指以字节为单位的对齐偏移量,用于指定资源所需的分配的存储空间内的偏移量。

memoryTypeBits :这是一个按位标志,指示图像资源支持的内存类型。 如果位被设置成 i,则意味着它将支持图像资源的 VkPhysicalDeviceMemoryProperties 结构中的内存类型为 i。

在设备上分配物理内存

物理内存使用 vkAllocateMemory()API 进行分配。 这个 API 在上一章讨论过。 有关此 API 的详细说明,请参阅第 5 章“Vulkan 中的命令缓冲区以及内存管理”中的“Vulkan 中的内存管理”下的“分配设备内存”小节。

把分配的内存绑定到图像对象

一旦从设备分配了物理内存,我们需要做的就是将此内存绑定到它自己的图像资源对象(VkImage)。 使用 vkBindImageMemory()API 将图像资源与分配的设备内存进行绑定。 代码如下:

VkResult vkBindBufferMemory( VkDevice device, VkBuffer buffer, VkDeviceMemor memory, VkDeviceSize memoryOffset);

这个 API 的参数描述如下:

device :这是拥有内存和图像对象的逻辑设备。

image :这是指我们需要绑定内存的 VkImage 对象。

memory :这是指分配的 VkDeviceMemory。

memoryOffset :这是以字节为单位的偏移量,指定图像将被绑定到的内存的起始点。

介绍交换链

交换链是一种机制,通过这种机制,就会在平台特定的展示窗口或者表面上显示渲染的绘图图元。 交换链可能包含一个或多个图形图像,这些绘图图像被称为彩色图像。 彩色图像只是一组像素信息,它们以特殊的布局驻留在内存中。 交换链中的绘制图像的数量是特定于具体实现的。 当使用双图像时,它被称为双缓冲,当使用三个表面时,就是三重缓冲。

在这些图像中,当一个图像在后台完成绘图过程时,它就会被交换到展示窗口。 为了充分利用 GPU,不同的图像会被作为绘图过程中的后台缓冲区。 这个过程来回反复进行,图像交换不断发生。 使用多个图像可以改善帧率的输出,因为 GPU 始终忙于图像的处理部分,从而减少整体的空闲时间。

绘图图像的交换或翻转行为取决于呈现模式 presentation mode; 这个操作可能会在垂直消隐间隔(场消隐期垂直回扫期 — 我们通常收看的电视图象是由电子枪发射的电子串高速轰击显象管上的荧光物质而产生的,电子串按从左至右,从上至下的方式扫描整个屏幕,因为速度十分快,所以我们的眼睛感觉不到,当电子枪的扫描位置从左上角达到右下角时,必须由右下角回到左上角,开始下一次扫描,从右下角回到左上角所花费的时间就是垂直回扫期)Vertical Blanking Interval(VBI)期间或绘图可用时立即更新。 这意味着显示器刷新时,后台图像与前台图像会发生交换,以此来显示新的图像。 交换链以 API 扩展的形式提供,需要使用 VK_KHR_SWAPCHAIN_EXTENSION_NAME 启用。 有关更多信息,请参阅“查询交换链扩展”部分。

理解交换链实现的流程

下图将为您介绍交换链实现从开始到结束的大体情况。 这会以一种非常简短的方式涵盖流程的每个部分,使您可以在整个实现过程中保持良好的过渡,我们会在接下来的章节详细介绍其中的内容:

让我们进入流程并快速了解其中的每个细节:

创建一个空窗口:该过程提供了一个空白的本地平台窗口,该窗口被连接到交换链的彩色图像。 每次写入帧(图像)时,都会将其交换到展示层。 展示层将这些信息传递到附加的本地窗口,用于显示目的。查询交换链扩展:交换链 API 不是标准 API 规范的一部分。 它们是特定于实现的,并且加载器也会以扩展的形式加载这些 API。 加载的扩展以函数指针的形式存储,其函数签名在 Vulkan 规范中是预先定义的。创建表面并将其与创建的窗口关联:这个过程会创建一个逻辑平台特定的表面对象。 这个时候,它还没有彩色图像的物理内存支持,即还没有分配存储空间,仅仅是逻辑层面的一种表示而已。 这个逻辑表面对象会被附加到空窗口,声明该窗口为其所有者。获取支持的图像格式:在这一步中,我们查询物理设备,以检查其支持的所有图像格式。查询交换链图像表面特性:这个过程会获得有关基本表面特性的信息,因为在创建交换链图像时需要用到这些信息。 另外,它还会检查可用的表现模式。管理表现模式信息:这将使用可用的表现模式信息并决定应该在交换链中使用的表现模式技术。 交换链的表现模式决定了传入的展示请求会如何在内部处理以及队列化。创建交换链并检索展示图像:让我们使用上面收集的信息并创建交换链图像。 WSI 扩展返回彩色图像对象;这些图像属于 VkImage 类型,并由应用程序使用。

提示

WSI 图像不属于应用程序,而是属于 WSI,因此无法应用图像布局。 图像布局只能应用于应用程序拥有的图像。

创建彩色图像视图 image views:根据系统特性,WSI 实现可能会返回 1-3 个交换链图像(基于一个缓冲区,双个缓冲区以及三个缓冲区)。 要在应用程序中使用每个图像,就需要创建相应的图像视图。创建深度图 depth image:与彩色图像类似,您需要深度图进行深度测试;但与 WSI 预烘烤的交换链图像不同,深度图需要由应用程序创建。首先,您需要创建一个深度图对象(VkImage),按照下面的步骤分配内存,创建图像布局并最终生成图像视图对象(VkImageView)。深度图的内存分配:您需要分配物理设备内存并将其绑定到深度图对象。创建命令池:我们需要命令缓冲区 —— 用于深度图,由于此图像属于我们;因此我们要使用命令缓冲区来应用图像布局。创建命令缓冲区:您需要创建命令缓冲区并开始使用创建的深度图对象记录负责创建深度图布局的命令。图像布局:这可以让您在深度图对象上应用深度、模板兼容的图像布局。添加管线屏障:为了确保在创建图像视图之前总是执行了图像布局,请添加管线屏障。 当插入管线屏障时,它确保在命令缓冲区中,屏障前面的命令会在屏障后面的命令之前执行。结束命令缓冲区的记录:这允许您停止命令缓冲区的记录。创建深度图视图:将图像对象转换为兼容图像布局后,创建一个图像视图对象(VkImageView)。 该图像不能直接在应用程序中使用;他们必须以图像视图的形式使用。

创建的彩色图像视图会被提交给图形队列,让展示引擎将其渲染到显示窗口中。 到本章的末尾,您就能显示空白的窗口了,因为到目前为止交换链彩色图像上没有渲染任何内容。

交换链实现的类框图

本节会简要介绍实现交换链的相关内容, 这有助于我们理解这些类的作用,因为我们正逐步落实到具体的实现。

注意

在本章中,我们将介绍三个用户自定义的新类:VulkanRenderer,VulkanSwapChain 和 VulkanPipeline。 这些类不与任何官方的 Vulkan 规范 API 或数据结构相关联。 这些是用户定义的,并能够帮助我们有组织的管理应用程序。

以下框图显示了这些模块类及其层次关系。 除此之外,此图形化的表示还会告诉您每个模块的责任。 在我们继续下一章时,我们还会介绍更多的类:

窗口管理自定义类

VulkanRender 类在 VulkanRenderer.h / .cpp 中定义。 在应用程序中,该类管理一个特殊的展示窗口及其相关资源,例如设备,交换链,管线等。 一个应用程序可以有多个渲染窗口,如下图所示;每个渲染器都会处理一个单独的展示窗口及其对应的所有资源。

但是,由于我们还处于初学水平,因此我们的示例仅假设只有一个输出展示窗口。 因此,VulkanApplication 包含一个 VulkanRenderer 类对象:

以下是 VulkanRenderer 头文件的声明。 该类创建了展示层的空窗口(createPresentationWindow()),稍后就会使用交换链中的的彩色图像对其进行填充。 空窗口的创建过程是极其特定于具体平台的;此示例演示的仅适用于 Windows 平台:

另外,VulkanRenderer 类管理初始化(initialize())以及渲染展示窗口(render())。 该类还管理各种命令缓冲区的命令池:

/************* VulkanRenderer.h *************/ class VulkanRenderer{ // Many line skipped in this header, please refer to // corresponding source with this chapter. public: void initialize(); //Simple life cycle // Create an empty window void createPresentationWindow(int& w, int& height); // Windows procedure method for handling events. static LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); void createCommandPool(); // Create command pool void createSwapChain(); // Create swapchain Color image void createDepthImage(); // Create Depth image public: HINSTANCE connection; // hInstance – Windows Instance char name[80]; // name – App name appearing on the window HWND window; // hWnd – the window handle // Data structure used for depth image struct{ VkFormat format; VkImage image; VkDeviceMemory mem; VkImageView view; }Depth; VkCommandBuffer cmdDepthImage; // Depth image command buffer VkCommandPool cmdPool; // Command pool int width, height; // Window Width and Height private: // Class managers VulkanSwapChain* swapChainObj; VulkanApplication* application; // The device object associated with this Presentation layer. VulkanDevice* deviceObj; }; VulkanRenderer::VulkanRenderer(VulkanApplication * app, VulkanDevice* deviceObject){ // Many lines skipped application = app; deviceObj = deviceObject; swapChainObj = new VulkanSwapChain(this); }

创建展示窗口

本部分将实现窗口系统,以便在显示窗口上呈现交换链的彩色图像。 本示例主要针对 Windows 系统,并使用 CreateWindowEx()API 创建具有扩展窗口样式的重叠弹出窗口或子窗口。 创建窗口很常见,讨论这个主题超出了本书的范围。

API 相关的更多信息,请参阅联机 MSDN 文档 (https://msdn.microsoft.com/en-us/library/windows/desktop/ms632680(v=vs.85).aspx):

void VulkanRenderer::createPresentationWindow(const int& windowWidth, const int& windowHeight){ width = windowWidth; height = windowHeight; WNDCLASSEX winInfo; // Initialize the window class structure: memset(&winInfo, 0, sizeof(WNDCLASSEX)); winInfo.cbSize = sizeof(WNDCLASSEX); winInfo.lpfnWndProc = WndProc; winInfo.hInstance = connection; winInfo.lpszClassName = name; // Register window class if (!RegisterClassEx(&winInfo)) { exit(1); } // Create window with the registered class RECT wr = { 0, 0, width, height }; AdjustWindowRect(&wr, WS_OVERLAPPEDWINDOW, FALSE); window = CreateWindowEx(0, name, name, WS_OVERLAPPEDWINDOW |WS_VISIBLE | WS_SYSMENU, 100, 100, wr.right – wr.left, wr.bottom – wr.top, NULL, NULL, connection, NULL); SetWindowLongPtr(window,GWLP_USERDATA,&application); }

在 Microsoft Windows 窗口系统中,我们需要一个 Windows 过程函数来处理事件的相关操作。 这里我们要处理 WM_PAINT、WM_SIZE 和 WM_CLOSE 事件。 当用户单击窗口上的关闭按钮时,会调用 WM_CLOSE 事件。 而 WM_PAINT 事件用于渲染窗口。 我们会在后面的章节中处理 WM_PAINT 和 WM_SIZE 消息:

// Windows procedure handlers for MS Windows LRESULT CALLBACK VulkanRenderer::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam){ VulkanApplication* appObj = VulkanApplication::GetInstance(); switch (uMsg){ case WM_CLOSE: PostQuitMessage(0); break; default: break; } return (DefWindowProc(hWnd, uMsg, wParam, lParam)); }

初始化渲染器

初始化操作会使用给定的尺寸创建一个展示窗口,并满足交换链实现的各种先决条件。 其中包括查询交换链扩展 API,创建表面对象,为展示层找到支持最佳的队列,获取用于绘制的兼容图像格式等等。 在我们继续阅读本章时,我们将详细讨论每个部分。 如以下代码所述,使用 initialize()函数完成 Renderer 类的初始化;请参阅以粗体突出显示的注释获得详细的描述:

void VulkanRenderer::initialize(){ // Create an empty window with dimension 500×500 createPresentationWindow(500, 500); // Initialize swapchain swapChainObj->intializeSwapChain(); // We need command buffers, so create a command buffer pool createCommandPool(); // Lets create the swapchain color images buildSwapChainAndDepthImage(); }

创建命令池

VulkanRenderer 类包含命令池,用于命令缓冲区的分配以及各种操作,例如创建深度图,管线状态的设置,绘制图元等等。 本章将实际演示一下命令缓冲区的相关操作,我们还会创建深度图。

在以下代码中,命令池是使用 VkCommandPoolCreateInfo 控制结构中指定的属性创建的。 该控制结构包含需要分配命令缓冲区的图形队列的索引。 Vulkan 提前在控制结构中指定这些信息,使底层管线充分利用并进行预优化。 有关命令缓冲区和命令池的更多信息,请参阅第 5 章“Vulkan 中命令缓冲区以及内存管理”中的“了解命令池以及缓冲区 API”部分:

void VulkanRenderer::createCommandPool(){ VulkanDevice* deviceObj = application->deviceObj; VkCommandPoolCreateInfo cmdPoolInfo = {}; cmdPoolInfo.sType=VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO; cmdPoolInfo.queueFamilyIndex = deviceObj-> graphicsQueueWithPresentIndex; vkCreateCommandPool(deviceObj->device, &cmdPoolInfo, NULL, &cmdPool); }

构建交换链以及深度图

buildSwapChainAndDepthImage()函数是创建交换链和深度图的入口点。 我们将继续详细介绍这些内部函数:

void VulkanRenderer::buildSwapChainAndDepthImage(){ // Get the appropriate queue to submit the command into deviceObj->getDeviceQueue(); // Create swapchain and get the color images swapChainObj->createSwapChain(cmdDepthImage); // Create the depth image createDepthImage(); }

渲染展示窗口

在显示屏上绘制展示窗口并处理 Windows 消息。 当用户按下关闭按钮时,退出展示窗口并中断无限的渲染循环:

void VulkanRenderer::render(){ MSG msg; // message while (1) { PeekMessage(&msg, NULL, 0, 0, PM_REMOVE); if (msg.message == WM_QUIT) { break; // If Quit message the exit the render loop } TranslateMessage(&msg); DispatchMessage(&msg); RedrawWindow(window, NULL, NULL, RDW_INTERNALPAINT); } }

交换链管理器

交换链在 VulkanSwapChain.h / .cpp 中的 VulkanSwapChain 类中实现。 该类管理交换链的整个生命周期,从其初始化、创建一直到销毁(公有函数)。 生命周期包括几个较小的中间阶段,例如查询 API 扩展,获取适当的图像格式,获取表面特性和展示模式等(私有函数)。

以下是 VulkanSwapChain 类的头文件声明,我们会讨论所有重要的函数:

class VulkanSwapChain{ // Many line are skipped, please refer to source code public: void intializeSwapChain(); void createSwapChain(const VkCommandBuffer& cmd); void destroySwapChain(); private: VkResult createSwapChainExtensions(); void getSupportedFormats(); VkResult createSurface(); uint32_t getGraphicsQueueWithPresentationSupport(); void getSurfaceCapabilitiesAndPresentMode(); void managePresentMode(); void createSwapChainColorBufferImages(); void createColorImageView(const VkCommandBuffer& cmd); public: // User define structure containing public variables used // by the swapchain private and public functions. SwapChainPublicVariables scPublicVars; private: // User define structure containing private variables used // by the swapchain private and public functions. SwapChainPrivateVariables scPrivateVars; VulkanRenderer* rendererObj; VulkanApplication* appObj; };

除此之外,该类还有两个用户定义的结构,即 SwapChainPrivateVariables 和 SwapChainPublicVariables。 其中包括类的私有和公有成员变量:

struct SwapChainPrivateVariables { // Store the image surface capabilities VkSurfaceCapabilitiesKHR surfCapabilities; // Stores the number of present modes uint32_t presentModeCount; // Arrays for retrived present modes std::vector<VkPresentModeKHR> presentModes; // Size of the swapchain color images VkExtent2D swapChainExtent; // Number of color images supported uint32_t desiredNumberOfSwapChainImages; VkSurfaceTransformFlagBitsKHR preTransform; // Stores present mode bitwise flag for swapchain creation VkPresentModeKHR swapchainPresentMode; // The retrived drawing color swapchain images std::vector<VkImage> swapchainImages; std::vector<VkSurfaceFormatKHR> surfFormats; }; struct SwapChainPublicVariables { // The logical platform dependent surface object VkSurfaceKHR surface; // Number of buffer image used for swapchain uint32_t swapchainImageCount; // Swapchain object VkSwapchainKHR swapChain; // List of color swapchain images std::vector<SwapChainBuffer> colorBuffer; // Current drawing surface index in use uint32_t currentColorBuffer; // Format of the image VkFormat format; };

查询交换链扩展

交换链实现需要一些 API, 这些 API 不是 Vulkan SDK 的一部分,并且需要动态使用。 这些 API 是非常特定于具体平台实现的,并且以 API 扩展的形式提供,这些 API 扩展可以动态查询,并以函数指针的形式存储。 通过创建 VkDevice 对象时指定 VK_KHR_SWAPCHAIN_EXTENSION_NAME 设备扩展名,就可以成功查询此 API 扩展:

std::vector<const char *> deviceExtensionNames = { VK_KHR_SWAPCHAIN_EXTENSION_NAME }; // Look into VulkanDevice::createDevice() for more info VkDeviceCreateInfo deviceInfo = {}; deviceInfo.ppEnabledExtensionNames = deviceExtensionNames; vkCreateDevice(*gpu, &deviceInfo, NULL, &device);

以下代码片段构成了一个宏,从而帮助我们查询实例和设备特定的扩展。 我们使用这个宏来获取 WSI 扩展 API 的函数指针。 有关实例级和设备级扩展的更多信息,请参阅第 3 章“与设备握手”中的“介绍层和扩展”部分:

#define INSTANCE_FUNC_PTR(instance, entrypoint){ fp##entrypoint = (PFN_vk##entrypoint) vkGetInstanceProcAddr (instance, “vk”#entrypoint); if (fp##entrypoint == NULL) { exit(-1); } #define DEVICE_FUNC_PTR(dev, entrypoint){ fp##entrypoint = (PFN_vk##entrypoint)vkGetDeviceProcAddr (dev, “vk”#entrypoint); if (fp##entrypoint == NULL) { exit(-1); }

以下是实例级和设备级的扩展名称:

std::vector<const char *> instanceExtensionNames = { VK_KHR_SURFACE_EXTENSION_NAME, VK_KHR_WIN32_SURFACE_EXTENSION_NAME, VK_EXT_DEBUG_REPORT_EXTENSION_NAME, }; std::vector<const char *> deviceExtensionNames = { VK_KHR_SWAPCHAIN_EXTENSION_NAME };

该宏使用 vkGetInstanceProcAddr()和 vkGetDeviceProcAddr()来获取实例级和设备级扩展。 有关 vkGetInstanceProcAddr()API 的更多信息,请参阅第 4 章“Vulkan 中的调试”中的“在 Vulkan 中实现调试”部分。

以下代码使用用户自定义的 createSwapChainExtensions()函数查询实例级和设备级扩展:

VkResult VulkanSwapChain::createSwapChainExtensions(){ // Dependency on createPresentationWindow() VkInstance& instance = appObj->instanceObj.instance; VkDevice& device = appObj->deviceObj->device; // Get Instance based swapchain extension function pointer INSTANCE_FUNC_PTR(instce,GetPhysicalDeviceSurfaceSupportKHR); INSTANCE_FUNC_PTR(instance, GetPhysicalDeviceSurfaceCapabilitiesKHR); INSTANCE_FUNC_PTR(instance, GetPhysicalDeviceSurfaceFormatsKHR); INSTANCE_FUNC_PTR(instance, GetPhysicalDeviceSurfacePresentModesKHR); INSTANCE_FUNC_PTR(instance, DestroySurfaceKHR); // Get Device based swapchain extension function pointer DEVICE_FUNC_PTR(device, CreateSwapchainKHR); DEVICE_FUNC_PTR(device, DestroySwapchainKHR); DEVICE_FUNC_PTR(device, GetSwapchainImagesKHR); DEVICE_FUNC_PTR(device, AcquireNextImageKHR); DEVICE_FUNC_PTR(device, QueuePresentKHR); }

下表显示了实现和管理交换链所需的扩展 API。 这些扩展都属于实例级和设备级,必须对它们进行查询。 在我们浏览各种函数实现时,我们会详细讨论它们的扩展:

Instance 实例级别:

对于 vkGetPhysicalDeviceSurfaceSupportKHR, 函数指针为 fpGetPhysicalDeviceSurfaceSupportKHR。

对于 vkGetPhysicalDeviceSurfaceCapabilitiesKHR,函数指针为 fpGetPhysicalDeviceSurfaceCapabilitiesKHR。

对于 vkGetPhysicalDeviceSurface 对于 matsKHR,函数指针为 fpGetPhysicalDeviceSurfaceFormatsKHR。

对于 vkGetPhysicalDeviceSurfacePresentModesKHR,函数指针为 fpGetPhysicalDeviceSurfacePresentModesKHR。

对于 vkDestroySurfaceKHR,函数指针为 fpDestroySurfaceKHR。

Device 设备级:

对于 vkCreateSwapchainKHR,函数指针为 fpGetPhysical。

对于 vkDestroySwapchainKHR,函数指针为 fpDestroySwapchainKHR。

对于 vkGetSwapchainImagesKHR,函数指针为 fpGetSwapchainImagesKHR。

对于 vkAcquireNextImageKHR,函数指针为 fpAcquireNextImageKHR。

对于 vkQueuePresentKHR, 函数指针为 fpQueuePresentKHR。

检索到的 API 扩展存储在用户定义的函数指针变量中。 通过用 fp 替换前缀 vk 的方式对它们进行了重命名。 例如,API 扩展 vkGetPhysicalDeviceSurfaceCapabilitiesKHR()存储为 fpGetPhysicalDeviceSurfaceCapabilitiesKHR()。 对于其他 API 扩展也是如此。

使用 WSI 创建表面并将其与创建的窗口相关联

Vulkan API 可以在每个平台上无缝实现。 平台是窗口和 OS 服务的抽象, 例如 MS Windows,Android 和 Wayland。 窗口系统集成Window System Integration (WSI)为实现窗口或表面管理提供了一种独立于平台的方式。

Vulkan 使用 VkSurfaceKHR 表示逻辑表面对象。 不同的平台有不同的 API 来创建 VkSurfaceKHR 表面对象,例如 vkCreateWin32SurfaceKHR(),vkCreateAndroidSurfaceKHR()和 vkCreateXcbSurfaceKHR():

这个例子将重点介绍 Windows 平台:

VkResult vkCreateWin32SurfaceKHR( VkInstance instance, const VkWin32SurfaceCreateInfoKHR* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkSurfaceKHR* surface);

以下是对每个参数的描述:

instance :这是指与表面关联的 VkInstance 对象。

pCreateInfo :这指的是 VkWin32SurfaceCreateInfoKHR 结构对象,用来控制表面管理。 以下信息框中提供了更多信息。

pAllocator :这用于控制主机特定的内存分配过程。

surface :这指的是返回创建的表面对象的指针。

注意

vkCreate SurfaceKHR 结构对象是在逻辑表面上创建的, 它还没有物理内存的支持,并没有进行实际的物理存储分配。

API 把 VkWin32SurfaceCreateInfo 结构作为输入参数,其中可以指定表面管理相关的各种控制属性。 以下是 API 的语法:

typedef struct VkWin32SurfaceCreateInfoKHR { VkStructureType type; const void* next; VkWin32SurfaceCreateFlagsKHR flags; HINSTANCE hinstance; HWND hwnd; } VkWin32SurfaceCreateInfoKHR;

我们来看看控制结构的所有参数:

type :这是这个结构的类型信息,它必须是 VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR。 next | 这是 NULL 或特定于扩展的指针。

flag :该字段保留供将来使用。

hInstance :这是创建窗口的实例 ID。

hwnd :这是创建窗口的句柄;hwnd 和 hInstance 用于把表面与展示窗口关联起来。

VulkanSwapChain 类使用 createSurface()方法实现逻辑 WSI 表面的创建:

// Depends on createPresentationWindow(), need window handle VkResult VulkanSwapChain::createSurface(){ VkInstance& instance = appObj->instanceObj.instance; // Construct the surface description: VkWin32SurfaceCreateInfoKHR createInfo = {}; createInfo.sType = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR; createInfo.pNext = NULL; createInfo.hinstance = rendererObj->connection; createInfo.hwnd = rendererObj->window; // Create the VkSurfaceKHR object return result = vkCreateWin32SurfaceKHR(instance, &createInfo, NULL, &scPublicVars.surface); }

使用当前支持的图形队列

当交换链彩色图像由绘图图元命令进行绘制时,会把它们提交给展示引擎从而显示在屏幕上。 该请求以命令缓冲区的形式提交到队列中,队列可以获得展示请求并对其进行处理。 因此,我们需要一个图形队列,它不仅能够接受绘图命令缓冲区,而且还支持展示。 这可以通过查询物理设备上可用的每个图形队列并检查它们是否支持展示属性来完成。 下面的代码可以帮助我们实现相同的功能,我们实现了一个辅助函数 getGraphicsQueueWithPresentationSupport(),最终获取了支持图形和展示命令缓冲区请求的一个队列:

uint32_t VulkanSwapChain::getGraphicsQueueWithPresentationSupport(){ VulkanDevice* device = appObj->deviceObj; uint32_t queueCount = device->queueCount; VkPhysicalDevice gpu = *device->gpu; vector<VkQueueFamilyProperties>& queueProps = device->queueProps; // Iterate each queue and get presentation status for each. VkBool32* supportsPresent = (VkBool32 *)malloc(queueCount * sizeof(VkBool32)); for (uint32_t i = 0; i < queueCount; i++) { fpGetPhysicalDeviceSurfaceSupportKHR(gpu, i, scPublicVars.surface, &supportsPresent[i]); } // Search for a graphics queues that supports presentation uint32_t graphicsQueueNodeIndex = UINT32_MAX; uint32_t presentQueueNodeIndex = UINT32_MAX; for (uint32_t i = 0; i < queueCount; i++) { if ((queueProps[i].queueFlags & VK_QUEUE_GRAPHICS_BIT) != 0) { if (graphicsQueueNodeIndex == UINT32_MAX) { graphicsQueueNodeIndex = i; } if (supportsPresent[i] == VK_TRUE) { graphicsQueueNodeIndex = i; presentQueueNodeIndex = i; break; } } } if (presentQueueNodeIndex == UINT32_MAX) { // If didnt find a queue that supports both graphics // and present, then find a separate present queue. for (uint32_t i = 0; i < queueCount; ++i) { if (supportsPresent[i] == VK_TRUE) { presentQueueNodeIndex = i; break; } } } free(supportsPresent); // Generate error if could not find queue with present queue if (graphicsQueueNodeIndex == UINT32_MAX || presentQueueNodeIndex == UINT32_MAX) {return UINT32_MAX;} return graphicsQueueNodeIndex; }

查询交换链图像格式

交换链需要一种表面支持的色彩空间格式。 所有支持的格式都可以使用 vkGetPhysicalDeviceSurfaceFormatsKHR()API 检索。 这是它的语法:

VkResult vkGetPhysicalDeviceSurfaceFormatsKHR( VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, uint32_t* pSurfaceFormatCount, VkSurfaceFormatKHR* pSurfaceFormats);

下表介绍了此 API 的各个参数:

physicalDevice :这是指与交换链关联的逻辑设备。

surface :这是指为交换链创建的逻辑表面。

pSurfaceFormatCount :这是一个输入、输出参数。 当把 surfaceFormats 设置为 NULL 调用 vKGetPhysicalDeviceSurfaceFormatKHR 时,它会返回支持的表面格式的数量。 否则,它用于根据表面指针的数量检索表面。

pSurfaceFormats :这是用来检索支持的表面格式。

以下实现获取支持的图像格式的数量。 利用 formatCount,表面格式会被检索到 VkSurfaceFormatKHR 数组中。 如果没有找到喜欢的表面格式信息,我们就把表面格式视为 32 位 RGBA:

void VulkanSwapChain::getSupportedFormats() { VkPhysicalDevice gpu = *rendererObj->getDevice()->gpu; VkResult result; // Get the number of VkFormats supported: uint32_t formatCount; fpGetPhysicalDeviceSurfaceFormatsKHR (gpu, scPublicVars.surface, &formatCount, NULL); scPrivateVars.surfFormats.clear(); scPrivateVars.surfFormats.resize(formatCount); // Get VkFormats in allocated objects result = fpGetPhysicalDeviceSurfaceFormatsKHR(gpu, scPublicVars.surface, &formatCount, &scPrivateVars.surfFormats[0]); // In case its a VK_FORMAT_UNDEFINED, then surface has no // preferred format. We use RGBA 32 bit format if (formatCount == 1 && surfFormats[0].format == VK_FORMAT_UNDEFINED) { scPublicVars.format = VK_FORMAT_B8G8R8A8_UNORM; } else }

创建交换链

在接下来的小节中,我们将学习交换链的实现。 这包括查询表面特性和展示模式,检索彩色图像以及创建图像视图。

交换链表面特性以及展示模式

交换链的创建过程需要您知道两件事才能创建图像表面:表面特性和展示模式:

表面特性:这指定了物理设备提供的图像表面的特性。 vkGetPhysicalDeviceSurfaceCapabilitiesKHR()API 扩展可用于查询这些特性。 该扩展存储在用户定义的函数指针 fpGetPhysicalDeviceSurface-CapabilitiesKHR()中。 其语法如下:
VkResult vkGetPhysicalDeviceSurfaceCapabilitiesKHR( VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, VkSurfaceCapabilitiesKHR* surfaceCapabilities);

公开的特性在 VkSurfaceCapabilitiesKHR 对象中检索。 这里包括有用的信息,例如支持的图像表面的最小、最大数量,图像尺寸范围,可能的图像阵列的最大数量,表面支持的转换功能的类型(例如旋转或镜像旋转 90,180 和 270 度)等等。

展示模式:交换链可以具有各种类型的展示模式,并且可以使用 vkGetPhysicalDeviceSurfacePresentModesKHR()API 扩展动态地检索。 检索到的信息通过支持四种显示模式的 VkPresentModeKHR 枚举来表示(有关模式的更多信息,请参阅下一节)。 presentModelCount 包含显示模式的数量。 这个 API 的语法如下:
VkResult vkGetPhysicalDeviceSurfacePresentModesKHR( VkPhysicalDevice physicalDevice, VkSurfaceKHR surface, uint32_t* presentModeCount, VkPresentModeKHR* presentModes);

以下代码会检索表面特性以及展示模式,并将所需信息存储在私有类成员变量中:

void VulkanSwapChain::getSurfaceCapabilitiesAndPresentMode(){ // Some lines are skipped, please refer to the source code VkPhysicalDevice gpu = *appObj->deviceObj->gpu; fpGetPhysicalDeviceSurfaceCapabilitiesKHR(gpu, scPublicVars. surface, &scPrivateVars.surfCapabilities); fpGetPhysicalDeviceSurfacePresentModesKHR(gpu, scPublicVars. surface, &scPrivateVars.presentModeCount, NULL); scPrivateVars.presentModes.clear(); scPrivateVars.presentModes.resize (scPrivateVars.presentModeCount); assert(scPrivateVars.presentModes.size()>=1); result = fpGetPhysicalDeviceSurfacePresentModesKHR (gpu, scPublicVars.surface, &scPrivateVars.presentModeCount, &scPrivateVars.presentModes[0]); fpGetPhysicalDeviceSurfacePresentModesKHR(gpu, scPublicVars. surface, &scPrivateVars.presentModeCount, scPrivateVars.presentModes); if(scPrivateVars.surfCapabilities.currentExtent.width == (uint32_t)-1){ // If the surface width and height is not defined, } else{ // then set it equal to image size. scPrivateVars.swapChainExtent.width = rendererObj->width; scPrivateVars.swapChainExtent.height = rendererObj->height; // If the surface size is defined, then it must // match the swapchain size scPrivateVars.swapChainExtent = scPrivateVars. surfCapabilities.currentExtent; } }

管理展示模式

交换链包含的彩色图像由展示引擎使用展示模式方案进行管理。 这些方案用来确定传入的展示请求将会如何在内部进行处理以及队列化。 VkPresentModeKHR()API 支持四种类型的展示模式:

VK_PRESENT_MODE_IMMEDIATE_KHR :该模式立即渲染展示请求而不等待垂直消隐。 展示请求不需要内部队列管理。 该模式非常容易发生图像撕裂。

VK_PRESENT_MODE_MAILBOX_KHR :这里,展示请求在一个入口队列中排队,而且它们还会等待下一个垂直消隐信号,该信号将会允许展示引擎更新图像。 此模式不会造成撕裂效果。 队列满时,最新的展示请求就会替换先前的展示请求。 在垂直消隐的情况下,弹出一个队列请求并进行处理。 任何与先前入口队列相关联的图像都可供应用程序重新使用。

VK_PRESENT_MODE_FIFO_KHR:这里,展示请求在一个入口队列中排队,并且它们等待下一次的垂直消隐来更新当前图像,其中前面的图像会被移除并处理(因此是 FIFO)。 在这里不会看到撕裂现象。 一个新的请求会被添加到队列的末尾并从头开始删除。

VK_PRESENT_MODE_FIFO_RELAXED_KHR :这里,展示引擎通常在垂直消隐期间更新当前图像。 如果自上次更新当前图像以来已经经过了垂直消隐期,则展示引擎不会等待随后的垂直消隐期来 push 要处理的下一个展示图像, 紧接着是下一张图片进行更新。 该模式可能会导致可见的撕裂。 新的请求被附加到队列的末尾,并且从队列的开始移除一个请求,并且在队列不为空的每个垂直消隐期之间或之后处理该请求。

以下代码实现了展示模式方案。 首先它检查 MAILBOX 方案(非撕裂)是否可用。 如果这种模式不可用,那么 IMMEDIATE 模式是首选。 默认的回退机制是 FIFO:

void VulkanSwapChain::managePresentMode() { // MAILBOX – lowest- latency non- tearing mode.If not,try // IMMEDIATE, the fastest (but tears). Else, fall back to FIFO. scPrivateVars.swapchainPresentMode = VK_PRESENT_MODE_FIFO_KHR; for (size_t i = 0; i < scPrivateVars.presentModeCount; i++) { if(scPrivateVars.presentModes[i]==VK_PRESENT_MODE_MAILBOX_KHR){ scPrivateVars.swapchainPresentMode = VK_PRESENT_MODE_MAILBOX_KHR; break; } if(scPrivateVars.swapchainPresentMode!=VK_PRESENT_MODE_MAILBOX_KHR &&scPrivateVars.presentModes[i]== VK_PRESENT_MODE_IMMEDIATE_KHR){ scPrivateVars.swapchainPresentMode=VK_PRESENT_MODE_IMMEDIATE_KHR; } } // Determine the number of VkImages to use in the swapchain scPrivateVars.desiredNumberOfSwapChainImages = scPrivateVars. surfCapabilities.minImageCount + 1; if ((scPrivateVars.surfCapabilities.maxImageCount > 0) && (scPrivateVars.desiredNumberOfSwapChainImages > scPrivateVars.surfCapabilities.maxImageCount)) { // Application must settle for fewer images than desired: scPrivateVars.desiredNumberOfSwapChainImages = scPrivateVars.surfCapabilities.maxImageCount; } if(scPrivateVars.surfCapabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) { scPrivateVars.preTransform=VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR; } else { scPrivateVars.preTransform= scPrivateVars.surfCapabilities.currentTransform; } }

注意

在真正的应用程序中,建议将 PRESENT_MODE_FIFO_RELAXED_KHR 作为展示模式,因为它只在应用程序未命中时才会出现画面撕裂,但在应用程序足够快时不会发生撕裂现象。

检索交换链的彩色图像

交换链的彩色图像可以通过使用 vkCreateSwapchainKHR()API 扩展函数指针来创建 VkSwapchainKHR 交换链对象进行检索。 执行此操作的语法如下所示:

VkResult vkCreateSwapchainKHR( VkDevice device, const VkSwapchainCreateInfoKHR* createInfo, const VkAllocationCallbacks* allocator, VkSwapchainKHR* swapchain);

该 API 接受 VKSwapChainCreateInforKHR 控制结构。 该结构包含了控制交换链对象(在 VkSwapchainKHR 结构中检索到的)创建相关的必要信息,。 这是它的语法:

typedef struct VkSwapchainCreateInfoKHR { VkStructureType type; const void* next; VkSwapchainCreateFlagsKHR flags; VkSurfaceKHR surface; uint32_t minImageCount; VkFormat imageFormat; VkColorSpaceKHR imageColorSpace; VkExtent2D imageExtent; uint32_t imageArrayLayers; VkImageUsageFlags imageUsage; VkSharingMode imageSharingMode; uint32_t queueFamilyIndexCount; const uint32_t* queueFamilyIndices; VkSurfaceTransformFlagBitsKHR preTransform; VkCompositeAlphaFlagBitsKHR compositeAlpha; VkPresentModeKHR presentMode; VkBool32 clipped; VkSwapchainKHR oldSwapchain; } VkSwapchainCreateInfoKHR;

VkSwapchainCreateInfoKHR 结构具有以下字段:

type :这指定了结构的类型。 它必须是 VK_-STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR。

next :这个是 NULL 或一个指向扩展特定的结构的指针。

flags :这必须是零。 该字段保留供将来使用。

surface :这指的是把交换链图像呈现到其上的表面。

minImageCount:这是指应用程序实现交换链机制所需的、可展示图像的最小数量。

imageFormat :这是用于交换链彩色图像的格式。

imageColorSpace :这表示交换链支持的色彩空间(VkColorSpaceKHR)。 imageExtent :这是指交换链的图像大小或以像素指定的尺寸。

imageArrayLayers :这表示多视图表面或者立体表面中的视图数量。

imageUsage :这是 VkImageUsageFlagBits 位字段,指示应用程序如何使用交换链的可展示图像。

imageSharingMode :这是用于交换链图像的共享模式。

queueFamilyIndexCount | 指的是如果 imageSharingMode 设置为 VK_SHARING_MODE_CONCURRENT,有权访问交换链图像的、队列族的数量。

queueFamilyIndices:这指的是如果 imageSharingMode 设置为 VK_SHARING_MODE_CONCURRENT,有权访问交换链图像的队列族的一个索引数组。

preTransform :这是 VkSurfaceTransformFlag-BitsKHR 的一个位字段,描述了相对于呈现引擎的自然方向的变换,该变换在呈现其内容之前会先应用于图像内容。

compositeAlpha :这是 VkCompositeAlphaFlagBitsKHR 位字段,用于指示在某些窗口系统上,当把此表面与其他表面合并在一起时要使用的 Alpha 合成模式。

presentMode :这表明是否允许 Vulkan 实现,放弃影响不可见表面区域的渲染操作。

oldSwapchain :这个字段是非空的,它指定正在创建的新交换链所要替换掉的旧交换链。 一旦在使用旧的非空交换链调用 vkCreateSwapchainKHR,展示引擎拥有的任何图像以及当前未显示的所有图像都会被立即释放,而当前正在被显示的图像一旦不在需要显示就会进行释放。 即使创建新的交换链失败,也可能发生这种情况。 应用程序必须销毁旧的交换链以释放与旧交换链相关的所有内存,包括应用程序当前拥有的任何可呈现的图像。 在这么做之前,它必须先等待任何未完成的渲染完成,然后再渲染已成功提交到展示层的可呈现图像(如下所述),但它们(这些图像?)仍不归展示引擎所有。

在下面的代码中,createSwapChainColorBufferImage()函数使用 vkCreateSwapchainKHR(fpCreateSwapchainKHR())的函数指针创建了交换链。 成功创建交换链后,VkImage 对象 — 图像表面就会在幕后创建。 在获取图像表面之前,我们分配了足够的内存空间来容纳图像缓冲区。 交换链图像和物理表面的数量由 vkGetSwapchainImagesKHR()API 扩展的函数指针返回,即(fpGetSwapchainImagesKHR())。

首先使用所需的字段值填充 VkSwapchainCreateInfoKHR 控制结构,例如图像的格式,大小,展示模式,色彩空间等。 fpCreateSwapchainKHR()API 函数指针用该信息创建交换链。成功创建 VkSwapchainKHR 交换链对象(swapchainImages)后,使用它并通过 vkGetSwapchainImagesKHR()API 获取图像。 这个 API 会被调用两次:当第一次把最后一个参数设置为 NULL 进行调用时,它会检索交换链中存在的图像数量(swapchainImageCount)。swapchainImageCount 用于分配足够的内存来保存表面图像数组(VkImage *)。 当第二次调用该 API 时,则会在分配的名为 swapchainImages 的 VkImage 数组对象中检索图像,如以下 API 说明中所述:
VkResult vkGetSwapchainImagesKHR( VkDevice device, VkSwapchainKHR swapchain, uint32_t* swapchainImageCount, VkImage* swapchainImages);

device :这是与交换链相关联的逻辑设备。

swapchain :这指的是 VkSwapChainKHR 对象。

swapchainImageCount :这是指交换链包含的图像的数量。

swapchainImages:这指的是检索交换链的 VkImage 数组对象。

以下代码显示了交换链对象创建过程的实现以及彩色图像的检索。 彩色图像用于存储每个对应像素的颜色信息:

void VulkanSwapChain::createSwapChainColorBufferImages() { VkSwapchainCreateInfoKHR scInfo = {}; scInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR; scInfo.pNext = NULL; scInfo.surface = scPublicVars.surface; scInfo.minImageCount = scPrivateVars.desiredNumberOfSwapChainImages; scInfo.imageFormat = scPublicVars.format; scInfo.imageExtent.width = scPrivateVars.swapChainExtent.width; scInfo.imageExtent.height = scPrivateVars.swapChainExtent.height; scInfo.preTransform = scPrivateVars.preTransform; scInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR; scInfo.imageArrayLayers = 1; scInfo.presentMode = scPrivateVars.swapchainPresentMode; scInfo.oldSwapchain = VK_NULL_HANDLE; scInfo.clipped = true; scInfo.imageColorSpace = VK_COLOR_SPACE_SRGB_NONLINEAR_KHR; scInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; scInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE; scInfo.queueFamilyIndexCount = 0; scInfo.pQueueFamilyIndices = NULL; // Create the swapchain object fpCreateSwapchainKHR(rendererObj->getDevice()->device, &swapChainInfo, NULL, &scPublicVars.swapChain); // Get the number of images the swapchain has fpGetSwapchainImagesKHR(rendererObj->getDevice()->device, scPublicVars.swapChain, &scPublicVars.swapchainImageCount, NULL); scPrivateVars.swapchainImages.clear(); // Make array of swapchain image to retrieve the images scPrivateVars.swapchainImages.resize (scPublicVars.swapchainImageCount); assert(scPrivateVars.swapchainImages.size() >= 1); // Retrieve the swapchain image surfaces fpGetSwapchainImagesKHR(rendererObj->getDevice()->device, scPublicVars.swapChain, &scPublicVars.swapchainImageCount, scPrivateVars.swapchainImages); }

创建彩色图像视图

正如本章开始所讨论的,应用程序不会直接以图像对象(VkImage)的形式使用图像;相反,而是使用图像视图(VkImageView)。 在本节中,我们将学习如何使用图像对象创建图像视图。

我们的应用程序在 createColorImageView()函数中实现了图像视图的创建过程。 图像视图是使用 vkCreateImageView()API 创建的。 此 API 接受一些重要参数,例如图像视图格式,mipmap 级别,级别数量,数组层数量等。 有关图像视图 API 的更多信息,请参阅本章的“理解图像资源”部分下的“创建图像视图”子部分。

在接下来的实现中,对于每个交换链图像对象(VkImage),我们通过遍历 scPublicVars.swapchainImageCount 中可用的图像对象列表来创建相应的图像视图。 如前一节所述,使用 fpGetSwapchainImagesKHR()API 检索此计数。 然后将创建的图像视图推回到 vector 列表中,稍后将会用它们来引用正确的前端、后端缓冲图像:

void VulkanSwapChain::createColorImageView(const VkCommandBuffer& cmd){ VkResult result; for(uint32_t i = 0; i < scPublicVars.swapchainImageCount; i++){ SwapChainBuffer sc_buffer; VkImageViewCreateInfo imgViewInfo = {}; imgViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; imgViewInfo.pNext = NULL; imgViewInfo.format = scPublicVars.format; imgViewInfo.components.r = VK_COMPONENT_SWIZZLE_R; imgViewInfo.components.g = VK_COMPONENT_SWIZZLE_G; imgViewInfo.components.b = VK_COMPONENT_SWIZZLE_B; imgViewInfo.components.a = VK_COMPONENT_SWIZZLE_A; imgViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; imgViewInfo.subresourceRange.baseMipLevel = 0; imgViewInfo.subresourceRange.levelCount = 1; imgViewInfo.subresourceRange.baseArrayLayer = 0; imgViewInfo.subresourceRange.layerCount = 1; imgViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; imgViewInfo.flags = 0; sc_buffer.image = scPrivateVars.swapchainImages[i]; // Since the swapchain is not owned by us we cannot set // the image layout. Upon setting, the implementation // may give error, the images layout were // created by the WSI implementation not by us. imgViewInfo.image = sc_buffer.image; result = vkCreateImageView(rendererObj->getDevice()->device, &imgViewInfo, NULL, &sc_buffer.view); scPublicVars.colorBuffer.push_back(sc_buffer); } scPublicVars.currentColorBuffer = 0; }

创建深度图

深度图表面在 3D 图形应用中起着重要作用。 它使用深度测试在渲染场景中带来了纵深感。 在深度测试中,每个片段的深度都存储在称为深度图的特殊缓冲区中。 与存储颜色信息的彩色图像不同,深度图存储来自相机视角的图元相应片段的深度信息。 深度图的尺寸通常与彩色图像相同。 不是一条硬性规则,但通常来说,深度图会将深度信息存储为 16,24 或 32 位浮点值。

注意

深度图的创建与彩色图像不同。 您必须注意到,在检索交换链图像时,我们没有使用 vkCreateImage()API 来获取彩色图像对象。 这些图像直接从 fpGetSwapchainImagesKHR()扩展 API 返回。 在本节中,我们将通过分步的过程创建深度图。

引入平铺

图像数据存储在连续类型的内存中,并映射为以线性方式存储的 2D 图像内存。 在线性排列中,纹理像素排列在连续的逐行内存位置中,如下图所示:

间距通常表示图像的宽度,通常而言,这个值可能比为了满足对齐要求而添加的填充字节要多。 给定纹理元素的位置偏移量可以使用其行位置和列位置以及给定的间距来计算,如前图所示。

只要在沿“行”的地方访问纹素就不需要相邻的纹理信息,这种线性布局就是整齐紧凑的。 但是,一般来说,许多应用程序要求图像信息读取多行数据。 当图像尺寸较大时,线性布局中的行间距长度会增加并延伸到多行。 在多重缓存级别的系统中,这会导致性能下降,这是由于转化后备缓冲区translation lookaside buffer(TLB)和缓存未命中导致的地址转换较慢。

在大多数 GPU 上,这种较慢的地址转换是通过使用 Z 字格(swizzling)的形式存储纹理元素来解决的。 这种存储图像纹理元素的方式称为优化平铺 Optimal tiling,其中图像的纹理元素以平铺的方式存储,表示连续内存块中的多个列和行。 例如,在下图中,用不同颜色表示四个图块,其中每个图块具有 2 x 2 行(间距)和列:

显然,以线性方式,相同颜色的块被其他块间隔开;然而,在优化的布局中,相同颜色的块保持在一起,提供了一种更有效的方法来访问相邻的纹素,而不会导致性能损失。 请注意,这种最佳平铺的例子只是模仿原理的工作方式;在引擎内部,存在高度复杂的 swizzling 算法,有助于实现最佳平铺。

在 Vulkan 中,平铺由 VkImageTiling 定义,它表示线性平铺(VK_IMAGE_TILING_LINEAR)和最佳平铺(VK_IMAGE_TILING_OPTIMAL)。 以下是此语法:

typedef enum VkImageTiling { VK_IMAGE_TILING_OPTIMAL = 0, VK_IMAGE_TILING_LINEAR = 1, } VkImageTiling;

我们来看看平铺类型及其相应的定义:

VK_IMAGE_TILING_OPTIMAL :这些都是不透明的平铺,并通过以实现相关的布置来排列纹理元素,从而提供对底层内存的最佳访问。

VK_IMAGE_TILING_LINEAR :正如名字所理解的那样,这里的纹理元素以线性方式按行排列。 一致性可能会导致每行中出现一些填充。

创建深度缓冲区图像对象

使用 16 字节浮点值初始化深度格式,并查询由 deviceObj-> gpu 指定的物理设备支持的格式属性。 检索到的属性用于为内存中的图像选择最佳平铺 /swizzling (VK_IMAGE_TILING_OPTIMAL)布局。

与深度相关的成员变量被打包在 Renderer 类中称为 Depth 的用户定义结构中。 下面是演示代码:

struct{ VkFormat format; VkImage image; VkDeviceMemory mem; VkImageView view; } Depth;

下表中定义了该结构的各个字段:

format :这是指深度图格式,即 VkFormat。

image :这是指 VkImage 深度图对象。

mem :这是与深度图对象关联的已分配的内存。

view :这是深度图对象(VkImage)的 VkImageView 对象。

深度格式,平铺信息和其他参数(如图像大小和图像类型)用于创建 VkImageCreateInfo 控制结构。 由于我们正在创建深度缓冲区,因此我们需要在相同结构的 usage 字段中将其指定为 VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT。 利用该结构,并通过 vkCreateImage()API 创建 VkImage 图像对象。 有关 VkImageCreateInfo 和 vkCreateImage()的更多信息,请参阅本章中的“理解图像资源”部分的“创建图像”子部分:

VkResult result; VkImageCreateInfo imageInfo = {}; // If the depth format is undefined, // use fall back as 16- byte value if (Depth.format == VK_FORMAT_UNDEFINED) { Depth.format = VK_FORMAT_D16_UNORM; } const VkFormat depthFormat = Depth.format; VkFormatProperties props; vkGetPhysicalDeviceFormatProperties(*deviceObj->gpu, depthFormat, &props); if (props.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) { imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL; } else if (props.linearTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) { imageInfo.tiling = VK_IMAGE_TILING_LINEAR; } else { } std::cout << “Unsupported Depth Format, try other Depth formats.\n”; exit(-1); imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; imageInfo.pNext = NULL; imageInfo.imageType = VK_IMAGE_TYPE_2D; imageInfo.format = depthFormat; imageInfo.extent.width = width; imageInfo.extent.height = height; imageInfo.extent.depth = 1; imageInfo.mipLevels = 1; imageInfo.arrayLayers = 1; imageInfo.samples = NUM_SAMPLES; imageInfo.queueFamilyIndexCount= 0; imageInfo.pQueueFamilyIndices = NULL; imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; imageInfo.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT; imageInfo.flags = 0; // User create image info and create the image objects result = vkCreateImage(deviceObj->device, &imageInfo, NULL, &Depth.image); assert(result == VK_SUCCESS);

获得深度图的内存需求

使用 vkGetImageMemoryRequirements()API 查询缓冲区的图像内存需求。 这会检索用于分配深度图对象的物理内存支持所需的总大小。 有关 API 使用的更多信息,请参阅本章中的“收集内存分配要求”小节:

// Get the image memory requirements VkMemoryRequirements memRqrmnt; vkGetImageMemoryRequirements (deviceObj->device, Depth.image, &memRqrmnt);

检测内存类型

使用查询到的内存需求 memRqrmnt 中的 memoryTypeBits 字段,并使用 VulkanDevice :: memoryTypeFromProperties()确定适合分配深度图内存的内存类型:

VkMemoryAllocateInfo memAlloc = {}; memAlloc.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; memAlloc.pNext = NULL; memAlloc.allocationSize = 0; memAlloc.memoryTypeIndex = 0; memAlloc.allocationSize = memRqrmnt.size; bool pass; // Determine the type of memory required // with memory properties pass = deviceObj->memoryTypeFromProperties(memRqrmnt. memoryTypeBits, 0, &memAlloc.memoryTypeIndex); assert(pass);

VulkanDevice :: memoryTypeFromProperties()函数有三个参数作为输入。 第一个(typeBits)表示内存类型,第二个参数(requirementsMask)指定特定内存类型的用户需求,最后一个(typeIndex)返回内存索引句柄。

该函数会迭代并检查请求的内存类型是否存在。 接下来,它检查找到的内存是否满足用户需求。 如果成功,它将返回布尔值 true 和内存类型的索引;失败时,它返回布尔值 false:

bool VulkanDevice::memoryTypeFromProperties(uint32_t typeBits, VkFlags requirementsMask, uint32_t *typeIndex) { // Search memtypes to find first index with those properties for (uint32_t i = 0; i < 32; i++) { if ((typeBits & 1) == 1) { // Type is available, does it match user properties? if ((memoryProperties.memoryTypes[i].propertyFlags & requirementsMask) == requirementsMask) { *typeIndex = i; return true; } } typeBits >>= 1; } // No memory types matched, return failure return false; }

分配物理内存并绑定到深度图

内存要求指导应用程序为深度图分配指定数量的内存。 一旦使用 vkAllocateMemory()成功分配了内存,就需要把它绑定到深度图(Depth.image),使图像成为所分配内存的所有者:

// Allocate the physical backing for the depth image result = vkAllocateMemory(deviceObj->device, &memAlloc, NULL, &Depth.mem); assert(result == VK_SUCCESS); // Bind the allocated memory to the depth image result = vkBindImageMemory(deviceObj->device, Depth.image, Depth.mem, 0); assert(result == VK_SUCCESS);

图像布局转换

支持最佳布局的 GPU 硬件需要能够从最佳布局转换到线性布局,反之亦然。 用于读取和写入目的的消费者组件不能直接访问最佳布局。 最佳布局的不透明特性需要进行布局转换,即将一种类型(旧类型)布局转换为另一种类型(新类型)的过程。

注意

CPU 可以将图像数据存储在线性布局的缓冲区中,然后将其转换为最佳布局,以便 GPU 以更高效的方式对其进行读取。

支持最佳布局的 GPU 硬件允许您通过布局转换以线性或最佳布局对数据进行存储。 布局转换的过程可以使用内存屏障。 内存屏障检查指定的旧图像布局和新图像布局并执行布局转换。 每个布局转换可能不需要在 GPU 上触发实际的布局转换操作。 例如,当一个图像对象第一次被创建时,它可能具有未定义的初始布局;在这种情况下,GPU 可能只需要以最佳模式访问内存。 有关内存屏障的更多信息,请继续下一节。

使用内存屏障进行图像布局转换

内存屏障是帮助同步数据读取和写入的指令, 它保证在内存屏障之前和之后指定的操作会被同步。 当插入此指令时,它会确保在这个指令之前发出的内存操作比屏障指令之后发出的内存指令先执行并完成操作。

有三种类型的内存障碍:

全局内存障碍:这种类型的内存障碍适用于可执行的各种内存对象,并适用于各自对应的内存访问类型。 全局内存障碍由 VkMemoryBrier 结构的实例来表示。缓冲区内存障碍:此内存障碍类型适用于指定缓冲区对象的特定范围,并适用于其各自对应的内存访问类型。 这些内存障碍由 VkBufferMemoryBarrier 结构的实例来表示。图像内存障碍:这种图像内存障碍由 VkImageMemoryBarrier 实例表示,并且适用于多种不同的内存访问类型 —– 通过指定的图像对象的一个特定图像的子资源范围。

分配的图像内存需要根据其使用情况进行布局。 考虑到其用法的特性,图像布局有助于以特定于具体实现的方式访问内存的内容。 有一种可用于一般图像的通用布局,但这可能不是适合的布局(VK_IMAGE_LAYOUT_GENERAL)。 在 Vulkan 中,图像布局使用 VkImageLayout 表示。 以下是为这个枚举定义的字段:

VK_IMAGE_LAYOUT_UNDEFINED | 此布局及其子范围内的图像内容处于未定义状态,并且图像在创建后立即处于此状态。

VK_IMAGE_LAYOUT_GENERAL :该布局允许对图像或其子范围进行所有操作,通过用法标志(VkImageUsageFlag)指定。 VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL :此布局中的图像只能与帧缓冲区颜色附件一起使用。 它可以通过帧缓冲区 color 附件读取访问,并可以使用绘图命令写入。

VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL :此布局中的图像只能与帧缓冲区 深度 / 模板附件一起使用。 它可以通过帧缓冲区颜色附件读取访问,并可以使用绘图命令写入。

VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL :此布局使用图像作为只读的着色器资源。 因此只能通过采样图像描述符,组合图像采样器描述符或只读存储图像描述符(VkDescriptorType)完成着色器的读取来对其进行访问。

VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL : 此布局中的图像(或其子范围)只能用作命令 vkCmdCopyImage,vkCmdBlitImage,vkCmdCopyImageToBuffer 和 vkCmdResolveImage 的源操作数。

VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL :此布局中的图像(或其子范围)只能用作命令 vkCmdCopyImage,vkCmdBlitImage,vkCmdCopyBufferToImage,vkCmdResolveImage,vkCmdClearColorImage 和 vkCmdClearDepthStencilImage 的目标操作数。

通过特定的内存屏障(memory barriers)应用在图像中的布局称为 VkImageMemoryBarrier。 内存障碍是在 vkCmdPipelineBarrier()API 的帮助下插入的。 这个 API 的语法如下:

void vkCmdPipelineBarrier( VkCommandBuffer commandBuffer, VkPipelineStageFlags srcStageMask, VkPipelineStageFlags dstStageMask, VkDependencyFlags dependencyFlags, uint32_t memoryBarrierCount, const VkMemoryBarrier* pMemoryBarriers, uint32_t bufferMemoryBarrierCount, const VkBufferMemoryBarrier* pBufferMemoryBarriers, uint32_t imageMemoryBarrierCount, const VkImageMemoryBarrier* pImageMemoryBarriers);

我们来看看所有字段的规范约定:

commandBuffer :这是在其中指定内存屏障的命令缓冲区。

srcStageMask :这是用来指定管线阶段的位掩码字段,在实施屏障之前必须完成管线阶段的执行。

dstStageMask:这是用来指定管线阶段的位掩码字段,在屏障完成之前不应该开始管线阶段的执行。

dependencyFlags :这指的是 VkDependencyFlagBits 值,用来指示屏障是否具有屏幕空间局部性的。

memoryBarrierCount :这是指内存屏障的数量。

pMemoryBarriers :这是 VkBufferMemoryBarreir 对象数组,其元素个数等于 memoryBarrierCount。

bufferMemoryBarrierCount : 这是指缓冲区内存屏障的数量。

pBufferMemoryBarrier :这是指元素个数等于 bufferMemoryBarrierCount 的 VkMemoryBarreir 对象数组。

imageMemoryBarrierCount :这是指图像类型内存屏障的数量。

pImageMemoryBarriers :这指的是元素个数等于 imageMemoryBarrierCount 的 VkImageMemoryBarrier 对象数组。

以下代码使用了一个图像屏障并在 VkImageMemoryBarrier 控制结构(imgMemoryBarrier)中设置适当的图像布局信息。 此控制结构传递给 vkCmdPipelineBarrier()API,用来设置操作的执行并应用内存屏障。 通过把 VkImageMemoryBarrier 的 newLayout 字段指定为 VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL,将创建的深度图(Depth.image)设置为帧缓冲深度、模板附件布局 framebuffer depth/stencil attachment layout。

使用创建的命令池,分配 cmdDepthImage 命令缓冲区。 如此处所述,该命令缓冲区将用于记录图像布局的转换:

/****** void VulkanRenderer::createDepthImage()******/ // Use command buffer to create the depth image. This includes – // Command buffer allocation, recording with begin/end // scope and submission. CommandBufferMgr::allocCommandBuffer(&deviceObj->device, cmdPool, &cmdDepthImage); CommandBufferMgr::beginCommandBuffer(cmdDepthImage); { // Set the image layout to depth stencil optimal setImageLayout(Depth.image, VK_IMAGE_ASPECT_DEPTH_BIT, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL, (VkAccessFlagBits)0, cmdDepthImage); } CommandBufferMgr::endCommandBuffer(cmdDepthImage); CommandBufferMgr::submitCommandBuffer(deviceObj->queue, &cmdDepthImage);

图像布局使用 setImageLayout()函数设置。 这是使用 vkCmdPipelineBarrier()命令记录内存障碍的辅助函数。

这个命令被记录在 cmdDepthImage 命令缓冲区中,并保证在允许依赖资源访问它之前,会满足合理图像布局的要求。

setImageLayout()辅助函数将现有的旧图像布局格式转换为指定的新布局类型。 在本示例中,旧图像布局被指定为 VK_IMAGE_LAYOUT_UNDEFINED,因为图像对象是第一次创建的,并且没有应用预定义的布局。 由于我们正在实现深度 / 模板测试的图像布局,因此必须使用 VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL 用法类型来设置新的预期图像布局:

void VulkanRenderer::setImageLayout(VkImage image, VkImageAspectFlags aspectMask, VkImageLayout oldImageLayout, VkImageLayout newImageLayout, VkAccessFlagBits srcAccessMask, const VkCommandBuffer& cmd){ // Dependency on cmd assert(cmd != VK_NULL_HANDLE); // The deviceObj->queue must be initialized assert(deviceObj->queue != VK_NULL_HANDLE); VkImageMemoryBarrier imgMemoryBarrier = {}; imgMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; imgMemoryBarrier.pNext = NULL; imgMemoryBarrier.srcAccessMask = srcAccessMask; imgMemoryBarrier.dstAccessMask = 0; imgMemoryBarrier.oldLayout = oldImageLayout; imgMemoryBarrier.newLayout = newImageLayout; imgMemoryBarrier.image = image; imgMemoryBarrier.subresourceRange.aspectMask = aspectMask; imgMemoryBarrier.subresourceRange.baseMipLevel = 0; imgMemoryBarrier.subresourceRange.levelCount = 1; imgMemoryBarrier.subresourceRange.layerCount = 1; if (oldImageLayout == VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL) { imgMemoryBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; } switch (newImageLayout) { // Ensure that anything that was copying from this image // has completed. An image in this layout can only be // used as the destination operand of the commands case VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: case VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: imgMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; break; // Ensure any Copy or CPU writes to image are flushed. An image // in this layout can only be used as a read- only shader resource case VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: imgMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; imgMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT; break; // An image in this layout can only be used as a // framebuffer color attachment case VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL: imgMemoryBarrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_READ_BIT; break; // An image in this layout can only be used as a // framebuffer depth/stencil attachment case VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL: imgMemoryBarrier.dstAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT; break; } VkPipelineStageFlags srcStages= VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; VkPipelineStageFlags destStages = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT; vkCmdPipelineBarrier(cmd, srcStages, destStages, 0, 0, NULL, 0, NULL, 1, &imgMemoryBarrier); }

创建图像视图

最后,我们将通过图像视图的方式让应用程序使用深度图。 我们非常清楚,图像不能直接在 Vulkan 应用程序中使用, 它们以图像视图的形式使用。 以下代码使用 vkCreateImageView()API 创建图像视图。 有关 API 的更多信息,请参阅本章中“理解图像资源”部分下的“创建图像视图”子部分:

/****** void VulkanRenderer::createDepthImage()******/ VkImageViewCreateInfo imgViewInfo = {}; imgViewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO; imgViewInfo.pNext = NULL; imgViewInfo.image = VK_NULL_HANDLE; imgViewInfo.format = depthFormat; imgViewInfo.components = { VK_COMPONENT_SWIZZLE_IDENTITY }; imgViewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT; imgViewInfo.subresourceRange.baseMipLevel = 0; imgViewInfo.subresourceRange.levelCount = 1; imgViewInfo.subresourceRange.baseArrayLayer = 0; imgViewInfo.subresourceRange.layerCount = 1; imgViewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D; imgViewInfo.flags = 0; if ( depthFormat == VK_FORMAT_D16_UNORM_S8_UINT || depthFormat == VK_FORMAT_D24_UNORM_S8_UINT || depthFormat == VK_FORMAT_D32_SFLOAT_S8_UINT) { imgViewInfo.subresourceRange.aspectMask |= VK_IMAGE_ASPECT_STENCIL_BIT; } // Create the image view and allow the application to // use the images. imgViewInfo.image = Depth.image; result = vkCreateImageView(deviceObj->device, &imgViewInfo, NULL, &Depth.view); assert(result == VK_SUCCESS);

总结应用流程

在本节中,我们将汇总交换链创建以及构建展示窗口的过程,由两部分组成:初始化和渲染。

初始化

初始化过程包括初始化、创建以及处理交换链。 交换链尚未连接到帧缓冲区的渲染过程和图元。 因此,现在渲染的输出会是一个空白的展示窗口。

首先,VulkanRenderer 初始化展示窗口并创建本机平台特定的空窗口(500 x 500)。 窗口会渲染交换链的前端缓冲区绘图图像。 接下来,它会初始化交换链,以符合交换链的先决条件。 交换链图像视图布局是使用命令缓冲区(它们是从预分配的命令缓冲池分配的)创建的。

在交换链初始化期间,查询 WSI 扩展并以函数指针的形式进行存储。逻辑交换链表面对象被创建并且与展示窗口相关联。接下来,从支持展示功能的逻辑设备查询一个合适的图形队列;此队列用于绘制操作并将交换链图像呈现到显示输出窗口。最后,还会检查设备可以用于交换链图像的、所有可能的图像格式。交换链的创建包括查询指定表面配置的交换链表面特性,例如绘图表面的最大尺寸,可用的显示模式等。使用这个表面配置,就可以检索交换链图像对象。一旦交换链检索到图像,就可以用来渲染图元。交换链图像由 WSI 检索;因此,应用程序不拥有这些。这些图像最终转换为图像视图以允许应用程序在实现中使用它们。

我们还需要为深度 / 模板测试创建深度图,与交换链图像不同,深度图完全是应用程序的责任,应用程序拥有它,因此可以使用一个最佳深度布局方案在深度图上应用图像布局转换。 使用内存屏障命令应用图像转换,该命令会打包在命令缓冲区中并提交到队列以进行前期的处理。 内存屏障会插入一些特殊指令,以保证布局在使用之前完成转换。

渲染 – 显示输出窗口

以下代码渲染展示窗口:

void VulkanRenderer::render(){ MSG msg; // message while (1) { PeekMessage(&msg, NULL, 0, 0, PM_REMOVE); if (msg.message == WM_QUIT) { break; // If Quit message the exit the render loop } TranslateMessage(&msg); DispatchMessage(&msg); // Display the window RedrawWindow(window, NULL, NULL, RDW_INTERNALPAINT); } }

以下是上述代码实现的输出效果:

总结

本章详细介绍了图像资源。 我们首先对 Vulkan 中的图像资源进行了基本的了解,并学习了图像对象,图像布局以及图像视图。 然后我们创建了图像对象并为它们分配了设备内存。 我们还使用 WSI 扩展来实现交换链并检索交换链图像;然后这些图像与展示窗口相关联。 最后,我们从交换链图像中创建了图像视图。

在本章后续的内容中,我们会实现深度缓冲区图像。 我们还会知道不同的 Vulkan 图像拼图方式以及它们之间的基本区别。 除此之外,我们还会了解使用内存屏障的图像布局及其实现。

在下一章中,我们会介绍帧缓冲区和渲染通道。 帧缓冲区使用交换链和深度图的图像视图,并将它们与颜色 color 和深度 depth 附件相关联。 然后这个信息被渲染通道用来定义一个工作单元。 我们还将了解缓冲区资源并使用它创建几何图形缓冲区。 除此之外,我们还会看看 SPIR-V,以便学习 Vulkan 中的着色器编程。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Proudly powered by WordPress | Theme: HoneyWaves by SpiceThemes