Socially Distant OS
  • Docs
  • API
Search Results for

    Show / Hide Table of Contents
    • Accessibility
      • In-Game TTS
    • Development and Modding: Getting started
      • Building from source
      • Contribution guidelines
      • Project structure
      • Code style guide
    • Game scripting (sdsh)
      • The Basics
    • Game Framework
      • Event Bus
      • Playing Audio
    • Story Scripting
      • Career Mode Narrative Objects
      • Narrative Identifiers
      • News Articles
    • User Interface
      • UI Overview
      • Signals
      • List Adapters
      • Optimizing UI for Performance
      • Advanced UI features
        • Visual Styles

    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.

    1. 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.
    2. 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.
      1. Measurement Pass: All parent widgets will calculate their desired size by calculating their child/content sizes.
      2. 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.
    3. Render Pass: All widgets are rendered to the screen.
      1. Widget repaint: Widgets with stale geometry are given a chance to rebuild their geometry.
      2. Pre-submit pass: Widget Effects are given a chance to run shaders on a widget's geometry.
      3. 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:

    1. add your root widget as a toplevel
    // Using another widget:
    widget.GuiManager.TopLevels.Add(myLayoutIsland);
    
    1. 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.

    In this article
    Back to top Generated by DocFX