glVertexAttribPointer error when using PyOpenGL

Hello, I am trying to render some 3D graphics into a wxPython app using opengl. I’m using VBOs to load in data from an obj file and then drawing the arrays:

class Mesh(LogicDrawer3D): 

    def __init__(self, filename, device_id, pos_x, pos_y, names, devices, monitors,        vertex_loader, context = None, canvas = None) -> None:
    
    super().__init__(names, devices, monitors, vertex_loader)
    
    self.x = pos_x
    self.y = pos_y
    self.filename = filename
    self.context = context
    self.canvas = canvas 
    
    vertices = None
    try: 
        str_id = str(de`your text`vice_id)
        parent_dict = dict(self.vertex_loader)
        vertices = parent_dict[str_id]
    except Exception as ex: 
        pass

    if vertices is None: 
        vertices = self.load_mesh()
        str_id = str(device_id)
        self.vertex_loader[str_id] =  vertices

    vertices = np.array(vertices, dtype=np.float32)
    self.vertex_count = len(vertices)//8
    self.canvas.SetCurrent(self.context)
    self.vao = GL.glGenVertexArrays(1)
    #GL.glColor3f(0.7, 0.5, 0.1)
    GL.glBindVertexArray(self.vao)

    #Vertices
    self.vbo = GL.glGenBuffers(1)
    GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vbo)
    GL.glBufferData(GL.GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL.GL_STATIC_DRAW)
    #position
    GL.glEnableVertexAttribArray(0)
    GL.glVertexAttribPointer(0, 3, GL.GL_FLOAT, GL.GL_FALSE, 32, GL.ctypes.c_void_p(0))
    #texture
    GL.glEnableVertexAttribArray(1)
    GL.glVertexAttribPointer(1, 2, GL.GL_FLOAT, GL.GL_FALSE, 32, GL.ctypes.c_void_p(12))

    self.draw()

def load_mesh(self) -> list[float]: 

    v = []
    vt = []
    vn = [] 
    vertices = []

    with open(self.filename, "r") as file:
        line = file.readline()
        while line:
            words = line.split(" ")
            if words[0] == "v":
                v.append(self.read_vertex_data(words))
            elif words[0] == "vt":
                vt.append(self.read_texcoord_data(words))
            elif words[0] == "vn":
                vn.append(self.read_normal_data(words))
            elif words[0] == "f":
                self.read_face_data(words, v, vt, vn, vertices)
            
            line = file.readline()

    return vertices

def read_vertex_data(self, words : list[str]) -> list[float]: 

    return [
            self.scale * float(float(words[1]) + self.x), 
            self.scale * float(float(words[2]) + self.y), 
            self.scale * float(float(words[3]))
    ]

def read_texcoord_data(self, words: list[str]) -> list[float]: 
     return [
            float(words[1]), 
            float(words[2])
    ]

def read_normal_data(self, words: list[str]) -> list[float]: 
     return [
            float(words[1]), 
            float(words[2]), 
            float(words[3])
    ]

def read_face_data(self, words: list[float], 
                    v  : list[list[float]], 
                    vt : list[list[float]], 
                    vn : list[list[float]], 
                    vertices : list[float]) -> None: 

    triangle_count = len(words) - 3

    for i in range(triangle_count): 

        self.make_corner(words[1], v, vt, vn, vertices)
        self.make_corner(words[2 + i], v, vt, vn, vertices)
        self.make_corner(words[3 + i], v, vt, vn, vertices)


def make_corner(self, corner_description: str, 
                    v  : list[list[float]], 
                    vt : list[list[float]], 
                    vn : list[list[float]], 
                    vertices : list[float]) -> None: 
    
    v_vt_vn = corner_description.split("/") # OBJ splits corners this way

    for element in v[int(v_vt_vn[0]) - 1]:
        vertices.append(element)
    for element in vt[int(v_vt_vn[1]) - 1]: 
        vertices.append(element)
    for element in vn[int(v_vt_vn[2]) - 1]: 
        vertices.append(element)
    
    # OBJ files store faces in the format a/b/c a'/b'/c' ...
    # for the first element for example, a corresponds to vertex, b to texture coord, 
    # c to normal (all are in indices)

def destroy(self): 
    GL.glDeleteVertexArrays(1, (self.vao,))
    GL.glDeleteBuffers(1, (self.vbo,))

