Fast 2D graphics in WPF
Asked Answered
S

5

42

I need to draw a large amount of 2D elements in WPF, such as lines and polygons. Their position also needs to be updated constantly.

I have looked at many of the answers here which mostly suggested using DrawingVisual or overriding the OnRender function. To test these methods I've implemented a simple particle system rendering 10000 ellipses and I find that the drawing performance is still really terrible using both of these approaches. On my PC I can't get much above 5-10 frames a second. which is totally unacceptable when you consider that I easily draw 1/2 million particles smoothly using other technologies.

So my question is, am I running against a technical limitation here of WPF or am I missing something? Is there something else I can use? any suggestions welcome.

Here the code I tried

content of MainWindow.xaml:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="500" Width="500" Loaded="Window_Loaded">
    <Grid Name="xamlGrid">

    </Grid>
</Window>

content of MainWindow.xaml.cs:

using System.Windows.Threading;

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }


        EllipseBounce[]     _particles;
        DispatcherTimer     _timer = new DispatcherTimer();

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {

            //particles with Ellipse Geometry
            _particles = new EllipseBounce[10000];

            //define area particles can bounce around in
            Rect stage = new Rect(0, 0, 500, 500);

            //seed particles with random velocity and position
            Random rand = new Random();

            //populate
            for (int i = 0; i < _particles.Length; i++)
            {
               Point pos = new Point((float)(rand.NextDouble() * stage.Width + stage.X), (float)(rand.NextDouble() * stage.Height + stage.Y));
               Point vel = new Point((float)(rand.NextDouble() * 5 - 2.5), (float)(rand.NextDouble() * 5 - 2.5));
                _particles[i] = new EllipseBounce(stage, pos, vel, 2);
            }

            //add to particle system - this will draw particles via onrender method
            ParticleSystem ps = new ParticleSystem(_particles);


            //at this element to the grid (assumes we have a Grid in xaml named 'xmalGrid'
            xamlGrid.Children.Add(ps);

            //set up and update function for the particle position
            _timer.Tick += _timer_Tick;
            _timer.Interval = new TimeSpan(0, 0, 0, 0, 1000 / 60); //update at 60 fps
            _timer.Start();

        }

        void _timer_Tick(object sender, EventArgs e)
        {
            for (int i = 0; i < _particles.Length; i++)
            {
                _particles[i].Update();
            }
        }
    }

    /// <summary>
    /// Framework elements that draws particles
    /// </summary>
    public class ParticleSystem : FrameworkElement
    {
        private DrawingGroup _drawingGroup;

        public ParticleSystem(EllipseBounce[] particles)
        {
            _drawingGroup = new DrawingGroup();

            for (int i = 0; i < particles.Length; i++)
            {
                EllipseGeometry eg = particles[i].EllipseGeometry;

                Brush col = Brushes.Black;
                col.Freeze();

                GeometryDrawing gd = new GeometryDrawing(col, null, eg);

                _drawingGroup.Children.Add(gd);
            }

        }


        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);

            drawingContext.DrawDrawing(_drawingGroup);
        }
    }

    /// <summary>
    /// simple class that implements 2d particle movements that bounce from walls
    /// </summary>
    public class SimpleBounce2D
    {
        protected Point     _position;
        protected Point     _velocity;
        protected Rect     _stage;

        public SimpleBounce2D(Rect stage, Point pos,Point vel)
        {
            _stage = stage;

            _position = pos;
            _velocity = vel;
        }

        public double X
        {
            get
            {
                return _position.X;
            }
        }


        public double Y
        {
            get
            {
                return _position.Y;
            }
        }

        public virtual void Update()
        {
            UpdatePosition();
            BoundaryCheck();
        }

        private void UpdatePosition()
        {
            _position.X += _velocity.X;
            _position.Y += _velocity.Y;
        }

        private void BoundaryCheck()
        {
            if (_position.X > _stage.Width + _stage.X)
            {
                _velocity.X = -_velocity.X;
                _position.X = _stage.Width + _stage.X;
            }

            if (_position.X < _stage.X)
            {
                _velocity.X = -_velocity.X;
                _position.X = _stage.X;
            }

            if (_position.Y > _stage.Height + _stage.Y)
            {
                _velocity.Y = -_velocity.Y;
                _position.Y = _stage.Height + _stage.Y;
            }

            if (_position.Y < _stage.Y)
            {
                _velocity.Y = -_velocity.Y;
                _position.Y = _stage.Y;
            }
        }
    }


    /// <summary>
    /// extend simplebounce2d to add ellipse geometry and update position in the WPF construct
    /// </summary>
    public class EllipseBounce : SimpleBounce2D
    {
        protected EllipseGeometry _ellipse;

        public EllipseBounce(Rect stage,Point pos, Point vel, float radius)
            : base(stage, pos, vel)
        {
            _ellipse = new EllipseGeometry(pos, radius, radius);
        }

        public EllipseGeometry EllipseGeometry
        {
            get
            {
                return _ellipse;
            }
        }

        public override void Update()
        {
            base.Update();
            _ellipse.Center = _position;
        }
    }
}
Saraband answered 19/4, 2013 at 15:13 Comment(11)
I was just doing some tests by overriding OnRender() and throwing in some 10000 random drawingContext.DrawLine(). I discovered it makes a HUGE difference in performance just by Freezing Freezables such as Pen and Brush.Maternal
ok, thanks will give that a try. apart from pen (which is null in my implementation) and Brush is there anything else that should be frozen?Saraband
unfortunately i can't get a noticeable change in performance when freezing Brush. my test particle renderer is still only running at around 5 frames a second, which is just way too slow. at this rate it would probably be faster to manually draw particles to bitmap on the CPU - i just don't understand how WPF can be this slow when it's build on DirectXSaraband
Post some sample code... also have you looked at this?Maternal
thanks again. posted the code i used to test performance. drawing 10000 ellipses.Saraband
WPF is a retained mode system, overriding OnRender is, most of the time, not the way to go. Compose your scene and let it draw. You can check this out to draw a million polygons: blogs.msdn.com/b/kaelr/archive/2010/08/11/… it's using a "VirtualCanvas"Deming
@SimonMourier +1 Definitely agree. By overriding OnRender I suspect you are moving the balance of CPU/GPU work and putting a little more work on the CPU with the net result that there is a LOT more P/Invoke. The largest hurdle that the WPF team had was the slow performance of P/Invoke. So a HUGE amount of code is written in C++ to consolidate that.Radiography
However WPF is still fundamentally going to be "slow" due to the P/Invoke issue. For pure performance you want to use unmanaged C++ with the new Direct2D libraries (a 2D facade for the Direct3D library).Radiography
everything i tried is roughly equally slow, OnRender or letting it 'just draw' makes no perceptible difference. In WPF i can easily update HD sized bitmaps at 60fps which is infinitely more data throughput than drawing a few circles. XNA is managed and it would do this without any problems. so why not WPF if it is also based on DirectX?Saraband
@dr.mo, Have you had progress on this problem? I need to draw to a live camera image, so it needs to be fast too... :)Lombroso
@pedro - no, i've given up for the time being. i think the answer is to use directX for anything really needing fast graphics. As Aron said, you can implement this via D3DImage in WPF. OpenGL is of course an option too, except it won't work on windows store or phone apps (yet?)Saraband
J
14

