C# How to Improve Efficiency in Direct2D Drawing
Asked Answered
J

1

7

Good morning,

I have been teaching myself a bit of Direct2D programming in C#, utilizing native wrappers that are available (currently using d2dSharp, but have also tried SharpDX). I'm running into problems with efficiency, though, where the basic drawing Direct2D drawing methods are taking approximately 250 ms to draw 45,000 basic polygons. The performance I am seeing is on par, or even slower than, Windows GDI+. I'm hoping that someone can take a look at what I've done and propose a way(s) that I can dramatically improve the time it takes to draw.

The background to this is that I have a personal project in which I am developing a basic but functional CAD interface capable of performing a variety of tasks, including 2D finite element analysis. In order to make it at all useful, the interface needs to be able to display tens-of-thousands of primitive elements (polygons, circles, rectangles, points, arcs, etc.).

I initially wrote the drawing methods using Windows GDI+ (System.Drawing), and performance is pretty good until I reach about 3,000 elements on screen at any given time. The screen must be updated any time the user pans, zooms, draws new elements, deletes elements, moves, rotates, etc. Now, in order to improve efficiency, I utilize a quad tree data structure to store my elements, and I only draw elements that actually fall within the bounds of the control window. This helped significantly when zoomed in, but obviously, when fully zoomed out and displaying all elements, it makes no difference. I also use a timer and tick events to update the screen at the refresh rate (60 Hz), so I'm not trying to update thousands of times per second or on every mouse event.

This is my first time programming with DirectX and Direct2D, so I'm definitely learning here. That being said, I've spent days reviewing tutorials, examples, and forums, and could not find much that helped. I've tried a dozen different methods of drawing, pre-processing, multi-threading, etc. My code is below

Code to Loop Through and Draw Elements

List<IDrawingElement> elementsInBounds = GetElementsInDraftingWindow();

_d2dContainer.Target.BeginDraw();
_d2dContainer.Target.Clear(ColorD2D.FromKnown(Colors.White, 1));

if (elementsInBounds.Count > 0)
{
    Stopwatch watch = new Stopwatch();
    watch.Start();

    #region Using Drawing Element DrawDX Method

    foreach (IDrawingElement elem in elementsInBounds)
    {
        elem.DrawDX(ref _d2dContainer.Target, ref _d2dContainer.Factory, ZeroPoint, DrawingScale, _selectedElementBrush, _selectedElementPointBrush);
    }

    #endregion

    watch.Stop();
    double drawingTime = watch.ElapsedMilliseconds;
    Console.WriteLine("DirectX drawing time = " + drawingTime);
    watch.Reset();
    watch.Start();

    Matrix3x2 scale = Matrix3x2.Scale(new SizeFD2D((float)DrawingScale, (float)DrawingScale), new PointFD2D(0, 0));
    Matrix3x2 translate = Matrix3x2.Translation((float)ZeroPoint.X, (float)ZeroPoint.Y);

    _d2dContainer.Target.Transform = scale * translate;

    watch.Stop();
    double transformTime = watch.ElapsedMilliseconds;
    Console.WriteLine("DirectX transform time = " + transformTime);
}

DrawDX Function for Polygon

public override void DrawDX(ref WindowRenderTarget rt, ref Direct2DFactory fac, Point zeroPoint, double drawingScale, SolidColorBrush selectedLineBrush, SolidColorBrush selectedPointBrush)
    {
        if (_pathGeometry == null)
        {
            CreatePathGeometry(ref fac);
        }

        float brushWidth = (float)(Layer.Width / (drawingScale));
        brushWidth = (float)(brushWidth * 2);

        if (Selected == false)
        {
            rt.DrawGeometry(Layer.Direct2DBrush, brushWidth, _pathGeometry);
            //Note that _pathGeometry is a PathGeometry
        }
        else
        {
            rt.DrawGeometry(selectedLineBrush, brushWidth, _pathGeometry);
        }
    }

Code to Create Direct2D Factory & Render Target

private void CreateD2DResources(float dpiX, float dpiY)
    {
        Factory = Direct2DFactory.CreateFactory(FactoryType.SingleThreaded, DebugLevel.None, FactoryVersion.Auto);

        RenderTargetProperties props = new RenderTargetProperties(
            RenderTargetType.Default, new PixelFormat(DxgiFormat.B8G8R8A8_UNORM,
            AlphaMode.Premultiplied), dpiX, dpiY, RenderTargetUsage.None, FeatureLevel.Default);

        Target = Factory.CreateWindowRenderTarget(_targetPanel, PresentOptions.None, props);
        Target.AntialiasMode = AntialiasMode.Aliased;

        if (_selectionBoxLeftStrokeStyle != null)
        {
            _selectionBoxLeftStrokeStyle.Dispose();
        }

        _selectionBoxLeftStrokeStyle = Factory.CreateStrokeStyle(new StrokeStyleProperties1(LineCapStyle.Flat,
                LineCapStyle.Flat, LineCapStyle.Flat, LineJoin.Bevel, 10, DashStyle.Dash, 0, StrokeTransformType.Normal), null);
    }

