Vulkan, Wanna Be Friends? #3 Window Resize and SwapChain Recreation

We want to be able to resize our window. 

Many Games don't let dynamic and flexible resize maybe because of the check and additional work each frame and It's not logical to change windows size and resolution so often, so It's a win-win!

After one resize we are freeing and allocating memory which has cost, thus hard to do each frame when resizing dynamically.


Here's a version where we don't handle Resize:


Handling Resize In Vulkan

To Resize our window we have to Recreate our whole SwapChain and thus these below as before when initiating Vulkan:
  1. ReCreate Image Views Because of New SwapChain Images.
  2. ReCreate FrameBuffers Because Width and Height and Image Views(Views to SwapChain Images) Changed
  3. ReCreate Graphics Pipeline Because viewport and scissor need to change.
  4. ReCreate and Record new Command Buffers Because of New SwapChain Images
  5. Vulkan Tutorial: The render pass needs to be recreated because it depends on the format of the swap chain images.
Note (Step 5): It's good practice to recreate the render pass in swap_chain_recreate(); but can't reason about why would swap chain format change after resizing swap chain and buffers.

Note (from Vulkan-Tutorial): We could recreate the command pool from scratch, but that is rather wasteful.

Note: We Don't need to ReCreate Graphics Pipeline if we set Viewport and Scissor as Dynamic Stages and in the GitHub project I used dynamic stages otherwise you have to delete pipeline and pipeline layout and recreate them all over which is rather wasteful.

So we just resize viewport and scissor on resize and not the whole pipeline.

Note: Use vkDeviceWaitIdle(this->device); to be sure device isn't in use when recreating Vulkan (objects?). 

So Our recreate_swap_chain() function will look like this:




void VulkanApp::recreate_swap_chain()
{
vkDeviceWaitIdle(this->device);

cleanup_swap_chain();

create_swap_chain();
create_image_views();
create_renderpass();!set_viewport_scissor();
create_frame_buffers();
create_command_buffers();                                                     
}
We need to handle minimization as well so we if width and height are 0 we wait for new GLFW Events until they are greater than 0 and then continue with our SwapChain Recreation:


void VulkanApp::recreate_swap_chain()
{
vkDeviceWaitIdle(this->device);

int width = 0, height = 0;
while(width == 0 || height == 0)
{
glfwGetFramebufferSize(this->window, &width, &height);
glfwWaitEvents();
}

cleanup_swap_chain();

create_swap_chain();
create_image_views();
create_renderpass();!set_viewport_scissor();
create_frame_buffers();
create_command_buffers();                                                     
}

Also, don't forget to free previous SwapChain and other memories (framebuffers + image views + renderpass) before allocating new ones to avoid memory leaks.



Dynamic Stages


When first creating Graphics Pipeline:




VkDynamicState dynamic_states[2] = { VK_DYNAMIC_STATE_VIEWPORT, VK_DYNAMIC_STATE_SCISSOR };
VkPipelineDynamicStateCreateInfo dynamic_state_info = {};
dynamic_state_info.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO;
dynamic_state_info.dynamicStateCount = 2;
dynamic_state_info.pDynamicStates = dynamic_states;

When Recording Command Buffers:



pipeline_create_info.pDynamicState = &dynamic_state_info;

When To Resize?

  1. GLFW Callbacks -> Set Flag to True
  2. Result Code in vkAcquireNextImageKHR(..)
  3. Result Code in vkQueuePresentKHR(..)
GLFW Callback :

Note: Our Callback is static function so we can't access our class members.
Solution: Set this as user pointer in GLFW window.


bool VulkanApp::setup_window()
{
glfwInit();
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);

this->window = glfwCreateWindow(INIT_WIDTH, INIT_HEIGHT, "Vulkan-Learn-1", nullptr, nullptr);

glfwSetFramebufferSizeCallback(this->window, resize_callback);
glfwSetWindowUserPointer(this->window, this);

return true;
}

static void resize_callback(GLFWwindow* window, int width, int height)
{
auto app = reinterpret_cast<VulkanApp*>(glfwGetWindowUserPointer(window));    
app->window_resize(); // Sets flag to true
}

Acquire Image VkResult:


const auto acq_image_result = vkAcquireNextImageKHR(...);  

Handling Resize:

if (acq_image_result == VK_SUBOPTIMAL_KHR
|| acq_image_result == VK_ERROR_OUT_OF_DATE_KHR
|| this->should_recreate_swapchain)
{
if (recreate_swap_chain())
{
this->should_recreate_swapchain = false;
Log("SwapChain Recreate");
return true;
}
else
{
Log("Failed SwapChain Recreatation.");
}
}

According to Specs

VK_SUBOPTIMAL_KHR

A swapchain no longer matches the surface properties exactly, but can still be used to present to the surface successfully.

VK_ERROR_OUT_OF_DATE_KHR

A surface has changed in such a way that it is no longer compatible with the swapchain, and further presentation requests using the swapchain will fail. Applications must query the new surface properties and recreate their swapchain if they wish to continue presenting to the surface.



Problems I Encountered

As Seen in the GIF Below It hangs when resizing.



the problems are whenever you decide to resize, you cancel everything and you wait for a new frame to be drawn.


draw_frame(): 

vkWaitForFences(this->device, 1, &this->draw_fences[this->current_frame], VK_TRUE, std::numeric_limits<uint64_t>::max());
vkResetFences(this->device, 1, &this->draw_fences[this->current_frame]);
uint32_t image_index;
const auto acq_image_result = vkAcquireNextImageKHR(...);

if(needs resize and ....)
{
return;
}

Validation Layer Error when Resizing Window:


validation layer: vkWaitForFences called for fence 0x1a which has not been submitted on a Queue or during acquiring next image.

The Problem is fence is reset to unsignaled by vkResetFences and when we return it waits and waits and because it was never submitted on a queue it will never get Signaled!


Solution!  

Just Move the vkResetFences after checking for window resize, and It's more reasonable to be before calling vkQueueSubmit



Dream Big Dreams.




Comments