[RenderPass] Image Layout Transition doens't work?

I got an error message below, but I don’t know why this occur. Is there anyone who is familiar with the validation layer and is able to know what invokes this?

1303270965 - UNASSIGNED-CoreValidation-DrawState-InvalidImageLayout: Validation Error: [ UNASSIGNED-CoreValidation-DrawState-InvalidImageLayout ] Object 0: handle = 0x1ec15f6f090, type = VK_OBJECT_TYPE_COMMAND_BUFFER; | MessageID = 0x4dae5635 | Submitted command buffer expects VkImage 0x421a0f0000000074[] (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_UNDEFINED.

What I did are:

  1. Create a color image and depth image with their relative image views and samples.
  2. Create an offline render pass then create a frame buffer with it and the views above.
  3. Use the color view, color sampler, and color image layout VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL to create an Image Descriptor.
  4. Allocate a descriptor set and update the write descriptor with the image descriptor.

Then:

// Offline render
vkBeginCommandBuffer(drawCommandbuffer[i]);
vkBeginRenderPass()
// draw something to this render pass
vkEndRenderPass()
vkEndCommandBuffer(drawCommandbuffer[i]);

// Present Render
vkBeginCommandBuffer(drawCommandbuffer[i]);
vkBeginRenderPass();
// Using the descriptor Set allocated above here to display the color attachment as a texture for viewport display.
VkEndRenderPass();
vkEndCommandBuffer(drawCommandbuffer[i]);

Is something important missing or what I am wrong? This problem has interrupted me for a long long time and I have no idea how to get it resolved for I am brand-new to Vulkan.

Below is how I create my render pass:

    std::array<VkAttachmentDescription, 2> attachments{};

    auto &colorAttachment          = attachments[0];
    colorAttachment.format         = colorFormat;
    colorAttachment.samples        = VK_SAMPLE_COUNT_1_BIT;
    colorAttachment.loadOp         = VK_ATTACHMENT_LOAD_OP_CLEAR;
    colorAttachment.storeOp        = VK_ATTACHMENT_STORE_OP_STORE;
    colorAttachment.stencilLoadOp  = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
    colorAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
    colorAttachment.initialLayout  = VK_IMAGE_LAYOUT_UNDEFINED;
    colorAttachment.finalLayout    = isPresent ? VK_IMAGE_LAYOUT_PRESENT_SRC_KHR : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

    auto &depthAttachment          = attachments[1];
    depthAttachment.format         = depthFormat;
    depthAttachment.samples        = VK_SAMPLE_COUNT_1_BIT;
    depthAttachment.loadOp         = VK_ATTACHMENT_LOAD_OP_CLEAR;
    depthAttachment.storeOp        = VK_ATTACHMENT_STORE_OP_DONT_CARE;
    depthAttachment.stencilLoadOp  = VK_ATTACHMENT_LOAD_OP_CLEAR;
    depthAttachment.stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
    depthAttachment.initialLayout  = VK_IMAGE_LAYOUT_UNDEFINED;
    depthAttachment.finalLayout    = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

    VkAttachmentReference colorReference{};
    colorReference.attachment = 0;
    colorReference.layout     = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;

    VkAttachmentReference depthReference{};
    depthReference.attachment = 1;
    depthReference.layout     = VK_IMAGE_LAYOUT_DEPTH_STENCIL_ATTACHMENT_OPTIMAL;

    VkSubpassDescription subpassDescription{};
    subpassDescription.pipelineBindPoint       = VK_PIPELINE_BIND_POINT_GRAPHICS;
    subpassDescription.colorAttachmentCount    = 1;
    subpassDescription.pColorAttachments       = &colorReference;
    subpassDescription.pDepthStencilAttachment = &depthReference;
    subpassDescription.inputAttachmentCount    = 0;
    subpassDescription.pInputAttachments       = nullptr;
    subpassDescription.preserveAttachmentCount = 0;
    subpassDescription.pPreserveAttachments    = nullptr;
    subpassDescription.pResolveAttachments     = nullptr;

    std::array<VkSubpassDependency, 2> dependencies{};

    dependencies[0].srcSubpass      = VK_SUBPASS_EXTERNAL;
    dependencies[0].dstSubpass      = 0;
    dependencies[0].srcStageMask    = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
    dependencies[0].dstStageMask    = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    dependencies[0].srcAccessMask   = VK_ACCESS_SHADER_READ_BIT;
    dependencies[0].dstAccessMask   = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    dependencies[0].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;

    dependencies[1].srcSubpass      = 0;
    dependencies[1].dstSubpass      = VK_SUBPASS_EXTERNAL;
    dependencies[1].srcStageMask    = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
    dependencies[1].dstStageMask    = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
    dependencies[1].srcAccessMask   = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
    dependencies[1].dstAccessMask   = VK_ACCESS_SHADER_READ_BIT;
    dependencies[1].dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT;

    VkRenderPassCreateInfo createInfo{};
    createInfo.sType           = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO;
    createInfo.pNext           = nullptr;
    createInfo.attachmentCount = U32(attachments.size());
    createInfo.pAttachments    = attachments.data();
    createInfo.subpassCount    = 1;
    createInfo.pSubpasses      = &subpassDescription;
    createInfo.dependencyCount = U32(dependencies.size());
    createInfo.pDependencies   = dependencies.data();

    Check(device->Create(&createInfo, nullptr, &handle));

It is a very common issue for newbies (you can search for lot of these here and stack overflow with the VUID). The error message is relatively straightforward, but often hard to understand for some reason. Maybe because of the async nature, while validation need to be somewhat synchronous.

So, Vulkan needs an image in VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, which I assume is added with a descriptor as a texture imput.

But the validation layers could not find it in this layout when the command buffer is to be executed. Instead last time the validation layers could be sure what the layout was is when it was VK_IMAGE_LAYOUT_UNDEFINED (so likely when the image was created).

That means you either forgot to change the layout inbetween. Forgot to submit the command buffer that changes the layout. Or the layout change is not properly synchronized with the target usage.

Prose is not code, so I can’t be sure what the exact problem is. Just show your command buffer recording code and both render pass creation. Avoid prose and pseudocode; it never shows the bugs, only author’s intentions.

Hi, Thanks for your answer.
The two render pass creation is identical above, except that the swap chain will use the present image layout.

colorAttachment.finalLayout    = isPresent ? VK_IMAGE_LAYOUT_PRESENT_SRC_KHR : VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
    template <class T>
    void Record(T &&process = [](auto, auto)->void {}, CommandBuffer::Usage usage = CommandBuffer::Usage::OneTimeSubmit)
    {
        auto cmdbuf = GetCommandBuffer();
        Check(cmdbuf->Begin(usage));
        {
            VkRenderPassBeginInfo beginInfo = {};
            beginInfo.sType       = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
            beginInfo.pNext       = nullptr;
            beginInfo.renderPass  = swapchainRenderPass;
            beginInfo.framebuffer = *swapchainFramebuffer;
            process(cmdbuf, &beginInfo);
        }
        Check(cmdbuf->End());
    }
    
    template <class T>
    void Begin(T &&postProcess = [](auto) -> void {}, CommandBuffer::Usage usage = CommandBuffer::Usage::OneTimeSubmit)
    {
        auto cmdBuf = GetCommandBuffer();
        Check(cmdBuf->Begin(usage));
        postProcess(cmdBuf);
    }

    template <class T>
    void End(T &&preprocess = [](auto) -> void {})
    {
        auto cmdbuf = GetCommandBuffer();
        preprocess(cmdbuf);
        Check(cmdbuf->End());
    }

/* recore */
    
    auto nativeRenderTarget = std::dynamic_pointer_cast<RenderTarget>(renderTarget);
    static VkClearValue clearValues[2];
    clearValues[0].color = { 0.0f, 0.0f, 0.0f, 0.0f };
    clearValues[1].depthStencil = { 0.0f, 100 };

    context->Begin([&](CommandBuffer *cmdbuf) {
        auto &desc = nativeRenderTarget->Desc();

        VkRenderPassBeginInfo beginInfo{};
        beginInfo.sType                    = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
        beginInfo.pNext                    = nullptr;
        beginInfo.framebuffer           = nativeRenderTarget->GetFramebuffer();
        beginInfo.clearValueCount   = 2;
        beginInfo.pClearValues        = clearValues;
        beginInfo.renderPass           = nativeRenderTarget->GetRenderPass();
        beginInfo.renderArea.extent.width  = desc.Width;
        beginInfo.renderArea.extent.height = desc.Height;
        beginInfo.renderArea.offset        = { 0, 0 };

        cmdbuf->BeginRenderPass(&beginInfo, VK_SUBPASS_CONTENTS_INLINE);
        cmdbuf->SetViewport(ncast<float>(desc.Width), ncast<float>(desc.Height));
        cmdbuf->SetScissor(desc.Width, desc.Height);
    });


    context->End([&](CommandBuffer *cmdbuf) {
        cmdbuf->EndRenderPass();
    });

context->Record([&](CommandBuffer *cmdbuf, VkRenderPassBeginInfo *beginInfo) -> void {
        beginInfo->clearValueCount          = 2;
        beginInfo->pClearValues             = clearValues;
        beginInfo->renderArea.extent.width  = io.DisplaySize.x;
        beginInfo->renderArea.extent.height = io.DisplaySize.y;
        beginInfo->renderArea.offset        = { 0, 0 };

        cmdbuf->BeginRenderPass(beginInfo, VK_SUBPASS_CONTENTS_INLINE);

        ImGui_ImplVulkan_RenderDrawData(primaryDrawData, *cmdbuf);

        cmdbuf->EndRenderPass();
        });

This is the Command Buffer recording code.

That means you either forgot to change the layout inbetween. Forgot to submit the command buffer that changes the layout. Or the layout change is not properly synchronized with the target usage.

I have a question here. Does the image layout need to change manually? I follow this sample: Vulkan/offscreen.cpp at master · SaschaWillems/Vulkan (github.com), where there is a comment saying it’s no need to Explicit synchronization is not required between the render pass. Should I need to use an image barrier to transition the image layout?

Vulkan is an explicit API, so the likelihood something has to be done manually is very high. The layout has to be changed by either barriers or render pass settings.

Hehe, I think you like them lambdas too much. You come from functional programming?

Let’s see if I can decode it:

VkClearValue clearValues[2];
clearValues[0].color = { 0.0f, 0.0f, 0.0f, 0.0f };
clearValues[1].depthStencil = { 0.0f, 100 };

auto cmdBuf = GetCommandBuffer(); // hm, what does this return?
Check(cmdBuf->Begin(Usage::OneTimeSubmit));

VkRenderPassBeginInfo beginInfo{};
beginInfo.sType                    = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
beginInfo.pNext                    = nullptr;
beginInfo.framebuffer           = nativeRenderTarget->GetFramebuffer();
beginInfo.clearValueCount   = 2;
beginInfo.pClearValues        = clearValues;
beginInfo.renderPass           = nativeRenderTarget->GetRenderPass();
beginInfo.renderArea.extent.width  = nativeRenderTarget->Desc().Width;
beginInfo.renderArea.extent.height = nativeRenderTarget->Desc().Height;
beginInfo.renderArea.offset        = { 0, 0 };

cmdbuf->BeginRenderPass(&beginInfo, VK_SUBPASS_CONTENTS_INLINE);
cmdbuf->SetViewport(ncast<float>(desc.Width), ncast<float>(desc.Height));
cmdbuf->SetScissor(desc.Width, desc.Height);

auto cmdbuf = GetCommandBuffer(); // is it even the same command buffer than we begun?
cmdbuf->EndRenderPass();
Check(cmdbuf->End());

auto cmdbuf = GetCommandBuffer();            // if it is the same command buffer,
Check(cmdbuf->Begin(Usage::OneTimeSubmit));  // I am now destroying it by second begin

VkRenderPassBeginInfo beginInfo = {};
beginInfo.sType       = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
beginInfo.pNext       = nullptr;
beginInfo.renderPass  = swapchainRenderPass;
beginInfo.framebuffer = *swapchainFramebuffer;
beginInfo->clearValueCount          = 2;
beginInfo->pClearValues             = clearValues;
beginInfo->renderArea.extent.width  = io.DisplaySize.x;
beginInfo->renderArea.extent.height = io.DisplaySize.y;
beginInfo->renderArea.offset        = { 0, 0 };

cmdbuf->BeginRenderPass(beginInfo, VK_SUBPASS_CONTENTS_INLINE);

ImGui_ImplVulkan_RenderDrawData(primaryDrawData, *cmdbuf);

cmdbuf->EndRenderPass();

Check(cmdbuf->End());

So, how does GetCommandBuffer() work? If it returns the same command buffer, then it has two Begins, which would destroy the previous recording. If it returns a different command buffer each time, then it has mismatched End().

Nope. Actually, I use C all the time in my daily life, but I am really fond of javascript. XD

You’re right. I used two begin and two ends for the same command buffer. I just think to use the two begin and end, and the commands would be appended to the former. Alright, it will clear the previous.

I am much appreciated that you spend the time to analyze my codes. Not know how to express it further in English. Anyway, Have a beautiful day ahead. Thank you! :grinning: