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:
- handle events
- draw()
- swap buffers (vsync causes this to block until vertical monitor refresh)
However consider the main loop when there are 3 windows:
- each window handle events
- each window draw()
- window 1 swap buffers (block until vsync)
- (some time later) window 2 swap buffers (block until vsync)
- (some time later) window 3 swap buffers (block until vsync)
Oops... now rendering one frame of the application is happening at 1/3 of the proper framerate.
Workaround: Utility Window
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:
- it is inelegant to have one window be special
- a race condition could still cause screen tearing
- some platforms ignore the vsync setting and force it to be on
- I read that switching which OpenGL context is bound is an expensive operation and should be avoided.
Workaround: One Thread Per Window
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)
- lock global mutex
- handle events
- draw()
- unlock global mutex
- swapBuffers()
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:
- Linux, X11, 4.4.0 NVIDIA 346.47 (2015-04-13)
- frame rate is 60fps no matter how many windows are open.
- OSX 10.9.5 (2015-04-13)
- frame rate is not capped; swap buffers is not blocking.
Workaround: Only One Context, One Big Framebuffer
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:
- Will it be OK to have such a big framebuffer?
- Is it OK to call
glViewport
multiple times every frame? - Will the windowing library API that I am using even allow me to create OpenGL contexts independent of windows?
- Wasted space in the framebuffer if the windows are all different sizes?
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.
Workaround: Only One Context
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
.
The Question
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 Questions
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.