nlraley's picture

Help with OpenGL and Threading

I have my application running just fine without threading, but I'd really like to run some simple threads in the background for loading and unloading textures that are just outside of the viewable area.

However, when I split the texture loading functionality into threads, none of the tiles that are loaded via the thread are ever displayed.

Is there something I need to do to allow the textures to be loaded into OpenGL from another thread?


Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
tksuoran's picture

You need to make your GL context current before you can use it from a new thread.

First, before you start loading, you must release the context in the main thread: Context.MakeCurrent(null);
Second, in your thread, make the context current for that thread: Context.MakeCurrent(WindowInfo);
When you are done loading, release the context in the loader thread and make it current again in the main thread.

You can not use a GL context from multiple threads at the same time, so you can not use the GL context at all from the main thread while your loading thread has it. You also can not load textures with 4 threads, to GL. You can do load them to CPU memory with threads, but you need to delay any GL operations until you have the GL context.

If you want to show progress or something else during loading, you might want to create yet another thread where you launch a new GameWindow, it will have it's own GL context and you can keep updating it while the main and loader threads do other stuff.

nlraley's picture

How exactly do you make a context?

Is it possible to share 2 contexts with OpenGL? The only drawing I do is from the main thread. I have a list of textures that include the OpenGL texture id. I'm just wanting the other thread to load the bitmap and push it to the OpenGL memory so the main thread can use the texture object to render the scene. This is doable correct. How would I create the 2nd context and share it with the OpenGL?

the Fiddler's picture

Edit: to answer your question, simply create another GameWindow, GLControl, GLWidget in a background thread. Use GameWindow if your first context was created with a GameWindow and GLControl with GLControl (this is not a strict requirement but mixing different toolkits is generally not advisable). Don't call GameWindow.Run(), GLControl.Show() - you only need a context, not an actual visible window.