I create a Direct2D factory and render target once and keep references to them at all times (that way I'm not recreating each time). I also create all of the brushes when the drawing layer (which describes color, width, etc.) is created. As such, I am not creating a new brush every time I draw, simply referencing a brush that already exists. Same with the geometry, as can be seen in the second code-snippet. I create the geometry once, and only update the geometry if the element itself is moved or rotated. Otherwise, I simply apply a transform to the render target after drawing.

Based on my stopwatches, the time taken to loop through and call the elem.DrawDX methods takes about 225-250 ms (for 45,000 polygons). The time taken to apply the transform is 0-1 ms, so it appears that the bottleneck is in the RenderTarget.DrawGeometry() function.

I've done the same tests with RenderTarget.DrawEllipse() or RenderTarget.DrawRectangle(), as I've read that using DrawGeometry is slower than DrawRectangle or DrawEllipse as the rectangle / ellipse geometry is known beforehand. However, in all of my tests, it hasn't mattered which draw function I use, the time for the same number of elements is always about equal.

I've tried building a multi-threaded Direct2D factory and running the draw functions through tasks, but that is much slower (about two times slower). The Direct2D methods appear to be utilizing my graphics card (hardware accelerated is enabled), as when I monitor my graphics card usage, it spikes when the screen is updating (my laptop has an NVIDIA Quadro mobile graphics card).

Apologies for the long-winded post. I hope this was enough background and description of things I've tried. Thanks in advance for any help!

Edit #1 So changed the code from iterating over a list using foreach to iterating over an array using for and that cut the drawing time down by half! I hadn't realized how much slower lists were than arrays (I knew there was some performance advantage, but didn't realize this much!). It still, however, takes 125 ms to draw. This is much better, but still not smooth. Any other suggestions?

Janinajanine answered 21/3, 2019 at 17:7 Comment(14)
A list is not slow - foreach is. List<T> is pretty fast and better than using arrays in general.Illuminative
See this article: codingsight.com/foreach-or-for-that-is-the-question Haven't vetted it myself, and it seems like it might not be 100% correct, but there seems to be some indication that lists are slower than arrays. I will check out using just the list though. See what happens.Janinajanine
There's a reason that article has 2 stars. List<T> is just an array with internal logic to resize it as needed but that doesn't apply when accessing elements.Illuminative
Thanks, I didn't see the comment at the bottom of the post until now. Looks like you are correct. In process of changing back to list to make sure performance is the same.Janinajanine
@Janinajanine hey man, I also have the same kind of project (CAD) and I am also using Direct2D (Win2D), I can't get to draw over 2000 element at 60 FPS 😐.Tottering
@Illuminative Just switched back to list. It is approximately 20 ms slower when using for loop. The drawing time went from 125 ms to approximately 145-150 ms. So some performance loss, but the for loop was definitely the key.Janinajanine
@Tottering It's kinda frustrating! I mean...it's pretty clear that it's possible to do this, as there are plenty of programs out there that have great rendering times. They might be using custom rendering though. It's just surprising that Windows GDI+ is, in most cases (for me), faster than Direct2D. I know that GDI+ is somewhat hardware accelerated, but just odd that it is faster.Janinajanine
GDI+ is not HW-accelerated at all. It's an incredibly poor graphics library that can easily be outperformed by straight Win32 graphics.Illuminative
@Illuminative Maybe it's just GDI (still figuring out difference between GDI and GDI+). From Microsoft's website: "Direct2D and GDI are both immediate mode 2D rendering APIs and both offer some degree of hardware acceleration. This topic explores the differences between Direct2D and GDI, including past and present differences in the hardware acceleration features of both APIs."Janinajanine
Ah, ok. Direct2D should definitely be faster than Win32 GDI (even though both are accelerated). I'm not a D2D expert so I can't help you much there - make sure your output surface is correctly configured. You may also need to consider double- or triple-buffering for fast output and efficient use of clipping things that didn't change from the previous draw.Illuminative
@Illuminative The clipping is something I hadn't thought of. I will investigate that. Regarding double buffering, I understand in theory what it does, but I'm not sure how to apply it to this code. I'll see what more I can find on double buffering.Janinajanine
@Janinajanine hey I have asked the Win2D team about the performance and how to improve hereTottering
Your big mistake is that you think that large scale drawing in a CAD layer is easy. It's not and you need a long time to optimize it in all ends. Redrawing everything will never help to scale. Use multiple layers and other techniques. The fastest redrawing is redrawing that don't need to be done. Also realize that brushes and text layouts are terrible (and i really mena terrible inefficient). A DirectWrite TextLayout with 10 characters can consume 12KByte memory. Profile everything and optimize everything. And plan to work 6 month on just the fast drawing layer.Phlyctena
@Phlyctena Or apparently just dump Direct2D, and use GDI if you want speed.Morrismorrison
T
1

Direct2D can be used with P/Invoke See the sample "VB Direct2D Pixel Perfect Collision" from https://social.msdn.microsoft.com/Forums/en-US/cea42526-4b82-454d-9d79-2e1d94083552/collisions?forum=vbgeneral the animation is perfect, even done in VB

Triable answered 17/4, 2019 at 8:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.