【Vulkan 入门系列】创建纹理图像,将暂存区图像拷贝到纹理图像,创建纹理图像视图(七)-二、将暂存区图像拷贝到纹理图像

时间:2025-04-21 07:40:28
void HelloVK::initVulkan() {
  createInstance();
  createSurface();
  pickPhysicalDevice();
  createLogicalDeviceAndQueue();
  setupDebugMessenger();
  establishDisplaySizeIdentity();
  createSwapChain();
  createImageViews();
  createRenderPass();
  createDescriptorSetLayout();
  createGraphicsPipeline();
  createFramebuffers();
  createCommandPool();
  createCommandBuffer();
  decodeImage();
  createTextureImage();
  copyBufferToImage();
  ...
}

void HelloVK::copyBufferToImage() {
  VkImageSubresourceRange subresourceRange{};
  subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
  subresourceRange.baseMipLevel = 0;
  subresourceRange.levelCount = 1;
  subresourceRange.layerCount = 1;

  VkImageMemoryBarrier imageMemoryBarrier{};
  imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
  imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
  imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
  imageMemoryBarrier.image = textureImage;
  imageMemoryBarrier.subresourceRange = subresourceRange;
  imageMemoryBarrier.srcAccessMask = 0;
  imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
  imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
  imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;

  VkCommandBuffer cmd;
  VkCommandBufferAllocateInfo cmdAllocInfo{};
  cmdAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
  cmdAllocInfo.commandPool = commandPool;
  cmdAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
  cmdAllocInfo.commandBufferCount = 1;

  VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &cmd));

  VkCommandBufferBeginInfo beginInfo{};
  beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
  vkBeginCommandBuffer(cmd, &beginInfo);

  vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_HOST_BIT,
                       VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0,
                       nullptr, 1, &imageMemoryBarrier);

  VkBufferImageCopy bufferImageCopy{};
  bufferImageCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
  bufferImageCopy.imageSubresource.mipLevel = 0;
  bufferImageCopy.imageSubresource.baseArrayLayer = 0;
  bufferImageCopy.imageSubresource.layerCount = 1;
  bufferImageCopy.imageExtent.width = textureWidth;
  bufferImageCopy.imageExtent.height = textureHeight;
  bufferImageCopy.imageExtent.depth = 1;
  bufferImageCopy.bufferOffset = 0;

  vkCmdCopyBufferToImage(cmd, stagingBuffer, textureImage,
                         VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
                         1, &bufferImageCopy);

  imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
  imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
  imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
  imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

  vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT,
                       VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr,
                       0, nullptr, 1, &imageMemoryBarrier);

  vkEndCommandBuffer(cmd);

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

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

2.1 图像子资源范围定义 (VkImageSubresourceRange)

定义图像中被操作的部分(颜色通道、Mip 层级、数组层),用于后续的屏障和复制操作。

VkImageSubresourceRange subresourceRange{};
subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; // 操作颜色通道
subresourceRange.baseMipLevel = 0;                        // 起始 Mip 层级
subresourceRange.levelCount = 1;                          // 仅操作 1 个 Mip 层级
subresourceRange.layerCount = 1;                          // 仅操作 1 个数组层
  • VK_IMAGE_ASPECT_COLOR_BIT:深度通道
  • VK_IMAGE_ASPECT_DEPTH_BIT:深度通道

2.2 第一次图像内存屏障 (VkImageMemoryBarrier)

转换图像布局为 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,确保图像准备好接收传输数据。布局转换必须通过管线屏障完成,以保障 GPU 操作的顺序性。

VkImageMemoryBarrier imageMemoryBarrier{};
imageMemoryBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
imageMemoryBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; // 无队列族切换
imageMemoryBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
imageMemoryBarrier.image = textureImage;                          // 目标图像
imageMemoryBarrier.subresourceRange = subresourceRange;           // 操作的子资源范围
imageMemoryBarrier.srcAccessMask = 0;                             // 之前没有需要同步的访问操作。
imageMemoryBarrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;  // 后续操作需要写入权限
imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;         // 原始布局
imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; // 目标布局

VkImageMemoryBarrier 是 Vulkan 中用于同步图像内存访问和布局转换的核心结构体。它定义了图像资源的访问依赖关系和布局转换规则,确保 GPU 操作之间的正确顺序和数据一致性。以下是对其关键字段和作用的详细解释:

  1. srcAccessMaskdstAccessMask

srcAccessMask:在屏障之前的操作需要同步的访问类型。
dstAccessMask:在屏障之后的操作需要的访问类型。

常见值

  • VK_ACCESS_TRANSFER_WRITE_BIT(传输写入)
  • VK_ACCESS_SHADER_READ_BIT(着色器读取)
  • VK_ACCESS_HOST_READ_BIT(主机读取)
  1. oldLayoutnewLayout

oldLayout:当前图像布局。
newLayout:目标图像布局。

定义图像布局转换。Vulkan 要求显式声明图像布局,不同布局对 GPU 操作的访问方式有严格限制。

