Skip to content

Conversation

@mahkoh
Copy link
Contributor

@mahkoh mahkoh commented Nov 22, 2025

You can test this with the following file: test_pattern-PQ.jxl.gz

This image contains gradients that go up to 10,000 cd/m^2. If you set the paper white luminance in your compositor to 1% of its normal value, you create a lot of artificial head room. Without this patch, the entire image becomes dark. With this patch, the bright end of the gradient continues to fully light up the output.

@mahkoh
Copy link
Contributor Author

mahkoh commented Nov 22, 2025

This patch relies on the assumption that the reference luminance can be compared to the max_target_luminance, which is not entirely clear to me: https://gitlab.freedesktop.org/pq/color-and-hdr/-/issues/52. However, mpv already relies on this property.

@mahkoh mahkoh force-pushed the jorth/anchoring branch 2 times, most recently from b7b3384 to bac8a9a Compare November 22, 2025 18:16
if (sw->fns->target_csp) {
float source_max_luma = 0.0f;
if (frame->current) {
source_max_luma = frame->current->params.color.hdr.max_luma;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value is base HDR10 value. And won't be in fact used by libplacebo if HDR10+/DV metadata is also available.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do I get the correct value here?

Copy link
Member

@kasper93 kasper93 Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something like this, but I'm still reluctant of using those values to infer any target parameters. Depending on the source this may or may not be valid/useful value. It may be transfer nominal value, which we have to avoid for example.

            pl_color_space_nominal_luma_ex(pl_nominal_luma_params(
                .color      = &frame->current->params.color,
                .metadata   = PL_HDR_METADATA_ANY,
                .scaling    = PL_HDR_NITS,
                .out_max    = &source_max_luma,
            ));

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some point libplacebo has to choose some value to determine if and how it needs to do tone mapping. That is the value I'm interested in. It is also ok if this value is too large since in that case the worst that should happen is that we de-optimize by using PQ instead of the preferred SDR transfer function.

Copy link
Contributor Author

@mahkoh mahkoh Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code below again, we don't actually need the max_luma of the source. We only need to know if libplacebo is going to do tone mapping when targeting an SDR display, because that's the case in which it will need accurate metadata from the target.

            // Otherwise, if the max_luma of the source is brighter than paper white, then
            // libplacebo will always perform tone mapping. To do this correctly, it will
            // need to know the exact max_luma of the display.
            if (source_max_luma > PL_COLOR_SDR_WHITE)
                csp.transfer = PL_COLOR_TRC_PQ;

Copy link
Contributor Author

@mahkoh mahkoh Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the first commit it would have to be

        if (wd->csp.hdr.max_luma != PL_COLOR_SDR_WHITE) {

See below for why not just >.

Also note that if requested luminances is lower than PL_COLOR_SDR_WHITE, we can also switch to PQ, but we can just ignore this. This is what we actually do, because we don't want to map SDR lower than nominal ref.

If the reference luminance is higher than the max target luminance, then the first commit will cause wd->csp.hdr.max_luma < PL_COLOR_SDR_WHITE. In this case I don't understand why you don't want to tone-map to max_luma. This can obviously only happen in weird situations like when the user sets his brightness higher than 100% but it is easy to handle. Does libplacebo not handle tone mapping to below PL_COLOR_SDR_WHITE correctly?

Either way, if ref_lum==max_lum we should be outputting "SDR" regardless of the actual luminances.

The first commit ensures that in that case wd->csp.hdr.max_luma == PL_COLOR_SDR_WHITE.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason that I don't want to use this coarse hammer is that it means that mpv will always ignore the preferred transfer function when the user is using an HDR display even if mpv is playing an SDR video.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, if that is a piece of contention, then I can drop the second commit in favor of what you've suggested. The user-facing improvement is in the first commit.

Copy link
Member

@kasper93 kasper93 Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason that I don't want to use this coarse hammer is that it means that mpv will always ignore the preferred transfer function when the user is using an HDR display even if mpv is playing an SDR video.

This is actually expected behavior. I prefer PQ output on HDR targets, even for SDR video. You can change it by using target-mode=source.

This is how =target mode works on Windows. Where target_csp is on the wire format, hence whenever HDR mode is enabled, we output PQ. And the conversion to PQ is done inside mpv.

I know Wayland is much smarter than that, and uses preferred transfer, which most likely is not related to the actual display, but if preferred transfer function is not compatible with mpv definition of HDR, I would prefer to keep it simple and avoid doing "complex" heuristics and fallback to PQ.

Feel free to correct my thinking, I don't have strong opinion how this should work. I just feel like the target_csp should not depend on source. Well the hint can depend on the source, so maybe I'm wrong here.


My thinking is, if Wayland preferred function is "HDR-like" gamma2.2, then we should respect the HDR of it. I understand that in some cases, the definition may be compatible with mpv's SDR output and we can use gamma2.2 in this case, but this is slightly messy with all the heuristics. But I still don't see why it should depend on source itself.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're not wrong. The problem is pl_color_transfer_is_hdr and the way it is used. As others have already pointed out, there are no HDR transfer functions as far as wayland is concerned. Ideally, all of the uses of that function should be removed from both mpv and libplacebo and the transfer function of the target hint should only be taken into account as late as possible: Once libplacebo knows everything there is to know about the image it is going to render, it decides if the preferred TF is suitable for that and otherwise switches to a different one.

Getting there would be a much larger change and my thinking was that going the other way and passing the source into the windowing backend would get us roughly the same result with much less effort.

@mahkoh
Copy link
Contributor Author

mahkoh commented Nov 24, 2025

I've completely rewritten the code that decides when to switch to PQ. I've resolved all related threads since the referenced code was outdated. There was indeed at least one case where inverse tone mapping was not taken into account.

@github-actions
Copy link

github-actions bot commented Nov 24, 2025

// when ref_luma != PL_COLOR_SDR_WHITE.
float a = wd->min_luma;
float b = (PL_COLOR_SDR_WHITE - a) / (wd->ref_luma - a);
wd->csp.hdr.min_luma = (wd->csp.hdr.min_luma - a) * b + a;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value can realistically become negative and not all components might handle this. So this should probably be wrapped in MPMAX.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PS: a and b are chosen such that paper white in the compositor space maps to paper white in libplacebo and paper black (SDR black, all bits 0) maps to itself. This is the inverse of how I've implemented this mapping in my compositor and KDE seems to do something similar.

Copy link
Contributor Author

@mahkoh mahkoh Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the discussion in https://gitlab.freedesktop.org/pq/color-and-hdr/-/issues/52#note_3206665, I've changed the implementation in my compositor to map min_luma (primary color volume) of the source space to min_luma (primary color volume) of the target space. It also seems that KDE is doing the same which I have tried to confirm by comparing the behavior of a gradient from black to dark gray on my compositor and KDE. I've also adjusted https://github.com/mahkoh/wayland-color-test to behave like this.

The problem is that I don't know the minimum primary color volume luminance assumed by libplacebo. There is PL_COLOR_HDR_BLACK but I don't know if that applies in general or only if an HDR transfer function is being used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that I don't know the minimum primary color volume luminance assumed by libplacebo. There is PL_COLOR_HDR_BLACK but I don't know if that applies in general or only if an HDR transfer function is being used.

libplacebo assumes 0 (PL_COLOR_HDR_BLACK) for PQ transfer and 1000:1 contrast for anything else. Unless it is specified otherwise, which it is currently based on values reported by compositor.

if (csp.hdr.max_luma < PL_COLOR_SDR_WHITE)
csp.transfer = PL_COLOR_TRC_PQ;

// NOTE: If source_max_luma > csp.hdr.max_luma, which seems to be missing from
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact this source_max_luma > csp.hdr.max_luma would be more in line with what would be assumed. i.e. there is only tonemapping in this case. For example there is no tonemapping in case source_max_luma > PL_COLOR_SDR_WHITE && source_max_luma <= csp.hdr.max_luma, while of course we still need to set luminances to notify compositor about this.

That being said, with all this logic, which cases do you want to preserve to use "SDR" transfer? In practice we assume PL_COLOR_SDR_WHITE for all "SDR" transfers.

Given that it is always safe to use PQ transfer and "SDR" transfer definition is different in mpv, maybe it would be more readable to inverse this logic and check when it's safe to use preferred transfer and use PQ in all other cases.

Actually I think the check should be simply

if (csp.hdr.max_luma != PL_COLOR_SDR_WHITE && !pl_color_transfer_is_hdr(csp.transfer))
     csp.transfer = PL_COLOR_TRC_PQ;

I'm ignoring here min_luma, because I assume that we can allow BPC to be done by mpv and still use G2.2 without notifying luminances. If that's not the cases, it should also check if csp.hdr.min_luma != PL_COLOR_SDR_WHITE / PL_COLOR_SDR_CONTRAST.

Which in fact could be rewritten to use pl_color_space_nominal_luma_ex to get nominal luminances and compare if the are the same as reported.

Note that while in case of csp.hdr.max_luma > PL_COLOR_SDR_WHITE, we will internally switch to PQ anyway, because anything above sdr white is considered "hdr".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mahkoh: any comment on this? I can merge as-is if you want, but I feel like it might bite us. Anyway, @sfan5 is pushing for release, so we should merge this before.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That being said, with all this logic, which cases do you want to preserve to use "SDR" transfer? In practice we assume PL_COLOR_SDR_WHITE for all "SDR" transfers.

The preferred transfer function is preserved if

source_max_luma <= PL_COLOR_SDR_WHITE <= csp.hdr.max_luma

or

PL_COLOR_SDR_WHITE == csp.hdr.max_luma

Given that it is always safe to use PQ transfer and "SDR" transfer definition is different in mpv, maybe it would be more readable to inverse this logic and check when it's safe to use preferred transfer and use PQ in all other cases.

Maybe. I'd have to write it and see how it looks with comments.

Copy link
Member

@kasper93 kasper93 Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for bike-shedding this too much. This HDR thing is messy at times.

@mahkoh
Copy link
Contributor Author

mahkoh commented Dec 19, 2025

I've moved the first commit to #17149. The rest of this PR is only the optimization that limits the cases in which the preferred transfer function is ignored.

@sfan5 sfan5 changed the title wayland: luminance improvements wayland: switch to PQ when necessary Dec 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants