变换屏障掩码

如果读者在开启校验层的情况下运行程序,会发现校验层报告transitionImageLayout中使用的访问掩码和管线阶段是无效的。我们需要根据布局变换设置transitionImageLayout。

我们需要处理两种变换:

  • 未定义->传输目的:传输操作的数据写入不需要等待

  • 传输目的->着色器读取:着色器读取图像数据需要等待传输操作的写入结束。

我们使用下面的代码指定变换规则:

VkPipelineStageFlags sourceStage;
VkPipelineStageFlags destinationStage;

if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL) {
    barrier.srcAccessMask = 0;
    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) {
        barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
        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);

传输的写入操作必须在管线传输阶段进行。这里因为我们的写入操作不需要等待任何对象,我们可以指定一个空的的访问掩码,使用VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT指定最早出现的管线阶段。需要注意VK_PIPELINE_STAGE_TRANSFER_BIT并非图形和计算管线中真实存在的管线阶段,它实际上是一个伪阶段,出现在传输操作发生时。更多信息可以参考官方文档中有关伪阶段的说明。

图像数据需要在片段着色器读取,所以我们指定了片段着色器管线阶段的读取访问掩码。

之后如果还需要进行更多的图像布局变换,我们会扩展transitionImageLayout函数。

指令缓冲的提交会隐式地进行VK_ACCESS_HOST_WRITE_BIT同步。因为transitionImageLayout函数执行的指令缓冲只包含了一条指令,如果布局转换依赖VK_ACCESS_HOST_WRITE_BIT,我们可以通过设置srcAccessMask的值为0来使用这一隐含的同步。不过,我们最好显式地定义一切,依赖于隐含属性,不就变得和OpenGL一样容易出现错误。

有一个特殊的支持所有操作的图像布局类型:VK_IMAGE_LAYOUT_GENERAL。但它并不保证能为所有操作都带来最佳性能表现。对于一些特殊情况,比如将图像同时用作输入和输出对象,或读取一个已经改变初始化时的布局的图像时,需要使用它。

目前为止,我们编写的包含提交指令操作的辅助函数都被设置为通过等待队列空闲来进行同步。对于实用的程序,更推荐组合多个操作到一个指令缓冲对象,通过异步执行来增大吞吐量,特别对于createTextureImage函数中的变换和数据复制操作,这样做可以获得很大的性能提升。我们可以编写一个叫做setupCommandBuffer的辅助函数来记录指令,编写一个叫做flushSetupCommands来提交执行记录的指令,通过实验得到最佳的同步策略。