Qt Graphics and Performance – Fast Text

Published Monday January 18th, 2010
11 Comments on Qt Graphics and Performance – Fast Text
Posted in Graphics Dojo, OpenGL, Painting

Previously in this topic:

In my previous post, The Cost of Convenience, we saw quite clearly that text drawing was a major bottleneck. Text drawing is quite common in GUI applications though, so we need a solution for that. If we break down what happens behind QPainter::drawText(), it is split into two distinct parts. Converting the string into a set of positioned glyphs, often refer to as “text layout” because it positions the glyphs, does text wrapping and adjustments for alignment. The second part is passing the glyphs to the paint engines to be rendered. When the text is the same all the time, the first part could be done once and the glyphs/positions just reused.

We have a class in Qt which allows you to cache the first part and only do the drawing for each frame. The class is QTextLayout. This is a low-level class, throwing asserts at you for the most trivial of mistakes. It also comes with a really inconvenient API, but it does reduce the most costly step of text drawing, which is the layout part. It is also only fair to mention that QTextLayout uses a lot more memory than just the glyph-array and positions array, as one could expect, so in a memory constrained setup, it should be used with caution. In 4.7, we plan to introduce an API for static text, which takes care of all the layout and stores only the required parts, reducing the overall memory footprint, but for now, QTextLayout is how you do it.

Going back to my virtual keyboard, updated Source Code, I’ve changed the “-buttonview” example to make use of QTextLayout. In the constructor, I build the layout:


    ButtonView() {
        QString content;
        for (int i='0'; i< ='Z'; ++i) {
            content += QLatin1Char(i);
            content += QChar(QChar::LineSeparator);
        }
        m_layout = new QTextLayout(content, font());
        QFontMetricsF fm(font());
        m_layout->beginLayout();
        for (int i=0; icreateLine();
            line.setNumColumns(1);
            int x = (i) % 10;
            int y = (i) / 10;
            QSizeF s = fm.boundingRect(content.at(i*2)).size();
            line.setPosition(QPointF(x * 32, y * 32) + QPointF(16 + s.width() / 2, 16 + s.height() / 2));
        }
        m_layout->endLayout();
        m_layout->setCacheEnabled(true);
    }

If you look at the source code, there is more stuff going on in the constructor than I show above. This is because I extracted the text layout relevant parts only. So what we do is to build a string of the characters. Between each character I insert a LineSeparator. Without this, I wouldn’t be able to split the text into multiple QTextLine objects. From the content string, I construct the layout. For each character, I find its position in the grid and construct a QTextLine and move the line to its position. Each line is one column/character big. Finally I enabled caching on the layout. This is the step where we start caching the laid out text.

When it comes to the paint method, the code is rather straightforward. All the text is contained inside a single layout object so I can just call its draw function.


    void paint(QPainter *p, const QStyleOptionGraphicsItem *, QWidget *) {

        // Draw background pixmaps...

        m_layout->draw(p, QPointF(0, 0));
    }

Now, lets have a look at what this gains us:

Text Layouts

The graph shows the number of milliseconds per frame including the blit. Measured on an N900 with composition disabled. Smaller is better!

If we compare the “-no-indexing -optimize-flags” to the one with “-no-indexing -optimize-flags -text-layout”, we see that there is a significant reduction per frame. It brings raster from 9.3 ms per frame down to 5.5, OpenGL drops from 16 ms per frame to 9.1 ms when using a text layout. A drop of about 4 ms is also visible in the X11 paint engine.

Needless to say, using the QTextLayout class introduces a huge benefit, but it requires a bit more setup to get there. In this implementation I merged all the text into a single object which also makes it impossible for me to move one item relative to the others, such as adding an offset when a button is pressed. I could have one QTextLayout for each item, which would have been roughly the same performance, but at a higher memory cost.

Until next time, take care!

PS: A small comment on the item cache / X11 numbers. The connection is asynchronous and Qt completes its job at about 2.7 ms pr frame. With “-sync” on the command line, which makes all X calls synchronous, raises the time to about 10 ms per frame. If I had put a QApp::syncX() into each frame, synchronizing once per frame which is essentially what GL and VG are doing, I would probably get a number that is in between these two. What this means is that the numbers for X11 in this test are actually quite a bit worse than the graphs show.

Do you like this? Share it
Share on LinkedInGoogle+Share on FacebookTweet about this on Twitter

Posted in Graphics Dojo, OpenGL, Painting

11 comments

unec says:

First off: Excellent blog series… Very informative…

