Wx's GLContext not usable by pyopengl under linux; windows ok (OpenGL)


Short background: I am developing an application where I am building the UI with wxPython. I’d like wxPython to take care of window and OpenGL context creation. I will use ctypes to call .so/.dlls that will do the heavy lifting (data analysis). My first testcase was ripping all X11-specific stuff from glxgears putting only the GL calls in an .so. That worked under windows&linux. This is my first foray into OpenGL so I only found out after this exercise that glxgears is more than ancient. However, my attempts to use modern OpenGL are foiled by some issue with the GLContext.

I found a suitable python-only test case using wxPython and pyopengl that demonstrates the issue on stackoverflow. This is my modified version:

#! /usr/bin/env python
# Adapted from:
# https://stackoverflow.com/questions/39734211/how-do-i-get-pyopengl-to-work-with-> a-wxpython-context-based-on-this-c-modern
# Works on windows but not on linux
# Tested with:
# --------------------------------------
# Windows 7 AMD64
# python : 3.8.2 (tags/v3.8.2:7b3ab59, Feb 25 2020, 23:03:10) [MSC v.1916 64 bit (AMD64)]
# wxpython : 4.1.0 msw (phoenix) wxWidgets 3.1.4
# pyopengl : 3.1.5
# OpenGL : 4.6.0 NVIDIA 460.89
# --------------------------------------
# Fedora 32  kernel 5.9.14-100.fc32.x86_64
# python : 3.8.6 (default, Sep 25 2020, 00:00:00) 
# [GCC 10.2.1 20200723 (Red Hat 10.2.1-1)]
# wxpython : 4.1.1 gtk3 (phoenix) wxWidgets 3.1.5
# pyopengl : 3.1.5
# OpenGL : 4.6.0 NVIDIA 455.45.01
# --------------------------------------
# On linux the GL context created by wx seems to be partially visible by
# other "code": pyopengl for one, but the same problem occurs in low-level
# code in a .so imported using the ctypes module - the code in the InitGL()
# function works up to the call: glEnableVertexAttribArray()
# - maybe at the driver level the context is known and hence most GL stuff works?
# - but perhaps it is not "visible" in the X11-OpenGL interface, i.e. GLX?
# Also of interest: an earlier .so that used the drawing commands from glxgears,
# i.e. OGL 1.x only, did work. In any case, the current pyopengl based example
# demonstrates the issue sufficiently, I think

# I think wx uses libepoxy to load OGL. I tried to CDLL libepoxy, libGL,
# libGLX with RTLD_GLOBAL before importing pyopengl (import OpenGL) and wx
# thinking maybe some global variable was define more than once but to no
# avail
# Setting g_exclude_crash_trigger = True   allows the code to run by
# not calling glEnableVertexAttribArray() and since it then does not make sense
# glDrawArrays - in this case the window turns up with a dark green background
# so glClear() *is* working

import platform as pfinfo    # prevent conflict with OpenGL.platform
import sys

import OpenGL
from OpenGL.GL import *          # the package is called pyopengl
import wx
from wx import glcanvas
import numpy as np

g_exclude_crash_trigger = False

def show_version_info():
        import distro
        print('{} {}  kernel {}'.format(distro.name(),distro.version(),pfinfo.uname().release))
    except ImportError:
        print('{} {} {}'.format(pfinfo.system(), pfinfo.release(), pfinfo.machine()))
    print('python : {}'.format(sys.version))
    print('wxpython : {}'.format(wx.version()))
    print('pyopengl : {}'.format(OpenGL.version.__version__))
    print('OpenGL : {}'.format(glGetString(GL_VERSION).decode('utf8')))

vertexSource = """
#version 130
in vec2 position;
void main()
    gl_Position = vec4(position, 0.0, 1.0);
fragmentSource = """
#version 130
out vec4 outColor;
void main()
    outColor = vec4(0.2, 0.3, 1.0, 1.0);