常见布局

  • VK_IMAGE_LAYOUT_UNDEFINED:初始布局(内容可能被丢弃)。
  • VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL:传输写入最优布局。
  • VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL:着色器采样最优布局。
  1. srcQueueFamilyIndexdstQueueFamilyIndex

srcQueueFamilyIndex:源队列族索引。
dstQueueFamilyIndex:目标队列族索引。

处理队列族所有权转移。若图像需要从一个队列族转移到另一个队列族(如从传输队列到图形队列),需在此指定队列族索引。

特殊值

  • VK_QUEUE_FAMILY_IGNORED:不进行所有权转移。
  • VK_QUEUE_FAMILY_EXTERNAL:用于跨设备或外部 API 的同步。
  1. image:指定要操作的图像。

2.3 分配并开始记录命令缓冲区

分配一个主命令缓冲区并开始记录传输操作。

VkCommandBuffer cmd;
// 分配命令缓冲区
VkCommandBufferAllocateInfo cmdAllocInfo{};
cmdAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
cmdAllocInfo.commandPool = commandPool;                   // 使用的命令池
cmdAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;     // 主命令缓冲区
cmdAllocInfo.commandBufferCount = 1;
VK_CHECK(vkAllocateCommandBuffers(device, &cmdAllocInfo, &cmd));

// 开始记录命令
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
vkBeginCommandBuffer(cmd, &beginInfo);

2.4 执行第一次管线屏障(布局转换)

插入管线屏障,将图像布局从 VK_IMAGE_LAYOUT_UNDEFINED 转换为 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL。此屏障确保图像在传输操作开始前处于正确的布局。

vkCmdPipelineBarrier(
    cmd, 
    VK_PIPELINE_STAGE_HOST_BIT,           // 源阶段:主机准备数据完成
    VK_PIPELINE_STAGE_TRANSFER_BIT,       // 目标阶段:传输操作
    0, 0, nullptr, 0, nullptr, 
    1, &imageMemoryBarrier
);
  • 源阶段:VK_PIPELINE_STAGE_HOST_BIT 表示数据已由 CPU(主机)准备就绪(例如通过暂存缓冲区写入)。
  • 目标阶段:VK_PIPELINE_STAGE_TRANSFER_BIT 表示后续将进行传输操作。

2.5 从缓冲区复制数据到图像

将数据从暂存缓冲区 (stagingBuffer) 复制到图像。

VkBufferImageCopy bufferImageCopy{};
bufferImageCopy.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
bufferImageCopy.imageSubresource.mipLevel = 0;                    // 目标Mip层级
bufferImageCopy.imageSubresource.baseArrayLayer = 0;              // 起始数组层
bufferImageCopy.imageSubresource.layerCount = 1;                  // 复制的层数
bufferImageCopy.imageExtent.width = textureWidth;                 //图像宽
bufferImageCopy.imageExtent.height = textureHeight;                //图像高
bufferImageCopy.imageExtent.depth = 1;                   //深度
bufferImageCopy.bufferOffset = 0;                                 // 缓冲区起始偏移

vkCmdCopyBufferToImage(
    cmd, 
    stagingBuffer,                        // 源缓冲区(暂存缓冲区)
    textureImage,                         // 目标图像
    VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, // 当前图像布局
    1, &bufferImageCopy                   // 复制区域描述
);
  • stagingBuffer 通常是 CPU 可写的缓冲区,用于临时存储纹理数据。
  • 图像必须处于 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL 布局才能接收数据。

2.6 第二次图像内存屏障(布局转换)

将图像布局从 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL 转换为 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL。此屏障确保图像在着色器读取前处于正确布局,并避免读写冲突。

imageMemoryBarrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT; // 传输写入完成
imageMemoryBarrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;    // 后续需要着色器读取
imageMemoryBarrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
imageMemoryBarrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

vkCmdPipelineBarrier(
    cmd, 
    VK_PIPELINE_STAGE_TRANSFER_BIT,          // 源阶段:传输完成
    VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,   // 目标阶段:片段着色器读取
    0, 0, nullptr, 0, nullptr, 
    1, &imageMemoryBarrier
);
  • 源阶段:VK_PIPELINE_STAGE_TRANSFER_BIT 表示传输操作已完成。
  • 目标阶段:VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT 表示后续片段着色器可能读取此图像。

2.7 提交命令并等待完成

提交命令缓冲区到图形队列执行,并等待操作完成。

vkEndCommandBuffer(cmd); // 结束记录
// 提交到队列执行
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &cmd;

VK_CHECK(vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE));
vkQueueWaitIdle(graphicsQueue); // 等待操作完成

vkQueueWaitIdle 确保数据复制和布局转换完成后再继续后续操作。

2.8 关键总结

  1. 同步与布局管理

Vulkan 要求显式管理图像布局和内存依赖。两次管线屏障确保:

  • 传输前图像布局正确。
  • 传输后图像布局适合着色器读取。
  1. 数据传输流程

典型流程为:

CPU写入暂存缓冲区
传输到GPU本地图像
转换布局供着色器使用