AvaloniaUI / Avalonia

Develop Desktop, Embedded, Mobile and WebAssembly apps with C# and XAML. The most popular .NET UI client technology
https://avaloniaui.net
MIT License
25.84k stars 2.24k forks source link

Drawing many ellipses slows down rendering Win32 D2D #918

Closed predatorgor closed 5 years ago

predatorgor commented 7 years ago

Hi, when I create more then 1500 ellipses on the canvas every new ellipse takes longer time to be created. It looks like every time ellipse is added to the canvas and positioned, whole canvas gets a compleate redraw and slows down. Is there a builtin machanism to draw many primitive shapes on the canvas or on some surface? I would like to create an ellipse on every mouse move event and position it on the canvas. What is the best aproach to do this using Avalonia UI? This is just a test I made with ellipse. Next step would be to make something similiar to an InkCanvas(WPF,UWP), so I would like to get some hint to take right aproach. For instance, create own caching machanism or use some low level drawing methods to draw on the surface without hit testing like a texture. Is there a way to create antialised shapes and rasterise them in realtime for fast drawing? `public class MainWindow : Window { private SolidColorBrush _brush; private Canvas _canvas; private int _count;

    public MainWindow()
    {
        this.InitializeComponent();
        App.AttachDevTools(this); 
    }

    private void InitializeComponent()
    {
        _brush = new SolidColorBrush(Color.Parse("red"));
        AvaloniaXamlLoader.Load(this);

        _canvas = new Canvas();
        this.Content = _canvas;
        PointerMoved += MainWindow_PointerMoved;
    }

    private void MainWindow_PointerMoved(object sender, Avalonia.Input.PointerEventArgs e)
    {
        var newEllipse = new Ellipse();
        newEllipse.Fill = _brush;
        newEllipse.Width = 10;
        newEllipse.Height = 10;

        _canvas.Children.Add(newEllipse);
        Canvas.SetLeft(newEllipse, e.GetPosition(this).X - newEllipse.Width / 2);
        Canvas.SetTop(newEllipse, e.GetPosition(this).Y - newEllipse.Height / 2);
        Console.WriteLine(++_count);
    }
}`
wieslawsoltes commented 7 years ago

@predatorgor Using shapes is not most efficient way of rendering many elements. To get access to low level rendering system in Avalonia you can create custom control by deriving from for example from Canvas and ovvriding public override void Render(DrawingContext context) method. Than use DrawingContext for drawing shapes. Here is example of such control: https://github.com/wieslawsoltes/SpiroNet/blob/master/src/SpiroNet.Editor.Avalonia/Renderer/CanvasRenderer.cs

grokys commented 7 years ago

@predatorgor 👍 to what @wieslawsoltes said, but also you might want to try out the Avalonia scenegraph branch which should improve drawing performance (but it still a work in progress currently).

wieslawsoltes commented 7 years ago

@predatorgor Try this code (create new Avalonia app named DrawCanvasDemo):

EllipseShape.cs


namespace DrawCanvasDemo
{
    public class EllipseShape
    {
        public double X { get; set; }
        public double Y { get; set; }
        public double Width { get; set; }
        public double Height { get; set; }
        public EllipseShape(double x, double y, double width, double height)
        {
            this.X = x;
            this.Y = y;
            this.Width = width;
            this.Height = height;
        }
    }
}

DrawCanvas.cs

using System.Collections.Generic;
using System.Diagnostics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;

namespace DrawCanvasDemo
{
    public class DrawCanvas : Canvas
    {
        private double _width;
        private double _height;
        private SolidColorBrush _brush;
        private IList<EllipseShape> _ellipses;
        private IDictionary<EllipseShape, EllipseGeometry> _ellipsesCache;

