QOpenGLWidget swapBuffers happening without redrawing the scene

424 views Asked by At

I am porting code from QGLWidget to QOpenGLWidget and I encounter a different behavior: using QOpenGLWidget some swapBuffers() occur in some window events (like Enter or Leave events), but paintGL() is not called, which ends up showing the wrong buffer. To demonstrate I use a QMainWindow with a menu (opening or even hovering the menu triggers the events), and a very simple example, here in python/PyQt (but it's the same in C++). I use PyQt5 in order to compare to QGLWidget but the behavior is the same in Qt6.

#!/usr/bin/env python

from PyQt5 import Qt
from OpenGL.GL import *


class GLW(Qt.QOpenGLWidget):

    def initializeGL(self):
        print('initializeGL')
        glClearColor(0, 0, 1, 1)
        self.timer = Qt.QTimer(self)
        self.timer.timeout.connect(self.prepare_sel)

    def paintGL(self):
        print('paintGL')
        glClearColor(1, 0, 0, 1)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glClearColor(0, 0, 1, 1)
        self.timer.stop()
        self.timer.setSingleShot(True)
        self.timer.start(500)

    def resizeGL(self, w, h):
        print('resizeGL')
        glClearColor(0, 1, 0, 1)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glClearColor(0, 0, 1, 1)

    def buffers_swapped(self):
        print('buffers_swapped')

    def prepare_sel(self):
        print('prepare_sel')
        self.makeCurrent()
        glClearColor(0, 1, 1, 1)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glClearColor(0, 0, 1, 1)


class GLW2(Qt.QGLWidget):

    def initializeGL(self):
        print('initializeGL (2)')
        glClearColor(0, 0, 1, 1)
        self.timer = Qt.QTimer(self)
        self.timer.timeout.connect(self.prepare_sel)

    def paintGL(self):
        print('paintGL (2)')
        glClearColor(1, 0, 0, 1)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glClearColor(0, 0, 1, 1)
        self.timer.stop()
        self.timer.setSingleShot(True)
        self.timer.start(500)

    def resizeGL(self, w, h):
        print('resizeGL (2)')
        glClearColor(0, 1, 0, 1)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glClearColor(0, 0, 1, 1)

    def buffers_swapped(self):
        print('buffers_swapped (2)')

    def prepare_sel(self):
        print('prepare_sel (2)')
        self.makeCurrent()
        glClearColor(0, 1, 1, 1)
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
        glClearColor(0, 0, 1, 1)


app = Qt.QApplication([])

w = Qt.QMainWindow()
glw = GLW()
w.setCentralWidget(glw)
glw.frameSwapped.connect(glw.buffers_swapped)
qa = Qt.QAction('Quit')
m = Qt.QMenu('File')
m.addAction(qa)
w.menuBar().addMenu(m)
qa.triggered.connect(w.close)

w.show()

w2 = Qt.QMainWindow()
glw2 = GLW2()
w2.setCentralWidget(glw2)
#glw2.frameSwapped.connect(glw2.buffers_swapped)
qa2 = Qt.QAction('Quit')
m2 = Qt.QMenu('File')
m2.addAction(qa2)
w2.menuBar().addMenu(m2)
qa2.triggered.connect(w2.close)
w2.show()

app.exec()

w uses the QOpenGLWidget implementation whereas w2 is the same, using legacy QGLWidget. The widget normally just displays a red background in paintGL(), but also triggers a timer to prepare another buffer (let's say for future mouse selection, whatever), which is done 500ms later in prepare_sel(). Here the buffer is cyan, but is not displayed, and is not intended to be visible at any time. In w2 (QGLWidget) things are as expected, the window is always red. in w (QOpenGLWidget) the window is red, at first, but when I open the menu, or just move the mouse over it, then the window becomes cyan: swapBuffers() has been called here (as confirmed by the print in the debug callback buffers_swapped(), but paintGL() has not, thus a new buffer has not been prepared before being displayed. In comparison, in w2, the redrawing is not called when I move the mouse over the menu, but when I actually open or close it, the scene is completely refreshed, paintGL() is called. It is not in w.

Is this behavior normal ? How can I prevent this: is there a way to avoid the additional swapBuffers() which does not occur using QGLWidget ? Or is there a way to force a paintGL() before it ? Or is this way of doing things really wrong ? I mean, the "selection" buffer (cyan here) is perhaps not done the way it should, how could I do the same another way (do I need to use an additional framebuffer for this) ? But even then, the swapBuffers() occuring without paintGL() will at some point, when in a moving scene, display a buffer "from the past" and show a rendering which is not up-to-date, right ?

I just want to ensure I can call paintGL() before any swapBuffers() happens.

1

There are 1 answers

0
Denis Rivière On

I found a solution, using separate framebuffers for selection or other off-screen renderings (probably using more memory resources but I had no choice). The problem was actually that:

  1. QOpenGLWidget uses a framebuffer, and not a double buffering as QGLWidget was doing. Thus swapBuffers() is not actually a swap but a copy to visible screen, and
  2. the widget assumes that the framebuffer is never modified until a new paint event is issued, and copies it to visible screen when it needs, without redrawing - this second point was the key actually. This is not documented at any place, so I could not guess...

So we cannot avoid creating a second framebuffer for selection in order to leave the widget's one untouched. When doing so, everything looks as expected.