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).
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 Scroolbars
If the view is flickering when you scrool the scene, disabling the scroolbars 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With