Friday, November 26, 2010

Your Japanese Name for iOS, part 3

Goal for today will be to:
- Initialize offscreen buffer (CGBitmapContextCreate?)
- Draw to offscreen buffer with some kind of brush
- Flip offscreen buffer so that it's visible for debugging purposes

I've seen a few examples of CGBitmapContextCreate, but my biggest question right now is how to know what kind of buffer to create. Depending on the device the resolution differs.

...

Managed to get something on the screen. Got the default context for the view with UIGraphicsGetCurrentContext and drew some stuff. Was surprised that every time all the previous contents disappeared. So actually this default context isn't a bitmap at all?

Trying to make an offscreen buffer. self.frame.size seems to give same values both on old iPhone and iPhone 4, so it would seem to be the size of the display in points, not in pixels. Yes, it seems you can't depend on these values, other people on the internets are having problems with app not working as expected on retina display when doing this. So should take the scaling value into account I suppose.

I see some example code calling CGBitmapContextCreate with NULL as first argument. The first arg is supposed to be the data used for the bitmap context. Seems like a big no-no to have it be NULL. Or maybe it's a magic arg that causes the function to allocate its own space somehow?

"In iOS 4.0 and later, and Mac OS X v10.6 and later, you can pass NULL if you want Quartz to allocate memory for the bitmap. This frees you from managing your own memory, which reduces memory leak issues."

...

[UIScreen mainScreen].scale is 2 on retina display, 1 on older iPhone. So it seems the width of the offscreen buffer should be [UIScreen mainScreen].scale*self.frame.size.width pixels and similarly for height. Seems that many code snippets floating around on the net are missing this detail and then wonder why their bitmaps look pixelated.

...

- (CGSize)offscreenBufferSizeInPixels {
float scale = [UIScreen mainScreen].scale;
CGSize sizeInPoints = self.frame.size;
CGSize sizeInPixels = CGSizeMake(sizeInPoints.width * scale, sizeInPoints.height * scale);
return sizeInPixels;
}

- (CGContextRef)createOffscreenContext {
CGSize size = [self offscreenBufferSizeInPixels];
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(NULL, size.width, size.height, 8, size.width*4, colorSpace, kCGImageAlphaPremultipliedLast);
CGColorSpaceRelease(colorSpace);

// "Because you use a lower-left origin when drawing into a bitmap or PDF context, you must
// compensate for that coordinate system when rendering the resulting content into a view."
CGContextTranslateCTM(context, 0, size.height);
CGContextScaleCTM(context, 1.0, -1.0);
return context;
}

...

Now that I have a presumably empty offscreen context, how do I blit it to screen?

Answer:

CGImageRef offscreenContextAsImage = CGBitmapContextCreateImage(offscreenContext);
CGSize fsize = [self offscreenBufferSizeInPixels];
CGContextDrawImage(context, CGRectMake(0, 0, fsize.width, fsize.height), offscreenContextAsImage);

Strangely, even though I didn't wrap with UIImage, I had to comment out CGContextTranslateCTM and CGContextScaleCTM to get it to display correctly. Hit a new problem now; if I draw shapes for a while I get a memory warning. Am I reallocating something repeatedly? Let's see if I can use Instruments to see what it is even before I try to guess.

Ran Instruments by selecting Run > Run with Performance Tool > Allocations in Xcode. As I paint more things memory usage very gradually goes up. Starting at under 2MB it gradually goes to nearly 4MB as I draw more shapes, and then the application gets terminated. Tried another tool called "Leaks". It says the leaked object is a CGImage and that the responsible frame is CGTypeCreateInstanceWithAllocator. Hmm...

...

Aha, apparently CGBitmapContextCreateImage can actually allocate memory (or lazily copy-on-write, anyway) and not just get a pointer to existing image data. So I should CGImageRelease(offscreenContextAsImage). Works without leaks after this change, but disappointed with how slow this is. Only getting maybe ~15fps.

...

Mission accomplished. Can draw with a brush now, but it's slower than I hoped.