Monday, September 14, 2009

You May Not Be Able To Update A Texture In A Thread

It took me a while to understand why this was the case - thanks to Chris for helping me wrap my head around it. Here's the deal: you may not be able to update an existing texture from a thread without getting "white flashes". I discovered this the hard way while working on X-Plane's texture pager but only recently understood why the OpenGL specification makes this so.

In a previous post I described the technique to update OpenGl textures on a thread by creating a new texure, flushing, and then using an atomic operation to "swap" the active texture ID for the rendering engine.

But this begs the question: why not just use glTexSubImage2D to update the texture on the fly and save all of that effort?

So first: if you can use glTexSubImage2D and you don't need mip-mapping, you can, maybe. You won't be able to guarantee which texture the main rendering thread uses unless you are willing to block the main thread (which you don't want to do). It's possible that the main thread could grab an old version.

I say maybe because: the gl spec allows you to do this kind of on-the-fly update for operations that change the contents of memory but not the layout of memory. So you are counting on the GL to not reallocate memory when you sub-texture. One would hope this is the case, but I have never tried it on Windows. I don't think the spec is very clear about when texture operations change memory.

The gl spec also requires glFinish (or the newer fence/sync APIs in OpenGL 3.2) to guarantee your changes take effect. In practice you really only need a flush on current drivers, but I don't know if this will continue to be the case - the flush is enough because current drivers are serialized at the command-to-GPU level, but one would almost hope that this bottleneck goes away.

Before we're done with glTexSubImage2D, we must note that your mip-maps can't all be updated at once unless you are using some kind of auto-mipping extension, so if you manually push mipmaps then you might have a case of inconsistent texturing due to old and new mipmaps being mixed.

What if you can't use glTexSubImage2D? (For example, X-Plane won't use glTexSubImage2D in about 16 different cases to work around driver bugs we've hit over the years.) Then you have a real problem.

The problem is that a texture object is not complete until all of its mip-map levels are complete and its texture parameters have been set. Until then, using it will give you white textures.

But you cannot atomically specify the entire texture object without blocking the main thread.

This is exactly what went wrong with my original implementation of on-the-fly texture updating: I would re-texture the largest mip level (using glTexImage2D) which promptly invalidated the texture, and then the main thread (which was not blocked) would flash white until the rest of the mip-map could be rebuilt.

It is for this reason that I suggest the atomic-operation double-buffering technique. It lets you build your mip-map levels in peace and guarantees consistent rendering, e.g. you always use the old or new texture.

As a final note, my implementation will delete the old texture from a thread after the atomic swap. This is a little bit dangerous if I read section D.1.2. of the spec correctly. From what I can tell, the object should still be valid to use if it is bound (e.g. until I unbind everywhere, we're okay) but the texture ID can be recycled quickly, so theoretically it might get updated mid-use, which would be undesirable.

If the goal is to guarantee a non-blocking rendering thread, a better design would be to "send" the dead objects to the main thread via message queue where they can be deleted in between frames; this would let memory bubble up just a little bit more.

No comments:

Post a Comment