Skip to content

Initialization #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: init
run: |
sudo apt update -yqq && sudo apt install -yqq clang-format-19
sudo update-alternatives --remove-all clang-format
sudo update-alternatives --install /usr/bin/clang-format clang-format /usr/bin/clang-format-19 10
clang-format --version
- name: format code
run: scripts/format_code.sh
- name: check diff
Expand All @@ -14,9 +20,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: init
run: uname -m; sudo apt install -yqq ninja-build
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
- name: configure
run: export CXX=g++-14; cmake -S . --preset=default -B build
run: export CXX=g++-14; cmake -S . --preset=default -B build -DGLFW_BUILD_X11=OFF
- name: build debug
run: cmake --build build --config=Debug
- name: build release
Expand All @@ -30,9 +36,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: init
run: uname -m; sudo apt install -yqq ninja-build
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
- name: configure
run: cmake -S . --preset=ninja-clang -B build
run: cmake -S . --preset=ninja-clang -B build -DGLFW_BUILD_X11=OFF
- name: build debug
run: cmake --build build --config=Debug
- name: build release
Expand All @@ -46,9 +52,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: init
run: uname -m
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
- name: configure
run: export CXX=g++-14; cmake -S . --preset=default -B build
run: export CXX=g++-14; cmake -S . --preset=default -B build -DGLFW_BUILD_X11=OFF
- name: build debug
run: cmake --build build --config=Debug
- name: build release
Expand All @@ -62,9 +68,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: init
run: uname -m
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
- name: configure
run: cmake -S . --preset=ninja-clang -B build
run: cmake -S . --preset=ninja-clang -B build -DGLFW_BUILD_X11=OFF
- name: build debug
run: cmake --build build --config=Debug
- name: build release
Expand Down
10 changes: 10 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,21 @@ set(CMAKE_CXX_EXTENSIONS OFF)
set(CMAKE_DEBUG_POSTFIX "-d")
set(BUILD_SHARED_LIBS OFF)

# add learn-vk::ext target
add_subdirectory(ext)

# declare executable target
add_executable(${PROJECT_NAME})

# link to ext target
target_link_libraries(${PROJECT_NAME} PRIVATE
learn-vk::ext
)

# setup precompiled header
target_precompile_headers(${PROJECT_NAME} PRIVATE
<glm/glm.hpp>
<vulkan/vulkan.hpp>
)

# enable including headers in 'src/'
Expand Down
1 change: 1 addition & 0 deletions ext/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
src/
50 changes: 50 additions & 0 deletions ext/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
project(learn-vk-ext)

# extract src.zip
file(ARCHIVE_EXTRACT INPUT "${CMAKE_CURRENT_SOURCE_DIR}/src.zip" DESTINATION "${CMAKE_CURRENT_SOURCE_DIR}")

# add GLFW to build tree
set(GLFW_INSTALL OFF)
set(GLFW_BUILD_DOCS OFF)
message(STATUS "[glfw]")
add_subdirectory(src/glfw)
add_library(glfw::glfw ALIAS glfw)

# add GLM to build tree
set(GLM_ENABLE_CXX_20 ON)
message(STATUS "[glm]")
add_subdirectory(src/glm)
target_compile_definitions(glm PUBLIC
GLM_FORCE_XYZW_ONLY
GLM_FORCE_RADIANS
GLM_FORCE_DEPTH_ZERO_TO_ONE
GLM_FORCE_SILENT_WARNINGS
GLM_ENABLE_EXPERIMENTAL
GLM_EXT_INCLUDED
)

# add Vulkan-Headers to build tree
message(STATUS "[Vulkan-Headers]")
add_subdirectory(src/Vulkan-Headers)

# declare ext library target
add_library(${PROJECT_NAME} INTERFACE)
add_library(learn-vk::ext ALIAS ${PROJECT_NAME})

# link to all dependencies
target_link_libraries(${PROJECT_NAME} INTERFACE
glfw::glfw
glm::glm
Vulkan::Headers
)

# setup preprocessor defines
target_compile_definitions(${PROJECT_NAME} INTERFACE
GLFW_INCLUDE_VULKAN # enable GLFW's Vulkan API
VK_NO_PROTOTYPES # Dynamically load Vulkan at runtime
)

if(CMAKE_SYSTEM_NAME STREQUAL Linux)
# link to dynamic loader
target_link_libraries(${PROJECT_NAME} INTERFACE dl)
endif()
Binary file added ext/src.zip
Binary file not shown.
9 changes: 8 additions & 1 deletion guide/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,11 @@
- [Project Layout](getting_started/project_layout.md)
- [Validation Layers](getting_started/validation_layers.md)
- [class App](getting_started/class_app.md)

- [Initialization](initialization/README.md)
- [GLFW Window](initialization/glfw_window.md)
- [Vulkan Instance](initialization/instance.md)
- [Vulkan Surface](initialization/surface.md)
- [Vulkan Physical Device](initialization/gpu.md)
- [Vulkan Device](initialization/device.md)
- [Scoped Waiter](initialization/scoped_waiter.md)
- [Swapchain](initialization/swapchain.md)
12 changes: 12 additions & 0 deletions guide/src/initialization/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Initialization

This section deals with initialization of all the systems needed, including:

