From 19dde63e7c49c4716c342f2eaeaefafc39400450 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 1 Feb 2024 10:34:59 -0800
Subject: [PATCH] Added a simple linear scale for tonemapped HDR to SDR surface
conversion
---
include/SDL3/SDL_surface.h | 2 +
src/video/SDL_blit_slow.c | 81 ++++++++++++++++++++++++++------------
2 files changed, 58 insertions(+), 25 deletions(-)
diff --git a/include/SDL3/SDL_surface.h b/include/SDL3/SDL_surface.h
index 93e1e8b7b8c2..575949ac0604 100644
--- a/include/SDL3/SDL_surface.h
+++ b/include/SDL3/SDL_surface.h
@@ -232,6 +232,7 @@ extern DECLSPEC void SDLCALL SDL_DestroySurface(SDL_Surface *surface);
* cd/m2 or nits) of the entire playback sequence. MaxFALL is calculated by
* averaging the decoded luminance values of all the pixels within a frame.
* MaxFALL is usually much lower than MaxCLL.
+ * - `SDL_PROP_SURFACE_TONEMAP_OPERATOR_STRING`: the tone mapping operator used when converting between different colorspaces. Currently this supports the form "*=N", where N is a floating point scale factor applied in linear space.
*
* \param surface the SDL_Surface structure to query
* \returns a valid property ID on success or 0 on failure; call
@@ -247,6 +248,7 @@ extern DECLSPEC SDL_PropertiesID SDLCALL SDL_GetSurfaceProperties(SDL_Surface *s
#define SDL_PROP_SURFACE_COLORSPACE_NUMBER "SDL.surface.colorspace"
#define SDL_PROP_SURFACE_MAXCLL_NUMBER "SDL.surface.maxCLL"
#define SDL_PROP_SURFACE_MAXFALL_NUMBER "SDL.surface.maxFALL"
+#define SDL_PROP_SURFACE_TONEMAP_OPERATOR_STRING "SDL.surface.tonemap"
/**
* Set the colorspace used by a surface.
diff --git a/src/video/SDL_blit_slow.c b/src/video/SDL_blit_slow.c
index 424e09c17a28..c863bcd74e7f 100644
--- a/src/video/SDL_blit_slow.c
+++ b/src/video/SDL_blit_slow.c
@@ -664,12 +664,44 @@ static void WriteFloatPixel(Uint8 *pixels, SlowBlitPixelAccess access, SDL_Pixel
}
}
-static float CompressPQtoSDR(float v)
+typedef enum
+{
+ SDL_TONEMAP_NONE,
+ SDL_TONEMAP_LINEAR,
+} SDL_TonemapOperator;
+
+typedef struct
+{
+ SDL_TonemapOperator op;
+
+ union {
+ struct {
+ float scale;
+ } linear;
+ } data;
+
+} SDL_TonemapContext;
+
+static void ApplyTonemap(SDL_TonemapContext *ctx, float *r, float *g, float *b)
{
- /* This gives generally good results for PQ HDR -> SDR conversion, scaling from 400 nits to 80 nits,
- * which is scRGB 1.0
- */
- return (v / 5.0f);
+ switch (ctx->op) {
+ case SDL_TONEMAP_LINEAR:
+ *r *= ctx->data.linear.scale;
+ *g *= ctx->data.linear.scale;
+ *b *= ctx->data.linear.scale;
+ break;
+ default:
+ break;
+ }
+}
+
+static SDL_bool IsHDRColorspace(SDL_Colorspace colorspace)
+{
+ if (colorspace == SDL_COLORSPACE_SCRGB ||
+ SDL_COLORSPACETRANSFER(colorspace) == SDL_TRANSFER_CHARACTERISTICS_PQ) {
+ return SDL_TRUE;
+ }
+ return SDL_FALSE;
}
/* The SECOND TRUE BLITTER
@@ -694,29 +726,30 @@ void SDL_Blit_Slow_Float(SDL_BlitInfo *info)
SlowBlitPixelAccess src_access;
SlowBlitPixelAccess dst_access;
SDL_Colorspace src_colorspace;
- SDL_ColorPrimaries src_primaries;
- SDL_TransferCharacteristics src_transfer;
SDL_Colorspace dst_colorspace;
- SDL_ColorPrimaries dst_primaries;
- SDL_TransferCharacteristics dst_transfer;
- const float *color_primaries_matrix;
- SDL_bool compress_PQ = SDL_FALSE;
+ const float *color_primaries_matrix = NULL;
+ SDL_TonemapContext tonemap;
if (SDL_GetSurfaceColorspace(info->src_surface, &src_colorspace) < 0 ||
SDL_GetSurfaceColorspace(info->dst_surface, &dst_colorspace) < 0) {
return;
}
- src_primaries = SDL_COLORSPACEPRIMARIES(src_colorspace);
- src_transfer = SDL_COLORSPACETRANSFER(src_colorspace);
- dst_primaries = SDL_COLORSPACEPRIMARIES(dst_colorspace);
- dst_transfer = SDL_COLORSPACETRANSFER(dst_colorspace);
- color_primaries_matrix = SDL_GetColorPrimariesConversionMatrix(src_primaries, dst_primaries);
-
- if (src_transfer == SDL_TRANSFER_CHARACTERISTICS_PQ &&
- dst_transfer != SDL_TRANSFER_CHARACTERISTICS_PQ &&
- dst_colorspace != SDL_COLORSPACE_SCRGB) {
- //compress_PQ = SDL_TRUE;
+ tonemap.op = SDL_TONEMAP_NONE;
+ if (src_colorspace != dst_colorspace) {
+ SDL_ColorPrimaries src_primaries = SDL_COLORSPACEPRIMARIES(src_colorspace);
+ SDL_ColorPrimaries dst_primaries = SDL_COLORSPACEPRIMARIES(dst_colorspace);
+ color_primaries_matrix = SDL_GetColorPrimariesConversionMatrix(src_primaries, dst_primaries);
+
+ if (IsHDRColorspace(src_colorspace) != IsHDRColorspace(dst_colorspace)) {
+ const char *tonemap_operator = SDL_GetStringProperty(SDL_GetSurfaceProperties(info->src_surface), SDL_PROP_SURFACE_TONEMAP_OPERATOR_STRING, NULL);
+ if (tonemap_operator) {
+ if (SDL_strncmp(tonemap_operator, "*=", 2) == 0) {
+ tonemap.op = SDL_TONEMAP_LINEAR;
+ tonemap.data.linear.scale = SDL_atof(tonemap_operator + 2);
+ }
+ }
+ }
}
src_access = GetPixelAccessMethod(src_fmt);
@@ -742,10 +775,8 @@ void SDL_Blit_Slow_Float(SDL_BlitInfo *info)
SDL_ConvertColorPrimaries(&srcR, &srcG, &srcB, color_primaries_matrix);
}
- if (compress_PQ) {
- srcR = CompressPQtoSDR(srcR);
- srcG = CompressPQtoSDR(srcG);
- srcB = CompressPQtoSDR(srcB);
+ if (tonemap.op) {
+ ApplyTonemap(&tonemap, &srcR, &srcG, &srcB);
}
if (flags & SDL_COPY_COLORKEY) {