SDL: Added support for other HDR color primaries

From 8fe257b541777f554470c427bf6c297484d6cee9 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Tue, 23 Jan 2024 20:15:10 -0800
Subject: [PATCH] Added support for other HDR color primaries

Specifically, SDL_COLOR_PRIMARIES_XYZ, SDL_COLOR_PRIMARIES_SMPTE431, and SDL_COLOR_PRIMARIES_SMPTE432
---
 include/SDL3/SDL_surface.h |  3 ---
 src/video/SDL_blit.c       | 10 +++++---
 src/video/SDL_blit.h       |  2 ++
 src/video/SDL_blit_slow.c  | 38 +++++++++++++++---------------
 src/video/SDL_pixels.c     | 48 ++++++++++++++++++++++++++++++++++++++
 src/video/SDL_pixels_c.h   |  1 +
 6 files changed, 77 insertions(+), 25 deletions(-)

diff --git a/include/SDL3/SDL_surface.h b/include/SDL3/SDL_surface.h
index e6b59bd9d874..7c636b4b9ca9 100644
--- a/include/SDL3/SDL_surface.h
+++ b/include/SDL3/SDL_surface.h
@@ -262,8 +262,6 @@ extern DECLSPEC void SDLCALL SDL_DestroySurface(SDL_Surface *surface);
  *
  * The following properties are understood by SDL:
  *
- * - `SDL_PROPERTY_SURFACE_HDR_BOOLEAN`: true if this surface has HDR
- *   properties
  * - `SDL_PROPERTY_SURFACE_COLOR_PRIMARIES_NUMBER`: an SDL_ColorPrimaries
  *   value describing the surface colorspace
  * - `SDL_PROPERTY_SURFACE_TRANSFER_CHARACTERISTICS_NUMBER`: an
@@ -291,7 +289,6 @@ extern DECLSPEC void SDLCALL SDL_DestroySurface(SDL_Surface *surface);
  */
 extern DECLSPEC SDL_PropertiesID SDLCALL SDL_GetSurfaceProperties(SDL_Surface *surface);
 
-#define SDL_PROPERTY_SURFACE_HDR_BOOLEAN                        "SDL.surface.HDR"
 #define SDL_PROPERTY_SURFACE_COLOR_PRIMARIES_NUMBER             "SDL.surface.color_primaries"
 #define SDL_PROPERTY_SURFACE_TRANSFER_CHARACTERISTICS_NUMBER    "SDL.surface.transfer_characteristics"
 #define SDL_PROPERTY_SURFACE_MAXCLL_NUMBER                      "SDL.surface.maxCLL"
diff --git a/src/video/SDL_blit.c b/src/video/SDL_blit.c
index 350d95843b38..c0c9bc7791bb 100644
--- a/src/video/SDL_blit.c
+++ b/src/video/SDL_blit.c
@@ -187,7 +187,9 @@ static SDL_bool IsSurfaceHDR(SDL_Surface *surface)
 {
     if (surface->flags & SDL_SURFACE_USES_PROPERTIES) {
         SDL_PropertiesID props = SDL_GetSurfaceProperties(surface);
-        return SDL_GetBooleanProperty(props, SDL_PROPERTY_SURFACE_HDR_BOOLEAN, SDL_FALSE);
+        if (SDL_GetNumberProperty(props, SDL_PROPERTY_SURFACE_TRANSFER_CHARACTERISTICS_NUMBER, SDL_TRANSFER_CHARACTERISTICS_UNKNOWN) == SDL_TRANSFER_CHARACTERISTICS_SMPTE2084) {
+            return SDL_TRUE;
+        }
     }
     return SDL_FALSE;
 }
@@ -215,8 +217,10 @@ int SDL_CalculateBlit(SDL_Surface *surface)
 #endif
 
     map->blit = SDL_SoftBlit;
+    map->info.src_surface = surface;
     map->info.src_fmt = surface->format;
     map->info.src_pitch = surface->pitch;
+    map->info.dst_surface = dst;
     map->info.dst_fmt = dst->format;
     map->info.dst_pitch = dst->pitch;
 
