Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Low Allocation Drawing in WPF

I am having some serious issues with WPF and using DrawingContext, or specifically VisualDrawingContext coming from overriding OnRender on an element or if using DrawingVisual.RenderOpen().

The problem is this allocates a lot. For example, it seems to be allocating a byte[] buffer each time a drawing context is used.

Examples, of how drawing context is used.

using (var drawingContext = m_drawingVisual.RenderOpen())
{
    // Many different drawingContext.Draw calls
    // E.g. DrawEllipse, DrawRectangle etc.
}

or

override void OnRender(DrawingContext drawingContext)
{
    // Many different drawingContext.Draw calls
    // E.g. DrawEllipse, DrawRectangle etc.
}

This causes a lot of allocations, causing some unwanted garbage collections. So yes I need this, and please stay on topic :).

What are the options for drawing in WPF with zero or low number of managed heap allocations? Reusing objects is fine, but I have yet to find a way to do this... or doesn't then have issues with DependencyProperty and allocations around/inside it.

I do know about WritableBitmapEx but was hoping for a solution that does not involve rasterising to predefined bitmap, but instead proper "vector" graphics that can still be zoomed for example.

NOTE: CPU usage is a concern but much less than the massive garbage pressure caused by this.

UPDATE: I am looking for a solution for .NET Framework 4.5+, if there is anything in later versions e.g. 4.7 that might help answer this then that is fine. But it is for the desktop .NET Framework.

UPDATE 2: A brief description of the two main scenarios. All examples have been profiled with CLRProfiler, and it shows clearly that lots of allocations occur due to this and that this is a problem for our use case. Note that this is example code intended to convey the principles not the exact code.

A: This scenario is shown below. Basically, an image is shown and some overlay graphics are drawn via a custom DrawingVisualControl, which then uses using (var drawingContext = m_drawingVisual.RenderOpen()) to get a drawing context and then draws via that. Lots of ellipse, rectangles and text is drawn. This example also shows some scaling stuff, this is just for zooming etc.

<Viewbox x:Name="ImageViewbox"  VerticalAlignment="Center" HorizontalAlignment="Center">
    <Grid x:Name="ImageGrid" SnapsToDevicePixels="True" ClipToBounds="True">
        <Grid.LayoutTransform>
            <ScaleTransform x:Name="ImageTransform" CenterX="0" CenterY="0" 
                            ScaleX="{Binding ElementName=ImageScaleSlider, Path=Value}"
                            ScaleY="{Binding ElementName=ImageScaleSlider, Path=Value}" />
        </Grid.LayoutTransform>
        <Image x:Name="ImageSource" RenderOptions.BitmapScalingMode="NearestNeighbor" SnapsToDevicePixels="True"
               MouseMove="ImageSource_MouseMove" /> 
        <v:DrawingVisualControl x:Name="DrawingVisualControl" Visual="{Binding DrawingVisual}" 
                                SnapsToDevicePixels="True" 
                                RenderOptions.BitmapScalingMode="NearestNeighbor" 
                                IsHitTestVisible="False" />
    </Grid>
</Viewbox>

