PDA

View Full Version : Performance issue with complex QGraphicsScene



Nicuvëo
20th January 2011, 13:29
Hi all!

I'm currently working on a game made with Qt and I'm struggling with performance issues in the rendering of the game scene, hence this little question. I've browsed those forums a bit and haven't been able to find a solution to my problem, so here it goes!


Context
The map of our game is a cylinder: exiting on the "right" of the map brings you back at the "left" transparently. The map must therefore be scrollable without the user noticing when the "shift" occurs.

The map features many background elements like regions, rivers, sea... and also foreground features (player tokens).


Implementation
To implement the aforementioned features, I used QGraphicsScene and QGraphicsView. All the elements in the game (such as rivers for instance) are represented by three distinct QGraphicsItems, grouped in a subclass of QGraphicsItemGroup (that reimplements "paint" with an empty method to be totally invisible): the world is therefore represented thrice. When users scroll the map (using shortcuts; scrollbars are hidden), the view automatically translates itself to always be centered on the central view. See image below to have an illustration of what I'm talking about.

5800
(over 9000 hours in MSPAINT!)

We currently have up to a few thousands of QPolygonItem on the map, and a few dozens QPixmapItems on top of them. We use OpenGL rendering (with QGLWidget).


Issue
The issue that we have here is that rendering of the scene is slow. By using a naive call to "clock" I've gathered stats about QGraphicsView::paintEvent. It currently takes around 50ms to render a small test scene on my (powerful) dev machine. This is far too much, as we want to let the user scroll the map smoothly (so we aim at keeping that time below 30ms as much as possible). Callgrind shows that 90% percent of that time is spent in QGraphicsPolygonItem::paint.

The issue here is that as far as I know, with OpenGL rendering the ViewPortUpdateMode option is ignored: scrolling the map means a full rendering at each step. Scaling is even worse (but that might be linked to the fact some items have "cosmetic" (scale-independent size) lines; many things might need to be recomputed).


Tested solutions
Before coming here to ask about this issue, I tried or thought about several solutions, none of which proved to boost the performance of the rendering.

