From 708979fa93c596495b409cf177ea01b9ae884d72 Mon Sep 17 00:00:00 2001 From: Joao Matos Date: Tue, 11 Aug 2026 01:30:48 +0100 Subject: [PATCH 1/2] egl: preserve caller state and support surfaceless contexts --- src/glue/gl.cpp | 6 + src/glue/gl_egl.cpp | 422 +++++++++++++++++++++++++++++++++----------- 2 files changed, 326 insertions(+), 102 deletions(-) diff --git a/src/glue/gl.cpp b/src/glue/gl.cpp index 2425717eb90..dd73a2f6188 100644 --- a/src/glue/gl.cpp +++ b/src/glue/gl.cpp @@ -120,6 +120,11 @@ initialization spit out lots of info about the underlying OpenGL implementation. + - COIN_EGL: set to "1" to select EGL for offscreen contexts, or to "0" + to select GLX when both bindings are available. If unset, Coin selects + the binding associated with the current context and otherwise defaults to + GLX. + - COIN_PREFER_GLPOLYGONOFFSET_EXT: when set to "1" and both glPolygonOffset() and glPolygonOffsetEXT() are available, the latter will be used. This can be useful to work around a @@ -4384,6 +4389,7 @@ cc_glglue_context_create_offscreen(unsigned int width, unsigned int height) #elif defined(HAVE_WGL) return wglglue_context_create_offscreen(width, height); #else + check_egl(); #if defined(HAVE_EGL) if (COIN_USE_EGL > 0) return eglglue_context_create_offscreen(width, height); #endif diff --git a/src/glue/gl_egl.cpp b/src/glue/gl_egl.cpp index 85c8e89dfbf..8feb5098baa 100644 --- a/src/glue/gl_egl.cpp +++ b/src/glue/gl_egl.cpp @@ -33,8 +33,8 @@ /* * Environment variable controls available: * - * - COIN_EGLGLUE_NO_PBUFFERS: set to 1 to force software rendering of - * offscreen contexts. + * - COIN_EGL_CORE_PROFILE: set to 1 to request an OpenGL core-profile + * context for offscreen rendering. */ #include "glue/gl_egl.h" @@ -47,6 +47,7 @@ #include #include #include +#include #include #include @@ -98,6 +99,12 @@ SbBool eglglue_context_pbuffer_max(void * ctx, unsigned int * lims) #include EGLDisplay eglglue_display = EGL_NO_DISPLAY; +// A display borrowed from an application's current context must not be +// terminated when Coin's EGL glue is cleaned up. +static SbBool eglglue_display_owned = FALSE; +static SbBool eglglue_display_initialized = FALSE; +static EGLint eglglue_display_major = 0; +static EGLint eglglue_display_minor = 0; struct eglglue_contextdata; #define CASE_STR( value ) case value: return #value; @@ -135,11 +142,53 @@ const char* eglAPIString( EGLenum api ) } #undef CASE_STR +struct eglglue_binding { + EGLenum api; + EGLDisplay display; + EGLContext context; + EGLSurface drawSurface; + EGLSurface readSurface; +}; + +static eglglue_binding +eglglue_capture_binding() +{ + eglglue_binding binding; + binding.api = eglQueryAPI(); + binding.display = eglGetCurrentDisplay(); + binding.context = eglGetCurrentContext(); + binding.drawSurface = eglGetCurrentSurface(EGL_DRAW); + binding.readSurface = eglGetCurrentSurface(EGL_READ); + return binding; +} + +static SbBool +eglglue_bind_api(EGLenum api) +{ + return api == EGL_NONE || eglBindAPI(api) == EGL_TRUE; +} + +static SbBool +eglglue_restore_binding(const eglglue_binding & binding, + EGLDisplay fallbackDisplay) +{ + const EGLDisplay display = binding.display != EGL_NO_DISPLAY + ? binding.display : fallbackDisplay; + if (display == EGL_NO_DISPLAY || !eglglue_bind_api(binding.api)) { + return FALSE; + } + return eglMakeCurrent(display, + binding.drawSurface, + binding.readSurface, + binding.context) == EGL_TRUE; +} + struct eglglue_contextdata { + EGLDisplay display; EGLContext context; EGLSurface surface; - EGLContext storedcontext; - EGLSurface storedsurface; + EGLConfig config; + eglglue_binding previousBinding; unsigned int width; unsigned int height; }; @@ -148,52 +197,88 @@ static struct eglglue_contextdata * eglglue_contextdata_init(unsigned int width, unsigned int height) { struct eglglue_contextdata * ctx; - ctx = (struct eglglue_contextdata *)malloc(sizeof(struct eglglue_contextdata)); + ctx = static_cast(malloc(sizeof(*ctx))); + if (!ctx) return NULL; + ctx->display = EGL_NO_DISPLAY; ctx->context = EGL_NO_CONTEXT; ctx->surface = EGL_NO_SURFACE; - ctx->storedcontext = EGL_NO_CONTEXT; - ctx->storedsurface = EGL_NO_SURFACE; + ctx->config = (EGLConfig) 0; + ctx->previousBinding.api = EGL_NONE; + ctx->previousBinding.display = EGL_NO_DISPLAY; + ctx->previousBinding.context = EGL_NO_CONTEXT; + ctx->previousBinding.drawSurface = EGL_NO_SURFACE; + ctx->previousBinding.readSurface = EGL_NO_SURFACE; ctx->width = width; ctx->height = height; return ctx; } static EGLDisplay -eglglue_get_display(void) +eglglue_acquire_display(void) { PFNEGLGETPLATFORMDISPLAYEXTPROC eglGetPlatformDisplayEXT; - - if (eglglue_display != EGL_NO_DISPLAY) { - return eglglue_display; - } - - eglglue_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); - if (eglglue_display != EGL_NO_DISPLAY) { - goto found; + const char * platform = coin_getenv("EGL_PLATFORM"); + const EGLDisplay currentDisplay = eglGetCurrentDisplay(); + + // Reuse an active EGL display so Coin follows the platform selected by the + // application. Fall back to EGL_DEFAULT_DISPLAY only when no EGL context + // is current. + // Surfaceless rendering is selected explicitly because it has no native + // Wayland or X11 display to probe. + const bool requestedSurfaceless = + platform && strcmp(platform, "surfaceless") == 0; + + if (!requestedSurfaceless && currentDisplay != EGL_NO_DISPLAY) { + eglglue_display = currentDisplay; + eglglue_display_owned = FALSE; + return eglglue_display; } eglGetPlatformDisplayEXT = (PFNEGLGETPLATFORMDISPLAYEXTPROC)eglGetProcAddress("eglGetPlatformDisplayEXT"); - if (!eglGetPlatformDisplayEXT) { - return EGL_NO_DISPLAY; - } - eglglue_display = eglGetPlatformDisplay(EGL_PLATFORM_WAYLAND_KHR, EGL_DEFAULT_DISPLAY, NULL); - if (eglglue_display != EGL_NO_DISPLAY) { - goto found; +#ifdef EGL_PLATFORM_SURFACELESS_MESA + if (requestedSurfaceless) { + if (!eglGetPlatformDisplayEXT) { + cc_debugerror_post("eglglue_acquire_display", + "EGL_PLATFORM=surfaceless requested, but " + "eglGetPlatformDisplayEXT is unavailable."); + return EGL_NO_DISPLAY; + } + eglglue_display = eglGetPlatformDisplayEXT( + EGL_PLATFORM_SURFACELESS_MESA, EGL_DEFAULT_DISPLAY, NULL); + if (eglglue_display == EGL_NO_DISPLAY) { + cc_debugerror_post("eglglue_acquire_display", + "EGL_PLATFORM=surfaceless requested, but the " + "surfaceless display is unavailable. %s", + eglErrorString(eglGetError())); + return EGL_NO_DISPLAY; + } + } else { + eglglue_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); } - - eglglue_display = eglGetPlatformDisplay(EGL_PLATFORM_X11_EXT, EGL_DEFAULT_DISPLAY, NULL); +#else + if (requestedSurfaceless) { + cc_debugerror_post("eglglue_acquire_display", + "EGL_PLATFORM=surfaceless requested, but the " + "surfaceless Mesa platform is unavailable."); + return EGL_NO_DISPLAY; + } + eglglue_display = eglGetDisplay(EGL_DEFAULT_DISPLAY); +#endif if (eglglue_display == EGL_NO_DISPLAY) { - cc_debugerror_post("eglglue_get_display", "Display not found."); + cc_debugerror_post("eglglue_acquire_display", + "Could not obtain the default EGL display. %s", + eglErrorString(eglGetError())); return EGL_NO_DISPLAY; } -found: + eglglue_display_owned = TRUE; + if (coin_glglue_debug()) { - cc_debugerror_postinfo("eglglue_get_display", + cc_debugerror_postinfo("eglglue_acquire_display", "got EGLDisplay==%p", eglglue_display); } @@ -201,9 +286,49 @@ eglglue_get_display(void) return eglglue_display; } +static EGLDisplay +eglglue_get_display(void) +{ + if (eglglue_display == EGL_NO_DISPLAY) { + eglglue_display = eglglue_acquire_display(); + } + return eglglue_display; +} + +static SbBool +eglglue_initialize_display(EGLDisplay display, EGLint * major, EGLint * minor) +{ + if (display == EGL_NO_DISPLAY) { + return FALSE; + } + + if (display == eglglue_display && eglglue_display_initialized) { + if (major) *major = eglglue_display_major; + if (minor) *minor = eglglue_display_minor; + return TRUE; + } + + EGLint initializedMajor; + EGLint initializedMinor; + if (eglInitialize(display, &initializedMajor, &initializedMinor) == EGL_FALSE) { + return FALSE; + } + + if (display == eglglue_display) { + eglglue_display_major = initializedMajor; + eglglue_display_minor = initializedMinor; + eglglue_display_initialized = TRUE; + } + if (major) *major = initializedMajor; + if (minor) *minor = initializedMinor; + return TRUE; +} + void eglglue_init(cc_glglue * w) { + EGLDisplay display = eglglue_get_display(); + const EGLenum previousApi = eglQueryAPI(); w->glx.isdirect = 1; w->glx.serverversion = NULL; w->glx.servervendor = NULL; @@ -215,14 +340,16 @@ eglglue_init(cc_glglue * w) w->glx.glXGetCurrentDisplay = (COIN_PFNGLXGETCURRENTDISPLAYPROC)eglglue_getprocaddress(w, "eglglue_get_display"); - if (eglInitialize(eglglue_get_display(), &w->glx.version.major, &w->glx.version.minor) == EGL_FALSE) { + if (!eglglue_initialize_display(display, + &w->glx.version.major, + &w->glx.version.minor)) { cc_debugerror_post("eglglue_init", "Couldn't initialize EGL. %s", eglErrorString(eglGetError())); return; } - if (eglBindAPI(EGL_OPENGL_API) == EGL_FALSE) { + if (!eglglue_bind_api(EGL_OPENGL_API)) { cc_debugerror_post("eglglue_init", "eglBindAPI(EGL_OPENGL_API) failed. %s", eglErrorString(eglGetError())); @@ -250,26 +377,76 @@ eglglue_init(cc_glglue * w) "eglQueryString(EGL_EXTENSIONS)=='%s'", eglQueryString(eglglue_get_display(), EGL_EXTENSIONS)); } + + if (previousApi != EGL_NONE && !eglglue_bind_api(previousApi)) { + cc_debugerror_post("eglglue_init", + "Could not restore the EGL client API (%s). %s", + eglAPIString(previousApi), + eglErrorString(eglGetError())); + } } static void eglglue_contextdata_cleanup(struct eglglue_contextdata * ctx) { if (ctx == NULL) { return; } - if (eglglue_get_display() != EGL_NO_DISPLAY && ctx->context != EGL_NO_CONTEXT) eglDestroyContext(eglglue_get_display(), ctx->context); - if (eglglue_get_display() != EGL_NO_DISPLAY && ctx->surface != EGL_NO_SURFACE) eglDestroySurface(eglglue_get_display(), ctx->surface); - if (eglglue_get_display() != EGL_NO_DISPLAY && ctx->storedcontext != EGL_NO_CONTEXT) eglDestroyContext(eglglue_get_display(), ctx->storedcontext); - if (eglglue_get_display() != EGL_NO_DISPLAY && ctx->storedsurface != EGL_NO_SURFACE) eglDestroySurface(eglglue_get_display(), ctx->storedsurface); + EGLDisplay display = ctx->display; + if (display != EGL_NO_DISPLAY && ctx->context != EGL_NO_CONTEXT) { + if (eglGetCurrentContext() == ctx->context) { + eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); + } + eglDestroyContext(display, ctx->context); + } + if (display != EGL_NO_DISPLAY && ctx->surface != EGL_NO_SURFACE) { + eglDestroySurface(display, ctx->surface); + } free(ctx); } +static SbBool +eglglue_core_profile_requested(void) +{ + const char * coreprofile = coin_getenv("COIN_EGL_CORE_PROFILE"); + return coreprofile && atoi(coreprofile) > 0; +} + +static SbBool +eglglue_core_profile_supported(EGLDisplay display, EGLint major, EGLint minor) +{ + const char * extensions = eglQueryString(display, EGL_EXTENSIONS); + return (major > 1 || (major == 1 && minor >= 5)) || + (extensions != NULL && + coin_glglue_extension_available(extensions, "EGL_KHR_create_context")); +} + +static void +eglglue_context_attributes(SbBool requestCoreProfile, EGLint * attributes) +{ + if (!requestCoreProfile) { + attributes[0] = EGL_NONE; + return; + } + + attributes[0] = EGL_CONTEXT_MAJOR_VERSION_KHR; + attributes[1] = 3; + attributes[2] = EGL_CONTEXT_MINOR_VERSION_KHR; + attributes[3] = 3; + attributes[4] = EGL_CONTEXT_OPENGL_PROFILE_MASK_KHR; + attributes[5] = EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT_KHR; + attributes[6] = EGL_NONE; +} + void * eglglue_context_create_offscreen(unsigned int width, unsigned int height) { - struct eglglue_contextdata * ctx; - EGLint format; - EGLint numConfigs; - EGLConfig config; + struct eglglue_contextdata * ctx = NULL; + EGLint numConfigs = 0; + EGLDisplay display = EGL_NO_DISPLAY; + EGLint eglmajor = 0; + EGLint eglminor = 0; + EGLenum previousApi = EGL_NONE; + SbBool requestCoreProfile = FALSE; + SbBool success = FALSE; EGLint attrib[] = { EGL_RENDERABLE_TYPE, EGL_OPENGL_BIT, EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, @@ -281,14 +458,12 @@ eglglue_context_create_offscreen(unsigned int width, unsigned int height) EGL_STENCIL_SIZE, 1, EGL_NONE }; - - EGLAttrib surface_attrib[] = { - EGL_TEXTURE_FORMAT, EGL_TEXTURE_RGBA, - EGL_TEXTURE_TARGET, EGL_TEXTURE_2D, - EGL_WIDTH, (EGLint) ctx->width, - EGL_HEIGHT, (EGLint) ctx->height, + EGLint surface_attrib[] = { + EGL_WIDTH, (EGLint) width, + EGL_HEIGHT, (EGLint) height, EGL_NONE }; + EGLint context_attribs[7]; ctx = eglglue_contextdata_init(width, height); if (!ctx) return NULL; @@ -303,72 +478,85 @@ eglglue_context_create_offscreen(unsigned int width, unsigned int height) "Creating offscreen context."); } - if (eglBindAPI(EGL_OPENGL_API) == EGL_FALSE) { + previousApi = eglQueryAPI(); + display = eglglue_get_display(); + ctx->display = display; + if (!eglglue_initialize_display(display, &eglmajor, &eglminor)) { + cc_debugerror_post("eglglue_context_create_offscreen", + "eglInitialize failed. %s", + eglErrorString(eglGetError())); + goto cleanup; + } + + if (!eglglue_bind_api(EGL_OPENGL_API)) { cc_debugerror_post("eglglue_context_create_offscreen", "eglBindAPI(EGL_OPENGL_API) failed. %s", eglErrorString(eglGetError())); - return NULL; + goto cleanup; } - const char * env = coin_getenv("COIN_EGLGLUE_NO_PBUFFERS"); - if (env && atoi(env) > 0) { - attrib[3] = EGL_PIXMAP_BIT; - if (coin_glglue_debug()) { - cc_debugerror_postinfo("eglglue_context_create_offscreen", - "Force software rendering."); + requestCoreProfile = eglglue_core_profile_requested(); + if (requestCoreProfile) { + if (!eglglue_core_profile_supported(display, eglmajor, eglminor)) { + cc_debugerror_post("eglglue_context_create_offscreen", + "COIN_EGL_CORE_PROFILE requested, but EGL does not " + "support core-profile context attributes."); + goto cleanup; } } - eglChooseConfig(eglglue_get_display(), attrib, &config, 1, &numConfigs); - if (numConfigs == 0) { - if (attrib[3] == EGL_PBUFFER_BIT) { - if (coin_glglue_debug()) { - cc_debugerror_postinfo("eglglue_context_create_offscreen", - "PBuffer offscreen rendering is NOT supported " - "by the OpenGL driver. Try software rendering."); - } - attrib[3] = EGL_PIXMAP_BIT; - eglChooseConfig(eglglue_get_display(), attrib, &config, 1, &numConfigs); - } + if (eglChooseConfig(display, attrib, &ctx->config, 1, &numConfigs) == EGL_FALSE) { + cc_debugerror_post("eglglue_context_create_offscreen", + "eglChooseConfig failed. %s", + eglErrorString(eglGetError())); + goto cleanup; } if (numConfigs == 0) { cc_debugerror_post("eglglue_context_create_offscreen", - "No matching EGL config. %s", + "No matching EGL pbuffer config. %s", eglErrorString(eglGetError())); - eglglue_contextdata_cleanup(ctx); - return NULL; + goto cleanup; } - if (attrib[3] == EGL_PBUFFER_BIT) { - ctx->surface = eglCreatePlatformWindowSurface(eglglue_get_display(), config, 0, surface_attrib); - } else { - ctx->surface = eglCreatePlatformPixmapSurface(eglglue_get_display(), config, 0, surface_attrib); - } + ctx->surface = eglCreatePbufferSurface(display, ctx->config, surface_attrib); if (ctx->surface == EGL_NO_SURFACE) { cc_debugerror_post("eglglue_context_create_offscreen", "Couldn't create EGL surface. %s", eglErrorString(eglGetError())); - eglglue_contextdata_cleanup(ctx); - return NULL; + goto cleanup; } - ctx->context = eglCreateContext(eglglue_get_display(), config, EGL_NO_CONTEXT, NULL); + eglglue_context_attributes(requestCoreProfile, context_attribs); + ctx->context = eglCreateContext(display, ctx->config, EGL_NO_CONTEXT, + context_attribs); if (ctx->context == EGL_NO_CONTEXT) { cc_debugerror_post("eglglue_context_create_offscreen", "Couldn't create EGL context. %s", eglErrorString(eglGetError())); - eglglue_contextdata_cleanup(ctx); - return NULL; + goto cleanup; } + success = TRUE; if (coin_glglue_debug()) { cc_debugerror_postinfo("eglglue_context_create_offscreen", - "created new %s offscreen context == %p", - attrib[3] == EGL_PBUFFER_BIT ? "pBuffer" : "software", + "created new pBuffer offscreen context == %p", ctx->context); } + +cleanup: + if (previousApi != EGL_NONE && !eglglue_bind_api(previousApi)) { + cc_debugerror_post("eglglue_context_create_offscreen", + "Could not restore the EGL client API (%s). %s", + eglAPIString(previousApi), + eglErrorString(eglGetError())); + success = FALSE; + } + if (!success) { + eglglue_contextdata_cleanup(ctx); + ctx = NULL; + } return ctx; } @@ -376,13 +564,30 @@ SbBool eglglue_context_make_current(void * ctx) { struct eglglue_contextdata * context = (struct eglglue_contextdata *)ctx; + if (context == NULL || context->display == EGL_NO_DISPLAY) { + return FALSE; + } - context->storedcontext = eglGetCurrentContext(); - context->storedsurface = eglGetCurrentSurface(EGL_DRAW); - if (eglMakeCurrent(eglglue_get_display(), context->surface, context->surface, context->context) == EGL_FALSE) { + context->previousBinding = eglglue_capture_binding(); + if (!eglglue_bind_api(EGL_OPENGL_API)) { + cc_debugerror_post("eglglue_context_make_current", + "eglBindAPI(EGL_OPENGL_API) failed: %s", + eglErrorString(eglGetError())); + return FALSE; + } + + if (eglMakeCurrent(context->display, + context->surface, + context->surface, + context->context) == EGL_FALSE) { cc_debugerror_post("eglglue_context_make_current", "eglMakeCurrent failed: %s", eglErrorString(eglGetError())); + if (!eglglue_restore_binding(context->previousBinding, context->display)) { + cc_debugerror_post("eglglue_context_make_current", + "Could not restore the caller EGL binding: %s", + eglErrorString(eglGetError())); + } return FALSE; } @@ -398,19 +603,21 @@ void eglglue_context_reinstate_previous(void * ctx) { struct eglglue_contextdata * context = (struct eglglue_contextdata *)ctx; + if (context == NULL || context->display == EGL_NO_DISPLAY) { + return; + } - if (context->storedcontext != EGL_NO_CONTEXT && context->storedsurface != EGL_NO_SURFACE) { - if (eglMakeCurrent(eglglue_get_display(), context->storedsurface, context->storedsurface, context->storedcontext) == EGL_TRUE) { - if (coin_glglue_debug()) { - cc_debugerror_postinfo("eglglue_context_make_current", - "EGL Context (0x%X)\n", - context->context); - } - } else { - cc_debugerror_post("eglglue_context_make_current", - "eglMakeCurrent failed: %s", - eglErrorString(eglGetError())); - } + // The API must be restored before the context and its surfaces are made + // current. The display stored with the caller binding is required when it + // belongs to a foreign EGL display; the Coin display is only the fallback + // for restoring EGL_NO_CONTEXT. + if (!eglglue_restore_binding(context->previousBinding, context->display)) { + cc_debugerror_post("eglglue_context_reinstate_previous", + "Could not restore the caller EGL binding: %s", + eglErrorString(eglGetError())); + } else if (coin_glglue_debug()) { + cc_debugerror_postinfo("eglglue_context_reinstate_previous", + "restored caller EGL binding"); } } @@ -431,7 +638,7 @@ eglglue_context_bind_pbuffer(void * ctx) { struct eglglue_contextdata * context = (struct eglglue_contextdata *)ctx; - if (eglBindTexImage(eglglue_get_display(), context->surface, EGL_BACK_BUFFER) == EGL_FALSE) { + if (eglBindTexImage(context->display, context->surface, EGL_BACK_BUFFER) == EGL_FALSE) { cc_debugerror_post("eglglue_context_bind_pbuffer()" "after binding pbuffer: %s", eglErrorString(eglGetError())); @@ -443,7 +650,7 @@ eglglue_context_release_pbuffer(void * ctx) { struct eglglue_contextdata * context = (struct eglglue_contextdata *)ctx; - if (eglReleaseTexImage(eglglue_get_display(), context->surface, EGL_BACK_BUFFER) == EGL_FALSE) { + if (eglReleaseTexImage(context->display, context->surface, EGL_BACK_BUFFER) == EGL_FALSE) { cc_debugerror_post("eglglue_context_release_pbuffer()" "releasing pbuffer: %s", eglErrorString(eglGetError())); @@ -456,7 +663,7 @@ eglglue_context_pbuffer_is_bound(void * ctx) struct eglglue_contextdata * context = (struct eglglue_contextdata *)ctx; GLint buffer = EGL_NONE; - if(eglQueryContext(eglglue_get_display(), context->context, EGL_RENDER_BUFFER, &buffer) == EGL_FALSE) { + if(eglQueryContext(context->display, context->context, EGL_RENDER_BUFFER, &buffer) == EGL_FALSE) { cc_debugerror_post("eglglue_context_pbuffer_is_bound()" "after query pbuffer: %s", eglErrorString(eglGetError())); @@ -465,10 +672,10 @@ eglglue_context_pbuffer_is_bound(void * ctx) } SbBool -eglglue_context_can_render_to_texture(void * ctx) +eglglue_context_can_render_to_texture(void * COIN_UNUSED_ARG(ctx)) { - struct eglglue_contextdata * context = (struct eglglue_contextdata *)ctx; - return context->surface != EGL_NO_SURFACE; + /* Surfaceless pbuffers are not EGL texture targets in this implementation. */ + return FALSE; } SbBool @@ -483,15 +690,20 @@ eglglue_context_pbuffer_max(void * ctx, unsigned int * lims) if (context->surface == EGL_NO_SURFACE) { return FALSE; } for (i = 0; i < 3; i++) { - if(eglQuerySurface(eglglue_get_display(), context->surface, attribs[i], &attribval) == EGL_FALSE) { + if(eglGetConfigAttrib(context->display, context->config, attribs[i], &attribval) == EGL_FALSE) { cc_debugerror_post("eglglue_context_pbuffer_max", - "eglQuerySurface() failed, " + "eglGetConfigAttrib() failed, " "returned error code %s", eglErrorString(eglGetError())); return FALSE; } assert(attribval >= 0); - lims[i] = (unsigned int)attribval; + // EGL permits EGL_MAX_PBUFFER_PIXELS to be reported as zero when the + // implementation does not impose a pixel-count limit. Coin uses zero as + // an actual limit, so normalize that value to the representable maximum. + lims[i] = (attribs[i] == EGL_MAX_PBUFFER_PIXELS && attribval == 0) + ? UINT_MAX + : (unsigned int)attribval; } return TRUE; } @@ -505,8 +717,14 @@ eglglue_getprocaddress(const cc_glglue * glue_in, const char * fname) void eglglue_cleanup(void) { - if (eglglue_display != EGL_NO_DISPLAY) eglTerminate(eglglue_display); + if (eglglue_display_owned && eglglue_display != EGL_NO_DISPLAY) { + eglTerminate(eglglue_display); + } eglglue_display = EGL_NO_DISPLAY; + eglglue_display_owned = FALSE; + eglglue_display_initialized = FALSE; + eglglue_display_major = 0; + eglglue_display_minor = 0; } #endif /* HAVE_EGL */ From 63706367d2c25c0cfcee133242c9347649b749f8 Mon Sep 17 00:00:00 2001 From: Joao Matos Date: Tue, 11 Aug 2026 01:30:48 +0100 Subject: [PATCH 2/2] tests: cover EGL context preservation --- .../continuous-integration-workflow.yml | 2 +- testsuite/CMakeLists.txt | 29 +++ testsuite/EGLBindingTest.cpp | 243 ++++++++++++++++++ 3 files changed, 273 insertions(+), 1 deletion(-) create mode 100644 testsuite/EGLBindingTest.cpp diff --git a/.github/workflows/continuous-integration-workflow.yml b/.github/workflows/continuous-integration-workflow.yml index 7c5fd988f6d..b9b8df173c1 100644 --- a/.github/workflows/continuous-integration-workflow.yml +++ b/.github/workflows/continuous-integration-workflow.yml @@ -29,7 +29,7 @@ jobs: - name: Create build directory and run CMake run: | sudo apt-get -y update - sudo apt-get -y install freeglut3-dev + sudo apt-get -y install freeglut3-dev libegl1-mesa-dev cmake -S . -B cmake_build_dir -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=cmake_install_dir -DCOIN_BUILD_LEGACY_GL_RENDERER=ON -DCOIN_BUILD_TESTS=ON -DCOIN_STRICT_WARNINGS=ON - name: Build project run: cmake --build cmake_build_dir --target install --config Release --parallel diff --git a/testsuite/CMakeLists.txt b/testsuite/CMakeLists.txt index e7861f4359e..605c5ada9a8 100644 --- a/testsuite/CMakeLists.txt +++ b/testsuite/CMakeLists.txt @@ -1,3 +1,5 @@ +set(COIN_TEST_SKIP_RETURN_CODE 77) + macro(create_testsuite input) get_filename_component(FLNAME "${input}" NAME_WE) get_filename_component(FLPATH "${input}" PATH) @@ -89,6 +91,33 @@ if (USE_PTHREAD) endif() add_test(NAME CoinTests COMMAND CoinTests) +function(coin_add_egl_test) + cmake_parse_arguments(ARG "" "NAME" "SOURCES;LABELS" ${ARGN}) + if(NOT ARG_NAME OR NOT ARG_SOURCES) + message(FATAL_ERROR "coin_add_egl_test requires NAME and SOURCES") + endif() + add_executable(${ARG_NAME} ${ARG_SOURCES}) + target_link_libraries(${ARG_NAME} PRIVATE Coin OpenGL::EGL) + target_compile_definitions(${ARG_NAME} PRIVATE + COIN_INTERNAL + COIN_TEST_SKIP_RETURN_CODE=${COIN_TEST_SKIP_RETURN_CODE}) + target_include_directories(${ARG_NAME} PRIVATE + ${PROJECT_SOURCE_DIR}/src) + add_test(NAME ${ARG_NAME} COMMAND ${ARG_NAME}) + set(test_labels "egl") + if(ARG_LABELS) + list(APPEND test_labels ${ARG_LABELS}) + endif() + set_tests_properties(${ARG_NAME} PROPERTIES + SKIP_RETURN_CODE ${COIN_TEST_SKIP_RETURN_CODE} + LABELS "${test_labels}") +endfunction() + +if(HAVE_EGL AND CMAKE_SYSTEM_NAME STREQUAL "Linux") + coin_add_egl_test(NAME EGLBindingTest SOURCES EGLBindingTest.cpp + LABELS requires-egl) +endif() + # Many warnings are generated from test macros on macOS with Xcode. include(CheckCXXCompilerFlag) CHECK_CXX_COMPILER_FLAG(-Wtautological-compare DISABLE_WARNING_TAUTOLOGCOMPARE) diff --git a/testsuite/EGLBindingTest.cpp b/testsuite/EGLBindingTest.cpp new file mode 100644 index 00000000000..b334828510f --- /dev/null +++ b/testsuite/EGLBindingTest.cpp @@ -0,0 +1,243 @@ +#include +#include + +#include "glue/gl_egl.h" + +#include + +#include +#include + +#ifndef COIN_TEST_SKIP_RETURN_CODE +#error COIN_TEST_SKIP_RETURN_CODE must match the CTest SKIP_RETURN_CODE property +#endif + +namespace { + +int skip(const char * reason) +{ + std::cout << "SKIP: " << reason << std::endl; + return COIN_TEST_SKIP_RETURN_CODE; +} + +EGLDisplay acquire_surfaceless_display() +{ + PFNEGLGETPLATFORMDISPLAYEXTPROC getPlatformDisplay = + reinterpret_cast( + eglGetProcAddress("eglGetPlatformDisplayEXT")); + if (!getPlatformDisplay) return EGL_NO_DISPLAY; + return getPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA, + EGL_DEFAULT_DISPLAY, NULL); +} + +struct EGLTestState { + EGLDisplay display; + EGLContext foreignContext; + EGLSurface drawSurface; + EGLSurface readSurface; + void * coinContext; + bool coinMadeCurrent; + + explicit EGLTestState(EGLDisplay displayIn) + : display(displayIn), + foreignContext(EGL_NO_CONTEXT), + drawSurface(EGL_NO_SURFACE), + readSurface(EGL_NO_SURFACE), + coinContext(NULL), + coinMadeCurrent(false) + { + } + + void cleanup() + { + if (this->coinMadeCurrent) { + cc_glglue_context_reinstate_previous(this->coinContext); + } + if (this->coinContext) { + cc_glglue_context_destruct(this->coinContext); + } + eglMakeCurrent(this->display, + EGL_NO_SURFACE, + EGL_NO_SURFACE, + EGL_NO_CONTEXT); + if (this->foreignContext != EGL_NO_CONTEXT) { + eglDestroyContext(this->display, this->foreignContext); + } + if (this->drawSurface != EGL_NO_SURFACE) { + eglDestroySurface(this->display, this->drawSurface); + } + if (this->readSurface != EGL_NO_SURFACE) { + eglDestroySurface(this->display, this->readSurface); + } + eglTerminate(this->display); + } +}; + +int fail(const char * message) +{ + std::cerr << "FAIL: " << message << std::endl; + return 1; +} + +int run_owned_surfaceless_test() +{ + setenv("EGL_PLATFORM", "surfaceless", 1); + void * coinContext = cc_glglue_context_create_offscreen(16, 16); + unsetenv("EGL_PLATFORM"); + if (!coinContext) { + eglglue_cleanup(); + return skip("Coin could not create an owned surfaceless context"); + } + + if (!cc_glglue_context_make_current(coinContext)) { + cc_glglue_context_destruct(coinContext); + eglglue_cleanup(); + return fail("Coin owned surfaceless context could not be made current"); + } + + int result = 0; + if (eglQueryAPI() != EGL_OPENGL_API || + eglGetCurrentDisplay() == EGL_NO_DISPLAY || + eglGetCurrentContext() == EGL_NO_CONTEXT) { + result = fail("Coin did not activate its owned surfaceless context"); + } + + cc_glglue_context_reinstate_previous(coinContext); + if (eglGetCurrentDisplay() != EGL_NO_DISPLAY || + eglGetCurrentContext() != EGL_NO_CONTEXT) { + result = fail("Coin did not restore the empty caller EGL binding"); + } + cc_glglue_context_destruct(coinContext); + eglglue_cleanup(); + return result; +} + +int run_borrowed_display_test(EGLTestState & state) +{ + if (eglBindAPI(EGL_OPENGL_ES_API) == EGL_FALSE) { + return skip("OpenGL ES client API is unavailable"); + } + + EGLint configAttributes[] = { + EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, + EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, + EGL_RED_SIZE, 8, + EGL_GREEN_SIZE, 8, + EGL_BLUE_SIZE, 8, + EGL_ALPHA_SIZE, 8, + EGL_NONE + }; + EGLConfig config = (EGLConfig) 0; + EGLint configCount = 0; + if (eglChooseConfig(state.display, + configAttributes, + &config, + 1, + &configCount) == EGL_FALSE || configCount == 0) { + return skip("no OpenGL ES pbuffer configuration"); + } + + EGLint surfaceAttributes[] = { + EGL_WIDTH, 16, + EGL_HEIGHT, 16, + EGL_NONE + }; + state.drawSurface = + eglCreatePbufferSurface(state.display, config, surfaceAttributes); + state.readSurface = + eglCreatePbufferSurface(state.display, config, surfaceAttributes); + if (state.drawSurface == EGL_NO_SURFACE || + state.readSurface == EGL_NO_SURFACE) { + return skip("OpenGL ES pbuffers could not be created"); + } + + EGLint contextAttributes[] = { + EGL_CONTEXT_CLIENT_VERSION, 2, + EGL_NONE + }; + state.foreignContext = eglCreateContext(state.display, + config, + EGL_NO_CONTEXT, + contextAttributes); + if (state.foreignContext == EGL_NO_CONTEXT || + eglMakeCurrent(state.display, + state.drawSurface, + state.readSurface, + state.foreignContext) == EGL_FALSE) { + return skip("OpenGL ES context could not be made current"); + } + + const EGLenum previousApi = eglQueryAPI(); + const EGLDisplay previousDisplay = eglGetCurrentDisplay(); + const EGLContext previousContext = eglGetCurrentContext(); + const EGLSurface previousDrawSurface = eglGetCurrentSurface(EGL_DRAW); + const EGLSurface previousReadSurface = eglGetCurrentSurface(EGL_READ); + + state.coinContext = cc_glglue_context_create_offscreen(16, 16); + if (!state.coinContext) return fail("Coin offscreen context creation failed"); + if (eglQueryAPI() != previousApi) { + return fail("Coin did not restore the caller API after context creation"); + } + if (!cc_glglue_context_make_current(state.coinContext)) { + return fail("Coin offscreen context could not be made current"); + } + state.coinMadeCurrent = true; + if (eglQueryAPI() != EGL_OPENGL_API) { + return fail("Coin context did not bind the OpenGL client API"); + } + if (eglGetCurrentDisplay() != previousDisplay) { + return fail("Coin context did not use the caller's EGL display"); + } + + cc_glglue_context_reinstate_previous(state.coinContext); + state.coinMadeCurrent = false; + if (eglQueryAPI() != previousApi) { + return fail("Coin did not restore the caller API"); + } + if (eglGetCurrentDisplay() != previousDisplay || + eglGetCurrentContext() != previousContext || + eglGetCurrentSurface(EGL_DRAW) != previousDrawSurface || + eglGetCurrentSurface(EGL_READ) != previousReadSurface) { + return fail("Coin did not restore the complete caller EGL binding"); + } + + cc_glglue_context_destruct(state.coinContext); + state.coinContext = NULL; + eglglue_cleanup(); + if (eglQueryAPI() != previousApi || + eglGetCurrentDisplay() != previousDisplay || + eglGetCurrentContext() != previousContext || + eglGetCurrentSurface(EGL_DRAW) != previousDrawSurface || + eglGetCurrentSurface(EGL_READ) != previousReadSurface) { + return fail("Coin cleanup did not preserve the borrowed EGL binding"); + } + + return 0; +} + +} // namespace + +int main() +{ + setenv("COIN_EGL", "1", 1); + setenv("COIN_EGL_CORE_PROFILE", "1", 1); + + const int ownedResult = run_owned_surfaceless_test(); + if (ownedResult != 0) return ownedResult; + + // Acquire the caller's platform directly. EGL_PLATFORM remains unset so + // Coin must discover and borrow the already-current display. + EGLDisplay display = acquire_surfaceless_display(); + if (display == EGL_NO_DISPLAY) return skip("no EGL display"); + + EGLint major = 0; + EGLint minor = 0; + if (eglInitialize(display, &major, &minor) == EGL_FALSE) { + return skip("EGL display could not be initialized"); + } + + EGLTestState state(display); + const int result = run_borrowed_display_test(state); + state.cleanup(); + return result; +}