diff --git a/include/SDL3/SDL_surface.h b/include/SDL3/SDL_surface.h index 3f378a7d84379..6240d8abe603a 100644 --- a/include/SDL3/SDL_surface.h +++ b/include/SDL3/SDL_surface.h @@ -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. * diff --git a/src/video/SDL_blit.c b/src/video/SDL_blit.c index 56c61a19e35af..71854c8eac8b7 100644 --- a/src/video/SDL_blit.c +++ b/src/video/SDL_blit.c @@ -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; diff --git a/src/video/SDL_blit.h b/src/video/SDL_blit.h index be30fa64fa8f6..9679a8a86771a 100644 --- a/src/video/SDL_blit.h +++ b/src/video/SDL_blit.h @@ -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) { \ diff --git a/src/video/SDL_blit_slow.c b/src/video/SDL_blit_slow.c index 000c5419df7d9..5fdfe6f370bdc 100644 --- a/src/video/SDL_blit_slow.c +++ b/src/video/SDL_blit_slow.c @@ -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; + } +} diff --git a/src/video/SDL_blit_slow.h b/src/video/SDL_blit_slow.h index 40e1a44cdcdb2..d789aa8a3aa43 100644 --- a/src/video/SDL_blit_slow.h +++ b/src/video/SDL_blit_slow.h @@ -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_ */