class OpenGLCanvas(glcanvas.GLCanvas):
    def __init__(self, parent):
        glcanvas.GLCanvas.__init__(self, parent, -1, size=(640, 480))
        self.init = False
        #self.context = glcanvas.GLContext(self)
        cxtAttrs = glcanvas.GLContextAttrs()
        self.context = glcanvas.GLContext(self, ctxAttrs=cxtAttrs)

        #self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
        self.Bind(wx.EVT_PAINT, self.OnPaint)

    def OnEraseBackground(self, event):
        pass # Do nothing, to avoid flashing on MSW.
    def OnPaint(self, event):
        dc = wx.PaintDC(self)
        if not self.init:
            # Note: this is the proper time to initialise the OGL stuff. I
            # tried other events (EVT_SHOW, EVT_CREATE) that occur before the
            # first EVT_PAINT but the GL context does not exist yet
            # Note, the wx "view" of the GL context is that it has been created
            # and is proper:
            self.init = True
    def InitGL(self):
        # Vertex Input
        ## Vertex Array Objects
        vao = glGenVertexArrays(1)
        ## Vertex Buffer Object
        vbo = glGenBuffers(1) # Generate 1 buffer
        vertices = np.array([0.0,  0.5, 0.5, -0.5, -0.5, -0.5], dtype=np.float32)
        ## Upload data to GPU
        glBindBuffer(GL_ARRAY_BUFFER, vbo)
        glBufferData(GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL_STATIC_DRAW)
        # Compile shaders and combining them into a program
        ## Create and compile the vertex shader
        vertexShader = glCreateShader(GL_VERTEX_SHADER)
        glShaderSource(vertexShader, vertexSource)
        ## Create and compile the fragment shader
        fragmentShader = glCreateShader(GL_FRAGMENT_SHADER)
        glShaderSource(fragmentShader, fragmentSource)


        ## Link the vertex and fragment shader into a shader program
        shaderProgram = glCreateProgram()
        glAttachShader(shaderProgram, vertexShader)
        glAttachShader(shaderProgram, fragmentShader)
        glBindFragDataLocation(shaderProgram, 0, "outColor")
        # Making the link between vertex data and attributes
        posAttrib = glGetAttribLocation(shaderProgram, "position")
        if not g_exclude_crash_trigger:
          glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 0, None)

    def OnDraw(self):
        # Set clear color
        glClearColor(0.0, 0.2, 0.0, 1.0)
        #Clear the screen to dark green
        # draw the triangle
        if not g_exclude_crash_trigger:
          glDrawArrays(GL_TRIANGLES, 0, 3)

class Frame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, title="Blue triangle on dark green background", size=(640,480))
        canvas = OpenGLCanvas(self)

app = wx.App()
frame = Frame()

I have put in some details of the issue as comments in the code so that it can all be found in one place. This runs fine under windows but not under linux (that is a first!) and a triangle is seen on a dark green background. It fails on linux with the following error trace:

Traceback (most recent call last):
  File "./glctxproblem.py", line 116, in OnPaint
  File "./glctxproblem.py", line 162, in InitGL
    glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 0, None)
  File "/home/ichneumwn/.local/lib/python3.8/site-packages/OpenGL/latebind.py", line 63, in __call__
    return self.wrapperFunction( self.baseFunction, *args, **named )
  File "/home/ichneumwn/.local/lib/python3.8/site-packages/OpenGL/GL/VERSION/GL_2_0.py", line 469, in glVertexAttribPointer
    contextdata.setValue( key, array )
  File "/home/ichneumwn/.local/lib/python3.8/site-packages/OpenGL/contextdata.py", line 58, in setValue
    context = getContext( context )
  File "/home/ichneumwn/.local/lib/python3.8/site-packages/OpenGL/contextdata.py", line 40, in getContext
    raise error.Error(
OpenGL.error.Error: Attempt to retrieve context when no valid context

Any ideas what might be the issue? Is there a way to get the handle to the GLContext and somehow pass it on to pyopengl explicitly?


Follow-up by myself :slight_smile:

Maybe someone could run the sample on a different flavour of Linux to exclude the possibility this is specific to my system?