0

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;
}
McFlyboy
  • 23
  • 1
  • 1
  • 9
  • I don't see anywhere that you are setting the layout of these images. – Nicol Bolas Aug 16 '22 at 01:46
  • @NicolBolas Added my initialization of the renderpass in my question above – McFlyboy Aug 16 '22 at 08:35
  • I have more or less concluded that it must be some sort of bug in the validation layers. By just creating an extra semaphore without even using it, I managed to make the second swap work without error. Then I added a third semaphore and now I don't have any validation errors... O_o – McFlyboy Aug 16 '22 at 14:58
  • Your misuse of the API is not a bug in the validation layer. – Nicol Bolas Aug 16 '22 at 15:04
  • I know that what I'm doing is not how anyone should do synchronization here, but I want to atleast understand why doing it like this wouldn't work, and more curiously why the error only shows up twice, and disappears once I've created semaphores when I'm not using them. What I'm trying to say is that I want to understand and learn from this – McFlyboy Aug 16 '22 at 17:25

2 Answers2

0

Outside of specific circumstances or explicit synchronization, operations on the GPU execute in an arbitrary order.

Your graphics submission and your queue presentation have no synchronization between them. Therefore, they can execute in whatever order the implementation wants regardless of when you issue them.

However, since the graphics operation acts on an object which the presentation operation uses, there is a de-facto dependency. The graphics operation must act first. But you have no actual synchronization to enforce that dependency.

Hence the validation errors. You need to ensure that the queue presentation happens after the rendering operation.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Yes, I am aware that there needs to be synchronization to make sure that the graphics operations finish before presentation happens, but wouldn't calling `waitIdle()` on the device inbetween act as a form of synchronization here? Wouldn't it block execution on the CPU and therefore making the CPU have to wait with issuing the order to present until all work on the GPU was finished? – McFlyboy Aug 16 '22 at 17:14
  • @McFlyboy: "*wouldn't calling waitIdle() on the device inbetween act as a form of synchronization here?*" I don't know; what does the specification say about the synchronization behavior between submit and presentation when there's an idle between them? I mean, I could go sifting through the specification to find out, but to be honest, nobody should *ever* do this. There's absolutely no reason not to express the dependency via a semaphore. – Nicol Bolas Aug 16 '22 at 17:16
  • I know, but after I posted this problem I tried using a semaphore to do the synchronization instead, but weirdly enough I still get the errors, but only if I create the semaphore after creating the swapchain. If I create it before swapchain creation then it solves one of the two errors. If I create another semaphore before swapchain creation as well then both errors disappear. So I get the impression that the answer is a bit more complicated than just "use a semaphore". Again, I intend to use semaphores, I just want to know what is going on with this weird behaviour with the layers – McFlyboy Aug 16 '22 at 17:38
0

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

McFlyboy
  • 23
  • 1
  • 1
  • 9
  • UPDATE: I have now found the bug in my code, and after fixing it the errors go away. I have described the cause of the bug and how I fixed it in the issue linked above – McFlyboy Jul 05 '23 at 14:09