SDL: x11: Deal with difference in GLX_EXT_swap_control_tear behavior.

From 74a25425646d64edeff508ec8e99622a41576905 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Thu, 23 Nov 2023 13:47:13 -0500
Subject: [PATCH] x11: Deal with difference in GLX_EXT_swap_control_tear
 behavior.

Mesa and Nvidia handle it differently, and one or the other may fix their
implementation in the future, so test which way it works at runtime.

Reference Issue #8004.
---
 src/video/x11/SDL_x11opengl.c | 68 ++++++++++++++++++++++++++++++++---
 src/video/x11/SDL_x11opengl.h | 10 ++++++
 test/testgl.c                 | 21 ++++++-----
 3 files changed, 86 insertions(+), 13 deletions(-)

diff --git a/src/video/x11/SDL_x11opengl.c b/src/video/x11/SDL_x11opengl.c
index 1a24358b24f7..aad96462d56c 100644
--- a/src/video/x11/SDL_x11opengl.c
+++ b/src/video/x11/SDL_x11opengl.c
@@ -237,6 +237,8 @@ int X11_GL_LoadLibrary(SDL_VideoDevice *_this, const char *path)
         return SDL_SetError("GLX is not supported");
     }
 
+    _this->gl_data->swap_interval_tear_behavior = SDL_SWAPINTERVALTEAR_UNTESTED;
+
     /* Initialize extensions */
     /* See lengthy comment about the inc/dec in
        ../windows/SDL_windowsopengl.c. */
@@ -902,7 +904,6 @@ int X11_GL_SetSwapInterval(SDL_VideoDevice *_this, int interval)
         X11_GL_GetSwapInterval(_this, &currentInterval);
         _this->gl_data->glXSwapIntervalEXT(display, drawable, currentInterval);
         _this->gl_data->glXSwapIntervalEXT(display, drawable, interval);
-
         status = 0;
         swapinterval = interval;
     } else if (_this->gl_data->glXSwapIntervalMESA) {
@@ -925,6 +926,53 @@ int X11_GL_SetSwapInterval(SDL_VideoDevice *_this, int interval)
     return status;
 }
 
