Jason Smith
Jason Smith

Reputation: 469

QPainter::drawImage() glitches in Qt 5.10

My application draws a 3D rendering to an offline QImage (without OpenGL for historical reasons) and then draws that QImage in a widget like this:

void Render3dModel::paintEvent(QPaintEvent *event) {
    // Update model and render new image
    m_model.calculate();
    QImage image = m_model.getImage();

    // Draw image in widget
    QPainter painter(this);
    painter.drawImage(QPointF(0, 0), image);
}

The image gets repainted based on mouse movements using QWidget::update():

void Render3dModel::mouseMoveEvent(QMouseEvent *event) {
    m_model.rotate(event);
    update();
}

This approach worked fast and flawless in Qt 5.6 and Qt 5.9. However, rebuilding the exact same code in Qt 5.10 results in strange glitches. Parts of two frames are visible at the same time. This is not a temporary effect (e.g. delayed drawing), because if I stop moving the mouse, it actually keeps showing parts two frames. Below is an example (using alternating gray and white backgrounds for even/odd frames for clarity):

tearing

I have tried creating deep copies using image.copy() in a QImage ringbuffer to see if this is some race-condition between destruction/reuse of the QImage and some deferred/delayed painting, but that does not help.

Apparently the mechanics of QPainter have changed between Qt 5.9 and Qt 5.10, but I am unable to find any clues in the documentation.

Please advice

Update

After playing with several window sizes, I came to the conclusion that the fault somehow corresponds to the same size rectangle aligned at the upper-left corner with the "S" of the "Section dimensions" label. However, as you can see in the screenshot, the frame with the white background has that part of the cone drawn at the correct position. So the image is not shifted, but clipped somehow?!

I have tried adding

painter.setClipping(false);
painter.setClipRect(QRect(0, 0, size().width(), size().height()));

but that has no effect. Does my Render3dModel occasionally get paintEvent's from its parents?

Update 2

Adding a mouseMoveEvent handler to Kuba Ober's minimal example below, reproduces the problem on a MacBook (OS X 10.12.6, Intel HD Graphics 515):

Reproduced glitch

Could this perhaps be a regression in Qt 5.10 because I am unable to reproduce this in Qt 5.9? Should I file a bug report?

Update 3

This appears indeed to be regression in Qt 5.10 on OS X. Filed bug report https://bugreports.qt.io/browse/QTBUG-67998

Upvotes: 0

Views: 1768

Answers (3)

Tor Arne
Tor Arne

Reputation: 811

This is your bug:

void Render3dModel::paintEvent(QPaintEvent *event) {
    // Update model and render new image
    m_model.calculate();
    ...
}

You should not tie your model update to the paint event. The paint event is supposed to draw the current state of the model, and you may get multiple subsequent and/or partial paint events, when e.g. revealing parts of the window.

What you are seeing in your application is a partial expose event, which in this case is the result of a bug in Qt, but in general you may receive partial expose events and should be ready handle them.

Upvotes: 0

Sergio Monteleone
Sergio Monteleone

Reputation: 2886

I tested the minimal example provided by @KubaOber, adding the mouseMoveEvent() handler as suggested by the OP, but I couldn't reproduce the issue on my platform (Qt 5.10.1 - Linux).

It looks to me it could be a platform specific issue. There are quite a few differences in the cocoa platform plugin between Qt 5.9 and Qt 5.10, in particular about QCocoaBackingStore and its flush() method (changelog here).

If the minimal, complete example shows well the issue I would suggest to report it to Qt's bug tracker at https://bugreports.qt.io.

Upvotes: 1

There can be no race conditions in single-threaded code. There is no such thing as delayed/deferred painting. By the time paintEvent returns, the painting of that widget is done.

What you see is what you get. The getImage() method likely returns junk - perhaps an undersized image - and then you paint that junk. Replace getImage() with a simple alternating fill, and you'll see that it works fine:

screenshot of the example code

// https://github.com/KubaO/stackoverflown/tree/master/questions/glitchy-paint-49930405
#include <QtWidgets>
#include <array>

class Render3dModel : public QWidget {
   struct Model {
      mutable int counter = {};
      QSize size;
      QFont font{"Helvetica", 48};
      QImage getImage() const {
         static auto const format = QPixmap{1,1}.toImage().format();
         QImage image{size, format};
         image.fill((counter & 1) ? Qt::blue : Qt::yellow);
         QPainter p(&image);
         p.setFont(font);
         p.setPen((counter & 1) ? Qt::yellow : Qt::blue);
         p.drawText(image.rect(), QString::number(counter));
         counter++;
         return image;
      }
   } m_model;
   void paintEvent(QPaintEvent *) override {
      m_model.size = size();
      auto image = m_model.getImage();
      QPainter{this}.drawImage(QPoint{0, 0}, image);
   }
};
int main(int argc, char **argv) {
   QApplication app{argc, argv};
   QWidget win;
   QVBoxLayout topLayout{&win};
   QTabWidget tabs;
   topLayout.addWidget(&tabs);
   // Tabs
   for (auto text : { "Shape", "Dimensions", "Layout"}) tabs.addTab(new QWidget, text);
   tabs.setCurrentIndex(1);
   QHBoxLayout tabLayout{tabs.currentWidget()};
   QGroupBox dims{"Section Dimensions"}, model{"3D Model"};
   QGridLayout dimsLayout{&dims}, modelLayout{&model};
   for (auto w : {&dims, &model}) tabLayout.addWidget(w);
   // Section Dimensions
   for (auto text : {"Diameter 1", "Diameter 2", "Length"}) {
      auto row = dimsLayout.rowCount();
      std::array<QWidget*, 3> widgets{{new QLabel{text}, new QDoubleSpinBox, new QLabel{"inch"}}};
      for (auto *w : widgets)
         dimsLayout.addWidget(w, row, dimsLayout.count() % widgets.size());
   }
   tabLayout.setAlignment(&dims, Qt::AlignLeft | Qt::AlignTop);
   dims.setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Maximum);
   // Model
   Render3dModel render;
   modelLayout.addWidget(&render);
   model.setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
   model.setMinimumSize(250, 250);
   win.show();
   return app.exec();
}

Upvotes: 0

Related Questions