Musings on OpenGL and CADisplayLink

Musings on OpenGL rendering in iOS

by David Springer

Intro

To make Relaxatron 2.0, I wanted to move the simulation engine and the rendering into a common code base, and write it in C++. While the rest of the iOS world is progressing into Swift and Metal, I'm regressing to C++ and OpenGL.

Why? Because I want Relaxatron 2.0 to work on Android and iOS. This means as much common code as possible. I'll do another post on what I learned about where to draw the "native" versus "common" line when building Relaxatron.

The Challenge

The challenge in this exercise is to write an OpenGL animation loop that starts and stops. In particular, Relaxatron has two main loops: the outer loop that computes a new generation of the Game Of Life simulation; and an inner animation loop that draws the transition form one generation to the next. This second loop is the one I will talk about.

Why this is a challenge: Using OpenGL on iOS is really easy, if you use GLKViewController and GLKView. The problem I faced is that this set up is ideal if you run an "open loop" style of animation, where your animation updates continuously. Relaxatron's animation loop runs only between generation updates so it starts and stops. It doesn't run continuously.

My Answer to the Challenge

There are five parts to my solution.

  1. Use CADisplayLink to provide the animation "tick".
  2. Use a UIViewController (not a GLKViewController) as the main view controller.
  3. Use a subclass of GLKView as the simulation view.
  4. Use -[GLKView display] and set enableSetNeedsDisplay to NO.
  5. Send a message to the main view controller when the inner animation loop is finished.

Setting up CADisplayLink is really easy. The examples in the docs are great sources for this. The main trick in rendering the GLKView from within the updateDisplay: target method is to (1) set GLKView.enableSetNeedsDisplay to NO (I do this in -[GLKView initWithFrame:context:]) and (2) call -[GLKView display], not -[UIView setNeedsDisplay]. That's pretty much it!

Note: contrary to what people say, I found that CADisplayLink is quite reliable in the simulator. While I strongly agree that you need to always test on a device, I found that the simulator works just fine for getting things right. I use Xcode 6.1 and iOS 8.

Here is my -updateDisplay: CADisplayLink target implementation, note the call to -[self display]. Notice also where I send the message to the gameDelegate when the animation is done.

- (void)updateDisplay:(CADisplayLink *)displayLink {
  NSTimeInterval timestamp = displayLink.timestamp;
  if (_lastUpdateTime == 0.0) {
    _lastUpdateTime = timestamp;
  }
  _elapsedUpdateTime = timestamp - _lastUpdateTime;
  if (_elapsedUpdateTime < kCellDrawDuration) {
    [self display];
  } else {
    _elapsedUpdateTime = 0.0;
    [_displayLink invalidate];
    _displayLink = nil;
    if ([self.gameDelegate respondsToSelector:@selector(gameView:animationDidStop:)]) {
      [self.gameDelegate gameView:self animationDidStop:YES];
    }
  }
}