SwallowJoe的博客

Be a real go-getter,
NEVER SETTLE!

0%

Vulkan入门(14)-VkImage图像的创建

简述

在之前我们使用顶点着色器以及描述符来实现绘制有颜色的几何体,还实现了旋转动画。接下来我们学习一下纹理贴图,这个将是我们实现加载绘制基本3D模型的基础。

添加纹理的基本步骤有:

  1. 创建由设备内存支持的图像对象
  2. 用图像文件中的像素填充创建的图像对象
  3. 创建图像采样器
  4. 添加一个组合的图像采样器描述符来从纹理中采样颜色

我们以前已经使用过图像对象,但是这些对象是由swap chain扩展自动创建的。这次需要手动创建,创建图像并填充数据类似于创建顶点缓冲区。我们将通过创建一个暂存资源和填充它与像素数据,然后我们复制这到我们将用于渲染的最终图像对象。

可以创建一个暂存图像,不过Vulkan允许将像素从VkBuffer复制到image中,而且这个API在某些硬件上实际上更快。我们将首先创建这个缓冲区并填充像素值,然后我们将创建一个图像复制像素到。创建image与创建缓冲区并没有太大的不同。它包括查询内存需求、分配设备内存并绑定它,就像我们之前看到的那样。

图像可以有不同的布局,影响像素在内存中的存储方式。例如,由于图形硬件的工作方式,简单地逐行存储像素可能不会带来最好的性能。当对图像执行任何操作时,确保它们具有在该操作中使用的最佳布局。比如指定渲染通道时其中一些布局有:

  1. VK_IMAGE_LAYOUT_PRESENT_SRC_KHR: 适合呈现(present)
  2. VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL:片段着色器中写入颜色的最佳附件
  3. VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL: 作为传输操作的最佳源,如vkCmdCopyImageToBuffer
  4. VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: 作为传输操作的最佳目的地,如vkCmdCopyBufferToImage
  5. VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: 适合着色器采样

转换图像布局的最常见方法之一是管道屏障(pipeline barrier)。管道屏障主要用于同步对资源的访问,例如确保在读取图像之前将其写入。后面我们将了解如何将管道壁垒用于转换布局。

使用VK_SHARING_MODE_EXCLUSIVE时,可以另外使用屏障来转移队列系列的所有权。

一. 纹理贴图

1.1 图像库

有许多库可用于加载图像,您甚至可以编写自己的代码来加载BMP和PPM等简单格式。 这里我们将使用stb集合中的stb_image库。 这样做的好处是所有代码都在一个文件中,因此不需要任何棘手的构建配置。 下载stb_image.h并将其存储在方便的位置,例如保存GLFW和GLM的目录。 将位置添加到您的包含路径。

stb_image库地址: https://github.com/nothings/stb

下载后解压,放在指定目录,然后修改我们的Makefile文件:

1
2
3
4
VULKAN_SDK_PATH = /home/jh/Program/vulkan/1.2.170.0/x86_64
STB_IMAGE_PATH = /home/jh/Program/stb-image

CFLAGS = -std=c++17 -I$(VULKAN_SDK_PATH)/include -I$(STB_IMAGE_PATH)

1.1 读取图片

在shaders目录旁边创建一个新的目录textures来存储纹理图像:

texture

首先添加头文件:

1
2
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

默认情况下,头文件只定义函数的原型。一个代码文件需要包含STB_IMAGE_IMPLEMENTATION定义的头文件来包含函数体,否则会有链接错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void initVulkan() {
...
createCommandPool();
// 因为需要使用指令缓冲,所以在创建指令池之后调用
createTextureImage();
createVertexBuffer();
...
}

void createTextureImage() {
int texWidth, texHeight, texChannels;
// 加载texture.jpg图像
stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
// 每个像素4个字节
VkDeviceSize imageSize = texWidth * texHeight * 4;
if (!pixels) {
throw std::runtime_error("failed to load texture image!");
}
}

stbi_load函数将文件路径和要加载的通道数量作为参数。STBI_rgb_alpha值会强制为图像加载Alpha通道,即使它没有通道也是如此, 与其他纹理保持一致性。中间的三个参数是输出图像中通道的宽度、高度和实际数量。返回的指针是像素值数组中的第一个元素。在STBI_rgba_alpha中,像素逐行排列,每个像素4个字节,总共texWidth * texHeight * 4个值。

1.2 缓存读取的图片

现在,我们将在主机可见内存中创建一个缓冲区,以便我们可以使用vkMapMemory并将像素复制到其中。 将此临时缓冲区的变量添加到createTextureImage函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void createTextureImage() {
int texWidth, texHeight, texChannels;
// 加载texture.jpg图像
stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
// 每个像素4个字节
VkDeviceSize imageSize = texWidth * texHeight * 4;
if (!pixels) {
throw std::runtime_error("failed to load texture image!");
}

VkBuffer stagingBuffer;
VkDeviceMemory stagingBufferMemory;
// 缓冲区应该在主机可见内存中,以便我们可以映射它,并且它应该可用作传输源,以便我们以后可以复制
createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer,
stagingBufferMemory);
// 内存映射
void* data;
vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data);
memcpy(data, pixels, static_cast<size_t>(imageSize));
vkUnmapMemory(device, stagingBufferMemory);
// 最后释放原始像素数据
stbi_image_free(pixels);
}

1.3 纹理图像(Texture Image)

尽管我们可以设置着色器来访问缓冲区中的像素值,但为此目的最好使用Vulkan中的图像对象-VkImage。 通过使用2D坐标,图像对象将使检索颜色更加容易和快捷。 图像对象中的像素称为纹理像素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
VkImage textureImage;
VkDeviceMemory textureImageMemory;

