8

I'm working on an iOS app that requires drawing Bézier curves in real time in response to the user's input. At first, I decided to try using CoreGraphics, which has a fantastic vector drawing API. However, I quickly discovered that performance was painfully, excruciatingly slow, to the point where the framerate started dropping severely with just ONE curve on my retina iPad. (Admittedly, this was a quick test with inefficient code. For example, the curve was getting redrawn every frame. But surely today's computers are fast enough to handle drawing a simple curve every 1/60th of a second, right?!)

After this experiment, I switched to OpenGL and the MonkVG library, and I couldn't be happier. I can now render HUNDREDS of curves simultaneously without any framerate drop, with only a minimal impact on fidelity (for my use case).

  1. Is it possible that I misused CoreGraphics somehow (to the point where it was several orders of magnitude slower than the OpenGL solution), or is performance really that terrible? My hunch is that the problem lies with CoreGraphics, based on the number of StackOverflow/forum questions and answers regarding CG performance. (I've seen several people state that CG isn't meant to go in a run loop, and that it should only be used for infrequent rendering.) Why is this the case, technically speaking?
  2. If CoreGraphics really is that slow, how on earth does Safari work so smoothly? I was under the impression that Safari isn't hardware-accelerated, and yet it has to display hundreds (if not thousands) of vector characters simultaneously without dropping any frames.
  3. More generally, how do applications with heavy vector use (browsers, Illustrator, etc.) stay so fast without hardware acceleration? (As I understand it, many browsers and graphics suites now come with a hardware acceleration option, but it's often not turned on by default.)

UPDATE:

I have written a quick test app to more accurately measure performance. Below is the code for my custom CALayer subclass.

With NUM_PATHS set to 5 and NUM_POINTS set to 15 (5 curve segments per path), the code runs at 20fps in non-retina mode and 6fps in retina mode on my iPad 3. The profiler lists CGContextDrawPath as having 96% of the CPU time. Yes — obviously, I can optimize by limiting my redraw rect, but what if I really, truly needed full-screen vector animation at 60fps?

OpenGL eats this test for breakfast. How is it possible for vector drawing to be so incredibly slow?

#import "CGTLayer.h"

@implementation CGTLayer

- (id) init
{
    self = [super init];
    if (self)
    {
        self.backgroundColor = [[UIColor grayColor] CGColor];
        displayLink = [[CADisplayLink displayLinkWithTarget:self selector:@selector(updatePoints:)] retain];
        [displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
        initialized = false;

        previousTime = 0;
        frameTimer = 0;
    }
    return self;
}

- (void) updatePoints:(CADisplayLink*)displayLink
{
    for (int i = 0; i < NUM_PATHS; i++)
    {
        for (int j = 0; j < NUM_POINTS; j++)
        {
            points[i][j] = CGPointMake(arc4random()%768, arc4random()%1024);
        }
    }

    for (int i = 0; i < NUM_PATHS; i++)
    {
        if (initialized)
        {
            CGPathRelease(paths[i]);
        }

        paths[i] = CGPathCreateMutable();

        CGPathMoveToPoint(paths[i], &CGAffineTransformIdentity, points[i][0].x, points[i][0].y);

        for (int j = 0; j < NUM_POINTS; j += 3)
        {
            CGPathAddCurveToPoint(paths[i], &CGAffineTransformIdentity, points[i][j].x, points[i][j].y, points[i][j+1].x, points[i][j+1].y, points[i][j+2].x, points[i][j+2].y);
        }
    }

    [self setNeedsDisplay];

    initialized = YES;

    double time = CACurrentMediaTime();

    if (frameTimer % 30 == 0)
    {
        NSLog(@"FPS: %f\n", 1.0f/(time-previousTime));
    }

    previousTime = time;
    frameTimer += 1;
}

- (void)drawInContext:(CGContextRef)ctx
{
//    self.contentsScale = [[UIScreen mainScreen] scale];

    if (initialized)
    {
        CGContextSetLineWidth(ctx, 10);

        for (int i = 0; i < NUM_PATHS; i++)
        {
            UIColor* randomColor = [UIColor colorWithRed:(arc4random()%RAND_MAX/((float)RAND_MAX)) green:(arc4random()%RAND_MAX/((float)RAND_MAX)) blue:(arc4random()%RAND_MAX/((float)RAND_MAX)) alpha:1];
            CGContextSetStrokeColorWithColor(ctx, randomColor.CGColor);

            CGContextAddPath(ctx, paths[i]);
            CGContextStrokePath(ctx);
        }
    }
}

@end
Archagon
  • 2,470
  • 2
  • 25
  • 38
  • 2
    It's hard to say if you misused Core Graphics without seeing your code, or at least a more detailed description. Did you create a new CGPathRef on each frame (explicitly or implicitly) or did you create one in advance and re-use it? I bet that would have a performance impact. – benzado Mar 12 '13 at 19:46
  • I may have created a new CGPathRef each frame, but I'll have to double-check. (But even if I did, I can't imagine performance improving by several orders of magnitude, you know?) I know I tried to limit my redraws to only each newly-added segment of the spline, but even that didn't help very much. – Archagon Mar 12 '13 at 19:47
  • 1
    I built an app that redrew a complex path several times each frame using Core Graphics. Performance was good, even better than expected. The path consisted of around 100 elements, line width up to 100 px, with many round caps in between unconnected parts. I was impressed with the performance when drawing full screen on an iPad 2 and 3 (with retina resolution). – Nikolai Ruhe Mar 12 '13 at 19:58
  • You might have better luck with `CAShapeLayer` – nielsbot Mar 12 '13 at 20:00
  • Hmm, I just built a test app with rapid random spline drawing and ran it on an iPhone 5. Worked fine at 60fps. Maybe I DID do something catastrophically wrong. I'll try again on my iPad 3 when I get home. – Archagon Mar 12 '13 at 20:51
  • I've added a code sample. It's running incredibly slowly on my iPad 3. Nikolai, how did you manage to run 100 elements smoothly on a retina iPad? – Archagon Mar 13 '13 at 03:53
  • Just FYI, constructions like `arc4random() % upper_bound` are wrong. See `man arc4random`, DESCRIPTION section. – alecail Jun 30 '13 at 20:46
  • Part of what you're seeing is just the iPad 3. iPad 2 had the horsepower to do full-screen CPU-based rendering at an okay framerate... iPad 3 quadrupled the number of pixels on the screen without quadrupling the CPU performance. You'll see better results on iPad 4, iPad Air, iPad Air 2, and any iPad Mini, but you're still limited by CPU-based CG rendering as described in several of the answers posted so far. – rickster Nov 07 '14 at 18:21

4 Answers4

4

You really should not compare Core Graphics drawing with OpenGL, you are comparing completely different features for very different purposes.

In terms of image quality, Core Graphics and Quartz are going to be far superior than OpenGL with less effort. The Core Graphics framework is designed for optimal appearance , naturally antialiased lines and curves and a polish associated with Apple UIs. But this image quality comes at a price: rendering speed.

OpenGL on the other hand is designed with speed as a priority. High performance, fast drawing is hard to beat with OpenGL. But this speed comes at a cost: It is much harder to get smooth and polished graphics with OpenGL. There are many different strategies to do something as "simple" as antialiasing in OpenGL, something which is more easily handled by Quartz/Core Graphics.

Peter O.
  • 32,158
  • 14
  • 82
  • 96
johnbakers
  • 24,158
  • 24
  • 130
  • 258
  • What I'm mainly surprised by is just *how* slow CoreGraphics is for vector graphics. It doesn't seem like a particularly difficult task, but maybe I'm underestimating just how hard it is to push almost a million pixels a second. – Archagon Apr 12 '13 at 20:40
  • No, what you are underestimating is all of the mathematical work that goes on under the hood to create smooth lines and curves when calculating shades for adjacent pixels. With OpenGL, you have to do this yourself. – johnbakers Apr 13 '13 at 04:57
  • To extend this line of argument, @Archagon: your points of comparison aren't very equivalent. Safari isn't as slow as your test app because it's not trying to rasterize Bézier curves full-screen at 60fps. Rendering (and then scrolling) type is a very different process, which involves smaller rasterization jobs and a lot of caching. – rickster Nov 07 '14 at 18:18
3

First, see Why is UIBezierPath faster than Core Graphics path? and make sure you're configuring your path optimally. By default, CGContext adds a lot of "pretty" options to paths that can add a lot of overhead. If you turn these off, you will likely find dramatic speed improvements.

The next problem I've found with Core Graphics Bézier curves is when you have many components in a single curve (I was seeing problems when I went over about 3000-5000 elements). I found very surprising amounts of time spent in CGPathAdd.... Reducing the number of elements in your path can be a major win. From my talks with the Core Graphics team last year, this may have been a bug in Core Graphics and may have been fixed. I haven't re-tested.


EDIT: I'm seeing 18-20FPS in Retina on an iPad 3 by making the following changes:

Move the CGContextStrokePath() outside the loop. You shouldn't stroke every path. You should stroke once at the end. This takes my test from ~8FPS to ~12FPS.

Turn off anti-aliasing (which is probably turned off by default in your OpenGL tests):

CGContextSetShouldAntialias(ctx, false);

That gets me to 18-20FPS (Retina) and up to around 40FPS non-Retina.

I don't know what you're seeing in OpenGL. Remember that Core Graphics is designed to make things beautiful; OpenGL is designed to make things fast. Core Graphics relies on OpenGL; so I would always expect well-written OpenGL code to be faster.

Community
  • 1
  • 1
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • I've added a code sample above: 20fps in SD mode, 6fps in retina mode for just 5 curves animating at an attempted 60fps. Using the UIBezierPath method only added a couple of frames per second. – Archagon Mar 13 '13 at 03:56
3

Disclaimer: I'm the author of MonkVG.

The biggest reason that MonkVG is so much faster then CoreGraphics is actually not so much that it is implemented with OpenGL ES as a render backing but because it "cheats" by tessellating the contours into polygons before any rendering is done. The contour tessellation is actually painfully slow, and if you were to dynamically generate contours you would see a big slowdown. The great benefit of an OpenGL backing (verse CoreGraphics using direct bitmap rendering) is that any transform such a translation, rotation or scaling does not force a complete re-tessellation of the contours -- it's essentially for "free".

zerodog
  • 568
  • 4
  • 11
1

Your slowdown is because of this line of code:

[self setNeedsDisplay];

You need to change this to:

[self setNeedsDisplayInRect:changedRect];

It's up to you to calculate what rectangle has changed every frame, but if you do this properly, you will likely see over an order of magnitude performance improvement with no other changes.

jjxtra
  • 20,415
  • 16
  • 100
  • 140