1

Can anyone explain why redrawing to an offscreen CGLayer would cause rendering to slow down over time? Let me show you a test I've created to illustrate the problem.

@implementation DrawView


- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        //setup frame rate monitoring
        fps = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 100, 20)];
        fps.textColor = [UIColor whiteColor];
        fps.font = [UIFont boldSystemFontOfSize:15];
        fps.text = @"0 fps";
        [self addSubview:fps];
        frames = 0;
        lastRecord = [NSDate timeIntervalSinceReferenceDate];

        //create a cglayer and draw the background graphic to it
        CGContextRef context = UIGraphicsGetCurrentContext();
        cacheLayer = CGLayerCreateWithContext(context, self.bounds.size, NULL);

        CGImageRef background = [[UIImage imageNamed:@"background.jpg"] CGImage];
        CGContextRef cacheContext = CGLayerGetContext(cacheLayer);
        CGContextDrawImage(cacheContext, CGRectMake(0, 0, 768, 1024), background);

        //initialize cgimage stamp
        stamp = [[UIImage imageNamed:@"stamp.png"] CGImage];
        stampTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/60 target:self selector:@selector(stamp) userInfo:nil repeats:YES];
    }
    return self;
}

- (void) stamp {
    //calculate fps
    NSTimeInterval interval = [NSDate timeIntervalSinceReferenceDate];
    NSTimeInterval diff = interval-lastRecord;
    if (diff > 1.0) {
        float rate = frames/diff;
        frames = 0;
        lastRecord = [NSDate timeIntervalSinceReferenceDate];
        fps.text = [NSString stringWithFormat:@"%0.1f fps", rate];
    }
    //stamp the offscreen cglayer with the cgimage graphic
    CGRect stampRect = CGRectMake(0, 0, 200, 200);
    CGContextRef cacheContext = CGLayerGetContext(cacheLayer);
    CGContextDrawImage(cacheContext, stampRect, stamp);
    [self setNeedsDisplayInRect:stampRect];
}

- (void)drawRect:(CGRect)dirtyRect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextDrawLayerInRect(context, self.bounds, cacheLayer);
    frames++;
}

When I run this test in the ipad simulator or device it starts at 40 fps and drops at a constant rate over the course of 10 seconds until it's running at like 3 fps. Why is this happening? Shouldn't this run at a constant framerate? What kind of solution would allow me to 'stamp' an image over and over while maintaining a constant framerate?

seanalltogether
  • 3,542
  • 3
  • 26
  • 24
  • +1 I solved the same problem by creating the CGLayer in the initial drawRect call. – 9dan Oct 12 '11 at 09:31
  • For the full and definitive investigation of this amazing problem, check out all of this including all the comments: http://stackoverflow.com/questions/4739748/is-there-a-way-to-make-drawrect-work-right-now! And enjoy this including all the sample projects: http://stackoverflow.com/questions/4786754/mysterious-progressive-slowing-problem-in-run-loop-drawrect and the various sample solution projects provided by the participants. (Note! Many of the early comments are totally wrong, so read through fully.) It's amazing stuff... Felz is the bloke who actually solved it. He rocks. – Fattie Apr 22 '16 at 19:12

2 Answers2

5

Your problem is that you created your layer with a NULL context. UIGraphicsGetCurrentContext() is only valid during the draw loop. You call it outside the draw loop so it's NULL and so the layer can't cache anything. It still surprises me how badly this trashes performance over time. I suspect there might be a bug in CoreGraphics; you'd think this would just be slow; not "ever slowing." But still, it's not designed to work this way.

If you create a bitmap context and use that for your layer, you'll get your 60fps. You don't need to abandon CGLayer here.

All I did to get back to 60fps is to replace this:

CGContextRef context = UIGraphicsGetCurrentContext();

with this:

CGContextRef context = CreateBitmapContext(self.bounds.size);

