Developer PSA: The Discrete GPU and You

• Chris Liscio

Do you ship an application for the Mac? If so, you'll want to read this bit of info in light of the new 2016 MacBook Pros.

The Problem

The 15" model of the new 2016 MacBook Pro is another in a long line of mobile Macs that ship with a discrete GPU (which I'll call the dGPU for short, throughout.) The AMD GPU that Apple chose is quite efficient compared to others that are available today, but at the same time the MacBook Pro shipped with a smaller battery than previous generations.

The combination of a dGPU and lower-capacity battery means that the practical battery life of these machines gets cut in half (or worse) when the dGPU is active. I'm not sure that previous models had nearly as bad a "battery life delta" as these ones, but boy-howdy is it noticeable in practice on this system. (My choice of the top-end CPU and top-end GPU choice probably doesn't help matters either, but I knew what I was getting into from previous experience.)

I'm not convinced that the problem falls squarely on Apple here, though. We as developers need to be more energy-conscious while developing software that's used on portable machines—with or without discrete GPUs to worry about. It's another axis of optimization, really. What does that mean? Some examples of Good Behavior would be using the Accelerate.framework for high-performance code as much as possible, and not turning the dGPU on unless you really need it.

Unless you develop a high-end game or ship some serious GPGPU code, you probably don't need the discrete GPU to be enabled at all. Unfortunately, your app might be turning the discrete GPU on without you even realizing it.

How to know if the GPU is active

If you have access to a portable Mac with a discrete GPU—it doesn't have to be the latest & greatest—you can check the Activity Monitor in the Energy tab to see whether the the Integrated, or High Performance Graphics Card is active. If the High Performance card is active, sort the 'Requires High Perf GPU' column in descending order (arrow pointing down) and apps that "require" the GPU appear at the top of the list.

If your app doesn't show up in the list of apps that "require" the discrete GPU after running it for a while, you're in the clear. Read on and remember this info for the future, or pass it along to the developers of apps that you find appearing in this list unnecessarily.

Opting in to Automatic Graphics Switching

This whole problem can be very easy to solve. You just have to set NSSupportsAutomaticGraphicsSwitching key to YES in your application's Info.plist. The trouble is that an OpenGL context is being created, which defaults to switching the dGPU on. Enabling this flag in the plist will very likely fix the problem on its own, as the frameworks should Do the Right Thing (more details below) if they need access to OpenGL.

Oddly, many years after this feature was introduced, it still doesn't appear by default in the Info.plist. It doesn't even show up in the default Info.plist generated for new apps by Xcode. Is this a big deal? Not really, but it seems like an oversight this many years later. I mean, the dGPU's going to turn on anyway when the GL context is created, so I don't see the harm.

This lack of a default is nothing to be alarmed about. If you create a new project in Xcode without this flag, it's not like launching the app turns on the GPU. It's only when applications create an OpenGL context that the dGPU turns on when NSSupportsAutomaticGraphicsSwitching is not enabled.

But I don't create an OpenGL context!

You don't, but the frameworks you use might. Think CoreAnimation, CoreImage, etc. For the most part, enabling the flag as above will get you the behavior you're after. If that's not the case, then you should try and figure out the source of the dGPU getting turned on (see below) and file appropriate bugs.

OK, I create a GL context and opt into switching, but the dGPU still turns on

Now things get a little trickier, but only a little. For me, I create a custom NSOpenGLPixelFormat in Capo which means I can use the instructions found here.

Specifically, you add the NSOpenGLPFAAllowOfflineRenderers attribute to your pixel format, and all should be OK. Maybe.

But what does that flag even mean?

What you're saying with the flag above, is that your rendering code supports an "offline renderer". That is, you're OK with using a GPU that is not currently attached to a display. In the MacBook Pro, that's the integrated GPU.

I don't think there are any negative consequences to turning this flag on with modern OpenGL code unless you need to render to the screen with maximum performance. Almost everything you'd need to do in OpenGL is safe to do on a GPU that's not attached to a display.

Why is this sub-optimal from a performance standpoint? Why even supply a flag?

This is an educated guess, but bear with me here.

Consider the Mac Pro and its two GPUs, for example. Imagine you're rendering into a 4K frame buffer on the "offline" GPU that has no screen attached to it. Now that frame buffer needs to get transferred from the offline GPU to the one that's attached to the screen. This is likely going to run "fast enough" for most things these days, but could be much faster if that transfer wasn't necessary.

I followed all the tips above, but the dGPU still turns on. Now what?

After I was certain that I had the problem solved, I got surprised by a situation where the dGPU was still turning on and I couldn't figure out why. I stumbled on a helpful cocoa-dev thread from 2011 where someone mentioned that breaking on IOServiceOpen would help you determine when the dGPU was switched on.

In my specific case, it wasn't a 1:1 correspondence between IOServiceOpen and the dGPU being enabled, so prepare to skip a few calls while also watching the status of your dGPU during the debugging session. Count how many skips over the breakpoint before you see the dGPU turn on again, and re-run to inspect the call that is causing the switch.

To solve the bug I had, it turned out that I was making a call to -[NSOpenGLContext clearDrawable] in the dealloc method of my custom NSOpenGLView. My NSOpenGLContext was already destructed, and a whole new NSOpenGLPixelFormat was getting created and kicked the GPU on again. The call I should have used instead was -[NSOpenGLView clearGLContext] (though I don't even know if that's really required, to be honest. I took the code from something I built in FuzzMeasure years ago.) You live, you learn, etc.

The point of my boring little story above is that you might need to do a little digging beyond just setting up the plist and adjusting your custom pixel format.

In Closing

I hope that the above information helps developers get past any issues they're having with their apps keeping the dGPU turned on. I have no idea if the above is totally accurate or comprehensive, but it worked for me. Expect to see an update to Capo without this dGPU "stickiness" very soon.