SDL: Implemented scRGB colorspace and HDR support on macOS

From ba074acad43fe3253fcdb9b7a696152fb01c5e42 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Tue, 6 Feb 2024 05:31:43 -0800
Subject: [PATCH] Implemented scRGB colorspace and HDR support on macOS

---
 src/render/SDL_render.c                       |   30 +
 src/render/SDL_sysrender.h                    |    4 +
 src/render/direct3d/SDL_render_d3d.c          |   12 +-
 src/render/direct3d11/SDL_render_d3d11.c      |   22 +-
 src/render/direct3d12/SDL_render_d3d12.c      |   22 +-
 src/render/metal/SDL_render_metal.m           |  156 +-
 src/render/metal/SDL_shaders_metal.metal      |  125 +-
 src/render/metal/SDL_shaders_metal_ios.h      | 3250 +++++++------
 .../metal/SDL_shaders_metal_iphonesimulator.h | 4012 ++++++++++-------
 src/render/metal/SDL_shaders_metal_macos.h    | 3243 +++++++------
 src/render/metal/SDL_shaders_metal_tvos.h     | 3250 +++++++------
 .../metal/SDL_shaders_metal_tvsimulator.h     | 3807 +++++++++-------
 src/render/opengl/SDL_render_gl.c             |   12 +-
 src/render/opengles2/SDL_render_gles2.c       |   12 +-
 src/render/ps2/SDL_render_ps2.c               |    9 +-
 src/render/psp/SDL_render_psp.c               |   12 +-
 src/render/software/SDL_render_sw.c           |   12 +-
 src/render/vitagxm/SDL_render_vita_gxm.c      |   12 +-
 src/video/cocoa/SDL_cocoaevents.m             |   11 +
 src/video/cocoa/SDL_cocoamodes.h              |    1 +
 src/video/cocoa/SDL_cocoamodes.m              |   69 +-
 src/video/uikit/SDL_uikitmodes.m              |   17 +-
 test/testcolorspace.c                         |    3 +
 test/testsprite.c                             |    2 +
 24 files changed, 10321 insertions(+), 7784 deletions(-)

diff --git a/src/render/SDL_render.c b/src/render/SDL_render.c
index 72599dfc9c62..6b3d05a790a9 100644
--- a/src/render/SDL_render.c
+++ b/src/render/SDL_render.c
@@ -295,6 +295,7 @@ static int FlushRenderCommands(SDL_Renderer *renderer)
     renderer->vertex_data_used = 0;
     renderer->render_command_generation++;
     renderer->color_queued = SDL_FALSE;
+    renderer->color_scale_queued = SDL_FALSE;
     renderer->viewport_queued = SDL_FALSE;
     renderer->cliprect_queued = SDL_FALSE;
     return retval;
@@ -481,6 +482,31 @@ static int QueueCmdSetDrawColor(SDL_Renderer *renderer, SDL_FColor *color)
     return retval;
 }
 
