Low Allocation Drawing in WPF

Viewed 1444

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.

3 Answers
Related