void createTextureImage() {
...
VkImageCreateInfo imageInfo = {};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D; //二维图像
imageInfo.extent.width = static_cast<uint32_t>(texWidth);
imageInfo.extent.height = static_cast<uint32_t>(texHeight);
imageInfo.extent.depth = 1;
// 图像的最小采样的细节级别
imageInfo.mipLevels = 1;
// 图像中的层数
imageInfo.arrayLayers = 1;
// 指定图像格式,对于像素像素,使用与缓冲区中像素相同的格式,否则复制操作将失败
imageInfo.format = VK_FORMAT_R8G8B8A8_UNORM;
// 图像平铺模式,这里指定图像像素最佳内存拼接布局
// 与图像的布局不同,平铺模式不能在以后更改。如果希望能够直接访问图像内存中的texel,则必须使用VK_IMAGE_TILING_OPTIMAL
imageInfo.tiling = VK_IMAGE_TILING_OPTIMAL;

// 图像的initialLayout只有两个可能的值:VK_IMAGE_LAYOUT_UNDEFINED || VK_IMAGE_LAYOUT_PREINITIALIZED
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;

imageInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
// 图像将仅由一个队列族使用, 因此独占模式
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
// 图像采样
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.flags = 0; // Optional
// 创建图像
if (vkCreateImage(device, &imageInfo, nullptr, &textureImage) != VK_SUCCESS) {
throw std::runtime_error("failed to create image!");
}

// 同样的,需要给Image分配内存空间
VkMemoryRequirements memRequirements;
vkGetImageMemoryRequirements(device, textureImage, &memRequirements);
VkMemoryAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex =
findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
if (vkAllocateMemory(device, &allocInfo, nullptr, &textureImageMemory) != VK_SUCCESS) {
throw std::runtime_error("failed to allocate image memory!");
}
// 绑定图像和内存
vkBindImageMemory(device, textureImage, textureImageMemory, 0);
}

对于initialLayout,很少有情况需要在第一次过渡期间保留纹理像素,但是如果想将图像与VK_IMAGE_TILING_LINEAR布局结合使用作为缓存图像。 在这种情况下,将纹理像素数据上传到其中,然后将图像转换为传输源而又不丢失数据。但是,我们首先将图像转换为传输目标,然后从缓冲区对象将纹理像素数据复制到该图像,因此使用VK_IMAGE_LAYOUT_UNDEFINED。

对于usage, 与缓冲区创建期间的含义相同。 该图像将用作缓冲区副本的目的地,因此应将其设置为传输目的地。 我们还希望能够从着色器访问图像来为网格着色,因此用法应包括VK_IMAGE_USAGE_SAMPLED_BIT。

采样标志与多重采样有关。 这仅与将用作附件的图像有关,这里使用一个样本。 对于与稀疏图像有关的图像,有一些可选的标志。 稀疏图像是其中实际上仅某些区域由内存支持的图像。 例如,如果将3D纹理用于体素地形,则可以使用它来避免分配内存来存储大量的“空”值,这里我们设置为0。

1.3.1 VkImageCreateInfo

创建图像的一系列参数是在VkImageCreateInfo中指明的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
  1. sType是此结构的类型,VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO
  2. pNext是NULL或指向扩展特定结构的指针
  3. flag是VkImageCreateFlagBits的位掩码,用于描述图像的其他参数
  4. imageType是VkImageType值,用于指定图像的基本尺寸。就图像类型而言,阵列纹理中的图层不算作尺寸
    1. VK_IMAGE_TYPE_1D指定一维图像
    2. VK_IMAGE_TYPE_2D指定二维图像
    3. VK_IMAGE_TYPE_3D指定三维图像
  5. format是一种VkFormat,它描述了将包含在图像中的texel块的格式和类型
  6. extent是一个VkExtent3D,它描述基本级别的每个维度中的数据元素数量
  7. mipLevels描述可用于图像的最小采样的细节级别的数量
  8. arrayLayers是图像中的层数
  9. samples是VkSampleCountFlagBits,用于指定每个纹理像素的样本数
  10. tiling是一个VkImageTiling值,它指定内存中纹理元素块的平铺模式
    1. VK_IMAGE_TILING_LINEAR: 以主要行顺序排列像素
    2. VK_IMAGE_TILING_OPTIMAL: 指定最佳平铺(纹理像素以实现相关的安排进行布局,以实现更好的内存访问)
    3. VK_IMAGE_TILING_DRM_FORMAT_MODIFIER_EXT: 表示图片的拼贴是由Linux DRM格式修饰符定义的
  11. usage是VkImageUsageFlagBits的位掩码,用于描述图像的预期用法
  12. SharingMode是VkSharingMode值,用于指定多个队列系列将访问图像时的图像共享模式
  13. queueFamilyIndexCount是pQueueFamilyIndi​​ces数组中的条目数
  14. pQueueFamilyIndi​​ces是将访问此图像的队列系列的列表(如果sharedMode不是VK_SHARING_MODE_CONCURRENT,则将被忽略)
  15. initialLayout是一个VkImageLayout值,它指定图像的所有图像子资源的初始VkImageLayout。请参阅图像布局
    1. VK_IMAGE_LAYOUT_UNDEFINED: GPU不可用,第一次转换将丢弃纹理像素
    2. VK_IMAGE_LAYOUT_PREINITIALIZED:GPU无法使用,但第一个过渡将保留纹理像素

1.3.2 vkCreateImage

图像表示多维(最多3个)数据数组,可用于各种目的(例如附件、纹理),通过描述符集将其绑定到图形或计算管道,或直接将其指定为特定命令的参数。

1
2
3
4
5
VkResult vkCreateImage(
VkDevice device,
const VkImageCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkImage* pImage);
  1. device是创建Image的逻辑设备
  2. pCreateInfo是指向VkImageCreateInfo结构的指针,该结构包含用于创建图像的参数
  3. pAllocator如“内存分配”一章中所述控制主机内存分配
  4. pImage是指向VkImage句柄的指针,在该句柄中返回生成的图像对象

1.3.3 createImage

