vo_gpu_next: add --target-colorspace-hint-mode option

Useful to actually set target display paramters to colorspace hint.
This commit is contained in:
Kacper Michajłow committed 2025-07-11 21:53:58 +02:00
1 parent 5b1df3c5d9
commit ce076de31f
3 files changed
+117 -32

No files matched your search

@@ -0,0 +1 @@
add `--target-colorspace-hint-mode` option
+82 -2
View File
@@ -7023,6 +7023,84 @@ them.
if the display signals support for HDR colorspace. if the display signals support for HDR colorspace.
Requires a supporting driver and ``--vo=gpu-next``. (Default: ``no``) Requires a supporting driver and ``--vo=gpu-next``. (Default: ``no``)
``--target-colorspace-hint-mode=<target|source>``
Select which metadata to use for the ``--target-colorspace-hint``.
(Only for ``--vo=gpu-next``)
target
Uses metadata based on the target display's actual capabilities. This
mode adapts the source content to the target display before output.
Note: HDR primaries are not overridden by the ``--target-prim`` option
this only affects the enclosing container for the colorspace.
source
Uses the source content's metadata. This is the traditional
"HDR passthrough" mode (SDR too), where it is assumed that the compositor
and display will handle the colorspace directly and perform any necessary
mappings.
Default is ``target``. If target display parameters are not available, this
will fall back to ``source``. Note that this is done on individual properties
basis, i.e. it will merge source params into target for unknown properties,
though not the other way around.
``--target-*`` options override the metadata in both modes.
.. note::
It is highly recommended to use ``--target-colorspace-hint=<auto|yes>``
to ensure the output colorspace is set correctly. This is crucial for
all non-sRGB content, even SDR, to allow the compositor, driver, and
display to properly interpret the signal.
Unfortunately, it's not as easy as it sounds. While mpv performs
high-quality color processing, we cannot guarantee what will happen
after the signal leaves mpv. Therefore, you may need to adjust additional
settings to ensure proper output. A one-size-fits-all default is not
feasible.
Now with the backstory out of the way:
For HDR output the default of ``target`` should work fine, it will
automatically infer the best target HDR parameters and surface format.
For compatibility, displays are assumed to be in HDR mode, unless it's
reported otherwise. (you can override this with ``--target-trc``).
This way the HDR metadata is set and hopefully the compositor will
handle the rest. If the input is SDR, it will be converted to PQ with
primaries set to source values.
For SDR output, for targets where mpv cannot determine whether the target
is HDR or SDR, you can use ``source`` mode. Metadata set will match the
input colorspace. In case of HDR input, it will be passthrough as-is.
Alternatively, you can use ``target`` mode and set ``--target-trc`` to
a SDR transfer function. This way any input will be converted to SDR.
Use the stats display to verify the input and output colorspace settings.
TL;DR: Use ``--target-colorspace-hint=auto`` and adjust ``--target-*``
parameters to match your target display capabilities, until it looks best
for you. Use `Conditional auto profiles`_ for specific adjustments. Avoid
using ``--target-colorspace-hint=no`` unless it's sRGB content, but even
then it's better to set the colorspace metadata.
.. note::
Additional chatter about the "HDR passthrough" mode: There is a belief
that this mode should send the source HDR signal as-is to the display,
and the display will magically handle it, no matter what. This is not
always true. In some cases it is better to send the HDR signal
tone-mapped to the target display's capabilities, and the best way to do
this is within mpv itself.
This is generally handled by either the compositor or the GPU driver.
You can probably find HDR "calibration" options somewhere in your system.
You can choose which metadata to send to the display and manually tweak
it using the ``--target-*`` options. You can also try
``--inverse-tone-mapping`` if you want to make everything appear more
HDR-like.
Your mileage may vary, this highly depends on the target display, there
is no single answer, but try experimenting, you may be surprised.
``--target-prim=<value>`` ``--target-prim=<value>``
Specifies the primaries of the display. Video colors will be adapted to Specifies the primaries of the display. Video colors will be adapted to
this colorspace when ICC color management is not being used. Valid values this colorspace when ICC color management is not being used. Valid values
@@ -7252,8 +7330,10 @@ them.
Specifies the contrast (slope) at the knee point. Defaults to 1.0. Specifies the contrast (slope) at the knee point. Defaults to 1.0.
``--inverse-tone-mapping`` ``--inverse-tone-mapping``
If set, allows inverse tone mapping (expanding SDR to HDR). Not supported If set, allows inverse tone mapping (expanding dynamic range). Can be used
by all tone mapping curves. Use with caution. (``--vo=gpu-next`` only) for upscaling SDR content to HDR, or for making HDR content brighter.
Not supported by all tone mapping curves. Use with caution.
(``--vo=gpu-next`` only)
``--tone-mapping-max-boost=<1.0..10.0>`` ``--tone-mapping-max-boost=<1.0..10.0>``
Upper limit for how much the tone mapping algorithm is allowed to boost Upper limit for how much the tone mapping algorithm is allowed to boost
+34 -30
View File
@@ -174,6 +174,7 @@ struct gl_next_opts {
struct user_lut image_lut; struct user_lut image_lut;
struct user_lut target_lut; struct user_lut target_lut;
int target_hint; int target_hint;
int target_hint_mode;
char **raw_opts; char **raw_opts;
}; };
@@ -205,6 +206,7 @@ const struct m_sub_options gl_next_conf = {
{"image-lut-type", OPT_CHOICE_C(image_lut.type, lut_types)}, {"image-lut-type", OPT_CHOICE_C(image_lut.type, lut_types)},
{"target-lut", OPT_STRING(target_lut.opt), .flags = M_OPT_FILE}, {"target-lut", OPT_STRING(target_lut.opt), .flags = M_OPT_FILE},
{"target-colorspace-hint", OPT_CHOICE(target_hint, {"auto", -1}, {"no", 0}, {"yes", 1})}, {"target-colorspace-hint", OPT_CHOICE(target_hint, {"auto", -1}, {"no", 0}, {"yes", 1})},
{"target-colorspace-hint-mode", OPT_CHOICE(target_hint_mode, {"target", 0}, {"source", 1})},
// No `target-lut-type` because we don't support non-RGB targets // No `target-lut-type` because we don't support non-RGB targets
{"libplacebo-opts", OPT_KEYVALUELIST(raw_opts)}, {"libplacebo-opts", OPT_KEYVALUELIST(raw_opts)},
{0}, {0},
@@ -1003,12 +1005,15 @@ static bool draw_frame(struct vo *vo, struct vo_frame *frame)
struct ra_swapchain *sw = p->ra_ctx->swapchain; struct ra_swapchain *sw = p->ra_ctx->swapchain;
bool pass_colorspace = false; bool pass_colorspace = false;
struct pl_color_space target_csp; struct pl_color_space target_csp = {0};
// Assume HDR is supported, if query is not available
// TODO: Implement this for all backends // TODO: Implement this for all backends
target_csp = sw->fns->target_csp if (sw->fns->target_csp)
? sw->fns->target_csp(sw) target_csp = sw->fns->target_csp(sw);
: (struct pl_color_space){ .transfer = PL_COLOR_TRC_PQ }; // Assume HDR is supported, if query is not available
if (target_csp.transfer == PL_COLOR_TRC_UNKNOWN) {
target_csp = (struct pl_color_space){
.transfer = opts->target_trc ? opts->target_trc : PL_COLOR_TRC_PQ };
}
if (!pl_color_transfer_is_hdr(target_csp.transfer)) { if (!pl_color_transfer_is_hdr(target_csp.transfer)) {
// Don't use reported display peak in SDR mode. Mostly because libplacebo // Don't use reported display peak in SDR mode. Mostly because libplacebo
// forcefully switches to PQ if hinting hdr metadata, ignoring the transfer // forcefully switches to PQ if hinting hdr metadata, ignoring the transfer
@@ -1016,6 +1021,8 @@ static bool draw_frame(struct vo *vo, struct vo_frame *frame)
// very specific usecase, needs proper calibration, users can set it manually. // very specific usecase, needs proper calibration, users can set it manually.
target_csp.hdr.max_luma = 0; target_csp.hdr.max_luma = 0;
target_csp.hdr.min_luma = 0; target_csp.hdr.min_luma = 0;
target_csp.hdr.max_cll = 0;
target_csp.hdr.max_fall = 0;
} }
struct pl_color_space hint; struct pl_color_space hint;
@@ -1023,36 +1030,33 @@ static bool draw_frame(struct vo *vo, struct vo_frame *frame)
(p->next_opts->target_hint == -1 && (p->next_opts->target_hint == -1 &&
pl_color_transfer_is_hdr(target_csp.transfer)); pl_color_transfer_is_hdr(target_csp.transfer));
if (target_hint && frame->current) { if (target_hint && frame->current) {
hint = frame->current->params.color; const struct pl_color_space *source = &frame->current->params.color;
float target_peak = opts->target_peak ? opts->target_peak : target_csp.hdr.max_luma; const struct pl_color_space *target = &target_csp;
float target_trc = opts->target_trc; hint = *(p->next_opts->target_hint_mode == 0 ? target : source);
if (!target_trc) { if (pl_color_transfer_is_hdr(hint.transfer) && !pl_primaries_valid(&hint.hdr.prim))
// If we are targeting HDR display, repack SDR to target trc. This way pl_color_space_merge(&hint, source);
// we ensure that conversion is done by mpv and not by the compositor, // Infer missing bits now. This is important so that we don't lose
// which is often worse in terms of quality. Do this only if we are sure // information after user option overrides. For example, if the user
// that display is in HDR mode, i.e. target_csp() reported HDR transfer. // sets target_trc to PQ, but the hint(source) is SDR, we want to fill
// libplacebo already does the same when peak > 203, but we want this // in SDR luminance values instead of the default PQ range.
// for any peak value. pl_color_space_infer(&hint);
// Note: We don't have information if the compositor/driver is able to // Always prefer target luminance and transfer for inverse tone mapping
// reconfigure the display, currently target_csp() is only implemented if (pl_color_transfer_is_hdr(target->transfer) && opts->tone_map.inverse) {
// for D3D11 where we know automatic reconfiguration never happens. hint.transfer = target->transfer;
// Also I like to minimize possible reconfigurations, and outputting SDR hint.hdr.max_luma = target->hdr.max_luma;
// in PQ is fine. hint.hdr.min_luma = target->hdr.min_luma;
if (pl_color_transfer_is_hdr(target_csp.transfer) && target_csp.hdr.max_luma > 0 && hint.hdr.max_cll = target->hdr.max_cll;
!pl_color_transfer_is_hdr(frame->current->params.color.transfer)) hint.hdr.max_fall = target->hdr.max_fall;
{
target_trc = target_trc ? target_trc : target_csp.transfer;
}
} }
if (p->ra_ctx->fns->pass_colorspace && p->ra_ctx->fns->pass_colorspace(p->ra_ctx)) if (p->ra_ctx->fns->pass_colorspace && p->ra_ctx->fns->pass_colorspace(p->ra_ctx))
pass_colorspace = true; pass_colorspace = true;
if (opts->target_prim) if (opts->target_prim)
hint.primaries = opts->target_prim; hint.primaries = opts->target_prim;
if (target_trc) if (opts->target_trc)
hint.transfer = target_trc; hint.transfer = opts->target_trc;
if (target_peak) if (opts->target_peak)
hint.hdr.max_luma = target_peak; hint.hdr.max_luma = opts->target_peak;
apply_target_contrast(p, &hint, target_csp.hdr.min_luma); apply_target_contrast(p, &hint, hint.hdr.min_luma);
if (!pass_colorspace) if (!pass_colorspace)
pl_swapchain_colorspace_hint(p->sw, &hint); pl_swapchain_colorspace_hint(p->sw, &hint);
} else if (!target_hint) { } else if (!target_hint) {