SDL Vulkan window crashes on resize (before event handling).

Yeah, as I mentioned, apparently you can’t solely rely on getting VK_ERROR_OUT_OF_DATE_KHR to know when to rebuild the swapchain, which is why you have to track when to rebuild it yourself

My brain is officially fried… so…

  1. I created a function to catch when the window size changes:
bool VulkanEngine::staleWindow()
{
    int w, h;
    SDL_GetWindowSize(mWindow, &w, &h);
    std::cout << w << "x" << h << std::endl;
    return (mWindowExtent.width != w) || (mWindowExtent.height != h);
}

2 I check to see if my function works:

    VkResult result = vkAcquireNextImageKHR(mDevice, mSwapchain, 1000000000, getCurrentFrame().mPresentSemaphore, nullptr, &swapchainImageIndex);
    if (result != VK_SUCCESS)
    {
       std::cout << __LINE__ << " - " <<__FILE__ << "\n" << "\n" << "\n" << "\n" << "\n" << "\n" << "\n";
       while(1) {}
       //rebuild_swap_chain();
    }
    else
    {
        std::cout << "everything is just fine" << std::endl;
    }

    if(staleWindow())
    {
        std::cout << "window is stale" << std::endl;
    }

And the output is…

1700x900
456 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
458 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
460 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
everything is just fine
1700x900
456 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
458 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
460 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
everything is just fine
1700x900
456 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
458 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
Detected Vulkan error: -1000001004
459
Aborted (core dumped)

THE GEOMETRY NEVER CHANGED!!! :crazy_face: :exploding_head:

FYI: SDL_GetWindowSize() is not always the correct way of getting the surface size.

I’d use SDL_Vulkan_GetDrawableSize(), which will handle High-DPI systems correctly. Even then — particularly under X11 — the window size seen by SDL and Vulkan can be slightly out-of-sync, so you’ll need to clamp it to the values given in the Vulkan surface capabilities.

See the testvulkan program for an example.

SDL_Vulkan_GetDrawableSize( ) still spits out 1700x900

The swap chain can become invalid for other reasons.

Also, it’s valid (not an error you should be calling abort() over) for vkQueuePresentKHR() to return VK_ERROR_OUT_OF_DATE_KHR and it means you need to rebuild the swap chain then too.

So:
Check if a resize happened before trying to acquire the next image (rebuild the swap chain if it did)

Acquire the next image (rebuild swap chain if return value says to)

And then when you present to the queue you also need to check the return value and possibly rebuild the swapchain
Take a look at the code I posted earlier

edit: also, maybe find a way to make the error more human readable by looking up the error code in VK_CHECK or something

You’re right, VK_CHECK was killing it before the resize. Thanks for the catch. Now I actually see the size change. Now to get the swap chain rebuilt…

Thanks for the catch @sjr.

1 Like

Sorry, just rolling back into this. I was incorrect about two things…first: VK_CHECK doesn’t just log an error message:

std::cout <<Detected Vulkan error:  << err << std::endl;
abort();

Which means the “crash” is abort terminating the program. Which makes this code incorrect, because not every Vulkan error is meant to be fatal (such as out-of-date errors). What is happening is the tutorial code is getting a notice that the swapchain needs to be rebuilt, not crashing in the Vulkan call, and since it didn’t get a successful return, it’s killing the process.

So I would start by taking the abort line out of the #define VK_CHECK at the top of the source file, so you’ll get an error but keep going.

Now, the other thing I was wrong about: vkAcquireNextImageKHR can return VK_ERROR_OUT_OF_DATE_KHR, but so can vkQueuePresentKHR (and probably several other things). You’re hitting it on the QueuePresent call. You really should check for it at both.

But I suspect if you take out the abort call it won’t crash anymore, it just won’t draw anything until you rebuild the swapchain.

Remember that you need to rebuild any depth buffers etc, and also make the viewport size part of the dynamic state so you can change it when the window resizes without having to rebuild all your pipeline objects (I just set it after acquiring the next swapchain image regardless of if there’s been a size change or not, dunno if that’s the most optimal way)

Also, like Ryan said, there are a number of Vulkan calls that return “errors” that don’t mean your program should call abort(), so definitely take that out of the VK_CHECK macro. Worst case is either the actual Vulkan call crashes, or you just don’t see anything (but VK_CHECK will log it). And definitely use the Vulkan validation layer during development if you aren’t already, it’s super helpful (but don’t ship it, it incurs a performance overhead and the layer itself is hundreds of megabytes on some platforms).

