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?
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()
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,
World!
%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.