def draw(self) -> None:
    GL.glDrawArrays(GL.GL_TRIANGLES, 0, self.vertex_count)

This method works perfectly fine in windows, I am able to get it to render on the canvas I want. As soon as I test this on linux I get the following error:

Traceback (most recent call last):
  File "/homes/ab2810/GF2P2/logsim/monitor_canvas_3D.py", line 250, in on_paint
    self.init_gl()
  File "/homes/ab2810/GF2P2/logsim/monitor_canvas_3D.py", line 150, in init_gl
    GL.glVertexAttribPointer(0, 3, GL.GL_FLOAT, GL.GL_FALSE, 32, GL.ctypes.c_void_p(0))
  File "src/latebind.pyx", line 51, in OpenGL_accelerate.latebind.Curry.__call__
  File "/usr/local/python-venv/lib64/python3.9/site-packages/OpenGL/GL/VERSION/GL_2_0.py", line 469, in glVertexAttribPointer
    contextdata.setValue( key, array )
  File "/usr/local/python-venv/lib64/python3.9/site-packages/OpenGL/contextdata.py", line 58, in setValue
    context = getContext( context )
  File "/usr/local/python-venv/lib64/python3.9/site-packages/OpenGL/contextdata.py", line 40, in getContext
    raise error.Error(
OpenGL.error.Error: Attempt to retrieve context when no valid context

I suspect its because of some weird context setting happening when I call wx.GLcanvas to set up a canvas and set a context there. I have been trying to debug this in linux for about 2 days to no avail. I am not sure how context setting works in this case - if its a global context or not

I am using Rocky Linux 9.4 for testing

I have tried to fix this by resetting the context just before I call glVertexAtrribPointer but that does not seem to work either. The issue seems to mainly concern the glVertexAtrribPointer function - I cannot see why this is failing on linux machines but working just fine on Windows. I have not imported or installed anything different on windows either.

Would anyone have an idea about what this may be about?

I have made a minimal example for the same - this works on windows but throws the same no valid context error on linux. Change the file_name to any obj type object you have:

import wx
import wx.glcanvas as wxcanvas
from OpenGL import platform
import numpy as np
import math
from OpenGL import GL, GLU, GLUT


class TESTGUI(wx.Frame):
    
    def __init__(self, title):
        super().__init__(parent=None, title=title, size=(800, 600))

        # Configure the file menu
        fileMenu = wx.Menu()
        menuBar = wx.MenuBar()
        fileMenu.Append(wx.ID_ABOUT, "&About")
        fileMenu.Append(wx.ID_EXIT, "&Exit")
        menuBar.Append(fileMenu, "&File")
        self.SetMenuBar(menuBar)

        # Canvas for drawing signals

        # Configure the widgets
        self.text = wx.StaticText(self, wx.ID_ANY, "Cycles")
        self.spin = wx.SpinCtrl(self, wx.ID_ANY, "10")
        self.run_button = wx.Button(self, wx.ID_ANY, "Run")
        self.text_box = wx.TextCtrl(self, wx.ID_ANY, "",
                                    style=wx.TE_PROCESS_ENTER)

        # Bind events to widgets
        self.Bind(wx.EVT_MENU, self.on_menu)
        self.spin.Bind(wx.EVT_SPINCTRL, self.on_spin)
        self.run_button.Bind(wx.EVT_BUTTON, self.on_run_button)
        self.text_box.Bind(wx.EVT_TEXT_ENTER, self.on_text_box)

        self.canvas = TestCanvas(self)

        # Configure sizers for layout
        main_sizer = wx.BoxSizer(wx.HORIZONTAL)
        side_sizer = wx.BoxSizer(wx.VERTICAL)

        main_sizer.Add(self.canvas, 5, wx.EXPAND | wx.ALL, 5)
        main_sizer.Add(side_sizer, 1, wx.ALL, 5)

        side_sizer.Add(self.text, 1, wx.TOP, 10)
        side_sizer.Add(self.spin, 1, wx.ALL, 5)
        side_sizer.Add(self.run_button, 1, wx.ALL, 5)
        side_sizer.Add(self.text_box, 1, wx.ALL, 5)

        self.SetSizeHints(600, 600)
        self.SetSizer(main_sizer)

    def on_menu(self, event):
        Id = event.GetId()
        if Id == wx.ID_EXIT:
            self.Close(True)
        if Id == wx.ID_ABOUT:
            wx.MessageBox("Logic Simulator\nCreated by Mojisola Agboola\n2017",
                        "About Logsim", wx.ICON_INFORMATION | wx.OK)

    def on_spin(self, event):
        spin_value = self.spin.GetValue()
        self.canvas.render()

    def on_run_button(self, event):
        self.canvas.render()

    def on_text_box(self, event):
        text_box_value = self.text_box.GetValue()
        self.canvas.render()


class TestCanvas(wxcanvas.GLCanvas):
    
    def __init__(self, parent):
        super().__init__(parent, -1,
                        attribList=[wxcanvas.WX_GL_RGBA,
                                    wxcanvas.WX_GL_DOUBLEBUFFER,
                                    wxcanvas.WX_GL_DEPTH_SIZE, 16, 0])
        GLUT.glutInit()
        self.init = False
        self.context = wxcanvas.GLContext(self)


        # Constants for OpenGL materials and lights
        self.mat_diffuse = [0.0, 0.0, 0.0, 1.0]
        self.mat_no_specular = [0.0, 0.0, 0.0, 0.0]
        self.mat_no_shininess = [0.2]
        self.mat_specular = [0.5, 0.5, 0.5, 1.0]
        self.mat_shininess = [50.0]
        self.top_right = [10.0, 10.0, 10.0, 0.0]
        self.straight_on = [0.0, 0.0, 1.0, 0.0]
        self.no_ambient = [0.1, 0.1, 0.1, 1.0]
        self.dim_diffuse = [0.5, 0.5, 0.5, 1.0]
        self.bright_diffuse = [1.0, 1.0, 1.0, 1.0]
        self.med_diffuse = [0.75, 0.75, 0.75, 1.0]
        self.full_specular = [0.5, 0.5, 0.5, 1.0]
        self.no_specular = [0.0, 0.0, 0.0, 1.0]

        # Initialise variables for panning
        self.pan_x = 0
        self.pan_y = 0
        self.last_mouse_x = 0  # previous mouse x position
        self.last_mouse_y = 0  # previous mouse y position

        # Initialise the scene rotation matrix
        self.scene_rotate = np.identity(4, 'f')


        # Initialise variables for zooming
        self.zoom = 20
        self.parent = parent

        # Offset between viewpoint and origin of the scene
        self.depth_offset = 1000
        # Bind events to the canvas
        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_SIZE, self.on_size)
        self.Bind(wx.EVT_MOUSE_EVENTS, self.on_mouse)

        self.vertex_loader = {}

    def init_gl(self):

        if not self.init:
            size = self.GetClientSize()
            self.SetCurrent(self.context)

            GL.glViewport(0, 0, size.width, size.height)

            GL.glMatrixMode(GL.GL_PROJECTION)
            GL.glLoadIdentity()
            GLU.gluPerspective(45, size.width / size.height, 10, 10000)

            GL.glMatrixMode(GL.GL_MODELVIEW)
            GL.glLoadIdentity()  # lights positioned relative to the viewer
            GL.glLightfv(GL.GL_LIGHT0, GL.GL_AMBIENT, self.no_ambient)
            GL.glLightfv(GL.GL_LIGHT0, GL.GL_DIFFUSE, self.med_diffuse)
            GL.glLightfv(GL.GL_LIGHT0, GL.GL_SPECULAR, self.full_specular)
            GL.glLightfv(GL.GL_LIGHT0, GL.GL_POSITION, self.top_right)
            GL.glLightfv(GL.GL_LIGHT1, GL.GL_AMBIENT, self.no_ambient)
            GL.glLightfv(GL.GL_LIGHT1, GL.GL_DIFFUSE, self.dim_diffuse)
            GL.glLightfv(GL.GL_LIGHT1, GL.GL_SPECULAR, self.no_specular)
            GL.glLightfv(GL.GL_LIGHT1, GL.GL_POSITION, self.straight_on)

            GL.glMaterialfv(GL.GL_FRONT, GL.GL_SPECULAR, self.mat_specular)
            GL.glMaterialfv(GL.GL_FRONT, GL.GL_SHININESS, self.mat_shininess)
            GL.glMaterialfv(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE,
                            self.mat_diffuse)
            GL.glColorMaterial(GL.GL_FRONT, GL.GL_AMBIENT_AND_DIFFUSE)

            GL.glClearColor(0.0, 0.0, 0.0, 0.0)
            GL.glDepthFunc(GL.GL_LEQUAL)
            GL.glShadeModel(GL.GL_SMOOTH)
            GL.glDrawBuffer(GL.GL_BACK)
            GL.glCullFace(GL.GL_BACK)
            GL.glEnable(GL.GL_COLOR_MATERIAL)
            GL.glEnable(GL.GL_CULL_FACE)
            GL.glEnable(GL.GL_DEPTH_TEST)
            GL.glEnable(GL.GL_LIGHTING)
            GL.glEnable(GL.GL_LIGHT0)
            GL.glEnable(GL.GL_LIGHT1)
            GL.glEnable(GL.GL_NORMALIZE)

            # Viewing transformation - set the viewpoint back from the scene
            GL.glTranslatef(0.0, 0.0, -self.depth_offset)

            # Modelling transformation - pan, zoom and rotate
            GL.glTranslatef(self.pan_x, self.pan_y, 0.0)
            GL.glMultMatrixf(self.scene_rotate)
            GL.glScalef(self.zoom, self.zoom, self.zoom)
    
            self.init = True
    
        GL.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT)
        self.test_render()
        GL.glFlush()
        self.SwapBuffers()

    def test_render(self):
        TestMesh("device_objs/AND.obj", self.vertex_loader, 1)

    def render(self):
        self.SetCurrent(self.context)
        self.init_gl()

        # Clear everything
    
    
    def on_paint(self, event):
        self.SetCurrent(self.context)
        self.init_gl()

        size = self.GetClientSize()
        text = "".join(["Canvas redrawn on paint event, size is ",
                        str(size.width), ", ", str(size.height)])
        self.render()

    def on_size(self, event):
        # Forces reconfiguration of the viewport, modelview and projection
        # matrices on the next paint event
        self.init = False

    def on_mouse(self, event):
        self.SetCurrent(self.context)

        if event.ButtonDown():
            self.last_mouse_x = event.GetX()
            self.last_mouse_y = event.GetY()

        if event.Dragging():
            GL.glMatrixMode(GL.GL_MODELVIEW)
            GL.glLoadIdentity()
            x = event.GetX() - self.last_mouse_x
            y = event.GetY() - self.last_mouse_y
            if event.LeftIsDown():
                GL.glRotatef(math.sqrt((x * x) + (y * y)), y, x, 0)
            if event.MiddleIsDown():
                GL.glRotatef((x + y), 0, 0, 1)
            if event.RightIsDown():
                self.pan_x += x
                self.pan_y -= y
            GL.glMultMatrixf(self.scene_rotate)
            GL.glGetFloatv(GL.GL_MODELVIEW_MATRIX, self.scene_rotate)
            self.last_mouse_x = event.GetX()
            self.last_mouse_y = event.GetY()
            self.init = False

        if event.GetWheelRotation() < 0:
            self.zoom *= (1.0 + (
                event.GetWheelRotation() / (20 * event.GetWheelDelta())))
            self.init = False

        if event.GetWheelRotation() > 0:
            self.zoom /= (1.0 - (
                event.GetWheelRotation() / (20 * event.GetWheelDelta())))
            self.init = False

        self.Refresh()  # triggers the paint event

    def render_text(self, text, x_pos, y_pos, z_pos):
        GL.glDisable(GL.GL_LIGHTING)
        GL.glRasterPos3f(x_pos, y_pos, z_pos)
        font = GLUT.GLUT_BITMAP_HELVETICA_10

        for character in text:
            if character == '\n':
                y_pos = y_pos - 20
                GL.glRasterPos3f(x_pos, y_pos, z_pos)
            else:
                GLUT.glutBitmapCharacter(font, ord(character))

        GL.glEnable(GL.GL_LIGHTING)
    

