Why is renderInContext so much slower than drawing to the screen? - ios

I have a pretty large CAShapeLayer that I'm rendering. The layer is completely static, but it's contained in a UIScrollView so it can move around and be zoomed -- basically, it must be redrawn every now and then. In an attempt to improve the framerate of this scrolling, I set shouldRasterize = YES on the layer, which worked perfectly. Because I never change any property of the layer it never has a rasterization miss and I get a solid 60 fps. High fives all around, right?
Until the layer gets a little bigger. Eventually -- and it doesn't take long -- the rasterized image gets too large for the GPU to handle. According to my console, <Notice>: CoreAnimation: surface 2560 x 4288 is too large, and it just doesn't draw anything on the screen. I don't really blame it -- 2560 x 4288 is pretty big -- but I spent a while scratching my head before I noticed this in the device console.
Now, my question is: how can I work around this limitation? How can I rasterize a really large layer?
The obvious solution seems to be to break the layer up into multiple sublayers, say one for each quadrant, and rasterize each one independently. Is there an "easy" way to do this? Can I create a new layer that renders a rectangular area from another layer? Or is there some other solution I should explore?
Edit
Creating a tiled composite seems to have really bad performance because the layers are re-rasterized every time they enter the screen, creating for a very jerky scrolling experience. Is there some way to cache those rasterizations? Or is this the wrong approach altogether?
Edit
Alright, here's my current solution: render the layer once to a CGImageRef. Create multiple tile layers using sub-rectangles from that image, and actually put those on the screen.
- (CALayer *)getTiledLayerFromLayer:(CALayer *)sourceLayer withHorizontalTiles:(int)horizontalTiles verticalTiles:(int)verticalTiles
{
CALayer *containerLayer = [CALayer layer];
CGFloat tileWidth = sourceLayer.bounds.size.width / horizontalTiles;
CGFloat tileHeight = sourceLayer.bounds.size.height / verticalTiles;
// make sure these are integral, otherwise you'll have image alignment issues!
NSLog(#"tileWidth:%f height:%f", tileWidth, tileHeight);
UIGraphicsBeginImageContextWithOptions(sourceLayer.bounds.size, NO, 0);
CGContextRef tileContext = UIGraphicsGetCurrentContext();
[sourceLayer renderInContext:tileContext];
CGImageRef image = CGBitmapContextCreateImage(tileContext);
UIGraphicsEndImageContext();
for(int horizontalIndex = 0; horizontalIndex < horizontalTiles; horizontalIndex++) {
for(int verticalIndex = 0; verticalIndex < verticalTiles; verticalIndex++) {
CGRect frame = CGRectMake(horizontalIndex * tileWidth, verticalIndex * tileHeight, tileWidth, tileHeight);
CGRect visibleRect = CGRectMake(horizontalIndex / (CGFloat)horizontalTiles, verticalIndex / (CGFloat)verticalTiles, 1.0f / horizontalTiles, 1.0f / verticalTiles);
CALayer *tile = [CALayer layer];
tile.frame = frame;
tile.contents = (__bridge id)image;
tile.contentsRect = visibleRect;
[containerLayer addSublayer:tile];
}
}
CGImageRelease(image);
return containerLayer;
}
This works great...sort of. One the one hand, I get 60fps panning and zooming of a 1980 x 3330 layer on a retina iPad. On the other hand, it takes 20 seconds to start up! So while this solution solves my original problem, it gives me a new one: how can I generate the tiles faster?
Literally all of the time is spent in the [sourceLayer renderInContext:tileContext]; call. This seems weird to me, because if I just add that layer directly I can render it about 40 times per second, according to the Core Animation Instrument. Is it possible that creating my own image context causes it to not use the GPU or something?

Breaking the layer into tiles is the only solution. You can however implement it in many different ways. I suggest doing it manually (creating layers & sublayers on your own), but many recommend using CATiledLayer http://www.mlsite.net/blog/?p=1857, which is the way maps are usually implemented - zooming and rotating is quite easy with this one. The tiles of CATiledLayer are loaded (drawn) on demand, just after they are put on the screen. This implies a short delay (blink) before the tile is fully drawn and AFAIK it is quite hard to get rid of this behaviour.

Related

iOS: Poor performance with CAGradientLayer

I'm using a class method to return a CAGradientLayer and insert it into a sublayer in a view. If I set the frame to a quarter of the screen size, width and height, my application runs smooth as butter, but most of the time if I set the frame size to the full width and height, the smoothness of the animation takes a big hit.
I found that most of the time when the animation smoothness is ruined, I can load in a large set of images, and even though the CAGradientLayer still exists, the smoothness is restored. This makes me think that the act of loading in images triggers the OS to empty some kind of cache. Is there anything that comes to mind that can be emptied manually?
Would somehow setting the size of the CAGradientLayer to something small and then stretching it out help even if I was to lose quality?
Before the CAGradientLayer is returned, I also tried setting these properties with little or no effect to the problem:
headerLayer.shouldRasterize = YES;
headerLayer.rasterizationScale=[UIScreen mainScreen].scale;
headerLayer.drawsAsynchronously = YES;
headerLayer.opaque = YES;

SKSpriteNode stretching when manually animating textures

I have a fairly simple animation with 8 identically sized images. I'm not using the built in animation methods as I want to manually control the speed of the animation on the fly. I'm using preloaded SKTexture's and doing [object setTexture:texture]; inside of the update:currentTime method.
The problem is that sometimes the texture gets really distorted/stretched. After a lot of debugging, I have narrowed it down to only happening when the node is stationary. In fact, if I move the node a pixel and move it back like this, the problem never occurs:
[self setTexture:texture];
CGPoint currentPosition = self.position;
self.position = CGPointMake(currentPosition.x + 1, currentPosition.y + 1);
self.position = currentPosition;
This feels extremely hacky to me. I think under the hood, it's triggering a redraw on the parent node. Has anyone else experienced this? I have two major questions. 1) What is the cause? and 2) How can I resolve this without resorting to a hack?
Here is a normal frame and a stretched version (I apologize for the quality, placeholder art...)
Edit: After a few comments, I realized that I forgot to mention that I scaled the size of the node smaller than the size of the texture. Even though the textures are the same size, applying a new texture to a node with a smaller size causes the bug.
It seems that upon setting the texture using setTexture: sprite node doesn't change it size, until being moved, resized, etc...
You can resolve this by manually setting the size after setting the texture.
[spriteNode setTexture:texture];
[spriteNode setSize:texture.size];

Core graphics using too much CPU

I am implementing the following graphics drawRect function but it uses more than 50% of the CPU - any idea on how I could solve that? I just draw a few random lines, but I want that they all have a different width.
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
#autoreleasepool {
CGContextRef context = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
float width = rect.size.width;
int nbLine=10; // i want to draw 10 paths
for (int iLine=1;iLine<nbLine;iLine++){
float Pathwidth=0.8*(nbLine-(float)iLine)/nbLine;
CGContextBeginPath(context);
CGContextSetLineWidth(context, Pathwidth); //each path should have its own width
CGPathMoveToPoint(path, NULL, 0,0);
for (int i=0;i<10;i++){
float x=width/(i+1);
float y=1;//for this example, I just put a fixed number here - it's normally an external variable
CGPathAddQuadCurveToPoint(path, NULL, x+width/10, y, x,0);
}
CGContextAddPath(context, path);
CGContextStrokePath(context);
}
CGPathRelease(path);
}
}
thank you !
There are a few things you can try.
Use instruments to find out exactly which line(s) are using the CPU.
Build the path in a UIBezierPath once and then draw them each time in drawRect.
Look at where setNeedsDisplay is being called from. Most likely each time it draws it isn't using up too much CPU. It is very possible that the problem is that it is rapidly drawing over and over.
If you are pressed for performance you can use a GLKView. Core Drawing is based in OpenGL with a whole bunch of optimizations set for graphical clarity and quality. But if those options are slowing you down to the point of non-usability then that may be your best bet.
My second suggestion would be to not call the draw so often. You said it gets called every 4 ms which is 250 times per second. The user can't see that fine of detail, so that is extravagant.
My third suggestion is to use a UIView, draw once, then modify it's transform based on your y variable. It appears as though you could do a simple y scaling to achieve what you are trying to do (no x changes, no width of line changes (after drawing it once)). I could be over simplifying based on your code, but it would be a good thing to try. You could also do a mix of this suggestion and your code and redraw if the y scale transform becomes too large.

