This commit is contained in:
sam-kpm 2026-04-29 18:57:23 +08:00 committed by GitHub
commit ad449c95e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -2,6 +2,7 @@ import os
import sys import sys
import re import re
import logging import logging
import ctypes
import ctypes.util import ctypes.util
import importlib.util import importlib.util
from typing import TypedDict from typing import TypedDict
@ -196,6 +197,73 @@ def _init_glfw():
raise raise
def _egl_device_display(eglInitialize):
"""Obtain an EGLDisplay via EGL_EXT_platform_device for headless GPU rendering.
EGL_DEFAULT_DISPLAY fails on headless NVIDIA (EGL_BAD_ACCESS) because there is
no X/Wayland compositor. The correct approach is to enumerate EGL devices and
obtain a display from a specific device handle using EGL_EXT_platform_device.
PyOpenGL's egl_get_devices() wrapper does not reliably resolve eglQueryDevicesEXT
in this scenario, so both extension functions are loaded from libEGL directly via
ctypes. Returns an initialized (display, major, minor) tuple.
"""
logger.debug("_egl_device_display: starting")
libegl_name = ctypes.util.find_library("EGL")
if not libegl_name:
raise RuntimeError("libEGL not found")
_libegl = ctypes.CDLL(libegl_name)
_get_proc = _libegl.eglGetProcAddress
_get_proc.restype = ctypes.c_void_p
_get_proc.argtypes = [ctypes.c_char_p]
_query_devices_ptr = _get_proc(b"eglQueryDevicesEXT")
if not _query_devices_ptr:
raise RuntimeError("eglQueryDevicesEXT not available — install libnvidia-egl-gbm1 or libegl-mesa0")
# EGLboolean is unsigned int (32-bit) in the EGL spec, not C99 _Bool.
_query_devices = ctypes.CFUNCTYPE(
ctypes.c_uint32,
ctypes.c_int32, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_int32),
)(_query_devices_ptr)
_get_platform_display_ptr = _get_proc(b"eglGetPlatformDisplayEXT")
if not _get_platform_display_ptr:
raise RuntimeError("eglGetPlatformDisplayEXT not available in libEGL")
_get_platform_display = ctypes.CFUNCTYPE(
ctypes.c_void_p, ctypes.c_uint32, ctypes.c_void_p, ctypes.c_void_p,
)(_get_platform_display_ptr)
# Two-pass query: first get the count, then fetch all devices.
count = ctypes.c_int32(0)
if not _query_devices(0, None, ctypes.byref(count)) or count.value == 0:
raise RuntimeError("eglQueryDevicesEXT() found no EGL devices")
raw_devices = (ctypes.c_void_p * count.value)()
if not _query_devices(count.value, raw_devices, ctypes.byref(count)):
raise RuntimeError("eglQueryDevicesEXT() failed on second call")
logger.debug(f"_egl_device_display: found {count.value} EGL device(s)")
EGL_PLATFORM_DEVICE_EXT = 0x313F
# Try each device in order; some may not support eglInitialize (e.g. non-render nodes).
for i, raw_device in enumerate(raw_devices[:count.value]):
raw_display = _get_platform_display(EGL_PLATFORM_DEVICE_EXT, raw_device, None)
if not raw_display:
logger.debug(f"_egl_device_display: device {i} eglGetPlatformDisplayEXT returned NULL, skipping")
continue
# Cast the raw pointer to the opaque EGLDisplay type (c_void_p) that PyOpenGL uses.
display = ctypes.c_void_p(raw_display)
major, minor = ctypes.c_int32(0), ctypes.c_int32(0)
try:
if eglInitialize(display, major, minor):
logger.debug(f"_egl_device_display: device {i} succeeded, EGL version {major.value}.{minor.value}")
return display, major, minor
except Exception:
pass
logger.debug(f"_egl_device_display: device {i} eglInitialize failed, skipping")
raise RuntimeError(f"eglInitialize() failed on all {count.value} enumerated EGL device(s)")
def _init_egl(): def _init_egl():
"""Initialize EGL for headless rendering. Returns (display, context, surface, EGL_module). Raises RuntimeError on failure.""" """Initialize EGL for headless rendering. Returns (display, context, surface, EGL_module). Raises RuntimeError on failure."""
logger.debug("_init_egl: starting") logger.debug("_init_egl: starting")
@ -216,16 +284,24 @@ def _init_egl():
surface = None surface = None
try: try:
logger.debug("_init_egl: calling eglGetDisplay()") # Try EGL_DEFAULT_DISPLAY first (works when a display server is present).
display = eglGetDisplay(EGL_DEFAULT_DISPLAY) # Fall back to device enumeration for headless setups (e.g. NVIDIA with no
if display == _EGL.EGL_NO_DISPLAY: # X/Wayland). Both eglGetDisplay failure and eglInitialize failure (which may
raise RuntimeError("eglGetDisplay() failed") # return False or raise EGLError depending on PyOpenGL version) trigger the
# fallback.
logger.debug("_init_egl: calling eglInitialize()")
major, minor = _EGL.EGLint(), _EGL.EGLint() major, minor = _EGL.EGLint(), _EGL.EGLint()
if not eglInitialize(display, major, minor): default_ok = False
try:
display = eglGetDisplay(EGL_DEFAULT_DISPLAY)
if display != _EGL.EGL_NO_DISPLAY and eglInitialize(display, major, minor):
default_ok = True
except Exception:
pass
if not default_ok:
display = None # Not initialized, don't terminate display = None # Not initialized, don't terminate
raise RuntimeError("eglInitialize() failed") logger.debug("_init_egl: EGL_DEFAULT_DISPLAY failed, falling back to EGL device enumeration")
display, major, minor = _egl_device_display(eglInitialize)
logger.debug(f"_init_egl: EGL version {major.value}.{minor.value}") logger.debug(f"_init_egl: EGL version {major.value}.{minor.value}")
config_attribs = [ config_attribs = [