        public DrawCanvas()
        {
            _width = 10;
            _height = 10;
            _brush = new SolidColorBrush(Color.Parse("red"));
            _ellipses = new List<EllipseShape>();
            _ellipsesCache = new Dictionary<EllipseShape, EllipseGeometry>();

            this.PointerMoved += (sender, args) =>
            {
                var p = args.GetPosition(this);

                var ellipse = new EllipseShape(p.X - _width / 2, p.Y - _height / 2, _width, _height);
                _ellipses.Add(ellipse);

                var geometry = new EllipseGeometry(new Rect(ellipse.X, ellipse.Y, ellipse.Width, ellipse.Height));
                _ellipsesCache.Add(ellipse, geometry);

                Debug.WriteLine(_ellipses.Count);
                this.InvalidateVisual();
            };
        }

        public override void Render(DrawingContext context)
        {
            base.Render(context);

            foreach (var ellipse in _ellipses)
            {
                var geometry = _ellipsesCache[ellipse];
                context.DrawGeometry(_brush, null, geometry);
            }
        }
    }
}

MainWindow.xaml

<Window x:Class="DrawCanvasDemo.MainWindow"
        xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:DrawCanvasDemo;assembly=DrawCanvasDemo"
        Title="DrawCanvasDemo" Height="700" Width="1000"
        UseLayoutRounding="True">
    <Grid>
        <local:DrawCanvas Background="LightGray" Width="600" Height="600"/>
    </Grid>
</Window>

MainWindow.xaml.cs

using Avalonia.Controls;
using Avalonia.Markup.Xaml;

namespace DrawCanvasDemo
{
    public class MainWindow : Window
    {
        public MainWindow()
        {
            this.InitializeComponent();
        }

        private void InitializeComponent()
        {
            AvaloniaXamlLoader.Load(this);
        }
    }
}

App.xaml

<Application xmlns="https://github.com/avaloniaui">
    <Application.Styles>
        <StyleInclude Source="resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default"/>
        <StyleInclude Source="resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"/>
    </Application.Styles>
</Application>

App.xaml.cs

using Avalonia;
using Avalonia.Markup.Xaml;

namespace DrawCanvasDemo
{
    class App : Application
    {
        public override void Initialize()
        {
            AvaloniaXamlLoader.Load(this);
            base.Initialize();
        }

        static void Main(string[] args)
        {
            var app = new App();
            AppBuilder.Configure<App>()
                .UsePlatformDetect()
                .Start<MainWindow>();
        }
    }
}

Full source code: https://github.com/wieslawsoltes/DrawCanvasDemo

predatorgor commented 7 years ago

@wieslawsoltes it is an improvement and it is faster due to caching geometry. However, it still renders every geometry that makes rendering time drift with every new item. So with every tiny stroke in future the whole canvas has to be redrawn. Do I need to go Direct2d way and make rendering to a texture first and call that texture on render of my "FastCanvas" controle? Maybe there is something higher level in Avolonia to do this? Another way is bitmap cache, but I don't know how much the RAM bitmap cache helps... It will probably waste all the CPU time on moving pixels from RAM to GPU memory?

wieslawsoltes commented 7 years ago

@predatorgor Do not know any better solution. Maybe someone else could help you.

grokys commented 7 years ago

@predatorgor have you been able to try it with the scenegraph branch?

predatorgor commented 7 years ago

I have not tried the scenegraph branch yet. However I experimented with the Direct2D api itself. The fast and low latency approach is to render to shared Direct3D texture first and later show it on screen using second thread... The pointing devices like mouse, pen, touch can sample position at frequency up to 1000Hz so rendering 1000 fps on screen can be difficult because it needs to go through all the DWM and vertical synchronization to be efficient. I will give a try to scenegraph and check if it is possible to optimize it for texture rendering...

Logically it should be possible to optimize it even for master branch. Create a control with texture rendering in onRender method, and use other thread to capture mouse and render to texture.

grokys commented 7 years ago

Yep, on the scenegraph branch the rendering is done on a separate rendering thread. See https://github.com/AvaloniaUI/Avalonia/pull/827 for more details.

jkoritzinsky commented 6 years ago

@predatorgor can you try again on the current CI package?

grokys commented 5 years ago

Closing this due to inactivity and the fact that this should now be improved.