10

I'm on Yosemite 10.10.5 and Xcode 7, using Swift to make a game targeting iOS 8 and above.

EDIT: More details that might be useful: This is a 2D puzzle/arcade game where the player moves stones around to match them up. There is no 3D rendering at all. Drawing is already too slow and I haven't even gotten to explosions with debris yet. There is also a level fade-in, very concerning. But this is all on the simulator so far. I don't yet have an actual iPhone to test with yet and I'm betting the actual device will be at least a little faster.

I have my own Draw2D class, which is a type of UIView, set up as in this tutorial. I have a single NSTimer which initiates the following chain of calls in Draw2D:

[setNeedsDisplay]; // which calls drawRect, which is the master draw function of Draw2D

drawRect(rect: CGRect)
{
  scr_step(); // the master update function, which loops thru all objects and calls their individual update functions. I put it here so that updating and drawing are always in sync

  CNT = UIGraphicsGetCurrentContext(); // get the curret drawing context

  switch (Realm) // based on what realm im in, call the draw function for that realm
  {
    case rlm.intro: scr_draw_intro();
    case rlm.mm: scr_draw_mm();
    case rlm.level: scr_draw_level(); // this in particular loops thru all objects and calls their individual draw functions

    default: return;
  }

  var i = AARR.count - 1; // loop thru my own animation objects and draw them too, note it's iterating backwards because sometimes they destroy themselves
  while (i >= 0)
  {
    let A = AARR[i];
    A.scr_draw();

    i -= 1;
  }
}

And all the drawing works fine, but slow.

The problem is now I want to optimize drawing. I want to draw only in the dirty rectangles that need drawing, not the whole screen, which is what setNeedsDisplay is doing.

I could not find any tutorials or good example code for this. The closest I found was apple's documentation here, but it does not explain, among other things, how to get a list of all dirty rectangles so far. It does not also explicitly state if the list of dirty rectangles is automatically cleared at the end of each call to drawRect?

It also does not explain if I have to manually clip all drawing based on the rectangles. I found conflicting info about that around the web, apparently different iOS versions do it differently. In particular, if I'm gonna hafta manually clip things then I don't see the point of apple's core function in the first place. I could just maintain my own list of rectangles and manually compare each drawing destination rectangle to the dirty rectangle to see if I should draw anything. That would be a huge pain, however, because I have a background picture in each level and I would hafta draw a piece of it behind every moving object. What I'm really hoping for is the proper way to use setNeedsDisplayInRect to let the core framework do automatic clipping for everything that gets drawn on the next draw cycle, so that it automatically draws only that piece of the background plus the moving object on top.

So I tried some experiments: First in my array of stones:

func scr_draw_stone()
{
  // the following 3 lines are new, I added them to try to draw in only dirty rectangles
  if (xvp != xv || yvp != yv) // if the stone's coordinates have changed from its previous coordinates
  {
    MyD.setNeedsDisplayInRect(CGRectMake(x, y, MyD.swc, MyD.shc)); // MyD.swc is Draw2D's current square width in points, maintained to softcode things for different screen sizes.
  }

  MyD.img_stone?.drawInRect(CGRectMake(x, y, MyD.swc, MyD.shc)); // draw the plain stone
  img?.drawInRect(CGRectMake(x, y, MyD.swc, MyD.shc)); // draw the stone's icon
}

This did not seem to change anything. Things were drawing just as slow as before. So then I put it in brackets:

[MyD.setNeedsDisplayInRect(CGRectMake(x, y, MyD.swc, MyD.shc))];

I have no idea what the brackets do, but my original setNeedsDisplay was in brackets just like they said to do in the tutorial. So I tried it in my stone object, but it had no effect either.

So what do I need to do to make setNeedsDisplayInRect work properly?

Right now, I suspect there's some conditional check I need in my master draw function, something like:

if (ListOfDirtyRectangles.count == 0)
{
  [setNeedsDisplay]; // just redraw the whole view
}
else
{
  [setNeedsDisplayInRect(ListOfDirtyRecangles)];
}

However I don't know the name of the built-in list of dirty rectangles. I found this saying the method name is getRectsBeingDrawn, but that is for Mac OSX. It doesn't exist in iOS.

Can anyone help me out? Am I on the right track with this? I'm still fairly new to Macs and iOS.