现在我们重构下createTextureImage, 将创建VkImage的部分单独做个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
void createImage(uint32_t width, uint32_t height, VkFormat format,
VkImageTiling tiling, VkImageUsageFlags usage,
VkMemoryPropertyFlags properties, VkImage& image,
VkDeviceMemory& imageMemory) {

VkImageCreateInfo imageInfo = {};
imageInfo.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
imageInfo.imageType = VK_IMAGE_TYPE_2D; //二维图像
imageInfo.extent.width = static_cast<uint32_t>(width);
imageInfo.extent.height = static_cast<uint32_t>(height);
imageInfo.extent.depth = 1;
// 图像的最小采样的细节级别
imageInfo.mipLevels = 1;
// 图像中的层数
imageInfo.arrayLayers = 1;
imageInfo.format = format;
// 图像平铺模式,这里指定图像像素最佳内存拼接布局
// 与图像的布局不同,平铺模式不能在以后更改。如果希望能够直接访问图像内存中的texel,则必须使用VK_IMAGE_TILING_OPTIMAL
imageInfo.tiling = tiling;

// 图像的initialLayout只有两个可能的值:VK_IMAGE_LAYOUT_UNDEFINED || VK_IMAGE_LAYOUT_PREINITIALIZED
imageInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;

imageInfo.usage = usage;
// 图像将仅由一个队列族使用, 因此独占模式
imageInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
// 图像采样
imageInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageInfo.flags = 0; // Optional
// 创建图像
if (vkCreateImage(device, &imageInfo, nullptr, &image) != VK_SUCCESS) {
throw std::runtime_error("failed to create image!");
}

// 同样的,需要给Image分配内存空间
VkMemoryRequirements memRequirements;
vkGetImageMemoryRequirements(device, image, &memRequirements);
VkMemoryAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
allocInfo.allocationSize = memRequirements.size;
allocInfo.memoryTypeIndex =
findMemoryType(memRequirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
if (vkAllocateMemory(device, &allocInfo, nullptr, &imageMemory) != VK_SUCCESS) {
throw std::runtime_error("failed to allocate image memory!");
}
// 绑定图像和内存
vkBindImageMemory(device, image, imageMemory, 0);
}

void createTextureImage() {
int texWidth, texHeight, texChannels;
// 加载texture.jpg图像
stbi_uc* pixels = stbi_load("textures/texture.jpg", &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
// 每个像素4个字节
VkDeviceSize imageSize = texWidth * texHeight * 4;
if (!pixels) {
throw std::runtime_error("failed to load texture image!");
}

VkBuffer stagingBuffer;
VkDeviceMemory stagingBufferMemory;
// 缓冲区应该在主机可见内存中,以便我们可以映射它,并且它应该可用作传输源,以便我们以后可以复制
createBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT,
VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT |
VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer,
stagingBufferMemory, VK_SHARING_MODE_EXCLUSIVE);
// 内存映射
void* data;
vkMapMemory(device, stagingBufferMemory, 0, imageSize, 0, &data);
memcpy(data, pixels, static_cast<size_t>(imageSize));
vkUnmapMemory(device, stagingBufferMemory);
// 最后释放原始像素数据
stbi_image_free(pixels);

createImage(texWidth, texHeight, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_TILING_OPTIMAL,
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, textureImage, textureImageMemory);
}

1.4 布局转换

我们需要再次记录和执行一个命令缓冲区以完成布局转换功能,所以最好是将执行指令缓冲区的部分逻辑抽离:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
VkCommandBuffer beginSingleTimeCommands() {
VkCommandBufferAllocateInfo allocInfo = {};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandPool = commandPool;
allocInfo.commandBufferCount = 1;

VkCommandBuffer commandBuffer;
vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer);

VkCommandBufferBeginInfo beginInfo = {};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
vkBeginCommandBuffer(commandBuffer, &beginInfo);
return commandBuffer;
}

void endSingleTimeCommands(VkCommandBuffer commandBuffer) {
vkEndCommandBuffer(commandBuffer);

VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;

vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE);
vkQueueWaitIdle(graphicsQueue);

vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
}

现在有了beginSingleTimeCommands和endSingleTimeCommands函数,可以对执行单条指令缓冲区的函数进行优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
VkCommandBuffer commandBuffer= beginSingleTimeCommands();
// 缓冲拷贝指令
VkBufferCopy copyRegion = {};
copyRegion.srcOffset = 0; // Optional
copyRegion.dstOffset = 0; // Optional
copyRegion.size = size;
// std::cout<<"copyBuffer vkCmdCopyBuffer"<<std::endl;
// 缓冲区的内容使用vkCmdCopyBuffer命令传输。
// 源和目标缓冲区以及要复制的区域数组作为参数。copyRegion由源缓冲区偏移量、目标缓冲区偏移量和大小组成
vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);

endSingleTimeCommands(commandBuffer);
}

如果我们仍然使用缓冲区,那么我们现在可以编写一个函数来记录并执行vkCmdCopyBufferToImage,但是这个命令要求首先将Image置于正确的布局中。

创建一个新函数来处理布局转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
void transitionImageLayout(VkImage image, VkFormat format,
VkImageLayout oldLayout, VkImageLayout newLayout) {

VkCommandBuffer commandBuffer = beginSingleTimeCommands();
// 使用图像内存屏障,用于同步资源访问
VkImageMemoryBarrier barrier = {};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
// 指定布局转换。如果不关心图像的现有内容,可以将VK_IMAGE_LAYOUT_UNDEFINED用作oldLayout
barrier.oldLayout = oldLayout;
barrier.newLayout = newLayout;

// 如果使用屏障来传递队列族的所有权,那么这两个字段应该是队列族的索引
// 如果不这样做,则必须将它们设置为VK_QUEUE_FAMILY_IGNORED
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
// image和subresourceRange指定受影响的图像以及图像的特定部分
barrier.image = image;
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
// 我们的图像不是数组,也没有mipmapping级别,因此只指定了一个级别和层
barrier.subresourceRange.baseMipLevel = 0;
barrier.subresourceRange.levelCount = 1;
barrier.subresourceRange.baseArrayLayer = 0;
barrier.subresourceRange.layerCount = 1;

// 屏障主要用于同步目的,因此必须指定哪些涉及资源的操作类型必须在屏障之前发生,哪些涉及资源的操作必须在屏障上等待
barrier.srcAccessMask = 0; // TODO
barrier.dstAccessMask = 0; // TODO

// 在管道上执行barrier指令, 所有类型的管道屏障都使用相同的函数提交
vkCmdPipelineBarrier(commandBuffer,
0 /* TODO */, 0 /* TODO */,
0,
0, nullptr,
0, nullptr,
1, &barrier
);

endSingleTimeCommands(commandBuffer);
}

执行布局转换的最常见方法之一是使用图像内存屏障。像这样的管道屏障通常用于同步对资源的访问,例如确保在从缓冲区读取之前完成对缓冲区的写入,但是当使用VK_SHARING_MODE_EXCLUSIVE时,它也可以用于转换映像布局和传输队列族所有权。对于缓冲区,有一个等效的缓冲存储器屏障来实现这一点。

1.4.1 VkImageMemoryBarrier

