I’ve showed you how to use CompositionTarget.Rendering for moving and changing visual
objects in synchronization with the refresh rate of the video display.
While certainly convenient for some scenarios, this feature of
Silverlight should be used with discretion. If you’re really interested
in using CompositionTarget.Rendering for a full-fledged game loop, for example, perhaps it’s time to start thinking about XNA.
The big problem is that sometimes CompositionTarget.Rendering does not work as well as you might anticipate. Here is a program that attempts to use CompositionTarget.Rendering to rotate that spiral.
Example 1. Silverlight Project: RotatedSpiral File: MainPage.xaml.cs (excerpt)
public partial class MainPage : PhoneApplicationPage { RotateTransform rotateTransform = new RotateTransform();
public MainPage() { InitializeComponent(); Loaded += OnLoaded; }
void OnLoaded(object sender, RoutedEventArgs args) { Point center = new Point(ContentPanel.ActualWidth / 2 - 1, ContentPanel.ActualHeight / 2 - 1); double radius = Math.Min(center.X, center.Y);
Polyline polyline = new Polyline(); polyline.Stroke = this.Resources["PhoneForegroundBrush"] as Brush; polyline.StrokeThickness = 3;
for (double angle = 0; angle < 3600; angle += 0.25) { double scaledRadius = radius * angle / 3600; double radians = Math.PI * angle / 180; double x = center.X + scaledRadius * Math.Cos(radians); double y = center.Y + scaledRadius * Math.Sin(radians); polyline.Points.Add(new Point(x, y)); } ContentPanel.Children.Add(polyline);
rotateTransform.CenterX = center.X; rotateTransform.CenterY = center.Y; polyline.RenderTransform = rotateTransform;
CompositionTarget.Rendering += OnCompositionTargetRendering; }
void OnCompositionTargetRendering(object sender, EventArgs args) { TimeSpan elapsedTime = (args as RenderingEventArgs).RenderingTime; rotateTransform.Angle = 360 * elapsedTime.TotalSeconds / 3 % 360; } }
|
Most of this code is the same as the Spiral program, but notice the RotateTransform field. At the end of the Loaded handler, this RotateTransform is set to the RenderTransform property of the Polyline defining the spiral, and a handler is attached to the CompositionTarget.Rendering event. That event handler changes the Angle property of the RotateTransform to rotate the spiral once every 3 seconds.
There’s nothing really wrong with this code, but if your phone is similar to my phone, the performance will be terrible. The screen will be updated only once or twice a second, and the resultant animation will seem very jumpy.
I’m going to tell you three
ways to fix the problem and improve the performance. Fortunately, all
three ways to fix the problem are general enough to be usable beyond
this particular application.
Solution 1: Simplify the graphics. This spiral is a Polyline with 14,400 points. That is way more than sufficient. If you change the increment in the for loop from 0.25 to 5, the animation will be much smoother and the spiral itself will still seem round. The lesson: Fewer visual objects often result in better performance. Simplify your graphics and simplify your visual trees.
Solution 2: Cache the visuals. Silverlight is attempting to rotate a Polyline with very many individual points. It would find this job a lot easier if the spiral were a simple bitmap rather than a complex Polyline. You could make a WriteableBitmap
of this graphic yourself and rotate that. Or you could let Silverlight
do the equivalent optimization by simply setting the following property
on Polyline:
polyline.CacheMode = new BitmapCache();
This instructs Silverlight to
create a bitmap of the element and to use that bitmap for rendering. You
shouldn’t use this option for vector graphics that dynamically change.
But complex graphics that are static within themselves, and which might
be subjected to animations, are excellent candidates for bitmap caching. In XAML it looks like this:
CacheMode="BitmapCache"
Solution 3: Use Silverlight animations instead of CompositionTarget.Rendering.
Let’s rewrite the RotatedSpiral program with the same number of points in the Polyline and without explicit bitmap caching but replacing CompositionTarget.Rendering with a DoubleAnimation:
Example 2. Silverlight Project: AnimatedSpiral File: MainPage.xaml.cs (excerpt)
public partial class MainPage : PhoneApplicationPage { public MainPage() { InitializeComponent(); Loaded += OnLoaded; }
void OnLoaded(object sender, RoutedEventArgs args) { Point center = new Point(ContentPanel.ActualWidth / 2 - 1, ContentPanel.ActualHeight / 2 - 1); double radius = Math.Min(center.X, center.Y);
Polyline polyline = new Polyline(); polyline.Stroke = this.Resources["PhoneForegroundBrush"] as Brush; polyline.StrokeThickness = 3;
for (double angle = 0; angle < 3600; angle += 0.25) { double scaledRadius = radius * angle / 3600; double radians = Math.PI * angle / 180; double x = center.X + scaledRadius * Math.Cos(radians); double y = center.Y + scaledRadius * Math.Sin(radians); polyline.Points.Add(new Point(x, y)); } ContentPanel.Children.Add(polyline);
RotateTransform rotateTransform = new RotateTransform(); rotateTransform.CenterX = center.X; rotateTransform.CenterY = center.Y; polyline.RenderTransform = rotateTransform;
DoubleAnimation anima = new DoubleAnimation { From = 0, To = 360, Duration = new Duration(TimeSpan.FromSeconds(3)), RepeatBehavior = RepeatBehavior.Forever };
Storyboard.SetTarget(anima, rotateTransform); Storyboard.SetTargetProperty(anima, new PropertyPath(RotateTransform.AngleProperty));
Storyboard storyboard = new Storyboard(); storyboard.Children.Add(anima); storyboard.Begin(); } }
|
And it runs much smoother than the previous version.
Why the big difference? Surely on some level the Silverlight animations are making use of something equivalent to CompositionTarget.Rendering, right?
Actually, that’s not true.
It’s pretty much true for the desktop version of Silverlight, but
Silverlight for Windows Phone has been enhanced to make greater use of
the phone’s graphics processing unit (GPU). Although GPUs are
customarily associated with hardware accelerations of complex texture
processing and other algorithms associated with 3D graphics, Silverlight
puts the GPU to work performing simple 2D animations.
Much of a Silverlight application runs in a single thread called the UI thread. The UI thread handles touch input, layout, and the CompositionTarget.Rendering event. Some worker threads are also used for jobs such as rasterization, media decoding, sensors, and asynchronous web access.
Silverlight for Windows Phone also supports a compositor or render thread that involves the GPU. This render thread is used for several types of animations of properties of type double, specifically:
Transforms you set to the RenderTransform property
Perspective transforms you set to Projection property
Canvas.Left and Canvas.Top attached properties
Opacity property
Anything that causes rectangular clipping to occur
Animations that target properties of type Color or Point continue to be performed in the UI thread. Non-rectangular clipping or use of OpacityMask are also performed in the UI thread and can result in poor performance.
As a little demonstration, the UIThreadVsRenderThread project rotates some text in two different ways:
Example 3. Silverlight Project: UIThreadVsRenderThread File: MainPage.xaml (excerpt)
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0"> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="UI Thread" FontSize="{StaticResource PhoneFontSizeLarge}" HorizontalAlignment="Center" VerticalAlignment="Center" RenderTransformOrigin="0.5 0.5"> <TextBlock.RenderTransform> <RotateTransform x:Name="rotate1" /> </TextBlock.RenderTransform> </TextBlock>
<TextBlock Grid.Row="1" Text="Render Thread" FontSize="{StaticResource PhoneFontSizeLarge}" HorizontalAlignment="Center" VerticalAlignment="Center" RenderTransformOrigin="0.5 0.5"> <TextBlock.RenderTransform> <RotateTransform x:Name="rotate2" /> </TextBlock.RenderTransform> </TextBlock>
<Button Grid.Row="2" Content="Hang for 5 seconds" HorizontalAlignment="Center" Click="OnButtonClick" /> </Grid>
|
The first TextBlock is rotated in code using CompositionTarget.Rendering. The second TextBlock is animated by the following Storyboard defined in the page’s Resources collection:
Example 4. Silverlight Project: UIThreadVsRenderThread File: MainPage.xaml (excerpt)
<phone:PhoneApplicationPage.Resources> <Storyboard x:Name="storyboard"> <DoubleAnimation Storyboard.TargetName="rotate2" Storyboard.TargetProperty="Angle" From="0" To="360" Duration="0:1:0" RepeatBehavior="Forever" /> </Storyboard> </phone:PhoneApplicationPage.Resources>
|
The MainPage constructor starts the animation going and attaches a handler for the CompositionTarget.Rendering event.
Example 5. Silverlight Project: UIThreadVsRenderThread File: MainPage.xaml.cs (excerpt)
public partial class MainPage : PhoneApplicationPage { DateTime startTime;
public MainPage() { InitializeComponent();
storyboard.Begin(); startTime = DateTime.Now; CompositionTarget.Rendering += OnCompositionTargetRendering; }
void OnCompositionTargetRendering(object sender, EventArgs args) { TimeSpan elapsedTime = DateTime.Now - startTime; rotate1.Angle = (elapsedTime.TotalMinutes * 360) % 360; }
void OnButtonClick(object sender, RoutedEventArgs args) { Thread.Sleep(5000); } }
|
I’m using the time-based logic for CompositionTarget.Rendering so the two animations move at the same rate. But press that button and you’ll see something amazing: The TextBlock rotated by CompositionTarget.Rendering stops dead for five seconds, but the one powered by DoubleAnimation keeps right on going! That’s the render thread working for you even though the UI thread is blocked.
If you’re applying rotation
and scaling to text, there’s another setting you might want to know
about. This is the attached property you set in XAML like this:
TextOptions.TextHintingMode="Animated"
The alternative is Fixed, which is the default. When you indicate that text is to be animated, certain optimizations for readability won’t be performed.
Silverlight has three built-in features that can help you visualize performance
issues. Although you can use these on the phone emulator, the emulator
tends to run faster than the actual phone, so you’re not getting a true
view of performance anyway.
For more details about performance issues,
you’ll want to study the document “Creating High Performance
Silverlight Applications for Windows Phone” available online The Settings class in the System.Windows.Interop namespace has three Boolean properties that can help you visualize performance. You access these three properties through the SilverlightHost object available as the Host property of the current Application
object. These properties are considered so important that you’ll find
them set—and all but one commented out—in the constructor of the App class in the standard App.xaml.cs file.
Here’s the first:
Application.Current.Host.Settings.EnableFrameRateCounter = true;
This flag enables a little display at the side of the phone showing several items:
The frame rate (in frames per second) of the render thread (GPU)
The frame rate (in frames per second) of the UI thread (CPU)
The amount of video RAM in use in kilobytes
The number of textures stored on the GPU
The number of intermediate objects created for complex graphics
The fraction of total screen pixels painted per frame
The first two items are the
most important. When your program is running on the phone these two
numbers should both be in the region of 30 frames per second. It is
probably best to use these numbers (and the others) in comparative ways:
Get accustomed to what the numbers are like in relatively simple
programs, and then observe the changes as the program gets more complex.
The second diagnostics flag is:
Application.Current.Host.Settings.EnableRedrawRegions = true;
This flag is rather fun.
Whenever the UI thread needs to rasterize graphics for a particular
region of the video display, that region is highlighted with a different
color. (These are sometimes called “dirty regions”
because they need to be refreshed with new visuals.) If you see a lot
of flickering over wide areas of the video display, the UI thread is
working overtime to update the display. Optimally, you should be seeing
very little color flashing, which is the case when the render thread is
performing animations rather than the UI thread.
Here’s the third flag:
Application.Current.Host.Settings.EnableCacheVisualization = true;
This flag uses a color
overlay to highlight areas of the display that are cached to bitmaps.
You might want to try this flag with the RotatedSpiral program (the
version that uses CompositionTarget.Rendering).
When you run the program as it’s shown above, the whole display is
tinted, meaning that the whole display needs to rasterized every time
the Polyline changes position. That takes some times, and that’s why the performance is so poor. Now set:
polyline.CacheMode = new BitmapCache();
Now you’ll see the spiral in a tinted box, and the box itself is rotated. That’s the cached bitmap.