如何提高 Direct2D 绘制的效率:C#技巧

6
早上好,
我自学了一些使用C#编写Direct2D程序的技术,利用现有的本地包装器(目前使用d2dSharp,但也尝试过SharpDX)。然而,我遇到了效率问题,基本的Direct2D绘图方法需要大约250毫秒才能绘制45,000个基本多边形。我看到的性能与Windows GDI+相当,甚至更慢。我希望有人可以看看我的代码,并提出可能显著改善绘制时间的方法。
这背后的背景是,我有一个个人项目,正在开发一个基本但功能齐全的CAD接口,能够执行各种任务,包括二维有限元分析。为了使其有用,界面需要能够显示数万个原始元素(多边形、圆、矩形、点、弧等)。
我最初使用Windows GDI+(System.Drawing)编写了绘图方法,在任何给定时间屏幕上显示的元素不超过3,000时,性能相当不错。每当用户进行平移、缩放、绘制新元素、删除元素、移动、旋转等操作时,都必须更新屏幕。现在,为了提高效率,我利用四叉树数据结构存储元素,只绘制实际位于控件窗口范围内的元素。这在放大时有很大帮助,但显然,在完全缩小和显示所有元素时没有任何作用。我还使用计时器和tick事件以刷新率(60 Hz)更新屏幕,因此我不会尝试每秒数千次或每个鼠标事件都进行更新。
这是我第一次使用DirectX和Direct2D进行编程,所以我肯定正在学习。也就是说,我花了几天时间查看教程、示例和论坛,并没有找到太多有用的东西。我已经尝试了一打不同的绘图、预处理、多线程等方法。以下是我的代码:
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函数

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);
        }
    }

创建Direct2D工厂和渲染目标的代码
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);
    }

我创建了一个Direct2D工厂和渲染目标,并在始终保持对它们的引用(这样我就不需要每次重新创建)。当绘图层(描述颜色、宽度等信息)创建时,我也同时创建所有的笔刷。因此,我不是每次绘制都创建新的笔刷,而是引用已经存在的笔刷。正如第二段代码片段所示,几何图形也是一样。我只在创建时创建几何图形,并且仅在元素本身移动或旋转时更新几何图形。否则,在绘制后,我只需对渲染目标应用变换即可。
根据我的计时器显示,循环调用elem.DrawDX方法所需的时间大约为225-250毫秒(用于45,000个多边形)。应用变换所需的时间为0-1毫秒,因此似乎瓶颈在于RenderTarget.DrawGeometry()函数上。
我已经尝试使用RenderTarget.DrawEllipse()或RenderTarget.DrawRectangle()进行相同的测试,因为我读到使用DrawGeometry比使用DrawRectangle或DrawEllipse慢,因为矩形/椭圆的几何形状事先已知。然而,在我所有的测试中,无论使用哪个绘制函数,相同数量的元素所需的时间总是大致相等的。
我尝试过构建一个多线程的Direct2D工厂,并通过任务运行绘图函数,但那样会慢得多(大约是两倍)。Direct2D方法似乎正在利用我的显卡(启用了硬件加速),因为当屏幕更新时,我监视我的显卡使用情况,它会猛增(我的笔记本电脑配备了一张NVIDIA Quadro移动显卡)。
对不起,我的帖子有点啰嗦。我希望这足够描述我尝试过的事情和背景信息。感谢您提前的任何帮助!
编辑#1:我将代码从foreach迭代列表更改为使用for迭代数组,这将绘制时间缩短了一半!我没有意识到列表比数组慢了这么多(我知道某些性能优势,但没有意识到这么多!)。然而,仍需要125毫秒才能完成绘制。这要好得多,但仍不够平滑。您还有其他建议吗?

一个列表(list)不是慢的,foreach才是。List<T>通常非常快,并且比使用数组更好。 - xxbbcc
@xxbbcc 或许这只是GDI(仍在弄清GDI和GDI+之间的区别)。来自微软网站的信息:“Direct2D和GDI都是即时模式2D渲染API,两者都提供了一定程度的硬件加速。本主题探讨了Direct2D和GDI之间的差异,包括两个API在过去和现在硬件加速特性方面的差异。” - Anthony
1
啊,好的。Direct2D肯定比Win32 GDI快(尽管两者都是加速的)。我不是D2D专家,所以在那方面帮不了你太多 - 确保你的输出表面正确配置。您还需要考虑双重或三重缓冲以实现快速输出和有效使用剪辑未更改的内容。 - xxbbcc
@xxbbcc 剪裁是我没有考虑到的问题。我会进行调查。关于双缓冲,理论上我知道它的作用,但我不确定如何将其应用于此代码。我会看看有关双缓冲的更多信息。 - Anthony
1
你的一个大错误是认为在CAD图层中进行大规模绘图很容易。事实并非如此,你需要花费很长时间来优化它的各个方面。重新绘制一切永远不会帮助缩放。使用多个图层和其他技术。最快的重绘是不需要完成的重绘。还要意识到画笔和文本布局效率非常低下(我真的是指非常低下)。一个包含10个字符的DirectWrite TextLayout可能会消耗12KByte的内存。对所有内容进行分析并进行优化。计划在快速绘图层上工作6个月。 - Lothar
显示剩余8条评论
1个回答

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接