Monday, May 05, 2008

Performance appliance of RenderTargetBitmap

Challenge: draw complicated Path and animate anything over it
Problem: high CPU while animating object
So, we got another challenge – draw Path object with thousand of points and animate another object over the Path by having least possible impact on CPU. What’s the problem?

First of all, let’s create out XAML

<Path Name="path" Stroke="Black" StrokeThickness="1" Data="{x:Static l:Window1.Data}"/>   

and generate graphics geometry for it

public static Geometry Data
                for (int i = 0; i < 1024; i++)
                    pts.Add(new Point((double)rnd.Next(2000), (double)rnd.Next(2000)));

                StreamGeometry geometry = new StreamGeometry();
                geometry.FillRule = FillRule.EvenOdd;
                using (StreamGeometryContext ctx = geometry.Open())
                    ctx.BeginFigure(new Point(0, 0), false, false);
                    ctx.PolyLineTo(pts, true, true);
                return (Geometry)geometry.GetAsFrozen();

Next, let’s create simple rectangle and animate TranslateTransform object, relays in RenderTransform collection

redRect.Width = 100;
            redRect.Height = 100;
            redRect.Fill = Brushes.Red;
            redRect.HorizontalAlignment = HorizontalAlignment.Left;
            redRect.RenderTransform = trans;

            DoubleAnimation da = new DoubleAnimation(0, this.ActualWidth, new Duration(TimeSpan.FromSeconds(10)));
            da.Completed += (EventHandler) delegate {root.Children.Remove(redRect);};
            trans.BeginAnimation(TranslateTransform.XProperty, da);

Animating RenderTransform collection is the most efficient way to change things in WPF visual tree. It does not tickles the tree, does not performs layout and rendering engine is very smart to update only dirty regions. 

See yourself (dirty regions update is colored)


Very well. Let’s see CPU rate – 60%? WTF? Why? I’m using most performant RenderTransform, transforming only necessary things and doing it over static object. Static? Well, it’s retained object, so WPF trying to redraw each region under the moving object. What to do?

We can try to use Adorners. It might help us to avoid underlying object redraw.

So, we’ll create our own Adorner, that knows to hold UIElement inside it

class CanvasAdorner : Adorner
            UIElement parent,child;
            List<UIElement> children;
            public CanvasAdorner(UIElement adornedElement, UIElement Child)
                : base(adornedElement)
                children = new List<UIElement>();
                parent = adornedElement;

Then we’ll create adorner layer from our root panel and animate rectangle inside it.

AdornerLayer layer = AdornerLayer.GetAdornerLayer(root);
            CanvasAdorner ca = new CanvasAdorner(root, redRect);
            DoubleAnimation da = new DoubleAnimation(0, this.ActualWidth, new Duration(TimeSpan.FromSeconds(10)));
            da.Completed += (EventHandler)delegate { layer.Remove(ca); ca.Remove(redRect); };
            trans.BeginAnimation(TranslateTransform.XProperty, da);

We should see performance boost? No, we’re not. This is because of rendering engine, that draws everything together


It still works very hard to redraw each pixel in geometry.

What to do? We can either create new DirectX surface and draw over it or, maybe another rendering (or UI) thread? Net very simple solution. I prefer “GDI+ way”.

What is it? Don’t you remember, old good way, when we pin pixels in bitmap in order to make visual cache? We can do it in WPF too. To do it, we’ll use RenderTargetBitmap object. This object knows convert WPF Visual into Bitmap and this is exactly what we need.

So we’ll create Image and it source will be our Visual, rendered by RenderTargetBitmap (this is software accelerated!)

RenderTargetBitmap rtb = new RenderTargetBitmap((int)path.ActualWidth, (int)path.ActualHeight, 96, 96, PixelFormats.Pbgra32);
            Image img = new Image();
            img.Stretch = Stretch.None;
            img.Source = rtb;

Now we can move and animate our rectangle over bitmap. It still works and renders only necessary regions (BTW, pay attention, that those regions much smaller, thus animation performance is much better), but what’s happened with CPU?


It’s around 2% – we reach the goal. But why his happens? What the difference between rendering Path and Bitmap? After all both pixels – yes, but in Bitmap case those pixels are not retained, thus rendering thread almost does not work. See the CPU comparison for three of those methods


Don’t it really cool? There are some problems with this method – one of those problem (and most serious) is that creation of RenderTargetBitmap and it’s rendering takes a lot of time and CPU (in my case it was about 0.5 seconds) another problem, that we’re playing with Visual and Logical tree, so we’re rerender and layout whole application window twice – before and after creation. However even with those problems this way is the best to get performant animation over retained WPF objects.

Have a nice day and be good people. Source code for this article.

No comments: