vo_gpu_next: linearize sRGB as a pure 2.2 power function

sRGB reference display is defined a 2.2 gamma device. To preserve the
look of the sRGB as mastered on such device, linearize it as such.

Note that sRGB encoding is piecewise with linear segment, which creates
mismatch to pure power 2.2 function, but this is intended to be viewed
on such display.

See:
IEC 61966-2-1-1999
https://community.acescentral.com/t/srgb-piece-wise-eotf-vs-pure-gamma/4024
https://github.com/KhronosGroup/DataFormat/issues/19
https://gitlab.freedesktop.org/pq/color-and-hdr/-/issues/12
https://github.com/dylanraga/win11hdr-srgb-to-gamma2.2-icm
This commit is contained in:
Kacper Michajłow committed 2025-10-29 15:36:15 +01:00
1 parent 813c112329
commit 3e628c8d64
5 files changed
+61

No files matched your search

@@ -0,0 +1 @@
add `--treat-srgb-as-power22`
+10
View File
@@ -7354,6 +7354,16 @@ them.
transfer function is SDR. This way you can control SDR output separately
from HDR output.
``--treat-srgb-as-power22=<no|input|output|both|auto>``
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=<value>``
Specifies the algorithm used for tone-mapping images onto the target
display. This is relevant for both HDR->SDR conversion as well as gamut
+3
View File
@@ -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)},
+1
View File
@@ -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;
+46
View File
@@ -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: <https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/456>
#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);