Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to achieve smooth UI updates every 16 ms?

Tags:

wpf

I am trying to create sort of a radar. Radar is VisualCollection that consists of 360 DrawingVisual's (which represent radar beams). Radar is placed on Viewbox.

class Radar : FrameworkElement
{
    private VisualCollection visuals;
    private Beam[] beams = new Beam[BEAM_POSITIONS_AMOUNT]; // all geometry calculation goes here

    public Radar()
    {
        visuals = new VisualCollection(this);

        for (int beamIndex = 0; beamIndex < BEAM_POSITIONS_AMOUNT; beamIndex++)
        {
            DrawingVisual dv = new DrawingVisual();
            visuals.Add(dv);
            using (DrawingContext dc = dv.RenderOpen())
            {
                dc.DrawGeometry(Brushes.Black, null, beams[beamIndex].Geometry);
            }
        }

        DrawingVisual line = new DrawingVisual();
        visuals.Add(line);

        // DISCRETES_AMOUNT is about 500
        this.Width = DISCRETES_AMOUNT * 2;
        this.Height = DISCRETES_AMOUNT * 2;
    }

    public void Draw(int beamIndex, Brush brush)
    {
        using (DrawingContext dc = ((DrawingVisual)visuals[beamIndex]).RenderOpen())
        {
            dc.DrawGeometry(brush, null, beams[beamIndex].Geometry);
        }
    }

    protected override Visual GetVisualChild(int index)
    {
        return visuals[index];
    }

    protected override int VisualChildrenCount
    {
        get { return visuals.Count; }
    }
}

Each DrawingVisual has precalculated geometry for DrawingContext.DrawGeometry(brush, pen, geometry). Pen is null and brush is a LinearGradientBrush with about 500 GradientStops. The brush gets updated every few milliseconds, lets say 16 ms for this example. And that is what gets laggy. Here goes the overall logic.

In MainWindow() constructor I create the radar and start a background thread:

    private Radar radar;

    public MainWindow()
    {
        InitializeComponent();

        radar = new Radar();
        viewbox.Child = radar;

        Thread t = new Thread(new ThreadStart(Run));
        t.Start();
    }

In Run() method there is an infinite loop, where random brush is generated, Dispatcher.Invoke() is called and a delay for 16 ms is set:

    private int beamIndex = 0;
    private Random r = new Random();
    private const int turnsPerMinute = 20;
    private static long delay = 60 / turnsPerMinute * 1000 / (360 / 2);
    private long deltaDelay = delay;

    public void Run()
    {
        int beginTime = Environment.TickCount;
        while (true)
        {
            GradientStopCollection gsc = new GradientStopCollection(DISCRETES_AMOUNT);
            for (int i = 1; i < Settings.DISCRETES_AMOUNT + 1; i++)
            {
                byte color = (byte)r.Next(255);
                gsc.Add(new GradientStop(Color.FromArgb(255, 0, color, 0), (double)i / (double)DISCRETES_AMOUNT));
            }

            LinearGradientBrush lgb = new LinearGradientBrush(gsc);
            lgb.StartPoint = Beam.GradientStarts[beamIndex];
            lgb.EndPoint = Beam.GradientStops[beamIndex];
            lgb.Freeze();

            viewbox.Dispatcher.Invoke(new Action( () =>
            {
                radar.Draw(beamIndex, lgb);
            }));

            beamIndex++;
            if (beamIndex >= BEAM_POSITIONS_AMOUNT)
            {
                beamIndex = 0;
            }

            while (Environment.TickCount - beginTime < delay) { }
            delay += deltaDelay;
        }
    }

Every Invoke() call it performs one simple thing: dc.DrawGeometry(), which redraws the beam under current beamIndex. However, sometimes it seems, like before UI updates, radar.Draw() is called few times and instead of drawing 1 beam per 16 ms, it draws 2-4 beams per 32-64 ms. And it is disturbing. I really want to achieve smooth movement. I need one beam to get drawn per exact period of time. Not this random stuff. This is the list of what I have tried so far (nothing helped):

  • placing radar in Canvas;
  • using Task, BackgroundWorker, Timer, custom Microtimer.dll and setting different Thread Priorities;
  • using different ways of implementing delay: Environment.TickCount, DateTime.Now.Ticks, Stopwatch.ElapsedMilliseconds;
  • changing LinearGradientBrush to predefined SolidColorBrush;
  • using BeginInvoke() instead of Invoke() and changing Dispatcher Priorities;
  • using InvalidateVisuals() and ugly DoEvents();
  • using BitmapCache, WriteableBitmap and RenderTargetBitmap (using DrawingContext.DrawImage(bitmap);
  • working with 360 Polygon objects instead of 360 DrawingVisuals. This way I could avoid using Invoke() method. Polygon.FillProperty of each polygon was bound to ObservableCollection, and INotifyPropertyChanged was implemented. So simple line of code {brushCollection[beamIndex] = (new created and frozen brush)} led to polygon FillProperty update and UI was getting redrawn. But still no smooth movement;
  • probably there were few more little workarounds I could forget about.

What I did not try:

  • use tools to draw 3D (Viewport) to draw 2D radar;
  • ...

So, this is it. I am begging for help.

EDIT: These lags are not about PC resources - without delay radar can do about 5 full circles per second (moving pretty fast). Most likely it is something about multithread/UI/Dispatcher or something else that I am yet to understand.

EDIT2: Attaching an .exe file so you could see what is actually going on: https://dl.dropboxusercontent.com/u/8761356/Radar.exe

EDIT3: DispatcherTimer(DispatcherPriority.Render) did not help aswell.

like image 697
Denver Avatar asked May 27 '14 12:05

Denver


1 Answers

For smooth WPF animations you should make use of the CompositionTarget.Rendering event.

No need for a thread or messing with the dispatcher. The event will automatically be fired before each new frame, similar to HTML's requestAnimationFrame().

In the event update your WPF scene and you're done!

There is a complete example available on MSDN.

like image 64
Sebastian Avatar answered Oct 18 '22 22:10

Sebastian