I believe the sample code provided is pretty much as good as it gets, and is showcasing the limits of the framework. In my measurements I profiled an average cost of 15-25ms is attributed to render-overhead. In essence we speak here about just the modification of the centre (dependency-) property, which is quite expensive. I presume it is expensive because it propagates the changes to mil-core directly.

One important note is that the overhead cost is proportional to the amount of objects whose position are changed in the simulation. Rendering a large quantity of objects on itself is not an issue when a majority of objects are temporal coherent i.e. don't change positions.

The best alternative approach for this situation is to resort to D3DImage, which is an element for the Windows Presentation Foundation to present information rendered with DirectX. Generally spoken that approach should be effective, performance wise.

Juggler answered 27/4, 2013 at 0:13 Comment(0)
H
4

You could try a WriteableBitmap, and produce the image using faster code on a background thread. However, the only thing you can do with it is copy bitmap data, so you either have to code your own primitive drawing routines, or (which might even work in your case) create a "stamp" image which you copy to everywhere your particles go...

Heliport answered 29/4, 2013 at 6:43 Comment(2)
yes absolutely. i bet using agg i could draw more particles on the CPU than with WPF on the GPU. however, i need the CPU for other stuff, and it just seems wrong when i know it's possible to do this very fast on the GPU.Saraband
i've tested different kind of drawing functionality, rendering to bitmap first than only drawing that bitmap(not relying wpf draw functions) is way faster than other wpf methods.Hernardo
A
2

