August 17, 2015

Utilizing slate Sleep and Active Timers

By Dan Hertzka

Slate is an “immediate mode” UI framework, meaning it redraws the entire UI each frame, which is great for very dynamic interfaces that are rich with graphics and animations, but results in unnecessary processor usage when nothing in the UI needs to change. This is often the case with the Launcher and Editor.

This feature introduces a new “Active Timer” system to Slate, allowing it to enter a “Sleep” state when no UI needs to update.

Does the change affect you?

If you’re working on UI for the Editor or Launcher, yes. If you’re working on UI for a game with a real-time viewport, no.

When does Slate go to sleep?

When both of the following are true for a given frame:

  • There is no user action.
  • No active timers need to execute.
What qualifies as “user action”?

Mouse movement, clicks, and key presses. 

The following diagram illustrates how the Slate application now ticks each frame:

DH1

Does sleeping work?

It does indeed. Here’s a graph showing the results:

DH2

What is an “active timer”?

An active timer is a delegate function explicitly registered by a widget that causes a Slate tick/paint pass upon execution, even in the absence of user action (thus the “active” nature of the timer). Active timers will continue to execute indefinitely at the frequency determined by their execution period until unregistered.

How does this affect the old Tick()?

It’s still there, but think of it now as a “passive” tick. It will be called by Slate as before, but only when Slate is awake. We’re still considering if/how to rename it to PassiveTick() and deprecate Tick().

How do I implement an active timer?

Registering - 3 steps
  1. Define a function with this signature: "EActiveTimerReturnType Foo(double InCurrentTime, float InDeltaTime)".
  2. Bind it to an FWidgetActiveTimerDelegate.
  3. Pass the delegate and the time period between timer executions (0 to call every frame) to SWidget::RegisterActiveTimer().
Un-registering - 3 ways
  1. Return EActiveTimerReturnType::Stop from the delegate.
  2. Pass the FActiveTimerHandle that was returned by SWidget::RegisterActiveTimer() to SWidget::UnRegisterActiveTick().
  3. Destroy the widget that the active timer is registered under.

 

Beware duplicate registrations!

Currently, active timers are an all-or-nothing implementation regarding Slate, meaning all of Slate will be ticked if a single active timer needs to execute. Furthermore, there is no limit to the number of active timers a widget can have registered concurrently. This can be extremely useful, but also introduces the possibility of duplicate registrations. To prevent them, track registration status in one of the following ways:

  1. Keep a flag in the widget to track whether the active timer is registered.
    1. Search “bIsActiveTimerRegistered“ for examples.
  2. Store a weak pointer to the FActiveTimerHandle returned by RegisterActiveTimer() and only register when it’s invalid.
    1. Search “TWeakPtr<FActiveTimerHandle> ActiveTimerHandle“.
    2. To save memory, only bother with this if the active timer ever needs to be explicitly unregistered.

 

Common use cases

  • Triggering some action
    • Before, we would store a flag and check it during each tick to see if it is true.
    • Now, we register an active timer w/ period 0 that always returns EActiveTimerReturnType::Stop.
  • Performing some sort of animation or interpolation that is not controlled by an FCurveSequence (for those that are, see below). 
    • Example: Inertial scrolling - See SScrollBox
      • When inertial scrolling begins, registers an active timer w/ period 0 to update the scroll each frame.
      • Until the destination is reached, returns EActiveTimerReturnType::Continue.
      • Upon reaching the target destination, returns EActiveTimerReturnType::Stop to unregister.
  • Taking action after a delay.
    • Example: Opening a different sub-menu, docking a tab.
      • Register with a positive non-zero delay.
      • The period on an active timer cannot be changed once registered.
        • To “reset” the delay before execution, the active timer must be explicitly unregistered and re-registered.
  • Perform an action periodically and indefinitely.
    • Register with a positive non-zero period and just keep returning EActiveTimerReturnType::Continue.

Updated FCurveSequence API

The API of FCurveSequence has been updated to simplify the process of registering active ticks when a widget is being animated. Here are some of the changes:

  • Playing the sequence now requires a reference to the widget that will be animated by it. 
    • An empty active tick is automatically registered on behalf of the passed widget for the duration of the sequence playback. 
  • Whether the sequence should loop is now specified when playing the sequence.
    • Doing so will register the active tick indefinitely, so be careful with this!
  • Whether looping or not, the active tick will unregister when calling Pause(), JumpToStart(), or JumpToEnd().

Is it all working?

Yeah!... we think. The active tick system is live in Main and all Editor widgets (~90 that I could find) that needed one have been updated accordingly. There’s bound to be some fallout, however, so there are two cvars in the Editor to handle some potential problems.

  1. Slate.AllowSlateToSleep controls whether Slate can ever enter a sleep state, enabled by default. 
  2. Slate.SleepBufferPostInput specifies some amount of time following the last user action that Slate will continue to stay awake. This defaults to zero and is currently used only for debugging and will not remain a feature of the system.

 

How can I tell if Slate is asleep?

Since the goal is to make Slate that can sleep indistinguishable from Slate that never sleeps, it can be hard to tell.

Currently, the best way to monitor this is to show the Editor frame rate (Editor Settings->Miscellaneous->Show Frame Rate and Memory). When it freezes, Slate is dozing.

So what should I do?

If you have either authored widgets of significant complexity in the past or are currently creating a new widget, it’s very important that you test it out with sleeping enabled (keep the post-input buffer at 0) and with no persistent real-time viewport open.

If you find a malfunctioning widget and/or have questions about getting your widget updated, let us know over on the forums.