您如何正确地将图像布局从传输最佳转换为着色器读取最佳,同时还更改 Vulkan 中的队列所有权?

How do you properly transition the image layout from transfer optimal to shader read optimal while also changing queue ownership in Vulkan?

我目前正在使用 Vulkan API 编写渲染引擎,如果可能的话,我的设置使用不同的传输操作队列而不是图形操作队列。渲染图像时,它们应该在 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL 布局中,但是由于图像当前属于仅标记有传输位的队列,因此我还需要同时将图像的所有权传输到图形队列.

然而,由于某种原因,这似乎失败了,因为即使执行了带有管道屏障的命令缓冲区,图像仍保留在 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL 布局中,导致验证错误。

这是转换图像布局的方法:

int PGraphicsContext::transitionImageLayout(VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout)
{
    VkCommandBuffer cmdBuffer = this->beginTransferCmdBuffer();

    VkImageMemoryBarrier barrier{};
    barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    barrier.oldLayout = oldLayout;
    barrier.newLayout = newLayout;
    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;

    VkPipelineStageFlags srcStage;
    VkPipelineStageFlags dstStage;

    if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
    {
        PApplication::getInstance()->getLogger()->debug("Transitioning image layout from VK_IMAGE_LAYOUT_UNDEFINED to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL");
        barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
        barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
        barrier.srcAccessMask = 0;
        barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;

        srcStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
        dstStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
    }
    else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
    {
        PApplication::getInstance()->getLogger()->debug("Transitioning image layout from VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL");

        if (this->queueFamilies[PQueueIndex::TRANSFER_QUEUE].index != this->queueFamilies[PQueueIndex::GRAPHICS_QUEUE].index)
        {
            barrier.srcQueueFamilyIndex = this->queueFamilies[PQueueIndex::TRANSFER_QUEUE].index;
            barrier.dstQueueFamilyIndex = this->queueFamilies[PQueueIndex::GRAPHICS_QUEUE].index;
        }
        else
        {
            barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
            barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
        }
        barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
        barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

        srcStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
        dstStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
    }
    else
    {
        // TODO: implement this
        this->endTransferCmdBuffer(cmdBuffer);
        PApplication::getInstance()->getLogger()->error("Failed to transition image layout from 0x%X to 0x%X", oldLayout, newLayout);
        return PINE_ERROR_VULKAN_UNSUPPORTED_LAYER_TRANSITION;
    }

    vkCmdPipelineBarrier(cmdBuffer, srcStage, dstStage, {}, 0, nullptr, 0, nullptr, 1, &barrier);

    this->endTransferCmdBuffer(cmdBuffer);
    return PINE_SUCCESS;
}

如果传输队列和图形队列的索引相同,这似乎工作正常,这意味着源和目标队列系列索引都设置为 VK_QUEUE_FAMILY_IGNORED

这是一些日志消息的示例:

[15 JUN 23:17:27][Taiga::DEBUG]: Transitioning image layout from VK_IMAGE_LAYOUT_UNDEFINED to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
[15 JUN 23:17:27][Taiga::DEBUG]: Transitioning image layout from VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL to VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
[15 JUN 23:17:27][Taiga::ERROR]: Validation Error: [ UNASSIGNED-CoreValidation-DrawState-InvalidImageLayout ] Object 0: handle = 0x7f34b4842668, type = VK_OBJECT_TYPE_COMMAND_BUFFER; | MessageID = 0x4dae5635 | Submitted command buffer expects VkImage 0xec4bec000000000b[] (subresource: aspectMask 0x1 array layer 0, mip level 0) to be in layout VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL--instead, current layout is VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL.

仅当 this->queueFamilies[PQueueIndex::TRANSFER_QUEUE].index 是与 this->queueFamilies[PQueueIndex::GRAPHICS_QUEUE].index 不同的队列系列索引时才会出现验证错误。

