A cheap way to get around the list rebuilding would be to ensure that there is already a widget active using the same atlas and within the same draw call.
I briefly experimented with that sort of solution, but it wasn't working out too well - it avoided the spike when enabling the widget, but removing it still caused a spike, due to this:
if (depth == w.drawCall.depthStart || depth == w.drawCall.depthEnd) mRebuild = true;
And there were lots of things in our HUD scene still triggering the expensive UIPanel.mFullRebuild (for example, enabling/disabling panels for pop-up HUD elements - less frequent events, but still an undesirable performance spike)
Our GUI layouts are not ideal/optimized, there can be quite a lot of draw calls. It's mostly just two fonts and a HUD atlas, but split into a number of panels, and layered up in a way that doesn't batch amazingly well. But they're split up in such a way that no panel contains more than a few widgets, so a single-panel update should be a cheap operation. But with 3.0.x, panels are not independent, and it's easy to cause a rebuild of the entire HUD.
I've spent some time now modifying the internals of 3.0.6f6 fairly significantly (mostly UIPanel/UIDrawCall) to make panels independent again:
- mFullRebuild is per-panel
- Widget lists are per-panel
- DrawCall lists are per-panel
- Panel depth directly controls the render queue of the first draw call in the panel
So nothing will force a rebuild of all panels, at worst there'll be a single-panel update (and minor changes will still only update the single draw call). It seems to be working fairly well so far...
There's a definite trade-off between optimizing draw calls and optimizing update speeds. If you have a complex UI down to one draw call, any small change to it will mean an expensive rebuild of that draw call.
If things are split into more independent panels/draw calls, updates are likely to be less expensive. And in our case, a few more draw calls is a fairly smalls sacrifice to avoid big performance spikes.