diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index ea7420a73..f26887547 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 @@ -196,6 +197,73 @@ 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") + # 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(): """Initialize EGL for headless rendering. Returns (display, context, surface, EGL_module). Raises RuntimeError on failure.""" logger.debug("_init_egl: starting") @@ -216,16 +284,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() - 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 - 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}") config_attribs = [