SDL: Added support for full range BT.709 YUV conversion

From bb764e3106a163d9b49cd5119761dc3ac09466b7 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 14 Oct 2024 15:19:59 -0700
Subject: [PATCH] Added support for full range BT.709 YUV conversion

Also added simple colorspace testing to testyuv.
---
 src/video/SDL_yuv.c                  |  31 ++++--
 src/video/yuv2rgb/yuv_rgb_common.h   |   1 +
 src/video/yuv2rgb/yuv_rgb_internal.h |   8 +-
 test/testyuv.c                       | 144 ++++++++++++++++++++++++++-
 4 files changed, 166 insertions(+), 18 deletions(-)

diff --git a/src/video/SDL_yuv.c b/src/video/SDL_yuv.c
index 81ace699a1fbb..6c7ab1cd7e014 100644
--- a/src/video/SDL_yuv.c
+++ b/src/video/SDL_yuv.c
@@ -171,8 +171,10 @@ static bool GetYUVConversionType(SDL_Colorspace colorspace, YCbCrType *yuv_type)
     if (SDL_ISCOLORSPACE_MATRIX_BT709(colorspace)) {
         if (SDL_ISCOLORSPACE_LIMITED_RANGE(colorspace)) {
             *yuv_type = YCBCR_709_LIMITED;
-            return true;
+        } else {
+            *yuv_type = YCBCR_709_FULL;
         }
+        return true;
     }
 
     if (SDL_ISCOLORSPACE_MATRIX_BT2020_NCL(colorspace)) {
@@ -691,6 +693,13 @@ static struct RGB2YUVFactors RGB2YUVFactorTables[] = {
         { -0.1482f, -0.2910f, 0.4392f },
         { 0.4392f, -0.3678f, -0.0714f },
     },
+    // ITU-R BT.709-6 full range
+    {
+        0,
+        { 0.2126f, 0.7152f, 0.0722f },
+        { -0.1141f, -0.3839f, 0.498f },
+        { 0.498f, -0.4524f, -0.0457f },
+    },
     // ITU-R BT.709-6
     {
         16,
@@ -707,7 +716,7 @@ static struct RGB2YUVFactors RGB2YUVFactorTables[] = {
     },
 };
 
-static bool SDL_ConvertPixels_ARGB8888_to_YUV(int width, int height, const void *src, int src_pitch, SDL_PixelFormat dst_format, void *dst, int dst_pitch, YCbCrType yuv_type)
+static bool SDL_ConvertPixels_XRGB8888_to_YUV(int width, int height, const void *src, int src_pitch, SDL_PixelFormat dst_format, void *dst, int dst_pitch, YCbCrType yuv_type)
 {
     const int src_pitch_x_2 = src_pitch * 2;
     const int height_half = height / 2;
@@ -718,9 +727,9 @@ static bool SDL_ConvertPixels_ARGB8888_to_YUV(int width, int height, const void
 
     const struct RGB2YUVFactors *cvt = &RGB2YUVFactorTables[yuv_type];
 
-#define MAKE_Y(r, g, b) (Uint8)((int)(cvt->y[0] * (r) + cvt->y[1] * (g) + cvt->y[2] * (b) + 0.5f) + cvt->y_offset)
-#define MAKE_U(r, g, b) (Uint8)((int)(cvt->u[0] * (r) + cvt->u[1] * (g) + cvt->u[2] * (b) + 0.5f) + 128)
-#define MAKE_V(r, g, b) (Uint8)((int)(cvt->v[0] * (r) + cvt->v[1] * (g) + cvt->v[2] * (b) + 0.5f) + 128)
+#define MAKE_Y(r, g, b) (Uint8)SDL_clamp(((int)(cvt->y[0] * (r) + cvt->y[1] * (g) + cvt->y[2] * (b) + 0.5f) + cvt->y_offset), 0, 255)
+#define MAKE_U(r, g, b) (Uint8)SDL_clamp(((int)(cvt->u[0] * (r) + cvt->u[1] * (g) + cvt->u[2] * (b) + 0.5f) + 128), 0, 255)
+#define MAKE_V(r, g, b) (Uint8)SDL_clamp(((int)(cvt->v[0] * (r) + cvt->v[1] * (g) + cvt->v[2] * (b) + 0.5f) + 128), 0, 255)
 
 #define READ_2x2_PIXELS                                                                                     \
     const Uint32 p1 = ((const Uint32 *)curr_row)[2 * i];                                                    \
@@ -1149,9 +1158,9 @@ bool SDL_ConvertPixels_RGB_to_YUV(int width, int height,
 #endif
 
     // ARGB8888 to FOURCC
-    if (src_format == SDL_PIXELFORMAT_ARGB8888 &&
+    if ((src_format == SDL_PIXELFORMAT_ARGB8888 || src_format == SDL_PIXELFORMAT_XRGB8888) &&
         SDL_COLORSPACEPRIMARIES(src_colorspace) == SDL_COLORSPACEPRIMARIES(dst_colorspace)) {
-        return SDL_ConvertPixels_ARGB8888_to_YUV(width, height, src, src_pitch, dst_format, dst, dst_pitch, yuv_type);
+        return SDL_ConvertPixels_XRGB8888_to_YUV(width, height, src, src_pitch, dst_format, dst, dst_pitch, yuv_type);
     }
 
     if (dst_format == SDL_PIXELFORMAT_P010) {
@@ -1194,15 +1203,15 @@ bool SDL_ConvertPixels_RGB_to_YUV(int width, int height,
             return false;
         }
 
-        // convert src/src_format to tmp/ARGB8888
-        result = SDL_ConvertPixelsAndColorspace(width, height, src_format, src_colorspace, src_properties, src, src_pitch, SDL_PIXELFORMAT_ARGB8888, dst_colorspace, dst_properties, tmp, tmp_pitch);
+        // convert src/src_format to tmp/XRGB8888
+        result = SDL_ConvertPixelsAndColorspace(width, height, src_format, src_colorspace, src_properties, src, src_pitch, SDL_PIXELFORMAT_XRGB8888, SDL_COLORSPACE_SRGB, 0, tmp, tmp_pitch);
         if (!result) {
             SDL_free(tmp);
             return false;
         }
 
-        // convert tmp/ARGB8888 to dst/FOURCC
-        result = SDL_ConvertPixels_ARGB8888_to_YUV(width, height, tmp, tmp_pitch, dst_format, dst, dst_pitch, yuv_type);
+        // convert tmp/XRGB8888 to dst/FOURCC
+        result = SDL_ConvertPixels_XRGB8888_to_YUV(width, height, tmp, tmp_pitch, dst_format, dst, dst_pitch, yuv_type);
         SDL_free(tmp);
         return result;
     }
diff --git a/src/video/yuv2rgb/yuv_rgb_common.h b/src/video/yuv2rgb/yuv_rgb_common.h
index 774292b0cd0fd..a4ef8eaeb7bb4 100644
--- a/src/video/yuv2rgb/yuv_rgb_common.h
+++ b/src/video/yuv2rgb/yuv_rgb_common.h
@@ -7,6 +7,7 @@ typedef enum
 {
     YCBCR_601_FULL,
     YCBCR_601_LIMITED,
+    YCBCR_709_FULL,
     YCBCR_709_LIMITED,
     YCBCR_2020_NCL_FULL,
 } YCbCrType;
diff --git a/src/video/yuv2rgb/yuv_rgb_internal.h b/src/video/yuv2rgb/yuv_rgb_internal.h
index 23ae705669bad..d5939ed651cba 100644
--- a/src/video/yuv2rgb/yuv_rgb_internal.h
+++ b/src/video/yuv2rgb/yuv_rgb_internal.h
@@ -40,22 +40,26 @@ typedef struct
 // for ITU-R BT.2020 values are assuming RGB is encoded using full 10-bit range ([0-1]<->[0-1023])
 // all values are rounded to the fourth decimal
 
-static const YUV2RGBParam YUV2RGB[4] = {
+static const YUV2RGBParam YUV2RGB[] = {
 	// ITU-T T.871 (JPEG)
 	{/*.y_shift=*/ 0, /*.y_factor=*/ V(1.0), /*.v_r_factor=*/ V(1.402), /*.u_g_factor=*/ -V(0.3441), /*.v_g_factor=*/ -V(0.7141), /*.u_b_factor=*/ V(1.772)},
 	// ITU-R BT.601-7
 	{/*.y_shift=*/ 16, /*.y_factor=*/ V(1.1644), /*.v_r_factor=*/ V(1.596), /*.u_g_factor=*/ -V(0.3918), /*.v_g_factor=*/ -V(0.813), /*.u_b_factor=*/ V(2.0172)},
+	// ITU-R BT.709-6 full range
+	{/*.y_shift=*/ 0, /*.y_factor=*/ V(1.0), /*.v_r_factor=*/ V(1.581), /*.u_g_factor=*/ -V(0.1881), /*.v_g_factor=*/ -V(0.47), /*.u_b_factor=*/ V(1.8629)},
 	// ITU-R BT.709-6
 	{/*.y_shift=*/ 16, /*.y_factor=*/ V(1.1644), /*.v_r_factor=*/ V(1.7927), /*.u_g_factor=*/ -V(0.2132), /*.v_g_factor=*/ -V(0.5329), /*.u_b_factor=*/ V(2.1124)},
 	// ITU-R BT.2020 10-bit full range
 	{/*.y_shift=*/ 0, /*.y_factor=*/ V(1.0), /*.v_r_factor=*/ V(1.4760), /*.u_g_factor=*/ -V(0.1647), /*.v_g_factor=*/ -V(0.5719), /*.u_b_factor=*/ V(1.8832) }
 };
 
-static const RGB2YUVParam RGB2YUV[4] = {
+static const RGB2YUVParam RGB2YUV[] = {
 	// ITU-T T.871 (JPEG)
 	{/*.y_shift=*/ 0, /*.matrix=*/ {{V(0.299), V(0.587), V(0.114)}, {-V(0.1687), -V(0.3313), V(0.5)}, {V(0.5), -V(0.4187), -V(0.0813)}}},
 	// ITU-R BT.601-7
 	{/*.y_shift=*/ 16, /*.matrix=*/ {{V(0.2568), V(0.5041), V(0.0979)}, {-V(0.1482), -V(0.291), V(0.4392)}, {V(0.4392), -V(0.3678), -V(0.0714)}}},
+	// ITU-R BT.709-6 full range
+	{/*.y_shift=*/ 0, /*.matrix=*/ {{V(0.2126), V(0.7152), V(0.0722)}, {-V(0.1141), -V(0.3839), V(0.498)}, {V(0.498), -V(0.4524), -V(0.0457)}}},
 	// ITU-R BT.709-6
 	{/*.y_shift=*/ 16, /*.matrix=*/ {{V(0.1826), V(0.6142), V(0.062)}, {-V(0.1006), -V(0.3386), V(0.4392)}, {V(0.4392), -V(0.3989), -V(0.0403)}}},
 	// ITU-R BT.2020 10-bit full range
diff --git a/test/testyuv.c b/test/testyuv.c
index 2406ce784445c..8048742477745 100644
--- a/test/testyuv.c
+++ b/test/testyuv.c
@@ -104,7 +104,7 @@ static bool verify_yuv_data(Uint32 format, SDL_Colorspace colorspace, const Uint
     return result;
 }
 
-static int run_automated_tests(int pattern_size, int extra_pitch)
+static bool run_automated_tests(int pattern_size, int extra_pitch)
 {
     const Uint32 formats[] = {
         SDL_PIXELFORMAT_YV12,
@@ -125,7 +125,7 @@ static int run_automated_tests(int pattern_size, int extra_pitch)
     SDL_Colorspace colorspace;
     const int tight_tolerance = 20;
     const int loose_tolerance = 333;
-    int result = -1;
+    bool result = false;
 
     if (!pattern || !yuv1 || !yuv2) {
         SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't allocate test surfaces");
@@ -230,7 +230,7 @@ static int run_automated_tests(int pattern_size, int extra_pitch)
         goto done;
     }
 
-    result = 0;
+    result = true;
 
 done:
     SDL_free(yuv1);
@@ -239,6 +239,129 @@ static int run_automated_tests(int pattern_size, int extra_pitch)
     return result;
 }
 
+static bool run_colorspace_test()
+{
+    bool result = false;
+    SDL_Window *window;
+    SDL_Renderer *renderer;
+    struct {
+        const char *name;
+        SDL_Colorspace colorspace;
+    } colorspaces[] = {
+#define COLORSPACE(X) { #X, X }
+        COLORSPACE(SDL_COLORSPACE_JPEG),
+#if 0 /* We don't support converting color primaries here */
+        COLORSPACE(SDL_COLORSPACE_BT601_LIMITED),
+        COLORSPACE(SDL_COLORSPACE_BT601_FULL),
+#endif
+        COLORSPACE(SDL_COLORSPACE_BT709_LIMITED),
+        COLORSPACE(SDL_COLORSPACE_BT709_FULL)
+#undef COLORSPACE
+    };
+    SDL_Surface *rgb = NULL;
+    SDL_Surface *yuv = NULL;
+    SDL_Texture *texture = NULL;
+    int allowed_error = 2;
+    int i;
+
+    if (!SDL_CreateWindowAndRenderer("testyuv", 320, 240, 0, &window, &renderer)) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create window and renderer: %s\n", SDL_GetError());
+        goto done;
+    }
+
+    rgb = SDL_CreateSurface(32, 32, SDL_PIXELFORMAT_XRGB8888);
+    if (!rgb) {
+        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create RGB surface: %s\n", SDL_GetError());
+        goto done;
+    }
+    SDL_FillSurfaceRect(rgb, NULL, SDL_MapSurfaceRGB(rgb, 255, 0, 0));
+
+    for (i = 0; i < SDL_arraysize(colorspaces); ++i) {
+        bool next = false;
+        Uint8 r, g, b, a;
+
+        SDL_Log("Checking colorspace %s\n", colorspaces[i].name);
+
+        yuv = SDL_ConvertSurfaceAndColorspace(rgb, SDL_PIXELFORMAT_NV12, NULL, colorspaces[i].colorspace, 0);
+        if (!yuv) {
+            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create YUV surface: %s\n", SDL_GetError());
+            goto done;
+        }
+
+        if (!SDL_ReadSurfacePixel(yuv, 0, 0, &r, &g, &b, &a)) {
+            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't read YUV surface: %s\n", SDL_GetError());
+            goto done;
+        }
+
+        if (SDL_abs((int)r - 255) > allowed_error ||
+            SDL_abs((int)g - 0) > allowed_error ||
+            SDL_abs((int)b - 0) > allowed_error) {
+            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed color conversion, expected 255,0,0, got %d,%d,%d\n", r, g, b);
+        }
+
+        texture = SDL_CreateTextureFromSurface(renderer, yuv);
+        if (!texture) {
+            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't create YUV texture: %s\n", SDL_GetError());
+            goto done;
+        }
+
+        SDL_DestroySurface(yuv);
+        yuv = NULL;
+
+        SDL_RenderTexture(renderer, texture, NULL, NULL);
+        yuv = SDL_RenderReadPixels(renderer, NULL);
+
+        if (!SDL_ReadSurfacePixel(yuv, 0, 0, &r, &g, &b, &a)) {
+            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Couldn't read YUV surface: %s\n", SDL_GetError());
+            goto done;
+        }
+
+        if (SDL_abs((int)r - 255) > allowed_error ||
+            SDL_abs((int)g - 0) > allowed_error ||
+            SDL_abs((int)b - 0) > allowed_error) {
+            SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Failed renderer color conversion, expected 255,0,0, got %d,%d,%d\n", r, g, b);
+        }
+
+        while (!next) {
+            SDL_Event event;
+            while (SDL_PollEvent(&event)) {
+                switch (event.type) {
+                case SDL_EVENT_KEY_DOWN:
+                    if (event.key.key == SDLK_ESCAPE) {
+                        result = true;
+                        goto done;
+                    }
+                    if (event.key.key == SDLK_SPACE) {
+                        next = true;
+                    }
+                    break;
+                case SDL_EVENT_QUIT:
+                    result = true;
+                    goto done;
+                }
+            }
+
+            SDL_RenderTexture(renderer, texture, NULL, NULL);
+            SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
+            SDL_RenderDebugText(renderer, 4, 4, colorspaces[i].name);
+            SDL_RenderPresent(renderer);
+            SDL_Delay(10);
+        }
+
+        SDL_DestroyTexture(texture);
+        texture = NULL;
+    }
+
+    result = true;
+
+done:
+    SDL_DestroySurface(rgb);
+    SDL_DestroySurface(yuv);
+    SDL_DestroyTexture(texture);
+    SDL_Quit();
+    return result;
+}
+
 int main(int argc, char **argv)
 {
     struct
@@ -297,6 +420,7 @@ int main(int argc, char **argv)
     Uint64 then, now;
     int i, iterations = 100;
     bool should_run_automated_tests = false;
+    bool should_run_colorspace_test = false;
     SDLTest_CommonState *state;
 
     /* Initialize test framework */
@@ -377,6 +501,9 @@ int main(int argc, char **argv)
             } else if (SDL_strcmp(argv[i], "--automated") == 0) {
                 should_run_automated_tests = true;
                 consumed = 1;
+            } else if (SDL_strcmp(argv[i], "--colorspace-test") == 0) {
+                should_run_colorspace_test = true;
+                consumed = 1;
             } else if (!filename) {
                 filename = argv[i];
                 consumed = 1;
@@ -388,7 +515,7 @@ int main(int argc, char **argv)
                 "[--yv12|--iyuv|--yuy2|--uyvy|--yvyu|--nv12|--nv21]",
                 "[--rgb555|--rgb565|--rgb24|--argb|--abgr|--rgba|--bgra]",
                 "[--monochrome] [--luminance N%]",
-                "[--automated]",
+                "[--automated] [--colorspace-test]",
                 "[sample.bmp]",
                 NULL,
             };
@@ -407,13 +534,20 @@ int main(int argc, char **argv)
                         automated_test_params[i].pattern_size,
                         automated_test_params[i].extra_pitch,
                         automated_test_params[i].enable_intrinsics ? "enabled" : "disabled");
-            if (run_automated_tests(automated_test_params[i].pattern_size, automated_test_params[i].extra_pitch) < 0) {
+            if (!run_automated_tests(automated_test_params[i].pattern_size, automated_test_params[i].extra_pitch)) {
                 return 2;
             }
         }
         return 0;
     }
 
+    if (should_run_colorspace_test) {
+        if (!run_colorspace_test()) {
+            return 2;
+        }
+        return 0;
+    }
+
     filename = GetResourceFilename(filename, "testyuv.bmp");
     bmp = SDL_LoadBMP(filename);
     original = SDL_ConvertSurface(bmp, SDL_PIXELFORMAT_RGB24);