DrZ214
  • 486
  • 5
  • 19
  • 1
    "...I'm betting the actual device will be at least a little faster." Most likely not. Unless your device is an iPad Pro, and your development machine an 11 inch Macbook Air. The simulator runs on your Mac's hardware and uses your Mac's drawing engine (not Open-GL ES like your iOS device). So the better assumption is that the device will be even slower. Have you tried using Instruments? Specifically the Time Profiler and the Core Animation templates? – Joride Jan 19 '16 at 20:13
  • 1
    You are probably looking at obj-c code when you see the square brackets around a call like that. In Swift, it creates an array with the return value, which you don't use, so hopefully the optimizer doesn't really create the array and it's a no-op after the call. – Lou Franco Jan 21 '16 at 22:09

1 Answers1

7

You should really avoid overriding drawRect if at all possible. Existing view/technologies take advantage of any hardware capabilities to make things a lot faster than manually drawing in a graphics context could, including buffering the contents of views, using the GPU, etc. This is repeated many times in the "View Programming Guide for iOS".

If you have a background and other objects on top of that, you should probably use separate views or layers for those rather than redraw them.

You may also consider technologies such as SpriteKit, SceneKit, OpenGL ES, etc.

Beyond that, I'm not quite sure I understand your question. When you call setNeedsDisplayInRect, it will add that rect to those that need to be redrawn (possibly merging with rectangles that are already in the list). drawRect: will then be called a bit later to draw those rectangles one at a time.

The whole point of the setNeedsDisplayInRect / drawRect: separation is to make sure multiple requests to redraw a given part of the view are merged together, and drawing only happens once per redraw cycle.

You should not call your scr_step method in drawRect:, as it may be called multiple times in a cycle redraw cycle. This is clearly stated in the "View Programming Guide for iOS" (emphasis mine):

The implementation of your drawRect: method should do exactly one thing: draw your content. This method is not the place to be updating your application’s data structures or performing any tasks not related to drawing. It should configure the drawing environment, draw your content, and exit as quickly as possible. And if your drawRect: method might be called frequently, you should do everything you can to optimize your drawing code and draw as little as possible each time the method is called.

Regarding clipping, the documentation of drawRect states that:

You should limit any drawing to the rectangle specified in the rect parameter. In addition, if the opaque property of your view is set to YES, your drawRect: method must totally fill the specified rectangle with opaque content.

Not having any idea what your view shows, what the various method you call do, what actually takes time, it's difficult to provide much more insight into what you could do. Provide more details into your actual needs, and we may be able to help.