One big item versus many small ones.
Rivers being totally static (not clickable nor interactive in any way), I've tried to use one big subclassed QGraphicsItem to paint them all. That way, in the paint method, I had only one Painter::save, one Painter::restore and one Painter::setBrush for all the rivers, instead of having hundreds of scattered QGraphicsPolygonItems. Rendering time doubled. Idea scrapped.
I suppose it's because Qt is able to sort between displayed items and not displayed items and draw only what's useful (if it's indeed the reason, that's a shame that QGLWidget do not support partial updates; that'd solve our problem here, I guess).

Caching the map in a pixmap.
This seems like a interesting idea, since we have a lot of static elements in the background. Also, as the map is modified only once in a while but scrolled far more often, caching the result seems like a perfect idea.
The problem with that solution is that we have lots of cosmetic items, as mentioned before. This would make each zoom change a bit slow. The other problem is that we have huge variations of zoom levels, and that rendering the whole map at the maximum scale is impossible (unless there's a way to easily cache, crop and draw >2Go bitmaps I haven't heard of ^^).
I've also been unable to paint directly on a QQraphicsView: attempts to create a QPainter in the paintEvent method all failed with a "Widget painting can only begin as a result of a paintEvent" error. And I can't see any other way to circumvent items drawing if I cache the scene with a custom call to QGraphicsView::render.

Not using transparency.
Most of my items use alpha blending; it's nicer that way. :)
I've tried to disable transparency by ensuring all alpha values were set to 1. This didn't change anything, which surprised me. I suppose there is a way to totally deactivate transparency (which prevent Qt from doing the calculations at all), but I haven't been able to find anything relevant yet.

Not having three items for each element
That might seem obvious, as it would cut by a solid three the rendering time. But this is not as easy as it might seem. One of the problems is that most elements are interactive. I can click on them and act with them (also they're raised when clicked). As the picture above shows, I might interact without knowing it with items that are not on the "main map" but on one of the duplicates on the sides. As most of the interaction is custom-made (my custom mousePressEvent method uses "itemsAt" to know what item was clicked), I suppose I COULD wrap all interactions to shift user input, but then how would I manage to draw everything thrice?


Questions
So I'm left with this issue: I can't find a way to cache the map; I can't seem to find a way to draw each item only once and then "copy / paste" the rendered result; I can't find a way to simulate partial rendering.

So, a few questions: are there rendering tweaks I might haven't heard of? Is there a way to speed up polygons rendering, maybe by caching calculations as OpenGL call lists would? Is the use of QGraphicsItemGroup for scattered items a problem as far as cropping go? Is there a way to simulate partial rendering with QGLWidget?

Thank you if you made it that far. ^^
I'm open to all suggestions or leads; thank you in advance for your time.

Stef
20th January 2011, 15:17
At the moment I am dealing with a similar problem. Also using qgraphicsview / scene with rendering done by opengl.

Some tricks that work for me are:

Caching the more expensive drawing items in a pixmap. For this to work I had to
increase the pixmap cachesize in the main routine.

// Item is cached into a pixmap
setCacheMode(QGraphicsItem::DeviceCoordinateCache) ;

QPixmapCache::setCacheLimit(102400);

Caching the background drawing:

_ui->graphicsView->setCacheMode(QGraphicsView::CacheBackground);

Disable indexing because I have many moving items:

//If your scene uses many animations and you are experiencing slowness,
//you can disable indexing by calling setItemIndexMethod(NoIndex).
_scene->setItemIndexMethod(QGraphicsScene::NoIndex);

Scaling down the number of samples used for anti-aliasing:

QGLFormat fmt;
fmt.setSampleBuffers(true);
fmt.setSamples(2);
_ui->graphicsView->setViewport(new QGLWidget(fmt));

These tricks give some advantage but I am still looking for better tricks to have a satisfactory result...

Nicuvëo
20th January 2011, 15:41
Caching the more expensive drawing items in a pixmap. For this to work I had to increase the pixmap cachesize in the main routine.

I tried that already; the problem is that I have lots of small items instead of a few expensive ones.


Caching the background drawing.

Already tried, also... but my background is currently nothing more than a pixmap, so nothing gained here either. ^^


Disable indexing because I have many moving items.

My items aren't moving (yet), but I'll remember that tip when I try to introduce such features.


Scaling down the number of samples used for anti-aliasing.

Aaah, this one is useful. :)
It does indeed give a bit of a boost to the rendering speed, without looking as ugly as no anti-aliasing at all.


These tricks give some advantage but I am still looking for better tricks to have a satisfactory result...

It is a first step in the right direction. Thanks a lot!

wysota
20th January 2011, 15:42
I can suggest a couple of things to try:
1. enable caching for items (as suggested by my preposter but with ItemCoordinateCache)
2. set ItemHasNoContents flag on all items that paint nothing
3. set "interactive" property of the view to false if you don't need your scene to be clickable
4. make shapes as simple as you can, if you scale use the level of detail information to avoid painting things that can't be noticed by the user
5. make use of drawBackground() and drawForeground() instead of having items that cover the whole scene
6. avoid clipping
7. try using the raster engine instead of OpenGL.

Nicuvëo
20th January 2011, 16:22
1. enable caching for items (as suggested by my preposter but with ItemCoordinateCache)

As I answered, I don't have any expensive item, but lots of small ones. But I'll try on some of them.


2. set ItemHasNoContents flag on all items that paint nothing

Thanks for that! It doesn't seem to do a lot, though, as my empty objects reimplement both paint and boundingRect as empty methods.


3. set "interactive" property of the view to false if you don't need your scene to be clickable

Done that! Doesn't seem to have a huge impact on rendering speed, but worth setting anyway.


4. make shapes as simple as you can, if you scale use the level of detail information to avoid painting things that can't be noticed by the user

Shapes are already simple, but I can indeed try to simplify things when zoomed out... I'll try to see what I can do!


5. make use of drawBackground() and drawForeground() instead of having items that cover the whole scene

Hmmmmm... The thing is that most of my scene is made of distinct "interactive" elements... But for the static elements that might be a very good idea! Ill try that.


6. avoid clipping

I'm sorry to ask, but could you be more specific? Do you mean that I should avoid items on top of others?


7. try using the raster engine instead of OpenGL.

Three whole seconds to render the scene. ^^"


Thanks for your answer anyway, I'm going to try to move the static elements in drawBackground and I'll let you know how that improves the rendering speed.

Added after 17 minutes:

So... I tried moving some static elements of the map (the aforementioned rivers) in DrawBackground. So instead of having ~1000 QGraphicsPolygonItems, I have ~1000 painter->drawPolygon in WorldView::DrawBackgorund (WorldView being my subclass of QGraphicsView). Strangely enough it is now slower than it was before...
Another strange thing: caching background does not work. It is WAY slower, and it glitches (a the new part of the screen seems to be redrawn on the previous, or something like that).

Would it do any difference if I was drawing in QGraphicsScene::DrawBackground instead of QGraphicsView::DrawBackground?

wysota
20th January 2011, 16:58
As I answered, I don't have any expensive item, but lots of small ones. But I'll try on some of them.
Caching for small items is more important than caching for big items as the cost of preparing painting is relatively bigger for simpler items (compared to painting them).


Thanks for that! It doesn't seem to do a lot, though, as my empty objects reimplement both paint and boundingRect as empty methods.
It does a lot. It makes GraphicsView skip preparing transformation matrices for your empty items.


Done that! Doesn't seem to have a huge impact on rendering speed, but worth setting anyway.
All those little things added together can have a significant impact.


Shapes are already simple, but I can indeed try to simplify things when zoomed out... I'll try to see what I can do!
This also involves things such as antialiasing and polygons.


I'm sorry to ask, but could you be more specific? Do you mean that I should avoid items on top of others?
If you're asking about it then you're not doing clipping.



Three whole seconds to render the scene. ^^"
How did you enable the raster engine?



So... I tried moving some static elements of the map (the aforementioned rivers) in DrawBackground. So instead of having ~1000 QGraphicsPolygonItems, I have ~1000 painter->drawPolygon in WorldView::DrawBackgorund (WorldView being my subclass of QGraphicsView). Strangely enough it is now slower than it was before...
It only makes sense to do so if you cache the background and your background doesn't change in every frame. If it does, you get two blits instead of one on every frame (first to the cache and then the cache blitted to the view).

There are more things you can do but they are really case-specific. If what takes the most time is implementation of paint() for some of your items then enabling caching for items should improve things a lot. Especially for a GL viewport.

If you show us some of your code, I might be able to suggest some more optimizations.

Nicuvëo
20th January 2011, 17:37
If you're asking about it then you're not doing clipping.

Thing is, I almost never reimplement paint method, as I mostly have basic primitives. I was thinking that Qt would automatically do the needed clipping... I'll try when drawing rivers in drawBackground, however.


How did you enable the raster engine?

By commenting out my line setting a QGLWidget as the viewport of my QGraphicsView. Was I wrong when supposing the raster engine was the default one?


It only makes sense to do so if you cache the background and your background doesn't change in every frame. If it does, you get two blits instead of one on every frame (first to the cache and then the cache blitted to the view).

Okay, I understand. As what triggers a redraw is almost always a change in the view (scale or centerOn being called), that's no use to me. :(


There are more things you can do but they are really case-specific. If what takes the most time is implementation of paint() for some of your items then enabling caching for items should improve things a lot. Especially for a GL viewport.

Most of my caching attemps ended up with thousands of X errors such as GLXBadPixmap and X using my whole CPU and my whole RAM, before crashing in a rebooting agony. It seems that enabling the cache for so many items at a time wasn't a very bright idea; I'll have to increase the cache size, methinks.


If you show us some of your code, I might be able to suggest some more optimizations.

I'll try to make a small working exemple tomorrow.

Thanks a lot for your help, anyway!

Added after 21 minutes:

EDIT: my mistake, sorry...

wysota
20th January 2011, 17:47
Thing is, I almost never reimplement paint method, as I mostly have basic primitives. I was thinking that Qt would automatically do the needed clipping... I'll try when drawing rivers in drawBackground, however.
The thing is you don't want to do clipping because it slows things down.


By commenting out my line setting a QGLWidget as the viewport of my QGraphicsView. Was I wrong when supposing the raster engine was the default one?
It depends on the platform. It is the default on Windows. On other platforms you need to run your app with "-graphicssystem render" commandline switch.



Okay, I understand. As what triggers a redraw is almost always a change in the view (scale or centerOn being called), that's no use to me. :(
Background is done is scene coordinates so moving the view around the scene doesn't invalidate the background cache (I think). Scaling probably does but I'm not sure.


Most of my caching attemps ended up with thousands of X errors such as GLXBadPixmap and X using my whole CPU and my whole RAM, before crashing in a rebooting agony. It seems that enabling the cache for so many items at a time wasn't a very bright idea; I'll have to increase the cache size, methinks.
Use ItemCoordinateCache and not DeviceCoordinateCache. Also calculate how much memory the cache will take and compare it to the amount of memory available for your graphics card. An item size of 64x64 pixels will take about 4kB of memory. Note that for ItemCoordinateCache scaling causes the cache to be invalidated. On the other hand DeviceCoordinateCache provides worse quality in exchange for smaller memory footprint.

Stef
27th January 2011, 14:11
I was wondering do you already have made some progress on this issue and useful tips to share? I am very interested in every aspect to make the QGraphicsView / Scene faster.

Nicuvëo
1st February 2011, 12:56
I haven't found the time to work again on that issue. I'll do (and keep this thread updated) as soon as I can.

jano_alex_es
20th April 2011, 08:03
I suppose it's because Qt is able to sort between displayed items and not displayed items and draw only what's useful (if it's indeed the reason, that's a shame that QGLWidget do not support partial updates; that'd solve our problem here, I guess).

If you are using a QGLWidget to render


setViewport(new QGLWidget)

don't forget to set "fullviewportupdate".



By commenting out my line setting a QGLWidget as the viewport of my QGraphicsView. Was I wrong when supposing the raster engine was the default one?
It depends on the platform, but in order to be sure, try:


int main(int argc, char *argv[])
{
QApplication::setGraphicsSystem("raster");
QApplication a(argc, argv);

Its a HUGE performance improvement. At least in my case.


Background is done is scene coordinates so moving the view around the scene doesn't invalidate the background cache (I think).

I though so... but it does. Caching the background is a good option if the viewport is static. When the viewport moves the big pixmap is loaded again on cache, taking too much time.


In my opinion this video is a must if you are trying to develop a videogame with the QGraphicsView system. And if you want to have it for a mobile device (and OVI store, where Qt4.7 with QGLWidget for Symbian is still not accepted)
http://qt.nokia.com/developer/learning/online/talks/developerdays2010/tech-talks/qt-graphics-view-in-depth/

Those are the improvements I'm using, so far. (http://agurines.blogspot.com/2011/02/qgraphicsview-performance.html)