WPF Animation has Tearing and Flicker

3.8k views Asked by At

I'm having trouble with tearing and flickering in WPF animations. I have a toy app that demonstrates the problems. The app animates squares across the screen. The edges of the squares show tearing and the animation as a whole does not feel smooth.

Perforator shows >60fps, ~10mb video memory, 0 IRTs.

I have tried this on two new high end computers and both show the same poor animation (>1gb vram, quad core etc).

SimpleWindow.zip

3

There are 3 answers

0
Christopher Bennage On BEST ANSWER

I raised this question with WPF team and in summary they said that they believe that there are some glitches with animation smoothness that could be improved.

They also added:

We try very hard to schedule our scene updates in sync with the VBlank to get very regular, reliable animations. Any work on the UI thread can interfere tough. In this example, they are using DispatcherTimers which schedule work onto the UI thread to create new storyboards, remove old elements, etc.

They also demonstrated a purely declarative version of the animations, and it appeared smoother to me. Special thanks to Dwayne Need for this information.

3
Marino Šimić On

The reason why this has tearing is that a lot of object that must be UI thread owned are created and all those calls plus adding them to UI container is going through the main thread.

I even tried to make a Thread driven version instead of Timers, but that did not change anything since all FrameWorkElement objects must be created with Dispatcher.Invoke.

The creation of storyboards and beginStoryboard + EventTrigger all must be done on the Ui thread. This is what is blocking the fluency.

Unfortunately with this design there is no way of achieving flickerfree operation :/

using System;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Shapes;
using System.Windows.Media.Animation;
using System.Windows.Threading;
using System.Collections.Generic;

namespace SimpleWindow
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow
    {
        readonly SolidColorBrush _fillBrush = new SolidColorBrush(Colors.CadetBlue);

        // Timers
        //private DispatcherTimer _addItemsTimer;
        //private DispatcherTimer _removeItemsTimer;
        private Thread _addItemsTimer;
        private Thread _removeItemsTimer;
        private volatile bool formClosing = false;

        private readonly TimeSpan _addInterval = TimeSpan.FromSeconds(0.21);
        private readonly TimeSpan _removeInterval = TimeSpan.FromSeconds(1);
        public MainWindow()
        {
            InitializeComponent();
            Closing += MainWindow_Closing;
            Loaded += OnLoaded;
        }

        void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            formClosing = true;
            //_addItemsTimer.Join();
            //_removeItemsTimer.Join();
        }

        private void OnLoaded(object o, RoutedEventArgs args)
        {
            _addItemsTimer = new Thread((ThreadStart)delegate() {
                while (!formClosing)
                {
                    Thread.Sleep(_addInterval);
                    AddItems();
                }
            });

            _removeItemsTimer = new Thread((ThreadStart)delegate()
            {
                while (!formClosing)
                {
                    Thread.Sleep(_removeInterval);
                    RemoveOffScreenItems();
                }
            });

            _addItemsTimer.SetApartmentState(ApartmentState.STA);
            _addItemsTimer.Start();
            _removeItemsTimer.SetApartmentState(ApartmentState.STA);
            _removeItemsTimer.Start();

            WindowState = WindowState.Maximized;
        }

        //private static DispatcherTimer CreateTimer(TimeSpan interval, EventHandler handler)
        //{
        //    var timer = new DispatcherTimer();
        //    timer.Interval = interval;
        //    timer.Tick += handler;
        //    timer.Start();

        //    return timer;
        //}

        // Timer callback
        private readonly Rectangle _canvasChildrenLock = new Rectangle();
        public void AddItems()
        {
            lock (_canvasChildrenLock)
            {
                Dispatcher.Invoke((Action)delegate() {
                    var rect = CreateRectangle();
                    rect.Triggers.Add(BeginStoryboardEventTrigger(CreateStoryboard()));
                    MainCanvas.Children.Add(rect); 
                });
            }
        }

        private static EventTrigger BeginStoryboardEventTrigger(Storyboard storyboard)
        {
            var beginStoryboard = new BeginStoryboard {Storyboard = storyboard};

            var eventTrigger = new EventTrigger(LoadedEvent);
            eventTrigger.Actions.Add(beginStoryboard);
            return eventTrigger;
        }

        // Timer callback 
        public void RemoveOffScreenItems()
        {
            lock (_canvasChildrenLock)
            {
                var itemsToRemove = (List<FrameworkElement>)Dispatcher.Invoke((Func<List<FrameworkElement>>)delegate()
                {
                    return (from FrameworkElement element in MainCanvas.Children
                            let topLeft = new Point((double)element.GetValue(Canvas.LeftProperty), (double)element.GetValue(Canvas.TopProperty))
                            where IsOffScreen(topLeft)
                            select element).ToList();
                });

                if (itemsToRemove == null) return;

                foreach (FrameworkElement element in itemsToRemove)
                {
                    Dispatcher.Invoke((Action)delegate() { MainCanvas.Children.Remove(element); });
                }
            }
        }

        private bool IsOffScreen(Point pt)
        {
            return 
                pt.X > MainCanvas.ActualWidth ||
                pt.Y < 0 || pt.Y > MainCanvas.ActualHeight;
        }

        private Rectangle CreateRectangle()
        {
            var rect = new Rectangle
            {
                Width = 100, 
                Height = 100, 
                Fill = _fillBrush
            };

            return rect;
        }

        private const double OffScreenPosition = 100;
        private const double AnimationDuration = 2;
        private Storyboard CreateStoryboard()
        {
            var xAnimation = CreateDoubleAnimationForTranslation();
            xAnimation.From = -OffScreenPosition;
            xAnimation.To = MainCanvas.ActualWidth + OffScreenPosition;
            Storyboard.SetTargetProperty(xAnimation, new PropertyPath(Canvas.LeftProperty));

            var yAnimation = CreateDoubleAnimationForTranslation();
            yAnimation.From = MainCanvas.ActualHeight * Rand.NextDouble();
            yAnimation.To = MainCanvas.ActualHeight * Rand.NextDouble();
            Storyboard.SetTargetProperty(yAnimation, new PropertyPath(Canvas.TopProperty));

            var storyboard = new Storyboard();
            storyboard.Children.Add(xAnimation);
            storyboard.Children.Add(yAnimation);

            storyboard.Freeze();

            return storyboard;
        }

        private DoubleAnimation CreateDoubleAnimationForTranslation()
        {
            var animation = (DoubleAnimation)Dispatcher.Invoke((Func<DoubleAnimation>)delegate()
            {
                return new DoubleAnimation
                {
                    Duration = TimeSpan.FromSeconds(AnimationDuration),
                    EasingFunction = new ShiftedQuadraticEase() { EasingMode = EasingMode.EaseInOut }
                };
            });
            return animation;
        }

        private static readonly Random Rand = new Random(DateTime.Now.Millisecond);
    }
}
1
Sebastian Edelmeier On

Are you absolutely sure your Code is running hardware accelerated? Please look into this list : http://blogs.msdn.com/b/jgoldb/archive/2010/06/22/software-rendering-usage-in-wpf.aspx.

If so - given that ubercool hardware you got - you could try running it on CPU instead of GPU. You can enforce that by setting the RenderMode to SoftwareOnly (Item 6 in the list linked to above)