关注专栏 写文章
第 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 创建图像资源的步骤说明:

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


理解图像资源

本节将介绍用于创建图像资源的 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 启用。 有关更多信息,请参阅“查询交换链扩展”部分。

理解交换链实现的流程

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

<img src="https://pic1.zhimg.com/v2-e357d62b1cbf535fbc280073f40a7ac4_b.jpg" data-rawwidth="1021" data-rawheight="678" data-size="normal" data-caption="" class="origin_image zh-lightbox-thumb" width="1021" data-original="https://pic1.zhimg.com/v2-e357d62b1cbf535fbc280073f40a7ac4_r.jpg"/>


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

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

提示

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

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

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

交换链实现的类框图

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

注意

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

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

<img src="https://pic4.zhimg.com/v2-6e40e53e2a97bbf116174e220fdd1277_b.jpg" data-rawwidth="1609" data-rawheight="646" data-size="normal" data-caption="" class="origin_image zh-lightbox-thumb" width="1609" data-original="https://pic4.zhimg.com/v2-6e40e53e2a97bbf116174e220fdd1277_r.jpg"/>


窗口管理自定义类

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

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

<img src="https://pic2.zhimg.com/v2-0fd36ef0fa63b4bad08c0381b6812a35_b.jpg" data-rawwidth="651" data-rawheight="359" data-size="normal" data-caption="" class="origin_image zh-lightbox-thumb" width="651" data-original="https://pic2.zhimg.com/v2-0fd36ef0fa63b4bad08c0381b6812a35_r.jpg"/>


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

<img src="https://pic3.zhimg.com/v2-c6204c91f0e48b299035b5c3438ce556_b.jpg" data-rawwidth="776" data-rawheight="384" data-size="normal" data-caption="" class="origin_image zh-lightbox-thumb" width="776" data-original="https://pic3.zhimg.com/v2-c6204c91f0e48b299035b5c3438ce556_r.jpg"/>


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

/************* VulkanRenderer.h *************/
class VulkanRenderer{
// Many line skipped in this header, please refer to