4
\$\begingroup\$

I'd like to introduce seamless level loading which means I need multiple threads. The main thread is for rendering the current scene (or for non-seamless level loading a progress bar) while the other thread is loading the resource files and sending data to the graphics card.

Now I have the following single threaded architecture: when the graphics device is initialized, a dummy window and a "shared context" is created. This is the only rendering context (HGLRC in Windows terms) I'm creating. Every time a new window is created and the graphics device is asked to create a "renderable surface" into that window, the same "shared rendering context" is used. I can do this because I set the same pixel format for each window, so there is no point to make multiple rendering contexts (that would just make my life harder than it should). Here is a small image representing the current design:

Current single-threaded architecture

I've read some topics about this but now I'm confused since I read really different point of views, so I have some questions:

  • As far as I know an OpenGL context can be active in a single thread only (at a time). This means that I need another "loading rc" which shares resources (through context creation or *glShareList) with the "shared rc". Every time I create a new thread to load something, I have to set the "loading rc" active in that thread. Am I right?
  • Can I use the same dummy device context (HDC) in the loading thread?
  • Because I have a single GPU the commands sent to the graphics card are queued up. Right? Will this behaviour introduce switches between shared and loading contexts?
  • Is there anything else I should take care of?
\$\endgroup\$
  • \$\begingroup\$ I've been experimenting with an array buffer rendering mechanic. Basically I have an array of integers, which take the integer id of the models currently in the frustum. When a model falls out of the frustum its index is set to 0 in the array and skipped over. New id is added to its place. This could achieve the same thing. \$\endgroup\$ – Krythic Oct 21 '16 at 13:48
4
\$\begingroup\$
  1. Each OpenGL context can only be "active" in a single thread at a time.

If you wish to be making OpenGL calls from two threads simultaneously (as in your example of one "loading thread" which is creating textures, VBOs, etc), and one "rendering thread", then as you say, you need to create a second context, with shared data between the two contexts.

Do note that shared contexts do not share all OpenGL data. Particularly notable exceptions are FBOs and VAOs, which will remain only in the context where they were created.

  1. Yes, you can create multiple OpenGL contexts referring to a single window (HDCs, on Windows OS).

  2. You are given no guarantees about what order the commands given to shared OpenGL contexts will be handled.

If you wish to make use of data inserted into one shared context from the other context, you'll need to put a fence in, to make certain that the data is definitely in sync between the two contexts before you try to use it. For example, if you want to draw something which has just finished loading, then you'd want the loading thread, after it has finished loading its data and pushing it into its shared context, to do something like this, before signalling the main thread that it's okay to draw the loaded data:

GLsync fenceId = glFenceSync( GL_SYNC_GPU_COMMANDS_COMPLETE, 0 );
while(true)
{
    result = glClientWaitSync(fenceId, GL_SYNC_FLUSH_COMMANDS_BIT, GLuint64(5000000000)); //5 Second timeout
    if(result != GL_TIMEOUT_EXPIRED) break; //we ignore timeouts and wait until all OpenGL commands have been processed!
}
  1. The common wisdom is that you're generally a lot better off using only a single context, rather than two shared contexts.

Instead of making OpenGL calls directly from a second thread via a shared context, the usual advice is to instead simply prepare data from that second thread, but actually perform all the OpenGL calls from the main thread. Drivers, they say, are generally much better-optimised for the single-context case, and it's worth trying to fit the way the drivers want to work, rather than do anything complicated and weird, like shared contexts.

So it's worth considering whether you really want to go down this path. It gets complicated fast, and you pay a performance penalty for every extra shared context, even when you're not actually using them.

\$\endgroup\$
  • \$\begingroup\$ Thanks for your answer! First I though the same that I should skip the multiple context thing, but I'd like to have instant feedback on uploading operations. I mean it's okay to load and decode the image from the disk, it does not need any OpenGL-specific call, but texture or any buffer uploading/creating can (can?) fail. Can I do something like signaling? I mean the loading thread loads the necessary data then fire an event that the loading of a single resource is done and the OpenGL object should be created. \$\endgroup\$ – csisy Oct 21 '16 at 12:09
  • \$\begingroup\$ My previous comment is a bit off topic and I got my answers so I press that tick. \$\endgroup\$ – csisy Oct 21 '16 at 12:27
  • \$\begingroup\$ Yes, that's what the code snippet in my answer does; verifies from the loading thread that the objects have actually been created and synchronised. You can then notify the other thread in any standard way, to let it know that the object exists and may now be used. (remember that OpenGL commands don't necessarily actually get run immediately when you call them; you need to make sure that they've actually finished properly, before telling the main thread that it's okay to proceed. This code is how you do that.) \$\endgroup\$ – Trevor Powell Oct 21 '16 at 12:36
2
\$\begingroup\$

The short answer: No!

Don't do that. Just, don't.

The slightly longer answer

Unluckily, OpenGL does not truly (contrary to folklore) support multithreading. No, really, it does not.

You are of course aware that a context can be (must be) active for exactly one thread using that context, and making a context current between threads effectively causes lockstep behavior and is horribly expensive.

Now, sharing two contexts (with one context each bound to one thread) sounds like it would give you just what you want. However, this is an illusion.

Sharing contexts will significantly degrade overall performance due to internal synchronization and will be slower than just doing everything in one thread.
When I tried this some years ago, sharing contexts, even if there were no changes made (so, really nothing to synchronize!) would cause an approximate 8-10% performance drop compared to using a single context and a single thread. Maybe drivers have somewhat improved since then, but I would not rely on that since sharing contexts is not a very commonly used feature, and not a top priority.

The somewhat less pessimistic answer

There is still a way to get some parallelism into that beast. You can map buffers in your render thread, pass a pointer to a worker thread, and have the worker thread fill the buffer with data. Then, signal a condition variable, causing the render thread to unmap the buffer again ready for use. You still have the mapping/unmapping on your render thread, but at least you can offload the shuffling around (and loading from disk, unpacking) of data.

Be sure to use a technique like buffer orphaning (or use the copy write buffer) and the unsynchronized bit to avoid unnecessary stalls on modifying buffers that are still being read from and client/server syncs. Unluckily, even unsynchronized maps will have some syncs that are unavoidable.

If updates are frequent (probably not the case in your example) and persistent mappings are supported, you may want to use these, making the repeated mapping/unmapping unnecessary. Just map once and pass a pointer. Be aware that persistent mapping is slightly slower overall, so it's only advantageous if you have frequent (every frame, or nearly so) updates where the cost of mapping/unmapping is dominating.

\$\endgroup\$
  • \$\begingroup\$ These problems were the main reason why I started this topic. I wasn't sure about how to do thing but I also felt this whole process will/could cause performance problems. Thanks for the explanation! \$\endgroup\$ – csisy Oct 21 '16 at 16:01
  • \$\begingroup\$ @Damon don't be some negative. Shared context in separate thread can be good practice for parallel resource loading during application loading. \$\endgroup\$ – Michael IV Jan 4 '18 at 14:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.