Skip to content

Conversation

@kasper93
Copy link
Member

Mesa's Wayland Vulakn WSI doesn't set luminances for SDR transfer, as they are only set for HDR. Which makes sense. In this case however, we have to use default BT.1886 luminance values as defined by Wayland protocol.

I don't like it at all... but will do the job for now.

@kasper93
Copy link
Member Author

@mahkoh: This ugly thing should work... for now.

@kasper93 kasper93 force-pushed the wayland_bt1886 branch 2 times, most recently from 859c987 to 3f8c45d Compare January 25, 2026 17:49
Mesa's Wayland Vulakn WSI doesn't set luminances for SDR transfer, as
they are only set for HDR. Which makes sense. In this case however, we
have to use default BT.1886 luminance values as defined by Wayland
protocol.

I don't like it at all... but will do the job for now.
@Lompik
Copy link

Lompik commented Jan 25, 2026

why ?

Edit the next day: I did found why eventually: Mesa WSI does indeed select WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_BT1886 with VK_COLOR_SPACE_BT709_NONLINEAR_EXT

@kasper93
Copy link
Member Author

why ?

See discussion in #17322, transfer function between mpv and compositor has to agree, else compositor will interpret it incorrectly.

@Lompik
Copy link

Lompik commented Jan 25, 2026

move that to dmabuf-wayland then. gpu-next doesn't even set a transfer function for opengl.

@kasper93
Copy link
Member Author

move that to dmabuf-wayland then.

dmabuf-wayland doesn't use Vulkan WSI, and is not related to what happens here at all.

gpu-next doesn't even set a transfer function for opengl.

So, you use default, and everything works.

@mahkoh
Copy link
Contributor

mahkoh commented Jan 25, 2026

I've written a small utility to test that with this change the same output is produced whether the compositor supports bt1886 or not: https://github.com/mahkoh/wl-proxy/tree/master/apps/wl-cm-filter

You'd use it like this to filter out the bt1886 transfer function:

$ wl-cm-filter --transfer-functions bt1886 mpv

However, when I did that, I noticed something weird: The OSD still showed that bt1886 was being used. So then I ran

~$ WAYLAND_DEBUG=1 mpv 2>&1 | rg set_tf_named
[3477007.694] {mesa vk display queue}  -> wp_image_description_creator_params_v1#87.set_tf_named(9)
[3477051.106] {mesa vk display queue}  -> wp_image_description_creator_params_v1#75.set_tf_named(9)

And indeed even by default mpv always sets srgb as the transfer function and not bt1886 but still shows bt1886 as being used in the OSD.

Can you explain what's going on here?

@Lompik
Copy link

Lompik commented Jan 25, 2026

dmabuf-wayland doesn't use Vulkan WSI

but it does use the bt.1886 wayland transfer function and we do need the correct min/max to agree with compositor.

bt.1886 in the case gpu-next / vulkan full SDR pipeline is a noop. min_luma max_luma doesn't affect it.
in gpu-next, bt.1886 is used internally in mpv then taken as is to gamma2.2/srgb screens. That's the whole selling point of bt.1886 in gpu-next (plus black point compensation).

Anyway, the mesa vulkan WSI gpu-next / vulkan will only set a tf if a color space other that VK_COLOR_SPACE_PASS_THOUGH is used.

@kasper93
Copy link
Member Author

And indeed even by default mpv always sets srgb as the transfer function and not bt1886 but still shows bt1886 as being used in the OSD.

Can you explain what's going on here?

Is your input bt.1886 by any chance? --sdr-adjust-gamma=<auto|yes|no> is default for SDR. Because people complain when their gamma is converted.

So when mpv output sRGB, it will still by default not convert bt.1886 to sRGB. Should be fixed by --sdr-adjust-gamma=yes.

Frankly it's not clear to me what to do with bt.709/bt.1886 content, I'm even inclined to always use infinite contrast (gamma 2.4) for this when converting to anything else.

The big issue is that virtually all content, is tagged as bt.1886. PC capture, movies, everything... And for me the adaptation that it does, based on contrast is mostly useful only for TV content.

