Wednesday, October 26, 2011

Loading PVRTC textures without blocking iOS User Interface

While working on my new iOS game, I decided to add one of those nice view transitions when you start a new level. At first I was puzzled by the lack of a nice animation, but then it dawned on me why: iOS will only remain smooth, and show its fancy animations as long as you do not block the main thread. When starting a level, I would first load large textures on the main thread. So how to avoid this?

iOS makes it easy to offload heavy lifting to other threads by using Grand Central Dispatch and its Objective-C construct for code blocks. The trick is to decide what to offload. At first I naively did the whole texture creation on another thread. The downside of this is that you now have two threads competing for the same OpenGL context. A way around would be mutex, but there is a more elegant and simple approach to it that does not involve locking the context or adding a mutex.

The slow part of texture creation is not the uploading with glTexImage() neither the creation of the texture name with glGenTextures(). It is the file loading of the image data, and the decoding of the PVRTC that gobbles up cycles. So only the loading and decoding has to be moved to another thread. All the OpenGL calls can remain on the main thread, and no context locking is required.

I expect that most iOS developers are using the Apple-provided class PVRTexture.h/PVRTexture.m which can be found at the developer.apple.com site. Adapting this class to offload the texture load/decode on another execution thread is simple. Just follow this recipe:

First add a new member variable to PVRTexture.h


dispatch_group_t dispatchGroup;

Then, add a static var and a class method to PVRTexture.m


static dispatch_queue_t dispatchQueue;

+(void)initialize
{
dispatchQueue = dispatch_queue_create( "textureLoadingQueue", nil );
}

Next, replace this section in createGLTexture


if ([_imageData count] > 0)
{
if (_name != 0)
glDeleteTextures(1, &_name);

glGenTextures(1, &_name);
glBindTexture(GL_TEXTURE_2D, _name);
}

with just a single line


glBindTexture(GL_TEXTURE_2D, _name);

last, replace the implementation of initWithContentsOfFile with


- (id)initWithContentsOfFile:(NSString *)path
{
self = [super init];

glGenTextures( 1, &_name );

dispatchGroup = dispatch_group_create();

dispatch_group_async( dispatchGroup, dispatchQueue, ^{
//NSLog( @"Loading texture data from file" );
NSData *data = [NSData dataWithContentsOfFile:path];

_imageData = [[NSMutableArray alloc] initWithCapacity:12];

_width = _height = 0;
_internalFormat = GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
_hasAlpha = FALSE;

assert( data );
const BOOL unpacked = [ self unpackPVRData:data ];
assert( unpacked );
});

dispatch_group_notify( dispatchGroup, dispatch_get_main_queue(), ^{
//NSLog( @"Creating GL texture for name %d", _name );
const BOOL created = [ self createGLTexture ];
assert( created );
});

dispatch_release( dispatchGroup );
dispatchGroup = 0;

return self;
}

Some notes: I added asserts to catch missing texture data and such. If you want more graceful failures, like the original had, you may want to adapt that. Also: with this change you can do some really neat stuff, like starting your game before the textures are loaded. OpenGL will happily draw the scene if the texture data is not yet uploaded to the GPU. The world will incrementally be populated with textures, before the player's eyes. Less Waiting, More Playing: how is that not a win?

I used the same tactic for the creation of OpenDE triangle meshes used for my physics simulation. I simply keep the sim delta-time at 0.0 as long as the triangle mesh has not been built yet: let the player absorb the environment while the game is still doing its initializations.

No comments: