From d57bf9decbc6c5a4230f50258b4897e9610f562f Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 22 Feb 2026 03:55:28 -0300 Subject: [PATCH 01/15] `gl`, `glfw`, `EGL` declaration - to the top of the module --- comfy_extras/nodes_glsl.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index 75ffb6d80..ef5c1bee0 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -16,6 +16,11 @@ from utils.install_util import get_missing_requirements_message logger = logging.getLogger(__name__) +# OpenGL modules - initialized lazily when context is created +gl = None +glfw = None +EGL = None + def _check_opengl_availability(): """Early check for OpenGL availability. Raises RuntimeError if unlikely to work.""" @@ -63,11 +68,6 @@ def _check_opengl_availability(): logger.debug("nodes_glsl: running _check_opengl_availability at import time") _check_opengl_availability() -# OpenGL modules - initialized lazily when context is created -gl = None -glfw = None -EGL = None - def _import_opengl(): """Import OpenGL module. Called after context is created.""" From c1877d3e4a68f19bfa5fa9f2ab3d8fdab242f88f Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:42:51 -0300 Subject: [PATCH 02/15] `GLContext`: extract `__init_try_backend()` (readability + try-return) --- comfy_extras/nodes_glsl.py | 145 ++++++++++++++++++++----------------- 1 file changed, 78 insertions(+), 67 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index ef5c1bee0..1a9180933 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -315,16 +315,18 @@ class GLContext: Tries backends in order: GLFW (desktop) → EGL (headless GPU) → OSMesa (software). """ - _instance = None - _initialized = False + __instance: 'GLContext' = None # The singleton def __new__(cls): - if cls._instance is None: - cls._instance = super().__new__(cls) - return cls._instance + # Since ``GLContext`` is a singleton anyway, we should store it + # explicitly in ``GLContext.__instance``, NOT in ``cls.__instance``. + if GLContext.__instance is None: + GLContext.__instance = super().__new__(cls) + assert isinstance(GLContext.__instance, GLContext) + return GLContext.__instance def __init__(self): - if GLContext._initialized: + if GLContext.__instance is not None: logger.debug("GLContext.__init__: already initialized, skipping") return @@ -333,7 +335,7 @@ class GLContext: global glfw, EGL import time - start = time.perf_counter() + start_time: float = time.perf_counter() self._backend = None self._window = None @@ -344,64 +346,7 @@ class GLContext: self._osmesa_buffer = None self._vao = None - # Try backends in order: GLFW → EGL → OSMesa - errors = [] - - logger.debug("GLContext.__init__: trying GLFW backend") - try: - self._window, glfw = _init_glfw() - self._backend = "glfw" - logger.debug("GLContext.__init__: GLFW backend succeeded") - except Exception as e: - logger.debug(f"GLContext.__init__: GLFW backend failed: {e}") - errors.append(("GLFW", e)) - - if self._backend is None: - logger.debug("GLContext.__init__: trying EGL backend") - try: - self._egl_display, self._egl_context, self._egl_surface, EGL = _init_egl() - self._backend = "egl" - logger.debug("GLContext.__init__: EGL backend succeeded") - except Exception as e: - logger.debug(f"GLContext.__init__: EGL backend failed: {e}") - errors.append(("EGL", e)) - - if self._backend is None: - logger.debug("GLContext.__init__: trying OSMesa backend") - try: - self._osmesa_ctx, self._osmesa_buffer = _init_osmesa() - self._backend = "osmesa" - logger.debug("GLContext.__init__: OSMesa backend succeeded") - except Exception as e: - logger.debug(f"GLContext.__init__: OSMesa backend failed: {e}") - errors.append(("OSMesa", e)) - - if self._backend is None: - if sys.platform == "win32": - platform_help = ( - "Windows: Ensure GPU drivers are installed and display is available.\n" - " CPU-only/headless mode is not supported on Windows." - ) - elif sys.platform == "darwin": - platform_help = ( - "macOS: GLFW is not supported.\n" - " Install OSMesa via Homebrew: brew install mesa\n" - " Then: pip install PyOpenGL PyOpenGL-accelerate" - ) - else: - platform_help = ( - "Linux: Install one of these backends:\n" - " Desktop: sudo apt install libgl1-mesa-glx libglfw3\n" - " Headless with GPU: sudo apt install libegl1-mesa libgl1-mesa-dri\n" - " Headless (CPU): sudo apt install libosmesa6" - ) - - error_details = "\n".join(f" {name}: {err}" for name, err in errors) - raise RuntimeError( - f"Failed to create OpenGL context.\n\n" - f"Backend errors:\n{error_details}\n\n" - f"{platform_help}" - ) + self._init_try_backend() # Now import OpenGL.GL (after context is current) logger.debug("GLContext.__init__: importing OpenGL.GL") @@ -424,7 +369,7 @@ class GLContext: except Exception: pass - elapsed = (time.perf_counter() - start) * 1000 + elapsed = (time.perf_counter() - start_time) * 1000 # Log device info renderer = gl.glGetString(gl.GL_RENDERER) @@ -434,9 +379,75 @@ class GLContext: vendor = vendor.decode() if vendor else "Unknown" version = version.decode() if version else "Unknown" - GLContext._initialized = True logger.info(f"GLSL context initialized in {elapsed:.1f}ms ({self._backend}) - {renderer} ({vendor}), GL {version}") + def _init_try_backend(self): + """Try to init backends in fallback order: GLFW → EGL → OSMesa. Raises RuntimeError on failure. + """ + global glfw, EGL + + errors = [] + self._backend = None + + logger.debug("GLContext.__init__: trying GLFW backend") + try: + self._window, glfw = _init_glfw() + self._backend = "glfw" + logger.debug("GLContext.__init__: GLFW backend succeeded") + return + except Exception as e: + logger.debug(f"GLContext.__init__: GLFW backend failed: {e}") + errors.append(("GLFW", e)) + + logger.debug("GLContext.__init__: trying EGL backend") + try: + self._egl_display, self._egl_context, self._egl_surface, EGL = _init_egl() + self._backend = "egl" + logger.debug("GLContext.__init__: EGL backend succeeded") + return + except Exception as e: + logger.debug(f"GLContext.__init__: EGL backend failed: {e}") + errors.append(("EGL", e)) + + logger.debug("GLContext.__init__: trying OSMesa backend") + try: + self._osmesa_ctx, self._osmesa_buffer = _init_osmesa() + self._backend = "osmesa" + logger.debug("GLContext.__init__: OSMesa backend succeeded") + return + except Exception as e: + logger.debug(f"GLContext.__init__: OSMesa backend failed: {e}") + errors.append(("OSMesa", e)) + + # If we still haven't returned, none of the backends succeeded. + # Let's raise the error. + + if sys.platform == "win32": + platform_help = ( + "Windows: Ensure GPU drivers are installed and display is available.\n" + " CPU-only/headless mode is not supported on Windows." + ) + elif sys.platform == "darwin": + platform_help = ( + "macOS: GLFW is not supported.\n" + " Install OSMesa via Homebrew: brew install mesa\n" + " Then: pip install PyOpenGL PyOpenGL-accelerate" + ) + else: + platform_help = ( + "Linux: Install one of these backends:\n" + " Desktop: sudo apt install libgl1-mesa-glx libglfw3\n" + " Headless with GPU: sudo apt install libegl1-mesa libgl1-mesa-dri\n" + " Headless (CPU): sudo apt install libosmesa6" + ) + + error_details = "\n".join(f" {name}: {err}" for name, err in errors) + raise RuntimeError( + f"Failed to create OpenGL context.\n\n" + f"Backend errors:\n{error_details}\n\n" + f"{platform_help}" + ) + def make_current(self): if self._backend == "glfw": glfw.make_context_current(self._window) From f241bfca816a3411c06561b1b751aed8ad1ed384 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:44:34 -0300 Subject: [PATCH 03/15] `GLContext` singleton factory: scaffolding concrete classes --- comfy_extras/nodes_glsl.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index 1a9180933..e4dc3a43c 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -4,7 +4,7 @@ import re import logging import ctypes.util import importlib.util -from typing import TypedDict +from typing import Type, TypedDict import numpy as np import torch @@ -309,10 +309,15 @@ def _init_osmesa(): return ctx, buffer +# ---------------------------------------------------------- + + class GLContext: """Manages OpenGL context and resources for shader execution. - Tries backends in order: GLFW (desktop) → EGL (headless GPU) → OSMesa (software). + Acts as a singleton factory: ``GLContext`` itself is an "abstract" class (not a true ABC though) and never instantiates itself directly. Instead, its instance is always one of "concrete backend" contexts - a first valid subclass in the fallback sequence. ``GLContext`` doesn't inherit from ABC just to prevent IDE warnings caused by this polymorphism. For all intents and purposes, it **IS** a singleton-ABC. + + Backends fallback order: GLFW (desktop) → EGL (headless GPU) → OSMesa (software). See ``__subclass_fallback_order()``. """ __instance: 'GLContext' = None # The singleton @@ -325,6 +330,11 @@ class GLContext: assert isinstance(GLContext.__instance, GLContext) return GLContext.__instance + @staticmethod + def __concrete_class_fallback_order() -> tuple[Type['GLContext'], ...]: + """The order concrete subclasses are tried in: GLFW → EGL → OSMesa.""" + return _GLContextGLFW, _GLContextEGL, _GLContextOSMesa + def __init__(self): if GLContext.__instance is not None: logger.debug("GLContext.__init__: already initialized, skipping") @@ -462,6 +472,21 @@ class GLContext: gl.glBindVertexArray(self._vao) +class _GLContextGLFW(GLContext): + pass + + +class _GLContextEGL(GLContext): + pass + + +class _GLContextOSMesa(GLContext): + pass + + +# ---------------------------------------------------------- + + def _compile_shader(source: str, shader_type: int) -> int: """Compile a shader and return its ID.""" shader = gl.glCreateShader(shader_type) From d097f7b023c0c1cba67ec6508573be83c72ba639 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Sun, 22 Feb 2026 22:40:44 -0300 Subject: [PATCH 04/15] singleton factory: actual implementation --- comfy_extras/nodes_glsl.py | 228 ++++++++++++++++++++----------------- 1 file changed, 125 insertions(+), 103 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index e4dc3a43c..69b68eccf 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -326,7 +326,7 @@ class GLContext: # Since ``GLContext`` is a singleton anyway, we should store it # explicitly in ``GLContext.__instance``, NOT in ``cls.__instance``. if GLContext.__instance is None: - GLContext.__instance = super().__new__(cls) + GLContext.__instance = GLContext.__new_instance_using_concrete_class_fallback_order() assert isinstance(GLContext.__instance, GLContext) return GLContext.__instance @@ -335,99 +335,29 @@ class GLContext: """The order concrete subclasses are tried in: GLFW → EGL → OSMesa.""" return _GLContextGLFW, _GLContextEGL, _GLContextOSMesa - def __init__(self): - if GLContext.__instance is not None: - logger.debug("GLContext.__init__: already initialized, skipping") - return + @staticmethod + def __new_instance_using_concrete_class_fallback_order() -> 'GLContext': + """Try to init backends in the fallback order. - logger.debug("GLContext.__init__: starting initialization") - - global glfw, EGL - - import time - start_time: float = time.perf_counter() - - self._backend = None - self._window = None - self._egl_display = None - self._egl_context = None - self._egl_surface = None - self._osmesa_ctx = None - self._osmesa_buffer = None - self._vao = None - - self._init_try_backend() - - # Now import OpenGL.GL (after context is current) - logger.debug("GLContext.__init__: importing OpenGL.GL") - _import_opengl() - - # Create VAO (required for core profile, but OSMesa may use compat profile) - logger.debug("GLContext.__init__: creating VAO") - try: - vao = gl.glGenVertexArrays(1) - gl.glBindVertexArray(vao) - self._vao = vao # Only store after successful bind - logger.debug("GLContext.__init__: VAO created successfully") - except Exception as e: - logger.debug(f"GLContext.__init__: VAO creation failed (may be expected for OSMesa): {e}") - # OSMesa with older Mesa may not support VAOs - # Clean up if we created but couldn't bind - if vao: - try: - gl.glDeleteVertexArrays(1, [vao]) - except Exception: - pass - - elapsed = (time.perf_counter() - start_time) * 1000 - - # Log device info - renderer = gl.glGetString(gl.GL_RENDERER) - vendor = gl.glGetString(gl.GL_VENDOR) - version = gl.glGetString(gl.GL_VERSION) - renderer = renderer.decode() if renderer else "Unknown" - vendor = vendor.decode() if vendor else "Unknown" - version = version.decode() if version else "Unknown" - - logger.info(f"GLSL context initialized in {elapsed:.1f}ms ({self._backend}) - {renderer} ({vendor}), GL {version}") - - def _init_try_backend(self): - """Try to init backends in fallback order: GLFW → EGL → OSMesa. Raises RuntimeError on failure. + Called from ``__new__()`` on first attempt to instantiate the singleton. + Raises RuntimeError if none of the backends work. """ - global glfw, EGL + errors: list[tuple[str, Exception]] = [] - errors = [] - self._backend = None - - logger.debug("GLContext.__init__: trying GLFW backend") - try: - self._window, glfw = _init_glfw() - self._backend = "glfw" - logger.debug("GLContext.__init__: GLFW backend succeeded") - return - except Exception as e: - logger.debug(f"GLContext.__init__: GLFW backend failed: {e}") - errors.append(("GLFW", e)) - - logger.debug("GLContext.__init__: trying EGL backend") - try: - self._egl_display, self._egl_context, self._egl_surface, EGL = _init_egl() - self._backend = "egl" - logger.debug("GLContext.__init__: EGL backend succeeded") - return - except Exception as e: - logger.debug(f"GLContext.__init__: EGL backend failed: {e}") - errors.append(("EGL", e)) - - logger.debug("GLContext.__init__: trying OSMesa backend") - try: - self._osmesa_ctx, self._osmesa_buffer = _init_osmesa() - self._backend = "osmesa" - logger.debug("GLContext.__init__: OSMesa backend succeeded") - return - except Exception as e: - logger.debug(f"GLContext.__init__: OSMesa backend failed: {e}") - errors.append(("OSMesa", e)) + for cls in GLContext.__concrete_class_fallback_order(): + name = cls.backend_name() + logger.debug(f"GLContext.__init__: trying {name} backend") + try: + instance: GLContext = object.__new__(cls) + # Since this code is called while in `__new__()`, we need to manually call `__init__()`, too. + # Otherwise, Python would call it only AFTER `__new__()`, causing init errors outside our try-except check. + instance.__init__() + logger.debug(f"GLContext.__init__: {name} backend succeeded. The singleton is: {cls!r}") + logger.info(f"Concrete GLSL context initialized as: {name}") + return instance + except Exception as e: + logger.debug(f"GLContext.__init__: {name} backend failed: {e}") + errors.append((name, e)) # If we still haven't returned, none of the backends succeeded. # Let's raise the error. @@ -458,30 +388,122 @@ class GLContext: f"{platform_help}" ) - def make_current(self): - if self._backend == "glfw": - glfw.make_context_current(self._window) - elif self._backend == "egl": - from OpenGL.EGL import eglMakeCurrent - eglMakeCurrent(self._egl_display, self._egl_surface, self._egl_surface, self._egl_context) - elif self._backend == "osmesa": - from OpenGL.osmesa import OSMesaMakeCurrent - OSMesaMakeCurrent(self._osmesa_ctx, self._osmesa_buffer, gl.GL_UNSIGNED_BYTE, 64, 64) + def __init__(self): + try: + if self.__initialized: + # 99% of the time (after first init) we get here and just return + logger.debug("GLContext.__init__: already initialized, skipping") + return + logger.warning("GLContext.__init__: weird state: the singleton has <__initialized> attribute, but is NOT initialized.") + except AttributeError: + # First instance creation: it was created with `__new__()`, but hasn't been initialized yet + pass + logger.debug("GLContext.__init__: starting initialization") + + self.__initialized: bool = False + self._vao = None + + import time + start_time: float = time.perf_counter() + + self._init_backend_concrete() # must fully initialize backend + + # Now import OpenGL.GL (after context is current) + logger.debug("GLContext.__init__: importing OpenGL.GL") + _import_opengl() + + # Create VAO (required for core profile, but OSMesa may use compat profile) + logger.debug("GLContext.__init__: creating VAO") + try: + vao = gl.glGenVertexArrays(1) + gl.glBindVertexArray(vao) + self._vao = vao # Only store after successful bind + logger.debug("GLContext.__init__: VAO created successfully") + except Exception as e: + logger.debug(f"GLContext.__init__: VAO creation failed (may be expected for OSMesa): {e}") + # OSMesa with older Mesa may not support VAOs + # Clean up if we created but couldn't bind + if vao: + try: + gl.glDeleteVertexArrays(1, [vao]) + except Exception: + pass + + self.__initialized = True + + elapsed = (time.perf_counter() - start_time) * 1000 + + # Log device info + + def gl_string(value) -> str: + string = gl.glGetString(value) + return string.decode() if string else "Unknown" + + renderer, vendor, version = ( + gl_string(x) for x in [gl.GL_RENDERER, gl.GL_VENDOR, gl.GL_VERSION] + ) + logger.info(f"GLSL context initialized in {elapsed:.1f}ms ({self.backend_name()}) - {renderer} ({vendor}), GL {version}") + + @classmethod + def backend_name(cls) -> str: + """Per-concrete-class unique string identifier. Used for log messages.""" + raise NotImplementedError("Must be implemented in a concrete subclass.") + + def _init_backend_concrete(self): + """Actual initialisation hook of a concrete backend. Called mid-init.""" + raise NotImplementedError("Must be implemented in a concrete subclass.") + + def _make_current_concrete(self): + raise NotImplementedError("Must be implemented in a concrete subclass.") + + def make_current(self): + self._make_current_concrete() if self._vao is not None: gl.glBindVertexArray(self._vao) class _GLContextGLFW(GLContext): - pass + """Concrete GLContext using GLFW backend.""" + @classmethod + def backend_name(cls) -> str: + return "GLFW" + + def _init_backend_concrete(self): + global glfw + self._window, glfw = _init_glfw() + + def _make_current_concrete(self): + glfw.make_context_current(self._window) class _GLContextEGL(GLContext): - pass + """Concrete GLContext using EGL backend.""" + @classmethod + def backend_name(cls) -> str: + return "EGL" + + def _init_backend_concrete(self): + global EGL + self._egl_display, self._egl_context, self._egl_surface, EGL = _init_egl() + + def _make_current_concrete(self): + from OpenGL.EGL import eglMakeCurrent + eglMakeCurrent(self._egl_display, self._egl_surface, self._egl_surface, self._egl_context) class _GLContextOSMesa(GLContext): - pass + """Concrete GLContext using OSMesa backend.""" + @classmethod + def backend_name(cls) -> str: + return "OSMesa" + + def _init_backend_concrete(self): + self._osmesa_ctx, self._osmesa_buffer = _init_osmesa() + + def _make_current_concrete(self): + from OpenGL.osmesa import OSMesaMakeCurrent + OSMesaMakeCurrent(self._osmesa_ctx, self._osmesa_buffer, gl.GL_UNSIGNED_BYTE, 64, 64) # ---------------------------------------------------------- From 40fa050d3b37d2c5057fa870f8c1b65dc4d05770 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:12:38 -0300 Subject: [PATCH 05/15] turn all backend-init functions into static class methods --- comfy_extras/nodes_glsl.py | 342 +++++++++++++++++++------------------ 1 file changed, 174 insertions(+), 168 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index 69b68eccf..dcf5b2e1e 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -69,17 +69,6 @@ logger.debug("nodes_glsl: running _check_opengl_availability at import time") _check_opengl_availability() -def _import_opengl(): - """Import OpenGL module. Called after context is created.""" - global gl - if gl is None: - logger.debug("_import_opengl: importing OpenGL.GL") - import OpenGL.GL as _gl - gl = _gl - logger.debug("_import_opengl: import completed") - return gl - - class SizeModeInput(TypedDict): size_mode: str width: int @@ -157,158 +146,6 @@ def _detect_pass_count(source: str) -> int: return 1 -def _init_glfw(): - """Initialize GLFW. Returns (window, glfw_module). Raises RuntimeError on failure.""" - logger.debug("_init_glfw: starting") - # On macOS, glfw.init() must be called from main thread or it hangs forever - if sys.platform == "darwin": - logger.debug("_init_glfw: skipping on macOS") - raise RuntimeError("GLFW backend not supported on macOS") - - logger.debug("_init_glfw: importing glfw module") - import glfw as _glfw - - logger.debug("_init_glfw: calling glfw.init()") - if not _glfw.init(): - raise RuntimeError("glfw.init() failed") - - try: - logger.debug("_init_glfw: setting window hints") - _glfw.window_hint(_glfw.VISIBLE, _glfw.FALSE) - _glfw.window_hint(_glfw.CONTEXT_VERSION_MAJOR, 3) - _glfw.window_hint(_glfw.CONTEXT_VERSION_MINOR, 3) - _glfw.window_hint(_glfw.OPENGL_PROFILE, _glfw.OPENGL_CORE_PROFILE) - - logger.debug("_init_glfw: calling create_window()") - window = _glfw.create_window(64, 64, "ComfyUI GLSL", None, None) - if not window: - raise RuntimeError("glfw.create_window() failed") - - logger.debug("_init_glfw: calling make_context_current()") - _glfw.make_context_current(window) - logger.debug("_init_glfw: completed successfully") - return window, _glfw - except Exception: - logger.debug("_init_glfw: failed, terminating glfw") - _glfw.terminate() - raise - - -def _init_egl(): - """Initialize EGL for headless rendering. Returns (display, context, surface, EGL_module). Raises RuntimeError on failure.""" - logger.debug("_init_egl: starting") - from OpenGL import EGL as _EGL - from OpenGL.EGL import ( - eglGetDisplay, eglInitialize, eglChooseConfig, eglCreateContext, - eglMakeCurrent, eglCreatePbufferSurface, eglBindAPI, - eglTerminate, eglDestroyContext, eglDestroySurface, - EGL_DEFAULT_DISPLAY, EGL_NO_CONTEXT, EGL_NONE, - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, - EGL_RED_SIZE, EGL_GREEN_SIZE, EGL_BLUE_SIZE, EGL_ALPHA_SIZE, EGL_DEPTH_SIZE, - EGL_WIDTH, EGL_HEIGHT, EGL_OPENGL_API, - ) - logger.debug("_init_egl: imports completed") - - display = None - context = None - 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()") - major, minor = _EGL.EGLint(), _EGL.EGLint() - if not eglInitialize(display, major, minor): - display = None # Not initialized, don't terminate - raise RuntimeError("eglInitialize() failed") - logger.debug(f"_init_egl: EGL version {major.value}.{minor.value}") - - config_attribs = [ - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, - EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, - EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, - EGL_DEPTH_SIZE, 0, EGL_NONE - ] - configs = (_EGL.EGLConfig * 1)() - num_configs = _EGL.EGLint() - if not eglChooseConfig(display, config_attribs, configs, 1, num_configs) or num_configs.value == 0: - raise RuntimeError("eglChooseConfig() failed") - config = configs[0] - logger.debug(f"_init_egl: config chosen, num_configs={num_configs.value}") - - if not eglBindAPI(EGL_OPENGL_API): - raise RuntimeError("eglBindAPI() failed") - - logger.debug("_init_egl: calling eglCreateContext()") - context_attribs = [ - _EGL.EGL_CONTEXT_MAJOR_VERSION, 3, - _EGL.EGL_CONTEXT_MINOR_VERSION, 3, - _EGL.EGL_CONTEXT_OPENGL_PROFILE_MASK, _EGL.EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, - EGL_NONE - ] - context = eglCreateContext(display, config, EGL_NO_CONTEXT, context_attribs) - if context == EGL_NO_CONTEXT: - raise RuntimeError("eglCreateContext() failed") - - logger.debug("_init_egl: calling eglCreatePbufferSurface()") - pbuffer_attribs = [EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE] - surface = eglCreatePbufferSurface(display, config, pbuffer_attribs) - if surface == _EGL.EGL_NO_SURFACE: - raise RuntimeError("eglCreatePbufferSurface() failed") - - logger.debug("_init_egl: calling eglMakeCurrent()") - if not eglMakeCurrent(display, surface, surface, context): - raise RuntimeError("eglMakeCurrent() failed") - - logger.debug("_init_egl: completed successfully") - return display, context, surface, _EGL - - except Exception: - logger.debug("_init_egl: failed, cleaning up") - # Clean up any resources on failure - if surface is not None: - eglDestroySurface(display, surface) - if context is not None: - eglDestroyContext(display, context) - if display is not None: - eglTerminate(display) - raise - - -def _init_osmesa(): - """Initialize OSMesa for software rendering. Returns (context, buffer). Raises RuntimeError on failure.""" - import ctypes - - logger.debug("_init_osmesa: starting") - os.environ["PYOPENGL_PLATFORM"] = "osmesa" - - logger.debug("_init_osmesa: importing OpenGL.osmesa") - from OpenGL import GL as _gl - from OpenGL.osmesa import ( - OSMesaCreateContextExt, OSMesaMakeCurrent, OSMesaDestroyContext, - OSMESA_RGBA, - ) - logger.debug("_init_osmesa: imports completed") - - ctx = OSMesaCreateContextExt(OSMESA_RGBA, 24, 0, 0, None) - if not ctx: - raise RuntimeError("OSMesaCreateContextExt() failed") - - width, height = 64, 64 - buffer = (ctypes.c_ubyte * (width * height * 4))() - - logger.debug("_init_osmesa: calling OSMesaMakeCurrent()") - if not OSMesaMakeCurrent(ctx, buffer, _gl.GL_UNSIGNED_BYTE, width, height): - OSMesaDestroyContext(ctx) - raise RuntimeError("OSMesaMakeCurrent() failed") - - logger.debug("_init_osmesa: completed successfully") - return ctx, buffer - - # ---------------------------------------------------------- @@ -411,7 +248,8 @@ class GLContext: # Now import OpenGL.GL (after context is current) logger.debug("GLContext.__init__: importing OpenGL.GL") - _import_opengl() + self.__import_opengl() + gl = self._gl # Create VAO (required for core profile, but OSMesa may use compat profile) logger.debug("GLContext.__init__: creating VAO") @@ -431,6 +269,7 @@ class GLContext: pass self.__initialized = True + self._glBindVertexArray = gl.glBindVertexArray elapsed = (time.perf_counter() - start_time) * 1000 @@ -445,6 +284,18 @@ class GLContext: ) logger.info(f"GLSL context initialized in {elapsed:.1f}ms ({self.backend_name()}) - {renderer} ({vendor}), GL {version}") + def __import_opengl(self): + """Import OpenGL module. Called after context is created.""" + global gl + if gl is not None: + return + + logger.debug("__import_opengl: importing OpenGL.GL") + import OpenGL.GL as _gl + gl = _gl + self._gl = _gl + logger.debug("__import_opengl: import completed") + @classmethod def backend_name(cls) -> str: """Per-concrete-class unique string identifier. Used for log messages.""" @@ -460,8 +311,9 @@ class GLContext: def make_current(self): self._make_current_concrete() if self._vao is not None: - gl.glBindVertexArray(self._vao) + self._glBindVertexArray(self._vao) +########## class _GLContextGLFW(GLContext): """Concrete GLContext using GLFW backend.""" @@ -471,11 +323,49 @@ class _GLContextGLFW(GLContext): def _init_backend_concrete(self): global glfw - self._window, glfw = _init_glfw() + self._window, glfw = self.__init_glfw() def _make_current_concrete(self): glfw.make_context_current(self._window) + @staticmethod + def __init_glfw(): + """Initialize GLFW. Returns (window, glfw_module). Raises RuntimeError on failure.""" + logger.debug("__init_glfw: starting") + # On macOS, glfw.init() must be called from main thread or it hangs forever + if sys.platform == "darwin": + logger.debug("__init_glfw: skipping on macOS") + raise RuntimeError("GLFW backend not supported on macOS") + + logger.debug("__init_glfw: importing glfw module") + import glfw as _glfw + + logger.debug("__init_glfw: calling glfw.init()") + if not _glfw.init(): + raise RuntimeError("glfw.init() failed") + + try: + logger.debug("__init_glfw: setting window hints") + _glfw.window_hint(_glfw.VISIBLE, _glfw.FALSE) + _glfw.window_hint(_glfw.CONTEXT_VERSION_MAJOR, 3) + _glfw.window_hint(_glfw.CONTEXT_VERSION_MINOR, 3) + _glfw.window_hint(_glfw.OPENGL_PROFILE, _glfw.OPENGL_CORE_PROFILE) + + logger.debug("__init_glfw: calling create_window()") + window = _glfw.create_window(64, 64, "ComfyUI GLSL", None, None) + if not window: + raise RuntimeError("glfw.create_window() failed") + + logger.debug("__init_glfw: calling make_context_current()") + _glfw.make_context_current(window) + logger.debug("__init_glfw: completed successfully") + return window, _glfw + except Exception: + logger.debug("__init_glfw: failed, terminating glfw") + _glfw.terminate() + raise + +########## class _GLContextEGL(GLContext): """Concrete GLContext using EGL backend.""" @@ -485,12 +375,97 @@ class _GLContextEGL(GLContext): def _init_backend_concrete(self): global EGL - self._egl_display, self._egl_context, self._egl_surface, EGL = _init_egl() + self._egl_display, self._egl_context, self._egl_surface, EGL = self.__init_egl() def _make_current_concrete(self): from OpenGL.EGL import eglMakeCurrent eglMakeCurrent(self._egl_display, self._egl_surface, self._egl_surface, self._egl_context) + @staticmethod + def __init_egl(): + """Initialize EGL for headless rendering. Returns (display, context, surface, EGL_module). Raises RuntimeError on failure.""" + logger.debug("__init_egl: starting") + from OpenGL import EGL as _EGL + from OpenGL.EGL import ( + eglGetDisplay, eglInitialize, eglChooseConfig, eglCreateContext, + eglMakeCurrent, eglCreatePbufferSurface, eglBindAPI, + eglTerminate, eglDestroyContext, eglDestroySurface, + EGL_DEFAULT_DISPLAY, EGL_NO_CONTEXT, EGL_NONE, + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, + EGL_RED_SIZE, EGL_GREEN_SIZE, EGL_BLUE_SIZE, EGL_ALPHA_SIZE, EGL_DEPTH_SIZE, + EGL_WIDTH, EGL_HEIGHT, EGL_OPENGL_API, + ) + logger.debug("__init_egl: imports completed") + + display = None + context = None + 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()") + major, minor = _EGL.EGLint(), _EGL.EGLint() + if not eglInitialize(display, major, minor): + display = None # Not initialized, don't terminate + raise RuntimeError("eglInitialize() failed") + logger.debug(f"__init_egl: EGL version {major.value}.{minor.value}") + + config_attribs = [ + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, + EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, + EGL_DEPTH_SIZE, 0, EGL_NONE + ] + configs = (_EGL.EGLConfig * 1)() + num_configs = _EGL.EGLint() + if not eglChooseConfig(display, config_attribs, configs, 1, num_configs) or num_configs.value == 0: + raise RuntimeError("eglChooseConfig() failed") + config = configs[0] + logger.debug(f"__init_egl: config chosen, num_configs={num_configs.value}") + + if not eglBindAPI(EGL_OPENGL_API): + raise RuntimeError("eglBindAPI() failed") + + logger.debug("__init_egl: calling eglCreateContext()") + context_attribs = [ + _EGL.EGL_CONTEXT_MAJOR_VERSION, 3, + _EGL.EGL_CONTEXT_MINOR_VERSION, 3, + _EGL.EGL_CONTEXT_OPENGL_PROFILE_MASK, _EGL.EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, + EGL_NONE + ] + context = eglCreateContext(display, config, EGL_NO_CONTEXT, context_attribs) + if context == EGL_NO_CONTEXT: + raise RuntimeError("eglCreateContext() failed") + + logger.debug("__init_egl: calling eglCreatePbufferSurface()") + pbuffer_attribs = [EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE] + surface = eglCreatePbufferSurface(display, config, pbuffer_attribs) + if surface == _EGL.EGL_NO_SURFACE: + raise RuntimeError("eglCreatePbufferSurface() failed") + + logger.debug("__init_egl: calling eglMakeCurrent()") + if not eglMakeCurrent(display, surface, surface, context): + raise RuntimeError("eglMakeCurrent() failed") + + logger.debug("__init_egl: completed successfully") + return display, context, surface, _EGL + + except Exception: + logger.debug("__init_egl: failed, cleaning up") + # Clean up any resources on failure + if surface is not None: + eglDestroySurface(display, surface) + if context is not None: + eglDestroyContext(display, context) + if display is not None: + eglTerminate(display) + raise + +########## class _GLContextOSMesa(GLContext): """Concrete GLContext using OSMesa backend.""" @@ -499,12 +474,43 @@ class _GLContextOSMesa(GLContext): return "OSMesa" def _init_backend_concrete(self): - self._osmesa_ctx, self._osmesa_buffer = _init_osmesa() + self._osmesa_ctx, self._osmesa_buffer = self.__init_osmesa() def _make_current_concrete(self): from OpenGL.osmesa import OSMesaMakeCurrent OSMesaMakeCurrent(self._osmesa_ctx, self._osmesa_buffer, gl.GL_UNSIGNED_BYTE, 64, 64) + @staticmethod + def __init_osmesa(): + """Initialize OSMesa for software rendering. Returns (context, buffer). Raises RuntimeError on failure.""" + import ctypes + + logger.debug("__init_osmesa: starting") + os.environ["PYOPENGL_PLATFORM"] = "osmesa" + + logger.debug("__init_osmesa: importing OpenGL.osmesa") + from OpenGL import GL as _gl + from OpenGL.osmesa import ( + OSMesaCreateContextExt, OSMesaMakeCurrent, OSMesaDestroyContext, + OSMESA_RGBA, + ) + logger.debug("__init_osmesa: imports completed") + + ctx = OSMesaCreateContextExt(OSMESA_RGBA, 24, 0, 0, None) + if not ctx: + raise RuntimeError("OSMesaCreateContextExt() failed") + + width, height = 64, 64 + buffer = (ctypes.c_ubyte * (width * height * 4))() + + logger.debug("__init_osmesa: calling OSMesaMakeCurrent()") + if not OSMesaMakeCurrent(ctx, buffer, _gl.GL_UNSIGNED_BYTE, width, height): + OSMesaDestroyContext(ctx) + raise RuntimeError("OSMesaMakeCurrent() failed") + + logger.debug("__init_osmesa: completed successfully") + return ctx, buffer + # ---------------------------------------------------------- From d2d750d9c466bbcb5d1b3d0c6fe40206ae49b853 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Mon, 23 Feb 2026 01:37:25 -0300 Subject: [PATCH 06/15] globals -> concrete backend attribs --- comfy_extras/nodes_glsl.py | 189 +++++++++++++++++-------------------- 1 file changed, 87 insertions(+), 102 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index dcf5b2e1e..6f9e804b5 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -18,8 +18,6 @@ logger = logging.getLogger(__name__) # OpenGL modules - initialized lazily when context is created gl = None -glfw = None -EGL = None def _check_opengl_availability(): @@ -322,49 +320,47 @@ class _GLContextGLFW(GLContext): return "GLFW" def _init_backend_concrete(self): - global glfw - self._window, glfw = self.__init_glfw() - - def _make_current_concrete(self): - glfw.make_context_current(self._window) - - @staticmethod - def __init_glfw(): - """Initialize GLFW. Returns (window, glfw_module). Raises RuntimeError on failure.""" - logger.debug("__init_glfw: starting") + """Initialize GLFW. Raises RuntimeError on failure.""" + logger.debug("_init_backend_concrete (GLFW): starting") # On macOS, glfw.init() must be called from main thread or it hangs forever if sys.platform == "darwin": - logger.debug("__init_glfw: skipping on macOS") + logger.debug("_init_backend_concrete (GLFW): skipping on macOS") raise RuntimeError("GLFW backend not supported on macOS") - logger.debug("__init_glfw: importing glfw module") - import glfw as _glfw + logger.debug("_init_backend_concrete (GLFW): importing glfw module") + import glfw - logger.debug("__init_glfw: calling glfw.init()") - if not _glfw.init(): + logger.debug("_init_backend_concrete (GLFW): calling glfw.init()") + if not glfw.init(): raise RuntimeError("glfw.init() failed") try: - logger.debug("__init_glfw: setting window hints") - _glfw.window_hint(_glfw.VISIBLE, _glfw.FALSE) - _glfw.window_hint(_glfw.CONTEXT_VERSION_MAJOR, 3) - _glfw.window_hint(_glfw.CONTEXT_VERSION_MINOR, 3) - _glfw.window_hint(_glfw.OPENGL_PROFILE, _glfw.OPENGL_CORE_PROFILE) + logger.debug("_init_backend_concrete (GLFW): setting window hints") + glfw.window_hint(glfw.VISIBLE, glfw.FALSE) + glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3) + glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3) + glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE) - logger.debug("__init_glfw: calling create_window()") - window = _glfw.create_window(64, 64, "ComfyUI GLSL", None, None) + logger.debug("_init_backend_concrete (GLFW): calling create_window()") + window = glfw.create_window(64, 64, "ComfyUI GLSL", None, None) if not window: raise RuntimeError("glfw.create_window() failed") - logger.debug("__init_glfw: calling make_context_current()") - _glfw.make_context_current(window) - logger.debug("__init_glfw: completed successfully") - return window, _glfw + logger.debug("_init_backend_concrete (GLFW): calling make_context_current()") + glfw.make_context_current(window) except Exception: - logger.debug("__init_glfw: failed, terminating glfw") - _glfw.terminate() + logger.debug("_init_backend_concrete (GLFW): failed, terminating glfw") + glfw.terminate() raise + self._window = window + self._glfw = glfw + + logger.debug("_init_backend_concrete (GLFW): completed successfully") + + def _make_current_concrete(self): + self._glfw.make_context_current(self._window) + ########## class _GLContextEGL(GLContext): @@ -374,97 +370,88 @@ class _GLContextEGL(GLContext): return "EGL" def _init_backend_concrete(self): - global EGL - self._egl_display, self._egl_context, self._egl_surface, EGL = self.__init_egl() - - def _make_current_concrete(self): - from OpenGL.EGL import eglMakeCurrent - eglMakeCurrent(self._egl_display, self._egl_surface, self._egl_surface, self._egl_context) - - @staticmethod - def __init_egl(): - """Initialize EGL for headless rendering. Returns (display, context, surface, EGL_module). Raises RuntimeError on failure.""" - logger.debug("__init_egl: starting") - from OpenGL import EGL as _EGL - from OpenGL.EGL import ( - eglGetDisplay, eglInitialize, eglChooseConfig, eglCreateContext, - eglMakeCurrent, eglCreatePbufferSurface, eglBindAPI, - eglTerminate, eglDestroyContext, eglDestroySurface, - EGL_DEFAULT_DISPLAY, EGL_NO_CONTEXT, EGL_NONE, - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, - EGL_RED_SIZE, EGL_GREEN_SIZE, EGL_BLUE_SIZE, EGL_ALPHA_SIZE, EGL_DEPTH_SIZE, - EGL_WIDTH, EGL_HEIGHT, EGL_OPENGL_API, - ) - logger.debug("__init_egl: imports completed") + """Initialize EGL for headless rendering. Raises RuntimeError on failure.""" + logger.debug("_init_backend_concrete (EGL): starting") + from OpenGL import EGL + logger.debug("_init_backend_concrete (EGL): imports completed") display = None context = None surface = None try: - logger.debug("__init_egl: calling eglGetDisplay()") - display = eglGetDisplay(EGL_DEFAULT_DISPLAY) - if display == _EGL.EGL_NO_DISPLAY: + logger.debug("_init_backend_concrete (EGL): calling eglGetDisplay()") + display = EGL.eglGetDisplay(EGL.EGL_DEFAULT_DISPLAY) + if display == EGL.EGL_NO_DISPLAY: raise RuntimeError("eglGetDisplay() failed") - logger.debug("__init_egl: calling eglInitialize()") - major, minor = _EGL.EGLint(), _EGL.EGLint() - if not eglInitialize(display, major, minor): + logger.debug("_init_backend_concrete (EGL): calling eglInitialize()") + major, minor = EGL.EGLint(), EGL.EGLint() + if not EGL.eglInitialize(display, major, minor): display = None # Not initialized, don't terminate raise RuntimeError("eglInitialize() failed") - logger.debug(f"__init_egl: EGL version {major.value}.{minor.value}") + logger.debug(f"_init_backend_concrete (EGL): EGL version {major.value}.{minor.value}") config_attribs = [ - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, - EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, - EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8, - EGL_DEPTH_SIZE, 0, EGL_NONE + EGL.EGL_SURFACE_TYPE, EGL.EGL_PBUFFER_BIT, + EGL.EGL_RENDERABLE_TYPE, EGL.EGL_OPENGL_BIT, + EGL.EGL_RED_SIZE, 8, EGL.EGL_GREEN_SIZE, 8, EGL.EGL_BLUE_SIZE, 8, EGL.EGL_ALPHA_SIZE, 8, + EGL.EGL_DEPTH_SIZE, 0, EGL.EGL_NONE ] - configs = (_EGL.EGLConfig * 1)() - num_configs = _EGL.EGLint() - if not eglChooseConfig(display, config_attribs, configs, 1, num_configs) or num_configs.value == 0: + configs = (EGL.EGLConfig * 1)() + num_configs = EGL.EGLint() + if not EGL.eglChooseConfig(display, config_attribs, configs, 1, num_configs) or num_configs.value == 0: raise RuntimeError("eglChooseConfig() failed") config = configs[0] - logger.debug(f"__init_egl: config chosen, num_configs={num_configs.value}") + logger.debug(f"_init_backend_concrete (EGL): config chosen, num_configs={num_configs.value}") - if not eglBindAPI(EGL_OPENGL_API): + if not EGL.eglBindAPI(EGL.EGL_OPENGL_API): raise RuntimeError("eglBindAPI() failed") - logger.debug("__init_egl: calling eglCreateContext()") + logger.debug("_init_backend_concrete (EGL): calling eglCreateContext()") context_attribs = [ - _EGL.EGL_CONTEXT_MAJOR_VERSION, 3, - _EGL.EGL_CONTEXT_MINOR_VERSION, 3, - _EGL.EGL_CONTEXT_OPENGL_PROFILE_MASK, _EGL.EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, - EGL_NONE + EGL.EGL_CONTEXT_MAJOR_VERSION, 3, + EGL.EGL_CONTEXT_MINOR_VERSION, 3, + EGL.EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL.EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, + EGL.EGL_NONE ] - context = eglCreateContext(display, config, EGL_NO_CONTEXT, context_attribs) - if context == EGL_NO_CONTEXT: + context = EGL.eglCreateContext(display, config, EGL.EGL_NO_CONTEXT, context_attribs) + if context == EGL.EGL_NO_CONTEXT: raise RuntimeError("eglCreateContext() failed") - logger.debug("__init_egl: calling eglCreatePbufferSurface()") - pbuffer_attribs = [EGL_WIDTH, 64, EGL_HEIGHT, 64, EGL_NONE] - surface = eglCreatePbufferSurface(display, config, pbuffer_attribs) - if surface == _EGL.EGL_NO_SURFACE: + logger.debug("_init_backend_concrete (EGL): calling eglCreatePbufferSurface()") + pbuffer_attribs = [EGL.EGL_WIDTH, 64, EGL.EGL_HEIGHT, 64, EGL.EGL_NONE] + surface = EGL.eglCreatePbufferSurface(display, config, pbuffer_attribs) + if surface == EGL.EGL_NO_SURFACE: raise RuntimeError("eglCreatePbufferSurface() failed") - logger.debug("__init_egl: calling eglMakeCurrent()") - if not eglMakeCurrent(display, surface, surface, context): + logger.debug("_init_backend_concrete (EGL): calling eglMakeCurrent()") + if not EGL.eglMakeCurrent(display, surface, surface, context): raise RuntimeError("eglMakeCurrent() failed") - logger.debug("__init_egl: completed successfully") - return display, context, surface, _EGL - except Exception: - logger.debug("__init_egl: failed, cleaning up") + logger.debug("_init_backend_concrete (EGL): failed, cleaning up") # Clean up any resources on failure if surface is not None: - eglDestroySurface(display, surface) + EGL.eglDestroySurface(display, surface) if context is not None: - eglDestroyContext(display, context) + EGL.eglDestroyContext(display, context) if display is not None: - eglTerminate(display) + EGL.eglTerminate(display) raise + self._egl_display = display + self._egl_context = context + self._egl_surface = surface + + self._EGL = EGL + self._eglMakeCurrent = EGL.eglMakeCurrent + + logger.debug("_init_backend_concrete (EGL): completed successfully") + + def _make_current_concrete(self): + self._eglMakeCurrent(self._egl_display, self._egl_surface, self._egl_surface, self._egl_context) + ########## class _GLContextOSMesa(GLContext): @@ -474,27 +461,19 @@ class _GLContextOSMesa(GLContext): return "OSMesa" def _init_backend_concrete(self): - self._osmesa_ctx, self._osmesa_buffer = self.__init_osmesa() - - def _make_current_concrete(self): - from OpenGL.osmesa import OSMesaMakeCurrent - OSMesaMakeCurrent(self._osmesa_ctx, self._osmesa_buffer, gl.GL_UNSIGNED_BYTE, 64, 64) - - @staticmethod - def __init_osmesa(): """Initialize OSMesa for software rendering. Returns (context, buffer). Raises RuntimeError on failure.""" import ctypes - logger.debug("__init_osmesa: starting") + logger.debug("_init_backend_concrete (OSMesa): starting") os.environ["PYOPENGL_PLATFORM"] = "osmesa" - logger.debug("__init_osmesa: importing OpenGL.osmesa") + logger.debug("_init_backend_concrete (OSMesa): importing OpenGL.osmesa") from OpenGL import GL as _gl from OpenGL.osmesa import ( OSMesaCreateContextExt, OSMesaMakeCurrent, OSMesaDestroyContext, OSMESA_RGBA, ) - logger.debug("__init_osmesa: imports completed") + logger.debug("_init_backend_concrete (OSMesa): imports completed") ctx = OSMesaCreateContextExt(OSMESA_RGBA, 24, 0, 0, None) if not ctx: @@ -503,13 +482,19 @@ class _GLContextOSMesa(GLContext): width, height = 64, 64 buffer = (ctypes.c_ubyte * (width * height * 4))() - logger.debug("__init_osmesa: calling OSMesaMakeCurrent()") + logger.debug("_init_backend_concrete (OSMesa): calling OSMesaMakeCurrent()") if not OSMesaMakeCurrent(ctx, buffer, _gl.GL_UNSIGNED_BYTE, width, height): OSMesaDestroyContext(ctx) raise RuntimeError("OSMesaMakeCurrent() failed") - logger.debug("__init_osmesa: completed successfully") - return ctx, buffer + self._osmesa_ctx = ctx + self._osmesa_buffer = buffer + + logger.debug("_init_backend_concrete (OSMesa): completed successfully") + + def _make_current_concrete(self): + from OpenGL.osmesa import OSMesaMakeCurrent + OSMesaMakeCurrent(self._osmesa_ctx, self._osmesa_buffer, self._gl.GL_UNSIGNED_BYTE, 64, 64) # ---------------------------------------------------------- From 0a99f6828fbc8a856e9c48150754a11916d20fa0 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:35:05 -0300 Subject: [PATCH 07/15] change separator-comment lines --- comfy_extras/nodes_glsl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index 6f9e804b5..2f029a76e 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -144,7 +144,7 @@ def _detect_pass_count(source: str) -> int: return 1 -# ---------------------------------------------------------- +############################################################ class GLContext: @@ -497,7 +497,7 @@ class _GLContextOSMesa(GLContext): OSMesaMakeCurrent(self._osmesa_ctx, self._osmesa_buffer, self._gl.GL_UNSIGNED_BYTE, 64, 64) -# ---------------------------------------------------------- +############################################################ def _compile_shader(source: str, shader_type: int) -> int: From 2b63d8c26a6ccd24828fa99a0793b78621ca4cc4 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:37:11 -0300 Subject: [PATCH 08/15] `_compile_shader()` -> to `GLContext` method --- comfy_extras/nodes_glsl.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index 2f029a76e..19a6251f4 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -311,6 +311,21 @@ class GLContext: if self._vao is not None: self._glBindVertexArray(self._vao) + def compile_shader(self, source: str, shader_type: int) -> int: + """Compile a shader and return its ID.""" + gl = self._gl + + shader = gl.glCreateShader(shader_type) + gl.glShaderSource(shader, source) + gl.glCompileShader(shader) + + if gl.glGetShaderiv(shader, gl.GL_COMPILE_STATUS) != gl.GL_TRUE: + error = gl.glGetShaderInfoLog(shader).decode() + gl.glDeleteShader(shader) + raise RuntimeError(f"Shader compilation failed:\n{error}") + + return shader + ########## class _GLContextGLFW(GLContext): @@ -500,25 +515,12 @@ class _GLContextOSMesa(GLContext): ############################################################ -def _compile_shader(source: str, shader_type: int) -> int: - """Compile a shader and return its ID.""" - shader = gl.glCreateShader(shader_type) - gl.glShaderSource(shader, source) - gl.glCompileShader(shader) - - if gl.glGetShaderiv(shader, gl.GL_COMPILE_STATUS) != gl.GL_TRUE: - error = gl.glGetShaderInfoLog(shader).decode() - gl.glDeleteShader(shader) - raise RuntimeError(f"Shader compilation failed:\n{error}") - - return shader - - def _create_program(vertex_source: str, fragment_source: str) -> int: """Create and link a shader program.""" - vertex_shader = _compile_shader(vertex_source, gl.GL_VERTEX_SHADER) + ctx = GLContext() + vertex_shader = ctx.compile_shader(vertex_source, gl.GL_VERTEX_SHADER) try: - fragment_shader = _compile_shader(fragment_source, gl.GL_FRAGMENT_SHADER) + fragment_shader = ctx.compile_shader(fragment_source, gl.GL_FRAGMENT_SHADER) except RuntimeError: gl.glDeleteShader(vertex_shader) raise From c9442a21a792bbdd95942088dea2d8238fbc3fed Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:38:48 -0300 Subject: [PATCH 09/15] `_create_program()` -> to `GLContext` method --- comfy_extras/nodes_glsl.py | 55 +++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index 19a6251f4..5a2bc0f76 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -326,6 +326,33 @@ class GLContext: return shader + def create_program(self, vertex_source: str, fragment_source: str) -> int: + """Create and link a shader program.""" + gl = self._gl + compile = self.compile_shader + + vertex_shader = compile(vertex_source, gl.GL_VERTEX_SHADER) + try: + fragment_shader = compile(fragment_source, gl.GL_FRAGMENT_SHADER) + except RuntimeError: + gl.glDeleteShader(vertex_shader) + raise + + program = gl.glCreateProgram() + gl.glAttachShader(program, vertex_shader) + gl.glAttachShader(program, fragment_shader) + gl.glLinkProgram(program) + + gl.glDeleteShader(vertex_shader) + gl.glDeleteShader(fragment_shader) + + if gl.glGetProgramiv(program, gl.GL_LINK_STATUS) != gl.GL_TRUE: + error = gl.glGetProgramInfoLog(program).decode() + gl.glDeleteProgram(program) + raise RuntimeError(f"Program linking failed:\n{error}") + + return program + ########## class _GLContextGLFW(GLContext): @@ -515,32 +542,6 @@ class _GLContextOSMesa(GLContext): ############################################################ -def _create_program(vertex_source: str, fragment_source: str) -> int: - """Create and link a shader program.""" - ctx = GLContext() - vertex_shader = ctx.compile_shader(vertex_source, gl.GL_VERTEX_SHADER) - try: - fragment_shader = ctx.compile_shader(fragment_source, gl.GL_FRAGMENT_SHADER) - except RuntimeError: - gl.glDeleteShader(vertex_shader) - raise - - program = gl.glCreateProgram() - gl.glAttachShader(program, vertex_shader) - gl.glAttachShader(program, fragment_shader) - gl.glLinkProgram(program) - - gl.glDeleteShader(vertex_shader) - gl.glDeleteShader(fragment_shader) - - if gl.glGetProgramiv(program, gl.GL_LINK_STATUS) != gl.GL_TRUE: - error = gl.glGetProgramInfoLog(program).decode() - gl.glDeleteProgram(program) - raise RuntimeError(f"Program linking failed:\n{error}") - - return program - - def _render_shader_batch( fragment_code: str, width: int, @@ -597,7 +598,7 @@ def _render_shader_batch( try: # Compile shaders (once for all batches) try: - program = _create_program(VERTEX_SHADER, fragment_source) + program = ctx.create_program(VERTEX_SHADER, fragment_source) except RuntimeError: logger.error(f"Fragment shader:\n{fragment_source}") raise From 562f4ef48c3e7c18ec83c03c7676185fc528c3e2 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:09:58 -0300 Subject: [PATCH 10/15] `_render_shader_batch()` -> to `GLContext` method (part 1: add) --- comfy_extras/nodes_glsl.py | 242 +++++++++++++++++++++++++++++++++++++ 1 file changed, 242 insertions(+) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index 5a2bc0f76..e0dd03c5e 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -353,6 +353,248 @@ class GLContext: return program + def render_shader_batch( + self, + fragment_code: str, + width: int, + height: int, + image_batches: list[list[np.ndarray]], + floats: list[float], + ints: list[int], + ) -> list[list[np.ndarray]]: + """ + Render a fragment shader for multiple batches efficiently. + + Compiles shader once, reuses framebuffer/textures across batches. + Supports multi-pass rendering via #pragma passes N directive. + + Args: + fragment_code: User's fragment shader code + width: Output width + height: Output height + image_batches: List of batches, each batch is a list of input images (H, W, C) float32 [0,1] + floats: List of float uniforms + ints: List of int uniforms + + Returns: + List of batch outputs, each is a list of output images (H, W, 4) float32 [0,1] + """ + import time + + gl = self._gl + + start_time = time.perf_counter() + + if not image_batches: + return [] + + self.make_current() + + # Convert from GLSL ES to desktop GLSL 330 + fragment_source = _convert_es_to_desktop(fragment_code) + + # Detect how many outputs the shader actually uses + num_outputs = _detect_output_count(fragment_code) + + # Detect multi-pass rendering + num_passes = _detect_pass_count(fragment_code) + + # Track resources for cleanup + program = None + fbo = None + output_textures = [] + input_textures = [] + ping_pong_textures = [] + ping_pong_fbos = [] + + num_inputs = len(image_batches[0]) + + try: + # Compile shaders (once for all batches) + try: + program = self.create_program(VERTEX_SHADER, fragment_source) + except RuntimeError: + logger.error(f"Fragment shader:\n{fragment_source}") + raise + + gl.glUseProgram(program) + + # Create framebuffer with only the needed color attachments + fbo = gl.glGenFramebuffers(1) + gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo) + + draw_buffers = [] + for i in range(num_outputs): + tex = gl.glGenTextures(1) + output_textures.append(tex) + gl.glBindTexture(gl.GL_TEXTURE_2D, tex) + gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA32F, width, height, 0, gl.GL_RGBA, gl.GL_FLOAT, None) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) + gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0 + i, gl.GL_TEXTURE_2D, tex, 0) + draw_buffers.append(gl.GL_COLOR_ATTACHMENT0 + i) + + gl.glDrawBuffers(num_outputs, draw_buffers) + + if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE: + raise RuntimeError("Framebuffer is not complete") + + # Create ping-pong resources for multi-pass rendering + if num_passes > 1: + for _ in range(2): + pp_tex = gl.glGenTextures(1) + ping_pong_textures.append(pp_tex) + gl.glBindTexture(gl.GL_TEXTURE_2D, pp_tex) + gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA32F, width, height, 0, gl.GL_RGBA, gl.GL_FLOAT, None) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE) + + pp_fbo = gl.glGenFramebuffers(1) + ping_pong_fbos.append(pp_fbo) + gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, pp_fbo) + gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, pp_tex, 0) + gl.glDrawBuffers(1, [gl.GL_COLOR_ATTACHMENT0]) + + if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE: + raise RuntimeError("Ping-pong framebuffer is not complete") + + # Create input textures (reused for all batches) + for i in range(num_inputs): + tex = gl.glGenTextures(1) + input_textures.append(tex) + gl.glActiveTexture(gl.GL_TEXTURE0 + i) + gl.glBindTexture(gl.GL_TEXTURE_2D, tex) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE) + + loc = gl.glGetUniformLocation(program, f"u_image{i}") + if loc >= 0: + gl.glUniform1i(loc, i) + + # Set static uniforms (once for all batches) + loc = gl.glGetUniformLocation(program, "u_resolution") + if loc >= 0: + gl.glUniform2f(loc, float(width), float(height)) + + for i, v in enumerate(floats): + loc = gl.glGetUniformLocation(program, f"u_float{i}") + if loc >= 0: + gl.glUniform1f(loc, v) + + for i, v in enumerate(ints): + loc = gl.glGetUniformLocation(program, f"u_int{i}") + if loc >= 0: + gl.glUniform1i(loc, v) + + # Get u_pass uniform location for multi-pass + pass_loc = gl.glGetUniformLocation(program, "u_pass") + + gl.glViewport(0, 0, width, height) + gl.glDisable(gl.GL_BLEND) # Ensure no alpha blending - write output directly + + # Process each batch + all_batch_outputs = [] + for images in image_batches: + # Update input textures with this batch's images + for i, img in enumerate(images): + gl.glActiveTexture(gl.GL_TEXTURE0 + i) + gl.glBindTexture(gl.GL_TEXTURE_2D, input_textures[i]) + + # Flip vertically for GL coordinates, ensure RGBA + h, w, c = img.shape + if c == 3: + img_upload = np.empty((h, w, 4), dtype=np.float32) + img_upload[:, :, :3] = img[::-1, :, :] + img_upload[:, :, 3] = 1.0 + else: + img_upload = np.ascontiguousarray(img[::-1, :, :]) + + gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA32F, w, h, 0, gl.GL_RGBA, gl.GL_FLOAT, img_upload) + + if num_passes == 1: + # Single pass - render directly to output FBO + gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo) + if pass_loc >= 0: + gl.glUniform1i(pass_loc, 0) + gl.glClearColor(0, 0, 0, 0) + gl.glClear(gl.GL_COLOR_BUFFER_BIT) + gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3) + else: + # Multi-pass rendering with ping-pong + for p in range(num_passes): + is_last_pass = (p == num_passes - 1) + + # Set pass uniform + if pass_loc >= 0: + gl.glUniform1i(pass_loc, p) + + if is_last_pass: + # Last pass renders to the main output FBO + gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo) + else: + # Intermediate passes render to ping-pong FBO + target_fbo = ping_pong_fbos[p % 2] + gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, target_fbo) + + # Set input texture for this pass + gl.glActiveTexture(gl.GL_TEXTURE0) + if p == 0: + # First pass reads from original input + gl.glBindTexture(gl.GL_TEXTURE_2D, input_textures[0]) + else: + # Subsequent passes read from previous pass output + source_tex = ping_pong_textures[(p - 1) % 2] + gl.glBindTexture(gl.GL_TEXTURE_2D, source_tex) + + gl.glClearColor(0, 0, 0, 0) + gl.glClear(gl.GL_COLOR_BUFFER_BIT) + gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3) + + # Read back outputs for this batch + # (glGetTexImage is synchronous, implicitly waits for rendering) + batch_outputs = [] + for tex in output_textures: + gl.glBindTexture(gl.GL_TEXTURE_2D, tex) + data = gl.glGetTexImage(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, gl.GL_FLOAT) + img = np.frombuffer(data, dtype=np.float32).reshape(height, width, 4) + batch_outputs.append(img[::-1, :, :].copy()) + + # Pad with black images for unused outputs + black_img = np.zeros((height, width, 4), dtype=np.float32) + for _ in range(num_outputs, MAX_OUTPUTS): + batch_outputs.append(black_img) + + all_batch_outputs.append(batch_outputs) + + elapsed = (time.perf_counter() - start_time) * 1000 + num_batches = len(image_batches) + pass_info = f", {num_passes} passes" if num_passes > 1 else "" + logger.info(f"GLSL shader executed in {elapsed:.1f}ms ({num_batches} batch{'es' if num_batches != 1 else ''}, {width}x{height}{pass_info})") + + return all_batch_outputs + + finally: + # Unbind before deleting + gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) + gl.glUseProgram(0) + + for tex in input_textures: + gl.glDeleteTextures(tex) + for tex in output_textures: + gl.glDeleteTextures(tex) + for tex in ping_pong_textures: + gl.glDeleteTextures(tex) + if fbo is not None: + gl.glDeleteFramebuffers(1, [fbo]) + for pp_fbo in ping_pong_fbos: + gl.glDeleteFramebuffers(1, [pp_fbo]) + if program is not None: + gl.glDeleteProgram(program) + ########## class _GLContextGLFW(GLContext): From 1b2604d3a8db28c64f022bfec38045f1b01cd2de Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:41:26 -0300 Subject: [PATCH 11/15] `_render_shader_batch()` -> to `GLContext` method (part 2: remove) --- comfy_extras/nodes_glsl.py | 241 +------------------------------------ 1 file changed, 1 insertion(+), 240 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index e0dd03c5e..933fc8729 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -784,245 +784,6 @@ class _GLContextOSMesa(GLContext): ############################################################ -def _render_shader_batch( - fragment_code: str, - width: int, - height: int, - image_batches: list[list[np.ndarray]], - floats: list[float], - ints: list[int], -) -> list[list[np.ndarray]]: - """ - Render a fragment shader for multiple batches efficiently. - - Compiles shader once, reuses framebuffer/textures across batches. - Supports multi-pass rendering via #pragma passes N directive. - - Args: - fragment_code: User's fragment shader code - width: Output width - height: Output height - image_batches: List of batches, each batch is a list of input images (H, W, C) float32 [0,1] - floats: List of float uniforms - ints: List of int uniforms - - Returns: - List of batch outputs, each is a list of output images (H, W, 4) float32 [0,1] - """ - import time - start_time = time.perf_counter() - - if not image_batches: - return [] - - ctx = GLContext() - ctx.make_current() - - # Convert from GLSL ES to desktop GLSL 330 - fragment_source = _convert_es_to_desktop(fragment_code) - - # Detect how many outputs the shader actually uses - num_outputs = _detect_output_count(fragment_code) - - # Detect multi-pass rendering - num_passes = _detect_pass_count(fragment_code) - - # Track resources for cleanup - program = None - fbo = None - output_textures = [] - input_textures = [] - ping_pong_textures = [] - ping_pong_fbos = [] - - num_inputs = len(image_batches[0]) - - try: - # Compile shaders (once for all batches) - try: - program = ctx.create_program(VERTEX_SHADER, fragment_source) - except RuntimeError: - logger.error(f"Fragment shader:\n{fragment_source}") - raise - - gl.glUseProgram(program) - - # Create framebuffer with only the needed color attachments - fbo = gl.glGenFramebuffers(1) - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo) - - draw_buffers = [] - for i in range(num_outputs): - tex = gl.glGenTextures(1) - output_textures.append(tex) - gl.glBindTexture(gl.GL_TEXTURE_2D, tex) - gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA32F, width, height, 0, gl.GL_RGBA, gl.GL_FLOAT, None) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) - gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0 + i, gl.GL_TEXTURE_2D, tex, 0) - draw_buffers.append(gl.GL_COLOR_ATTACHMENT0 + i) - - gl.glDrawBuffers(num_outputs, draw_buffers) - - if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE: - raise RuntimeError("Framebuffer is not complete") - - # Create ping-pong resources for multi-pass rendering - if num_passes > 1: - for _ in range(2): - pp_tex = gl.glGenTextures(1) - ping_pong_textures.append(pp_tex) - gl.glBindTexture(gl.GL_TEXTURE_2D, pp_tex) - gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA32F, width, height, 0, gl.GL_RGBA, gl.GL_FLOAT, None) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE) - - pp_fbo = gl.glGenFramebuffers(1) - ping_pong_fbos.append(pp_fbo) - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, pp_fbo) - gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0, gl.GL_TEXTURE_2D, pp_tex, 0) - gl.glDrawBuffers(1, [gl.GL_COLOR_ATTACHMENT0]) - - if gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) != gl.GL_FRAMEBUFFER_COMPLETE: - raise RuntimeError("Ping-pong framebuffer is not complete") - - # Create input textures (reused for all batches) - for i in range(num_inputs): - tex = gl.glGenTextures(1) - input_textures.append(tex) - gl.glActiveTexture(gl.GL_TEXTURE0 + i) - gl.glBindTexture(gl.GL_TEXTURE_2D, tex) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE) - - loc = gl.glGetUniformLocation(program, f"u_image{i}") - if loc >= 0: - gl.glUniform1i(loc, i) - - # Set static uniforms (once for all batches) - loc = gl.glGetUniformLocation(program, "u_resolution") - if loc >= 0: - gl.glUniform2f(loc, float(width), float(height)) - - for i, v in enumerate(floats): - loc = gl.glGetUniformLocation(program, f"u_float{i}") - if loc >= 0: - gl.glUniform1f(loc, v) - - for i, v in enumerate(ints): - loc = gl.glGetUniformLocation(program, f"u_int{i}") - if loc >= 0: - gl.glUniform1i(loc, v) - - # Get u_pass uniform location for multi-pass - pass_loc = gl.glGetUniformLocation(program, "u_pass") - - gl.glViewport(0, 0, width, height) - gl.glDisable(gl.GL_BLEND) # Ensure no alpha blending - write output directly - - # Process each batch - all_batch_outputs = [] - for images in image_batches: - # Update input textures with this batch's images - for i, img in enumerate(images): - gl.glActiveTexture(gl.GL_TEXTURE0 + i) - gl.glBindTexture(gl.GL_TEXTURE_2D, input_textures[i]) - - # Flip vertically for GL coordinates, ensure RGBA - h, w, c = img.shape - if c == 3: - img_upload = np.empty((h, w, 4), dtype=np.float32) - img_upload[:, :, :3] = img[::-1, :, :] - img_upload[:, :, 3] = 1.0 - else: - img_upload = np.ascontiguousarray(img[::-1, :, :]) - - gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA32F, w, h, 0, gl.GL_RGBA, gl.GL_FLOAT, img_upload) - - if num_passes == 1: - # Single pass - render directly to output FBO - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo) - if pass_loc >= 0: - gl.glUniform1i(pass_loc, 0) - gl.glClearColor(0, 0, 0, 0) - gl.glClear(gl.GL_COLOR_BUFFER_BIT) - gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3) - else: - # Multi-pass rendering with ping-pong - for p in range(num_passes): - is_last_pass = (p == num_passes - 1) - - # Set pass uniform - if pass_loc >= 0: - gl.glUniform1i(pass_loc, p) - - if is_last_pass: - # Last pass renders to the main output FBO - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo) - else: - # Intermediate passes render to ping-pong FBO - target_fbo = ping_pong_fbos[p % 2] - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, target_fbo) - - # Set input texture for this pass - gl.glActiveTexture(gl.GL_TEXTURE0) - if p == 0: - # First pass reads from original input - gl.glBindTexture(gl.GL_TEXTURE_2D, input_textures[0]) - else: - # Subsequent passes read from previous pass output - source_tex = ping_pong_textures[(p - 1) % 2] - gl.glBindTexture(gl.GL_TEXTURE_2D, source_tex) - - gl.glClearColor(0, 0, 0, 0) - gl.glClear(gl.GL_COLOR_BUFFER_BIT) - gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3) - - # Read back outputs for this batch - # (glGetTexImage is synchronous, implicitly waits for rendering) - batch_outputs = [] - for tex in output_textures: - gl.glBindTexture(gl.GL_TEXTURE_2D, tex) - data = gl.glGetTexImage(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, gl.GL_FLOAT) - img = np.frombuffer(data, dtype=np.float32).reshape(height, width, 4) - batch_outputs.append(img[::-1, :, :].copy()) - - # Pad with black images for unused outputs - black_img = np.zeros((height, width, 4), dtype=np.float32) - for _ in range(num_outputs, MAX_OUTPUTS): - batch_outputs.append(black_img) - - all_batch_outputs.append(batch_outputs) - - elapsed = (time.perf_counter() - start_time) * 1000 - num_batches = len(image_batches) - pass_info = f", {num_passes} passes" if num_passes > 1 else "" - logger.info(f"GLSL shader executed in {elapsed:.1f}ms ({num_batches} batch{'es' if num_batches != 1 else ''}, {width}x{height}{pass_info})") - - return all_batch_outputs - - finally: - # Unbind before deleting - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0) - gl.glUseProgram(0) - - for tex in input_textures: - gl.glDeleteTextures(tex) - for tex in output_textures: - gl.glDeleteTextures(tex) - for tex in ping_pong_textures: - gl.glDeleteTextures(tex) - if fbo is not None: - gl.glDeleteFramebuffers(1, [fbo]) - for pp_fbo in ping_pong_fbos: - gl.glDeleteFramebuffers(1, [pp_fbo]) - if program is not None: - gl.glDeleteProgram(program) - class GLSLShader(io.ComfyNode): @classmethod @@ -1133,7 +894,7 @@ class GLSLShader(io.ComfyNode): batch_images = [img_tensor[batch_idx].cpu().numpy().astype(np.float32) for img_tensor in image_list] image_batches.append(batch_images) - all_batch_outputs = _render_shader_batch( + all_batch_outputs = GLContext().render_shader_batch( fragment_shader, out_width, out_height, From 8d45d2c90c098ec428862182a46816e2d9b1cd90 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Mon, 23 Feb 2026 02:20:24 -0300 Subject: [PATCH 12/15] global `gl` -> `GLContext.GL` --- comfy_extras/nodes_glsl.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index 933fc8729..741f73880 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -16,9 +16,6 @@ from utils.install_util import get_missing_requirements_message logger = logging.getLogger(__name__) -# OpenGL modules - initialized lazily when context is created -gl = None - def _check_opengl_availability(): """Early check for OpenGL availability. Raises RuntimeError if unlikely to work.""" @@ -284,13 +281,8 @@ class GLContext: def __import_opengl(self): """Import OpenGL module. Called after context is created.""" - global gl - if gl is not None: - return - logger.debug("__import_opengl: importing OpenGL.GL") import OpenGL.GL as _gl - gl = _gl self._gl = _gl logger.debug("__import_opengl: import completed") @@ -311,6 +303,11 @@ class GLContext: if self._vao is not None: self._glBindVertexArray(self._vao) + @property + def GL(self): + """Imported ``OpenGL.GL`` module.""" + return self._gl + def compile_shader(self, source: str, shader_type: int) -> int: """Compile a shader and return its ID.""" gl = self._gl From b285831a629cdcf1efef37c14eee88c709724b45 Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Thu, 26 Feb 2026 02:55:25 -0300 Subject: [PATCH 13/15] `GLRender` class - to decouple context from rendering methods --- comfy_extras/nodes_glsl.py | 405 ++++++++++++++++++++----------------- 1 file changed, 218 insertions(+), 187 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index 741f73880..9cdda3cc9 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -222,6 +222,7 @@ class GLContext: def __init__(self): try: + # noinspection PyUnresolvedReferences if self.__initialized: # 99% of the time (after first init) we get here and just return logger.debug("GLContext.__init__: already initialized, skipping") @@ -305,12 +306,219 @@ class GLContext: @property def GL(self): - """Imported ``OpenGL.GL`` module.""" + """Properly yet lazily imported ``OpenGL.GL`` module.""" return self._gl +########## + +class _GLContextGLFW(GLContext): + """Concrete GLContext using GLFW backend.""" + @classmethod + def backend_name(cls) -> str: + return "GLFW" + + def _init_backend_concrete(self): + """Initialize GLFW. Raises RuntimeError on failure.""" + logger.debug("_init_backend_concrete (GLFW): starting") + # On macOS, glfw.init() must be called from main thread or it hangs forever + if sys.platform == "darwin": + logger.debug("_init_backend_concrete (GLFW): skipping on macOS") + raise RuntimeError("GLFW backend not supported on macOS") + + logger.debug("_init_backend_concrete (GLFW): importing glfw module") + import glfw + + logger.debug("_init_backend_concrete (GLFW): calling glfw.init()") + if not glfw.init(): + raise RuntimeError("glfw.init() failed") + + try: + logger.debug("_init_backend_concrete (GLFW): setting window hints") + glfw.window_hint(glfw.VISIBLE, glfw.FALSE) + glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3) + glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3) + glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE) + + logger.debug("_init_backend_concrete (GLFW): calling create_window()") + window = glfw.create_window(64, 64, "ComfyUI GLSL", None, None) + if not window: + raise RuntimeError("glfw.create_window() failed") + + logger.debug("_init_backend_concrete (GLFW): calling make_context_current()") + glfw.make_context_current(window) + except Exception: + logger.debug("_init_backend_concrete (GLFW): failed, terminating glfw") + glfw.terminate() + raise + + self._window = window + self._glfw = glfw + + logger.debug("_init_backend_concrete (GLFW): completed successfully") + + def _make_current_concrete(self): + self._glfw.make_context_current(self._window) + +########## + +class _GLContextEGL(GLContext): + """Concrete GLContext using EGL backend.""" + @classmethod + def backend_name(cls) -> str: + return "EGL" + + def _init_backend_concrete(self): + """Initialize EGL for headless rendering. Raises RuntimeError on failure.""" + logger.debug("_init_backend_concrete (EGL): starting") + from OpenGL import EGL + logger.debug("_init_backend_concrete (EGL): imports completed") + + display = None + context = None + surface = None + + try: + logger.debug("_init_backend_concrete (EGL): calling eglGetDisplay()") + display = EGL.eglGetDisplay(EGL.EGL_DEFAULT_DISPLAY) + if display == EGL.EGL_NO_DISPLAY: + raise RuntimeError("eglGetDisplay() failed") + + logger.debug("_init_backend_concrete (EGL): calling eglInitialize()") + major, minor = EGL.EGLint(), EGL.EGLint() + if not EGL.eglInitialize(display, major, minor): + display = None # Not initialized, don't terminate + raise RuntimeError("eglInitialize() failed") + logger.debug(f"_init_backend_concrete (EGL): EGL version {major.value}.{minor.value}") + + config_attribs = [ + EGL.EGL_SURFACE_TYPE, EGL.EGL_PBUFFER_BIT, + EGL.EGL_RENDERABLE_TYPE, EGL.EGL_OPENGL_BIT, + EGL.EGL_RED_SIZE, 8, EGL.EGL_GREEN_SIZE, 8, EGL.EGL_BLUE_SIZE, 8, EGL.EGL_ALPHA_SIZE, 8, + EGL.EGL_DEPTH_SIZE, 0, EGL.EGL_NONE + ] + configs = (EGL.EGLConfig * 1)() + num_configs = EGL.EGLint() + if not EGL.eglChooseConfig(display, config_attribs, configs, 1, num_configs) or num_configs.value == 0: + raise RuntimeError("eglChooseConfig() failed") + config = configs[0] + logger.debug(f"_init_backend_concrete (EGL): config chosen, num_configs={num_configs.value}") + + if not EGL.eglBindAPI(EGL.EGL_OPENGL_API): + raise RuntimeError("eglBindAPI() failed") + + logger.debug("_init_backend_concrete (EGL): calling eglCreateContext()") + context_attribs = [ + EGL.EGL_CONTEXT_MAJOR_VERSION, 3, + EGL.EGL_CONTEXT_MINOR_VERSION, 3, + EGL.EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL.EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, + EGL.EGL_NONE + ] + context = EGL.eglCreateContext(display, config, EGL.EGL_NO_CONTEXT, context_attribs) + if context == EGL.EGL_NO_CONTEXT: + raise RuntimeError("eglCreateContext() failed") + + logger.debug("_init_backend_concrete (EGL): calling eglCreatePbufferSurface()") + pbuffer_attribs = [EGL.EGL_WIDTH, 64, EGL.EGL_HEIGHT, 64, EGL.EGL_NONE] + surface = EGL.eglCreatePbufferSurface(display, config, pbuffer_attribs) + if surface == EGL.EGL_NO_SURFACE: + raise RuntimeError("eglCreatePbufferSurface() failed") + + logger.debug("_init_backend_concrete (EGL): calling eglMakeCurrent()") + if not EGL.eglMakeCurrent(display, surface, surface, context): + raise RuntimeError("eglMakeCurrent() failed") + + except Exception: + logger.debug("_init_backend_concrete (EGL): failed, cleaning up") + # Clean up any resources on failure + if surface is not None: + EGL.eglDestroySurface(display, surface) + if context is not None: + EGL.eglDestroyContext(display, context) + if display is not None: + EGL.eglTerminate(display) + raise + + self._egl_display = display + self._egl_context = context + self._egl_surface = surface + + self._EGL = EGL + self._eglMakeCurrent = EGL.eglMakeCurrent + + logger.debug("_init_backend_concrete (EGL): completed successfully") + + def _make_current_concrete(self): + self._eglMakeCurrent(self._egl_display, self._egl_surface, self._egl_surface, self._egl_context) + +########## + +class _GLContextOSMesa(GLContext): + """Concrete GLContext using OSMesa backend.""" + @classmethod + def backend_name(cls) -> str: + return "OSMesa" + + def _init_backend_concrete(self): + """Initialize OSMesa for software rendering. Returns (context, buffer). Raises RuntimeError on failure.""" + import ctypes + + logger.debug("_init_backend_concrete (OSMesa): starting") + os.environ["PYOPENGL_PLATFORM"] = "osmesa" + + logger.debug("_init_backend_concrete (OSMesa): importing OpenGL.osmesa") + from OpenGL import GL as _gl + from OpenGL.osmesa import ( + OSMesaCreateContextExt, OSMesaMakeCurrent, OSMesaDestroyContext, + OSMESA_RGBA, + ) + logger.debug("_init_backend_concrete (OSMesa): imports completed") + + ctx = OSMesaCreateContextExt(OSMESA_RGBA, 24, 0, 0, None) + if not ctx: + raise RuntimeError("OSMesaCreateContextExt() failed") + + width, height = 64, 64 + buffer = (ctypes.c_ubyte * (width * height * 4))() + + logger.debug("_init_backend_concrete (OSMesa): calling OSMesaMakeCurrent()") + if not OSMesaMakeCurrent(ctx, buffer, _gl.GL_UNSIGNED_BYTE, width, height): + OSMesaDestroyContext(ctx) + raise RuntimeError("OSMesaMakeCurrent() failed") + + self._osmesa_ctx = ctx + self._osmesa_buffer = buffer + + logger.debug("_init_backend_concrete (OSMesa): completed successfully") + + def _make_current_concrete(self): + from OpenGL.osmesa import OSMesaMakeCurrent + OSMesaMakeCurrent(self._osmesa_ctx, self._osmesa_buffer, self._gl.GL_UNSIGNED_BYTE, 64, 64) + + +############################################################ + + +class __GLRenderMeta(type): + """Internal metaclass for ``GLRender``. + + Implemented as meta - to make ``GLRender`` truly static, including class-level properties which are also properly type-detected by IDEs. + """ + + @property + def context(self) -> GLContext: + """Global OpenGL context.""" + try: + # noinspection PyUnresolvedReferences + return self.__context + except AttributeError: + pass + # noinspection PyAttributeOutsideInit + self.__context = GLContext() + return self.__context + def compile_shader(self, source: str, shader_type: int) -> int: """Compile a shader and return its ID.""" - gl = self._gl + gl = self.context.GL shader = gl.glCreateShader(shader_type) gl.glShaderSource(shader, source) @@ -325,7 +533,7 @@ class GLContext: def create_program(self, vertex_source: str, fragment_source: str) -> int: """Create and link a shader program.""" - gl = self._gl + gl = self.context.GL compile = self.compile_shader vertex_shader = compile(vertex_source, gl.GL_VERTEX_SHADER) @@ -378,14 +586,14 @@ class GLContext: """ import time - gl = self._gl + gl = self.context.GL start_time = time.perf_counter() if not image_batches: return [] - self.make_current() + self.context.make_current() # Convert from GLSL ES to desktop GLSL 330 fragment_source = _convert_es_to_desktop(fragment_code) @@ -594,188 +802,11 @@ class GLContext: ########## -class _GLContextGLFW(GLContext): - """Concrete GLContext using GLFW backend.""" - @classmethod - def backend_name(cls) -> str: - return "GLFW" +class GLRender(metaclass=__GLRenderMeta): + """Static class for all the high-level methods to render with OpenGL. Never instantiated, methods called directly as functions.""" - def _init_backend_concrete(self): - """Initialize GLFW. Raises RuntimeError on failure.""" - logger.debug("_init_backend_concrete (GLFW): starting") - # On macOS, glfw.init() must be called from main thread or it hangs forever - if sys.platform == "darwin": - logger.debug("_init_backend_concrete (GLFW): skipping on macOS") - raise RuntimeError("GLFW backend not supported on macOS") - - logger.debug("_init_backend_concrete (GLFW): importing glfw module") - import glfw - - logger.debug("_init_backend_concrete (GLFW): calling glfw.init()") - if not glfw.init(): - raise RuntimeError("glfw.init() failed") - - try: - logger.debug("_init_backend_concrete (GLFW): setting window hints") - glfw.window_hint(glfw.VISIBLE, glfw.FALSE) - glfw.window_hint(glfw.CONTEXT_VERSION_MAJOR, 3) - glfw.window_hint(glfw.CONTEXT_VERSION_MINOR, 3) - glfw.window_hint(glfw.OPENGL_PROFILE, glfw.OPENGL_CORE_PROFILE) - - logger.debug("_init_backend_concrete (GLFW): calling create_window()") - window = glfw.create_window(64, 64, "ComfyUI GLSL", None, None) - if not window: - raise RuntimeError("glfw.create_window() failed") - - logger.debug("_init_backend_concrete (GLFW): calling make_context_current()") - glfw.make_context_current(window) - except Exception: - logger.debug("_init_backend_concrete (GLFW): failed, terminating glfw") - glfw.terminate() - raise - - self._window = window - self._glfw = glfw - - logger.debug("_init_backend_concrete (GLFW): completed successfully") - - def _make_current_concrete(self): - self._glfw.make_context_current(self._window) - -########## - -class _GLContextEGL(GLContext): - """Concrete GLContext using EGL backend.""" - @classmethod - def backend_name(cls) -> str: - return "EGL" - - def _init_backend_concrete(self): - """Initialize EGL for headless rendering. Raises RuntimeError on failure.""" - logger.debug("_init_backend_concrete (EGL): starting") - from OpenGL import EGL - logger.debug("_init_backend_concrete (EGL): imports completed") - - display = None - context = None - surface = None - - try: - logger.debug("_init_backend_concrete (EGL): calling eglGetDisplay()") - display = EGL.eglGetDisplay(EGL.EGL_DEFAULT_DISPLAY) - if display == EGL.EGL_NO_DISPLAY: - raise RuntimeError("eglGetDisplay() failed") - - logger.debug("_init_backend_concrete (EGL): calling eglInitialize()") - major, minor = EGL.EGLint(), EGL.EGLint() - if not EGL.eglInitialize(display, major, minor): - display = None # Not initialized, don't terminate - raise RuntimeError("eglInitialize() failed") - logger.debug(f"_init_backend_concrete (EGL): EGL version {major.value}.{minor.value}") - - config_attribs = [ - EGL.EGL_SURFACE_TYPE, EGL.EGL_PBUFFER_BIT, - EGL.EGL_RENDERABLE_TYPE, EGL.EGL_OPENGL_BIT, - EGL.EGL_RED_SIZE, 8, EGL.EGL_GREEN_SIZE, 8, EGL.EGL_BLUE_SIZE, 8, EGL.EGL_ALPHA_SIZE, 8, - EGL.EGL_DEPTH_SIZE, 0, EGL.EGL_NONE - ] - configs = (EGL.EGLConfig * 1)() - num_configs = EGL.EGLint() - if not EGL.eglChooseConfig(display, config_attribs, configs, 1, num_configs) or num_configs.value == 0: - raise RuntimeError("eglChooseConfig() failed") - config = configs[0] - logger.debug(f"_init_backend_concrete (EGL): config chosen, num_configs={num_configs.value}") - - if not EGL.eglBindAPI(EGL.EGL_OPENGL_API): - raise RuntimeError("eglBindAPI() failed") - - logger.debug("_init_backend_concrete (EGL): calling eglCreateContext()") - context_attribs = [ - EGL.EGL_CONTEXT_MAJOR_VERSION, 3, - EGL.EGL_CONTEXT_MINOR_VERSION, 3, - EGL.EGL_CONTEXT_OPENGL_PROFILE_MASK, EGL.EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT, - EGL.EGL_NONE - ] - context = EGL.eglCreateContext(display, config, EGL.EGL_NO_CONTEXT, context_attribs) - if context == EGL.EGL_NO_CONTEXT: - raise RuntimeError("eglCreateContext() failed") - - logger.debug("_init_backend_concrete (EGL): calling eglCreatePbufferSurface()") - pbuffer_attribs = [EGL.EGL_WIDTH, 64, EGL.EGL_HEIGHT, 64, EGL.EGL_NONE] - surface = EGL.eglCreatePbufferSurface(display, config, pbuffer_attribs) - if surface == EGL.EGL_NO_SURFACE: - raise RuntimeError("eglCreatePbufferSurface() failed") - - logger.debug("_init_backend_concrete (EGL): calling eglMakeCurrent()") - if not EGL.eglMakeCurrent(display, surface, surface, context): - raise RuntimeError("eglMakeCurrent() failed") - - except Exception: - logger.debug("_init_backend_concrete (EGL): failed, cleaning up") - # Clean up any resources on failure - if surface is not None: - EGL.eglDestroySurface(display, surface) - if context is not None: - EGL.eglDestroyContext(display, context) - if display is not None: - EGL.eglTerminate(display) - raise - - self._egl_display = display - self._egl_context = context - self._egl_surface = surface - - self._EGL = EGL - self._eglMakeCurrent = EGL.eglMakeCurrent - - logger.debug("_init_backend_concrete (EGL): completed successfully") - - def _make_current_concrete(self): - self._eglMakeCurrent(self._egl_display, self._egl_surface, self._egl_surface, self._egl_context) - -########## - -class _GLContextOSMesa(GLContext): - """Concrete GLContext using OSMesa backend.""" - @classmethod - def backend_name(cls) -> str: - return "OSMesa" - - def _init_backend_concrete(self): - """Initialize OSMesa for software rendering. Returns (context, buffer). Raises RuntimeError on failure.""" - import ctypes - - logger.debug("_init_backend_concrete (OSMesa): starting") - os.environ["PYOPENGL_PLATFORM"] = "osmesa" - - logger.debug("_init_backend_concrete (OSMesa): importing OpenGL.osmesa") - from OpenGL import GL as _gl - from OpenGL.osmesa import ( - OSMesaCreateContextExt, OSMesaMakeCurrent, OSMesaDestroyContext, - OSMESA_RGBA, - ) - logger.debug("_init_backend_concrete (OSMesa): imports completed") - - ctx = OSMesaCreateContextExt(OSMESA_RGBA, 24, 0, 0, None) - if not ctx: - raise RuntimeError("OSMesaCreateContextExt() failed") - - width, height = 64, 64 - buffer = (ctypes.c_ubyte * (width * height * 4))() - - logger.debug("_init_backend_concrete (OSMesa): calling OSMesaMakeCurrent()") - if not OSMesaMakeCurrent(ctx, buffer, _gl.GL_UNSIGNED_BYTE, width, height): - OSMesaDestroyContext(ctx) - raise RuntimeError("OSMesaMakeCurrent() failed") - - self._osmesa_ctx = ctx - self._osmesa_buffer = buffer - - logger.debug("_init_backend_concrete (OSMesa): completed successfully") - - def _make_current_concrete(self): - from OpenGL.osmesa import OSMesaMakeCurrent - OSMesaMakeCurrent(self._osmesa_ctx, self._osmesa_buffer, self._gl.GL_UNSIGNED_BYTE, 64, 64) + def __init__(self): + raise NotImplementedError(f"{self.__class__!r} is a static class - call its methods directly, as just functions, without instantiating.") ############################################################ @@ -891,7 +922,7 @@ class GLSLShader(io.ComfyNode): batch_images = [img_tensor[batch_idx].cpu().numpy().astype(np.float32) for img_tensor in image_list] image_batches.append(batch_images) - all_batch_outputs = GLContext().render_shader_batch( + all_batch_outputs = GLRender.render_shader_batch( fragment_shader, out_width, out_height, From f35dda630fa11c4d1e69c10a2b0583818cfe477f Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Thu, 26 Feb 2026 03:24:02 -0300 Subject: [PATCH 14/15] small cleanup/bugfixes - from IDE warnings --- comfy_extras/nodes_glsl.py | 42 ++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index 9cdda3cc9..67d0349cd 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -249,6 +249,7 @@ class GLContext: # Create VAO (required for core profile, but OSMesa may use compat profile) logger.debug("GLContext.__init__: creating VAO") + vao = None try: vao = gl.glGenVertexArrays(1) gl.glBindVertexArray(vao) @@ -264,8 +265,8 @@ class GLContext: except Exception: pass - self.__initialized = True self._glBindVertexArray = gl.glBindVertexArray + self.__initialized = True elapsed = (time.perf_counter() - start_time) * 1000 @@ -283,8 +284,8 @@ class GLContext: def __import_opengl(self): """Import OpenGL module. Called after context is created.""" logger.debug("__import_opengl: importing OpenGL.GL") - import OpenGL.GL as _gl - self._gl = _gl + from OpenGL import GL + self._gl = GL logger.debug("__import_opengl: import completed") @classmethod @@ -304,6 +305,7 @@ class GLContext: if self._vao is not None: self._glBindVertexArray(self._vao) + # noinspection PyPep8Naming @property def GL(self): """Properly yet lazily imported ``OpenGL.GL`` module.""" @@ -466,7 +468,7 @@ class _GLContextOSMesa(GLContext): os.environ["PYOPENGL_PLATFORM"] = "osmesa" logger.debug("_init_backend_concrete (OSMesa): importing OpenGL.osmesa") - from OpenGL import GL as _gl + from OpenGL import GL from OpenGL.osmesa import ( OSMesaCreateContextExt, OSMesaMakeCurrent, OSMesaDestroyContext, OSMESA_RGBA, @@ -481,7 +483,7 @@ class _GLContextOSMesa(GLContext): buffer = (ctypes.c_ubyte * (width * height * 4))() logger.debug("_init_backend_concrete (OSMesa): calling OSMesaMakeCurrent()") - if not OSMesaMakeCurrent(ctx, buffer, _gl.GL_UNSIGNED_BYTE, width, height): + if not OSMesaMakeCurrent(ctx, buffer, GL.GL_UNSIGNED_BYTE, width, height): OSMesaDestroyContext(ctx) raise RuntimeError("OSMesaMakeCurrent() failed") @@ -505,20 +507,20 @@ class __GLRenderMeta(type): """ @property - def context(self) -> GLContext: + def context(cls) -> GLContext: """Global OpenGL context.""" try: # noinspection PyUnresolvedReferences - return self.__context + return cls.__context except AttributeError: pass # noinspection PyAttributeOutsideInit - self.__context = GLContext() - return self.__context + cls.__context = GLContext() + return cls.__context - def compile_shader(self, source: str, shader_type: int) -> int: + def compile_shader(cls, source: str, shader_type: int) -> int: """Compile a shader and return its ID.""" - gl = self.context.GL + gl = cls.context.GL shader = gl.glCreateShader(shader_type) gl.glShaderSource(shader, source) @@ -531,14 +533,14 @@ class __GLRenderMeta(type): return shader - def create_program(self, vertex_source: str, fragment_source: str) -> int: + def create_program(cls, vertex_source: str, fragment_source: str) -> int: """Create and link a shader program.""" - gl = self.context.GL - compile = self.compile_shader + gl = cls.context.GL + compile_shader = cls.compile_shader - vertex_shader = compile(vertex_source, gl.GL_VERTEX_SHADER) + vertex_shader = compile_shader(vertex_source, gl.GL_VERTEX_SHADER) try: - fragment_shader = compile(fragment_source, gl.GL_FRAGMENT_SHADER) + fragment_shader = compile_shader(fragment_source, gl.GL_FRAGMENT_SHADER) except RuntimeError: gl.glDeleteShader(vertex_shader) raise @@ -559,7 +561,7 @@ class __GLRenderMeta(type): return program def render_shader_batch( - self, + cls, fragment_code: str, width: int, height: int, @@ -586,14 +588,14 @@ class __GLRenderMeta(type): """ import time - gl = self.context.GL + gl = cls.context.GL start_time = time.perf_counter() if not image_batches: return [] - self.context.make_current() + cls.context.make_current() # Convert from GLSL ES to desktop GLSL 330 fragment_source = _convert_es_to_desktop(fragment_code) @@ -617,7 +619,7 @@ class __GLRenderMeta(type): try: # Compile shaders (once for all batches) try: - program = self.create_program(VERTEX_SHADER, fragment_source) + program = cls.create_program(VERTEX_SHADER, fragment_source) except RuntimeError: logger.error(f"Fragment shader:\n{fragment_source}") raise From eac870324ca2fa50157b3aef88131c84edcbabfe Mon Sep 17 00:00:00 2001 From: "Lex Darlog (DRL)" <3897975+Lex-DRL@users.noreply.github.com> Date: Thu, 26 Feb 2026 03:43:58 -0300 Subject: [PATCH 15/15] `__gen_default_texture()` for reused texture-init code --- comfy_extras/nodes_glsl.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/comfy_extras/nodes_glsl.py b/comfy_extras/nodes_glsl.py index 67d0349cd..9ec1d7be2 100644 --- a/comfy_extras/nodes_glsl.py +++ b/comfy_extras/nodes_glsl.py @@ -560,6 +560,16 @@ class __GLRenderMeta(type): return program + def __gen_default_texture(cls, width: int, height: int) -> int: + """Initializes a texture of default type. Returns the handle.""" + gl = cls.context.GL + tex: int = gl.glGenTextures(1) + gl.glBindTexture(gl.GL_TEXTURE_2D, tex) + gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA32F, width, height, 0, gl.GL_RGBA, gl.GL_FLOAT, None) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) + return tex + def render_shader_batch( cls, fragment_code: str, @@ -632,12 +642,8 @@ class __GLRenderMeta(type): draw_buffers = [] for i in range(num_outputs): - tex = gl.glGenTextures(1) + tex = cls.__gen_default_texture(width, height) output_textures.append(tex) - gl.glBindTexture(gl.GL_TEXTURE_2D, tex) - gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA32F, width, height, 0, gl.GL_RGBA, gl.GL_FLOAT, None) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, gl.GL_COLOR_ATTACHMENT0 + i, gl.GL_TEXTURE_2D, tex, 0) draw_buffers.append(gl.GL_COLOR_ATTACHMENT0 + i) @@ -649,14 +655,10 @@ class __GLRenderMeta(type): # Create ping-pong resources for multi-pass rendering if num_passes > 1: for _ in range(2): - pp_tex = gl.glGenTextures(1) - ping_pong_textures.append(pp_tex) - gl.glBindTexture(gl.GL_TEXTURE_2D, pp_tex) - gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA32F, width, height, 0, gl.GL_RGBA, gl.GL_FLOAT, None) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) + pp_tex = cls.__gen_default_texture(width, height) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE) + ping_pong_textures.append(pp_tex) pp_fbo = gl.glGenFramebuffers(1) ping_pong_fbos.append(pp_fbo)