diff --git a/DOCS/interface-changes/treat-srgb-as-power22.txt b/DOCS/interface-changes/treat-srgb-as-power22.txt new file mode 100644 index 0000000000..1729d1f0e7 --- /dev/null +++ b/DOCS/interface-changes/treat-srgb-as-power22.txt @@ -0,0 +1 @@ +add `--treat-srgb-as-power22` diff --git a/DOCS/man/options.rst b/DOCS/man/options.rst index 4a983ac81e..e90a2e2875 100644 --- a/DOCS/man/options.rst +++ b/DOCS/man/options.rst @@ -7354,6 +7354,16 @@ them. transfer function is SDR. This way you can control SDR output separately from HDR output. +``--treat-srgb-as-power22=`` + When enabled, sRGB is (de)linearized using a pure power 2.2 curve instead of + the standard sRGB piecewise transfer function. + + ``auto`` behaves like ``both``, with possible platform-specific adjustments + to ensure a consistent appearance. Depending on the platform, the sRGB EOTF + used by the system compositor may differ. + + The default is ``input``. (Only for ``--vo=gpu-next``) + ``--tone-mapping=`` Specifies the algorithm used for tone-mapping images onto the target display. This is relevant for both HDR->SDR conversion as well as gamut diff --git a/video/out/gpu/video.c b/video/out/gpu/video.c index b09d9d9671..3492844426 100644 --- a/video/out/gpu/video.c +++ b/video/out/gpu/video.c @@ -409,6 +409,7 @@ static const struct gl_video_opts gl_video_opts_def = { .early_flush = -1, .shader_cache = true, .hwdec_interop = "auto", + .treat_srgb_as_power22 = 1, // input }; static OPT_STRING_VALIDATE_FUNC(validate_error_diffusion_opt); @@ -445,6 +446,8 @@ const struct m_sub_options gl_video_conf = { M_RANGE(10, 10000)}, {"hdr-reference-white", OPT_CHOICE(hdr_reference_white, {"auto", 0}), M_RANGE(10, 10000)}, + {"treat-srgb-as-power22", OPT_CHOICE(treat_srgb_as_power22, + {"no", 0}, {"input", 1}, {"output", 2}, {"both", 1|2}, {"auto", 1|2|4})}, {"target-contrast", OPT_CHOICE(target_contrast, {"auto", 0}, {"inf", -1}), M_RANGE(10, 10 / PL_COLOR_HDR_BLACK)}, {"target-gamut", OPT_CHOICE_C(target_gamut, pl_csp_prim_names)}, diff --git a/video/out/gpu/video.h b/video/out/gpu/video.h index d96d623ef7..096e3243b9 100644 --- a/video/out/gpu/video.h +++ b/video/out/gpu/video.h @@ -139,6 +139,7 @@ struct gl_video_opts { int target_trc; int target_peak; int hdr_reference_white; + int treat_srgb_as_power22; int target_contrast; int target_gamut; struct gl_tone_map_opts tone_map; diff --git a/video/out/vo_gpu_next.c b/video/out/vo_gpu_next.c index 619d45751b..a777c1db75 100644 --- a/video/out/vo_gpu_next.c +++ b/video/out/vo_gpu_next.c @@ -675,6 +675,13 @@ static bool map_frame(pl_gpu gpu, pl_tex *tex, const struct pl_source_frame *src if (opts->hdr_reference_white && !pl_color_transfer_is_hdr(frame->color.transfer)) frame->color.hdr.max_luma = opts->hdr_reference_white; + + if (opts->treat_srgb_as_power22 & 1 && frame->color.transfer == PL_COLOR_TRC_SRGB) { + // The sRGB EOTF is a pure gamma 2.2 function. See reference display in + // IEC 61966-2-1-1999. Linearize sRGB to display light. + frame->color.transfer = PL_COLOR_TRC_GAMMA22; + } + if (fp->hwdec) { struct mp_imgfmt_desc desc = mp_imgfmt_get_desc(par.imgfmt); @@ -1246,6 +1253,34 @@ static bool draw_frame(struct vo *vo, struct vo_frame *frame) pl_frame_from_swapchain(&target, &swframe); bool strict_sw_params = target_hint && !pass_colorspace && p->next_opts->target_hint_strict; apply_target_options(p, &target, hint.hdr.min_luma, strict_sw_params); + if (target.color.transfer == PL_COLOR_TRC_SRGB) { + // sRGB reference display is pure 2.2 power function, see IEC 61966-2-1-1999. + if (opts->treat_srgb_as_power22 & 2) + target.color.transfer = PL_COLOR_TRC_GAMMA22; + + // TODO: Vulkan on Wayland currently interprets VK_COLOR_SPACE_SRGB_NONLINEAR_KHR + // in ambiguous way, depending if compositor advertises sRGB support. + // There is currently no clear path forward to resolve this ambiguity. + // Depending how it's resolved in Wayland Protocol, Mesa, things will + // change. + // See: +#ifdef _WIN32 + // Windows uses the sRGB piecewise function. Send piecewise sRGB to + // Windows in HDR mode so that it can be converted to PQ, the same way + // as mpv does internally. Note that in SDR mode, even with ACM enabled, + // Windows assumes the display is sRGB. It doesn't perform gamma + // conversion, or any conversions would roundtrip back to sRGB. + // In which case the EOTF depends on the display. + // Ideally, compositors would agree on how to handle sRGB, but I’ll + // leave that part of the story for the reader to explore. + // Note: Older Windows versions, without ACM, were not able to convert + // sRGB to PQ output. We are not concerned about this case, as it would + // look wrong anyway. + bool target_pq = !target_unknown && target_csp.transfer == PL_COLOR_TRC_PQ; + if (opts->treat_srgb_as_power22 & 4 && target_pq) + target.color.transfer = PL_COLOR_TRC_SRGB; +#endif + } update_overlays(vo, p->osd_res, (frame->current && opts->blend_subs) ? OSD_DRAW_OSD_ONLY : 0, PL_OVERLAY_COORDS_DST_FRAME, &p->osd_state, &target, frame->current); @@ -1623,6 +1658,17 @@ static void video_screenshot(struct vo *vo, struct voctrl_screenshot *args) target.color = pl_color_space_srgb; } + // sRGB reference display is pure 2.2 power function, see IEC 61966-2-1-1999. + // Round-trip back to sRGB if the source is also sRGB. In other cases, we + // use piecewise sRGB transfer function, as this is likely the be expected + // for file encoding. + if (opts->treat_srgb_as_power22 & 1 && + target.color.transfer == PL_COLOR_TRC_SRGB && + mpi->params.color.transfer == PL_COLOR_TRC_SRGB) + { + target.color.transfer = PL_COLOR_TRC_GAMMA22; + } + apply_crop(&image, src, mpi->params.w, mpi->params.h); apply_crop(&target, dst, fbo->params.w, fbo->params.h); update_tm_viz(&pars->color_map_params, &target);