class TestMesh():

    def __init__(self, filename, vertex_loader, device_id) -> None:
    
        self.x = 0
        self.y = 0
        self.filename = filename
        self.scale = 1
        self.vertex_loader = vertex_loader
    
        vertices = None

        try:
            str_id = str(device_id)
            parent_dict = self.vertex_loader
            if str_id not in parent_dict: 
                pass
            else: 
                vertices = parent_dict[str_id]
        except KeyError:
            pass

        if vertices is None:
            vertices = self.load_mesh()
            str_id = str(device_id)
            self.vertex_loader[str_id] =  vertices

        vertices = np.array(vertices, dtype=np.float32)
        self.vertex_count = len(vertices)//8

        self.vao = GL.glGenVertexArrays(1)
        GL.glBindVertexArray(self.vao)
        #Vertices
        self.vbo = GL.glGenBuffers(1)
        GL.glBindBuffer(GL.GL_ARRAY_BUFFER, self.vbo)
        GL.glBufferData(GL.GL_ARRAY_BUFFER, vertices.nbytes, vertices, GL.GL_STATIC_DRAW)
        #position
        GL.GL_CONTEXT_FLAG_NO_ERROR_BIT = True
        GL.glVertexAttribPointer(0, 3, GL.GL_FLOAT, GL.GL_FALSE, 32, GL.ctypes.c_void_p(0))
        GL.glEnableVertexAttribArray(0)
        #self.brute_force(vertices)
        self.draw()

    def load_mesh(self) -> list[float]:

        v = []
        vt = []
        vn = []
        vertices = []

        with open(self.filename, "r") as file:
            line = file.readline()
            while line:
                words = line.split(" ")
                if words[0] == "v":
                    v.append(self.read_vertex_data(words))
                elif words[0] == "vt":
                    vt.append(self.read_texcoord_data(words))
                elif words[0] == "vn":
                    vn.append(self.read_normal_data(words))
                elif words[0] == "f":
                    self.read_face_data(words, v, vt, vn, vertices)
            
                line = file.readline()

        return vertices

    def read_vertex_data(self, words : list[str]) -> list[float]:

        return [
                self.scale * float(float(words[1]) + self.x),
                self.scale * float(float(words[2]) + self.y),
                self.scale * float(float(words[3]))
        ]

    def read_texcoord_data(self, words: list[str]) -> list[float]:
        return [
                float(words[1]),
                float(words[2])
        ]

    def read_normal_data(self, words: list[str]) -> list[float]:
        return [
                float(words[1]),
                float(words[2]),
                float(words[3])
        ]

    def read_face_data(self, words: list[float],
                        v  : list[list[float]],
                        vt : list[list[float]],
                        vn : list[list[float]],
                        vertices : list[float]) -> None:

        triangle_count = len(words) - 3

        for i in range(triangle_count):

            self.make_corner(words[1], v, vt, vn, vertices)
            self.make_corner(words[2 + i], v, vt, vn, vertices)
            self.make_corner(words[3 + i], v, vt, vn, vertices)


    def make_corner(self, corner_description: str,
                        v  : list[list[float]],
                        vt : list[list[float]],
                        vn : list[list[float]],
                        vertices : list[float]) -> None:
    
        v_vt_vn = corner_description.split("/") # OBJ splits corners this way

        for element in v[int(v_vt_vn[0]) - 1]:
            vertices.append(element)
        for element in vt[int(v_vt_vn[1]) - 1]:
            vertices.append(element)
        for element in vn[int(v_vt_vn[2]) - 1]:
            vertices.append(element)
    
        # OBJ files store faces in the format a/b/c a'/b'/c' ...
        # for the first element for example, a corresponds to vertex, b to texture coord,
        # c to normal (all are in indices)

    def destroy(self):
        GL.glDeleteVertexArrays(1, (self.vao,))
        GL.glDeleteBuffers(1, (self.vbo,))

    def draw(self) -> None:
        GL.glDrawArrays(GL.GL_TRIANGLES, 0, self.vertex_count)

    def brute_force(self, vertices):

        normal = []
        vertex = []

        all_vertices = []
        all_normals = []

        for index, element in enumerate(vertices):
            pos = index % 8
        
            if pos < 3:
                vertex.append(element)
            elif 4 < pos < 8:
                normal.append(element)
            else:
                pass

            all_vertices.append(vertex)
            all_normals.append(normal)
        GL.glBegin(GL.GL_TRIANGLES)
        for vertex, normal in zip(all_vertices, all_normals):
            GL.glVertex3f(vertex[0], vertex[1], vertex[2])
        GL.glEnd()


if __name__ == "__main__":

    app = wx.App()
    gui = TESTGUI("Logic Simulator")
    gui.Show(True)
    app.MainLoop()