3

I need to draw pixel data that is being held by a library as uint8_t * and that is updated frequently and partially. I get a call-back from the library every time an update is done, that looks like this:

void gotFrameBufferUpdate(int x, int y, int w, int h);

I've tried creating a QImage using the pixel data pointer

QImage bufferImage(frameBuffer, width, height, QImage::Format_RGBX8888);

and let the call-back trigger update() of my widget

void gotFrameBufferUpdate(int x, int y, int w, int h)
{
    update(QRect(QPoint(x, y), QSize(w, h)));
}

which simply draws the updated area of the QImage via paint():

void MyWidget::paint(QPainter *painter)
{
    QRect rect = painter->clipBoundingRect().toRect();
    painter->drawImage(rect, bufferImage, rect);
}

The problem with this approach is that the QImage does not seem to reflect any updates to the pixel buffer. It keeps showing its initial contents.

My current workaround is to re-create a QImage instance each time the buffer is updated:

void gotFrameBufferUpdate(int x, int y, int w, int h)
{
    if (bufferImage)
        delete bufferImage;
    bufferImage = new QImage(frameBuffer, width, height,
                             QImage::Format_RGBX8888);

    update(QRect(QPoint(x, y), QSize(w, h)));
}

This works but seems very inefficient to me. Is there a better way of dealing with externally updated pixel data in Qt? Can I make my QImage aware of updates to its memory buffer?

(Background: I'm writing a custom QML type with C++ backend that shall display the contents of a VNC session. I'm using LibVNC/libvncclient for this.)

akoch
  • 33
  • 3
  • "My current workaround is to re-create a QImage instance each time the buffer is updated" - I don't see the problem with that, since that's essentially what `QImage` itself would have to do if told its memory buffer was changed - it would have to throw out everything and re-create itself. So, what's the problem? – Jesper Juhl Apr 26 '19 at 14:43
  • 1
    Based on the "buffer must remain valid for the lifetime of the QImage" warnings in the documentation of the `QImage` constructors, it appears the QImage keeps the uchar-pointer provided to the constructor rather than making a copy into an internal buffer. – Jeremy Friesner Apr 26 '19 at 14:46
  • Why would it need to re-create itself if just a few byte values inside the buffer have changed? I'd understand re-creation of the image format or buffer dimensions change, but this is all static. – akoch Apr 26 '19 at 14:48
  • @akoch depending on what bytes changed, and the image format, it might need to reinterpret a lot of other image bytes. The simple approach is to just re-parse the image data from scratch upon any change. – Jesper Juhl Apr 26 '19 at 14:50

3 Answers3

2

The QImage is updated if one calls QImage::bits().

It does not allocate a new buffer, you can discard the result, but it magically triggers a refresh of the image. It is required every time you want a refresh.

I don't know if this is guaranteed behaviour, nor if it saves anything over recreating it.

Andrea
  • 21
  • 2
0

I would guess that some sort of caching mechanism is interfering with your expectations. QImage has a cacheKey, which changes if the QImage is altered. This can of course only happen if you change the image through QImage functions. As far as I can see, you're changing the underlying buffer directly, so QImage's cacheKey will remain the same. Qt's pixmap cache then has that key in its cache, and uses the cached pixmap for performance reasons.

Unfortunately, there doesn't seem to be a direct way to update this cacheKey or otherwise "invalidate" a QImage. You have two options:

  1. Recreate the QImage when you need it. No need to new it, so you can save a heap allocation. Creating a back-buffered QImage seems like a "cheap" operation, so I doubt this is a bottleneck.
  2. Do a trivial operation on the QImage (i.e. setPixel on a single pixel to black and then to the old value). This is somewhat "hackish" but probably the most efficient way to work around this API deficiency (it will trigger an update to cacheKey as far as I can tell).
rubenvb
  • 74,642
  • 33
  • 187
  • 332
  • 1
    Thanks, I can confirm both options do work for my scenario. I'm going with 2) for now. – akoch Apr 30 '19 at 08:05
0

AFAICT the QImage class already does work the way you think it should -- in particular, simply writing into the external frame-buffer does, in fact, update the contents of the QImage. My guess is that in your program, some other bit of code is copying the QImage data into a QPixmap internally somewhere (since a QPixmap will always store its internal buffer in the hardware's native format and thus it will be more efficient to paint onto the screen repeatedly) and it is that QPixmap that is not getting modified when the frameBuffer is updated.

As evidence that a QImage does in fact always contain the data from the frameBuffer, here is a program that writes a new color into its frame-buffer every time you click on the window, and then calls update() to force the widget to re-draw itself. I see that the widget changes color on every mouse-click:

#include <stdio.h>
#include <stdint.h>
#include <QPixmap>
#include <QWidget>
#include <QApplication>
#include <QPainter>

const int width = 500;
const int height = 500;
const int frameBufferSizeBytes = width*height*sizeof(uint32_t);
unsigned char * frameBuffer = NULL;

class MyWidget : public QWidget
{
public:
   MyWidget(QImage * img) : _image(img) {/* empty */}
   virtual ~MyWidget() {delete _image;}

   virtual void paintEvent(QPaintEvent * e)
   {
      QPainter p(this);
      p.drawImage(rect(), *_image);
   }

   virtual void mousePressEvent(QMouseEvent * e)
   {
      const uint32_t someColor = rand();
      const size_t frameBufferSizeWords = frameBufferSizeBytes/sizeof(uint32_t);
      uint32_t * fb32 = (uint32_t *) frameBuffer;
      for (size_t i=0; i<frameBufferSizeWords; i++) fb32[i] = someColor;

      update();
   }

private:
   QImage * _image;
};

int main(int argc, char ** argv)
{
   QApplication app(argc, argv);

   frameBuffer = new unsigned char[frameBufferSizeBytes];
   memset(frameBuffer, 0xFF, frameBufferSizeBytes);

   QImage * img = new QImage(frameBuffer, width, height, QImage::Format_RGBX8888);
   MyWidget * w = new MyWidget(img);
   w->resize(width, height);
   w->show();

   return app.exec();
}
Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234
  • Thanks for the example. I can reproduce this with a `QQuickPaintedItem` subclass instead of `QWidget` as well. No idea why it doesn't work in my real usage scenario, though. – akoch Apr 30 '19 at 08:03
  • You might try calculating a hash code of the data-array returned by myImage.bits() and printing it out every time you try to repaint with it, just to verify that the data in the `QImage` is actually being changed. – Jeremy Friesner Apr 30 '19 at 16:06