#pragma mark CALayer Texture Atlases

Monday, November 19, 2012 • Chris Liscio

Here’s a nifty trick I used recently to implement the fast-moving low-overhead time display in Capo. Maybe you can use it somewhere that you might have fast moving digits in your UI.

The naïve implementation of my time display actually involved a CATextLayer that rendered the time string of the format XX:XX.XXX. I then applied a CIBloom filter to get the desired glowing effect, and it worked out pretty well. Fast Macs are fast, so it’s no big deal…

I later decided to create individual images for digits, and just blitted them to the time display at draw time. Needless to say it was a big improvement over the previous approach and I kept it this way for a while.

However, when it came time to do the work for Retina, I decided to cut down CPU usage in a few spots, and that time display was unnecessarily hard on the CPU. Also, I already produce a texture atlas of digits for Capo on iOS and I preferred the look of the numbers there on the Retina display.

Now, how I approach this might be a little bit of dead-chicken-waving, but I like it so I’m sticking with it. In essence, these are the key bits of my approach:

The first two items should be fairly obvious to implement. Set up each individual CALayer so that it has a bounds rect that is equal to the size of one digit, and contentsGravity is set to kCAGravityBottomLeft to make the positioning math easier.

Pointing all of the layers’ contents properties to the same image reference is where the dead-chicken-waving comes in. I do this because I hope there are some CALayer smarts that share the image on the GPU between all the layers that reference it.

The third item above is where the magic happens. If you skipped the third step, you’d get all of your digits squashed together into the layer. That’s because you’re taking an image that is 10x the width of your layer (digits 0-9) and smushing it into the size of a single digit.

So how do we get around this? We use the contentsRect property on CALayer to specify the coordinates of the digit within the atlas image. The coordinates in the contextRect property are in the range of [0,1], so you just have to divide your locating rectangles by the dimensions of your image to get this right. If you know your OpenGL, interpreting this should feel familiar (except for the fact that OpenGL doesn’t actually work this way for GL_TEXTURE_RECTANGLE…)

Now, if you stopped here, then you’ll see something funny. By default, the layer is going to draw the whole texture atlas outside of its bounds, centered on the digit you requested. This is easily corrected by specifying YES for masksToBounds. You’ll also notice that the digits are sliding around like crazy rather than just flashing abruptly like you (probably) want.

You could solve this by wrapping all of your contentsRect changes in CATransaction blocks that disable actions. However, I strongly advise against doing this too much for a number of reasons (that I don’t wish to get into.)

Instead, I recommend that when you set the layer up, you explicitly disable actions on the properties you plan on changing regularly (and don’t usually want to see movement on).

myLayer.actions = @{ @"contentsRect" : [NSNull null], @"contents" : [NSNull null], @"contentsScale" : [NSNull null] };

Feel free to null out any other animatable properties that you might be changing regularly, but don’t want implicit animations fired for. If you ever do want specific actions, it’s probably better that you set up CAAnimations as needed. You’ll probably want to tweak the defaults, anyway.

I would have supplied code to illustrate the above, but I figured that it was better I shared the idea rather than letting it sit on my todo list forever. I think I have enough above to get you going, so here you are.

Thanks to Guy English for putting the seed of this idea in my head a while ago. He started out suggesting that I try maintaining a pool of CALayers for the digits to encourage reuse inside Core Animation. It wasn’t being as smart as we’d hoped, and I managed to stumble on this cool idea in the process of my research. Funny how ideas like this start out, and where they eventually end up.