SDL: egl: implement callbacks for defining custom EGL attributes

From 085a276d6c53392dcf1467723e881d78e94e8a5e Mon Sep 17 00:00:00 2001
From: Steven Noonan <[EMAIL REDACTED]>
Date: Thu, 25 Aug 2022 22:41:06 -0700
Subject: [PATCH] egl: implement callbacks for defining custom EGL attributes

Depending on the underlying EGL library, it may be desirable to
conditionally set some specific EGL attributes depending on available
extensions and other application state.

SDL's EGL usage makes this a little bit complicated because:

- there are multiple functions used to set up a working EGL context
- some of these functions take different types of EGL attributes
  (EGLAttrib vs EGLint)
- the EGL extension list before creating an EGLDisplay differs from the
  extension list after (i.e. display vs client extensions)
- all of the above happens in a single SDL_CreateWindow call

This leaves no place for the application to discover what EGL extensions
are available and provide custom attribute lists.

Until now, if a developer wants to add a custom EGL attribute for
eglGetPlatformDisplay, eglCreateWindowSurface or eglCreateContext, they
needed to patch SDL itself. This is very undesirable, since such
developers would have to disable the SDL dynapi in order to maintain
compatibility with their needs.

This patch implements some callbacks which developers can use to
dynamically generate custom EGL attributes for SDL to use during
SDL_CreateWindow.
---
 include/SDL_video.h      | 32 ++++++++++++++++++++
 src/video/SDL_egl.c      | 65 ++++++++++++++++++++++++++++++++++++----
 src/video/SDL_sysvideo.h |  4 +++
 src/video/SDL_video.c    | 16 ++++++++++
 4 files changed, 112 insertions(+), 5 deletions(-)

diff --git a/include/SDL_video.h b/include/SDL_video.h
index cfbed9c89213..ed7bcd36f1a5 100644
--- a/include/SDL_video.h
+++ b/include/SDL_video.h
@@ -223,6 +223,14 @@ typedef void *SDL_GLContext;
 typedef void *SDL_EGLDisplay;
 typedef void *SDL_EGLConfig;
 typedef void *SDL_EGLSurface;
+typedef intptr_t SDL_EGLAttrib;
+typedef int SDL_EGLint;
+
+/**
+ *  \brief EGL attribute initialization callback types.
+ */
+typedef SDL_EGLAttrib *(SDLCALL *SDL_EGLAttribArrayCallback)(void);
+typedef SDL_EGLint *(SDLCALL *SDL_EGLIntArrayCallback)(void);
 
 /**
  *  \brief OpenGL configuration attributes
@@ -2089,6 +2097,30 @@ extern DECLSPEC SDL_EGLConfig SDLCALL SDL_EGL_GetCurrentEGLConfig(void);
  */
 extern DECLSPEC SDL_EGLSurface SDLCALL SDL_EGL_GetWindowEGLSurface(SDL_Window * window);
 
+/**
+ * Sets the callbacks for defining custom EGLAttrib arrays for EGL
+ * initialization.
+ *
+ * Each callback should return a pointer to an EGL attribute array terminated
+ * with EGL_NONE. Callbacks may return NULL pointers to signal an error, which
+ * will cause the SDL_CreateWindow process to fail gracefully.
+ *
+ * The arrays returned by each callback will be appended to the existing
+ * attribute arrays defined by SDL.
+ *
+ * NOTE: These callback pointers will be reset after SDL_GL_ResetAttributes.
+ *
+ * \param platformAttribCallback Callback for attributes to pass to
+ *                               eglGetPlatformDisplay.
+ * \param surfaceAttribCallback Callback for attributes to pass to
+ *                              eglCreateSurface.
+ * \param contextAttribCallback Callback for attributes to pass to
+ *                              eglCreateContext.
+ */
+extern DECLSPEC void SDLCALL SDL_EGL_SetEGLAttributeCallbacks(SDL_EGLAttribArrayCallback platformAttribCallback,
+                                                              SDL_EGLIntArrayCallback surfaceAttribCallback,
+                                                              SDL_EGLIntArrayCallback contextAttribCallback);
+
 /**
  * Get the size of a window's underlying drawable in pixels.
  *
diff --git a/src/video/SDL_egl.c b/src/video/SDL_egl.c
index 072b99ba9b2a..78f64b59f5bd 100644
--- a/src/video/SDL_egl.c
+++ b/src/video/SDL_egl.c
@@ -514,7 +514,16 @@ SDL_EGL_LoadLibrary(_THIS, const char *egl_path, NativeDisplayType native_displa
         }
 
         if (_this->egl_data->eglGetPlatformDisplay) {
-            _this->egl_data->egl_display = _this->egl_data->eglGetPlatformDisplay(platform, (void *)(uintptr_t)native_display, NULL);
+            EGLAttrib *attribs = NULL;
+            if (_this->egl_platformattrib_callback) {
+                attribs = _this->egl_platformattrib_callback();
+                if (!attribs) {
+                    _this->gl_config.driver_loaded = 0;
+                    *_this->gl_config.driver_path = '\0';
+                    return SDL_SetError("EGL platform attribute callback returned NULL pointer");
+                }
+            }
+            _this->egl_data->egl_display = _this->egl_data->eglGetPlatformDisplay(platform, (void *)(uintptr_t)native_display, attribs);
         } else {
             if (SDL_EGL_HasExtension(_this, SDL_EGL_CLIENT_EXTENSION, "EGL_EXT_platform_base")) {
                 _this->egl_data->eglGetPlatformDisplayEXT = SDL_EGL_GetProcAddressInternal(_this, "eglGetPlatformDisplayEXT");
@@ -934,8 +943,8 @@ SDL_EGL_ChooseConfig(_THIS)
 SDL_GLContext
 SDL_EGL_CreateContext(_THIS, EGLSurface egl_surface)
 {
-    /* max 14 values plus terminator. */
-    EGLint attribs[15];
+    /* max 16 key+value pairs plus terminator. */
+    EGLint attribs[33];
     int attr = 0;
 
     EGLContext egl_context, share_context = EGL_NO_CONTEXT;