图像存储器屏障仅适用于涉及特定图像子资源范围的存储器访问。也就是说,从图像存储器屏障形成的存储器依赖被限定为通过指定的图像子资源范围进行访问。图像内存屏障还可用于定义指定图像子资源范围的图像布局转换或队列族所有权转移。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct VkImageMemoryBarrier {
VkStructureType sType;
const void* pNext;
VkAccessFlags srcAccessMask;
VkAccessFlags dstAccessMask;
VkImageLayout oldLayout;
VkImageLayout newLayout;
uint32_t srcQueueFamilyIndex;
uint32_t dstQueueFamilyIndex;
VkImage image;
VkImageSubresourceRange subresourceRange;
} VkImageMemoryBarrier;
  1. sType就是这种结构的类型, VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER
  2. pNext为NULL或指向特定于扩展的结构的指针
  3. srccessmask是指定源访问掩码的VkAccessFlagBits的位掩码, 指定在哪个管道阶段发生操作,这些操作应该在屏障之前发生
  4. dstAccessMask是指定目标访问掩码的VkAccessFlagBits位掩码, 指定操作将在其中等待屏障的管道阶段
  5. oldLayout是图像布局转换中的旧布局
  6. newLayout是图像布局转换中的新布局
  7. srcQueueFamilyIndex是队列系列所有权转移的源队列系列
  8. dstQueueFamilyIndex是队列系列所有权转移的目标队列系列
  9. image是受此屏障影响的图像
  10. subresourceRange描述图像中受此屏障影响的图像子资源范围

1.4.2 vkCmdPipelineBarrier

vkCmdPipelineBarrier是一个同步命令,它在提交到同一队列的命令之间或同一子类中的命令之间插入依赖关系。

1
2
3
4
5
6
7
8
9
10
11
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);
  1. commandBuffer是将命令记录到的命令缓冲区
  2. srcStageMask是一个指定源级掩码的VkPipelineStageFlagBits的位掩码
  3. dstStageMask是指定目标阶段掩码的VkPipelineStageFlagBits的位掩码
  4. dependencyFlags是VkdePendencyFlags的位掩码,指定如何形成执行和内存依赖关系
  5. memoryBarrierCount是pMemoryBarriers数组的长度
  6. pMemoryBarriers是指向VKMemorySbarrier结构数组的指针
  7. bufferMemoryBarrierCount是pBufferMemoryBarriers数组的长度
  8. pBufferMemoryBarriers是指向VkBufferMemoryBarrier结构数组的指针
  9. imageMemoryBarrierCount是pImageMemoryBarriers数组的长度
  10. pImageMemoryBarriers是指向VkimAgemoryBarrier结构数组的指针

当vkCmdPipelineBarrier提交到队列时,它定义了在它之前提交的命令和在它之后提交的命令之间的内存依赖关系。

如果vkCmdPipelineBarrier是在渲染过程实例外部录制的,则第一个同步作用域将包括按提交顺序较早出现的所有命令。如果vkCmdPipelineBarrier记录在渲染过程实例中,则第一个同步作用域仅包括在同一子过程中以提交顺序较早出现的命令。在这两种情况下,第一个同步作用域仅限于由srcStageMask指定的源阶段掩码确定的管道阶段上的操作。

如果vkCmdPipelineBarrier是在渲染过程实例外部录制的,则第二个同步作用域将包括以后按提交顺序执行的所有命令。如果vkCmdPipelineBarrier记录在渲染过程实例中,则第二个同步作用域仅包括稍后在同一子过程中按提交顺序出现的命令。在任何一种情况下,第二同步作用域都限于由dstStageMask指定的目的级掩码确定的管道级上的操作。

第一个访问范围被限制为在由srcStageMask指定的源阶段掩码确定的管道阶段中进行访问。其中,第一访问作用域仅包括由pMemoryBarriers、pBufferMemoryBarriers和pImageMemoryBarriers数组的元素定义的第一访问作用域,每个元素定义一组内存屏障。如果未指定内存屏障,则第一个访问作用域不包括任何访问。

第二访问范围被限制为在由dstStageMask指定的目标阶段掩码确定的管道阶段中的访问。其中,第二访问作用域仅包括由pMemoryBarriers、pBufferMemoryBarriers和pImageMemoryBarriers数组的元素定义的第二访问作用域,它们各自定义了一组内存屏障。如果未指定内存屏障,则第二访问作用域不包括任何访问。

1.5 拷贝缓存数据至Image

就像缓冲区复制一样,需要指定缓冲区的哪个部分将被复制到图像的哪个部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void copyBufferToImage(VkBuffer buffer, VkImage image, uint32_t width,
uint32_t height) {

VkCommandBuffer commandBuffer = beginSingleTimeCommands();
// 使用VkBufferImageCopy指定缓冲区复制行为
VkBufferImageCopy region = {};
// 指定缓冲区中像素值开始的字节偏移量
region.bufferOffset = 0;
// 指定像素在内存中的布局方式, 指定0表示像素紧密打包
region.bufferRowLength = 0;
region.bufferImageHeight = 0;

// 指示要将像素复制到图像的哪个部分
region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
region.imageSubresource.mipLevel = 0;
region.imageSubresource.baseArrayLayer = 0;
region.imageSubresource.layerCount = 1;
region.imageOffset = {0, 0, 0};
region.imageExtent = {width, height, 1};

// 使用vkCmdCopyBufferToImage函数将缓冲区到图像的复制操作排队
// 第四个参数指示图像当前使用的布局
vkCmdCopyBufferToImage(commandBuffer, buffer, image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1, &region);

endSingleTimeCommands(commandBuffer);
}

1.5.1 VkBufferImageCopy

1
2
3
4
5
6
7
8
typedef struct VkBufferImageCopy {
VkDeviceSize bufferOffset;
uint32_t bufferRowLength;
uint32_t bufferImageHeight;
VkImageSubresourceLayers imageSubresource;
VkOffset3D imageOffset;
VkExtent3D imageExtent;
} VkBufferImageCopy;
  1. bufferOffset是从复制图像数据的缓冲区对象的起始处开始的以字节为单位的偏移量
  2. bufferRowLength和bufferImageHeight以texel为单位指定缓冲存储器中较大的二维或三维图像的子区域,并控制寻址计算。如果这些值中的任何一个为零,则根据imageExtent,缓冲存储器的这一方面被认为是紧密压缩的
  3. imageSubresource是一个VkImageSubresourceLayers,用于指定用于源或目标图像数据的图像的特定图像子资源
  4. imageOffset选择源或目标图像数据子区域的初始x、y、z偏移(以texel为单位)
  5. imageExtent是要在宽度、高度和深度上复制的图像的大小(以texel为单位)

