Skip to content

Commit aa4496b

Browse files
committed
Swapchain
1 parent 61930b5 commit aa4496b

File tree

9 files changed

+396
-0
lines changed

9 files changed

+396
-0
lines changed

guide/src/SUMMARY.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
- [Vulkan Physical Device](initialization/gpu.md)
1616
- [Vulkan Device](initialization/device.md)
1717
- [Scoped Waiter](initialization/scoped_waiter.md)
18+
- [Swapchain](initialization/swapchain.md)

guide/src/initialization/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ This section deals with initialization of all the systems needed, including:
77
- Creating a Vulkan Surface
88
- Selecting a Vulkan Physical Device
99
- Creating a Vulkan logical Device
10+
- Creating a Vulkan Swapchain
1011

1112
If any step here fails, it is a fatal error as we can't do anything meaningful beyond that point.

guide/src/initialization/swapchain.md

+216
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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+
```

src/app.cpp

+6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ void App::run() {
1212
create_surface();
1313
select_gpu();
1414
create_device();
15+
create_swapchain();
1516

1617
main_loop();
1718
}
@@ -84,6 +85,11 @@ void App::create_device() {
8485
m_waiter = ScopedWaiter{*m_device};
8586
}
8687

88+
void App::create_swapchain() {
89+
auto const size = glfw::framebuffer_size(m_window.get());
90+
m_swapchain.emplace(*m_device, m_gpu, *m_surface, size);
91+
}
92+
8793
void App::main_loop() {
8894
while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) {
8995
glfwPollEvents();

src/app.hpp

+4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22
#include <gpu.hpp>
33
#include <scoped_waiter.hpp>
4+
#include <swapchain.hpp>
45
#include <vulkan/vulkan.hpp>
56
#include <window.hpp>
67

@@ -15,6 +16,7 @@ class App {
1516
void create_surface();
1617
void select_gpu();
1718
void create_device();
19+
void create_swapchain();
1820

1921
void main_loop();
2022

@@ -25,6 +27,8 @@ class App {
2527
vk::UniqueDevice m_device{};
2628
vk::Queue m_queue{};
2729

30+
std::optional<Swapchain> m_swapchain{};
31+
2832
ScopedWaiter m_waiter{};
2933
};
3034
} // namespace lvk

0 commit comments

Comments
 (0)