diff --git a/video/csputils.c b/video/csputils.c index 4c9cfbeebd..8ed3b289b1 100644 --- a/video/csputils.c +++ b/video/csputils.c @@ -437,17 +437,18 @@ struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim spc) } } -// Get the relative peak of a transfer curve, that is: (source reference / -// display reference), or 0 if there is none (i.e. source has an absolute peak) -float mp_csp_trc_rel_peak(enum mp_csp_trc trc) +// Get the nominal peak for a given colorspace, based on a known reference peak +// (i.e. the display of a reference white illuminant. This may or may not +// be the actual signal peak) +float mp_csp_trc_nom_peak(enum mp_csp_trc trc, float ref_peak) { switch (trc) { - case MP_CSP_TRC_SMPTE_ST2084: return 0.0; // This has a fixed peak - case MP_CSP_TRC_ARIB_STD_B67: return 12.0; - case MP_CSP_TRC_V_LOG: return 46.0855; + case MP_CSP_TRC_SMPTE_ST2084: return 10000; // fixed peak + case MP_CSP_TRC_ARIB_STD_B67: return 12.0 * ref_peak; + case MP_CSP_TRC_V_LOG: return 46.0855 * ref_peak; } - return 1.0; + return ref_peak; } bool mp_trc_is_hdr(enum mp_csp_trc trc) @@ -781,7 +782,8 @@ bool mp_colorspace_equal(struct mp_colorspace c1, struct mp_colorspace c2) c1.levels == c2.levels && c1.primaries == c2.primaries && c1.gamma == c2.gamma && - c1.peak == c2.peak; + c1.sig_peak == c2.sig_peak && + c1.nom_peak == c2.nom_peak; } // Copy settings from eq into params. diff --git a/video/csputils.h b/video/csputils.h index 0743f2bee5..0406ddf35f 100644 --- a/video/csputils.h +++ b/video/csputils.h @@ -121,7 +121,8 @@ struct mp_colorspace { enum mp_csp_levels levels; enum mp_csp_prim primaries; enum mp_csp_trc gamma; - float peak; // 0 = auto/unknown + float nom_peak; // nominal (absolute) peak. 0 = auto/unknown + float sig_peak; // signal peak, highest value that occurs in the source }; struct mp_csp_params { @@ -226,7 +227,7 @@ int mp_chroma_location_to_av(enum mp_chroma_location mploc); void mp_get_chroma_location(enum mp_chroma_location loc, int *x, int *y); struct mp_csp_primaries mp_get_csp_primaries(enum mp_csp_prim csp); -float mp_csp_trc_rel_peak(enum mp_csp_trc trc); +float mp_csp_trc_nom_peak(enum mp_csp_trc trc, float ref_peak); bool mp_trc_is_hdr(enum mp_csp_trc trc); /* Color conversion matrix: RGB = m * YUV + c diff --git a/video/decode/lavc.h b/video/decode/lavc.h index 689222d872..e76dff50bc 100644 --- a/video/decode/lavc.h +++ b/video/decode/lavc.h @@ -25,6 +25,9 @@ typedef struct lavc_ctx { bool hwdec_failed; bool hwdec_notified; + // For HDR side-data caching + int cached_hdr_peak; + struct mp_image **delay_queue; int num_delay_queue; int max_delay_queue; diff --git a/video/filter/vf_format.c b/video/filter/vf_format.c index 36388e6288..d406d98d9b 100644 --- a/video/filter/vf_format.c +++ b/video/filter/vf_format.c @@ -96,7 +96,7 @@ static int reconfig(struct vf_instance *vf, struct mp_image_params *in, if (p->gamma) out->color.gamma = p->gamma; if (p->peak) - out->color.peak = p->peak; + out->color.sig_peak = p->peak; if (p->chroma_location) out->chroma_location = p->chroma_location; if (p->stereo_in) diff --git a/video/mp_image.c b/video/mp_image.c index 286e40bf62..a4ce6d1cc5 100644 --- a/video/mp_image.c +++ b/video/mp_image.c @@ -395,7 +395,8 @@ void mp_image_copy_attributes(struct mp_image *dst, struct mp_image *src) } dst->params.color.primaries = src->params.color.primaries; dst->params.color.gamma = src->params.color.gamma; - dst->params.color.peak = src->params.color.peak; + dst->params.color.nom_peak = src->params.color.nom_peak; + dst->params.color.sig_peak = src->params.color.sig_peak; if ((dst->fmt.flags & MP_IMGFLAG_YUV) == (src->fmt.flags & MP_IMGFLAG_YUV)) { dst->params.color.space = src->params.color.space; dst->params.color.levels = src->params.color.levels; @@ -668,8 +669,8 @@ void mp_image_params_guess_csp(struct mp_image_params *params) // Guess the nominal peak (independent of the colorspace) if (params->color.gamma == MP_CSP_TRC_SMPTE_ST2084) { - if (!params->color.peak) - params->color.peak = 10000; // As per the spec + if (!params->color.nom_peak) + params->color.nom_peak = 10000; // As per the spec } } diff --git a/video/out/opengl/video.c b/video/out/opengl/video.c index 59dd64cb65..a4cc6cfac8 100644 --- a/video/out/opengl/video.c +++ b/video/out/opengl/video.c @@ -2158,19 +2158,29 @@ static void pass_scale_main(struct gl_video *p) } } -// Adapts the colors from the given color space to the display device's native -// gamut. -static void pass_colormanage(struct gl_video *p, float peak_src, - enum mp_csp_prim prim_src, - enum mp_csp_trc trc_src) +// Adapts the colors to the right output color space. (Final pass during +// rendering) +// If OSD is true, ignore any changes that may have been made to the video +// by previous passes (i.e. linear scaling) +static void pass_colormanage(struct gl_video *p, struct mp_colorspace src, bool osd) { - GLSLF("// color management\n"); - enum mp_csp_trc trc_dst = p->opts.target_trc; - enum mp_csp_prim prim_dst = p->opts.target_prim; - float peak_dst = p->opts.target_brightness; + struct mp_colorspace ref = src; + + if (p->use_linear && !osd) + src.gamma = MP_CSP_TRC_LINEAR; + + // Figure out the target color space from the options, or auto-guess if + // none were set + struct mp_colorspace dst = { + .gamma = p->opts.target_trc, + .primaries = p->opts.target_prim, + .nom_peak = mp_csp_trc_nom_peak(p->opts.target_trc, p->opts.target_brightness), + }; if (p->use_lut_3d) { - // The 3DLUT is always generated against the original source space + // The 3DLUT is always generated against the video's original source + // space, *not* the reference space. (To avoid having to regenerate + // the 3DLUT for the OSD on every frame) enum mp_csp_prim prim_orig = p->image_params.color.primaries; enum mp_csp_trc trc_orig = p->image_params.color.gamma; @@ -2186,87 +2196,66 @@ static void pass_colormanage(struct gl_video *p, float peak_src, } if (gl_video_get_lut3d(p, prim_orig, trc_orig)) { - prim_dst = prim_orig; - trc_dst = trc_orig; + dst.primaries = prim_orig; + dst.gamma = trc_orig; } } - if (prim_dst == MP_CSP_PRIM_AUTO) { + if (dst.primaries == MP_CSP_PRIM_AUTO) { // The vast majority of people are on sRGB or BT.709 displays, so pick // this as the default output color space. - prim_dst = MP_CSP_PRIM_BT_709; + dst.primaries = MP_CSP_PRIM_BT_709; - if (p->image_params.color.primaries == MP_CSP_PRIM_BT_601_525 || - p->image_params.color.primaries == MP_CSP_PRIM_BT_601_625) + if (ref.primaries == MP_CSP_PRIM_BT_601_525 || + ref.primaries == MP_CSP_PRIM_BT_601_625) { // Since we auto-pick BT.601 and BT.709 based on the dimensions, // combined with the fact that they're very similar to begin with, // and to avoid confusing the average user, just don't adapt BT.601 // content automatically at all. - prim_dst = p->image_params.color.primaries; + dst.primaries = ref.gamma; } } - if (trc_dst == MP_CSP_TRC_AUTO) { + if (dst.gamma == MP_CSP_TRC_AUTO) { // Most people seem to complain when the image is darker or brighter // than what they're "used to", so just avoid changing the gamma // altogether by default. The only exceptions to this rule apply to // very unusual TRCs, which even hardcode technoluddites would probably // not enjoy viewing unaltered. - trc_dst = p->image_params.color.gamma; + dst.gamma = ref.gamma; // Avoid outputting linear light or HDR content "by default". For these // just pick gamma 2.2 as a default, since it's a good estimate for // the response of typical displays - if (trc_dst == MP_CSP_TRC_LINEAR || mp_trc_is_hdr(trc_dst)) - trc_dst = MP_CSP_TRC_GAMMA22; + if (dst.gamma == MP_CSP_TRC_LINEAR || mp_trc_is_hdr(dst.gamma)) + dst.gamma = MP_CSP_TRC_GAMMA22; } - if (!peak_src) { - // If the source has no information known, it's display-referred - // (and should be treated relative to the specified desired peak_dst) - peak_src = peak_dst * mp_csp_trc_rel_peak(p->image_params.color.gamma); + // For the src peaks, the correct brightness metadata may be present for + // sig_peak, nom_peak, both, or neither. To handle everything in a generic + // way, it's important to never automatically infer a sig_peak that is + // below the nom_peak (since we don't know what bits the image contains, + // doing so would potentially badly clip). The only time in which this + // may be the case is when the mastering metadata explicitly says so, i.e. + // the sig_peak was already set. So to simplify the logic as much as + // possible, make sure the nom_peak is present and correct first, and just + // set sig_peak = nom_peak if missing. + if (!src.nom_peak) { + // For display-referred colorspaces, we treat it as relative to + // target_brightness + src.nom_peak = mp_csp_trc_nom_peak(src.gamma, p->opts.target_brightness); } - // All operations from here on require linear light as a starting point, - // so we linearize even if trc_src == trc_dst when one of the other - // operations needs it - bool need_gamma = trc_src != trc_dst || prim_src != prim_dst || - peak_src != peak_dst; - if (need_gamma) - pass_linearize(p->sc, trc_src); + if (!src.sig_peak) + src.sig_peak = src.nom_peak; - // Adapt and tone map for a different reference peak brightness - if (peak_src != peak_dst) - { - GLSLF("// HDR tone mapping\n"); - float rel_peak = peak_src / peak_dst; - // Normalize such that 1 is the target brightness (and values above - // 1 are out of range) - GLSLF("color.rgb *= vec3(%f);\n", rel_peak); - // Tone map back down to the range [0,1] - pass_tone_map(p->sc, rel_peak, p->opts.hdr_tone_mapping, - p->opts.tone_mapping_param); - } + MP_DBG(p, "HDR src nom: %f sig: %f, dst: %f\n", + src.nom_peak, src.sig_peak, dst.nom_peak); - // Adapt to the right colorspace if necessary - if (prim_src != prim_dst) { - struct mp_csp_primaries csp_src = mp_get_csp_primaries(prim_src), - csp_dst = mp_get_csp_primaries(prim_dst); - float m[3][3] = {{0}}; - mp_get_cms_matrix(csp_src, csp_dst, MP_INTENT_RELATIVE_COLORIMETRIC, m); - gl_sc_uniform_mat3(p->sc, "cms_matrix", true, &m[0][0]); - GLSL(color.rgb = cms_matrix * color.rgb;) - } - - if (need_gamma) { - // If the target encoding function has a fixed peak, we need to - // un-normalize back to the encoding signal range - if (trc_dst == MP_CSP_TRC_SMPTE_ST2084) - GLSLF("color.rgb *= vec3(%f);\n", peak_dst / 10000); - - pass_delinearize(p->sc, trc_dst); - } + // Adapt from src to dst as necessary + pass_color_map(p->sc, src, dst, p->opts.hdr_tone_mapping, + p->opts.tone_mapping_param); if (p->use_lut_3d) { gl_sc_uniform_sampler(p->sc, "lut_3d", GL_TEXTURE_3D, TEXUNIT_3DLUT); @@ -2407,11 +2396,15 @@ static void pass_draw_osd(struct gl_video *p, int draw_flags, double pts, default: abort(); } - // Subtitle color management, they're assumed to be display-referred - // sRGB by default + // When subtitles need to be color managed, assume they're in sRGB + // (for lack of anything saner to do) if (cms) { - pass_colormanage(p, p->opts.target_brightness, - MP_CSP_PRIM_BT_709, MP_CSP_TRC_SRGB); + static const struct mp_colorspace csp_srgb = { + .primaries = MP_CSP_PRIM_BT_709, + .gamma = MP_CSP_TRC_SRGB, + }; + + pass_colormanage(p, csp_srgb, true); } gl_sc_set_vao(p->sc, mpgl_osd_get_vao(p->osd)); gl_sc_gen_shader_and_reset(p->sc); @@ -2542,8 +2535,7 @@ static void pass_draw_to_screen(struct gl_video *p, int fbo) GLSL(color.rgb = pow(color.rgb, vec3(user_gamma));) } - pass_colormanage(p, p->image_params.color.peak, p->image_params.color.primaries, - p->use_linear ? MP_CSP_TRC_LINEAR : p->image_params.color.gamma); + pass_colormanage(p, p->image_params.color, false); // Draw checkerboard pattern to indicate transparency if (p->has_alpha && p->opts.alpha_mode == ALPHA_BLEND_TILES) { diff --git a/video/out/opengl/video_shaders.c b/video/out/opengl/video_shaders.c index 7b736f1d5d..eded7d59c2 100644 --- a/video/out/opengl/video_shaders.c +++ b/video/out/opengl/video_shaders.c @@ -361,9 +361,11 @@ void pass_delinearize(struct gl_shader_cache *sc, enum mp_csp_trc trc) } // Tone map from a known peak brightness to the range [0,1] -void pass_tone_map(struct gl_shader_cache *sc, float peak, - enum tone_mapping algo, float param) +static void pass_tone_map(struct gl_shader_cache *sc, float ref_peak, + enum tone_mapping algo, float param) { + GLSLF("// HDR tone mapping\n"); + switch (algo) { case TONE_MAPPING_CLIP: GLSL(color.rgb = clamp(color.rgb, 0.0, 1.0);) @@ -373,7 +375,7 @@ void pass_tone_map(struct gl_shader_cache *sc, float peak, float contrast = isnan(param) ? 0.5 : param, offset = (1.0 - contrast) / contrast; GLSLF("color.rgb = color.rgb / (color.rgb + vec3(%f));\n", offset); - GLSLF("color.rgb *= vec3(%f);\n", (peak + offset) / peak); + GLSLF("color.rgb *= vec3(%f);\n", (ref_peak + offset) / ref_peak); break; } @@ -384,20 +386,20 @@ void pass_tone_map(struct gl_shader_cache *sc, float peak, A, C*B, D*E, A, B, D*F, E/F); GLSLHF("}\n"); - GLSLF("color.rgb = hable(color.rgb) / hable(vec3(%f));\n", peak); + GLSLF("color.rgb = hable(color.rgb) / hable(vec3(%f));\n", ref_peak); break; } case TONE_MAPPING_GAMMA: { float gamma = isnan(param) ? 1.8 : param; GLSLF("color.rgb = pow(color.rgb / vec3(%f), vec3(%f));\n", - peak, 1.0/gamma); + ref_peak, 1.0/gamma); break; } case TONE_MAPPING_LINEAR: { float coeff = isnan(param) ? 1.0 : param; - GLSLF("color.rgb = vec3(%f) * color.rgb;\n", coeff / peak); + GLSLF("color.rgb = vec3(%f) * color.rgb;\n", coeff / ref_peak); break; } @@ -406,6 +408,49 @@ void pass_tone_map(struct gl_shader_cache *sc, float peak, } } +// Map colors from one source space to another. These source spaces +// must be known (i.e. not MP_CSP_*_AUTO), as this function won't perform +// any auto-guessing. +void pass_color_map(struct gl_shader_cache *sc, + struct mp_colorspace src, struct mp_colorspace dst, + enum tone_mapping algo, float tone_mapping_param) +{ + GLSLF("// color mapping\n"); + + // All operations from here on require linear light as a starting point, + // so we linearize even if src.gamma == dst.gamma when one of the other + // operations needs it + bool need_gamma = src.gamma != dst.gamma || + src.primaries != dst.primaries || + src.nom_peak != dst.nom_peak || + src.sig_peak > dst.nom_peak; + + if (need_gamma) + pass_linearize(sc, src.gamma); + + // Stretch the signal value to renormalize to the dst nominal peak + if (src.nom_peak != dst.nom_peak) + GLSLF("color.rgb *= vec3(%f);\n", src.nom_peak / dst.nom_peak); + + // Tone map to prevent clipping when the source signal peak exceeds the + // encodable range. + if (src.sig_peak > dst.nom_peak) + pass_tone_map(sc, src.sig_peak / dst.nom_peak, algo, tone_mapping_param); + + // Adapt to the right colorspace if necessary + if (src.primaries != dst.primaries) { + struct mp_csp_primaries csp_src = mp_get_csp_primaries(src.primaries), + csp_dst = mp_get_csp_primaries(dst.primaries); + float m[3][3] = {{0}}; + mp_get_cms_matrix(csp_src, csp_dst, MP_INTENT_RELATIVE_COLORIMETRIC, m); + gl_sc_uniform_mat3(sc, "cms_matrix", true, &m[0][0]); + GLSL(color.rgb = cms_matrix * color.rgb;) + } + + if (need_gamma) + pass_delinearize(sc, dst.gamma); +} + // Wide usage friendly PRNG, shamelessly stolen from a GLSL tricks forum post. // Obtain random numbers by calling rand(h), followed by h = permute(h) to // update the state. Assumes the texture was hooked. diff --git a/video/out/opengl/video_shaders.h b/video/out/opengl/video_shaders.h index 0ee3d81fb5..3bc2f210b8 100644 --- a/video/out/opengl/video_shaders.h +++ b/video/out/opengl/video_shaders.h @@ -38,8 +38,9 @@ void pass_sample_oversample(struct gl_shader_cache *sc, struct scaler *scaler, void pass_linearize(struct gl_shader_cache *sc, enum mp_csp_trc trc); void pass_delinearize(struct gl_shader_cache *sc, enum mp_csp_trc trc); -void pass_tone_map(struct gl_shader_cache *sc, float peak, - enum tone_mapping algo, float param); +void pass_color_map(struct gl_shader_cache *sc, + struct mp_colorspace src, struct mp_colorspace dst, + enum tone_mapping algo, float tone_mapping_param); void pass_sample_deband(struct gl_shader_cache *sc, struct deband_opts *opts, AVLFG *lfg);