Какова общая идиома изменения размера виджета с большим изображением в Qt?

Это может показаться преждевременной оптимизацией, но я хочу понять, что происходит внутри и как это обычно программируется с использованием библиотеки Qt.

Представьте себе приложение, которое постоянно создает изображение, которое заполняет все окно, например. средство 3D-рендеринга в реальном времени. (Кажется, редактор фотографий не имеет этой проблемы, поскольку он предназначен для сохранения размера выходного изображения вместо добавления полос прокрутки, когда изображение не подходит.) Очевидно, что выходное (буферное) изображение должно изменяться при изменении размера окна. .

Теперь в Qt нет способа изменить размер QImage, вместо этого нужно освободить текущее изображение и выделить новое. Изображение с разрешением 1280х1024 и 3-мя 8-битными каналами занимает 3,75 Мб. События изменения размера поступают (я проверял это) очень часто, то есть каждые несколько пикселей угла движения окна (при использовании Qt5 на X11 под 64-битным Linux). Отсюда вопросы:

  • На современном настольном ЦП (учитывая всю платформу, то есть ОЗУ, шину и другие аспекты) является ли значительной нагрузкой перераспределение нескольких МБ несколько раз в секунду?
  • На описанной выше платформе, когда происходит перераспределение, происходит ли это в кеше или в ОЗУ, если можно сказать?
  • Какова общая идиома в Qt для решения такого рода проблем? Существует сжатие событий, но даже при его применении события поступают несколько раз в секунду (см. введение). Является ли хорошей идеей использование одноразового QTimer с тайм-аутом в диапазоне 100-200 мс для ожидания прекращения потока событий изменения размера?

Знаком с возможным ответом «машина справится с этим просто отлично», но если бы я относился к этому таким образом, я бы считал себя неграмотным программистом. Независимо от того, насколько сильны процессоры сегодня, я хотел бы понять, как это работает.