Painfully slow software vectors, particularly CoreGraphics vs. OpenGL

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).
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?
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.
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
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.
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.
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".
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.

How to animate layer distance from viewer - user - camera change with perspective?

I'm trying to make an animation very similar to the one at the start of Air bnb iOS app.
Here's a video of the animation : video
The idea is to simulate a layer flying from being very close to the user to end sticking on a far away surface.
I've read some articles talking about manipulating the layer.transform.m34 and the one that helped me more is this one.
By applying perspective and a translation on the z-axis, I managed to get the layer look bigger.
Here's the code I used :
CALayer *aLayer = [CALayer layer];
aLayer.frame = ...
aLayer.backgroundColor = ...
CATransform3D perspectiveTransform = CATransform3DIdentity;
perspectiveTransform.m34 = 1.0f/-250.0f;
perspectiveTransform.m44 = 0.0f;
perspectiveTransform = CATransform3DTranslate(perspectiveTransform, 0.0f, 0.0f. -100.0f);
aLayer.transform = perspectiveTransform;
The problem is I can't get it to animate back to CATransform3DIdentity .
I'm not used to CoreAnimation so I may be trying a bad approach.
It would help a lot if someone could point me to what I'm doing wrong or to a better solution.
Thanks in advance!
You need to first create your layer and add it to the layer tree. Once the layer is part of the layer tree then implicit animations should work.
I think you may need to do this:
Create layer
Add layer to parent layer
run remaining animation code with performSelector:withObject:afterDelay: so system gets a chance to add the layer to the layer before your code to do implicit animations is run.

Resources