I have a QGraphicsSvgItem that I need to rotate a certain amount based on an external variable coming into my application. Currently I'm directly setting the rotation property of the item, but this appears to significantly drop my frame rate. (117.0 fps -> 98.0 fps) is this normal? Is this the correct way to animate a QGraphicsItem? I realize that the framerate sounds plenty high, but I'm concerned that as more items in the view are animated, that the performance will become a problem.
The code below is being called via a QTimer timeout signal at an interval of 0.
Note that I have also tried using a QGraphicsItem with a custom QPainterPath to draw the item rather than an SVG and it made no noticeable performance difference.
qreal diff = m_rope_length_ft[0] - m_rope_length_ft_prev[0];
qreal angle = diff * 5.0;
m_rope_reel[0]->setRotation(m_rope_reel[0]->rotation() + angle);
m_rope_reel[1]->setRotation(m_rope_reel[1]->rotation() + angle);
m_rope_reel[0]->update();
m_rope_reel[1]->update();
A more proper way to animate in Qt is to use QPropertyAnimation. It requires that:
your graphics item is inherited from both QObject and QGraphicsSvgItem;
rotation is registered as property with Q_PROPERTY (getters and setters already exist so there is no need to implement them).
Also you should try to tune graphics item's cache mode. ItemCoordinateCache seems to be suitable for your case. There is also view cache mode, but it's hard to say which is better in your case.
Note that performance optimization should be done using average amount of items your program should be able to process. Optimizing a single item rendering may be pointless.
Related
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.
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);
}
I have a QAbstractListModel with custom objects as items. Each object has a QImage that is loaded from a database. I use ListView in QML to visualize it but I do not see any mean to represent QImage in the delegate. Image primitive seems to accept only URLs.
Is the only way for me to show QImages is to create a QQuickImageProvider with some custom system of a URL per element (looks like a total overkill)?
I think QQuickImageProvider is the proper way.
Also, I think you can use the word 'overkill' if you know exactly how the Qt internals work. Otherwise it's just guessing.
AFAIK there is a complex caching system of images (and other data) underneath, so once an image pixmap is loaded (and doesn't change) data retrieval is immediate. So no overkill at all, since in any case at some point you need to load those QImage, but just once.
I believe a QQuickImageProvider provides pointers to the cached data, and not the whole rasterized data every time. Moreover blitting operations are nowadays performed with hardware accelerations, so it's a single operation taking a fraction of millisecond.
In other words you end up having:
give me image with url "image://xyz"
Qt looks up in the cache and returns the data pointer or performs a full load of the image if not found
the QML renderer passes the data array to OpenGL
one single blit operation (microseconds) and you have it on screen
A QML ShaderEffect will bind a QImage to a GLSL Sampler2D. See the list of "how properties are mapped to GLSL uniform variables" in the ShaderEffect docs. Needs a few lines of GLSL writing in the ShaderEffect to pass pixels through, (will update this post with an example sometime, when I have access to my code), but the result is a fast, QSG-friendly all-QML element which can render QImages.
I am trying to implemente a CoverFlow like effect using a QGLWidget, the problem is the texture loading process.
I have a worker (QThread) for loading images from disk, and the main thread checks for new loaded images, if it finds any then uses bindTexture for loading them into QGLContext. While the texture is being bound, the main thread is blocked, so I have a fps drop.
What is the right way to do this?
I have found that the default behaviour of bindTexture in Qt4 is extremelly slow:
bindTexture(image,target,format,LinearFilteringBindOption | InvertedYBindOption | MipmapBindOption)
using only the LinearFilteringBindOption in the binding options speeds up the things a lot, this is my current call:
bindTexture(image, GL_TEXTURE_2D,GL_RGBA,QGLContext::LinearFilteringBindOption);
more info here : load time for a 3800x2850 bmp file reduced from 2 seconds to 34 milliseconds
Of course, if you need mipmapping, this is not the solution. In this case, I think that the way to go is Pixel Buffer Objects.
Binding in the main thread (single QGLWidget solution):
decide on maximum texture size. You could decide it based on maximum possible widget size for example. Say you know that the widget can be at most (approximately) 800x600 pixels and the largest cover visible has 30 pixels margins up and down and 1:2 aspect ratio -> 600-2*30 = 540 -> maximum size of the cover is 270x540, e.g. stored in m_maxCoverSize.
scale the incoming images to that size in the loader thread. It doesn't make sense to bind larger textures and the larger it is, the longer it'll take to upload to the graphics card. Use QImage::scaled(m_maxCoverSize, Qt::KeepAspectRatio) to scale loaded image and pass it to the main thread.
limit the number of textures or better time spent binding them per frame. I.e. remember the time at which you started binding textures (e.g. QTime bindStartTime;) and after binding each texture do:
if (bindStartTime.elapsed() > BIND_TIME_LIMIT)
break;
BIND_TIME_LIMIT would depend on frame rate you want to keep. But of course if binding each one texture takes much longer than BIND_TIME_LIMIT you haven't solved anything.
You might still experience framerate drop while loading images though on slower machines / graphics cards. The rest of the code should be prepared to live with it (e.g. use actual time to drive animation).
Alternative solution is to bind in a separate thread (using a second invisible QGLWidget, see documentation):
2. Texture uploading in a thread.
Doing texture uploads in a thread may be very useful for applications handling large amounts of images that needs to be displayed, like for instance a photo gallery application. This is supported in Qt through the existing bindTexture() API. A simple way of doing this is to create two sharing QGLWidgets. One is made current in the main GUI thread, while the other is made current in the texture upload thread. The widget in the uploading thread is never shown, it is only used for sharing textures with the main thread. For each texture that is bound via bindTexture(), notify the main thread so that it can start using the texture.
I am working on a multi -segment download manager. I want to display the segmentation procedure. QGraphicsScene works fine but , unfortunately it slows the download. Is there any better option, other than using QProgressBars.
I am using QNetworkAccessManager to download files. If I connect the downloadProgress signal of QNetworkManager object to a slot of Main Gui Thread which draws on QGraphicsView, the download speed decreases even upto 10 times in some cases
// a custom progress bar
void Download::showGProgress(int num, float prgrss) //slot
{
prgrss=prgrss/100;
x_coord=(ui->graphicsView_2->width()-3)*prgrss;
for(float b=0;b<=x_coord;b=b+0.5)
{
progress.addRect(0,0,x_coord,y_coord);
}
}
QNetworkAccessManager is not threaded. It is asynchronous, using the current threads eventloop. It is the HTTP requests which it create that are the threaded aspect.
This would explain why anything you do in your main thread could theoretically slow down the operations of the download. Though not necessarily the underlying threaded download itself, but rather the signaling response time that would allow you to have fast feedback about it.
What you should probably do is create your own QThread subclass, and create the QNetworkAccessManager in the run() method. And then create a QEventLoop in the thread and call exec()
In a nutshell, you need to create your own Threaded QNetworkAccessManager.
create your own widget to do what you would like
this is easier than it sounds.
Make a class that subclasses from a QWidget. And in this widget make a Horizontal Sizer that contains 100 Qlabels (store the QLabels in a vector). Give it slots to 'update' the current progress by setting the background color of each QLabel to a different color. This should be fairly easy to do progressively, meaning you store the current 'percentage' as a member variable and then only adjust the fields that are necessary to get to the percentage that you're looking for (This will eliminate some flickering if you were to do it from scratch every time).
Add functions that will switch the sizer to a vertical one instead of a horizontal one to make it even more customizable.
This allows you to get creative in the what you can do for the progress bar as each element could be a different picture, or a different color, or whatever you would like.
Did you try QProgressBar? Maybe you can write a subclass of it to handle your own properties.
Is there a fast way to get the viewport coordinates of QGraphicsItems in a QGraphicsView? The only way I can think of is to call QGraphicsView::items(), and then QGraphicsItem::pos() followed by QGraphicsView::mapFromScene.
I must be missing something, though, because items are already converted to viewport coordinates to position them correctly on the QGraphicsView, so converting it to viewport coordinates again with mapFromScene seems inefficient--especially because in my case this is occurring often and for many items. Is there a more direct approach?
Probably not. A QGraphicScene can be rendered by more than one QGraphicsView simultaneously. It makes no sense to keep only one set of view port coordinates.
Also. All operation between QGraphicsItems are calculated in scene coordinate directly. Events from view port are convert to scene before processing. Working off view port which is only integer-based can also loose precision. A QGraphicsView is only a representation of the mathematical model of a scene. It's not the actual model.
Maybe you can ask a more specific question on what exactly you are trying to accomplish. There may be a better way to do it in scene coordinate.