如何在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()方法中,当我还需要更改所有权时,我现在使用它来创建一个信号量,当使用管道屏障完成传输队列时,就会发出信号。然后,我还在图形队列上创建第二个命令缓冲区,该缓冲区在信号量运行相同的管道屏障命令之前等待信号量。

最新更新