Where CreateBitmapContext() is a function that returns the same thing your createBitmapContext sets up.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thanks for the update there. I'm curious, is there any advantage to using a CGLayer if i have my own bitmap context now? – seanalltogether Mar 10 '11 at 04:36
  • Read the CGLayer reference introduction for discussion of the performance benefits of using CGLayer. Note that generally CGLayer is not appropriate for offscreen buffering. Quartz handles this itself, and if this is why you're doing it, then you're fighting Quartz which is highly optimized for this problem. – Rob Napier Mar 10 '11 at 18:49
  • @Joe Blow, I guess it depends on what you consider intuitive. The fact that subverting the optimized drawing system and recursively reentering it in the middle causes performance problems should not be surprising. Forcing "drawing right now" may draw views that are obscured or hidden, which is wasteful, and fights animation optimizations. UIKit does not generally use a canvas object, which is what you're trying to recreate. But you can easily create a canvas object by drawing into a bitmap context which is the content for a layer. It does not require fancy trickery with the runloop. – Rob Napier Mar 29 '11 at 15:05
  • The CGLayer docs I describe goes into this. CGLayer can optimize to the video card (at least on Mac; I don't know how much optimization it gets on iOS). But my recommendation of using a bitmap context with a layer relates to CALayer, not CGLayer. Using a bitmap context and a CALayer will give you the canvas-approach you were describing without any trickery. – Rob Napier Mar 29 '11 at 18:46
  • +1 Thanks!, I solved my problem by creating the CGLayer in the initial drawRect call. – 9dan Oct 12 '11 at 09:29
0

I found the solution, although seemingly counter-intuitive, it's faster to create a cached bitmap context and then copy an image out of it to draw to the current context.

- (void) createBitmapContext {
    // Create the bitmap context
    void *          bitmapData;
    int             bitmapByteCount;
    int             bitmapBytesPerRow;
    CGSize          size = self.bounds.size;

    // Declare the number of bytes per row. Each pixel in the bitmap in this
    // example is represented by 4 bytes; 8 bits each of red, green, blue, and
    // alpha.
    bitmapBytesPerRow   = (size.width * 4);
    bitmapByteCount     = (bitmapBytesPerRow * size.height);

    // Allocate memory for image data. This is the destination in memory
    // where any drawing to the bitmap context will be rendered.
    bitmapData = malloc( bitmapByteCount );
    if (bitmapData == NULL)
    {
        //return nil;
    }
    cacheContext = CGBitmapContextCreate (bitmapData, size.width, size.height,8,bitmapBytesPerRow,
                                           CGColorSpaceCreateDeviceRGB(),kCGImageAlphaNoneSkipFirst);   
}


- (id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        //setup frame rate monitoring
        fps = [[UITextField alloc] initWithFrame:CGRectMake(0, 0, 100, 20)];
        fps.textColor = [UIColor whiteColor];
        fps.font = [UIFont boldSystemFontOfSize:15];
        fps.text = @"0 fps";
        [self addSubview:fps];
        frames = 0;
        lastRecord = [NSDate timeIntervalSinceReferenceDate];

        //create a bitmap context and draw the background graphic to it
        [self createBitmapContext];

        CGImageRef background = [[UIImage imageNamed:@"background.jpg"] CGImage];
        CGContextDrawImage(cacheContext, CGRectMake(0, 0, 768, 1024), background);

        //initialize cgimage stamp
        stamp = [[UIImage imageNamed:@"stamp.png"] CGImage];
        stampTimer = [NSTimer scheduledTimerWithTimeInterval:1.0/60 target:self selector:@selector(stamp) userInfo:nil repeats:YES];
    }
    return self;
}

- (void) stamp {
    //calculate fps
    NSTimeInterval interval = [NSDate timeIntervalSinceReferenceDate];
    NSTimeInterval diff = interval-lastRecord;
    if (diff > 1.0) {
        float rate = frames/diff;
        frames = 0;
        lastRecord = [NSDate timeIntervalSinceReferenceDate];
        fps.text = [NSString stringWithFormat:@"%0.1f fps", rate];
    }
    //stamp the offscreen bitmap context with the cgimage graphic
    CGRect stampRect = CGRectMake(0, 0, 200, 200);
    CGContextDrawImage(cacheContext, stampRect, stamp);
    [self setNeedsDisplayInRect:stampRect];
}

- (void)drawRect:(CGRect)dirtyRect {
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGImageRef cacheImage = CGBitmapContextCreateImage(cacheContext);
    CGContextDrawImage(context, self.bounds, cacheImage);
    CGImageRelease(cacheImage);
    frames++;
}
seanalltogether
  • 3,542
  • 3
  • 26
  • 24