And definitely use the Vulkan validation layer

Yes! This thing is a giant pain, but it will catch tons of mistakes in your code (some extremely minor and harmless, some deeply serious)…you definitely end up with more robust code when you’re done with it, but when running new code through it, productivity basically grinds to a halt while you fix all sorts of things.

If it’s helpful, here’s the complete Vulkan rendering code from my game: Project drift Vulkan renderer implementation. https://slembcke.github.io/Drift-Renderer · GitHub

In particular, look search for DriftVkAquireImage. It checks if the swap chain matches SDL_Vulkan_GetDrawableSize, and also catches the VK_ERROR_OUT_OF_DATE_KHR result code. It seems to work on Linux (AMD, Intel, Nvidia, RPi 4), and Windows (AMD, Nvidia). Though… I have a bug where vkAcquireNextImageKHR blocks indefinitely when entering fullscreen on AMD + Windows. I haven’t figured out if that’s my bug or not yet.

Also, I will second enabling validations! Absolutely 100% they will catch a lot of bugs you didn’t know you had. Search for DRIFT_DEBUG in my code to find all the relevant bits of code.

1 Like

@icculus and @sjr sorry for the delay, surgery+work=hell. Nothing like a ruptured appendix. Anyway, getting back to the the issue at hand:

creating swapchain
creating framebuffers
creating semaphores/fence
creating pipelines
init with 1700x900
...
1700x900 <--- no change @ VK_ERROR_OUT_OF_DATE_KHR == vkQueuePresentKHR()
deleting pipeline
deleting fence
deleting semaphores
deleting frame buffers
deleting swapchain
creating swapchain
vulkan_guide.AppImage: /home/adminz/projects/vulcanPrac/vulkanTest/third_party/vkbootstrap/VkBootstrap.h:94: T&& vkb::detail::Result<T>::value() && [with T = vkb::Swapchain]: Assertion `m_init' failed.
Aborted (core dumped)

edit:
SDL_Vulkan_GetDrawableSize(mWindow, &w, &h) and SDL_GetWindowSize(mWindow, &w, &h) both tried.
Going to try with my other computer… will update tomorrow…

update:
Just spammed vkBootstrap.cpp with print statements, failure happens at fp_vkCreateSwapchainKHR() with an error code of -1000000001 (VK_ERROR_NATIVE_WINDOW_IN_USE_KHR)

After looking this error up, this seems to be caused if you try to create a swap chain when it already exists. Did I miss something when deleting my swapchain?:

            vkDeviceWaitIdle(mDevice);
            vkDestroyImageView(mDevice, mDepthImageView, nullptr);
            vmaDestroyImage(mAllocator, mDepthImage.mImage, mDepthImage.mAllocation);

Update #2:
Yup, missed a call: vkDestroySwapchainKHR(mDevice, mSwapchain, nullptr);

Now I’m back to the initial issue:

[ERROR: Validation]
vkCreateFramebuffer(): VkFramebufferCreateInfo attachment #0 mip level 0 has dimensions smaller than the corresponding framebuffer dimensions. Here are the respective dimensions for attachment #0, framebuffer:
width: 1697, 1700
height: 894, 900
layerCount: 1, 1

@icculus and @sjr TLDR of my last post is neither SDL_Vulkan_GetDrawableSize(mWindow, &w, &h) or SDL_GetWindowSize(mWindow, &w, &h) are giving me the current size at the time of rebuilding the swapchain.

Update:
This issue might be self inflicted since I’m calling SDL_Vulkan_GetDrawableSize() at each rebuild step. I’ll follow up on this…

@icculus and @sjr I might be doing something stupid with c++ but let me know what’s going on wrong here. I have a lambda returning a lambda, and on resize it crashes when the outer lambda returns the inner one, but it’s fine when this was invoked the first time.

void VulkanEngine::initPipelines()
{
    auto setupPipeline = [this]()
    {
        std::cout << "creating pipelines" << std::endl;
...
        auto temp = [this, meshPipeline, texPipeline, meshPipLayout, texturedPipeLayout]()
        {
            std::cout << "deleting pipeline" << std::endl;

            vkDeviceWaitIdle(mDevice);
            vkDestroyPipeline(mDevice, meshPipeline, nullptr);
            vkDestroyPipeline(mDevice, texPipeline, nullptr);

            vkDestroyPipelineLayout(mDevice, meshPipLayout, nullptr);
            vkDestroyPipelineLayout(mDevice, texturedPipeLayout, nullptr);
        };
        //<--reaches here fine both on init and resize (line 1175)
        return temp; //only crashes here on resize; lambda invoked from queue
    };

    auto deletePipeline = setupPipeline();
    mMainDeletionQueue.push(deletePipeline, setupPipeline,
                            {"window_resize"});

console output:

1699x900
deleting pipeline
deleting descriptors
deleting fence
deleting semaphores
deleting frame buffers
deleting default render pass
deleting command pools
deleting swapchain
creating swapchain
creating command pools
creating default render pass
creating framebuffers
creating semaphores/fence
creating descriptors
creating pipelines
1050 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
1053 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
1058 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
1076 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
1104 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
1117 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
[WARNING: Performance]
vertex shader writes to output location 1.0 which is not consumed by fragment shader
[WARNING: Performance]
vertex shader writes to output location 1.0 which is not consumed by fragment shader
1156 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
1162 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
1163 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
1175 - /home/adminz/projects/vulcanPrac/vulkanTest/src/vkEngine.cpp
[ERROR: Validation]
VkPipeline 0x86 uses set #2 but that set is not bound.
[ERROR: Validation]
VkPipeline 0x86 uses set #2 but that set is not bound.
Segmentation fault (core dumped)

Update, so the good news is, the issue isn’t because the current data members are being prematurely deleted, however the bad news is there’s linkage to prior iteration; so after recreating the pipeline, something is still referencing the old one. I’ll update as I progress…

You don’t need to delete and recreate your pipelines, texture descriptors, command pools, or any of that.

You need to recreate:

  1. swapchain
  2. swapchain images
  3. swapchain image views
  4. depth buffer
  5. any framebuffers that need to be the same size as the window
  6. maybe render passes, I haven’t used Vulkan in a while and don’t remember

When you’re creating your pipelines, make the viewport size part of the dynamic state. That way you don’t have to recreate them, you just set the viewport size each frame.

Vulkan Tutorial does a decent job of showing how to set up & recreate your swapchain (minus not making the viewport size part of the dynamic state, so it has you needlessly recreate the render pipelines too). Beware that the sample code isn’t always great and encourages putting everything in one monolithic, unmaintainable .cpp file, however.

Also, I’m not usually one of those “You shouldn’t be using any C++ features when writing a game in C++!!!” kind of programmers, but if you have lambdas returning lambdas then you probably have too much C++

edit: you can set the pipelines to have the viewport size as part of their dynamic state using VkPipelineDynamicStateCreateInfo struct.

Something like (not tested)

// when creating a pipeline
uint32_t dynamicStateCount = 1;    // or however many below
VkDynamicState dynamicStates[] = {
    VK_DYNAMIC_STATE_VIEWPORT,
    // VK_DYNAMIC_STATE_SCISSOR also if you're using scissor rectangles
};

VkDynamicStateCreateInfo dynamicCreateInfo = {
    VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,    // gotta love Vulkan ;)
    nullptr,    // always NULL
    0,        // flags is always 0
    dynamicStateCount,    // how many dynamic states
    &dynamicStates
};

// Your VkPipelineStateCreateInfo struct, pretend it's called pipelineCreateInfo
pipelineCreateInfo.pDynamicState = &dynamicCreateInfo;

Then, in your drawing code, after you’ve bound a pipeline that was created with dynamic viewport state (you only have to do this once per frame IIRC)

VkViewport viewport = {
    0, 0, // x and y
    windowWidth, windowHeight,
    depthMin, depthMax    // probably 0.0f and 1.0f IIRC
};

vkCmdSetViewport(myCommandBuffer,
    0, // first viewport
    1, // number of viewports (almost certainly just 1 for a game)
    &viewport
);

That way, if you have a bunch of pipelines you won’t need to recreate them all just because the user resized the window

Ya, I kind of took the nuclear option after the first attempt at just rebuilding items that involve the window size. I’ll give it another shot and if that doesn’t work, I’m planning on creating another window, but slowly building it up with resize enabled and seeing where it breaks. My mistake was trying to preserve the framework of the SDL-Vulkan tutorial while factoring in window resize.

Update:
Nope, first approach failed, onto the second approach… on a side note, looking at the tutorial vkDestroyPipeline(device, graphicsPipeline, nullptr); is called in the swapchain cleanup routine. My guess is the sdl-vulkan tutorial I started with has a bug and I just need to identify it.