The `DrawingVisualControl is defined as:

public class DrawingVisualControl : FrameworkElement
{
    public DrawingVisual Visual
    {
        get { return GetValue(DrawingVisualProperty) as DrawingVisual; }
        set { SetValue(DrawingVisualProperty, value); }
    }

    private void UpdateDrawingVisual(DrawingVisual visual)
    {
        var oldVisual = Visual;
        if (oldVisual != null)
        {
            RemoveVisualChild(oldVisual);
            RemoveLogicalChild(oldVisual);
        }

        AddVisualChild(visual);
        AddLogicalChild(visual);
    }

    public static readonly DependencyProperty DrawingVisualProperty =
          DependencyProperty.Register("Visual", 
                                      typeof(DrawingVisual),
                                      typeof(DrawingVisualControl),
                                      new FrameworkPropertyMetadata(OnDrawingVisualChanged));

    private static void OnDrawingVisualChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var dcv = d as DrawingVisualControl;
        if (dcv == null) { return; }

        var visual = e.NewValue as DrawingVisual;
        if (visual == null) { return; }

        dcv.UpdateDrawingVisual(visual);
    }

    protected override int VisualChildrenCount
    {
        get { return (Visual != null) ? 1 : 0; }
    }

    protected override Visual GetVisualChild(int index)
    {
        return this.Visual;
    }
}

B: The second scenario involves drawing a moving "grid" of data e.g. 20 rows of 100 columns, with elements consisting of a border and text with different colors to display some status. The grid moves depending on external input, and for now is only updated 5-10 times per second. 30 fps would be better. This, thus, updates 2000 items in an ObservableCollection tied to a ListBox (with VirtualizingPanel.IsVirtualizing="True") and the ItemsPanel being a Canvas. We can't even show this during our normal use case, since it allocates so much that the GC pauses become way too long and frequent.

<ListBox x:Name="Items" Background="Black" 
     VirtualizingPanel.IsVirtualizing="True" SnapsToDevicePixels="True">
    <ListBox.ItemTemplate>
        <DataTemplate DataType="{x:Type vm:ElementViewModel}">
            <Border Width="{Binding Width_mm}" Height="{Binding Height_mm}"
                    Background="{Binding BackgroundColor}" 
                    BorderBrush="{Binding BorderColor}" 
                    BorderThickness="3">
                <TextBlock Foreground="{Binding DrawColor}" Padding="0" Margin="0"
                   Text="{Binding TextResult}" FontSize="{Binding FontSize_mm}" 
                   TextAlignment="Center" VerticalAlignment="Center" 
                   HorizontalAlignment="Center"/>
            </Border>
        </DataTemplate>
    </ListBox.ItemTemplate>
    <ListBox.ItemContainerStyle>
        <Style TargetType="{x:Type ListBoxItem}">
            <Setter Property="Canvas.Left" Value="{Binding X_mm}"/>
            <Setter Property="Canvas.Top" Value="{Binding Y_mm}"/>
        </Style>
    </ListBox.ItemContainerStyle>
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <Canvas IsItemsHost="True"
                Width="{Binding CanvasWidth_mm}"
                Height="{Binding CanvasHeight_mm}"
                />
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
</ListBox>

There is a lot of data binding here, and the box'ing of value types do incur a lot of allocations, but that is not the main problem here. It is the allocations done by WPF.

like image 319
nietras Avatar asked Jul 06 '17 19:07

nietras


2 Answers

Some Inputs

Your piece of code is not available so I can suggest only. When its comes to performance, use profiling tools available from Microsoft. You can find the tools here

The one more important link where you can read is WPF graphics

Note:- Try using Drawing Group

like image 92
Ramankingdom Avatar answered Nov 06 '22 01:11

Ramankingdom


The WinForms Canvas solution has some issues, especially the so-called "airspace" issues due to how WindowsFormsHost interacts with WPF. To keep it short, this means that no WPF visuals can be drawn on top of the host.

This can be solved by recognizing that since we have to double buffer anyway, we might as well buffer into a WriteableBitmap that can then be drawn as usual via an Image control.

This can be fascillitated by using a utility class like the below:

using System;
using System.Drawing;
using System.Windows;
using SWM = System.Windows.Media;
using SWMI = System.Windows.Media.Imaging;

public class GdiGraphicsWriteableBitmap
{
    readonly Action<Rectangle, Graphics> m_draw;
    SWMI.WriteableBitmap m_wpfBitmap = null;
    Bitmap m_gdiBitmap = null;

    public GdiGraphicsWriteableBitmap(Action<Rectangle, Graphics> draw)
    {
        if (draw == null) { throw new ArgumentNullException(nameof(draw)); }
        m_draw = draw;
    }

    public SWMI.WriteableBitmap WriteableBitmap => m_wpfBitmap;

    public bool IfNewSizeResizeAndDraw(int width, int height)
    {
        if (m_wpfBitmap == null ||
            m_wpfBitmap.PixelHeight != height ||
            m_wpfBitmap.PixelWidth != width)
        {
            Reset();
            // Can't dispose wpf
            const double Dpi = 96;
            m_wpfBitmap = new SWMI.WriteableBitmap(width, height, Dpi, Dpi,
                SWM.PixelFormats.Bgr24, null);
            var ptr = m_wpfBitmap.BackBuffer;
            m_gdiBitmap = new Bitmap(width, height, m_wpfBitmap.BackBufferStride,
                System.Drawing.Imaging.PixelFormat.Format24bppRgb, ptr);

            Draw();

            return true;
        }
        return false;
    }

    public void Draw()
    {
        if (m_wpfBitmap != null)
        {
            m_wpfBitmap.Lock();
            int width = m_wpfBitmap.PixelWidth;
            int height = m_wpfBitmap.PixelHeight;
            {
                using (var g = Graphics.FromImage(m_gdiBitmap))
                {
                    m_draw(new Rectangle(0, 0, width, height), g);
                }
            }
            m_wpfBitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
            m_wpfBitmap.Unlock();
        }
    }

    // If window containing this is not shown, one can Reset to stop draw or similar...
    public void Reset()
    {
        m_gdiBitmap?.Dispose();
        m_wpfBitmap = null;
    }
}

And then binding the ImageSource to an Image in XAML:

    <Grid x:Name="ImageContainer" SnapsToDevicePixels="True">
        <Image x:Name="ImageSource" 
               RenderOptions.BitmapScalingMode="HighQuality" SnapsToDevicePixels="True">
        </Image>
    </Grid>

And the handling resize on the Grid to make the WriteableBitmap match in size e.g.:

public partial class SomeView : UserControl
{
    ISizeChangedViewModel m_viewModel = null;

    public SomeView()
    {
        InitializeComponent();

        this.DataContextChanged += OnDataContextChanged;
    }

    void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        if (m_viewModel != null)
        {
            this.ImageContainer.SizeChanged -= ImageSource_SizeChanged;
        }
        m_viewModel = e.NewValue as ISizeChangedViewModel;
        if (m_viewModel != null)
        {
            this.ImageContainer.SizeChanged += ImageSource_SizeChanged;
        }
    }

    private void ImageSource_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        var newSize = e.NewSize;
        var width = (int)Math.Round(newSize.Width);
        var height = (int)Math.Round(newSize.Height);
        m_viewModel?.SizeChanged(width, height);
    }
}

This way you can use WinForms/GDI+ for drawing with zero heap allocations and even WriteableBitmapEx if you prefer. Note that you then get great DrawString support with GDI+ incl. MeasureString.

The drawback is this is rasterized and sometimes can have some interpolation issues. So be sure to also set UseLayoutRounding="True" on the parent window/user control.

like image 35
nietras Avatar answered Nov 06 '22 01:11

nietras