Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PyQT5 OpenGL 4.1 Core profile - invalid frame buffer operation - Mac OS

This question is maybe related to another SO question.

I'm running MacOS 10.11.2 El Capitan. Wishing rich GUI features around my OpenGL applications I decided to give PyQT5 a shot to create the OpenGL context so I can integrate my OpenGL as a QtWidget int a GUI application.

QT5 provides several API methods for QGlWidget which I summarize shortly here:

  • initializeGL: gets invoked once before paintGL
  • paintGL: place to draw stuff to active frame buffer

I'm able to create the widget and initialize shaders etc. But when it comes to framebuffer related operations like glClear an error appears:

File "errorchecker.pyx", line 53, in OpenGL_accelerate.errorchecker._ErrorChecker.glCheckError (src/errorchecker.c:1218)

OpenGL.error.GLError: GLError(
    err = 1286,
    description = 'invalid framebuffer operation',
    baseOperation = glClear,
    cArguments = (GL_COLOR_BUFFER_BIT,)
)

I found a website reporting about related issue. It seems that there is no framebuffer configured when the API methods get invoked. As I feel it should be the task of QT I did not try to configure window framebuffer myself. But I found that after some calls of the API methods the framebuffer was created magically. Thus I build a little hack which will wait until paintGL was invoked NSKIP_PAINTGL=3 times. Then I configure my object so the normal paintGL process starts to work. This seems to work. But sometimes it needs more than NSKIP_PAINTGL times, so I included a little sleep within the workaround. QT seems to create the frame buffer a little after it should. Maybe QT does it in a separate thread? The QOpenGLWidget confirm that the framebuffer may not be created at some time:

Returns The frame buffer object handle or 0 if not yet initialized.

I don't like work arounds like this, since I am afraid of raised conditions here. Also I do not have a lot of control here (I need to rely on the fact that QT invokes paintGL often enough in first place so the hack can work). I'm not familiar with QT framework at the moment so here is my question:

How can I create some kinda loop which, when QGLControllerWidget was created, runs updateGL methods covered by a try/catch and retries as long as the GlError appears? Alternatively the loop may listen to QOpenGLWidget::defaultFramebufferObject() and waits for an object handle.

Of course I want to integrate this hack as elegant as possible into QT application flow - doing it the cute way.

Or did I miss something here? Is it possible to setup PyQT in some way so it won't invoke the OpenGL API methods before a valid framebuffer exists?

Here is an isolated code with the hack which runs on my Mac:

from PyQt5 import QtGui, QtCore, QtOpenGL, QtWidgets
from PyQt5.QtOpenGL import QGLWidget
from OpenGL.GL import *

from time import sleep

NSKIP_PAINTGL = 3

class QGLControllerWidget(QGLWidget):
    """
    basic test widget: a black screen which clears
    framebuffer on paintGL event so it should stay
    black on resize and so on.
    """
    def __init__(self, format = None):
        super(QGLControllerWidget, self).__init__(format, None)

        self._weird_pyqt5_framebuffer_hack = 0

        # replace paintGL by workaround
        self._weird_pyqt5_framebuffer_hack_original_paintGL = self.paintGL
        self.paintGL = self._weird_pyqt5_framebuffer_hack_paintGL

    def initializeGL(self): 
        pass

    def _weird_pyqt5_framebuffer_hack_paintGL(self):
        self._weird_pyqt5_framebuffer_hack += 1 
        if self._weird_pyqt5_framebuffer_hack < NSKIP_PAINTGL:
            return

        sleep(0.1)
        # restore original paintGL
        self.paintGL = self._weird_pyqt5_framebuffer_hack_original_paintGL
        self.updateGL()

    def paintGL(self):
        glClear(GL_COLOR_BUFFER_BIT)

