Core Graphics Performance on iOS

旧街凉风 提交于 2019-12-20 08:12:28

问题


Summary

I'm working on a fairly straightforward 2D tower defense game for iOS.

So far, I've been using Core Graphics exclusively to handle rendering. There are no image files in the app at all (yet). I've been experiencing some significant performance issues doing relatively simple drawing, and I'm looking for ideas as to how I can fix this, short of moving to OpenGL.

Game Setup

At a high level, I have a Board class, which is a subclass of UIView, to represent the game board. All other objects in the game (towers, creeps, weapons, explosions, etc) are also subclasses of UIView, and are added as subviews to the Board when they are created.

I keep game state totally separate from view properties within the objects, and each object's state is updated in the main game loop (fired by an NSTimer at 60-240 Hz, depending on the game speed setting). The game is totally playable without ever drawing, updating, or animating the views.

I handle view updates using a CADisplayLink timer at the native refresh rate (60 Hz), which calls setNeedsDisplay on the board objects that need to have their view properties updated based on changes in the game state. All the objects on the board override drawRect: to paint some pretty simple 2D shapes within their frame. So when a weapon, for example, is animated, it will redraw itself based on the weapon's new state.

Performance Issues

Testing on an iPhone 5, with about 2 dozen total game objects on the board, the frame rate drops significantly below 60 FPS (the target frame rate), usually into the 10-20 FPS range. With more action on the screen, it goes downhill from here. And on an iPhone 4, things are even worse.

Using Instruments I've determined that only roughly 5% of the CPU time is being spent on actually updating the game state -- the vast majority of it is going towards rendering. Specifically, the CGContextDrawPath function (which from my understanding is where the rasterization of vector paths is done) is taking an enormous amount of CPU time. See the Instruments screenshot at the bottom for more details.

From some research on StackOverflow and other sites, it seems as though Core Graphics just isn't up to the task for what I need. Apparently, stroking vector paths is extremely expensive (especially when drawing things that aren't opaque and have some alpha value < 1.0). I'm almost certain OpenGL would solve my problems, but it's pretty low level and I'm not really excited to have to use it -- it doesn't seem like it should be necessary for what I'm doing here.

The Question

Are there any optimizations I should be looking at to try to get a smooth 60 FPS out of Core Graphics?

Some Ideas...

Someone suggested that I consider drawing all my objects onto a single CALayer instead of having each object on its own CALayer, but I'm not convinced that this would help based on what Instruments is showing.

Personally, I have a theory that using CGAffineTransforms to do my animation (i.e. draw the object's shape(s) in drawRect: once, then do transforms to move/rotate/resize its layer in subsequent frames) would solve my problem, since those are based directly on OpenGL. But I don't think it would be any easier to do that than just use OpenGL outright.

Sample Code

To give you a feel for the level of drawing I'm doing, here's an example of the drawRect: implementation for one of my weapon objects (a "beam" fired from a tower).

Note: this beam can be "retargeted" and it crosses the entire board, so for simplicity its frame is the same dimensions as the board. However most other objects on the board have their frame set to the smallest circumscribed rectangle possible.

- (void)drawRect:(CGRect)rect
{
    CGContextRef c = UIGraphicsGetCurrentContext();

    // Draw beam
    CGContextSetStrokeColorWithColor(c, [UIColor greenColor].CGColor);
    CGContextSetLineWidth(c, self.width);
    CGContextMoveToPoint(c, self.origin.x, self.origin.y);
    CGPoint vector = [TDBoard vectorFromPoint:self.origin toPoint:self.destination];
    double magnitude = sqrt(pow(self.board.frame.size.width, 2) + pow(self.board.frame.size.height, 2));
    CGContextAddLineToPoint(c, self.origin.x+magnitude*vector.x, self.origin.y+magnitude*vector.y);
    CGContextStrokePath(c);

}

Instruments Run

Here's a look at Instruments after letting the game run for a while:

The TDGreenBeam class has the exact drawRect: implementation shown above in the Sample Code section.

Full Size Screenshot


回答1:


Core Graphics work is performed by the CPU. The results are then pushed to the GPU. When you call setNeedsDisplay you indicate that the drawing work needs to occur afresh.

Assuming that many of your objects retain a consistent shape and merely move around or rotate you should simply call setNeedsLayout on the parent view, then push the latest object positions in that view's layoutSubviews, probably directly to the center property. Merely adjusting positions does not cause a thing to need to be redrawn; the compositor will simply ask the GPU to reproduce the graphic it already has at a different position.

A more general solution for games might be to ignore center, bounds and frame other than for initial setup. Simply push the affine transforms you want to transform, probably created using some combination of these helpers. That'll allow you aribtrarily to reposition, rotate and scale your objects without CPU intervention — it'll all be GPU work.

If you want even more control then each view has a CALayer with its own affineTransform but they also have a sublayerTransform that combines with the transforms of sublayers. So if you're so interested in 3d then the easiest way is to load a suitable perspective matrix as the sublayerTransform on the superlayer and then push suitable 3d transforms to the sublayers or subviews.

There's a single obvious downside to this approach in that if you draw once and then scale up you'll be able to see the pixels. You can adjust your layer's contentsScale in advance to try to ameliorate for that but otherwise you're just going to see the natural consequence of allowing the GPU to proceed with compositing. There's a magnificationFilter property on the layer if you want to switch between linear and nearest filtering; linear is the default.




回答2:


Chances are, you're overdrawing. That is, drawing redundant information.

So you will want to break up your view hierarchy into layers (as you also mentioned). Update/draw only what is needed. The layers can cache the composited intermediates, then the GPU can composite all those layers quickly. But you need to be careful to draw only what you need to draw, and invalidate only regions of layers which actually change.

Debug it: Open "Quartz Debug" and enable "Flash Identical Screen Updates", then run your app in the simulator. You want to minimize those colored flashes.

Once overdrawing is fixed, consider what you can render on a secondary thread (e.g. CALayer.drawsAsynchronously), or how you could approach compositing intermediate representations (e.g. caching) or rasterizing invariant layers/rects. Be careful to measure costs (e.g. memory and CPU) when performing these changes.




回答3:


If your code "has to redraw" on every frame, then it is better to think about how you can use the graphics hardware to implement the same thing, instead of calling back into your code to redraw the CALayer every time.

For example, with your line example, you could create render logic that consists of a series of tiles. The filled center portion could be a solid tile that you cache as a CALayer and then draw over and over to fill the area to a certain height. Then, have another CALayer that is the edge of the ray where it alpha fades to transparent going away from the ray edge. Render this CALayer at the top and bottom (with 180 degrees of rotation) so that you end up with a ray of a certain height that has a nice blended edge below and above. Then, repeat this process making the ray wider and then shorter until it finally ends.

You can then render the larger shape with graphics card hardware acceleration but your calling code does not need to actually draw and then transfer image data in every loop. Each "tile" will have already been transferred from the CPU to the GPU, and affine transforms in the GPU are very fast. Basically, you just don't want to render every time and then have to wait for all the rendered image memory to have to transfer to the GPU.



来源:https://stackoverflow.com/questions/13277031/core-graphics-performance-on-ios

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!