SDL: Added the concept of colorspace to the SDL renderer

From f257eb44816d092f6ba797e8c8532a717a4a44ba Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Mon, 29 Jan 2024 18:32:27 -0800
Subject: [PATCH] Added the concept of colorspace to the SDL renderer

This allows color operations to happen in linear space between sRGB input and sRGB output. This is currently supported on the direct3d11, direct3d12 and opengl renderers.

This is a good resource on blending in linear space vs sRGB space:
https://blog.johnnovak.net/2016/09/21/what-every-coder-should-know-about-gamma/

Also added testcolorspace to verify colorspace changes
---
 include/SDL3/SDL_pixels.h                |  10 +-
 include/SDL3/SDL_render.h                |  21 +-
 src/render/SDL_render.c                  | 124 ++++++-
 src/render/SDL_sysrender.h               |  12 +
 src/render/direct3d11/SDL_render_d3d11.c |  38 +-
 src/render/direct3d12/SDL_render_d3d12.c |  34 +-
 src/render/opengl/SDL_render_gl.c        |  28 +-
 test/CMakeLists.txt                      |   1 +
 test/testcolorspace.c                    | 420 +++++++++++++++++++++++
 9 files changed, 648 insertions(+), 40 deletions(-)
 create mode 100644 test/testcolorspace.c

diff --git a/include/SDL3/SDL_pixels.h b/include/SDL3/SDL_pixels.h
index 57e640fff2c3..b92e42992888 100644
--- a/include/SDL3/SDL_pixels.h
+++ b/include/SDL3/SDL_pixels.h
@@ -196,8 +196,14 @@ typedef enum
       (SDL_PIXELORDER(format) == SDL_PACKEDORDER_BGRA))))
 
 #define SDL_ISPIXELFORMAT_10BIT(format)    \
-      ((SDL_PIXELTYPE(format) == SDL_PIXELTYPE_PACKED32) && \
-       (SDL_PIXELLAYOUT(format) == SDL_PACKEDLAYOUT_2101010))
+      (!SDL_ISPIXELFORMAT_FOURCC(format) && \
+       ((SDL_PIXELTYPE(format) == SDL_PIXELTYPE_PACKED32) && \
+        (SDL_PIXELLAYOUT(format) == SDL_PACKEDLAYOUT_2101010)))
+
+#define SDL_ISPIXELFORMAT_FLOAT(format)    \
+      (!SDL_ISPIXELFORMAT_FOURCC(format) && \
+       ((SDL_PIXELTYPE(format) == SDL_PIXELTYPE_ARRAYF16) || \
+        (SDL_PIXELTYPE(format) == SDL_PIXELTYPE_ARRAYF32)))
 
 /* The flag is set to 1 because 0x1? is not in the printable ASCII range */
 #define SDL_ISPIXELFORMAT_FOURCC(format)    \
