-
-
Notifications
You must be signed in to change notification settings - Fork 10.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Nesting multiple imgui contexts (glfw+opengl3) #2004
Comments
Sharing OpenGL context is really easy and recommended, it’s only a single extra parameter to glfwCreateWindow().
Have you looked at what the Viewport branch does? It is meant to facilitate this kind of thing while also reusing the same imgui context.
|
Thanks I will take a look. Maybe I'm making it more complicated than it needs to be. But you're sure that I can spawn a new window and enter a new rendering loop while between the |
None of those assumptions are valid. You don’t need to enter a separate newframe/render loop. Build and run the examples applications in the Viewport branch and find out what happens when you drag windows outside. You can essentially seamlessly create imgui windows within multiple os windows.
Note that GLFW backend has a few focus-related issues that are not solved on Linux yet, waiting for changes to be made on GLFW.
|
But I do want to enter a separate render loop. My viewer is a class (this is for libigl), and I want to easily be able to spawn a new viewer at any point for debugging purposes. |
I fail to understand the benefit of entering a separate render loop (do you mean you want to freeze the underlying application completely to create a sort of debugger inspecting the frozen contents?). Anyway:
You can create multiple imgui context so there's no need to "clear" or "destroy" the first context.
What is the atlas modification causing the error? What's the callstack. |
Yes that's exactly it. Right now in libigl the code you need to launch a viewer and display a mesh is just 3 lines: igl::opengl::glfw::Viewer viewer;
viewer.data().set_mesh(V, F);
viewer.launch(); Where (V,F) is the triangle mesh you want to inspect (points and triangles). There's a one-liner to plot points, and one for lines as well. The last line initializes the glfw viewer and launches the main rendering loop. It is convenient to throw that inside a function that is doing some computation, as a way to debug the 3D data you are manipulating. Since this computation may very well be done within a
Yes, but the problem lies in the
Sorry I'll do more testing tomorrow and provide that information =) |
Each imgui context created can share the same ImFontAtlas (it is a parameter to ImGui::CreateContext).
etc.
As for the opengl context you need shared context to use the imgui_impl_opengl3.cpp code as is (g_FontTexture/g_ShaderHandle are the same for all shared context). It is setup in the last parameter of glfwCreateWindow() so I'm not sure why resisting this. |
Well, it kind of is. Here is an example of the kind of workflow I am looking for: // #include headers
int main(void) {
Eigen::MatrixXd V;
Eigen::MatrixXi F;
// Load a triangle mesh
igl::readOFF("bunny.off", V, F);
igl::opengl::glfw::Viewer viewer;
viewer.data().set_mesh(V, F);
menu.callback_draw_custom_window = [&]() {
ImGui::Begin("New Window");
if (ImGui::Button("Do Stuff")) {
igl::opengl::glfw::Viewer subviewer;
subviewer.data().set_mesh(V, F);
subviewer.launch(); // enter subviewer render loop
}
ImGui::End();
};
viewer.launch(); // main viewer render loop
return 0;
} The Again, I don't mind sharing the OpenGL context, but this wouldn't help in the kind of scenario I am looking to have (since the subviewer here cannot be launched inside a ImGui frame). |
But nothing in that code will MODIFY the ImFontAtlas used by the main context. |
Ah, got it. That's probably because the initialization code for the Viewer is reloading the font and that could be avoided. Let's see if I can refactor my code to avoid that. There's one case where I need to reload the font though: when moving the window between screens with different DPI. In this case moving the subviewer to a different DPI screen will lead to an error. Granted that's a pretty rare use case, and I know better hidpi support is something on your roadmap so I think that's ok. |
It's also ok to reload the font if you do it in a new ImFontAtlas instance.
It's also perfectly fine (and frequent for many users) do add/remove/reload fonts but you need to do it before NewFrame(). Vertices have already been submitted for rendering and altering the font atlas would lead to corrupted visuals. |
I do it before the |
I see, sorry I didn't connect the DPI change issue with the nested viewers. It's almost as you are spawning a new process here (and in fact, you could perhaps even |
Does |
Surrounding the nested viewer code with an outer Neither will affect the ImFontAtlas of the main imgui context, so ImFontAtlas::Build() won't be called as it it's been already built. Essentially this issue needs the call-stack for how you got this error in the first place (notice the Issue Template/Contribute documents "If you are discussing an assert or a crash, please provide a debugger callstack.", there's a reason for that!). Make sure you also surround your viewer with two |
Ok I will try that and provide the call stack if I run into the assert again! |
Alright. You're gonna hate me, but the application actually crashes without a callstack this time:
I tried with the latest tip of the GLFW github repo, doesn't help. Here is the code have in my viewer class: void ImGuiMenu::init() {
ImGui_ImplOpenGL3_Shutdown(); // destroy parent GL objects
context_ = ImGui::CreateContext();
ImGui::SetCurrentContext(context_);
ImGui_ImplGlfw_InitForOpenGL(viewer->window, false);
ImGui_ImplOpenGL3_Init("#version 150");
reload_font();
}
void ImGuiMenu::reload_font() {
ImGuiIO& io = ImGui::GetIO();
io.Fonts->Clear();
io.Fonts->AddFontFromMemoryCompressedTTF(...);
}
void ImGuiMenu::shutdown() {
// Cleanup
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext(context_);
context_ = nullptr;
}
void ImGuiMenu::restore() {
ImGui::SetCurrentContext(context_);
ImGui_ImplOpenGL3_Init(); // recreate GL objects on parent viewer
}
void draw() {
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
draw_menu();
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
}
void draw_menu() {
ImGui::Begin("Menu");
if (ImGui::Button("New Viewer")) {
igl::opengl::glfw::Viewer viewer;
igl::opengl::glfw::imgui::ImGuiMenu menu;
viewer.plugins.push_back(&menu);
viewer.data().set_mesh(V, F);
viewer.launch();
ImGui::End();
return;
}
ImGui::End();
} |
Ok turns out the void ImGuiMenu::init() {
ImGui_ImplOpenGL3_Shutdown(); // destroy parent GL objects
ImGui_ImplGlfw_Shutdown();
context_ = ImGui::CreateContext();
ImGui::SetCurrentContext(context_);
ImGui_ImplGlfw_InitForOpenGL(viewer->window, false);
ImGui_ImplOpenGL3_Init("#version 150");
reload_font();
}
void ImGuiMenu::restore() {
ImGui::SetCurrentContext(context_);
ImGui_ImplOpenGL3_Init("#version 150");
ImGui_ImplOpenGL3_Init(); // recreate GL objects on parent viewer
} Then I get a crash when closing the subviewer with the following call stack:
So I guess the problem is that I need to call the parent's |
Ok I can get something kinda working with an exception, like if (ImGui::Button("Sub Viewer")) {
ImGui::End();
throw AbortFrame([&]() {
igl::opengl::glfw::Viewer viewer;
igl::opengl::glfw::imgui::ImGuiMenu menu;
viewer.plugins.push_back(&menu);
viewer.data().set_mesh(V, F);
viewer.launch();
});
} And changing the drawing call to the following: struct AbortFrame : public std::exception {
AbortFrame(std::function<void(void)> func) : deferred_callback_(func) { }
std::function<void(void)> deferred_callback_;
};
void draw() {
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
try {
draw_menu();
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
} catch (const AbortFrame &e) {
ImGui::EndFrame();
e.deferred_callback_();
}
} The problem is that I still have to call |
You may read g.CurrentWindowStack.Size and call End() the appropriate of time. But then it looks like you are looking for weird workaround to a problem that probably have a simple solution.
Is that a typo or missing some code, or are you restoring the child imgui context you just created? Also where is the destroy call for that child imgui context? You may just call Shutdown() on the imgui context and recreate one, the problem will always be that the code following the closure of the viewer may be doing ImGui:: calls that are invalid. You may also just create your own OpenGL3 renderer if that's relevant to you. Those files are in an examples/ folder for the reason that you can freely rework them. The code that interface with your OS and/or OpenGL is small and under your control, you have the code and debugger, there's no magic, you should be able to figure it out without weird exception hacks, but I can't be debugging your project/code for you. |
No it's not a typo. I haven't included the whole code to not pollute the thread but basically the shutdown sequence is just: subviewer.shutdown();
parent.restore(); So the
That's exactly what I'm struggling with. I don't see a way around that right now without the ugly exception hacks, or without rewriting parts of the glfw/opengl binding code (to have a |
OK. The saner solution we devised above should work, so the OpenGL crash you mention in: Should probably be investigated and clarified. There's an error somewhere waiting to be found, either in your code, either something that make a Shutdown/Init sequence in imgui_impl_opengl3.cpp not totally without side-effect? |
The error I mentioned in #2004 (comment) was caused by the line glDrawElements(GL_TRIANGLES, (GLsizei)pcmd->ElemCount, sizeof(ImDrawIdx) == 2 ? GL_UNSIGNED_SHORT : GL_UNSIGNED_INT, idx_buffer_offset); So probably it is due to the ImGui draw list referring to OpenGL objects that were destroyed during the initialization of the child viewer. For the record I did a quick test implementing a state stack for the OpenGL/GLFW backends: static GLFWwindow* g_Window = NULL;
static GlfwClientApi g_ClientApi = GlfwClientApi_Unknown;
static double g_Time = 0.0;
static std::array<bool, 5> g_MouseJustPressed = { false, false, false, false, false };
static std::array<GLFWcursor*, ImGuiMouseCursor_COUNT> g_MouseCursors = { 0 };
struct ImGui_ImplGlfw_Data {
GLFWwindow* g_Window;
GlfwClientApi g_ClientApi;
double g_Time;
std::array<bool, 5> g_MouseJustPressed;
std::array<GLFWcursor*, ImGuiMouseCursor_COUNT> g_MouseCursors;
};
static std::vector<ImGui_ImplGlfw_Data> g_DataStack;
void ImGui_ImplGlfw_PushState() {
ImGui_ImplGlfw_Data state;
state.g_Window = g_Window;
state.g_ClientApi = g_ClientApi;
state.g_Time = g_Time;
state.g_MouseJustPressed = g_MouseJustPressed;
state.g_MouseCursors = g_MouseCursors;
g_DataStack.push_back(state);
g_Window = NULL;
g_ClientApi = GlfwClientApi_Unknown;
g_Time = 0.0;
g_MouseJustPressed = { false, false, false, false, false };
g_MouseCursors = { 0 };
}
void ImGui_ImplGlfw_PopState() {
if (g_DataStack.empty()) {
return;
}
const ImGui_ImplGlfw_Data & state = g_DataStack.back();
g_Window = state.g_Window;
g_ClientApi = state.g_ClientApi;
g_Time = state.g_Time;
g_MouseJustPressed = state.g_MouseJustPressed;
g_MouseCursors = state.g_MouseCursors;
g_DataStack.pop_back();
} And for OpenGL: // OpenGL Data
static std::array<char, 32> g_GlslVersionString { '\0' };
static GLuint g_FontTexture = 0;
static GLuint g_ShaderHandle = 0, g_VertHandle = 0, g_FragHandle = 0;
static int g_AttribLocationTex = 0, g_AttribLocationProjMtx = 0;
static int g_AttribLocationPosition = 0, g_AttribLocationUV = 0, g_AttribLocationColor = 0;
static unsigned int g_VboHandle = 0, g_ElementsHandle = 0;
// OpenGL Data
struct ImGui_ImplOpenGL3_Data {
std::array<char, 32> g_GlslVersionString { '\0' };
GLuint g_FontTexture = 0;
GLuint g_ShaderHandle = 0, g_VertHandle = 0, g_FragHandle = 0;
int g_AttribLocationTex = 0, g_AttribLocationProjMtx = 0;
int g_AttribLocationPosition = 0, g_AttribLocationUV = 0, g_AttribLocationColor = 0;
unsigned int g_VboHandle = 0, g_ElementsHandle = 0;
};
std::vector<ImGui_ImplOpenGL3_Data> g_DataStack;
// Nested states
void ImGui_ImplOpenGL3_PushState() {
ImGui_ImplOpenGL3_Data state;
state.g_GlslVersionString = g_GlslVersionString;
state.g_FontTexture = g_FontTexture;
state.g_ShaderHandle = g_ShaderHandle;
state.g_VertHandle = g_VertHandle;
state.g_FragHandle = g_FragHandle;
state.g_AttribLocationTex = g_AttribLocationTex;
state.g_AttribLocationProjMtx = g_AttribLocationProjMtx;
state.g_AttribLocationPosition = g_AttribLocationPosition;
state.g_AttribLocationUV = g_AttribLocationUV;
state.g_AttribLocationColor = g_AttribLocationColor;
state.g_VboHandle = g_VboHandle;
state.g_ElementsHandle = g_ElementsHandle;
g_DataStack.push_back(state);
g_GlslVersionString = { '\0' };
g_FontTexture = 0;
g_ShaderHandle = 0;
g_VertHandle = 0;
g_FragHandle = 0;
g_AttribLocationTex = 0;
g_AttribLocationProjMtx = 0;
g_AttribLocationPosition = 0;
g_AttribLocationUV = 0;
g_AttribLocationColor = 0;
g_VboHandle = 0;
g_ElementsHandle = 0;
}
void ImGui_ImplOpenGL3_PopState() {
if (g_DataStack.empty()) {
return;
}
const ImGui_ImplOpenGL3_Data & state = g_DataStack.back();
g_GlslVersionString = state.g_GlslVersionString;
g_FontTexture = state.g_FontTexture;
g_ShaderHandle = state.g_ShaderHandle;
g_VertHandle = state.g_VertHandle;
g_FragHandle = state.g_FragHandle;
g_AttribLocationTex = state.g_AttribLocationTex;
g_AttribLocationProjMtx = state.g_AttribLocationProjMtx;
g_AttribLocationPosition = state.g_AttribLocationPosition;
g_AttribLocationUV = state.g_AttribLocationUV;
g_AttribLocationColor = state.g_AttribLocationColor;
g_VboHandle = state.g_VboHandle;
g_ElementsHandle = state.g_ElementsHandle;
} Which I then call in my void ImGuiMenu::init() {
ImGui_ImplOpenGL3_PushState();
ImGui_ImplGlfw_PushState();
context_ = ImGui::CreateContext();
ImGui::SetCurrentContext(context_);
ImGui_ImplGlfw_InitForOpenGL(viewer->window, false);
ImGui_ImplOpenGL3_Init("#version 150");
reload_font();
}
void ImGuiMenu::restore() {
ImGui::SetCurrentContext(context_);
ImGui_ImplOpenGL3_PopState();
ImGui_ImplGlfw_PopState();
} And it works great, without exception hacks or anything! Idk if you'd accept a PR for that kind of stuff though, so let me know. |
OK I understand the issue now: in this back-end we're using raw GLuint texture identifier for ImTextureId and that value is stored in the ImDrawList contents. If your GPU driver doesn't give you back the same texture id on a glDeleteTextures/glGenTextures sequence then you are toast (of course OpenGL doesn't guarantee that at all). Understanding this, we can devise a solution. One hacky workaround would be: to record Fonts->TexID before calling There are probably other solution you can find based on the above understand of what's going wrong. I'd prefer to avoid a sort of PR in imgui_imp_opengl3.cpp. From my point of view, every new features like that I have to maintain forever and I am already largely drained and overwhelmed with the maintenance of this examples/ folder. So any solution to not make them more featured than they already are is better for me. Considering you are doing something extremely unusual there, I think it would fail in your camp to maintain an OpenGL3 renderer (it's little amount of code and it has its individual ChangeLog). |
Just came over a simpler workaround: just skip rendering for 1 frame after you exit the viewer loop. EDIT |
I don't know how/why the main instance's drawlist knows at all about the new texture ID and why this assert would trigger. We only fetch the texture atlas ID in ImGui::Begin(): You'll have to debug this. |
Well I think the reason here is pretty simple, it's because the subviewer is calling In the current implementation, void ImGui_ImplOpenGL3_DestroyFontsTexture()
{
if (g_FontTexture)
{
ImGuiIO& io = ImGui::GetIO();
glDeleteTextures(1, &g_FontTexture);
io.Fonts->TexID = 0;
g_FontTexture = 0;
}
} It looks like the drawlist doesn't know about the new texture ID yet, but the assert is triggered because the old one was destroyed and the |
You may overwrite io.Fonts->TexID with the old texid for one frame then patch it afterward. In the future I'd like to make it easier to override the texture binding function within an existing binding (from outside) so you could decide to have a special and non-changing identifier to signify "the font texture" with this scheme, but it's not possible at the moment. Right I guess you'll need to choose your poison! |
Ok I can get it to work by hacking around the |
Good to hear! |
Hi. Sorry to bring this issue back up, but I was wondering how you're handling multiple windows with the current GLFW bindings in imgui? It seems that GLFW sets a callback per window ( |
Check out the back-end in the viewport branch which handles multi-windows: In each ImGuiViewport we store a We install the callback on each new window, and at it matters for most of them when we receive them it doesn't matter for us which window we received them from. For example EDIT
Attached patch.zip not sure it is worth applying this and generally to all back-ends now. |
Ah nice! So basically you are implementing your own version of |
Argh, I tried to use the |
Could you clarify why?
|
Ok so back to the problem at hand (multiple/nested viewers with an ImGui menu), I'm still having troubles with the viewport branch when I hook it up to my viewer. The problem is again the use of globals ( |
And just to not post an empty message, I've pushed the current state of my bindings (for the libigl viewer I'm working on) on a branch of my own fork. See in particular this file that hooks up our viewer to the OpenGL+GLFW bindings of ImGui, and this tutorial file that shows how to spawn a concurrent or nested viewer windows in this setting. In the current implementation, I am sharing the OpenGL context between windows as well as the ImGui texture atlas. I know we've discussed this already but I would like to come up with a solution where I don't have to modify the upstream example bindings to achieve this behavior (e.g. something like storing the current binding data pointer in the ImGui context would be nice). |
Ok how about this: without modifying existing ImGui code, how about applying the patch you previously posted (to wrap all globals in a single |
The problem I have is that all those changes are adding confusion and cognitive load to what should be the simplest possible binding example provided to new users. Perhaps adding the void* user pointers in ImGuiIO for use by the back-end would be a good idea. I'm not ruling out looking at this but I have hundreds of more important fishes to fry at the moment.. |
Yes, I agree that increasing the cognitive cost when you open the |
…nguageUserData void* for storage use by back-ends. (#2004 + for cimgui)
@jdumas I added |
Thanks! I've been using the modified backend bindings for libigl for now since it was easier to set up. I'll try to get back to this whenever I have some free time =) |
Hello, @jdumas This should now be solved by 70c6038 + 4cec3a0. I confirmed that both OpenGL3 backends worked my test bed. Details (posted in a few threads) With 70c6038 i have rearranged all backends to pull their data from structures. PS: Current change being generaly healthy I don't mind it but I'm not very excited with promoting and encouraging support of multiple-imgui-context-each-in-their-own-backend-context. I think that feature is likely to be incompatible with multi-viewports (single imgui context + multiple backend windows) and I think instead we should aim at reworking this to let user register any existing window. |
Thanks @ocornut, this looks great! I agree if multi-viewport support allows us to have multiple viewer windows running in nested mode maybe we can use that. It's been a while since I looked at this feature though, but I'll let you know once I get back to it :) |
Hi,
I'm struggling a bit to create nested GLFW windows with ImGui menus, and I was wondering what would be the best way to approach the problem. In my case, I have a single thread, and I just want to be able to spawn a subviewer from anywhere for debugging purposes. Once the user closes the subviewer, life should go on in the parent window. I am not really interested in sharing OpenGL context between the windows, that sounds a bit messy.
In the current implementation of
imgui_impl_opengl3.cpp
, we have a bunch of global variables holding data for the current context. What I'm doing right now is, before spawning the new viewer, I clear the current object/context/whatever, spawn the viewer, and then basically reinitialize ImGui for the parent window once the subviewer is closed.This kind of works, but only if the subviewer is created outside a
ImGui::NewFrame()/ImGui::Render()
block. Otherwise I'm getting the error"Cannot modify a locked ImFontAtlas between NewFrame() and EndFrame/Render()!"
Lmk if you have any suggestion on how to achieve this.
The text was updated successfully, but these errors were encountered: