CADisplayLink OpenGL rendering breaks UIScrollView behaviour
There are a few similar questions out there on SO (links at end), but none of them has allowed me to fix my problem, so here goes:
I'm using OpenGL rendering to make an image tiling and caching library for use in a game project, and I want to hijack the physics of the UIScrollView to allow the user to navigate around the images (since it has nice bounce behaviour, might as well use it). So I have a UIScrollView which I'm using to get the rendering view for my textures, but there's a problem - moving around on the scroll view prevents the CADisplayLink from firing until the user has finished scrolling (which looks horrible). One temporary fix has been to use NSRunLoopCommonModes instead of the default run mode, but unfortunately this breaks some aspects of scroll view behaviour on certain phones I'm testing on (the 3GS and simulator seem to work fine, while the iPhone4 and the 3G don't).
Does anyone know how I could get around this clash between the CADisplayLink and the UIScrollView, or know how to fix the UIScrollView working in other run modes? Thanks in advance :)
Promised links to similar questions: UIScrollView broken and halts scrolling with OpenGL rendering (related CADisplayLink, NSRunLoop)
Animation in OpenGL ES view freezes when UIScrollView is dragged on iPhone
It's possible that slow updates on the main thread triggered by the CADisplayLink are what's breaking UIScrollView's scrolling behavior here. Your OpenGL ES rendering might be taking long enough for each frame to throw off the timing of a UIScrollView when using NSRunLoopCommonModes
for the CADisplayLink.
One way around this is to perform your OpenGL ES rendering actions on a background thread by using a Grand Central Dispatch serial queue. I did this in my recent update to Molecules (source code for which can be found at that link), and in testing with using NSRunLoopCommonModes
on my CADisplayLink, I don't see any interruption of the native scrolling behavior of a table view that's onscreen at the same time as the rendering.
For this, you can create a GCD serial dispatch queue and use it for all of your rendering updates to a particular OpenGL ES context to avoid two actions writing to the context at the same time. Then, within your CADisplayLink callback you can use code like the following:
if (dispatch_semaphore_wait(frameRenderingSemaphore, DISPATCH_TIME_NOW) != 0)
{
return;
}
dispatch_async(openGLESContextQueue, ^{
[EAGLContext setCurrentContext:context];
// Render here
dispatch_semaphore_signal(frameRenderingSemaphore);
});
where frameRenderingSemaphore
is created earlier as follows:
frameRenderingSemaphore = dispatch_semaphore_create(1);
This code will only add a new frame rendering action onto the queue if one isn't in the middle of executing. That way, the CADisplayLink can fire continuously, but it won't overload the queue with pending rendering actions if a frame takes longer than 1/60th of a second to process.
Again, I tried this on my iPad here and found no disruption to the scrolling action of a table view, just a little slowdown as the OpenGL ES rendering consumed GPU cycles.
My simple solution is to halve the rendering rate when the run loop is in tracking mode. All my UIScrollViews now work smoothly.
Here is the code fragment:
- (void) drawView: (CADisplayLink*) displayLink
{
if (displayLink != nil)
{
self.tickCounter++;
if(( [[ NSRunLoop currentRunLoop ] currentMode ] == UITrackingRunLoopMode ) && ( self.tickCounter & 1 ))
{
return;
}
/*** Rendering code goes here ***/
}
}
The answer at the following post works very well for me (it appears to be quite similar to Till's answer):
UIScrollView pauses NSTimer until scrolling finishes
To summarize: disable the CADisplayLink or GLKViewController render loop when the UIScrollView appears and start a NSTimer to perform the update/render loop at the desired framerate. When the UIScrollView is dismissed/removed from the view hierarchy, re-enable the displayLink/GLKViewController loop.
In the GLKViewController subclass I use the following code
on appear of UIScrollView:
// disable GLKViewController update/render loop, it will be interrupted
// by the UIScrollView of the MPMediaPicker
self.paused = YES;
updateAndRenderTimer = [NSTimer timerWithTimeInterval:1.0f/60.0f target:self selector:@selector(updateAndRender) userInfo:nil repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:updateAndRenderTimer forMode:NSRunLoopCommonModes];
on dismiss of UIScrollView:
// enable the GLKViewController update/render loop and cancel our own.
// UIScrollView wont interrupt us anymore
self.paused = NO;
[updateAndRenderTimer invalidate];
updateAndRenderTimer = nil;
Simple and effective. I'm not sure if this could cause artifacts/tearing of some sort since the rendering is decoupled from screen refreshes, but using CADisplayLink with NSRunLoopCommonModes totally breaks the UIScrollView in our case. Using NSTimer looks just fine for our app and definitely a whole lot better than no rendering.