How to optimize QGraphicsView's performance?
Asked Answered
L

2

17

I'm developing a CAD application using Qt 5.6.2 that is required to run in cheap computers at the same time that it needs to handle thousands of items in the same scene. Because of that, I had to make a lot of experimentations in order to get the best performance.

I decided to create this post to help others and also myself, as long as other people also contribute with more optimization tips.

My text is still a work in progress and I may update it if I discover better techniques (or that I said something really stupid).

Leupold answered 6/5, 2017 at 23:24 Comment(0)
L
47

Disable Scene Interaction

Event handling is responsible by a good part of the CPU usage by the QGraphicsView engine. At each mouse movement the view asks the scene for the items under the mouse, which calls the QGraphicsItem::shape() method to detect intersections. It 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 (measurement and move/rotate) where the scene was basically static and all edit operations were executed by QGraphicsView instead. By doing that, I was able to increase the frame rate in 30%, unfortunately ViewportAnchor::AnchorUnderMouse stopped working.

Reuse Your QPainterPaths

Cache your QPainterPaths inside your QGraphicsItem object. Constructing and populating it can be very slow. In my case it was taking 6 seconds to read a file just because I was converting a point-cloud with 6000 points into a QPainterPath with multiple rectangles. You won't want to do it more than once. Also, in Qt 5.13 it is now possible to reserve the QPainterPaths 's internal vector size, avoiding multiple copies as it grows.

Simplify your QGraphicsItem::shape()

This method is called multiple times during mouse events, even if the item is not enabled. Try to make it as efficient as possible. Sometimes, even caching the QPainterPath is not enough since the path intersection algorithm, executed by the scene, can be very slow for complex shapes. In my case I was returning a shape with around 6000 rectangles and it was pretty slow. After downsampling the point-cloud I was able to reduce the number of rectangles to around 1000, that improved the performance significantly but still wasn't ideal since shape() was still being called even when the item was disabled. Because of that I decided to maintain the original QGraphicsItem:shape() (which returns the bounding box rectangle) and just return the more complex cached shape when the item is enabled. It improved the frame-rate when moving the mouse in almost 40% but I still think it is a hack and will update this post if I come up with a better solution. Despite that, in my tests I didn't have any issue as long as I keep its bounding box untouched. If it is not the case, you'll have to call prepareGeometryChange() and then update the bounding box and the shape caches elsewhere.

Test Both: Raster and OpenGL engines

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

Use FullViewportUpdate with OpenGL and prefer other partial viewport update method with raster (requires a more strict bounding rectangle maintaining of the items though).

Try to disable/enable VSync to see which one works better for you: QSurfaceFormat::defaultFormat().setSwapInterval(0 or 1). Enabling can reduce the frame rate and disabling can cause "tearing". https://www.khronos.org/opengl/wiki/Swap_Interval

Cache Complex QGraphicsItems

If your QGraphicsItem::paint operation is too complex and at the same type mostly static, try to enable caching. Use DeviceCoordinateCache if you are not applying transformation (like rotation) to the items or ItemCoordinateCache otherwise. Avoid call QGraphicsItem::update() very often, or it can be even slower than without caching. If you need to change something in your item, two options are: to draw it in a child, or use QGraphicsView::drawForeground().

Group Similar QPainter Drawing Operations

Prefer drawLines over calling drawLine multiple times; favor drawPoints over drawPoint. Use QVarLengthArray (uses stack, so can be faster) or QVector (uses heap) as container. Avoid change the brush very often (I suspect it is more important when using OpenGL). Also QPoint can be faster and is smaller than QPointF.

Prefer Drawing Using Cosmetic Lines, Avoiding Transparency and Antialiasing

Antialiasing can be disabled, especially if all you are drawing are horizontal, vertical or 45deg lines (they actually look better this way) or you are using a "retina" display.

Search for Hotspots

Bottlenecks may occur in surprising places. Use a profiler (in macOS I use Instruments/Time Profiler) or other methods like elapsed timer, qDebug or FPS counter (I put it in my QGraphicsView::drawForeground) to help locate them. Don't make your code ugly trying to optimize things you are not sure if they are hotspots or not. Example of a 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;
}