Another thing that seems to be slower that it needs to be in Qt is drawing antialiased straight lines…
The worst is dotted or dashed lines, but even if the lines are solid and perfectly horizontal or vertical, they can still noticably slow down your application if you draw lots of them (e.g. drawing a dynamically changing grid in the background of a QWidget).
(Just my personal subjective experience though… Also, haven’t tried with Qt 4.5 or Qt 4.6 yet…)

Could you maybe give some hints about that in your blog, too?

Will Stokes says:

I look forward to the new API in Qt4.7! Rendering over and over a lot of text is actually a large bottle neck in parts of my application.

Scorp1us says:

I have an application where I use a QPainterPath for the text, but want to have QLabel- like automatic features, like HTML support.

Is there a way that I could use QTextLayout or some other class to create rendering instructions and rather than have it use a QLabel or display, send it to a Painter path?

For example, give “I want this to be a painter path” Somehow have a label or painter path receive it? I figure it would break down like this:
init font
text: “I ”
itallic
text: “want ”
unitallic
” to be ”
break
text :” a ”
bold
text: “painter path”
unbold

I would imagine it gets called a QTextPainter, and can either produce you a QPixmap or a QPainterPath,

QTextPainter tp(“I want this to be a painter path“);
QPixmap p = tp.toPixmap();
QPainterPath pp= tp.toPainterPath();

Along with some other functions: setWrapWidth(qreal pix), fitInRect(cont QRectF& rect), setFont(const QFont& font), etc..

gunnar says:

unec: The engine that is fastest for antialiased line drawing is the OpenGL paint engine with multisampling enabled. Most of the paint engines use the same mechansim for dashing as the raster engine does, so if you have a look in that post for dashing, you see why it is slow. OpenVG supports this natively, but I don’t know about the performance there. The OpenGL paint engine has a custom stroker which does dashes very fast, so it sounds like that is the engine you should be using.

Scorp1us: There is actually a way to do this, though it is not super-optimal, nor “nice”. Reimplement your own QPaintDevice, say TextToPathDevice, and implement the paintEngine() function which returns a TextToPathPaintEngine in which you reimplement only the drawPath() function. In the drawPath() use the QPainterPath::addPath() function to accumulate all textdrawing calls into the same path. It would support only a single color, but it would work.

Making use of these classes would look something like:


TextToPathDevice dev;
QPainter p(&dev);
QTextDocument doc("I want this to be a painter path");
doc.draw(&p);
p.end();
QPainterPath thePath = dev.path();
Thorbjørn Lindeijer says:

@unec, @gunnar: Actually, raster is really fast at drawing dashed lines while X11 is unusably slow. This seems to be due to raster taking a shortcut for lines with 0 width, which doesn’t seem to exist in the X11 paint engine, where even a simple dashed line is converted into lots of shapes which are checked for intersections with each other etc.

Scorp1us says:

Oh, thanks Gunnar. What would the “optimal”way to be to do it? Laying out text as paths I mean. The painter trick is what I just came up with. What would gunnar do?

scorp1us says:

Oh, here’s an idea to support more than one color: return a QMap(QColor, QPainterPath) then in a loop, switch to the pen then draw the corresponding path. Of course, QColor is a simplification, it Should probably be a QPair of (QPen,QBrush). QMap(QPair(QPen,QBrush), QPainterPath)

FF says:

@gunnar
I’ve a question, why does Qt use for QImage ARGB, instead of RGBA, like OpenGL does?
What is the reason for that?

gunnar says:

scorp1us: If I had to render a lot of text as paths, that is the best approach I can think of right now. If you are going to support multiple colors, then you should use QPainter::fillPath(path, brush) though as this, at least on the GL, Raster and VG paint engines, will involve fewer state changes.

FF: Mostly historical. Most pixel graphics tend to be laid out as AARRGGBB which is what we choose too. Luckily most GL implementations these days support either GL_BGRA as external format. The exception is GLES where external and internal format needs to be the same…

Benjamin Poiesz says:

I was trying to do the ‘item-cache’ option, which I’m assuming is setting the cache mode to ItemCoordinateCache or DeviceCordinateCache for each QGraphicsTextItem. But when I do this on my WindowsCE system all I get is a black box within the bounds of the QGraphicsItem. Any ideas on what I’m doing wrong? This is using OpenGL in the QGraphicsView, and commenting the one line for CacheMode makes it work again, and regular OpenGL content draws fine behind it.

I was thinking of writing my own cacher using pixmaps, but would prefer to use the built in DeviceCoordinateCache…Thanks if anyone’s got some ideas.

scorp1us says:

Thanks Gunnar. It took about 1/2 hr to figure out what you were saying, but I got it working. AWESOME!

Commenting closed.

Get started today with Qt Download now