person iksemyonov    schedule 18.10.2016    source источник
comment
В наши дни высокопроизводительные приложения Qt, использующие Qt5+ и Qt Quick 2.0, будут иметь любое интерактивное изменение размера изображения, выполняемое QSG и OpenGL на графическом процессоре, который имеет более чем достаточную производительность для простой передискретизации изображения. QtQuick, основанный на QPainter старой эпохи Qt4, действительно был более ограничен производительностью процессора (например, вы видели совет отключить сглаживание во время анимации; сравните doc.qt.io/qt-5/qquickitem.html#smooth-prop с doc.qt.io/qt-4.8/qdeclarativeitem.html#smooth-prop ).   -  person timday    schedule 18.10.2016


Ответы (1)


На современном настольном ЦП (учитывая всю платформу, то есть ОЗУ, шину и другие аспекты) является ли значительной нагрузкой перераспределение нескольких МБ несколько раз в секунду?

В типичных современных распределителях стоимость одного выделения фиксирована и не зависит от размера выделения для «маленьких» выделений. Для больших выделений это O(N) по размеру выделения с очень низкой константой пропорциональности.

Виджет Qt верхнего уровня поддерживается либо буфером QImage, либо контекстом OpenGL, если вы используете файл QOpenGLWidget. Изменение размера буфера поддержки окна обрабатывается Qt автоматически — это уже происходит, и вы даже не замечаете этого! Это не имеет большого значения, с точки зрения производительности. Современные распределители не глупы и не фрагментируют кучу.

На описанной выше платформе, когда происходит перераспределение, происходит ли это в кеше или в ОЗУ, если можно сказать?

Это не имеет значения, так как вы все равно собираетесь перезаписать его. Конечно, это помогает, если есть доступные кэш-линии, и повторное использование одного и того же адреса для объекта поможет в этом.

Какова общая идиома в Qt для решения такого рода проблем?

  1. У вас есть слот, который используется для обновления отображаемых данных (например, для обновления изображения или какого-либо параметра), и вызовите QWidget::update()

  2. Рендерим в paintEvent.

Остальное происходит автоматически. Неважно, сколько времени займет paintEvent — если это займет много времени, скорость отклика пользовательского интерфейса упадет, но он никогда не будет пытаться отображать устаревшие данные. Нет накопления событий.


Масштабирование изображения обычно выполняется с помощью QImage::scaled, возвращающего временное изображение, которое вы затем рисуете с помощью QPainter::drawImage. Да, там есть выделения, но эти выделения быстрые.

Обойти бурю событий производителя изображений очень просто: производитель сигнализирует, когда доступно новое изображение. У потребителя образа есть слот, который принимает образ, копирует его во внутренний элемент и запускает обновление. Обновление вступает в силу, когда элемент управления возвращается в цикл обработки событий и использует самое последнее установленное изображение. Перерисовка будет продолжаться, когда нет других событий для обработки, поэтому не имеет значения, сколько времени это займет: всегда будет отображаться самое последнее изображение. Он никогда не будет «отставать».

Это поведение легко проверить. В приведенном ниже примере ImageSource создает новые кадры так быстро, как только может (порядка 1 кГц). В каждом кадре отображается текущее время. Viewer спит в своем paintEvent, ограничивая частоту обновления экрана менее чем 4 Гц: в реальной жизни он никогда не будет таким медленным, если только вы не работаете на сильно перегретом ядре. На каждое обновление экрана приходится не менее 25 новых кадров. Тем не менее, время, которое вы видите на экране, является текущим временем. Устаревшие кадры автоматически отбрасываются.

// 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"

Обычно имеет смысл перенести масштабирование изображения на GPU. Этот ответ предлагает полное решение этой проблемы.

person Kuba hasn't forgotten Monica    schedule 18.10.2016
comment
Отличный ответ, он подтверждает, что я с самого начала пошел правильным путем с семантикой уведомлений. Я создал механизм предпросмотра-рендеринга с двумя флагами и таймером для обработки очереди запросов на стороне потребителя, поскольку рендеринг кадров занимает много времени, а зависания не приветствуются в пользовательском интерфейсе. Еще не запускал код, но я, безусловно, ценю усилия! - person iksemyonov; 23.10.2016
comment
@iksemyonov Производитель и потребитель, вероятно, не должны знать друг о друге. Флаги мешают этому - вам не нужны ни флаги, ни таймеры, кроме таймера с нулевой продолжительностью. - person Kuba hasn't forgotten Monica; 24.10.2016
comment
В моей ситуации требуется таймер для автоматического запуска рендеринга в полном разрешении вскоре после рендеринга предварительного просмотра, поэтому это не тот таймер, который вы могли бы подумать :) - person iksemyonov; 24.10.2016
comment
Вероятно, вы можете сделать это без использования таймера, верно? Сделать рендер в полном разрешении как можно скорее? Или есть какие-то требования, которые исключают это? (пытаюсь понять, что происходит) - person Kuba hasn't forgotten Monica; 24.10.2016
comment
Это для интерактивности. Сначала уменьшенная версия, затем, пока я, например. вращая или масштабируя камеру, рендерер все равно выводит в уменьшенном масштабе, потому что, например, это 1-2 с против 0,2 с. Затем, когда пользовательский ввод прекращается, программа может выполнять рендеринг в полном разрешении. Однако я не думал об этом таким образом, может быть, я действительно смогу избавиться от таймера, попробую это позже! - person iksemyonov; 24.10.2016
comment
Учитывая, что рендерер работает в отдельном потоке и никак не влияет на отзывчивость UI (на многоядерной машине), а отрисовка отрендеренного изображения на виджете происходит мгновенно, сомневаюсь в необходимости использования таймера вы говорите о. Если вы обеспокоены тем, что средство визуализации может вытеснить пользовательский интерфейс, понизьте приоритет его потока на ступеньку выше. - person Kuba hasn't forgotten Monica; 24.10.2016