🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

Would anybody know how to properly load an opengl texture after loading an image with std::async?

Started by
9 comments, last by Shaarigan 3 years, 10 months ago

I'm trying to asynchronously load images with stb_image and use them to create OpenGL textures. Thankfully std::async is working nicely and the image is apparently loaded on another thread. However, how do I create an OpenGL Texture after the image is loaded? OpenGL is not on that other thread so I'm getting compiler errors.

How do you all manage a situation like this?

One way I thought was storing the unsigned char *datas loaded with stb_image into a vector and then create OpenGL textures using this data but from the main thread. But when I try to free the data in the vector, I get a compiler error.

I'm pretty lost here. Thanks for reading guys, hope this makes sense.

OpenGL Version: 3.3
OS: Windows 10
Window/Input Framework: GLFW
Image Loading: stb_image

Advertisement

EBZ said:
I'm trying to asynchronously load images with stb_image and use them to create OpenGL textures. Thankfully std::async is working nicely and the image is apparently loaded on another thread. However, how do I create an OpenGL Texture after the image is loaded? OpenGL is not on that other thread so I'm getting compiler errors.

“Compiler errors"? As in, errors while compiling? I don't think thats what you mean now is it? Sounds more like some type of runtime-error

EBZ said:
One way I thought was storing the unsigned char *datas loaded with stb_image into a vector and then create OpenGL textures using this data but from the main thread. But when I try to free the data in the vector, I get a compiler error.

Are a you using synchronisation (mutexes) to make sure no two threads can write to the vector the same time? Also, you should be doing

std::vector<std::unique_ptr<char[]>> images;

(instead of) std::vector<char*> images;

This will automatically take care of freeing the image-data; meaning its even less likely for anything to go wrong when trying to delete the data.

@Juliean Thanks for the reply! Sorry, I should have shared some code in order put more information in my question. Here's a very short pastebin of what I'm doing if you're willing to take a look: https://pastebin.com/FhdJYf1X​ (for some reason i can't paste code here)

Regarding if I'm using mutexes, I am. Making sure I'm locking that thread until it's done. And you're right, I got my terminology completely wrong, I actually got a runtime error.

Sorry about the dumb questions but you're suggesting to use: std::vector<std::unique_ptr<char[]>> images.

When I'm pushing into the vector, what would it look like: images.push(std::make_unique<char[]>(data)); and then loaded in like:
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, images[i]); ?


Thanks again!

EBZ said:
Regarding if I'm using mutexes, I am. Making sure I'm locking that thread until it's done. And you're right, I got my terminology completely wrong, I actually got a runtime error.

Can you show us the runtime-errors you are getting then? Would be really helpful, because I'm not seing anything actually wrong with the code you just showed.

Sorry about the dumb questions but you're suggesting to use: std::vector<std::unique_ptr<char[]>> images.

When I'm pushing into the vector, what would it look like: images.push(std::make_unique<char[]>(data)); and then loaded in like:
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_RGB, w, h, 0, GL_RGB, GL_UNSIGNED_BYTE, images[i]); ?

Yeah, like that. I mean, now that I have seen your code I'm not sure how well that would work in your case, since that data returned by stbi_load I assume would have to be deleted by something like stbi_destory, no? In that case you could still use an std::unique_ptr<char*, CustomDeleter>, but yeah it would be a little more work

Other than that, there are probably just some read flags in your code:

  1. You are creating “Texture texture” on the stack, then create a shared-ptr by copying that object and putting that into a container. You should std::move the texture-object, otherwise you are at the very least wasting performance. In the worse case, if Texture has a destructor that clears that owned data, that would explain why you are getting errors since the local variables ~Texture-destructor would run, and since the vector of data has not been moved it would invoke delete on the data.
  2. Why are you even using an shared_ptr? Is there any shared ownership of the Texture-proxy object later on? If this is your final texture object, I think it is better if you instead created a local proxy just for loading and fully create the texture later on; which should then be eigther an std::unique_ptr; or even better just std::vector<TextureProxy>. Unless you really do intend on using dataPayload later on after the texture is loaded, but if I assume correctly and this is just for loading; it should not be part of your final Texture-class.

