Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple OpenGL contexts, multiple windows, multithreading, and vsync

Tags:

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:

  1. handle events
  2. draw()
  3. swap buffers (vsync causes this to block until vertical monitor refresh)

However consider the main loop when there are 3 windows:

  1. each window handle events
  2. each window draw()
  3. window 1 swap buffers (block until vsync)
  4. (some time later) window 2 swap buffers (block until vsync)
  5. (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)

  1. lock global mutex
  2. handle events
  3. draw()
  4. unlock global mutex
  5. 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.

like image 484
andrewrk Avatar asked Apr 14 '15 00:04

andrewrk


People also ask

Can you have multiple OpenGL contexts?

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.

What is OpenGL context?

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.


1 Answers

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() 
like image 164
datenwolf Avatar answered Oct 04 '22 17:10

datenwolf