@kasper93
Copy link
Member Author

but it does use the bt.1886 wayland transfer function and we do need the correct min/max to agree with compositor.

  1. dmabuf-wayland doesn't do any processing on mpv side
  2. We can directly set lumiances in this case, so no need to use default ones.

I can check double check later what we set, but this is completely separate and different case to gpu-next.

@mahkoh
Copy link
Contributor

mahkoh commented Jan 25, 2026

Is your input bt.1886 by any chance?

I tried the PQ test image, a PQ video, a movie, and an OBS screen recording and mpv always shows bt1886 as the display transfer function.

For the PQ inputs, it shows PQ as the input transfer function.


I tested the code in this PR and it is not being invoked for the test images from #17322 (comment).

@kasper93
Copy link
Member Author

kasper93 commented Jan 25, 2026

Hard to tell, could you dump a --log-file=mpv.log? And try with libplacebo from master, like I said before, there were changes there.

Generally flow is like so: select preferred or fallback to sRGB. Old libplacebo though was always using bt.1886 for VK_COLOR_SPACE_SRGB_NONLINEAR_KHR output. So this is what you are seeing likely.

EDIT: and by old I mean current stable version...

@mahkoh
Copy link
Contributor

mahkoh commented Jan 25, 2026

And try with libplacebo from master

I don't know how to do that.

mpv.log

@kasper93
Copy link
Member Author

And try with libplacebo from master

I don't know how to do that.

mpv.log

Yes, it uses VK_COLOR_SPACE_SRGB_NONLINEAR_KHR which for libplacebo v7.351.0 is bt.1886. Note that for 1000:1 contrast it's almost the same as sRGB.

git clone --recursive https://code.videolan.org/videolan/libplacebo.git subprojects/libplacebo and meson configure --force-fallback-for=libplacebo if you want to use it for mpv, which you already build.

Also not sure if you run mpv through your tool here, but VK_COLOR_SPACE_BT709_NONLINEAR_EXT is still listed, so we would

@mahkoh
Copy link
Contributor

mahkoh commented Jan 25, 2026

I tested again with the latest libplacebo and the hardcoded hdr.min_luma values:

PL_COLOR_SDR_WHITE / PL_COLOR_SDR_CONTRAST:

image

PL_COLOR_HDR_BLACK:

image

Your BT1886 does not get invoked here.

The difference is much larger than with the current libplacebo version.

@kasper93
Copy link
Member Author

Might want --treat-srgb-as-power22=no for real sRGB output.

@kasper93
Copy link
Member Author

Note, that you can override preferred transfer with --target-trc=bt.1886, else it will likely always fallback to sRGB when preferred is gamma2.2.

@Lompik
Copy link

Lompik commented Jan 26, 2026

I can confirm that mpv git + libplacebo git ./mpv -v --no-config --vo=gpu-next --target-trc=bt.1886 --sdr-adjust-gamma=yes --treat-srgb-as-power22=no --gpu-api=vulkan will output:

...
[vo/gpu-next/libplacebo] Picked surface configuration 57: VK_FORMAT_A2B10G10R10_UNORM_PACK32 + VK_COLOR_SPACE_BT709_NONLINEAR_EXT
...
[ 490129.295] {mesa vk display queue}  -> wp_image_description_creator_params_v1#59.set_tf_named(1)

@kasper93
Copy link
Member Author

kasper93 commented Jan 26, 2026

The difference is much larger than with the current libplacebo version.

Yes, because now it correctly sets bt.1886 transfer if requested. Generally bt.1886 output in libplacebo should only be used if the input is also bt.1886 to roundtrip back to original transfer. We already do tonemapping and bpc, we don't need bt.1886 to mess with this afterwards. Note that we have a single set of min/max lumiance, which is used both for tonemaping and later for encoding. And it looks like without setting the luminances for Wayland we would have to hardcode those values, which has adverse effect on HDR tonemapping.

