Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

qt optimize drawing of outlined text



  • I use such code to draw outlined text:

    painter.setPen(Qt::black);
    painter.setBrush(Qt::white);
    QPainterPath ppath;
    ppath.addText({200., 200.}, font(), line);
    painter.drawPath(ppath);
    

    it works, but so slow... For 100 captions it takes 30-50 milleseconds on 4GHz CPU, it is too long, because I want at least 10FPS, and I need much more stuff in addition to drawing of captions.

    Any hint how speedup code bellow? I thought that Qt has font cache, but for some reason 50% of program spend inside libfreetype in TT_RunIns code, while I draw only digits, so I thought that it should cache all glyphs after several iterations. May be I need call some function to turn on font cache? Note for simplicity I draw text on the same positon, in real program of course I do it in the right way.

    #include <QApplication>
    #include <QElapsedTimer>
    #include <QPainter>
    #include <QTimer>
    #include <QWidget>
    #include <algorithm>
    
    struct MyW : public QWidget {
      void paintEvent(QPaintEvent *) override {
        QPainter painter{this};
        painter.fillRect(rect(), Qt::red);
    
        painter.setPen(Qt::black);
        painter.setBrush(Qt::white);
    
        QVector<QString> lines;
        lines.resize(100);
        std::generate(std::begin(lines), std::end(lines),
                      [] { return QString::number(qrand()); });
    
        QElapsedTimer timer;
        timer.start();
        for (const auto &line : lines) {
          QPainterPath ppath;
          ppath.addText({200., 200.}, font(), line);
          painter.drawPath(ppath);
        }
        qInfo("paint text takes %d ms", static_cast<int>(timer.elapsed()));
      }
    };
    
    int main(int argc, char *argv[]) {
      QApplication app(argc, argv);
      MyW w;
      w.resize(600, 800);
      w.show();
      QTimer timer;
      QObject::connect(&timer, &QTimer::timeout, &app, [&w] { w.update(); });
      timer.start(1000);
      return app.exec();
    }
    
    

  • Lifetime Qt Champion

    Hi,

    You should profile your code however I think that the generation of your random list of text is one of the part that takes most of the time.

    Do you really expect your application to generate that kind of data every time you paint.



  • @SGaist

    You should profile your code

    If you reread my post you see that I already did this.

    but for some reason 50% of program spend inside libfreetype in TT_RunIns code

    however I think that the generation of your random list of text is one of the part that takes most of the time.
    Do you really expect your application to generate that kind of data every time you paint.

    You joking right? As you can see the time of generation was NOT counted, QElapsedTimer is started after all generation done.


  • Lifetime Qt Champion

    Hi
    since you kindly provided ready to run code i played around with it a bit.
    It seems that
    ppath.addText({200., 200.}, font(), line);
    is pretty heavy.
    I tried with ppath.addRect instead and it much, much faster.
    So im guessing on converting the font info to QPainterPath data is the heavy part and
    if you google TT_RunIns, there is also mention of that being slow outside of Qt.

    So if you only draw numbers 0-9
    you could pre create paths for them and use that ?

    just as a proof of concept. Warning. ugly code!

    
    struct MyW : public QWidget {
        QPainterPath *cache[10] = {nullptr};
        MyW(QWidget *par = nullptr) : QWidget(par)
        {
            for (int var = 0; var < 10; ++var) {
                cache[var] = new QPainterPath; // this leaks!
                cache[var]->addText({0,0}, font(), QString::number(var)); 
            }
        }
        void paintEvent(QPaintEvent *) override
        {
            QPainter painter{this};
            painter.fillRect(rect(), Qt::red);
    
            painter.setPen(Qt::black);
            painter.setBrush(Qt::white);
    
            QVector<QString> lines;
            lines.resize(100);
            std::generate(std::begin(lines), std::end(lines),
                          [] { return QString::number(qrand()); });
    
            QElapsedTimer timer;
            timer.start();
            for (const auto &line : lines) {
                for (int ch = 0; ch < line.length(); ++ch) {
                    const QPainterPath *path = cache[ line.at(ch).digitValue()];
                    painter.drawPath( path->translated(200+(ch*10),200) );
                }
            }
            qInfo("paint text takes %d ms", static_cast<int>(timer.elapsed()));
        }
    };
    

    note it leaks the paths! and the digit placement is off.
    Just for speed test. this is on my pc
    3 ms where the original code is 30-35.

    However, it might not be feasible for your real app if its not just numbers.



  • @mrjj

    3 ms where the original code is 30-35.

    I don't see so huge difference on my machine,
    I enable permanently turbo-boost for one core, to make time measurements more fair,
    and now original code takes 20ms (still too slow),
    with your modification it takes 9ms.

    So 2x time speedup, great, but it is still 1/10 of frame (1 seconds / 10 / 10)
    spend on just 100 captions, it looks like too slow for me.

    But if I emulate the glyph cache all works 0ms,
    but for some reason QPainterPath don't use internal Qt's glyphs cache :(

      void paintEvent(QPaintEvent *) override {
        QPainter painter{this};
        painter.fillRect(rect(), Qt::red);
    
        QVector<QString> lines;
        lines.resize(100);
        std::generate(std::begin(lines), std::end(lines),
                      [] { return QString::number(qrand()); });
    
        QFontMetrics fm{font()};
    
        std::vector<QPixmap> ppaths;
        ppaths.reserve(lines.size());
        for (const auto &line : lines) {
          const auto r = fm.boundingRect(line);
          QPixmap pix{r.size()};
          QPainter pix_painter{&pix};
          pix_painter.setPen(Qt::black);
          pix_painter.setBrush(Qt::white);
    
          QPainterPath ppath;
          ppath.addText({0., float(r.height()) / 2}, font(), line);
          pix_painter.drawPath(ppath);
          ppaths.push_back(pix);
        }
        QElapsedTimer timer;
        timer.start();
        for (const auto &ppath : ppaths) {
          painter.drawPixmap(QPoint{200, 200}, ppath);
        }
        qInfo("paint text takes %d ms", static_cast<int>(timer.elapsed()));
      }
    

  • Lifetime Qt Champion

    Hi
    Hmm, yes. Drawing as pixmap really is much faster than drawPath (tried your new code also)
    so i guess that will be the way to go.
    However, if you plan on much more drawing,
    QPainter might end up being to slow anyway to reach 10 FPS.

    What else are you planning to paint ?


Log in to reply