您如何正确地将图像布局从传输优化转换为着色器读取优化,同时更改 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,{},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,{},nullptr,&barrier);

        this->endCmdBuffer(cmdBuffer,PQueueIndex::TRANSFER_QUEUE,this->syncFence);
        vkWaitForFences(this->device,&this->syncFence,VK_TRUE,UINT64_MAX);
        vkResetFences(this->device,&this->syncFence);
        vkFreeCommandBuffers(this->device,this->transferCommandPool,&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_BOTTOM_OF_PIPE_BIT,&barrier);

            this->endCmdBuffer(cmdBuffer,VK_NULL_HANDLE,&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_FRAGMENT_SHADER_BIT,&barrier);

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

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

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

            vkCmdPipelineBarrier(cmdBuffer,this->syncFence);
            vkWaitForFences(this->device,&this->syncFence);
            vkFreeCommandBuffers(this->device,&cmdBuffer);
        }
    }
    else
    {
        // TODO: implement this
        this->endCmdBuffer(cmdBuffer,&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() 方法中,我现在使用它来创建一个信号量,当我还需要更改所有权时,当传输队列完成管道屏障时发出信号。然后我还在图形队列上创建了第二个命令缓冲区,它在运行相同的管道屏障命令之前等待信号量。