This is what happened in my last project...
I worked hard to optimize the several UI panels of our port to Oculus Quest. This was mostly about reducing the overdraw level to an acceptable amount to make sure the GPU would be all comfy with the real 3D rendering.
So I worked on Unity UI Optimization for at least a month and made pretty damn good progress.
At some point, it was so well optimized that the GPU timings were barely moved by the UI. The opaque UI shading techniques I applied compensated most of the overdraw caused by UI Layering (elements drawn on top of other elements).
There I was, with a super optimized hybrid UI system that effectively occluded the 3D elements drawn behind it. It became very easy to discard the rendering of these occluded fragments.
I saw an overwhelmed CPU taking over 1 ms per frame on UI rendering. That's a hell lot of time for a platform that gives you a budget of 13 ms for the whole game execution: physics, logic, 3D rendering, input, VR, networking are all in the same bucket.
And that is the thing: UI can be optimized to be GPU-friendly, but that doesn't directly translate into being CPU-performing.
In fact, CPU and GPU have very different tasks to accomplish in Unity UI Rendering. No wonder, I suggest you approach CPU and GPU optimization very differently, as seen in my previous blog post about Unity UI Optimization.
Doing more of Unity UI Profiling showed me the obvious problem: the UI was constantly being re-created every single frame, i.e. there was a Canvas Rebuild happening every frame.
A constant hit of 1 ms on the CPU... ouch.
I thought Unity cached the UI Canvases...
Actually yes, that is correct. Unity effectively caches the canvases to make sure they are built just once.
The problem arises, though, when you change the properties of any of the UI elements of the canvas, such as a color, a position and so on.
That means, all animations we love, such as button hover effects, are killing your performance and you might not know it.
A Unity UI Canvas Rebuild makes Unity iterate over all UI elements of that Canvas to generate an optimized list of draw calls (a set of vertices, colors, materials, etc.).. And Canvas Rebuilds take longer than a Seat Panda doing a 0-60 mph test.
Answering that innocent question led me to spending 5+ hours researching this topic and empowering the Unity UI Profiler.
Let's see how.
1. Unity UI Profiling: All Good, until...
Let's say we have a weirdo of a UI in front of us.
That UI is barely doing anything but sitting there, being annoying to the player who just want to see something through it.
And that's fine, even if it contains 350+ images. They will normally be rendered in just two draw calls, as there are two unique images that are not atlased in a sprite atlas.
(...Most of the time)
2. Unity UI Profiling: A wild Canvas Rebuild appears!
What has just happened there at the end of the Unity Profile? The Unity UI CPU cost has more than doubled in just a second, how weird.
I want to play a game
I'll give you five seconds to find it out.
PostLateUpdate.UpdateRectTransform and UGUI.Rendering.UpdateBatches really wanted to take all the highlight in today's show.
What do these regions do?
The first, UpdateRectTransform, implies that a transform of a specific object has changed, and therefore Unity has to run some expensive logic to keep visuals coherent. We don't know whether it was a position, a rotation, a scale or any other of the RectTransform properties.
Heck, we don't even know if it was just one attribute or all of them. Was it one object, or multiple? In any case, which ones? This is the problem: we do not know.
The second cost, UpdateBatches, relates to the fact that the whole Canvas geometry has to be rebuilt. This process is famously known as a Canvas Rebuild. A canvas rebuild implies that Unity goes through all the Canvas hierarchy to generate a list of draw calls, so to speak. The vertices, indices, colors, uv's of all elements are computed and then a batching pass is done to we merge as many draw calls as possible to reduce the CPU overhead of issuing them to the graphics driver.
Now we know what's going on, kind of. We're on the right track. But how do we go about avoiding these canvas rebuilds? What is causing them?
We just need to find out more specific information...
3. Finding the Saboteur: a politically incorrect brute-force approach
We are still to give an answer to the following question:
Who's triggering that sucky Unity UI Canvas Rebuild?
It turns out, there's no fast way of finding that out, especially if your canvas hierarchy is immense.
Filter the metrics so you can focus on what is important: Rendering, Scripts and UI.
Keep an eye on the baseline to have a visual cue of your current baseline cost, which should include the expensive Canvas Rebuilds.
Select a group of game objects and deactivate it.
Compare the performance baseline.
If the baseline didn't improve much, continue deactivating game objects till you see a significant improvement.
Now you managed to isolate which object is triggering your Canvas Rebuilds. But, who's actually causing those?
Is it a script scaling it? Or maybe an animation changing its position?
It helps to do a right-click on the RectTransform and press "Find References in Scene"
Once you know who's causing the UI canvas rebuilds, do something about it, such as disabling animations or transforms.
Ruben, how am I supposed to follow this approach in a huge UI hierarchy? Don't give me crap
I told you it was going to be neither fast nor fun, but your players asked for it.
That's the thing. Having a huge hierarchy in place is not ideal in the first place. Exactly those massive, deep hierarchies will make your Canvas Rebuilds incredibly expensive on the CPU.
But big and nested UI hierarchies can (and will) happen, so expect canvas rebuilds to hit you where it hurts the most: your players' game-play experience.
4. Bonus: Augmenting the Unity Profiler for UI Optimization
By now, hopefully I stressed enough how frequent and impactful UI Canvas Rebuilds can be.
These troll canvas rebuilds that infested my game stole 10% of my entire CPU budget!
As we saw, there is a slow brute-force approach for finding the source of a canvas rebuild. Then, hopefully you'll be able to do something about it, based on the strategies I posted on my Unity UI Optimization post (visit it, it's free, I promise!).
But such as error-prone approach is a process a real guru would never settle for. You can literally spend days trying to avoid canvas rebuilds, but the moment you expect it the least, they'll come back just to disappear as soon as you attach the Unity UI Profiler.
This becomes crucial if you're doing VR development. You don't want canvas rebuilds in your world-space UI. Like not at all. If you don't get rid of these, you're very much likely to convert your players into patients.
I'm glad you asked. It turns out we can convince the Unity Profiler to give us useful information about who's messing with the performance of our UI.
You and I can augment the functionality of the Unity UI Profiler. We do so by altering the Unity UI source code that is publicly available. Once you have the source code, you'll want to find the code functions where the Canvas Rebuilds take place. Then, all we need is some BeginSample and EndSample Profiler API love magic.
If you're running Unity 2019.1 or earlier, the Unity UI source code is available for free in their Bitbucket repository. You can follow their guide there to download, install and modify it.
The free tool will allow you to quickly switch over profiling modes to make sure the performance of your game is on top.
The best part of the Unity Profiler enhancer? It just works outside of the editor, effectively replacing all the aspirins you've been taking while profiling your UI in Android and other platforms.