Skip to content
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
32 changes: 29 additions & 3 deletions ext/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Binary file modified ext/src.zip
Binary file not shown.
3 changes: 3 additions & 0 deletions guide/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
3 changes: 3 additions & 0 deletions guide/src/dear_imgui/README.md
Original file line number Diff line number Diff line change
@@ -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.
137 changes: 137 additions & 0 deletions guide/src/dear_imgui/dear_imgui.md
Original file line number Diff line number Diff line change
@@ -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<vk::Device, Deleter> 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<vk::Instance*>(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<std::uint32_t>(resource_buffering_v);
init_info.MSAASamples =
static_cast<VkSampleCountFlagBits>(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<vk::Device, Deleter>{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);
}
```
Binary file added guide/src/dear_imgui/imgui_demo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions guide/src/dear_imgui/imgui_integration.md
Original file line number Diff line number Diff line change
@@ -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<DearImGui>` 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)
26 changes: 26 additions & 0 deletions src/app.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ void App::run() {
create_device();
create_swapchain();
create_render_sync();
create_imgui();

main_loop();
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions src/app.hpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#pragma once
#include <dear_imgui.hpp>
#include <gpu.hpp>
#include <resource_buffering.hpp>
#include <scoped_waiter.hpp>
Expand Down Expand Up @@ -30,6 +31,7 @@ class App {
void create_device();
void create_swapchain();
void create_render_sync();
void create_imgui();

void main_loop();

Expand All @@ -49,6 +51,8 @@ class App {
// Current virtual frame index.
std::size_t m_frame_index{};

std::optional<DearImGui> m_imgui{};

// waiter must be the last member to ensure it blocks until device is idle
// before other members get destroyed.
ScopedWaiter m_waiter{};
Expand Down
Loading