5

I use a CATiledLayer in my app, and as a result, drawing of that layer is done in a background thread. That is, the drawLayer:inContext: method of my delegate is called from a background thread. The setNeedsDisplayInRect used to invalidate parts of the CATiledLayer is always called from the main thread.

Because they are independent threads, it occasionally happens that the setNeedsDisplayInRect is called while the background thread is in the drawLayer:inContext method. I have noticed that the setNeedsDisplayInRect is ignored in that situation (drawLayer:inContext is not called again).

I have logged a bug to Apple, because I think that is not correct. But I have a hard time figuring out how to work around this situation. Do you have good ideas?

EDIT:

I tested Stanislaw's answer using the following code:

- (void) setNeedsDisplayInRect:(CGRect)rect
{
    NSLog(@"setNeedsDisplayInRect:%@", NSStringFromCGRect(rect));
    [super setNeedsDisplayInRect:rect];
}

- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)gc
{
    CGRect bounds = CGContextGetClipBoundingBox(gc);
    NSLog(@"drawLayer:inContext: bounds=%@", NSStringFromCGRect(bounds));

    dispatch_async(dispatch_get_current_queue(), ^{
        [self setNeedsDisplayInRect:bounds];
    });

    CGContextSetFillColorWithColor(gc, testColor.CGColor);
    CGContextFillRect(gc, bounds);
    sleep(0.2); // simulate the time it takes to draw complicated graphics
    NSLog(@"end drawLayer:inContext: bounds=%@", NSStringFromCGRect(bounds));
}

As given, the code does cause drawing to repeat indefinitely, but sometimes there is a delay up to 5 seconds between the setNeedsDisplayInRect:, and the corresponding drawLayer:inContext:, in which nothing else is happening. See the log below as example. Note the irregular behaviour: in the first second, some tiles are redrawn multiple times, others only once. Then there is a pause of 5 seconds, and the cycle starts over again.

This was tested on the simulator with IOS6.0 (I choose that version, because earlier versions have another bug that is fixed in 6.0: they draw the same tiles twice sometimes).

