diff --git a/ext/CMakeLists.txt b/ext/CMakeLists.txt index 0a02c51..94c6e10 100644 --- a/ext/CMakeLists.txt +++ b/ext/CMakeLists.txt @@ -27,21 +27,47 @@ target_compile_definitions(glm PUBLIC message(STATUS "[Vulkan-Headers]") add_subdirectory(src/Vulkan-Headers) +# setup Dear ImGui library +message(STATUS "[Dear ImGui]") +add_library(imgui) +add_library(imgui::imgui ALIAS imgui) +target_include_directories(imgui SYSTEM PUBLIC src/imgui) +target_link_libraries(imgui PUBLIC + glfw::glfw + Vulkan::Headers +) +target_compile_definitions(imgui PUBLIC + VK_NO_PROTOTYPES # Dynamically load Vulkan at runtime +) +target_sources(imgui PRIVATE + src/imgui/imconfig.h + src/imgui/imgui_demo.cpp + src/imgui/imgui_draw.cpp + src/imgui/imgui_internal.h + src/imgui/imgui_tables.cpp + src/imgui/imgui_widgets.cpp + src/imgui/imgui.cpp + src/imgui/imgui.h + + src/imgui/backends/imgui_impl_glfw.cpp + src/imgui/backends/imgui_impl_glfw.h + src/imgui/backends/imgui_impl_vulkan.cpp + src/imgui/backends/imgui_impl_vulkan.h +) + # 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 + imgui::imgui ) # 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) diff --git a/ext/src.zip b/ext/src.zip index d3f9930..032ad24 100644 Binary files a/ext/src.zip and b/ext/src.zip differ diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 7e29f42..f04d2c3 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -21,3 +21,6 @@ - [Render Sync](rendering/render_sync.md) - [Swapchain Update](rendering/swapchain_update.md) - [Dynamic Rendering](rendering/dynamic_rendering.md) +- [Dear ImGui](dear_imgui/README.md) + - [class DearImGui](dear_imgui/dear_imgui.md) + - [ImGui Integration](dear_imgui/imgui_integration.md) diff --git a/guide/src/dear_imgui/README.md b/guide/src/dear_imgui/README.md new file mode 100644 index 0000000..324cd8f --- /dev/null +++ b/guide/src/dear_imgui/README.md @@ -0,0 +1,3 @@ +# Dear ImGui + +Dear ImGui does not have native CMake support, and while adding the sources to the executable is an option, we will add it as an external library target: `imgui` to isolate it (and compile warnings etc) from our own code. This requires some changes to the `ext` target structure, since `imgui` will itself need to link to GLFW and Vulkan-Headers, have `VK_NO_PROTOTYPES` defined, etc. `learn-vk-ext` then links to `imgui` and any other libraries (currently only `glm`). We are using Dear ImGui v1.91.9, which has decent support for Dynamic Rendering. diff --git a/guide/src/dear_imgui/dear_imgui.md b/guide/src/dear_imgui/dear_imgui.md new file mode 100644 index 0000000..c988005 --- /dev/null +++ b/guide/src/dear_imgui/dear_imgui.md @@ -0,0 +1,137 @@ +# class DearImGui + +Dear ImGui has its own initialization and loop, which we encapsulate into `class DearImGui`: + +```cpp +struct DearImGuiCreateInfo { + GLFWwindow* window{}; + std::uint32_t api_version{}; + vk::Instance instance{}; + vk::PhysicalDevice physical_device{}; + std::uint32_t queue_family{}; + vk::Device device{}; + vk::Queue queue{}; + vk::Format color_format{}; // single color attachment. + vk::SampleCountFlagBits samples{}; +}; + +class DearImGui { + public: + using CreateInfo = DearImGuiCreateInfo; + + explicit DearImGui(CreateInfo const& create_info); + + void new_frame(); + void end_frame(); + void render(vk::CommandBuffer command_buffer) const; + + private: + enum class State : std::int8_t { Ended, Begun }; + + struct Deleter { + void operator()(vk::Device device) const; + }; + + State m_state{}; + + Scoped m_device{}; +}; +``` + +In the constructor, we start by creating the ImGui Context, loading Vulkan functions, and initializing GLFW for Vulkan: + +```cpp +IMGUI_CHECKVERSION(); +ImGui::CreateContext(); + +static auto const load_vk_func = +[](char const* name, void* user_data) { + return VULKAN_HPP_DEFAULT_DISPATCHER.vkGetInstanceProcAddr( + *static_cast(user_data), name); +}; +auto instance = create_info.instance; +ImGui_ImplVulkan_LoadFunctions(create_info.api_version, load_vk_func, + &instance); + +if (!ImGui_ImplGlfw_InitForVulkan(create_info.window, true)) { + throw std::runtime_error{"Failed to initialize Dear ImGui"}; +} +``` + +Then initialize Dear ImGui for Vulkan: + +```cpp +auto init_info = ImGui_ImplVulkan_InitInfo{}; +init_info.ApiVersion = create_info.api_version; +init_info.Instance = create_info.instance; +init_info.PhysicalDevice = create_info.physical_device; +init_info.Device = create_info.device; +init_info.QueueFamily = create_info.queue_family; +init_info.Queue = create_info.queue; +init_info.MinImageCount = 2; +init_info.ImageCount = static_cast(resource_buffering_v); +init_info.MSAASamples = + static_cast(create_info.samples); +init_info.DescriptorPoolSize = 2; +auto pipline_rendering_ci = vk::PipelineRenderingCreateInfo{}; +pipline_rendering_ci.setColorAttachmentCount(1).setColorAttachmentFormats( + create_info.color_format); +init_info.PipelineRenderingCreateInfo = pipline_rendering_ci; +init_info.UseDynamicRendering = true; +if (!ImGui_ImplVulkan_Init(&init_info)) { + throw std::runtime_error{"Failed to initialize Dear ImGui"}; +} +ImGui_ImplVulkan_CreateFontsTexture(); +``` + +Since we are using an sRGB format and Dear ImGui is not color-space aware, we need to convert its style colors to linear space (so that they shift back to the original values by gamma correction): + +```cpp +ImGui::StyleColorsDark(); +// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay) +for (auto& colour : ImGui::GetStyle().Colors) { + auto const linear = glm::convertSRGBToLinear( + glm::vec4{colour.x, colour.y, colour.z, colour.w}); + colour = ImVec4{linear.x, linear.y, linear.z, linear.w}; +} +ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = 0.99f; // more opaque +``` + +Finally, create the deleter and its implementation: + +```cpp +m_device = Scoped{create_info.device}; + +// ... +void DearImGui::Deleter::operator()(vk::Device const device) const { + device.waitIdle(); + ImGui_ImplVulkan_DestroyFontsTexture(); + ImGui_ImplVulkan_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); +} +``` + +The remaining functions are straightforward: + +```cpp +void DearImGui::new_frame() { + if (m_state == State::Begun) { end_frame(); } + ImGui_ImplGlfw_NewFrame(); + ImGui_ImplVulkan_NewFrame(); + ImGui::NewFrame(); + m_state = State::Begun; +} + +void DearImGui::end_frame() { + if (m_state == State::Ended) { return; } + ImGui::Render(); + m_state = State::Ended; +} + +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) +void DearImGui::render(vk::CommandBuffer const command_buffer) const { + auto* data = ImGui::GetDrawData(); + if (data == nullptr) { return; } + ImGui_ImplVulkan_RenderDrawData(data, command_buffer); +} +``` diff --git a/guide/src/dear_imgui/imgui_demo.png b/guide/src/dear_imgui/imgui_demo.png new file mode 100644 index 0000000..94eae35 Binary files /dev/null and b/guide/src/dear_imgui/imgui_demo.png differ diff --git a/guide/src/dear_imgui/imgui_integration.md b/guide/src/dear_imgui/imgui_integration.md new file mode 100644 index 0000000..c7ce6db --- /dev/null +++ b/guide/src/dear_imgui/imgui_integration.md @@ -0,0 +1,50 @@ +# ImGui Integration + +Update `Swapchain` to expose its image format: + +```cpp +[[nodiscard]] auto get_format() const -> vk::Format { + return m_ci.imageFormat; +} +``` + +`class App` can now store a `std::optional` member and add/call its create function: + +```cpp +void App::create_imgui() { + auto const imgui_ci = DearImGui::CreateInfo{ + .window = m_window.get(), + .api_version = vk_version_v, + .instance = *m_instance, + .physical_device = m_gpu.device, + .queue_family = m_gpu.queue_family, + .device = *m_device, + .queue = m_queue, + .color_format = m_swapchain->get_format(), + .samples = vk::SampleCountFlagBits::e1, + }; + m_imgui.emplace(imgui_ci); +} +``` + +Start a new ImGui frame after resetting the render fence, and show the demo window: + +```cpp +m_device->resetFences(*render_sync.drawn); +m_imgui->new_frame(); + +ImGui::ShowDemoWindow(); +``` + +We use a separate render pass for Dear ImGui, again for isolation, and to enable us to change the main render pass later, eg by adding a depth buffer attachment (`DearImGui` is setup assuming its render pass will only use a single color attachment). + +```cpp +m_imgui->end_frame(); +rendering_info.setColorAttachments(attachment_info) + .setPDepthAttachment(nullptr); +render_sync.command_buffer.beginRendering(rendering_info); +m_imgui->render(render_sync.command_buffer); +render_sync.command_buffer.endRendering(); +``` + +![ImGui Demo](./imgui_demo.png) diff --git a/src/app.cpp b/src/app.cpp index 06eb6ec..98c273d 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -17,6 +17,7 @@ void App::run() { create_device(); create_swapchain(); create_render_sync(); + create_imgui(); main_loop(); } @@ -137,6 +138,21 @@ void App::create_render_sync() { } } +void App::create_imgui() { + auto const imgui_ci = DearImGui::CreateInfo{ + .window = m_window.get(), + .api_version = vk_version_v, + .instance = *m_instance, + .physical_device = m_gpu.device, + .queue_family = m_gpu.queue_family, + .device = *m_device, + .queue = m_queue, + .color_format = m_swapchain->get_format(), + .samples = vk::SampleCountFlagBits::e1, + }; + m_imgui.emplace(imgui_ci); +} + void App::main_loop() { while (glfwWindowShouldClose(m_window.get()) == GLFW_FALSE) { glfwPollEvents(); @@ -165,6 +181,9 @@ void App::main_loop() { // reset fence _after_ acquisition of image: if it fails, the // fence remains signaled. m_device->resetFences(*render_sync.drawn); + m_imgui->new_frame(); + + ImGui::ShowDemoWindow(); auto command_buffer_bi = vk::CommandBufferBeginInfo{}; // this flag means recorded commands will not be reused. @@ -206,6 +225,13 @@ void App::main_loop() { // draw stuff here. render_sync.command_buffer.endRendering(); + m_imgui->end_frame(); + rendering_info.setColorAttachments(attachment_info) + .setPDepthAttachment(nullptr); + render_sync.command_buffer.beginRendering(rendering_info); + m_imgui->render(render_sync.command_buffer); + render_sync.command_buffer.endRendering(); + // AttachmentOptimal => PresentSrc // the barrier must wait for color attachment operations to complete. // we don't need any post-synchronization as the present Sempahore takes diff --git a/src/app.hpp b/src/app.hpp index de9946b..63c9765 100644 --- a/src/app.hpp +++ b/src/app.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include #include @@ -30,6 +31,7 @@ class App { void create_device(); void create_swapchain(); void create_render_sync(); + void create_imgui(); void main_loop(); @@ -49,6 +51,8 @@ class App { // Current virtual frame index. std::size_t m_frame_index{}; + std::optional m_imgui{}; + // waiter must be the last member to ensure it blocks until device is idle // before other members get destroyed. ScopedWaiter m_waiter{}; diff --git a/src/dear_imgui.cpp b/src/dear_imgui.cpp new file mode 100644 index 0000000..744484e --- /dev/null +++ b/src/dear_imgui.cpp @@ -0,0 +1,88 @@ +#include +#include +#include +#include +#include +#include +#include + +namespace lvk { +DearImGui::DearImGui(CreateInfo const& create_info) { + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + + static auto const load_vk_func = +[](char const* name, void* user_data) { + return VULKAN_HPP_DEFAULT_DISPATCHER.vkGetInstanceProcAddr( + *static_cast(user_data), name); + }; + auto instance = create_info.instance; + ImGui_ImplVulkan_LoadFunctions(create_info.api_version, load_vk_func, + &instance); + + if (!ImGui_ImplGlfw_InitForVulkan(create_info.window, true)) { + throw std::runtime_error{"Failed to initialize Dear ImGui"}; + } + + auto init_info = ImGui_ImplVulkan_InitInfo{}; + init_info.ApiVersion = create_info.api_version; + init_info.Instance = create_info.instance; + init_info.PhysicalDevice = create_info.physical_device; + init_info.Device = create_info.device; + init_info.QueueFamily = create_info.queue_family; + init_info.Queue = create_info.queue; + init_info.MinImageCount = 2; + init_info.ImageCount = static_cast(resource_buffering_v); + init_info.MSAASamples = + static_cast(create_info.samples); + init_info.DescriptorPoolSize = 2; + auto pipline_rendering_ci = vk::PipelineRenderingCreateInfo{}; + pipline_rendering_ci.setColorAttachmentCount(1).setColorAttachmentFormats( + create_info.color_format); + init_info.PipelineRenderingCreateInfo = pipline_rendering_ci; + init_info.UseDynamicRendering = true; + if (!ImGui_ImplVulkan_Init(&init_info)) { + throw std::runtime_error{"Failed to initialize Dear ImGui"}; + } + ImGui_ImplVulkan_CreateFontsTexture(); + + ImGui::StyleColorsDark(); + // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay) + for (auto& colour : ImGui::GetStyle().Colors) { + auto const linear = glm::convertSRGBToLinear( + glm::vec4{colour.x, colour.y, colour.z, colour.w}); + colour = ImVec4{linear.x, linear.y, linear.z, linear.w}; + } + ImGui::GetStyle().Colors[ImGuiCol_WindowBg].w = 0.99f; // more opaque + + m_device = Scoped{create_info.device}; +} + +void DearImGui::new_frame() { + if (m_state == State::Begun) { end_frame(); } + ImGui_ImplGlfw_NewFrame(); + ImGui_ImplVulkan_NewFrame(); + ImGui::NewFrame(); + m_state = State::Begun; +} + +void DearImGui::end_frame() { + if (m_state == State::Ended) { return; } + ImGui::Render(); + m_state = State::Ended; +} + +// NOLINTNEXTLINE(readability-convert-member-functions-to-static) +void DearImGui::render(vk::CommandBuffer const command_buffer) const { + auto* data = ImGui::GetDrawData(); + if (data == nullptr) { return; } + ImGui_ImplVulkan_RenderDrawData(data, command_buffer); +} + +void DearImGui::Deleter::operator()(vk::Device const device) const { + device.waitIdle(); + ImGui_ImplVulkan_DestroyFontsTexture(); + ImGui_ImplVulkan_Shutdown(); + ImGui_ImplGlfw_Shutdown(); + ImGui::DestroyContext(); +} +} // namespace lvk diff --git a/src/dear_imgui.hpp b/src/dear_imgui.hpp new file mode 100644 index 0000000..57b6c18 --- /dev/null +++ b/src/dear_imgui.hpp @@ -0,0 +1,42 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace lvk { +struct DearImGuiCreateInfo { + GLFWwindow* window{}; + std::uint32_t api_version{}; + vk::Instance instance{}; + vk::PhysicalDevice physical_device{}; + std::uint32_t queue_family{}; + vk::Device device{}; + vk::Queue queue{}; + vk::Format color_format{}; // single color attachment. + vk::SampleCountFlagBits samples{}; +}; + +class DearImGui { + public: + using CreateInfo = DearImGuiCreateInfo; + + explicit DearImGui(CreateInfo const& create_info); + + void new_frame(); + void end_frame(); + void render(vk::CommandBuffer command_buffer) const; + + private: + enum class State : std::int8_t { Ended, Begun }; + + struct Deleter { + void operator()(vk::Device device) const; + }; + + State m_state{}; + + Scoped m_device{}; +}; +} // namespace lvk diff --git a/src/swapchain.hpp b/src/swapchain.hpp index a9aa8ac..3ad5d13 100644 --- a/src/swapchain.hpp +++ b/src/swapchain.hpp @@ -17,6 +17,10 @@ class Swapchain { return {m_ci.imageExtent.width, m_ci.imageExtent.height}; } + [[nodiscard]] auto get_format() const -> vk::Format { + return m_ci.imageFormat; + } + [[nodiscard]] auto acquire_next_image(vk::Semaphore to_signal) -> std::optional;