Back to the topic. I've tested some patterns, on some screen with low contrast, and I generally get proper output (tested on kwin) when doing:
--target-trc=srgb default and preferred by most compositors
--treat-srgb-as-power22=auto default, this is what most of sRGB displays expect
--target-contrast=750 exact value don't matter, but it's in the 1000:1 range, for SDR transfers, at least if directly scanned to the display.
--sdr-adjust-gamma=yes that's for SDR->SDR, really depends on input content, so shrug, default is no.

Now when --target-trc=bt.1886 and the input is HDR, the luminances values have to be carefully adjusted to make it work, but like I said, it should be avoided as a target, unless you want to passthrough input trc.

For --target-trc=pq:
--target-contrast=inf (already done) this has to be set, otherwise KWin clips everything <min_luminance value.

So realistically, we probably should set better target-contrast for SDR output, not scale it to 0, like with PQ. And we should ignore bt.1886 output, it should only be used for bt.1886->bt.1886.

@kasper93 kasper93 added priority:on-ice may be revisited later and removed priority:on-ice may be revisited later labels Jan 26, 2026
@mahkoh
Copy link
Contributor

mahkoh commented Jan 26, 2026

Yes, because now it correctly sets bt.1886 transfer if requested.

bt1886 shouldn't be involved at all in the screenshots. It takes PQ input and gives srgb output to wayland.

@kasper93
Copy link
Member Author

kasper93 commented Jan 26, 2026

Yes, because now it correctly sets bt.1886 transfer if requested.

bt1886 shouldn't be involved at all in the screenshots. It takes PQ input and gives srgb output to wayland.

Which compositor? In this case you likely need --treat-srgb-as-power22=no, else it will output G2.2, which will be lots brighter as on the screenshot.

I cannot tell you what --treat-srgb-as-power22 and --target-contrast is valid for you in this case. It depends on your display... Also I cannot tell you from those screenshots, which is correct for your output device.

For me it's --target-contrast=750

@mahkoh
Copy link
Contributor

mahkoh commented Jan 26, 2026

My point is that the first screenshot looks vastly different from the second screenshot. Both were taken with the same set of parameters passed to mpv with the only difference being that one hardcodes 0.203 as hdr.min_luma and the other one hardcodes 10^-6 as hdr.min_luma.

@kasper93
Copy link
Member Author

My point is that the first screenshot looks vastly different from the second screenshot. Both were taken with the same set of parameters passed to mpv with the only difference being that one hardcodes 0.203 as hdr.min_luma and the other one hardcodes 10^-6 as hdr.min_luma.

