Skip to content

Commit 5a220f3

Browse files
authored
Initialization (#1)
* Create Window and Instance * Create Surface * Select GPU * CI: Use clang-19 for format * Update guide * Create Device * `ScopedWaiter` * `Swapchain`
1 parent fba46b2 commit 5a220f3

24 files changed

+1240
-10
lines changed

.github/workflows/ci.yml

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ jobs:
55
runs-on: ubuntu-latest
66
steps:
77
- uses: actions/checkout@v4
8+
- name: init
9+
run: |
10+
sudo apt update -yqq && sudo apt install -yqq clang-format-19
11+
sudo update-alternatives --remove-all clang-format
12+
sudo update-alternatives --install /usr/bin/clang-format clang-format /usr/bin/clang-format-19 10
13+
clang-format --version
814
- name: format code
915
run: scripts/format_code.sh
1016
- name: check diff
@@ -14,9 +20,9 @@ jobs:
1420
steps:
1521
- uses: actions/checkout@v4
1622
- name: init
17-
run: uname -m; sudo apt install -yqq ninja-build
23+
run: uname -m; sudo apt update -yqq && sudo apt install -yqq ninja-build mesa-common-dev libwayland-dev libxkbcommon-dev wayland-protocols extra-cmake-modules
1824
- name: configure
19-
run: export CXX=g++-14; cmake -S . --preset=default -B build
25+
run: export CXX=g++-14; cmake -S . --preset=default -B build -DGLFW_BUILD_X11=OFF
2026
- name: build debug
2127
run: cmake --build build --config=Debug
2228
- name: build release
@@ -30,9 +36,9 @@ jobs:
3036
steps:
3137
- uses: actions/checkout@v4
3238
- name: init
33-
run: uname -m; sudo apt install -yqq ninja-build
39+
run: uname -m; sudo apt update -yqq && sudo apt install -yqq ninja-build mesa-common-dev libwayland-dev libxkbcommon-dev wayland-protocols extra-cmake-modules
3440
- name: configure
35-
run: cmake -S . --preset=ninja-clang -B build
41+
run: cmake -S . --preset=ninja-clang -B build -DGLFW_BUILD_X11=OFF
3642
- name: build debug
3743
run: cmake --build build --config=Debug
3844
- name: build release
@@ -46,9 +52,9 @@ jobs:
4652
steps:
4753
- uses: actions/checkout@v4
4854
- name: init
49-
run: uname -m
55+
run: uname -m; sudo apt update -yqq && sudo apt install -yqq ninja-build mesa-common-dev libwayland-dev libxkbcommon-dev wayland-protocols extra-cmake-modules
5056
- name: configure
51-
run: export CXX=g++-14; cmake -S . --preset=default -B build
57+
run: export CXX=g++-14; cmake -S . --preset=default -B build -DGLFW_BUILD_X11=OFF
5258
- name: build debug
5359
run: cmake --build build --config=Debug
5460
- name: build release
@@ -62,9 +68,9 @@ jobs:
6268
steps:
6369
- uses: actions/checkout@v4
6470
- name: init
65-
run: uname -m
71+
run: uname -m; sudo apt update -yqq && sudo apt install -yqq ninja-build mesa-common-dev libwayland-dev libxkbcommon-dev wayland-protocols extra-cmake-modules
6672
- name: configure
67-
run: cmake -S . --preset=ninja-clang -B build
73+
run: cmake -S . --preset=ninja-clang -B build -DGLFW_BUILD_X11=OFF
6874
- name: build debug
6975
run: cmake --build build --config=Debug
7076
- name: build release

CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,21 @@ set(CMAKE_CXX_EXTENSIONS OFF)
1111
set(CMAKE_DEBUG_POSTFIX "-d")
1212
set(BUILD_SHARED_LIBS OFF)
1313

14+
# add learn-vk::ext target
15+
add_subdirectory(ext)
16+
1417
# declare executable target
1518
add_executable(${PROJECT_NAME})
1619

20+
# link to ext target
21+
target_link_libraries(${PROJECT_NAME} PRIVATE
22+
learn-vk::ext
23+
)
24+
1725
# setup precompiled header
1826
target_precompile_headers(${PROJECT_NAME} PRIVATE
27+
<glm/glm.hpp>
28+
<vulkan/vulkan.hpp>
1929
)
2030

2131
# enable including headers in 'src/'

ext/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src/

ext/CMakeLists.txt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
project(learn-vk-ext)
2+
3+
# extract src.zip
4+
file(ARCHIVE_EXTRACT INPUT "${CMAKE_CURRENT_SOURCE_DIR}/src.zip" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}")
5+
6+
# add GLFW to build tree
7+
set(GLFW_INSTALL OFF)
8+
set(GLFW_BUILD_DOCS OFF)
9+
message(STATUS "[glfw]")
10+
add_subdirectory(src/glfw)
11+
add_library(glfw::glfw ALIAS glfw)
12+
13+
# add GLM to build tree
14+
set(GLM_ENABLE_CXX_20 ON)
15+
message(STATUS "[glm]")
16+
add_subdirectory(src/glm)
17+
target_compile_definitions(glm PUBLIC
18+
GLM_FORCE_XYZW_ONLY
19+
GLM_FORCE_RADIANS
20+
GLM_FORCE_DEPTH_ZERO_TO_ONE
21+
GLM_FORCE_SILENT_WARNINGS
22+
GLM_ENABLE_EXPERIMENTAL
23+
GLM_EXT_INCLUDED
24+
)
25+
26+
# add Vulkan-Headers to build tree
27+
message(STATUS "[Vulkan-Headers]")
28+
add_subdirectory(src/Vulkan-Headers)
29+
30+
# declare ext library target
31+
add_library(${PROJECT_NAME} INTERFACE)
32+
add_library(learn-vk::ext ALIAS ${PROJECT_NAME})
33+
34+
# link to all dependencies
35+
target_link_libraries(${PROJECT_NAME} INTERFACE
36+
glfw::glfw
37+
glm::glm
38+
Vulkan::Headers
39+
)
40+
41+
# setup preprocessor defines
42+
target_compile_definitions(${PROJECT_NAME} INTERFACE
43+
GLFW_INCLUDE_VULKAN # enable GLFW's Vulkan API
44+
VK_NO_PROTOTYPES # Dynamically load Vulkan at runtime
45+
)
46+
47+
if(CMAKE_SYSTEM_NAME STREQUAL Linux)
48+
# link to dynamic loader
49+
target_link_libraries(${PROJECT_NAME} INTERFACE dl)
50+
endif()

ext/src.zip

2.99 MB
Binary file not shown.

guide/src/SUMMARY.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,11 @@
88
- [Project Layout](getting_started/project_layout.md)
99
- [Validation Layers](getting_started/validation_layers.md)
1010
- [class App](getting_started/class_app.md)
11-
11+
- [Initialization](initialization/README.md)
12+
- [GLFW Window](initialization/glfw_window.md)
13+
- [Vulkan Instance](initialization/instance.md)
14+
- [Vulkan Surface](initialization/surface.md)
15+
- [Vulkan Physical Device](initialization/gpu.md)
16+
- [Vulkan Device](initialization/device.md)
17+
- [Scoped Waiter](initialization/scoped_waiter.md)
18+
- [Swapchain](initialization/swapchain.md)

guide/src/initialization/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Initialization
2+
3+
This section deals with initialization of all the systems needed, including:
4+
5+
- Initializing GLFW and creating a Window
6+
- Creating a Vulkan Instance
7+
- Creating a Vulkan Surface
8+
- Selecting a Vulkan Physical Device
9+
- Creating a Vulkan logical Device
10+
- Creating a Vulkan Swapchain
11+
12+
If any step here fails, it is a fatal error as we can't do anything meaningful beyond that point.

guide/src/initialization/device.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# Vulkan Device
2+
3+
A [Vulkan Device](https://registry.khronos.org/vulkan/specs/latest/man/html/VkDevice.html) is a logical instance of a Physical Device, and will the primary interface for everything Vulkan now onwards. [Vulkan Queues](https://registry.khronos.org/vulkan/specs/latest/man/html/VkQueue.html) are owned by the Device, we will need one from the queue family stored in the `Gpu`, to submit recorded command buffers. We also need to explicitly declare all features we want to use, eg [Dynamic Rendering](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_dynamic_rendering.html) and [Synchronization2](https://registry.khronos.org/vulkan/specs/latest/man/html/VK_KHR_synchronization2.html).
4+
5+
Setup a `vk::QueueCreateInfo` object:
6+
7+
```cpp
8+
auto queue_ci = vk::DeviceQueueCreateInfo{};
9+
// since we use only one queue, it has the entire priority range, ie, 1.0
10+
static constexpr auto queue_priorities_v = std::array{1.0f};
11+
queue_ci.setQueueFamilyIndex(m_gpu.queue_family)
12+
.setQueueCount(1)
13+
.setQueuePriorities(queue_priorities_v);
14+
```
15+
16+
Setup the core device features:
17+
18+
```cpp
19+
auto enabled_features = vk::PhysicalDeviceFeatures{};
20+
enabled_features.fillModeNonSolid = m_gpu.features.fillModeNonSolid;
21+
enabled_features.wideLines = m_gpu.features.wideLines;
22+
enabled_features.samplerAnisotropy = m_gpu.features.samplerAnisotropy;
23+
enabled_features.sampleRateShading = m_gpu.features.sampleRateShading;
24+
```
25+
26+
Setup the additional features, using `setPNext()` to chain them:
27+
28+
```cpp
29+
auto sync_feature = vk::PhysicalDeviceSynchronization2Features{vk::True};
30+
auto dynamic_rendering_feature =
31+
vk::PhysicalDeviceDynamicRenderingFeatures{vk::True};
32+
sync_feature.setPNext(&dynamic_rendering_feature);
33+
```
34+
35+
Setup a `vk::DeviceCreateInfo` object:
36+
37+
```cpp
38+
auto device_ci = vk::DeviceCreateInfo{};
39+
static constexpr auto extensions_v =
40+
std::array{VK_KHR_SWAPCHAIN_EXTENSION_NAME};
41+
device_ci.setPEnabledExtensionNames(extensions_v)
42+
.setQueueCreateInfos(queue_ci)
43+
.setPEnabledFeatures(&enabled_features)
44+
.setPNext(&sync_feature);
45+
```
46+
47+
Declare a `vk::UniqueDevice` member after `m_gpu`, create it, and initialize the dispatcher against it:
48+
49+
```cpp
50+
m_device = m_gpu.device.createDeviceUnique(device_ci);
51+
VULKAN_HPP_DEFAULT_DISPATCHER.init(*m_device);
52+
```
53+
54+
Declare a `vk::Queue` member (order doesn't matter since it's just a handle, the actual Queue is owned by the Device) and initialize it:
55+
56+
```cpp
57+
static constexpr std::uint32_t queue_index_v{0};
58+
m_queue = m_device->getQueue(m_gpu.queue_family, queue_index_v);
59+
```
60+
61+
## ScopedWaiter
62+
63+
A useful abstraction to have is an object that in its destructor waits/blocks until the Device is idle. Being able to do arbitary things on scope exit is useful in general too, but it requires some custom class template like `UniqueResource<Type, Deleter>`. We shall "abuse" `std::unique_ptr<Type, Deleter>` instead: it will not manage the pointer (`Type*`) at all, but instead `Deleter` will call a member function on it (if it isn't null).
64+
65+
Adding this to a new header `scoped_waiter.hpp`:
66+
67+
```cpp
68+
class ScopedWaiter {
69+
public:
70+
ScopedWaiter() = default;
71+
72+
explicit ScopedWaiter(vk::Device const* device) : m_device(device) {}
73+
74+
private:
75+
struct Deleter {
76+
void operator()(vk::Device const* device) const noexcept {
77+
if (device == nullptr) { return; }
78+
device->waitIdle();
79+
}
80+
};
81+
std::unique_ptr<vk::Device const, Deleter> m_device{};
82+
};
83+
```
84+
85+
This requires the passed `vk::Device*` to outlive itself, so to be defensive we make `App` be non-moveable and non-copiable, and create a member factory function for waiters:
86+
87+
```cpp
88+
auto operator=(App&&) = delete;
89+
// ...
90+
91+
[[nodiscard]] auto create_waiter() const -> ScopedWaiter {
92+
return ScopedWaiter{&*m_device};
93+
}
94+
```
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# GLFW Window
2+
3+
We will use GLFW (3.4) for windowing and related events. The library - like all external dependencies - is configured and added to the build tree in `ext/CMakeLists.txt`. `GLFW_INCLUDE_VULKAN` is defined for all consumers, to enable GLFW's Vulkan related functions (known as **Window System Integration (WSI)**). GLFW 3.4 supports Wayland on Linux, and by default it builds backends for both X11 and Wayland. For this reason it will need the development headers for both platforms (and some other Wayland/CMake dependencies) to configure/build successfully. A particular backend can be requested at runtime if desired via `GLFW_PLATFORM`.
4+
5+
Although it is quite feasible to have multiple windows in a Vulkan-GLFW application, that is out of scope of this guide. So for our purposes GLFW (the library) and a single window are a monolithic unit - initialized and destroyed together. This can be encapsulated in a `std::unique_ptr` with a custom deleter, especially since GLFW returns an opaque pointer (`GLFWwindow*`).
6+
7+
```cpp
8+
// window.hpp
9+
namespace lvk::glfw {
10+
struct Deleter {
11+
void operator()(GLFWwindow* window) const noexcept;
12+
};
13+
14+
using Window = std::unique_ptr<GLFWwindow, Deleter>;
15+
16+
// Returns a valid Window if successful, else throws.
17+
[[nodiscard]] auto create_window(glm::ivec2 size, char const* title) -> Window;
18+
} // namespace lvk::glfw
19+
20+
// window.cpp
21+
void Deleter::operator()(GLFWwindow* window) const noexcept {
22+
glfwDestroyWindow(window);
23+
glfwTerminate();
24+
}
25+
```
26+
27+
GLFW can create fullscreen and borderless windows, but we will stick to a standard window with decorations. Since we cannot do anything useful if we are unable to create a window, all other branches throw a fatal exception (caught in main).
28+
29+
```cpp
30+
auto glfw::create_window(glm::ivec2 const size, char const* title) -> Window {
31+
static auto const on_error = [](int const code, char const* description) {
32+
std::println(stderr, "[GLFW] Error {}: {}", code, description);
33+
};
34+
glfwSetErrorCallback(on_error);
35+
if (glfwInit() != GLFW_TRUE) {
36+
throw std::runtime_error{"Failed to initialize GLFW"};
37+
}
38+
// check for Vulkan support.
39+
if (glfwVulkanSupported() != GLFW_TRUE) {
40+
throw std::runtime_error{"Vulkan not supported"};
41+
}
42+
auto ret = Window{};
43+
// tell GLFW that we don't want an OpenGL context.
44+
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
45+
ret.reset(glfwCreateWindow(size.x, size.y, title, nullptr, nullptr));
46+
if (!ret) { throw std::runtime_error{"Failed to create GLFW Window"}; }
47+
return ret;
48+
}
49+
```
50+
51+
`App` can now store a `glfw::Window` and keep polling it in `run()` until it gets closed by the user. We will not be able to draw anything to the window for a while, but this is the first step in that journey.
52+
53+
Declare it as a private member:
54+
55+
```cpp
56+
private:
57+
glfw::Window m_window{};
58+
```
59+
60+
Add some private member functions to encapsulate each operation:
61+
62+
```cpp
63+
void create_window();
64+
65+
void main_loop();
66+
```
67+
68+
Implement them and call them in `run()`:
69+
70+
```cpp
71+
void App::run() {
72+
create_window();
73+
74+
main_loop();
75+
}
76+
77+
void App::create_window() {
78+
m_window = glfw::create_window({1280, 720}, "Learn Vulkan");
79+
}
80+
81+
void App::main_loop() {
82+
while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) {
83+
glfwPollEvents();
84+
}
85+
}
86+
```
87+
88+
> On Wayland you will not even see a window yet: it is only shown _after_ the application presents a framebuffer to it.
89+

0 commit comments

Comments
 (0)