2012-10-27 15:51:38.771 TiledLayerTest[39934:15a13] drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:38.774 TiledLayerTest[39934:15a13] end drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:38.774 TiledLayerTest[39934:1570f] drawLayer:inContext: bounds={{300, 0}, {20, 300}}
2012-10-27 15:51:38.776 TiledLayerTest[39934:1570f] end drawLayer:inContext: bounds={{300, 0}, {20, 300}}
2012-10-27 15:51:38.776 TiledLayerTest[39934:1630b] setNeedsDisplayInRect:{{0, 300}, {300, 180}}
2012-10-27 15:51:38.777 TiledLayerTest[39934:1540f] setNeedsDisplayInRect:{{300, 0}, {20, 300}}
2012-10-27 15:51:38.780 TiledLayerTest[39934:15a13] drawLayer:inContext: bounds={{300, 0}, {20, 300}}
2012-10-27 15:51:38.781 TiledLayerTest[39934:15a13] end drawLayer:inContext: bounds={{300, 0}, {20, 300}}
2012-10-27 15:51:38.782 TiledLayerTest[39934:1540f] setNeedsDisplayInRect:{{300, 0}, {20, 300}}
2012-10-27 15:51:38.789 TiledLayerTest[39934:1570f] drawLayer:inContext: bounds={{0, 0}, {300, 300}}
2012-10-27 15:51:38.791 TiledLayerTest[39934:15a13] drawLayer:inContext: bounds={{300, 300}, {20, 180}}
2012-10-27 15:51:38.792 TiledLayerTest[39934:15a13] end drawLayer:inContext: bounds={{300, 300}, {20, 180}}
2012-10-27 15:51:38.793 TiledLayerTest[39934:1570f] end drawLayer:inContext: bounds={{0, 0}, {300, 300}}
2012-10-27 15:51:38.795 TiledLayerTest[39934:1540f] setNeedsDisplayInRect:{{0, 0}, {300, 300}}
2012-10-27 15:51:38.795 TiledLayerTest[39934:1540f] setNeedsDisplayInRect:{{300, 300}, {20, 180}}
2012-10-27 15:51:38.798 TiledLayerTest[39934:15a13] drawLayer:inContext: bounds={{0, 0}, {300, 300}}
2012-10-27 15:51:38.800 TiledLayerTest[39934:15a13] end drawLayer:inContext: bounds={{0, 0}, {300, 300}}
2012-10-27 15:51:38.802 TiledLayerTest[39934:1630b] setNeedsDisplayInRect:{{0, 0}, {300, 300}}
2012-10-27 15:51:38.806 TiledLayerTest[39934:1570f] drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:38.808 TiledLayerTest[39934:1630b] setNeedsDisplayInRect:{{0, 300}, {300, 180}}
2012-10-27 15:51:38.809 TiledLayerTest[39934:1570f] end drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:38.813 TiledLayerTest[39934:15a13] drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:38.816 TiledLayerTest[39934:1630b] setNeedsDisplayInRect:{{0, 300}, {300, 180}}
2012-10-27 15:51:38.816 TiledLayerTest[39934:15a13] end drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:43.774 TiledLayerTest[39934:1540f] drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:43.776 TiledLayerTest[39934:1540f] end drawLayer:inContext: bounds={{0, 300}, {300, 180}}
2012-10-27 15:51:43.776 TiledLayerTest[39934:1630f] drawLayer:inContext: bounds={{0, 0}, {300, 300}}
fishinear
  • 6,101
  • 3
  • 36
  • 84
  • did you ever find a solution to this? I'm having a similar issue with MapKit, which uses CATiledLayer... – TomSwift Apr 13 '12 at 20:45
  • Unfortunately not. I did not get an answer from Apple either. I think the best you can do is to send the setNeedsDisplayInRect with a tiny delay, so that it comes after the drawLayer:inContext: is finished. – fishinear Apr 14 '12 at 11:27
  • If it is a problem in your work, then log a bug towards Apple as well. That raises the priority for Apple. – fishinear Apr 14 '12 at 11:49
  • @fishinear, did you have any progress on this issue since then? – Stanislav Pankevich Oct 10 '12 at 21:09
  • @Stanislaw Not really. And I have had no feed-back from Apple. I have moved on, and made my own tiled layer implementation, due to this and other limitations of CATiledLayer. – fishinear Oct 12 '12 at 14:40
  • @fishinear, I guess, this issue is probably irrelevant for you now, but could you please review the answer, I've posted? – Stanislav Pankevich Oct 26 '12 at 23:05
  • @fishinear, my little apologize: I had somehow overlooked that you worked with CATilesLayer while my issue is related to the MapKit's canDrawMapRect and DrawMapRect methods for overlays. Though it seems to me that this problem with threads is common for both. I don't see 5 seconds delays using dispatch_async in my "Map's overlays" case. – Stanislav Pankevich Oct 27 '12 at 15:10
  • @Stanislaw I suspect that MapKit uses CATiledLayer underneath, hence the similar behavior. And I have no idea where the 5 seconds come from. My suspicion is, that it does not matter much which thread you call the setNeedsDisplay from, as long as you make sure it is not in the middle of a drawLayer:inContext: (or canDraw... in your case). The easiest way to do that, IMHO, is still to call it after a small delay. – fishinear Oct 28 '12 at 14:17
  • you should also see [this question](http://stackoverflow.com/questions/8175374/block-mac-ui-controls-painting-redrawing/8175444#8175444) as the first answer would seem to have a better solution. – Grady Player Nov 16 '11 at 19:30
  • I thought about that, but thought that this might cause the threads to switch immediately after my code in the drawLayer:inContext:, before the OS has a chance to reset the setNeedsDisplay flag (or whatever it uses internally to detect whether it needs to call drawLayer again). That would still cause the same issue. – fishinear Nov 16 '11 at 19:51
  • Yes and no - I want to postpone setting the needsDisplay flag while I am drawing _and until the Apple code resets the flag_. Hopefully, Apple resets it right after my drawing code, which would reduce the chances of it going wrong, but there would still be a race condition. – fishinear Nov 21 '11 at 10:52
  • all you want to prevent is the setting the needsDisplay flag while you are drawing, you could do that easily with a spin lock or mutex, you essentially want to block the non-drawing thread for the duration of the refresh – Grady Player Nov 17 '11 at 17:40

2 Answers2

0

I've posted my answer to the similar issue: setNeedsDisplayInMapRect doesn't trigger new drawMapRect: call (just a link to not duplicate the answer here).

Shortly: you should dispatch a call of setNeedsDisplayInRect method to dispatch_get_main_queue().

Community
  • 1
  • 1
Stanislav Pankevich
  • 11,044
  • 8
  • 69
  • 129
  • Thanks for your efforts, but unfortunately, that does not work reliably. First of all, when I test your approach, it does keep on doing redraws reliably, but sometimes there is a delay of up to 5 seconds between the setNeedsDisplayInRect, and the drawLayer:inContext. – fishinear Oct 27 '12 at 13:59
  • Also, in a practical program, you would not detect that you need to do a redraw on the drawing thread. Typically you would detect that on the main thread. If I change your example to do the setNeedsDisplayInRect on the main thread (on get_main_queue() instead of get_current_queue()), then it fails again (no redraws at all). – fishinear Oct 27 '12 at 14:01
  • Just a guess, on your first comment: don't you think that this 5 seconds are being taken to process the other tiles that were added to the map queue before you dispatch_async'ed you setNeedsDisplayInRect, so it _is being_ processed, but after these "old" ones? – Stanislav Pankevich Oct 27 '12 at 14:10
  • On your second: I did inspected the queue that is used for tiles inside _canDraw..._ method body - I don't know how it is related to dispatch_get_main_queue() (need to check!) but I am sure that this queue is the same for all _canDraw..._ invocations by map. Just interesting: Do you call _setNeedsDisplatInRect_ inside _canDraw..._ method? Would be interesting to look at the code you are using. I am myself thinking hard to have any final point about this issue. – Stanislav Pankevich Oct 27 '12 at 14:20
  • I will add what I did to the question, makes it easier to talk about it. – fishinear Oct 27 '12 at 14:23
-1

You can try using a NSRecursiveLock in this way:

- (void) setNeedsDisplayInRect:(CGRect)rect
{
    [self.lock lock]
    NSLog(@"setNeedsDisplayInRect:%@", NSStringFromCGRect(rect));
    [super setNeedsDisplayInRect:rect];
    [self.lock unlock]
}

- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)gc
{
    [self.lock lock]
    CGRect bounds = CGContextGetClipBoundingBox(gc);
    NSLog(@"drawLayer:inContext: bounds=%@", NSStringFromCGRect(bounds));


    // drawing code


    NSLog(@"end drawLayer:inContext: bounds=%@", NSStringFromCGRect(bounds));
    [self.lock unlock]
}

This ensures that setNeedsDisplayInRect is not called while the drawing is in progress. However this can affect the performance because the lock can block the main thread and you cannot draw multiple tiles in parallel.

Felix
  • 35,354
  • 13
  • 96
  • 143
  • 2
    There is another disadvantage besides the ones you state yourself already. Due to the way thread handling works, if another thread is waiting on the lock, the unlock at the end of drawLayer:inContext: pretty much guarantees that a thread-switch occurs. That means that a waiting setNeedsDisplayInRect: will always execute before drawLayer:inContext: returns. And that is exactly what we wanted to avoid. – fishinear Oct 27 '12 at 16:46