1

This may look like premature optimization, but I want to understand what happens on the inside and how this is typically programmed using the Qt library.

Imagine an application that constantly produces an image that fills the complete window, e.g. a 3D realtime renderer. (A photo editor doesn't seem to have this problem since it's meant to preserve the output image size, instead adding scrollbars when the image doesn't fit.) Obviously, the output (buffer) image should get resized when the window gets resized.

Now, in Qt, there appears no way to resize a QImage, instead, one has to deallocate the current image and allocate a new one. An image with a resolution of 1280x1024 and 3 8-bit channels take 3.75 Mb. Resize events arrive (I've tested this) really often, i.e. every few pixels of the window movement corner (that's using Qt5 on X11 under 64 bit Linux). Hence, the questions:

  • On a modern desktop CPU (considering the whole platform, i.e the RAM, the bus, and other aspects), is it any significant load to reallocate a few Mb a few times a second?
  • On the kind of platform described above, When the reallocation occurs, does it take place in the cache or in RAM, if possible to tell?
  • What is the common idiom in Qt to handle this kind of problem? There is event compression, but even with it applied, events arrive a few times a second (see introduction). Is using a single-shot QTimer with a timeout in the range of 100-200ms to wait for the resize events to stop flowing in a good idea?

Familiar with the possible answer of "the machine is going to handle it just fine" but if I treated it that way, I'd consider myself to be an illiterate programmer. Regardless of how strong CPU's are today, I'd like to understand how this works.

iksemyonov
  • 4,106
  • 1
  • 22
  • 42
  • These days, high-performance Qt apps using Qt5+ and Qt Quick 2.0 will be having any interactive image resizing done by QSG and OpenGL on the GPU, which has more than enough performance for mere image resampling. Old Qt4-era QPainter-based QtQuick was indeed more constrained by CPU performance though (you used to see advice to disable "smooth" during animation, for example; compare qt5's http://doc.qt.io/qt-5/qquickitem.html#smooth-prop with qt4's http://doc.qt.io/qt-4.8/qdeclarativeitem.html#smooth-prop ). – timday Oct 18 '16 at 16:17

1 Answers1

2

On a modern desktop CPU (considering the whole platform, i.e the RAM, the bus, and other aspects), is it any significant load to reallocate a few Mb a few times a second?

On typical modern allocators, the cost of one allocation is fixed and independent of the allocation size for "small" allocations. For larger allocations it is O(N) in allocation size with a very low proportionality constant.

A top-level Qt widget is backed by either a QImage buffer, or an OpenGL context if you use a QOpenGLWidget. The resizing of the window-backing buffer is handled automatically by Qt - it already happens and you don't even notice it! It's not a big deal, performance-wise. Modern allocators aren't dumb and are not fragmenting the heap.

On the kind of platform described above, When the reallocation occurs, does it take place in the cache or in RAM, if possible to tell?

That doesn't matter since you're going to overwrite it anyway. Of course it helps if there are cachelines available, and reusing the same address for an object would help with that.

What is the common idiom in Qt to handle this kind of problem?

  1. Have a slot that is used to update the data to be shown (e.g. update an image, or some parameter), and invoke QWidget::update()

  2. Render it in paintEvent.

The rest happens automagically. It doesn't matter how long paintEvent takes - if it takes long, the responsiveness of the UI will drop, but it won't ever be attempting to display out-of-date data. There is no cumulation of events.


The image scaling would be ordinarily handled by QImage::scaled returning a temporary image that you then draw using QPainter::drawImage. Yes, there are allocations there, but these allocations are quick.

The image producer's event storm is very simple to work around: the producer signals when a new image is available. The image consumer has a slot that accepts the image, copies it to an internal member, and triggers an update. The update takes effect when the control returns to the event loop, and uses the most recently set image. The repaint will proceed when there are no other events to process, thus it doesn't matter how long it takes: it will always show the most recent image. It won't ever "lag".

It's easy to verify this behavior. In the example below, the ImageSource produces new frames as fast as it can (on the order of 1kHz). Each frame displays the current time. The Viewer sleeps in its paintEvent, limiting the screen refresh rate to less than 4Hz: it won't ever be that slow in real life unless you run on a seriously overheated core. There are at least 25 new frames per each screen refresh. Yet the time you see on screen is the current time. The out-of-date frames are automatically discarded.

// https://github.com/KubaO/stackoverflown/tree/master/questions/update-storm-image-40111359
#include <QtWidgets>

class ImageSource : public QObject {
  Q_OBJECT
  QImage m_frame{640, 480, QImage::Format_ARGB32_Premultiplied};
  QBasicTimer m_timer;
  double m_period{};
  void timerEvent(QTimerEvent * event) override {
    if (event->timerId() != m_timer.timerId()) return;
    m_frame.fill(Qt::blue);
    QElapsedTimer t;
    t.start();
    QPainter p{&m_frame};
    p.setFont({"Helvetica", 48});
    p.setPen(Qt::white);
    p.drawText(m_frame.rect(), Qt::AlignCenter,
               QStringLiteral("Hello,\nWorld!\n%1").arg(
                 QTime::currentTime().toString(QStringLiteral("hh:mm:ss.zzz"))));
    auto const alpha = 0.001;
    m_period = (1.-alpha)*m_period + alpha*(t.nsecsElapsed()*1E-9);
    emit newFrame(m_frame, m_period);
  }
public:
  ImageSource() {
    m_timer.start(0, this);
  }
  Q_SIGNAL void newFrame(const QImage &, double period);
};

class Viewer : public QWidget {
  Q_OBJECT
  double m_framePeriod;
  QImage m_image;
  QImage m_scaledImage;
  void paintEvent(QPaintEvent *) override {
    qDebug() << "Waiting events" << d_ptr->postedEvents;
    QPainter p{this};
    if (m_image.isNull()) return;
    if (m_scaledImage.isNull() || m_scaledImage.size() != size())
      m_scaledImage = m_image.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
    p.drawImage(0, 0, m_scaledImage);
    p.drawText(rect(), Qt::AlignTop | Qt::AlignLeft, QStringLiteral("%1 FPS").arg(1./m_framePeriod));
    if (true) QThread::msleep(250);
  }
public:
  Q_SLOT void setImage(const QImage & image, double period) {
    Q_ASSERT(QThread::currentThread() == thread());
    m_image = image;
    m_scaledImage = {};
    m_framePeriod = period;
    update();
  }
};

class Thread final : public QThread { public: ~Thread() { quit(); wait(); } };

int main(int argc, char ** argv) {
  QApplication app{argc, argv};
  Viewer viewer;
  viewer.setMinimumSize(200, 200);
  ImageSource source;
  Thread thread;
  QObject::connect(&source, &ImageSource::newFrame, &viewer, &Viewer::setImage);
  QObject::connect(&thread, &QThread::destroyed, [&]{ source.moveToThread(app.thread()); });
  source.moveToThread(&thread);
  thread.start();
  viewer.show();
  return app.exec();
}
#include "main.moc"

It usually makes sense to offload image scaling to the GPU. This answer offers a complete solution to that.

Community
  • 1
  • 1
Kuba hasn't forgotten Monica
  • 95,931
  • 16
  • 151
  • 313
  • Great answer, it confirms that I've gone the right way from the beginning with the notification semantics. I've created a preview-render mechanism with 2 flags and a timer to handle request queuing on the consumer side, since the frames take a lot of time to render and stalls aren't welcome in UI. Haven't ran the code yet, but I surely appreciate the effort! – iksemyonov Oct 23 '16 at 03:45
  • @iksemyonov The producer and consumer should be probably unaware of each other. Flags get in the way of that - you need neither flags nor timers other than a zero-duration timer. – Kuba hasn't forgotten Monica Oct 23 '16 at 21:11
  • In my situation, a timer is required to start a full-res render automatically shortly after a preview gets rendered, so it's not the timer you might think it is :) – iksemyonov Oct 23 '16 at 21:15
  • You can probably do that without using a timer, right? Get the full-res render done ASAP? Or is there some requirement that precludes that? (I'm trying to understand what's going on) – Kuba hasn't forgotten Monica Oct 24 '16 at 13:06
  • It's for interactivity. First a downscaled version, then while I'm e.g. rotating or zooming the camera, the renderer still outputs downscaled, because it's 1-2s vs 0.2 s for example. Then, when the user input stops, the program can render in full resolution. Didn't think about it that way though, maybe I can get rid of the timer indeed, will try that out later! – iksemyonov Oct 24 '16 at 13:27
  • Given that the renderer works in a separate thread and doesn't affect the UI responsiveness at all (on a multi-core machine), and that the painting of the rendered image on the widget is instantaneous, I question the need to use the timer you speak of. If you're concerned that the renderer might preempt the UI, lower its thread's priority a notch. – Kuba hasn't forgotten Monica Oct 24 '16 at 13:29