Wednesday, August 26, 2009

Updating Textures On The Fly With Threads

In my previous post I described how to load textures "in the background" using threads. It's a riff on loading GL objects on a thread - in this case, we use an atomic object to swap the ID of a stub resource for the real resource.

What if we want to update a texture while rendering? Ideally we'd like to have the texture updating running 100% asynchronously from the render loop, with no blocking or locks. The short of it is that this is possible by using a second texture (which is swapped in using atomic operations); why you can't just call glTexImage2D from a thread has a more complex answer.

The answer lies in Appendix D of the OpenGL specification, which describes the rules for state update when an object is changed. The basic rules are:
  • You always see state change immediately in your own context.
  • You don't see a change in another context until you bind the object at least once after the state change is known to have completed.
That second point is a really monstrous condition. Here are a few ways this can kill you:
  • The pointer to storage of an object is state. If a command changes that storage (e.g. respecify a texture in another format), the state rules apply!
  • You don't see the state until after you bind, but if you're dealing with a stale pointer, the state may not be useful before you bind. I'm not sure how actual GL implementations manage this - I've seen white flashes for textures on OS X when incorrectly dealing with state propagation.
  • The spec requires completion in the server, e.g. after a "finish" (or a sync object in the new 3.2 spec, but that's another blog post). That's a pretty harsh condition, and I believe that most gl drivers will accept a flush. Newer, more modern drivers may require a real sync and not a flush - glFlush only works because there is serialization in communication to the card.
When you put this all together, it becomes clear that you need to have your rendering thread stop using the object before the async load starts, then restart using it with a rebind after it ends.

That's an ugly enough condition that with X-Plane we simply double-buffer: we allocate a whole new texture object, then use atomic operations to swap it in by ID, then deallocate the old one (on the worker thread). If you think about how texture memory works, there are only two other options:
  • Client code holds off rendering while the async happens (the texture itself could be in an inconsistent state).
  • The driver double-buffers for you, so that the old bind of the old texture isn't invalid. It's unclear to me from the spec whether this is required of the driver and what the cost would be. It strikes me as inefficient at best.
By double-buffering with two texture objects we also get around the problem of having to rebind - since the new texture has a new ID, we have to bind anyway.

No comments:

Post a Comment