class DearImGui

Dear ImGui has its own initialization and loop, which we encapsulate into class DearImGui:

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:

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:

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

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:

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:

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);
}