From a145bae065df95c1a57bed7edd4d794cc7e6ed9b Mon Sep 17 00:00:00 2001 From: ecker Date: Tue, 5 May 2026 14:54:34 -0500 Subject: [PATCH] updates to imgui integration (added thread metrics, moved things to a behavior, fixed bug i dont remember existing where imgui doesn't render for new scenes because it doesnt rebind to the new gui mode --- bin/data/config.json | 4 +- bin/data/entities/gui/pause/main.json | 3 +- bin/data/scenes/startmenu/scene.json | 1 + engine/inc/uf/ext/imgui/imgui.h | 5 +- engine/inc/uf/utils/string/ext.h | 3 + engine/src/ext/imgui/behavior.cpp | 58 ++++ engine/src/ext/imgui/behavior.h | 20 ++ engine/src/ext/imgui/consoleWindow.inl | 194 ++++++++++++ engine/src/ext/imgui/imgui.cpp | 408 ++++++++----------------- engine/src/ext/imgui/threadMetrics.inl | 109 +++++++ engine/src/utils/string/ext.cpp | 19 ++ 11 files changed, 544 insertions(+), 280 deletions(-) create mode 100644 engine/src/ext/imgui/behavior.cpp create mode 100644 engine/src/ext/imgui/behavior.h create mode 100644 engine/src/ext/imgui/consoleWindow.inl create mode 100644 engine/src/ext/imgui/threadMetrics.inl diff --git a/bin/data/config.json b/bin/data/config.json index ff06d6d7..7887b566 100644 --- a/bin/data/config.json +++ b/bin/data/config.json @@ -1,7 +1,7 @@ { "engine": { "scenes": { - "start": "StartMenu", + "start": "SourceEngine", "matrix": { "reverseInfinite": true }, "meshes": { "interleaved": false }, "lights": { "enabled": false, @@ -299,7 +299,7 @@ "enabled": true }, "imgui": { - "enabled": false + "enabled": true }, "fsr": { "enabled": true, diff --git a/bin/data/entities/gui/pause/main.json b/bin/data/entities/gui/pause/main.json index 3e49a3da..91541a0e 100644 --- a/bin/data/entities/gui/pause/main.json +++ b/bin/data/entities/gui/pause/main.json @@ -2,7 +2,8 @@ "name": "Gui: Menu", "type": "Gui", "behaviors": [ - "GuiBehavior" + "GuiBehavior", + "ImguiBehavior" ], "ignore": false, "transform": { diff --git a/bin/data/scenes/startmenu/scene.json b/bin/data/scenes/startmenu/scene.json index 0571c89d..3031e1e8 100644 --- a/bin/data/scenes/startmenu/scene.json +++ b/bin/data/scenes/startmenu/scene.json @@ -3,6 +3,7 @@ "behaviors": [ "SceneBehavior", "ExtSceneBehavior", + "ImguiBehavior", "BgmEmitterBehavior" ], "assets": [ diff --git a/engine/inc/uf/ext/imgui/imgui.h b/engine/inc/uf/ext/imgui/imgui.h index e7bf492c..25ff814b 100644 --- a/engine/inc/uf/ext/imgui/imgui.h +++ b/engine/inc/uf/ext/imgui/imgui.h @@ -1,17 +1,18 @@ #pragma once #include +#include #if UF_USE_IMGUI namespace ext { namespace imgui { + extern UF_API bool focused; + void initialize(); void tick(); void render(); void terminate(); - - extern UF_API bool focused; } } diff --git a/engine/inc/uf/utils/string/ext.h b/engine/inc/uf/utils/string/ext.h index 685a8b80..f977110f 100644 --- a/engine/inc/uf/utils/string/ext.h +++ b/engine/inc/uf/utils/string/ext.h @@ -19,6 +19,9 @@ namespace uf { uf::stl::string UF_API replace( const uf::stl::string&, const uf::stl::string&, const uf::stl::string& ); uf::stl::string UF_API lowercase( const uf::stl::string& ); uf::stl::string UF_API uppercase( const uf::stl::string& ); + uf::stl::string UF_API ltrim( const uf::stl::string& ); + uf::stl::string UF_API rtrim( const uf::stl::string& ); + uf::stl::string UF_API trim( const uf::stl::string& ); uf::stl::vector UF_API split( const uf::stl::string&, const uf::stl::string& ); uf::stl::string UF_API si( double value, const uf::stl::string& unit, size_t precision = 3 ); bool UF_API contains( const uf::stl::string&, const uf::stl::string& ); diff --git a/engine/src/ext/imgui/behavior.cpp b/engine/src/ext/imgui/behavior.cpp new file mode 100644 index 00000000..d8b3fcd3 --- /dev/null +++ b/engine/src/ext/imgui/behavior.cpp @@ -0,0 +1,58 @@ +#include "behavior.h" + +#include +#include +#include +#include +#include +#include + +#if UF_USE_IMGUI + #include + #include + #include + + #include "consoleWindow.inl" + #include "threadMetrics.inl" +#endif + +UF_BEHAVIOR_REGISTER_CPP(ext::ImguiBehavior) +UF_BEHAVIOR_TRAITS_CPP(ext::ImguiBehavior, ticks = false, renders = false, thread = "") +#define this (&self) +void ext::ImguiBehavior::initialize( uf::Object& self ) { + auto& metadata = this->getComponent(); + auto& metadataJson = this->getComponent(); + +#if UF_USE_IMGUI + this->addHook( "gui:IMGUI.tick", [&](){ + tick( self ); + } ); +#endif + + ::consoleWindow.position.x = uf::renderer::settings::width - ::consoleWindow.size.x; + + UF_BEHAVIOR_METADATA_BIND_SERIALIZER_HOOKS(metadata, metadataJson); +} +void ext::ImguiBehavior::tick( uf::Object& self ) { +#if UF_USE_IMGUI + // console window + { + bool opened; + ::consoleWindow.Draw("Console", opened); + } + // thread metrics window + { + bool opened = true; + ::threadMetrics.Draw("Thread Metrics", opened); + } +#endif +} +void ext::ImguiBehavior::render( uf::Object& self ){ + +} +void ext::ImguiBehavior::destroy( uf::Object& self ){} +void ext::ImguiBehavior::Metadata::serialize( uf::Object& self, uf::Serializer& serializer ){ +} +void ext::ImguiBehavior::Metadata::deserialize( uf::Object& self, uf::Serializer& serializer ){ +} +#undef this \ No newline at end of file diff --git a/engine/src/ext/imgui/behavior.h b/engine/src/ext/imgui/behavior.h new file mode 100644 index 00000000..a724ee94 --- /dev/null +++ b/engine/src/ext/imgui/behavior.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + + +namespace ext { + namespace ImguiBehavior { + UF_BEHAVIOR_DEFINE_TYPE(); + EXT_BEHAVIOR_DEFINE_TRAITS(); + EXT_BEHAVIOR_DEFINE_FUNCTIONS(); + UF_BEHAVIOR_DEFINE_METADATA( + + ); + } +} \ No newline at end of file diff --git a/engine/src/ext/imgui/consoleWindow.inl b/engine/src/ext/imgui/consoleWindow.inl new file mode 100644 index 00000000..6e7a3237 --- /dev/null +++ b/engine/src/ext/imgui/consoleWindow.inl @@ -0,0 +1,194 @@ +namespace { + struct ConsoleWindow { + ImGuiTextFilter filter; + int historyPos = -1; + + struct { + bool automatic = true; + bool bottom = false; + } scroll; + + uf::stl::string commandBuf; + + pod::Vector2ui size{800, 600}; + pod::Vector2ui position{64, 32}; + + void Draw( const uf::stl::string& title, bool& open ) { + ImGui::SetNextWindowSize(ImVec2(size.x, size.y), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(position.x, position.y), ImGuiCond_FirstUseEver); + + if (!ImGui::Begin(title.c_str(), &open)) { + ImGui::End(); + return; + } + + if ( ImGui::BeginPopupContextItem() ) { + if ( ImGui::MenuItem("Close Console") ) open = false; + ImGui::EndPopup(); + } + + DrawToolbar(); + ImGui::Separator(); + DrawLog(); + ImGui::Separator(); + DrawInput(); + + ImGui::End(); + } + + private: + void DrawToolbar() { + if ( ImGui::SmallButton("Clear") ) uf::console::clear(); + + ImGui::SameLine(); + bool copyToClipboard = ImGui::SmallButton("Copy"); + + if ( ImGui::Button("Options") ) { + ImGui::OpenPopup("Options"); + } + if ( ImGui::BeginPopup("Options") ) { + ImGui::Checkbox("Auto-scroll", &scroll.automatic); + ImGui::EndPopup(); + } + + ImGui::SameLine(); + filter.Draw("Filter (\"incl,-excl\") (\"error\")", 180); + + if ( copyToClipboard ) { + ImGui::LogToClipboard(); + for ( auto& item : uf::console::log ) { + if (filter.PassFilter(item.c_str())) ImGui::LogText("%s\n", item.c_str()); + } + ImGui::LogFinish(); + } + } + + void DrawLog() { + const float footerReserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); + ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footerReserve), false, ImGuiWindowFlags_HorizontalScrollbar); + + if ( ImGui::BeginPopupContextWindow() ) { + if ( ImGui::Selectable("Clear") ) uf::console::clear(); + ImGui::EndPopup(); + } + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1)); + + for ( auto& item : uf::console::log ) { + if ( !filter.PassFilter(item.c_str()) ) continue; + + ImVec4 color = ImVec4(1.0f, 1.0f, 1.0f, 1.0f); + bool hasColor = false; + + if ( uf::string::matched(item, "^\\[ERROR\\]") ) { + color = ImVec4(1.0f, 0.4f, 0.4f, 1.0f); hasColor = true; + } else if ( uf::string::matched(item, "^\\> ")) { + color = ImVec4(1.0f, 0.8f, 0.6f, 1.0f); hasColor = true; + } + + if ( hasColor ) ImGui::PushStyleColor(ImGuiCol_Text, color); + ImGui::TextUnformatted(item.c_str()); + if ( hasColor ) ImGui::PopStyleColor(); + } + + if ( scroll.bottom || (scroll.automatic && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) ) { + ImGui::SetScrollHereY(1.0f); + } + scroll.bottom = false; + + ImGui::PopStyleVar(); + ImGui::EndChild(); + } + + void DrawInput() { + bool reclaimFocus = false; + ImGuiInputTextFlags inputTextFlags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory; + + if ( ImGui::InputText("Input", &commandBuf, inputTextFlags, &TextEditCallbackStub, (void*) this) ) { + uf::string::trim( commandBuf ); + if ( !commandBuf.empty() ) { + uf::console::execute(commandBuf); + commandBuf.clear(); + } + historyPos = -1; + scroll.bottom = true; + reclaimFocus = true; + } + + ImGui::SetItemDefaultFocus(); + if ( reclaimFocus ) ImGui::SetKeyboardFocusHere(-1); + } + + static int TextEditCallbackStub( ImGuiInputTextCallbackData* data ) { + ConsoleWindow* console = (ConsoleWindow*) data->UserData; + return console->TextEditCallback(data); + } + + int TextEditCallback( ImGuiInputTextCallbackData* data ) { + switch ( data->EventFlag ) { + case ImGuiInputTextFlags_CallbackCompletion: { + const char* end = data->Buf + data->CursorPos; + const char* start = end; + while ( start > data->Buf ) { + const char c = start[-1]; + if (c == ' ' || c == '\t' || c == ',' || c == ';') break; + start--; + } + + uf::stl::string input{ start, end }; + uf::stl::vector candidates; + for ( auto& pair : uf::console::commands ) { + if (pair.first.find(input) == 0) candidates.emplace_back(pair.first); + } + + if ( candidates.empty() ) { + UF_MSG_ERROR("No match for `{}`", input); + } else if ( candidates.size() == 1 ) { + data->DeleteChars((int)(start - data->Buf), (int)(end - start)); + data->InsertChars(data->CursorPos, candidates[0].c_str()); + data->InsertChars(data->CursorPos, " "); + } else { + int matchLen = (int)(end - start); + while ( true ) { + if ( matchLen >= candidates[0].size() ) break; + char c = candidates[0][matchLen]; + bool allMatch = true; + for ( size_t i = 1; i < candidates.size(); i++ ) { + if ( matchLen >= candidates[i].size() || candidates[i][matchLen] != c ) { + allMatch = false; + break; + } + } + if ( !allMatch ) break; + matchLen++; + } + + if ( matchLen > (int) (end - start) ) { + data->DeleteChars((int)(start - data->Buf), (int)(end - start)); + data->InsertChars(data->CursorPos, candidates[0].substr(0, matchLen).c_str()); + } else { + UF_MSG_DEBUG("Matches: {}", uf::string::join(candidates, " ")); + } + } + } break; + + case ImGuiInputTextFlags_CallbackHistory: { + const int prevPos = historyPos; + if ( data->EventKey == ImGuiKey_UpArrow ) { + if ( historyPos == -1 ) historyPos = (int)uf::console::history.size() - 1; + else if ( historyPos > 0 ) historyPos--; + } else if ( data->EventKey == ImGuiKey_DownArrow ) { + if ( historyPos != -1 && ++historyPos >= (int)uf::console::history.size() ) historyPos = -1; + } + + if ( prevPos != historyPos ) { + const char* historyStr = (historyPos >= 0) ? uf::console::history[historyPos].c_str() : ""; + data->DeleteChars(0, data->BufTextLen); + data->InsertChars(0, historyStr); + } + } break; + } + return 0; + } + } consoleWindow; +} \ No newline at end of file diff --git a/engine/src/ext/imgui/imgui.cpp b/engine/src/ext/imgui/imgui.cpp index 6618efec..d4678fcc 100644 --- a/engine/src/ext/imgui/imgui.cpp +++ b/engine/src/ext/imgui/imgui.cpp @@ -2,8 +2,10 @@ #include #include #include +#include #include #include +#include #if UF_USE_VULKAN #include @@ -14,235 +16,35 @@ #if UF_ENV_WINDOWS #include #endif - -#include - -#include -#include -#include -#include +#if UF_ENV_LINUX + // to-do: linux +#endif bool ext::imgui::focused = false; namespace { #if UF_USE_VULKAN - VkDescriptorPool descriptorPool{}; + uf::renderer::RenderMode* boundRenderMode = NULL; + VkDescriptorPool descriptorPool = VK_NULL_HANDLE; #endif - - struct ConsoleWindow { - ImGuiTextFilter filter; - int history = 0; - - struct { - bool automatic = true; - bool bottom = false; - } scroll; - - uf::stl::unordered_map> commands; - - pod::Vector2ui size{800, 600}; - pod::Vector2ui position{64, 32}; - - void Draw( const uf::stl::string& title, bool& open ) { - ImGui::SetNextWindowSize(ImVec2(size.x, size.y), ImGuiCond_FirstUseEver); - ImGui::SetNextWindowPos(ImVec2(position.x, position.y), ImGuiCond_FirstUseEver); - - if (!ImGui::Begin(title.c_str(), &open)) { - ImGui::End(); - return; - } - - if ( ImGui::BeginPopupContextItem() ) { - if ( ImGui::MenuItem("Close Console") ) open = false; - ImGui::EndPopup(); - } - - if ( ImGui::SmallButton("Clear") ) uf::console::clear(); - - ImGui::SameLine(); - bool copyToClipboard = ImGui::SmallButton("Copy"); - ImGui::Separator(); - - if ( ImGui::BeginPopup("Options") ) { - ImGui::Checkbox("Auto-scroll", &this->scroll.bottom); - ImGui::EndPopup(); - } - - if ( ImGui::Button("Options") ) { - ImGui::OpenPopup("Options"); - } - - ImGui::SameLine(); - this->filter.Draw("Filter (\"incl,-excl\") (\"error\")", 180); - ImGui::Separator(); - - const float footerReserve = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing(); - ImGui::BeginChild("ScrollingRegion", ImVec2(0, -footerReserve), false, ImGuiWindowFlags_HorizontalScrollbar); - if ( ImGui::BeginPopupContextWindow() ) { - if ( ImGui::Selectable("Clear") ) uf::console::clear(); - ImGui::EndPopup(); - } - - ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4, 1)); - - if ( copyToClipboard ) ImGui::LogToClipboard(); - for ( auto& item : uf::console::log ) { - if ( !this->filter.PassFilter(item.c_str()) ) continue; - - ImVec4 color = ImVec4(0, 0, 0, 0); - - if ( uf::string::matched(item, "^\\[ERROR\\]") ) color = ImVec4(1.0f, 0.4f, 0.4f, 1.0f); - else if ( uf::string::matched(item, "^\\> ") ) color = ImVec4(1.0f, 0.8f, 0.6f, 1.0f); - - if ( color.w > 0.0f ) ImGui::PushStyleColor(ImGuiCol_Text, color); - ImGui::TextUnformatted( item.c_str() ); - if ( color.w > 0.0f ) ImGui::PopStyleColor(); - } - if ( copyToClipboard ) ImGui::LogFinish(); - - if ( this->scroll.bottom || (this->scroll.automatic && ImGui::GetScrollY() >= ImGui::GetScrollMaxY()) ) { - ImGui::SetScrollHereY(1.0f); - } - this->scroll.bottom = false; - - ImGui::PopStyleVar(); - ImGui::EndChild(); - ImGui::Separator(); - - bool reclaimFocus = false; - ImGuiInputTextFlags inputTextFlags = ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_CallbackCompletion | ImGuiInputTextFlags_CallbackHistory; - - uf::stl::string command; - if ( ImGui::InputText("Input", &command, inputTextFlags, &TextEditCallbackStub, (void *)this) ) { - this->history = -1; - this->scroll.bottom = true; - reclaimFocus = true; - - - // to-do: add a way to either asynchronously invoke commands or not - uf::console::execute( command ); - /* - uf::thread::queue( uf::thread::asyncThreadName, [=](){ - uf::console::execute( command ); - }); - */ - /* - // this blocks - uf::thread::queue( uf::thread::fetchWorker(), [=](){ - uf::console::execute( command ); - }); - */ - /* - // this still blocks - auto tasks = uf::thread::schedule(true); - tasks.queue([=](){ - uf::console::execute( command ); - }); - uf::thread::execute( tasks ); - */ - - } - - ImGui::SetItemDefaultFocus(); - if ( reclaimFocus ) ImGui::SetKeyboardFocusHere(-1); - ImGui::End(); - } - - static int TextEditCallbackStub( ImGuiInputTextCallbackData *data ) { - ConsoleWindow *console = (ConsoleWindow*) data->UserData; - return console->TextEditCallback(data); - } - - int TextEditCallback( ImGuiInputTextCallbackData* data ) { - switch ( data->EventFlag ) { - case ImGuiInputTextFlags_CallbackCompletion: { - const char* end = data->Buf + data->CursorPos; - const char* start = end; - while ( start > data->Buf ) { - const char c = start[-1]; - if (c == ' ' || c == '\t' || c == ',' || c == ';') - break; - start--; - } - - uf::stl::string input{ start, end }; - uf::stl::vector candidates; - for ( auto& pair : uf::console::commands ) { - if ( pair.first.find(input) == 0 ) candidates.emplace_back( input ); - } - if ( candidates.empty() ) { - UF_MSG_ERROR("No match for `{}`", input); - } else if ( candidates.size() == 1 ) { - data->DeleteChars((int)(start - data->Buf), (int)(end - start)); - data->InsertChars(data->CursorPos, candidates[0].c_str()); - data->InsertChars(data->CursorPos, " "); - } else { - /* - int match = (int) (end - start); - while ( true ) { - int c = 0; - bool allCandidatesMatches = true; - for ( auto i = 0; i < candidates.size() && allCandidatesMatches; ++i ) { - if ( i == 0 ) c = uf::string::uppercase( candidates[i].at(match) ); - } - } - */ - } - } break; - case ImGuiInputTextFlags_CallbackHistory: { - const int previousPosition = this->history; - if ( data->EventKey == ImGuiKey_UpArrow ) { - if (this->history == -1) this->history = uf::console::history.size() - 1; - else if (this->history > 0) this->history--; - } else if ( data->EventKey == ImGuiKey_DownArrow ) { - if ( this->history != -1 && ++this->history >= uf::console::history.size() ) this->history = -1; - } - - if ( previousPosition != this->history ) { - const char* history = (this->history >= 0) ? uf::console::history[this->history].c_str() : ""; - data->DeleteChars(0, data->BufTextLen); - data->InsertChars(0, history); - } - } break; - } - return 0; - } - }; - - ConsoleWindow console; bool initialized = false; -} -void ext::imgui::initialize() { -#if UF_USE_VULKAN - if ( !uf::renderer::hasRenderMode("Gui", true) ) return; -#endif - - IMGUI_CHECKVERSION(); - ImGui::CreateContext(); - ImGuiIO& io = ImGui::GetIO(); - io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls - io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls - io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; // -// io.ConfigFlags |= ImGuiConfigFlags_NoMouse; // - io.DisplaySize = ImVec2(uf::renderer::settings::width,uf::renderer::settings::height); - io.MouseDrawCursor = false; - io.IniFilename = NULL; - - uf::hooks.addHook( "window:Mouse.CursorVisibility", [&]( pod::payloads::windowMouseCursorVisibility& payload ){ - io.MouseDrawCursor = payload.mouse.visible; - }); - - ::console.position.x = uf::renderer::settings::width - ::console.size.x; + void initPlatform() { #if UF_ENV_WINDOWS - ImGui_ImplWin32_Init(uf::renderer::device.window->getHandle()); + ImGui_ImplWin32_Init(uf::renderer::device.window->getHandle()); #endif +#if UF_ENV_LINUX + // to-do: linux +#endif + } + void initRenderer() { #if UF_USE_VULKAN - auto& renderMode = uf::renderer::getRenderMode("Gui", true); - { - VkDescriptorPoolSize poolSizes[] = - { + auto& renderMode = uf::renderer::getRenderMode("Gui", true); + ::boundRenderMode = &renderMode; + + // create descriptor pool + VkDescriptorPoolSize poolSizes[] = { { VK_DESCRIPTOR_TYPE_SAMPLER, 1000 }, { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1000 }, { VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 1000 }, @@ -261,99 +63,155 @@ void ext::imgui::initialize() { poolInfo.maxSets = 1000 * IM_ARRAYSIZE(poolSizes); poolInfo.poolSizeCount = (uint32_t)IM_ARRAYSIZE(poolSizes); poolInfo.pPoolSizes = poolSizes; - VK_CHECK_RESULT(vkCreateDescriptorPool(uf::renderer::device, &poolInfo, NULL, &::descriptorPool)); - } - { + VK_CHECK_RESULT(vkCreateDescriptorPool(uf::renderer::device, &poolInfo, NULL, &descriptorPool)); + + // initialize ImGui vulkan backend ImGui_ImplVulkan_InitInfo imguiInitInfo = {}; imguiInitInfo.Instance = uf::renderer::device.instance; imguiInitInfo.PhysicalDevice = uf::renderer::device.physicalDevice; imguiInitInfo.Device = uf::renderer::device.logicalDevice; imguiInitInfo.QueueFamily = uf::renderer::device.queueFamilyIndices.graphics; - imguiInitInfo.Queue = uf::renderer::device.getQueue( uf::renderer::QueueEnum::GRAPHICS ); + imguiInitInfo.Queue = uf::renderer::device.getQueue(uf::renderer::QueueEnum::GRAPHICS); imguiInitInfo.PipelineCache = uf::renderer::device.pipelineCache; - imguiInitInfo.DescriptorPool = ::descriptorPool; + imguiInitInfo.DescriptorPool = descriptorPool; imguiInitInfo.Subpass = 0; imguiInitInfo.MinImageCount = uf::renderer::swapchain.buffers; imguiInitInfo.ImageCount = uf::renderer::swapchain.buffers; imguiInitInfo.MSAASamples = VK_SAMPLE_COUNT_1_BIT; - imguiInitInfo.Allocator = NULL; - imguiInitInfo.CheckVkResultFn = NULL; - + imguiInitInfo.Allocator = nullptr; + imguiInitInfo.CheckVkResultFn = nullptr; ImGui_ImplVulkan_Init(&imguiInitInfo, renderMode.renderTarget.renderPass); - } - { - // Use any command queue + + // upload fonts auto commandBuffer = uf::renderer::device.fetchCommandBuffer(uf::renderer::QueueEnum::GRAPHICS); ImGui_ImplVulkan_CreateFontsTexture(commandBuffer.handle); uf::renderer::device.flushCommandBuffer(commandBuffer); ImGui_ImplVulkan_DestroyFontUploadObjects(); - } - { - renderMode.bindCallback( 0, [&]( VkCommandBuffer commandBuffer, size_t _ ){ + // bind render callback + renderMode.bindCallback(0, [&]( VkCommandBuffer cb, size_t _ ) { ImDrawData* drawData = ImGui::GetDrawData(); - ImGui_ImplVulkan_RenderDrawData(drawData, commandBuffer); + if (drawData) ImGui_ImplVulkan_RenderDrawData(drawData, cb); }); - } #elif UF_USE_OPENGL - ImGui_ImplOpenGL2_Init(); + ImGui_ImplOpenGL2_Init(); +#endif + } + + void newFramePlatform() { +#if UF_ENV_WINDOWS + ImGui_ImplWin32_NewFrame(); +#endif +#if UF_ENV_LINUX + // to-do: linux +#endif + } + + void newFrameRenderer() { +#if UF_USE_VULKAN + auto& renderMode = uf::renderer::getRenderMode("Gui", true); + renderMode.rerecord = true; + ImGui_ImplVulkan_NewFrame(); +#elif UF_USE_OPENGL + ImGui_ImplOpenGL2_NewFrame(); +#endif + } + + void shutdownPlatform() { +#if UF_ENV_WINDOWS + ImGui_ImplWin32_Shutdown(); +#endif +#if UF_ENV_LINUX + // to-do: linux +#endif + } + + void shutdownRenderer() { +#if UF_USE_VULKAN + ImGui_ImplVulkan_Shutdown(); + if ( descriptorPool != VK_NULL_HANDLE ) { + vkDestroyDescriptorPool(uf::renderer::device, descriptorPool, nullptr); + descriptorPool = VK_NULL_HANDLE; + } +#elif UF_USE_OPENGL + ImGui_ImplOpenGL2_Shutdown(); +#endif + } +} + +void ext::imgui::initialize() { +#if UF_USE_VULKAN + if ( !uf::renderer::hasRenderMode("Gui", true) ) return; #endif - ::initialized = true; + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; + io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; + io.DisplaySize = ImVec2((float) uf::renderer::settings::width, (float) uf::renderer::settings::height); + io.MouseDrawCursor = false; + io.IniFilename = nullptr; + + uf::hooks.addHook("window:Mouse.CursorVisibility", [&](pod::payloads::windowMouseCursorVisibility& payload) { + ImGui::GetIO().MouseDrawCursor = payload.mouse.visible; + }); + + ::initPlatform(); + ::initRenderer(); + + ::initialized = true; } + void ext::imgui::tick() { - if ( !::initialized ) ext::imgui::initialize(); - if ( !::initialized ) return; + if ( !::initialized ) ext::imgui::initialize(); + if ( !::initialized ) return; + // no GUI render mode found + if ( !uf::renderer::hasRenderMode("Gui", true) ) return; + auto& renderMode = uf::renderer::getRenderMode("Gui", true); + + // check if rendermode changed + if ( ::boundRenderMode != &renderMode ) { + ::shutdownRenderer(); + ::initRenderer(); + } + ImGuiIO& io = ImGui::GetIO(); io.DeltaTime = uf::physics::time::delta; - io.DisplaySize = ImVec2(uf::renderer::settings::width,uf::renderer::settings::height); + io.DisplaySize = ImVec2((float)uf::renderer::settings::width, (float)uf::renderer::settings::height); ext::imgui::focused = io.WantCaptureKeyboard || io.WantCaptureMouse; -#if UF_USE_VULKAN - auto& renderMode = uf::renderer::getRenderMode("Gui", true); - renderMode.rerecord = true; - - ImGui_ImplVulkan_NewFrame(); -#elif UF_USE_OPENGL - ImGui_ImplOpenGL2_NewFrame(); -#endif - -#if UF_ENV_WINDOWS - ImGui_ImplWin32_NewFrame(); -#endif + ::newFrameRenderer(); + ::newFramePlatform(); ImGui::NewFrame(); - - auto& scene = uf::scene::getCurrentScene(); - if ( scene.globalFindByName("Gui: Menu") ) { - bool opened; - console.Draw("Console", opened); - } - + + uf::hooks.call("gui:IMGUI.tick"); + ImGui::Render(); } + void ext::imgui::render() { - if ( !::initialized ) return; + if (!::initialized) return; + #if UF_USE_OPENGL auto renderMode = uf::renderer::getCurrentRenderMode(); if ( !renderMode || renderMode->getName() != "Gui" ) return; - auto data = ImGui::GetDrawData(); - if ( !data ) return; - ImGui_ImplOpenGL2_RenderDrawData(data); -#endif -} -void ext::imgui::terminate() { - if ( !::initialized ) return; -#if UF_USE_VULKAN - ImGui_ImplVulkan_Shutdown(); -#elif UF_USE_OPENGL - ImGui_ImplOpenGL2_Shutdown(); -#endif -#if UF_ENV_WINDOWS - ImGui_ImplWin32_Shutdown(); -#endif - ImGui::DestroyContext(); + ImDrawData* data = ImGui::GetDrawData(); + if (data) ImGui_ImplOpenGL2_RenderDrawData(data); +#endif } -#endif \ No newline at end of file + +void ext::imgui::terminate() { + if (!::initialized) return; + + ::shutdownRenderer(); + ::shutdownPlatform(); + ImGui::DestroyContext(); + + ::initialized = false; +} +#endif diff --git a/engine/src/ext/imgui/threadMetrics.inl b/engine/src/ext/imgui/threadMetrics.inl new file mode 100644 index 00000000..ed97881f --- /dev/null +++ b/engine/src/ext/imgui/threadMetrics.inl @@ -0,0 +1,109 @@ +namespace { + struct ThreadMetricsWindow { + static const int HISTORY_SIZE = 120; + + struct ThreadHistory { + float activeTime[HISTORY_SIZE] = { 0.0f }; + float idleTime[HISTORY_SIZE] = { 0.0f }; + float totalTime[HISTORY_SIZE] = { 0.0f }; + int offset = 0; + bool filled = false; + }; + + uf::stl::unordered_map histories; + + pod::Vector2ui size{400, 500}; + pod::Vector2ui position{32, 32}; + + void Draw(const uf::stl::string& title, bool& open) { + ImGui::SetNextWindowSize(ImVec2(size.x, size.y), ImGuiCond_FirstUseEver); + ImGui::SetNextWindowPos(ImVec2(position.x, position.y), ImGuiCond_FirstUseEver); + + if (!ImGui::Begin(title.c_str(), &open)) { + ImGui::End(); + return; + } + + #if UF_THREAD_METRICS + auto metrics = uf::thread::collectStats(); + + if (ImGui::BeginTable("ThreadMetricsTable", 2, ImGuiTableFlags_BordersInnerH | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable)) { + ImGui::TableSetupColumn("Thread Info", ImGuiTableColumnFlags_WidthFixed, 150.0f); + ImGui::TableSetupColumn("History", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableHeadersRow(); + + for (auto& [name, stats] : metrics) { + float active = std::get<0>(stats); + float idle = std::get<1>(stats); + float total = std::get<2>(stats); + uint32_t tasks = std::get<3>(stats); + + auto& history = histories[name]; + + history.activeTime[history.offset] = active; + history.idleTime[history.offset] = idle; + history.totalTime[history.offset] = total; + history.offset++; + if (history.offset >= HISTORY_SIZE) { + history.offset = 0; + history.filled = true; + } + + int count = history.filled ? HISTORY_SIZE : (history.offset == 0 ? 1 : history.offset); + float avgActive = 0.0f, avgIdle = 0.0f, avgTotal = 0.0f; + + for (int i = 0; i < count; ++i) { + avgActive += history.activeTime[i]; + avgIdle += history.idleTime[i]; + avgTotal += history.totalTime[i]; + } + avgActive /= count; + avgIdle /= count; + avgTotal /= count; + + ImGui::TableNextRow(); + + ImGui::PushID(name.c_str()); + + ImGui::TableSetColumnIndex(0); + ImGui::TextUnformatted(name.c_str()); + ImGui::Text("Tasks: %u", tasks); + ImGui::Text("Time: %.2f ms", avgTotal); + + float utilization = (avgTotal > 0.0f) ? (avgActive / avgTotal) : 0.0f; + char utilText[32]; + snprintf(utilText, sizeof(utilText), "Load: %.0f%%", utilization * 100.0f); + + if (utilization > 0.8f) ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.8f, 0.2f, 0.2f, 1.0f)); + ImGui::ProgressBar(utilization, ImVec2(-FLT_MIN, 0), utilText); + if (utilization > 0.8f) ImGui::PopStyleColor(); + + ImGui::TableSetColumnIndex(1); + + float graphWidth = ImGui::GetContentRegionAvail().x; + float max_val = 16.6f; + + char overlayActive[32]; + snprintf(overlayActive, sizeof(overlayActive), "Act: %.2fms", avgActive); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.2f, 0.7f, 0.2f, 1.0f)); + ImGui::PlotHistogram("##Act", history.activeTime, HISTORY_SIZE, history.offset, overlayActive, 0.0f, max_val, ImVec2(graphWidth, 30)); + ImGui::PopStyleColor(); + + char overlayIdle[32]; + snprintf(overlayIdle, sizeof(overlayIdle), "Idl: %.2fms", avgIdle); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, ImVec4(0.5f, 0.5f, 0.5f, 0.5f)); + ImGui::PlotHistogram("##Idl", history.idleTime, HISTORY_SIZE, history.offset, overlayIdle, 0.0f, max_val, ImVec2(graphWidth, 20)); + ImGui::PopStyleColor(); + + ImGui::PopID(); + } + ImGui::EndTable(); + } + #else + ImGui::TextColored(ImVec4(1,0,0,1), "UF_THREAD_METRICS is disabled!"); + #endif + + ImGui::End(); + } + } threadMetrics; +} \ No newline at end of file diff --git a/engine/src/utils/string/ext.cpp b/engine/src/utils/string/ext.cpp index 094ac58b..cf51a006 100644 --- a/engine/src/utils/string/ext.cpp +++ b/engine/src/utils/string/ext.cpp @@ -81,6 +81,25 @@ uf::stl::vector uf::string::cStrings( const uf::stl::vector