Silverlight 5 Animations on the Composition Thread

Being a passionate front-end developer, I am constantly on the quest for creating the smoothest and most intuitive user experience possible. That’s why I was really excited when I heard about some of the performance features and enhancements coming in Silverlight 5. One of the most exciting was the concept of a Composition thread; an idea borrowed from Windows Phone 7 that allows certain elements and animations to be offloaded to the GPU and thus independent of the main UI thread.

Anybody who has ever had to pack the visual tree chock-full of elements in Silverlight knows that performance starts to suffer. A major pet peeve of mine is when the loading indicator stops animating when the UI thread is tied up with other processing (such as adding elements to a DataGrid, and rendering the individual rows). With the Silverlight 5 beta ready to go, I figured I’d try my hand at putting that composition thread to work.

Setting Everything Up

Firstly, I’d like to briefly outline what it takes to get up and running with the Silverlight 5 beta, and then what changes are necessary to be eligible to take advantage of the composition thread.

Step 1. Download Silverlight 5 beta SDK and tools for Visual Studio 2010. Make sure you have Service Pack 1 installed first.

Step 2. If you are working with an existing solution, target Silverlight 5 in all of your Silverlight projects:

Step 3. Now that your projects are targeting Silverlight 5, it’s now time to turn on GPU Acceleration. In the html (or aspx) page that hosts your Silverlight object, make sure the enableGpuAcceleration param is set to true:

[xml]<param name="enableGpuAcceleration" value="true" />[/xml]

At this point, your project is eligible to use the composition thread, but no 2d elements or animations will take advantage of it by default; there is still work to be done, and there are currently some very tricky gotchas that can occur along the way. I will talk about these quirks using a custom BusyIndicator control as an example.

Configure BusyIndicator for GPU Acceleration

Let’s start by showing the xaml for a stripped down version of the Silverlight Control Toolkit’s BusyIndicator:

[xml]
<Style TargetType="local:BusyIndicator">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="OverlayStyle">
<Setter.Value>
<Style TargetType="Rectangle">
<Setter Property="Fill" Value="Black"/>
<Setter Property="Opacity" Value="0.5"/>
</Style>
</Setter.Value>
</Setter>
<Setter Property="ProgressBarStyle">
<Setter.Value>
<Style TargetType="ProgressBar">
<Setter Property="IsIndeterminate" Value="True"/>
<Setter Property="Height" Value="15"/>
<Setter Property="Margin" Value="8,0,8,8"/>
</Style>
</Setter.Value>
</Setter>
<Setter Property="DisplayAfter" Value="00:00:00.1"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Stretch"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:BusyIndicator">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="VisibilityStates">
<VisualStateGroup.Transitions>
<VisualTransition GeneratedDuration="0:0:0.3">
<VisualTransition.GeneratedEasingFunction>
<ExponentialEase EasingMode="EaseInOut"/>
</VisualTransition.GeneratedEasingFunction>
</VisualTransition>
</VisualStateGroup.Transitions>
<VisualState x:Name="Hidden">
<Storyboard>
<DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="(UIElement.Opacity)"
Storyboard.TargetName="overlay" />
<DoubleAnimation Duration="0" To="0" Storyboard.TargetProperty="(UIElement.Opacity)"
Storyboard.TargetName="busycontent" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
Storyboard.TargetName="overlay">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
Storyboard.TargetName="busycontent">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Visible">
<Storyboard>
<DoubleAnimation Duration="0" To="1" Storyboard.TargetProperty="(UIElement.Opacity)"
Storyboard.TargetName="busycontent" />
<DoubleAnimation Duration="0" To="0.5" Storyboard.TargetProperty="(UIElement.Opacity)"
Storyboard.TargetName="overlay" />
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.Visibility)"
Storyboard.TargetName="overlay">
<DiscreteObjectKeyFrame KeyTime="0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="BusyStatusStates">
<VisualState x:Name="Idle">
<Storyboard>
</Storyboard>
</VisualState>
<VisualState x:Name="Busy">
<Storyboard RepeatBehavior="Forever">
<DoubleAnimation Duration="0:0:1.5" From="-180" To="180"
Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.Rotation)"
Storyboard.TargetName="LoadingIcon"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Rectangle x:Name="overlay" Style="{TemplateBinding OverlayStyle}" />
<ContentPresenter x:Name="busycontent">
<Grid HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid.Effect>
<DropShadowEffect ShadowDepth="0" BlurRadius="4"/>
</Grid.Effect>
<Image x:Name="LoadingIcon"
Source="/MyProject;component/Assets/Images/refresh-yellow.png" Stretch="None"
RenderTransformOrigin="0.5,0.5" Margin="0,2,10,0" HorizontalAlignment="Right"
VerticalAlignment="Center">
<Image.RenderTransform>
<CompositeTransform/>
</Image.RenderTransform>
</Image>
</Grid>
</ContentPresenter>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
[/xml]

