/*
* This file is part of libplacebo.
*
* libplacebo is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* libplacebo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with libplacebo. If not, see .
*/
#include
#include "cache.h"
#include "shaders.h"
#include
// Common constants for SMPTE ST.2084 (PQ)
static const float PQ_M1 = 2610./4096 * 1./4,
PQ_M2 = 2523./4096 * 128,
PQ_C1 = 3424./4096,
PQ_C2 = 2413./4096 * 32,
PQ_C3 = 2392./4096 * 32;
// Common constants for ARIB STD-B67 (HLG)
static const float HLG_A = 0.17883277,
HLG_B = 0.28466892,
HLG_C = 0.55991073,
HLG_REF = 1000.0 / PL_COLOR_SDR_WHITE;
// Common constants for Panasonic V-Log
static const float VLOG_B = 0.00873,
VLOG_C = 0.241514,
VLOG_D = 0.598206;
// Common constants for Sony S-Log
static const float SLOG_A = 0.432699,
SLOG_B = 0.037584,
SLOG_C = 0.616596 + 0.03,
SLOG_P = 3.538813,
SLOG_Q = 0.030001,
SLOG_K2 = 155.0 / 219.0;
void pl_shader_set_alpha(pl_shader sh, struct pl_color_repr *repr,
enum pl_alpha_mode mode)
{
if (repr->alpha == PL_ALPHA_PREMULTIPLIED && mode == PL_ALPHA_INDEPENDENT) {
GLSL("if (color.a > 1e-6) \n"
" color.rgb /= vec3(color.a); \n");
repr->alpha = PL_ALPHA_INDEPENDENT;
}
if (repr->alpha == PL_ALPHA_INDEPENDENT && mode == PL_ALPHA_PREMULTIPLIED) {
GLSL("color.rgb *= vec3(color.a); \n");
repr->alpha = PL_ALPHA_PREMULTIPLIED;
}
}
#ifdef PL_HAVE_DOVI
static inline void reshape_mmr(pl_shader sh, ident_t mmr, bool single,
int min_order, int max_order)
{
if (single) {
GLSL("const uint mmr_idx = 0u; \n");
} else {
GLSL("uint mmr_idx = uint(coeffs.y); \n");
}
assert(min_order <= max_order);
if (min_order < max_order)
GLSL("uint order = uint(coeffs.w); \n");
GLSL("vec4 sigX; \n"
"s = coeffs.x; \n"
"sigX.xyz = sig.xxy * sig.yzz; \n"
"sigX.w = sigX.x * sig.z; \n"
"s += dot("$"[mmr_idx + 0].xyz, sig); \n"
"s += dot("$"[mmr_idx + 1], sigX); \n",
mmr, mmr);
if (max_order >= 2) {
if (min_order < 2)
GLSL("if (order >= 2) { \n");
GLSL("vec3 sig2 = sig * sig; \n"
"vec4 sigX2 = sigX * sigX; \n"
"s += dot("$"[mmr_idx + 2].xyz, sig2); \n"
"s += dot("$"[mmr_idx + 3], sigX2); \n",
mmr, mmr);
if (max_order == 3) {
if (min_order < 3)
GLSL("if (order >= 3 { \n");
GLSL("s += dot("$"[mmr_idx + 4].xyz, sig2 * sig); \n"
"s += dot("$"[mmr_idx + 5], sigX2 * sigX); \n",
mmr, mmr);
if (min_order < 3)
GLSL("} \n");
}
if (min_order < 2)
GLSL("} \n");
}
}
static inline void reshape_poly(pl_shader sh)
{
GLSL("s = (coeffs.z * s + coeffs.y) * s + coeffs.x; \n");
}
#endif
void pl_shader_dovi_reshape(pl_shader sh, const struct pl_dovi_metadata *data)
{
#ifdef PL_HAVE_DOVI
if (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0) || !data)
return;
sh_describe(sh, "reshaping");
GLSL("// pl_shader_reshape \n"
"{ \n"
"vec3 sig; \n"
"vec4 coeffs; \n"
"float s; \n"
"sig = clamp(color.rgb, 0.0, 1.0); \n");
float coeffs_data[8][4];
float mmr_packed_data[8*6][4];
for (int c = 0; c < 3; c++) {
const struct pl_reshape_data *comp = &data->comp[c];
if (!comp->num_pivots)
continue;
pl_assert(comp->num_pivots >= 2 && comp->num_pivots <= 9);
GLSL("s = sig[%d]; \n", c);
// Prepare coefficients for GPU
bool has_poly = false, has_mmr = false, mmr_single = true;
int mmr_idx = 0, min_order = 3, max_order = 1;
memset(coeffs_data, 0, sizeof(coeffs_data));
for (int i = 0; i < comp->num_pivots - 1; i++) {
switch (comp->method[i]) {
case 0: // polynomial
has_poly = true;
coeffs_data[i][3] = 0.0; // order=0 signals polynomial
for (int k = 0; k < 3; k++)
coeffs_data[i][k] = comp->poly_coeffs[i][k];
break;
case 1:
min_order = PL_MIN(min_order, comp->mmr_order[i]);
max_order = PL_MAX(max_order, comp->mmr_order[i]);
mmr_single = !has_mmr;
has_mmr = true;
coeffs_data[i][3] = (float) comp->mmr_order[i];
coeffs_data[i][0] = comp->mmr_constant[i];
coeffs_data[i][1] = (float) mmr_idx;
for (int j = 0; j < comp->mmr_order[i]; j++) {
// store weights per order as two packed vec4s
float *mmr = &mmr_packed_data[mmr_idx][0];
mmr[0] = comp->mmr_coeffs[i][j][0];
mmr[1] = comp->mmr_coeffs[i][j][1];
mmr[2] = comp->mmr_coeffs[i][j][2];
mmr[3] = 0.0; // unused
mmr[4] = comp->mmr_coeffs[i][j][3];
mmr[5] = comp->mmr_coeffs[i][j][4];
mmr[6] = comp->mmr_coeffs[i][j][5];
mmr[7] = comp->mmr_coeffs[i][j][6];
mmr_idx += 2;
}
break;
default:
pl_unreachable();
}
}
if (comp->num_pivots > 2) {
// Skip the (irrelevant) lower and upper bounds
float pivots_data[7];
memcpy(pivots_data, comp->pivots + 1,
(comp->num_pivots - 2) * sizeof(pivots_data[0]));
// Fill the remainder with a quasi-infinite sentinel pivot
for (int i = comp->num_pivots - 2; i < PL_ARRAY_SIZE(pivots_data); i++)
pivots_data[i] = 1e9f;
ident_t pivots = sh_var(sh, (struct pl_shader_var) {
.data = pivots_data,
.var = {
.name = "pivots",
.type = PL_VAR_FLOAT,
.dim_v = 1,
.dim_m = 1,
.dim_a = PL_ARRAY_SIZE(pivots_data),
},
});
ident_t coeffs = sh_var(sh, (struct pl_shader_var) {
.data = coeffs_data,
.var = {
.name = "coeffs",
.type = PL_VAR_FLOAT,
.dim_v = 4,
.dim_m = 1,
.dim_a = PL_ARRAY_SIZE(coeffs_data),
},
});
// Efficiently branch into the correct set of coefficients
GLSL("#define test(i) bvec4(s >= "$"[i]) \n"
"#define coef(i) "$"[i] \n"
"coeffs = mix(mix(mix(coef(0), coef(1), test(0)), \n"
" mix(coef(2), coef(3), test(2)), \n"
" test(1)), \n"
" mix(mix(coef(4), coef(5), test(4)), \n"
" mix(coef(6), coef(7), test(6)), \n"
" test(5)), \n"
" test(3)); \n"
"#undef test \n"
"#undef coef \n",
pivots, coeffs);
} else {
// No need for a single pivot, just set the coeffs directly
GLSL("coeffs = "$"; \n", sh_var(sh, (struct pl_shader_var) {
.var = pl_var_vec4("coeffs"),
.data = coeffs_data,
}));
}
ident_t mmr = NULL_IDENT;
if (has_mmr) {
mmr = sh_var(sh, (struct pl_shader_var) {
.data = mmr_packed_data,
.var = {
.name = "mmr",
.type = PL_VAR_FLOAT,
.dim_v = 4,
.dim_m = 1,
.dim_a = mmr_idx,
},
});
}
if (has_mmr && has_poly) {
GLSL("if (coeffs.w == 0.0) { \n");
reshape_poly(sh);
GLSL("} else { \n");
reshape_mmr(sh, mmr, mmr_single, min_order, max_order);
GLSL("} \n");
} else if (has_poly) {
reshape_poly(sh);
} else {
assert(has_mmr);
GLSL("{ \n");
reshape_mmr(sh, mmr, mmr_single, min_order, max_order);
GLSL("} \n");
}
ident_t lo = sh_var(sh, (struct pl_shader_var) {
.var = pl_var_float("lo"),
.data = &comp->pivots[0],
});
ident_t hi = sh_var(sh, (struct pl_shader_var) {
.var = pl_var_float("hi"),
.data = &comp->pivots[comp->num_pivots - 1],
});
GLSL("color[%d] = clamp(s, "$", "$"); \n", c, lo, hi);
}
GLSL("} \n");
#else
SH_FAIL(sh, "libplacebo was compiled without support for dolbyvision reshaping");
#endif
}
void pl_shader_decode_color(pl_shader sh, struct pl_color_repr *repr,
const struct pl_color_adjustment *params)
{
if (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0))
return;
sh_describe(sh, "color decoding");
GLSL("// pl_shader_decode_color \n"
"{ \n");
// Do this first because the following operations are potentially nonlinear
pl_shader_set_alpha(sh, repr, PL_ALPHA_INDEPENDENT);
if (repr->sys == PL_COLOR_SYSTEM_XYZ ||
repr->sys == PL_COLOR_SYSTEM_DOLBYVISION)
{
ident_t scale = SH_FLOAT(pl_color_repr_normalize(repr));
GLSL("color.rgb *= vec3("$"); \n", scale);
}
if (repr->sys == PL_COLOR_SYSTEM_XYZ) {
pl_shader_linearize(sh, &(struct pl_color_space) {
.transfer = PL_COLOR_TRC_ST428,
});
}
if (repr->sys == PL_COLOR_SYSTEM_DOLBYVISION)
pl_shader_dovi_reshape(sh, repr->dovi);
enum pl_color_system orig_sys = repr->sys;
pl_transform3x3 tr = pl_color_repr_decode(repr, params);
if (memcmp(&tr, &pl_transform3x3_identity, sizeof(tr))) {
ident_t cmat = sh_var(sh, (struct pl_shader_var) {
.var = pl_var_mat3("cmat"),
.data = PL_TRANSPOSE_3X3(tr.mat.m),
});
ident_t cmat_c = sh_var(sh, (struct pl_shader_var) {
.var = pl_var_vec3("cmat_c"),
.data = tr.c,
});
GLSL("color.rgb = "$" * color.rgb + "$"; \n", cmat, cmat_c);
}
switch (orig_sys) {
case PL_COLOR_SYSTEM_BT_2020_C:
// Conversion for C'rcY'cC'bc via the BT.2020 CL system:
// C'bc = (B'-Y'c) / 1.9404 | C'bc <= 0
// = (B'-Y'c) / 1.5816 | C'bc > 0
//
// C'rc = (R'-Y'c) / 1.7184 | C'rc <= 0
// = (R'-Y'c) / 0.9936 | C'rc > 0
//
// as per the BT.2020 specification, table 4. This is a non-linear
// transformation because (constant) luminance receives non-equal
// contributions from the three different channels.
GLSL("// constant luminance conversion \n"
"color.br = color.br * mix(vec2(1.5816, 0.9936), \n"
" vec2(1.9404, 1.7184), \n"
" lessThanEqual(color.br, vec2(0.0))) \n"
" + color.gg; \n");
// Expand channels to camera-linear light. This shader currently just
// assumes everything uses the BT.2020 12-bit gamma function, since the
// difference between 10 and 12-bit is negligible for anything other
// than 12-bit content.
GLSL("vec3 lin = mix(color.rgb * vec3(1.0/4.5), \n"
" pow((color.rgb + vec3(0.0993))*vec3(1.0/1.0993), \n"
" vec3(1.0/0.45)), \n"
" lessThanEqual(vec3(0.08145), color.rgb)); \n");
// Calculate the green channel from the expanded RYcB, and recompress to G'
// The BT.2020 specification says Yc = 0.2627*R + 0.6780*G + 0.0593*B
GLSL("color.g = (lin.g - 0.2627*lin.r - 0.0593*lin.b)*1.0/0.6780; \n"
"color.g = mix(color.g * 4.5, \n"
" 1.0993 * pow(color.g, 0.45) - 0.0993, \n"
" 0.0181 <= color.g); \n");
break;
case PL_COLOR_SYSTEM_BT_2100_PQ:;
// Conversion process from the spec:
//
// 1. L'M'S' = cmat * ICtCp
// 2. LMS = linearize(L'M'S') (EOTF for PQ, inverse OETF for HLG)
// 3. RGB = lms2rgb * LMS
//
// After this we need to invert step 2 to arrive at non-linear RGB.
// (It's important we keep the transfer function conversion separate
// from the color system decoding, so we have to partially undo our
// work here even though we will end up linearizing later on anyway)
GLSL(// PQ EOTF
"color.rgb = pow(max(color.rgb, 0.0), vec3(1.0/%f)); \n"
"color.rgb = max(color.rgb - vec3(%f), 0.0) \n"
" / (vec3(%f) - vec3(%f) * color.rgb); \n"
"color.rgb = pow(color.rgb, vec3(1.0/%f)); \n"
// LMS matrix
"color.rgb = mat3( 3.43661, -0.79133, -0.0259499, \n"
" -2.50645, 1.98360, -0.0989137, \n"
" 0.06984, -0.192271, 1.12486) * color.rgb; \n"
// PQ OETF
"color.rgb = pow(max(color.rgb, 0.0), vec3(%f)); \n"
"color.rgb = (vec3(%f) + vec3(%f) * color.rgb) \n"
" / (vec3(1.0) + vec3(%f) * color.rgb); \n"
"color.rgb = pow(color.rgb, vec3(%f)); \n",
PQ_M2, PQ_C1, PQ_C2, PQ_C3, PQ_M1,
PQ_M1, PQ_C1, PQ_C2, PQ_C3, PQ_M2);
break;
case PL_COLOR_SYSTEM_BT_2100_HLG:
GLSL(// HLG OETF^-1
"color.rgb = mix(vec3(4.0) * color.rgb * color.rgb, \n"
" exp((color.rgb - vec3(%f)) * vec3(1.0/%f)) \n"
" + vec3(%f), \n"
" lessThan(vec3(0.5), color.rgb)); \n"
// LMS matrix
"color.rgb = mat3( 3.43661, -0.79133, -0.0259499, \n"
" -2.50645, 1.98360, -0.0989137, \n"
" 0.06984, -0.192271, 1.12486) * color.rgb; \n"
// HLG OETF
"color.rgb = mix(vec3(0.5) * sqrt(color.rgb), \n"
" vec3(%f) * log(color.rgb - vec3(%f)) + vec3(%f), \n"
" lessThan(vec3(1.0), color.rgb)); \n",
HLG_C, HLG_A, HLG_B,
HLG_A, HLG_B, HLG_C);
break;
case PL_COLOR_SYSTEM_DOLBYVISION:;
#ifdef PL_HAVE_DOVI
// Dolby Vision always outputs BT.2020-referred HPE LMS, so hard-code
// the inverse LMS->RGB matrix corresponding to this color space.
pl_matrix3x3 dovi_lms2rgb = {{
{ 3.06441879, -2.16597676, 0.10155818},
{-0.65612108, 1.78554118, -0.12943749},
{ 0.01736321, -0.04725154, 1.03004253},
}};
pl_matrix3x3_mul(&dovi_lms2rgb, &repr->dovi->linear);
ident_t mat = sh_var(sh, (struct pl_shader_var) {
.var = pl_var_mat3("lms2rgb"),
.data = PL_TRANSPOSE_3X3(dovi_lms2rgb.m),
});
// PQ EOTF
GLSL("color.rgb = pow(max(color.rgb, 0.0), vec3(1.0/%f)); \n"
"color.rgb = max(color.rgb - vec3(%f), 0.0) \n"
" / (vec3(%f) - vec3(%f) * color.rgb); \n"
"color.rgb = pow(color.rgb, vec3(1.0/%f)); \n",
PQ_M2, PQ_C1, PQ_C2, PQ_C3, PQ_M1);
// LMS matrix
GLSL("color.rgb = "$" * color.rgb; \n", mat);
// PQ OETF
GLSL("color.rgb = pow(max(color.rgb, 0.0), vec3(%f)); \n"
"color.rgb = (vec3(%f) + vec3(%f) * color.rgb) \n"
" / (vec3(1.0) + vec3(%f) * color.rgb); \n"
"color.rgb = pow(color.rgb, vec3(%f)); \n",
PQ_M1, PQ_C1, PQ_C2, PQ_C3, PQ_M2);
break;
#else
SH_FAIL(sh, "libplacebo was compiled without support for dolbyvision reshaping");
return;
#endif
case PL_COLOR_SYSTEM_UNKNOWN:
case PL_COLOR_SYSTEM_RGB:
case PL_COLOR_SYSTEM_XYZ:
case PL_COLOR_SYSTEM_BT_601:
case PL_COLOR_SYSTEM_BT_709:
case PL_COLOR_SYSTEM_SMPTE_240M:
case PL_COLOR_SYSTEM_BT_2020_NC:
case PL_COLOR_SYSTEM_YCGCO:
break; // no special post-processing needed
case PL_COLOR_SYSTEM_COUNT:
pl_unreachable();
}
// Gamma adjustment. Doing this here (in non-linear light) is technically
// somewhat wrong, but this is just an aesthetic parameter and not really
// meant for colorimetric precision, so we don't care too much.
if (params && params->gamma == 0) {
// Avoid division by zero
GLSL("color.rgb = vec3(0.0); \n");
} else if (params && params->gamma != 1) {
ident_t gamma = sh_var(sh, (struct pl_shader_var) {
.var = pl_var_float("gamma"),
.data = &(float){ 1 / params->gamma },
});
GLSL("color.rgb = pow(max(color.rgb, vec3(0.0)), vec3("$")); \n", gamma);
}
GLSL("}\n");
}
void pl_shader_encode_color(pl_shader sh, const struct pl_color_repr *repr)
{
if (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0))
return;
sh_describe(sh, "color encoding");
GLSL("// pl_shader_encode_color \n"
"{ \n");
switch (repr->sys) {
case PL_COLOR_SYSTEM_BT_2020_C:
// Expand R'G'B' to RGB
GLSL("vec3 lin = mix(color.rgb * vec3(1.0/4.5), \n"
" pow((color.rgb + vec3(0.0993))*vec3(1.0/1.0993), \n"
" vec3(1.0/0.45)), \n"
" lessThanEqual(vec3(0.08145), color.rgb)); \n");
// Compute Yc from RGB and compress to R'Y'cB'
GLSL("color.g = dot(vec3(0.2627, 0.6780, 0.0593), lin); \n"
"color.g = mix(color.g * 4.5, \n"
" 1.0993 * pow(color.g, 0.45) - 0.0993, \n"
" 0.0181 <= color.g); \n");
// Compute C'bc and C'rc into color.br
GLSL("color.br = color.br - color.gg; \n"
"color.br *= mix(vec2(1.0/1.5816, 1.0/0.9936), \n"
" vec2(1.0/1.9404, 1.0/1.7184), \n"
" lessThanEqual(color.br, vec2(0.0))); \n");
break;
case PL_COLOR_SYSTEM_BT_2100_PQ:;
GLSL("color.rgb = pow(max(color.rgb, 0.0), vec3(1.0/%f)); \n"
"color.rgb = max(color.rgb - vec3(%f), 0.0) \n"
" / (vec3(%f) - vec3(%f) * color.rgb); \n"
"color.rgb = pow(color.rgb, vec3(1.0/%f)); \n"
"color.rgb = mat3(0.412109, 0.166748, 0.024170, \n"
" 0.523925, 0.720459, 0.075440, \n"
" 0.063965, 0.112793, 0.900394) * color.rgb; \n"
"color.rgb = pow(color.rgb, vec3(%f)); \n"
"color.rgb = (vec3(%f) + vec3(%f) * color.rgb) \n"
" / (vec3(1.0) + vec3(%f) * color.rgb); \n"
"color.rgb = pow(color.rgb, vec3(%f)); \n",
PQ_M2, PQ_C1, PQ_C2, PQ_C3, PQ_M1,
PQ_M1, PQ_C1, PQ_C2, PQ_C3, PQ_M2);
break;
case PL_COLOR_SYSTEM_BT_2100_HLG:
GLSL("color.rgb = mix(vec3(4.0) * color.rgb * color.rgb, \n"
" exp((color.rgb - vec3(%f)) * vec3(1.0/%f)) \n"
" + vec3(%f), \n"
" lessThan(vec3(0.5), color.rgb)); \n"
"color.rgb = mat3(0.412109, 0.166748, 0.024170, \n"
" 0.523925, 0.720459, 0.075440, \n"
" 0.063965, 0.112793, 0.900394) * color.rgb; \n"
"color.rgb = mix(vec3(0.5) * sqrt(color.rgb), \n"
" vec3(%f) * log(color.rgb - vec3(%f)) + vec3(%f), \n"
" lessThan(vec3(1.0), color.rgb)); \n",
HLG_C, HLG_A, HLG_B,
HLG_A, HLG_B, HLG_C);
break;
case PL_COLOR_SYSTEM_DOLBYVISION:
SH_FAIL(sh, "Cannot un-apply dolbyvision yet (no inverse reshaping)!");
return;
case PL_COLOR_SYSTEM_UNKNOWN:
case PL_COLOR_SYSTEM_RGB:
case PL_COLOR_SYSTEM_XYZ:
case PL_COLOR_SYSTEM_BT_601:
case PL_COLOR_SYSTEM_BT_709:
case PL_COLOR_SYSTEM_SMPTE_240M:
case PL_COLOR_SYSTEM_BT_2020_NC:
case PL_COLOR_SYSTEM_YCGCO:
break; // no special pre-processing needed
case PL_COLOR_SYSTEM_COUNT:
pl_unreachable();
}
// Since this is a relatively rare operation, bypass it as much as possible
bool skip = true;
skip &= PL_DEF(repr->sys, PL_COLOR_SYSTEM_RGB) == PL_COLOR_SYSTEM_RGB;
skip &= PL_DEF(repr->levels, PL_COLOR_LEVELS_FULL) == PL_COLOR_LEVELS_FULL;
skip &= !repr->bits.sample_depth || !repr->bits.color_depth ||
repr->bits.sample_depth == repr->bits.color_depth;
skip &= !repr->bits.bit_shift;
if (!skip) {
struct pl_color_repr copy = *repr;
ident_t xyzscale = NULL_IDENT;
if (repr->sys == PL_COLOR_SYSTEM_XYZ)
xyzscale = SH_FLOAT(1.0 / pl_color_repr_normalize(©));
pl_transform3x3 tr = pl_color_repr_decode(©, NULL);
pl_transform3x3_invert(&tr);
ident_t cmat = sh_var(sh, (struct pl_shader_var) {
.var = pl_var_mat3("cmat"),
.data = PL_TRANSPOSE_3X3(tr.mat.m),
});
ident_t cmat_c = sh_var(sh, (struct pl_shader_var) {
.var = pl_var_vec3("cmat_c"),
.data = tr.c,
});
GLSL("color.rgb = "$" * color.rgb + "$"; \n", cmat, cmat_c);
if (repr->sys == PL_COLOR_SYSTEM_XYZ) {
pl_shader_delinearize(sh, &(struct pl_color_space) {
.transfer = PL_COLOR_TRC_ST428,
});
GLSL("color.rgb *= vec3("$"); \n", xyzscale);
}
}
if (repr->alpha == PL_ALPHA_PREMULTIPLIED)
GLSL("color.rgb *= vec3(color.a); \n");
GLSL("}\n");
}
static ident_t sh_luma_coeffs(pl_shader sh, const struct pl_color_space *csp)
{
pl_matrix3x3 rgb2xyz;
rgb2xyz = pl_get_rgb2xyz_matrix(pl_raw_primaries_get(csp->primaries));
// FIXME: Cannot use `const vec3` due to glslang bug #2025
ident_t coeffs = sh_fresh(sh, "luma_coeffs");
GLSLH("#define "$" vec3("$", "$", "$") \n", coeffs,
SH_FLOAT(rgb2xyz.m[1][0]), // RGB->Y vector
SH_FLOAT(rgb2xyz.m[1][1]),
SH_FLOAT(rgb2xyz.m[1][2]));
return coeffs;
}
void pl_shader_linearize(pl_shader sh, const struct pl_color_space *csp)
{
if (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0))
return;
if (csp->transfer == PL_COLOR_TRC_LINEAR)
return;
float csp_min, csp_max;
pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
.color = csp,
.metadata = PL_HDR_METADATA_HDR10,
.scaling = PL_HDR_NORM,
.out_min = &csp_min,
.out_max = &csp_max,
));
// Note that this clamp may technically violate the definition of
// ITU-R BT.2100, which allows for sub-blacks and super-whites to be
// displayed on the display where such would be possible. That said, the
// problem is that not all gamma curves are well-defined on the values
// outside this range, so we ignore it and just clamp anyway for sanity.
GLSL("// pl_shader_linearize \n"
"color.rgb = max(color.rgb, 0.0); \n");
switch (csp->transfer) {
case PL_COLOR_TRC_SRGB:
GLSL("color.rgb = mix(color.rgb * vec3(1.0/12.92), \n"
" pow((color.rgb + vec3(0.055))/vec3(1.055), \n"
" vec3(2.4)), \n"
" lessThan(vec3(0.04045), color.rgb)); \n");
goto scale_out;
case PL_COLOR_TRC_BT_1886: {
const float lb = powf(csp_min, 1/2.4f);
const float lw = powf(csp_max, 1/2.4f);
const float a = powf(lw - lb, 2.4f);
const float b = lb / (lw - lb);
GLSL("color.rgb = "$" * pow(color.rgb + vec3("$"), vec3(2.4)); \n",
SH_FLOAT(a), SH_FLOAT(b));
return;
}
case PL_COLOR_TRC_GAMMA18:
GLSL("color.rgb = pow(color.rgb, vec3(1.8));\n");
goto scale_out;
case PL_COLOR_TRC_GAMMA20:
GLSL("color.rgb = pow(color.rgb, vec3(2.0));\n");
goto scale_out;
case PL_COLOR_TRC_UNKNOWN:
case PL_COLOR_TRC_GAMMA22:
GLSL("color.rgb = pow(color.rgb, vec3(2.2));\n");
goto scale_out;
case PL_COLOR_TRC_GAMMA24:
GLSL("color.rgb = pow(color.rgb, vec3(2.4));\n");
goto scale_out;
case PL_COLOR_TRC_GAMMA26:
GLSL("color.rgb = pow(color.rgb, vec3(2.6));\n");
goto scale_out;
case PL_COLOR_TRC_GAMMA28:
GLSL("color.rgb = pow(color.rgb, vec3(2.8));\n");
goto scale_out;
case PL_COLOR_TRC_PRO_PHOTO:
GLSL("color.rgb = mix(color.rgb * vec3(1.0/16.0), \n"
" pow(color.rgb, vec3(1.8)), \n"
" lessThan(vec3(0.03125), color.rgb)); \n");
goto scale_out;
case PL_COLOR_TRC_ST428:
GLSL("color.rgb = vec3(52.37/48.0) * pow(color.rgb, vec3(2.6));\n");
goto scale_out;
case PL_COLOR_TRC_PQ:
GLSL("color.rgb = pow(color.rgb, vec3(1.0/%f)); \n"
"color.rgb = max(color.rgb - vec3(%f), 0.0) \n"
" / (vec3(%f) - vec3(%f) * color.rgb); \n"
"color.rgb = pow(color.rgb, vec3(1.0/%f)); \n"
// PQ's output range is 0-10000, but we need it to be relative to
// to PL_COLOR_SDR_WHITE instead, so rescale
"color.rgb *= vec3(%f); \n",
PQ_M2, PQ_C1, PQ_C2, PQ_C3, PQ_M1, 10000.0 / PL_COLOR_SDR_WHITE);
return;
case PL_COLOR_TRC_HLG: {
const float y = fmaxf(1.2f + 0.42f * log10f(csp_max / HLG_REF), 1);
const float b = sqrtf(3 * powf(csp_min / csp_max, 1 / y));
// OETF^-1
GLSL("color.rgb = "$" * color.rgb + vec3("$"); \n"
"color.rgb = mix(vec3(4.0) * color.rgb * color.rgb, \n"
" exp((color.rgb - vec3(%f)) * vec3(1.0/%f))\n"
" + vec3(%f), \n"
" lessThan(vec3(0.5), color.rgb)); \n",
SH_FLOAT(1 - b), SH_FLOAT(b),
HLG_C, HLG_A, HLG_B);
// OOTF
GLSL("color.rgb *= 1.0 / 12.0; \n"
"color.rgb *= "$" * pow(max(dot("$", color.rgb), 0.0), "$"); \n",
SH_FLOAT(csp_max), sh_luma_coeffs(sh, csp), SH_FLOAT(y - 1));
return;
}
case PL_COLOR_TRC_V_LOG:
GLSL("color.rgb = mix((color.rgb - vec3(0.125)) * vec3(1.0/5.6), \n"
" pow(vec3(10.0), (color.rgb - vec3(%f)) * vec3(1.0/%f)) \n"
" - vec3(%f), \n"
" lessThanEqual(vec3(0.181), color.rgb)); \n",
VLOG_D, VLOG_C, VLOG_B);
return;
case PL_COLOR_TRC_S_LOG1:
GLSL("color.rgb = pow(vec3(10.0), (color.rgb - vec3(%f)) * vec3(1.0/%f)) \n"
" - vec3(%f); \n",
SLOG_C, SLOG_A, SLOG_B);
return;
case PL_COLOR_TRC_S_LOG2:
GLSL("color.rgb = mix((color.rgb - vec3(%f)) * vec3(1.0/%f), \n"
" (pow(vec3(10.0), (color.rgb - vec3(%f)) * vec3(1.0/%f)) \n"
" - vec3(%f)) * vec3(1.0/%f), \n"
" lessThanEqual(vec3(%f), color.rgb)); \n",
SLOG_Q, SLOG_P, SLOG_C, SLOG_A, SLOG_B, SLOG_K2, SLOG_Q);
return;
case PL_COLOR_TRC_LINEAR:
case PL_COLOR_TRC_COUNT:
break;
}
pl_unreachable();
scale_out:
if (csp_max != 1 || csp_min != 0) {
GLSL("color.rgb = "$" * color.rgb + vec3("$"); \n",
SH_FLOAT(csp_max - csp_min), SH_FLOAT(csp_min));
}
}
void pl_shader_delinearize(pl_shader sh, const struct pl_color_space *csp)
{
if (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0))
return;
if (csp->transfer == PL_COLOR_TRC_LINEAR)
return;
float csp_min, csp_max;
pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
.color = csp,
.metadata = PL_HDR_METADATA_HDR10,
.scaling = PL_HDR_NORM,
.out_min = &csp_min,
.out_max = &csp_max,
));
GLSL("// pl_shader_delinearize \n");
switch (csp->transfer) {
case PL_COLOR_TRC_UNKNOWN:
case PL_COLOR_TRC_SRGB:
case PL_COLOR_TRC_LINEAR:
case PL_COLOR_TRC_GAMMA18:
case PL_COLOR_TRC_GAMMA20:
case PL_COLOR_TRC_GAMMA22:
case PL_COLOR_TRC_GAMMA24:
case PL_COLOR_TRC_GAMMA26:
case PL_COLOR_TRC_GAMMA28:
case PL_COLOR_TRC_PRO_PHOTO:
case PL_COLOR_TRC_ST428: ;
if (csp_max != 1 || csp_min != 0) {
GLSL("color.rgb = "$" * color.rgb + vec3("$"); \n",
SH_FLOAT(1 / (csp_max - csp_min)),
SH_FLOAT(-csp_min / (csp_max - csp_min)));
}
break;
case PL_COLOR_TRC_BT_1886:
case PL_COLOR_TRC_PQ:
case PL_COLOR_TRC_HLG:
case PL_COLOR_TRC_V_LOG:
case PL_COLOR_TRC_S_LOG1:
case PL_COLOR_TRC_S_LOG2:
break; // scene-referred or absolute scale
case PL_COLOR_TRC_COUNT:
pl_unreachable();
}
GLSL("color.rgb = max(color.rgb, 0.0); \n");
switch (csp->transfer) {
case PL_COLOR_TRC_SRGB:
GLSL("color.rgb = mix(color.rgb * vec3(12.92), \n"
" vec3(1.055) * pow(color.rgb, vec3(1.0/2.4)) \n"
" - vec3(0.055), \n"
" lessThanEqual(vec3(0.0031308), color.rgb)); \n");
return;
case PL_COLOR_TRC_BT_1886: {
const float lb = powf(csp_min, 1/2.4f);
const float lw = powf(csp_max, 1/2.4f);
const float a = powf(lw - lb, 2.4f);
const float b = lb / (lw - lb);
GLSL("color.rgb = pow("$" * color.rgb, vec3(1.0/2.4)) - vec3("$"); \n",
SH_FLOAT(1.0 / a), SH_FLOAT(b));
return;
}
case PL_COLOR_TRC_GAMMA18:
GLSL("color.rgb = pow(color.rgb, vec3(1.0/1.8));\n");
return;
case PL_COLOR_TRC_GAMMA20:
GLSL("color.rgb = pow(color.rgb, vec3(1.0/2.0));\n");
return;
case PL_COLOR_TRC_UNKNOWN:
case PL_COLOR_TRC_GAMMA22:
GLSL("color.rgb = pow(color.rgb, vec3(1.0/2.2));\n");
return;
case PL_COLOR_TRC_GAMMA24:
GLSL("color.rgb = pow(color.rgb, vec3(1.0/2.4));\n");
return;
case PL_COLOR_TRC_GAMMA26:
GLSL("color.rgb = pow(color.rgb, vec3(1.0/2.6));\n");
return;
case PL_COLOR_TRC_GAMMA28:
GLSL("color.rgb = pow(color.rgb, vec3(1.0/2.8));\n");
return;
case PL_COLOR_TRC_ST428:
GLSL("color.rgb = pow(color.rgb * vec3(48.0/52.37), vec3(1.0/2.6));\n");
return;
case PL_COLOR_TRC_PRO_PHOTO:
GLSL("color.rgb = mix(color.rgb * vec3(16.0), \n"
" pow(color.rgb, vec3(1.0/1.8)), \n"
" lessThanEqual(vec3(0.001953), color.rgb)); \n");
return;
case PL_COLOR_TRC_PQ:
GLSL("color.rgb *= vec3(1.0/%f); \n"
"color.rgb = pow(color.rgb, vec3(%f)); \n"
"color.rgb = (vec3(%f) + vec3(%f) * color.rgb) \n"
" / (vec3(1.0) + vec3(%f) * color.rgb); \n"
"color.rgb = pow(color.rgb, vec3(%f)); \n",
10000 / PL_COLOR_SDR_WHITE, PQ_M1, PQ_C1, PQ_C2, PQ_C3, PQ_M2);
return;
case PL_COLOR_TRC_HLG: {
const float y = fmaxf(1.2f + 0.42f * log10f(csp_max / HLG_REF), 1);
const float b = sqrtf(3 * powf(csp_min / csp_max, 1 / y));
// OOTF^-1
GLSL("color.rgb *= 1.0 / "$"; \n"
"color.rgb *= 12.0 * max(1e-6, pow(dot("$", color.rgb), "$")); \n",
SH_FLOAT(csp_max), sh_luma_coeffs(sh, csp), SH_FLOAT((1 - y) / y));
// OETF
GLSL("color.rgb = mix(vec3(0.5) * sqrt(color.rgb), \n"
" vec3(%f) * log(color.rgb - vec3(%f)) + vec3(%f), \n"
" lessThan(vec3(1.0), color.rgb)); \n"
"color.rgb = "$" * color.rgb + vec3("$"); \n",
HLG_A, HLG_B, HLG_C,
SH_FLOAT(1 / (1 - b)), SH_FLOAT(-b / (1 - b)));
return;
}
case PL_COLOR_TRC_V_LOG:
GLSL("color.rgb = mix(vec3(5.6) * color.rgb + vec3(0.125), \n"
" vec3(%f) * log(color.rgb + vec3(%f)) \n"
" + vec3(%f), \n"
" lessThanEqual(vec3(0.01), color.rgb)); \n",
VLOG_C / M_LN10, VLOG_B, VLOG_D);
return;
case PL_COLOR_TRC_S_LOG1:
GLSL("color.rgb = vec3(%f) * log(color.rgb + vec3(%f)) + vec3(%f);\n",
SLOG_A / M_LN10, SLOG_B, SLOG_C);
return;
case PL_COLOR_TRC_S_LOG2:
GLSL("color.rgb = mix(vec3(%f) * color.rgb + vec3(%f), \n"
" vec3(%f) * log(vec3(%f) * color.rgb + vec3(%f)) \n"
" + vec3(%f), \n"
" lessThanEqual(vec3(0.0), color.rgb)); \n",
SLOG_P, SLOG_Q, SLOG_A / M_LN10, SLOG_K2, SLOG_B, SLOG_C);
return;
case PL_COLOR_TRC_LINEAR:
case PL_COLOR_TRC_COUNT:
break;
}
pl_unreachable();
}
const struct pl_sigmoid_params pl_sigmoid_default_params = { PL_SIGMOID_DEFAULTS };
void pl_shader_sigmoidize(pl_shader sh, const struct pl_sigmoid_params *params)
{
if (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0))
return;
params = PL_DEF(params, &pl_sigmoid_default_params);
float center = PL_DEF(params->center, pl_sigmoid_default_params.center);
float slope = PL_DEF(params->slope, pl_sigmoid_default_params.slope);
// This function needs to go through (0,0) and (1,1), so we compute the
// values at 1 and 0, and then scale/shift them, respectively.
float offset = 1.0 / (1 + expf(slope * center));
float scale = 1.0 / (1 + expf(slope * (center - 1))) - offset;
GLSL("// pl_shader_sigmoidize \n"
"color = clamp(color, 0.0, 1.0); \n"
"color = vec4("$") - vec4("$") * \n"
" log(vec4(1.0) / (color * vec4("$") + vec4("$")) \n"
" - vec4(1.0)); \n",
SH_FLOAT(center), SH_FLOAT(1.0 / slope),
SH_FLOAT(scale), SH_FLOAT(offset));
}
void pl_shader_unsigmoidize(pl_shader sh, const struct pl_sigmoid_params *params)
{
if (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0))
return;
// See: pl_shader_sigmoidize
params = PL_DEF(params, &pl_sigmoid_default_params);
float center = PL_DEF(params->center, pl_sigmoid_default_params.center);
float slope = PL_DEF(params->slope, pl_sigmoid_default_params.slope);
float offset = 1.0 / (1 + expf(slope * center));
float scale = 1.0 / (1 + expf(slope * (center - 1))) - offset;
GLSL("// pl_shader_unsigmoidize \n"
"color = clamp(color, 0.0, 1.0); \n"
"color = vec4("$") / \n"
" (vec4(1.0) + exp(vec4("$") * (vec4("$") - color))) \n"
" - vec4("$"); \n",
SH_FLOAT(1.0 / scale),
SH_FLOAT(slope), SH_FLOAT(center),
SH_FLOAT(offset / scale));
}
const struct pl_peak_detect_params pl_peak_detect_default_params = { PL_PEAK_DETECT_DEFAULTS };
const struct pl_peak_detect_params pl_peak_detect_high_quality_params = { PL_PEAK_DETECT_HQ_DEFAULTS };
static bool peak_detect_params_eq(const struct pl_peak_detect_params *a,
const struct pl_peak_detect_params *b)
{
return a->smoothing_period == b->smoothing_period &&
a->scene_threshold_low == b->scene_threshold_low &&
a->scene_threshold_high == b->scene_threshold_high &&
a->percentile == b->percentile;
// don't compare `allow_delayed` because it doesn't change measurement
}
enum {
// Split the peak buffer into several independent slices to reduce pressure
// on global atomics
SLICES = 12,
// How many bits to use for storing PQ data. Be careful when setting this
// too high, as it may overflow `unsigned int` on large video sources.
//
// The value chosen is enough to guarantee no overflow for an 8K x 4K frame
// consisting entirely of 100% 10k nits PQ values, with 16x16 workgroups.
PQ_BITS = 14,
PQ_MAX = (1 << PQ_BITS) - 1,
// How many bits to use for the histogram. We bias the histogram down
// by half the PQ range (~90 nits), effectively clumping the SDR part
// of the image into a single histogram bin.
HIST_BITS = 7,
HIST_BIAS = 1 << (HIST_BITS - 1),
HIST_BINS = (1 << HIST_BITS) - HIST_BIAS,
// Convert from histogram bin to (starting) PQ value
#define HIST_PQ(bin) (((bin) + HIST_BIAS) << (PQ_BITS - HIST_BITS))
};
pl_static_assert(PQ_BITS >= HIST_BITS);
struct peak_buf_data {
unsigned frame_wg_count[SLICES]; // number of work groups processed
unsigned frame_wg_active[SLICES];// number of active (nonzero) work groups
unsigned frame_sum_pq[SLICES]; // sum of PQ Y values over all WGs (PQ_BITS)
unsigned frame_max_pq[SLICES]; // maximum PQ Y value among these WGs (PQ_BITS)
unsigned frame_hist[SLICES][HIST_BINS]; // always allocated, conditionally used
};
static const struct pl_buffer_var peak_buf_vars[] = {
#define VAR(field) { \
.var = { \
.name = #field, \
.type = PL_VAR_UINT, \
.dim_v = 1, \
.dim_m = 1, \
.dim_a = sizeof(((struct peak_buf_data *) NULL)->field) / \
sizeof(unsigned), \
}, \
.layout = { \
.offset = offsetof(struct peak_buf_data, field), \
.size = sizeof(((struct peak_buf_data *) NULL)->field), \
.stride = sizeof(unsigned), \
}, \
}
VAR(frame_wg_count),
VAR(frame_wg_active),
VAR(frame_sum_pq),
VAR(frame_max_pq),
VAR(frame_hist),
#undef VAR
};
struct sh_color_map_obj {
// Tone map state
struct {
struct pl_tone_map_params params;
pl_shader_obj lut;
} tone;
// Gamut map state
struct {
pl_shader_obj lut;
} gamut;
// Peak detection state
struct {
struct pl_peak_detect_params params; // currently active parameters
pl_buf buf; // pending peak detection buffer
pl_buf readback; // readback buffer (fallback)
float avg_pq; // current (smoothed) values
float max_pq;
} peak;
};
// Excluding size, since this is checked by sh_lut
static uint64_t gamut_map_signature(const struct pl_gamut_map_params *par)
{
uint64_t sig = CACHE_KEY_GAMUT_LUT;
pl_hash_merge(&sig, pl_str0_hash(par->function->name));
pl_hash_merge(&sig, pl_var_hash(par->input_gamut));
pl_hash_merge(&sig, pl_var_hash(par->output_gamut));
pl_hash_merge(&sig, pl_var_hash(par->min_luma));
pl_hash_merge(&sig, pl_var_hash(par->max_luma));
pl_hash_merge(&sig, pl_var_hash(par->constants));
return sig;
}
static void sh_color_map_uninit(pl_gpu gpu, void *ptr)
{
struct sh_color_map_obj *obj = ptr;
pl_shader_obj_destroy(&obj->tone.lut);
pl_shader_obj_destroy(&obj->gamut.lut);
pl_buf_destroy(gpu, &obj->peak.buf);
pl_buf_destroy(gpu, &obj->peak.readback);
memset(obj, 0, sizeof(*obj));
}
static inline float iir_coeff(float rate)
{
if (!rate)
return 1.0f;
return 1.0f - expf(-1.0f / rate);
}
static float measure_peak(const struct peak_buf_data *data, float percentile)
{
unsigned frame_max_pq = data->frame_max_pq[0];
for (int k = 1; k < SLICES; k++)
frame_max_pq = PL_MAX(frame_max_pq, data->frame_max_pq[k]);
const float frame_max = (float) frame_max_pq / PQ_MAX;
if (percentile <= 0 || percentile >= 100)
return frame_max;
unsigned total_pixels = 0;
for (int k = 0; k < SLICES; k++) {
for (int i = 0; i < HIST_BINS; i++)
total_pixels += data->frame_hist[k][i];
}
if (!total_pixels) // no histogram data available?
return frame_max;
const unsigned target_pixel = ceilf(percentile / 100.0f * total_pixels);
if (target_pixel >= total_pixels)
return frame_max;
unsigned sum = 0;
for (int i = 0; i < HIST_BINS; i++) {
unsigned next = sum;
for (int k = 0; k < SLICES; k++)
next += data->frame_hist[k][i];
if (next < target_pixel) {
sum = next;
continue;
}
// Upper and lower frequency boundaries of the matching histogram bin
const unsigned count_low = sum; // last pixel of previous bin
const unsigned count_high = next + 1; // first pixel of next bin
pl_assert(count_low < target_pixel && target_pixel < count_high);
// PQ luminance associated with count_low/high respectively
const float pq_low = (float) HIST_PQ(i) / PQ_MAX;
float pq_high = (float) HIST_PQ(i + 1) / PQ_MAX;
if (count_high > total_pixels) // special case for last histogram bin
pq_high = frame_max;
// Position of `target_pixel` inside this bin, assumes pixels are
// equidistributed inside a histogram bin
const float ratio = (float) (target_pixel - count_low) /
(count_high - count_low);
return PL_MIX(pq_low, pq_high, ratio);
}
pl_unreachable();
}
// if `force` is true, ensures the buffer is read, even if `allow_delayed`
static void update_peak_buf(pl_gpu gpu, struct sh_color_map_obj *obj, bool force)
{
const struct pl_peak_detect_params *params = &obj->peak.params;
if (!obj->peak.buf)
return;
if (!force && params->allow_delayed && pl_buf_poll(gpu, obj->peak.buf, 0))
return; // buffer not ready yet
bool ok;
struct peak_buf_data data = {0};
if (obj->peak.readback) {
pl_buf_copy(gpu, obj->peak.readback, 0, obj->peak.buf, 0, sizeof(data));
ok = pl_buf_read(gpu, obj->peak.readback, 0, &data, sizeof(data));
} else {
ok = pl_buf_read(gpu, obj->peak.buf, 0, &data, sizeof(data));
}
if (ok && data.frame_wg_count[0] > 0) {
// Peak detection completed successfully
pl_buf_destroy(gpu, &obj->peak.buf);
} else {
// No data read? Possibly this peak obj has not been executed yet
if (!ok) {
PL_ERR(gpu, "Failed reading peak detection buffer!");
} else if (params->allow_delayed) {
PL_TRACE(gpu, "Peak detection buffer not yet ready, ignoring..");
} else {
PL_WARN(gpu, "Peak detection usage error: attempted detecting peak "
"and using detected peak in the same shader program, "
"but `params->allow_delayed` is false! Ignoring, but "
"expect incorrect output.");
}
if (force || !ok)
pl_buf_destroy(gpu, &obj->peak.buf);
return;
}
uint64_t frame_sum_pq = 0u, frame_wg_count = 0u, frame_wg_active = 0u;
for (int k = 0; k < SLICES; k++) {
frame_sum_pq += data.frame_sum_pq[k];
frame_wg_count += data.frame_wg_count[k];
frame_wg_active += data.frame_wg_active[k];
}
float avg_pq, max_pq;
if (frame_wg_active) {
avg_pq = (float) frame_sum_pq / (frame_wg_active * PQ_MAX);
max_pq = measure_peak(&data, params->percentile);
} else {
// Solid black frame
avg_pq = max_pq = PL_COLOR_HDR_BLACK;
}
if (!obj->peak.avg_pq) {
// Set the initial value accordingly if it contains no data
obj->peak.avg_pq = avg_pq;
obj->peak.max_pq = max_pq;
} else {
// Ignore small deviations from existing peak (rounding error)
static const float epsilon = 1.0f / PQ_MAX;
if (fabsf(avg_pq - obj->peak.avg_pq) < epsilon)
avg_pq = obj->peak.avg_pq;
if (fabsf(max_pq - obj->peak.max_pq) < epsilon)
max_pq = obj->peak.max_pq;
}
// Use an IIR low-pass filter to smooth out the detected values
const float coeff = iir_coeff(params->smoothing_period);
obj->peak.avg_pq += coeff * (avg_pq - obj->peak.avg_pq);
obj->peak.max_pq += coeff * (max_pq - obj->peak.max_pq);
// Scene change hysteresis
if (params->scene_threshold_low > 0 && params->scene_threshold_high > 0) {
const float log10_pq = 1e-2f; // experimentally determined approximate
const float thresh_low = params->scene_threshold_low * log10_pq;
const float thresh_high = params->scene_threshold_high * log10_pq;
const float bias = (float) frame_wg_active / frame_wg_count;
const float delta = bias * fabsf(avg_pq - obj->peak.avg_pq);
const float mix_coeff = pl_smoothstep(thresh_low, thresh_high, delta);
obj->peak.avg_pq = PL_MIX(obj->peak.avg_pq, avg_pq, mix_coeff);
obj->peak.max_pq = PL_MIX(obj->peak.max_pq, max_pq, mix_coeff);
}
}
bool pl_shader_detect_peak(pl_shader sh, struct pl_color_space csp,
pl_shader_obj *state,
const struct pl_peak_detect_params *params)
{
params = PL_DEF(params, &pl_peak_detect_default_params);
if (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0))
return false;
pl_gpu gpu = SH_GPU(sh);
if (!gpu || gpu->limits.max_ssbo_size < sizeof(struct peak_buf_data)) {
PL_ERR(sh, "HDR peak detection requires a GPU with support for at "
"least %zu bytes of SSBO data (supported: %zu)",
sizeof(struct peak_buf_data), gpu ? gpu->limits.max_ssbo_size : 0);
return false;
}
const bool use_histogram = params->percentile > 0 && params->percentile < 100;
size_t shmem_req = 3 * sizeof(uint32_t);
if (use_histogram)
shmem_req += sizeof(uint32_t[HIST_BINS]);
if (!sh_try_compute(sh, 16, 16, true, shmem_req)) {
PL_ERR(sh, "HDR peak detection requires compute shaders with support "
"for at least %zu bytes of shared memory! (avail: %zu)",
shmem_req, sh_glsl(sh).max_shmem_size);
return false;
}
struct sh_color_map_obj *obj;
obj = SH_OBJ(sh, state, PL_SHADER_OBJ_COLOR_MAP, struct sh_color_map_obj,
sh_color_map_uninit);
if (!obj)
return false;
if (peak_detect_params_eq(&obj->peak.params, params)) {
update_peak_buf(gpu, obj, true); // prevent over-writing previous frame
} else {
pl_reset_detected_peak(*state);
}
pl_assert(!obj->peak.buf);
static const struct peak_buf_data zero = {0};
retry_ssbo:
if (obj->peak.readback) {
obj->peak.buf = pl_buf_create(gpu, pl_buf_params(
.size = sizeof(struct peak_buf_data),
.storable = true,
.initial_data = &zero,
));
} else {
obj->peak.buf = pl_buf_create(gpu, pl_buf_params(
.size = sizeof(struct peak_buf_data),
.memory_type = PL_BUF_MEM_DEVICE,
.host_readable = true,
.storable = true,
.initial_data = &zero,
));
}
if (!obj->peak.buf && !obj->peak.readback) {
PL_WARN(sh, "Failed creating host-readable peak detection SSBO, "
"retrying with fallback buffer");
obj->peak.readback = pl_buf_create(gpu, pl_buf_params(
.size = sizeof(struct peak_buf_data),
.host_readable = true,
));
if (obj->peak.readback)
goto retry_ssbo;
}
if (!obj->peak.buf) {
SH_FAIL(sh, "Failed creating peak detection SSBO!");
return false;
}
obj->peak.params = *params;
sh_desc(sh, (struct pl_shader_desc) {
.desc = {
.name = "PeakBuf",
.type = PL_DESC_BUF_STORAGE,
.access = PL_DESC_ACCESS_READWRITE,
},
.binding.object = obj->peak.buf,
.buffer_vars = (struct pl_buffer_var *) peak_buf_vars,
.num_buffer_vars = PL_ARRAY_SIZE(peak_buf_vars),
});
sh_describe(sh, "peak detection");
GLSL("// pl_shader_detect_peak \n"
"{ \n"
"const uint wg_size = gl_WorkGroupSize.x * gl_WorkGroupSize.y; \n"
"uint wg_idx = gl_WorkGroupID.y * gl_NumWorkGroups.x + \n"
" gl_WorkGroupID.x; \n"
"uint slice = wg_idx %% %du; \n"
"vec4 color_orig = color; \n",
SLICES);
// For performance, we want to do as few atomic operations on global
// memory as possible, so use an atomic in shmem for the work group.
ident_t wg_sum = sh_fresh(sh, "wg_sum"),
wg_max = sh_fresh(sh, "wg_max"),
wg_black = sh_fresh(sh, "wg_black"),
wg_hist = NULL_IDENT;
GLSLH("shared uint "$", "$", "$"; \n", wg_sum, wg_max, wg_black);
if (use_histogram) {
wg_hist = sh_fresh(sh, "wg_hist");
GLSLH("shared uint "$"[%u]; \n", wg_hist, HIST_BINS);
GLSL("for (uint i = gl_LocalInvocationIndex; i < %du; i += wg_size) \n"
" "$"[i] = 0u; \n",
HIST_BINS, wg_hist);
}
GLSL($" = 0u; "$" = 0u; "$" = 0u; \n"
"barrier(); \n",
wg_sum, wg_max, wg_black);
// Decode color into linear light representation
pl_color_space_infer(&csp);
pl_shader_linearize(sh, &csp);
// Measure luminance as N-bit PQ
GLSL("float luma = dot("$", color.rgb); \n"
"luma *= %f; \n"
"luma = pow(clamp(luma, 0.0, 1.0), %f); \n"
"luma = (%f + %f * luma) / (1.0 + %f * luma); \n"
"luma = pow(luma, %f); \n"
"luma *= smoothstep(0.0, 1e-2, luma); \n"
"uint y_pq = uint(%d.0 * luma); \n",
sh_luma_coeffs(sh, &csp),
PL_COLOR_SDR_WHITE / 10000.0,
PQ_M1, PQ_C1, PQ_C2, PQ_C3, PQ_M2,
PQ_MAX);
// Update the work group's shared atomics
bool has_subgroups = sh_glsl(sh).subgroup_size > 0;
if (use_histogram) {
GLSL("int bin = (int(y_pq) >> %d) - %d; \n"
"bin = clamp(bin, 0, %d); \n",
PQ_BITS - HIST_BITS, HIST_BIAS,
HIST_BINS - 1);
if (has_subgroups) {
// Optimize for the very common case of identical histogram bins
GLSL("if (subgroupAllEqual(bin)) { \n"
" if (subgroupElect()) \n"
" atomicAdd("$"[bin], gl_SubgroupSize); \n"
"} else { \n"
" atomicAdd("$"[bin], 1u); \n"
"} \n",
wg_hist, wg_hist);
} else {
GLSL("atomicAdd("$"[bin], 1u); \n", wg_hist);
}
}
if (has_subgroups) {
GLSL("uint group_sum = subgroupAdd(y_pq); \n"
"uint group_max = subgroupMax(y_pq); \n"
"uvec4 b = subgroupBallot(y_pq == 0u); \n"
"if (subgroupElect()) { \n"
" atomicAdd("$", group_sum); \n"
" atomicMax("$", group_max); \n"
" atomicAdd("$", subgroupBallotBitCount(b));\n"
"} \n"
"barrier(); \n",
wg_sum, wg_max, wg_black);
} else {
GLSL("atomicAdd("$", y_pq); \n"
"atomicMax("$", y_pq); \n"
"if (y_pq == 0u) \n"
" atomicAdd("$", 1u); \n"
"barrier(); \n",
wg_sum, wg_max, wg_black);
}
if (use_histogram) {
GLSL("if (gl_LocalInvocationIndex == 0u) \n"
" "$"[0] -= "$"; \n"
"for (uint i = gl_LocalInvocationIndex; i < %du; i += wg_size) \n"
" atomicAdd(frame_hist[slice * %du + i], "$"[i]); \n",
wg_hist, wg_black,
HIST_BINS,
HIST_BINS, wg_hist);
}
// Have one thread per work group update the global atomics
GLSL("if (gl_LocalInvocationIndex == 0u) { \n"
" uint num = wg_size - "$"; \n"
" atomicAdd(frame_wg_count[slice], 1u); \n"
" atomicAdd(frame_wg_active[slice], min(num, 1u)); \n"
" if (num > 0u) { \n"
" atomicAdd(frame_sum_pq[slice], "$" / num); \n"
" atomicMax(frame_max_pq[slice], "$"); \n"
" } \n"
"} \n"
"color = color_orig; \n"
"} \n",
wg_black, wg_sum, wg_max);
return true;
}
bool pl_get_detected_hdr_metadata(const pl_shader_obj state,
struct pl_hdr_metadata *out)
{
if (!state || state->type != PL_SHADER_OBJ_COLOR_MAP)
return false;
struct sh_color_map_obj *obj = state->priv;
update_peak_buf(state->gpu, obj, false);
if (!obj->peak.avg_pq)
return false;
out->max_pq_y = obj->peak.max_pq;
out->avg_pq_y = obj->peak.avg_pq;
return true;
}
bool pl_get_detected_peak(const pl_shader_obj state,
float *out_peak, float *out_avg)
{
struct pl_hdr_metadata data;
if (!pl_get_detected_hdr_metadata(state, &data))
return false;
// Preserves old behavior
*out_peak = pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NORM, data.max_pq_y);
*out_avg = pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NORM, data.avg_pq_y);
return true;
}
void pl_reset_detected_peak(pl_shader_obj state)
{
if (!state || state->type != PL_SHADER_OBJ_COLOR_MAP)
return;
struct sh_color_map_obj *obj = state->priv;
pl_buf readback = obj->peak.readback;
pl_buf_destroy(state->gpu, &obj->peak.buf);
memset(&obj->peak, 0, sizeof(obj->peak));
obj->peak.readback = readback;
}
void pl_shader_extract_features(pl_shader sh, struct pl_color_space csp)
{
if (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0))
return;
sh_describe(sh, "feature extraction");
pl_shader_linearize(sh, &csp);
GLSL("// pl_shader_extract_features \n"
"{ \n"
"vec3 lms = %f * "$" * color.rgb; \n"
"lms = pow(max(lms, 0.0), vec3(%f)); \n"
"lms = (vec3(%f) + %f * lms) \n"
" / (vec3(1.0) + %f * lms); \n"
"lms = pow(lms, vec3(%f)); \n"
"float I = dot(vec3(%f, %f, %f), lms); \n"
"color = vec4(I, 0.0, 0.0, 1.0); \n"
"} \n",
PL_COLOR_SDR_WHITE / 10000,
SH_MAT3(pl_ipt_rgb2lms(pl_raw_primaries_get(csp.primaries))),
PQ_M1, PQ_C1, PQ_C2, PQ_C3, PQ_M2,
pl_ipt_lms2ipt.m[0][0], pl_ipt_lms2ipt.m[0][1], pl_ipt_lms2ipt.m[0][2]);
}
const struct pl_color_map_params pl_color_map_default_params = { PL_COLOR_MAP_DEFAULTS };
const struct pl_color_map_params pl_color_map_high_quality_params = { PL_COLOR_MAP_HQ_DEFAULTS };
static ident_t rect_pos(pl_shader sh, pl_rect2df rc)
{
if (!rc.x0 && !rc.x1)
rc.x1 = 1.0f;
if (!rc.y0 && !rc.y1)
rc.y1 = 1.0f;
return sh_attr_vec2(sh, "tone_map_coords", &(pl_rect2df) {
.x0 = -rc.x0 / (rc.x1 - rc.x0),
.x1 = (1.0f - rc.x0) / (rc.x1 - rc.x0),
.y0 = -rc.y1 / (rc.y0 - rc.y1),
.y1 = (1.0f - rc.y1) / (rc.y0 - rc.y1),
});
}
static void visualize_tone_map(pl_shader sh, pl_rect2df rc, float alpha,
const struct pl_tone_map_params *params)
{
pl_assert(params->input_scaling == PL_HDR_PQ);
pl_assert(params->output_scaling == PL_HDR_PQ);
GLSL("// Visualize tone mapping \n"
"{ \n"
"vec2 pos = "$"; \n"
"if (min(pos.x, pos.y) >= 0.0 && \n" // visualizer rect
" max(pos.x, pos.y) <= 1.0) \n"
"{ \n"
"float xmin = "$"; \n"
"float xmax = "$"; \n"
"float xavg = "$"; \n"
"float ymin = "$"; \n"
"float ymax = "$"; \n"
"float alpha = 0.8 * "$"; \n"
"vec3 viz = color.rgb; \n"
"float vv = tone_map(pos.x); \n"
// Color based on region
"if (pos.x < xmin || pos.x > xmax) { \n" // outside source
"} else if (pos.y < ymin || pos.y > ymax) {\n" // outside target
" if (pos.y < xmin || pos.y > xmax) { \n" // and also source
" viz = vec3(0.1, 0.1, 0.5); \n"
" } else { \n"
" viz = vec3(0.2, 0.05, 0.05); \n" // but inside source
" } \n"
"} else { \n" // inside domain
" if (abs(pos.x - pos.y) < 1e-3) { \n" // main diagonal
" viz = vec3(0.2); \n"
" } else if (pos.y < vv) { \n" // inside function
" alpha *= 0.6; \n"
" viz = vec3(0.05); \n"
" if (vv > pos.x && pos.y > pos.x) \n" // output brighter than input
" viz.rg = vec2(0.5, 0.7); \n"
" } else { \n" // outside function
" if (vv < pos.x && pos.y < pos.x) \n" // output darker than input
" viz = vec3(0.0, 0.1, 0.2); \n"
" } \n"
" if (pos.y > xmax) { \n" // inverse tone-mapping region
" vec3 hi = vec3(0.2, 0.5, 0.8); \n"
" viz = mix(viz, hi, 0.5); \n"
" } else if (pos.y < xmin) { \n" // black point region
" viz = mix(viz, vec3(0.0), 0.3); \n"
" } \n"
" if (xavg > 0.0 && abs(pos.x - xavg) < 1e-3)\n" // source avg brightness
" viz = vec3(0.5); \n"
"} \n"
"color.rgb = mix(color.rgb, viz, alpha); \n"
"} \n"
"} \n",
rect_pos(sh, rc),
SH_FLOAT_DYN(params->input_min),
SH_FLOAT_DYN(params->input_max),
SH_FLOAT_DYN(params->input_avg),
SH_FLOAT(params->output_min),
SH_FLOAT_DYN(params->output_max),
SH_FLOAT_DYN(alpha));
}
static void visualize_gamut_map(pl_shader sh, pl_rect2df rc,
ident_t lut, float hue, float theta,
const struct pl_gamut_map_params *params)
{
ident_t ipt2lms = SH_MAT3(pl_ipt_ipt2lms);
ident_t lms2rgb_src = SH_MAT3(pl_ipt_lms2rgb(¶ms->input_gamut));
ident_t lms2rgb_dst = SH_MAT3(pl_ipt_lms2rgb(¶ms->output_gamut));
GLSL("// Visualize gamut mapping \n"
"vec2 pos = "$"; \n"
"float pqmin = "$"; \n"
"float pqmax = "$"; \n"
"float rgbmin = "$"; \n"
"float rgbmax = "$"; \n"
"vec3 orig = ipt; \n"
"if (min(pos.x, pos.y) >= 0.0 && \n"
" max(pos.x, pos.y) <= 1.0) \n"
"{ \n"
// Source color to visualize
"float mid = mix(pqmin, pqmax, 0.6); \n"
"vec3 base = vec3(0.5, 0.0, 0.0); \n"
"float hue = "$", theta = "$"; \n"
"base.x = mix(base.x, mid, sin(theta)); \n"
"mat3 rot1 = mat3(1.0, 0.0, 0.0, \n"
" 0.0, cos(hue), sin(hue), \n"
" 0.0, -sin(hue), cos(hue)); \n"
"mat3 rot2 = mat3( cos(theta), 0.0, sin(theta), \n"
" 0.0, 1.0, 0.0, \n"
" -sin(theta), 0.0, cos(theta)); \n"
"vec3 dir = vec3(pos.yx - vec2(0.5), 0.0); \n"
"ipt = base + rot1 * rot2 * dir; \n"
// Convert back to RGB (for gamut boundary testing)
"lmspq = "$" * ipt; \n"
"lms = pow(max(lmspq, 0.0), vec3(1.0/%f)); \n"
"lms = max(lms - vec3(%f), 0.0) \n"
" / (vec3(%f) - %f * lms); \n"
"lms = pow(lms, vec3(1.0/%f)); \n"
"lms *= %f; \n"
// Check against src/dst gamut boundaries
"vec3 rgbsrc = "$" * lms; \n"
"vec3 rgbdst = "$" * lms; \n"
"bool insrc, indst; \n"
"insrc = all(lessThan(rgbsrc, vec3(rgbmax))) && \n"
" all(greaterThan(rgbsrc, vec3(rgbmin))); \n"
"indst = all(lessThan(rgbdst, vec3(rgbmax))) && \n"
" all(greaterThan(rgbdst, vec3(rgbmin))); \n"
// Sample from gamut mapping 3DLUT
"idx.x = (ipt.x - pqmin) / (pqmax - pqmin); \n"
"idx.y = 2.0 * length(ipt.yz); \n"
"idx.z = %f * atan(ipt.z, ipt.y) + 0.5; \n"
"vec3 mapped = "$"(idx).xyz; \n"
"mapped.yz -= vec2(32768.0/65535.0); \n"
"float mappedhue = atan(mapped.z, mapped.y); \n"
"float mappedchroma = length(mapped.yz); \n"
"ipt = mapped; \n"
// Visualize gamuts
"if (!insrc && !indst) { \n"
" ipt = orig; \n"
"} else if (insrc && !indst) { \n"
" ipt.x -= 0.1; \n"
"} else if (indst && !insrc) { \n"
" ipt.x += 0.1; \n"
"} \n"
// Visualize iso-luminance and iso-hue lines
"vec3 line; \n"
"if (insrc && fract(50.0 * mapped.x) < 1e-1) { \n"
" float k = smoothstep(0.1, 0.0, abs(sin(theta))); \n"
" line.x = mix(mapped.x, 0.3, 0.5); \n"
" line.yz = sqrt(length(mapped.yz)) * \n"
" normalize(mapped.yz); \n"
" ipt = mix(ipt, line, k); \n"
"} \n"
"if (insrc && fract(10.0 * (mappedhue - hue)) < 1e-1) {\n"
" float k = smoothstep(0.3, 0.0, abs(cos(theta))); \n"
" line.x = mapped.x - 0.05; \n"
" line.yz = 1.2 * mapped.yz; \n"
" ipt = mix(ipt, line, k); \n"
"} \n"
"if (insrc && fract(100.0 * mappedchroma) < 1e-1) { \n"
" line.x = mapped.x + 0.1; \n"
" line.yz = 0.4 * mapped.yz; \n"
" ipt = mix(ipt, line, 0.5); \n"
"} \n"
"} \n",
rect_pos(sh, rc),
SH_FLOAT(params->min_luma), SH_FLOAT(params->max_luma),
SH_FLOAT(pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NORM, params->min_luma)),
SH_FLOAT(pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NORM, params->max_luma)),
SH_FLOAT_DYN(hue), SH_FLOAT_DYN(theta),
ipt2lms,
PQ_M2, PQ_C1, PQ_C2, PQ_C3, PQ_M1,
10000 / PL_COLOR_SDR_WHITE,
lms2rgb_src,
lms2rgb_dst,
0.5f / M_PI,
lut);
}
static void fill_tone_lut(void *data, const struct sh_lut_params *params)
{
const struct pl_tone_map_params *lut_params = params->priv;
pl_tone_map_generate(data, lut_params);
}
static void fill_gamut_lut(void *data, const struct sh_lut_params *params)
{
const struct pl_gamut_map_params *lut_params = params->priv;
const int lut_size = params->width * params->height * params->depth;
void *tmp = pl_alloc(NULL, lut_size * sizeof(float) * lut_params->lut_stride);
pl_gamut_map_generate(tmp, lut_params);
// Convert to 16-bit unsigned integer for GPU texture
const float *in = tmp;
uint16_t *out = data;
pl_assert(lut_params->lut_stride == 3);
pl_assert(params->comps == 4);
for (int i = 0; i < lut_size; i++) {
out[0] = roundf(in[0] * UINT16_MAX);
out[1] = roundf(in[1] * UINT16_MAX + (UINT16_MAX >> 1));
out[2] = roundf(in[2] * UINT16_MAX + (UINT16_MAX >> 1));
in += 3;
out += 4;
}
pl_free(tmp);
}
void pl_shader_color_map_ex(pl_shader sh, const struct pl_color_map_params *params,
const struct pl_color_map_args *args)
{
if (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0))
return;
struct pl_color_space src = args->src, dst = args->dst;
pl_color_space_infer_map(&src, &dst);
if (pl_color_space_equal(&src, &dst)) {
if (args->prelinearized)
pl_shader_delinearize(sh, &dst);
return;
}
struct sh_color_map_obj *obj = NULL;
if (args->state) {
pl_get_detected_hdr_metadata(*args->state, &src.hdr);
obj = SH_OBJ(sh, args->state, PL_SHADER_OBJ_COLOR_MAP, struct sh_color_map_obj,
sh_color_map_uninit);
if (!obj)
return;
}
params = PL_DEF(params, &pl_color_map_default_params);
GLSL("// pl_shader_color_map \n"
"{ \n");
struct pl_tone_map_params tone = {
.function = PL_DEF(params->tone_mapping_function, &pl_tone_map_clip),
.constants = params->tone_constants,
.param = params->tone_mapping_param,
.input_scaling = PL_HDR_PQ,
.output_scaling = PL_HDR_PQ,
.lut_size = PL_DEF(params->lut_size, pl_color_map_default_params.lut_size),
.hdr = src.hdr,
};
pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
.color = &src,
.metadata = params->metadata,
.scaling = tone.input_scaling,
.out_min = &tone.input_min,
.out_max = &tone.input_max,
.out_avg = &tone.input_avg,
));
pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
.color = &dst,
.metadata = PL_HDR_METADATA_HDR10,
.scaling = tone.output_scaling,
.out_min = &tone.output_min,
.out_max = &tone.output_max,
));
pl_tone_map_params_infer(&tone);
// Round sufficiently similar values
if (fabs(tone.input_max - tone.output_max) < 1e-6)
tone.output_max = tone.input_max;
if (fabs(tone.input_min - tone.output_min) < 1e-6)
tone.output_min = tone.input_min;
if (!params->inverse_tone_mapping) {
// Never exceed the source unless requested, but still allow
// black point adaptation
tone.output_max = PL_MIN(tone.output_max, tone.input_max);
}
const int *lut3d_size_def = pl_color_map_default_params.lut3d_size;
struct pl_gamut_map_params gamut = {
.function = PL_DEF(params->gamut_mapping, &pl_gamut_map_clip),
.constants = params->gamut_constants,
.input_gamut = src.hdr.prim,
.output_gamut = dst.hdr.prim,
.lut_size_I = PL_DEF(params->lut3d_size[0], lut3d_size_def[0]),
.lut_size_C = PL_DEF(params->lut3d_size[1], lut3d_size_def[1]),
.lut_size_h = PL_DEF(params->lut3d_size[2], lut3d_size_def[2]),
.lut_stride = 3,
};
float src_peak_static;
pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
.color = &src,
.metadata = PL_HDR_METADATA_HDR10,
.scaling = PL_HDR_PQ,
.out_max = &src_peak_static,
));
pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
.color = &dst,
.metadata = PL_HDR_METADATA_HDR10,
.scaling = PL_HDR_PQ,
.out_min = &gamut.min_luma,
.out_max = &gamut.max_luma,
));
// Clip the gamut mapping output to the input gamut if disabled
if (!params->gamut_expansion && gamut.function->bidirectional) {
if (pl_primaries_compatible(&gamut.input_gamut, &gamut.output_gamut)) {
gamut.output_gamut = pl_primaries_clip(&gamut.output_gamut,
&gamut.input_gamut);
}
}
// Backwards compatibility with older API
switch (params->gamut_mode) {
case PL_GAMUT_CLIP:
switch (params->intent) {
case PL_INTENT_AUTO:
case PL_INTENT_PERCEPTUAL:
case PL_INTENT_RELATIVE_COLORIMETRIC:
break; // leave default
case PL_INTENT_SATURATION:
gamut.function = &pl_gamut_map_saturation;
break;
case PL_INTENT_ABSOLUTE_COLORIMETRIC:
gamut.function = &pl_gamut_map_absolute;
break;
}
break;
case PL_GAMUT_DARKEN:
gamut.function = &pl_gamut_map_darken;
break;
case PL_GAMUT_WARN:
gamut.function = &pl_gamut_map_highlight;
break;
case PL_GAMUT_DESATURATE:
gamut.function = &pl_gamut_map_desaturate;
break;
case PL_GAMUT_MODE_COUNT:
pl_unreachable();
}
bool can_fast = !params->force_tone_mapping_lut;
if (!args->state) {
// No state object provided, forcibly disable advanced methods
can_fast = true;
if (tone.function != &pl_tone_map_clip)
tone.function = &pl_tone_map_linear;
if (gamut.function != &pl_gamut_map_clip)
gamut.function = &pl_gamut_map_saturation;
}
pl_fmt gamut_fmt = pl_find_fmt(SH_GPU(sh), PL_FMT_UNORM, 4, 16, 16, PL_FMT_CAP_LINEAR);
if (!gamut_fmt) {
gamut.function = &pl_gamut_map_saturation;
can_fast = true;
}
bool need_tone_map = !pl_tone_map_params_noop(&tone);
bool need_gamut_map = !pl_gamut_map_params_noop(&gamut);
if (!args->prelinearized)
pl_shader_linearize(sh, &src);
pl_matrix3x3 rgb2lms = pl_ipt_rgb2lms(pl_raw_primaries_get(src.primaries));
pl_matrix3x3 lms2rgb = pl_ipt_lms2rgb(pl_raw_primaries_get(dst.primaries));
ident_t lms2ipt = SH_MAT3(pl_ipt_lms2ipt);
ident_t ipt2lms = SH_MAT3(pl_ipt_ipt2lms);
if (need_gamut_map && gamut.function == &pl_gamut_map_saturation && can_fast) {
const pl_matrix3x3 lms2src = pl_ipt_lms2rgb(&gamut.input_gamut);
const pl_matrix3x3 dst2lms = pl_ipt_rgb2lms(&gamut.output_gamut);
sh_describe(sh, "gamut map (saturation)");
pl_matrix3x3_mul(&lms2rgb, &dst2lms);
pl_matrix3x3_mul(&lms2rgb, &lms2src);
need_gamut_map = false;
}
// Fast path: simply convert between primaries (if needed)
if (!need_tone_map && !need_gamut_map) {
if (src.primaries != dst.primaries) {
sh_describe(sh, "colorspace conversion");
pl_matrix3x3_mul(&lms2rgb, &rgb2lms);
GLSL("color.rgb = "$" * color.rgb; \n", SH_MAT3(lms2rgb));
}
goto done;
}
// Full path: convert input from normalized RGB to IPT
GLSL("vec3 lms = "$" * color.rgb; \n"
"vec3 lmspq = %f * lms; \n"
"lmspq = pow(max(lmspq, 0.0), vec3(%f)); \n"
"lmspq = (vec3(%f) + %f * lmspq) \n"
" / (vec3(1.0) + %f * lmspq); \n"
"lmspq = pow(lmspq, vec3(%f)); \n"
"vec3 ipt = "$" * lmspq; \n"
"float i_orig = ipt.x; \n",
SH_MAT3(rgb2lms),
PL_COLOR_SDR_WHITE / 10000,
PQ_M1, PQ_C1, PQ_C2, PQ_C3, PQ_M2,
lms2ipt);
if (params->show_clipping) {
const float eps = 1e-6f;
GLSL("bool clip_hi, clip_lo; \n"
"clip_hi = any(greaterThan(color.rgb, vec3("$"))); \n"
"clip_lo = any(lessThan(color.rgb, vec3("$"))); \n"
"clip_hi = clip_hi || ipt.x > "$"; \n"
"clip_lo = clip_lo || ipt.x < "$"; \n",
SH_FLOAT_DYN(pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NORM, tone.input_max) + eps),
SH_FLOAT(pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NORM, tone.input_min) - eps),
SH_FLOAT_DYN(tone.input_max + eps),
SH_FLOAT(tone.input_min - eps));
}
if (need_tone_map) {
const struct pl_tone_map_function *fun = tone.function;
sh_describef(sh, "%s tone map (%.0f -> %.0f)", fun->name,
pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NITS, tone.input_max),
pl_hdr_rescale(PL_HDR_PQ, PL_HDR_NITS, tone.output_max));
if (fun == &pl_tone_map_clip && can_fast) {
GLSL("#define tone_map(x) clamp((x), "$", "$") \n",
SH_FLOAT(tone.input_min),
SH_FLOAT_DYN(tone.input_max));
} else if (fun == &pl_tone_map_linear && can_fast) {
const float gain = tone.constants.exposure;
const float scale = tone.input_max - tone.input_min;
ident_t linfun = sh_fresh(sh, "linear_pq");
GLSLH("float "$"(float x) { \n"
// Stretch the input range (while clipping)
" x = "$" * x + "$"; \n"
" x = clamp(x, 0.0, 1.0); \n"
" x = "$" * x + "$"; \n"
" return x; \n"
"} \n",
linfun,
SH_FLOAT_DYN(gain / scale),
SH_FLOAT_DYN(-gain / scale * tone.input_min),
SH_FLOAT_DYN(tone.output_max - tone.output_min),
SH_FLOAT(tone.output_min));
GLSL("#define tone_map(x) ("$"(x)) \n", linfun);
} else {
pl_assert(obj);
ident_t lut = sh_lut(sh, sh_lut_params(
.object = &obj->tone.lut,
.var_type = PL_VAR_FLOAT,
.lut_type = SH_LUT_AUTO,
.method = SH_LUT_LINEAR,
.width = tone.lut_size,
.comps = 1,
.update = !pl_tone_map_params_equal(&tone, &obj->tone.params),
.dynamic = tone.input_avg > 0, // dynamic metadata
.fill = fill_tone_lut,
.priv = &tone,
));
obj->tone.params = tone;
if (!lut) {
SH_FAIL(sh, "Failed generating tone-mapping LUT!");
return;
}
const float lut_range = tone.input_max - tone.input_min;
GLSL("#define tone_map(x) ("$"("$" * (x) + "$")) \n",
lut, SH_FLOAT_DYN(1.0f / lut_range),
SH_FLOAT_DYN(-tone.input_min / lut_range));
}
bool need_recovery = tone.input_max >= tone.output_max;
if (need_recovery && params->contrast_recovery && args->feature_map) {
ident_t pos, pt;
ident_t lowres = sh_bind(sh, args->feature_map, PL_TEX_ADDRESS_CLAMP,
PL_TEX_SAMPLE_LINEAR, "feature_map",
NULL, &pos, &pt);
// Obtain HF detail map from bicubic interpolation of LF features
GLSL("vec2 lpos = "$"; \n"
"vec2 lpt = "$"; \n"
"vec2 lsize = vec2(textureSize("$", 0)); \n"
"vec2 frac = fract(lpos * lsize + vec2(0.5)); \n"
"vec2 frac2 = frac * frac; \n"
"vec2 inv = vec2(1.0) - frac; \n"
"vec2 inv2 = inv * inv; \n"
"vec2 w0 = 1.0/6.0 * inv2 * inv; \n"
"vec2 w1 = 2.0/3.0 - 0.5 * frac2 * (2.0 - frac); \n"
"vec2 w2 = 2.0/3.0 - 0.5 * inv2 * (2.0 - inv); \n"
"vec2 w3 = 1.0/6.0 * frac2 * frac; \n"
"vec4 g = vec4(w0 + w1, w2 + w3); \n"
"vec4 h = vec4(w1, w3) / g + inv.xyxy; \n"
"h.xy -= vec2(2.0); \n"
"vec4 p = lpos.xyxy + lpt.xyxy * h; \n"
"float l00 = textureLod("$", p.xy, 0.0).r; \n"
"float l01 = textureLod("$", p.xw, 0.0).r; \n"
"float l0 = mix(l01, l00, g.y); \n"
"float l10 = textureLod("$", p.zy, 0.0).r; \n"
"float l11 = textureLod("$", p.zw, 0.0).r; \n"
"float l1 = mix(l11, l10, g.y); \n"
"float luma = mix(l1, l0, g.x); \n"
// Mix low-resolution tone mapped image with high-resolution
// tone mapped image according to desired strength.
"float highres = clamp(ipt.x, 0.0, 1.0); \n"
"float lowres = clamp(luma, 0.0, 1.0); \n"
"float detail = highres - lowres; \n"
"float base = tone_map(highres); \n"
"float sharp = tone_map(lowres) + detail; \n"
"ipt.x = clamp(mix(base, sharp, "$"), "$", "$"); \n",
pos, pt, lowres,
lowres, lowres, lowres, lowres,
SH_FLOAT(params->contrast_recovery),
SH_FLOAT(tone.output_min), SH_FLOAT_DYN(tone.output_max));
} else {
GLSL("ipt.x = tone_map(ipt.x); \n");
}
// Avoid raising saturation excessively when raising brightness, and
// also desaturate when reducing brightness greatly to account for the
// reduction in gamut volume.
GLSL("vec2 hull = vec2(i_orig, ipt.x); \n"
"hull = ((hull - 6.0) * hull + 9.0) * hull; \n"
"ipt.yz *= min(i_orig / ipt.x, hull.y / hull.x); \n");
}
if (need_gamut_map) {
const struct pl_gamut_map_function *fun = gamut.function;
sh_describef(sh, "gamut map (%s)", fun->name);
pl_assert(obj);
ident_t lut = sh_lut(sh, sh_lut_params(
.object = &obj->gamut.lut,
.var_type = PL_VAR_FLOAT,
.lut_type = SH_LUT_TEXTURE,
.fmt = gamut_fmt,
.method = params->lut3d_tricubic ? SH_LUT_CUBIC : SH_LUT_LINEAR,
.width = gamut.lut_size_I,
.height = gamut.lut_size_C,
.depth = gamut.lut_size_h,
.comps = 4,
.signature = gamut_map_signature(&gamut),
.cache = SH_CACHE(sh),
.fill = fill_gamut_lut,
.priv = &gamut,
));
if (!lut) {
SH_FAIL(sh, "Failed generating gamut-mapping LUT!");
return;
}
// 3D LUT lookup (in ICh space)
const float lut_range = gamut.max_luma - gamut.min_luma;
GLSL("vec3 idx; \n"
"idx.x = "$" * ipt.x + "$"; \n"
"idx.y = 2.0 * length(ipt.yz); \n"
"idx.z = %f * atan(ipt.z, ipt.y) + 0.5;\n"
"ipt = "$"(idx).xyz; \n"
"ipt.yz -= vec2(32768.0/65535.0); \n",
SH_FLOAT(1.0f / lut_range),
SH_FLOAT(-gamut.min_luma / lut_range),
0.5f / M_PI, lut);
if (params->show_clipping) {
GLSL("clip_lo = clip_lo || any(lessThan(idx, vec3(0.0))); \n"
"clip_hi = clip_hi || any(greaterThan(idx, vec3(1.0))); \n");
}
if (params->visualize_lut) {
visualize_gamut_map(sh, params->visualize_rect, lut,
params->visualize_hue, params->visualize_theta,
&gamut);
}
}
// Convert IPT back to linear RGB
GLSL("lmspq = "$" * ipt; \n"
"lms = pow(max(lmspq, 0.0), vec3(1.0/%f)); \n"
"lms = max(lms - vec3(%f), 0.0) \n"
" / (vec3(%f) - %f * lms); \n"
"lms = pow(lms, vec3(1.0/%f)); \n"
"lms *= %f; \n"
"color.rgb = "$" * lms; \n",
ipt2lms,
PQ_M2, PQ_C1, PQ_C2, PQ_C3, PQ_M1,
10000 / PL_COLOR_SDR_WHITE,
SH_MAT3(lms2rgb));
if (params->show_clipping) {
GLSL("if (clip_hi) { \n"
" float k = dot(color.rgb, vec3(2.0 / 3.0)); \n"
" color.rgb = clamp(vec3(k) - color.rgb, 0.0, 1.0); \n"
" float cmin = min(min(color.r, color.g), color.b); \n"
" float cmax = max(max(color.r, color.g), color.b); \n"
" float delta = cmax - cmin; \n"
" vec3 sat = smoothstep(cmin - 1e-6, cmax, color.rgb); \n"
" const vec3 red = vec3(1.0, 0.0, 0.0); \n"
" color.rgb = mix(red, sat, smoothstep(0.0, 0.3, delta)); \n"
"} else if (clip_lo) { \n"
" vec3 hi = vec3(0.0, 0.3, 0.3); \n"
" color.rgb = mix(color.rgb, hi, 0.5); \n"
"} \n");
}
if (need_tone_map) {
if (params->visualize_lut) {
float alpha = need_gamut_map ? powf(cosf(params->visualize_theta), 5.0f) : 1.0f;
visualize_tone_map(sh, params->visualize_rect, alpha, &tone);
}
GLSL("#undef tone_map \n");
}
done:
pl_shader_delinearize(sh, &dst);
GLSL("}\n");
}
// Backwards compatibility wrapper around `pl_shader_color_map_ex`
void pl_shader_color_map(pl_shader sh, const struct pl_color_map_params *params,
struct pl_color_space src, struct pl_color_space dst,
pl_shader_obj *state, bool prelinearized)
{
pl_shader_color_map_ex(sh, params, pl_color_map_args(
.src = src,
.dst = dst,
.prelinearized = prelinearized,
.state = state,
.feature_map = NULL
));
}
void pl_shader_cone_distort(pl_shader sh, struct pl_color_space csp,
const struct pl_cone_params *params)
{
if (!sh_require(sh, PL_SHADER_SIG_COLOR, 0, 0))
return;
if (!params || !params->cones)
return;
sh_describe(sh, "cone distortion");
GLSL("// pl_shader_cone_distort\n");
GLSL("{\n");
pl_color_space_infer(&csp);
pl_shader_linearize(sh, &csp);
pl_matrix3x3 cone_mat;
cone_mat = pl_get_cone_matrix(params, pl_raw_primaries_get(csp.primaries));
GLSL("color.rgb = "$" * color.rgb; \n", sh_var(sh, (struct pl_shader_var) {
.var = pl_var_mat3("cone_mat"),
.data = PL_TRANSPOSE_3X3(cone_mat.m),
}));
pl_shader_delinearize(sh, &csp);
GLSL("}\n");
}