是否有可能同时转移所有权和过渡布局,或者正确的方法是先记录一个命令缓冲区,该缓冲区仅将图像的所有权从传输转移到图形队列,然后再记录第二个一个实际上转换布局的方法,还是我应该放弃使用单独的传输队列(用于图像)的整个想法?

好的,我找到了问题的解决方案。

文档指出我必须在执行第一个屏障后在另一个队列中发出相同的管道屏障命令,我完全错过了。

所以这是一个可行的解决方案,尽管它远非最佳,因为它在 CPU 上的一个线程上同步运行,并且每次我想转换布局时都会重新创建命令缓冲区。

int PGraphicsContext::beginCmdBuffer(VkCommandBuffer *cmdBuffer, const PQueueIndex queueIndex)
{
    if (queueIndex != PQueueIndex::GRAPHICS_QUEUE && queueIndex != PQueueIndex::TRANSFER_QUEUE)
        return PINE_ERROR_INVALID_ARGUMENT;

    VkCommandBufferAllocateInfo allocInfo{};
    allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
    allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
    allocInfo.commandPool = queueIndex == PQueueIndex::TRANSFER_QUEUE ? this->transferCommandPool : this->graphicsCommandPool;
    allocInfo.commandBufferCount = 1;

    vkAllocateCommandBuffers(this->device, &allocInfo, cmdBuffer);

    VkCommandBufferBeginInfo beginInfo{};
    beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
    beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;

    vkBeginCommandBuffer(*cmdBuffer, &beginInfo);
    return PINE_SUCCESS;
}

int PGraphicsContext::endCmdBuffer(VkCommandBuffer cmdBuffer, const PQueueIndex queueIndex, VkFence fence, const VkSemaphore *waitSemaphore, VkPipelineStageFlags *waitStageFlags, const VkSemaphore *signalSemaphore)
{
    vkEndCommandBuffer(cmdBuffer);

    VkSubmitInfo submitInfo{};
    submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submitInfo.commandBufferCount = 1;
    submitInfo.pCommandBuffers = &cmdBuffer;
    if (waitSemaphore != nullptr)
    {
        submitInfo.waitSemaphoreCount = 1;
        submitInfo.pWaitSemaphores = waitSemaphore;
        submitInfo.pWaitDstStageMask = waitStageFlags;
    }
    if (signalSemaphore != nullptr)
    {
        submitInfo.signalSemaphoreCount = 1;
        submitInfo.pSignalSemaphores = signalSemaphore;
    }

    vkQueueSubmit(this->queueFamilies[queueIndex].queue, 1, &submitInfo, fence);

    return PINE_SUCCESS;
}

