Someone else’s cat playing with someone else’s iPhone

The sun is streaming in the window, and I have a very warm cat on my lap – so what better time to write about overheating? This post follows on from one I wrote last year about overheating causing CPU throttling, and contains some tips and other thoughts I’ve had dealing with thermal issues since.

How hot is too hot?

As described in the last post, Unreal reports temperature severity changes via the UApplicationLifecycleComponent‘s OnTemperatureChangeDelegate. On iOS this is mapped to the ‘thermal state’ enum, which has four values: nominal, fair, serious and critical. The iOS API docs say performance will be reduced when the system reaches ‘serious’ i.e. that’s when CPU throttling will kick in. Beyond ‘critical’ the phone will display a warning before switching itself off.

I think both those cases (switching off and reducing performance) are unpleasantly user-hostile. More than that, despite Apple saying it’s kinda to be expected, I’m not hugely comfortable with my phone noticeably heating up at all.

But there’s a practical limit to how much you can prevent overheating: because iOS doesn’t report the device’s actual temperature, only reporting when it passes a threshold, you can’t easily prove your game doesn’t cause overheating – only that it doesn’t cause overheating within a time limit you’re willing to regularly run a soak test for.

The game I’m making is very casual, so I expect an average user’s play session to be 5-10 minutes. But when I test gameplay I tend to play for about 30 minutes, so that’s the time limit I chose: if the temperature doesn’t go from ‘nominal’ to ‘fair’ inside half an hour, I consider that acceptable.

How not to be a boiling frog

Because the ‘traffic light’ is a bit subtle, I added a less subtle arrow. It’s the big red thing

The first thing I did back when I wrote the previous post was to put a ‘traffic light‘ on the screen. This is a little green square in the corner (small enough to not get in the way of gameplay or distract testers) that changes colour in response to the temperature severity delegate – this is much more obvious than just thinking “Is this running a bit slow? Come to think of it, is the phone getting a bit warm? Or is that just me..?”

That wasn’t enough though – during a half-hour soak test, while I might remember to regularly tap the screen to prevent it going off, I sometimes didn’t notice a little square change colour. So I added a sound effect – again I wanted something unmistakable but not jarring for testers. On freesound.org someone had uploaded the chime their Nissan makes when the door is left open, which fit the bill perfectly!

Once I’d resolved the most obvious causes of overheating, those remaining were rare, and by the time they chimed it wasn’t necessarily obvious which parts of the game were responsible (remember overheating != low fps/high GPU time etc.) So the last thing I added was some metrics recording how much time I’d spent in each mode, which was spat out into the log any time the traffic light changed colour. That way I could see, for example, while I was convinced the issue must be in mode A the data said every time the temperature signal triggered I was either in or had just left mode B (a real example!)

Cool runnings

Advice about how to make your game generate less heat is about the same as advice to improve your game’s frame rate – it depends very heavily on your game.

The simplest way to immediately solve all your thermal issues is to reduce your target frame rate in Unreal from 60 to 30fps. I don’t like this option at all, because iOS has run at 60fps for years (and at 120fps in some cases) so a game that runs at 30fps feels slooow. But compared to 60, running your game at 30fps allows the CPU to sleep for half the time, so it has a big impact.

Somewhat more usefully, I’d say in general you should be a little wary of parallelism. This is counterintuitive because on any other platform splitting tasks across multiple cores is always a good thing, but on mobile the heat generated by all those extra cores coming to life can’t be dissipated anywhere near as fast. That’s not to say “don’t ever use multiple cores”, just be aware that for the duration you’ll be generating excess heat. That’s fine as long as you allow the heat to dissipate – running all the cores all the time will melt the phone, but running all the cores for a tenth of a second, once a second might well be fine.

Native iOS apps have a really simple optimisation you can potentially steal too, depending what kind of game you’re making: for the most part apps are visually stationary. When they’re not animating a control, or scrolling the screen, the image on the screen doesn’t change, so Cocoa stops refreshing the screen. Essentially reducing the frame rate from 60/120fps to 0fps and letting the CPU have a nice long nap until the user touches the screen again. My game isn’t an action game, it’s very heavily driven by the player – if they aren’t actively interacting with the game, it doesn’t do a lot. So I tried implementing a similar thing: when the player isn’t touching the screen, reduce the frame rate from 60 to 30fps. I do this by enabling vsync (by setting r.vsync to 1 in the IOS device profile) then during gameplay I set rhi.syncinterval to 1 to run at 60, or to 2 to run at 30. Depending on your game, as long as you reset to 60fps on interaction the player might never notice the difference…

Misc tips

A handful of miscellaneous things I’ve noticed while managing temperature:

  • be aware when you’re testing with the device plugged in. Drawing power to charge the battery generates heat, it’s up to you whether you consider that a fair test.
  • certain material nodes are very expensive in heat terms: the most recent issue I had turned was resolved by replacing a SpiralBlur node (obvious in hindsight, to blur it’s going to need a lot of samples, it’s only sensible to split that across multiple cores)

By crussel

Leave a Reply

Your email address will not be published. Required fields are marked *