A rather common mistake arising while handling resize requests is to initiate a
resize in response to vkAcquireNextImageKHR or vkQueuePresentKHR returning
VK_ERROR_OUT_OF_DATE_KHR and then query the swapchain extent with either
vkGetPhysicalDeviceSurfaceCapabilitiesKHR or through the windowing system
-specific means.
On Windows, resizing windows of programs that interleave DispatchMessage with
redrawing will cause DispatchMessage to block. The window will become
unresponsive for the duration of the resize. Programs that have message loop and
redrawing concurrent will race vkCreateSwapchainKHR against resize and violate
VUID-VkSwapchainCreateInfoKHR-imageExtent-01274.
On Wayland, VK_ERROR_OUT_OF_DATE_KHR is never returned and there’s no window
size to query: VkSurfaceCapabilitiesKHR returned by
vkGetPhysicalDeviceSurfaceCapabilitiesKHR will have currentExtent be set to
0xFFFFFFFFĂ—0xFFFFFFFF, minImageExtent to 1Ă—1 and maxImageExtent to some
large value. Instead, the window size is determined by whatever value the
program specifies in the imageExtent field when creating the swapchain. When
the user resizes the window, a resize message will be delivered, specifying the
desired size, but it is up to the program to respect that.
Another common mistake is assuming that the width and height are always positive. This assumption is easily broken by shrinking the window to zero.
The correct approach to handling resizes is to simply listen to the windowing system’s resize message and be defensive about the window size. After a swapchain is created, at least a single frame should be redrawn so that the program is not stuck waiting for resize events without ever having something to present to the user.
Note that this article does not currently address the recently released surface and swapchain maintenance extensions.
The example program is structured into redraw and resize functions. To avoid
the mistake covered in the first paragraph of this section, redraw will simply
return instead of initiating resize (creating new swapchain).
VkDevice device;
VkSurfaceKHR surface;
VkSwapchainKHR swapchain;
bool swapchain_ok;
void
redraw(void)
{
VkResult r;
// Calling vkAcquireNextImageKHR or vkQueuePresentKHR on swapchain
// for which a prior call returned VK_ERROR_OUT_OF_DATE_KHR is an
// error, so it is our responsibility to make it sticky.
if (swapchain_ok) {
r = vkAcquireNextImageKHR(/* ... */);
if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) {
swapchain_ok = false;
} else if (r != VK_SUCCESS) {
// Handle the error.
}
}
if (!swapchain_ok)
return;
// Record commands and submit.
// In this case we can ignore the result of vkQueuePresentKHR. If
// there is an error, we will get it on the next vkAcquireNextImage.
vkQueuePresentKHR(/* ... */);
}
void
resize(uint32_t width, uint32_t height)
{
VkResult r;
assert(width > 0 && height > 0);
vkDeviceWaitIdle(device);
VkSwapchainKHR oldSwapchain = swapchain;
if ((r = vkCreateSwapchainKHR(device, &(VkSwapchainCreateInfoKHR) {
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
.surface = surface,
.imageExtent = {width, height},
/* ... */
})) != VK_SUCCESS) {
// Handle the error.
}
// If it is possible for an image acquired from oldSwapchain to still
// not be presented at this point, it should be made a zombie instead.
vkDestroySwapchainKHR(device, oldSwapchain, NULL);
// Create swapchain-dependent resources.
swapchain_ok = true;
redraw();
}
Now, to wire up with the windowing system. This article focuses on SDL3, but all SDL3-specific material in this article is expected to straightforwardly translate to other windowing APIs.
int main(int argc, char **argv) {
// ...
for (;;) {
SDL_Event e;
while ((swapchain_ok ? SDL_PollEvent(&e) : SDL_WaitEvent(&e)) != 0) {
switch (e.type) {
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
if (e.window.data1 > 0 && e.window.data2 > 0)
resize(e.window.data1, e.window.data2);
break;
// Handle the remaining cases.
}
}
redraw();
}
// ...
}
There are some gotchas to consider:
-
On X11,
vkCreateSwapchainKHRalways races against resize. This will inevitably breakVUID-VkSwapchainCreateInfoKHR-imageExtent-01274, which is expected and should be muted in the validation layer. -
On Wayland, the application never receives
VK_ERROR_OUT_OF_DATE_KHR. It is also much easier to handle the case when the window is shrunk to zero.
Inevitably, there will be bugs. Worse, drivers sometimes have hard to reproduce
bugs of their own, lurking deep in their swapchain implementations. Should an
issue arise, a vkDeviceWaitIdle at the beginning of resize is often a
sufficient workaround.
Concurrency
Some programs may wish to process input independently from redrawing. This requires that the windowing system message handling and redrawing are made independent of each other, and are allowed to execute concurrently.
Let’s introduce synchronization objects that will let us coordinate the input handling thread performing the resize, and the redrawing thread performing the draw.
mtx_t mu; // protects state accessed by resize and redraw
cnd_t cond;
The redraw, resize and redrawLoop functions will now look as follows:
bool
redraw(void)
{
VkResult r;
// Calling vkAcquireNextImageKHR or vkQueuePresentKHR on swapchain
// for which a prior call returned VK_ERROR_OUT_OF_DATE_KHR is an
// error, so it is our responsibility to make it sticky.
if (swapchain_ok) {
r = vkAcquireNextImageKHR(/* ... */);
if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) {
swapchain_ok = false;
} else if (r != VK_SUCCESS) {
// Handle the error.
}
}
if (!swapchain_ok)
return false;
// Record commands and submit.
// Intentionally ignore vkQueuePresentKHR result. Should there be
// any error, we'll get it on the next vkAcquireNextImageKHR.
vkQueuePresentKHR(/* ... */);
return true;
}
void
resize(uint32_t width, uint32_t height)
{
VkResult r;
assert(width > 0 && height > 0);
mtx_lock(&mu);
vkDeviceWaitIdle(device);
VkSwapchainKHR oldSwapchain = swapchain;
if ((r = vkCreateSwapchainKHR(device, &(VkSwapchainCreateInfoKHR) {
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
.surface = surface,
.imageExtent = {width, height},
/* ... */
})) != VK_SUCCESS) {
// Handle the error.
}
// If some images acquired from oldSwapchain have not yet been
// presented, it should be made a zombie instead.
vkDestroySwapchainKHR(device, oldSwapchain, NULL);
// Create swapchain-dependent resources.
swapchain_ok = true;
// Don't care if redraw fails, if it does, the next resize event will
// be handled shortly.
redraw();
mtx_unlock(&mu);
// Wake up waiters on cond. There is only ever at most a single waiter.
// Doesn't matter if signal happens before dropping mu or after.
cnd_signal(&cond);
}
void
redrawLoop(void *a)
{
while (/* redraw stopping condition */) {
mtx_lock(&mu);
redraw();
if (!redraw()) {
// Unlock mu and begin waiting on cond. Spurious wake
// ups are okay, because redraw will just fail and end
// up waiting again (or resize happens in the
// meanwhile).
cnd_wait(&cond, &mu);
}
mtx_unlock(&mu);
}
}
Always remember to sanitize your threads once in a while!
Redraw on Demand
Coming soon