int PGraphicsContext::transitionImageLayout(VkImage image, VkImageLayout oldLayout, VkImageLayout newLayout)
{
    VkCommandBuffer cmdBuffer;
    this->beginCmdBuffer(&cmdBuffer);

    VkImageMemoryBarrier barrier{};
    barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
    barrier.oldLayout = oldLayout;
    barrier.newLayout = newLayout;
    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;

    if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
    {
        barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
        barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
        barrier.srcAccessMask = 0;
        barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;

        vkCmdPipelineBarrier(cmdBuffer, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, {}, 0, nullptr, 0, nullptr, 1, &barrier);

        this->endCmdBuffer(cmdBuffer, PQueueIndex::TRANSFER_QUEUE, this->syncFence);
        vkWaitForFences(this->device, 1, &this->syncFence, VK_TRUE, UINT64_MAX);
        vkResetFences(this->device, 1, &this->syncFence);
        vkFreeCommandBuffers(this->device, this->transferCommandPool, 1, &cmdBuffer);
    }
    else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
    {
        barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;

        if (this->queueFamilies[PQueueIndex::TRANSFER_QUEUE].index != this->queueFamilies[PQueueIndex::GRAPHICS_QUEUE].index)
        {
            barrier.dstAccessMask = VK_IMAGE_LAYOUT_UNDEFINED;
            barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
            barrier.srcQueueFamilyIndex = this->queueFamilies[PQueueIndex::TRANSFER_QUEUE].index;
            barrier.dstQueueFamilyIndex = this->queueFamilies[PQueueIndex::GRAPHICS_QUEUE].index;

            VkSemaphoreCreateInfo semaphoreInfo{};
            semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;

            VkSemaphore transferSemaphore;
            if (vkCreateSemaphore(this->device, &semaphoreInfo, this->allocator, &transferSemaphore) != VK_SUCCESS)
            {
                this->endCmdBuffer(cmdBuffer, PQueueIndex::TRANSFER_QUEUE);
                return PINE_ERROR_VULKAN_UNSUPPORTED_LAYER_TRANSITION;
            }

            vkCmdPipelineBarrier(cmdBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, {}, 0, nullptr, 0, nullptr, 1, &barrier);

            this->endCmdBuffer(cmdBuffer, PQueueIndex::TRANSFER_QUEUE, VK_NULL_HANDLE, nullptr, nullptr, &transferSemaphore);

            barrier.srcAccessMask = VK_IMAGE_LAYOUT_UNDEFINED;
            barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
            VkCommandBuffer queueBuffer;
            this->beginCmdBuffer(&queueBuffer, PQueueIndex::GRAPHICS_QUEUE);
            vkCmdPipelineBarrier(queueBuffer, VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, {}, 0, nullptr, 0, nullptr, 1, &barrier);

            VkPipelineStageFlags flags = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
            this->endCmdBuffer(queueBuffer, PQueueIndex::GRAPHICS_QUEUE, this->syncFence, &transferSemaphore, &flags, nullptr);
            vkWaitForFences(this->device, 1, &this->syncFence, VK_TRUE, UINT64_MAX);
            vkResetFences(this->device, 1, &this->syncFence);

            vkFreeCommandBuffers(this->device, this->transferCommandPool, 1, &cmdBuffer);
            vkFreeCommandBuffers(this->device, this->graphicsCommandPool, 1, &queueBuffer);

            vkDestroySemaphore(this->device, transferSemaphore, this->allocator);
        }
        else
        {
            barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
            barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;

            vkCmdPipelineBarrier(cmdBuffer, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, {}, 0, nullptr, 0, nullptr, 1, &barrier);

            this->endCmdBuffer(cmdBuffer, PQueueIndex::TRANSFER_QUEUE, this->syncFence);
            vkWaitForFences(this->device, 1, &this->syncFence, VK_TRUE, UINT64_MAX);
            vkResetFences(this->device, 1, &this->syncFence);
            vkFreeCommandBuffers(this->device, this->transferCommandPool, 1, &cmdBuffer);
        }
    }
    else
    {
        // TODO: implement this
        this->endCmdBuffer(cmdBuffer, PQueueIndex::TRANSFER_QUEUE, this->syncFence);
        vkWaitForFences(this->device, 1, &this->syncFence, VK_TRUE, UINT64_MAX);
        vkResetFences(this->device, 1, &this->syncFence);
        vkFreeCommandBuffers(this->device, this->transferCommandPool, 1, &cmdBuffer);
        PApplication::getInstance()->getLogger()->error("Failed to transition image layout from 0x%X to 0x%X", oldLayout, newLayout);
        return PINE_ERROR_VULKAN_UNSUPPORTED_LAYER_TRANSITION;
    }

    return PINE_SUCCESS;
}

我现在添加了在结束命令缓冲区时向队列提交操作提供等待和信号量的功能。在 transitionImageLayout() 方法中,当我还需要更改所有权时,我现在使用它来创建一个信号量,当传输队列完成管道屏障时,它会发出信号。然后我还在图形队列上创建第二个命令缓冲区,它在运行相同的管道屏障命令之前等待信号量。