Optimizing UI for Performance
Socially Distant uses UI everywhere. That being said, it's still a game that really benefits from a stable framereate. We're also targeting a wide range of hardware, including systems with relatively low amounts of RAM and lower-end GPUs. All that to say, we should try to keep the UI well-optimized. Here are some ways you can do so.
Understanding how the UI system does things
Every frame, the UI system does the following things in order.
- Process input events: The UI system receives input events from the mouse/keyboard/rest of the game, and propagates them to the widgets that need to receive them. The widgets then modify their internal state accordingly in response.
- Layout Pass: The UI system instructs all top-levels (widgets with no parents) to update their layout. This action is recursive, eventually every single widget on-screen will have its layout updated.
- Measurement Pass: All parent widgets will calculate their desired size by calculating their child/content sizes.
- Arrangement Pass: After all widgets have had their sizes measured, all parents will arrange their children according to a layout algorithm specific to the given parent widget.
- Render Pass: All widgets are rendered to the screen.
- Widget repaint: Widgets with stale geometry are given a chance to rebuild their geometry.
- Pre-submit pass: Widget Effects are given a chance to run shaders on a widget's geometry.
- Geometry submission: Widget geometry is submitted to the GPU for rendering, and drawn to the screen.
The Layout Pass and Render Pass, being recursive, are extremely hot paths. For this reason, the UI system caches the results of the Layout Pass and Widget Repaint stages. They will be run once on a widget when it is first added to a parent, and again when the widget is invalidated.
Invalidation
Widgets must invalidate their layout and geometry if a property on that widget changes, when that property affects the widget's layout or geometry. It is up to the widget's developer to decide whether a property affects a widget's layout/geometry, and up to the developer to invalidate the widget accordingly.
You should only invalidate when you need to, as invalidating a widget causes other widgets to invalidate. In some cases, invalidating a single widget can necessarily cause the entire widget tree to invalidate.
To invalidate a widget's layout, call InvalidateLayout()
on the widget. Invalidating a widget's layout will invalidate the widget's LayoutRoot
- causing a recursive invalidation of every widget within the LayoutRoot. In most cases, LayoutRoot is just the top-level widget. This is mostly unavoidable, as a layout invalidation implies a possible change in widget size and thus may require a parent to rearrange its children (and we can't predict whether that's actually true). You should only invalidate a widget's layout when a layout-related property changes.
If you need to invalidate the geometry of a widget, call InvalidateGeometry()
on it. By default, this will only invalidate the one widget's geometry and cause it to repaint. If you need to invalidate a widget's geometry and that of all of its children recursively, call InvalidateGeometry(true)
. You will rarely need to do this. You should only invalidate a widget's geometry when a visual property of the widget, such as a font/color, changes. Note that, when invalidating a widget's layout, the UI system will also recursively invalidating its geometry.
Avoiding unnecessary layout updates
Create layout islands
Separate complex UIs into their own layout islands when possible. When a giant layout invalidation occurs, it will only affect the widgets in the layout island.
In Socially Distant, modal overlays (such as System Settings and message boxes) are added as new toplevels to the UI system. This means that, when you navigate through System Settings, the game isn't needlessly refreshing the layout of the blurred desktop in the background.
On the desktop, Socially Distant marks the Info Panel as a layout root. This prevents Info Panel widgets from needlessly invalidating other desktop elements like the dock, or woese, open program windows.
To create a layout island, either:
- add your root widget as a toplevel
// Using another widget:
widget.GuiManager.TopLevels.Add(myLayoutIsland);
- create a custom widget that marks itself as its layout root:
public sealed class MyWidget : Widget
{
public MyWidget()
{
LayoutRoot = this;
}
}
⚠️ Warning
Messing with
LayoutRoot
can cause layout bugs if your custom widget changes its size based onits children. You should only mark a widget as its own LayoutRoot if you know its size will be static. If you do get layout bugs, you need to invalidate the LayoutRoot's parent and that's what we're trying to actively avoid.
Collapsing widgets
Setting a widget's Visibility to Collapsed suppresses future layout invalidations of that widget and its children. This is because the UI system knows that all collapsed widgets can't possibly have a non-zero size, and so doesn't bother to do a full layout pass on them at all.
Avoid animating layout properties excessively
Sliding, growing, and shrinking animations are nice. But as with many things, there's an art in subtlety. You cannot avoid the layout updates needed during an animation, so avoid heavy animations.
Remove parents before removing children
When removing widgets from other widgets, remove parent widgets first. Removing a child from a parent always causes a full invalidation of layout and geometry, so getting the widget out of the layout hierarchy should be top priority. One call to Widget.InvalidateLayout()
resulting in a full layout invalidation is better than several.
Handle Custom Properties with care
In most cases, changing a Custom Property on a widget requires invalidating that widget's layout. This is because there's no way for the UI system to know whether a custom property actually affects widget layout, and assumes that all of them do. Since many of them do, this works out well when custom properties are used effectively.
If possible, try to batch your custom properties into a CustomPropertyObject
. You can only do this if your code is the one reading the custom properties, but it means you have control over whether the properties invalidate a widget's layout. For an example of how to implement this, see the FlexPanel
and FlexPanelProperties
code.
Use ListAdapter for large lists
Using ListAdapter
for larger lists allows common UI optimizations to be applied to all items in the list without you needing to worry about it. This allows you to benefit from future optimizations without needing to implement them.
Learn how to create a List Adapter
Avoiding geometry invalidations
Use RenderOpacity
for fade animations
You can avoid a widget repaint by using RenderOpacity
to implement fade animations. This is because RenderOpacity
can be applied to cached widget geometry.
Use Visibility.Hidden
If you'd like a widget to be visually hidden, but still want it to contribute to layout (like a CompositeIconWidget
does), it's prferrable to set the widget to Hidden instead of 0% opacity. Hidden and Collapsed widgets never get submitted to the GPU during render pass, and therefore do not repaint even if their geometry is invalidated.
Use Widget Effects to your advantage
Socially Distant has a default user avatar texture. Avatars can change their color depending on context. The default avatar uses a Widget Effect to implement the recoloring. This allows the recoloring to be done on the GPU with a fragment shader, as part of the geometry submit pass. This means changing the color of a default avatar in Socially Distant doesn't cause a geometry invalidation. If you can pull this off, you're golden.
...But also use them sparingly.
We all love ourselves a nice Gaussian blur effect for a translucent widget background. Our GPUs? Not so much. Remember that art of subtlety thing, only use complex Widget Effects when you need to. If possible, write them in such a way where they can be accessed as a singleton and have their GPU resources shared across multiple widgets. For an example of how to do this, see the BackgroundBlurWidgetEffect
code.