Drawrect with CGBitmapContext is too slow
Asked Answered
G

7

5

So I've got a basic drawing app in the process that allows me to draw lines. I draw to an off screen bitmap then present the image in drawRect. It works but its way too slow, updating about half a second after you've drawn it with your finger. I took the code and adapted it from this tutorial, http://www.youtube.com/watch?v=UfWeMIL-Nu8&feature=relmfu , as you can see in the comments people are also saying its too slow but the guy hasn't responded.

So how can I speed it up? or is there a better way to do it? any pointers will be appreciated.

Heres the code in my DrawView.m.

-(id)initWithCoder:(NSCoder *)aDecoder {
     if ((self=[super initWithCoder:aDecoder])) {
         [self setUpBuffer];
     }

     return self;
}

-(void)setUpBuffer {
     CGContextRelease(offscreenBuffer);

     CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();

     offscreenBuffer = CGBitmapContextCreate(NULL, self.bounds.size.width, self.bounds.size.height, 8, self.bounds.size.width*4, colorSpace, kCGImageAlphaPremultipliedLast);
     CGColorSpaceRelease(colorSpace);

     CGContextTranslateCTM(offscreenBuffer, 0, self.bounds.size.height);
     CGContextScaleCTM(offscreenBuffer, 1.0, -1.0);
}


-(void)drawToBuffer:(CGPoint)coordA :(CGPoint)coordB :(UIColor *)penColor :(int)thickness {

     CGContextBeginPath(offscreenBuffer);
     CGContextMoveToPoint(offscreenBuffer, coordA.x,coordA.y);
     CGContextAddLineToPoint(offscreenBuffer, coordB.x,coordB.y);
     CGContextSetLineWidth(offscreenBuffer, thickness);
     CGContextSetLineCap(offscreenBuffer, kCGLineCapRound);
     CGContextSetStrokeColorWithColor(offscreenBuffer, [penColor CGColor]);
     CGContextStrokePath(offscreenBuffer);

}

- (void)drawRect:(CGRect)rect {
    CGImageRef cgImage = CGBitmapContextCreateImage(offscreenBuffer);
    UIImage *image =[[UIImage alloc] initWithCGImage:cgImage];
    CGImageRelease(cgImage);
    [image drawInRect:self.bounds];

}

Works perfectly on the simulator but not device, I imagine that's something to do with processor speed.

I'm using ARC.

Grubby answered 29/6, 2012 at 12:5 Comment(8)
Maybe calling: [self setNeedsDisplay]; or [self setNeedsDisplayInRect:rect]; in the method that draws the line might force a faster update?Varden
Have you used instruments to determine where the time is being spent? One obvious thing that jumps out is the pointless creation of the UIImage. CGContextDrawImage... can use a CGimage directly.Squirt
What's the iOS device you're using to test?Bolduc
@Squirt the drawrect method is where the time is being spentGrubby
Please see this answer: #10410606Bolduc
@jammycoder well yes, we knew it would be drawRect! You can double-click on that line and it will open your actual method, each line will have a number next to it indicating the time spent.Squirt
@Squirt this line [image drawInRect:self.bounds];Grubby
Why would you want to redraw the image every time. You could simply place an UIImageView below your custom view.Monarchy
Y
7

I tried to fix your code, however as you only seem to have posted half of it I couldn't get it working (Copy+pasting code results in lots of errors, let alone start performance tuning it).

However there are some tips you can use to VASTLY improve performance.

The first, and probably most noticeably, is -setNeedsDisplayInRect: rather then -setNeedsDisplay. This will mean that it only redraws the little rect that changed. For an iPad 3 with 1024*768*4 pixels that is a lot of work. Reducing that down to about 20*20 or less for each frame will massively improve performance.

CGRect rect;
rect.origin.x = minimum(coordA.x, coordB.x) - (thickness * 0.5);
rect.size.width = (maximum(coordA.x, coordB.x) + (thickness * 0.5)) - rect.origin.x;
rect.origin.y = minimum(coordA.y, coordB.y) - (thickness * 0.5);
rect.size.height = (maximum(coordA.y, coordB.y) + (thickness * 0.5)) - rect.origin.y;
[self setNeedsDisplayInRect:rect];

Another big improvement you could make is to only draw the CGPath for this current touch (which you do). However you then draw that saved/cached image in the draw rect. So, again, it is redrawn each frame. A better approach is to have the draw view being transparent and then to use a UIImageView behind that. UIImageView is the best way to display images on iOS.

- DrawView (1 finger)
   -drawRect:
- BackgroundView (the image of the old touches)
   -self.image