当复制到或从深度或模具方面时,缓冲区内存中的数据使用的布局是深度或模具数据的(大部分)紧密封装的表示形式。具体地说:

  1. 复制到或从任何深度/模板格式的模板方面的数据都用每个texel的VK_FORMAT_S8_UINT值紧密打包
  2. 复制到或从VK_FORMAT_D16_UNORM或VK_FORMAT_D16_UNORM_S8_UINT格式的深度方面的数据使用每个texel的VK_FORMAT_D16_UNORM值紧密打包
  3. 复制到或从VK_FORMAT_D32_SFLOAT或VK_FORMAT_D32_SFLOAT_S8_UINT格式的深度方面的数据使用每个texel的一个VK_FORMAT_D32_SFLOAT值紧密打包
  4. 复制到或从VK_FORMAT_X8_D24_UNORM_PACK32或VK_FORMAT_D24_UNORM_S8_UINT格式的深度方面的数据被打包为每个texel一个32位单词,每个单词的lsb中有D24值,8个msb中有未定义的值

由于图像副本的深度或模板方面缓冲区在某些实现上可能需要格式转换,因此不支持图形的队列不支持格式转换。
当复制到深度方面时,并且没有启用VK_EXT_depth_range_unrestricted扩展名,缓冲区内存中的数据必须在[0,1]范围内,否则结果值是未定义的。
复制从imageSubresource的图像图层baseArrayLayer成员开始一层一层地进行。layerCount层从源图像或目标图像复制。

1.5.2 vkCmdCopyBufferToImage

在缓冲区和图像之间复制数据, 从buffer对象复制数据到image对象, 调用vkCmdCopyBufferToImage:

1
2
3
4
5
6
7
void vkCmdCopyBufferToImage(
VkCommandBuffer commandBuffer,
VkBuffer srcBuffer,
VkImage dstImage,
VkImageLayout dstImageLayout,
uint32_t regionCount,
const VkBufferImageCopy* pRegions);
  1. commandBuffer是命令将被记录到的命令缓冲区
  2. srcBuffer是源缓冲区
  3. dstImage是目标图像
  4. dstImageLayout是复制的目标图像子资源的布局
  5. regionCount是要复制的区域数
  6. pRegions是一个指向VkBufferImageCopy结构数组的指针,该结构数组指定要复制的区域

区域中的每个区域从源缓冲区的指定区域复制到目标图像的指定区域。

如果dstImage的格式是一个多平面的图像格式),必须使用VkBufferImageCopy结构的pRegions成员单独指定作为拷贝目标的每个平面的区域。在本例中,imageSubresource的aspectMask必须为VK_IMAGE_ASPECT_PLANE_0_BIT、VK_IMAGE_ASPECT_PLANE_1_BIT或VK_IMAGE_ASPECT_PLANE_2_BIT。对于vkCmdCopyBufferToImage来说,多平面图像的每个平面都被视为具有由相应子资源的aspectMask标识的平面的多平面格式的兼容平面格式中列出的格式。这既适用于VkFormat,也适用于复制中使用的坐标,它对应于平面中的texel,而不是这些texel如何映射到整个图像中的坐标。

1.6 准备纹理图像

回到createTextureImage函数。我们在那里做的最后一件事是创建纹理图像。下一步是将暂存缓冲区复制到纹理图像。这包括两个步骤:

  1. 转换纹理图像到VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
  2. 执行缓冲区到图像复制操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 该图像是使用VK_IMAGE_LAYOUT_UNDEFINED布局创建的,因此在转换textureImage时应将oldLayout指定为VK_IMAGE_LAYOUT_UNDEFINED
// 在执行复制操作之前,不关心图像内容,所以可以这样做
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);

// 拷贝stagingBuffer中缓存的图像数据至Image(GPU可见内存)
copyBufferToImage(stagingBuffer, textureImage,
static_cast<uint32_t>(texWidth), static_cast<uint32_t>(texHeight));

// 为了能够从着色器中的纹理图像开始采样,我们需要最后一个过渡来准备着色器访问(用于同步对资源的访问):
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

1.7 转换屏障的含义 VkAccessFlags

现在在启用验证层的情况下运行应用程序,那么将看到transitionImageLayout中的访问掩码和管道阶段无效。

我们需要根据过渡中的布局来设置它们,拷贝前后的两种转换都需要设置:

  1. VK_IMAGE_LAYOUT_UNDEFINED->VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL: 不需要等待任何内容的传输写入
  2. VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL-> VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL: shader reads应该等待Transfer writes,特别是shader在片段着色器中读取,因为这就是我们要使用纹理的地方
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
VkPipelineStageFlags sourceStage;
VkPipelineStageFlags destinationStage;

if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED
&& newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
barrier.srcAccessMask = 0;
// Image或缓冲区在清除或复制操作中的写访问
barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
// 指定队列最初接收到任何命令的管道阶段
sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
// 指定所有复制命令和清除命令管道阶段
destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
} else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
&& newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL) {
// Image或缓冲区在清除或复制操作中的写访问
barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
// 指定对存储缓冲区、物理存储缓冲区、统一texel缓冲区、存储texel缓冲区、采样图像或存储图像的读访问
barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
// 指定所有复制命令和清除命令管道阶段
sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
// 指定片段着色器阶段
destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
} else {
throw std::invalid_argument("unsupported layout transition!");
}

vkCmdPipelineBarrier(commandBuffer, sourceStage, destinationStage,
0,
0, nullptr,
0, nullptr,
1, &barrier);

传输写入必须在管道传输阶段进行。因为写操作不需要等待任何东西,所以您可以为预barrier操作指定一个空的访问掩码和尽可能早的管道阶段VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT。需要注意的是,VK_PIPELINE_STAGE_TRANSFER_BIT并不是图形和计算管道中的一个真正的阶段。它更多的是一个发生转移的伪阶段。

图像将在相同的管道阶段被写入,然后被片段着色器读取,这就是为什么我们在片段着色器管道阶段指定着色器读取访问。需要注意的一点是,命令缓冲区提交在开始时会导致隐式的VK_ACCESS_HOST_WRITE_BIT同步。由于transitionImageLayout函数只使用一个命令来执行一个命令缓冲区,所以如果在布局转换中需要VK_ACCESS_HOST_WRITE_BIT依赖项,您可以使用这个隐式同步并将srcAccessMask设置为0。

