Handling Window Resize
A rather common mistake in handling resize requests is initiating resize in
response to vkAcquireNextImageKHR
or vkQueuePresentKHR
returning
VK_ERROR_OUT_OF_DATE_KHR
and querying swapchain extent with either
vkGetPhysicalDeviceSurfaceCapabilitiesKHR
or through windowing system-specific
means.
On Windows, resizing windows of programs that interleave DispatchMessage
with
redrawing will cause DispatchMessage
to block. The window will appear to have
its contents “frozen” for duration of the resize and “unfreeze” when the user is
done. Programs that have message loop and redrawing concurrent will race
vkCreateSwapchainKHR
against resize and break
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
set to
0xFFFFFFFFĂ—0xFFFFFFFF, minImageExtent
to 1Ă—1 and maxImageExtent
to some
large value. Instead, the window size is specified by the program using
imageExtent
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 resize the swapchain (and thus the window).
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 thus to 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 handling resize events, without ever having something to present to the user.
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) {
swapchain_ok = false;
} else if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) {
// Handle the error.
}
}
if (!swapchain_ok)
return;
// Record commands and submit.
r = vkQueuePresentKHR(/* ... */);
if (r == VK_ERROR_OUT_OF_DATE_KHR) {
swapchain_ok = false;
} else if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) {
// Handle the error.
}
}
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 = (VkExtent2D) {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
GLFW
static void resize_callback(GLFWwindow *w, int width, int height)
{
if (width > 0 && height > 0)
resize(width, height);
}
int main(int argc, char **argv)
{
// ...
glfwSetWindowSizeCallback(w, resize_callback);
while (!glfwWindowShouldClose(window))
{
if (swapchain_ok)
{
glfwPollEvents();
}
else
{
glfwWaitEvents(); // don't burn cpu
continue;
}
redraw();
}
// ...
}
SDL2
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_WINDOWEVENT:
if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED && e.window.data1 > 0 && e.window.data2 > 0)
resize(e.window.data1, e.window.data2);
break;
// Handle the remaining cases.
}
}
redraw();
}
// ...
}
If Windows API is used, it is important to be careful when using CreateWindow
.
Misusing CreateWindow
will lead to the first WM_SIZE
being lost.
static LRESULT CALLBACK
WndProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
switch (msg) {
case WM_CREATE:
// Create VkSurfaceKHR here
break;
case WM_SIZE: {
uint32_t width = lparam & 0xffff;
uint32_t height = (lparam >> 16) & 0xffff;
if (width > 0 && height > 0)
resize(width, height);
break;
}
// Handle the remaining cases.
}
}
On X11, XCB_CONFIGURE_NOTIFY
is not sent in response to window being created.
The program should create the swapchain with size of the window it created. Note
that on X11, vkCreateSwapchainKHR
always races against resize. This will
inevitably break VUID-VkSwapchainCreateInfoKHR-imageExtent-01274
, which is
expected and should be muted in the validation layer.
int
main(int argc, char **argv)
{
// ...
resize(/* width and height the window was created with */);
for (;;) {
void *e;
while ((e = xcb_poll_for_event(X)) != NULL) {
xcb_generic_event_t *generic = e;
switch (generice->response_type&~0x80) {
case XCB_CONFIGURE_NOTIFY: {
xcb_configure_notify_event_t *configure_notify = e;
// Note that this message is also sent when
// the window is being moved and not resized.
// It might be desirable to ignore configure
// notifications that do not change width and
// height.
if (configure_notify->width > 0 && configure_notify->height > 0)
resize(configure_notify->width, configure_notify->height);
break;
}
// Handle the remaining cases.
}
free(e);
}
redraw();
}
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.
bool swapchain_ok = true; // can be made a constant on Wayland :-)
bool closed;
static void xdg_surface_handle_configure(void *data,
struct xdg_surface *xdg_surface, uint32_t serial)
{
xdg_surface_ack_configure(xdg_surface, serial);
}
static const struct xdg_surface_listener xdg_surface_listener = {
.configure = xdg_surface_handle_configure,
};
static void
xdg_toplevel_configure(void *data,
struct xdg_toplevel *xdg_toplevel, int32_t width, int32_t height,
struct wl_array *states)
{
if (width <= 1)
width = 1;
if (height <= 1)
height = 1;
resize(width, height);
}
static void
xdg_toplevel_close(void *data, struct xdg_toplevel *toplevel)
{
closed = true;
}
static const struct xdg_toplevel_listener xdg_toplevel_listener = {
.configure = xdg_toplevel_configure,
.close = xdg_toplevel_close,
};
int
main(int argc, char **argv)
{
// ...
resize(/* any width and height desired */);
while (!closed) {
wl_display_dispatch_pending(display);
redraw();
}
// ...
}
Inevitably, there will be bugs. Worse, drivers sometimes have hard to reproduce
bugs of their own, lurking in the swapchain. Should an issue arise, a
vkDeviceWaitIdle
at the beginning of resize
is often a sufficient
workaround.
Concurrency
Some programs may wish to process input at a rate independent from that of redrawing. This requires that the windowing system message handling and redrawing are made independent of each other, and are allowed to execute concurrently.
typedef /* intentionally left blank */ Mutex;
void threadcreate(void (*f)(void*), void *a); // spawns a thread executing f(a)
void lock(Mutex *m); // locks m
void unlock(Mutex *m); // unlocks m
Mutex mu; // protects state accessed by resize and redraw
void
resize(uint32_t width, uint32_t height)
{
VkResult r;
assert(width > 0 && height > 0);
+
+ lock(&mu);
vkDeviceWaitIdle(device);
VkSwapchainKHR oldSwapchain = swapchain;
if ((r = vkCreateSwapchainKHR(device, &(VkSwapchainCreateInfoKHR) {
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
.surface = surface,
.imageExtent = (VkExtent2D) {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;
redraw();
+
+ unlock(&mu);
}
void
redrawLoop(void *a)
{
while (/* redraw stopping condition */) {
lock(&mu);
redraw();
unlock(&mu);
}
}
int
main(int argc, char **argv)
{
// Setup.
threadcreate(redrawLoop, NULL);
while (/* message loop stopping condition */) {
// Exchange messages with the windowing system.
}
// Done. Some programs may want to join thread execing redrawLoop at
// this point.
}
Careful readers will note the busy-waiting that occurs when the window is shrunk
to zero. It is desirable that the waiting is done by communicating what the
program is waiting for to the environment, so that CPU is not being burned in
vain. redraw
needs to be modified so as to communicate to the caller when to
begin waiting. The caller will then wait on a condition variable, which will be
signaled after a swapchain is created.
typedef /* intentionally left blank */ Cond;
void wait(Cond *c, Mutex *m); // unlocks m, waits on c, locks m
void signal(Cond *c); // wakes up waiters on c
Cond cond;
-void
+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) {
swapchain_ok = false;
} else if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) {
// Handle the error.
}
}
if (!swapchain_ok)
- return;
+ return false;
// Record commands and submit.
r = vkQueuePresentKHR(/* ... */);
if (r == VK_ERROR_OUT_OF_DATE_KHR) {
swapchain_ok = false;
} else if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) {
// Handle the error.
}
+
+ return true;
}
void
resize(uint32_t width, uint32_t height)
{
VkResult r;
assert(width > 0 && height > 0);
lock(&mu);
vkDeviceWaitIdle(device);
VkSwapchainKHR oldSwapchain = swapchain;
if ((r = vkCreateSwapchainKHR(device, &(VkSwapchainCreateInfoKHR) {
.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
.surface = surface,
.imageExtent = (VkExtent2D) {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();
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.
+ signal(&cond);
}
void
redrawLoop(void *a)
{
while (/* redraw stopping condition */) {
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).
+ wait(&cond, &mu);
+ }
unlock(&mu);
}
}
Always remember to sanitize your threads once in a while!