diff --git a/include/SDL3/SDL_render.h b/include/SDL3/SDL_render.h
index 41b87b44107f..7552672f8f8a 100644
--- a/include/SDL3/SDL_render.h
+++ b/include/SDL3/SDL_render.h
@@ -240,9 +240,14 @@ extern DECLSPEC SDL_Renderer * SDLCALL SDL_CreateRenderer(SDL_Window *window, co
  *   is displayed, if you want a software renderer without a window
  * - `SDL_PROP_RENDERER_CREATE_NAME_STRING`: the name of the rendering driver
  *   to use, if a specific one is desired
+ * - `SDL_PROP_RENDERER_CREATE_INPUT_COLORSPACE_NUMBER`: an SDL_ColorSpace value describing the colorspace for input colors, defaults to SDL_COLORSPACE_SRGB
+ * - `SDL_PROP_RENDERER_CREATE_OUTPUT_COLORSPACE_NUMBER`: an SDL_ColorSpace value describing the colorspace for output to the display, defaults to SDL_COLORSPACE_SRGB
+ * - `SDL_PROP_RENDERER_CREATE_COLORSPACE_CONVERSION_BOOLEAN`: true if you want conversion between the input colorspace and the output colorspace, defaults to SDL_TRUE
  * - `SDL_PROP_RENDERER_CREATE_PRESENT_VSYNC_BOOLEAN`: true if you want
  *   present synchronized with the refresh rate
  *
+ * Note that enabling colorspace conversion between sRGB input and sRGB output implies that the rendering is done in a linear colorspace for more correct blending results. If colorspace conversion is disabled, then input colors are passed directly through to the output.
+ *
  * \param props the properties to use
  * \returns a valid rendering context or NULL if there was an error; call
  *          SDL_GetError() for more information.
@@ -256,10 +261,13 @@ extern DECLSPEC SDL_Renderer * SDLCALL SDL_CreateRenderer(SDL_Window *window, co
  */
 extern DECLSPEC SDL_Renderer * SDLCALL SDL_CreateRendererWithProperties(SDL_PropertiesID props);
 
-#define SDL_PROP_RENDERER_CREATE_WINDOW_POINTER         "window"
-#define SDL_PROP_RENDERER_CREATE_SURFACE_POINTER        "surface"
-#define SDL_PROP_RENDERER_CREATE_NAME_STRING            "name"
-#define SDL_PROP_RENDERER_CREATE_PRESENT_VSYNC_BOOLEAN  "present_vsync"
+#define SDL_PROP_RENDERER_CREATE_WINDOW_POINTER                 "window"
+#define SDL_PROP_RENDERER_CREATE_SURFACE_POINTER                "surface"
+#define SDL_PROP_RENDERER_CREATE_NAME_STRING                    "name"
+#define SDL_PROP_RENDERER_CREATE_INPUT_COLORSPACE_NUMBER        "input_colorspace"
+#define SDL_PROP_RENDERER_CREATE_OUTPUT_COLORSPACE_NUMBER       "output_colorspace"
+#define SDL_PROP_RENDERER_CREATE_COLORSPACE_CONVERSION_BOOLEAN  "colorspace_conversion"
+#define SDL_PROP_RENDERER_CREATE_PRESENT_VSYNC_BOOLEAN          "present_vsync"
 
 /**
  * Create a 2D software rendering context for a surface.
@@ -447,6 +455,7 @@ extern DECLSPEC SDL_Texture *SDLCALL SDL_CreateTextureFromSurface(SDL_Renderer *
  *
  * These are the supported properties:
  *
+ * - `SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER`: an SDL_ColorSpace value describing the texture colorspace, defaults to SDL_COLORSPACE_SCRGB for floating point textures, SDL_COLORSPACE_HDR10 for 10-bit textures, SDL_COLORSPACE_SRGB for other RGB textures and SDL_COLORSPACE_BT709_FULL for YUV textures.
  * - `SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER`: one of the enumerated values in
  *   SDL_PixelFormatEnum, defaults to the best RGBA format for the renderer
  * - `SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER`: one of the enumerated values in
@@ -524,6 +533,7 @@ extern DECLSPEC SDL_Texture *SDLCALL SDL_CreateTextureFromSurface(SDL_Renderer *
  */
 extern DECLSPEC SDL_Texture *SDLCALL SDL_CreateTextureWithProperties(SDL_Renderer *renderer, SDL_PropertiesID props);
 
+#define SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER           "colorspace"
 #define SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER               "format"
 #define SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER               "access"
 #define SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER                "width"
@@ -550,6 +560,8 @@ extern DECLSPEC SDL_Texture *SDLCALL SDL_CreateTextureWithProperties(SDL_Rendere
  *
  * The following read-only properties are provided by SDL:
  *
+ * - `SDL_PROP_TEXTURE_COLORSPACE_NUMBER`: an SDL_ColorSpace value describing the colorspace used by the texture
+ *
  * With the direct3d11 renderer:
  *
  * - `SDL_PROP_TEXTURE_D3D11_TEXTURE_POINTER`: the ID3D11Texture2D associated
@@ -609,6 +621,7 @@ extern DECLSPEC SDL_Texture *SDLCALL SDL_CreateTextureWithProperties(SDL_Rendere
  */
 extern DECLSPEC SDL_PropertiesID SDLCALL SDL_GetTextureProperties(SDL_Texture *texture);
 
+#define SDL_PROP_TEXTURE_COLORSPACE_NUMBER                  "SDL.texture.colorspace"
 #define SDL_PROP_TEXTURE_D3D11_TEXTURE_POINTER              "SDL.texture.d3d11.texture"
 #define SDL_PROP_TEXTURE_D3D11_TEXTURE_U_POINTER            "SDL.texture.d3d11.texture_u"
 #define SDL_PROP_TEXTURE_D3D11_TEXTURE_V_POINTER            "SDL.texture.d3d11.texture_v"
diff --git a/src/render/SDL_render.c b/src/render/SDL_render.c
index 9bb163f52b0b..9a943739a703 100644
--- a/src/render/SDL_render.c
+++ b/src/render/SDL_render.c
@@ -125,6 +125,66 @@ static const SDL_RenderDriver *render_drivers[] = {
 char SDL_renderer_magic;
 char SDL_texture_magic;
 
+
+void SDL_SetupRendererColorspace(SDL_Renderer *renderer, SDL_PropertiesID props)
+{
+    renderer->input_colorspace = (SDL_Colorspace)SDL_GetNumberProperty(props, SDL_PROP_RENDERER_CREATE_INPUT_COLORSPACE_NUMBER, SDL_COLORSPACE_SRGB);
+    renderer->output_colorspace = (SDL_Colorspace)SDL_GetNumberProperty(props, SDL_PROP_RENDERER_CREATE_OUTPUT_COLORSPACE_NUMBER, SDL_COLORSPACE_SRGB);
+    renderer->colorspace_conversion = SDL_GetBooleanProperty(props, SDL_PROP_RENDERER_CREATE_COLORSPACE_CONVERSION_BOOLEAN, SDL_TRUE);
+}
+
+static float sRGBtoLinear(float v)
+{
+    return v <= 0.04045f ? (v / 12.92f) : SDL_powf(((v + 0.055f) / 1.055f), 2.4f);
+}
+
+static float sRGBfromLinear(float v)
+{
+    return v <= 0.0031308f ? (v * 12.92f) : (SDL_powf(v, 1.0f / 2.4f) * 1.055f - 0.055f);
+}
+
+void SDL_ConvertToLinear(SDL_Renderer *renderer, SDL_FColor *color)
+{
+    if (!renderer->colorspace_conversion) {
+        return;
+    }
+
+    switch (SDL_COLORSPACETRANSFER(renderer->input_colorspace)) {
+    case SDL_TRANSFER_CHARACTERISTICS_SRGB:
+        color->r = sRGBtoLinear(color->r);
+        color->g = sRGBtoLinear(color->g);
+        color->b = sRGBtoLinear(color->b);
+        break;
+    case SDL_TRANSFER_CHARACTERISTICS_LINEAR:
+        /* No conversion needed */
+        break;
+    default:
+        /* Unsupported */
+        break;
+    }
+}
+
+void SDL_ConvertFromLinear(SDL_Renderer *renderer, SDL_FColor *color)
+{
+    if (!renderer->colorspace_conversion) {
+        return;
+    }
+
+    switch (SDL_COLORSPACETRANSFER(renderer->input_colorspace)) {
+    case SDL_TRANSFER_CHARACTERISTICS_SRGB:
+        color->r = sRGBfromLinear(color->r);
+        color->g = sRGBfromLinear(color->g);
+        color->b = sRGBfromLinear(color->b);
+        break;
+    case SDL_TRANSFER_CHARACTERISTICS_LINEAR:
+        /* No conversion needed */
+        break;
+    default:
+        /* Unsupported */
+        break;
+    }
+}
+
 static SDL_INLINE void DebugLogRenderCommands(const SDL_RenderCommand *cmd)
 {
 #if 0
@@ -1115,6 +1175,19 @@ static SDL_ScaleMode SDL_GetScaleMode(void)
     }
 }
 
+static SDL_Colorspace SDL_GetDefaultTextureColorspace(Uint32 format)
+{
+    if (SDL_ISPIXELFORMAT_FOURCC(format)) {
+        return SDL_COLORSPACE_BT709_FULL;
+    } else if (SDL_ISPIXELFORMAT_FLOAT(format)) {
+        return SDL_COLORSPACE_SCRGB;
+    } else if (SDL_ISPIXELFORMAT_10BIT(format)) {
+        return SDL_COLORSPACE_HDR10;
+    } else {
+        return SDL_COLORSPACE_SRGB;
+    }
+}
+
 SDL_Texture *SDL_CreateTextureWithProperties(SDL_Renderer *renderer, SDL_PropertiesID props)
 {
     SDL_Texture *texture;
@@ -1122,6 +1195,7 @@ SDL_Texture *SDL_CreateTextureWithProperties(SDL_Renderer *renderer, SDL_Propert
     int access = (int)SDL_GetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER, SDL_TEXTUREACCESS_STATIC);
     int w = (int)SDL_GetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER, 0);
     int h = (int)SDL_GetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER, 0);
+    Uint32 default_colorspace;
     SDL_bool texture_is_fourcc_and_target;
 
     CHECK_RENDERER_MAGIC(renderer, NULL);
@@ -1148,11 +1222,15 @@ SDL_Texture *SDL_CreateTextureWithProperties(SDL_Renderer *renderer, SDL_Propert
         SDL_SetError("Texture dimensions are limited to %dx%d", renderer->info.max_texture_width, renderer->info.max_texture_height);
         return NULL;
     }
+
+    default_colorspace = SDL_GetDefaultTextureColorspace(format);
+
     texture = (SDL_Texture *)SDL_calloc(1, sizeof(*texture));
     if (!texture) {
         return NULL;
     }
     texture->magic = &SDL_texture_magic;
+    texture->colorspace = (Uint32)SDL_GetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER, default_colorspace);
     texture->format = format;
     texture->access = access;
     texture->w = w;
@@ -1176,9 +1254,9 @@ SDL_Texture *SDL_CreateTextureWithProperties(SDL_Renderer *renderer, SDL_Propert
     renderer->textures = texture;
 
     /* FOURCC format cannot be used directly by renderer back-ends for target texture */
-    texture_is_fourcc_and_target = (access == SDL_TEXTUREACCESS_TARGET && SDL_ISPIXELFORMAT_FOURCC(texture->format));
+    texture_is_fourcc_and_target = (access == SDL_TEXTUREACCESS_TARGET && SDL_ISPIXELFORMAT_FOURCC(format));
 
-    if (texture_is_fourcc_and_target == SDL_FALSE && IsSupportedFormat(renderer, format)) {
+    if (!texture_is_fourcc_and_target && IsSupportedFormat(renderer, format)) {
         if (renderer->CreateTexture(renderer, texture, props) < 0) {
             SDL_DestroyTexture(texture);
             return NULL;
@@ -1186,7 +1264,7 @@ SDL_Texture *SDL_CreateTextureWithProperties(SDL_Renderer *renderer, SDL_Propert
     } else {
         int closest_format;
 
-        if (texture_is_fourcc_and_target == SDL_FALSE) {
+        if (!texture_is_fourcc_and_target) {
             closest_format = GetClosestSupportedFormat(renderer, format);
         } else {
             closest_format = renderer->info.texture_formats[0];
@@ -1255,6 +1333,8 @@ SDL_Texture *SDL_CreateTextureFromSurface(SDL_Renderer *renderer, SDL_Surface *s
     int i;
     Uint32 format = SDL_PIXELFORMAT_UNKNOWN;
     SDL_Texture *texture;
+    SDL_PropertiesID props;
+    Uint32 default_colorspace, colorspace;
 
     CHECK_RENDERER_MAGIC(renderer, NULL);
 
@@ -1320,8 +1400,16 @@ SDL_Texture *SDL_CreateTextureFromSurface(SDL_Renderer *renderer, SDL_Surface *s
         }
     }
 
-    texture = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_STATIC,
-                                surface->w, surface->h);
+    default_colorspace = SDL_GetDefaultTextureColorspace(format);
+    colorspace = (Uint32)SDL_GetNumberProperty(SDL_GetSurfaceProperties(surface), SDL_PROP_SURFACE_COLORSPACE_NUMBER, default_colorspace);
+
+    props = SDL_CreateProperties();
+    SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_COLORSPACE_NUMBER, colorspace);
+    SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_FORMAT_NUMBER, format);
+    SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_ACCESS_NUMBER, SDL_TEXTUREACCESS_STATIC);
+    SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_WIDTH_NUMBER, surface->w);
+    SDL_SetNumberProperty(props, SDL_PROP_TEXTURE_CREATE_HEIGHT_NUMBER, surface->h);
+    texture = SDL_CreateTextureWithProperties(renderer, props);
     if (!texture) {
         return NULL;
     }
@@ -1441,6 +1529,7 @@ int SDL_SetTextureColorModFloat(SDL_Texture *texture, float r, float g, float b)
     texture->color.r = r;
     texture->color.g = g;
     texture->color.b = b;
+    SDL_ConvertToLinear(texture->renderer, &texture->color);
     if (texture->native) {
         return SDL_SetTextureColorModFloat(texture->native, r, g, b);
     }
@@ -1469,16 +1558,21 @@ int SDL_GetTextureColorMod(SDL_Texture *texture, Uint8 *r, Uint8 *g, Uint8 *b)
 
 int SDL_GetTextureColorModFloat(SDL_Texture *texture, float *r, float *g, float *b)
 {
+    SDL_FColor color;
+
     CHECK_TEXTURE_MAGIC(texture, -1);
 
+    color = texture->color;
+    SDL_ConvertFromLinear(texture->renderer, &color);
+
     if (r) {
-        *r = texture->color.r;
+        *r = color.r;
     }
     if (g) {
-        *g = texture->color.g;
+        *g = color.g;
     }
     if (b) {
-        *b = texture->color.b;
+        *b = color.b;
     }
     return 0;
 }
@@ -2689,6 +2783,7 @@ int SDL_SetRenderDrawColorFloat(SDL_Renderer *renderer, float r, float g, float
     renderer->color.g = g;
     renderer->color.b = b;
     renderer->color.a = a;
+    SDL_ConvertToLinear(renderer, &renderer->color);
     return 0;
 }
 
@@ -2717,19 +2812,24 @@ int SDL_GetRenderDrawColor(SDL_Renderer *renderer, Uint8 *r, Uint8 *g, Uint8 *b,
 
 int SDL_GetRenderDrawColorFloat(SDL_Renderer *renderer, float *r, float *g, float *b, float *a)
 {
+    SDL_FColor color;
+
     CHECK_RENDERER_MAGIC(renderer, -1);
 
+    color = renderer->color;
+    SDL_ConvertFromLinear(renderer, &color);
+
     if (r) {
-        *r = renderer->color.r;
+        *r = color.r;
     }
     if (g) {
-        *g = renderer->color.g;
+        *g = color.g;
     }
     if (b) {
-        *b = renderer->color.b;
+        *b = color.b;
     }
     if (a) {
-        *a = renderer->color.a;
+        *a = color.a;
     }
     return 0;
 }
diff --git a/src/render/SDL_sysrender.h b/src/render/SDL_sysrender.h
index c54f00aeee11..58536876f3f6 100644
--- a/src/render/SDL_sysrender.h
+++ b/src/render/SDL_sysrender.h
@@ -63,6 +63,7 @@ typedef struct SDL_RenderViewState
 struct SDL_Texture
 {
     const void *magic;
+    SDL_Colorspace colorspace;  /**< The colorspace of the texture */
     Uint32 format;              /**< The pixel format of the texture */
     int access;                 /**< SDL_TextureAccess */
     int w;                      /**< The width of the texture */
@@ -249,6 +250,10 @@ struct SDL_Renderer
     SDL_Texture *target;
     SDL_Mutex *target_mutex;
 
+    SDL_Colorspace input_colorspace;
+    SDL_Colorspace output_colorspace;
+    SDL_bool colorspace_conversion;
+
     SDL_FColor color;        /**< Color for drawing operations values */
     SDL_BlendMode blendMode; /**< The drawing blend mode */
 
@@ -294,6 +299,13 @@ extern SDL_RenderDriver PSP_RenderDriver;
 extern SDL_RenderDriver SW_RenderDriver;
 extern SDL_RenderDriver VITA_GXM_RenderDriver;
 
+/* Setup colorspace conversion */
+extern void SDL_SetupRendererColorspace(SDL_Renderer *renderer, SDL_PropertiesID props);
+
+/* Colorspace conversion functions */
+extern void SDL_ConvertToLinear(SDL_Renderer *renderer, SDL_FColor *color);
+extern void SDL_ConvertFromLinear(SDL_Renderer *renderer, SDL_FColor *color);
+
 /* Blend mode functions */
 extern SDL_BlendFactor SDL_GetBlendModeSrcColorFactor(SDL_BlendMode blendMode);
 extern SDL_BlendFactor SDL_GetBlendModeDstColorFactor(SDL_BlendMode blendMode);
diff --git a/src/render/direct3d11/SDL_render_d3d11.c b/src/render/direct3d11/SDL_render_d3d11.c
index 65ce0155b4f3..8966bda40eb1 100644
--- a/src/render/direct3d11/SDL_render_d3d11.c
+++ b/src/render/direct3d11/SDL_render_d3d11.c
@@ -198,20 +198,28 @@ Uint32 D3D11_DXGIFormatToSDLPixelFormat(DXGI_FORMAT dxgiFormat)
 {
     switch (dxgiFormat) {
     case DXGI_FORMAT_B8G8R8A8_UNORM:
+    case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
         return SDL_PIXELFORMAT_ARGB8888;
     case DXGI_FORMAT_B8G8R8X8_UNORM:
+    case DXGI_FORMAT_B8G8R8X8_UNORM_SRGB:
         return SDL_PIXELFORMAT_XRGB8888;
     default:
         return SDL_PIXELFORMAT_UNKNOWN;
     }
 }
 
-static DXGI_FORMAT SDLPixelFormatToDXGITextureFormat(Uint32 sdlFormat)
+static DXGI_FORMAT SDLPixelFormatToDXGITextureFormat(Uint32 format, Uint32 colorspace, SDL_bool colorspace_conversion)
 {
-    switch (sdlFormat) {
+    switch (format) {
     case SDL_PIXELFORMAT_ARGB8888:
+        if (colorspace_conversion && colorspace == SDL_COLORSPACE_SRGB) {
+            return DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
+        }
         return DXGI_FORMAT_B8G8R8A8_UNORM;
     case SDL_PIXELFORMAT_XRGB8888:
+        if (colorspace_conversion && colorspace == SDL_COLORSPACE_SRGB) {
+            return DXGI_FORMAT_B8G8R8X8_UNORM_SRGB;
+        }
         return DXGI_FORMAT_B8G8R8X8_UNORM;
     case SDL_PIXELFORMAT_YV12:
     case SDL_PIXELFORMAT_IYUV:
@@ -224,12 +232,18 @@ static DXGI_FORMAT SDLPixelFormatToDXGITextureFormat(Uint32 sdlFormat)
     }
 }
 
-static DXGI_FORMAT SDLPixelFormatToDXGIMainResourceViewFormat(Uint32 sdlFormat)
+static DXGI_FORMAT SDLPixelFormatToDXGIMainResourceViewFormat(Uint32 format, Uint32 colorspace, SDL_bool colorspace_conversion)
 {
-    switch (sdlFormat) {
+    switch (format) {
     case SDL_PIXELFORMAT_ARGB8888:
+        if (colorspace_conversion && colorspace == SDL_COLORSPACE_SRGB) {
+            return DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
+        }
         return DXGI_FORMAT_B8G8R8A8_UNORM;
     case SDL_PIXELFORMAT_XRGB8888:
+        if (colorspace_conversion && colorspace == SDL_COLORSPACE_SRGB) {
+            return DXGI_FORMAT_B8G8R8X8_UNORM_SRGB;
+        }
         return DXGI_FORMAT_B8G8R8X8_UNORM;
     case SDL_PIXELFORMAT_YV12:
     case SDL_PIXELFORMAT_IYUV:
@@ -988,9 +1002,17 @@ static HRESULT D3D11_CreateWindowSizeDependentResources(SDL_Renderer *renderer)
     }
 
     /* Create a render target view of the swap chain back buffer. */
+    D3D11_RENDER_TARGET_VIEW_DESC desc;
+    SDL_zero(desc);
+    if (renderer->colorspace_conversion && renderer->output_colorspace == SDL_COLORSPACE_SRGB) {
+        desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
+    } else {
+        desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
+    }
+    desc.ViewDimension = D3D11_RTV_DIMENSION_TEXTURE2D;
     result = ID3D11Device_CreateRenderTargetView(data->d3dDevice,
                                                  (ID3D11Resource *)backBuffer,
-                                                 NULL,
+                                                 &desc,
                                                  &data->mainRenderTargetView);
     if (FAILED(result)) {
         WIN_SetErrorFromHRESULT(SDL_COMPOSE_ERROR("ID3D11Device::CreateRenderTargetView"), result);
@@ -1083,7 +1105,7 @@ static int D3D11_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SDL
     D3D11_RenderData *rendererData = (D3D11_RenderData *)renderer->driverdata;
     D3D11_TextureData *textureData;
     HRESULT result;
-    DXGI_FORMAT textureFormat = SDLPixelFormatToDXGITextureFormat(texture->format);
+    DXGI_FORMAT textureFormat = SDLPixelFormatToDXGITextureFormat(texture->format, texture->colorspace, renderer->colorspace_conversion);
     D3D11_TEXTURE2D_DESC textureDesc;
     D3D11_SHADER_RESOURCE_VIEW_DESC resourceViewDesc;
 
@@ -1182,7 +1204,7 @@ static int D3D11_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SDL
     }
 #endif /* SDL_HAVE_YUV */
     SDL_zero(resourceViewDesc);
-    resourceViewDesc.Format = SDLPixelFormatToDXGIMainResourceViewFormat(texture->format);
+    resourceViewDesc.Format = SDLPixelFormatToDXGIMainResourceViewFormat(texture->format, texture->colorspace, renderer->colorspace_conversion);
     resourceViewDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
     resourceViewDesc.Texture2D.MostDetailedMip = 0;
     resourceViewDesc.Texture2D.MipLevels = textureDesc.MipLevels;
@@ -2441,6 +2463,8 @@ SDL_Renderer *D3D11_CreateRenderer(SDL_Window *window, SDL_PropertiesID create_p
         return NULL;
     }
 
+    SDL_SetupRendererColorspace(renderer, create_props);
+
     data->identity = MatrixIdentity();
 
     renderer->WindowEvent = D3D11_WindowEvent;
diff --git a/src/render/direct3d12/SDL_render_d3d12.c b/src/render/direct3d12/SDL_render_d3d12.c
index 8c6a18003008..10fe443509e3 100644
--- a/src/render/direct3d12/SDL_render_d3d12.c
+++ b/src/render/direct3d12/SDL_render_d3d12.c
@@ -273,20 +273,28 @@ Uint32 D3D12_DXGIFormatToSDLPixelFormat(DXGI_FORMAT dxgiFormat)
 {
     switch (dxgiFormat) {
     case DXGI_FORMAT_B8G8R8A8_UNORM:
+    case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB:
         return SDL_PIXELFORMAT_ARGB8888;
     case DXGI_FORMAT_B8G8R8X8_UNORM:
+    case DXGI_FORMAT_B8G8R8X8_UNORM_SRGB:
         return SDL_PIXELFORMAT_XRGB8888;
     default:
         return SDL_PIXELFORMAT_UNKNOWN;
     }
 }
 
-static DXGI_FORMAT SDLPixelFormatToDXGITextureFormat(Uint32 sdlFormat)
+static DXGI_FORMAT SDLPixelFormatToDXGITextureFormat(Uint32 format, Uint32 colorspace, SDL_bool colorspace_conversion)
 {
-    switch (sdlFormat) {
+    switch (format) {
     case SDL_PIXELFORMAT_ARGB8888:
+        if (colorspace_conversion && colorspace == SDL_COLORSPACE_SRGB) {
+            return DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
+        }
         return DXGI_FORMAT_B8G8R8A8_UNORM;
     case SDL_PIXELFORMAT_XRGB8888:
+        if (colorspace_conversion && colorspace == SDL_COLORSPACE_SRGB) {
+            return DXGI_FORMAT_B8G8R8X8_UNORM_SRGB;
+        }
         return DXGI_FORMAT_B8G8R8X8_UNORM;
     case SDL_PIXELFORMAT_YV12:
     case SDL_PIXELFORMAT_IYUV:
@@ -299,12 +307,18 @@ static DXGI_FORMAT SDLPixelFormatToDXGITextureFormat(Uint32 sdlFormat)
     }
 }
 
-static DXGI_FORMAT SDLPixelFormatToDXGIMainResourceViewFormat(Uint32 sdlFormat)
+static DXGI_FORMAT SDLPixelFormatToDXGIMainResourceViewFormat(Uint32 format, Uint32 colorspace, SDL_bool colorspace_conversion)
 {
-    switch (sdlFormat) {
+    switch (format) {
     case SDL_PIXELFORMAT_ARGB8888:
+        if (colorspace_conversion && colorspace == SDL_COLORSPACE_SRGB) {
+            return DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
+        }
         return DXGI_FORMAT_B8G8R8A8_UNORM;
     case SDL_PIXELFORMAT_XRGB8888:
+        if (colorspace_conversion && colorspace == SDL_COLORSPACE_SRGB) {
+            return DXGI_FORMAT_B8G8R8X8_UNORM_SRGB;
+        }
         return DXGI_FORMAT_B8G8R8X8_UNORM;
     case SDL_PIXELFORMAT_YV12:
     case SDL_PIXELFORMAT_IYUV:
@@ -1340,7 +1354,11 @@ static HRESULT D3D12_CreateWindowSizeDependentResources(SDL_Renderer *renderer)
 #endif
 
         SDL_zero(rtvDesc);
-        rtvDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
+        if (renderer->colorspace_conversion && renderer->output_colorspace == SDL_COLORSPACE_SRGB) {
+            rtvDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM_SRGB;
+        } else {
+            rtvDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
+        }
         rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D;
 
         SDL_zero(rtvDescriptor);
@@ -1450,7 +1468,7 @@ static int D3D12_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SDL
     D3D12_RenderData *rendererData = (D3D12_RenderData *)renderer->driverdata;
     D3D12_TextureData *textureData;
     HRESULT result;
-    DXGI_FORMAT textureFormat = SDLPixelFormatToDXGITextureFormat(texture->format);
+    DXGI_FORMAT textureFormat = SDLPixelFormatToDXGITextureFormat(texture->format,  texture->colorspace, renderer->colorspace_conversion);
     D3D12_RESOURCE_DESC textureDesc;
     D3D12_HEAP_PROPERTIES heapProps;
     D3D12_SHADER_RESOURCE_VIEW_DESC resourceViewDesc;
@@ -1563,7 +1581,7 @@ static int D3D12_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SDL
 #endif /* SDL_HAVE_YUV */
     SDL_zero(resourceViewDesc);
     resourceViewDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
-    resourceViewDesc.Format = SDLPixelFormatToDXGIMainResourceViewFormat(texture->format);
+    resourceViewDesc.Format = SDLPixelFormatToDXGIMainResourceViewFormat(texture->format, texture->colorspace, renderer->colorspace_conversion);
     resourceViewDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
     resourceViewDesc.Texture2D.MipLevels = textureDesc.MipLevels;
 
@@ -2985,6 +3003,8 @@ SDL_Renderer *D3D12_CreateRenderer(SDL_Window *window, SDL_PropertiesID create_p
         return NULL;
     }
 
+    SDL_SetupRendererColorspace(renderer, create_props);
+
     data->identity = MatrixIdentity();
 
     renderer->WindowEvent = D3D12_WindowEvent;
diff --git a/src/render/opengl/SDL_render_gl.c b/src/render/opengl/SDL_render_gl.c
index cac1fb3a9fe6..17c438b499cb 100644
--- a/src/render/opengl/SDL_render_gl.c
+++ b/src/render/opengl/SDL_render_gl.c
@@ -49,8 +49,6 @@
    http://developer.apple.com/library/mac/#documentation/GraphicsImaging/Conceptual/OpenGL-MacProgGuide/opengl_texturedata/opengl_texturedata.html
 */
 
-static const float inv255f = 1.0f / 255.0f;
-
 typedef struct GL_FBOList GL_FBOList;
 
 struct GL_FBOList
@@ -403,19 +401,27 @@ static SDL_bool GL_SupportsBlendMode(SDL_Renderer *renderer, SDL_BlendMode blend
     return SDL_TRUE;
 }
 
-static SDL_bool convert_format(GL_RenderData *renderdata, Uint32 pixel_format,
+static SDL_bool convert_format(Uint32 pixel_format, Uint32 colorspace, SDL_bool colorspace_conversion,
                GLint *internalFormat, GLenum *format, GLenum *type)
 {
     switch (pixel_format) {
     case SDL_PIXELFORMAT_ARGB8888:
     case SDL_PIXELFORMAT_XRGB8888:
-        *internalFormat = GL_RGBA8;
+        if (colorspace_conversion && colorspace == SDL_COLORSPACE_SRGB) {
+            *internalFormat = GL_SRGB8_ALPHA8;
+        } else {
+            *internalFormat = GL_RGBA8;
+        }
         *format = GL_BGRA;
         *type = GL_UNSIGNED_INT_8_8_8_8_REV;
         break;
     case SDL_PIXELFORMAT_ABGR8888:
     case SDL_PIXELFORMAT_XBGR8888:
-        *internalFormat = GL_RGBA8;
+        if (colorspace_conversion && colorspace == SDL_COLORSPACE_SRGB) {
+            *internalFormat = GL_SRGB8_ALPHA8;
+        } else {
+            *internalFormat = GL_RGBA8;
+        }
         *format = GL_RGBA;
         *type = GL_UNSIGNED_INT_8_8_8_8_REV;
         break;
@@ -460,8 +466,8 @@ static int GL_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SDL_Pr
         return SDL_SetError("Render targets not supported by OpenGL");
     }
 
-    if (!convert_format(renderdata, texture->format, &internalFormat,
-                        &format, &type)) {
+    if (!convert_format(texture->format, texture->colorspace, renderer->colorspace_conversion,
+                        &internalFormat, &format, &type)) {
         return SDL_SetError("Texture format %s not supported by OpenGL",
                             SDL_GetPixelFormatName(texture->format));
     }
@@ -1475,7 +1481,8 @@ static int GL_RenderReadPixels(SDL_Renderer *renderer, const SDL_Rect *rect,
 
     GL_ActivateRenderer(renderer);
 
-    if (!convert_format(data, temp_format, &internalFormat, &format, &type)) {
+    if (!convert_format(temp_format, renderer->input_colorspace, renderer->colorspace_conversion,
+                        &internalFormat, &format, &type)) {
         return SDL_SetError("Texture format %s not supported by OpenGL",
                             SDL_GetPixelFormatName(temp_format));
     }
@@ -1709,6 +1716,8 @@ static SDL_Renderer *GL_CreateRenderer(SDL_Window *window, SDL_PropertiesID crea
         goto error;
     }
 
+    SDL_SetupRendererColorspace(renderer, create_props);
+
     renderer->WindowEvent = GL_WindowEvent;
     renderer->SupportsBlendMode = GL_SupportsBlendMode;
     renderer->CreateTexture = GL_CreateTexture;
@@ -1910,6 +1919,9 @@ static SDL_Renderer *GL_CreateRenderer(SDL_Window *window, SDL_PropertiesID crea
     data->glDisable(data->textype);
     data->glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
     data->glColor4f(1.0f, 1.0f, 1.0f, 1.0f);
+    if (renderer->colorspace_conversion && renderer->output_colorspace == SDL_COLORSPACE_SRGB) {
+        data->glEnable(GL_FRAMEBUFFER_SRGB);
+    }
     /* This ended up causing video discrepancies between OpenGL and Direct3D */
     /* data->glEnable(GL_LINE_SMOOTH); */
 
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 014e29d1c099..2fc1d953da1e 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -350,6 +350,7 @@ files2headers(gamepad_image_headers
 files2headers(icon_bmp_header icon.bmp)
 
 add_sdl_test_executable(testaudio MAIN_CALLBACKS NEEDS_RESOURCES TESTUTILS SOURCES testaudio.c)
+add_sdl_test_executable(testcolorspace SOURCES testcolorspace.c)
 add_sdl_test_executable(testfile NONINTERACTIVE SOURCES testfile.c)
 add_sdl_test_executable(testcontroller TESTUTILS SOURCES testcontroller.c gamepadutils.c ${gamepad_image_headers})
 add_sdl_test_executable(testgeometry TESTUTILS SOURCES testgeometry.c)
diff --git a/test/testcolorspace.c b/test/testcolorspace.c
new file mode 100644
index 000000000000..ad427c6729ef
--- /dev/null
+++ b/test/testcolorspace.c
@@ -0,0 +1,420 @@
+/*
+  Copyright (C) 1997-2024 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely.
+*/
+
+#include <SDL3/SDL.h>
+#include <SDL3/SDL_test.h>
+#include <SDL3/SDL_main.h>
+
+#ifdef SDL_PLATFORM_EMSCRIPTEN
+#include <emscripten/emscripten.h>
+#endif
+
+#define WINDOW_WIDTH  640
+#define WINDOW_HEIGHT 480
+
+#define TEXT_START_X 6.0f
+#define TEXT_START_Y 6.0f
+#define TEXT_LINE_ADVANCE FONT_CHARACTER_SIZE * 2
+
+static SDL_Window *window;
+static SDL_Renderer *renderer;
+static const char *renderer_name;
+static int renderer_count = 0;
+static int renderer_index = 0;
+static int stage_count = 4;
+static int stage_index = 0;
+static int done;
+
+static void FreeRenderer(void)
+{
+    SDLTest_CleanupTextDrawing();
+    SDL_DestroyRenderer(renderer);
+    renderer = NULL;
+}
+
+st

(Patch may be truncated, please check the link at the top of this post.)