Why struggle with that data passing?

In OpenGL you can have another context on a second thread. If you do that, you have to mark that context as data context, so it won't render anything but you can pass data to it even if it isn't on the main thread. However this depends on the OS, I haven't used it ever in deep practice but did some toying on it.

However, to use another context created on a thread, you first need to detach the one from the main thread so GL can switch to your loader thread. Then you make it current in your loader thread after dat has been loaded properly, push those data to GL by glTexImage2D and undo everything by detaching the context from the thread and attaching it right back in the main thread

There are a few ways you can go about this, but the bottom line is that the thread doing the actual OpenGL texture creation will need to have an active OpenGL context. You do not need to detach the context from the current thread unless you are utilizing a single context on multiple thread. Each thread can have its own OpenGL context, with the only stipulation being that a context can be active only on 1 thread at at time. However, either case will involve synchronization between the loading thread and the thread using the OpenGL texture. Before I go into the ways, I took a look at the code in the link posted and I don't see where you are waiting for the futures returned by std::async, also you would have to validate that stbi_load doesn't maintain any global state and is thread-safe( I haven't use it so no idea). So here goes:

1. Loader Thread( no OpenGL context) : Load the texture data on a separate thread. When the load is complete, signal the draw thread or the thread with the active OpenGL context to create the texture using the loaded data. This assumes that data is loaded correctly and is available, From reading your post, this seems to be what you are having issues with and not the actual texture creation itself.

2. Loader Thread( OpenGL context): Load the texture data then issue the texture creation call via any of the OpenGL texture creation routine. Insert a fence into to command stream after the texture creation call and wait on the fence. This way you are guaranteed that the texture is ‘created'. There a few caveats here, but for simplicity, lets assume the texture is actually created then. Signal the main/draw thread that the texture is created by any of your standard synchronization primitive(s).

You can also go the route of using a PBO for uploading the texture via a thread, but that is a little more complex.
However, first I would validate that you can load the texture data via a thread and have it available to the main thread without any of the issues you mentioned above. What I've outline is the what come next which is what you ask, but doesn't seem to be the issue you are currently having.

Or, you can do my favorite, dump decoded pixel data on a socket that OGL context thread listens to and reads them. You can have unblocking read that will succeed only if the image thread has written data on it. Have a little header indicating size of pixel data , though, local reads/writes should complete, in a bunch, of fast way.

This is another take on it. maybe it works for you.

Load the data (images, vertices etc) asynchronous but create the OpenGL objects that use the loaded data (Textures, VBOs etc) on the main thread (where you created the GL context). If you are creating an engine for fun this might be a good enough solution to the problem because it is often the slow file access that stalls the main thread not creating the actual GL-stuff.

Texture can look something like this

//pseudo code
struct Texture {
	int m_oglId = GL_OBJECT_NOT_CREATED; //Make sure it is created before using it.
	Image m_image;
	
	//Method that can be called from whatever thread you like.
	void set_data(Image image, ...) {
		m_image = image;
		...
	}
	
	//Please just make a call from the main thread and 
	//FFS make sure that set_data has finished before calling
	void to_GPU() {
		if m_oglId is GL_OBJECT_NOT_CREATED
			m_oglId = createTheOglTextureObject(m_image.x, m_image.y, ...);
	}
};

Thank you all again for the great answers! Using all the suggestions you all provided, I was able to not only get my textures loading asynchronously, but also the models I was loading using assimp. So nice to see the game respond immediately and now I can even make a loading screen.

This is definitely more difficult than using Unity, but it's a lot more fun. Wouldn't like to go back. Thanks again all!

EBZ said:
This is definitely more difficult than using Unity, but it's a lot more fun

Now you have an impression of the work modern game engines abstract away in the background for you ?

This topic is closed to new replies.

Advertisement