The draw view would itself then only ever draw the current touch only the part that changes each time. When the user lifts their finger you can cache that to a UIImage, draw that over the current background/cache UIImageView's image and set the imageView.image to the new image.

That final bit when combining the images involves drawing 2 full screen images into an off screen CGContext and so will cause lag if done on the main thread, instead this should be done in a background thread and then the result pushed back to the main thread.

* touch starts *
- DrawView : draw current touch
* touch ends *
- 'background thread' : combine backgroundView.image and DrawView.drawRect
    * thread finished *
    send resulting UIImage to main queue and set backgroundView.image to it;
    Clear DrawView's current path that is now in the cache;

All of this combined can make a very smooth 60fps drawing app. However, views are not updated as quickly as we'd like so the drawing when moving the figure faster looks jagged. This can be improved by using UIBezierPath's instead of CGPaths.

CGPoint lastPoint = [touch previousLocationInView:self];
CGPoint mid = midPoint(currentPoint, lastPoint);
-[UIBezierPath addQuadCurveToPoint:mid controlPoint:lastPoint];
Yarber answered 7/7, 2012 at 13:50 Comment(0)
S
4

The reason it is slow is because every frame you are creating a bitmap and trying to draw that.

You asked for better ways of doing it? Have you looked at the apple sample code for a drawing app on iOS? If you don't like that, then you can always use cocos2d which provides a CCRenderTexture class (and sample code).

Currently, you are using a method which you already know is not efficient.

Succession answered 2/7, 2012 at 8:41 Comment(0)
F
2

With this approach I suppose you should consider using background thread for all hard work of image rendering and main thread for UI updates only, i. e.

__block UIImage *__imageBuffer = nil;

- (UIImage *)drawSomeImage
{
    UIGraphicsBeginImageContext(self.bounds);

    // draw image with CoreGraphics

    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();

    return image;
}

- (void)updateUI
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

        // prepare image on background thread

        __imageBuffer = [self drawSomeImage];

        dispatch_async(dispatch_get_main_queue(), ^{

            // calling drawRect with prepared image

            [self setNeedsDisplay];

        });
    });
}

- (void)drawRect
{
    // draw image buffer on current context

    [__imageBuffer drawInRect:self.bounds];
}

I am omitting some details for making the optimization more clear. Even better to switch to UIImageView. This way you could get rid from critically important - (void)drawDect method and update image property of the UIImageView when the image is ready.

Footfall answered 3/7, 2012 at 22:41 Comment(0)
S
1

Well I think you need to change your logic. You may get some very good idea with the help of this link http://devmag.org.za/2011/04/05/bzier-curves-a-tutorial/ and if you think that you have no time to make understanding then you may go directly to this code https://github.com/levinunnink/Smooth-Line-View :) I hop this will help you a lot.

Spiritless answered 4/7, 2012 at 9:55 Comment(0)
E
0

Use CgLayer for caching your paths, read the docs, Its best for optimization.

Embody answered 5/7, 2012 at 13:37 Comment(0)
L
0

Whatever the method u've suggested is way too inefficient, because creating the image every time you move the finger is inappropriate.

If its just paths that you need to draw, then have a CGMutablePathref as a member variable, and in draw rect just move to the specified point using CGPath functions.

And more importantly, while refreshing the view, call setNeedsDisplayInRect passing only the area that you need to draw. Hope it will work for you.

Lofty answered 5/7, 2012 at 19:8 Comment(0)
B
0

I did something exactly like this. Check out the Pixelate app on AppStore. In order to draw , I used tiles in my code. After all , when you toch the screen and draw something you need to re-draw the entire image which is a very heavy operation. If you like the way Pixelate is moving , here's how I did it:

1)Split my image in n x m tiles. That was so I can change those values and obtain bigger/smaller tiles. In the worst case scenario (the user taps at the intersection of 4 tiles) you have to re-draw those 4 tiles. Not the entire image.

2) Make a 3 dimensional matrix in which I was storring the pixel information of each tile. So matrix[0][0][0] was the red value ( each pixel has a RGB or RGBA value depending if you are using pngs or jpgs) of the first pixel of the first tile.

3) Get the location the user pressed and calculate the tiles that need to be modified.

4) Modify the values in the matrix and update the tiles that need to update.

NOTE: This most certainly isn't the best option. It's just an alternative. I mentioned it because I think it is close to what you have right now. And it worked for me on an iPhone 3GS. If you are targeting >= iPhone 4 , you should be more than ok.

Regards,

George

Bazluke answered 6/7, 2012 at 7:52 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.