void MyGraphicsView::drawForeground(QPainter * painter, const QRectF & rect)
{
    numFrames++;
    //...
}

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 items[i], when iterating over QT containers, to avoid detachment. Use const operator and call const methods as much as possible. Always try to initialize (reserve() ) your vectors/arrays with a good estimation of its actual size. https://www.slideshare.net/qtbynokia/optimizing-performance-in-qtbased-applications/37-Implicit_data_sharing_in_Qt

Scene Indexing

Favor NoIndex for scenes with few items and/or dynamic scenes (with animations), and BspTreeIndex for scenes with many (mostly static) items. BspTreeIndex allows fast searching when using QGraphicsScene::itemAt() method.

Different Paint Algorithms for Different Zoom Levels

As in the Qt 40000 Chips example, you don't need to use the same detailed drawing algorithm to draw things that will look very small at the screen. You can use 2 different QPainterPath cached objects for this task, or as in my case, to have 2 different point-cloud vectors (one with a simplified subset of the original vector, and another with the complement). So, depending on the zoom level, I draw one or both. Another option is to shuffle your point-cloud and draw just the n first elements of the vector, according to the zoom level. This last technique alone increased my frame rate from 5 to 15fps (in a scene where I had originally 1 million points). Use in your QGraphicsItem::painter() something like:

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

Oversize Your QGraphicsScene::sceneRect()

If you are constantly increasing your scene rectangle in size, reindexing can freeze your application for a short period of time. To avoid that, you can set a fixed size or add and remove a temporary rectangle to force the scene to increase to a bigger initial size:

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

Disable the Scrollbars

If the view is flickering when you scroll the scene, disabling the scrollbars can fix it:

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

Apply Mouse-controlled Transformations to Multiple Items Using Grouping

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

Compare multiple Qt versions

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

Leupold answered 6/5, 2017 at 23:24 Comment(0)
I
5

My application, while not exactly a CAD program, is CAD-like in that it allows the user to construct a "blueprint" of various items in a space, and the user is allowed to add as many items as he likes, and some users' designs can get quite crowded and elaborate, with hundreds or thousands of items present at once.

Most of the items in the view are going to be more or less static (i.e. they move or change appearance only when the user clicks/drags on them, which is rarely). But there are often also a few foreground items in the scene that are constantly animated and moving around at 20fps.

To avoid having to re-render complex static elements on a regular basis, I pre-render all the static elements into the QGraphicsView's background-cache whenever any of them change, or whenever the zoom/pan/size settings of the QGraphicsView change, and exclude them from being rendered as part of the normal foreground-view-redrawing process.

That way, when there are moving elements running around the QGraphicsView at 20fps, all of the very-numerous-and-elaborate-static-objects are drawn (by the code in QGraphicsScene::drawBackground()) via a single call to drawPixmap() rather than having to algorithmically re-render each item individually. The always-moving elements can then be drawn on top in the usual fashion.

Implementing this involves calling setOptimizationFlag(IndirectPainting) and setCacheMode(CacheBackground) on the QGraphicsView(s), and also calling resetCachedContent() on them whenever any aspect of the any of the static-items changes (so that the cached-background-image will be re-rendered ASAP).

The only tricky part is getting all of the "background" QGraphicsItems to render inside the QGraphicsScene's drawBackground() callback, and also to not render inside the usual QGraphicsScene::drawItems() callback (which is typically called much more often than QGraphicsScene::drawBackground()).

In my stress-test this reduces my program's steady-state CPU usage by about 50% relative to the "vanilla" QGraphicsScene/QGraphicsView approach (and by about 80% if I'm using OpenGL via calling setViewport(new QOpenGLWidget) on my QGraphicsView).

The only downside (other than added code complexity) is that this approach relies on using QGraphicsView::setOptimizationFlag(IndirectPainting) and QGraphicsView::drawItems(), both of which have been more-or-less deprecated by Qt, so this approach might not continue to work with future Qt versions. (It does work at least as far as Qt 5.10.1 though; that's the latest Qt version I've tried it with)

Some illustrative code:

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);
}
Irra answered 16/5, 2018 at 2:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.