mirror of
https://github.com/comfyanonymous/ComfyUI.git
synced 2026-03-29 21:13:33 +08:00
Merge 8114516ee6 into fc1fdf3389
This commit is contained in:
commit
34f5019b1a
@ -1,85 +1,67 @@
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
|
import ctypes
|
||||||
import logging
|
import logging
|
||||||
import ctypes.util
|
|
||||||
import importlib.util
|
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
import nodes
|
import nodes
|
||||||
|
import comfy_angle
|
||||||
from comfy_api.latest import ComfyExtension, io, ui
|
from comfy_api.latest import ComfyExtension, io, ui
|
||||||
from typing_extensions import override
|
from typing_extensions import override
|
||||||
from utils.install_util import get_missing_requirements_message
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _check_opengl_availability():
|
def _preload_angle():
|
||||||
"""Early check for OpenGL availability. Raises RuntimeError if unlikely to work."""
|
egl_path = comfy_angle.get_egl_path()
|
||||||
logger.debug("_check_opengl_availability: starting")
|
gles_path = comfy_angle.get_glesv2_path()
|
||||||
missing = []
|
|
||||||
|
|
||||||
# Check Python packages (using find_spec to avoid importing)
|
if sys.platform == "win32":
|
||||||
logger.debug("_check_opengl_availability: checking for glfw package")
|
angle_dir = comfy_angle.get_lib_dir()
|
||||||
if importlib.util.find_spec("glfw") is None:
|
os.add_dll_directory(angle_dir)
|
||||||
missing.append("glfw")
|
os.environ["PATH"] = angle_dir + os.pathsep + os.environ.get("PATH", "")
|
||||||
|
|
||||||
logger.debug("_check_opengl_availability: checking for OpenGL package")
|
mode = 0 if sys.platform == "win32" else ctypes.RTLD_GLOBAL
|
||||||
if importlib.util.find_spec("OpenGL") is None:
|
ctypes.CDLL(str(egl_path), mode=mode)
|
||||||
missing.append("PyOpenGL")
|
ctypes.CDLL(str(gles_path), mode=mode)
|
||||||
|
|
||||||
if missing:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"OpenGL dependencies not available.\n{get_missing_requirements_message()}\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
# On Linux without display, check if headless backends are available
|
|
||||||
logger.debug(f"_check_opengl_availability: platform={sys.platform}")
|
|
||||||
if sys.platform.startswith("linux"):
|
|
||||||
has_display = os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")
|
|
||||||
logger.debug(f"_check_opengl_availability: has_display={bool(has_display)}")
|
|
||||||
if not has_display:
|
|
||||||
# Check for EGL or OSMesa libraries
|
|
||||||
logger.debug("_check_opengl_availability: checking for EGL library")
|
|
||||||
has_egl = ctypes.util.find_library("EGL")
|
|
||||||
logger.debug("_check_opengl_availability: checking for OSMesa library")
|
|
||||||
has_osmesa = ctypes.util.find_library("OSMesa")
|
|
||||||
|
|
||||||
# Error disabled for CI as it fails this check
|
|
||||||
# if not has_egl and not has_osmesa:
|
|
||||||
# raise RuntimeError(
|
|
||||||
# "GLSL Shader node: No display and no headless backend (EGL/OSMesa) found.\n"
|
|
||||||
# "See error below for installation instructions."
|
|
||||||
# )
|
|
||||||
logger.debug(f"Headless mode: EGL={'yes' if has_egl else 'no'}, OSMesa={'yes' if has_osmesa else 'no'}")
|
|
||||||
|
|
||||||
logger.debug("_check_opengl_availability: completed")
|
|
||||||
|
|
||||||
|
|
||||||
# Run early check at import time
|
# Pre-load ANGLE *before* any PyOpenGL import so that the EGL platform
|
||||||
logger.debug("nodes_glsl: running _check_opengl_availability at import time")
|
# plugin picks up ANGLE's libEGL / libGLESv2 instead of system libs.
|
||||||
_check_opengl_availability()
|
_preload_angle()
|
||||||
|
os.environ.setdefault("PYOPENGL_PLATFORM", "egl")
|
||||||
|
|
||||||
# OpenGL modules - initialized lazily when context is created
|
import OpenGL
|
||||||
gl = None
|
OpenGL.USE_ACCELERATE = False
|
||||||
glfw = None
|
|
||||||
EGL = None
|
|
||||||
|
|
||||||
|
|
||||||
def _import_opengl():
|
def _patch_find_library():
|
||||||
"""Import OpenGL module. Called after context is created."""
|
"""PyOpenGL's EGL platform looks for 'EGL' and 'GLESv2' by short name
|
||||||
global gl
|
via ctypes.util.find_library, but ANGLE ships as 'libEGL' and
|
||||||
if gl is None:
|
'libGLESv2'. Patch find_library to return the full ANGLE paths so
|
||||||
logger.debug("_import_opengl: importing OpenGL.GL")
|
PyOpenGL loads the same libraries we pre-loaded."""
|
||||||
import OpenGL.GL as _gl
|
if sys.platform == "linux":
|
||||||
gl = _gl
|
return
|
||||||
logger.debug("_import_opengl: import completed")
|
import ctypes.util
|
||||||
return gl
|
_orig = ctypes.util.find_library
|
||||||
|
def _patched(name):
|
||||||
|
if name == 'EGL':
|
||||||
|
return comfy_angle.get_egl_path()
|
||||||
|
if name == 'GLESv2':
|
||||||
|
return comfy_angle.get_glesv2_path()
|
||||||
|
return _orig(name)
|
||||||
|
ctypes.util.find_library = _patched
|
||||||
|
|
||||||
|
|
||||||
|
_patch_find_library()
|
||||||
|
|
||||||
|
from OpenGL import EGL
|
||||||
|
from OpenGL import GLES3 as gl
|
||||||
|
|
||||||
class SizeModeInput(TypedDict):
|
class SizeModeInput(TypedDict):
|
||||||
size_mode: str
|
size_mode: str
|
||||||
width: int
|
width: int
|
||||||
@ -102,7 +84,7 @@ MAX_OUTPUTS = 4 # fragColor0-3 (MRT)
|
|||||||
# (-1,-1)---(3,-1)
|
# (-1,-1)---(3,-1)
|
||||||
#
|
#
|
||||||
# v_texCoord is computed from clip space: * 0.5 + 0.5 maps (-1,1) -> (0,1)
|
# v_texCoord is computed from clip space: * 0.5 + 0.5 maps (-1,1) -> (0,1)
|
||||||
VERTEX_SHADER = """#version 330 core
|
VERTEX_SHADER = """#version 300 es
|
||||||
out vec2 v_texCoord;
|
out vec2 v_texCoord;
|
||||||
void main() {
|
void main() {
|
||||||
vec2 verts[3] = vec2[](vec2(-1, -1), vec2(3, -1), vec2(-1, 3));
|
vec2 verts[3] = vec2[](vec2(-1, -1), vec2(3, -1), vec2(-1, 3));
|
||||||
@ -126,14 +108,21 @@ void main() {
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def _convert_es_to_desktop(source: str) -> str:
|
|
||||||
"""Convert GLSL ES (WebGL) shader source to desktop GLSL 330 core."""
|
def _egl_attribs(*values):
|
||||||
# Remove any existing #version directive
|
"""Build an EGL_NONE-terminated EGLint attribute array."""
|
||||||
source = re.sub(r"#version\s+\d+(\s+es)?\s*\n?", "", source, flags=re.IGNORECASE)
|
vals = list(values) + [EGL.EGL_NONE]
|
||||||
# Remove precision qualifiers (not needed in desktop GLSL)
|
return (ctypes.c_int32 * len(vals))(*vals)
|
||||||
source = re.sub(r"precision\s+(lowp|mediump|highp)\s+\w+\s*;\s*\n?", "", source)
|
|
||||||
# Prepend desktop GLSL version
|
|
||||||
return "#version 330 core\n" + source
|
def _gl_str(name):
|
||||||
|
"""Get an OpenGL string parameter."""
|
||||||
|
v = gl.glGetString(name)
|
||||||
|
if not v:
|
||||||
|
return "Unknown"
|
||||||
|
if isinstance(v, bytes):
|
||||||
|
return v.decode(errors="replace")
|
||||||
|
return ctypes.string_at(v).decode(errors="replace")
|
||||||
|
|
||||||
|
|
||||||
def _detect_output_count(source: str) -> int:
|
def _detect_output_count(source: str) -> int:
|
||||||
@ -159,163 +148,8 @@ def _detect_pass_count(source: str) -> int:
|
|||||||
return 1
|
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
|
|
||||||
|
|
||||||
|
|
||||||
class GLContext:
|
class GLContext:
|
||||||
"""Manages OpenGL context and resources for shader execution.
|
"""Manages an OpenGL ES 3.0 context via EGL/ANGLE (singleton)."""
|
||||||
|
|
||||||
Tries backends in order: GLFW (desktop) → EGL (headless GPU) → OSMesa (software).
|
|
||||||
"""
|
|
||||||
|
|
||||||
_instance = None
|
_instance = None
|
||||||
_initialized = False
|
_initialized = False
|
||||||
@ -327,131 +161,111 @@ class GLContext:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
if GLContext._initialized:
|
if GLContext._initialized:
|
||||||
logger.debug("GLContext.__init__: already initialized, skipping")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
logger.debug("GLContext.__init__: starting initialization")
|
|
||||||
|
|
||||||
global glfw, EGL
|
|
||||||
|
|
||||||
import time
|
import time
|
||||||
start = time.perf_counter()
|
start = time.perf_counter()
|
||||||
|
|
||||||
self._backend = None
|
self._display = None
|
||||||
self._window = None
|
self._surface = None
|
||||||
self._egl_display = None
|
self._context = None
|
||||||
self._egl_context = None
|
|
||||||
self._egl_surface = None
|
|
||||||
self._osmesa_ctx = None
|
|
||||||
self._osmesa_buffer = None
|
|
||||||
self._vao = None
|
self._vao = None
|
||||||
|
|
||||||
# Try backends in order: GLFW → EGL → OSMesa
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
logger.debug("GLContext.__init__: trying GLFW backend")
|
|
||||||
try:
|
try:
|
||||||
self._window, glfw = _init_glfw()
|
self._display = EGL.eglGetDisplay(EGL.EGL_DEFAULT_DISPLAY)
|
||||||
self._backend = "glfw"
|
if not self._display:
|
||||||
logger.debug("GLContext.__init__: GLFW backend succeeded")
|
raise RuntimeError("eglGetDisplay() returned no display")
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"GLContext.__init__: GLFW backend failed: {e}")
|
|
||||||
errors.append(("GLFW", e))
|
|
||||||
|
|
||||||
if self._backend is None:
|
major, minor = ctypes.c_int32(0), ctypes.c_int32(0)
|
||||||
logger.debug("GLContext.__init__: trying EGL backend")
|
if not EGL.eglInitialize(self._display, ctypes.byref(major), ctypes.byref(minor)):
|
||||||
try:
|
err = EGL.eglGetError()
|
||||||
self._egl_display, self._egl_context, self._egl_surface, EGL = _init_egl()
|
self._display = None
|
||||||
self._backend = "egl"
|
raise RuntimeError(f"eglInitialize() failed (EGL error: 0x{err:04X})")
|
||||||
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:
|
if not EGL.eglBindAPI(EGL.EGL_OPENGL_ES_API):
|
||||||
logger.debug("GLContext.__init__: trying OSMesa backend")
|
raise RuntimeError("eglBindAPI(EGL_OPENGL_ES_API) failed")
|
||||||
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:
|
config = EGL.EGLConfig()
|
||||||
if sys.platform == "win32":
|
n_configs = ctypes.c_int32(0)
|
||||||
platform_help = (
|
if not EGL.eglChooseConfig(
|
||||||
"Windows: Ensure GPU drivers are installed and display is available.\n"
|
self._display,
|
||||||
" CPU-only/headless mode is not supported on Windows."
|
_egl_attribs(
|
||||||
)
|
EGL.EGL_RENDERABLE_TYPE, EGL.EGL_OPENGL_ES3_BIT,
|
||||||
elif sys.platform == "darwin":
|
EGL.EGL_SURFACE_TYPE, EGL.EGL_PBUFFER_BIT,
|
||||||
platform_help = (
|
EGL.EGL_RED_SIZE, 8, EGL.EGL_GREEN_SIZE, 8,
|
||||||
"macOS: GLFW is not supported.\n"
|
EGL.EGL_BLUE_SIZE, 8, EGL.EGL_ALPHA_SIZE, 8,
|
||||||
" Install OSMesa via Homebrew: brew install mesa\n"
|
),
|
||||||
" Then: pip install PyOpenGL PyOpenGL-accelerate"
|
ctypes.byref(config), 1, ctypes.byref(n_configs),
|
||||||
)
|
) or n_configs.value == 0:
|
||||||
else:
|
raise RuntimeError("eglChooseConfig() failed")
|
||||||
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)
|
self._surface = EGL.eglCreatePbufferSurface(
|
||||||
raise RuntimeError(
|
self._display, config,
|
||||||
f"Failed to create OpenGL context.\n\n"
|
_egl_attribs(EGL.EGL_WIDTH, 64, EGL.EGL_HEIGHT, 64),
|
||||||
f"Backend errors:\n{error_details}\n\n"
|
|
||||||
f"{platform_help}"
|
|
||||||
)
|
)
|
||||||
|
if not self._surface:
|
||||||
|
raise RuntimeError("eglCreatePbufferSurface() failed")
|
||||||
|
|
||||||
# Now import OpenGL.GL (after context is current)
|
self._context = EGL.eglCreateContext(
|
||||||
logger.debug("GLContext.__init__: importing OpenGL.GL")
|
self._display, config, EGL.EGL_NO_CONTEXT,
|
||||||
_import_opengl()
|
_egl_attribs(EGL.EGL_CONTEXT_CLIENT_VERSION, 3),
|
||||||
|
)
|
||||||
|
if not self._context:
|
||||||
|
raise RuntimeError("eglCreateContext() failed")
|
||||||
|
|
||||||
# Create VAO (required for core profile, but OSMesa may use compat profile)
|
if not EGL.eglMakeCurrent(self._display, self._surface, self._surface, self._context):
|
||||||
logger.debug("GLContext.__init__: creating VAO")
|
raise RuntimeError("eglMakeCurrent() failed")
|
||||||
try:
|
|
||||||
vao = gl.glGenVertexArrays(1)
|
self._vao = gl.glGenVertexArrays(1)
|
||||||
gl.glBindVertexArray(vao)
|
gl.glBindVertexArray(self._vao)
|
||||||
self._vao = vao # Only store after successful bind
|
|
||||||
logger.debug("GLContext.__init__: VAO created successfully")
|
except Exception:
|
||||||
except Exception as e:
|
self._cleanup()
|
||||||
logger.debug(f"GLContext.__init__: VAO creation failed (may be expected for OSMesa): {e}")
|
raise
|
||||||
# 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) * 1000
|
elapsed = (time.perf_counter() - start) * 1000
|
||||||
|
|
||||||
# Log device info
|
renderer = _gl_str(gl.GL_RENDERER)
|
||||||
renderer = gl.glGetString(gl.GL_RENDERER)
|
vendor = _gl_str(gl.GL_VENDOR)
|
||||||
vendor = gl.glGetString(gl.GL_VENDOR)
|
version = _gl_str(gl.GL_VERSION)
|
||||||
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"
|
|
||||||
|
|
||||||
GLContext._initialized = True
|
GLContext._initialized = True
|
||||||
logger.info(f"GLSL context initialized in {elapsed:.1f}ms ({self._backend}) - {renderer} ({vendor}), GL {version}")
|
logger.info(f"GLSL context initialized in {elapsed:.1f}ms - {renderer} ({vendor}), GL {version}")
|
||||||
|
|
||||||
def make_current(self):
|
def make_current(self):
|
||||||
if self._backend == "glfw":
|
EGL.eglMakeCurrent(self._display, self._surface, self._surface, self._context)
|
||||||
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)
|
|
||||||
|
|
||||||
if self._vao is not None:
|
if self._vao is not None:
|
||||||
gl.glBindVertexArray(self._vao)
|
gl.glBindVertexArray(self._vao)
|
||||||
|
|
||||||
|
def _cleanup(self):
|
||||||
|
if not self._display:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
if self._vao is not None:
|
||||||
|
gl.glDeleteVertexArrays(1, [self._vao])
|
||||||
|
self._vao = None
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
EGL.eglMakeCurrent(self._display, EGL.EGL_NO_SURFACE, EGL.EGL_NO_SURFACE, EGL.EGL_NO_CONTEXT)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if self._context:
|
||||||
|
EGL.eglDestroyContext(self._display, self._context)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if self._surface:
|
||||||
|
EGL.eglDestroySurface(self._display, self._surface)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
EGL.eglTerminate(self._display)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._display = None
|
||||||
|
|
||||||
|
|
||||||
def _compile_shader(source: str, shader_type: int) -> int:
|
def _compile_shader(source: str, shader_type: int) -> int:
|
||||||
"""Compile a shader and return its ID."""
|
"""Compile a shader and return its ID."""
|
||||||
@ -459,8 +273,10 @@ def _compile_shader(source: str, shader_type: int) -> int:
|
|||||||
gl.glShaderSource(shader, source)
|
gl.glShaderSource(shader, source)
|
||||||
gl.glCompileShader(shader)
|
gl.glCompileShader(shader)
|
||||||
|
|
||||||
if gl.glGetShaderiv(shader, gl.GL_COMPILE_STATUS) != gl.GL_TRUE:
|
if not gl.glGetShaderiv(shader, gl.GL_COMPILE_STATUS):
|
||||||
error = gl.glGetShaderInfoLog(shader).decode()
|
error = gl.glGetShaderInfoLog(shader)
|
||||||
|
if isinstance(error, bytes):
|
||||||
|
error = error.decode(errors="replace")
|
||||||
gl.glDeleteShader(shader)
|
gl.glDeleteShader(shader)
|
||||||
raise RuntimeError(f"Shader compilation failed:\n{error}")
|
raise RuntimeError(f"Shader compilation failed:\n{error}")
|
||||||
|
|
||||||
@ -484,8 +300,10 @@ def _create_program(vertex_source: str, fragment_source: str) -> int:
|
|||||||
gl.glDeleteShader(vertex_shader)
|
gl.glDeleteShader(vertex_shader)
|
||||||
gl.glDeleteShader(fragment_shader)
|
gl.glDeleteShader(fragment_shader)
|
||||||
|
|
||||||
if gl.glGetProgramiv(program, gl.GL_LINK_STATUS) != gl.GL_TRUE:
|
if not gl.glGetProgramiv(program, gl.GL_LINK_STATUS):
|
||||||
error = gl.glGetProgramInfoLog(program).decode()
|
error = gl.glGetProgramInfoLog(program)
|
||||||
|
if isinstance(error, bytes):
|
||||||
|
error = error.decode(errors="replace")
|
||||||
gl.glDeleteProgram(program)
|
gl.glDeleteProgram(program)
|
||||||
raise RuntimeError(f"Program linking failed:\n{error}")
|
raise RuntimeError(f"Program linking failed:\n{error}")
|
||||||
|
|
||||||
@ -530,9 +348,6 @@ def _render_shader_batch(
|
|||||||
ctx = GLContext()
|
ctx = GLContext()
|
||||||
ctx.make_current()
|
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
|
# Detect how many outputs the shader actually uses
|
||||||
num_outputs = _detect_output_count(fragment_code)
|
num_outputs = _detect_output_count(fragment_code)
|
||||||
|
|
||||||
@ -558,9 +373,9 @@ def _render_shader_batch(
|
|||||||
try:
|
try:
|
||||||
# Compile shaders (once for all batches)
|
# Compile shaders (once for all batches)
|
||||||
try:
|
try:
|
||||||
program = _create_program(VERTEX_SHADER, fragment_source)
|
program = _create_program(VERTEX_SHADER, fragment_code)
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
logger.error(f"Fragment shader:\n{fragment_source}")
|
logger.error(f"Fragment shader:\n{fragment_code}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
gl.glUseProgram(program)
|
gl.glUseProgram(program)
|
||||||
@ -723,13 +538,13 @@ def _render_shader_batch(
|
|||||||
gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3)
|
gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3)
|
||||||
|
|
||||||
# Read back outputs for this batch
|
# Read back outputs for this batch
|
||||||
# (glGetTexImage is synchronous, implicitly waits for rendering)
|
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fbo)
|
||||||
batch_outputs = []
|
batch_outputs = []
|
||||||
for tex in output_textures:
|
for i in range(num_outputs):
|
||||||
gl.glBindTexture(gl.GL_TEXTURE_2D, tex)
|
gl.glReadBuffer(gl.GL_COLOR_ATTACHMENT0 + i)
|
||||||
data = gl.glGetTexImage(gl.GL_TEXTURE_2D, 0, gl.GL_RGBA, gl.GL_FLOAT)
|
buf = np.empty((height, width, 4), dtype=np.float32)
|
||||||
img = np.frombuffer(data, dtype=np.float32).reshape(height, width, 4)
|
gl.glReadPixels(0, 0, width, height, gl.GL_RGBA, gl.GL_FLOAT, buf)
|
||||||
batch_outputs.append(img[::-1, :, :].copy())
|
batch_outputs.append(buf[::-1, :, :].copy())
|
||||||
|
|
||||||
# Pad with black images for unused outputs
|
# Pad with black images for unused outputs
|
||||||
black_img = np.zeros((height, width, 4), dtype=np.float32)
|
black_img = np.zeros((height, width, 4), dtype=np.float32)
|
||||||
@ -750,18 +565,18 @@ def _render_shader_batch(
|
|||||||
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
|
gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, 0)
|
||||||
gl.glUseProgram(0)
|
gl.glUseProgram(0)
|
||||||
|
|
||||||
for tex in input_textures:
|
if input_textures:
|
||||||
gl.glDeleteTextures(int(tex))
|
gl.glDeleteTextures(len(input_textures), input_textures)
|
||||||
for tex in curve_textures:
|
if curve_textures:
|
||||||
gl.glDeleteTextures(int(tex))
|
gl.glDeleteTextures(len(curve_textures), curve_textures)
|
||||||
for tex in output_textures:
|
if output_textures:
|
||||||
gl.glDeleteTextures(int(tex))
|
gl.glDeleteTextures(len(output_textures), output_textures)
|
||||||
for tex in ping_pong_textures:
|
if ping_pong_textures:
|
||||||
gl.glDeleteTextures(int(tex))
|
gl.glDeleteTextures(len(ping_pong_textures), ping_pong_textures)
|
||||||
if fbo is not None:
|
if fbo is not None:
|
||||||
gl.glDeleteFramebuffers(1, [fbo])
|
gl.glDeleteFramebuffers(1, [fbo])
|
||||||
for pp_fbo in ping_pong_fbos:
|
if ping_pong_fbos:
|
||||||
gl.glDeleteFramebuffers(1, [pp_fbo])
|
gl.glDeleteFramebuffers(len(ping_pong_fbos), ping_pong_fbos)
|
||||||
if program is not None:
|
if program is not None:
|
||||||
gl.glDeleteProgram(program)
|
gl.glDeleteProgram(program)
|
||||||
|
|
||||||
|
|||||||
@ -33,5 +33,5 @@ kornia>=0.7.1
|
|||||||
spandrel
|
spandrel
|
||||||
pydantic~=2.0
|
pydantic~=2.0
|
||||||
pydantic-settings~=2.0
|
pydantic-settings~=2.0
|
||||||
PyOpenGL
|
PyOpenGL>=3.1.8
|
||||||
glfw
|
comfy-angle
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user