From 2ed245ed09c2fdcfaed389408691f5dfb9c95638 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Wed, 12 Nov 2025 13:15:18 +0100 Subject: [PATCH 1/3] Add notes on performance for Qt bitmap present --- rendercanvas/qt.py | 51 +++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/rendercanvas/qt.py b/rendercanvas/qt.py index e1fb064..ec8959d 100644 --- a/rendercanvas/qt.py +++ b/rendercanvas/qt.py @@ -26,6 +26,7 @@ QtCore = importlib.import_module(".QtCore", libname) QtGui = importlib.import_module(".QtGui", libname) QtWidgets = importlib.import_module(".QtWidgets", libname) + # QtOpenGLWidgets = importlib.import_module(".QtOpenGLWidgets", libname) try: # pyqt6 WA_PaintOnScreen = QtCore.Qt.WidgetAttribute.WA_PaintOnScreen @@ -374,39 +375,39 @@ def _rc_force_draw(self): self.repaint() def _rc_present_bitmap(self, *, data, format, **kwargs): - width, height = data.shape[1], data.shape[0] # width, height - rect1 = QtCore.QRect(0, 0, width, height) - rect2 = self.rect() + # Notes on performance: + # + # In the early stage of https://github.com/pygfx/rendercanvas/pull/138, with a single copy-buffer, + # running the cube example on my M1, with bitmap-present, I get about 75 FPS. + # + # AK: I tried to make this a QLabel and update a QPixmap by doing self._pixmap.convertFromImage(qImage), + # but this is much slower. + # + # AK: I tried to maintain a self._qimage, so that maybe gets bound internally to a texture, but that + # even makes it slightly slower. + # + # AK: I tried inheriting from QOpenGLWidget, because I saw a blog post (https://doc.qt.io/archives/qt-5.15/qtopengl-2dpainting-example.html) + # that says it will make the painter hardware accelerated. Interestingly, the content is drawn different to screen, as if + # the rect args to drawImage are interpreted differently (or wrong), which suggests that the painter *does* take a different path. + # However, the performance does not increase. Which may also suggest that with Qt6.x drawImage is accelerated by default. - painter = QtGui.QPainter(self) - # backingstore = self.backingStore() - # backingstore.beginPaint(rect2) - # painter = QtGui.QPainter(backingstore.paintDevice()) - - # We want to simply blit the image (copy pixels one-to-one on framebuffer). - # Maybe Qt does this when the sizes match exactly (like they do here). - # Converting to a QPixmap and painting that only makes it slower. - - # Just in case, set render hints that may hurt performance. - painter.setRenderHints( - painter.RenderHint.Antialiasing | painter.RenderHint.SmoothPixmapTransform, - False, - ) + width, height = data.shape[1], data.shape[0] # width, height + # Wrap the data in a QImage (no copy) qtformat = BITMAP_FORMAT_MAP[format] bytes_per_line = data.strides[0] image = QtGui.QImage(data, width, height, bytes_per_line, qtformat) - painter.drawImage(rect2, image, rect1) - - # Uncomment for testing purposes - # painter.setPen(QtGui.QColor("#0000ff")) - # painter.setFont(QtGui.QFont("Arial", 30)) - # painter.drawText(100, 100, "This is an image") + # Prep drawImage rects + rect1 = QtCore.QRect(0, 0, width, height) + rect2 = self.rect() + # Paint the image. Nearest neighbor interpolation, like the other backends. + painter = QtGui.QPainter(self) + painter.setRenderHints(painter.RenderHint.Antialiasing, False) + painter.setRenderHints(painter.RenderHint.SmoothPixmapTransform, False) + painter.drawImage(rect2, image, rect1) painter.end() - # backingstore.endPaint() - # backingstore.flush(rect2) def _rc_set_logical_size(self, width, height): width, height = int(width), int(height) From b19139a77f2bfcf9fdce1c0f621d7d3007e36711 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Wed, 12 Nov 2025 15:18:48 +0100 Subject: [PATCH 2/3] add comments --- rendercanvas/qt.py | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/rendercanvas/qt.py b/rendercanvas/qt.py index ec8959d..3383ddf 100644 --- a/rendercanvas/qt.py +++ b/rendercanvas/qt.py @@ -26,6 +26,7 @@ QtCore = importlib.import_module(".QtCore", libname) QtGui = importlib.import_module(".QtGui", libname) QtWidgets = importlib.import_module(".QtWidgets", libname) + # Uncomment the line below to try QtOpenGLWidgets.QOpenGLWidget instead of QWidget # QtOpenGLWidgets = importlib.import_module(".QtOpenGLWidgets", libname) try: # pyqt6 @@ -377,19 +378,28 @@ def _rc_force_draw(self): def _rc_present_bitmap(self, *, data, format, **kwargs): # Notes on performance: # - # In the early stage of https://github.com/pygfx/rendercanvas/pull/138, with a single copy-buffer, - # running the cube example on my M1, with bitmap-present, I get about 75 FPS. + # In the early stage of https://github.com/pygfx/rendercanvas/pull/138, + # with a single copy-buffer, running the cube example on my M1, with + # bitmap-present, I get about 75 FPS. # - # AK: I tried to make this a QLabel and update a QPixmap by doing self._pixmap.convertFromImage(qImage), - # but this is much slower. + # AK: I tried to make this a QLabel and update a QPixmap by doing + # self._pixmap.convertFromImage(qImage), but this is much slower. # - # AK: I tried to maintain a self._qimage, so that maybe gets bound internally to a texture, but that - # even makes it slightly slower. + # AK: I tried to maintain a self._qimage, so that it maybe gets bound + # internally to a texture, but that even makes it slightly slower. # - # AK: I tried inheriting from QOpenGLWidget, because I saw a blog post (https://doc.qt.io/archives/qt-5.15/qtopengl-2dpainting-example.html) - # that says it will make the painter hardware accelerated. Interestingly, the content is drawn different to screen, as if - # the rect args to drawImage are interpreted differently (or wrong), which suggests that the painter *does* take a different path. - # However, the performance does not increase. Which may also suggest that with Qt6.x drawImage is accelerated by default. + # AK: I tried inheriting from QOpenGLWidget, because I saw a blog post + # (https://doc.qt.io/archives/qt-5.15/qtopengl-2dpainting-example.html) + # that says it will make the painter hardware accelerated. + # Interestingly, the content is drawn different to screen, as if the + # rect args to drawImage are interpreted differently (or wrong), which + # suggests that the painter *does* take a different path. Also, it can + # be observed that the CPU usage is less that with QWidget. However, the + # performance does not significantly increase (in my tests) + # + # If I understand things correctly, Qt uses composition on the CPU, so + # there is an inherent limit to the performance. Rendering with GL likely + # includes downloading the rendered image for composition. width, height = data.shape[1], data.shape[0] # width, height From bd780ea0a1115d030b224042d109cfafe3d540bd Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Wed, 12 Nov 2025 15:20:50 +0100 Subject: [PATCH 3/3] reference this pr --- rendercanvas/qt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rendercanvas/qt.py b/rendercanvas/qt.py index 3383ddf..7f84679 100644 --- a/rendercanvas/qt.py +++ b/rendercanvas/qt.py @@ -400,6 +400,8 @@ def _rc_present_bitmap(self, *, data, format, **kwargs): # If I understand things correctly, Qt uses composition on the CPU, so # there is an inherent limit to the performance. Rendering with GL likely # includes downloading the rendered image for composition. + # + # Also see https://github.com/pygfx/rendercanvas/pull/139 width, height = data.shape[1], data.shape[0] # width, height