I am creating a graphical user interface application using OpenGL in which there can be any number of windows - "multi-document interface" style.
If there were one window, the main loop could look like this:
However consider the main loop when there are 3 windows:
Oops... now rendering one frame of the application is happening at 1/3 of the proper framerate.
One workaround is to have only one of the windows with vsync turned on, and the rest of them with vsync turned off. Call swapBuffers() on the vsync window first and draw that one, then draw the rest of the windows and swapBuffers() on each one.
This workaround will probably look fine most of the time, but it's not without issues:
Since there can be one OpenGL context bound per thread, perhaps the answer is to have one thread per window.
I still want the GUI to be single threaded, however, so the main loop for a 3-window situation would look like this:
(for each window)
Will this work? This other question indicates that it will not:
It turns out that the windows are 'fighting' each other: it looks like the SwapBuffers calls are synchronized and wait for each other, even though they are in separate threads. I'm measuring the frame-to-frame time of each window and with two windows, this drops to 30 fps, with three to 20 fps, etc.
To investigate this claim I created a simple test program. This program creates N windows and N threads, binds one window per thread, requests each window to have vsync on, and then reports the frame rate. So far the results are as follows:
Another idea I thought of: have only one OpenGL context, and one big framebuffer, the size of all the windows put together.
Each frame, each window calls glViewport
to set their respective rectangle of the framebuffer before drawing.
After all drawing is complete, swapBuffers() on the only OpenGL context.
I'm about to investigate whether this workaround will work or not. Some questions I have are:
glViewport
multiple times every frame?Camilla Berglund, maintainer of GLFW says:
That's not how glViewport works. It's not how buffer swapping works either. Each window will have a framebuffer. You can't make them share one. Buffer swapping is per window framebuffer and a context can only be bound to a single window at a time. That is at OS level and not a limitation of GLFW.
This question indicates that this algorithm might work:
Activate OpenGL context on window 1 Draw scene in to window 1 Activate OpenGL context on window 2 Draw scene in to window 2 Activate OpenGL context on window 3 Draw scene in to window 3 For all Windows SwapBuffers
According to the question asker,
With V-Sync enabled, SwapBuffers will sync to the slowest monitor and windows on faster monitors will get slowed down.
It looks like they only tested this on Microsoft Windows and it's not clear that this solution will work everywhere.
Also once again many sources tell me that makeContextCurrent() is too slow to have in the draw() routine.
It also looks like this is not spec conformant with EGL. In order to allow another thread to eglSwapBuffers()
, you have to eglMakeCurrent(NULL)
which means your eglSwapBuffers
now is supposed to return EGL_BAD_CONTEXT
.
So, my question is: what's the best way to solve the problem of having a multi-windowed application with vsync on? This seems like a common problem but I have not yet read a satisfying solution for it.
Similar to this question: Synchronizing multiple OpenGL windows to vsync but I want a platform-agnostic solution - or at least a solution for each platform.
And this question: Using SwapBuffers() with multiple OpenGL canvases and vertical sync? but really this problem has nothing to do with Python.
A process can create multiple OpenGL contexts. Each context can represent a separate viewable surface, like a window in an application. Each context has its own set of OpenGL Objects, which are independent of those from other contexts. A context's objects can be shared with other contexts.
An OpenGL rendering context is a port through which all OpenGL commands pass. Every thread that makes OpenGL calls must have a current rendering context. Rendering contexts link OpenGL to the Windows windowing systems. An application specifies a Windows device context when it creates a rendering context.
swap buffers (vsync causes this to block until vertical monitor refresh)
No, it doesn't block. The buffer swap call may return immediately and not block. What it does however is inserting a synchronization point so that execution of commands altering the back buffer is delayed until the buffer swap happened. The OpenGL command queue is of limited length. Thus once the command queue is full, futher OpenGL calls will block the program until further commands can be pushes into the queue.
Also the buffer swap is not an OpenGL operation. It's a graphics / windowing system level operation and happens independent of the OpenGL context. Just look at the buffer swap functions: The only parameter they take are a handle to the drawable (=window). In fact even if you have multiple OpenGL contexts operating on a single drawable, you swap the buffer only once; and you can do it without a OpenGL context being current on the drawable at all.
So the usual approach is:
' first do all the drawing operations foreach w in windows: foreach ctx in w.contexts: ctx.make_current(w) do_opengl_stuff() glFlush() ' with all the drawing commands issued ' loop over all the windows and issue ' the buffer swaps. foreach w in windows: w.swap_buffers()
Since the buffer swap does not block, you can issue all the buffer swaps for all the windows, without getting delayed by V-Sync. However the next OpenGL drawing command that addresses a back buffer issued for swapping will likely stall.
A workaround for that is using an FBO into which the actual drawing happens and combine this with a loop doing the FBO blit to the back buffer before the swap buffer loop:
' first do all the drawing operations foreach w in windows: foreach ctx in w.contexts: ctx.make_current(w) glBindFramebuffer(GL_DRAW_BUFFER, ctx.master_fbo) do_opengl_stuff() glFlush() ' blit the FBOs' renderbuffers to the main back buffer foreach w in windows: foreach ctx in w.contexts: ctx.make_current(w) glBindFramebuffer(GL_DRAW_BUFFER, 0) blit_renderbuffer_to_backbuffer(ctx.master_renderbuffer) glFlush() ' with all the drawing commands issued ' loop over all the windows and issue ' the buffer swaps. foreach w in windows: w.swap_buffers()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With