实际上,有一种特殊的图像布局类型可以支持所有操作–VK_IMAGE_LAYOUT_GENERAL。当然,它的问题在于,它不一定能为任何操作提供最佳性能。在某些特殊情况下,例如使用图像作为输入和输出,或者在离开预初始化的布局后读取图像。到目前为止,所有提交命令的帮助程序功能都已设置为通过等待队列变为空闲状态而同步执行。对于实际应用,建议将这些操作组合在单个命令缓冲区中,并异步执行它们以提高吞吐量,尤其是createTextureImage函数中的过渡和复制。通过创建一个helper函数将命令记录到其中的setupCommandBuffer并尝试添加一个flushSetupCommands来执行到目前为止已记录的命令,来尝试进行此操作。最好在纹理贴图工作后执行此操作,以检查纹理资源是否仍正确设置。

1.7.1 VkAccessFlagBits

Vulkan中的内存可以通过shader调用和管道中的一些固定函数来访问。访问类型是所使用的描述符类型的函数,或者固定函数阶段如何访问内存。每个访问类型对应于VkAccessFlagBits中的一个位标志。

一些同步命令以访问类型集作为参数来定义内存依赖项的访问范围。如果同步命令包含源访问掩码,则其第一个访问作用域仅包括通过该掩码中指定的访问类型进行的访问。类似地,如果同步命令包含目标访问掩码,则其第二个访问作用域仅包括通过该掩码中指定的访问类型进行的访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
typedef enum VkAccessFlagBits {
VK_ACCESS_INDIRECT_COMMAND_READ_BIT = 0x00000001,
VK_ACCESS_INDEX_READ_BIT = 0x00000002,
VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT = 0x00000004,
VK_ACCESS_UNIFORM_READ_BIT = 0x00000008,
VK_ACCESS_INPUT_ATTACHMENT_READ_BIT = 0x00000010,
VK_ACCESS_SHADER_READ_BIT = 0x00000020,
VK_ACCESS_SHADER_WRITE_BIT = 0x00000040,
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT = 0x00000080,
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT = 0x00000100,
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT = 0x00000200,
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT = 0x00000400,
VK_ACCESS_TRANSFER_READ_BIT = 0x00000800,
VK_ACCESS_TRANSFER_WRITE_BIT = 0x00001000,
VK_ACCESS_HOST_READ_BIT = 0x00002000,
VK_ACCESS_HOST_WRITE_BIT = 0x00004000,
VK_ACCESS_MEMORY_READ_BIT = 0x00008000,
VK_ACCESS_MEMORY_WRITE_BIT = 0x00010000,
VK_ACCESS_TRANSFORM_FEEDBACK_WRITE_BIT_EXT = 0x02000000,
VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_READ_BIT_EXT = 0x04000000,
VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_WRITE_BIT_EXT = 0x08000000,
VK_ACCESS_CONDITIONAL_RENDERING_READ_BIT_EXT = 0x00100000,
VK_ACCESS_COMMAND_PROCESS_READ_BIT_NVX = 0x00020000,
VK_ACCESS_COMMAND_PROCESS_WRITE_BIT_NVX = 0x00040000,
VK_ACCESS_COLOR_ATTACHMENT_READ_NONCOHERENT_BIT_EXT = 0x00080000,
VK_ACCESS_SHADING_RATE_IMAGE_READ_BIT_NV = 0x00800000,
VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_NV = 0x00200000,
VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_NV = 0x00400000,
VK_ACCESS_FRAGMENT_DENSITY_MAP_READ_BIT_EXT = 0x01000000,
VK_ACCESS_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkAccessFlagBits;
  1. VK_ACCESS_INDIRECT_COMMAND_READ_BIT指定对作为间接绘图或调度命令一部分的间接命令数据的读访问
  2. VK_ACCESS_INDEX_READ_BIT指定对索引缓冲区的读访问,作为索引绘图命令的一部分,由vkCmdBindIndexBuffer绑定
  3. VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT指定对顶点缓冲区的读访问,作为绘图命令的一部分,由vkCmdBindVertexBuffers绑定
  4. VK_ACCESS_UNIFORM_READ_BIT统一缓冲区读访问权限
  5. VK_ACCESS_INPUT_ATTACHMENT_READ_BIT指定在片段着色期间渲染通道内对输入附件的读访问
  6. VK_ACCESS_SHADER_READ_BIT指定对存储缓冲区、物理存储缓冲区、统一texel缓冲区、存储texel缓冲区、采样图像或存储图像的读访问
  7. VK_ACCESS_SHADER_WRITE_BIT存储缓冲区、物理存储缓冲区、存储texel缓冲区或存储映像的写访问
  8. VK_ACCESS_COLOR_ATTACHMENT_READ_BIT指定对颜色附件的读访问,例如通过混合、逻辑操作或通过某些subpass加载操作。它不包括高级混合操作
  9. VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT指定在渲染通道期间或通过某些子通道加载和存储操作对颜色、解析或深度/模板解析附件的写访问
  10. VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT指定对深度/模板附件的读访问,通过深度或模板操作,或通过某些子传递加载操作
  11. VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT指定对深度/模板附件的写访问,通过深度或模板操作,或者通过某些子传递加载和存储操作
  12. VK_ACCESS_TRANSFER_READ_BIT拷贝操作中对镜像或缓冲区的读访问
  13. VK_ACCESS_TRANSFER_WRITE_BIT映像或缓冲区在清除或复制操作中的写访问
  14. VK_ACCESS_HOST_READ_BIT主机操作读访问。这种类型的访问不是通过资源执行的,而是直接在内存上执行的
  15. VK_ACCESS_HOST_WRITE_BIT主机操作写访问。这种类型的访问不是通过资源执行的,而是直接在内存上执行的
  16. VK_ACCESS_MEMORY_READ_BIT所有读访问。它在任何访问掩码中都是有效的,并被视为等同于设置所有在使用它时有效的读访问标志
  17. VK_ACCESS_MEMORY_WRITE_BIT所有写访问。它在任何访问掩码中都是有效的,并被视为等同于设置所有在使用它时有效的写访问标志
  18. VK_ACCESS_CONDITIONAL_RENDERING_READ_BIT_EXT指定对谓词的读访问,作为条件呈现的一部分
  19. VK_ACCESS_TRANSFORM_FEEDBACK_WRITE_BIT_EXT指定在转换反馈激活时对转换反馈缓冲区的写访问
  20. VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_READ_BIT_EXT指定对转换反馈计数器缓冲区的读访问,当vkCmdBeginTransformFeedbackEXT执行时读取该缓冲区
  21. VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_WRITE_BIT_EXT指定对转换反馈计数器缓冲区的写访问,该缓冲区在vkCmdEndTransformFeedbackEXT执行时写入
  22. VK_ACCESS_COMMAND_PROCESS_READ_BIT_NVX指定从VkBuffer输入读取vkCmdProcessCommandsNVX
  23. VK_ACCESS_COMMAND_PROCESS_WRITE_BIT_NVX指定写到vkCmdProcessCommandsNVX的目标命令缓冲区
  24. VK_ACCESS_COLOR_ATTACHMENT_READ_NONCOHERENT_BIT_EXT类似于VK_ACCESS_COLOR_ATTACHMENT_READ_BIT,但是也包括高级的混合操作
  25. VK_ACCESS_SHADING_RATE_IMAGE_READ_BIT_NV指定对着色率图像的读取访问,作为绘图命令的一部分,由vkcmdbindshadingraemimagenv绑定
  26. VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_NV指定对加速结构的读访问,作为跟踪或构建命令的一部分
  27. VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_NV指定对加速结构的写访问,作为构建命令的一部分
  28. VK_ACCESS_FRAGMENT_DENSITY_MAP_READ_BIT_EXT动态碎片密度图操作时对碎片密度图附件的读访问

1.7.2 VkPipelineStageFlags 管道阶段

操作或同步命令执行的工作由多个操作组成,这些操作作为逻辑上独立的步骤序列执行,称为管道阶段。执行的确切管道阶段取决于所使用的特定命令,以及记录命令时的当前命令缓冲区状态。绘制命令、分派命令、复制命令、清除命令和同步命令都在管道阶段的不同集合中执行。同步命令不会在已定义的管道中执行,但会执行VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT和VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT。

注意同步命令执行的操作(例如可用性和可见性操作)不是由定义的管道阶段执行的。但是,其他命令仍然可以通过VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT和VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT管道阶段与它们同步。

跨管道阶段执行操作必须遵循隐式排序保证,特别是包括管道阶段顺序。否则,与其他阶段相比,跨管道阶段的执行可能会重叠或无序执行,除非执行依赖项强制执行。

一些同步命令包括管道阶段参数,将该命令的同步范围限制在这些阶段。这允许对精确的执行依赖关系和操作命令执行的访问进行细粒度的控制。实现应该使用这些管道阶段来避免不必要的停顿或缓存刷新。

可以设置指定管道阶段通过VkPipelineStageFlags:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef enum VkPipelineStageFlagBits {
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT = 0x00000001,
VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT = 0x00000002,
VK_PIPELINE_STAGE_VERTEX_INPUT_BIT = 0x00000004,
VK_PIPELINE_STAGE_VERTEX_SHADER_BIT = 0x00000008,
VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT = 0x00000010,
VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT = 0x00000020,
VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT = 0x00000040,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT = 0x00000080,
VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT = 0x00000100,
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT = 0x00000200,
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT = 0x00000400,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT = 0x00000800,
VK_PIPELINE_STAGE_TRANSFER_BIT = 0x00001000,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT = 0x00002000,
VK_PIPELINE_STAGE_HOST_BIT = 0x00004000,
VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT = 0x00008000,
VK_PIPELINE_STAGE_ALL_COMMANDS_BIT = 0x00010000,
VK_PIPELINE_STAGE_TRANSFORM_FEEDBACK_BIT_EXT = 0x01000000,
VK_PIPELINE_STAGE_CONDITIONAL_RENDERING_BIT_EXT = 0x00040000,
VK_PIPELINE_STAGE_COMMAND_PROCESS_BIT_NVX = 0x00020000,
VK_PIPELINE_STAGE_SHADING_RATE_IMAGE_BIT_NV = 0x00400000,
VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_NV = 0x00200000,
VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_NV = 0x02000000,
VK_PIPELINE_STAGE_TASK_SHADER_BIT_NV = 0x00080000,
VK_PIPELINE_STAGE_MESH_SHADER_BIT_NV = 0x00100000,
VK_PIPELINE_STAGE_FRAGMENT_DENSITY_PROCESS_BIT_EXT = 0x00800000,
VK_PIPELINE_STAGE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkPipelineStageFlagBits;
  1. VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT指定队列最初接收到任何命令的管道阶段
  2. VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT指定使用Draw/DispatchIndirect数据结构的管道阶段。这个阶段还包括读取vkCmdProcessCommandsNVX写的命令
  3. VK_PIPELINE_STAGE_TASK_SHADER_BIT_NV指定任务着色器阶段
  4. VK_PIPELINE_STAGE_MESH_SHADER_BIT_NV指定网格着色器阶段
  5. VK_PIPELINE_STAGE_VERTEX_INPUT_BIT指定消耗顶点和索引缓冲区的流水线阶段
  6. VK_PIPELINE_STAGE_VERTEX_SHADER_BIT指定顶点着色器阶段
  7. VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT指定镶嵌控制着色器阶段
  8. VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT指定镶嵌评估着色器阶段
  9. VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT指定几何着色器阶段
  10. VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT指定片段着色器阶段
  11. VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT指定执行早期片段测试(片段着色之前的深度和模板测试)的管道阶段。此阶段还包括针对具有深度/模板格式的帧缓冲区附件的子传递加载操作
  12. VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT指定执行后期片段测试(片段着色后的深度和模板测试)的管道阶段。此阶段还包括用于具有深度/模板格式的帧缓冲区附件的子传递存储操作
  13. VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT指定混合后管道的阶段,从管道输出最终颜色值。此阶段还包括子通道加载和存储操作以及具有颜色或深度/模板格式的帧缓冲区附件的多样本解析操作
  14. VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT指定执行计算着色器
  15. VK_PIPELINE_STAGE_TRANSFER_BIT指定以下命令:
    1. 所有复制命令,包括vkCmdCopyQueryPoolResults,vkCmdBlitImage,vkCmdResolveImage
    2. 所有清除命令,但vkCmdClearAttachments除外
  16. VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT指定管道中由所有命令生成的操作完成执行的最后阶段
  17. VK_PIPELINE_STAGE_HOST_BIT指定一个伪阶段,指示在主机上执行设备存储器的读/写操作。记录在命令缓冲区中的任何命令都不会调用此阶段
  18. VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_NV指定光线跟踪着色器阶段的执行
  19. VK_PIPELINE_STAGE_ACCELERATION_STRUCTURE_BUILD_BIT_NV指定vkCmdBuildAccelerationStructureNV,vkCmdCopyAccelerationStructureNV和vkCmdWriteAccelerationStructuresPropertiesNV的执行
  20. VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT指定所有图形管线阶段的执行,并且等效于:
    1. VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT
    2. VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT
    3. VK_PIPELINE_STAGE_TASK_SHADER_BIT_NV
    4. VK_PIPELINE_STAGE_MESH_SHADER_BIT_NV
    5. VK_PIPELINE_STAGE_VERTEX_INPUT_BIT
    6. VK_PIPELINE_STAGE_VERTEX_SHADER_BIT
    7. VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT
    8. VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT
    9. VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT
    10. VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT
    11. VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT
    12. VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT
    13. VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
    14. VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT
    15. VK_PIPELINE_STAGE_CONDITIONAL_RENDERING_BIT_EXT
    16. VK_PIPELINE_STAGE_TRANSFORM_FEEDBACK_BIT_EXT
    17. VK_PIPELINE_STAGE_SHADING_RATE_IMAGE_BIT_NV
    18. VK_PIPELINE_STAGE_FRAGMENT_DENSITY_PROCESS_BIT_EXT
  21. VK_PIPELINE_STAGE_ALL_COMMANDS_BIT等效于与其一起使用的队列上支持的所有其他管道阶段标志的逻辑或
  22. VK_PIPELINE_STAGE_CONDITIONAL_RENDERING_BIT_EXT指定使用条件渲染谓词的管道阶段
  23. VK_PIPELINE_STAGE_TRANSFORM_FEEDBACK_BIT_EXT指定将顶点属性输出值写入转换反馈缓冲区的管线阶段
  24. VK_PIPELINE_STAGE_COMMAND_PROCESS_BIT_NVX指定了处理通过vkCmdProcessCommandsNVX在设备端生成命令的管道阶段
  25. VK_PIPELINE_STAGE_SHADING_RATE_IMAGE_BIT_NV指定管道的阶段,在该阶段中读取阴影率图像,以确定栅格化图元各部分的阴影率
  26. VK_PIPELINE_STAGE_FRAGMENT_DENSITY_PROCESS_BIT_EXT指定读取片段密度图以生成片段区域的管线阶段

1.8 清理

创建纹理贴图后,不能忘记在必要的时候将内存释放出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void createTextureImage() {
...
// 通过清除过渡缓冲区及其末尾的内存来完成createTextureImage函数:
transitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);

vkDestroyBuffer(device, stagingBuffer, nullptr);
vkFreeMemory(device, stagingBufferMemory, nullptr);
}

void cleanup() {
cleanupSwapChain();

vkDestroyImage(device, textureImage, nullptr);
vkFreeMemory(device, textureImageMemory, nullptr);
...
}

1.9 总结

到目前为止,我们从设备物理存储上读取了图片内容,将其转成临时缓存后又将其存储在对应GPU可见的内存中以及生成对应VkImage纹理贴图对象,接下来需要将其显示在屏幕上还需要把这个对象放入图形管道中。

在回顾下本章中的读取图像的步骤:

  1. 首先利用stb-image库读取图片,将其内容存储在临时缓存区VkBuffer中,注意需要开辟GPU可见内存
  2. 通过VkImageCreateInfo结构指明图像格式并通过vkCreateImage创建VkImage对象
  3. 用VkBuffer图像文件中的像素填充创建的VkImage图像对象
    1. 填充图像对象需要使用VkImageMemoryBarrier
    2. 使用vkCmdPipelineBarrier使得图像填充Barrier生效
    3. 通过vkCmdCopyBufferToImage完成图像像素从VkBuffer到VkImage的拷贝(填充)
    4. 再通过VkImageMemoryBarrier指定图像是能够从着色器中的纹理图像开始采样
  4. 创建图像视图和图像采样器(后续下一章开始处理)
  5. 添加一个组合的图像采样器描述符来从纹理中采样颜色

上面步骤中,4和5是下一章的内容。

1.10 Windows上的CMakefileLists.txt写法

windows平台上编译当前项目,可以使用cmake, CMakefileLists.txt文件如下(注意先安装Vulkan sdk):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

cmake_minimum_required (VERSION 3.7) #最低要求的CMake版本
project(MyVulkan) # 项目名称
set(VERSION 0.0.1)
set(CMAKE_BUILD_TYPE "Debug")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17 -g -Wall -Wno-unused-variable -pthread")

message(STATUS "This is " ${PROJECT_NAME} " version " ${VERSION})
message(STATUS "This is for windows platform")
message("Build Type:" ${CMAKE_BUILD_TYPE} ${CMAKE_CXX_FLAGS})

# Use FindVulkan module added with CMAKE 3.7
if (NOT CMAKE_VERSION VERSION_LESS 3.7.0)
message(STATUS "Using module to find Vulkan")
find_package(Vulkan)
endif()

find_library(Vulkan_LIBRARY NAMES vulkan-1 vulkan PATHS ${CMAKE_SOURCE_DIR}/libs/vulkan)
IF (Vulkan_LIBRARY)
set(Vulkan_FOUND ON)
MESSAGE("Using bundled Vulkan library version")
ENDIF()

message(STATUS "Using Vulkan lib: " ${Vulkan_LIBRARY})

# CMAKE_SOURCE_DIR 代表工程根目录CMakeLists.txt文件所在目录
set(ROOT_DIR ${CMAKE_SOURCE_DIR})

### GLFW3
set(GLFW_LIB_DIR ${ROOT_DIR}/lib/glfw3)
set(GLFW_LIBS ${GLFW_LIB_DIR}/glfw3dll.lib)
### GLM
set(GLM_INCLUDE_DIRS ${ROOT_DIR}/include/glm)
### stb-image
set(STB_IMAGE_DIRS ${ROOT_DIR}/include/stb-image)

message(STATUS "Lib path: ")
message(STATUS " GLFW3: " ${GLFW_LIBS})
message(STATUS " GLM : " ${GLM_INCLUDE_DIRS})
message(STATUS " STB_IMAGE: " ${STB_IMAGE_DIRS})

# 定义头文件搜索路径
include_directories(${ROOT_DIR}/inlcude
${GLM_INCLUDE_DIRS})

#aux_source_directory(./ SOURCE_DIR)
aux_source_directory(${ROOT_DIR}/inlcude SOURCE_DIR)
aux_source_directory(${ROOT_DIR}/src SOURCE_DIR)

# Target
add_executable(MyVulkan ${SOURCE_DIR})

####Vulkan
find_package(Vulkan REQUIRED)
# GLFW3 is dynamic link
target_link_libraries(${PROJECT_NAME} Vulkan::Vulkan ${GLFW_LIBS})

项目文件目录:

图像14-1

欢迎关注我的其它发布渠道