c++renderingvulkanvalidation-layers

Vulkan validation errors states my images are in the wrong layout, but only the two first times I present images from the swapchain


I have loosely been following this guide to setup some simple rendering in Vulkan using the raii headers in Vulkan-Hpp. I have skipped the Graphics Pipeline Basics section (except for the render pass chapter) just to see if I'm able to only get the render pass working and presenting images from the swapchain.

I am now at the point where I can clear the current swapchain image to a certain color and present it. However, this fails for the first two frames that I try to present, and after that it runs smoothly without any validation errors. I am completely stumped at why this is happening, so I'm just gonna give the details of what I know and what I have tried and hopefully someone might know the answer here.

The error I get for the first two frames is as follows:

Validation Error: [ VUID-VkPresentInfoKHR-pImageIndices-01296 ] Object 0: handle = 0x1f5d50ee1e0, type = VK_OBJECT_TYPE_QUEUE; | MessageID = 0xc7aabc16 | vkQueuePresentKHR(): pSwapchains[0] images passed to present must be in layout VK_IMAGE_LAYOUT_PRESENT_SRC_KHR or VK_IMAGE_LAYOUT_SHARED_PRESENT_KHR but is in VK_IMAGE_LAYOUT_UNDEFINED. The Vulkan spec states: Each element of pImageIndices must be the index of a presentable image acquired from the swapchain specified by the corresponding element of the pSwapchains array, and the presented image subresource must be in the VK_IMAGE_LAYOUT_PRESENT_SRC_KHR layout at the time the operation is executed on a VkDevice (https://github.com/KhronosGroup/Vulkan-Docs/search?q=)VUID-VkPresentInfoKHR-pImageIndices-01296)

This makes it seem like there might be a synchronization issue for the first two frames or something. Since I'm still doing early testing with Vulkan I'm just using device.waitIdle() instead of proper synchronization with semaphores and fences. I know that usage of waitIdle is a slow solution, but I thought it would at least serve to keep things synchronized, so I'm not sure if it is a synchronization issue.

My swapchain has 3 images, so if it was a problem with presenting images in the first round through the images then I should have gotten three errors...

The presentKHR function returns vk::Result::Success even on the first two frames. I have also tried turning off validation layers, and when I do so the first two frames are able to be presented, so it might be a bug in the validation layers?

Some of my initialization code:

// After swapchain creation

auto images = m_swapchain.getImages();

for (auto& image : images) {
    m_images.emplace(image, createImageView(image));
}

m_renderPass = createRenderPass();

m_frameBuffers.reserve(m_images.size());

for (auto& [image, imageView] : m_images) {
    m_frameBuffers.push_back(createFrameBuffer(imageView));
}

auto [result, imageIndex] = m_swapchain.acquireNextImage(
            std::numeric_limits<uint64_t>().max(),
            *m_imageAvailableSemaphore
        );
// I use a semaphore here because the Vulkan spec states that I must use a semaphore or fence here

m_imageIndex = imageIndex;


// Functions called above

vk::raii::ImageView Swapchain::createImageView(const vk::Image& image) const {
    try {
        return m_context.getDevice().createImageView(
            vk::ImageViewCreateInfo{
                .flags            = {},
                .image            = image,
                .viewType         = vk::ImageViewType::e2D,
                .format           = m_surfaceFormat.format,
                .components       = vk::ComponentMapping{
                    .r = vk::ComponentSwizzle::eIdentity,
                    .g = vk::ComponentSwizzle::eIdentity,
                    .b = vk::ComponentSwizzle::eIdentity,
                    .a = vk::ComponentSwizzle::eIdentity
                },
                .subresourceRange = vk::ImageSubresourceRange{
                    .aspectMask     = vk::ImageAspectFlagBits::eColor,
                    .baseMipLevel   = 0,
                    .levelCount     = 1,
                    .baseArrayLayer = 0,
                    .layerCount     = 1
                }
            }
        );
    }
    catch (const std::exception& e) {
        // Error handling...
    }
}

vk::raii::RenderPass Swapchain::createRenderPass() const {
    auto attachments = std::array{
        vk::AttachmentDescription{
            .flags          = {},
            .format         = m_surfaceFormat.format,
            .samples        = vk::SampleCountFlagBits::e1,
            .loadOp         = vk::AttachmentLoadOp::eClear,
            .storeOp        = vk::AttachmentStoreOp::eStore,
            .stencilLoadOp  = vk::AttachmentLoadOp::eDontCare,
            .stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
            .initialLayout  = vk::ImageLayout::eUndefined,
            .finalLayout    = vk::ImageLayout::ePresentSrcKHR
        }
    };

    auto attachmentReferences = std::array{
        vk::AttachmentReference{
            .attachment = 0,
            .layout     = vk::ImageLayout::eColorAttachmentOptimal
        }
    };

    auto subpasses = std::array{
        vk::SubpassDescription{
            .flags                   = {},
            .pipelineBindPoint       = vk::PipelineBindPoint::eGraphics,
            .inputAttachmentCount    = 0,
            .pInputAttachments       = nullptr,
            .colorAttachmentCount    = static_cast<uint32_t>(attachmentReferences.size()),
            .pColorAttachments       = attachmentReferences.data(),
            .pResolveAttachments     = nullptr,
            .pDepthStencilAttachment = nullptr,
            .preserveAttachmentCount = 0,
            .pPreserveAttachments    = nullptr
        }
    };

    try {
        return m_context.getDevice().createRenderPass(
            vk::RenderPassCreateInfo{
                .flags           = {},
                .attachmentCount = static_cast<uint32_t>(attachments.size()),
                .pAttachments    = attachments.data(),
                .subpassCount    = static_cast<uint32_t>(subpasses.size()),
                .pSubpasses      = subpasses.data(),
                .dependencyCount = 0,
                .pDependencies   = nullptr
            }
        );
    }
    catch (const std::exception& e) {
        // Error handling...
    }
}

vk::raii::Framebuffer Swapchain::createFrameBuffer(const vk::raii::ImageView& imageView) const {
    try {
        return m_context.getDevice().createFramebuffer(
            vk::FramebufferCreateInfo{
                .flags           = {},
                .renderPass      = *m_renderPass,
                .attachmentCount = 1,
                .pAttachments    = &*imageView,
                .width           = m_imageExtent.width,
                .height          = m_imageExtent.height,
                .layers          = 1
            }
        );
    }
    catch (const std::exception& e) {
        // Error handling...
    }
}

Rendering code executed every frame:

// The actual render function called every frame

void Renderer::render() {
    m_context->recordCommands(
        [&]() {
            m_swapchain->beginRenderPassCommand(0.125f, 0.125f, 0.125f);
            m_swapchain->endRenderPassCommand();
        }
    );

    m_context->submitRecording();

    m_swapchain->swap();
}

void GraphicsContext::recordCommands(const Application::Recording& recording) {
    m_device.waitIdle();

    m_commandBuffer.reset();
    m_commandBuffer.begin(
        vk::CommandBufferBeginInfo{
            .flags            = {},
            .pInheritanceInfo = {}
        }
    );

    recording();

    m_commandBuffer.end();
}

void Swapchain::beginRenderPassCommand(
        float clearColorRed,
        float clearColorGreen,
        float clearColorBlue
) {
    auto clearValues = std::array{
        vk::ClearValue(
            vk::ClearColorValue(
                std::array{
                    clearColorRed,
                    clearColorGreen,
                    clearColorBlue,
                    1.0f
                }
            )
        )
    };

    m_context.getCommandBuffer().beginRenderPass(
        vk::RenderPassBeginInfo{
            .renderPass      = *m_renderPass,
            .framebuffer     = *m_frameBuffers[m_imageIndex],
            .renderArea      = vk::Rect2D{
                .offset = vk::Offset2D{
                    .x = 0,
                    .y = 0
                },
                .extent = m_imageExtent
            },
            .clearValueCount = static_cast<uint32_t>(clearValues.size()),
            .pClearValues    = clearValues.data()
        },
        vk::SubpassContents::eInline
    );
}

void Swapchain::endRenderPassCommand() {
    m_context.getCommandBuffer().endRenderPass();
}

void GraphicsContext::submitRecording() {
    m_device.waitIdle();

    m_graphicsQueue.submit(
        vk::SubmitInfo{
            .waitSemaphoreCount   = 0,
            .pWaitSemaphores      = nullptr,
            .pWaitDstStageMask    = nullptr,
            .commandBufferCount   = 1,
            .pCommandBuffers      = &*m_commandBuffer,
            .signalSemaphoreCount = 0,
            .pSignalSemaphores    = nullptr
        }
    );
}

void Swapchain::swap() {
    m_context.getDevice().waitIdle();

    auto presentResult = m_context.getPresentQueue().presentKHR(
        vk::PresentInfoKHR{
            .waitSemaphoreCount = 0,
            .pWaitSemaphores    = nullptr,
            .swapchainCount     = 1,
            .pSwapchains        = &*m_swapchain,
            .pImageIndices      = &m_imageIndex,
            .pResults           = nullptr
        }
    );

    m_context.getDevice().waitIdle();

    auto [result, imageIndex] = m_swapchain.acquireNextImage(
        std::numeric_limits<uint64_t>().max(),
        *m_imageAvailableSemaphore
    );

    m_imageIndex = imageIndex;
}

Solution

  • I have now gotten confirmation that this issue is caused by a bug in the validation layers. Here's the issue on Github: https://github.com/KhronosGroup/Vulkan-ValidationLayers/issues/4422