|
| 1 | +# Swapchain |
| 2 | + |
| 3 | +A [Vulkan Swapchain](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSwapchainKHR.html) is an array of presentable images associated with a Surface, which acts as a bridge between the application and the platform's presentation engine (compositor / display engine). The Swapchain will be continually used in the main loop to acquire and present images. Since failing to create a Swapchain is a fatal error, its creation is part of the initialiation section. |
| 4 | + |
| 5 | +We shall wrap the Vulkan Swapchain into our own `class Swapchain`. It will also store the a copy of the Images owned by the Vulkan Swapchain, and create (and own) Image Views for each Image. The Vulkan Swapchain may need to be recreated in the main loop, eg when the framebuffer size changes, or an acquire/present operation returns `vk::ErrorOutOfDateKHR`. This will be encapsulated in a `recreate()` function which can simply be called during initialization as well. |
| 6 | + |
| 7 | +```cpp |
| 8 | +// swapchain.hpp |
| 9 | +class Swapchain { |
| 10 | + public: |
| 11 | + explicit Swapchain(vk::Device device, Gpu const& gpu, |
| 12 | + vk::SurfaceKHR surface, glm::ivec2 size); |
| 13 | + |
| 14 | + auto recreate(glm::ivec2 size) -> bool; |
| 15 | + |
| 16 | + [[nodiscard]] auto get_size() const -> glm::ivec2 { |
| 17 | + return {m_ci.imageExtent.width, m_ci.imageExtent.height}; |
| 18 | + } |
| 19 | + |
| 20 | + private: |
| 21 | + void populate_images(); |
| 22 | + void create_image_views(); |
| 23 | + |
| 24 | + vk::Device m_device{}; |
| 25 | + Gpu m_gpu{}; |
| 26 | + |
| 27 | + vk::SwapchainCreateInfoKHR m_ci{}; |
| 28 | + vk::UniqueSwapchainKHR m_swapchain{}; |
| 29 | + std::vector<vk::Image> m_images{}; |
| 30 | + std::vector<vk::UniqueImageView> m_image_views{}; |
| 31 | +}; |
| 32 | + |
| 33 | +// swapchain.cpp |
| 34 | +Swapchain::Swapchain(vk::Device const device, Gpu const& gpu, |
| 35 | + vk::SurfaceKHR const surface, glm::ivec2 const size) |
| 36 | + : m_device(device), m_gpu(gpu) {} |
| 37 | +``` |
| 38 | +
|
| 39 | +## Static Swapchain Properties |
| 40 | +
|
| 41 | +Some Swapchain creation parameters like the image extent (size) and count depend on the surface capabilities, which can change during runtime. We can setup the rest in the constructor, for which we need a helper function to obtain a desired [Surface Format](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSurfaceFormatKHR.html): |
| 42 | +
|
| 43 | +```cpp |
| 44 | +constexpr auto srgb_formats_v = std::array{ |
| 45 | + vk::Format::eR8G8B8A8Srgb, |
| 46 | + vk::Format::eB8G8R8A8Srgb, |
| 47 | +}; |
| 48 | +
|
| 49 | +[[nodiscard]] constexpr auto |
| 50 | +get_surface_format(std::span<vk::SurfaceFormatKHR const> supported) |
| 51 | + -> vk::SurfaceFormatKHR { |
| 52 | + for (auto const desired : srgb_formats_v) { |
| 53 | + auto const is_match = [desired](vk::SurfaceFormatKHR const& in) { |
| 54 | + return in.format == desired && |
| 55 | + in.colorSpace == |
| 56 | + vk::ColorSpaceKHR::eVkColorspaceSrgbNonlinear; |
| 57 | + }; |
| 58 | + auto const it = std::ranges::find_if(supported, is_match); |
| 59 | + if (it == supported.end()) { continue; } |
| 60 | + return *it; |
| 61 | + } |
| 62 | + return supported.front(); |
| 63 | +} |
| 64 | +``` |
| 65 | + |
| 66 | +An sRGB format is preferred because that is what the screen's color space is in. This is indicated by the fact that the only core [Color Format](https://registry.khronos.org/vulkan/specs/latest/man/html/VkColorSpaceKHR.html) is `vk::ColorSpaceKHR::eVkColorspaceSrgbNonlinear`, which specifies support for the images in sRGB color space. |
| 67 | + |
| 68 | +The constructor can now be implemented: |
| 69 | + |
| 70 | +```cpp |
| 71 | + auto const surface_format = |
| 72 | + get_surface_format(m_gpu.device.getSurfaceFormatsKHR(surface)); |
| 73 | + m_ci.setSurface(surface) |
| 74 | + .setImageFormat(surface_format.format) |
| 75 | + .setImageColorSpace(surface_format.colorSpace) |
| 76 | + .setImageArrayLayers(1) |
| 77 | + .setImageUsage(vk::ImageUsageFlagBits::eColorAttachment) |
| 78 | + .setPresentMode(vk::PresentModeKHR::eFifo); |
| 79 | + if (!recreate(size)) { |
| 80 | + throw std::runtime_error{"Failed to create Vulkan Swapchain"}; |
| 81 | + } |
| 82 | +``` |
| 83 | +
|
| 84 | +## Swapchain Recreation |
| 85 | +
|
| 86 | +The constraints on Swapchain creation parameters are specified by [Surface Capabilities](https://registry.khronos.org/vulkan/specs/latest/man/html/VkSurfaceCapabilitiesKHR.html). Based on the spec we add two helper functions and a constant: |
| 87 | +
|
| 88 | +```cpp |
| 89 | +constexpr std::uint32_t min_images_v{3}; |
| 90 | +
|
| 91 | +[[nodiscard]] constexpr auto |
| 92 | +get_image_extent(vk::SurfaceCapabilitiesKHR const& capabilities, |
| 93 | + glm::uvec2 const size) -> vk::Extent2D { |
| 94 | + constexpr auto limitless_v = 0xffffffff; |
| 95 | + if (capabilities.currentExtent.width < limitless_v && |
| 96 | + capabilities.currentExtent.height < limitless_v) { |
| 97 | + return capabilities.currentExtent; |
| 98 | + } |
| 99 | + auto const x = std::clamp(size.x, capabilities.minImageExtent.width, |
| 100 | + capabilities.maxImageExtent.width); |
| 101 | + auto const y = std::clamp(size.y, capabilities.minImageExtent.height, |
| 102 | + capabilities.maxImageExtent.height); |
| 103 | + return vk::Extent2D{x, y}; |
| 104 | +} |
| 105 | +
|
| 106 | +[[nodiscard]] constexpr auto |
| 107 | +get_image_count(vk::SurfaceCapabilitiesKHR const& capabilities) |
| 108 | + -> std::uint32_t { |
| 109 | + if (capabilities.maxImageCount < capabilities.minImageCount) { |
| 110 | + return std::max(min_images_v, capabilities.minImageCount); |
| 111 | + } |
| 112 | + return std::clamp(min_images_v, capabilities.minImageCount, |
| 113 | + capabilities.maxImageCount); |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +We want at least three images in order to be have the option to set up triple buffering. While it's possible for a Surface to have `maxImageCount < 3`, it is quite unlikely. It is in fact much more likely for `minImageCount > 3`. |
| 118 | + |
| 119 | +The dimensions of Vulkan Images must be positive, so if the incoming framebuffer size is not, we skip the attempt to recreate. This can happen eg on Windows when the window is minimized. (Until it is restored, rendering will basically be paused.) |
| 120 | + |
| 121 | +```cpp |
| 122 | +auto Swapchain::recreate(glm::ivec2 size) -> bool { |
| 123 | + if (size.x <= 0 || size.y <= 0) { return false; } |
| 124 | + |
| 125 | + auto const capabilities = |
| 126 | + m_gpu.device.getSurfaceCapabilitiesKHR(m_ci.surface); |
| 127 | + m_ci.setImageExtent(get_image_extent(capabilities, size)) |
| 128 | + .setMinImageCount(get_image_count(capabilities)) |
| 129 | + .setOldSwapchain(m_swapchain ? *m_swapchain : vk::SwapchainKHR{}) |
| 130 | + .setQueueFamilyIndices(m_gpu.queue_family); |
| 131 | + assert(m_ci.imageExtent.width > 0 && m_ci.imageExtent.height > 0 && |
| 132 | + m_ci.minImageCount >= min_images_v); |
| 133 | + |
| 134 | + m_device.waitIdle(); |
| 135 | + m_swapchain = m_device.createSwapchainKHRUnique(m_ci); |
| 136 | + |
| 137 | + return true; |
| 138 | +} |
| 139 | +``` |
| 140 | +
|
| 141 | +After successful recreation we want to fill up those vectors of images and views. For the images we use a more verbose approach to avoid having to assign the member vector to a newly returned one every time: |
| 142 | +
|
| 143 | +```cpp |
| 144 | +void require_success(vk::Result const result, char const* error_msg) { |
| 145 | + if (result != vk::Result::eSuccess) { throw std::runtime_error{error_msg}; } |
| 146 | +} |
| 147 | +// ... |
| 148 | +
|
| 149 | +void Swapchain::populate_images() { |
| 150 | + auto image_count = std::uint32_t{}; |
| 151 | + auto result = |
| 152 | + m_device.getSwapchainImagesKHR(*m_swapchain, &image_count, nullptr); |
| 153 | + require_success(result, "Failed to get Swapchain Images"); |
| 154 | +
|
| 155 | + m_images.resize(image_count); |
| 156 | + result = m_device.getSwapchainImagesKHR(*m_swapchain, &image_count, |
| 157 | + m_images.data()); |
| 158 | + require_success(result, "Failed to get Swapchain Images"); |
| 159 | +} |
| 160 | +``` |
| 161 | + |
| 162 | +Creation of the views is fairly straightforward: |
| 163 | + |
| 164 | +```cpp |
| 165 | +void Swapchain::create_image_views() { |
| 166 | + auto subresource_range = vk::ImageSubresourceRange{}; |
| 167 | + subresource_range.setAspectMask(vk::ImageAspectFlagBits::eColor) |
| 168 | + .setLayerCount(1) |
| 169 | + .setLevelCount(1); |
| 170 | + auto image_view_ci = vk::ImageViewCreateInfo{}; |
| 171 | + image_view_ci.setViewType(vk::ImageViewType::e2D) |
| 172 | + .setFormat(m_ci.imageFormat) |
| 173 | + .setSubresourceRange(subresource_range); |
| 174 | + m_image_views.clear(); |
| 175 | + m_image_views.reserve(m_images.size()); |
| 176 | + for (auto const image : m_images) { |
| 177 | + image_view_ci.setImage(image); |
| 178 | + m_image_views.push_back(m_device.createImageViewUnique(image_view_ci)); |
| 179 | + } |
| 180 | +} |
| 181 | +``` |
| 182 | + |
| 183 | +We can now call these functions in `recreate()`, before `return true`, and add a log for some feedback: |
| 184 | + |
| 185 | +```cpp |
| 186 | + populate_images(); |
| 187 | + create_image_views(); |
| 188 | + |
| 189 | + size = get_size(); |
| 190 | + std::println("[lvk] Swapchain [{}x{}]", size.x, size.y); |
| 191 | + return true; |
| 192 | +``` |
| 193 | +
|
| 194 | +> The log can get a bit noisy on incessant resizing (especially on Linux). |
| 195 | +
|
| 196 | +To get the framebuffer size, add a helper function in `window.hpp/cpp`: |
| 197 | +
|
| 198 | +```cpp |
| 199 | +auto glfw::framebuffer_size(GLFWwindow* window) -> glm::ivec2 { |
| 200 | + auto ret = glm::ivec2{}; |
| 201 | + glfwGetFramebufferSize(window, &ret.x, &ret.y); |
| 202 | + return ret; |
| 203 | +} |
| 204 | +``` |
| 205 | + |
| 206 | +Finally, add a `std::optional<Swapchain>` member to `App` after `m_device`, add the create function, and call it after `create_device()`: |
| 207 | + |
| 208 | +```cpp |
| 209 | + std::optional<Swapchain> m_swapchain{}; |
| 210 | + |
| 211 | +// ... |
| 212 | +void App::create_swapchain() { |
| 213 | + auto const size = glfw::framebuffer_size(m_window.get()); |
| 214 | + m_swapchain.emplace(*m_device, m_gpu, *m_surface, size); |
| 215 | +} |
| 216 | +``` |
0 commit comments