Canvases update: best practice and advices
-
Hi all,
I'm developing an application which draws a series of 12 line graphs. The maximum number of points shown in each graph is equal to 300, i.e. I have at most about 3600 points showing in my GUI at the same time.
I've investigated the usage of the canvas element as a solution to draw the points but I'm not sure that it can suite my needs. I've also come across several posts (here and on stackoverflow) which suggest to switch to other solutions (with better performances?) such as QPaintedItem or QQuickItem. Finally, I've also some doubts about the different options available for canvas items and how do they affect performances. Hence I'm sharing here my thoughts and my ideas, hoping to have a feedback from you, guys. Thanks in advance and sorry for the long post!! :)
As said, the current naive implementation of the graphs is based on canvas elements. Behind each canvas is positioned a background image, i.e. the background grid of the graph is NOT painted by the canvas. Data come from a background thread (actually a pipeline of background threads); around 125 values per second per canvas are generated (1500 values overall). The thread emits a signal with the new data available and the signal is captured by each canvas. The array of floats is saved in a variant property (one for each canvas) and the paint code is called to "consume" the newly available data. The paint code should "filter" data belonging to the current canvas as the data chunk contains the points for ALL the canvases.
Here is the code for the canvas item:
@
Item {
id: chart
width: element.width
height: element.height
property string name;
property variant values; // <--- stores data!!
property bool run;
property int interval;
property int offset: 0function paintData() { canvas.counter = values.length / 8; canvas.markDirty(Qt.rect(canvas.lastX, 35, canvas.counter, 160)) // <--- rectangular dirty area is smaller!! } Item { id: element x: 0 y: 0 width: 310 height: 176 anchors.centerIn: parent Connections { target: no // <-- thread object onReadData: { // <-- signal readData(QList<float> data) values = data; // <-- float values in the signature of the signal! paintData();
}
}Image {
id: background
width: 310
height: 160
source: "qrc:images/backTile.png"
fillMode: Image.Tile
}Canvas { id: canvas y: 0 width: 310 height: 160 anchors.horizontalCenter: parent.horizontalCenter contextType: "2d" renderStrategy: Canvas.Cooperative renderTarget: Canvas.FramebufferObject canvasSize: Qt.size(310, 160) tileSize: Qt.size(10, 160) antialiasing: false property real lastX: 0 property real lastY: 95 property int dataIndex: 0 property int counter: 0 property int arrayIndex: 0 x: 0 onPaint: { context.beginPath(); context.moveTo(lastX, lastY); for(arrayIndex = offset; arrayIndex < counter; arrayIndex+=8) // <--- take 1 value every 8 { context.lineTo(++lastX, values[arrayIndex]); if(lastX == 310) { lastX = 0; context.clearRect (0, 0, width, height); < } } // // stroke path context.stroke(); if(lastX == 310) { lastX = 0; context.clearRect (0, 0, width, height); } } } }
}
@This solution works perfectly fine on PCs but fails to reach the required perfomances on tablets (tested on a Galaxy tab 2). In particular the graphs are rendered at 70% of the aspected speed. My goal is obviously to match the required performances ALSO on tablets with an hardware similar to galaxy tablets.
Now, question time!!
- Canvas options(1): for what concerns "renderTarget" and "renderStrategy" what combination can best suite my needs? Probably it is me but docs does not help to shed a light on which combination I must prefer and why.
- Canvas options(2): what about "tileSize" and "canvasWindow" in my case? I didn't notice any improvement in the usage of tiles: did I used them the wrong way?
- What would be the best way to pass data to the canvas instead of using the current approach? Emit a signal and let canvases directly read the thread array (registered as a property)? Using a Timer for reading the thread array?
- What if data is rendered in background to an image and it is loaded on each canvas by the backthread via a "loadImage()" call? Working on a background image should improve performances, shouldn't it?
- Having several canvases concurrently calling their own paint routine is probably one of the biggest bottleneck. Is it possibile/feasible to have a unique paint routine for all the canvases? Does it make sense?
- Given the amount of data I'm managing, going for another solution (QQuickItem/QPaintedItem) can really improve the overall performance?
If you reached this point...thanks for your time! Any advice/criticism/clarification/idea is really, really, REALLY appreciated! :)
-
Hey, I have no experience with the Canvas object but if you are really concerned about the best performance have you thought about a native OpenGL ES implementation?
It is "fairly" easy to draw custom OpenGL stuff with the QQuickItem, you can also try a QPainter approach, but I think native OpenGL might be the fastest way and it works great on Android and iOS, I have used it in one of my QML projects. Also it no problem to have an overlay above the OpenGL renderer, but I don't think you can have a transparent background (so nothing below the OpenGL renderer).There should be some official OpenGL examples with QML.
Just an idea :)
-
have a look at how Chris describes it straight to the point based on his experience :
"link to forum topic":http://qt-project.org/forums/viewthread/40291/#168510 -
Thanks people for the lighting fast answers! :D
I'm concerned with the best performances as long as the overall user-experience is not affected. :) I've chosen to limit the number of visible points per graph to 300 on purpose. However, OpenGL seems the right way to raise this limit. I'll give it a try!
The Vertex buffer object code referred by Chris in his answer should be incorporeted in a QQuickItem. Is that the right approach?
-
A couple of points of interest:
-
the vast majority of your implementation could be moved to C++. Instead of using an outer Item with a variant 'values' property, which you update via a Connections dynamic signal connection, the top level type could instead be defined in C++ with a QList<real> (or other element-value type) sequence type. You could handle the incoming data from the thread in C++, update the sequence property and emit the property changed signal.
-
the lookup of values[arrayIndex] is probably going to be very slow, as it involves a property resolution on a QObject. You can improve the performance by caching the lookup into a locally-scoped js var outside of the loop:
@
var cachedValues = values;
for (..) {
context.lineTo(..., cachedValues[arrayIndex]);
}
@Other than that, I don't know much about the implementation of Canvas so I can't help much.
Regarding the Canvas vs QQuickPaintedItem vs QQuickItem question, I would say:
Don't use QQuickPaintedItem as that involves rasterisation on the CPU.
You can instead define a QQuickItem-derived type which defines its own QSGGeometryNode -- interfacing directly with QtQuick's scene graph.
There may be other ways to get close to the metal with raw GL too.
Cheers,
Chris. -
-
Hi Chris,
-
Got it. I've done something similar in another portion of code. Good point.
-
Interesting point. If I follow the suggestion in 1) this point still applies to an C++ element-value type as it is a property. Hence local cache should again be used. Right?
Given what you said, why not going even further? I can create a series of properties in C++, one for each linechart and directly create the data series for each chart. Then, I can emit a specific change signal for each set of data (and thus for each line graph) and just draw the data. What do you think?
Finally, what about the "other ways to get close to the metal with raw GL"?
-
-
Yes, the caching of the property lookup still applies, even if the property type is a C++ sequence type.
See http://qt-project.org/doc/qt-5.0/qtquick/qtquick-performance.html#sequence-tips for more information about it. Writes are definitely slow, but even reads have some overhead over cached accesses.
In general, I would say: yes, moving as much as you can to C++ will probably result in better performance. But as with all such things, that's just a hunch - the truth will only be known after you try it and benchmark it.
In regards to your final question, as other people before me have said, the best possible performance will come from raw openGL calls to draw your graphs. QtQuick's scene graph renderer uses openGL for its drawing, so you can either:
a) integrate with the scene graph, by implementing a custom item type with its own QSGGeometry, as I mentioned
b) do your openGL rendering separately from QtQuick - see http://qt-project.org/doc/qt-5.0/qtquick/scenegraph-openglunderqml.html for more information about that.In the future, the Qt3D module will allow even simpler integration, but I don't know if that stuff is released, yet. Certainly, it's available in repos, but I don't know how complete / polished it is.
Cheers,
Chris. -
Thanks again for your time!
I've been out of the office during the last few days. I'll take a look to your references asap. :)Cheers,
F.