Let's start at the end of your traceback:
File "C:\Python311\site-packages\glcontext\__init__.py", line 69, in create
return wgl.create_context(**kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Exception: cannot detect OpenGL context
Looking in __init__.py
, we don't find the string "cannot detect OpenGL context"
. But, on line 56, we do find:
from glcontext import wgl
In moderngl
's source code, we find a file wgl.cpp
, with the lines (119–123):
res->hrc = res->m_wglGetCurrentContext();
if (!res->hrc) {
PyErr_Format(PyExc_Exception, "cannot detect OpenGL context");
return NULL;
}
I don't know what res->hrc
is, but nor do I need to, because it's the return value of res->m_wglGetCurrentContext()
, and that function comes from line 80:
res->m_wglGetCurrentContext = (m_wglGetCurrentContextProc)GetProcAddress(res->libgl, "wglGetCurrentContext");
A little earlier (line 73), we see that res->libgl
is:
res->libgl = LoadLibraryEx(libgl, NULL, (DWORD)load_mode);
where libgl
is (line 50):
const char * libgl = "opengl32.dll";
So there's a problem with the wglGetCurrentContext
function from opengl32.dll
. Let's check Microsoft's Win32API documentation:
If the calling thread has a current OpenGL rendering context, wglGetCurrentContext
returns a handle to that rendering context. Otherwise, the return value is NULL
.
The !res->hrc
on line 120 is checking for NULL
, so this looks like our culprit!
Now, why is there no OpenGL rendering context...?
Standalone hypothesis
The Windows API documentation tells us how to create an OpenGL context – and, indeed, there's a function res->m_wglCreateContext
in wgl.cpp
. It's called on line 214, inside an if
block starting at line 180:
if (!strcmp(mode, "standalone")) {
Where does mode
come from? Lines 49–55:
const char * mode = "detect";
const char * libgl = "opengl32.dll";
int glversion = 330;
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|ssi", keywords, &mode, &libgl, &glversion)) {
return NULL;
}
It looks hardcoded… but it doesn't look like it's supposed to be. Let's check the Python documentation for PyArg_ParseTupleAndKeywords
:
Parse the parameters of a function that takes both positional and keyword parameters into local variables. The keywords argument is a NULL
-terminated array of keyword parameter names. Empty names denote positional-only parameters. Returns true on success; on failure, it returns false and raises the appropriate exception.
Not terribly useful, but Stack Overflow yields some code examples and, put together, we can see that mode
is a bog-standard Python kwarg.
So, following the keyword arguments up the traceback, consulting the source code of each function, we find that changing your line 16:
self.ctx = mgl.create_context()
to:
self.ctx = mgl.create_context(standalone=True)
fixes the immediate problem: now an OpenGL context is created. However, it doesn't seem to do anything.
Pygame hypothesis
The ModernGL documentation says:
ModernGL can only create headless contexts (no window), but it can also detect and use contexts from a large range of window libraries.
As Rabbid76 identified, ModernGL is supposed to pick up the context from pygame
. The fact it doesn't is curious. They should be created by pygame.init()
, so let's track that down.
>>> import pygame
pygame 1.9.6
Hello from the pygame community. https://www.pygame.org/contribute.html
>>> pygame.init
<built-in function init>
>>> pygame.init.__doc__
'init() -> (numpass, numfail)\ninitialize all imported pygame modules
So pygame.init
is defined in C – as one might expect. base.c
's pg_init
has an identical function and an identical return type, so I'll assume that's it. Lines 341–343:
/* Put all the module names we want to init in this array */
const char *modnames[] = {
IMPPREFIX "display", /* Display first, this also inits event,time */
IMPPREFIX "joystick", IMPPREFIX "font", IMPPREFIX "freetype",
IMPPREFIX "mixer",
/* IMPPREFIX "_sdl2.controller", Is this required? Comment for now*/
NULL};
Lines 361–368:
if (pg_mod_autoinit(modnames[i]))
success++;
else {
/* ImportError is neither counted as success nor failure */
if (!PyErr_ExceptionMatches(PyExc_ImportError))
fail++;
PyErr_Clear();
}
pgmod_autoinit
looks for a function named _internal_mod_init
and, failing that, a function called init
. The pygame.display
module handles SDL2, which handles OpenGL contexts; display._internal_mod_init
doesn't exist, but pygame.display.init
does. The relevant section:
const char *drivername;
/* Compatibility:
* windib video driver was renamed in SDL2, and we don't want it to fail.
*/
drivername = SDL_getenv("SDL_VIDEODRIVER");
if (drivername &&
!SDL_strncasecmp("windib", drivername, SDL_strlen(drivername))) {
SDL_setenv("SDL_VIDEODRIVER", "windows", 1);
}
if (!SDL_WasInit(SDL_INIT_VIDEO)) {
if (!_pg_mac_display_init())
return NULL;
if (SDL_InitSubSystem(SDL_INIT_VIDEO))
return RAISE(pgExc_SDLError, SDL_GetError());
}
(return NULL;
means an exception occurred, and it should be re-raised.)
SDL2's code is pretty solid, so there's probably no bug there. However, recall the code from base.c
: if this code fails, pygame.init()
will swallow the exception, and the only way you'd know anything went wrong is if you checked the return value.
It's probably possible to debug this further by looking into SDL2's source code, to find the conditions under which wglCreateContext
doesn't get called, but the easiest way would be to check pygame.init()
's return value.