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: ReCreate Image Views Because of New SwapChain Images.ReCreate FrameBuffers Because Width and Height and Image Views(Views to SwapChain Images) ChangedReCreate Graphics Pipeline Because viewport and scissor need to change.ReCreate and Record new Command Buffers Because of New SwapChain ImagesVulkan Tutorial: The render pass needs to be recreated because it depends on the format of the swap chain images.Note (Step 5): It's …

Vulkan, Wanna Be Friends? #2 Multi-Threading and Synchronization


Full Video (Vulkan Multi-Threaded Rendering on Mobile Devices)


One of the things I love about Vulkan is that it gives you a lot of control over what you're application is going to be. In the case of this blog post, It's Multi-Threading and Synchronization and It might be more challenging for developers who have little experience with multi-threading.


Great Representation by Nvidia [PDF]


The Idea Is Simple, just let another thread/worker (sometimes on GPU)  do work and you go deal with your own stuff but you can't continue at a point because you need that thread's result.

So you have to be careful not to use the result of the worker until it has finished its job otherwise It will mostly be incomplete if you want the result soon.

In Vulkan, you See 2 ideas for Synchronization :

  1. Fences
  2. Semaphores
If you don't think about it that much, you might say, "Why do we need 2? mutexes weren't enough?"

Well Let's Rephrase

In Vulkan, you See 2 types of Synchronization :
  1. CPU-GPU Synchronization
  2. GPU-GPU Synchronization

Of course, there are more to Vulkan (esp in its computing purposes) like barriers, but that's enough, for now, to get a nice triangle without memory leaks.



Semaphore

We'll get to memory leaks soon but let's go through some basics and see what our simple application does without synchronization.

DrawFrame() Function :
  1. Acquire image from SwapChain
  2. Submit our command buffer that renders the image acquired in (1)
  3. Present to Texture to Screen
The Code Respectively :
  1. vkAcquireNextImageKHR 
  2. vkQueueSubmit
  3. vkQueuePresentKHR
These functions run asynchronously, It means that after you call vkAcquireNextImageKHR, It is not guaranteed that the image is acquired, and when you present it, It's not guaranteed that It's rendered to the texture. (most cases you see crashes due to this)

So the solution is Semaphores which handles GPU-GPU Synchronization and it's one of the parameters wanted in those functions or in their info struct, and of course, you can just put VK_NULL_HANDLE.

We need 2 semaphores in this example.

  1. Hey, Image was Acquired!!!!, let's Submit to Queue (signal semaphore for vkAcquireNextImageKHR and wait semaphore vkQueueSubmit)
  2. Hey, Submit Queue's job is done!!! Let's present it to the surface. (you guess this one 😉)


VkSemaphore wait_semaphores[] = { this->image_available_semaphore[this->current_frame] };
VkSemaphore singnal_semaphores[] = { this->render_finished_semaphore[this->current_frame] };

VkPipelineStageFlags wait_stages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT };
VkSubmitInfo submit_info = {};
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit_info.commandBufferCount = 1;
submit_info.pCommandBuffers = &this->command_buffers[image_index];
submit_info.waitSemaphoreCount = 1;
submit_info.pWaitSemaphores = wait_semaphores;
submit_info.pWaitDstStageMask = wait_stages;
submit_info.signalSemaphoreCount = 1;
submit_info.pSignalSemaphores = singnal_semaphores;


You might've noticed wait_stages, what this does is continue the graphics pipeline and when it reached the VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT(the part when we write to FrameBuffer) It waits until the is wait_semaphores signaled by vkAcquireNextImageKHR.

You can also use VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT as wait stage which means wait for the semaphore before the pipeline starts and anything serious happens.



Because this is not a tutorial and I'm just a beginner sharing my experience I recommend reading Vulkan-Tutorial for better understanding and actual code, My GitHub Repository is also available 😇



So We'll get our really nice Rendered Triangle....... BUT!




As you know that is called Memory Leak 😀, but why?!?!?? (heads up! It's for validation layers)

Fences


When the CPU operates faster than GPU because it doesn't do too much work, It means that it tells the GPU to render to a texture when it's currently being used to display. In our example, a SwapChain Image should not be presented and rendered to at the same time but it doesn't result in crashes. It results in memory leaks of validation layers if enabled! (I do not completely understand it but I have some intuitions about It).

How To Fix It? It's Obvious, Fences or CPU-GPU Synchronization.

Beginning of DrawFrame function before Acquiring Image from SwapChain



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]);
(vkResetFences changes the fences state from signaled to unsignaled)

When Submitting Command Buffer


vkQueueSubmit(this->graphics_queue, 1, &submit_info, this->draw_fences[this->current_frame]);

So you can't acquire texture and use it again in the command buffer when it's being rendered to.

You have noticed by now that the semaphores and fences are an array not a single semaphore or a single fence, Well that could be possible but it's not optimized! and the Idea Is Simple: You CAN render to SwapChainTexture[1] while SwapChainTexture[0] is being presented or rendered to.
See also Vulkan present modes.

Vulkan-Tutorial: "If you run your application with validation layers enabled and you monitor the memory usage of your application, you may notice that it is slowly growing. The reason for this is that the application is rapidly submitting work in the drawFrame function, but doesn't actually check if any of it finishes If the CPU is submitting work faster than the GPU can keep up with then the queue will slowly fill up with work. Worse, even, is that we are reusing the imageAvailableSemaphore and renderFinishedSemaphore for multiple frames at the same time."

I also recommend reading the previous blog where you can find out more about subpass dependencies and synchronization between them.



Here are great synchronization examples by Khronos Group.


😊😊😊😊😊😊😊


Be Sure To keep up with the blog posts, if you have something to teach me, something to ask me or even you think I messed up somewhere in the post I'm more than willing to listen and Cooperate.

😊😊😊😊😊😊😊

Next is: Using Graphics Tools and Debuggers and Resizing Window.

Comments