if __name__ == '__main__':
    import sys

    class QTWithGLTest(QtWidgets.QMainWindow):
        def __init__(self, parent = None):
            super(QTWithGLTest, self).__init__(parent)

            # MacOS core profile 4.1
            qgl_format = QtOpenGL.QGLFormat()
            qgl_format.setVersion(4, 1)
            qgl_format.setProfile(QtOpenGL.QGLFormat.CoreProfile)
            qgl_format.setSampleBuffers(True)

            self.widget = QGLControllerWidget(qgl_format)
            self.setCentralWidget(self.widget)

            self.show()

    app = QtWidgets.QApplication(sys.argv)
    window = QTWithGLTest()
    window.show()
    app.exec_()

Note that also C++ snippets are welcome as I will try convert then into python.

like image 496
Nicolas Heimann Avatar asked Mar 07 '16 21:03

Nicolas Heimann


1 Answers

The QGL* stuff is deprecated in Qt5. That might be the reason why it's calling paintGL() ahead of time. You should try to use QOpenGLWidget or QOpenGLWindow instead.

Alternatively, you could try to use the isValid() method inside paintGL() and bail out early if it returns a falsy value:

def paintGL(self):
    if not self.isValid():
        return

If you want to try QOpenGLWidget, you can use this as starting point:

#!/usr/bin/env python

from PyQt5.QtGui import (
        QOpenGLBuffer,
        QOpenGLShader,
        QOpenGLShaderProgram,
        QOpenGLVersionProfile,
        QOpenGLVertexArrayObject,
        QSurfaceFormat,
    )
from PyQt5.QtWidgets import QApplication, QMainWindow, QOpenGLWidget


class QTWithGLTest(QMainWindow):
    """Main window."""

    def __init__(self, versionprofile=None, *args, **kwargs):
        """Initialize with an OpenGL Widget."""
        super(QTWithGLTest, self).__init__(*args, **kwargs)

        self.widget = QOpenGLControllerWidget(versionprofile=versionprofile)
        self.setCentralWidget(self.widget)
        self.show()


class QOpenGLControllerWidget(QOpenGLWidget):
    """Widget that sets up specific OpenGL version profile."""

    def __init__(self, versionprofile=None, *args, **kwargs):
        """Initialize OpenGL version profile."""
        super(QOpenGLControllerWidget, self).__init__(*args, **kwargs)

        self.versionprofile = versionprofile

    def initializeGL(self):
        """Apply OpenGL version profile and initialize OpenGL functions."""
        self.gl = self.context().versionFunctions(self.versionprofile)
        if not self.gl:
            raise RuntimeError("unable to apply OpenGL version profile")

        self.gl.initializeOpenGLFunctions()

        self.createShaders()
        self.createVBO()
        self.gl.glClearColor(0.0, 0.0, 0.0, 0.0)

    def paintGL(self):
        """Painting callback that uses the initialized OpenGL functions."""
        if not self.gl:
            return

        self.gl.glClear(self.gl.GL_COLOR_BUFFER_BIT | self.gl.GL_DEPTH_BUFFER_BIT)
        self.gl.glDrawArrays(self.gl.GL_TRIANGLES, 0, 3)

    def resizeGL(self, w, h):
        """Resize viewport to match widget dimensions."""
        self.gl.glViewport(0, 0, w, h)

    def createShaders(self):
        ...

    def createVBO(self):
        ...


if __name__ == '__main__':
    import sys

    fmt = QSurfaceFormat()
    fmt.setVersion(4, 1)
    fmt.setProfile(QSurfaceFormat.CoreProfile)
    fmt.setSamples(4)
    QSurfaceFormat.setDefaultFormat(fmt)

    vp = QOpenGLVersionProfile()
    vp.setVersion(4, 1)
    vp.setProfile(QSurfaceFormat.CoreProfile)

    app = QApplication(sys.argv)
    window = QTWithGLTest(versionprofile=vp)
    window.show()
    sys.exit(app.exec_())

Also, you don't need to use the OpenGL functions provided by Qt like I did. You can use the plain functions from OpenGL.GL as well or you can even mix and match (Qt provides some nice classes that make working with GL easier). If you just want to use OpenGL.GL, you can remove everything referring to QOpenGLVersionProfile, versionprofile, versionFunctions, initializeOpenGLFunctions and self.gl.

like image 153
blubberdiblub Avatar answered Oct 09 '22 11:10

blubberdiblub