Nusatad
Nusatad

Reputation: 3551

CADisplayLink is unable to achieve a constant frame rate in simulator

I am trying to use CADisplayLink to achieve a constant frame rate for a game loop. When testing it in a simulator I am not able achieve stable time frames. Should I consider a different approach?

Or is that just because of the simulator not being able to emulate the hardware appropriately?

According to Apple this is how to calculate FPS:

let actualFramesPerSecond = 1 / (displaylink.targetTimestamp - displaylink.timestamp)

In my plain iOS app that I've set up for testing this prints a constant frame rate:

 FPS: 59.99999875668439
 FPS: 59.99999875668439
 FPS: 59.99999875668439
 FPS: 59.99999875668439
 FPS: 59.99999875668439

However, when I calculate the FPS using the actual elapsed time, it gives something else:

 FPS: 64.35942918520792
 FPS: 58.30848150362142
 FPS: 57.640194044003465
 FPS: 64.47022656706324
 FPS: 59.392580005664115
 FPS: 60.282043174566674

Plus, when using the code for rendering some sprites it looks unstable in simulator. How is it possible that the above code snippet diverges so much from reality?

My main concern is: Why is there a constant frame rate when calculating the FPS the Apple way? Is it possible that their documentation just contains a little glitch? Besides, the 'actual' values are kind of expected when there is some load, but for an empty loop that is doing nothing?

This is the code I am using to execute my game loop:

private var previousTimeInSeconds: Double = 0

private lazy var displayLink: CADisplayLink = {
    let displayLink = CADisplayLink(target: self,
                                    selector: #selector(displayLoop))
    return displayLink;
}()

private func startLoop() {
    previousTimeInSeconds = Date().timeIntervalSince1970
    displayLink.add(to: .current, forMode: .common)
}

@objc private func displayLoop() {
    let currentTimeInSeconds = Date().timeIntervalSince1970
    let elapsedTimeInSeconds = currentTimeInSeconds - previousTimeInSeconds
    previousTimeInSeconds = currentTimeInSeconds

    //let actualFramesPerSecond = 1 / (displayLink.targetTimestamp - displayLink.timestamp) // is showing constant 59.xxx FPS
    let actualFramesPerSecond = 1 / elapsedTimeInSeconds

    print("FPS: \(actualFramesPerSecond)") // varies from 50.xxx to 70.xxx FPS
    /*
     FPS: 64.35942918520792
     FPS: 58.30848150362142
     FPS: 57.640194044003465
     FPS: 64.47022656706324
     FPS: 59.392580005664115
     FPS: 60.282043174566674
     */
}

Upvotes: 1

Views: 1625

Answers (1)

Nusatad
Nusatad

Reputation: 3551

I think I found the answer. Don't use Date().timeIntervalSince1970 to calculate the individual frame rates per second. It seems to be just not precise enough.

Instead, use displayLink.timestamp, which is the time value associated with the last frame that was displayed. Replace the Date() usages in my code above with timestamp and you get the expected FPS values:

FPS: 59.99999875668439
FPS: 59.99999896623196
FPS: 59.99999875668439
FPS: 59.99999875668439
FPS: 59.99999875668439
FPS: 29.999999430729087
FPS: 59.99999875668439
FPS: 59.99999875668439
FPS: 59.99999875668439
FPS: 59.99999875668439
FPS: 59.99999896623196
FPS: 59.99999875668439
FPS: 59.99999875668439
FPS: 59.99999875668439
FPS: 59.99999896623196

As you can see, it's mostly constant at 59 FPS, with a few drops here and there which is totally expected as there is no guarantee that CADisplayLink is called at a constant rate. But under 'labor situations' with (almost) no code to execute this is the frame rate you'd could expect.

The formular I took from Apple's documentation ("let actualFramesPerSecond = 1 / (displayLink.targetTimestamp - displayLink.timestamp)") is kind of useless if want to calculate the real frame rate times. Instead, it shows that targetTimestamp can be used to achieve a stable game loop (as the resulting value is indeed a constant). I wish the documetation could've been more clear in that.

Upvotes: 4

Related Questions