SDL: Prefer downscaling in SDL_GetSurfaceImage.

From 3586df3151a805fac8bfe914657e5fd65e44e4c1 Mon Sep 17 00:00:00 2001
From: Kaktus514 <[EMAIL REDACTED]>
Date: Thu, 15 Aug 2024 13:41:20 +0200
Subject: [PATCH] Prefer downscaling in SDL_GetSurfaceImage.

This implements the ideas described in  #10536
---
 include/SDL3/SDL_mouse.h |  5 +++--
 include/SDL3/SDL_video.h |  5 +++--
 src/video/SDL_surface.c  | 28 +++++++++++++++++++++++++---
 3 files changed, 31 insertions(+), 7 deletions(-)

diff --git a/include/SDL3/SDL_mouse.h b/include/SDL3/SDL_mouse.h
index 74954d63d5442..1d4e51fd8b9c8 100644
--- a/include/SDL3/SDL_mouse.h
+++ b/include/SDL3/SDL_mouse.h
@@ -423,8 +423,9 @@ extern SDL_DECLSPEC SDL_Cursor * SDLCALL SDL_CreateCursor(const Uint8 * data,
  * situations. For example, if the original surface is 32x32, then on a 2x
  * macOS display or 200% display scale on Windows, a 64x64 version of the
  * image will be used, if available. If a matching version of the image isn't
- * available, the closest size image will be scaled to the appropriate size
- * and be used instead.
+ * available, the closest larger size image will be downscaled to the 
+ * appropriate size and be used instead, if available. Otherwise, the closest
+ * smaller image will be upscaled and be used instead.
  *
  * \param surface an SDL_Surface structure representing the cursor image.
  * \param hot_x the x position of the cursor hot spot.
diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h
index 358288c1759b9..b75b33860794c 100644
--- a/include/SDL3/SDL_video.h
+++ b/include/SDL3/SDL_video.h
@@ -1341,8 +1341,9 @@ extern SDL_DECLSPEC const char * SDLCALL SDL_GetWindowTitle(SDL_Window *window);
  * situations. For example, if the original surface is 32x32, then on a 2x
  * macOS display or 200% display scale on Windows, a 64x64 version of the
  * image will be used, if available. If a matching version of the image isn't
- * available, the closest size image will be scaled to the appropriate size
- * and be used instead.
+ * available, the closest larger size image will be downscaled to the 
+ * appropriate size and be used instead, if available. Otherwise, the closest
+ * smaller image will be upscaled and be used instead.
  *
  * \param window the window to change.
  * \param icon an SDL_Surface structure containing the icon for the window.
diff --git a/src/video/SDL_surface.c b/src/video/SDL_surface.c
index 457cd55ce61a7..49c0c7b1170ce 100644
--- a/src/video/SDL_surface.c
+++ b/src/video/SDL_surface.c
@@ -514,18 +514,25 @@ SDL_Surface *SDL_GetSurfaceImage(SDL_Surface *surface, float display_scale)
         return surface;
     }
 
+    // Find closest image. Images that are larger than the
+    // desired size are preferred over images that are smaller.
     SDL_Surface *closest = NULL;
     int desired_w = (int)SDL_round(surface->w * display_scale);
     int desired_h = (int)SDL_round(surface->h * display_scale);
+    int desired_size = desired_w * desired_h;
     int closest_distance = -1;
+    int closest_size = -1;
     for (int i = 0; images[i]; ++i) {
         SDL_Surface *candidate = images[i];
+        int size = candidate->w * candidate->h;
         int delta_w = (candidate->w - desired_w);
         int delta_h = (candidate->h - desired_h);
         int distance = (delta_w * delta_w) + (delta_h * delta_h);
-        if (closest_distance < 0 || distance < closest_distance) {
+        if (closest_distance < 0 || distance < closest_distance ||
+            (size > desired_size && closest_size < desired_size)) {
             closest = candidate;
             closest_distance = distance;
+            closest_size = size;
         }
     }
     SDL_free(images);
@@ -536,8 +543,23 @@ SDL_Surface *SDL_GetSurfaceImage(SDL_Surface *surface, float display_scale)
         return closest;
     }
 
-    // We need to scale an image to the correct size
-    return SDL_ScaleSurface(closest, desired_w, desired_h, SDL_SCALEMODE_LINEAR);
+    // We need to scale the image to the correct size. To maintain good image quality, downscaling
+    // is done in steps, never reducing the width and height by more than half each time.
+    SDL_Surface *scaled = closest;
+    do {
+        int next_scaled_w = SDL_max(desired_w, (scaled->w + 1) / 2);
+        int next_scaled_h = SDL_max(desired_h, (scaled->h + 1) / 2);
+        SDL_Surface *next_scaled = SDL_ScaleSurface(scaled, next_scaled_w, next_scaled_h, SDL_SCALEMODE_LINEAR);
+        if (scaled != closest) {
+            SDL_DestroySurface(scaled);
+        }
+        scaled = next_scaled;
+        if (!scaled) {
+            return NULL;
+        }
+    } while (scaled->w != desired_w || scaled->h != desired_h);
+
+    return scaled;
 }
 
 void SDL_RemoveSurfaceAlternateImages(SDL_Surface *surface)