- Initializing GLFW and creating a Window
- Creating a Vulkan Instance
- Creating a Vulkan Surface
- Selecting a Vulkan Physical Device
- Creating a Vulkan logical Device
- Creating a Vulkan Swapchain

If any step here fails, it is a fatal error as we can't do anything meaningful beyond that point.
94 changes: 94 additions & 0 deletions guide/src/initialization/device.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# Vulkan Device

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).

Setup a `vk::QueueCreateInfo` object:

```cpp
auto queue_ci = vk::DeviceQueueCreateInfo{};
// since we use only one queue, it has the entire priority range, ie, 1.0
static constexpr auto queue_priorities_v = std::array{1.0f};
queue_ci.setQueueFamilyIndex(m_gpu.queue_family)
.setQueueCount(1)
.setQueuePriorities(queue_priorities_v);
```

Setup the core device features:

```cpp
auto enabled_features = vk::PhysicalDeviceFeatures{};
enabled_features.fillModeNonSolid = m_gpu.features.fillModeNonSolid;
enabled_features.wideLines = m_gpu.features.wideLines;
enabled_features.samplerAnisotropy = m_gpu.features.samplerAnisotropy;
enabled_features.sampleRateShading = m_gpu.features.sampleRateShading;
```

Setup the additional features, using `setPNext()` to chain them:

```cpp
auto sync_feature = vk::PhysicalDeviceSynchronization2Features{vk::True};
auto dynamic_rendering_feature =
vk::PhysicalDeviceDynamicRenderingFeatures{vk::True};
sync_feature.setPNext(&dynamic_rendering_feature);
```

Setup a `vk::DeviceCreateInfo` object:

```cpp
auto device_ci = vk::DeviceCreateInfo{};
static constexpr auto extensions_v =
std::array{VK_KHR_SWAPCHAIN_EXTENSION_NAME};
device_ci.setPEnabledExtensionNames(extensions_v)
.setQueueCreateInfos(queue_ci)
.setPEnabledFeatures(&enabled_features)
.setPNext(&sync_feature);
```

Declare a `vk::UniqueDevice` member after `m_gpu`, create it, and initialize the dispatcher against it:

```cpp
m_device = m_gpu.device.createDeviceUnique(device_ci);
VULKAN_HPP_DEFAULT_DISPATCHER.init(*m_device);
```

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:

```cpp
static constexpr std::uint32_t queue_index_v{0};
m_queue = m_device->getQueue(m_gpu.queue_family, queue_index_v);
```

## ScopedWaiter

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).

Adding this to a new header `scoped_waiter.hpp`:

```cpp
class ScopedWaiter {
public:
ScopedWaiter() = default;

explicit ScopedWaiter(vk::Device const* device) : m_device(device) {}

private:
struct Deleter {
void operator()(vk::Device const* device) const noexcept {
if (device == nullptr) { return; }
device->waitIdle();
}
};
std::unique_ptr<vk::Device const, Deleter> m_device{};
};
```

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:

```cpp
auto operator=(App&&) = delete;
// ...

[[nodiscard]] auto create_waiter() const -> ScopedWaiter {
return ScopedWaiter{&*m_device};
}
```
89 changes: 89 additions & 0 deletions guide/src/initialization/glfw_window.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# GLFW Window

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`.

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*`).

```cpp
// window.hpp
namespace lvk::glfw {
struct Deleter {
void operator()(GLFWwindow* window) const noexcept;
};

using Window = std::unique_ptr<GLFWwindow, Deleter>;

// Returns a valid Window if successful, else throws.
[[nodiscard]] auto create_window(glm::ivec2 size, char const* title) -> Window;
} // namespace lvk::glfw

// window.cpp
void Deleter::operator()(GLFWwindow* window) const noexcept {
glfwDestroyWindow(window);
glfwTerminate();
}
```

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).

```cpp
auto glfw::create_window(glm::ivec2 const size, char const* title) -> Window {
static auto const on_error = [](int const code, char const* description) {
std::println(stderr, "[GLFW] Error {}: {}", code, description);
};
glfwSetErrorCallback(on_error);
if (glfwInit() != GLFW_TRUE) {
throw std::runtime_error{"Failed to initialize GLFW"};
}
// check for Vulkan support.
if (glfwVulkanSupported() != GLFW_TRUE) {
throw std::runtime_error{"Vulkan not supported"};
}
auto ret = Window{};
// tell GLFW that we don't want an OpenGL context.
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
ret.reset(glfwCreateWindow(size.x, size.y, title, nullptr, nullptr));
if (!ret) { throw std::runtime_error{"Failed to create GLFW Window"}; }
return ret;
}
```

`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.

Declare it as a private member:

```cpp
private:
glfw::Window m_window{};
```

Add some private member functions to encapsulate each operation:

```cpp
void create_window();

void main_loop();
```

Implement them and call them in `run()`:

```cpp
void App::run() {
create_window();

main_loop();
}

void App::create_window() {
m_window = glfw::create_window({1280, 720}, "Learn Vulkan");
}

void App::main_loop() {
while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) {
glfwPollEvents();
}
}
```

> On Wayland you will not even see a window yet: it is only shown _after_ the application presents a framebuffer to it.

Loading