Reputation: 181
I'm currently writing a rendering engine with the Vulkan API and my setup uses a different queue for transfer operations than for graphics operations if possible. When rendering images, they should be in the VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL
layout, however since the image is currently owned by a queue that is only flagged with the transfer bit, i also need to transfer the ownership of the image to the graphics queue simultaneously.
This however seems to fail for some reason, as even though the command buffer with the pipeline barrier is executed, the image remains in the VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
layout, resulting in a validation error.
This is the method which transitions image layouts:
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;
}
This seems to work fine if the index of the transfer queue and the graphics queue are the same, meaning the source and destination queue family indices are both set to VK_QUEUE_FAMILY_IGNORED
.
Here is an example of some log messages:
[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.
The validation error only occurs if this->queueFamilies[PQueueIndex::TRANSFER_QUEUE].index
is a different queue family index than this->queueFamilies[PQueueIndex::GRAPHICS_QUEUE].index
.
Is it even possible to transfer ownership and transition layout at the same time or would the correct way be to first record a command buffer that only transfers the ownership of the image from the transfer to the graphics queue and then a second one which actually transtitions the layout or should I scrap the whole idea of using a separate transfer queues (for images)?
Upvotes: 2
Views: 3121
Reputation: 181
Ok, I found the solution to my problem.
The documentation states that I have to issue the same pipeline barrier command in the other queue as well after the first barrier was executed, which I completely missed.
So here is a solution that works, although it is far from optimal because it runs synchronously on one thread on the CPU and recreates command buffers everytime I want to transition a layout.
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;
}
I have now added the ability to provide a wait and signal semaphore to the queue submit operation when ending a command buffer. In the transitionImageLayout()
method I now use this to create a semaphore when I also need to change ownership which is signaled when the transfer queue is done with the pipeline barrier. I then also creates a second command buffer on the graphics queue which waits for the semaphore before it runs the same pipeline barrier command.
Upvotes: 0