I’m struggling to understand why the validation layer is reporting write-after-write hazards when synchronization involves dependency chains. A simple way to explain is with a transfer command and explicit image barriers. Start with an image in VK_IMAGE_LAYOUT_UNDEFINED
. We want to transition to VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
, transfer the contents from a buffer, then transition to VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
:
VkImageSubresourceRange imageSubresourceRange = {
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.baseMipLevel = 0,
.levelCount = 1,
.baseArrayLayer = 0,
.layerCount = 1,
};
VkImageMemoryBarrier preTransferImageMemoryBarrier = {
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_NONE,
.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = image
.subresourceRange = imageSubresourceRange,
};
// [1]
vkCmdPipelineBarrier(commandBuffer,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
0,
0, nullptr,
0, nullptr,
1, &preTransferImageMemoryBarrier);
VkImageSubresourceLayers imageSubresource = {
.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
.mipLevel = 0,
.baseArrayLayer = 0,
.layerCount = 1,
};
VkBufferImageCopy region = {
.bufferOffset = 0,
.bufferRowLength = 0,
.bufferImageHeight = 0,
.imageSubresource = imageSubresource,
.imageOffset = {0, 0, 0},
.imageExtent = {IMAGE_WIDTH, IMAGE_HEIGHT, 1},
};
// [2]
vkCmdCopyBufferToImage(commandBuffer, stagingBuffer, image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion);
VkImageMemoryBarrier postTransferImageMemoryBarrier = {
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_NONE,
.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = image
.subresourceRange = imageSubresourceRange,
};
// [3]
vkCmdPipelineBarrier(commandBuffer,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
0,
0, nullptr,
0, nullptr,
1, &postTransferImageMemoryBarrier);
The code above behaves as expected:
- [1]
vkCmdPipelineBarrier()
- Transition image fromVK_IMAGE_LAYOUT_UNDEFINED
toVK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
(layout transition finishes and memory is made visible forVK_ACCESS_TRANSFER_WRITE
beforeVK_PIPELINE_STAGE_TRANSFER
). - [2]
vkCmdCopyBufferToImage()
- Copy the staging buffer to image. - [3]
vkCmdPipelineBarrier()
- Transition image fromVK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
toVK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
(copying finishes andVK_ACCESS_TRANSFER_WRITE
memory is made available before the layout transition).
Now, I assume that memory dependencies can be chained, provided there is an intersection between the 2nd scope of the first dependency and the 1st scope of the second dependency. So barrier [3] may be split into two, with VK_PIPELINE_STAGE_TRANSFER
serving as intersection:
VkImageMemoryBarrier postTransferImageMemoryBarrierA = {
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
.dstAccessMask = VK_ACCESS_NONE,
.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = image
.subresourceRange = imageSubresourceRange,
};
// [3a]
vkCmdPipelineBarrier(commandBuffer,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
0,
0, nullptr,
0, nullptr,
1, &postTransferImageMemoryBarrierA);
VkImageMemoryBarrier postTransferImageMemoryBarrierB = {
.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
.srcAccessMask = VK_ACCESS_NONE,
.dstAccessMask = VK_ACCESS_NONE,
.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
.image = image
.subresourceRange = imageSubresourceRange,
};
// [3b]
vkCmdPipelineBarrier(commandBuffer,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT,
0, /* dependencyFlags */
0, nullptr,
0, nullptr,
1, &postTransferImageMemoryBarrierB);
Expected behavior:
- [3a]
vkCmdPipelineBarrier()
- Wait for copy to finish (execution finishes andVK_ACCESS_TRANSFER_WRITE
memory is made available beforeVK_PIPELINE_STAGE_TRANSFER
). - [3b]
vkCmdPipelineBarrier()
- Transition image fromVK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL
toVK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
(should form a dependency chain throughVK_PIPELINE_STAGE_TRANSFER
, so copying has finished and memory is available before the layout transition).
But now we see:
// Validation Error : [SYNC - HAZARD - WRITE - AFTER - WRITE] Object 0 : handle = 0xcb3ee80000000007, type = VK_OBJECT_TYPE_IMAGE; | MessageID = 0x5c0ec5d6 | vkCmdPipelineBarrier: Hazard WRITE_AFTER_WRITE for image barrier 0 VkImage 0xcb3ee80000000007[]. Access info (usage: SYNC_IMAGE_LAYOUT_TRANSITION, prior_usage: SYNC_COPY_TRANSFER_WRITE, write_barriers: 0, command: vkCmdCopyBufferToImage, seq_no: 2, reset_no: 1).
So it appears my reasoning is flawed – the final layout transition doesn’t wait for the transfer writes to be made available. Why is that?