jcaron
  • 17,302
  • 6
  • 32
  • 46
  • Okay i split up the update and draw code by having NSTimer call scr_run_gameloop {scr_step; [setNeedsDisplay];} But correct me if I'm wrong, but the native quartz drawing functions will use the GPU if at all possible, including the image.drawInRect function that draws images on the view, stretched to fit the rectangle you specify. Also when you say to provide more details, I can only guess at which details would be useful. What I think I need is, what is the name of the list of dirty rectangles that get stored with setNeedsDisplayInRect? and will setNeedsDisplay automatically clip things? – DrZ214 Jan 15 '16 at 02:43
  • Sorry, I am restrained by the character limit in comments. As for more details that might be useful, this is a puzzle/arcade game where the player moves stones around to match them up. But there will be explosions with debris and I haven't even gotten to that yet and it's already too slow (on the simulator. not sure if an actual iPhone will be fast enough). There is also a fade-in at the start of every level which has me concerned. My thoughts now are to put the background in its own view, which is opaque, and everything else in a second view that is not opaque. – DrZ214 Jan 15 '16 at 02:46
  • You should add details by editing your question, you'll have all the space you need. If you just have static images on top of each other that you move around, **do not** use `drawRect`, but regular views, layers, or SpriteKit. In the ideal case, that means the images will be uploaded into the GPU only once, and the GPU will do the compositing. Note that in some cases, performance on the simulator can be **very** different from an actual device (e.g. SpriteKit on the simulator makes very little use of the GPU). – jcaron Jan 15 '16 at 03:00
  • 1
    If you still want to use `drawRect`, why do you need the list of dirty rectangles? That's internal UIKit stuff. You only need to respond to calls to `drawRect:` which will be called once for each required rectangle. `setNeedsDisplay`/`setNeedsDisplayInRect:` does not clip anything, it just makes a note of the rectangles, which will later be passed on to `drawRect:`. As stated in the docs and above, it's your responsibility to only draw the bits that match the rectangle passed to `drawRect:`. – jcaron Jan 15 '16 at 03:02
  • But I would completely avoid `drawRect`/`setNeedsDisplay` and just use image views for each individual image, and move them around, or probably better yet, use SpriteKit (which is probably the best option for a game). – jcaron Jan 15 '16 at 03:06
  • yes that last part was what i feared. That means I have to manually clip or cull or whatever the proper term is to only draw content that exists inside that dirty rectangle, meaning Im gonna hafta do manual bounds checking. This is why I thought I needed the list of dirty rectangles, so i can get each one and do bounds checking. But now I see that drawRect is called one at a time for each dirty rectangle and so i reference it via the argument name that was passed, and its only one rectangle at a time. But boy that seems **really** inefficient. – DrZ214 Jan 15 '16 at 03:10
  • drawRect is the master draw function that loops thru all objects and draws them. Adding bounds checking for each one is only gonna slow it down more especially if the whole master draw is gonna be called 5 times for 5 dirt rectangles instead of just one time and each attempted draw bounds checks against 5 rectangles. I will look into SpriteKit, but I am seriously considering my own manual implementation of a dirty rectangle handler where each stone has a iAmDirty property and just gets skipped over in the mster draw if its false. – DrZ214 Jan 15 '16 at 03:11
  • Bounds checking is peanuts compared to copying around and compositing images. The whole goal of using rectangles is to only update the rectangles you need to. But again, I very strongly recommend you completely drop `drawRect` and `setNeedsDisplay*` and just use a hierarchy of views or SpriteKit. From your (very limited) description of what you want to do, there is no reason whatsoever to use `drawRect:`, and as explicitly recommended by Apple, you should avoid it at all costs. – jcaron Jan 15 '16 at 03:17
  • There was another Apple advisory from their official dev guide that said too many views can really slow things down and isn't recommended. It did not specify a solid number to that, though, and unfortunately I can't hunt it down now so I don't have the link. I could have 20 or maybe 30 stones per level so I'm suspicious. That leaves SpriteKit, which I'll hafta reinvestigate. If what you say is accurate, then using drawRect like I am could result in a rejection when I submit my game. – DrZ214 Jan 15 '16 at 03:38
  • No, it will not result in rejection, just very poor performance and additional complexity for no benefit. 20 or 30 views on screen, especially if they're static and reasonably sized images, is just peanuts. A single tab bar + navigation bar probably has a lot more than that. I've manipulated views with dozens of subviews, 3D transforms and more and iOS didn't even blink. Now if you get to hundreds of views, it could be another matter, and you would be better of with just layers, or Sprite Kit. – jcaron Jan 15 '16 at 03:42
  • They might be static 99% of the time, but unfortunately that pesky 1% raises its head. These stones must light up + explode. Sorry for the rant but I'm really sick of things like Unity that try to hide code from you and act like a wizard or force you into a hierarchy of controllers or something. Now it seems iOS is this way too. I already coded this and it works on PC, android, and iOS simulator, but slowly. I do not need something to code for me. Coding is my skill set. And i just cannot believe that native drawing via Quartz is somehow slower than setting up a network of dozens of views. – DrZ214 Jan 15 '16 at 04:02
  • In a nutshell, whatever SpriteKit does natively **must** be possible with "manual" code somehow... – DrZ214 Jan 15 '16 at 04:03
  • Definitely, but it mostly does not involve using `drawRect` at all, but rather using textures that are uploaded to the GPU. – jcaron Jan 15 '16 at 09:06
  • SpriteKit is done mostly in Metal for iOS 9, which means it is running the code on the GPU. – Good Doug Jan 21 '16 at 22:33
  • @GoodDoug Any word on of iOS 8? Right now that is my target and I'd hate to bump it up again...unless of course this game takes another year, by which time iOS 9 could be early enough. – DrZ214 Jan 25 '16 at 01:37
  • SpriteKit is done using native OpenGL ES calls in iOS 8... so it will still be done mostly on the GPU – Good Doug Jan 26 '16 at 19:29
  • @jcaron Could you help me with my problem? http://stackoverflow.com/questions/39855349/how-to-make-2-contradictory-methods-work-in-drawrect – Andy Jazz Oct 05 '16 at 11:54