Added HDR surface properties and tone mapping from HDR to SDR

This currently only supports PQ, but can be expanded in the future
main
Sam Lantinga 2024-01-23 16:59:19 -08:00
parent a71f99c71f
commit 7cd914593f
5 changed files with 391 additions and 18 deletions

View File

@ -137,6 +137,53 @@ typedef struct SDL_Surface
typedef int (SDLCALL *SDL_blit) (struct SDL_Surface *src, const SDL_Rect *srcrect,
struct SDL_Surface *dst, const SDL_Rect *dstrect);
/**
* The color primaries, as described by https://www.itu.int/rec/T-REC-H.273-201612-S/en
*/
typedef enum
{
SDL_COLOR_PRIMARIES_UNKNOWN = 0,
SDL_COLOR_PRIMARIES_BT709 = 1,
SDL_COLOR_PRIMARIES_IEC61966_2_4 = 1,
SDL_COLOR_PRIMARIES_UNSPECIFIED = 2,
SDL_COLOR_PRIMARIES_BT470M = 4,
SDL_COLOR_PRIMARIES_BT470BG = 5,
SDL_COLOR_PRIMARIES_BT601 = 6,
SDL_COLOR_PRIMARIES_SMPTE240 = 7,
SDL_COLOR_PRIMARIES_GENERIC_FILM = 8,
SDL_COLOR_PRIMARIES_BT2020 = 9,
SDL_COLOR_PRIMARIES_XYZ = 10,
SDL_COLOR_PRIMARIES_SMPTE431 = 11,
SDL_COLOR_PRIMARIES_SMPTE432 = 12, /* DCI P3 */
SDL_COLOR_PRIMARIES_EBU3213 = 22
} SDL_ColorPrimaries;
/**
* The transfer characteristics, as described by https://www.itu.int/rec/T-REC-H.273-201612-S/en
*/
typedef enum
{
SDL_TRANSFER_CHARACTERISTICS_UNKNOWN = 0,
SDL_TRANSFER_CHARACTERISTICS_BT709 = 1,
SDL_TRANSFER_CHARACTERISTICS_UNSPECIFIED = 2,
SDL_TRANSFER_CHARACTERISTICS_BT470M = 4, /* 2.2 gamma */
SDL_TRANSFER_CHARACTERISTICS_BT470BG = 5, /* 2.8 gamma */
SDL_TRANSFER_CHARACTERISTICS_BT601 = 6,
SDL_TRANSFER_CHARACTERISTICS_SMPTE240 = 7,
SDL_TRANSFER_CHARACTERISTICS_LINEAR = 8,
SDL_TRANSFER_CHARACTERISTICS_LOG100 = 9,
SDL_TRANSFER_CHARACTERISTICS_LOG100_SQRT10 = 10,
SDL_TRANSFER_CHARACTERISTICS_IEC61966 = 11,
SDL_TRANSFER_CHARACTERISTICS_BT1361 = 12,
SDL_TRANSFER_CHARACTERISTICS_SRGB = 13,
SDL_TRANSFER_CHARACTERISTICS_BT2020_10BIT = 14,
SDL_TRANSFER_CHARACTERISTICS_BT2020_12BIT = 15,
SDL_TRANSFER_CHARACTERISTICS_SMPTE2084 = 16, /* PQ */
SDL_TRANSFER_CHARACTERISTICS_SMPTE428 = 17,
SDL_TRANSFER_CHARACTERISTICS_HLG = 18
} SDL_TransferCharacteristics;
/**
* The formula used for converting between YUV and RGB
*/
@ -213,6 +260,14 @@ extern DECLSPEC void SDLCALL SDL_DestroySurface(SDL_Surface *surface);
/**
* Get the properties associated with a 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 SDL_TransferCharacteristics value describing the surface colorspace
* - `SDL_PROPERTY_SURFACE_MAXCLL_NUMBER`: MaxCLL (Maximum Content Light Level) indicates the maximum light level of any single pixel (in cd/m2 or nits) of the entire playback sequence. MaxCLL is usually measured off the final delivered content after mastering. If one uses the full light level of the HDR mastering display and adds a hard clip at its maximum value, MaxCLL would be equal to the peak luminance of the mastering monitor.
* - `SDL_PROPERTY_SURFACE_MAXFALL_NUMBER`: MaxFALL (Maximum Frame Average Light Level) indicates the maximum value of the frame average light level (in 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.
*
* \param surface the SDL_Surface structure to query
* \returns a valid property ID on success or 0 on failure; call
* SDL_GetError() for more information.
@ -224,6 +279,12 @@ 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"
#define SDL_PROPERTY_SURFACE_MAXFALL_NUMBER "SDL.surface.maxFALL"
/**
* Set the palette used by a surface.
*

View File

@ -183,12 +183,23 @@ static SDL_BlitFunc SDL_ChooseBlitFunc(Uint32 src_format, Uint32 dst_format, int
}
#endif /* SDL_HAVE_BLIT_AUTO */
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);
}
return SDL_FALSE;
}
/* Figure out which of many blit routines to set up on a surface */
int SDL_CalculateBlit(SDL_Surface *surface)
{
SDL_BlitFunc blit = NULL;
SDL_BlitMap *map = surface->map;
SDL_Surface *dst = map->dst;
SDL_bool src_HDR = IsSurfaceHDR(surface);
SDL_bool dst_HDR = IsSurfaceHDR(dst);
/* We don't currently support blitting to < 8 bpp surfaces */
if (dst->format->BitsPerPixel < 8) {
@ -219,33 +230,74 @@ int SDL_CalculateBlit(SDL_Surface *surface)
#endif
/* Choose a standard blit function */
if (map->identity && !(map->info.flags & ~SDL_COPY_RLE_DESIRED)) {
blit = SDL_BlitCopy;
} else if (surface->format->Rloss > 8 || dst->format->Rloss > 8) {
blit = SDL_Blit_Slow;
if (src_HDR || dst_HDR) {
if (src_HDR && dst_HDR) {
/* See if they're in the same colorspace and light level */
SDL_PropertiesID src_props = SDL_GetSurfaceProperties(surface);
SDL_PropertiesID dst_props = SDL_GetSurfaceProperties(dst);
if ((SDL_GetNumberProperty(src_props, SDL_PROPERTY_SURFACE_COLOR_PRIMARIES_NUMBER, SDL_COLOR_PRIMARIES_UNKNOWN) !=
SDL_GetNumberProperty(dst_props, SDL_PROPERTY_SURFACE_COLOR_PRIMARIES_NUMBER, SDL_COLOR_PRIMARIES_UNKNOWN)) ||
(SDL_GetNumberProperty(src_props, SDL_PROPERTY_SURFACE_TRANSFER_CHARACTERISTICS_NUMBER, SDL_TRANSFER_CHARACTERISTICS_UNKNOWN) !=
SDL_GetNumberProperty(dst_props, SDL_PROPERTY_SURFACE_TRANSFER_CHARACTERISTICS_NUMBER, SDL_TRANSFER_CHARACTERISTICS_UNKNOWN)) ||
(SDL_GetNumberProperty(src_props, SDL_PROPERTY_SURFACE_MAXCLL_NUMBER, 0) !=
SDL_GetNumberProperty(dst_props, SDL_PROPERTY_SURFACE_MAXCLL_NUMBER, 0)) ||
(SDL_GetNumberProperty(src_props, SDL_PROPERTY_SURFACE_MAXFALL_NUMBER, 0) !=
SDL_GetNumberProperty(dst_props, SDL_PROPERTY_SURFACE_MAXFALL_NUMBER, 0))) {
SDL_InvalidateMap(map);
return SDL_SetError("Tone mapping between HDR surfaces not supported");
}
/* Fall through to the normal blit calculation (is this correct?) */
} else if (dst_HDR) {
SDL_InvalidateMap(map);
return SDL_SetError("Tone mapping from an SDR to an HDR surface not supported");
} 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) {
if (SDL_ISPIXELFORMAT_10BIT(surface->format->format)) {
blit = SDL_Blit_Slow_PQtoSDR;
} else {
SDL_InvalidateMap(map);
return SDL_SetError("Surface has unknown HDR pixel format");
}
} else {
SDL_InvalidateMap(map);
return SDL_SetError("Surface has unknown HDR colorspace");
}
}
}
if (!blit) {
if (map->identity && !(map->info.flags & ~SDL_COPY_RLE_DESIRED)) {
blit = SDL_BlitCopy;
} else if (surface->format->Rloss > 8 || dst->format->Rloss > 8) {
blit = SDL_Blit_Slow;
}
#if SDL_HAVE_BLIT_0
else if (surface->format->BitsPerPixel < 8 &&
SDL_ISPIXELFORMAT_INDEXED(surface->format->format)) {
blit = SDL_CalculateBlit0(surface);
}
else if (surface->format->BitsPerPixel < 8 &&
SDL_ISPIXELFORMAT_INDEXED(surface->format->format)) {
blit = SDL_CalculateBlit0(surface);
}
#endif
#if SDL_HAVE_BLIT_1
else if (surface->format->BytesPerPixel == 1 &&
SDL_ISPIXELFORMAT_INDEXED(surface->format->format)) {
blit = SDL_CalculateBlit1(surface);
}
else if (surface->format->BytesPerPixel == 1 &&
SDL_ISPIXELFORMAT_INDEXED(surface->format->format)) {
blit = SDL_CalculateBlit1(surface);
}
#endif
#if SDL_HAVE_BLIT_A
else if (map->info.flags & SDL_COPY_BLEND) {
blit = SDL_CalculateBlitA(surface);
}
else if (map->info.flags & SDL_COPY_BLEND) {
blit = SDL_CalculateBlitA(surface);
}
#endif
#if SDL_HAVE_BLIT_N
else {
blit = SDL_CalculateBlitN(surface);
}
else {
blit = SDL_CalculateBlitN(surface);
}
#endif
}
#if SDL_HAVE_BLIT_AUTO
if (!blit) {
Uint32 src_format = surface->format->format;

View File

@ -360,6 +360,13 @@ extern SDL_BlitFunc SDL_CalculateBlitA(SDL_Surface *surface);
b = ((Pixel >> 2) & 0xFF); \
a = SDL_expand_byte[6][(Pixel >> 30)]; \
}
#define RGBAFLOAT_FROM_ARGB2101010(Pixel, r, g, b, a) \
{ \
r = (float)((Pixel >> 20) & 0x3FF) / 1023.0f; \
g = (float)((Pixel >> 10) & 0x3FF) / 1023.0f; \
b = (float)((Pixel >> 0) & 0x3FF) / 1023.0f; \
a = (float)SDL_expand_byte[6][(Pixel >> 30)] / 255.0f; \
}
#define RGBA_FROM_ABGR2101010(Pixel, r, g, b, a) \
{ \
r = ((Pixel >> 2) & 0xFF); \
@ -367,6 +374,13 @@ extern SDL_BlitFunc SDL_CalculateBlitA(SDL_Surface *surface);
b = ((Pixel >> 22) & 0xFF); \
a = SDL_expand_byte[6][(Pixel >> 30)]; \
}
#define RGBAFLOAT_FROM_ABGR2101010(Pixel, r, g, b, a) \
{ \
r = (float)((Pixel >> 0) & 0x3FF) / 1023.0f; \
g = (float)((Pixel >> 10) & 0x3FF) / 1023.0f; \
b = (float)((Pixel >> 20) & 0x3FF) / 1023.0f; \
a = (float)SDL_expand_byte[6][(Pixel >> 30)] / 255.0f; \
}
#define DISEMBLE_RGBA(buf, bpp, fmt, Pixel, r, g, b, a) \
do { \
switch (bpp) { \

View File

@ -235,3 +235,248 @@ void SDL_Blit_Slow(SDL_BlitInfo *info)
info->dst += info->dst_pitch;
}
}
static float PQtoNits(float pq)
{
const float c1 = 0.8359375f;
const float c2 = 18.8515625f;
const float c3 = 18.6875f;
const float oo_m1 = 1.0f / 0.1593017578125f;
const float oo_m2 = 1.0f / 78.84375f;
float num = SDL_max(SDL_powf(pq, oo_m2) - c1, 0.0f);
float den = c2 - c3 * pow(pq, oo_m2);
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)
{
float v[3];
int i;
v[0] = PQtoNits(floatR);
v[1] = PQtoNits(floatG);
v[2] = PQtoNits(floatB);
Convert2020to709(v);
for (i = 0; i < SDL_arraysize(v); ++i) {
v[i] /= 400.0f;
v[i] = SDL_clamp(v[i], 0.0f, 1.0f);
v[i] = SDL_powf(v[i], 1.0f / 2.2f);
}
*outR = (Uint32)(v[0] * 255);
*outG = (Uint32)(v[1] * 255);
*outB = (Uint32)(v[2] * 255);
}
void SDL_Blit_Slow_PQtoSDR(SDL_BlitInfo *info)
{
const int flags = info->flags;
const Uint32 modulateR = info->r;
const Uint32 modulateG = info->g;
const Uint32 modulateB = info->b;
const Uint32 modulateA = info->a;
Uint32 srcpixel;
float srcFloatR, srcFloatG, srcFloatB, srcFloatA;
Uint32 srcR, srcG, srcB, srcA;
Uint32 dstpixel;
Uint32 dstR, dstG, dstB, dstA;
Uint64 srcy, srcx;
Uint64 posy, posx;
Uint64 incy, incx;
SDL_PixelFormat *src_fmt = info->src_fmt;
SDL_PixelFormat *dst_fmt = info->dst_fmt;
int srcbpp = src_fmt->BytesPerPixel;
int dstbpp = dst_fmt->BytesPerPixel;
int dstfmt_val;
Uint32 rgbmask = ~src_fmt->Amask;
Uint32 ckey = info->colorkey & rgbmask;
dstfmt_val = detect_format(dst_fmt);
incy = ((Uint64)info->src_h << 16) / info->dst_h;
incx = ((Uint64)info->src_w << 16) / info->dst_w;
posy = incy / 2; /* start at the middle of pixel */
while (info->dst_h--) {
Uint8 *src = 0;
Uint8 *dst = info->dst;
int n = info->dst_w;
posx = incx / 2; /* start at the middle of pixel */
srcy = posy >> 16;
while (n--) {
srcx = posx >> 16;
src = (info->src + (srcy * info->src_pitch) + (srcx * srcbpp));
/* 10-bit pixel format */
srcpixel = *((Uint32 *)(src));
switch (src_fmt->format) {
case SDL_PIXELFORMAT_XRGB2101010:
RGBAFLOAT_FROM_ARGB2101010(srcpixel, srcFloatR, srcFloatG, srcFloatB, srcFloatA);
srcFloatA = 1.0f;
break;
case SDL_PIXELFORMAT_XBGR2101010:
RGBAFLOAT_FROM_ABGR2101010(srcpixel, srcFloatR, srcFloatG, srcFloatB, srcFloatA);
srcFloatA = 1.0f;
break;
case SDL_PIXELFORMAT_ARGB2101010:
RGBAFLOAT_FROM_ARGB2101010(srcpixel, srcFloatR, srcFloatG, srcFloatB, srcFloatA);
break;
case SDL_PIXELFORMAT_ABGR2101010:
RGBAFLOAT_FROM_ABGR2101010(srcpixel, srcFloatR, srcFloatG, srcFloatB, srcFloatA);
break;
default:
/* Unsupported pixel format */
srcFloatR = srcFloatG = srcFloatB = srcFloatA = 0.0f;
break;
}
PQtoSDR(srcFloatR, srcFloatG, srcFloatB, &srcR, &srcG, &srcB);
srcA = (Uint32)(srcFloatA * 255);
if (flags & SDL_COPY_COLORKEY) {
/* srcpixel isn't set for 24 bpp */
if (srcbpp == 3) {
srcpixel = (srcR << src_fmt->Rshift) |
(srcG << src_fmt->Gshift) | (srcB << src_fmt->Bshift);
}
if ((srcpixel & rgbmask) == ckey) {
posx += incx;
dst += dstbpp;
continue;
}
}
if ((flags & (SDL_COPY_BLEND | SDL_COPY_ADD | SDL_COPY_MOD | SDL_COPY_MUL))) {
if (FORMAT_HAS_ALPHA(dstfmt_val)) {
DISEMBLE_RGBA(dst, dstbpp, dst_fmt, dstpixel, dstR, dstG, dstB, dstA);
} else if (FORMAT_HAS_NO_ALPHA(dstfmt_val)) {
DISEMBLE_RGB(dst, dstbpp, dst_fmt, dstpixel, dstR, dstG, dstB);
dstA = 0xFF;
} else {
/* SDL_PIXELFORMAT_ARGB2101010 */
dstpixel = *((Uint32 *)(dst));
RGBA_FROM_ARGB2101010(dstpixel, dstR, dstG, dstB, dstA);
}
} else {
/* don't care */
dstR = dstG = dstB = dstA = 0;
}
if (flags & SDL_COPY_MODULATE_COLOR) {
srcR = (srcR * modulateR) / 255;
srcG = (srcG * modulateG) / 255;
srcB = (srcB * modulateB) / 255;
}
if (flags & SDL_COPY_MODULATE_ALPHA) {
srcA = (srcA * modulateA) / 255;
}
if (flags & (SDL_COPY_BLEND | SDL_COPY_ADD)) {
/* This goes away if we ever use premultiplied alpha */
if (srcA < 255) {
srcR = (srcR * srcA) / 255;
srcG = (srcG * srcA) / 255;
srcB = (srcB * srcA) / 255;
}
}
switch (flags & (SDL_COPY_BLEND | SDL_COPY_ADD | SDL_COPY_MOD | SDL_COPY_MUL)) {
case 0:
dstR = srcR;
dstG = srcG;
dstB = srcB;
dstA = srcA;
break;
case SDL_COPY_BLEND:
dstR = srcR + ((255 - srcA) * dstR) / 255;
dstG = srcG + ((255 - srcA) * dstG) / 255;
dstB = srcB + ((255 - srcA) * dstB) / 255;
dstA = srcA + ((255 - srcA) * dstA) / 255;
break;
case SDL_COPY_ADD:
dstR = srcR + dstR;
if (dstR > 255) {
dstR = 255;
}
dstG = srcG + dstG;
if (dstG > 255) {
dstG = 255;
}
dstB = srcB + dstB;
if (dstB > 255) {
dstB = 255;
}
break;
case SDL_COPY_MOD:
dstR = (srcR * dstR) / 255;
dstG = (srcG * dstG) / 255;
dstB = (srcB * dstB) / 255;
break;
case SDL_COPY_MUL:
dstR = ((srcR * dstR) + (dstR * (255 - srcA))) / 255;
if (dstR > 255) {
dstR = 255;
}
dstG = ((srcG * dstG) + (dstG * (255 - srcA))) / 255;
if (dstG > 255) {
dstG = 255;
}
dstB = ((srcB * dstB) + (dstB * (255 - srcA))) / 255;
if (dstB > 255) {
dstB = 255;
}
break;
}
if (FORMAT_HAS_ALPHA(dstfmt_val)) {
ASSEMBLE_RGBA(dst, dstbpp, dst_fmt, dstR, dstG, dstB, dstA);
} else if (FORMAT_HAS_NO_ALPHA(dstfmt_val)) {
ASSEMBLE_RGB(dst, dstbpp, dst_fmt, dstR, dstG, dstB);
} else {
/* 10-bit pixel format */
Uint32 pixel;
switch (dst_fmt->format) {
case SDL_PIXELFORMAT_XRGB2101010:
dstA = 0xFF;
SDL_FALLTHROUGH;
case SDL_PIXELFORMAT_ARGB2101010:
ARGB2101010_FROM_RGBA(pixel, dstR, dstG, dstB, dstA);
break;
case SDL_PIXELFORMAT_XBGR2101010:
dstA = 0xFF;
SDL_FALLTHROUGH;
case SDL_PIXELFORMAT_ABGR2101010:
ABGR2101010_FROM_RGBA(pixel, dstR, dstG, dstB, dstA);
break;
default:
pixel = 0;
break;
}
*(Uint32 *)dst = pixel;
}
posx += incx;
dst += dstbpp;
}
posy += incy;
info->dst += info->dst_pitch;
}
}

View File

@ -25,5 +25,6 @@
#include "SDL_internal.h"
extern void SDL_Blit_Slow(SDL_BlitInfo *info);
extern void SDL_Blit_Slow_PQtoSDR(SDL_BlitInfo *info);
#endif /* SDL_blit_slow_h_ */