From c8df28fecb4971233f04f7f71730a3be69eec7f9 Mon Sep 17 00:00:00 2001 From: Karn Kaul Date: Sun, 23 Mar 2025 16:00:08 -0700 Subject: [PATCH] Add more comments, fixup indents in guide --- guide/src/getting_started/class_app.md | 24 +-- guide/src/initialization/device.md | 59 +++--- guide/src/initialization/glfw_window.md | 60 +++--- guide/src/initialization/gpu.md | 119 ++++++----- guide/src/initialization/instance.md | 41 ++-- guide/src/initialization/scoped_waiter.md | 48 ++--- guide/src/initialization/surface.md | 18 +- guide/src/initialization/swapchain.md | 228 +++++++++++----------- src/app.cpp | 14 +- src/app.hpp | 7 +- src/scoped.hpp | 2 +- src/swapchain.cpp | 11 ++ 12 files changed, 336 insertions(+), 295 deletions(-) diff --git a/guide/src/getting_started/class_app.md b/guide/src/getting_started/class_app.md index 4f684db..9336a83 100644 --- a/guide/src/getting_started/class_app.md +++ b/guide/src/getting_started/class_app.md @@ -6,15 +6,15 @@ // app.hpp namespace lvk { class App { - public: - void run(); + public: + void run(); }; } // namespace lvk // app.cpp namespace lvk { void App::run() { - // TODO + // TODO } } // namespace lvk ``` @@ -26,14 +26,14 @@ void App::run() { ```cpp // main.cpp auto main() -> int { - try { - lvk::App{}.run(); - } catch (std::exception const& e) { - std::println(stderr, "PANIC: {}", e.what()); - return EXIT_FAILURE; - } catch (...) { - std::println("PANIC!"); - return EXIT_FAILURE; - } + try { + lvk::App{}.run(); + } catch (std::exception const& e) { + std::println(stderr, "PANIC: {}", e.what()); + return EXIT_FAILURE; + } catch (...) { + std::println("PANIC!"); + return EXIT_FAILURE; + } } ``` diff --git a/guide/src/initialization/device.md b/guide/src/initialization/device.md index 430dc52..660889b 100644 --- a/guide/src/initialization/device.md +++ b/guide/src/initialization/device.md @@ -5,55 +5,62 @@ A [Vulkan Device](https://registry.khronos.org/vulkan/specs/latest/man/html/VkDe 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); +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; +// nice-to-have optional core features, enable if GPU supports them. +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); +// extra features that need to be explicitly enabled. +auto sync_feature = vk::PhysicalDeviceSynchronization2Features{vk::True}; +auto dynamic_rendering_feature = + vk::PhysicalDeviceDynamicRenderingFeatures{vk::True}; +// sync_feature.pNext => dynamic_rendering_feature, +// and later device_ci.pNext => sync_feature. +// this is 'pNext chaining'. +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); +auto device_ci = vk::DeviceCreateInfo{}; +// we only need one device extension: Swapchain. +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); +m_device = m_gpu.device.createDeviceUnique(device_ci); +// initialize the dispatcher against the created Device. +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); +static constexpr std::uint32_t queue_index_v{0}; +m_queue = m_device->getQueue(m_gpu.queue_family, queue_index_v); ``` diff --git a/guide/src/initialization/glfw_window.md b/guide/src/initialization/glfw_window.md index 6ae6e03..1bcb376 100644 --- a/guide/src/initialization/glfw_window.md +++ b/guide/src/initialization/glfw_window.md @@ -8,7 +8,7 @@ Although it is quite feasible to have multiple windows in a Vulkan-GLFW applicat // window.hpp namespace lvk::glfw { struct Deleter { - void operator()(GLFWwindow* window) const noexcept; + void operator()(GLFWwindow* window) const noexcept; }; using Window = std::unique_ptr; @@ -19,8 +19,8 @@ using Window = std::unique_ptr; // window.cpp void Deleter::operator()(GLFWwindow* window) const noexcept { - glfwDestroyWindow(window); - glfwTerminate(); + glfwDestroyWindow(window); + glfwTerminate(); } ``` @@ -28,23 +28,23 @@ GLFW can create fullscreen and borderless windows, but we will stick to a standa ```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; + 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; } ``` @@ -53,35 +53,35 @@ auto glfw::create_window(glm::ivec2 const size, char const* title) -> Window { Declare it as a private member: ```cpp - private: - glfw::Window m_window{}; +private: + glfw::Window m_window{}; ``` Add some private member functions to encapsulate each operation: ```cpp - void create_window(); +void create_window(); - void main_loop(); +void main_loop(); ``` Implement them and call them in `run()`: ```cpp void App::run() { - create_window(); +create_window(); - main_loop(); +main_loop(); } void App::create_window() { - m_window = glfw::create_window({1280, 720}, "Learn Vulkan"); + m_window = glfw::create_window({1280, 720}, "Learn Vulkan"); } void App::main_loop() { - while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) { - glfwPollEvents(); - } + while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) { + glfwPollEvents(); + } } ``` diff --git a/guide/src/initialization/gpu.md b/guide/src/initialization/gpu.md index a7601ea..af5d5f9 100644 --- a/guide/src/initialization/gpu.md +++ b/guide/src/initialization/gpu.md @@ -14,82 +14,81 @@ We wrap the actual Physical Device and a few other useful objects into `struct G constexpr auto vk_version_v = VK_MAKE_VERSION(1, 3, 0); struct Gpu { - vk::PhysicalDevice device{}; - vk::PhysicalDeviceProperties properties{}; - vk::PhysicalDeviceFeatures features{}; - std::uint32_t queue_family{}; + vk::PhysicalDevice device{}; + vk::PhysicalDeviceProperties properties{}; + vk::PhysicalDeviceFeatures features{}; + std::uint32_t queue_family{}; }; [[nodiscard]] auto get_suitable_gpu(vk::Instance instance, - vk::SurfaceKHR surface) -> Gpu; + vk::SurfaceKHR surface) -> Gpu; ``` The implementation: ```cpp auto lvk::get_suitable_gpu(vk::Instance const instance, - vk::SurfaceKHR const surface) -> Gpu { - auto const supports_swapchain = [](Gpu const& gpu) { - static constexpr std::string_view name_v = - VK_KHR_SWAPCHAIN_EXTENSION_NAME; - static constexpr auto is_swapchain = - [](vk::ExtensionProperties const& properties) { - return properties.extensionName.data() == name_v; - }; - auto const properties = gpu.device.enumerateDeviceExtensionProperties(); - auto const it = std::ranges::find_if(properties, is_swapchain); - return it != properties.end(); - }; - - auto const set_queue_family = [](Gpu& out_gpu) { - static constexpr auto queue_flags_v = - vk::QueueFlagBits::eGraphics | vk::QueueFlagBits::eTransfer; - for (auto const [index, family] : - std::views::enumerate(out_gpu.device.getQueueFamilyProperties())) { - if ((family.queueFlags & queue_flags_v) == queue_flags_v) { - out_gpu.queue_family = static_cast(index); - return true; - } - } - return false; - }; - - auto const can_present = [surface](Gpu const& gpu) { - return gpu.device.getSurfaceSupportKHR(gpu.queue_family, surface) == - vk::True; - }; - - auto fallback = Gpu{}; - for (auto const& device : instance.enumeratePhysicalDevices()) { - auto gpu = Gpu{.device = device, .properties = device.getProperties()}; - if (gpu.properties.apiVersion < vk_version_v) { continue; } - if (!supports_swapchain(gpu)) { continue; } - if (!set_queue_family(gpu)) { continue; } - if (!can_present(gpu)) { continue; } - gpu.features = gpu.device.getFeatures(); - if (gpu.properties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) { - return gpu; - } - // keep iterating in case we find a Discrete Gpu later. - fallback = gpu; - } - if (fallback.device) { return fallback; } - - throw std::runtime_error{"No suitable Vulkan Physical Devices"}; + vk::SurfaceKHR const surface) -> Gpu { + auto const supports_swapchain = [](Gpu const& gpu) { + static constexpr std::string_view name_v = + VK_KHR_SWAPCHAIN_EXTENSION_NAME; + static constexpr auto is_swapchain = + [](vk::ExtensionProperties const& properties) { + return properties.extensionName.data() == name_v; + }; + auto const properties = gpu.device.enumerateDeviceExtensionProperties(); + auto const it = std::ranges::find_if(properties, is_swapchain); + return it != properties.end(); + }; + + auto const set_queue_family = [](Gpu& out_gpu) { + static constexpr auto queue_flags_v = + vk::QueueFlagBits::eGraphics | vk::QueueFlagBits::eTransfer; + for (auto const [index, family] : + std::views::enumerate(out_gpu.device.getQueueFamilyProperties())) { + if ((family.queueFlags & queue_flags_v) == queue_flags_v) { + out_gpu.queue_family = static_cast(index); + return true; + } + } + return false; + }; + + auto const can_present = [surface](Gpu const& gpu) { + return gpu.device.getSurfaceSupportKHR(gpu.queue_family, surface) == + vk::True; + }; + + auto fallback = Gpu{}; + for (auto const& device : instance.enumeratePhysicalDevices()) { + auto gpu = Gpu{.device = device, .properties = device.getProperties()}; + if (gpu.properties.apiVersion < vk_version_v) { continue; } + if (!supports_swapchain(gpu)) { continue; } + if (!set_queue_family(gpu)) { continue; } + if (!can_present(gpu)) { continue; } + gpu.features = gpu.device.getFeatures(); + if (gpu.properties.deviceType == vk::PhysicalDeviceType::eDiscreteGpu) { + return gpu; + } + // keep iterating in case we find a Discrete Gpu later. + fallback = gpu; + } + if (fallback.device) { return fallback; } + + throw std::runtime_error{"No suitable Vulkan Physical Devices"}; } ``` Finally, add a `Gpu` member in `App` and initialize it after `create_surface()`: ```cpp - create_surface(); - select_gpu(); - // ... - +create_surface(); +select_gpu(); +// ... void App::select_gpu() { - m_gpu = get_suitable_gpu(*m_instance, *m_surface); - std::println("Using GPU: {}", - std::string_view{m_gpu.properties.deviceName}); + m_gpu = get_suitable_gpu(*m_instance, *m_surface); + std::println("Using GPU: {}", + std::string_view{m_gpu.properties.deviceName}); } ``` diff --git a/guide/src/initialization/instance.md b/guide/src/initialization/instance.md index 6b146a3..eee82bc 100644 --- a/guide/src/initialization/instance.md +++ b/guide/src/initialization/instance.md @@ -20,11 +20,12 @@ In `App`, create a new member function `create_instance()` and call it after `c ```cpp void App::create_instance() { - VULKAN_HPP_DEFAULT_DISPATCHER.init(); - auto const loader_version = vk::enumerateInstanceVersion(); - if (loader_version < vk_version_v) { - throw std::runtime_error{"Loader does not support Vulkan 1.3"}; - } + // initialize the dispatcher without any arguments. + VULKAN_HPP_DEFAULT_DISPATCHER.init(); + auto const loader_version = vk::enumerateInstanceVersion(); + if (loader_version < vk_version_v) { + throw std::runtime_error{"Loader does not support Vulkan 1.3"}; + } } ``` @@ -32,37 +33,39 @@ We will need the WSI instance extensions, which GLFW conveniently provides for u ```cpp auto glfw::instance_extensions() -> std::span { - auto count = std::uint32_t{}; - auto const* extensions = glfwGetRequiredInstanceExtensions(&count); - return {extensions, static_cast(count)}; + auto count = std::uint32_t{}; + auto const* extensions = glfwGetRequiredInstanceExtensions(&count); + return {extensions, static_cast(count)}; } ``` Continuing with instance creation, create a `vk::ApplicationInfo` object and fill it up: ```cpp - auto app_info = vk::ApplicationInfo{}; - app_info.setPApplicationName("Learn Vulkan").setApiVersion(vk_version_v); +auto app_info = vk::ApplicationInfo{}; +app_info.setPApplicationName("Learn Vulkan").setApiVersion(vk_version_v); ``` Create a `vk::InstanceCreateInfo` object and fill it up: ```cpp - auto instance_ci = vk::InstanceCreateInfo{}; - auto const extensions = glfw::instance_extensions(); - instance_ci.setPApplicationInfo(&app_info).setPEnabledExtensionNames( - extensions); +auto instance_ci = vk::InstanceCreateInfo{}; +// need WSI instance extensions here (platform-specific Swapchains). +auto const extensions = glfw::instance_extensions(); +instance_ci.setPApplicationInfo(&app_info).setPEnabledExtensionNames( + extensions); ``` Add a `vk::UniqueInstance` member _after_ `m_window`: this must be destroyed before terminating GLFW. Create it, and initialize the dispatcher against it: ```cpp - glfw::Window m_window{}; - vk::UniqueInstance m_instance{}; +glfw::Window m_window{}; +vk::UniqueInstance m_instance{}; - // ... - m_instance = vk::createInstanceUnique(instance_ci); - VULKAN_HPP_DEFAULT_DISPATCHER.init(*m_instance); +// ... +// initialize the dispatcher against the created Instance. +m_instance = vk::createInstanceUnique(instance_ci); +VULKAN_HPP_DEFAULT_DISPATCHER.init(*m_instance); ``` Make sure VkConfig is running with validation layers enabled, and debug/run the app. If "Information" level loader messages are enabled, you should see quite a bit of console output at this point: information about layers being loaded, physical devices and their ICDs being enumerated, etc. diff --git a/guide/src/initialization/scoped_waiter.md b/guide/src/initialization/scoped_waiter.md index fecba63..fdc529b 100644 --- a/guide/src/initialization/scoped_waiter.md +++ b/guide/src/initialization/scoped_waiter.md @@ -10,36 +10,36 @@ Being able to do arbitary things on scope exit will be useful in other spots too ```cpp template concept Scopeable = - std::equality_comparable && std::is_default_constructible_v; + std::equality_comparable && std::is_default_constructible_v; template class Scoped { - public: - Scoped(Scoped const&) = delete; - auto operator=(Scoped const&) = delete; + public: + Scoped(Scoped const&) = delete; + auto operator=(Scoped const&) = delete; - Scoped() = default; + Scoped() = default; - constexpr Scoped(Scoped&& rhs) noexcept - : m_t(std::exchange(rhs.m_t, Type{})) {} + constexpr Scoped(Scoped&& rhs) noexcept + : m_t(std::exchange(rhs.m_t, Type{})) {} - constexpr auto operator=(Scoped&& rhs) noexcept -> Scoped& { - if (&rhs != this) { std::swap(m_t, rhs.m_t); } - return *this; - } + constexpr auto operator=(Scoped&& rhs) noexcept -> Scoped& { + if (&rhs != this) { std::swap(m_t, rhs.m_t); } + return *this; + } - explicit constexpr Scoped(Type t) : m_t(std::move(t)) {} + explicit(false) constexpr Scoped(Type t) : m_t(std::move(t)) {} - constexpr ~Scoped() { - if (m_t == Type{}) { return; } - Deleter{}(m_t); - } + constexpr ~Scoped() { + if (m_t == Type{}) { return; } + Deleter{}(m_t); + } - [[nodiscard]] auto get() const -> Type const& { return m_t; } - [[nodiscard]] auto get() -> Type& { return m_t; } + [[nodiscard]] auto get() const -> Type const& { return m_t; } + [[nodiscard]] auto get() -> Type& { return m_t; } - private: - Type m_t{}; + private: + Type m_t{}; }; ``` @@ -49,9 +49,9 @@ A `ScopedWaiter` can now be implemented quite easily: ```cpp struct ScopedWaiterDeleter { - void operator()(vk::Device const device) const noexcept { - device.waitIdle(); - } + void operator()(vk::Device const device) const noexcept { + device.waitIdle(); + } }; using ScopedWaiter = Scoped; @@ -60,5 +60,5 @@ using ScopedWaiter = Scoped; Add a `ScopedWaiter` member to `App` _at the end_ of its member list: this must remain at the end to be the first member that gets destroyed, thus guaranteeing the device will be idle before the destruction of any other members begins. Initialize it after creating the Device: ```cpp - m_waiter = ScopedWaiter{*m_device}; +m_waiter = *m_device; ``` diff --git a/guide/src/initialization/surface.md b/guide/src/initialization/surface.md index 6a901e0..fd51a5a 100644 --- a/guide/src/initialization/surface.md +++ b/guide/src/initialization/surface.md @@ -6,14 +6,14 @@ Add another helper function in `window.hpp/cpp`: ```cpp auto glfw::create_surface(GLFWwindow* window, vk::Instance const instance) - -> vk::UniqueSurfaceKHR { - VkSurfaceKHR ret{}; - auto const result = - glfwCreateWindowSurface(instance, window, nullptr, &ret); - if (result != VK_SUCCESS || ret == VkSurfaceKHR{}) { - throw std::runtime_error{"Failed to create Vulkan Surface"}; - } - return vk::UniqueSurfaceKHR{ret, instance}; + -> vk::UniqueSurfaceKHR { + VkSurfaceKHR ret{}; + auto const result = + glfwCreateWindowSurface(instance, window, nullptr, &ret); + if (result != VK_SUCCESS || ret == VkSurfaceKHR{}) { + throw std::runtime_error{"Failed to create Vulkan Surface"}; + } + return vk::UniqueSurfaceKHR{ret, instance}; } ``` @@ -21,6 +21,6 @@ Add a `vk::UniqueSurfaceKHR` member to `App` after `m_instance`, and create the ```cpp void App::create_surface() { - m_surface = glfw::create_surface(m_window.get(), *m_instance); + m_surface = glfw::create_surface(m_window.get(), *m_instance); } ``` diff --git a/guide/src/initialization/swapchain.md b/guide/src/initialization/swapchain.md index fa7fee0..18320d5 100644 --- a/guide/src/initialization/swapchain.md +++ b/guide/src/initialization/swapchain.md @@ -8,32 +8,32 @@ We shall wrap the Vulkan Swapchain into our own `class Swapchain`. It will also // swapchain.hpp class Swapchain { public: - explicit Swapchain(vk::Device device, Gpu const& gpu, - vk::SurfaceKHR surface, glm::ivec2 size); + explicit Swapchain(vk::Device device, Gpu const& gpu, + vk::SurfaceKHR surface, glm::ivec2 size); - auto recreate(glm::ivec2 size) -> bool; + auto recreate(glm::ivec2 size) -> bool; - [[nodiscard]] auto get_size() const -> glm::ivec2 { - return {m_ci.imageExtent.width, m_ci.imageExtent.height}; - } + [[nodiscard]] auto get_size() const -> glm::ivec2 { + return {m_ci.imageExtent.width, m_ci.imageExtent.height}; + } private: - void populate_images(); - void create_image_views(); + void populate_images(); + void create_image_views(); - vk::Device m_device{}; - Gpu m_gpu{}; + vk::Device m_device{}; + Gpu m_gpu{}; - vk::SwapchainCreateInfoKHR m_ci{}; - vk::UniqueSwapchainKHR m_swapchain{}; - std::vector m_images{}; - std::vector m_image_views{}; + vk::SwapchainCreateInfoKHR m_ci{}; + vk::UniqueSwapchainKHR m_swapchain{}; + std::vector m_images{}; + std::vector m_image_views{}; }; // swapchain.cpp Swapchain::Swapchain(vk::Device const device, Gpu const& gpu, - vk::SurfaceKHR const surface, glm::ivec2 const size) - : m_device(device), m_gpu(gpu) {} + vk::SurfaceKHR const surface, glm::ivec2 const size) + : m_device(device), m_gpu(gpu) {} ``` ## Static Swapchain Properties @@ -42,24 +42,25 @@ Some Swapchain creation parameters like the image extent (size) and count depend ```cpp constexpr auto srgb_formats_v = std::array{ - vk::Format::eR8G8B8A8Srgb, - vk::Format::eB8G8R8A8Srgb, + vk::Format::eR8G8B8A8Srgb, + vk::Format::eB8G8R8A8Srgb, }; +// returns a SurfaceFormat with SrgbNonLinear color space and an sRGB format. [[nodiscard]] constexpr auto get_surface_format(std::span supported) - -> vk::SurfaceFormatKHR { - for (auto const desired : srgb_formats_v) { - auto const is_match = [desired](vk::SurfaceFormatKHR const& in) { - return in.format == desired && - in.colorSpace == - vk::ColorSpaceKHR::eVkColorspaceSrgbNonlinear; - }; - auto const it = std::ranges::find_if(supported, is_match); - if (it == supported.end()) { continue; } - return *it; - } - return supported.front(); + -> vk::SurfaceFormatKHR { + for (auto const desired : srgb_formats_v) { + auto const is_match = [desired](vk::SurfaceFormatKHR const& in) { + return in.format == desired && + in.colorSpace == + vk::ColorSpaceKHR::eVkColorspaceSrgbNonlinear; + }; + auto const it = std::ranges::find_if(supported, is_match); + if (it == supported.end()) { continue; } + return *it; + } + return supported.front(); } ``` @@ -68,17 +69,19 @@ An sRGB format is preferred because that is what the screen's color space is in. The constructor can now be implemented: ```cpp - auto const surface_format = - get_surface_format(m_gpu.device.getSurfaceFormatsKHR(surface)); - m_ci.setSurface(surface) - .setImageFormat(surface_format.format) - .setImageColorSpace(surface_format.colorSpace) - .setImageArrayLayers(1) - .setImageUsage(vk::ImageUsageFlagBits::eColorAttachment) - .setPresentMode(vk::PresentModeKHR::eFifo); - if (!recreate(size)) { - throw std::runtime_error{"Failed to create Vulkan Swapchain"}; - } +auto const surface_format = + get_surface_format(m_gpu.device.getSurfaceFormatsKHR(surface)); +m_ci.setSurface(surface) + .setImageFormat(surface_format.format) + .setImageColorSpace(surface_format.colorSpace) + .setImageArrayLayers(1) + // Swapchain images will be used as color attachments (render targets). + .setImageUsage(vk::ImageUsageFlagBits::eColorAttachment) + // eFifo is guaranteed to be supported. + .setPresentMode(vk::PresentModeKHR::eFifo); +if (!recreate(size)) { + throw std::runtime_error{"Failed to create Vulkan Swapchain"}; +} ``` ## Swapchain Recreation @@ -88,29 +91,30 @@ The constraints on Swapchain creation parameters are specified by [Surface Capab ```cpp constexpr std::uint32_t min_images_v{3}; +// returns currentExtent if specified, else clamped size. [[nodiscard]] constexpr auto get_image_extent(vk::SurfaceCapabilitiesKHR const& capabilities, - glm::uvec2 const size) -> vk::Extent2D { - constexpr auto limitless_v = 0xffffffff; - if (capabilities.currentExtent.width < limitless_v && - capabilities.currentExtent.height < limitless_v) { - return capabilities.currentExtent; - } - auto const x = std::clamp(size.x, capabilities.minImageExtent.width, - capabilities.maxImageExtent.width); - auto const y = std::clamp(size.y, capabilities.minImageExtent.height, - capabilities.maxImageExtent.height); - return vk::Extent2D{x, y}; + glm::uvec2 const size) -> vk::Extent2D { + constexpr auto limitless_v = 0xffffffff; + if (capabilities.currentExtent.width < limitless_v && + capabilities.currentExtent.height < limitless_v) { + return capabilities.currentExtent; + } + auto const x = std::clamp(size.x, capabilities.minImageExtent.width, + capabilities.maxImageExtent.width); + auto const y = std::clamp(size.y, capabilities.minImageExtent.height, + capabilities.maxImageExtent.height); + return vk::Extent2D{x, y}; } [[nodiscard]] constexpr auto get_image_count(vk::SurfaceCapabilitiesKHR const& capabilities) - -> std::uint32_t { - if (capabilities.maxImageCount < capabilities.minImageCount) { - return std::max(min_images_v, capabilities.minImageCount); - } - return std::clamp(min_images_v, capabilities.minImageCount, - capabilities.maxImageCount); + -> std::uint32_t { + if (capabilities.maxImageCount < capabilities.minImageCount) { + return std::max(min_images_v, capabilities.minImageCount); + } + return std::clamp(min_images_v, capabilities.minImageCount, + capabilities.maxImageCount); } ``` @@ -120,21 +124,23 @@ The dimensions of Vulkan Images must be positive, so if the incoming framebuffer ```cpp auto Swapchain::recreate(glm::ivec2 size) -> bool { - if (size.x <= 0 || size.y <= 0) { return false; } - - auto const capabilities = - m_gpu.device.getSurfaceCapabilitiesKHR(m_ci.surface); - m_ci.setImageExtent(get_image_extent(capabilities, size)) - .setMinImageCount(get_image_count(capabilities)) - .setOldSwapchain(m_swapchain ? *m_swapchain : vk::SwapchainKHR{}) - .setQueueFamilyIndices(m_gpu.queue_family); - assert(m_ci.imageExtent.width > 0 && m_ci.imageExtent.height > 0 && - m_ci.minImageCount >= min_images_v); - - m_device.waitIdle(); - m_swapchain = m_device.createSwapchainKHRUnique(m_ci); - - return true; + // Image sizes must be positive. + if (size.x <= 0 || size.y <= 0) { return false; } + + auto const capabilities = + m_gpu.device.getSurfaceCapabilitiesKHR(m_ci.surface); + m_ci.setImageExtent(get_image_extent(capabilities, size)) + .setMinImageCount(get_image_count(capabilities)) + .setOldSwapchain(m_swapchain ? *m_swapchain : vk::SwapchainKHR{}) + .setQueueFamilyIndices(m_gpu.queue_family); + assert(m_ci.imageExtent.width > 0 && m_ci.imageExtent.height > 0 && + m_ci.minImageCount >= min_images_v); + + // wait for the device to be idle before destroying the current swapchain. + m_device.waitIdle(); + m_swapchain = m_device.createSwapchainKHRUnique(m_ci); + + return true; } ``` @@ -142,20 +148,22 @@ After successful recreation we want to fill up those vectors of images and views ```cpp void require_success(vk::Result const result, char const* error_msg) { - if (result != vk::Result::eSuccess) { throw std::runtime_error{error_msg}; } + if (result != vk::Result::eSuccess) { throw std::runtime_error{error_msg}; } } -// ... +// ... void Swapchain::populate_images() { - auto image_count = std::uint32_t{}; - auto result = - m_device.getSwapchainImagesKHR(*m_swapchain, &image_count, nullptr); - require_success(result, "Failed to get Swapchain Images"); - - m_images.resize(image_count); - result = m_device.getSwapchainImagesKHR(*m_swapchain, &image_count, - m_images.data()); - require_success(result, "Failed to get Swapchain Images"); + // we use the more verbose two-call API to avoid assigning m_images to a new + // vector on every call. + auto image_count = std::uint32_t{}; + auto result = + m_device.getSwapchainImagesKHR(*m_swapchain, &image_count, nullptr); + require_success(result, "Failed to get Swapchain Images"); + + m_images.resize(image_count); + result = m_device.getSwapchainImagesKHR(*m_swapchain, &image_count, + m_images.data()); + require_success(result, "Failed to get Swapchain Images"); } ``` @@ -163,32 +171,34 @@ Creation of the views is fairly straightforward: ```cpp void Swapchain::create_image_views() { - auto subresource_range = vk::ImageSubresourceRange{}; - subresource_range.setAspectMask(vk::ImageAspectFlagBits::eColor) - .setLayerCount(1) - .setLevelCount(1); - auto image_view_ci = vk::ImageViewCreateInfo{}; - image_view_ci.setViewType(vk::ImageViewType::e2D) - .setFormat(m_ci.imageFormat) - .setSubresourceRange(subresource_range); - m_image_views.clear(); - m_image_views.reserve(m_images.size()); - for (auto const image : m_images) { - image_view_ci.setImage(image); - m_image_views.push_back(m_device.createImageViewUnique(image_view_ci)); - } + auto subresource_range = vk::ImageSubresourceRange{}; + // this is a color image with 1 layer and 1 mip-level (the default). + subresource_range.setAspectMask(vk::ImageAspectFlagBits::eColor) + .setLayerCount(1) + .setLevelCount(1); + auto image_view_ci = vk::ImageViewCreateInfo{}; + // set common parameters here (everything except the Image). + image_view_ci.setViewType(vk::ImageViewType::e2D) + .setFormat(m_ci.imageFormat) + .setSubresourceRange(subresource_range); + m_image_views.clear(); + m_image_views.reserve(m_images.size()); + for (auto const image : m_images) { + image_view_ci.setImage(image); + m_image_views.push_back(m_device.createImageViewUnique(image_view_ci)); + } } ``` We can now call these functions in `recreate()`, before `return true`, and add a log for some feedback: ```cpp - populate_images(); - create_image_views(); +populate_images(); +create_image_views(); - size = get_size(); - std::println("[lvk] Swapchain [{}x{}]", size.x, size.y); - return true; +size = get_size(); +std::println("[lvk] Swapchain [{}x{}]", size.x, size.y); +return true; ``` > The log can get a bit noisy on incessant resizing (especially on Linux). @@ -197,20 +207,20 @@ To get the framebuffer size, add a helper function in `window.hpp/cpp`: ```cpp auto glfw::framebuffer_size(GLFWwindow* window) -> glm::ivec2 { - auto ret = glm::ivec2{}; - glfwGetFramebufferSize(window, &ret.x, &ret.y); - return ret; + auto ret = glm::ivec2{}; + glfwGetFramebufferSize(window, &ret.x, &ret.y); + return ret; } ``` Finally, add a `std::optional` member to `App` after `m_device`, add the create function, and call it after `create_device()`: ```cpp - std::optional m_swapchain{}; +std::optional m_swapchain{}; // ... void App::create_swapchain() { - auto const size = glfw::framebuffer_size(m_window.get()); - m_swapchain.emplace(*m_device, m_gpu, *m_surface, size); + auto const size = glfw::framebuffer_size(m_window.get()); + m_swapchain.emplace(*m_device, m_gpu, *m_surface, size); } ``` diff --git a/src/app.cpp b/src/app.cpp index 74404f5..499e885 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1,8 +1,6 @@ #include #include -#include - VULKAN_HPP_DEFAULT_DISPATCH_LOADER_DYNAMIC_STORAGE namespace lvk { @@ -22,6 +20,7 @@ void App::create_window() { } void App::create_instance() { + // initialize the dispatcher without any arguments. VULKAN_HPP_DEFAULT_DISPATCHER.init(); auto const loader_version = vk::enumerateInstanceVersion(); if (loader_version < vk_version_v) { @@ -32,11 +31,13 @@ void App::create_instance() { app_info.setPApplicationName("Learn Vulkan").setApiVersion(vk_version_v); auto instance_ci = vk::InstanceCreateInfo{}; + // need WSI instance extensions here (platform-specific Swapchains). auto const extensions = glfw::instance_extensions(); instance_ci.setPApplicationInfo(&app_info).setPEnabledExtensionNames( extensions); m_instance = vk::createInstanceUnique(instance_ci); + // initialize the dispatcher against the created Instance. VULKAN_HPP_DEFAULT_DISPATCHER.init(*m_instance); } @@ -58,18 +59,24 @@ void App::create_device() { .setQueueCount(1) .setQueuePriorities(queue_priorities_v); + // nice-to-have optional core features, enable if GPU supports them. 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; + // extra features that need to be explicitly enabled. auto sync_feature = vk::PhysicalDeviceSynchronization2Features{vk::True}; auto dynamic_rendering_feature = vk::PhysicalDeviceDynamicRenderingFeatures{vk::True}; + // sync_feature.pNext => dynamic_rendering_feature, + // and later device_ci.pNext => sync_feature. + // this is 'pNext chaining'. sync_feature.setPNext(&dynamic_rendering_feature); auto device_ci = vk::DeviceCreateInfo{}; + // we only need one device extension: Swapchain. static constexpr auto extensions_v = std::array{VK_KHR_SWAPCHAIN_EXTENSION_NAME}; device_ci.setPEnabledExtensionNames(extensions_v) @@ -78,11 +85,12 @@ void App::create_device() { .setPNext(&sync_feature); m_device = m_gpu.device.createDeviceUnique(device_ci); + // initialize the dispatcher against the created Device. VULKAN_HPP_DEFAULT_DISPATCHER.init(*m_device); static constexpr std::uint32_t queue_index_v{0}; m_queue = m_device->getQueue(m_gpu.queue_family, queue_index_v); - m_waiter = ScopedWaiter{*m_device}; + m_waiter = *m_device; } void App::create_swapchain() { diff --git a/src/app.hpp b/src/app.hpp index 01adf3c..6c86da2 100644 --- a/src/app.hpp +++ b/src/app.hpp @@ -20,15 +20,18 @@ class App { void main_loop(); + // the order of these RAII members is crucially important. glfw::Window m_window{}; vk::UniqueInstance m_instance{}; vk::UniqueSurfaceKHR m_surface{}; - Gpu m_gpu{}; + Gpu m_gpu{}; // not an RAII member. vk::UniqueDevice m_device{}; - vk::Queue m_queue{}; + vk::Queue m_queue{}; // not an RAII member. std::optional m_swapchain{}; + // waiter must be the last member to ensure it blocks until device is idle + // before other members get destroyed. ScopedWaiter m_waiter{}; }; } // namespace lvk diff --git a/src/scoped.hpp b/src/scoped.hpp index 6376f48..cd60dbe 100644 --- a/src/scoped.hpp +++ b/src/scoped.hpp @@ -23,7 +23,7 @@ class Scoped { return *this; } - explicit constexpr Scoped(Type t) : m_t(std::move(t)) {} + explicit(false) constexpr Scoped(Type t) : m_t(std::move(t)) {} constexpr ~Scoped() { if (m_t == Type{}) { return; } diff --git a/src/swapchain.cpp b/src/swapchain.cpp index 331ab99..7cb288b 100644 --- a/src/swapchain.cpp +++ b/src/swapchain.cpp @@ -14,6 +14,7 @@ constexpr auto srgb_formats_v = std::array{ vk::Format::eB8G8R8A8Srgb, }; +// returns a SurfaceFormat with SrgbNonLinear color space and an sRGB format. [[nodiscard]] constexpr auto get_surface_format(std::span supported) -> vk::SurfaceFormatKHR { @@ -30,6 +31,7 @@ get_surface_format(std::span supported) return supported.front(); } +// returns currentExtent if specified, else clamped size. [[nodiscard]] constexpr auto get_image_extent(vk::SurfaceCapabilitiesKHR const& capabilities, glm::uvec2 const size) -> vk::Extent2D { @@ -55,6 +57,7 @@ get_image_count(vk::SurfaceCapabilitiesKHR const& capabilities) capabilities.maxImageCount); } +// throws if result is not eSuccess. void require_success(vk::Result const result, char const* error_msg) { if (result != vk::Result::eSuccess) { throw std::runtime_error{error_msg}; } } @@ -69,7 +72,9 @@ Swapchain::Swapchain(vk::Device const device, Gpu const& gpu, .setImageFormat(surface_format.format) .setImageColorSpace(surface_format.colorSpace) .setImageArrayLayers(1) + // Swapchain images will be used as color attachments (render targets). .setImageUsage(vk::ImageUsageFlagBits::eColorAttachment) + // eFifo is guaranteed to be supported. .setPresentMode(vk::PresentModeKHR::eFifo); if (!recreate(size)) { throw std::runtime_error{"Failed to create Vulkan Swapchain"}; @@ -77,6 +82,7 @@ Swapchain::Swapchain(vk::Device const device, Gpu const& gpu, } auto Swapchain::recreate(glm::ivec2 size) -> bool { + // Image sizes must be positive. if (size.x <= 0 || size.y <= 0) { return false; } auto const capabilities = @@ -88,6 +94,7 @@ auto Swapchain::recreate(glm::ivec2 size) -> bool { assert(m_ci.imageExtent.width > 0 && m_ci.imageExtent.height > 0 && m_ci.minImageCount >= min_images_v); + // wait for the device to be idle before destroying the current swapchain. m_device.waitIdle(); m_swapchain = m_device.createSwapchainKHRUnique(m_ci); @@ -100,6 +107,8 @@ auto Swapchain::recreate(glm::ivec2 size) -> bool { } void Swapchain::populate_images() { + // we use the more verbose two-call API to avoid assigning m_images to a new + // vector on every call. auto image_count = std::uint32_t{}; auto result = m_device.getSwapchainImagesKHR(*m_swapchain, &image_count, nullptr); @@ -113,10 +122,12 @@ void Swapchain::populate_images() { void Swapchain::create_image_views() { auto subresource_range = vk::ImageSubresourceRange{}; + // this is a color image with 1 layer and 1 mip-level (the default). subresource_range.setAspectMask(vk::ImageAspectFlagBits::eColor) .setLayerCount(1) .setLevelCount(1); auto image_view_ci = vk::ImageViewCreateInfo{}; + // set common parameters here (everything except the Image). image_view_ci.setViewType(vk::ImageViewType::e2D) .setFormat(m_ci.imageFormat) .setSubresourceRange(subresource_range);