How to optimize the performance of QGraphicsView?

I am developing a CAD application using Qt 5.6.2 which is required to run on cheap computers at the same time as it needs to handle thousands of items in the same scene. Because of this, I had to do a lot of experimentation to get the best performance.

I decided to create this post to help others, as well as myself if other people also contribute further to the optimization tips.

My text still works and I can update it if I open up better methods (or that I said something really stupid).

+3


source to share


2 answers


Disable scene interaction

Event handling is a good part of the CPU utilization with the QGraphicsView engine. Each time you move the mouse, the view asks for a scene for items under the mouse, which calls the QGraphicsItem :: shape () method to detect intersections. This happens even with disabled items. So, if you don't need your scene to interact with mouse events, you can set QGraphicsView :: setIntenteractive (false). In my case, I had two modes in my tool (measure and move / rotate) where the scene was mostly static and all editing was done by the QGraphicsView. By doing this, I was able to increase the frame rate by 30%, unfortunately ViewportAnchor :: AnchorUnderMouse stopped working (one hack to fix this to re-enable interaction and override QGraphicsView ::mouseMoveEvent (QMouseEvent e) to artificially click one of the mouse buttons using a new e-based QMouseEvent).

Reusing QPainterPaths

Load the QPainterPath into your QGraphicsItem object. Building and settling can be very slow. In my case, it took 6 seconds to read the file because I converted a 6000-point point cloud to a QPainterPath with multiple rectangles. You don't want to do this more than once.

Simplify QGraphicsItem :: shape ()

This method is called multiple times during mouse events, even if the item is not enabled. Try to make it as effective as possible. Sometimes even caching QPainterPath is not enough, as the path crossing algorithm performed by the scene can be very slow for complex shapes. In my case, I was returning a shape with about 6000 rectangles and it was pretty slow. After downsampling the point cloud, I was able to reduce the number of rectangles to 1000, which greatly improved performance, but still not perfect as shape () was still called even when the item was disabled. Because of this, I decided to keep the original QGraphicsItem: shape () (which returns the rectangle of the rectangle) and just returns a more complex cached shape when the item is included.This improved the frame rate on mouse movement by nearly 40%, but I still think this is a hack and will update this post if I come up with a better solution. Regardless, in my tests I have not had any problems as long as I keep my bounding box intact. If not, you need to call prepareGeometryChange () and then update the bounding box and form caching elsewhere.

Test Both: raster and OpenGL engines

I expected OpenGL to always be better than raster, and it's probably true if all you want is to reduce CPU usage for obvious reasons. However, if you want to increase the number of frames per second, especially on cheap / old computers, it is worth trying to test the bitmap too (in the default QGraphicsView). In my tests, the new QOpenGLWidget was slightly faster than the old QGLWidget, but the FPS was almost 20% slower than using Raster. Of course, this can be application specific and the result can be different depending on what you are rendering.

Use FullViewportUpdate with OpenGL and prefer another method to update the partial view with a raster (requires a stricter bounding box to support items).

Try disabling / enabling VSync to see which one works best for you: QSurfaceFormat :: defaultFormat (). setSwapInterval (0 or 1). Turning it on can lower frame rates, and turning it off can cause tearing. https://www.khronos.org/opengl/wiki/Swap_Interval

QGraphicsItems cache complex

If your QGraphicsItem :: paint operation is too complex and mostly static, try enabling caching. Use DeviceCoordinateCache if you are not applying transformation (such as rotation) to items or ItemCoordinateCache otherwise. Avoid calling QGraphicsItem :: update () very often, or it may even be slower than no caching. If you need to change something in your element, there are two options: draw it in a child or use QGraphicsView :: drawForeground ().

QPainter's batch-like drawing operations

Prefer drawLines over drawLine calls multiple times; Pay points for a draw. Use QVarLengthArray (uses a stack, so might be faster) or QVector (uses a heap) as container. Avoid changing the brush very often (I suspect this is more important when using OpenGL). Also QPoint can be faster and smaller than QPointF.

Prefer to paint using cosmetic lines, avoiding transparency and anti-aliasing

Anti-aliasing can be turned off, especially if all you're drawing is horizontal, vertical, or 45-degree lines (they actually look better), or you're using a retina screen.

Search for hot spots

Bottlenecks can occur in surprising locations. Use a profiler or other methods like elapsed timer, qDebug counter or FPS (I put it in my QGraphicsView :: drawForeground) to help find them. Don't make your code ugly by trying to optimize things that you are not sure whether they are hotspots or not. Example of an FPS counter (try to keep it above 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;
}

      



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

