【问题标题】:How to optimize QGraphicsView's performance?如何优化 QGraphicsView 的性能?
【发布时间】:2022-02-11 21:33:57
【问题描述】:

我正在使用 Qt 5.6.2 开发一个 CAD 应用程序,该应用程序需要在廉价计算机上运行,​​同时它需要在同一场景中处理数千个项目。正因为如此,为了获得最佳性能,我不得不进行大量实验。

我决定创建这篇文章来帮助他人和我自己,只要其他人也提供更多优化技巧。

我的文字仍在进行中,如果我发现更好的技术(或者我说了一些非常愚蠢的话),我可能会更新它。

【问题讨论】:

    标签: qt optimization qgraphicsview qgraphicsscene qgraphicsitem


    【解决方案1】:

    禁用场景交互

    事件处理由 QGraphicsView 引擎的大部分 CPU 使用负责。在每次鼠标移动时,视图都会向场景询问鼠标下的项目,它调用 QGraphicsItem::shape() 方法来检测交叉点。即使是禁用的物品也会发生这种情况。所以,如果你不需要你的场景与鼠标事件交互,你可以设置QGraphicsView::setIntenteractive(false)。就我而言,我的工具中有两种模式(测量和移动/旋转),其中场景基本上是静态的,所有编辑操作都由 QGraphicsView 执行。通过这样做,我能够将帧速率提高 30%,不幸的是 ViewportAnchor::AnchorUnderMouse 停止工作。

    重用你的 QPainterPaths

    在 QGraphicsItem 对象中缓存 QPainterPaths。构建和填充它可能非常缓慢。在我的情况下,读取文件需要 6 秒,因为我将具有 6000 个点的点云转换为具有多个矩形的 QPainterPath。你不会想不止一次这样做。 此外,在 Qt 5.13 中,现在可以保留 QPainterPaths 的内部矢量大小,避免在其增长时出现多个副本。

    简化您的 QGraphicsItem::shape()

    在鼠标事件期间多次调用此方法,即使该项目未启用。尝试使其尽可能高效。 有时,即使缓存 QPainterPath 也是不够的,因为场景执行的路径相交算法对于复杂的形状可能非常慢。在我的例子中,我返回了一个大约有 6000 个矩形的形状,而且速度很慢。在对点云进行下采样后,我能够将矩形的数量减少到 1000 个左右,这显着提高了性能,但仍然不理想,因为即使在项目被禁用时仍会调用 shape()。因此,我决定保留原始 QGraphicsItem:shape() (返回边界框矩形),并在启用项目时返回更复杂的缓存形状。它在移动鼠标时将帧速率提高了近 40%,但我仍然认为这是一个 hack,如果我想出更好的解决方案,我会更新这篇文章。尽管如此,在我的测试中,只要我保持它的边界框不变,我就没有任何问题。如果不是这种情况,您将不得不调用 prepareGeometryChange(),然后更新边界框和其他地方的形状缓存。

    同时测试:光栅和 OpenGL 引擎

    我期待 OpenGL 总是比光栅更好,如果你想要的只是出于明显的原因减少 CPU 使用率,这可能是正确的。但是,如果您只想增加每秒的帧数,尤其是在廉价/旧计算机中,那么也值得尝试测试光栅(默认的 QGraphicsView 视口)。在我的测试中,新的 QOpenGLWidget 比旧的 QGLWidget 稍快,但 FPS 的数量比使用 Raster 慢了近 20%。当然,它可以是特定于应用程序的,并且结果可能会根据您正在渲染的内容而有所不同。

    将 FullViewportUpdate 与 OpenGL 一起使用,并更喜欢使用光栅的其他部分视口更新方法(尽管需要对项目进行更严格的边界矩形维护)。

    尝试禁用/启用 VSync 以查看哪个更适合您:QSurfaceFormat::defaultFormat().setSwapInterval(0 或 1)。启用会降低帧速率,禁用会导致“撕裂”。 https://www.khronos.org/opengl/wiki/Swap_Interval

    缓存复杂的 QGraphicsItems

    如果您的 QGraphicsItem::paint 操作过于复杂并且同一类型大多是静态的,请尝试启用缓存。如果您没有对项目应用变换(如旋转),则使用 DeviceCoordinateCache,否则使用 ItemCoordinateCache。避免经常调用 QGraphicsItem::update() ,否则它可能比没有缓存时更慢。如果您需要更改项目中的某些内容,有两种选择:在子项中绘制它,或使用 QGraphicsView::drawForeground()。

    对类似的 QPainter 绘图操作进行分组

    优先使用drawLines而不是多次调用drawLine;偏爱drawPoints而不是drawPoint。使用 QVarLengthArray(使用堆栈,因此可以更快)或 QVector(使用堆)作为容器。避免经常更换画笔(我怀疑在使用 OpenGL 时更重要)。此外,QPoint 可以更快,并且比 QPointF 更小。

    更喜欢使用修饰线绘制,避免透明度和抗锯齿

    可以禁用抗锯齿功能,尤其是当您绘制的是水平线、垂直线或 45 度线(实际上这样看起来更好)或者您使用的是“视网膜”显示器时。

    搜索热点

    瓶颈可能出现在令人惊讶的地方。使用分析器(在 macOS 中我使用 Instruments/Time Profiler)或其他方法,如经过的计时器、qDebug 或 FPS 计数器(我把它放在我的 QGraphicsView::drawForeground 中)来帮助定位它们。不要让你的代码变得丑陋,试图优化你不确定它们是否是热点的东西。 FPS 计数器示例(尽量保持在 25 以上):

    MyGraphicsView:: MyGraphicsView(){
        ...
        timer = new QTimer(this);
        connect(timer, SIGNAL(timeout()), this, SLOT(oneSecTimeout()));
        timer->setInterval(1000);
        timer->start();
    }
    
    void MyGraphicsView::oneSecTimeout()
    {
        frameRate=(frameRate+numFrames)/2;
        qInfo() << frameRate;
        numFrames=0;
    }
    
    void MyGraphicsView::drawForeground(QPainter * painter, const QRectF & rect)
    {
        numFrames++;
        //...
    }
    

    http://doc.qt.io/qt-4.8/qelapsedtimer.html

    避免深拷贝

    在对 QT 容器进行迭代时,使用 foreach(const auto& item, items)、const_iterator 或 items.at(i) 代替 items[i],以避免分离。尽可能使用 const 运算符并调用 const 方法。始终尝试通过对其实际大小的良好估计来初始化(reserve())您的向量/数组。 https://www.slideshare.net/qtbynokia/optimizing-performance-in-qtbased-applications/37-Implicit_data_sharing_in_Qt

    场景索引

    对于具有少量项目和/或动态场景(带有动画)的场景,支持 NoIndex,对于具有许多(大部分是静态)项目的场景,支持 BspTreeIndex。 BspTreeIndex 允许在使用 QGraphicsScene::itemAt() 方法时进行快速搜索。

    不同缩放级别的不同绘制算法

    就像在 Qt 40000 芯片示例中一样,您不需要使用相同的详细绘图算法来绘制在屏幕上看起来非常小的东西。您可以为此任务使用 2 个不同的 QPainterPath 缓存对象,或者在我的情况下,拥有 2 个不同的点云向量(一个具有原始向量的简化子集,另一个具有补集)。所以,根据缩放级别,我画一个或两个。另一种选择是打乱你的点云并根据缩放级别仅绘制向量的前 n 个元素。仅最后一项技术就将我的帧速率从 5 fps 提高到 15 fps(在我最初有 100 万点的场景中)。在您的 QGraphicsItem::painter() 中使用类似:

    const qreal lod = option->levelOfDetailFromTransform(painter->worldTransform());
    const int n = qMin(pointCloud.size(), pointCloud.size() * lod/0.08);
    painter->drawPoints(pointCloud.constData(), n);
    

    超大你的 QGraphicsScene::sceneRect()

    如果您不断增加场景矩形的大小,重新索引可能会在短时间内冻结您的应用程序。为避免这种情况,您可以设置固定大小或添加和删除临时矩形以强制场景增加到更大的初始大小:

    auto marginRect = addRect(sceneRect().adjusted(-25000, -25000, 25000, 25000));
    sceneRect(); // hack to force update of scene bounding box
    delete marginRect;
    

    禁用滚动条

    如果在滚动场景时视图闪烁,禁用滚动条可以解决此问题:

    setHorizontalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
    setVerticalScrollBarPolicy( Qt::ScrollBarAlwaysOff );
    

    使用分组将鼠标控制的转换应用于多个项目

    使用 QGraphicsScene::createItemGroup() 进行分组可以避免在转换过程中多次调用 QGraphicsItem::itemChange。仅在创建和销毁组时调用两次。

    比较多个 Qt 版本

    我还没有足够的时间来研究它,但至少在我当前的项目中,Qt 5.6.2(在 Mac OS 上)比 Qt 5.8 快得多。

    【讨论】:

      【解决方案2】:

      我的应用程序虽然不完全是 CAD 程序,但它与 CAD 类似,因为它允许用户在空间中构建各种项目的“蓝图”,并且允许用户添加任意​​数量的项目,并且一些用户的设计可能会变得非常拥挤和精致,同时出现数百或数千个项目。

      视图中的大多数项目或多或少都是静态的(即它们仅在用户单击/拖动它们时才会移动或改变外观,这种情况很少见)。但场景中通常也有一些前景项目,它们不断动画并以 20 fps 的速度移动。

      为了避免定期重新渲染复杂的静态元素,我将所有静态元素预渲染到 QGraphicsView 的后台缓存中,只要它们中的任何一个发生变化,或者缩放/平移/大小时QGraphicsView 的设置发生变化,并排除它们作为正常前景视图重绘过程的一部分进行渲染。

      这样,当QGraphicsView 周围有移动元素以 20fps 运行时,所有非常多且精细的静态对象都通过一次调用绘制(通过QGraphicsScene::drawBackground() 中的代码) drawPixmap() 而不必在算法上单独重新渲染每个项目。然后可以以通常的方式将始终移动的元素绘制在顶部。

      实现这一点包括在QGraphicsView(s) 上调用setOptimizationFlag(IndirectPainting)setCacheMode(CacheBackground),并且在任何静态项的任何方面发生变化时也对它们调用resetCachedContent()(以便缓存背景-图像将尽快重新渲染)。

      唯一棘手的部分是让所有“背景”QGraphicsItems 呈现在 QGraphicsScenedrawBackground() 回调中,并且不呈现在通常的 QGraphicsScene::drawItems() 回调中(通常称为比QGraphicsScene::drawBackground() 更频繁)。

      在我的压力测试中,相对于“vanilla”QGraphicsScene/QGraphicsView 方法,我的程序的稳态 CPU 使用率降低了约 50%(如果我通过调用 @ 使用 OpenGL,则降低了约 80%) 987654337@) 在我的QGraphicsView) 上。

      唯一的缺点(除了增加的代码复杂性)是这种方法依赖于使用QGraphicsView::setOptimizationFlag(IndirectPainting)QGraphicsView::drawItems(),这两者都已被 Qt 或多或少弃用,因此这种方法可能不会继续有效与未来的 Qt 版本。 (不过,它至少在 Qt 5.10.1 中有效;这是我尝试过的最新 Qt 版本)

      一些说明性代码:

      void MyGraphicsScene :: drawBackground(QPainter * p, const QRectF & r)
      {
         if (_isInBackgroundUpdate == false)  // anti-infinite-recursion guard
         {
            QGraphicsScene::drawBackground(p, r);
      
            const QRectF rect = sceneRect();
      
            p->fillRect(rect, backgroundBrush().color());
      
            // Render the scene's static objects as pixels 
            // into the QGraphicsView's view-background-cache
            this->_isInBackgroundUpdate = true;  // anti-infinite-recursion guard
            render(p, sceneRect());
            this->_isInBackgroundUpdate = false;
         }
      }
      
      // overridden to draw only the items appropriate to our current
      // mode (foreground items OR background items but not both!)
      void MyGraphicsScene :: drawItems(QPainter *painter, int numItems, QGraphicsItem *items[], const QStyleOptionGraphicsItem options[], QWidget *widget)
      {
         // Go through the items-list and only keep items that we are supposed to be
         // drawing in this pass (either foreground or background, depending)
         int count = 0;
         for (int i=0; i<numItems; i++)
         {
            const bool isItemBackgroundItem = (_backgroundItemsTable.find(items[i]) != _backgroundItemsTable.end());
            if (isItemBackgroundItem == this->_isInBackgroundUpdates) items[count++] = items[i];
         }
      
         QGraphicsScene::drawItems(painter, count, items, options, widget);
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-08-05
        • 1970-01-01
        • 2016-02-02
        • 2011-06-26
        • 2019-06-19
        • 2014-03-08
        • 2021-03-04
        • 2017-08-22
        相关资源
        最近更新 更多