Romen
Romen

Reputation: 1766

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.

Upvotes: 4

Views: 13202

Answers (3)

Rohit Kumar J
Rohit Kumar J

Reputation: 137

As you have discovered, thread_local makes things faster. It does this magic by creating separate instances of ImGuiContext* threadCTX and passes them to various threads that do the multi-threading. So each core does not have to compete for access the common resource which would otherwise have been a single ImGuiContext* threadCTX.

Threads competing for a shared resource is bad for preformance

ImGui code section is heavily integrated with controlling and responding to this machine vision code.

As you have mentioned, ImGui is trying to access a single resource across multiple threads, many times; then each core would compete to get the (latest/correct) state from the main memory as it is now a shared resource. Since this is shared, it would probably not be loaded onto the cache and each time a thread tries to access the instance it would invalidate the cached memory. This competition between the threads and the constant invalidation of the cached memory(fast memory) is probably the bottleneck. The CPU is wasting time acquiring a shared resource.

It is better to have not integrate your code as much with ImGui or any other shared instance. That being said I have a few suggestions... The following are some suggestions that you could do, but are not implemented/tested.

Suggestions?

Depends on your implementation.

  1. If you can use atomics to synchronize threads by using them as flags, it could improve performance. For example, only call the clear/read/write in each thread after an epoch or a full training loop is complete, while maintaing a particular state. However, what exactly you intend to share between the threads should be very specific. Do not share instances across and call them on each pass every thread.

and the windows do not interact with each other or share resources.

  1. Create a wrapper function in the main thread that selects a rendering context that switches on the ImGui Loop that calls the shared instance, temporarily binding glfw callbacks to the particular rendering context.

When dealing with edge cases, both of these are a hassle IMHO. If someone has better ideas I would like to their suggestions.

ML-specific suggestions:

  1. For sharing ML resources, be specific on what resources need to be shared. (Preferably only checkpoint files for the ml state). Do a write-to-file-and-close-file from each thread upon completion of an epoch and then either dispatch to each thread that the nth thread has completed the task or let each thread decide when it wants to check the shared/stored/static state on the main thread(perhaps a mutex implementation here would be a good idea). This call would anyways need to invalidate the cache as it needs to load in new data to/from the checkpoint files.

Upvotes: 0

Romen
Romen

Reputation: 1766

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.

Upvotes: 6

Omar
Omar

Reputation: 762

Why are you creating multiple threads in the first place? You can perfectly create multiple GLFW windows and multiple Dear ImGui contexts and manage everything within a same thread. Working from different threads is only going to make everything more difficult to deal with.

In the specific case of Dear ImGui, you can use the multi-viewport features in the 'docking' branch which natively support extracting any dear imgui windows outside the main viewport and creates/manages the GLFW windows for you. It's all handled by a single dear imgui contexts, so you can e.g. drag and drop from one window to another.

Upvotes: 0

Related Questions