From 76f5be2749ed17f02f0fcef402894340baece0ac Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 25 Jun 2026 22:10:52 +0200 Subject: [PATCH 01/16] Settings: minor struct packing. --- imgui.h | 2 +- imgui_internal.h | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/imgui.h b/imgui.h index d498355f4..4f659c38f 100644 --- a/imgui.h +++ b/imgui.h @@ -4035,7 +4035,7 @@ struct ImGuiPlatformIO // Optional: Platform locale // [Experimental] Configure decimal point e.g. '.' or ',' useful for some languages (e.g. German), generally pulled from *localeconv()->decimal_point - ImWchar Platform_LocaleDecimalPoint; // '.' + ImWchar Platform_LocaleDecimalPoint; // '.' //------------------------------------------------------------------ // Input - Interface with Renderer Backend diff --git a/imgui_internal.h b/imgui_internal.h index 36b205d3c..7f07a5439 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -2014,13 +2014,13 @@ struct ImGuiViewportP : public ImGuiViewport // (this is designed to be stored in a ImChunkStream buffer, with the variable-length Name following our structure) struct ImGuiWindowSettings { - ImGuiID ID; - ImVec2ih Pos; - ImVec2ih Size; - bool Collapsed; - bool IsChild; - bool WantApply; // Set when loaded from .ini data (to enable merging/loading .ini data into an already running context) - bool WantDelete; // Set to invalidate/delete the settings entry + ImGuiID ID; + ImVec2ih Pos; + ImVec2ih Size; + bool Collapsed : 1; + bool IsChild : 1; + bool WantApply : 1; // Set when loaded from .ini data (to enable merging/loading .ini data into an already running context) + bool WantDelete : 1; // Set to invalidate/delete the settings entry ImGuiWindowSettings() { memset((void*)this, 0, sizeof(*this)); } char* GetName() { return (char*)(this + 1); } @@ -3203,7 +3203,7 @@ struct ImGuiTableSettings float RefScale; // Reference scale to be able to rescale columns on font/dpi changes. ImGuiTableColumnIdx ColumnsCount; ImGuiTableColumnIdx ColumnsCountMax; // Maximum number of columns this settings instance can store, we can recycle a settings instance with lower number of columns but not higher - bool WantApply; // Set when loaded from .ini data (to enable merging/loading .ini data into an already running context) + bool WantApply : 1; // Set when loaded from .ini data (to enable merging/loading .ini data into an already running context) ImGuiTableSettings() { memset((void*)this, 0, sizeof(*this)); } ImGuiTableColumnSettings* GetColumnSettings() { return (ImGuiTableColumnSettings*)(this + 1); } From c66d9a8190eb5bd0e1cfeff9ebdb0b01747c9175 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 29 Jun 2026 11:49:06 +0200 Subject: [PATCH 02/16] Examples, SDL2/SDL3: use SDL_GetWindowSizeInPixels() to create framebuffers. (#8761, #9124) + cc #7433 --- docs/CHANGELOG.txt | 4 +++- examples/example_sdl2_vulkan/main.cpp | 4 ++-- examples/example_sdl3_vulkan/main.cpp | 4 ++-- examples/example_sdl3_wgpu/main.cpp | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index c90713b14..12c359693 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -127,7 +127,9 @@ Other Changes: - Uses `SetProcessDpiAwarenessContext()` instead of `SetThreadDpiAwarenessContext()` when available, fixing OpenGL DPI scaling issues as e.g. NVIDIA drivers tends to spawn multiple-thread to manage OpenGL. (#9403) - +- Examples: + - SDL2/SDL3: use `SDL_GetWindowSizeInPixels()` to create frame-buffers. Fixes issues + with non-fractional framebuffer size on Wayland. (#8761, #9124) [@billtran1632001] ----------------------------------------------------------------------- VERSION 1.92.8 (Released 2026-05-12) diff --git a/examples/example_sdl2_vulkan/main.cpp b/examples/example_sdl2_vulkan/main.cpp index 131712927..45a5a6de5 100644 --- a/examples/example_sdl2_vulkan/main.cpp +++ b/examples/example_sdl2_vulkan/main.cpp @@ -386,7 +386,7 @@ int main(int, char**) // Create Framebuffers int w, h; - SDL_GetWindowSize(window, &w, &h); + SDL_GetWindowSizeInPixels(window, &w, &h); ImGui_ImplVulkanH_Window* wd = &g_MainWindowData; SetupVulkanWindow(wd, surface, w, h); @@ -475,7 +475,7 @@ int main(int, char**) // Resize swap chain? int fb_width, fb_height; - SDL_GetWindowSize(window, &fb_width, &fb_height); + SDL_GetWindowSizeInPixels(window, &fb_width, &fb_height); if (fb_width > 0 && fb_height > 0 && (g_SwapChainRebuild || g_MainWindowData.Width != fb_width || g_MainWindowData.Height != fb_height)) { ImGui_ImplVulkan_SetMinImageCount(g_MinImageCount); diff --git a/examples/example_sdl3_vulkan/main.cpp b/examples/example_sdl3_vulkan/main.cpp index 04d17ba70..0073b37c5 100644 --- a/examples/example_sdl3_vulkan/main.cpp +++ b/examples/example_sdl3_vulkan/main.cpp @@ -383,7 +383,7 @@ int main(int, char**) // Create Framebuffers int w, h; - SDL_GetWindowSize(window, &w, &h); + SDL_GetWindowSizeInPixels(window, &w, &h); ImGui_ImplVulkanH_Window* wd = &g_MainWindowData; SetupVulkanWindow(wd, surface, w, h); SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); @@ -477,7 +477,7 @@ int main(int, char**) // Resize swap chain? int fb_width, fb_height; - SDL_GetWindowSize(window, &fb_width, &fb_height); + SDL_GetWindowSizeInPixels(window, &fb_width, &fb_height); if (fb_width > 0 && fb_height > 0 && (g_SwapChainRebuild || g_MainWindowData.Width != fb_width || g_MainWindowData.Height != fb_height)) { ImGui_ImplVulkan_SetMinImageCount(g_MinImageCount); diff --git a/examples/example_sdl3_wgpu/main.cpp b/examples/example_sdl3_wgpu/main.cpp index 4aee4ef6a..bc1787829 100644 --- a/examples/example_sdl3_wgpu/main.cpp +++ b/examples/example_sdl3_wgpu/main.cpp @@ -155,7 +155,7 @@ int main(int, char**) // [If using SDL_MAIN_USE_CALLBACKS: all code below would likely be your SDL_AppIterate() function] // React to changes in screen size int width, height; - SDL_GetWindowSize(window, &width, &height); + SDL_GetWindowSizeInPixels(window, &width, &height); if (width != wgpu_surface_width || height != wgpu_surface_height) ResizeSurface(width, height); From 9e6c0416fe1bc159b4fe029415a047d910cfb918 Mon Sep 17 00:00:00 2001 From: ocornut Date: Mon, 29 Jun 2026 18:26:59 +0200 Subject: [PATCH 03/16] Backends: GLFW,SDL2,SDL3: added wayland/driver marker in io.BackendPlatformName. + Stored bd->IsWayland variable for e.g. #7433 --- backends/imgui_impl_glfw.cpp | 16 ++++++++-------- backends/imgui_impl_sdl2.cpp | 12 +++++++----- backends/imgui_impl_sdl3.cpp | 13 ++++++++----- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/backends/imgui_impl_glfw.cpp b/backends/imgui_impl_glfw.cpp index 6af9f5d10..678129a07 100644 --- a/backends/imgui_impl_glfw.cpp +++ b/backends/imgui_impl_glfw.cpp @@ -213,7 +213,7 @@ struct ImGui_ImplGlfw_Data bool IsWayland; bool InstalledCallbacks; bool CallbacksChainForAllWindows; - char BackendPlatformName[32]; + char BackendPlatformName[40]; #ifdef EMSCRIPTEN_USE_EMBEDDED_GLFW3 const char* CanvasSelector; #endif @@ -676,7 +676,13 @@ static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, Glfw // Setup backend capabilities flags ImGui_ImplGlfw_Data* bd = IM_NEW(ImGui_ImplGlfw_Data)(); - snprintf(bd->BackendPlatformName, sizeof(bd->BackendPlatformName), "imgui_impl_glfw (%d)", GLFW_VERSION_COMBINED); + bd->Context = ImGui::GetCurrentContext(); + bd->Window = window; + bd->Time = 0.0; + bd->IsWayland = ImGui_ImplGlfw_IsWayland(); + ImGui_ImplGlfw_ContextMap_Add(window, bd->Context); + + snprintf(bd->BackendPlatformName, sizeof(bd->BackendPlatformName), "imgui_impl_glfw (%d)%s", GLFW_VERSION_COMBINED, bd->IsWayland ? " (Wayland)" : ""); io.BackendPlatformUserData = (void*)bd; io.BackendPlatformName = bd->BackendPlatformName; #if GLFW_HAS_CREATECURSOR @@ -684,12 +690,6 @@ static bool ImGui_ImplGlfw_Init(GLFWwindow* window, bool install_callbacks, Glfw #endif io.BackendFlags |= ImGuiBackendFlags_HasSetMousePos; // We can honor io.WantSetMousePos requests (optional, rarely used) - bd->Context = ImGui::GetCurrentContext(); - bd->Window = window; - bd->Time = 0.0; - bd->IsWayland = ImGui_ImplGlfw_IsWayland(); - ImGui_ImplGlfw_ContextMap_Add(window, bd->Context); - ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); #if GLFW_VERSION_COMBINED < 3300 platform_io.Platform_SetClipboardTextFn = [](ImGuiContext*, const char* text) { glfwSetClipboardString(ImGui_ImplGlfw_GetBackendData()->Window, text); }; diff --git a/backends/imgui_impl_sdl2.cpp b/backends/imgui_impl_sdl2.cpp index 3f4a6ace1..4def68dd6 100644 --- a/backends/imgui_impl_sdl2.cpp +++ b/backends/imgui_impl_sdl2.cpp @@ -147,7 +147,8 @@ struct ImGui_ImplSDL2_Data SDL_Renderer* Renderer; Uint64 Time; char* ClipboardTextData; - char BackendPlatformName[48]; + char BackendPlatformName[64]; + bool IsWayland; // Mouse handling Uint32 MouseWindowID; @@ -499,11 +500,12 @@ static bool ImGui_ImplSDL2_Init(SDL_Window* window, SDL_Renderer* renderer, void SDL_version ver_runtime; SDL_VERSION(&ver_compiled); SDL_GetVersion(&ver_runtime); + const char* sdl_video_driver = SDL_GetCurrentVideoDriver(); // Setup backend capabilities flags ImGui_ImplSDL2_Data* bd = IM_NEW(ImGui_ImplSDL2_Data)(); - snprintf(bd->BackendPlatformName, sizeof(bd->BackendPlatformName), "imgui_impl_sdl2 (%u.%u.%u, %u.%u.%u)", - ver_compiled.major, ver_compiled.minor, ver_compiled.patch, ver_runtime.major, ver_runtime.minor, ver_runtime.patch); + snprintf(bd->BackendPlatformName, sizeof(bd->BackendPlatformName), "imgui_impl_sdl2 (%u.%u.%u, %u.%u.%u) (%s)", + ver_compiled.major, ver_compiled.minor, ver_compiled.patch, ver_runtime.major, ver_runtime.minor, ver_runtime.patch, sdl_video_driver); io.BackendPlatformUserData = (void*)bd; io.BackendPlatformName = bd->BackendPlatformName; io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values (optional) @@ -512,16 +514,16 @@ static bool ImGui_ImplSDL2_Init(SDL_Window* window, SDL_Renderer* renderer, void bd->Window = window; bd->WindowID = SDL_GetWindowID(window); bd->Renderer = renderer; + bd->IsWayland = strcmp(sdl_video_driver, "Wayland") == 0; // Check and store if we are on a SDL backend that supports SDL_GetGlobalMouseState() and SDL_CaptureMouse() // ("wayland" and "rpi" don't support it, but we chose to use a white-list instead of a black-list) bd->MouseCanUseGlobalState = false; bd->MouseCaptureMode = ImGui_ImplSDL2_MouseCaptureMode_Disabled; #if SDL_HAS_CAPTURE_AND_GLOBAL_MOUSE - const char* sdl_backend = SDL_GetCurrentVideoDriver(); const char* capture_and_global_state_whitelist[] = { "windows", "cocoa", "x11", "DIVE", "VMAN" }; for (const char* item : capture_and_global_state_whitelist) - if (strncmp(sdl_backend, item, strlen(item)) == 0) + if (strncmp(sdl_video_driver, item, strlen(item)) == 0) { bd->MouseCanUseGlobalState = true; bd->MouseCaptureMode = (strcmp(item, "x11") == 0) ? ImGui_ImplSDL2_MouseCaptureMode_EnabledAfterDrag : ImGui_ImplSDL2_MouseCaptureMode_Enabled; diff --git a/backends/imgui_impl_sdl3.cpp b/backends/imgui_impl_sdl3.cpp index bab2d2312..05f6498f1 100644 --- a/backends/imgui_impl_sdl3.cpp +++ b/backends/imgui_impl_sdl3.cpp @@ -111,7 +111,8 @@ struct ImGui_ImplSDL3_Data SDL_Renderer* Renderer; Uint64 Time; char* ClipboardTextData; - char BackendPlatformName[48]; + char BackendPlatformName[64]; + bool IsWayland; // IME handling SDL_Window* ImeWindow; @@ -502,11 +503,13 @@ static bool ImGui_ImplSDL3_Init(SDL_Window* window, SDL_Renderer* renderer, void //SDL_SetHint(SDL_HINT_EVENT_LOGGING, "2"); const int ver_linked = SDL_GetVersion(); + const char* sdl_video_driver = SDL_GetCurrentVideoDriver(); // Setup backend capabilities flags ImGui_ImplSDL3_Data* bd = IM_NEW(ImGui_ImplSDL3_Data)(); - snprintf(bd->BackendPlatformName, sizeof(bd->BackendPlatformName), "imgui_impl_sdl3 (%d.%d.%d; %d.%d.%d)", - SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_MICRO_VERSION, SDL_VERSIONNUM_MAJOR(ver_linked), SDL_VERSIONNUM_MINOR(ver_linked), SDL_VERSIONNUM_MICRO(ver_linked)); + snprintf(bd->BackendPlatformName, sizeof(bd->BackendPlatformName), "imgui_impl_sdl3 (%d.%d.%d; %d.%d.%d) (%s)", + SDL_MAJOR_VERSION, SDL_MINOR_VERSION, SDL_MICRO_VERSION, SDL_VERSIONNUM_MAJOR(ver_linked), SDL_VERSIONNUM_MINOR(ver_linked), SDL_VERSIONNUM_MICRO(ver_linked), + sdl_video_driver); io.BackendPlatformUserData = (void*)bd; io.BackendPlatformName = bd->BackendPlatformName; io.BackendFlags |= ImGuiBackendFlags_HasMouseCursors; // We can honor GetMouseCursor() values (optional) @@ -515,16 +518,16 @@ static bool ImGui_ImplSDL3_Init(SDL_Window* window, SDL_Renderer* renderer, void bd->Window = window; bd->WindowID = SDL_GetWindowID(window); bd->Renderer = renderer; + bd->IsWayland = strcmp(sdl_video_driver, "wayland") == 0; // Check and store if we are on a SDL backend that supports SDL_GetGlobalMouseState() and SDL_CaptureMouse() // ("wayland" and "rpi" don't support it, but we chose to use a white-list instead of a black-list) bd->MouseCanUseGlobalState = false; bd->MouseCaptureMode = ImGui_ImplSDL3_MouseCaptureMode_Disabled; #if SDL_HAS_CAPTURE_AND_GLOBAL_MOUSE - const char* sdl_backend = SDL_GetCurrentVideoDriver(); const char* capture_and_global_state_whitelist[] = { "windows", "cocoa", "x11", "DIVE", "VMAN" }; for (const char* item : capture_and_global_state_whitelist) - if (strncmp(sdl_backend, item, strlen(item)) == 0) + if (strncmp(sdl_video_driver, item, strlen(item)) == 0) { bd->MouseCanUseGlobalState = true; bd->MouseCaptureMode = (strcmp(item, "x11") == 0) ? ImGui_ImplSDL3_MouseCaptureMode_EnabledAfterDrag : ImGui_ImplSDL3_MouseCaptureMode_Enabled; From e408733eccd89b7a2c0f0013a202cc48e199465b Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 1 Jul 2026 12:38:00 +0200 Subject: [PATCH 04/16] BeginMenu(), MenuItem() use NextItemData.Flags + Fixed typo in comment. (#9456) --- imgui.cpp | 2 +- imgui_widgets.cpp | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/imgui.cpp b/imgui.cpp index f6fb61dcd..addbe5e9a 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -1519,7 +1519,7 @@ ImGuiStyle::ImGuiStyle() GrabRounding = 0.0f; // Radius of grabs corners rounding. Set to 0.0f to have rectangular slider grabs. LogSliderDeadzone = 4.0f; // The size in pixels of the dead-zone around zero on logarithmic sliders that cross zero. ImageRounding = 0.0f; // Rounding of Image() calls. - ImageBorderSize = 0.0f; // Thickness of border around tabs. + ImageBorderSize = 0.0f; // Thickness of border around Image() calls. TabRounding = 5.0f; // Radius of upper corners of a tab. Set to 0.0f to have rectangular tabs. TabBorderSize = 0.0f; // Thickness of border around tabs. TabMinWidthBase = 1.0f; // Minimum tab width, to make tabs larger than their contents. TabBar buttons are not affected. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index c8458eba1..7b770b433 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -9382,7 +9382,7 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) // This is only done for items for the menu set and not the full parent window. const bool menuset_is_open = IsRootOfOpenMenuSet(); if (menuset_is_open) - PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true); + g.NextItemData.ItemFlags |= ImGuiItemFlags_NoWindowHoverableCheck; // The reference position stored in popup_pos will be used by Begin() to find a suitable position for the child menu, // However the final position is going to be different! It is chosen by FindBestWindowPosForPopup(). @@ -9445,8 +9445,6 @@ bool ImGui::BeginMenuEx(const char* label, const char* icon, bool enabled) } const bool hovered = (g.HoveredId == id) && enabled && !g.NavHighlightItemUnderNav; - if (menuset_is_open) - PopItemFlag(); bool want_open = false; bool want_open_nav_init = false; @@ -9609,7 +9607,7 @@ bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut // See BeginMenuEx() for comments about this. const bool menuset_is_open = IsRootOfOpenMenuSet(); if (menuset_is_open) - PushItemFlag(ImGuiItemFlags_NoWindowHoverableCheck, true); + g.NextItemData.ItemFlags |= ImGuiItemFlags_NoWindowHoverableCheck; // We've been using the equivalent of ImGuiSelectableFlags_SetNavIdOnHover on all Selectable() since early Nav system days (commit 43ee5d73), // but I am unsure whether this should be kept at all. For now moved it to be an opt-in feature used by menus only. @@ -9680,8 +9678,6 @@ bool ImGui::MenuItemEx(const char* label, const char* icon, const char* shortcut if (!enabled) EndDisabled(); PopID(); - if (menuset_is_open) - PopItemFlag(); return pressed; } From 0edad6d097bed1706515faf2ba4762a70fcffec8 Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 1 Jul 2026 14:42:23 +0200 Subject: [PATCH 05/16] Internals: track WasSelected/WasSoleSelected data for ActiveId. (#8337) --- imgui.cpp | 7 +++++++ imgui_internal.h | 5 +++++ imgui_widgets.cpp | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/imgui.cpp b/imgui.cpp index addbe5e9a..b237f956f 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -4234,6 +4234,7 @@ ImGuiContext::ImGuiContext(ImFontAtlas* shared_font_atlas) ActiveIdIsAlive = 0; ActiveIdTimer = 0.0f; ActiveIdIsJustActivated = false; + ActiveIdWasSelected = ActiveIdWasSoleSelected = false; ActiveIdAllowOverlap = false; ActiveIdNoClearOnFocusLoss = false; ActiveIdHasBeenPressedBefore = false; @@ -4250,6 +4251,7 @@ ImGuiContext::ImGuiContext(ImFontAtlas* shared_font_atlas) memset(&ActiveIdValueOnActivation, 0, sizeof(ActiveIdValueOnActivation)); LastActiveId = 0; LastActiveIdTimer = 0.0f; + LastActiveIdWasSelected = LastActiveIdWasSoleSelected = false; LastKeyboardKeyPressTime = LastKeyModsChangeTime = LastKeyModsChangeFromNoneTime = -1.0; @@ -5629,6 +5631,11 @@ void ImGui::NewFrame() // Update ActiveId data (clear reference to active widget if the widget isn't alive anymore) if (g.ActiveId) g.ActiveIdTimer += g.IO.DeltaTime; + if (g.ActiveId && g.ActiveId == g.LastActiveId) + { + g.LastActiveIdWasSelected = g.ActiveIdWasSelected; + g.LastActiveIdWasSoleSelected = g.ActiveIdWasSoleSelected; + } g.LastActiveIdTimer += g.IO.DeltaTime; g.ActiveIdPreviousFrame = g.ActiveId; g.ActiveIdIsAlive = 0; diff --git a/imgui_internal.h b/imgui_internal.h index 7f07a5439..299287e65 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -1940,6 +1940,7 @@ struct IMGUI_API ImGuiMultiSelectTempData bool NavIdPassedBy; bool RangeSrcPassedBy; // Set by the item that matches RangeSrcItem. bool RangeDstPassedBy; // Set by the item that matches NavJustMovedToId when IsSetRange is set. + bool IsSoleOrUnknownSelectionSize; ImGuiMultiSelectTempData() { Clear(); } void Clear() { size_t io_sz = sizeof(IO); ClearIO(); memset((void*)(&IO + 1), 0, sizeof(*this) - io_sz); } // Zero-clear except IO as we preserve IO.Requests[] buffer allocation. @@ -2281,6 +2282,8 @@ struct ImGuiContext ImGuiID ActiveIdIsAlive; // Active widget has been seen this frame (we can't use a bool as the ActiveId may change within the frame) float ActiveIdTimer; bool ActiveIdIsJustActivated; // Set at the time of activation for one frame + bool ActiveIdWasSelected; // Active ID was selected at the time of activating + bool ActiveIdWasSoleSelected; // Active ID was sole selection at the time of activating bool ActiveIdAllowOverlap; // Active widget allows another widget to steal active id (generally for overlapping widgets, but not always) bool ActiveIdNoClearOnFocusLoss; // Disable losing active id if the active id window gets unfocused. bool ActiveIdHasBeenPressedBefore; // Track whether the active id led to a press (this is to allow changing between PressOnClick and PressOnRelease without pressing twice). Used by range_select branch. @@ -2297,6 +2300,8 @@ struct ImGuiContext ImGuiDataTypeStorage ActiveIdValueOnActivation; // Backup of initial value at the time of activation. ONLY SET BY SPECIFIC WIDGETS: DragXXX and SliderXXX. ImGuiID LastActiveId; // Store the last non-zero ActiveId, useful for animation. float LastActiveIdTimer; // Store the last non-zero ActiveId timer since the beginning of activation, useful for animation. + bool LastActiveIdWasSelected; + bool LastActiveIdWasSoleSelected; // Key/Input Ownership + Shortcut Routing system // - The idea is that instead of "eating" a given key, we can link to an owner. diff --git a/imgui_widgets.cpp b/imgui_widgets.cpp index 7b770b433..ace655d4d 100644 --- a/imgui_widgets.cpp +++ b/imgui_widgets.cpp @@ -7489,6 +7489,11 @@ bool ImGui::Selectable(const char* label, bool selected, ImGuiSelectableFlags fl if (selected != was_selected) g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_ToggledSelection; + if (g.ActiveId == id && g.ActiveIdIsJustActivated) + { + g.ActiveIdWasSelected = was_selected; + g.ActiveIdWasSoleSelected = was_selected && (!is_multi_select || g.CurrentMultiSelect->IsSoleOrUnknownSelectionSize); + } // Render if (is_visible) @@ -8116,6 +8121,7 @@ ImGuiMultiSelectIO* ImGui::BeginMultiSelect(ImGuiMultiSelectFlags flags, int sel storage->LastSelectionSize = 0; } ms->LoopRequestSetAll = request_select_all ? 1 : request_clear ? 0 : -1; + ms->IsSoleOrUnknownSelectionSize = (storage->LastSelectionSize == 1) || (storage->LastSelectionSize == -1); //ms->PrevSubmittedItem = ImGuiSelectionUserData_Invalid; if (g.DebugLogFlags & ImGuiDebugLogFlags_EventSelection) From 279d04f7a3e17c4349715a0e7fde455e65facf4a Mon Sep 17 00:00:00 2001 From: ocornut Date: Wed, 1 Jul 2026 18:25:21 +0200 Subject: [PATCH 06/16] Added GetItemClickedCountWithSingleClickDelay(), io.MouseSingleClickDelay. (#8337) --- docs/CHANGELOG.txt | 9 +++++++ imgui.cpp | 67 +++++++++++++++++++++++++++++++++++++++++++++- imgui.h | 8 +++--- 3 files changed, 80 insertions(+), 4 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 12c359693..54a7422bb 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -100,6 +100,15 @@ Other Changes: instead of _NavEnableGamepad. (#9454, #8803, #9270) [@Clownacy] - TreeNode: - Fixed nav cursor rendering with rounding even though tree nodes don't have it. (#7589) +- Inputs: + - Added GetItemClickedCountWithSingleClickDelay() helper for easy disambiguation + between single-click and double-click for actions that needs single-click to do + something other than selection. (#8337) + - Returns 1 on single-click but delayed by io.MouseSingleClickDelay. + - Returns 2 on double-click, and 2+ on subsequent repeated cicks. + - Added io.MouseSingleClickDelay to configure default delayed single click delay when + using GetItemClickedCountWithSingleClickDelay() or IsMouseReleasedWithDelay(). (#8337) + Note that io.MouseSingleClickDelay is always > io.MouseDoubleClickTime. - Style: - Added style.MenuItemRounding, ImGuiStyleVar_MenuItemRounding. (#7589, #9375, #9453) - Added style.SelectableRounding, ImGuiStyleVar_SelectableRounding. (#7589, #9375, #9453) diff --git a/imgui.cpp b/imgui.cpp index b237f956f..a7d03caa4 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -1692,6 +1692,7 @@ ImGuiIO::ImGuiIO() // Inputs Behaviors MouseDoubleClickTime = 0.30f; MouseDoubleClickMaxDist = 6.0f; + MouseSingleClickDelay = 0.50f; MouseDragThreshold = 6.0f; KeyRepeatDelay = 0.275f; KeyRepeatRate = 0.050f; @@ -9427,6 +9428,7 @@ IM_MSVC_RUNTIME_CHECKS_RESTORE // - IsMouseReleased() // - IsMouseDoubleClicked() // - GetMouseClickedCount() +// - GetItemClickedCountWithSingleClickDelay() // - IsMouseHoveringRect() [Internal] // - IsMouseDragPastThreshold() [Internal] // - IsMouseDragging() @@ -10006,6 +10008,8 @@ bool ImGui::IsMouseReleased(ImGuiMouseButton button, ImGuiID owner_id) return g.IO.MouseReleased[button] && TestKeyOwner(MouseButtonToKey(button), owner_id); // Should be same as IsKeyReleased(MouseButtonToKey(button), owner_id) } + +// Prefer higher-level helper GetItemClickedCountWithSingleClickDelay() // Use if you absolutely need to distinguish single-click from double-click by introducing a delay. // Generally use with 'delay >= io.MouseDoubleClickTime' + combined with a 'io.MouseClickedLastCount == 1' test. // This is a very rarely used UI idiom, but some apps use this: e.g. MS Explorer single click on an icon to rename. @@ -10013,8 +10017,12 @@ bool ImGui::IsMouseReleasedWithDelay(ImGuiMouseButton button, float delay) { ImGuiContext& g = *GImGui; IM_ASSERT(button >= 0 && button < IM_COUNTOF(g.IO.MouseDown)); + if (IsMouseDown(button)) + return false; + if (delay < 0.0f) + delay = g.IO.MouseSingleClickDelay; const float time_since_release = (float)(g.Time - g.IO.MouseReleasedTime[button]); - return !IsMouseDown(button) && (time_since_release - g.IO.DeltaTime < delay) && (time_since_release >= delay); + return (time_since_release - g.IO.DeltaTime < delay) && (time_since_release >= delay); } bool ImGui::IsMouseDoubleClicked(ImGuiMouseButton button) @@ -10038,6 +10046,62 @@ int ImGui::GetMouseClickedCount(ImGuiMouseButton button) return g.IO.MouseClickedCount[button]; } +// FIXME: This is close to what BeginDragDropSource() is doing, maybe rework. +static ImGuiID LastItemOverlayButtonForNullId(ImGuiMouseButton mouse_button) +{ + ImGuiContext& g = *GImGui; + IM_ASSERT(g.LastItemData.ID == 0); + ImGuiWindow* window = g.CurrentWindow; + ImGuiID id = window->GetIDFromRectangle(g.LastItemData.Rect); + if (g.IO.MouseClicked[mouse_button] && ImGui::ItemHoverable(g.LastItemData.Rect, id, g.LastItemData.ItemFlags)) + { + ImGui::SetActiveID(id, window); + ImGui::FocusWindow(window); + } + else if (g.ActiveId == id) + { + ImGui::KeepAliveID(id); + if (!g.IO.MouseDown[mouse_button]) + ImGui::ClearActiveID(); + } + return id; +} + +// [BETA] Building block for disambiguation between single-click and double-click. +// - Returns 1 on single-click but delayed by io.MouseSingleClickDelay (which is always > io.MouseDoubleClickTime) after mouse release. +// - Returns 2+ on double-click and subsequent repeated clicks. +// In order use that to replicate Windows Explorer's "click on label to rename after a delay", +// When the function returns 1 for a delayed single click, you can add with further tests: +// - If you want to test that the mouse position AT THE TIME of the click (before the delay): you can use the 'io.MouseClickedPos[mouse_button]' position. +// - If you want to test that the mouse position is still over the item at the end of delay: you can use '&& IsItemHovered()'. +// - If you want to test that the item was selected or the sole selection AT THE TIME of the click (before the delay): you can test for 'g.LastActiveIdWasSelected' or 'g.LastActiveIdWasSoleSelected'. +// e.g. +// int click_count = ImGui::GetItemClickedCountWithSingleClickDelay(mouse_button); +// if (click_count == 1 && ImGui::GetCurrentContext()->LastActiveIdWasSoleSelected) +// StartRename(); +// if (click_count == 2) +// Launch(); +int ImGui::GetItemClickedCountWithSingleClickDelay(ImGuiMouseButton mouse_button, float delay) +{ + // Action: double-click and subsequent clicks + ImGuiContext& g = *GImGui; + if (g.IO.MouseClickedCount[mouse_button] >= 2 && IsItemClicked(mouse_button)) + return g.IO.MouseClickedCount[mouse_button]; + + // Action: second click, delayed + ImGuiID id = g.LastItemData.ID; + if (id == 0) + id = LastItemOverlayButtonForNullId(mouse_button); + if (g.LastActiveId == id) + { + if (delay >= 0.0f) + delay = ImMax(delay, g.IO.MouseDoubleClickTime + 0.01f); + if (IsMouseReleasedWithDelay(mouse_button, delay) && g.IO.MouseClickedLastCount[mouse_button] == 1) + return 1; + } + return 0; +} + // Test if mouse cursor is hovering given rectangle // NB- Rectangle is clipped by our current clip setting // NB- Expand the rectangle to be generous on imprecise inputs systems (g.Style.TouchExtraPadding) @@ -10985,6 +11049,7 @@ static void ImGui::ErrorCheckNewFrameSanityChecks() IM_ASSERT(g.Style.WindowMenuButtonPosition == ImGuiDir_None || g.Style.WindowMenuButtonPosition == ImGuiDir_Left || g.Style.WindowMenuButtonPosition == ImGuiDir_Right); IM_ASSERT(g.Style.ColorButtonPosition == ImGuiDir_Left || g.Style.ColorButtonPosition == ImGuiDir_Right); IM_ASSERT(g.Style.TreeLinesFlags == ImGuiTreeNodeFlags_DrawLinesNone || g.Style.TreeLinesFlags == ImGuiTreeNodeFlags_DrawLinesFull || g.Style.TreeLinesFlags == ImGuiTreeNodeFlags_DrawLinesToNodes); + IM_ASSERT(g.IO.MouseSingleClickDelay > g.IO.MouseDoubleClickTime); // Error handling: we do not accept 100% silent recovery! Please contact me if you feel this is getting in your way. if (g.IO.ConfigErrorRecovery) diff --git a/imgui.h b/imgui.h index 4f659c38f..0d4995699 100644 --- a/imgui.h +++ b/imgui.h @@ -1026,6 +1026,7 @@ namespace ImGui IMGUI_API ImVec2 GetItemRectMax(); // get lower-right bounding rectangle of the last item (screen space) IMGUI_API ImVec2 GetItemRectSize(); // get size of last item IMGUI_API ImGuiItemFlags GetItemFlags(); // get generic flags of last item + IMGUI_API int GetItemClickedCountWithSingleClickDelay(ImGuiMouseButton mouse_button = 0, float delay = -1.0f); // [BETA] building block for disambiguation between single-click and double-click. Returns 1 on single-click but delayed by io.MouseSingleClickDelay after mouse release. Returns 2+ on double-click or repeated clicks. // Viewports // - Currently represents the Platform Window created by the application which is hosting our Dear ImGui windows. @@ -1108,7 +1109,7 @@ namespace ImGui IMGUI_API bool IsMouseClicked(ImGuiMouseButton button, bool repeat = false); // did mouse button clicked? (went from !Down to Down). Same as GetMouseClickedCount() == 1. IMGUI_API bool IsMouseReleased(ImGuiMouseButton button); // did mouse button released? (went from Down to !Down) IMGUI_API bool IsMouseDoubleClicked(ImGuiMouseButton button); // did mouse button double-clicked? Same as GetMouseClickedCount() == 2. (note that a double-click will also report IsMouseClicked() == true) - IMGUI_API bool IsMouseReleasedWithDelay(ImGuiMouseButton button, float delay); // delayed mouse release (use very sparingly!). Generally used with 'delay >= io.MouseDoubleClickTime' + combined with a 'io.MouseClickedLastCount==1' test. This is a very rarely used UI idiom, but some apps use this: e.g. MS Explorer single click on an icon to rename. + IMGUI_API bool IsMouseReleasedWithDelay(ImGuiMouseButton button, float delay=-1.f);// delayed mouse release. Use sparingly. Prefer higher-level helper GetItemClickedCountWithSingleClickDelay(). Generally used with 'delay >= io.MouseDoubleClickTime' + combined with a 'io.MouseClickedLastCount==1' test. IMGUI_API int GetMouseClickedCount(ImGuiMouseButton button); // return the number of successive mouse-clicks at the time where a click happen (otherwise 0). IMGUI_API bool IsMouseHoveringRect(const ImVec2& r_min, const ImVec2& r_max, bool clip = true);// is mouse hovering given bounding rect (in screen space). clipped by current clipping settings, but disregarding of other consideration of focus/window ordering/popup-block. IMGUI_API bool IsMousePosValid(const ImVec2* mouse_pos = NULL); // by convention we use (-FLT_MAX,-FLT_MAX) to denote that there is no mouse available @@ -2451,8 +2452,9 @@ struct ImGuiIO // Inputs Behaviors // (other variables, ones which are expected to be tweaked within UI code, are exposed in ImGuiStyle) - float MouseDoubleClickTime; // = 0.30f // Time for a double-click, in seconds. - float MouseDoubleClickMaxDist; // = 6.0f // Distance threshold to stay in to validate a double-click, in pixels. + float MouseDoubleClickTime; // = 0.30f // Time for consecutive clicks to account as a double-click, in seconds. + float MouseDoubleClickMaxDist; // = 6.0f // Distance threshold to stay in to validate a double-click or multiple clicks, in pixels. + float MouseSingleClickDelay; // = 0.60f // Time for a delayed click when using GetItemClickedCountWithSingleClickDelay() or IsMouseReleasedWithDelay(), in seconds. Must be > io.MouseDoubleClickTime. float MouseDragThreshold; // = 6.0f // Distance threshold before considering we are dragging. float KeyRepeatDelay; // = 0.275f // When holding a key/button, time before it starts repeating, in seconds (for buttons in Repeat mode, etc.). float KeyRepeatRate; // = 0.050f // When holding a key/button, rate at which it repeats, in seconds. From 93dd4c1567a6593a2056395010f77f0da4f72225 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 2 Jul 2026 11:57:31 +0200 Subject: [PATCH 07/16] Backends: Metal4: copy imgui_imp_metal.* to imgui_impl_metal4.*, unmodified. (#9458, #9451) --- backends/imgui_impl_metal4.h | 78 ++++ backends/imgui_impl_metal4.mm | 686 ++++++++++++++++++++++++++++++++++ 2 files changed, 764 insertions(+) create mode 100644 backends/imgui_impl_metal4.h create mode 100644 backends/imgui_impl_metal4.mm diff --git a/backends/imgui_impl_metal4.h b/backends/imgui_impl_metal4.h new file mode 100644 index 000000000..8af2c8b6b --- /dev/null +++ b/backends/imgui_impl_metal4.h @@ -0,0 +1,78 @@ +// dear imgui: Renderer Backend for Metal +// This needs to be used along with a Platform Backend (e.g. OSX) + +// Implemented features: +// [X] Renderer: User texture binding. Use 'MTLTexture' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef! +// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset). +// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures). + +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp + +#pragma once +#include "imgui.h" // IMGUI_IMPL_API +#ifndef IMGUI_DISABLE + +//----------------------------------------------------------------------------- +// ObjC API +//----------------------------------------------------------------------------- + +#ifdef __OBJC__ + +@class MTLRenderPassDescriptor; +@protocol MTLDevice, MTLCommandBuffer, MTLRenderCommandEncoder; + +// Follow "Getting Started" link and check examples/ folder to learn about using backends! +IMGUI_IMPL_API bool ImGui_ImplMetal_Init(id device); +IMGUI_IMPL_API void ImGui_ImplMetal_Shutdown(); +IMGUI_IMPL_API void ImGui_ImplMetal_NewFrame(MTLRenderPassDescriptor* renderPassDescriptor); +IMGUI_IMPL_API void ImGui_ImplMetal_RenderDrawData(ImDrawData* drawData, + id commandBuffer, + id commandEncoder); + +// Called by Init/NewFrame/Shutdown +IMGUI_IMPL_API bool ImGui_ImplMetal_CreateDeviceObjects(id device); +IMGUI_IMPL_API void ImGui_ImplMetal_DestroyDeviceObjects(); + +// (Advanced) Use e.g. if you need to precisely control the timing of texture updates (e.g. for staged rendering), by setting ImDrawData::Textures = nullptr to handle this manually. +IMGUI_IMPL_API void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex); + +#endif + +//----------------------------------------------------------------------------- +// C++ API +//----------------------------------------------------------------------------- + +// Enable Metal C++ binding support with '#define IMGUI_IMPL_METAL_CPP' in your imconfig.h file +// More info about using Metal from C++: https://developer.apple.com/metal/cpp/ + +#ifdef IMGUI_IMPL_METAL_CPP +#include +#ifndef __OBJC__ + +// Follow "Getting Started" link and check examples/ folder to learn about using backends! +IMGUI_IMPL_API bool ImGui_ImplMetal_Init(MTL::Device* device); +IMGUI_IMPL_API void ImGui_ImplMetal_Shutdown(); +IMGUI_IMPL_API void ImGui_ImplMetal_NewFrame(MTL::RenderPassDescriptor* renderPassDescriptor); +IMGUI_IMPL_API void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data, + MTL::CommandBuffer* commandBuffer, + MTL::RenderCommandEncoder* commandEncoder); + +// Called by Init/NewFrame/Shutdown +IMGUI_IMPL_API bool ImGui_ImplMetal_CreateDeviceObjects(MTL::Device* device); +IMGUI_IMPL_API void ImGui_ImplMetal_DestroyDeviceObjects(); + +// (Advanced) Use e.g. if you need to precisely control the timing of texture updates (e.g. for staged rendering), by setting ImDrawData::Textures = nullptr to handle this manually. +IMGUI_IMPL_API void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex); + +#endif +#endif + +//----------------------------------------------------------------------------- + +#endif // #ifndef IMGUI_DISABLE diff --git a/backends/imgui_impl_metal4.mm b/backends/imgui_impl_metal4.mm new file mode 100644 index 000000000..652668c78 --- /dev/null +++ b/backends/imgui_impl_metal4.mm @@ -0,0 +1,686 @@ +// dear imgui: Renderer Backend for Metal +// This needs to be used along with a Platform Backend (e.g. OSX) + +// Implemented features: +// [X] Renderer: User texture binding. Use 'MTLTexture' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef! +// [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset). +// [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures). + +// You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. +// Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp + +// CHANGELOG +// (minor and older changes stripped away, please see git history for details) +// 2026-04-28: Added support for standard draw callbacks (in platform_io): DrawCallback_SetSamplerLinear and DrawCallback_SetSamplerNearest. (#9378, #9381) +// 2026-04-23: Added support for standard draw callbacks (in platform_io): DrawCallback_ResetRenderState (others are not yet supported). (#9378) +// 2026-04-14: Metal: use a dedicated bufferCacheLock to avoid crashing when bufferCache is replaced by a new object while being used for @synchronize(). (#9367) +// 2026-04-03: Metal: avoid redundant vertex buffer bind in SetupRenderState. (#9343) +// 2026-03-19: Fixed issue in ImGui_ImplMetal_RenderDrawData() if ImTextureID_Invalid is defined to be != 0, which became the default since 2026-03-12. (#9295, #9310) +// 2025-09-18: Call platform_io.ClearRendererHandlers() on shutdown. +// 2025-06-11: Added support for ImGuiBackendFlags_RendererHasTextures, for dynamic font atlas. Removed ImGui_ImplMetal_CreateFontsTexture() and ImGui_ImplMetal_DestroyFontsTexture(). +// 2025-02-03: Metal: Crash fix. (#8367) +// 2025-01-08: Metal: Fixed memory leaks when using metal-cpp (#8276, #8166) or when using multiple contexts (#7419). +// 2022-08-23: Metal: Update deprecated property 'sampleCount'->'rasterSampleCount'. +// 2022-07-05: Metal: Add dispatch synchronization. +// 2022-06-30: Metal: Use __bridge for ARC based systems. +// 2022-06-01: Metal: Fixed null dereference on exit inside command buffer completion handler. +// 2022-04-27: Misc: Store backend data in a per-context struct, allowing to use this backend with multiple contexts. +// 2022-01-03: Metal: Ignore ImDrawCmd where ElemCount == 0 (very rare but can technically be manufactured by user code). +// 2021-12-30: Metal: Added Metal C++ support. Enable with '#define IMGUI_IMPL_METAL_CPP' in your imconfig.h file. +// 2021-08-24: Metal: Fixed a crash when clipping rect larger than framebuffer is submitted. (#4464) +// 2021-05-19: Metal: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement) +// 2021-02-18: Metal: Change blending equation to preserve alpha in output buffer. +// 2021-01-25: Metal: Fixed texture storage mode when building on Mac Catalyst. +// 2019-05-29: Metal: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag. +// 2019-04-30: Metal: Added support for special ImDrawCallback_ResetRenderState callback to reset render state. +// 2019-02-11: Metal: Projecting clipping rectangles correctly using draw_data->FramebufferScale to allow multi-viewports for retina display. +// 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window. +// 2018-07-05: Metal: Added new Metal backend implementation. + +#include "imgui.h" +#ifndef IMGUI_DISABLE +#include "imgui_impl_metal.h" +#import +#import + +#pragma mark - Support classes + +// A wrapper around a MTLBuffer object that knows the last time it was reused +@interface MetalBuffer : NSObject +@property (nonatomic, strong) id buffer; +@property (nonatomic, assign) double lastReuseTime; +- (instancetype)initWithBuffer:(id)buffer; +@end + +// An object that encapsulates the data necessary to uniquely identify a +// render pipeline state. These are used as cache keys. +@interface FramebufferDescriptor : NSObject +@property (nonatomic, assign) unsigned long sampleCount; +@property (nonatomic, assign) MTLPixelFormat colorPixelFormat; +@property (nonatomic, assign) MTLPixelFormat depthPixelFormat; +@property (nonatomic, assign) MTLPixelFormat stencilPixelFormat; +- (instancetype)initWithRenderPassDescriptor:(MTLRenderPassDescriptor*)renderPassDescriptor; +@end + +@interface MetalTexture : NSObject +@property (nonatomic, strong) id metalTexture; +- (instancetype)initWithTexture:(id)metalTexture; +@end + +// A singleton that stores long-lived objects that are needed by the Metal +// renderer backend. Stores the render pipeline state cache and the default +// font texture, and manages the reusable buffer cache. +@interface MetalContext : NSObject +@property (nonatomic, strong) id device; +@property (nonatomic, strong) id depthStencilState; +@property (nonatomic, strong) id samplerStateLinear; +@property (nonatomic, strong) id samplerStateNearest; +@property (nonatomic, strong) FramebufferDescriptor* framebufferDescriptor; // framebuffer descriptor for current frame; transient +@property (nonatomic, strong) NSMutableDictionary* renderPipelineStateCache; // pipeline cache; keyed on framebuffer descriptors +@property (nonatomic, strong) NSMutableArray* bufferCache; +@property (nonatomic, strong) NSObject* bufferCacheLock; +@property (nonatomic, assign) double lastBufferCachePurge; +- (MetalBuffer*)dequeueReusableBufferOfLength:(NSUInteger)length device:(id)device; +- (id)renderPipelineStateForFramebufferDescriptor:(FramebufferDescriptor*)descriptor device:(id)device; +@end + +struct ImGui_ImplMetal_Data +{ + MetalContext* SharedMetalContext; + id RenderCommandEncoder; + + ImGui_ImplMetal_Data() { memset((void*)this, 0, sizeof(*this)); } +}; + +static ImGui_ImplMetal_Data* ImGui_ImplMetal_GetBackendData() { return ImGui::GetCurrentContext() ? (ImGui_ImplMetal_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; } +static void ImGui_ImplMetal_DestroyBackendData(){ IM_DELETE(ImGui_ImplMetal_GetBackendData()); } + +static inline CFTimeInterval GetMachAbsoluteTimeInSeconds() { return (CFTimeInterval)(double)(clock_gettime_nsec_np(CLOCK_UPTIME_RAW) / 1e9); } + +#ifdef IMGUI_IMPL_METAL_CPP + +#pragma mark - Dear ImGui Metal C++ Backend API + +bool ImGui_ImplMetal_Init(MTL::Device* device) +{ + return ImGui_ImplMetal_Init((__bridge id)(device)); +} + +void ImGui_ImplMetal_NewFrame(MTL::RenderPassDescriptor* renderPassDescriptor) +{ + ImGui_ImplMetal_NewFrame((__bridge MTLRenderPassDescriptor*)(renderPassDescriptor)); +} + +void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data, + MTL::CommandBuffer* commandBuffer, + MTL::RenderCommandEncoder* commandEncoder) +{ + ImGui_ImplMetal_RenderDrawData(draw_data, + (__bridge id)(commandBuffer), + (__bridge id)(commandEncoder)); + +} + +bool ImGui_ImplMetal_CreateDeviceObjects(MTL::Device* device) +{ + return ImGui_ImplMetal_CreateDeviceObjects((__bridge id)(device)); +} + +#endif // #ifdef IMGUI_IMPL_METAL_CPP + +#pragma mark - Dear ImGui Metal Backend API + +void ImGui_ImplMetal_NewFrame(MTLRenderPassDescriptor* renderPassDescriptor) +{ + ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); + IM_ASSERT(bd != nil && "Context or backend not initialized! Did you call ImGui_ImplMetal_Init()?"); +#ifdef IMGUI_IMPL_METAL_CPP + bd->SharedMetalContext.framebufferDescriptor = [[[FramebufferDescriptor alloc] initWithRenderPassDescriptor:renderPassDescriptor]autorelease]; +#else + bd->SharedMetalContext.framebufferDescriptor = [[FramebufferDescriptor alloc] initWithRenderPassDescriptor:renderPassDescriptor]; +#endif + if (bd->SharedMetalContext.depthStencilState == nil) + ImGui_ImplMetal_CreateDeviceObjects(bd->SharedMetalContext.device); +} + +static void ImGui_ImplMetal_SetupRenderState(ImDrawData* draw_data, id commandBuffer, + id commandEncoder, id renderPipelineState, + MetalBuffer* vertexBuffer, size_t vertexBufferOffset) +{ + IM_UNUSED(commandBuffer); + ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); + [commandEncoder setCullMode:MTLCullModeNone]; + [commandEncoder setDepthStencilState:bd->SharedMetalContext.depthStencilState]; + + // Setup viewport, orthographic projection matrix + // Our visible imgui space lies from draw_data->DisplayPos (top left) to + // draw_data->DisplayPos+data_data->DisplaySize (bottom right). DisplayMin is typically (0,0) for single viewport apps. + MTLViewport viewport = + { + .originX = 0.0, + .originY = 0.0, + .width = (double)(draw_data->DisplaySize.x * draw_data->FramebufferScale.x), + .height = (double)(draw_data->DisplaySize.y * draw_data->FramebufferScale.y), + .znear = 0.0, + .zfar = 1.0 + }; + [commandEncoder setViewport:viewport]; + + float L = draw_data->DisplayPos.x; + float R = draw_data->DisplayPos.x + draw_data->DisplaySize.x; + float T = draw_data->DisplayPos.y; + float B = draw_data->DisplayPos.y + draw_data->DisplaySize.y; + float N = (float)viewport.znear; + float F = (float)viewport.zfar; + const float ortho_projection[4][4] = + { + { 2.0f/(R-L), 0.0f, 0.0f, 0.0f }, + { 0.0f, 2.0f/(T-B), 0.0f, 0.0f }, + { 0.0f, 0.0f, 1/(F-N), 0.0f }, + { (R+L)/(L-R), (T+B)/(B-T), N/(F-N), 1.0f }, + }; + [commandEncoder setVertexBytes:&ortho_projection length:sizeof(ortho_projection) atIndex:1]; + + [commandEncoder setRenderPipelineState:renderPipelineState]; + [commandEncoder setFragmentSamplerState:bd->SharedMetalContext.samplerStateLinear atIndex:0]; + + [commandEncoder setVertexBuffer:vertexBuffer.buffer offset:vertexBufferOffset atIndex:0]; +} + +// Draw callbacks +static void ImGui_ImplMetal_DrawCallback_ResetRenderState(const ImDrawList*, const ImDrawCmd*) {} // Intentionally empty. Used as an identifier for rendering loop to call its code. Simpler to implement this way. +static void ImGui_ImplMetal_DrawCallback_SetSamplerLinear(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); [bd->RenderCommandEncoder setFragmentSamplerState:bd->SharedMetalContext.samplerStateLinear atIndex:0]; } +static void ImGui_ImplMetal_DrawCallback_SetSamplerNearest(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); [bd->RenderCommandEncoder setFragmentSamplerState:bd->SharedMetalContext.samplerStateNearest atIndex:0]; } + +// Metal Render function. +void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data, id commandBuffer, id commandEncoder) +{ + ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); + MetalContext* ctx = bd->SharedMetalContext; + + // Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates) + int fb_width = (int)(draw_data->DisplaySize.x * draw_data->FramebufferScale.x); + int fb_height = (int)(draw_data->DisplaySize.y * draw_data->FramebufferScale.y); + if (fb_width <= 0 || fb_height <= 0 || draw_data->CmdLists.Size == 0) + return; + + // Catch up with texture updates. Most of the times, the list will have 1 element with an OK status, aka nothing to do. + // (This almost always points to ImGui::GetPlatformIO().Textures[] but is part of ImDrawData to allow overriding or disabling texture updates). + if (draw_data->Textures != nullptr) + for (ImTextureData* tex : *draw_data->Textures) + if (tex->Status != ImTextureStatus_OK) + ImGui_ImplMetal_UpdateTexture(tex); + + // Try to retrieve a render pipeline state that is compatible with the framebuffer config for this frame + // The hit rate for this cache should be very near 100%. + id renderPipelineState = ctx.renderPipelineStateCache[ctx.framebufferDescriptor]; + if (renderPipelineState == nil) + { + // No luck; make a new render pipeline state + renderPipelineState = [ctx renderPipelineStateForFramebufferDescriptor:ctx.framebufferDescriptor device:commandBuffer.device]; + + // Cache render pipeline state for later reuse + ctx.renderPipelineStateCache[ctx.framebufferDescriptor] = renderPipelineState; + } + + size_t vertexBufferLength = (size_t)draw_data->TotalVtxCount * sizeof(ImDrawVert); + size_t indexBufferLength = (size_t)draw_data->TotalIdxCount * sizeof(ImDrawIdx); + MetalBuffer* vertexBuffer = [ctx dequeueReusableBufferOfLength:vertexBufferLength device:commandBuffer.device]; + MetalBuffer* indexBuffer = [ctx dequeueReusableBufferOfLength:indexBufferLength device:commandBuffer.device]; + + bd->RenderCommandEncoder = commandEncoder; + ImGui_ImplMetal_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, 0); + + // Will project scissor/clipping rectangles into framebuffer space + ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports + ImVec2 clip_scale = draw_data->FramebufferScale; // (1,1) unless using retina display which are often (2,2) + + // Render command lists + size_t vertexBufferOffset = 0; + size_t indexBufferOffset = 0; + for (const ImDrawList* draw_list : draw_data->CmdLists) + { + memcpy((char*)vertexBuffer.buffer.contents + vertexBufferOffset, draw_list->VtxBuffer.Data, (size_t)draw_list->VtxBuffer.Size * sizeof(ImDrawVert)); + memcpy((char*)indexBuffer.buffer.contents + indexBufferOffset, draw_list->IdxBuffer.Data, (size_t)draw_list->IdxBuffer.Size * sizeof(ImDrawIdx)); + + for (int cmd_i = 0; cmd_i < draw_list->CmdBuffer.Size; cmd_i++) + { + const ImDrawCmd* pcmd = &draw_list->CmdBuffer[cmd_i]; + if (pcmd->UserCallback) + { + // User callback, registered via ImDrawList::AddCallback() + if (pcmd->UserCallback == ImGui_ImplMetal_DrawCallback_ResetRenderState) + ImGui_ImplMetal_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, vertexBufferOffset); + else + pcmd->UserCallback(draw_list, pcmd); + } + else + { + // Project scissor/clipping rectangles into framebuffer space + ImVec2 clip_min((pcmd->ClipRect.x - clip_off.x) * clip_scale.x, (pcmd->ClipRect.y - clip_off.y) * clip_scale.y); + ImVec2 clip_max((pcmd->ClipRect.z - clip_off.x) * clip_scale.x, (pcmd->ClipRect.w - clip_off.y) * clip_scale.y); + + // Clamp to viewport as setScissorRect() won't accept values that are off bounds + if (clip_min.x < 0.0f) { clip_min.x = 0.0f; } + if (clip_min.y < 0.0f) { clip_min.y = 0.0f; } + if (clip_max.x > (float)fb_width) { clip_max.x = (float)fb_width; } + if (clip_max.y > (float)fb_height) { clip_max.y = (float)fb_height; } + if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) + continue; + if (pcmd->ElemCount == 0) // drawIndexedPrimitives() validation doesn't accept this + continue; + + // Apply scissor/clipping rectangle + MTLScissorRect scissorRect = + { + .x = NSUInteger(clip_min.x), + .y = NSUInteger(clip_min.y), + .width = NSUInteger(clip_max.x - clip_min.x), + .height = NSUInteger(clip_max.y - clip_min.y) + }; + [commandEncoder setScissorRect:scissorRect]; + + // Bind texture, Draw + ImTextureID tex_id = pcmd->GetTexID(); + if (tex_id != ImTextureID_Invalid) + [commandEncoder setFragmentTexture:(__bridge id)(void*)(intptr_t)(tex_id) atIndex:0]; + + [commandEncoder setVertexBufferOffset:(vertexBufferOffset + pcmd->VtxOffset * sizeof(ImDrawVert)) atIndex:0]; + [commandEncoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle + indexCount:pcmd->ElemCount + indexType:sizeof(ImDrawIdx) == 2 ? MTLIndexTypeUInt16 : MTLIndexTypeUInt32 + indexBuffer:indexBuffer.buffer + indexBufferOffset:indexBufferOffset + pcmd->IdxOffset * sizeof(ImDrawIdx)]; + } + } + + vertexBufferOffset += (size_t)draw_list->VtxBuffer.Size * sizeof(ImDrawVert); + indexBufferOffset += (size_t)draw_list->IdxBuffer.Size * sizeof(ImDrawIdx); + } + + MetalContext* sharedMetalContext = bd->SharedMetalContext; + [commandBuffer addCompletedHandler:^(id) + { + @synchronized(sharedMetalContext.bufferCacheLock) + { + [sharedMetalContext.bufferCache addObject:vertexBuffer]; + [sharedMetalContext.bufferCache addObject:indexBuffer]; + } + }]; + bd->RenderCommandEncoder = nil; +} + +static void ImGui_ImplMetal_DestroyTexture(ImTextureData* tex) +{ + if (MetalTexture* backend_tex = (__bridge_transfer MetalTexture*)(tex->BackendUserData)) + { + IM_ASSERT(backend_tex.metalTexture == (__bridge id)(void*)(intptr_t)tex->TexID); + backend_tex.metalTexture = nil; + + // Clear identifiers and mark as destroyed (in order to allow e.g. calling InvalidateDeviceObjects while running) + tex->SetTexID(ImTextureID_Invalid); + tex->BackendUserData = nullptr; + } + tex->SetStatus(ImTextureStatus_Destroyed); +} + +void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex) +{ + ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); + if (tex->Status == ImTextureStatus_WantCreate) + { + // Create and upload new texture to graphics system + //IMGUI_DEBUG_LOG("UpdateTexture #%03d: WantCreate %dx%d\n", tex->UniqueID, tex->Width, tex->Height); + IM_ASSERT(tex->TexID == ImTextureID_Invalid && tex->BackendUserData == nullptr); + IM_ASSERT(tex->Format == ImTextureFormat_RGBA32); + + // We are retrieving and uploading the font atlas as a 4-channels RGBA texture here. + // In theory we could call GetTexDataAsAlpha8() and upload a 1-channel texture to save on memory access bandwidth. + // However, using a shader designed for 1-channel texture would make it less obvious to use the ImTextureID facility to render users own textures. + // You can make that change in your implementation. + MTLTextureDescriptor* textureDescriptor = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatRGBA8Unorm + width:(NSUInteger)tex->Width + height:(NSUInteger)tex->Height + mipmapped:NO]; + textureDescriptor.usage = MTLTextureUsageShaderRead; + #if TARGET_OS_OSX || TARGET_OS_MACCATALYST + textureDescriptor.storageMode = MTLStorageModeManaged; + #else + textureDescriptor.storageMode = MTLStorageModeShared; + #endif + id texture = [bd->SharedMetalContext.device newTextureWithDescriptor:textureDescriptor]; + [texture replaceRegion:MTLRegionMake2D(0, 0, (NSUInteger)tex->Width, (NSUInteger)tex->Height) mipmapLevel:0 withBytes:tex->Pixels bytesPerRow:(NSUInteger)tex->Width * 4]; + MetalTexture* backend_tex = [[MetalTexture alloc] initWithTexture:texture]; + + // Store identifiers + tex->SetTexID((ImTextureID)(intptr_t)texture); + tex->SetStatus(ImTextureStatus_OK); + tex->BackendUserData = (__bridge_retained void*)(backend_tex); + } + else if (tex->Status == ImTextureStatus_WantUpdates) + { + // Update selected blocks. We only ever write to textures regions which have never been used before! + // This backend choose to use tex->Updates[] but you can use tex->UpdateRect to upload a single region. + MetalTexture* backend_tex = (__bridge MetalTexture*)(tex->BackendUserData); + for (ImTextureRect& r : tex->Updates) + { + [backend_tex.metalTexture replaceRegion:MTLRegionMake2D((NSUInteger)r.x, (NSUInteger)r.y, (NSUInteger)r.w, (NSUInteger)r.h) + mipmapLevel:0 + withBytes:tex->GetPixelsAt(r.x, r.y) + bytesPerRow:(NSUInteger)tex->Width * 4]; + } + tex->SetStatus(ImTextureStatus_OK); + } + else if (tex->Status == ImTextureStatus_WantDestroy && tex->UnusedFrames > 0) + { + ImGui_ImplMetal_DestroyTexture(tex); + } +} + +bool ImGui_ImplMetal_CreateDeviceObjects(id device) +{ + ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); + MTLDepthStencilDescriptor* depthStencilDescriptor = [[MTLDepthStencilDescriptor alloc] init]; + depthStencilDescriptor.depthWriteEnabled = NO; + depthStencilDescriptor.depthCompareFunction = MTLCompareFunctionAlways; + bd->SharedMetalContext.depthStencilState = [device newDepthStencilStateWithDescriptor:depthStencilDescriptor]; + MTLSamplerDescriptor* samplerDescriptor = [[MTLSamplerDescriptor alloc] init]; + samplerDescriptor.minFilter = MTLSamplerMinMagFilterLinear; + samplerDescriptor.magFilter = MTLSamplerMinMagFilterLinear; + samplerDescriptor.mipFilter = MTLSamplerMipFilterLinear; + bd->SharedMetalContext.samplerStateLinear = [device newSamplerStateWithDescriptor:samplerDescriptor]; + samplerDescriptor.minFilter = MTLSamplerMinMagFilterNearest; + samplerDescriptor.magFilter = MTLSamplerMinMagFilterNearest; + samplerDescriptor.mipFilter = MTLSamplerMipFilterNearest; + bd->SharedMetalContext.samplerStateNearest = [device newSamplerStateWithDescriptor:samplerDescriptor]; +#ifdef IMGUI_IMPL_METAL_CPP + [samplerDescriptor release]; + [depthStencilDescriptor release]; +#endif + + return true; +} + +void ImGui_ImplMetal_DestroyDeviceObjects() +{ + ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); + + // Destroy all textures + for (ImTextureData* tex : ImGui::GetPlatformIO().Textures) + if (tex->RefCount == 1) + ImGui_ImplMetal_DestroyTexture(tex); + + [bd->SharedMetalContext.renderPipelineStateCache removeAllObjects]; + bd->SharedMetalContext.samplerStateLinear = nil; + bd->SharedMetalContext.samplerStateNearest = nil; +} + +bool ImGui_ImplMetal_Init(id device) +{ + ImGuiIO& io = ImGui::GetIO(); + IMGUI_CHECKVERSION(); + IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!"); + + ImGui_ImplMetal_Data* bd = IM_NEW(ImGui_ImplMetal_Data)(); + io.BackendRendererUserData = (void*)bd; + io.BackendRendererName = "imgui_impl_metal"; + io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes. + io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; // We can honor ImGuiPlatformIO::Textures[] requests during render. + + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + platform_io.DrawCallback_ResetRenderState = ImGui_ImplMetal_DrawCallback_ResetRenderState; + platform_io.DrawCallback_SetSamplerLinear = ImGui_ImplMetal_DrawCallback_SetSamplerLinear; + platform_io.DrawCallback_SetSamplerNearest = ImGui_ImplMetal_DrawCallback_SetSamplerNearest; + + bd->SharedMetalContext = [[MetalContext alloc] init]; + bd->SharedMetalContext.device = device; + + return true; +} + +void ImGui_ImplMetal_Shutdown() +{ + ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); + IM_UNUSED(bd); + IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?"); + ImGuiIO& io = ImGui::GetIO(); + ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); + + ImGui_ImplMetal_DestroyDeviceObjects(); + ImGui_ImplMetal_DestroyBackendData(); + + io.BackendRendererName = nullptr; + io.BackendRendererUserData = nullptr; + io.BackendFlags &= ~(ImGuiBackendFlags_RendererHasVtxOffset | ImGuiBackendFlags_RendererHasTextures); + platform_io.ClearRendererHandlers(); +} + +#pragma mark - MetalBuffer implementation + +@implementation MetalBuffer +- (instancetype)initWithBuffer:(id)buffer +{ + if ((self = [super init])) + { + _buffer = buffer; + _lastReuseTime = GetMachAbsoluteTimeInSeconds(); + } + return self; +} +@end + +#pragma mark - FramebufferDescriptor implementation + +@implementation FramebufferDescriptor +- (instancetype)initWithRenderPassDescriptor:(MTLRenderPassDescriptor*)renderPassDescriptor +{ + if ((self = [super init])) + { + _sampleCount = renderPassDescriptor.colorAttachments[0].texture.sampleCount; + _colorPixelFormat = renderPassDescriptor.colorAttachments[0].texture.pixelFormat; + _depthPixelFormat = renderPassDescriptor.depthAttachment.texture.pixelFormat; + _stencilPixelFormat = renderPassDescriptor.stencilAttachment.texture.pixelFormat; + } + return self; +} + +- (nonnull id)copyWithZone:(nullable NSZone*)zone +{ + FramebufferDescriptor* copy = [[FramebufferDescriptor allocWithZone:zone] init]; + copy.sampleCount = self.sampleCount; + copy.colorPixelFormat = self.colorPixelFormat; + copy.depthPixelFormat = self.depthPixelFormat; + copy.stencilPixelFormat = self.stencilPixelFormat; + return copy; +} + +- (NSUInteger)hash +{ + NSUInteger sc = _sampleCount & 0x3; + NSUInteger cf = _colorPixelFormat & 0x3FF; + NSUInteger df = _depthPixelFormat & 0x3FF; + NSUInteger sf = _stencilPixelFormat & 0x3FF; + NSUInteger hash = (sf << 22) | (df << 12) | (cf << 2) | sc; + return hash; +} + +- (BOOL)isEqual:(id)object +{ + FramebufferDescriptor* other = object; + if (![other isKindOfClass:[FramebufferDescriptor class]]) + return NO; + return other.sampleCount == self.sampleCount && + other.colorPixelFormat == self.colorPixelFormat && + other.depthPixelFormat == self.depthPixelFormat && + other.stencilPixelFormat == self.stencilPixelFormat; +} + +@end + +#pragma mark - MetalTexture implementation + +@implementation MetalTexture +- (instancetype)initWithTexture:(id)metalTexture +{ + if ((self = [super init])) + self.metalTexture = metalTexture; + return self; +} + +@end + +#pragma mark - MetalContext implementation + +@implementation MetalContext +- (instancetype)init +{ + if ((self = [super init])) + { + self.renderPipelineStateCache = [NSMutableDictionary dictionary]; + self.bufferCache = [NSMutableArray array]; + self.bufferCacheLock = [[NSObject alloc] init]; + _lastBufferCachePurge = GetMachAbsoluteTimeInSeconds(); + } + return self; +} + +- (MetalBuffer*)dequeueReusableBufferOfLength:(NSUInteger)length device:(id)device +{ + double now = GetMachAbsoluteTimeInSeconds(); + + @synchronized(self.bufferCacheLock) + { + // Purge old buffers that haven't been useful for a while + if (now - self.lastBufferCachePurge > 1.0) + { + NSMutableArray* survivors = [NSMutableArray array]; + for (MetalBuffer* candidate in self.bufferCache) + if (candidate.lastReuseTime > self.lastBufferCachePurge) + [survivors addObject:candidate]; + self.bufferCache = [survivors mutableCopy]; + self.lastBufferCachePurge = now; + } + + // See if we have a buffer we can reuse + MetalBuffer* bestCandidate = nil; + for (MetalBuffer* candidate in self.bufferCache) + if (candidate.buffer.length >= length && (bestCandidate == nil || bestCandidate.lastReuseTime > candidate.lastReuseTime)) + bestCandidate = candidate; + + if (bestCandidate != nil) + { + [self.bufferCache removeObject:bestCandidate]; + bestCandidate.lastReuseTime = now; + return bestCandidate; + } + } + + // No luck; make a new buffer + id backing = [device newBufferWithLength:length options:MTLResourceStorageModeShared]; + return [[MetalBuffer alloc] initWithBuffer:backing]; +} + +// Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling. +- (id)renderPipelineStateForFramebufferDescriptor:(FramebufferDescriptor*)descriptor device:(id)device +{ + NSError* error = nil; + + NSString* shaderSource = @"" + "#include \n" + "using namespace metal;\n" + "\n" + "struct Uniforms {\n" + " float4x4 projectionMatrix;\n" + "};\n" + "\n" + "struct VertexIn {\n" + " float2 position [[attribute(0)]];\n" + " float2 texCoords [[attribute(1)]];\n" + " uchar4 color [[attribute(2)]];\n" + "};\n" + "\n" + "struct VertexOut {\n" + " float4 position [[position]];\n" + " float2 texCoords;\n" + " float4 color;\n" + "};\n" + "\n" + "vertex VertexOut vertex_main(VertexIn in [[stage_in]],\n" + " constant Uniforms &uniforms [[buffer(1)]]) {\n" + " VertexOut out;\n" + " out.position = uniforms.projectionMatrix * float4(in.position, 0, 1);\n" + " out.texCoords = in.texCoords;\n" + " out.color = float4(in.color) / float4(255.0);\n" + " return out;\n" + "}\n" + "\n" + "fragment half4 fragment_main(VertexOut in [[stage_in]],\n" + " texture2d texture [[texture(0)]],\n" + " sampler textureSampler [[sampler(0)]]) {\n" + " half4 texColor = texture.sample(textureSampler, in.texCoords);\n" + " return half4(in.color) * texColor;\n" + "}\n"; + + id library = [device newLibraryWithSource:shaderSource options:nil error:&error]; + if (library == nil) + { + NSLog(@"Error: failed to create Metal library: %@", error); + return nil; + } + + id vertexFunction = [library newFunctionWithName:@"vertex_main"]; + id fragmentFunction = [library newFunctionWithName:@"fragment_main"]; + + if (vertexFunction == nil || fragmentFunction == nil) + { + NSLog(@"Error: failed to find Metal shader functions in library: %@", error); + return nil; + } + + MTLVertexDescriptor* vertexDescriptor = [MTLVertexDescriptor vertexDescriptor]; + vertexDescriptor.attributes[0].offset = offsetof(ImDrawVert, pos); + vertexDescriptor.attributes[0].format = MTLVertexFormatFloat2; // position + vertexDescriptor.attributes[0].bufferIndex = 0; + vertexDescriptor.attributes[1].offset = offsetof(ImDrawVert, uv); + vertexDescriptor.attributes[1].format = MTLVertexFormatFloat2; // texCoords + vertexDescriptor.attributes[1].bufferIndex = 0; + vertexDescriptor.attributes[2].offset = offsetof(ImDrawVert, col); + vertexDescriptor.attributes[2].format = MTLVertexFormatUChar4; // color + vertexDescriptor.attributes[2].bufferIndex = 0; + vertexDescriptor.layouts[0].stepRate = 1; + vertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex; + vertexDescriptor.layouts[0].stride = sizeof(ImDrawVert); + + MTLRenderPipelineDescriptor* pipelineDescriptor = [[MTLRenderPipelineDescriptor alloc] init]; + pipelineDescriptor.vertexFunction = vertexFunction; + pipelineDescriptor.fragmentFunction = fragmentFunction; + pipelineDescriptor.vertexDescriptor = vertexDescriptor; + pipelineDescriptor.rasterSampleCount = self.framebufferDescriptor.sampleCount; + pipelineDescriptor.colorAttachments[0].pixelFormat = self.framebufferDescriptor.colorPixelFormat; + pipelineDescriptor.colorAttachments[0].blendingEnabled = YES; + pipelineDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd; + pipelineDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha; + pipelineDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + pipelineDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd; + pipelineDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorOne; + pipelineDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha; + pipelineDescriptor.depthAttachmentPixelFormat = self.framebufferDescriptor.depthPixelFormat; + pipelineDescriptor.stencilAttachmentPixelFormat = self.framebufferDescriptor.stencilPixelFormat; + + id renderPipelineState = [device newRenderPipelineStateWithDescriptor:pipelineDescriptor error:&error]; + if (error != nil) + NSLog(@"Error: failed to create Metal pipeline state: %@", error); + + return renderPipelineState; +} + +@end + +//----------------------------------------------------------------------------- + +#endif // #ifndef IMGUI_DISABLE From fc6395365d4acf55d75d248bb2e82bd962c6bb1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Am=C3=A9lie=20Heinrich?= Date: Tue, 30 Jun 2026 01:15:44 +0200 Subject: [PATCH 08/16] Backends: Metal4: Added Metal 4 backend. Added SDL3+Metal4 example. (#9458, #9451) --- .github/workflows/build.yml | 3 + .gitignore | 1 + backends/imgui_impl_metal4.h | 60 ++-- backends/imgui_impl_metal4.mm | 387 ++++++++++++++------------ examples/example_sdl3_metal4/Makefile | 48 ++++ examples/example_sdl3_metal4/main.mm | 239 ++++++++++++++++ 6 files changed, 513 insertions(+), 225 deletions(-) create mode 100644 examples/example_sdl3_metal4/Makefile create mode 100644 examples/example_sdl3_metal4/main.mm diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a4d3ed459..cc84ae2d3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -572,6 +572,9 @@ jobs: - name: Build macOS example_sdl2_metal run: make -C examples/example_sdl2_metal + - name: Build macOS example_sdl3_metal4 + run: make -C examples/example_sdl3_metal4 + - name: Build macOS example_sdl2_opengl2 run: make -C examples/example_sdl2_opengl2 if: github.event_name == 'workflow_run' diff --git a/.gitignore b/.gitignore index a8b4e38e3..83c0ab423 100644 --- a/.gitignore +++ b/.gitignore @@ -72,6 +72,7 @@ examples/example_sdl2_opengl3/example_sdl2_opengl3 examples/example_sdl2_sdlrenderer2/example_sdl2_sdlrenderer2 examples/example_sdl2_vulkan/example_sdl2_vulkan examples/example_sdl3_metal/example_sdl3_metal +examples/example_sdl3_metal4/example_sdl3_metal4 examples/example_sdl3_opengl3/example_sdl3_opengl3 examples/example_sdl3_sdlgpu3/example_sdl3_sdlgpu3 examples/example_sdl3_sdlrenderer3/example_sdl3_sdlrenderer3 diff --git a/backends/imgui_impl_metal4.h b/backends/imgui_impl_metal4.h index 8af2c8b6b..7f9ec2d16 100644 --- a/backends/imgui_impl_metal4.h +++ b/backends/imgui_impl_metal4.h @@ -1,8 +1,8 @@ -// dear imgui: Renderer Backend for Metal +// dear imgui: Renderer Backend for Metal 4 // This needs to be used along with a Platform Backend (e.g. OSX) // Implemented features: -// [X] Renderer: User texture binding. Use 'MTLTexture' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef! +// [X] Renderer: User texture binding. Use 'MTLTexture.gpuResourceID' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef! // [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset). // [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures). @@ -24,55 +24,31 @@ #ifdef __OBJC__ -@class MTLRenderPassDescriptor; -@protocol MTLDevice, MTLCommandBuffer, MTLRenderCommandEncoder; +@class MTL4RenderPassDescriptor; +@protocol MTLDevice, MTL4CommandBuffer, MTL4RenderCommandEncoder, MTL4CommandQueue; +// framesInFlight must match the number of frames your application keeps in flight (e.g. the size of your own +// command buffer/allocator ring). The backend uses it to size its own per-frame-in-flight resources (constant +// buffer, vertex/index buffer cache) so the CPU never overwrites a slot the GPU may still be reading. // Follow "Getting Started" link and check examples/ folder to learn about using backends! -IMGUI_IMPL_API bool ImGui_ImplMetal_Init(id device); -IMGUI_IMPL_API void ImGui_ImplMetal_Shutdown(); -IMGUI_IMPL_API void ImGui_ImplMetal_NewFrame(MTLRenderPassDescriptor* renderPassDescriptor); -IMGUI_IMPL_API void ImGui_ImplMetal_RenderDrawData(ImDrawData* drawData, - id commandBuffer, - id commandEncoder); +IMGUI_IMPL_API bool ImGui_ImplMetal4_Init(id device, id commandQueue, int framesInFlight); +IMGUI_IMPL_API void ImGui_ImplMetal4_Shutdown(); +// frameInFlightIndex must match the slot you use to index your own per-frame-in-flight resources +// (e.g. the same index used to pick your command buffer/allocator), and must be < framesInFlight passed to Init(). +IMGUI_IMPL_API void ImGui_ImplMetal4_NewFrame(MTL4RenderPassDescriptor* renderPassDescriptor, int frameInFlightIndex); +IMGUI_IMPL_API void ImGui_ImplMetal4_RenderDrawData(ImDrawData* drawData, + id commandBuffer, + id commandEncoder); // Called by Init/NewFrame/Shutdown -IMGUI_IMPL_API bool ImGui_ImplMetal_CreateDeviceObjects(id device); -IMGUI_IMPL_API void ImGui_ImplMetal_DestroyDeviceObjects(); +IMGUI_IMPL_API bool ImGui_ImplMetal4_CreateDeviceObjects(id device); +IMGUI_IMPL_API void ImGui_ImplMetal4_DestroyDeviceObjects(); // (Advanced) Use e.g. if you need to precisely control the timing of texture updates (e.g. for staged rendering), by setting ImDrawData::Textures = nullptr to handle this manually. -IMGUI_IMPL_API void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex); +IMGUI_IMPL_API void ImGui_ImplMetal4_UpdateTexture(ImTextureData* tex); #endif -//----------------------------------------------------------------------------- -// C++ API -//----------------------------------------------------------------------------- - -// Enable Metal C++ binding support with '#define IMGUI_IMPL_METAL_CPP' in your imconfig.h file -// More info about using Metal from C++: https://developer.apple.com/metal/cpp/ - -#ifdef IMGUI_IMPL_METAL_CPP -#include -#ifndef __OBJC__ - -// Follow "Getting Started" link and check examples/ folder to learn about using backends! -IMGUI_IMPL_API bool ImGui_ImplMetal_Init(MTL::Device* device); -IMGUI_IMPL_API void ImGui_ImplMetal_Shutdown(); -IMGUI_IMPL_API void ImGui_ImplMetal_NewFrame(MTL::RenderPassDescriptor* renderPassDescriptor); -IMGUI_IMPL_API void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data, - MTL::CommandBuffer* commandBuffer, - MTL::RenderCommandEncoder* commandEncoder); - -// Called by Init/NewFrame/Shutdown -IMGUI_IMPL_API bool ImGui_ImplMetal_CreateDeviceObjects(MTL::Device* device); -IMGUI_IMPL_API void ImGui_ImplMetal_DestroyDeviceObjects(); - -// (Advanced) Use e.g. if you need to precisely control the timing of texture updates (e.g. for staged rendering), by setting ImDrawData::Textures = nullptr to handle this manually. -IMGUI_IMPL_API void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex); - -#endif -#endif - //----------------------------------------------------------------------------- #endif // #ifndef IMGUI_DISABLE diff --git a/backends/imgui_impl_metal4.mm b/backends/imgui_impl_metal4.mm index 652668c78..1114b470b 100644 --- a/backends/imgui_impl_metal4.mm +++ b/backends/imgui_impl_metal4.mm @@ -1,4 +1,4 @@ -// dear imgui: Renderer Backend for Metal +// dear imgui: Renderer Backend for Metal 4 // This needs to be used along with a Platform Backend (e.g. OSX) // Implemented features: @@ -14,43 +14,26 @@ // - Documentation https://dearimgui.com/docs (same as your local docs/ folder). // - Introduction, links and more at the top of imgui.cpp +// FIXME: Metal-cpp support +// FIXME?: Texture view pool support + // CHANGELOG // (minor and older changes stripped away, please see git history for details) -// 2026-04-28: Added support for standard draw callbacks (in platform_io): DrawCallback_SetSamplerLinear and DrawCallback_SetSamplerNearest. (#9378, #9381) -// 2026-04-23: Added support for standard draw callbacks (in platform_io): DrawCallback_ResetRenderState (others are not yet supported). (#9378) -// 2026-04-14: Metal: use a dedicated bufferCacheLock to avoid crashing when bufferCache is replaced by a new object while being used for @synchronize(). (#9367) -// 2026-04-03: Metal: avoid redundant vertex buffer bind in SetupRenderState. (#9343) -// 2026-03-19: Fixed issue in ImGui_ImplMetal_RenderDrawData() if ImTextureID_Invalid is defined to be != 0, which became the default since 2026-03-12. (#9295, #9310) -// 2025-09-18: Call platform_io.ClearRendererHandlers() on shutdown. -// 2025-06-11: Added support for ImGuiBackendFlags_RendererHasTextures, for dynamic font atlas. Removed ImGui_ImplMetal_CreateFontsTexture() and ImGui_ImplMetal_DestroyFontsTexture(). -// 2025-02-03: Metal: Crash fix. (#8367) -// 2025-01-08: Metal: Fixed memory leaks when using metal-cpp (#8276, #8166) or when using multiple contexts (#7419). -// 2022-08-23: Metal: Update deprecated property 'sampleCount'->'rasterSampleCount'. -// 2022-07-05: Metal: Add dispatch synchronization. -// 2022-06-30: Metal: Use __bridge for ARC based systems. -// 2022-06-01: Metal: Fixed null dereference on exit inside command buffer completion handler. -// 2022-04-27: Misc: Store backend data in a per-context struct, allowing to use this backend with multiple contexts. -// 2022-01-03: Metal: Ignore ImDrawCmd where ElemCount == 0 (very rare but can technically be manufactured by user code). -// 2021-12-30: Metal: Added Metal C++ support. Enable with '#define IMGUI_IMPL_METAL_CPP' in your imconfig.h file. -// 2021-08-24: Metal: Fixed a crash when clipping rect larger than framebuffer is submitted. (#4464) -// 2021-05-19: Metal: Replaced direct access to ImDrawCmd::TextureId with a call to ImDrawCmd::GetTexID(). (will become a requirement) -// 2021-02-18: Metal: Change blending equation to preserve alpha in output buffer. -// 2021-01-25: Metal: Fixed texture storage mode when building on Mac Catalyst. -// 2019-05-29: Metal: Added support for large mesh (64K+ vertices), enable ImGuiBackendFlags_RendererHasVtxOffset flag. -// 2019-04-30: Metal: Added support for special ImDrawCallback_ResetRenderState callback to reset render state. -// 2019-02-11: Metal: Projecting clipping rectangles correctly using draw_data->FramebufferScale to allow multi-viewports for retina display. -// 2018-11-30: Misc: Setting up io.BackendRendererName so it can be displayed in the About Window. -// 2018-07-05: Metal: Added new Metal backend implementation. +// 2026-29-06: Metal 4: Added new Metal 4 backend implementation. #include "imgui.h" #ifndef IMGUI_DISABLE -#include "imgui_impl_metal.h" +#include "imgui_impl_metal4.h" #import #import -#pragma mark - Support classes +#pragma mark - Support classes and structs + +struct ImGui_Metal4_ConstantData +{ + float ModelViewProjectionMatrix[4][4]; +}; -// A wrapper around a MTLBuffer object that knows the last time it was reused @interface MetalBuffer : NSObject @property (nonatomic, strong) id buffer; @property (nonatomic, assign) double lastReuseTime; @@ -64,7 +47,7 @@ @property (nonatomic, assign) MTLPixelFormat colorPixelFormat; @property (nonatomic, assign) MTLPixelFormat depthPixelFormat; @property (nonatomic, assign) MTLPixelFormat stencilPixelFormat; -- (instancetype)initWithRenderPassDescriptor:(MTLRenderPassDescriptor*)renderPassDescriptor; +- (instancetype)initWithRenderPassDescriptor:(MTL4RenderPassDescriptor*)renderPassDescriptor; @end @interface MetalTexture : NSObject @@ -77,83 +60,63 @@ // font texture, and manages the reusable buffer cache. @interface MetalContext : NSObject @property (nonatomic, strong) id device; +@property (nonatomic, strong) id commandQueue; @property (nonatomic, strong) id depthStencilState; +@property (nonatomic, strong) id argumentTable; @property (nonatomic, strong) id samplerStateLinear; @property (nonatomic, strong) id samplerStateNearest; -@property (nonatomic, strong) FramebufferDescriptor* framebufferDescriptor; // framebuffer descriptor for current frame; transient -@property (nonatomic, strong) NSMutableDictionary* renderPipelineStateCache; // pipeline cache; keyed on framebuffer descriptors -@property (nonatomic, strong) NSMutableArray* bufferCache; +@property (nonatomic, strong) id residencySet; +@property (nonatomic, strong) FramebufferDescriptor* framebufferDescriptor; +@property (nonatomic, strong) NSMutableDictionary* renderPipelineStateCache; +@property (nonatomic, assign) NSUInteger framesInFlight; +@property (nonatomic, assign) NSUInteger currentFrameSlot; +@property (nonatomic, strong) NSArray>* constantBuffers; +@property (nonatomic, assign) ImGui_Metal4_ConstantData** constantBufferContentsArray; +@property (nonatomic, strong) NSMutableArray*>* bufferCaches; @property (nonatomic, strong) NSObject* bufferCacheLock; @property (nonatomic, assign) double lastBufferCachePurge; +- (id)currentConstantBuffer; +- (ImGui_Metal4_ConstantData*)currentConstantBufferContents; - (MetalBuffer*)dequeueReusableBufferOfLength:(NSUInteger)length device:(id)device; - (id)renderPipelineStateForFramebufferDescriptor:(FramebufferDescriptor*)descriptor device:(id)device; @end -struct ImGui_ImplMetal_Data +struct ImGui_ImplMetal4_Data { - MetalContext* SharedMetalContext; - id RenderCommandEncoder; + MetalContext* SharedMetalContext; + id RenderCommandEncoder; - ImGui_ImplMetal_Data() { memset((void*)this, 0, sizeof(*this)); } + ImGui_ImplMetal4_Data() { memset((void*)this, 0, sizeof(*this)); } }; -static ImGui_ImplMetal_Data* ImGui_ImplMetal_GetBackendData() { return ImGui::GetCurrentContext() ? (ImGui_ImplMetal_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; } -static void ImGui_ImplMetal_DestroyBackendData(){ IM_DELETE(ImGui_ImplMetal_GetBackendData()); } +static ImGui_ImplMetal4_Data* ImGui_ImplMetal4_GetBackendData() { return ImGui::GetCurrentContext() ? (ImGui_ImplMetal4_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; } +static void ImGui_ImplMetal4_DestroyBackendData(){ IM_DELETE(ImGui_ImplMetal4_GetBackendData()); } static inline CFTimeInterval GetMachAbsoluteTimeInSeconds() { return (CFTimeInterval)(double)(clock_gettime_nsec_np(CLOCK_UPTIME_RAW) / 1e9); } -#ifdef IMGUI_IMPL_METAL_CPP - -#pragma mark - Dear ImGui Metal C++ Backend API - -bool ImGui_ImplMetal_Init(MTL::Device* device) -{ - return ImGui_ImplMetal_Init((__bridge id)(device)); -} - -void ImGui_ImplMetal_NewFrame(MTL::RenderPassDescriptor* renderPassDescriptor) -{ - ImGui_ImplMetal_NewFrame((__bridge MTLRenderPassDescriptor*)(renderPassDescriptor)); -} - -void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data, - MTL::CommandBuffer* commandBuffer, - MTL::RenderCommandEncoder* commandEncoder) -{ - ImGui_ImplMetal_RenderDrawData(draw_data, - (__bridge id)(commandBuffer), - (__bridge id)(commandEncoder)); - -} - -bool ImGui_ImplMetal_CreateDeviceObjects(MTL::Device* device) -{ - return ImGui_ImplMetal_CreateDeviceObjects((__bridge id)(device)); -} - -#endif // #ifdef IMGUI_IMPL_METAL_CPP - #pragma mark - Dear ImGui Metal Backend API -void ImGui_ImplMetal_NewFrame(MTLRenderPassDescriptor* renderPassDescriptor) +void ImGui_ImplMetal4_NewFrame(MTL4RenderPassDescriptor* renderPassDescriptor, int frameInFlightIndex) { - ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); - IM_ASSERT(bd != nil && "Context or backend not initialized! Did you call ImGui_ImplMetal_Init()?"); + ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); + IM_ASSERT(bd != nil && "Context or backend not initialized! Did you call ImGui_ImplMetal4_Init()?"); + IM_ASSERT(frameInFlightIndex < bd->SharedMetalContext.framesInFlight && "frameInFlightIndex out of range! See framesInFlight passed to ImGui_ImplMetal4_Init()."); #ifdef IMGUI_IMPL_METAL_CPP bd->SharedMetalContext.framebufferDescriptor = [[[FramebufferDescriptor alloc] initWithRenderPassDescriptor:renderPassDescriptor]autorelease]; #else bd->SharedMetalContext.framebufferDescriptor = [[FramebufferDescriptor alloc] initWithRenderPassDescriptor:renderPassDescriptor]; #endif + bd->SharedMetalContext.currentFrameSlot = (NSUInteger)frameInFlightIndex; if (bd->SharedMetalContext.depthStencilState == nil) - ImGui_ImplMetal_CreateDeviceObjects(bd->SharedMetalContext.device); + ImGui_ImplMetal4_CreateDeviceObjects(bd->SharedMetalContext.device); } -static void ImGui_ImplMetal_SetupRenderState(ImDrawData* draw_data, id commandBuffer, - id commandEncoder, id renderPipelineState, +static void ImGui_ImplMetal4_SetupRenderState(ImDrawData* draw_data, id commandBuffer, + id commandEncoder, id renderPipelineState, MetalBuffer* vertexBuffer, size_t vertexBufferOffset) { IM_UNUSED(commandBuffer); - ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); + ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); [commandEncoder setCullMode:MTLCullModeNone]; [commandEncoder setDepthStencilState:bd->SharedMetalContext.depthStencilState]; @@ -184,23 +147,24 @@ static void ImGui_ImplMetal_SetupRenderState(ImDrawData* draw_data, idSharedMetalContext currentConstantBufferContents]; + memcpy(constantBufferContents->ModelViewProjectionMatrix, ortho_projection, sizeof(ortho_projection)); + id argumentTable = bd->SharedMetalContext.argumentTable; + [argumentTable setAddress:[bd->SharedMetalContext currentConstantBuffer].gpuAddress atIndex:1]; + [argumentTable setAddress:(vertexBuffer.buffer.gpuAddress + vertexBufferOffset) attributeStride:sizeof(ImDrawVert) atIndex:0]; + [argumentTable setSamplerState:bd->SharedMetalContext.samplerStateLinear.gpuResourceID atIndex:0]; + [commandEncoder setArgumentTable:argumentTable atStages:MTLRenderStageVertex | MTLRenderStageFragment]; [commandEncoder setRenderPipelineState:renderPipelineState]; - [commandEncoder setFragmentSamplerState:bd->SharedMetalContext.samplerStateLinear atIndex:0]; - - [commandEncoder setVertexBuffer:vertexBuffer.buffer offset:vertexBufferOffset atIndex:0]; } -// Draw callbacks -static void ImGui_ImplMetal_DrawCallback_ResetRenderState(const ImDrawList*, const ImDrawCmd*) {} // Intentionally empty. Used as an identifier for rendering loop to call its code. Simpler to implement this way. -static void ImGui_ImplMetal_DrawCallback_SetSamplerLinear(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); [bd->RenderCommandEncoder setFragmentSamplerState:bd->SharedMetalContext.samplerStateLinear atIndex:0]; } -static void ImGui_ImplMetal_DrawCallback_SetSamplerNearest(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); [bd->RenderCommandEncoder setFragmentSamplerState:bd->SharedMetalContext.samplerStateNearest atIndex:0]; } +static void ImGui_ImplMetal4_DrawCallback_ResetRenderState(const ImDrawList*, const ImDrawCmd*) {} // Intentionally empty. Used as an identifier for rendering loop to call its code. Simpler to implement this way. +static void ImGui_ImplMetal4_DrawCallback_SetSamplerLinear(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); [bd->SharedMetalContext.argumentTable setSamplerState:bd->SharedMetalContext.samplerStateLinear.gpuResourceID atIndex:0]; } +static void ImGui_ImplMetal4_DrawCallback_SetSamplerNearest(const ImDrawList*, const ImDrawCmd*) { ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); [bd->SharedMetalContext.argumentTable setSamplerState:bd->SharedMetalContext.samplerStateNearest.gpuResourceID atIndex:0]; } -// Metal Render function. -void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data, id commandBuffer, id commandEncoder) +void ImGui_ImplMetal4_RenderDrawData(ImDrawData* draw_data, id commandBuffer, id commandEncoder) { - ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); + ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); MetalContext* ctx = bd->SharedMetalContext; // Avoid rendering when minimized, scale coordinates for retina displays (screen coordinates != framebuffer coordinates) @@ -214,7 +178,7 @@ void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data, id if (draw_data->Textures != nullptr) for (ImTextureData* tex : *draw_data->Textures) if (tex->Status != ImTextureStatus_OK) - ImGui_ImplMetal_UpdateTexture(tex); + ImGui_ImplMetal4_UpdateTexture(tex); // Try to retrieve a render pipeline state that is compatible with the framebuffer config for this frame // The hit rate for this cache should be very near 100%. @@ -234,12 +198,15 @@ void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data, id MetalBuffer* indexBuffer = [ctx dequeueReusableBufferOfLength:indexBufferLength device:commandBuffer.device]; bd->RenderCommandEncoder = commandEncoder; - ImGui_ImplMetal_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, 0); + ImGui_ImplMetal4_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, 0); // Will project scissor/clipping rectangles into framebuffer space ImVec2 clip_off = draw_data->DisplayPos; // (0,0) unless using multi-viewports ImVec2 clip_scale = draw_data->FramebufferScale; // (1,1) unless using retina display which are often (2,2) + // Before rendering command lists, commit residency set + [bd->SharedMetalContext.residencySet commit]; + // Render command lists size_t vertexBufferOffset = 0; size_t indexBufferOffset = 0; @@ -254,8 +221,8 @@ void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data, id if (pcmd->UserCallback) { // User callback, registered via ImDrawList::AddCallback() - if (pcmd->UserCallback == ImGui_ImplMetal_DrawCallback_ResetRenderState) - ImGui_ImplMetal_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, vertexBufferOffset); + if (pcmd->UserCallback == ImGui_ImplMetal4_DrawCallback_ResetRenderState) + ImGui_ImplMetal4_SetupRenderState(draw_data, commandBuffer, commandEncoder, renderPipelineState, vertexBuffer, vertexBufferOffset); else pcmd->UserCallback(draw_list, pcmd); } @@ -288,14 +255,19 @@ void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data, id // Bind texture, Draw ImTextureID tex_id = pcmd->GetTexID(); if (tex_id != ImTextureID_Invalid) - [commandEncoder setFragmentTexture:(__bridge id)(void*)(intptr_t)(tex_id) atIndex:0]; + { + id texture = (__bridge id)(void*)(intptr_t)tex_id; + [bd->SharedMetalContext.argumentTable setTexture:texture.gpuResourceID atIndex:0]; + } - [commandEncoder setVertexBufferOffset:(vertexBufferOffset + pcmd->VtxOffset * sizeof(ImDrawVert)) atIndex:0]; + [bd->SharedMetalContext.argumentTable setAddress:(vertexBuffer.buffer.gpuAddress + vertexBufferOffset + (pcmd->VtxOffset * sizeof(ImDrawVert))) attributeStride:sizeof(ImDrawVert) atIndex:0]; + + size_t indexBufferCmdOffset = indexBufferOffset + (pcmd->IdxOffset * sizeof(ImDrawIdx)); [commandEncoder drawIndexedPrimitives:MTLPrimitiveTypeTriangle - indexCount:pcmd->ElemCount - indexType:sizeof(ImDrawIdx) == 2 ? MTLIndexTypeUInt16 : MTLIndexTypeUInt32 - indexBuffer:indexBuffer.buffer - indexBufferOffset:indexBufferOffset + pcmd->IdxOffset * sizeof(ImDrawIdx)]; + indexCount:pcmd->ElemCount + indexType:sizeof(ImDrawIdx) == 2 ? MTLIndexTypeUInt16 : MTLIndexTypeUInt32 + indexBuffer:indexBuffer.buffer.gpuAddress + indexBufferCmdOffset + indexBufferLength:indexBuffer.buffer.length - indexBufferCmdOffset]; } } @@ -304,18 +276,16 @@ void ImGui_ImplMetal_RenderDrawData(ImDrawData* draw_data, id } MetalContext* sharedMetalContext = bd->SharedMetalContext; - [commandBuffer addCompletedHandler:^(id) + @synchronized(sharedMetalContext.bufferCacheLock) { - @synchronized(sharedMetalContext.bufferCacheLock) - { - [sharedMetalContext.bufferCache addObject:vertexBuffer]; - [sharedMetalContext.bufferCache addObject:indexBuffer]; - } - }]; + NSMutableArray* slotCache = sharedMetalContext.bufferCaches[sharedMetalContext.currentFrameSlot]; + [slotCache addObject:vertexBuffer]; + [slotCache addObject:indexBuffer]; + } bd->RenderCommandEncoder = nil; } -static void ImGui_ImplMetal_DestroyTexture(ImTextureData* tex) +static void ImGui_ImplMetal4_DestroyTexture(ImTextureData* tex) { if (MetalTexture* backend_tex = (__bridge_transfer MetalTexture*)(tex->BackendUserData)) { @@ -329,9 +299,9 @@ static void ImGui_ImplMetal_DestroyTexture(ImTextureData* tex) tex->SetStatus(ImTextureStatus_Destroyed); } -void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex) +void ImGui_ImplMetal4_UpdateTexture(ImTextureData* tex) { - ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); + ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); if (tex->Status == ImTextureStatus_WantCreate) { // Create and upload new texture to graphics system @@ -348,12 +318,10 @@ void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex) height:(NSUInteger)tex->Height mipmapped:NO]; textureDescriptor.usage = MTLTextureUsageShaderRead; - #if TARGET_OS_OSX || TARGET_OS_MACCATALYST - textureDescriptor.storageMode = MTLStorageModeManaged; - #else textureDescriptor.storageMode = MTLStorageModeShared; - #endif + id texture = [bd->SharedMetalContext.device newTextureWithDescriptor:textureDescriptor]; + [bd->SharedMetalContext.residencySet addAllocation:texture]; [texture replaceRegion:MTLRegionMake2D(0, 0, (NSUInteger)tex->Width, (NSUInteger)tex->Height) mipmapLevel:0 withBytes:tex->Pixels bytesPerRow:(NSUInteger)tex->Width * 4]; MetalTexture* backend_tex = [[MetalTexture alloc] initWithTexture:texture]; @@ -378,18 +346,29 @@ void ImGui_ImplMetal_UpdateTexture(ImTextureData* tex) } else if (tex->Status == ImTextureStatus_WantDestroy && tex->UnusedFrames > 0) { - ImGui_ImplMetal_DestroyTexture(tex); + ImGui_ImplMetal4_DestroyTexture(tex); } } -bool ImGui_ImplMetal_CreateDeviceObjects(id device) +bool ImGui_ImplMetal4_CreateDeviceObjects(id device) { - ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); + ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); + + MTLResidencySetDescriptor* residencySetDescriptor = [[MTLResidencySetDescriptor alloc] init]; + residencySetDescriptor.initialCapacity = 1000; + + NSError* error = nil; + bd->SharedMetalContext.residencySet = [device newResidencySetWithDescriptor:residencySetDescriptor error:&error]; + IM_ASSERT(bd->SharedMetalContext.residencySet != nil && error == nil); + + [bd->SharedMetalContext.commandQueue addResidencySet:bd->SharedMetalContext.residencySet]; + MTLDepthStencilDescriptor* depthStencilDescriptor = [[MTLDepthStencilDescriptor alloc] init]; depthStencilDescriptor.depthWriteEnabled = NO; depthStencilDescriptor.depthCompareFunction = MTLCompareFunctionAlways; bd->SharedMetalContext.depthStencilState = [device newDepthStencilStateWithDescriptor:depthStencilDescriptor]; MTLSamplerDescriptor* samplerDescriptor = [[MTLSamplerDescriptor alloc] init]; + samplerDescriptor.supportArgumentBuffers = YES; samplerDescriptor.minFilter = MTLSamplerMinMagFilterLinear; samplerDescriptor.magFilter = MTLSamplerMinMagFilterLinear; samplerDescriptor.mipFilter = MTLSamplerMipFilterLinear; @@ -398,61 +377,84 @@ bool ImGui_ImplMetal_CreateDeviceObjects(id device) samplerDescriptor.magFilter = MTLSamplerMinMagFilterNearest; samplerDescriptor.mipFilter = MTLSamplerMipFilterNearest; bd->SharedMetalContext.samplerStateNearest = [device newSamplerStateWithDescriptor:samplerDescriptor]; -#ifdef IMGUI_IMPL_METAL_CPP - [samplerDescriptor release]; - [depthStencilDescriptor release]; -#endif + + NSMutableArray>* constantBuffers = [NSMutableArray array]; + ImGui_Metal4_ConstantData** constantBufferContentsArray = (ImGui_Metal4_ConstantData**)malloc(sizeof(ImGui_Metal4_ConstantData*) * bd->SharedMetalContext.framesInFlight); + for (NSUInteger i = 0; i < bd->SharedMetalContext.framesInFlight; i++) + { + id constantBuffer = [device newBufferWithLength:sizeof(ImGui_Metal4_ConstantData) options:MTLResourceStorageModeShared]; + [constantBuffers addObject:constantBuffer]; + constantBufferContentsArray[i] = (ImGui_Metal4_ConstantData*)constantBuffer.contents; + [bd->SharedMetalContext.residencySet addAllocation:constantBuffer]; + } + bd->SharedMetalContext.constantBuffers = constantBuffers; + bd->SharedMetalContext.constantBufferContentsArray = constantBufferContentsArray; + + MTL4ArgumentTableDescriptor* argumentTableDescriptor = [[MTL4ArgumentTableDescriptor alloc] init]; + argumentTableDescriptor.maxBufferBindCount = 8; + argumentTableDescriptor.maxTextureBindCount = 8; + argumentTableDescriptor.maxSamplerStateBindCount = 8; + argumentTableDescriptor.supportAttributeStrides = YES; // required: vertex buffer is bound via setAddress:stride:atIndex: for stage_in fetch + + bd->SharedMetalContext.argumentTable = [device newArgumentTableWithDescriptor:argumentTableDescriptor error:&error]; + IM_ASSERT(bd->SharedMetalContext.argumentTable != nil && error == nil); return true; } -void ImGui_ImplMetal_DestroyDeviceObjects() +void ImGui_ImplMetal4_DestroyDeviceObjects() { - ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); + ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); // Destroy all textures for (ImTextureData* tex : ImGui::GetPlatformIO().Textures) if (tex->RefCount == 1) - ImGui_ImplMetal_DestroyTexture(tex); + ImGui_ImplMetal4_DestroyTexture(tex); [bd->SharedMetalContext.renderPipelineStateCache removeAllObjects]; bd->SharedMetalContext.samplerStateLinear = nil; bd->SharedMetalContext.samplerStateNearest = nil; } -bool ImGui_ImplMetal_Init(id device) +bool ImGui_ImplMetal4_Init(id device, id commandQueue, int framesInFlight) { ImGuiIO& io = ImGui::GetIO(); IMGUI_CHECKVERSION(); IM_ASSERT(io.BackendRendererUserData == nullptr && "Already initialized a renderer backend!"); + IM_ASSERT(framesInFlight > 0 && "framesInFlight must be at least 1!"); - ImGui_ImplMetal_Data* bd = IM_NEW(ImGui_ImplMetal_Data)(); + ImGui_ImplMetal4_Data* bd = IM_NEW(ImGui_ImplMetal4_Data)(); io.BackendRendererUserData = (void*)bd; - io.BackendRendererName = "imgui_impl_metal"; + io.BackendRendererName = "imgui_impl_metal4"; io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes. io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures; // We can honor ImGuiPlatformIO::Textures[] requests during render. ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); - platform_io.DrawCallback_ResetRenderState = ImGui_ImplMetal_DrawCallback_ResetRenderState; - platform_io.DrawCallback_SetSamplerLinear = ImGui_ImplMetal_DrawCallback_SetSamplerLinear; - platform_io.DrawCallback_SetSamplerNearest = ImGui_ImplMetal_DrawCallback_SetSamplerNearest; + platform_io.DrawCallback_ResetRenderState = ImGui_ImplMetal4_DrawCallback_ResetRenderState; + platform_io.DrawCallback_SetSamplerLinear = ImGui_ImplMetal4_DrawCallback_SetSamplerLinear; + platform_io.DrawCallback_SetSamplerNearest = ImGui_ImplMetal4_DrawCallback_SetSamplerNearest; bd->SharedMetalContext = [[MetalContext alloc] init]; bd->SharedMetalContext.device = device; - + bd->SharedMetalContext.commandQueue = commandQueue; + bd->SharedMetalContext.framesInFlight = (NSUInteger)framesInFlight; + NSMutableArray*>* bufferCaches = [NSMutableArray array]; + for (NSUInteger i = 0; i < framesInFlight; i++) + [bufferCaches addObject:[NSMutableArray array]]; + bd->SharedMetalContext.bufferCaches = bufferCaches; return true; } -void ImGui_ImplMetal_Shutdown() +void ImGui_ImplMetal4_Shutdown() { - ImGui_ImplMetal_Data* bd = ImGui_ImplMetal_GetBackendData(); + ImGui_ImplMetal4_Data* bd = ImGui_ImplMetal4_GetBackendData(); IM_UNUSED(bd); IM_ASSERT(bd != nullptr && "No renderer backend to shutdown, or already shutdown?"); ImGuiIO& io = ImGui::GetIO(); ImGuiPlatformIO& platform_io = ImGui::GetPlatformIO(); - ImGui_ImplMetal_DestroyDeviceObjects(); - ImGui_ImplMetal_DestroyBackendData(); + ImGui_ImplMetal4_DestroyDeviceObjects(); + ImGui_ImplMetal4_DestroyBackendData(); io.BackendRendererName = nullptr; io.BackendRendererUserData = nullptr; @@ -460,8 +462,6 @@ void ImGui_ImplMetal_Shutdown() platform_io.ClearRendererHandlers(); } -#pragma mark - MetalBuffer implementation - @implementation MetalBuffer - (instancetype)initWithBuffer:(id)buffer { @@ -477,7 +477,7 @@ void ImGui_ImplMetal_Shutdown() #pragma mark - FramebufferDescriptor implementation @implementation FramebufferDescriptor -- (instancetype)initWithRenderPassDescriptor:(MTLRenderPassDescriptor*)renderPassDescriptor +- (instancetype)initWithRenderPassDescriptor:(MTL4RenderPassDescriptor*)renderPassDescriptor { if ((self = [super init])) { @@ -542,39 +542,57 @@ void ImGui_ImplMetal_Shutdown() if ((self = [super init])) { self.renderPipelineStateCache = [NSMutableDictionary dictionary]; - self.bufferCache = [NSMutableArray array]; self.bufferCacheLock = [[NSObject alloc] init]; _lastBufferCachePurge = GetMachAbsoluteTimeInSeconds(); } return self; } +- (void)dealloc +{ + free(_constantBufferContentsArray); +} + +- (id)currentConstantBuffer +{ + return self.constantBuffers[self.currentFrameSlot]; +} + +- (ImGui_Metal4_ConstantData*)currentConstantBufferContents +{ + return self.constantBufferContentsArray[self.currentFrameSlot]; +} + - (MetalBuffer*)dequeueReusableBufferOfLength:(NSUInteger)length device:(id)device { double now = GetMachAbsoluteTimeInSeconds(); + NSMutableArray* slotCache = self.bufferCaches[self.currentFrameSlot]; @synchronized(self.bufferCacheLock) { // Purge old buffers that haven't been useful for a while if (now - self.lastBufferCachePurge > 1.0) { - NSMutableArray* survivors = [NSMutableArray array]; - for (MetalBuffer* candidate in self.bufferCache) - if (candidate.lastReuseTime > self.lastBufferCachePurge) - [survivors addObject:candidate]; - self.bufferCache = [survivors mutableCopy]; + for (NSMutableArray* cache in self.bufferCaches) + { + NSMutableArray* survivors = [NSMutableArray array]; + for (MetalBuffer* candidate in cache) + if (candidate.lastReuseTime > self.lastBufferCachePurge) + [survivors addObject:candidate]; + [cache setArray:survivors]; + } self.lastBufferCachePurge = now; } - // See if we have a buffer we can reuse + // See if we have a buffer we can reuse, from this frame-in-flight slot's own cache MetalBuffer* bestCandidate = nil; - for (MetalBuffer* candidate in self.bufferCache) + for (MetalBuffer* candidate in slotCache) if (candidate.buffer.length >= length && (bestCandidate == nil || bestCandidate.lastReuseTime > candidate.lastReuseTime)) bestCandidate = candidate; if (bestCandidate != nil) { - [self.bufferCache removeObject:bestCandidate]; + [slotCache removeObject:bestCandidate]; bestCandidate.lastReuseTime = now; return bestCandidate; } @@ -582,51 +600,54 @@ void ImGui_ImplMetal_Shutdown() // No luck; make a new buffer id backing = [device newBufferWithLength:length options:MTLResourceStorageModeShared]; + [self.residencySet addAllocation:backing]; return [[MetalBuffer alloc] initWithBuffer:backing]; } -// Bilinear sampling is required by default. Set 'io.Fonts->Flags |= ImFontAtlasFlags_NoBakedLines' or 'style.AntiAliasedLinesUseTex = false' to allow point/nearest sampling. +const char* shaderCode = R"( +#include +using namespace metal; + +struct Uniforms { + float4x4 projectionMatrix; +}; + +struct VertexIn { + float2 position [[attribute(0)]]; + float2 texCoords [[attribute(1)]]; + uchar4 color [[attribute(2)]]; +}; + +struct VertexOut { + float4 position [[position]]; + float2 texCoords; + float4 color; +}; + +vertex VertexOut vertex_main(VertexIn in [[stage_in]], + constant Uniforms &uniforms [[buffer(1)]]) +{ + VertexOut out; + out.position = uniforms.projectionMatrix * float4(in.position, 0, 1); + out.texCoords = in.texCoords; + out.color = float4(in.color) / float4(255.0); + return out; +} + +fragment half4 fragment_main(VertexOut in [[stage_in]], + texture2d texture [[texture(0)]], + sampler textureSampler [[sampler(0)]]) +{ + half4 texColor = texture.sample(textureSampler, in.texCoords); + return half4(in.color) * texColor; +} +)"; + - (id)renderPipelineStateForFramebufferDescriptor:(FramebufferDescriptor*)descriptor device:(id)device { NSError* error = nil; - NSString* shaderSource = @"" - "#include \n" - "using namespace metal;\n" - "\n" - "struct Uniforms {\n" - " float4x4 projectionMatrix;\n" - "};\n" - "\n" - "struct VertexIn {\n" - " float2 position [[attribute(0)]];\n" - " float2 texCoords [[attribute(1)]];\n" - " uchar4 color [[attribute(2)]];\n" - "};\n" - "\n" - "struct VertexOut {\n" - " float4 position [[position]];\n" - " float2 texCoords;\n" - " float4 color;\n" - "};\n" - "\n" - "vertex VertexOut vertex_main(VertexIn in [[stage_in]],\n" - " constant Uniforms &uniforms [[buffer(1)]]) {\n" - " VertexOut out;\n" - " out.position = uniforms.projectionMatrix * float4(in.position, 0, 1);\n" - " out.texCoords = in.texCoords;\n" - " out.color = float4(in.color) / float4(255.0);\n" - " return out;\n" - "}\n" - "\n" - "fragment half4 fragment_main(VertexOut in [[stage_in]],\n" - " texture2d texture [[texture(0)]],\n" - " sampler textureSampler [[sampler(0)]]) {\n" - " half4 texColor = texture.sample(textureSampler, in.texCoords);\n" - " return half4(in.color) * texColor;\n" - "}\n"; - - id library = [device newLibraryWithSource:shaderSource options:nil error:&error]; + id library = [device newLibraryWithSource:[NSString stringWithUTF8String:shaderCode] options:nil error:&error]; if (library == nil) { NSLog(@"Error: failed to create Metal library: %@", error); diff --git a/examples/example_sdl3_metal4/Makefile b/examples/example_sdl3_metal4/Makefile new file mode 100644 index 000000000..55c83416c --- /dev/null +++ b/examples/example_sdl3_metal4/Makefile @@ -0,0 +1,48 @@ +# +# You will need SDL3 (http://www.libsdl.org): +# brew install sdl3 +# + +#CXX = g++ +#CXX = clang++ + +EXE = example_sdl3_metal4 +IMGUI_DIR = ../.. +SOURCES = main.mm +SOURCES += $(IMGUI_DIR)/imgui.cpp $(IMGUI_DIR)/imgui_demo.cpp $(IMGUI_DIR)/imgui_draw.cpp $(IMGUI_DIR)/imgui_tables.cpp $(IMGUI_DIR)/imgui_widgets.cpp +SOURCES += $(IMGUI_DIR)/backends/imgui_impl_sdl3.cpp $(IMGUI_DIR)/backends/imgui_impl_metal4.mm +OBJS = $(addsuffix .o, $(basename $(notdir $(SOURCES)))) + +LIBS = -framework Metal -framework MetalKit -framework Cocoa -framework IOKit -framework CoreVideo -framework QuartzCore +LIBS += `pkg-config --libs sdl3` +LIBS += -L/usr/local/lib -L/opt/local/lib + +CXXFLAGS += `pkg-config --cflags sdl3` +CXXFLAGS += -I/usr/local/include -I/opt/local/include +CXXFLAGS += -std=c++11 -I$(IMGUI_DIR) -I$(IMGUI_DIR)/backends +CXXFLAGS += -Wall -Wformat +CFLAGS = $(CXXFLAGS) + +%.o:%.cpp + $(CXX) $(CXXFLAGS) -c -o $@ $< + +%.o:$(IMGUI_DIR)/%.cpp + $(CXX) $(CXXFLAGS) -c -o $@ $< + +%.o:$(IMGUI_DIR)/backends/%.cpp + $(CXX) $(CXXFLAGS) -c -o $@ $< + +%.o:%.mm + $(CXX) $(CXXFLAGS) -ObjC++ -fobjc-weak -fobjc-arc -c -o $@ $< + +%.o:$(IMGUI_DIR)/backends/%.mm + $(CXX) $(CXXFLAGS) -ObjC++ -fobjc-weak -fobjc-arc -c -o $@ $< + +all: $(EXE) + @echo Build complete + +$(EXE): $(OBJS) + $(CXX) -o $@ $^ $(CXXFLAGS) $(LIBS) + +clean: + rm -f $(EXE) $(OBJS) diff --git a/examples/example_sdl3_metal4/main.mm b/examples/example_sdl3_metal4/main.mm new file mode 100644 index 000000000..f0bb8f0ca --- /dev/null +++ b/examples/example_sdl3_metal4/main.mm @@ -0,0 +1,239 @@ +// Dear ImGui: standalone example application for SDL3 + Metal 4 +// (SDL is a cross-platform general purpose library for handling windows, inputs, OpenGL/Vulkan/Metal graphics context creation, etc.) + +// Learn about Dear ImGui: +// - FAQ https://dearimgui.com/faq +// - Getting Started https://dearimgui.com/getting-started +// - Documentation https://dearimgui.com/docs (same as your local docs/ folder). +// - Introduction, links and more at the top of imgui.cpp + +#include "imgui.h" +#include "imgui_impl_sdl3.h" +#include "imgui_impl_metal4.h" +#include // printf, fprintf +#include + +#import +#import + +#define FRAMES_IN_FLIGHT 3 + +// Main code +int main(int, char**) +{ + // Setup SDL + // [If using SDL_MAIN_USE_CALLBACKS: all code below until the main loop starts would likely be your SDL_AppInit() function] + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMEPAD)) + { + printf("Error: SDL_Init(): %s\n", SDL_GetError()); + return 1; + } + + // Create SDL window graphics context + float main_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay()); + SDL_WindowFlags window_flags = SDL_WINDOW_METAL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN | SDL_WINDOW_HIGH_PIXEL_DENSITY; + SDL_Window* window = SDL_CreateWindow("Dear ImGui SDL3+Metal example", (int)(1280 * main_scale), (int)(800 * main_scale), window_flags); + if (window == nullptr) + { + printf("Error: SDL_CreateWindow(): %s\n", SDL_GetError()); + return 1; + } + SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); + SDL_ShowWindow(window); + + // Create Metal device _before_ creating the view/layer + id metalDevice = MTLCreateSystemDefaultDevice(); + if (!metalDevice) + { + printf("Error: failed to create Metal device.\n"); + SDL_DestroyWindow(window); + SDL_Quit(); + return 1; + } + SDL_MetalView view = SDL_Metal_CreateView(window); + CAMetalLayer* layer = (__bridge CAMetalLayer*)SDL_Metal_GetLayer(view); + layer.device = metalDevice; + layer.pixelFormat = MTLPixelFormatBGRA8Unorm; + + id commandQueue = [layer.device newMTL4CommandQueue]; + id sharedEvent = [layer.device newSharedEvent]; + MTL4RenderPassDescriptor* renderPassDescriptor = [MTL4RenderPassDescriptor new]; + + [commandQueue addResidencySet:layer.residencySet]; + + id commandBuffers[FRAMES_IN_FLIGHT]; + id commandAllocators[FRAMES_IN_FLIGHT]; + for (int i = 0; i < FRAMES_IN_FLIGHT; i++) + { + commandAllocators[i] = [layer.device newCommandAllocator]; + commandBuffers[i] = [layer.device newCommandBuffer]; + } + uint32_t frameIndex = 0; + uint32_t frameInFlight = 0; + + // Setup Dear ImGui context + IMGUI_CHECKVERSION(); + ImGui::CreateContext(); + ImGuiIO& io = ImGui::GetIO(); (void)io; + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls + io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls + + // Setup Dear ImGui style + ImGui::StyleColorsDark(); + //ImGui::StyleColorsLight(); + + // Setup scaling + ImGuiStyle& style = ImGui::GetStyle(); + style.ScaleAllSizes(main_scale); // Bake a fixed style scale. (until we have a solution for dynamic style scaling, changing this requires resetting Style + calling this again) + style.FontScaleDpi = main_scale; // Set initial font scale. (in docking branch: using io.ConfigDpiScaleFonts=true automatically overrides this for every window depending on the current monitor) + + // Setup Platform/Renderer backends + ImGui_ImplMetal4_Init(layer.device, commandQueue, FRAMES_IN_FLIGHT); + ImGui_ImplSDL3_InitForMetal(window); + + // Load Fonts + // - If fonts are not explicitly loaded, Dear ImGui will select an embedded font: either AddFontDefaultVector() or AddFontDefaultBitmap(). + // This selection is based on (style.FontSizeBase * style.FontScaleMain * style.FontScaleDpi) reaching a small threshold. + // - You can load multiple fonts and use ImGui::PushFont()/PopFont() to select them. + // - If a file cannot be loaded, AddFont functions will return a nullptr. Please handle those errors in your code (e.g. use an assertion, display an error and quit). + // - Read 'docs/FONTS.md' for more instructions and details. + // - Use '#define IMGUI_ENABLE_FREETYPE' in your imconfig file to use FreeType for higher quality font rendering. + // - Remember that in C/C++ if you want to include a backslash \ in a string literal you need to write a double backslash \\ ! + //style.FontSizeBase = 20.0f; + //io.Fonts->AddFontDefaultVector(); + //io.Fonts->AddFontDefaultBitmap(); + //io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\segoeui.ttf"); + //io.Fonts->AddFontFromFileTTF("../../misc/fonts/DroidSans.ttf"); + //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Roboto-Medium.ttf"); + //io.Fonts->AddFontFromFileTTF("../../misc/fonts/Cousine-Regular.ttf"); + //ImFont* font = io.Fonts->AddFontFromFileTTF("c:\\Windows\\Fonts\\ArialUni.ttf"); + //IM_ASSERT(font != nullptr); + + // Our state + bool show_demo_window = true; + bool show_another_window = false; + float clear_color[4] = { 0.45f, 0.55f, 0.60f, 1.00f }; + + // Main loop + bool done = false; + while (!done) + { + @autoreleasepool + { + // Poll and handle events (inputs, window resize, etc.) + // You can read the io.WantCaptureMouse, io.WantCaptureKeyboard flags to tell if dear imgui wants to use your inputs. + // - When io.WantCaptureMouse is true, do not dispatch mouse input data to your main application, or clear/overwrite your copy of the mouse data. + // - When io.WantCaptureKeyboard is true, do not dispatch keyboard input data to your main application, or clear/overwrite your copy of the keyboard data. + // Generally you may always pass all inputs to dear imgui, and hide them from your application based on those two flags. + // [If using SDL_MAIN_USE_CALLBACKS: call ImGui_ImplSDL3_ProcessEvent() from your SDL_AppEvent() function] + SDL_Event event; + while (SDL_PollEvent(&event)) + { + ImGui_ImplSDL3_ProcessEvent(&event); + if (event.type == SDL_EVENT_QUIT) + done = true; + if (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED && event.window.windowID == SDL_GetWindowID(window)) + done = true; + } + + // [If using SDL_MAIN_USE_CALLBACKS: all code below would likely be your SDL_AppIterate() function] + if (SDL_GetWindowFlags(window) & SDL_WINDOW_MINIMIZED) + { + SDL_Delay(10); + continue; + } + + int width, height; + SDL_GetWindowSizeInPixels(window, &width, &height); + + layer.drawableSize = CGSizeMake(width, height); + id drawable = [layer nextDrawable]; + + uint64_t waitValue = (frameIndex >= FRAMES_IN_FLIGHT) ? (frameIndex - FRAMES_IN_FLIGHT + 1) : 0; + [sharedEvent waitUntilSignaledValue:waitValue timeoutMS:1000]; + [commandAllocators[frameInFlight] reset]; + [commandBuffers[frameInFlight] beginCommandBufferWithAllocator:commandAllocators[frameInFlight]]; + + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColorMake(clear_color[0] * clear_color[3], clear_color[1] * clear_color[3], clear_color[2] * clear_color[3], clear_color[3]); + renderPassDescriptor.colorAttachments[0].texture = drawable.texture; + renderPassDescriptor.colorAttachments[0].loadAction = MTLLoadActionClear; + renderPassDescriptor.colorAttachments[0].storeAction = MTLStoreActionStore; + id renderEncoder = [commandBuffers[frameInFlight] renderCommandEncoderWithDescriptor:renderPassDescriptor]; + [renderEncoder pushDebugGroup:@"ImGui demo"]; + + // Start the Dear ImGui frame + ImGui_ImplMetal4_NewFrame(renderPassDescriptor, frameInFlight); + ImGui_ImplSDL3_NewFrame(); + ImGui::NewFrame(); + + // 1. Show the big demo window (Most of the sample code is in ImGui::ShowDemoWindow()! You can browse its code to learn more about Dear ImGui!). + if (show_demo_window) + ImGui::ShowDemoWindow(&show_demo_window); + + // 2. Show a simple window that we create ourselves. We use a Begin/End pair to create a named window. + { + static float f = 0.0f; + static int counter = 0; + + ImGui::Begin("Hello, world!"); // Create a window called "Hello, world!" and append into it. + + ImGui::Text("This is some useful text."); // Display some text (you can use a format strings too) + ImGui::Checkbox("Demo Window", &show_demo_window); // Edit bools storing our window open/close state + ImGui::Checkbox("Another Window", &show_another_window); + + ImGui::SliderFloat("float", &f, 0.0f, 1.0f); // Edit 1 float using a slider from 0.0f to 1.0f + ImGui::ColorEdit3("clear color", (float*)&clear_color); // Edit 3 floats representing a color + + if (ImGui::Button("Button")) // Buttons return true when clicked (most widgets return true when edited/activated) + counter++; + ImGui::SameLine(); + ImGui::Text("counter = %d", counter); + + ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / io.Framerate, io.Framerate); + ImGui::End(); + } + + // 3. Show another simple window. + if (show_another_window) + { + ImGui::Begin("Another Window", &show_another_window); // Pass a pointer to our bool variable (the window will have a closing button that will clear the bool when clicked) + ImGui::Text("Hello from another window!"); + if (ImGui::Button("Close Me")) + show_another_window = false; + ImGui::End(); + } + + // Rendering + ImGui::Render(); + ImDrawData* draw_data = ImGui::GetDrawData(); + ImGui_ImplMetal4_RenderDrawData(draw_data, commandBuffers[frameInFlight], renderEncoder); + + [renderEncoder popDebugGroup]; + [renderEncoder endEncoding]; + [commandBuffers[frameInFlight] endCommandBuffer]; + + id to_commit[] = { commandBuffers[frameInFlight] }; + + [commandQueue waitForDrawable:drawable]; + [commandQueue commit:to_commit count:1]; + + [commandQueue signalEvent:sharedEvent value:frameIndex + 1]; + [commandQueue signalDrawable:drawable]; + [drawable present]; + + frameIndex++; + frameInFlight = (frameInFlight + 1) % FRAMES_IN_FLIGHT; + } + } + + // Cleanup + // [If using SDL_MAIN_USE_CALLBACKS: all code below would likely be your SDL_AppQuit() function] + ImGui_ImplMetal4_Shutdown(); + ImGui_ImplSDL3_Shutdown(); + ImGui::DestroyContext(); + + SDL_DestroyWindow(window); + SDL_Quit(); + + return 0; +} From caddd26fb915c4fc8030b30ed72c8be26123f3ab Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 2 Jul 2026 12:09:06 +0200 Subject: [PATCH 09/16] Backends: Metal4: minor tweaks/comments. (#9458, #9451) --- backends/imgui_impl_metal4.h | 3 +++ backends/imgui_impl_metal4.mm | 8 ++++---- docs/BACKENDS.md | 1 + docs/CHANGELOG.txt | 4 ++++ docs/EXAMPLES.md | 4 ++++ docs/README.md | 2 +- examples/example_sdl3_metal4/main.mm | 2 +- 7 files changed, 18 insertions(+), 6 deletions(-) diff --git a/backends/imgui_impl_metal4.h b/backends/imgui_impl_metal4.h index 7f9ec2d16..6bfea6e56 100644 --- a/backends/imgui_impl_metal4.h +++ b/backends/imgui_impl_metal4.h @@ -5,6 +5,9 @@ // [X] Renderer: User texture binding. Use 'MTLTexture.gpuResourceID' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef! // [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset). // [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures). +// Missing features or Issues: +// [ ] Metal-cpp support. +// [ ] Texture view pool support? Reevaluate which type to use for ImtextureID. // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. diff --git a/backends/imgui_impl_metal4.mm b/backends/imgui_impl_metal4.mm index 1114b470b..5f8d4983a 100644 --- a/backends/imgui_impl_metal4.mm +++ b/backends/imgui_impl_metal4.mm @@ -5,6 +5,9 @@ // [X] Renderer: User texture binding. Use 'MTLTexture' as texture identifier. Read the FAQ about ImTextureID/ImTextureRef! // [X] Renderer: Large meshes support (64k+ vertices) even with 16-bit indices (ImGuiBackendFlags_RendererHasVtxOffset). // [X] Renderer: Texture updates support for dynamic font atlas (ImGuiBackendFlags_RendererHasTextures). +// Missing features or Issues: +// [ ] Metal-cpp support. +// [ ] Texture view pool support? Reevaluate which type to use for ImtextureID. // You can use unmodified imgui_impl_* files in your project. See examples/ folder for examples of using this. // Prefer including the entire imgui/ repository into your project (either as a copy or as a submodule), and only build the backends you need. @@ -14,12 +17,9 @@ // - Documentation https://dearimgui.com/docs (same as your local docs/ folder). // - Introduction, links and more at the top of imgui.cpp -// FIXME: Metal-cpp support -// FIXME?: Texture view pool support - // CHANGELOG // (minor and older changes stripped away, please see git history for details) -// 2026-29-06: Metal 4: Added new Metal 4 backend implementation. +// 2026-07-02: Metal 4: Added new Metal 4 backend implementation. (#9458) #include "imgui.h" #ifndef IMGUI_DISABLE diff --git a/docs/BACKENDS.md b/docs/BACKENDS.md index 17e34dd3a..2fb9cfa71 100644 --- a/docs/BACKENDS.md +++ b/docs/BACKENDS.md @@ -89,6 +89,7 @@ List of Renderer Backends: imgui_impl_dx11.cpp ; DirectX11 imgui_impl_dx12.cpp ; DirectX12 imgui_impl_metal.mm ; Metal (ObjC or C++) + imgui_impl_metal4.mm ; Metal 4 (ObjC or C++) imgui_impl_opengl2.cpp ; OpenGL 2 (legacy fixed pipeline. Don't use with modern OpenGL code!) imgui_impl_opengl3.cpp ; OpenGL 3/4, OpenGL ES 2/3, WebGL imgui_impl_sdlgpu3.cpp ; SDL_GPU (portable 3D graphics API of SDL3) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 54a7422bb..bbef65980 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -127,6 +127,9 @@ Other Changes: - Misc: - Added IM_DEBUG_BREAK() handler for GCC+AArch64/ARM64. [@tom-seddon] - Backends: + - Metal4: + - Added new Metal 4 backend (forked from Metal 3 backend). (#9458, #9451) [@AmelieHeinrich] + Note that Metal-cpp is not yet supported. - OpenGL3: - GLSL version detection assume GLSL 410 when GL context is 4.1. Fixes an issue running on macOS with Wine. [#9427, #6577) [@perminovVS] @@ -139,6 +142,7 @@ Other Changes: - Examples: - SDL2/SDL3: use `SDL_GetWindowSizeInPixels()` to create frame-buffers. Fixes issues with non-fractional framebuffer size on Wayland. (#8761, #9124) [@billtran1632001] + - SDL3+Metal4: added new example. (#9458, #9451) [@AmelieHeinrich] ----------------------------------------------------------------------- VERSION 1.92.8 (Released 2026-05-12) diff --git a/docs/EXAMPLES.md b/docs/EXAMPLES.md index 676d745bc..3d410477b 100644 --- a/docs/EXAMPLES.md +++ b/docs/EXAMPLES.md @@ -164,6 +164,10 @@ SDL3 + DirectX11 examples, Windows only.
SDL3 + Metal example, Mac only.
= main.cpp + imgui_impl_sdl3.cpp + imgui_impl_metal.mm
+[example_sdl3_metal4/](https://github.com/ocornut/imgui/blob/master/examples/example_sdl3_metal/)
+SDL3 + Metal4 example, Mac only.
+= main.cpp + imgui_impl_sdl3.cpp + imgui_impl_metal4.mm
+ [example_sdl3_opengl3/](https://github.com/ocornut/imgui/blob/master/examples/example_sdl3_opengl3/)
SDL3 (Win32, Mac, Linux, etc.) + OpenGL3+/ES2/ES3 example.
= main.cpp + imgui_impl_sdl3.cpp + imgui_impl_opengl3.cpp
diff --git a/docs/README.md b/docs/README.md index cc73e28ec..91bc92cde 100644 --- a/docs/README.md +++ b/docs/README.md @@ -141,7 +141,7 @@ Integrating Dear ImGui within your custom engine is a matter of mainly 1) wiring - Generally, **make sure to spend time reading the [FAQ](https://www.dearimgui.com/faq), comments, and the examples applications!** Officially maintained backends (in repository): -- Renderers: DirectX9, DirectX10, DirectX11, DirectX12, Metal, OpenGL/ES/ES2, SDL_GPU, SDL_Renderer2/3, Vulkan, WebGPU. +- Renderers: DirectX9, DirectX10, DirectX11, DirectX12, Metal 3/4, OpenGL/ES/ES2, SDL_GPU, SDL_Renderer2/3, Vulkan, WebGPU. - Platforms: GLFW, SDL2/SDL3, Win32, Glut, OSX, Android. - Frameworks: Allegro5, Emscripten. diff --git a/examples/example_sdl3_metal4/main.mm b/examples/example_sdl3_metal4/main.mm index f0bb8f0ca..d80b4dde0 100644 --- a/examples/example_sdl3_metal4/main.mm +++ b/examples/example_sdl3_metal4/main.mm @@ -32,7 +32,7 @@ int main(int, char**) // Create SDL window graphics context float main_scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay()); SDL_WindowFlags window_flags = SDL_WINDOW_METAL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN | SDL_WINDOW_HIGH_PIXEL_DENSITY; - SDL_Window* window = SDL_CreateWindow("Dear ImGui SDL3+Metal example", (int)(1280 * main_scale), (int)(800 * main_scale), window_flags); + SDL_Window* window = SDL_CreateWindow("Dear ImGui SDL3+Metal4 example", (int)(1280 * main_scale), (int)(800 * main_scale), window_flags); if (window == nullptr) { printf("Error: SDL_CreateWindow(): %s\n", SDL_GetError()); From e9ea19086b4701b37237f53e1ab457a8a6b90f92 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 2 Jul 2026 14:12:32 +0200 Subject: [PATCH 10/16] CI: fixed Android build on Ubuntu-latest by expliciting Gradle version to < 9.x. --- .github/workflows/build.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc84ae2d3..3ec0800a1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -654,8 +654,12 @@ jobs: name: Build - Android steps: - - uses: actions/checkout@v6 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v6 + with: + gradle-version: '8.14.5' + - uses: actions/checkout@v6 - name: Build example_android_opengl3 run: | cd examples/example_android_opengl3/android From 6824e2c2e4b3e1314f8b84ccd870197cc038972c Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 2 Jul 2026 14:26:46 +0200 Subject: [PATCH 11/16] Examples: Android: update to AGP 9.2.0 to support Gradle 9.6.0. --- .github/workflows/build.yml | 8 ++++---- docs/CHANGELOG.txt | 2 ++ examples/example_android_opengl3/android/app/build.gradle | 4 ---- .../android/gradle/libs.versions.toml | 4 ++-- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3ec0800a1..c67d981c8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -654,10 +654,10 @@ jobs: name: Build - Android steps: - - name: Setup Gradle - uses: gradle/actions/setup-gradle@v6 - with: - gradle-version: '8.14.5' + #- name: Setup Gradle + # uses: gradle/actions/setup-gradle@v6 + # with: + # gradle-version: '8.14.5' - uses: actions/checkout@v6 - name: Build example_android_opengl3 diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index bbef65980..1119b7ab6 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -140,10 +140,12 @@ Other Changes: when available, fixing OpenGL DPI scaling issues as e.g. NVIDIA drivers tends to spawn multiple-thread to manage OpenGL. (#9403) - Examples: + - Android: update to AGP 9.2.0 to support Gradle 9.6.0. - SDL2/SDL3: use `SDL_GetWindowSizeInPixels()` to create frame-buffers. Fixes issues with non-fractional framebuffer size on Wayland. (#8761, #9124) [@billtran1632001] - SDL3+Metal4: added new example. (#9458, #9451) [@AmelieHeinrich] + ----------------------------------------------------------------------- VERSION 1.92.8 (Released 2026-05-12) ----------------------------------------------------------------------- diff --git a/examples/example_android_opengl3/android/app/build.gradle b/examples/example_android_opengl3/android/app/build.gradle index e142f1917..65555fb6e 100644 --- a/examples/example_android_opengl3/android/app/build.gradle +++ b/examples/example_android_opengl3/android/app/build.gradle @@ -1,6 +1,5 @@ plugins { alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) } android { @@ -25,9 +24,6 @@ android { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = '11' - } externalNativeBuild { cmake { path file('../../CMakeLists.txt') diff --git a/examples/example_android_opengl3/android/gradle/libs.versions.toml b/examples/example_android_opengl3/android/gradle/libs.versions.toml index 82971810b..43d9021eb 100644 --- a/examples/example_android_opengl3/android/gradle/libs.versions.toml +++ b/examples/example_android_opengl3/android/gradle/libs.versions.toml @@ -1,6 +1,6 @@ [versions] -agp = "8.12.0" -kotlin = "2.0.21" +agp = "9.2.0" +kotlin = "2.3.21" [plugins] android-application = { id = "com.android.application", version.ref = "agp" } From 8fc25800e0f4164c6edc4e9b00cc0e8d988d3b3c Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 2 Jul 2026 15:02:59 +0200 Subject: [PATCH 12/16] Fonts, Context: assert that ImFontAtlas has no other references when destroying an owned atlas. (#9426) --- docs/CHANGELOG.txt | 1 + imgui.cpp | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 1119b7ab6..aca07b0ad 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -95,6 +95,7 @@ Other Changes: calling ClearFonts() during rendering. - Fixed an issue where passing a manually created ImFontAtlas to CreateContext() would incorrectly destroy it in DestroyContext() when ref-count gets back to zero. (#9426) + - Destroying an ImGui context using a ImFontAtlas checks that the later has no references. - Nav: - Fixed context menu activation with gamepad erroneously testing for _NavEnableKeyboard instead of _NavEnableGamepad. (#9454, #8803, #9270) [@Clownacy] diff --git a/imgui.cpp b/imgui.cpp index a7d03caa4..2feefc854 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -4495,10 +4495,14 @@ void ImGui::Shutdown() for (ImFontAtlas* atlas : g.FontAtlases) { UnregisterFontAtlas(atlas); - if (atlas->RefCount == 0 && atlas->OwnerContext == &g) + if (atlas->OwnerContext == &g) { - atlas->Locked = false; - IM_DELETE(atlas); + IM_ASSERT(atlas->RefCount == 0 && "Destroying context owning a ImFontAtlas which is still used elsewhere!"); + if (atlas->RefCount == 0) + { + atlas->Locked = false; + IM_DELETE(atlas); + } } } g.DrawListSharedData.TempBuffer.clear(); From 7d43c74b591ff539097138a7a6596d3df7610252 Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 2 Jul 2026 15:13:49 +0200 Subject: [PATCH 13/16] CI: update PVS studio. Reduce Windows CI workload for typical push. --- .github/workflows/build.yml | 5 ++++- .github/workflows/static-analysis.yml | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c67d981c8..dd1d0d47e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -116,7 +116,7 @@ jobs: cl.exe /D_USRDLL /D_WINDLL /I. example_single_file.cpp /LD /FeImGui.dll /link cl.exe /DIMGUI_API=__declspec(dllimport) -DIMGUI_IMPL_API= /I. ImGui.lib /Feexample_null.exe examples/example_null/main.cpp - # Win64 examples are more frequently compilted than the Win32 examples. + # Win64 examples are more frequently compiled than the Win32 examples. # More of the Win32 examples requires 'workflow_run' to reduce waste. - name: Build Win32 example_glfw_opengl2 shell: cmd @@ -160,6 +160,7 @@ jobs: - name: Build Win32 example_sdl3_opengl3 shell: cmd run: '"%MSBUILD_PATH%\MSBuild.exe" examples/example_sdl3_opengl3/example_sdl3_opengl3.vcxproj /p:Platform=Win32 /p:Configuration=Release' + if: github.event_name == 'workflow_run' - name: Build Win32 example_sdl3_sdlgpu3 shell: cmd @@ -200,6 +201,7 @@ jobs: - name: Build Win64 example_glfw_opengl3 shell: cmd run: '"%MSBUILD_PATH%\MSBuild.exe" examples/example_glfw_opengl3/example_glfw_opengl3.vcxproj /p:Platform=x64 /p:Configuration=Release' + if: github.event_name == 'workflow_run' - name: Build Win64 example_glfw_vulkan shell: cmd @@ -228,6 +230,7 @@ jobs: - name: Build Win64 example_sdl2_directx11 shell: cmd run: '"%MSBUILD_PATH%\MSBuild.exe" examples/example_sdl2_directx11/example_sdl2_directx11.vcxproj /p:Platform=x64 /p:Configuration=Release' + if: github.event_name == 'workflow_run' - name: Build Win64 example_sdl3_opengl3 shell: cmd diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 295628368..231d3a972 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -23,9 +23,9 @@ jobs: run: | if [[ "$PVS_STUDIO_LICENSE" != "" ]]; then - wget -q https://files.viva64.com/etc/pubkey.txt + wget -q https://files.pvs-studio.com/etc/pubkey.txt sudo apt-key add pubkey.txt - sudo wget -O /etc/apt/sources.list.d/viva64.list https://files.viva64.com/etc/viva64.list + sudo wget -O /etc/apt/sources.list.d/viva64.list https://files.pvs-studio.com/etc/viva64.list sudo apt-get update sudo apt-get install -y pvs-studio pvs-studio-analyzer credentials -o pvs-studio.lic $PVS_STUDIO_LICENSE From f7e8343ee97726e8e74e19097364dc77ccde79eb Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 25 Jun 2026 23:00:37 +0200 Subject: [PATCH 14/16] Settings, IO: added io.ConfigIniSettingsSaveLastUsedDate, platform_io.Platform_SessionDate, IMGUI_DISABLE_TIME_FUNCTIONS(). (#9460) cc #437 --- docs/CHANGELOG.txt | 8 ++++++++ imconfig.h | 1 + imgui.cpp | 27 +++++++++++++++++++++++++-- imgui.h | 7 +++++++ imgui_demo.cpp | 3 +++ imgui_internal.h | 18 ++++++++++++++++++ imgui_tables.cpp | 6 ++++++ 7 files changed, 68 insertions(+), 2 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index aca07b0ad..dba212f14 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -116,6 +116,14 @@ Other Changes: The use of this is discouraged because it can easily create problems rendering e.g. contiguous selection. - Scale the NavCursor border thickness when using large values with `ScallAllSizes()`. +- Settings: + - Windows/Tables settings entries can now record the last used date in YYYYMMDD format, + allowing cleanup tools to run to e.g. delete entries that haven't been used in X months. + - Added bool io.ConfigIniSettingsSaveLastUsedDate to disable saving that info. + - The current system date is fed through ImGuiPlatformIO::Platform_SessionDate, + which is automatically set by a call to time() done during context creation. + - Added IMGUI_DISABLE_TIME_FUNCTIONS to disable setting platform_io.Platform_SessionDate. + A custom backend may still set it manually. - DrawList: - Minor optimization to `AddLine()`, `AddLineH()`, `AddLineV()` functions. (#4091) - Added `ImDrawListFlags_TextNoPixelSnap` to disable snapping of AddText() diff --git a/imconfig.h b/imconfig.h index 851a44b1b..b40db3898 100644 --- a/imconfig.h +++ b/imconfig.h @@ -43,6 +43,7 @@ //#define IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS // [Win32] [Default with non-Visual Studio compilers] Don't implement default IME handler (won't require imm32.lib/.a) //#define IMGUI_DISABLE_WIN32_FUNCTIONS // [Win32] Won't use and link with any Win32 function (clipboard, IME). //#define IMGUI_ENABLE_OSX_DEFAULT_CLIPBOARD_FUNCTIONS // [OSX] Implement default OSX clipboard handler (need to link with '-framework ApplicationServices', this is why this is not the default). +//#define IMGUI_DISABLE_TIME_FUNCTIONS // Don't setup default platform_io.Platform_SessionDate value using time(), localtime_r(). //#define IMGUI_DISABLE_DEFAULT_SHELL_FUNCTIONS // Don't implement default platform_io.Platform_OpenInShellFn() handler (Win32: ShellExecute(), require shell32.lib/.a, Mac/Linux: use system("")). //#define IMGUI_DISABLE_DEFAULT_FORMAT_FUNCTIONS // Don't implement ImFormatString/ImFormatStringV so you can implement them yourself (e.g. if you don't want to link with vsnprintf) //#define IMGUI_DISABLE_DEFAULT_MATH_FUNCTIONS // Don't implement ImFabs/ImSqrt/ImPow/ImFmod/ImCos/ImSin/ImAcos/ImAtan2 so you can implement them yourself. diff --git a/imgui.cpp b/imgui.cpp index 2feefc854..844ca0342 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -1243,6 +1243,12 @@ IMPLEMENTING SUPPORT for ImGuiBackendFlags_RendererHasTextures: // System includes #include // vsnprintf, sscanf, printf #include // intptr_t +#ifndef IMGUI_DISABLE_TIME_FUNCTIONS +#include // time(), localtime_r()/localtime_s() +#if defined(_WIN32) +static tm* localtime_r(const time_t* timep, tm* result) { return localtime_s(result, timep) == 0 ? result : NULL; } +#endif +#endif // [Windows] On non-Visual Studio compilers, we default to IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS unless explicitly enabled #if defined(_WIN32) && !defined(_MSC_VER) && !defined(IMGUI_ENABLE_WIN32_DEFAULT_IME_FUNCTIONS) && !defined(IMGUI_DISABLE_WIN32_DEFAULT_IME_FUNCTIONS) @@ -1677,6 +1683,7 @@ ImGuiIO::ImGuiIO() ConfigWindowsMoveFromTitleBarOnly = false; ConfigWindowsCopyContentsWithCtrlC = false; ConfigScrollbarScrollByPage = true; + ConfigIniSettingsSaveLastUsedDate = true; ConfigMemoryCompactTimer = 60.0f; ConfigDebugIsDebuggerPresent = false; ConfigDebugHighlightIdConflicts = true; @@ -4452,6 +4459,14 @@ void ImGui::Initialize() g.PlatformIO.Platform_OpenInShellFn = Platform_OpenInShellFn_DefaultImpl; g.PlatformIO.Platform_SetImeDataFn = Platform_SetImeDataFn_DefaultImpl; + // Setup session starting date +#ifndef IMGUI_DISABLE_TIME_FUNCTIONS + const time_t session_time = time(NULL); + struct tm session_datetime = {}; + if (localtime_r(&session_time, &session_datetime)) + g.PlatformIO.Platform_SessionDate = (session_datetime.tm_year + 1900) * 10000 + (session_datetime.tm_mon + 1) * 100 + session_datetime.tm_mday; +#endif + // Create default viewport ImGuiViewportP* viewport = IM_NEW(ImGuiViewportP)(); viewport->ID = IMGUI_VIEWPORT_DEFAULT_ID; @@ -5568,6 +5583,7 @@ void ImGui::NewFrame() g.Time += g.IO.DeltaTime; g.FrameCount += 1; + g.SessionDate = ImGuiPackedDate(g.PlatformIO.Platform_SessionDate); g.TooltipOverrideCount = 0; g.WindowsActiveCount = 0; g.MenusIdSubmittedThisFrame.resize(0); @@ -6692,6 +6708,7 @@ static void InitOrLoadWindowSettings(ImGuiWindow* window, ImGuiWindowSettings* s { // Initial window state with e.g. default/arbitrary window position // Use SetNextWindowPos() with the appropriate condition flag to change the initial position of a window. + ImGuiContext& g = *GImGui; const ImGuiViewport* main_viewport = ImGui::GetMainViewport(); window->Pos = main_viewport->Pos + ImVec2(60, 60); window->Size = window->SizeFull = ImVec2(0, 0); @@ -6699,6 +6716,7 @@ static void InitOrLoadWindowSettings(ImGuiWindow* window, ImGuiWindowSettings* s if (settings != NULL) { + settings->LastUsedDate = g.SessionDate; SetWindowConditionAllowFlags(window, ImGuiCond_FirstUseEver, false); ApplyWindowSettings(window, settings); } @@ -15870,6 +15888,7 @@ static void WindowSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, else if (sscanf(line, "Size=%i,%i", &x, &y) == 2) { settings->Size = ImVec2ih((short)x, (short)y); } else if (sscanf(line, "Collapsed=%d", &i) == 1) { settings->Collapsed = (i != 0); } else if (sscanf(line, "IsChild=%d", &i) == 1) { settings->IsChild = (i != 0); } + else if (sscanf(line, "LastUsed=%d", &i) == 1) { settings->LastUsedDate = i; return; } } // Apply to existing windows (if any) @@ -15907,6 +15926,7 @@ static void WindowSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandl settings->IsChild = (window->Flags & ImGuiWindowFlags_ChildWindow) != 0; settings->Collapsed = window->Collapsed; settings->WantDelete = false; + settings->LastUsedDate = g.SessionDate; } // Write to text buffer @@ -15916,7 +15936,7 @@ static void WindowSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandl if (settings->WantDelete) continue; const char* settings_name = settings->GetName(); - buf->appendf("[%s][%s]\n", handler->TypeName, settings_name); + buf->appendf("[%s][%s]\n", handler->TypeName, settings_name); // [Window][name] if (settings->IsChild) { buf->appendf("IsChild=1\n"); @@ -15929,6 +15949,9 @@ static void WindowSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandl if (settings->Collapsed) buf->appendf("Collapsed=1\n"); } + if (g.IO.ConfigIniSettingsSaveLastUsedDate) + if (int last_used_date = settings->LastUsedDate.Unpack()) + buf->appendf("LastUsed=%08d\n", last_used_date); buf->append("\n"); } } @@ -17866,7 +17889,7 @@ void ImGui::DebugNodeWindowSettings(ImGuiWindowSettings* settings) { if (settings->WantDelete) BeginDisabled(); - Text("0x%08X \"%s\" Pos (%d,%d) Size (%d,%d) Collapsed=%d", + BulletText("0x%08X \"%s\" Pos (%d,%d) Size (%d,%d) Collapsed=%d", settings->ID, settings->GetName(), settings->Pos.x, settings->Pos.y, settings->Size.x, settings->Size.y, settings->Collapsed); if (settings->WantDelete) EndDisabled(); diff --git a/imgui.h b/imgui.h index 0d4995699..d469e0fb8 100644 --- a/imgui.h +++ b/imgui.h @@ -2436,6 +2436,9 @@ struct ImGuiIO bool ConfigNavCursorVisibleAuto; // = true // Using directional navigation key makes the cursor visible. Mouse click hides the cursor. bool ConfigNavCursorVisibleAlways; // = false // Navigation cursor is always visible. + // Ini Settings + bool ConfigIniSettingsSaveLastUsedDate;// = true // Enable loading/saving last used day (YYYYMMDD) in some .ini struct, making things easier to audit and allowing custom tools to cleanup old data. + // Miscellaneous options // (you can visualize and interact with all options in 'Demo->Configuration') bool MouseDrawCursor; // = false // Request ImGui to draw a mouse cursor for you (if you are on a platform without a mouse cursor). Cannot be easily renamed to 'io.ConfigXXX' because this is frequently used by backend implementations. @@ -4039,6 +4042,10 @@ struct ImGuiPlatformIO // [Experimental] Configure decimal point e.g. '.' or ',' useful for some languages (e.g. German), generally pulled from *localeconv()->decimal_point ImWchar Platform_LocaleDecimalPoint; // '.' + // Optional: Platform time/date + // This is automatically filled on startup. Used to store a "last used date" in some .ini structures. Facilitate creating tools to clean up old/unused data. + int Platform_SessionDate; // Integer storing YYYYMMDD e.g. 20261231 corresponding to the beginning of application session. + //------------------------------------------------------------------ // Input - Interface with Renderer Backend //------------------------------------------------------------------ diff --git a/imgui_demo.cpp b/imgui_demo.cpp index 1bd300666..91d7e030d 100644 --- a/imgui_demo.cpp +++ b/imgui_demo.cpp @@ -538,6 +538,9 @@ void ImGui::ShowDemoWindow(bool* p_open) ImGui::SameLine(); HelpMarker("Swap Cmd<>Ctrl keys, enable various MacOS style behaviors."); ImGui::Text("Also see Style->Rendering for rendering options."); + ImGui::SeparatorText("Settings"); + ImGui::Checkbox("io.ConfigIniSettingsSaveLastUsedDate", &io.ConfigIniSettingsSaveLastUsedDate); + // Also read: https://github.com/ocornut/imgui/wiki/Error-Handling ImGui::SeparatorText("Error Handling"); diff --git a/imgui_internal.h b/imgui_internal.h index 299287e65..c7e1a03b4 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -138,6 +138,7 @@ Index of this file: struct ImBitVector; // Store 1-bit per value struct ImRect; // An axis-aligned rectangle (2 points) struct ImGuiTextIndex; // Maintain a line index for a text buffer. +struct ImGuiPackedDate; // A date in YYYYMMDD format packed into 16-bits // ImDrawList/ImFontAtlas struct ImDrawDataBuilder; // Helper to build a ImDrawData instance @@ -833,6 +834,20 @@ struct ImGuiTextIndex void append(const char* base, int old_size, int new_size); }; +// Helper: ImGuiPackedDate (sizeof() == 2) +// Store a date in a way that is efficient to read/write in text form. If we stored e.g. number of days since Epoch we'd need costlier back and forth. +// This is specifically designed to be able to prune old .ini data. +struct ImGuiPackedDate +{ + ImU16 Year : 7; // Year since 2000 // We can change to another offset e.g. 1970 but this is easier to watch in debugger. + ImU16 Month : 4; // Month (1-12) + ImU16 Day : 5; // Day (1-31) + + ImGuiPackedDate() { Year = Month = Day = 0; } + ImGuiPackedDate(int yyyymmdd) { Year = (ImU16)((yyyymmdd / 10000) - 2000); Month = (ImU16)((yyyymmdd / 100) % 100); Day = (ImU16)(yyyymmdd % 100); } // Pack + int Unpack() const { return (Year && Month && Day) ? ((Year + 2000) * 10000) + (Month * 100) + Day : 0; } // Unpack +}; + // Helper: ImGuiStorage IMGUI_API ImGuiStoragePair* ImLowerBound(ImGuiStoragePair* in_begin, ImGuiStoragePair* in_end, ImGuiID key); @@ -2018,6 +2033,7 @@ struct ImGuiWindowSettings ImGuiID ID; ImVec2ih Pos; ImVec2ih Size; + ImGuiPackedDate LastUsedDate; bool Collapsed : 1; bool IsChild : 1; bool WantApply : 1; // Set when loaded from .ini data (to enable merging/loading .ini data into an already running context) @@ -2533,6 +2549,7 @@ struct ImGuiContext ImVector UserTextures; // List of textures created/managed by user or third-party extension. Automatically appended into platform_io.Textures[]. // Settings + ImGuiPackedDate SessionDate; // Packed copy of platform_io.Platform_SessionDate, when valid. bool SettingsLoaded; float SettingsDirtyTimer; // Save .ini Settings to memory when time reaches zero ImGuiTextBuffer SettingsIniData; // In memory .ini settings @@ -3208,6 +3225,7 @@ struct ImGuiTableSettings float RefScale; // Reference scale to be able to rescale columns on font/dpi changes. ImGuiTableColumnIdx ColumnsCount; ImGuiTableColumnIdx ColumnsCountMax; // Maximum number of columns this settings instance can store, we can recycle a settings instance with lower number of columns but not higher + ImGuiPackedDate LastUsedDate; bool WantApply : 1; // Set when loaded from .ini data (to enable merging/loading .ini data into an already running context) ImGuiTableSettings() { memset((void*)this, 0, sizeof(*this)); } diff --git a/imgui_tables.cpp b/imgui_tables.cpp index b71304f25..1ff368239 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -3933,6 +3933,7 @@ void ImGui::TableSaveSettings(ImGuiTable* table) table->SettingsOffset = g.SettingsTables.offset_from_ptr(settings); } settings->ColumnsCount = (ImGuiTableColumnIdx)table->ColumnsCount; + settings->LastUsedDate = g.SessionDate; // Serialize ImGuiTable/ImGuiTableColumn into ImGuiTableSettings/ImGuiTableColumnSettings IM_ASSERT(settings->ID == table->ID); @@ -4001,6 +4002,7 @@ void ImGui::TableLoadSettings(ImGuiTable* table) table->SettingsLoadedFlags = settings->SaveFlags; table->RefScale = settings->RefScale; + settings->LastUsedDate = g.SessionDate; // TableUpdateLayout() will then call TableLoadSettingsForColumns() to apply the data. } @@ -4163,6 +4165,7 @@ static void TableSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, int column_n = 0, r = 0, n = 0; if (sscanf(line, "RefScale=%f", &f) == 1) { settings->RefScale = f; return; } + if (sscanf(line, "LastUsed=%d", &n) == 1) { settings->LastUsedDate = n; return; } if (sscanf(line, "Column %d%n", &column_n, &r) == 1) { @@ -4218,6 +4221,9 @@ static void TableSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandle if (column->ID != 0) { buf->appendf(" ID=0x%08X", column->ID); } buf->append("\n"); } + if (g.IO.ConfigIniSettingsSaveLastUsedDate) + if (int last_used_date = settings->LastUsedDate.Unpack()) + buf->appendf("LastUsed=%08d\n", last_used_date); buf->append("\n"); } } From 5fb77dfde1260690af678a1b086288c073db89cd Mon Sep 17 00:00:00 2001 From: ocornut Date: Thu, 2 Jul 2026 18:28:56 +0200 Subject: [PATCH 15/16] Settings: added io.ConfigIniSettingsAutoDiscardMonths, trimming tool in in Metrics->Settings + internal CleanupIniSettings(). (#9460) cc #437 #2564 --- docs/CHANGELOG.txt | 11 +++++--- imgui.cpp | 69 +++++++++++++++++++++++++++++++++++++++++++++- imgui.h | 1 + imgui_internal.h | 15 ++++++++++ imgui_tables.cpp | 16 +++++++++++ 5 files changed, 107 insertions(+), 5 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index dba212f14..4647ca97f 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -118,12 +118,15 @@ Other Changes: - Scale the NavCursor border thickness when using large values with `ScallAllSizes()`. - Settings: - Windows/Tables settings entries can now record the last used date in YYYYMMDD format, - allowing cleanup tools to run to e.g. delete entries that haven't been used in X months. - - Added bool io.ConfigIniSettingsSaveLastUsedDate to disable saving that info. + allowing tools to run to e.g. delete entries that haven't been used in X months. (#9460) + - Added bool io.ConfigIniSettingsSaveLastUsedDate to disable saving that info. (#9460) + - Added int io.ConfigIniSettingsAutoDiscardMonths to enable a mode where unused settings + are automatically discard after xx months. (#9460) + - Added a trimming tool under Metrics->Settings, along with a yet-unexposed function. - The current system date is fed through ImGuiPlatformIO::Platform_SessionDate, - which is automatically set by a call to time() done during context creation. + which is automatically set by a call to time() done during context creation. (#9460) - Added IMGUI_DISABLE_TIME_FUNCTIONS to disable setting platform_io.Platform_SessionDate. - A custom backend may still set it manually. + A custom backend may still set it manually. (#9460) - DrawList: - Minor optimization to `AddLine()`, `AddLineH()`, `AddLineV()` functions. (#4091) - Added `ImDrawListFlags_TextNoPixelSnap` to disable snapping of AddText() diff --git a/imgui.cpp b/imgui.cpp index 844ca0342..f1729eb84 100644 --- a/imgui.cpp +++ b/imgui.cpp @@ -1365,6 +1365,7 @@ static void AddWindowToSortBuffer(ImVector* out_sorted // Settings static void WindowSettingsHandler_ClearAll(ImGuiContext*, ImGuiSettingsHandler*); +static void WindowSettingsHandler_Cleanup(ImGuiContext*, ImGuiSettingsHandler*, ImGuiSettingsCleanupArgs* args); static void* WindowSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name); static void WindowSettingsHandler_ReadLine(ImGuiContext*, ImGuiSettingsHandler*, void* entry, const char* line); static void WindowSettingsHandler_ApplyAll(ImGuiContext*, ImGuiSettingsHandler*); @@ -1684,6 +1685,7 @@ ImGuiIO::ImGuiIO() ConfigWindowsCopyContentsWithCtrlC = false; ConfigScrollbarScrollByPage = true; ConfigIniSettingsSaveLastUsedDate = true; + ConfigIniSettingsAutoDiscardMonths = 0; ConfigMemoryCompactTimer = 60.0f; ConfigDebugIsDebuggerPresent = false; ConfigDebugHighlightIdConflicts = true; @@ -4442,6 +4444,7 @@ void ImGui::Initialize() ini_handler.TypeName = "Window"; ini_handler.TypeHash = ImHashStr("Window"); ini_handler.ClearAllFn = WindowSettingsHandler_ClearAll; + ini_handler.CleanupFn = WindowSettingsHandler_Cleanup; ini_handler.ReadOpenFn = WindowSettingsHandler_ReadOpen; ini_handler.ReadLineFn = WindowSettingsHandler_ReadLine; ini_handler.ApplyAllFn = WindowSettingsHandler_ApplyAll; @@ -15692,6 +15695,19 @@ void ImGui::ClearIniSettings() handler.ClearAllFn(&g, &handler); } +void ImGui::CleanupIniSettings(ImGuiSettingsCleanupArgs* args) +{ + ImGuiContext& g = *GImGui; + if (g.PlatformIO.Platform_SessionDate == 0) + return; + ImGuiPackedDate discard_older_than_date_p = g.PlatformIO.Platform_SessionDate; + discard_older_than_date_p.SubtractMonths(args->DiscardOlderThanMonths); + args->_DiscardOlderThanDate = discard_older_than_date_p.Unpack(); + for (ImGuiSettingsHandler& handler : g.SettingsHandlers) + if (handler.CleanupFn != NULL) + handler.CleanupFn(&g, &handler, args); +} + void ImGui::LoadIniSettingsFromDisk(const char* ini_filename) { size_t file_data_size = 0; @@ -15770,6 +15786,9 @@ void ImGui::LoadIniSettingsFromMemory(const char* ini_data, size_t ini_size) memcpy(buf, ini_data, ini_size); // Call post-read handlers + ImGuiSettingsCleanupArgs cleanup_args; + if (g.IO.ConfigIniSettingsAutoDiscardMonths > 0) + cleanup_args.DiscardOlderThanMonths = g.IO.ConfigIniSettingsAutoDiscardMonths; for (ImGuiSettingsHandler& handler : g.SettingsHandlers) if (handler.ApplyAllFn != NULL) handler.ApplyAllFn(&g, &handler); @@ -15866,6 +15885,18 @@ static void WindowSettingsHandler_ClearAll(ImGuiContext* ctx, ImGuiSettingsHandl g.SettingsWindows.clear(); } +static void WindowSettingsHandler_Cleanup(ImGuiContext* ctx, ImGuiSettingsHandler*, ImGuiSettingsCleanupArgs* args) +{ + ImGuiContext& g = *ctx; + for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + { + if (args->_DiscardOlderThanDate != 0 && settings->LastUsedDate.Unpack() < args->_DiscardOlderThanDate) + settings->WantDelete = true; + if (args->SetCurrentSessionDateToAll || (args->SetCurrentSessionDateWhenMissingDate && settings->LastUsedDate.IsValid() == false)) + settings->LastUsedDate = g.SessionDate; + } +} + static void* WindowSettingsHandler_ReadOpen(ImGuiContext*, ImGuiSettingsHandler*, const char* name) { ImGuiID id = ImHashStr(name); @@ -15918,6 +15949,7 @@ static void WindowSettingsHandler_WriteAll(ImGuiContext* ctx, ImGuiSettingsHandl if (!settings) { settings = ImGui::CreateNewWindowSettings(window->Name); + settings->LastUsedDate = g.SessionDate; window->SettingsOffset = g.SettingsWindows.offset_from_ptr(settings); } IM_ASSERT(settings->ID == window->ID); @@ -16724,6 +16756,7 @@ void ImGui::ShowMetricsWindow(bool* p_open) { ImGuiContext& g = *GImGui; ImGuiIO& io = g.IO; + ImGuiPlatformIO& platform_io = g.PlatformIO; ImGuiMetricsConfig* cfg = &g.DebugMetricsConfig; if (cfg->ShowDebugLog) ShowDebugLogWindow(&cfg->ShowDebugLog); @@ -17056,8 +17089,36 @@ void ImGui::ShowMetricsWindow(bool* p_open) Text("\"%s\"", g.IO.IniFilename); else TextUnformatted(""); - Checkbox("io.ConfigDebugIniSettings", &io.ConfigDebugIniSettings); Text("SettingsDirtyTimer %.2f", g.SettingsDirtyTimer); + + int highlight_older_than_date = 0; + Text("SessionDate: %d", platform_io.Platform_SessionDate); + BeginDisabled(platform_io.Platform_SessionDate == 0); + Checkbox("Highlight Entries Older Than", &cfg->SettingsHighlightOldEntries); + SetNextItemWidth(GetFontSize() * 8); + SameLine(); + SliderInt("Months", &cfg->SettingsDiscardMonths, 1, 24); + if (cfg->SettingsHighlightOldEntries && cfg->SettingsDiscardMonths > 0) + { + ImGuiPackedDate cutoff_date = platform_io.Platform_SessionDate; + cutoff_date.SubtractMonths(cfg->SettingsDiscardMonths); + highlight_older_than_date = cutoff_date.Unpack(); + SameLine(); + ImGuiSettingsCleanupArgs cleanup_args; + cleanup_args.DiscardOlderThanMonths = cfg->SettingsDiscardMonths; + if (Button("Discard")) + CleanupIniSettings(&cleanup_args); + } + EndDisabled(); + Checkbox("io.ConfigDebugIniSettings", &io.ConfigDebugIniSettings); + + struct ScopedHighlightOlderThan + { + bool Highlight; + ScopedHighlightOlderThan(int cutoff_date, ImGuiPackedDate in_date) { Highlight = cutoff_date != 0 && in_date.Unpack() < cutoff_date; if (Highlight) PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.4f, 0.4f, 1.0f)); } + ~ScopedHighlightOlderThan() { if (Highlight) PopStyleColor(); } + }; + if (TreeNode("SettingsHandlers", "Settings handlers: (%d)", g.SettingsHandlers.Size)) { for (ImGuiSettingsHandler& handler : g.SettingsHandlers) @@ -17067,14 +17128,20 @@ void ImGui::ShowMetricsWindow(bool* p_open) if (TreeNode("SettingsWindows", "Settings packed data: Windows: %d bytes", g.SettingsWindows.size())) { for (ImGuiWindowSettings* settings = g.SettingsWindows.begin(); settings != NULL; settings = g.SettingsWindows.next_chunk(settings)) + { + ScopedHighlightOlderThan scoped_highlight(highlight_older_than_date, settings->LastUsedDate); DebugNodeWindowSettings(settings); + } TreePop(); } if (TreeNode("SettingsTables", "Settings packed data: Tables: %d bytes", g.SettingsTables.size())) { for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + { + ScopedHighlightOlderThan scoped_highlight(highlight_older_than_date, settings->LastUsedDate); DebugNodeTableSettings(settings, NULL); + } TreePop(); } diff --git a/imgui.h b/imgui.h index d469e0fb8..59aba2875 100644 --- a/imgui.h +++ b/imgui.h @@ -2438,6 +2438,7 @@ struct ImGuiIO // Ini Settings bool ConfigIniSettingsSaveLastUsedDate;// = true // Enable loading/saving last used day (YYYYMMDD) in some .ini struct, making things easier to audit and allowing custom tools to cleanup old data. + int ConfigIniSettingsAutoDiscardMonths; // = 0 // [BETA] Set number of months after which unused .ini entries are discarded on load. Require platform_io.Platform_SessionDate to be set. For systems supporting the feature, .ini entries without a LastUsed field will always be discarded! Please report if you are using this. // Miscellaneous options // (you can visualize and interact with all options in 'Demo->Configuration') diff --git a/imgui_internal.h b/imgui_internal.h index c7e1a03b4..129981aa5 100644 --- a/imgui_internal.h +++ b/imgui_internal.h @@ -98,6 +98,7 @@ Index of this file: #pragma clang diagnostic ignored "-Wimplicit-int-float-conversion" // warning: implicit conversion from 'xxx' to 'float' may lose precision #pragma clang diagnostic ignored "-Wmissing-noreturn" // warning: function 'xxx' could be declared with attribute 'noreturn' #pragma clang diagnostic ignored "-Wdeprecated-enum-enum-conversion"// warning: bitwise operation between different enumeration types ('XXXFlags_' and 'XXXFlagsPrivate_') is deprecated +#pragma clang diagnostic ignored "-Wreserved-identifier" // warning: identifier '_Xxx' is reserved because it starts with '_' followed by a capital letter #pragma clang diagnostic ignored "-Wunsafe-buffer-usage" // warning: 'xxx' is an unsafe pointer used for buffer access #pragma clang diagnostic ignored "-Wnontrivial-memaccess" // warning: first argument in call to 'memset' is a pointer to non-trivially copyable type #elif defined(__GNUC__) @@ -845,7 +846,9 @@ struct ImGuiPackedDate ImGuiPackedDate() { Year = Month = Day = 0; } ImGuiPackedDate(int yyyymmdd) { Year = (ImU16)((yyyymmdd / 10000) - 2000); Month = (ImU16)((yyyymmdd / 100) % 100); Day = (ImU16)(yyyymmdd % 100); } // Pack + bool IsValid() { return (Year && Month && Day); } int Unpack() const { return (Year && Month && Day) ? ((Year + 2000) * 10000) + (Month * 100) + Day : 0; } // Unpack + void SubtractMonths(int m) { while (m > 0) { Year -= Month == 1; Month = (Month == 1) ? 12 : Month - 1; m--; } } // FIXME-OPT: Stupid but enough for what we do with it. }; // Helper: ImGuiStorage @@ -2043,6 +2046,14 @@ struct ImGuiWindowSettings char* GetName() { return (char*)(this + 1); } }; +struct ImGuiSettingsCleanupArgs +{ + int DiscardOlderThanMonths = 0; // Enable to discard entries older than XX months. + bool SetCurrentSessionDateToAll = false; // Enable to write current SessionDate to all supporting entries. // Let us know in #9460 if you use this. + bool SetCurrentSessionDateWhenMissingDate = false; // Enable to write current SessionDate to all supporting entries missing a date. // Let us know in #9460 if you use this. + int _DiscardOlderThanDate = 0; // [Internal] +}; + struct ImGuiSettingsHandler { const char* TypeName; // Short description stored in .ini file. Disallowed characters: '[' ']' @@ -2053,6 +2064,7 @@ struct ImGuiSettingsHandler void (*ReadLineFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, void* entry, const char* line); // Read: Called for every line of text within an ini entry void (*ApplyAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler); // Read: Called after reading (in registration order) void (*WriteAllFn)(ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiTextBuffer* out_buf); // Write: Output every entries into 'out_buf' + void (*CleanupFn) (ImGuiContext* ctx, ImGuiSettingsHandler* handler, ImGuiSettingsCleanupArgs* args);// Cleanup/patch settings void* UserData; ImGuiSettingsHandler() { memset((void*)this, 0, sizeof(*this)); } @@ -2166,6 +2178,8 @@ struct ImGuiMetricsConfig int ShowTablesRectsType = -1; int HighlightMonitorIdx = -1; ImGuiID HighlightViewportID = 0; + int SettingsDiscardMonths = 6; + bool SettingsHighlightOldEntries = false; bool ShowFontPreview = true; }; @@ -3410,6 +3424,7 @@ namespace ImGui IMGUI_API void MarkIniSettingsDirty(); IMGUI_API void MarkIniSettingsDirty(ImGuiWindow* window); IMGUI_API void ClearIniSettings(); + IMGUI_API void CleanupIniSettings(ImGuiSettingsCleanupArgs* args); // [BETA] Expected to turn into a public API. Please report if you are using this! IMGUI_API void AddSettingsHandler(const ImGuiSettingsHandler* handler); IMGUI_API void RemoveSettingsHandler(const char* type_name); IMGUI_API ImGuiSettingsHandler* FindSettingsHandler(const char* type_name); diff --git a/imgui_tables.cpp b/imgui_tables.cpp index 1ff368239..aa0b89efe 100644 --- a/imgui_tables.cpp +++ b/imgui_tables.cpp @@ -4126,6 +4126,21 @@ static void TableSettingsHandler_ClearAll(ImGuiContext* ctx, ImGuiSettingsHandle g.SettingsTables.clear(); } +static void TableSettingsHandler_Cleanup(ImGuiContext* ctx, ImGuiSettingsHandler*, ImGuiSettingsCleanupArgs* args) +{ + ImGuiContext& g = *ctx; + for (int i = 0; i != g.Tables.GetMapSize(); i++) + if (ImGuiTable* table = g.Tables.TryGetMapData(i)) + table->SettingsOffset = -1; + for (ImGuiTableSettings* settings = g.SettingsTables.begin(); settings != NULL; settings = g.SettingsTables.next_chunk(settings)) + { + if (args->_DiscardOlderThanDate != 0 && settings->LastUsedDate.Unpack() < args->_DiscardOlderThanDate) + settings->ID = 0; + if (args->SetCurrentSessionDateToAll || (args->SetCurrentSessionDateWhenMissingDate && settings->LastUsedDate.IsValid() == false)) + settings->LastUsedDate = g.SessionDate; + } +} + // Apply to existing windows (if any) static void TableSettingsHandler_ApplyAll(ImGuiContext* ctx, ImGuiSettingsHandler*) { @@ -4234,6 +4249,7 @@ void ImGui::TableSettingsAddSettingsHandler() ini_handler.TypeName = "Table"; ini_handler.TypeHash = ImHashStr("Table"); ini_handler.ClearAllFn = TableSettingsHandler_ClearAll; + ini_handler.CleanupFn = TableSettingsHandler_Cleanup; ini_handler.ReadOpenFn = TableSettingsHandler_ReadOpen; ini_handler.ReadLineFn = TableSettingsHandler_ReadLine; ini_handler.ApplyAllFn = TableSettingsHandler_ApplyAll; From 5c2160ac85fd24f0e426e970966af9cd979edac8 Mon Sep 17 00:00:00 2001 From: ocornut Date: Fri, 3 Jul 2026 16:56:38 +0200 Subject: [PATCH 16/16] Settings, IO: move ConfigDebugIniSettings in same section. --- imgui.h | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/imgui.h b/imgui.h index 59aba2875..f6ac7773f 100644 --- a/imgui.h +++ b/imgui.h @@ -2439,6 +2439,7 @@ struct ImGuiIO // Ini Settings bool ConfigIniSettingsSaveLastUsedDate;// = true // Enable loading/saving last used day (YYYYMMDD) in some .ini struct, making things easier to audit and allowing custom tools to cleanup old data. int ConfigIniSettingsAutoDiscardMonths; // = 0 // [BETA] Set number of months after which unused .ini entries are discarded on load. Require platform_io.Platform_SessionDate to be set. For systems supporting the feature, .ini entries without a LastUsed field will always be discarded! Please report if you are using this. + bool ConfigDebugIniSettings; // = false // Save .ini data with extra comments (particularly helpful for Docking, but makes saving slower) // Miscellaneous options // (you can visualize and interact with all options in 'Demo->Configuration') @@ -2511,9 +2512,6 @@ struct ImGuiIO // - Backends may have other side-effects on focus loss, so this will reduce side-effects but not necessary remove all of them. bool ConfigDebugIgnoreFocusLoss; // = false // Ignore io.AddFocusEvent(false), consequently not calling io.ClearInputKeys()/io.ClearInputMouse() in input processing. - // Option to audit .ini data - bool ConfigDebugIniSettings; // = false // Save .ini data with extra comments (particularly helpful for Docking, but makes saving slower) - //------------------------------------------------------------------ // Platform Identifiers // (the imgui_impl_xxxx backend files are setting those up for you)