+static int QueueCmdSetColorScale(SDL_Renderer *renderer)
+{
+    int retval = 0;
+
+    if (!renderer->color_scale_queued ||
+        renderer->color_scale != renderer->last_queued_color_scale) {
+        SDL_RenderCommand *cmd = AllocateRenderCommand(renderer);
+        retval = -1;
+
+        if (cmd) {
+            cmd->command = SDL_RENDERCMD_SETCOLORSCALE;
+            cmd->data.color.first = 0; /* render backend will fill this in. */
+            cmd->data.color.color_scale = renderer->color_scale;
+            retval = renderer->QueueSetColorScale(renderer, cmd);
+            if (retval < 0) {
+                cmd->command = SDL_RENDERCMD_NO_OP;
+            } else {
+                renderer->last_queued_color_scale = renderer->color_scale;
+                renderer->color_scale_queued = SDL_TRUE;
+            }
+        }
+    }
+    return retval;
+}
+
 static int QueueCmdClear(SDL_Renderer *renderer)
 {
     SDL_RenderCommand *cmd = AllocateRenderCommand(renderer);
@@ -514,6 +540,10 @@ static SDL_RenderCommand *PrepQueueCmdDraw(SDL_Renderer *renderer, const SDL_Ren
         retval = QueueCmdSetDrawColor(renderer, color);
     }
 
+    if (retval == 0) {
+        retval = QueueCmdSetColorScale(renderer);
+    }
+
     /* Set the viewport and clip rect directly before draws, so the backends
      * don't have to worry about that state not being valid at draw time. */
     if (retval == 0 && !renderer->viewport_queued) {
diff --git a/src/render/SDL_sysrender.h b/src/render/SDL_sysrender.h
index a953be5ce8e3..838f8749081f 100644
--- a/src/render/SDL_sysrender.h
+++ b/src/render/SDL_sysrender.h
@@ -99,6 +99,7 @@ typedef enum
     SDL_RENDERCMD_SETVIEWPORT,
     SDL_RENDERCMD_SETCLIPRECT,
     SDL_RENDERCMD_SETDRAWCOLOR,
+    SDL_RENDERCMD_SETCOLORSCALE,
     SDL_RENDERCMD_CLEAR,
     SDL_RENDERCMD_DRAW_POINTS,
     SDL_RENDERCMD_DRAW_LINES,
@@ -166,6 +167,7 @@ struct SDL_Renderer
     int (*CreateTexture)(SDL_Renderer *renderer, SDL_Texture *texture, SDL_PropertiesID create_props);
     int (*QueueSetViewport)(SDL_Renderer *renderer, SDL_RenderCommand *cmd);
     int (*QueueSetDrawColor)(SDL_Renderer *renderer, SDL_RenderCommand *cmd);
+    int (*QueueSetColorScale)(SDL_Renderer *renderer, SDL_RenderCommand *cmd);
     int (*QueueDrawPoints)(SDL_Renderer *renderer, SDL_RenderCommand *cmd, const SDL_FPoint *points,
                            int count);
     int (*QueueDrawLines)(SDL_Renderer *renderer, SDL_RenderCommand *cmd, const SDL_FPoint *points,
@@ -262,10 +264,12 @@ struct SDL_Renderer
     SDL_RenderCommand *render_commands_pool;
     Uint32 render_command_generation;
     SDL_FColor last_queued_color;
+    float last_queued_color_scale;
     SDL_Rect last_queued_viewport;
     SDL_Rect last_queued_cliprect;
     SDL_bool last_queued_cliprect_enabled;
     SDL_bool color_queued;
+    SDL_bool color_scale_queued;
     SDL_bool viewport_queued;
     SDL_bool cliprect_queued;
 
diff --git a/src/render/direct3d/SDL_render_d3d.c b/src/render/direct3d/SDL_render_d3d.c
index 588a349127a0..9d3e080c87fc 100644
--- a/src/render/direct3d/SDL_render_d3d.c
+++ b/src/render/direct3d/SDL_render_d3d.c
@@ -808,7 +808,7 @@ static int D3D_SetRenderTarget(SDL_Renderer *renderer, SDL_Texture *texture)
     return D3D_SetRenderTargetInternal(renderer, texture);
 }
 
-static int D3D_QueueSetViewport(SDL_Renderer *renderer, SDL_RenderCommand *cmd)
+static int D3D_QueueNoOp(SDL_Renderer *renderer, SDL_RenderCommand *cmd)
 {
     return 0; /* nothing to do in this backend. */
 }
@@ -1179,6 +1179,11 @@ static int D3D_RunCommandQueue(SDL_Renderer *renderer, SDL_RenderCommand *cmd, v
             break;
         }
 
+        case SDL_RENDERCMD_SETCOLORSCALE:
+        {
+            break;
+        }
+
         case SDL_RENDERCMD_SETVIEWPORT:
         {
             SDL_Rect *viewport = &data->drawstate.viewport;
@@ -1615,8 +1620,9 @@ SDL_Renderer *D3D_CreateRenderer(SDL_Window *window, SDL_PropertiesID create_pro
     renderer->UnlockTexture = D3D_UnlockTexture;
     renderer->SetTextureScaleMode = D3D_SetTextureScaleMode;
     renderer->SetRenderTarget = D3D_SetRenderTarget;
-    renderer->QueueSetViewport = D3D_QueueSetViewport;
-    renderer->QueueSetDrawColor = D3D_QueueSetViewport; /* SetViewport and SetDrawColor are (currently) no-ops. */
+    renderer->QueueSetViewport = D3D_QueueNoOp;
+    renderer->QueueSetDrawColor = D3D_QueueNoOp;
+    renderer->QueueSetColorScale = D3D_QueueNoOp;
     renderer->QueueDrawPoints = D3D_QueueDrawPoints;
     renderer->QueueDrawLines = D3D_QueueDrawPoints; /* lines and points queue vertices the same way. */
     renderer->QueueGeometry = D3D_QueueGeometry;
diff --git a/src/render/direct3d11/SDL_render_d3d11.c b/src/render/direct3d11/SDL_render_d3d11.c
index 2b7ce5896cf9..1580928f8ba8 100644
--- a/src/render/direct3d11/SDL_render_d3d11.c
+++ b/src/render/direct3d11/SDL_render_d3d11.c
@@ -1851,7 +1851,7 @@ static int D3D11_SetRenderTarget(SDL_Renderer *renderer, SDL_Texture *texture)
     return 0;
 }
 
-static int D3D11_QueueSetViewport(SDL_Renderer *renderer, SDL_RenderCommand *cmd)
+static int D3D11_QueueNoOp(SDL_Renderer *renderer, SDL_RenderCommand *cmd)
 {
     return 0; /* nothing to do in this backend. */
 }
@@ -1860,6 +1860,7 @@ static int D3D11_QueueDrawPoints(SDL_Renderer *renderer, SDL_RenderCommand *cmd,
 {
     VertexPositionColor *verts = (VertexPositionColor *)SDL_AllocateRenderVertices(renderer, count * sizeof(VertexPositionColor), 0, &cmd->data.draw.first);
     int i;
+    SDL_FColor color = cmd->data.draw.color;
     SDL_bool convert_color = SDL_RenderingLinearSpace(renderer);
 
     if (!verts) {
@@ -1868,15 +1869,16 @@ static int D3D11_QueueDrawPoints(SDL_Renderer *renderer, SDL_RenderCommand *cmd,
 
     cmd->data.draw.count = count;
 
+    if (convert_color) {
+        SDL_ConvertToLinear(&color);
+    }
+
     for (i = 0; i < count; i++) {
         verts->pos.x = points[i].x + 0.5f;
         verts->pos.y = points[i].y + 0.5f;
         verts->tex.x = 0.0f;
         verts->tex.y = 0.0f;
-        verts->color = cmd->data.draw.color;
-        if (convert_color) {
-            SDL_ConvertToLinear(&verts->color);
-        }
+        verts->color = color;
         verts++;
     }
 
@@ -2358,6 +2360,11 @@ static int D3D11_RunCommandQueue(SDL_Renderer *renderer, SDL_RenderCommand *cmd,
             break; /* this isn't currently used in this render backend. */
         }
 
+        case SDL_RENDERCMD_SETCOLORSCALE:
+        {
+            break;
+        }
+
         case SDL_RENDERCMD_SETVIEWPORT:
         {
             SDL_Rect *viewport = &rendererData->currentViewport;
@@ -2661,8 +2668,9 @@ SDL_Renderer *D3D11_CreateRenderer(SDL_Window *window, SDL_PropertiesID create_p
     renderer->UnlockTexture = D3D11_UnlockTexture;
     renderer->SetTextureScaleMode = D3D11_SetTextureScaleMode;
     renderer->SetRenderTarget = D3D11_SetRenderTarget;
-    renderer->QueueSetViewport = D3D11_QueueSetViewport;
-    renderer->QueueSetDrawColor = D3D11_QueueSetViewport; /* SetViewport and SetDrawColor are (currently) no-ops. */
+    renderer->QueueSetViewport = D3D11_QueueNoOp;
+    renderer->QueueSetDrawColor = D3D11_QueueNoOp;
+    renderer->QueueSetColorScale = D3D11_QueueNoOp;
     renderer->QueueDrawPoints = D3D11_QueueDrawPoints;
     renderer->QueueDrawLines = D3D11_QueueDrawPoints; /* lines and points queue vertices the same way. */
     renderer->QueueGeometry = D3D11_QueueGeometry;
diff --git a/src/render/direct3d12/SDL_render_d3d12.c b/src/render/direct3d12/SDL_render_d3d12.c
index cd620cf83d63..725efe9f01b1 100644
--- a/src/render/direct3d12/SDL_render_d3d12.c
+++ b/src/render/direct3d12/SDL_render_d3d12.c
@@ -2288,7 +2288,7 @@ static int D3D12_SetRenderTarget(SDL_Renderer *renderer, SDL_Texture *texture)
     return 0;
 }
 
-static int D3D12_QueueSetViewport(SDL_Renderer *renderer, SDL_RenderCommand *cmd)
+static int D3D12_QueueNoOp(SDL_Renderer *renderer, SDL_RenderCommand *cmd)
 {
     return 0; /* nothing to do in this backend. */
 }
@@ -2297,6 +2297,7 @@ static int D3D12_QueueDrawPoints(SDL_Renderer *renderer, SDL_RenderCommand *cmd,
 {
     VertexPositionColor *verts = (VertexPositionColor *)SDL_AllocateRenderVertices(renderer, count * sizeof(VertexPositionColor), 0, &cmd->data.draw.first);
     int i;
+    SDL_FColor color = cmd->data.draw.color;
     SDL_bool convert_color = SDL_RenderingLinearSpace(renderer);
 
     if (!verts) {
@@ -2305,15 +2306,16 @@ static int D3D12_QueueDrawPoints(SDL_Renderer *renderer, SDL_RenderCommand *cmd,
 
     cmd->data.draw.count = count;
 
+    if (convert_color) {
+        SDL_ConvertToLinear(&color);
+    }
+
     for (i = 0; i < count; i++) {
         verts->pos.x = points[i].x + 0.5f;
         verts->pos.y = points[i].y + 0.5f;
         verts->tex.x = 0.0f;
         verts->tex.y = 0.0f;
-        verts->color = cmd->data.draw.color;
-        if (convert_color) {
-            SDL_ConvertToLinear(&verts->color);
-        }
+        verts->color = color;
         verts++;
     }
 
@@ -2775,6 +2777,11 @@ static int D3D12_RunCommandQueue(SDL_Renderer *renderer, SDL_RenderCommand *cmd,
             break; /* this isn't currently used in this render backend. */
         }
 
+        case SDL_RENDERCMD_SETCOLORSCALE:
+        {
+            break;
+        }
+
         case SDL_RENDERCMD_SETVIEWPORT:
         {
             SDL_Rect *viewport = &rendererData->currentViewport;
@@ -3168,8 +3175,9 @@ SDL_Renderer *D3D12_CreateRenderer(SDL_Window *window, SDL_PropertiesID create_p
     renderer->UnlockTexture = D3D12_UnlockTexture;
     renderer->SetTextureScaleMode = D3D12_SetTextureScaleMode;
     renderer->SetRenderTarget = D3D12_SetRenderTarget;
-    renderer->QueueSetViewport = D3D12_QueueSetViewport;
-    renderer->QueueSetDrawColor = D3D12_QueueSetViewport; /* SetViewport and SetDrawColor are (currently) no-ops. */
+    renderer->QueueSetViewport = D3D12_QueueNoOp;
+    renderer->QueueSetDrawColor = D3D12_QueueNoOp;
+    renderer->QueueSetColorScale = D3D12_QueueNoOp;
     renderer->QueueDrawPoints = D3D12_QueueDrawPoints;
     renderer->QueueDrawLines = D3D12_QueueDrawPoints; /* lines and points queue vertices the same way. */
     renderer->QueueGeometry = D3D12_QueueGeometry;
diff --git a/src/render/metal/SDL_render_metal.m b/src/render/metal/SDL_render_metal.m
index 158565a75889..876b1b5f8156 100644
--- a/src/render/metal/SDL_render_metal.m
+++ b/src/render/metal/SDL_render_metal.m
@@ -556,10 +556,18 @@ static int METAL_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SDL
 
         switch (texture->format) {
         case SDL_PIXELFORMAT_ABGR8888:
-            pixfmt = MTLPixelFormatRGBA8Unorm;
+            if (renderer->output_colorspace == SDL_COLORSPACE_SCRGB) {
+                pixfmt = MTLPixelFormatRGBA8Unorm_sRGB;
+            } else {
+                pixfmt = MTLPixelFormatRGBA8Unorm;
+            }
             break;
         case SDL_PIXELFORMAT_ARGB8888:
-            pixfmt = MTLPixelFormatBGRA8Unorm;
+            if (renderer->output_colorspace == SDL_COLORSPACE_SCRGB) {
+                pixfmt = MTLPixelFormatBGRA8Unorm_sRGB;
+            } else {
+                pixfmt = MTLPixelFormatBGRA8Unorm;
+            }
             break;
         case SDL_PIXELFORMAT_IYUV:
         case SDL_PIXELFORMAT_YV12:
@@ -567,6 +575,12 @@ static int METAL_CreateTexture(SDL_Renderer *renderer, SDL_Texture *texture, SDL
         case SDL_PIXELFORMAT_NV21:
             pixfmt = MTLPixelFormatR8Unorm;
             break;
+        case SDL_PIXELFORMAT_RGBA64_FLOAT:
+            pixfmt = MTLPixelFormatRGBA16Float;
+            break;
+        case SDL_PIXELFORMAT_RGBA128_FLOAT:
+            pixfmt = MTLPixelFormatRGBA32Float;
+            break;
         default:
             return SDL_SetError("Texture format %s not supported by Metal", SDL_GetPixelFormatName(texture->format));
         }
@@ -1042,15 +1056,29 @@ static int METAL_QueueSetViewport(SDL_Renderer *renderer, SDL_RenderCommand *cmd
     return 0;
 }
 
-static int METAL_QueueSetDrawColor(SDL_Renderer *renderer, SDL_RenderCommand *cmd)
+static int METAL_QueueNoOp(SDL_Renderer *renderer, SDL_RenderCommand *cmd)
 {
+    return 0; /* nothing to do in this backend. */
+}
+
+static int METAL_QueueSetColorScale(SDL_Renderer *renderer, SDL_RenderCommand *cmd)
+{
+    const size_t vertlen = (2 * sizeof(float));
+    float *verts = (float *)SDL_AllocateRenderVertices(renderer, vertlen, DEVICE_ALIGN(8), &cmd->data.color.first);
+    if (!verts) {
+        return -1;
+    }
+
+    *verts++ = (float)SDL_RenderingLinearSpace(renderer);
+    *verts++ = cmd->data.color.color_scale;
+
     return 0;
 }
 
 static int METAL_QueueDrawPoints(SDL_Renderer *renderer, SDL_RenderCommand *cmd, const SDL_FPoint *points, int count)
 {
     SDL_FColor color = cmd->data.draw.color;
-    const float color_scale = cmd->data.draw.color_scale;
+    SDL_bool convert_color = SDL_RenderingLinearSpace(renderer);
 
     const size_t vertlen = (2 * sizeof(float) + 4 * sizeof(float)) * count;
     float *verts = (float *)SDL_AllocateRenderVertices(renderer, vertlen, DEVICE_ALIGN(8), &cmd->data.draw.first);
@@ -1059,9 +1087,9 @@ static int METAL_QueueDrawPoints(SDL_Renderer *renderer, SDL_RenderCommand *cmd,
     }
     cmd->data.draw.count = count;
 
-    color.r *= color_scale;
-    color.g *= color_scale;
-    color.b *= color_scale;
+    if (convert_color) {
+        SDL_ConvertToLinear(&color);
+    }
 
     for (int i = 0; i < count; i++, points++) {
         *(verts++) = points->x;
@@ -1077,7 +1105,7 @@ static int METAL_QueueDrawPoints(SDL_Renderer *renderer, SDL_RenderCommand *cmd,
 static int METAL_QueueDrawLines(SDL_Renderer *renderer, SDL_RenderCommand *cmd, const SDL_FPoint *points, int count)
 {
     SDL_FColor color = cmd->data.draw.color;
-    const float color_scale = cmd->data.draw.color_scale;
+    SDL_bool convert_color = SDL_RenderingLinearSpace(renderer);
     size_t vertlen;
     float *verts;
 
@@ -1090,9 +1118,9 @@ static int METAL_QueueDrawLines(SDL_Renderer *renderer, SDL_RenderCommand *cmd,
     }
     cmd->data.draw.count = count;
 
-    color.r *= color_scale;
-    color.g *= color_scale;
-    color.b *= color_scale;
+    if (convert_color) {
+        SDL_ConvertToLinear(&color);
+    }
 
     for (int i = 0; i < count; i++, points++) {
         *(verts++) = points->x;
@@ -1135,7 +1163,7 @@ static int METAL_QueueGeometry(SDL_Renderer *renderer, SDL_RenderCommand *cmd, S
                                int num_vertices, const void *indices, int num_indices, int size_indices,
                                float scale_x, float scale_y)
 {
-    const float color_scale = cmd->data.draw.color_scale;
+    SDL_bool convert_color = SDL_RenderingLinearSpace(renderer);
     int count = indices ? num_indices : num_vertices;
     const size_t vertlen = (2 * sizeof(float) + 4 * sizeof(float) + (texture ? 2 : 0) * sizeof(float)) * count;
     float *verts = (float *)SDL_AllocateRenderVertices(renderer, vertlen, DEVICE_ALIGN(8), &cmd->data.draw.first);
@@ -1149,7 +1177,7 @@ static int METAL_QueueGeometry(SDL_Renderer *renderer, SDL_RenderCommand *cmd, S
     for (int i = 0; i < count; i++) {
         int j;
         float *xy_;
-        SDL_FColor *col_;
+        SDL_FColor col_;
         if (size_indices == 4) {
             j = ((const Uint32 *)indices)[i];
         } else if (size_indices == 2) {
@@ -1165,12 +1193,16 @@ static int METAL_QueueGeometry(SDL_Renderer *renderer, SDL_RenderCommand *cmd, S
         *(verts++) = xy_[0] * scale_x;
         *(verts++) = xy_[1] * scale_y;
 
-        col_ = (SDL_FColor *)((char *)color + j * color_stride);
+        col_ = *(SDL_FColor *)((char *)color + j * color_stride);
 
-        *(verts++) = col_->r * color_scale;
-        *(verts++) = col_->g * color_scale;
-        *(verts++) = col_->b * color_scale;
-        *(verts++) = col_->a;
+        if (convert_color) {
+            SDL_ConvertToLinear(&col_);
+        }
+
+        *(verts++) = col_.r;
+        *(verts++) = col_.g;
+        *(verts++) = col_.b;
+        *(verts++) = col_.a;
 
         if (texture) {
             float *uv_ = (float *)((char *)uv + j * uv_stride);
@@ -1196,6 +1228,8 @@ static int METAL_QueueGeometry(SDL_Renderer *renderer, SDL_RenderCommand *cmd, S
     size_t projection_offset;
     SDL_bool color_dirty;
     size_t color_offset;
+    SDL_bool color_scale_dirty;
+    size_t color_scale_offset;
 } METAL_DrawStateCache;
 
 static SDL_bool SetDrawState(SDL_Renderer *renderer, const SDL_RenderCommand *cmd, const SDL_MetalFragmentFunction shader,
@@ -1256,10 +1290,17 @@ static SDL_bool SetDrawState(SDL_Renderer *renderer, const SDL_RenderCommand *cm
         statecache->cliprect_dirty = SDL_FALSE;
     }
 
+#if 0 /* Not used... */
     if (statecache->color_dirty) {
         [data.mtlcmdencoder setFragmentBufferOffset:statecache->color_offset atIndex:0];
         statecache->color_dirty = SDL_FALSE;
     }
+#endif
+
+    if (statecache->color_scale_dirty) {
+        [data.mtlcmdencoder setFragmentBufferOffset:statecache->color_scale_offset atIndex:0];
+        statecache->color_dirty = SDL_FALSE;
+    }
 
     newpipeline = ChoosePipelineState(data, data.activepipelines, shader, blend);
     if (newpipeline != statecache->pipeline) {
@@ -1381,6 +1422,13 @@ static int METAL_RunCommandQueue(SDL_Renderer *renderer, SDL_RenderCommand *cmd,
                 break;
             }
 
+            case SDL_RENDERCMD_SETCOLORSCALE:
+            {
+                statecache.color_scale_offset = cmd->data.color.first;
+                statecache.color_scale_dirty = SDL_TRUE;
+                break;
+            }
+
             case SDL_RENDERCMD_CLEAR:
             {
                 /* If we're already encoding a command buffer, dump it without committing it. We'd just
@@ -1404,15 +1452,19 @@ static int METAL_RunCommandQueue(SDL_Renderer *renderer, SDL_RenderCommand *cmd,
                 statecache.viewport_dirty = SDL_TRUE;
 
                 {
-                    const float r = cmd->data.color.color.r * cmd->data.color.color_scale;
-                    const float g = cmd->data.color.color.g * cmd->data.color.color_scale;
-                    const float b = cmd->data.color.color.b * cmd->data.color.color_scale;
-                    const float a = cmd->data.color.color.a;
-                    MTLClearColor color = MTLClearColorMake(r, g, b, a);
+                    SDL_bool convert_color = SDL_RenderingLinearSpace(renderer);
+                    SDL_FColor color = cmd->data.color.color;
+                    if (convert_color) {
+                        SDL_ConvertToLinear(&color);
+                    }
+                    color.r *= cmd->data.color.color_scale;
+                    color.g *= cmd->data.color.color_scale;
+                    color.b *= cmd->data.color.color_scale;
+                    MTLClearColor mtlcolor = MTLClearColorMake(color.r, color.g, color.b, color.a);
 
                     // get new command encoder, set up with an initial clear operation.
                     // (this might fail, and future draw operations will notice.)
-                    METAL_ActivateRenderCommandEncoder(renderer, MTLLoadActionClear, &color, mtlbufvertex);
+                    METAL_ActivateRenderCommandEncoder(renderer, MTLLoadActionClear, &mtlcolor, mtlbufvertex);
                 }
                 break;
             }
@@ -1502,7 +1554,22 @@ static int METAL_RunCommandQueue(SDL_Renderer *renderer, SDL_RenderCommand *cmd,
 
         mtlregion = MTLRegionMake2D(rect->x, rect->y, rect->w, rect->h);
 
-        format = (mtltexture.pixelFormat == MTLPixelFormatBGRA8Unorm) ? SDL_PIXELFORMAT_ARGB8888 : SDL_PIXELFORMAT_ABGR8888;
+        switch (mtltexture.pixelFormat) {
+        case MTLPixelFormatBGRA8Unorm:
+        case MTLPixelFormatBGRA8Unorm_sRGB:
+            format = SDL_PIXELFORMAT_ARGB8888;
+            break;
+        case MTLPixelFormatRGBA8Unorm:
+        case MTLPixelFormatRGBA8Unorm_sRGB:
+            format = SDL_PIXELFORMAT_ABGR8888;
+            break;
+        case MTLPixelFormatRGBA16Float:
+            format = SDL_PIXELFORMAT_RGBA64_FLOAT;
+            break;
+        default:
+            SDL_SetError("Unknown framebuffer pixel format");
+            return NULL;
+        }
         surface = SDL_CreateSurface(rect->w, rect->h, format);
         if (surface) {
             [mtltexture getBytes:surface->pixels bytesPerRow:surface->pitch fromRegion:mtlregion mipmapLevel:0];
@@ -1672,6 +1739,7 @@ static SDL_MetalView GetWindowView(SDL_Window *window)
         id<MTLBuffer> mtlbufconstantstaging, mtlbufquadindicesstaging, mtlbufconstants, mtlbufquadindices;
         id<MTLCommandBuffer> cmdbuffer;
         id<MTLBlitCommandEncoder> blitcmd;
+        SDL_bool scRGB_supported = SDL_FALSE;
 
         /* Note: matrices are column major. */
         float identitytransform[16] = {
@@ -1726,10 +1794,19 @@ static SDL_MetalView GetWindowView(SDL_Window *window)
 
         SDL_SetupRendererColorspace(renderer, create_props);
 
+#ifndef SDL_PLATFORM_TVOS
+        if (@available(macos 10.11, iOS 16.0, *)) {
+            scRGB_supported = SDL_TRUE;
+        }
+#endif
         if (renderer->output_colorspace != SDL_COLORSPACE_SRGB) {
-            SDL_SetError("Unsupported output colorspace");
-            SDL_free(renderer);
-            return NULL;
+            if (renderer->output_colorspace == SDL_COLORSPACE_SCRGB && scRGB_supported) {
+                /* This colorspace is supported */
+            } else {
+                SDL_SetError("Unsupported output colorspace");
+                SDL_free(renderer);
+                return NULL;
+            }
         }
 
 #ifdef SDL_PLATFORM_MACOS
@@ -1790,6 +1867,22 @@ in case we want to use it later (recreating the renderer)
         layer = (CAMetalLayer *)[(__bridge UIView *)view layer];
 #endif
 
+#ifndef SDL_PLATFORM_TVOS
+        if (renderer->output_colorspace == SDL_COLORSPACE_SCRGB) {
+            if (@available(macos 10.11, iOS 16.0, *)) {
+                layer.wantsExtendedDynamicRangeContent = YES;
+            } else {
+                SDL_assert(!"Logic error, scRGB is not actually supported");
+            }
+            layer.pixelFormat = MTLPixelFormatRGBA16Float;
+
+            const CFStringRef name = kCGColorSpaceExtendedLinearSRGB;
+            CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(name);
+            layer.colorspace = colorspace;
+            CGColorSpaceRelease(colorspace);
+        }
+#endif /* !SDL_PLATFORM_TVOS */
+
         layer.device = mtldevice;
 
         /* Necessary for RenderReadPixels. */
@@ -1889,7 +1982,8 @@ in case we want to use it later (recreating the renderer)
         renderer->SetTextureScaleMode = METAL_SetTextureScaleMode;
         renderer->SetRenderTarget = METAL_SetRenderTarget;
         renderer->QueueSetViewport = METAL_QueueSetViewport;
-        renderer->QueueSetDrawColor = METAL_QueueSetDrawColor;
+        renderer->QueueSetDrawColor = METAL_QueueNoOp;
+        renderer->QueueSetColorScale = METAL_QueueSetColorScale;
         renderer->QueueDrawPoints = METAL_QueueDrawPoints;
         renderer->QueueDrawLines = METAL_QueueDrawLines;
         renderer->QueueGeometry = METAL_QueueGeometry;
@@ -1964,9 +2058,11 @@ in case we want to use it later (recreating the renderer)
     {
         "metal",
         (SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC),
-        6,
+        8,
         { SDL_PIXELFORMAT_ARGB8888,
           SDL_PIXELFORMAT_ABGR8888,
+          SDL_PIXELFORMAT_RGBA64_FLOAT,
+          SDL_PIXELFORMAT_RGBA128_FLOAT,
           SDL_PIXELFORMAT_YV12,
           SDL_PIXELFORMAT_IYUV,
           SDL_PIXELFORMAT_NV12,
diff --git a/src/render/metal/SDL_shaders_metal.metal b/src/render/metal/SDL_shaders_metal.metal
index fd9e3a816581..d8f14c9f9298 100644
--- a/src/render/metal/SDL_shaders_metal.metal
+++ b/src/render/metal/SDL_shaders_metal.metal
@@ -1,8 +1,88 @@
+#include <metal_common>
 #include <metal_texture>
 #include <metal_matrix>
 
 using namespace metal;
 
+float3 scRGBtoNits(float3 v)
+{
+    return v * 80.0;
+}
+
+float3 scRGBfromNits(float3 v)
+{
+    return v / 80.0;
+}
+
+float sRGBtoLinear(float v)
+{
+    if (v <= 0.04045) {
+        v = (v / 12.92);
+    } else {
+        v = pow(abs(v + 0.055) / 1.055, 2.4);
+    }
+    return v;
+}
+
+float sRGBfromLinear(float v)
+{
+    if (v <= 0.0031308) {
+        v = (v * 12.92);
+    } else {
+        v = (pow(abs(v), 1.0 / 2.4) * 1.055 - 0.055);
+    }
+    return v;
+}
+
+float4 GetOutputColor(float4 rgba, float color_scale)
+{
+    float4 output;
+
+    output.rgb = rgba.rgb * color_scale;
+    output.a = rgba.a;
+
+    return output;
+}
+
+float4 GetOutputColorFromSRGB(float3 rgb, float scRGB_output, float color_scale)
+{
+    float4 output;
+
+    if (scRGB_output) {
+        rgb.r = sRGBtoLinear(rgb.r);
+        rgb.g = sRGBtoLinear(rgb.g);
+        rgb.b = sRGBtoLinear(rgb.b);
+    }
+
+    output.rgb = rgb * color_scale;
+    output.a = 1.0;
+
+    return output;
+}
+
+float4 GetOutputColorFromSCRGB(float3 rgb, float scRGB_output, float color_scale)
+{
+    float4 output;
+
+    output.rgb = rgb * color_scale;
+    output.a = 1.0;
+
+    if (!scRGB_output) {
+        output.r = sRGBfromLinear(output.r);
+        output.g = sRGBfromLinear(output.g);
+        output.b = sRGBfromLinear(output.b);
+        output.rgb = clamp(output.rgb, 0.0, 1.0);
+    }
+
+    return output;
+}
+
+struct ShaderConstants
+{
+    float scRGB_output;
+    float color_scale;
+};
+
 struct SolidVertexInput
 {
     float2 position [[attribute(0)]];
@@ -27,9 +107,10 @@ vertex SolidVertexOutput SDL_Solid_vertex(SolidVertexInput in [[stage_in]],
     return v;
 }
 
-fragment float4 SDL_Solid_fragment(SolidVertexInput in [[stage_in]])
+fragment float4 SDL_Solid_fragment(SolidVertexInput in [[stage_in]],
+                                   constant ShaderConstants &c [[buffer(0)]])
 {
-    return in.color;
+    return GetOutputColor(1.0, c.color_scale) * in.color;
 }
 
 struct CopyVertexInput
@@ -58,10 +139,11 @@ vertex CopyVertexOutput SDL_Copy_vertex(CopyVertexInput in [[stage_in]],
 }
 
 fragment float4 SDL_Copy_fragment(CopyVertexOutput vert [[stage_in]],
+                                  constant ShaderConstants &c [[buffer(0)]],
                                   texture2d<float> tex [[texture(0)]],
                                   sampler s [[sampler(0)]])
 {
-    return tex.sample(s, vert.texcoord) * vert.color;
+    return GetOutputColor(tex.sample(s, vert.texcoord), c.color_scale) * vert.color;
 }
 
 struct YUVDecode
@@ -73,6 +155,7 @@ struct YUVDecode
 };
 
 fragment float4 SDL_YUV_fragment(CopyVertexOutput vert [[stage_in]],
+                                 constant ShaderConstants &c [[buffer(0)]],
                                  constant YUVDecode &decode [[buffer(1)]],
                                  texture2d<float> texY [[texture(0)]],
                                  texture2d_array<float> texUV [[texture(1)]],
@@ -83,38 +166,52 @@ fragment float4 SDL_YUV_fragment(CopyVertexOutput vert [[stage_in]],
     yuv.y = texUV.sample(s, vert.texcoord, 0).r;
     yuv.z = texUV.sample(s, vert.texcoord, 1).r;
 
+    float3 rgb;
     yuv += decode.offset;
+    rgb.r = dot(yuv, decode.Rcoeff);
+    rgb.g = dot(yuv, decode.Gcoeff);
+    rgb.b = dot(yuv, decode.Bcoeff);
 
-    return vert.color * float4(dot(yuv, decode.Rcoeff), dot(yuv, decode.Gcoeff), dot(yuv, decode.Bcoeff), 1.0);
+    return GetOutputColorFromSRGB(rgb, c.scRGB_output, c.color_scale) * vert.color;
 }
 
 fragment float4 SDL_NV12_fragment(CopyVertexOutput vert [[stage_in]],
-                                 constant YUVDecode &decode [[buffer(1)]],
-                                 texture2d<float> texY [[texture(0)]],
-                                 texture2d<float> texUV [[texture(1)]],
-                                 sampler s [[sampler(0)]])
+                                  constant ShaderConstants &c [[buffer(0)]],
+                                  constant YUVDecode &decode [[buffer(1)]],
+                                  texture2d<float> texY [[texture(0)]],
+                                  texture2d<float> texUV [[texture(1)]],
+                                  sampler s [[sampler(0)]])
 {
     float3 yuv;
     yuv.x = texY.sample(s, vert.texcoord).r;
     yuv.yz = texUV.sample(s, vert.texcoord).rg;
 
+    float3 rgb;
     yuv += decode.offset;
+    rgb.r = dot(yuv, decode.Rcoeff);
+    rgb.g = dot(yuv, decode.Gcoeff);
+    rgb.b = dot(yuv, decode.Bcoeff);
 
-    return vert.color * float4(dot(yuv, decode.Rcoeff), dot(yuv, decode.Gcoeff), dot(yuv, decode.Bcoeff), 1.0);
+    return GetOutputColorFromSRGB(rgb, c.scRGB_output, c.color_scale) * vert.color;
 }
 
 fragment float4 SDL_NV21_fragment(CopyVertexOutput vert [[stage_in]],
-                                 constant YUVDecode &decode [[buffer(1)]],
-                                 texture2d<float> texY [[texture(0)]],
-                                 texture2d<float> texUV [[texture(1)]],
-                                 sampler s [[sampler(0)]])
+                                  constant ShaderConstants &c [[buffer(0)]],
+                                  constant YUVDecode &decode [[buffer(1)]],
+                                  texture2d<float> texY [[texture(0)

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