Closed spikeyamk closed 7 months ago
We don't allocate vertex data in the SDLRenderer backend, as we only call SDL_RenderGeometryRaw()
and let SDL do its thing. While it is possible that SDL would maintain internal buffers, calling ImGui_ImplSDLRenderer3_Shutdown()
doesn't involve any meaningful call to SDL/SDLRenderer that would be tied to the growth of data, it only destroy the textures which is of fixed size.
Therefore something in your statement is incorrect and I think you may need to do further research.
High usage of ImDrawList in a given window will create a large buffer, which will be GC-ed when the window is unused for a certain time. io.ConfigMemoryCompactTimer
defaults to 60 = 60 seconds. You may want to adjust this value to see if it makes a difference, but it is 100% unrelated to SDL renderer. Also it won't compact if the window is in use. We might consider introducing another form of vector compaction for extreme cases where a large vector growth happens temporarily, but given your statements I'm not sure it would affect you.
I'm sorry, I probably didn't express my thought clear enough so I build this. It skips one frame after SDL_DestroyRenderer(renderer) call so that the screen won't blink. I know it's a hack but I can't come up with anything better than this:
#include <algorithm>
#include <chrono>
#include <stdio.h>
#include <thread>
#include <vector>
#include <cmath>
#include <SDL3/SDL.h>
#include <backends/imgui_impl_sdl3.h>
#include <backends/imgui_impl_sdlrenderer3.h>
#include <imgui.h>
#include <implot.h>
#if defined(IMGUI_IMPL_OPENGL_ES2)
#include <SDL3/SDL_opengles2.h>
#else
#include <SDL3/SDL_opengl.h>
#endif
int run() {
// Setup SDL
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_GAMEPAD) != 0)
{
printf("Error: SDL_Init(): %s\n", SDL_GetError());
return -1;
}
// Create window with SDL_Renderer graphics context
Uint32 window_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIDDEN;
SDL_Window* window = SDL_CreateWindow("Dear ImGui SDL3+SDL_Renderer example", 1920, 1080, window_flags);
if (window == nullptr)
{
printf("Error: SDL_CreateWindow(): %s\n", SDL_GetError());
return -1;
}
SDL_Renderer* renderer = SDL_CreateRenderer(window, nullptr, SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_ACCELERATED);
if (renderer == nullptr)
{
SDL_Log("Error: SDL_CreateRenderer(): %s\n", SDL_GetError());
return -1;
}
SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);
SDL_ShowWindow(window);
// Setup Dear ImGui context
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImPlot::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
// Setup Dear ImGui style
ImGui::StyleColorsDark();
// Setup Platform/Renderer backends
ImGui_ImplSDL3_InitForSDLRenderer(window, renderer);
ImGui_ImplSDLRenderer3_Init(renderer);
// Our state
bool show_demo_window { false };
bool show_implot_window { false };
bool show_another_window { true };
bool reload { false };
bool skip { false };
const size_t xs_ys_count { 2 << 17 };
std::vector<float> xs {
[&]() {
std::vector<float> ret;
ret.resize(xs_ys_count);
std::generate(ret.begin(), ret.end(), [index = 0.0f]() mutable {
return index++;
});
return ret;
}()
};
std::vector<float> ys {
[&]() {
std::vector<float> ret;
ret.resize(xs_ys_count);
std::generate(ret.begin(), ret.end(), [index = 0.0f]() mutable {
return std::sin(index++);
});
return ret;
}()
};
const ImVec4 clear_color { 0.45f, 0.55f, 0.60f, 1.00f };
// Main loop
bool done = false;
while (!done)
{
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;
}
// Start the Dear ImGui frame
ImGui_ImplSDLRenderer3_NewFrame();
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::Checkbox("ImPlot Window", &show_implot_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;
if (ImGui::Button("Reload")) {
reload = true;
}
ImGui::End();
}
if (show_implot_window) {
ImGui::Begin("ImPlot", &show_implot_window);
ImPlot::SetNextAxesToFit();
if (ImPlot::BeginPlot("ImPlot")) {
ImPlot::PlotLine("ImPlot", xs.data(), ys.data(), std::min(xs.size(), ys.size()));
ImPlot::EndPlot();
}
ImGui::End();
}
// Rendering
ImGui::Render();
//SDL_RenderSetScale(renderer, io.DisplayFramebufferScale.x, io.DisplayFramebufferScale.y);
SDL_SetRenderDrawColor(renderer, (Uint8)(clear_color.x * 255), (Uint8)(clear_color.y * 255), (Uint8)(clear_color.z * 255), (Uint8)(clear_color.w * 255));
SDL_RenderClear(renderer);
ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData());
if(skip) {
skip = false;
} else {
SDL_RenderPresent(renderer);
}
if(reload) {
reload = false;
skip = true;
// Cleanup
SDL_RenderClear(renderer);
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImPlot::DestroyContext();
ImGui::DestroyContext();
SDL_DestroyRenderer(renderer);
renderer = SDL_CreateRenderer(window, nullptr, SDL_RENDERER_PRESENTVSYNC | SDL_RENDERER_ACCELERATED);
if (renderer == nullptr)
{
SDL_Log("Error: SDL_CreateRenderer(): %s\n", SDL_GetError());
return -1;
}
ImGui::CreateContext();
ImPlot::CreateContext();
// Setup Dear ImGui style
ImGui::StyleColorsDark();
// Setup Platform/Renderer backends
ImGui_ImplSDL3_InitForSDLRenderer(window, renderer);
ImGui_ImplSDLRenderer3_Init(renderer);
}
}
// Cleanup
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImPlot::DestroyContext();
ImGui::DestroyContext();
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
SDL_Quit();
return 0;
}
// Main code
int main(int, char**)
{
while(1) {
run();
}
}
After I close the ImGui window with one ImPlot Plot widget the memory usage is around 550 MB and stays there. ImGui::DestroyContext() or ImPlot::DestroyContext() don't free up any meaningful amount of memory ImGui_ImplSDLRenderer3_Shutdown() and ImGui_ImplSDL3_Shutdown() don't free up much more either. It's only after I call SDL_DestroyRenderer(renderer) the memory usage goes down back to 17 MB so I was looking for some other way to free the memory from the SDL_Renderer, but I guess there's no way to do that unless I do serious modifications to the backend or write something else myself?
The code above to me looks like the only thing that could be used to free up unused memory.
Also I'm not really looking into reducing the memory usage just looking into freeing unused allocated memory, so this io.ConfigMemoryCompactTimer wouldn't really help
“ The only way to free them is to call ImGui_ImplSDLRenderer3_Shutdown() and then ImGui_ImplSDLRenderer3_Init() again, ” […] “It's only after I call SDL_DestroyRenderer(renderer)”
So that’s a different thing, and the answer is not really within the realm of dear imgui but entirely internal to SDL. I presume it is using a large buffer to temporarily convert/store some data.
Making a ImPlot call with 200k points is pretty unusual already. Before asking for a solution you may ask yourself why you really need to clear that temporary scratch buffer. After all you’ll need it again if you render a large plot again.
I presume SDLRenderer could decide to split very large draw calls into severals, if it knows it needs a temporary buffer, in order to cap the temporary buffer size. Our own backend for SDL renderer could easily do that arbitrary split, but it seems reasonable for SDL to be doing it to benefit the largest number of users.
So you ought to ask SDL3 team if the call to SDL_RenderGeometryRaw() for large numbers of vertices could perhaps perform multiple calls in order to avoid excessively large scratch buffer. Perhaps it is backend dependent for them.
Closing as something to report/suggest to SDL3.
You can see the code for SDL_RenderGeometryRaw()
here:
https://github.com/libsdl-org/SDL/blob/6ccdfffe2653467c42b60fc5eb8ea5548c295333/src/render/SDL_render.c#L4264
It's calling SDL_small_alloc()
/SDL_small_free()
macro but that's not the issue here as the buffer is immediately freed.
I would guess it may be the underlying per-backend implementation for SDL_RenderGeometryRawFloat()
which allocate a temporary vertex buffer, which seems like a reasonable thing to do to be honest.
Version/Branch of Dear ImGui:
Version 1.9.02, Branch: docking
Back-ends:
imgui_impl_sdl3.cpp + imgui_impl_sdlrenderer3.cpp
Compiler, OS:
Windows 10 + MSVC 2022
Full config/build information:
Details:
I'm using ImGui with ImPlot and I'm plotting quite big amounts of data. We're talking like 200'000 double values in LinePlots. Everything works just fine, it's just ImPlot uses quite a lot of memory for this around 500 MB and even after calling
ImPlot::DestroyContext()
this memory isn't freed. The only way to free them is to callImGui_ImplSDLRenderer3_Shutdown()
and thenImGui_ImplSDLRenderer3_Init()
again, this however will cause the screen to blink for one frame which is undesireable? I guess I could try and skip rendering of one frame in the loop when I call ImGui_ImplSDLRenderer3_Shutdown(); or something?I'm not quite sure how to setup some kind of wrapper or something that would track down what gets created inside the renderer so that it can free only the parts of the memory used by the ImPlot.
Screenshots/Video:
No response
Minimal, Complete and Verifiable Example code:
No response