This is certainly doable but it's generally more hassle than it's worth. There are better approaches that will yield as good or better results. From best (most compatible) to worst (least compatible):

  1. Load the Bitmap asynchronously on a worker (i.e. threadpool) thread, notify the main thread when complete and create the texture on the main thread. This is trivial to implement with a System.ComponentModel.BackgroundWorker, works on all hardware and fixes the real bottleneck (loading from disk).
  2. As above, but use GL.MapBuffer() on the main thread, write on the returned pointer from the worker thread directly and call GL.UnmapBuffer() on the main thread when complete. This can be slightly faster than the GL.TexImage2D approach above, but it won't generally make a difference unless you load lots of data continuously (e.g. when playing back an HD movie). GL.MapBuffer() requires OpenGL 1.5 or higher, so it's slightly less compatible than the previous approach.
  3. Use two shared contexts on two different threads (OpenTK shares contexts by default, unless otherwise told). Load textures on the second context, notify the first context when loading complete. This sounds good in theory but fails to work in practice: from the outset, you can write off Intel cards (30-40% of the market, they don't support context sharing). On the rest, context sharing is flakey at best and may or may not yield a performance improvement (some drivers use a global lock when performing OpenGL operations, so this may be just as slow as the single-threaded case). I would advise against this approach in most cases.

Do note that the real bottleneck is loading the file from disk, rather than uploading the decoded data to OpenGL. PCI-E buses have transfer rates in the range of several GB/s and modern IGPs share memory between the CPU and GPU, so you are unlikely to hit a bottleneck there.

My advice: fix the bottleneck with #1 and try #2 if you wish (there's a good chance it won't be necessary). Use #3 only if you have total control over your target hardware (i.e. we require Nvidia Quadro with driver x.y) and you can verify that it is, indeed, faster than #1 and #2.

nlraley's picture

I was pretty much wanting to go with a concept like you mention in 1 and 2. In my thread that loads the textures it is simply loading the area just outside the user's viewable area. This is done by loading the bitmap, then pushing it to the OpenGL.

What your saying from my interpretation is that loading the bitmap is the largest bottleneck followed by pushing that to to OpenGL

What my main thread merely does is render the already loaded textures.

So what I need to do is break up my thread into it's own class and during the constructor create the other context, making sure I set the context from the main thread before calling the constructor. Then set the context for to OpenGL control in the main for again

You k ow of any examples showing how to accomplish this? I've seen the documentation on crating a context, but how would I retrieve the context that the OpenGL control was using from the main thread?

Thanks for the help so far, it has helped a lot.

the Fiddler's picture

You can retrieve a context using GLControl.Context (instance property, returns the context bound to the specified GLControl instance) or GraphicsContext.CurrentContext (static property, returns the current context for the current thread, if one exists, or null otherwise).

Creating a second context is as simple as:

// In the main thread
GLControl glControl2 = new GLControl();
glControl2.CreateControl();
glControl2.Context.MakeCurrent(null); 
glControl1.MakeCurent(); // Ensure glControl1 is still current on the main thread
 
// In the background thread
glControl2.MakeCurent(); // glControl2 is now current on the background thread

After this, glControl1 will be current on the main thread and glControl2 on the background thread.

In any case, I would still recommend implementing approach #1 or #2 which don't require a second context. (In #1 and #2, the background thread does not issue any OpenGL commands, so it doesn't need an OpenGL context. It merely loads new tiles from disk as necessary and notifies the main thread for the actual bitmap->texture upload).

System.ComponentModel.BackgroundWorker makes this pattern very simple to implement: create a BackgroundWorker, place the new Bitmap() and LockBits() calls in the DoWork event (this runs in the background won't have access to OpenGL) and place the GL.TexImage2D() call in the RunWorkerCompleted event (this runs in the main thread and has access to OpenGL). This way you don't need two separate contexts and you avoid all issues that come with them.

nlraley's picture

Ah, that makes sense.

So do what I was doing just separate out the gl call after the thread loads the bmp.

Thanks a ton!

nlraley's picture

Okay, I've been trying to implement the background worker to load the Bitmaps and lock the bits in the thread, and then during the background worker complete event make my OpenGL calls; however, I am ending up with nothing but a black screen with a red X across it once it goes through this motion.

Here is what I have so far:

My background worker:

private void backgroundWorkerLoadBmp_DoWork(object sender, DoWorkEventArgs e)
{
	TextureLists lists = e.Argument as TextureLists;
 
	Texture.LoadBmps(lists);
 
	e.Result = lists;
}
 
private void backgroundWorkerLoadBmp_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
	TextureLists lists = e.Result as TextureLists;
 
	Texture.LoadTextures(lists);
 
	MapUtils.FetchTiles();
}

This receives a object with 2 lists, one of textures to load and another of textures to unload and is started right after determining which textures need to be loaded or unloaded.

In the doWork event of the background worker I call LoadBmps which in turn calls LoadBmp for each texture as follows:

public void LoadBmp()
{
	BMP = new Bitmap(Bitmap.FromFile(filename));
	BMPData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
}
 
public static void LoadBmps(object lists)
{
	TextureLists tLists = (TextureLists)lists;
 
	List<Texture> texturesToLoad = (List<Texture>)tLists.toLoad;
	List<Texture> texturesToUnload = (List<Texture>)tLists.toUnload;
 
	foreach (Texture texture in texturesToLoad)
		texture.LoadBmp();
}

Once these have completed I have successfully loaded each Bitmap and BitmapData for each of the textures and pass the lists as the result to the WorkerCompleted event.

This simply calls my LoadTextures call passing it the two lists which in turn calls load and unload for each texture in the two lists:

public void LoadTexture()
{            
	GL.GenTextures(1, out glTexture);
	GL.BindTexture(TextureTarget.Texture2D, GLTexture);           
 
	GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
	GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
 
	GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba, BMPData.Width, BMPData.Height, 0,
		OpenTK.Graphics.OpenGL.PixelFormat.Bgra, PixelType.UnsignedByte, BMPData.Scan0);
	GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);
 
	BMP.UnlockBits(BMPData);
	BMP.Dispose();
 
	Loaded = true;
}
 
public static void LoadTextures(object lists)
{
	TextureLists tLists = (TextureLists)lists;
 
	List<Texture> texturesToLoad = (List<Texture>)tLists.toLoad;
	List<Texture> texturesToUnload = (List<Texture>)tLists.toUnload;
 
	foreach (Texture texture in texturesToLoad)
		texture.LoadTexture();
 
	UnloadTextures(texturesToUnload);
}
 
public void UnloadTexture()
{
	GL.DeleteTexture(GLTexture);
	Loaded = false;
}

However, I am ending up with a screen with a red X across it after this is completed.

Any idea what I'm missing?

nlraley's picture

I traced the error down. It is actually occurring during the call to Bitmap.UnlockBits(bmpData).

Any idea why I'd receive an invalid parameter when calling this?

nlraley's picture

I figured it out. All my logic was correct.

I just needed to add locks to my texture objects in my lists and then I was calling BeginInvoke from the WorkCompleted event for the background worker instead of Invoke, which was inadvertently creating another thread.

Thanks for all the help.

Fidchells_eye's picture

Wow the hints I need for my project are here.