Well, obviously they will be different. You are setting different black level for tonemapping. You can see it on the --tone-mapping-visualize graph, if it makes it easier. Graph is from 0-10000 nits, input on x-axis, output on y-axis. Red zone is outside [min_luma, max_luma]. (not sure, if it's clear, but feel free to ask :))

image

@mahkoh
Copy link
Contributor

mahkoh commented Jan 26, 2026

Wouldn't this be on compositor to provide meaningful values here?

I don't yet understand what values the compositor would provide. The current behavior seems to be entirely controlled by the offset chosen by the client.

@mahkoh: stupid question, shouldn't we just scale wayland provided values by PL_COLOR_SDR_WHITE / wd->ref_luma factor? In fact I don't recall why we also offset it by minimum?

It's because compositors map primary color min luma in color space A to primary color min luma in color space B. I.e. SDR black in one color space is mapped to SDR black in the other.

Let's say we did as you say:

Most compositors send min=0.2 and ref=80. This would yield a contrast of 203/(0.2 * 203 / 80) = 80 / 0.2 = 400.

However, KDE sends min=0.01 and ref=200. This would yield a contrast of 203/(0.01 * 203 / 200) = 200 / 0.01 = 20_000.

@kasper93
Copy link
Member Author

Wouldn't this be on compositor to provide meaningful values here?

I don't yet understand what values the compositor would provide. The current behavior seems to be entirely controlled by the offset chosen by the client.

Using Vulkan WSI we wouldn't be able to set values for SDR. But I guess it's not necessary, as we can render to the provided values. Sorry, I'm still procrastinating doing deeper dive into it.

@mahkoh: stupid question, shouldn't we just scale wayland provided values by PL_COLOR_SDR_WHITE / wd->ref_luma factor? In fact I don't recall why we also offset it by minimum?

It's because compositors map primary color min luma in color space A to primary color min luma in color space B. I.e. SDR black in one color space is mapped to SDR black in the other.

Let's say we did as you say:

Most compositors send min=0.2 and ref=80. This would yield a contrast of 203/(0.2 * 203 / 80) = 80 / 0.2 = 400.

However, KDE sends min=0.01 and ref=200. This would yield a contrast of 203/(0.01 * 203 / 200) = 200 / 0.01 = 20_000.

Not sure where is the issue? If compositor define min=0.2 and ref=80, that means they request 400:1 contrast and we should respect that, no?

Think about it this way. Let's forget about hardcoded value in libplacebo, assume that libplacebo uses reference_luminance==80. (which in fact it does, because in practice when targeting "SDR" transfer ref_lum==max_lum in libplacebo)

Now I have 0-1 float value, which represent 0-10000 nits absolute scale, when min=0.2 and ref=max=80, that means we want to compress 0-10000 nits dynamic range to 0.2-80 nits and normalize this to 0-1. Which effectively means that as requested we have 400:1 contrast.

What am I missing here?

@mahkoh
Copy link
Contributor

mahkoh commented Jan 26, 2026

If compositor define min=0.2 and ref=80, that means they request 400:1 contrast and we should respect that, no?

I don't believe the developers of those compositors had anything like that in mind when they chose the values.

@kasper93
Copy link
Member Author

kasper93 commented Jan 26, 2026

If compositor define min=0.2 and ref=80, that means they request 400:1 contrast and we should respect that, no?

I don't believe the developers of those compositors had anything like that in mind when they chose the values.

Well, I can believe only what it says on the box. And it says, pretty explicit to me:

Provides the luminance range that the image description is targeting as the minimum and maximum absolute luminance L.

It's not my fault, they are using sRGB values, that are not applicable in practice. And copied from spec without thinking. Reference veiling glare is indeed defined to 0.2
image
and display luminance level to
image
but it hardly make sense in our application and nowadays.

I think KDE guys actually did the job and tested this in real life scenarios. 20_000:1 is very sensible default, without checking actual display hardware.

Either way, we could just not scale the min value, but I see no justification for this. Same as I see no justification that we currently just scale it to zero always.

Also we should be using get_image_description(output) instead of those preferred values, especially when we fallback to PQ, the expected luminances may change, if I understood correctly. This also avoids hardcoding any values, as they would be returned in descriptor.

@mahkoh
Copy link
Contributor

mahkoh commented Jan 26, 2026

KDE uses the formula currently used by mpv to scale luminance values.

@na-na-hi
Copy link
Contributor

but it hardly make sense in our application and nowadays

Assuming veiling glare is the same the contrast is not that far off:

sRGB standard (CRT display with a minimum luminance of 0): (80 + 0) / (0.2 + 0) = 400:1
A current display with 1000:1 contrast (not including glare) and 203 cd/m2: (203 + 0.2) / (0.203 + 0.2) = 504:1

In practice the current display is likely to be used in an ambient luminance larger than the 16 cd/m2 in the sRGB standard so the effective contrast is even smaller.

@kasper93
Copy link
Member Author

but it hardly make sense in our application and nowadays

Assuming veiling glare is the same the contrast is not that far off:

sRGB standard (CRT display with a minimum luminance of 0): (80 + 0) / (0.2 + 0) = 400:1 A current display with 1000:1 contrast (not including glare) and 203 cd/m2: (203 + 0.2) / (0.203 + 0.2) = 504:1

In practice the current display is likely to be used in an ambient luminance larger than the 16 cd/m2 in the sRGB standard so the effective contrast is even smaller.

But you cannot assume that...
image
https://www.researchgate.net/publication/8039549_Use_of_a_Human_Visual_System_Model_to_Predict_Observer_Performance_with_CRT_vs_LCD_Display_of_Images

QD-OLEDs are famously bad with ambient light rejection.
image

Also I would argue that if you are watching movies on 400:1, you might prefer black clipping instead of significantly raising whole image luminance, even on such small dynamic range display.

@kasper93
Copy link
Member Author

KDE uses the formula currently used by mpv to scale luminance values.

Are you talking about this? https://invent.kde.org/plasma/kwin/-/blob/b6103beb2ac1561c315e5a272447e213d7f5fbd9/src/core/colorspace.cpp#L582-619

This is mapping the luminances values, based on input / output colorspace. It's not meant to convert min/ref/max values.

BPC logic and all tonemapping curve is already implemented.

The more I look at this we should just rescale to 203 nits reference, to have SDR values anchored around that. I'm happy to be corrected, but at this point in time, I don't see justification of scaling min_luma to 0 always (which basically disables BPC in libplacebo.

@mahkoh
Copy link
Contributor

mahkoh commented Jan 26, 2026

I don't know what the code is. But I've experimentally confirmed that KDE scales luminance values this way.

@kasper93
Copy link
Member Author

I don't know what the code is. But I've experimentally confirmed that KDE scales luminance values this way.

What was the test to determine this?

@mahkoh
Copy link
Contributor

mahkoh commented Jan 26, 2026

https://github.com/mahkoh/wayland-color-test go to view color description, change type to parametric, enable luminances, change the values. The output remains the same. Therefore the algorithm used by KDE is the same as the algorithm used by the application.

(The application currently allows you to set non-integer values for max and ref luminances, this is a bug since the protocol doesn't allow that. If you set those fields to a non-integer value the output will be different.)

@mahkoh
Copy link
Contributor

mahkoh commented Jan 26, 2026

I still don't understand why 0.203 -> 10^-6 changes the tone mapping so much.

0.203/203*255 ~= 0.25 and even if we use a pure gamma22 curve that is only ~0.5 in electrical space.

I would expect this to change the colors by maybe 1 or 2 in electrical space. But in the sample image, the top left rectangle changes by 9 (!) in electrical space.

@mahkoh
Copy link
Contributor

mahkoh commented Jan 26, 2026

Ignore the previous comment. You have to calculate (0.203/203)^(1/2.2) * 255 which is in fact ~11.

@kasper93
Copy link
Member Author

https://github.com/mahkoh/wayland-color-test go to view color description, change type to parametric, enable luminances, change the values. The output remains the same. Therefore the algorithm used by KDE is the same as the algorithm used by the application.

This is hardly saying anything to me. It could just do nothing and the output will also not change.

I've created #17339 to fix current scaling. Let me know, if you see any issues with this, preferably with examples and steps to reproduce the wrong image reproduction.

@kasper93
Copy link
Member Author

kasper93 commented Jan 27, 2026

Ignore the previous comment. You have to calculate (0.203/203)^(1/2.2) * 255 which is in fact ~11.

Yes, this is the effect of BPC, which you can see on the graphs in #17329 (comment)

@Lompik
Copy link

Lompik commented Jan 27, 2026

Just a though, might be wrong:
for SDR/SDR, do we need bt1886(mpv_min_luma, mpv_max_luma) (src) / bt1886(wayland_min_luma, wayland_max_luma) (dst) ?

@kasper93
Copy link
Member Author

Just a though, might be wrong: for SDR/SDR, do we need bt1886(mpv_min_luma, mpv_max_luma) (src) / bt1886(wayland_min_luma, wayland_max_luma) (dst) ?

This is exactly why this PR exists. But to be quite honest, we should never output bt.1886, except when it's passthrough. It makes no sense to use it as encoding transfer. You can use it when converting to absolute luminance, but that's all that it's useful in our pipeline.

@Lompik
Copy link

Lompik commented Jan 27, 2026

I meant within mpv, inside libplacebo's shader ie:
bt1886(mpv_min_luma, mpv_max_luma) for pl_shader_linearize (src) & bt1886(wayland_min_luma, wayland_max_luma) for pl_shader_delinearize(dst).

@kasper93
Copy link
Member Author

I meant within mpv, inside libplacebo's shader ie: bt1886(mpv_min_luma, mpv_max_luma) for pl_shader_linearize (src) & bt1886(wayland_min_luma, wayland_max_luma) for pl_shader_delinearize(dst).

You can do that by specifying mpv_min_luma, mpv_max_luma using vf=format. By default contrast of input is the same as output.

Also it doesn't make any sense, what are you trying to achieve?

@Lompik
Copy link

Lompik commented Jan 27, 2026

Also it doesn't make any sense, what are you trying to achieve?

What's weird to me here is that we're just passing-through the data (for SDR/SDR). The wayland values don't matter, neither does target-contrast.

It was just an exploration

we should never output bt.1886

That's the way.

@kasper93
Copy link
Member Author

What's weird to me here is that we're just passing-through the data (for SDR/SDR).

For the same transfer function. And yes, input and output contrast is assumed to be the same, simply because SDR content is display referred, so it applies naturally to input.

@Lompik
Copy link

Lompik commented Jan 28, 2026

mpv should negotiate on its own color manager with wayland. Mesa just acts as middle-man here. opengl / vulkan+pass_through handling shouldn't be much different.

That being said, for SDR content on SDR screen,bt1886 content intended to be sent to screens as is. And mpv should try to achieve that, including in compositor pipelines. There is no way to know what compositor will do, it is fair to assume the advertised "preferred" TF only does a round-trip to linear (as-is to screen). That applies dmabuf-wayland too.

@kasper93
Copy link
Member Author

kasper93 commented Jan 28, 2026

it is fair to assume the advertised "preferred" TF only does a round-trip to linear (as-is to screen).

It's fair to assume, and this works like this in practice, we confirmed this with compositors. However this is still basing on implementation details.

I've already complained about this. But Wayland makes it really not convenient to try to adapt content to target output. It really, really wants to do all the processing itself. Certainly it's impossible when using Vulkan, where you are forced to use protocol directly. And even than, there there is no negotiation, I can't say, that I want PQ output, give me parameters that will not cause compositor to tonemap. All we get is preferred descriptor, which is in most cases sRGB 80/0.2 blend space of compositors. And we can try to infer parameters, based on implementation details of compositors to what we should tonemap to.

It's not possible to apply ICC profile, in client, because you have no idea what will compositor do to your image after. And so on.

I purposefully, refuse to implement passthrough in vulkan, even though it's currently like 10 lines of code to add this.

@mahkoh
Copy link
Contributor

mahkoh commented Jan 28, 2026

And we can try to infer parameters, based on implementation details of compositors to what we should tonemap to.

There is no need to infer this. This information is provided in the preferred description via the target_* events.

@kasper93
Copy link
Member Author

And we can try to infer parameters, based on implementation details of compositors to what we should tonemap to.

There is no need to infer this. This information is provided in the preferred description via the target_* events.

Ah, yes the gamma2.2 HDR, which forces us to use 16-bit backbuffer, because gpu bandwidth is free, no? Otherwise it would cause banding, PQ is optimized for lower bits, gamma2.2 was never meant to encode 10000 nits dynamic range, too many bits lost in dark area. Granted of course most modern usecases are in 400-800 nits range, so it kind of dodging the bullet if we limit the the range so much. But with anything else 10-bit unorm is no-go.

@mahkoh
Copy link
Contributor

mahkoh commented Jan 28, 2026

gamma2.2

The transfer function has nothing to do with tone mapping.

@kasper93
Copy link
Member Author

gamma2.2

The transfer function has nothing to do with tone mapping.

I haven't said it does. It however does imply the encoding and when you have 0-1024 values, you have to be smart how to allocate them to your dynamic range.

@kasper93
Copy link
Member Author

Will fix this in a different way. Also bt.1886 shouldn't be used as encoding for output in general, so it has little relevance.

@kasper93 kasper93 closed this Jan 28, 2026
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.

4 participants