ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
61.16k stars 10.31k forks source link

Freeing memory from the SDL_Renderer #7464

Closed spikeyamk closed 7 months ago

spikeyamk commented 7 months ago

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:

Dear ImGui 1.90.2 WIP (19015)
--------------------------------

sizeof(size_t): 8, sizeof(ImDrawIdx): 2, sizeof(ImDrawVert): 20
define: __cplusplus=199711
define: _WIN32
define: _WIN64
define: _MSC_VER=1937
define: _MSVC_LANG=202004
define: IMGUI_HAS_VIEWPORT
define: IMGUI_HAS_DOCK
--------------------------------

io.BackendPlatformName: imgui_impl_sdl3
io.BackendRendererName: imgui_impl_sdlrenderer3
io.ConfigFlags: 0x00000041
 NavEnableKeyboard
 DockingEnable
io.ConfigViewportsNoDecoration
io.ConfigInputTextCursorBlink
io.ConfigWindowsResizeFromEdges
io.ConfigMemoryCompactTimer = 60.0
io.BackendFlags: 0x00000C0E
 HasMouseCursors
 HasSetMousePos
 PlatformHasViewports
 HasMouseHoveredViewport
 RendererHasVtxOffset
--------------------------------

io.Fonts: 1 fonts, Flags: 0x00000000, TexSize: 1024,1024
io.DisplaySize: 3240.00,2041.00
io.DisplayFramebufferScale: 1.00,1.00
--------------------------------

style.WindowPadding: 18.00,18.00
style.WindowBorderSize: 1.00
style.FramePadding: 9.00,6.00
style.FrameRounding: 0.00
style.FrameBorderSize: 0.00
style.ItemSpacing: 18.00,9.00
style.ItemInnerSpacing: 9.00,9.00

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 call ImGui_ImplSDLRenderer3_Shutdown() and then ImGui_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

ocornut commented 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.

spikeyamk commented 7 months ago

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

ocornut commented 7 months ago

“ 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.

ocornut commented 7 months ago

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.