c++multithreadingglfwimgui

GLFW & ImGui: Creating ImGui controls from thread other than main


I am using GLFW and ImGui for a project that involves opening multiple windows. So far I have set this up so that each time a new window must be opened I spawn a thread that creates its own GLFW window and OpenGL context. The thread function looks something like this:

window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
// Check for creation error...
glfwMakeContextCurrent(window);

ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();   // Is this supposed to be done per-thread?
// Calling specific impl-specific ImGui setup methods for GLFW & OpenGL3...
// Set up OpenGL stuff ...

while (!glfwWindowShouldClose(window))
{
    // Some heavy-duty processing happens here...

    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();
    ImGui::NewFrame();

    // ImGui code is here...

    // Rendering some stuff in the window here...

    // Render ImGui last...
    ImGui::Render();
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

    glfwSwapBuffers(window);
}

// Calling impl-specific ImGui shutdown here...
glfwDestroyWindow(window);

I know that GLFW requires you to poll events from the main thread (the one that called glfwInit()), so I have a loop on my main thread that does that:

while (!appMustExit)
{
    glfwWaitEvents();
}
// appMustExit is set from another thread that waits for console input

So the issue I am having is that my ImGui controls don't respond to any sort of input and glfwWindowShouldClose() never returns true if I click on the Close button. It seems like the input state is only available on the thread that calls glfwPollEvents(), which leads me to believe that you can't combine ImGui & GLFW while still using a separate thread for rendering!

How can I fix this to allow ImGui & these windows to respond to GLFW events?

My previous attempt used a single thread to iterate over each window and update/render it, but I am hoping to use threads to help the application scale better with many windows open.

Update: I would like to clarify that this application involves processing complex machine vision in real-time, and the ImGui code section is heavily integrated with controlling and responding to this machine vision code. Therefore I would like to be able to call the ImGui functions on the same thread as this processing, which also means this thread must be able to respond to glfw input.


Solution

  • I was able to find a way to change Dear ImGui to a (hopefully) thread-safe library with liberal use of the thead_local specifier.

    In imconfig.h I had to create a new thread-local ImGuiContext pointer:

    struct ImGuiContext;
    extern thread_local ImGuiContext* threadCTX;
    #define GImGui threadCTX
    

    In imgui_impl_glfw.cpp I had to change all of the local/static variables into thread_local versions:

    thread_local GLFWwindow*    g_Window = NULL;    // Per-Thread Main window
    thread_local GlfwClientApi  g_ClientApi = GlfwClientApi_Unknown;
    thread_local double         g_Time = 0.0;
    thread_local bool           g_MouseJustPressed[5] = { false, false, false, false, false };
    thread_local GLFWcursor*    g_MouseCursors[ImGuiMouseCursor_COUNT] = { 0 };
    
    // Chain GLFW callbacks: our callbacks will call the user's previously installed callbacks, if any.
    static thread_local GLFWmousebuttonfun   g_PrevUserCallbackMousebutton = NULL;
    static thread_local GLFWscrollfun        g_PrevUserCallbackScroll = NULL;
    static thread_local GLFWkeyfun           g_PrevUserCallbackKey = NULL;
    static thread_local GLFWcharfun          g_PrevUserCallbackChar = NULL;
    

    Likewise, in imgui_impl_opengl3.h I did the same for the OpenGL object handles:

    static thread_local char         g_GlslVersionString[32] = "";
    static thread_local GLuint       g_FontTexture = 0;
    static thread_local GLuint       g_ShaderHandle = 0, g_VertHandle = 0, g_FragHandle = 0;
    static thread_local int          g_AttribLocationTex = 0, g_AttribLocationProjMtx = 0;                                // Uniforms location
    static thread_local int          g_AttribLocationVtxPos = 0, g_AttribLocationVtxUV = 0, g_AttribLocationVtxColor = 0; // Vertex attributes location
    static thread_local unsigned int g_VboHandle = 0, g_ElementsHandle = 0;
    

    With these few changes, I am now able to create a GLFW window & OpenGL context, initialize Dear ImGui, and call glfwPollEvents on each thread without them affecting each other at all. Essentially each thread that creates a GLFW window can be used as if it were the 'main' thread.

    This solution probably has a few setbacks, but it appears to work fine for my use-case where each window runs its event loop in its own thread, has its own OpenGL and ImGui contexts, and the windows do not interact with each other or share resources.