@@ -255,8 +259,8 @@ int SDL_CalculateBlit(SDL_Surface *surface)
         } else {
             /* Tone mapping from an HDR surface to SDR surface */
             SDL_PropertiesID props = SDL_GetSurfaceProperties(surface);
-            if (SDL_GetNumberProperty(props, SDL_PROPERTY_SURFACE_COLOR_PRIMARIES_NUMBER, SDL_COLOR_PRIMARIES_BT2020) == SDL_COLOR_PRIMARIES_BT2020 &&
-                SDL_GetNumberProperty(props, SDL_PROPERTY_SURFACE_TRANSFER_CHARACTERISTICS_NUMBER, SDL_TRANSFER_CHARACTERISTICS_SMPTE2084) == SDL_TRANSFER_CHARACTERISTICS_SMPTE2084) {
+            SDL_ColorPrimaries primaries = (SDL_ColorPrimaries)SDL_GetNumberProperty(props, SDL_PROPERTY_SURFACE_COLOR_PRIMARIES_NUMBER, SDL_COLOR_PRIMARIES_BT2020);
+            if (SDL_GetColorPrimariesConversionMatrix(primaries, SDL_COLOR_PRIMARIES_BT709) != NULL) {
                 if (SDL_ISPIXELFORMAT_10BIT(surface->format->format)) {
                     blit = SDL_Blit_Slow_PQtoSDR;
                 } else {
diff --git a/src/video/SDL_blit.h b/src/video/SDL_blit.h
index 9679a8a86771..b78ecdd8a2de 100644
--- a/src/video/SDL_blit.h
+++ b/src/video/SDL_blit.h
@@ -56,10 +56,12 @@ extern Uint8 *SDL_expand_byte[9];
 
 typedef struct
 {
+    SDL_Surface *src_surface;
     Uint8 *src;
     int src_w, src_h;
     int src_pitch;
     int src_skip;
+    SDL_Surface *dst_surface;
     Uint8 *dst;
     int dst_w, dst_h;
     int dst_pitch;
diff --git a/src/video/SDL_blit_slow.c b/src/video/SDL_blit_slow.c
index 5fdfe6f370bd..016bd936cb14 100644
--- a/src/video/SDL_blit_slow.c
+++ b/src/video/SDL_blit_slow.c
@@ -22,6 +22,7 @@
 
 #include "SDL_blit.h"
 #include "SDL_blit_slow.h"
+#include "SDL_pixels_c.h"
 
 #define FORMAT_ALPHA                0
 #define FORMAT_NO_ALPHA             (-1)
@@ -236,6 +237,18 @@ void SDL_Blit_Slow(SDL_BlitInfo *info)
     }
 }
 
+static void MatrixMultiply(float v[3], const float *matrix)
+{
+    float out[3];
+
+    out[0] = matrix[0 * 3 + 0] * v[0] + matrix[0 * 3 + 1] * v[1] + matrix[0 * 3 + 2] * v[2];
+    out[1] = matrix[1 * 3 + 0] * v[0] + matrix[1 * 3 + 1] * v[1] + matrix[1 * 3 + 2] * v[2];
+    out[2] = matrix[2 * 3 + 0] * v[0] + matrix[2 * 3 + 1] * v[1] + matrix[2 * 3 + 2] * v[2];
+    v[0] = out[0];
+    v[1] = out[1];
+    v[2] = out[2];
+}
+
 static float PQtoNits(float pq)
 {
     const float c1 = 0.8359375f;
@@ -251,25 +264,9 @@ static float PQtoNits(float pq)
     return 10000.0f * SDL_powf(num / den, oo_m1);
 }
 
-static void Convert2020to709(float v[3])
-{
-    static const float matrix[3][3] = {
-        { 1.660496f, -0.587656f, -0.072840f },
-        { -0.124547f, 1.132895f, -0.008348f },
-        { -0.018154f, -0.100597f, 1.118751f }
-    };
-
-    float out[3];
-    out[0] = matrix[0][0] * v[0] + matrix[0][1] * v[1] + matrix[0][2] * v[2];
-    out[1] = matrix[1][0] * v[0] + matrix[1][1] * v[1] + matrix[1][2] * v[2];
-    out[2] = matrix[2][0] * v[0] + matrix[2][1] * v[1] + matrix[2][2] * v[2];
-    v[0] = out[0];
-    v[1] = out[1];
-    v[2] = out[2];
-}
 
 /* This isn't really a tone mapping algorithm but it generally works well for HDR -> SDR display */
-static void PQtoSDR(float floatR, float floatG, float floatB, Uint32 *outR, Uint32 *outG, Uint32 *outB)
+static void PQtoSDR(const float *color_primaries_matrix, float floatR, float floatG, float floatB, Uint32 *outR, Uint32 *outG, Uint32 *outB)
 {
     float v[3];
     int i;
@@ -278,7 +275,7 @@ static void PQtoSDR(float floatR, float floatG, float floatB, Uint32 *outR, Uint
     v[1] = PQtoNits(floatG);
     v[2] = PQtoNits(floatB);
 
-    Convert2020to709(v);
+    MatrixMultiply(v, color_primaries_matrix);
 
     for (i = 0; i < SDL_arraysize(v); ++i) {
         v[i] /= 400.0f;
@@ -313,6 +310,9 @@ void SDL_Blit_Slow_PQtoSDR(SDL_BlitInfo *info)
     int dstfmt_val;
     Uint32 rgbmask = ~src_fmt->Amask;
     Uint32 ckey = info->colorkey & rgbmask;
+    SDL_PropertiesID props = SDL_GetSurfaceProperties(info->src_surface);
+    SDL_ColorPrimaries src_primaries = (SDL_ColorPrimaries)SDL_GetNumberProperty(props, SDL_PROPERTY_SURFACE_COLOR_PRIMARIES_NUMBER, SDL_COLOR_PRIMARIES_BT2020);
+    const float *color_primaries_matrix = SDL_GetColorPrimariesConversionMatrix(src_primaries, SDL_COLOR_PRIMARIES_BT709);
 
     dstfmt_val = detect_format(dst_fmt);
 
@@ -353,7 +353,7 @@ void SDL_Blit_Slow_PQtoSDR(SDL_BlitInfo *info)
                 break;
             }
 
-            PQtoSDR(srcFloatR, srcFloatG, srcFloatB, &srcR, &srcG, &srcB);
+            PQtoSDR(color_primaries_matrix, srcFloatR, srcFloatG, srcFloatB, &srcR, &srcG, &srcB);
             srcA = (Uint32)(srcFloatA * 255);
 
             if (flags & SDL_COPY_COLORKEY) {
diff --git a/src/video/SDL_pixels.c b/src/video/SDL_pixels.c
index 5779212e7273..ef38104f6419 100644
--- a/src/video/SDL_pixels.c
+++ b/src/video/SDL_pixels.c
@@ -1173,3 +1173,51 @@ void SDL_FreeBlitMap(SDL_BlitMap *map)
         SDL_free(map);
     }
 }
+
+const float *SDL_GetColorPrimariesConversionMatrix(SDL_ColorPrimaries src, SDL_ColorPrimaries dst)
+{
+    /* Conversion matrices generated using gamescope color helpers and the primaries definitions at:
+     * https://www.itu.int/rec/T-REC-H.273-201612-S/en
+     */
+    static const float mat2020to709[] = {
+        1.660496f, -0.587656f, -0.072840f,
+        -0.124547f, 1.132895f, -0.008348f,
+        -0.018154f, -0.100597f, 1.118751f
+    };
+    static const float matXYZto709[] = {
+        3.240969f, -1.537383f, -0.498611f,
+        -0.969243f, 1.875967f, 0.041555f,
+        0.055630f, -0.203977f, 1.056971f,
+    };
+    static const float matSMPTE431to709[] = {
+        1.120713f, -0.234649f, 0.000000f,
+        -0.038478f, 1.087034f, 0.000000f,
+        -0.017967f, -0.082030f, 0.954576f,
+    };
+    static const float matSMPTE432to709[] = {
+        1.224940f, -0.224940f, -0.000000f,
+        -0.042057f, 1.042057f, 0.000000f,
+        -0.019638f, -0.078636f, 1.098273f,
+    };
+
+    switch (dst) {
+    case SDL_COLOR_PRIMARIES_BT709:
+        switch (src) {
+        case SDL_COLOR_PRIMARIES_BT2020:
+            return mat2020to709;
+        case SDL_COLOR_PRIMARIES_XYZ:
+            return matXYZto709;
+        case SDL_COLOR_PRIMARIES_SMPTE431:
+            return matSMPTE431to709;
+        case SDL_COLOR_PRIMARIES_SMPTE432:
+            return matSMPTE432to709;
+        default:
+            break;
+        }
+        break;
+    default:
+        break;
+    }
+    return NULL;
+}
+
diff --git a/src/video/SDL_pixels_c.h b/src/video/SDL_pixels_c.h
index e0def8aac606..37a73569681a 100644
--- a/src/video/SDL_pixels_c.h
+++ b/src/video/SDL_pixels_c.h
@@ -44,5 +44,6 @@ extern void SDL_InvalidateAllBlitMap(SDL_Surface *surface);
 extern void SDL_DitherColors(SDL_Color *colors, int bpp);
 extern Uint8 SDL_FindColor(SDL_Palette *pal, Uint8 r, Uint8 g, Uint8 b, Uint8 a);
 extern void SDL_DetectPalette(SDL_Palette *pal, SDL_bool *is_opaque, SDL_bool *has_alpha_channel);
+extern const float *SDL_GetColorPrimariesConversionMatrix(SDL_ColorPrimaries src, SDL_ColorPrimaries dst);
 
 #endif /* SDL_pixels_c_h_ */