Fast 2D graphics in WPF

25,372

Solution 1

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.

Solution 2

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...

Solution 3

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.

Share:
25,372
morishuz
Author by

morishuz

Updated on June 12, 2020

Comments

  • morishuz
    morishuz almost 4 years

    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;
            }
        }
    }
    
  • Federico Berasategui
    Federico Berasategui about 11 years
    -1. How will a casting thing improve performance?? also putting an already frozen freezable (Brushes.Black) as a StaticResource won't help.
  • FHnainia
    FHnainia about 11 years
    @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.
  • Federico Berasategui
    Federico Berasategui about 11 years
    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.
  • morishuz
    morishuz about 11 years
    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.
  • Gorkem
    Gorkem over 7 years
    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.
  • 00jt
    00jt over 4 years
    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?