布局变换

接下来我们开始记录传输指令到指令缓冲,我们为此编写了两个辅助函数:

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);
}

上面的代码大部分来自copyBuffer函数,现在我们可以用它来简化copyBuffer函数的实现:

void copyBuffer(VkBuffer srcBuffer, VkBuffer dstBuffer, VkDeviceSize size) {
    VkCommandBuffer commandBuffer = beginSingleTimeCommands();

    VkBufferCopy copyRegion = {};
    copyRegion.size = size;
    vkCmdCopyBuffer(commandBuffer, srcBuffer, dstBuffer, 1, &copyRegion);

    endSingleTimeCommands(commandBuffer);
}

如果我们使用的是缓冲对象而不是图像对象,那么就可以记录传输指令,然后调用vkCmdCopyBufferToImage函数结束工作,但这一指令需要图像满足一定的布局要求,所以需要我们编写一个新的函数来进行图像布局变换:

void transitionImageLayout(VkImage image, VkFormat format,
VkImageLayout oldLayout, VkImageLayout newLayout) {
    VkCommandBuffer commandBuffer = beginSingleTimeCommands();

    endSingleTimeCommands(commandBuffer);
}

通过图像内存屏障(image memory barrier)我们可以对图像布局进行变换。管线屏障(pipeline barrier)主要被用来同步资源访问,比如保证图像在被读取之前数据被写入。它也可以被用来变换图像布局。在本章节,我们使用它进行图像布局变换。如果队列的所有模式为VK_SHARING_MODE_EXCLUSIVE,管线屏障(pipeline barrier)还可以被用来传递队列所有权。对于缓冲对象也有一个可以实现同样效果的缓冲内存屏障(buffer memory barrier)。

VkImageMemoryBarrier barrier = {};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = oldLayout;
barrier.newLayout = newLayout;

oldLayout和newLayout成员变量用于指定布局变换。如果不需要访问之前的图像数据,可以将oldLayout设置为VK_IMAGE_LAYOUT_UNDEFINED来获得更好的性能表现。

barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;

如果读者使用屏障来传递队列族所有权,那么就需要对srcQueueFamilyIndex和dstQueueFamilyIndex成员变量进行设置。如果读者不进行队列所有权传递,则必须将这两个成员变量的值设置为VK_QUEUE_FAMILY_IGNORED。

barrier.image = image;
barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
barrier.subresourceRange.baseMipLevel = 0;
barrier.subresourceRange.levelCount = 1;
barrier.subresourceRange.baseArrayLayer = 0;
barrier.subresourceRange.layerCount = 1;

image和subresourceRange成员变量用于指定进行布局变换的图像对象,以及受影响的图像范围。这里,我们使用的图像不存在细分级别,所以将level和layer的值都设置为1。

barrier.srcAccessMask = 0; // TODO
barrier.dstAccessMask = 0; // TODO

我们需要指定在屏障之前必须发生的资源操作类型,以及必须等待屏障的资源操作类型。虽然我们 已经使用vkQueueWaitIdle函数来手动地进行同步,但还是需要我们进行这一设置。但这一设置依赖旧布局和新布局,所以我们会在确定使用的布局变换后再来设置它。

vkCmdPipelineBarrier(
commandBuffer, 0 /* TODO */, 0 /* TODO */, 0, 0, nullptr, 0, nullptr, 1, &barrier);

提交管线屏障对象需要调用vkCmdPipelineBarrier函数。vkCmdPipelineBarrier函数除了指令缓冲对象外的第一个参数用于指定发生在屏障之前的管线阶段,第二个参数用于指定发生在屏障之后的管线阶段。如果读者想要在一个屏障之后读取uniform,应该指定VK_ACCESS_UNIFORM_READ_BIT使用标记和最早读取uniform的着色器阶段,比如VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT。当指定和使用标记不匹配的管线阶段时校验层会发出警告信息。

第三个参数可以设置为0或VK_DEPENDENCY_BY_REGION_BIT。设置为VK_DEPENDENCY_BY_REGION_BIT的话,屏障就变成了一个区域条件。这允许我们读取资源目前已经写入的那部分。

最后6个参数用于引用三种可用的管线屏障数组:内存屏障(memory barriers),缓冲内存屏障(buffer memory barriers)和图像内存屏障(image memory barriers)。我们这里使用的是图像内存屏障(image memory barriers)。需要注意这里我们没有使用VkFormat参数,但我们会在之后章节使用它进行特殊的变换操作。