From 8e0558c4a4d069159eda68a31da6f37bdd4a7260 Mon Sep 17 00:00:00 2001 From: sam-kpm Date: Wed, 8 Apr 2026 18:45:58 -0600 Subject: [PATCH 1/3] Fix EGL context creation on headless NVIDIA (EGL_BAD_ACCESS) On headless Linux with NVIDIA GPUs and no display server, eglInitialize() with EGL_DEFAULT_DISPLAY fails with EGL_BAD_ACCESS. The fix falls back to EGL_EXT_platform_device: enumerate EGL devices and obtain a display via eglGetPlatformDisplayEXT(EGL_PLATFORM_DEVICE_EXT, ...). PyOpenGL's egl_get_devices() wrapper doesn't reliably resolve the eglQueryDevicesEXT function pointer in this scenario, so both functions are called directly from libEGL.so.1 via ctypes. Also handles the case where eglInitialize raises EGLError rather than returning False, which varies by PyOpenGL version and EGL vendor. --- comfy_extras/nodes_glsl.py | 60 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index ea7420a73..164265d5d 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -2,6 +2,7 @@ import os import sys import re import logging +import ctypes import ctypes.util import importlib.util from typing import TypedDict @@ -223,9 +224,64 @@ def _init_egl(): logger.debug("_init_egl: calling eglInitialize()") major, minor = _EGL.EGLint(), _EGL.EGLint() - if not eglInitialize(display, major, minor): + # eglInitialize may return False or raise EGLError depending on the PyOpenGL + # version and EGL vendor. Catch both so we can fall through to device enumeration. + default_display_ok = False + try: + if eglInitialize(display, major, minor): + default_display_ok = True + except Exception: + pass + + if not default_display_ok: + # EGL_DEFAULT_DISPLAY fails on headless NVIDIA (EGL_BAD_ACCESS) because + # there is no X/Wayland compositor to back it. The correct approach for + # headless GPU rendering is to enumerate EGL devices and obtain a display + # from a specific device using EGL_EXT_platform_device. + # + # PyOpenGL's egl_get_devices() wrapper does not reliably resolve the + # eglQueryDevicesEXT function pointer in this scenario, so we call + # libEGL directly via ctypes instead. 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") + + _libegl = ctypes.CDLL("libEGL.so.1") + _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") + _query_devices = ctypes.CFUNCTYPE( + ctypes.c_bool, + 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) + + max_devices = 4 + raw_devices = (ctypes.c_void_p * max_devices)() + count = ctypes.c_int32(0) + if not _query_devices(max_devices, raw_devices, ctypes.byref(count)) or count.value == 0: + raise RuntimeError("eglQueryDevicesEXT() found no EGL devices") + logger.debug(f"_init_egl: found {count.value} EGL device(s)") + + EGL_PLATFORM_DEVICE_EXT = 0x313F + raw_display = _get_platform_display(EGL_PLATFORM_DEVICE_EXT, raw_devices[0], None) + if not raw_display: + raise RuntimeError("eglGetPlatformDisplayEXT() returned NULL") + # Cast the raw pointer to the opaque EGLDisplay type that PyOpenGL uses + # (c_void_p, same as EGL_NO_DISPLAY) so downstream EGL calls accept it. + display = ctypes.c_void_p(raw_display) + if not eglInitialize(display, major, minor): + display = None + raise RuntimeError("eglInitialize() failed on device display") logger.debug(f"_init_egl: EGL version {major.value}.{minor.value}") config_attribs = [ From e24d0f0ad128f0abb07254e226ff6aaf0b792832 Mon Sep 17 00:00:00 2001 From: sam-kpm Date: Wed, 8 Apr 2026 18:52:17 -0600 Subject: [PATCH 2/3] Refactor EGL device enumeration and fix eglGetDisplay fallback - Extract device enumeration into _egl_device_display() helper - Use ctypes.util.find_library("EGL") instead of hardcoded libEGL.so.1 - Fix eglGetDisplay(EGL_DEFAULT_DISPLAY) failure also falling through to device enumeration (previously raised immediately, skipping the fallback) - Two-pass eglQueryDevicesEXT to avoid arbitrary device count cap --- comfy_extras/nodes_glsl.py | 128 ++++++++++++++++++++----------------- 1 file changed, 70 insertions(+), 58 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index 164265d5d..65e30f1bf 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -197,6 +197,65 @@ def _init_glfw(): 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") + _query_devices = ctypes.CFUNCTYPE( + ctypes.c_bool, + 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 + raw_display = _get_platform_display(EGL_PLATFORM_DEVICE_EXT, raw_devices[0], None) + if not raw_display: + raise RuntimeError("eglGetPlatformDisplayEXT() returned NULL") + # 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) + if not eglInitialize(display, major, minor): + raise RuntimeError("eglInitialize() failed on device display") + logger.debug(f"_egl_device_display: EGL version {major.value}.{minor.value}") + return display, major, minor + + def _init_egl(): """Initialize EGL for headless rendering. Returns (display, context, surface, EGL_module). Raises RuntimeError on failure.""" logger.debug("_init_egl: starting") @@ -217,71 +276,24 @@ def _init_egl(): surface = None try: - logger.debug("_init_egl: calling eglGetDisplay()") - display = eglGetDisplay(EGL_DEFAULT_DISPLAY) - if display == _EGL.EGL_NO_DISPLAY: - raise RuntimeError("eglGetDisplay() failed") - - logger.debug("_init_egl: calling eglInitialize()") + # Try EGL_DEFAULT_DISPLAY first (works when a display server is present). + # Fall back to device enumeration for headless setups (e.g. NVIDIA with no + # X/Wayland). Both eglGetDisplay failure and eglInitialize failure (which may + # return False or raise EGLError depending on PyOpenGL version) trigger the + # fallback. major, minor = _EGL.EGLint(), _EGL.EGLint() - # eglInitialize may return False or raise EGLError depending on the PyOpenGL - # version and EGL vendor. Catch both so we can fall through to device enumeration. - default_display_ok = False + default_ok = False try: - if eglInitialize(display, major, minor): - default_display_ok = True + 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_display_ok: - # EGL_DEFAULT_DISPLAY fails on headless NVIDIA (EGL_BAD_ACCESS) because - # there is no X/Wayland compositor to back it. The correct approach for - # headless GPU rendering is to enumerate EGL devices and obtain a display - # from a specific device using EGL_EXT_platform_device. - # - # PyOpenGL's egl_get_devices() wrapper does not reliably resolve the - # eglQueryDevicesEXT function pointer in this scenario, so we call - # libEGL directly via ctypes instead. + if not default_ok: display = None # Not initialized, don't terminate logger.debug("_init_egl: EGL_DEFAULT_DISPLAY failed, falling back to EGL device enumeration") - - _libegl = ctypes.CDLL("libEGL.so.1") - _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") - _query_devices = ctypes.CFUNCTYPE( - ctypes.c_bool, - 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) - - max_devices = 4 - raw_devices = (ctypes.c_void_p * max_devices)() - count = ctypes.c_int32(0) - if not _query_devices(max_devices, raw_devices, ctypes.byref(count)) or count.value == 0: - raise RuntimeError("eglQueryDevicesEXT() found no EGL devices") - logger.debug(f"_init_egl: found {count.value} EGL device(s)") - - EGL_PLATFORM_DEVICE_EXT = 0x313F - raw_display = _get_platform_display(EGL_PLATFORM_DEVICE_EXT, raw_devices[0], None) - if not raw_display: - raise RuntimeError("eglGetPlatformDisplayEXT() returned NULL") - # Cast the raw pointer to the opaque EGLDisplay type that PyOpenGL uses - # (c_void_p, same as EGL_NO_DISPLAY) so downstream EGL calls accept it. - display = ctypes.c_void_p(raw_display) - if not eglInitialize(display, major, minor): - display = None - raise RuntimeError("eglInitialize() failed on device display") + display, major, minor = _egl_device_display(eglInitialize) logger.debug(f"_init_egl: EGL version {major.value}.{minor.value}") config_attribs = [ From 9e28569a0009ea334c5100b966abaaa9d78ed907 Mon Sep 17 00:00:00 2001 From: sam-kpm Date: Wed, 8 Apr 2026 18:59:03 -0600 Subject: [PATCH 3/3] Address CodeRabbit review comments - Use c_uint32 for EGLboolean return type (unsigned int per EGL spec, not _Bool) - Try all enumerated EGL devices in order rather than only the first; skip devices where eglGetPlatformDisplayEXT or eglInitialize fails --- comfy_extras/nodes_glsl.py | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index 65e30f1bf..f26887547 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -221,8 +221,9 @@ def _egl_device_display(eglInitialize): _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_bool, + ctypes.c_uint32, ctypes.c_int32, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_int32), )(_query_devices_ptr) @@ -243,17 +244,24 @@ def _egl_device_display(eglInitialize): logger.debug(f"_egl_device_display: found {count.value} EGL device(s)") EGL_PLATFORM_DEVICE_EXT = 0x313F - raw_display = _get_platform_display(EGL_PLATFORM_DEVICE_EXT, raw_devices[0], None) - if not raw_display: - raise RuntimeError("eglGetPlatformDisplayEXT() returned NULL") - # Cast the raw pointer to the opaque EGLDisplay type (c_void_p) that PyOpenGL uses. - display = ctypes.c_void_p(raw_display) + # 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") - major, minor = ctypes.c_int32(0), ctypes.c_int32(0) - if not eglInitialize(display, major, minor): - raise RuntimeError("eglInitialize() failed on device display") - logger.debug(f"_egl_device_display: EGL version {major.value}.{minor.value}") - return display, major, minor + raise RuntimeError(f"eglInitialize() failed on all {count.value} enumerated EGL device(s)") def _init_egl():