The goal here is to have the LoadingIcon image spin around while the VisualState is set to Busy, and we want that animation to be processed on the composition thread. The first step to this goal is to add set the CacheMode property on the desired element to “BitmapCache”. This tells the framework that the element (and it’s subtree of elements, if any) should be cached on the GPU. The first thing I tried was setting the CacheMode on the LoadingIcon element, since that was the animation that I wanted to be as consistent and smooth as possible. To verify whether or not it worked, I used a very archaic approach: Set IsBusy on the control to true, and then immediately invoke Thread.Sleep. In theory, if the animation is running on the composition thread, then a Thread.Sleep on the primary UI thread would not freeze the animation. Much to my dismay, the animation froze as soon as Thread.Sleep was executed. It turns out that the current Silverlight 5 beta has a bug in which an Image element will fail to properly cache as expected on the GPU. I received this answer by posting on the Silverlight 5 beta forum, which you can read about here.

Along with this bug, there are some rules about CacheMode that must be followed for your animations to be eligible for GPU caching, and thus the composition thread. I had a quick correspondence with Gerhard Schneider from Microsoft today, and this is what he had to say:

BitmapCache is not [currently] working on Image elements. It’s a bug that we will still try to fix for the release. Other than that, here are roughly the rules for BitmapCache and independent animations (animations on the composition thread).

You can set BitmapCache on any UIElement (ignoring the bug on image element – and I believe media element). This will render that element’s subtree into a video memory off-screen surface that we can then compose with independent animations (independent animations = animations on composition thread). To make sure things are fast we also cache the tree behind and in front of the element that has BitmapCache set.
Note that BitmapCache has no additional benefit when being nested. Only the BitmapCache flag closest to the root is respected.

Regarding independent animations, we currently support transform, perspective, and opacity animations. However, under certain tree configurations, we sometimes have to disable them. For example if used under a complex clip (complex being non-rectangular), we disable independent animations and BitmapCache. The exact rules will be published when we release SL5 since some of this is still changing.

So I followed his advice, and made a few changes:

  • Moved the CacheMode property up to the parent ContentPresenter element.
  • Removed the DropShadow effect, because it is not eligible for caching.

Unfortunately, I still encountered a freezing animation on Thread.Sleep. After another email to Gerhard, he had the answer:

If the animation is targeting a property under the cached element, it has to invalidate the cache and you will not get an independent animation. You need to move the animation to the Grid element. This assume that you are animating the CompositeTransform in the example below.

So the solution was to move the animation to the ContentPresenter, and point the VisualState animations to the ContentPresenter as well. Finally, the animation continued to spin on Thread.Sleep! And this makes sense, because the entire element and subtree will be cached as a bitmap, so any animations or unsupported settings on children will invalidate the bitmap. I think this is a key point that other Silverlight 5 articles failed to mention, and can be a real gotcha if just starting out with this stuff.

So here are the modified parts of BusyIndicator style that will run successfully on the Composite thread. Notice the DoubleAnimation points to busycontent:

[xml]
<ControlTemplate TargetType="local:BusyIndicator">
<Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="BusyStatusStates">
<!– … –>
<VisualState x:Name="Busy">
<Storyboard RepeatBehavior="Forever">
<DoubleAnimation Duration="0:0:1.5" From="-180" To="180"
Storyboard.TargetProperty="(UIElement.RenderTransform).(CompositeTransform.Rotation)"
Storyboard.TargetName="busycontent"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Rectangle x:Name="overlay" Style="{TemplateBinding OverlayStyle}" />
<ContentPresenter x:Name="busycontent" CacheMode="BitmapCache" RenderTransformOrigin="0.5,0.5">
<Grid Height="25" Width="25"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Image x:Name="LoadingIcon"
Source="/MyProject;component/assets/images/refresh-yellow.png"
Stretch="None" HorizontalAlignment="Right"
VerticalAlignment="Center">
</Image>
</Grid>
<ContentPresenter.RenderTransform>
<CompositeTransform />
</ContentPresenter.RenderTransform>
</ContentPresenter>
</Grid>
</ControlTemplate>
[/xml]

Taking full advantage of GPU Acceleration

Even though I was able to improve performance by moving some common animations to the composite thread, I am still weary of the work involved in order to take advantage of this feature in a large application. I really think that there should be an easier way to detect whether an element and it’s subtree are eligible for GPU caching, because there are quite a few limitations (not to mention bugs) that get in the way of implementation. Hopefully by the time Silverlight 5 goes RTW, the bugs will be ironed out and all limitations will be fully documented.

Further Reading

There were quite a few articles and blog posts that helped me get started with GPU acceleration in Silverlight 5. I strongly recommend reading through them if you are interested in this topic:

2 thoughts on “Silverlight 5 Animations on the Composition Thread

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>