/* Copyright (c) 2018-2025, Arm Limited and Contributors * Copyright (c) 2019-2025, Sascha Willems * Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. * * SPDX-License-Identifier: Apache-2.0 * * Licensed under the Apache License, Version 2.0 the "License"; * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include "common/vk_common.h" #include "core/command_buffer.h" #include "core/hpp_debug.h" #include "core/hpp_sampler.h" #include "debug_info.h" #include "drawer.h" #include "filesystem/legacy.h" #include "platform/input_events.h" #include "platform/window.h" #include "rendering/hpp_pipeline_state.h" #include "stats/hpp_stats.h" #include "stats/stats.h" #include #include #include #include #if defined(PLATFORM__MACOS) # include #endif namespace vkb { namespace { inline void reset_graph_max_value(StatGraphData &graph_data) { // If it does not have a fixed max if (!graph_data.has_fixed_max) { // Reset it graph_data.max_value = 0.0f; } } } // namespace /** * @brief Helper structure for fonts loaded from TTF */ struct Font { /** * @brief Constructor * @param name_ The name of the font file that exists within 'assets/fonts' (without extension) * @param size_ The font size, scaled by DPI */ Font(const std::string &name_, float size_) : name{name_}, data{vkb::fs::read_asset("fonts/" + name + ".ttf")}, size{size_} { // Keep ownership of the font data to avoid a double delete ImFontConfig font_config{}; font_config.FontDataOwnedByAtlas = false; if (size < 1.0f) { size = 20.0f; } ImGuiIO &io = ImGui::GetIO(); handle = io.Fonts->AddFontFromMemoryTTF(data.data(), static_cast(data.size()), size, &font_config); } ImFont *handle = nullptr; std::string name; std::vector data; float size = {}; }; /** * @brief Vulkan helper class for Dear ImGui */ template class Gui { public: static inline bool visible = true; // Used to show/hide the GUI using CommandBufferType = typename std::conditional::type; using DescriptorSetType = typename std::conditional::type; using PipelineType = typename std::conditional::type; using PipelineCacheType = typename std::conditional::type; using PipelineLayoutType = typename std::conditional::type; using PipelineShaderStageCreateInfoType = typename std::conditional::type; using RenderContextType = typename std::conditional::type; using RenderPassType = typename std::conditional::type; using StatsType = typename std::conditional::type; public: /** * @brief Helper struct for rendering debug statistics in the GUI */ struct DebugView { bool active = false; float scale = 1.7f; uint32_t max_fields = 8; float label_column_width = 0; }; /** * @brief Helper class for drawing statistics */ class StatsView { public: /** * @brief Constructs a StatsView * @param stats Const pointer to the Stats data object; may be null */ StatsView(const StatsType *stats); float get_graph_height() const; StatGraphData const &get_stat_graph_data(StatIndex const &index) const; float get_top_padding() const; /** * @brief Resets the max value for a specific stat */ void reset_max_value(const StatIndex index); /** * @brief Resets the max values for the stats which do not have a fixed max */ void reset_max_values(); private: std::map graph_map; float graph_height = 50.0f; float top_padding = 1.1f; }; public: /** * @brief Initializes the Gui * @param render_context A vulkan render context * @param window A Window object from which to draw DPI and content scaling information * @param stats A statistics object (null if no statistics are used) * @param font_size The font size * @param explicit_update If true, update buffers every frame */ Gui(RenderContextType &render_context, Window const &window, StatsType const *stats = nullptr, float font_size = 21.0f, bool explicit_update = false); /** * @brief Destroys the Gui */ ~Gui(); public: /** * @brief Draws the Gui * @param command_buffer Command buffer to register draw-commands */ void draw(CommandBufferType command_buffer); /** * @brief Draws the Gui using an external pipeline * @param command_buffer Command buffer to register draw-commands * @param pipeline Pipeline to bind to perform draw-commands * @param pipeline_layout PipelineLayout for given pieline * @param descriptor_set DescriptorSet to bind to perform draw-commands */ void draw(CommandBufferType command_buffer, PipelineType pipeline, PipelineLayoutType pipeline_layout, DescriptorSetType descriptor_set); /** * @brief Draws the Gui * @param command_buffer Command buffer to register draw-commands */ void draw(vkb::core::CommandBuffer &command_buffer); Drawer &get_drawer(); vk::ImageView get_font_image_view() const; vk::Sampler get_sampler() const; /** * @return The stats view */ StatsView &get_stats_view(); bool input_event(const InputEvent &input_event); bool is_debug_view_active() const; /** * @brief Starts a new ImGui frame * to be called before drawing any window */ void new_frame(); void prepare(PipelineCacheType pipeline_cache, RenderPassType render_pass, std::vector const &shader_stages); /** * @brief Handles resizing of the window * @param width New width of the window * @param height New height of the window */ void resize(const uint32_t width, const uint32_t height) const; void set_subpass(const uint32_t subpass); /** * @brief Shows an options windows, to be filled by the sample, * which will be positioned at the top * @param body ImGui commands defining the body of the window * @param lines The number of lines of text to draw in the window * These will help the gui to calculate the height of the window */ void show_options_window(std::function body, const uint32_t lines = 3); void show_simple_window(const std::string &name, uint32_t last_fps, std::function body); /** * @brief Shows an overlay top window with app info and maybe stats * @param app_name Application name * @param stats Statistics to show (can be null) * @param debug_info Debug info to show (can be null) */ void show_top_window(const std::string &app_name, const StatsType *stats = nullptr, DebugInfo *debug_info = nullptr); /** * @brief Updates the Gui * @param delta_time Time passed since last update */ void update(const float delta_time); bool update_buffers(); private: static constexpr char const *default_font = "Roboto-Regular"; // The name of the default font file to use static constexpr char const *default_window_font = "RobotoMono-Regular"; // The name of the default window font file to use static constexpr ImGuiWindowFlags common_flags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing; static constexpr float overlay_alpha = 0.3f; static constexpr double press_time_ms = 200.0; private: void draw_impl(vk::CommandBuffer command_buffer, vk::Pipeline pipeline, vk::PipelineLayout pipeline_layout, vk::DescriptorSet descriptor_set); void draw_impl(vkb::core::CommandBufferCpp &command_buffer); void prepare_impl(vk::PipelineCache pipeline_cache, vk::RenderPass render_pass, std::vector const &shader_stages); Font &get_font(const std::string &font_name = Gui::default_font); /** * @brief Shows an child with app info * @param app_name Application name */ void show_app_info(const std::string &app_name); /** * @brief Shows a moveable window with debug information * @param debug_info The object holding the data fields to be displayed * @param position The absolute position to set */ void show_debug_window(DebugInfo &debug_info, const ImVec2 &position); /** * @brief Shows a child with statistics * @param stats Statistics to show */ void show_stats(const StatsType &stats); /** * @brief Updates Vulkan buffers * @param command_buffer Command buffer to draw into * @return Vertex buffer allocation */ BufferAllocationCpp update_buffers(vkb::core::CommandBufferCpp &command_buffer); void upload_draw_data(const ImDrawData *draw_data, uint8_t *vertex_data, uint8_t *index_data); private: float content_scale_factor = 1.0f; // Scale factor to apply due to a difference between the window and GL pixel sizes DebugView debug_view; vk::DescriptorPool descriptor_pool; vk::DescriptorSet descriptor_set; vk::DescriptorSetLayout descriptor_set_layout; float dpi_factor = 1.0f; // Scale factor to apply to the size of gui elements (expressed in dp) Drawer drawer; bool explicit_update = false; std::vector fonts; std::unique_ptr font_image; std::unique_ptr font_image_view; std::unique_ptr index_buffer; vk::Pipeline pipeline; vkb::core::HPPPipelineLayout *pipeline_layout = nullptr; bool prev_visible = true; vkb::rendering::HPPRenderContext &render_context; std::unique_ptr sampler; StatsView stats_view; uint32_t subpass = 0; Timer timer; // Used to measure duration of input events bool two_finger_tap = false; // Whether or not the GUI has detected a multi touch gesture std::unique_ptr vertex_buffer; }; using GuiC = Gui; using GuiCpp = Gui; template inline Gui::Gui(RenderContextType &render_context_, Window const &window, StatsType const *stats, float font_size, bool explicit_update) : render_context{render_context_}, content_scale_factor{window.get_content_scale_factor()}, dpi_factor{window.get_dpi_factor() * content_scale_factor}, explicit_update{explicit_update}, stats_view(stats) { ImGui::CreateContext(); ImGuiStyle &style = ImGui::GetStyle(); // Color scheme style.Colors[ImGuiCol_WindowBg] = ImVec4(0.005f, 0.005f, 0.005f, 0.94f); style.Colors[ImGuiCol_TitleBg] = ImVec4(1.0f, 0.0f, 0.0f, 0.6f); style.Colors[ImGuiCol_TitleBgActive] = ImVec4(1.0f, 0.0f, 0.0f, 0.8f); style.Colors[ImGuiCol_MenuBarBg] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f); style.Colors[ImGuiCol_Header] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f); style.Colors[ImGuiCol_HeaderActive] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f); style.Colors[ImGuiCol_HeaderHovered] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f); style.Colors[ImGuiCol_FrameBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.8f); style.Colors[ImGuiCol_CheckMark] = ImVec4(0.0f, 1.0f, 0.0f, 1.0f); style.Colors[ImGuiCol_SliderGrab] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f); style.Colors[ImGuiCol_SliderGrabActive] = ImVec4(1.0f, 0.0f, 0.0f, 0.8f); style.Colors[ImGuiCol_FrameBgHovered] = ImVec4(1.0f, 1.0f, 1.0f, 0.1f); style.Colors[ImGuiCol_FrameBgActive] = ImVec4(1.0f, 1.0f, 1.0f, 0.2f); style.Colors[ImGuiCol_Button] = ImVec4(1.0f, 0.0f, 0.0f, 0.4f); style.Colors[ImGuiCol_ButtonHovered] = ImVec4(1.0f, 0.0f, 0.0f, 0.6f); style.Colors[ImGuiCol_ButtonActive] = ImVec4(1.0f, 0.0f, 0.0f, 0.8f); // Borderless window style.WindowBorderSize = 0.0f; // Global scale style.ScaleAllSizes(dpi_factor); // Dimensions ImGuiIO &io = ImGui::GetIO(); auto extent = render_context.get_surface_extent(); io.DisplaySize.x = static_cast(extent.width); io.DisplaySize.y = static_cast(extent.height); io.FontGlobalScale = 1.0f; io.DisplayFramebufferScale = ImVec2(1.0f, 1.0f); // Enable keyboard navigation io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; io.KeyMap[ImGuiKey_Space] = static_cast(KeyCode::Space); io.KeyMap[ImGuiKey_Enter] = static_cast(KeyCode::Enter); io.KeyMap[ImGuiKey_LeftArrow] = static_cast(KeyCode::Left); io.KeyMap[ImGuiKey_RightArrow] = static_cast(KeyCode::Right); io.KeyMap[ImGuiKey_UpArrow] = static_cast(KeyCode::Up); io.KeyMap[ImGuiKey_DownArrow] = static_cast(KeyCode::Down); io.KeyMap[ImGuiKey_Tab] = static_cast(KeyCode::Tab); io.KeyMap[ImGuiKey_Escape] = static_cast(KeyCode::Backspace); // Default font fonts.emplace_back(default_font, font_size * dpi_factor); // Debug window font fonts.emplace_back(default_window_font, (font_size / 2) * dpi_factor); // Create font texture unsigned char *font_data; int tex_width, tex_height; io.Fonts->GetTexDataAsRGBA32(&font_data, &tex_width, &tex_height); size_t upload_size = tex_width * tex_height * 4 * sizeof(char); auto &device = render_context.get_device(); // Create target image for copy vk::Extent3D font_extent{to_u32(tex_width), to_u32(tex_height), 1u}; font_image = std::make_unique(device, font_extent, vk::Format::eR8G8B8A8Unorm, vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferDst, VMA_MEMORY_USAGE_GPU_ONLY); font_image->set_debug_name("GUI font image"); font_image_view = std::make_unique(*font_image, vk::ImageViewType::e2D); font_image_view->set_debug_name("View on GUI font image"); // Upload font data into the vulkan image memory { vkb::core::BufferCpp stage_buffer = vkb::core::BufferCpp::create_staging_buffer(device, upload_size, font_data); auto command_buffer = device.get_command_pool().request_command_buffer(); vkb::HPPFencePool fence_pool{device}; // Begin recording command_buffer->begin(vk::CommandBufferUsageFlagBits::eOneTimeSubmit, 0); { // Prepare for transfer vkb::common::HPPImageMemoryBarrier memory_barrier = {.src_stage_mask = vk::PipelineStageFlagBits::eHost, .dst_stage_mask = vk::PipelineStageFlagBits::eTransfer, .dst_access_mask = vk::AccessFlagBits::eTransferWrite, .old_layout = vk::ImageLayout::eUndefined, .new_layout = vk::ImageLayout::eTransferDstOptimal}; command_buffer->image_memory_barrier(*font_image_view, memory_barrier); } // Copy vk::BufferImageCopy buffer_copy_region = {.imageSubresource = {.aspectMask = font_image_view->get_subresource_range().aspectMask, .layerCount = font_image_view->get_subresource_range().layerCount}, .imageExtent = font_image->get_extent()}; command_buffer->copy_buffer_to_image(stage_buffer, *font_image, {buffer_copy_region}); { // Prepare for fragment shader vkb::common::HPPImageMemoryBarrier memory_barrier = {.src_stage_mask = vk::PipelineStageFlagBits::eTransfer, .dst_stage_mask = vk::PipelineStageFlagBits::eFragmentShader, .src_access_mask = vk::AccessFlagBits::eTransferWrite, .dst_access_mask = vk::AccessFlagBits::eShaderRead, .old_layout = vk::ImageLayout::eTransferDstOptimal, .new_layout = vk::ImageLayout::eShaderReadOnlyOptimal}; command_buffer->image_memory_barrier(*font_image_view, memory_barrier); } // End recording command_buffer->end(); auto &queue = device.get_queue_by_flags(vk::QueueFlagBits::eGraphics, 0); queue.submit(*command_buffer, device.get_fence_pool().request_fence()); // Wait for the command buffer to finish its work before destroying the staging buffer device.get_fence_pool().wait(); device.get_fence_pool().reset(); device.get_command_pool().reset_pool(); } // Calculate valid filter vk::Filter filter = vk::Filter::eLinear; vkb::common::make_filters_valid(device.get_gpu().get_handle(), font_image->get_format(), &filter); // Load shaders vkb::core::HPPShaderSource vert_shader("imgui.vert.spv"); vkb::core::HPPShaderSource frag_shader("imgui.frag.spv"); std::vector shader_modules; shader_modules.push_back(&device.get_resource_cache().request_shader_module(vk::ShaderStageFlagBits::eVertex, vert_shader, {})); shader_modules.push_back(&device.get_resource_cache().request_shader_module(vk::ShaderStageFlagBits::eFragment, frag_shader, {})); pipeline_layout = &device.get_resource_cache().request_pipeline_layout(shader_modules); // Create texture sampler vk::SamplerCreateInfo sampler_info = {.magFilter = filter, .minFilter = filter, .mipmapMode = vk::SamplerMipmapMode::eNearest, .addressModeU = vk::SamplerAddressMode::eClampToEdge, .addressModeV = vk::SamplerAddressMode::eClampToEdge, .addressModeW = vk::SamplerAddressMode::eClampToEdge, .borderColor = vk::BorderColor::eFloatOpaqueWhite}; sampler = std::make_unique(device, sampler_info); sampler->set_debug_name("GUI sampler"); if (explicit_update) { vertex_buffer = std::make_unique(render_context.get_device(), 1, vk::BufferUsageFlagBits::eVertexBuffer, VMA_MEMORY_USAGE_GPU_TO_CPU); vertex_buffer->set_debug_name("GUI vertex buffer"); index_buffer = std::make_unique(render_context.get_device(), 1, vk::BufferUsageFlagBits::eIndexBuffer, VMA_MEMORY_USAGE_GPU_TO_CPU); index_buffer->set_debug_name("GUI index buffer"); } } template inline Gui::~Gui() { vk::Device device = render_context.get_device().get_handle(); device.destroyDescriptorPool(descriptor_pool); device.destroyDescriptorSetLayout(descriptor_set_layout); device.destroyPipeline(pipeline); ImGui::DestroyContext(); } template inline void Gui::draw(CommandBufferType command_buffer) { draw(command_buffer, pipeline, pipeline_layout->get_handle(), descriptor_set); } template inline void Gui::draw(CommandBufferType command_buffer, PipelineType pipeline, PipelineLayoutType pipeline_layout, DescriptorSetType descriptor_set) { if constexpr (bindingType == BindingType::Cpp) { draw_impl(command_buffer, pipeline, pipeline_layout, descriptor_set); } else { draw_impl(static_cast(command_buffer), static_cast(pipeline), static_cast(pipeline_layout), static_cast(descriptor_set)); } } template inline void Gui::draw_impl(vk::CommandBuffer command_buffer, vk::Pipeline pipeline, vk::PipelineLayout pipeline_layout, vk::DescriptorSet descriptor_set) { if (!visible) { return; } ImDrawData *draw_data = ImGui::GetDrawData(); if ((!draw_data) || (draw_data->CmdListsCount == 0)) { return; } command_buffer.bindPipeline(vk::PipelineBindPoint::eGraphics, pipeline); command_buffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, pipeline_layout, 0, descriptor_set, {}); // Push constants auto &io = ImGui::GetIO(); auto push_transform = glm::mat4(1.0f); push_transform = glm::translate(push_transform, glm::vec3(-1.0f, -1.0f, 0.0f)); push_transform = glm::scale(push_transform, glm::vec3(2.0f / io.DisplaySize.x, 2.0f / io.DisplaySize.y, 0.0f)); command_buffer.pushConstants(pipeline_layout, vk::ShaderStageFlagBits::eVertex, 0, sizeof(glm::mat4), &push_transform); vk::DeviceSize vertex_offsets[1] = {0}; vk::Buffer vertex_buffer_handle = vertex_buffer->get_handle(); command_buffer.bindVertexBuffers(0, vertex_buffer_handle, vertex_offsets); command_buffer.bindIndexBuffer(index_buffer->get_handle(), 0, vk::IndexType::eUint16); int32_t vertex_offset = 0; int32_t index_offset = 0; for (auto const *cmd_list : draw_data->CmdLists) { for (auto const &cmd : cmd_list->CmdBuffer) { vk::Rect2D scissor_rect; scissor_rect.offset.x = std::max(static_cast(cmd.ClipRect.x), 0); scissor_rect.offset.y = std::max(static_cast(cmd.ClipRect.y), 0); scissor_rect.extent.width = static_cast(cmd.ClipRect.z - cmd.ClipRect.x); scissor_rect.extent.height = static_cast(cmd.ClipRect.w - cmd.ClipRect.y); command_buffer.setScissor(0, scissor_rect); command_buffer.drawIndexed(cmd.ElemCount, 1, index_offset, vertex_offset, 0); index_offset += cmd.ElemCount; } #if defined(PLATFORM__MACOS) && TARGET_OS_IOS && TARGET_OS_SIMULATOR // iOS Simulator does not support vkCmdDrawIndexed() with vertex_offset > 0, so rebind vertex buffer instead vertex_offsets[0] += cmd_list->VtxBuffer.Size * sizeof(ImDrawVert); command_buffer.bindVertexBuffers(0, vertex_buffer_handle, vertex_offsets); #else vertex_offset += cmd_list->VtxBuffer.Size; #endif } } template inline void Gui::draw(vkb::core::CommandBuffer &command_buffer) { if constexpr (bindingType == BindingType::Cpp) { draw_impl(command_buffer); } else { draw_impl(reinterpret_cast(command_buffer)); } } template inline void Gui::draw_impl(vkb::core::CommandBufferCpp &command_buffer) { if (!visible) { return; } // Render commands ImDrawData *draw_data = ImGui::GetDrawData(); if (!draw_data || draw_data->CmdListsCount == 0) { return; } vkb::core::HPPScopedDebugLabel debug_label{command_buffer, "GUI"}; // Vertex input state vk::VertexInputBindingDescription vertex_input_binding = {.stride = sizeof(ImDrawVert)}; // Location 0: Position vk::VertexInputAttributeDescription pos_attr = {.location = 0, .format = vk::Format::eR32G32Sfloat, .offset = offsetof(ImDrawVert, pos)}; // Location 1: UV vk::VertexInputAttributeDescription uv_attr = {.location = 1, .format = vk::Format::eR32G32Sfloat, .offset = offsetof(ImDrawVert, uv)}; // Location 2: Color vk::VertexInputAttributeDescription col_attr = {.location = 2, .format = vk::Format::eR8G8B8A8Unorm, .offset = offsetof(ImDrawVert, col)}; vkb::rendering::HPPVertexInputState vertex_input_state = {.bindings = {vertex_input_binding}, .attributes = {pos_attr, uv_attr, col_attr}}; command_buffer.set_vertex_input_state(vertex_input_state); // Blend state vkb::rendering::HPPColorBlendAttachmentState color_attachment = {.blend_enable = true, .src_color_blend_factor = vk::BlendFactor::eSrcAlpha, .dst_color_blend_factor = vk::BlendFactor::eOneMinusSrcAlpha, .src_alpha_blend_factor = vk::BlendFactor::eOneMinusSrcAlpha, .color_write_mask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG | vk::ColorComponentFlagBits::eB}; vkb::rendering::HPPColorBlendState blend_state{.attachments = {color_attachment}}; command_buffer.set_color_blend_state(blend_state); vkb::rendering::HPPRasterizationState rasterization_state = {.cull_mode = vk::CullModeFlagBits::eNone}; command_buffer.set_rasterization_state(rasterization_state); vkb::rendering::HPPDepthStencilState depth_state = {.depth_test_enable = false, .depth_write_enable = false}; command_buffer.set_depth_stencil_state(depth_state); // Bind pipeline layout command_buffer.bind_pipeline_layout(*pipeline_layout); command_buffer.bind_image(*font_image_view, *sampler, 0, 0, 0); // Pre-rotation auto push_transform = glm::mat4(1.0f); if (render_context.has_swapchain()) { auto transform = render_context.get_swapchain().get_transform(); glm::vec3 rotation_axis = glm::vec3(0.0f, 0.0f, 1.0f); if (transform & vk::SurfaceTransformFlagBitsKHR::eRotate90) { push_transform = glm::rotate(push_transform, glm::radians(90.0f), rotation_axis); } else if (transform & vk::SurfaceTransformFlagBitsKHR::eRotate270) { push_transform = glm::rotate(push_transform, glm::radians(270.0f), rotation_axis); } else if (transform & vk::SurfaceTransformFlagBitsKHR::eRotate180) { push_transform = glm::rotate(push_transform, glm::radians(180.0f), rotation_axis); } } // GUI coordinate space to screen space auto &io = ImGui::GetIO(); push_transform = glm::translate(push_transform, glm::vec3(-1.0f, -1.0f, 0.0f)); push_transform = glm::scale(push_transform, glm::vec3(2.0f / io.DisplaySize.x, 2.0f / io.DisplaySize.y, 0.0f)); // Push constants command_buffer.push_constants(push_transform); std::vector> vertex_buffers; std::vector vertex_offsets; // If a render context is used, then use the frames buffer pools to allocate GUI vertex/index data from if (!explicit_update) { // Save vertex buffer allocation in case we need to rebind with vertex_offset, e.g. for iOS Simulator auto vertex_allocation = update_buffers(command_buffer); if (!vertex_allocation.empty()) { vertex_buffers.push_back(vertex_allocation.get_buffer()); vertex_offsets.push_back(vertex_allocation.get_offset()); } } else { vertex_buffers.push_back(*vertex_buffer); vertex_offsets.push_back(0); command_buffer.bind_vertex_buffers(0, vertex_buffers, vertex_offsets); command_buffer.bind_index_buffer(*index_buffer, 0, vk::IndexType::eUint16); } int32_t vertex_offset = 0; uint32_t index_offset = 0; for (auto const *cmd_list : draw_data->CmdLists) { for (auto const &cmd : cmd_list->CmdBuffer) { vk::Rect2D scissor_rect; scissor_rect.offset.x = std::max(static_cast(cmd.ClipRect.x), 0); scissor_rect.offset.y = std::max(static_cast(cmd.ClipRect.y), 0); scissor_rect.extent.width = static_cast(cmd.ClipRect.z - cmd.ClipRect.x); scissor_rect.extent.height = static_cast(cmd.ClipRect.w - cmd.ClipRect.y); // Adjust for pre-rotation if necessary if (render_context.has_swapchain()) { auto transform = render_context.get_swapchain().get_transform(); if (transform & vk::SurfaceTransformFlagBitsKHR::eRotate90) { scissor_rect.offset.x = static_cast(io.DisplaySize.y - cmd.ClipRect.w); scissor_rect.offset.y = static_cast(cmd.ClipRect.x); scissor_rect.extent.width = static_cast(cmd.ClipRect.w - cmd.ClipRect.y); scissor_rect.extent.height = static_cast(cmd.ClipRect.z - cmd.ClipRect.x); } else if (transform & vk::SurfaceTransformFlagBitsKHR::eRotate180) { scissor_rect.offset.x = static_cast(io.DisplaySize.x - cmd.ClipRect.z); scissor_rect.offset.y = static_cast(io.DisplaySize.y - cmd.ClipRect.w); scissor_rect.extent.width = static_cast(cmd.ClipRect.z - cmd.ClipRect.x); scissor_rect.extent.height = static_cast(cmd.ClipRect.w - cmd.ClipRect.y); } else if (transform & vk::SurfaceTransformFlagBitsKHR::eRotate270) { scissor_rect.offset.x = static_cast(cmd.ClipRect.y); scissor_rect.offset.y = static_cast(io.DisplaySize.x - cmd.ClipRect.z); scissor_rect.extent.width = static_cast(cmd.ClipRect.w - cmd.ClipRect.y); scissor_rect.extent.height = static_cast(cmd.ClipRect.z - cmd.ClipRect.x); } } command_buffer.set_scissor(0, {scissor_rect}); command_buffer.draw_indexed(cmd.ElemCount, 1, index_offset, vertex_offset, 0); index_offset += cmd.ElemCount; } #if defined(PLATFORM__MACOS) && TARGET_OS_IOS && TARGET_OS_SIMULATOR // iOS Simulator does not support vkCmdDrawIndexed() with vertex_offset > 0, so rebind vertex buffer instead if (!vertex_offsets.empty()) { vertex_offsets.back() += cmd_list->VtxBuffer.Size * sizeof(ImDrawVert); command_buffer.bind_vertex_buffers(0, vertex_buffers, vertex_offsets); } #else vertex_offset += cmd_list->VtxBuffer.Size; #endif } } template inline Drawer &Gui::get_drawer() { return drawer; } template inline Font &Gui::get_font(const std::string &font_name) { assert(!fonts.empty() && "No fonts exist"); auto it = std::ranges::find_if(fonts, [&font_name](Font &font) { return font.name == font_name; }); if (it != fonts.end()) { return *it; } else { LOGW("Couldn't find font with name {}", font_name); return *fonts.begin(); } } template inline vk::ImageView Gui::get_font_image_view() const { return font_image_view->get_handle(); } template inline vk::Sampler Gui::get_sampler() const { return sampler->get_handle(); } template inline typename Gui::StatsView &Gui::get_stats_view() { return stats_view; } template inline bool Gui::input_event(const InputEvent &input_event) { auto &io = ImGui::GetIO(); auto capture_move_event = false; if (input_event.get_source() == EventSource::Keyboard) { const auto &key_event = static_cast(input_event); if (key_event.get_action() == KeyAction::Down) { io.KeysDown[static_cast(key_event.get_code())] = true; } else if (key_event.get_action() == KeyAction::Up) { io.KeysDown[static_cast(key_event.get_code())] = false; } } else if (input_event.get_source() == EventSource::Mouse) { const auto &mouse_button = static_cast(input_event); io.MousePos = ImVec2{mouse_button.get_pos_x() * content_scale_factor, mouse_button.get_pos_y() * content_scale_factor}; auto button_id = static_cast(mouse_button.get_button()); if (mouse_button.get_action() == MouseAction::Down) { io.MouseDown[button_id] = true; } else if (mouse_button.get_action() == MouseAction::Up) { io.MouseDown[button_id] = false; } else if (mouse_button.get_action() == MouseAction::Move) { capture_move_event = io.WantCaptureMouse; } } else if (input_event.get_source() == EventSource::Touchscreen) { const auto &touch_event = static_cast(input_event); io.MousePos = ImVec2{touch_event.get_pos_x(), touch_event.get_pos_y()}; if (touch_event.get_action() == TouchAction::Down) { io.MouseDown[touch_event.get_pointer_id()] = true; } else if (touch_event.get_action() == TouchAction::Up) { io.MouseDown[touch_event.get_pointer_id()] = false; } else if (touch_event.get_action() == TouchAction::Move) { capture_move_event = io.WantCaptureMouse; } } // Toggle debug UI view when tap or clicking outside the GUI windows if (!io.WantCaptureMouse) { bool press_down = (input_event.get_source() == EventSource::Mouse && static_cast(input_event).get_action() == MouseAction::Down) || (input_event.get_source() == EventSource::Touchscreen && static_cast(input_event).get_action() == TouchAction::Down); bool press_up = (input_event.get_source() == EventSource::Mouse && static_cast(input_event).get_action() == MouseAction::Up) || (input_event.get_source() == EventSource::Touchscreen && static_cast(input_event).get_action() == TouchAction::Up); assert(!(press_down && press_up)); // can't be both, up and down at the same time if (press_down) { timer.start(); if (input_event.get_source() == EventSource::Touchscreen) { const auto &touch_event = static_cast(input_event); if (touch_event.get_touch_points() == 2) { two_finger_tap = true; } } } else if (press_up) { auto press_delta = timer.stop(); if (press_delta < press_time_ms) { if (input_event.get_source() == EventSource::Mouse) { const auto &mouse_button = static_cast(input_event); if (mouse_button.get_button() == MouseButton::Right) { debug_view.active = !debug_view.active; } } else if (input_event.get_source() == EventSource::Touchscreen) { const auto &touch_event = static_cast(input_event); if (two_finger_tap && touch_event.get_touch_points() == 2) { debug_view.active = !debug_view.active; } else { two_finger_tap = false; } } } } } return capture_move_event; } template inline bool Gui::is_debug_view_active() const { return debug_view.active; } template inline void Gui::new_frame() { ImGui::NewFrame(); } template inline void Gui::prepare(PipelineCacheType pipeline_cache, RenderPassType render_pass, std::vector const &shader_stages) { if constexpr (bindingType == BindingType::Cpp) { prepare_impl(pipeline_cache, render_pass, shader_stages); } else { prepare_impl(static_cast(pipeline_cache), static_cast(render_pass), reinterpret_cast const &>(shader_stages)); } } template inline void Gui::prepare_impl(vk::PipelineCache pipeline_cache, vk::RenderPass render_pass, std::vector const &shader_stages) { vk::Device const &device = render_context.get_device().get_handle(); // Descriptor pool vk::DescriptorPoolSize pool_size = {.type = vk::DescriptorType::eCombinedImageSampler, .descriptorCount = 1}; vk::DescriptorPoolCreateInfo descriptorPoolInfo = {.maxSets = 2, .poolSizeCount = 1, .pPoolSizes = &pool_size}; descriptor_pool = device.createDescriptorPool(descriptorPoolInfo); // Descriptor set layout vk::DescriptorSetLayoutBinding layout_binding = { .binding = 0, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .descriptorCount = 1, .stageFlags = vk::ShaderStageFlagBits::eFragment}; vk::DescriptorSetLayoutCreateInfo descriptor_set_layout_create_info = {.bindingCount = 1, .pBindings = &layout_binding}; descriptor_set_layout = device.createDescriptorSetLayout(descriptor_set_layout_create_info); // Descriptor set vk::DescriptorSetAllocateInfo descriptor_allocation = {.descriptorPool = descriptor_pool, .descriptorSetCount = 1, .pSetLayouts = &descriptor_set_layout}; descriptor_set = device.allocateDescriptorSets(descriptor_allocation).front(); // Update descriptor set with font image vk::DescriptorImageInfo font_descriptor = {.sampler = sampler->get_handle(), .imageView = font_image_view->get_handle(), .imageLayout = vk::ImageLayout::eShaderReadOnlyOptimal}; vk::WriteDescriptorSet write_descriptor_set = { .dstSet = descriptor_set, .descriptorCount = 1, .descriptorType = vk::DescriptorType::eCombinedImageSampler, .pImageInfo = &font_descriptor}; device.updateDescriptorSets(write_descriptor_set, nullptr); // Setup graphics pipeline for UI rendering vk::PipelineInputAssemblyStateCreateInfo input_assembly_state = {.topology = vk::PrimitiveTopology::eTriangleList}; vk::PipelineRasterizationStateCreateInfo rasterization_state = { .polygonMode = vk::PolygonMode::eFill, .cullMode = vk::CullModeFlagBits::eNone, .frontFace = vk::FrontFace::eCounterClockwise, .lineWidth = 1.0f}; // Enable blending vk::PipelineColorBlendAttachmentState blend_attachment_state = {.blendEnable = true, .srcColorBlendFactor = vk::BlendFactor::eSrcAlpha, .dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha, .colorBlendOp = vk::BlendOp::eAdd, .srcAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha, .dstAlphaBlendFactor = vk::BlendFactor::eZero, .alphaBlendOp = vk::BlendOp::eAdd, .colorWriteMask = vk::FlagTraits::allFlags}; vk::PipelineColorBlendStateCreateInfo color_blend_state = {.attachmentCount = 1, .pAttachments = &blend_attachment_state}; vk::PipelineDepthStencilStateCreateInfo depth_stencil_state = {.depthTestEnable = false, .depthWriteEnable = false, .depthCompareOp = vk::CompareOp::eAlways, .back = {.compareOp = vk::CompareOp::eAlways}}; vk::PipelineViewportStateCreateInfo viewport_state = {.viewportCount = 1, .scissorCount = 1}; vk::PipelineMultisampleStateCreateInfo multisample_state = {.rasterizationSamples = vk::SampleCountFlagBits::e1}; std::array dynamic_state_enables = {vk::DynamicState::eViewport, vk::DynamicState::eScissor}; vk::PipelineDynamicStateCreateInfo dynamic_state = {.dynamicStateCount = static_cast(dynamic_state_enables.size()), .pDynamicStates = dynamic_state_enables.data()}; // Vertex bindings an attributes based on ImGui vertex definition vk::VertexInputBindingDescription vertex_input_binding = {.binding = 0, .stride = sizeof(ImDrawVert), .inputRate = vk::VertexInputRate::eVertex}; std::array vertex_input_attributes = { {{.location = 0, .binding = 0, .format = vk::Format::eR32G32Sfloat, .offset = offsetof(ImDrawVert, pos)}, {.location = 1, .binding = 0, .format = vk::Format::eR32G32Sfloat, .offset = offsetof(ImDrawVert, uv)}, {.location = 2, .binding = 0, .format = vk::Format::eR8G8B8A8Unorm, .offset = offsetof(ImDrawVert, col)}}}; vk::PipelineVertexInputStateCreateInfo vertex_input_state_create_info = {.vertexBindingDescriptionCount = 1, .pVertexBindingDescriptions = &vertex_input_binding, .vertexAttributeDescriptionCount = static_cast(vertex_input_attributes.size()), .pVertexAttributeDescriptions = vertex_input_attributes.data()}; vk::GraphicsPipelineCreateInfo pipeline_create_info = {.stageCount = static_cast(shader_stages.size()), .pStages = shader_stages.data(), .pVertexInputState = &vertex_input_state_create_info, .pInputAssemblyState = &input_assembly_state, .pViewportState = &viewport_state, .pRasterizationState = &rasterization_state, .pMultisampleState = &multisample_state, .pDepthStencilState = &depth_stencil_state, .pColorBlendState = &color_blend_state, .pDynamicState = &dynamic_state, .layout = pipeline_layout->get_handle(), .renderPass = render_pass, .subpass = subpass, .basePipelineIndex = -1}; vk::Result result; std::tie(result, pipeline) = device.createGraphicsPipeline(pipeline_cache, pipeline_create_info); assert(result == vk::Result::eSuccess); } template inline void Gui::resize(const uint32_t width, const uint32_t height) const { auto &io = ImGui::GetIO(); io.DisplaySize.x = static_cast(width); io.DisplaySize.y = static_cast(height); } template inline void Gui::set_subpass(const uint32_t subpass) { this->subpass = subpass; } template inline void Gui::show_app_info(const std::string &app_name) { // Sample name ImGui::Text("%s", app_name.c_str()); // GPU name auto &device = render_context.get_device(); auto device_name_label = "GPU: " + std::string(device.get_gpu().get_properties().deviceName.data()); ImGui::SameLine(ImGui::GetWindowContentRegionMax().x - ImGui::CalcTextSize(device_name_label.c_str()).x); ImGui::Text("%s", device_name_label.c_str()); } template inline void Gui::show_debug_window(DebugInfo &debug_info, const ImVec2 &position) { auto &io = ImGui::GetIO(); auto &style = ImGui::GetStyle(); auto &font = get_font(default_window_font); // Calculate only once if (debug_view.label_column_width == 0) { debug_view.label_column_width = style.ItemInnerSpacing.x + debug_info.get_longest_label() * font.size / debug_view.scale; } ImGui::SetNextWindowBgAlpha(overlay_alpha); ImGui::SetNextWindowPos(position, ImGuiCond_FirstUseEver); ImGui::SetNextWindowContentSize(ImVec2{io.DisplaySize.x, 0.0f}); bool is_open = true; const ImGuiWindowFlags flags = ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; ImGui::Begin("Debug Window", &is_open, flags); ImGui::PushFont(font.handle); auto field_count = debug_info.get_fields().size() > debug_view.max_fields ? debug_view.max_fields : debug_info.get_fields().size(); ImGui::BeginChild("Table", ImVec2(0, field_count * (font.size + style.ItemSpacing.y)), false); ImGui::Columns(2); ImGui::SetColumnWidth(0, debug_view.label_column_width); ImGui::SetColumnWidth(1, io.DisplaySize.x - debug_view.label_column_width); for (auto &field : debug_info.get_fields()) { const std::string &label = field->label; const std::string &value = field->to_string(); ImGui::Text("%s", label.c_str()); ImGui::NextColumn(); ImGui::Text(" %s", value.c_str()); ImGui::NextColumn(); } ImGui::Columns(1); ImGui::EndChild(); ImGui::PopFont(); ImGui::End(); } template inline void Gui::show_options_window(std::function body, const uint32_t lines) { // Add padding around the text so that the options are not // too close to the edges and are easier to interact with. // Also add double vertical padding to avoid rounded corners. const float window_padding = ImGui::CalcTextSize("T").x; ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2{window_padding, window_padding * 2.0f}); auto window_height = lines * ImGui::GetTextLineHeightWithSpacing() + ImGui::GetStyle().WindowPadding.y * 2.0f; auto window_width = ImGui::GetIO().DisplaySize.x; ImGui::SetNextWindowBgAlpha(overlay_alpha); const ImVec2 size = ImVec2(window_width, 0); ImGui::SetNextWindowSize(size, ImGuiCond_Always); const ImVec2 pos = ImVec2(0.0f, ImGui::GetIO().DisplaySize.y - window_height); ImGui::SetNextWindowPos(pos, ImGuiCond_Always); const ImGuiWindowFlags flags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_AlwaysUseWindowPadding | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing; bool is_open = true; ImGui::Begin("Options", &is_open, flags); body(); ImGui::End(); ImGui::PopStyleVar(); } template inline void Gui::show_simple_window(const std::string &name, uint32_t last_fps, std::function body) { ImGuiIO &io = ImGui::GetIO(); ImGui::NewFrame(); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0); ImGui::SetNextWindowPos(ImVec2(10, 10)); ImGui::SetNextWindowSize(ImVec2(0, 0), ImGuiCond_FirstUseEver); ImGui::Begin("Vulkan Example", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove); ImGui::TextUnformatted(name.c_str()); ImGui::TextUnformatted(std::string(render_context.get_device().get_gpu().get_properties().deviceName.data()).c_str()); ImGui::Text("%.2f ms/frame (%.1d fps)", (1000.0f / last_fps), last_fps); ImGui::PushItemWidth(110.0f * dpi_factor); body(); ImGui::PopItemWidth(); ImGui::End(); ImGui::PopStyleVar(); } template inline void Gui::show_stats(const StatsType &stats) { for (const auto &stat_index : stats.get_requested_stats()) { // Draw graph auto &graph_data = stats_view.get_stat_graph_data(stat_index); const auto &graph_elements = stats.get_data(stat_index); float graph_min = 0.0f; float graph_max = graph_data.max_value; if (!graph_data.has_fixed_max) { auto new_max = *std::max_element(graph_elements.begin(), graph_elements.end()) * stats_view.get_top_padding(); if (new_max > graph_max) { graph_max = new_max; } } const ImVec2 graph_size = ImVec2{ImGui::GetIO().DisplaySize.x, stats_view.get_graph_height() /* dpi */ * dpi_factor}; std::stringstream graph_label; float avg = std::accumulate(graph_elements.begin(), graph_elements.end(), 0.0f) / graph_elements.size(); // Check if the stat is available in the current platform if (stats.is_available(stat_index)) { auto graph_value = avg * graph_data.scale_factor; graph_label << fmt::vformat(graph_data.name + ": " + graph_data.format, fmt::make_format_args(graph_value)); ImGui::PushItemFlag(ImGuiItemFlags_Disabled, true); ImGui::PlotLines("", &graph_elements[0], static_cast(graph_elements.size()), 0, graph_label.str().c_str(), graph_min, graph_max, graph_size); ImGui::PopItemFlag(); } else { graph_label << graph_data.name << ": not available"; ImGui::Text("%s", graph_label.str().c_str()); } } } template inline void Gui::show_top_window(const std::string &app_name, const StatsType *stats, DebugInfo *debug_info) { // Transparent background ImGui::SetNextWindowBgAlpha(overlay_alpha); ImVec2 size{ImGui::GetIO().DisplaySize.x, 0.0f}; ImGui::SetNextWindowSize(size, ImGuiCond_Always); // Top left ImVec2 pos{0.0f, 0.0f}; ImGui::SetNextWindowPos(pos, ImGuiCond_Always); bool is_open = true; ImGui::Begin("Top", &is_open, common_flags); show_app_info(app_name); if (stats) { show_stats(*stats); // Reset max values if user taps on this window if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(0 /* left */)) { stats_view.reset_max_values(); } } if (debug_info) { if (debug_view.active) { show_debug_window(*debug_info, ImVec2{0, ImGui::GetWindowSize().y}); } } ImGui::End(); } template inline void Gui::update(const float delta_time) { if (visible != prev_visible) { drawer.set_dirty(true); prev_visible = visible; } if (!visible) { ImGui::EndFrame(); return; } // Update imGui ImGuiIO &io = ImGui::GetIO(); auto extent = render_context.get_surface_extent(); resize(extent.width, extent.height); io.DeltaTime = delta_time; // Render to generate draw buffers ImGui::Render(); } template inline bool Gui::update_buffers() { ImDrawData *draw_data = ImGui::GetDrawData(); if (!draw_data) { return false; } size_t vertex_buffer_size = draw_data->TotalVtxCount * sizeof(ImDrawVert); size_t index_buffer_size = draw_data->TotalIdxCount * sizeof(ImDrawIdx); if ((vertex_buffer_size == 0) || (index_buffer_size == 0)) { return false; } bool updated = false; if (!vertex_buffer->get_handle() || (vertex_buffer_size != vertex_buffer->get_size())) { vertex_buffer.reset(); vertex_buffer = std::make_unique(render_context.get_device(), vertex_buffer_size, vk::BufferUsageFlagBits::eVertexBuffer, VMA_MEMORY_USAGE_GPU_TO_CPU); vertex_buffer->set_debug_name("GUI vertex buffer"); updated = true; } if (!index_buffer->get_handle() || (index_buffer_size != index_buffer->get_size())) { index_buffer.reset(); index_buffer = std::make_unique(render_context.get_device(), index_buffer_size, vk::BufferUsageFlagBits::eIndexBuffer, VMA_MEMORY_USAGE_GPU_TO_CPU); index_buffer->set_debug_name("GUI index buffer"); updated = true; } // Upload data upload_draw_data(draw_data, vertex_buffer->map(), index_buffer->map()); vertex_buffer->flush(); index_buffer->flush(); vertex_buffer->unmap(); index_buffer->unmap(); return updated; } template inline vkb::BufferAllocationCpp Gui::update_buffers(vkb::core::CommandBufferCpp &command_buffer) { ImDrawData *draw_data = ImGui::GetDrawData(); if (!draw_data) { return vkb::BufferAllocationCpp{}; } size_t vertex_buffer_size = draw_data->TotalVtxCount * sizeof(ImDrawVert); size_t index_buffer_size = draw_data->TotalIdxCount * sizeof(ImDrawIdx); if ((vertex_buffer_size == 0) || (index_buffer_size == 0)) { return vkb::BufferAllocationCpp{}; } std::vector vertex_data(vertex_buffer_size); std::vector index_data(index_buffer_size); upload_draw_data(draw_data, vertex_data.data(), index_data.data()); auto vertex_allocation = render_context.get_active_frame().allocate_buffer(vk::BufferUsageFlagBits::eVertexBuffer, vertex_buffer_size); vertex_allocation.update(vertex_data); std::vector> buffers; buffers.emplace_back(std::ref(vertex_allocation.get_buffer())); std::vector offsets{vertex_allocation.get_offset()}; command_buffer.bind_vertex_buffers(0, buffers, offsets); auto index_allocation = render_context.get_active_frame().allocate_buffer(vk::BufferUsageFlagBits::eIndexBuffer, index_buffer_size); index_allocation.update(index_data); command_buffer.bind_index_buffer(index_allocation.get_buffer(), index_allocation.get_offset(), vk::IndexType::eUint16); return vertex_allocation; } template inline void Gui::upload_draw_data(const ImDrawData *draw_data, uint8_t *vertex_data, uint8_t *index_data) { ImDrawVert *vtx_dst = reinterpret_cast(vertex_data); ImDrawIdx *idx_dst = reinterpret_cast(index_data); for (int n = 0; n < draw_data->CmdListsCount; n++) { const ImDrawList *cmd_list = draw_data->CmdLists[n]; memcpy(vtx_dst, cmd_list->VtxBuffer.Data, cmd_list->VtxBuffer.Size * sizeof(ImDrawVert)); memcpy(idx_dst, cmd_list->IdxBuffer.Data, cmd_list->IdxBuffer.Size * sizeof(ImDrawIdx)); vtx_dst += cmd_list->VtxBuffer.Size; idx_dst += cmd_list->IdxBuffer.Size; } } template inline Gui::StatsView::StatsView(const StatsType *stats) { if (stats == nullptr) { return; } // Request graph data information for each stat and record it in graph_map const std::set &indices = stats->get_requested_stats(); for (StatIndex i : indices) { graph_map[i] = stats->get_graph_data(i); } } template inline float Gui::StatsView::get_graph_height() const { return graph_height; } template inline StatGraphData const &Gui::StatsView::get_stat_graph_data(StatIndex const &stat_index) const { auto graph_map_it = graph_map.find(stat_index); assert(graph_map_it != graph_map.end() && "StatIndex not implemented in gui graph_map"); return graph_map_it->second; } template inline float Gui::StatsView::get_top_padding() const { return top_padding; } template inline void Gui::StatsView::reset_max_value(const StatIndex index) { auto pr = graph_map.find(index); if (pr != graph_map.end()) { reset_graph_max_value(pr->second); } } template inline void Gui::StatsView::reset_max_values() { // For every entry in the map std::ranges::for_each(graph_map, [](auto &pr) { reset_graph_max_value(pr.second); }); } } // namespace vkb