Skip to main content

Window Resizing

In vulkan, we have to handle window resizing ourselves. As part of chapter 0 we already had the code for minimization, but resizing the window is a lot more involved.

When the window resizes, the swapchain becomes invalid, and the vulkan operations with the swapchain like vk.AcquireNextImageKHR and vk.QueuePresentKHR can fail with a ERROR_OUT_OF_DATE_KHR error. We must handle those correctly, and make sure that we can re-create the swapchain with a new size.

For efficiency, we will not be reallocating the draw image. Right now we only have one draw image and depth image, but on a more developed engine it could be significantly more, and re-creating all that can be a considerable hassle. Instead, we create the draw and depth image at startup with a preset size, and then draw into a section of it if the window is small, or scale it up if the window is bigger. As we arent reallocating but just rendering into a corner, we can also use this same logic to perform dynamic resolution, which is a useful way of scaling performance, and can be handy for debugging. We are copying the rendering from the draw image into the swapchain with vk.CmdBlit, and that one performs scaling so it will work well here. This sort of scaling is not the highest quality, as normally you would want to perform some more complicated logic for the upscaling like applying some sharpening, or doing fake antialiasing as part of that scaling. The Imgui UI will still render into the swapchain image directly, so it will always render at native resolution.

GLFW by default allows us to resize the window. Run the engine and try to resize the window.

It should give an error on the vk_check procedure we have on either vk.AcquireNextImageKHR or vk.QueuePresentKHR. The error will be ERROR_OUT_OF_DATE_KHR. So to handle the resize, we need to stop the rendering if we see that error, and rebuild the swapchain when that happens.

On the engine_draw() procedure, replace the call to vk.AcquireNextImageKHR to check the error code.

// Request image from the swapchain
if result := vk.AcquireNextImageKHR(
self.vk_device,
self.vk_swapchain,
max(u64),
frame.swapchain_semaphore,
0,
&frame.swapchain_image_index,
); result == .ERROR_OUT_OF_DATE_KHR {
engine_resize_swapchain(self) or_return
}

If .ERROR_OUT_OF_DATE_KHR is returned, we call engine_resize_swapchain to recreate the swapchain with updated window dimensions. This ensures the rendering system remains in sync with the current display configuration.

Also replace the call to vk.QueuePresentKHR at the end in the same way.

if result := vk.QueuePresentKHR(self.graphics_queue, &present_info);
result == .ERROR_OUT_OF_DATE_KHR {
engine_resize_swapchain(self) or_return
}

Lets add a engine_resize_swapchain() procedure to re-create the swapchain.

engine_resize_swapchain :: proc(self: ^Engine) -> (ok: bool) {
vk_check(vk.DeviceWaitIdle(self.vk_device)) or_return

width, height := glfw.GetFramebufferSize(self.window)
self.window_extent = {u32(width), u32(height)}

engine_create_swapchain(self, self.window_extent.width, self.window_extent.height) or_return

return true
}

To resize the swapchain, we first begin by waiting until the GPU has finished all rendering commands. We dont want to change the images and views while the gpu is still handling them. Then we query the window size from GLFW and create it again.

Lets also change engine_create_swapchain to set the old swapchain before create a new one. When recreating a swapchain in Vulkan and setting the oldSwapchain parameter, you're telling the system to handle the transition from the old to the new swapchain more efficiently. This is especially helpful when the window is resized or the surface properties change.

engine_create_swapchain :: proc(self: ^Engine, width, height: u32) -> (ok: bool) {
vkb.swapchain_builder_add_image_usage_flags(&builder, {.TRANSFER_DST})

// Other code above ---

// If an existing swapchain is present,link it as the old swapchain
if self.vkb.swapchain != nil {
vkb.swapchain_builder_set_old_swapchain(&builder, self.vkb.swapchain)
}

// Build the new swapchain using the configured builder
swapchain := vkb.build_swapchain(&builder) or_return

// If there was an old swapchain, destroy it after the new one is created
if self.vkb.swapchain != nil {
engine_destroy_swapchain(self)
}

// Update engine state with new swapchain
self.vkb.swapchain = swapchain
self.vk_swapchain = swapchain.handle
self.swapchain_extent = swapchain.extent

// Retrieve and store the swapchain’s images and image views
self.swapchain_images = vkb.swapchain_get_images(self.vkb.swapchain) or_return
self.swapchain_image_views = vkb.swapchain_get_image_views(self.vkb.swapchain) or_return

return true
}

Since we are using vkb, the oldSwapchain parameter is set by calling vkb.swapchain_builder_set_old_swapchain.

Now we have the resizing implemented so try it. You should be able to resize the image down without running into errors. But if you make the window bigger, it will fail. We are going past the size of our draw-image and its try to render out of bounds. We can fix that by implementing a _drawExtent variable and making sure that it gets maxed at the size of the draw image.

Add render_scale float to Engine structure that we will use for dynamic resolution.

draw_extent:  vk.Extent2D,
// Other code above ---
render_scale: f32,

Set the render_scale default value to 1.0 in the engine_init procedure.

engine_init :: proc(self: ^Engine) -> (ok: bool) {
ensure(self != nil, "Invalid 'Engine' object")

// Store the current logger for later use inside callbacks
g_logger = context.logger

self.window_extent = DEFAULT_WINDOW_EXTENT
self.render_scale = 1.0 // < here

// Other code ---
}

Back to the engine_draw() procedure, we calculate the draw extent at the start of it, instead of using the draw image extent for it.

self.draw_extent = {
width = u32(
f32(min(self.swapchain_extent.width, self.draw_image.image_extent.width)) *
self.render_scale,
),
height = u32(
f32(min(self.swapchain_extent.height, self.draw_image.image_extent.height)) *
self.render_scale,
),
}

Now we are going to add a slider to imgui to control this draw scale parameter.

In the engine_run() procedure, inside the imgui window that calculates background parameters, add this to the top

if im.begin("Background", nil, {.Always_Auto_Resize}) {
im.slider_float("Render scale", &self.render_scale, 0.3, 1.0)

// Other code bellow ---
}

This will give us a render scale editable slider, that will go from 0.3 to 1.0. We dont want to go past 1 because it will break the resolution.

Run it, and try to resize the window and play with the render scale. You will see that now you can maximize or move around the window and change its resolution dynamically.

We are setting up the draw image a bit small, but if you want, try to increase the size of the draw image from the place its created in engine_init_swapchain(). Set the drawImageExtent to your monitor resolution instead of the window_extent, which is hardcoded to a small size.

platform.odin
get_monitor_resolution :: proc() -> (u32, u32) {
mode := glfw.GetVideoMode(glfw.GetPrimaryMonitor())
ensure(mode != nil)
return u32(mode.width), u32(mode.height)
}
engine.odin
engine_init_swapchain :: proc(self: ^Engine) -> (ok: bool) {
engine_create_swapchain(self, self.window_extent.width, self.window_extent.height) or_return

monitor_width, monitor_height := get_monitor_resolution()

// Draw image size will match the monitor resolution
draw_image_extent := vk.Extent3D {
width = monitor_width,
height = monitor_height,
depth = 1,
}

// Other code ---

return true
}

With this we have chapter 3 done, and can move forward to the next chapter.