+static SDL_GLSwapIntervalTearBehavior CheckSwapIntervalTearBehavior(SDL_VideoDevice *_this, Window drawable, unsigned int current_val, unsigned int current_allow_late)
+{
+    /* Mesa and Nvidia interpret GLX_EXT_swap_control_tear differently, as of this writing, so
+        figure out which behavior we have.
+       Technical details: https://github.com/libsdl-org/SDL/issues/8004#issuecomment-1819603282 */
+    if (_this->gl_data->swap_interval_tear_behavior == SDL_SWAPINTERVALTEAR_UNTESTED) {
+        if (!_this->gl_data->HAS_GLX_EXT_swap_control_tear) {
+            _this->gl_data->swap_interval_tear_behavior = SDL_SWAPINTERVALTEAR_UNKNOWN;
+        } else {
+            Display *display = _this->driverdata->display;
+            unsigned int allow_late_swap_tearing = 22;
+            int original_val = (int) current_val;
+
+            /*
+             * This is a workaround for a bug in NVIDIA drivers. Bug has been reported
+             * and will be fixed in a future release (probably 319.xx).
+             *
+             * There's a bug where glXSetSwapIntervalEXT ignores updates because
+             * it has the wrong value cached. To work around it, we just run a no-op
+             * update to the current value.
+             */
+            _this->gl_data->glXSwapIntervalEXT(display, drawable, current_val);
+
+            /* set it to no swap interval and see how it affects GLX_LATE_SWAPS_TEAR_EXT... */
+            _this->gl_data->glXSwapIntervalEXT(display, drawable, 0);
+            _this->gl_data->glXQueryDrawable(display, drawable, GLX_LATE_SWAPS_TEAR_EXT, &allow_late_swap_tearing);
+
+            if (allow_late_swap_tearing == 0) { /* GLX_LATE_SWAPS_TEAR_EXT says whether late swapping is currently in use */
+                _this->gl_data->swap_interval_tear_behavior = SDL_SWAPINTERVALTEAR_NVIDIA;
+                if (current_allow_late) {
+                    original_val = -original_val;
+                }
+            } else if (allow_late_swap_tearing == 1) {  /* GLX_LATE_SWAPS_TEAR_EXT says whether the Drawable can use late swapping at all */
+                _this->gl_data->swap_interval_tear_behavior = SDL_SWAPINTERVALTEAR_MESA;
+            } else {  /* unexpected outcome! */
+                _this->gl_data->swap_interval_tear_behavior = SDL_SWAPINTERVALTEAR_UNKNOWN;
+            }
+
+            /* set us back to what it was originally... */
+            _this->gl_data->glXSwapIntervalEXT(display, drawable, original_val);
+        }
+    }
+
+    return _this->gl_data->swap_interval_tear_behavior;
+}
+
+
 int X11_GL_GetSwapInterval(SDL_VideoDevice *_this, int *interval)
 {
     if (_this->gl_data->glXSwapIntervalEXT) {
@@ -935,6 +983,7 @@ int X11_GL_GetSwapInterval(SDL_VideoDevice *_this, int *interval)
         unsigned int val = 0;
 
         if (_this->gl_data->HAS_GLX_EXT_swap_control_tear) {
+            allow_late_swap_tearing = 22;  /* set this to nonsense. */
             _this->gl_data->glXQueryDrawable(display, drawable,
                                              GLX_LATE_SWAPS_TEAR_EXT,
                                              &allow_late_swap_tearing);
@@ -943,12 +992,21 @@ int X11_GL_GetSwapInterval(SDL_VideoDevice *_this, int *interval)
         _this->gl_data->glXQueryDrawable(display, drawable,
                                          GLX_SWAP_INTERVAL_EXT, &val);
 
-        if ((allow_late_swap_tearing) && (val > 0)) {
-            *interval = -((int)val);
-            return 0;
+        *interval = (int)val;
+
+        switch (CheckSwapIntervalTearBehavior(_this, drawable, val, allow_late_swap_tearing)) {
+            case SDL_SWAPINTERVALTEAR_MESA:
+                *interval = (int)val;  /* unsigned int cast to signed that generates negative value if necessary. */
+                break;
+
+            case SDL_SWAPINTERVALTEAR_NVIDIA:
+            default:
+                if ((allow_late_swap_tearing) && (val > 0)) {
+                    *interval = -((int)val);
+                }
+                break;
         }
 
-        *interval = (int)val;
         return 0;
     } else if (_this->gl_data->glXGetSwapIntervalMESA) {
         int val = _this->gl_data->glXGetSwapIntervalMESA();
diff --git a/src/video/x11/SDL_x11opengl.h b/src/video/x11/SDL_x11opengl.h
index f49f556683a8..70840231c7a7 100644
--- a/src/video/x11/SDL_x11opengl.h
+++ b/src/video/x11/SDL_x11opengl.h
@@ -29,6 +29,14 @@
 
 typedef void (*__GLXextFuncPtr)(void);
 
+typedef enum SDL_GLSwapIntervalTearBehavior
+{
+    SDL_SWAPINTERVALTEAR_UNTESTED,
+    SDL_SWAPINTERVALTEAR_UNKNOWN,
+    SDL_SWAPINTERVALTEAR_MESA,
+    SDL_SWAPINTERVALTEAR_NVIDIA
+} SDL_GLSwapIntervalTearBehavior;
+
 struct SDL_GLDriverData
 {
     int errorBase, eventBase;
@@ -50,6 +58,8 @@ struct SDL_GLDriverData
         int minor;
     } es_profile_max_supported_version;
 
+    SDL_GLSwapIntervalTearBehavior swap_interval_tear_behavior;
+
     Bool (*glXQueryExtension)(Display *, int *, int *);
     __GLXextFuncPtr (*glXGetProcAddress)(const GLubyte *);
     XVisualInfo *(*glXChooseVisual)(Display *, int, int *);
diff --git a/test/testgl.c b/test/testgl.c
index 635782e9bc54..0df67e73f9c1 100644
--- a/test/testgl.c
+++ b/test/testgl.c
@@ -199,6 +199,17 @@ static void Render(void)
     ctx.glRotatef(5.0, 1.0, 1.0, 1.0);
 }
 
+static void LogSwapInterval(void)
+{
+    int interval = 0;
+    const int ret_interval = SDL_GL_GetSwapInterval(&interval);
+    if (ret_interval < 0) {
+       SDL_Log("Swap Interval : %d error: %s\n", interval, SDL_GetError());
+    } else {
+       SDL_Log("Swap Interval : %d\n", interval);
+    }
+}
+
 int main(int argc, char *argv[])
 {
     int fsaa, accel;
@@ -211,8 +222,6 @@ int main(int argc, char *argv[])
     int status;
     int dw, dh;
     int swap_interval = 0;
-    int interval = 0;
-    int ret_interval = 0;
 
     /* Initialize parameters */
     fsaa = 0;
@@ -304,12 +313,7 @@ int main(int argc, char *argv[])
         SDL_Log("Screen BPP    : %" SDL_PRIu32 "\n", SDL_BITSPERPIXEL(mode->format));
     }
 
-    ret_interval = SDL_GL_GetSwapInterval(&interval);
-    if (ret_interval < 0) {
-       SDL_Log("Swap Interval : %d error: %s\n", interval, SDL_GetError());
-    } else {
-       SDL_Log("Swap Interval : %d\n", interval);
-    }
+    LogSwapInterval();
 
     SDL_GetWindowSize(state->windows[0], &dw, &dh);
     SDL_Log("Window Size   : %d,%d\n", dw, dh);
@@ -421,6 +425,7 @@ int main(int argc, char *argv[])
             SDL_GL_MakeCurrent(state->windows[i], context);
             if (update_swap_interval) {
                 SDL_GL_SetSwapInterval(swap_interval);
+                LogSwapInterval();
             }
             SDL_GetWindowSizeInPixels(state->windows[i], &w, &h);
             ctx.glViewport(0, 0, w, h);