The fastest WPF drawing method I have found is to:

  1. create a DrawingGroup "backingStore".
  2. during OnRender(), draw my drawing group to the drawing context
  3. anytime I want, backingStore.Open() and draw new graphics objects into it

The surprising thing about this for me, coming from Windows.Forms.. is that I can update my DrawingGroup after I've added it to the DrawingContext during OnRender(). This is updating the existing retained drawing commands in the WPF drawing tree and triggering an efficient repaint.

In a simple app I've coded in both Windows.Forms and WPF (SoundLevelMonitor), this method empirically feels pretty similar in performance to immediate OnPaint() GDI drawing.

I think WPF did a dis-service by calling the method OnRender(), it might be better termed AccumulateDrawingObjects()

This basically looks like:

DrawingGroup backingStore = new DrawingGroup();

protected override void OnRender(DrawingContext drawingContext) {      
    base.OnRender(drawingContext);            

    Render(); // put content into our backingStore
    drawingContext.DrawDrawing(backingStore);
}

// I can call this anytime, and it'll update my visual drawing
// without ever triggering layout or OnRender()
private void Render() {            
    var drawingContext = backingStore.Open();
    Render(drawingContext);
    drawingContext.Close();            
}

I've also tried using RenderTargetBitmap and WriteableBitmap, both to an Image.Source, and written directly to a DrawingContext. The above method is faster.

Albuminoid answered 7/6, 2017 at 21:58 Comment(1)
Render(drawingContext); -- what is this calling? There isn't a method "Render" that takes a DrawingContext. Was this supposed to be an OnRender call on something else?Inseparable
J
0

In windows forms these kind of things made me fall back to;

  • Set Visible=False for the highest level container (e.g. canvas of the form itself)
  • Draw a lot
  • Set Visible=True

Not sure if WPF supports this.

Jigaboo answered 29/4, 2013 at 10:10 Comment(0)
G
-3

Here are some of the things you may try: (I tried them with your sample and it seems to look faster (at least on my system)).

  • Use Canvas instead of Grid (unless you have other reasons). Play BitmapScalingMode and CachingHint:

    <Canvas Name="xamlGrid" RenderOptions.BitmapScalingMode="LowQuality" RenderOptions.CachingHint="Cache" IsHitTestVisible = "False">
    
    </Canvas>
    
  • Add a StaticResource for Brush used in GeometryDrawing:

    <SolidColorBrush x:Key="MyBrush" Color="DarkBlue"/>
    

in code use as:

    GeometryDrawing gd = new GeometryDrawing((SolidColorBrush)this.FindResource("MyBrush"), null, eg);

I hope this helps.

Genna answered 24/4, 2013 at 20:43 Comment(3)
-1. How will a casting thing improve performance?? also putting an already frozen freezable (Brushes.Black) as a StaticResource won't help.Maternal
@HighCore: Casting in general is to be avoided. However, in this case it is either as good or better than creating the brush for every item. I think you should test it before judging it! It would be better to use the StaticResource within a Style/template but that would involve changing the way he is creating the particles.Genna
Sorry, not true. System.Windows.Media.Brushes.Black is a static instance, therefore when you reference it you are not "creating a new one each time", but actually using the same one. Which, by the way is already frozen.Maternal

© 2022 - 2024 — McMap. All rights reserved.