@@ -1024,6 +1033,29 @@ SDL_EGL_CreateContext(_THIS, EGLSurface egl_surface)
     }
 #endif
 
+    if (_this->egl_contextattrib_callback) {
+        const int maxAttribs = sizeof(attribs) / sizeof(attribs[0]);
+        EGLint *userAttribs, *userAttribP;
+        userAttribs = _this->egl_contextattrib_callback();
+        if (!userAttribs) {
+            _this->gl_config.driver_loaded = 0;
+            *_this->gl_config.driver_path = '\0';
+            SDL_SetError("EGL context attribute callback returned NULL pointer");
+            return NULL;
+        }
+
+        for (userAttribP = userAttribs; *userAttribP != EGL_NONE; ) {
+            if (attr + 3 >= maxAttribs) {
+                _this->gl_config.driver_loaded = 0;
+                *_this->gl_config.driver_path = '\0';
+                SDL_SetError("EGL context attribute callback returned too many attributes");
+                return NULL;
+            }
+            attribs[attr++] = *userAttribP++;
+            attribs[attr++] = *userAttribP++;
+        }
+    }
+
     attribs[attr++] = EGL_NONE;
 
     /* Bind the API */
@@ -1190,8 +1222,8 @@ SDL_EGL_CreateSurface(_THIS, NativeWindowType nw)
     EGLint format_wanted;
     EGLint format_got;
 #endif
-    /* max 2 key+value pairs, plus terminator. */
-    EGLint attribs[5];
+    /* max 16 key+value pairs, plus terminator. */
+    EGLint attribs[33];
     int attr = 0;
 
     EGLSurface * surface;
@@ -1232,6 +1264,29 @@ SDL_EGL_CreateSurface(_THIS, NativeWindowType nw)
     }
 #endif
 
+    if (_this->egl_surfaceattrib_callback) {
+        const int maxAttribs = sizeof(attribs) / sizeof(attribs[0]);
+        EGLint *userAttribs, *userAttribP;
+        userAttribs = _this->egl_surfaceattrib_callback();
+        if (!userAttribs) {
+            _this->gl_config.driver_loaded = 0;
+            *_this->gl_config.driver_path = '\0';
+            SDL_SetError("EGL surface attribute callback returned NULL pointer");
+            return EGL_NO_SURFACE;
+        }
+
+        for (userAttribP = userAttribs; *userAttribP != EGL_NONE; ) {
+            if (attr + 3 >= maxAttribs) {
+                _this->gl_config.driver_loaded = 0;
+                *_this->gl_config.driver_path = '\0';
+                SDL_SetError("EGL surface attribute callback returned too many attributes");
+                return EGL_NO_SURFACE;
+            }
+            attribs[attr++] = *userAttribP++;
+            attribs[attr++] = *userAttribP++;
+        }
+    }
+
     attribs[attr++] = EGL_NONE;
     
     surface = _this->egl_data->eglCreateWindowSurface(
diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h
index 63d3eea84d23..5fd794a554e7 100644
--- a/src/video/SDL_sysvideo.h
+++ b/src/video/SDL_sysvideo.h
@@ -398,6 +398,10 @@ struct SDL_VideoDevice
         void *dll_handle;
     } gl_config;
 
+    SDL_EGLAttribArrayCallback egl_platformattrib_callback;
+    SDL_EGLIntArrayCallback egl_surfaceattrib_callback;
+    SDL_EGLIntArrayCallback egl_contextattrib_callback;
+
     /* * * */
     /* Cache current GL context; don't call the OS when it hasn't changed. */
     /* We have the global pointers here so Cocoa continues to work the way
diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c
index 36276e969829..294f93d544de 100644
--- a/src/video/SDL_video.c
+++ b/src/video/SDL_video.c
@@ -3646,6 +3646,18 @@ SDL_GL_DeduceMaxSupportedESProfile(int* major, int* minor)
 #endif
 }
 
+void SDL_EGL_SetEGLAttributeCallbacks(SDL_EGLAttribArrayCallback platformAttribCallback,
+                                      SDL_EGLIntArrayCallback surfaceAttribCallback,
+                                      SDL_EGLIntArrayCallback contextAttribCallback)
+{
+    if (!_this) {
+        return;
+    }
+    _this->egl_platformattrib_callback = platformAttribCallback;
+    _this->egl_surfaceattrib_callback = surfaceAttribCallback;
+    _this->egl_contextattrib_callback = contextAttribCallback;
+}
+
 void
 SDL_GL_ResetAttributes()
 {
@@ -3653,6 +3665,10 @@ SDL_GL_ResetAttributes()
         return;
     }
 
+    _this->egl_platformattrib_callback = NULL;
+    _this->egl_surfaceattrib_callback = NULL;
+    _this->egl_contextattrib_callback = NULL;
+
     _this->gl_config.red_size = 3;
     _this->gl_config.green_size = 3;
     _this->gl_config.blue_size = 2;