Avoid deep copy

Use foreach (const auto & item, items), const_iterator or items.at (i) instead of [i] items when iterating over QT containers to avoid detaching. Use the constant operator and const calling methods as much as possible. Always try to initialize (reserve ()) your vectors / arrays with a good estimate of its actual size. https://www.slideshare.net/qtbynokia/optimizing-performance-in-qtbased-applications/37-Implicit_data_sharing_in_Qt

Scene indexing

Thanks to NoIndex for scenes with multiple elements and / or dynamic scenes (with animation) and BspTreeIndex for scenes with many (mostly static) elements. BspTreeIndex provides fast lookups when using the QGraphicsScene :: itemAt () method.

Different drawing algorithms for different zoom levels

As with the Qt 40,000 Chips example, you don't need to use the same verbose drawing algorithm to draw things that look very small on the screen. You can use 2 different QPainterPath caches for this task, or, as in my case, have 2 different cloud point vectors (one with a simplified subset of the original vector and the other with padding). Therefore, depending on the zoom level, I draw one or both. Another option is to shuffle your point cloud and draw only the first n elements of the vector according to the zoom level. This latter technique alone increased the frame rate from 5 to 15 frames per second (in the scene where I originally had 1 million points). Use something like this in your 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);

      

Oversized QGraphicsScene :: sceneRect ()

If you continually increase the size of the scene rectangle, re-indexing can freeze your application for a short period of time. To avoid this, you can set a fixed size or add and remove a temporary rectangle to force the scene to grow to a larger size:

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

      

Disable Scroolbars

If the image flickers as you scroll through the scene, disabling scroolbars can fix it:

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

      

Apply mouse transformations to multiple elements using grouping

Grouping with QGraphicsScene :: createItemGroup () avoids calling QGraphicsItem :: itemChange multiple times during conversion. It is only called twice when the group is created and destroyed.

Compare multiple versions of Qt

I didn't have enough time to research it, but in my current project at least Qt 5.6.2 (on Mac OS) was much faster than Qt 5.8.

+9


source


My application, although not quite a CAD program, is similar to CAD in that it allows the user to create a "blueprint" of various elements in space, and the user is allowed to add as many elements as they like, and some user projects can get quite crowded and thoughtful at the same time hundreds or thousands of items will appear.

Most of the elements in the view will be more or less static (i.e. they only move or change appearance when the user clicks / drags them, which is rare). But there are often multiple foreground subjects in a scene that are constantly animating and moving at 20 frames per second.

To avoid re-rendering complex static elements on a regular basis, I pre-render all static elements in the background cache QGraphicsView

whenever they change, or when the zoom / pan / size settings change QGraphicsView

and exclude them from rendering as part of the normal foreground redrawing process.

Thus, when the moving elements are moving around QGraphicsView

at 20 frames per second, all objects with very numerous and complex static objects are drawn (by the c code QGraphicsScene::drawBackground()

) in a single call drawPixmap()

instead of algorithmically redrawing each element individually. Always moving elements can then be drawn on top in the usual way.

Implementing this involves calling setOptimizationFlag(IndirectPainting)

and setCacheMode(CacheBackground)

on QGraphicsView

(s), as well as calling resetCachedContent()

on them whenever any aspect of any of the static elements changes (so that the cached background -image will be re-rendered as soon as possible).



The only tricky part is getting all the "background" QGraphicsItems

to render inside the callback QGraphicsScene

drawBackground()

, and also not rendering it inside the normal callback QGraphicsScene::drawItems()

(which is usually called much more often than QGraphicsScene::drawBackground()

).

In my stress test, this reduces the CPU software load by about 50% compared to the "vanilla" QGraphicsScene

/ approach QGraphicsView

(and about 80% if I use OpenGL through a call setViewport(new QOpenGLWidget

) on mine QGraphicsView

).

The only drawback (other than the complexities of the added code) is that this approach is based on using QGraphicsView::setOptimizationFlag(IndirectPainting)

and QGraphicsView::drawItems()

, both of which are more or less deprecated by Qt, so this approach cannot continue with future versions of Qt. (It works at least up to Qt 5.10.1, although the latest version of Qt I tried) with

Some illustrative codes:

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 static objects as pixels 
      // into the QGraphicsView 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);
}

      

0


source







All Articles