Bug 524038 - HDR ICC profile in KWin 6.7.4 crushes PQ blacks and compresses midtones
Summary: HDR ICC profile in KWin 6.7.4 crushes PQ blacks and compresses midtones
Status: RESOLVED FIXED
Alias: None
Product: kwin
Classification: Plasma
Component: colour-management (other bugs)
Version First Reported In: 6.7.4
Platform: Other Linux
: NOR major
Target Milestone: ---
Assignee: KWin default assignee
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2026-08-08 21:28 UTC by nadrojcote
Modified: 2026-08-29 21:12 UTC (History)
3 users (show)

See Also:
Latest Commit:
Version Fixed/Implemented In: 6.7.5
Sentry Crash Report:


Attachments
Affected HDR ICC profile (589.23 KB, application/vnd.iccprofile)
2026-08-08 21:28 UTC, nadrojcote
Details
Measurements with the profile BToA cLUT applied in the native HDR client (4.27 KB, application/json)
2026-08-08 21:28 UTC, nadrojcote
Details
Measurements with the same profile applied by KWin (4.25 KB, application/json)
2026-08-08 21:28 UTC, nadrojcote
Details
MHC2 comparison profile (554.52 KB, application/vnd.iccprofile)
2026-08-08 21:28 UTC, nadrojcote
Details
Measurements with the MHC2 comparison profile applied by KWin (4.19 KB, application/json)
2026-08-08 21:28 UTC, nadrojcote
Details

Note You need to log in before you can comment on or make changes to this bug.
Description nadrojcote 2026-08-08 21:28:00 UTC
SUMMARY

Applying an HDR ICC profile in KWin 6.7.4 severely crushes the lower PQ curve and compresses midtones. The same profile produces the expected PQ response when its BToA cLUT is applied in the native Wayland HDR test application instead of by KWin.

This profile was created while the display was in HDR mode. The profiling patches were submitted by a native Wayland client using a Vulkan HDR10 ST 2084 swapchain. DisplayCAL and XWayland were not used.

STEPS TO REPRODUCE

1. Enable HDR on the display.
2. Assign the attached ICC profile as the HDR ICC profile in Display Configuration.
3. Select the ICC profile as the HDR color profile source.
4. Display neutral HDR10 ST 2084 patches through a native Wayland client.
5. Measure 20%, 30%, 50%, and 100% PQ patches.
6. Change the HDR color profile source back to EDID.
7. Apply the same profile's BToA cLUT inside the native Wayland test client.
8. Repeat the same patches without changing the display, meter, signal format, or patch geometry.

OBSERVED RESULT

With KWin applying the profile:

    PQ input    Measured luminance
    20%         No meter reading
    30%         0.758 cd/m2
    50%         51.425 cd/m2
    100%        307.868 cd/m2

With the same profile applied locally by the native HDR client:

    PQ input    ST 2084 value    Measured luminance
    20%         2.429 cd/m2       2.416 cd/m2
    30%         10.038 cd/m2      10.033 cd/m2
    50%         92.246 cd/m2      92.543 cd/m2
    100%        Display clips     307.726 cd/m2

With KWin applying the profile, 5%, 10%, and 15% all measured as black. The profile's local cLUT result follows ST 2084 closely from 20% through 50% and reaches the same display peak.

The attached profile has a 33 point AToB0 cLUT, a 33 point BToA0 cLUT, 2048 entry input and output tables, 256 entry TRCs, matrix tags, and no MHC2 or VCGT tag. Its SHA-256 is:

    2b704808224e5109baea26551cd16b2c41a7b972db95ae1af490c2f259644281

EXPECTED RESULT

KWin should apply the profile without changing the intended PQ luminance response. The result should be comparable to applying the same BToA cLUT in the native HDR client.

ADDITIONAL TESTS

An MHC2 version of the profile produced nearly the same result through KWin:

    PQ input    Measured luminance
    20%         No meter reading
    30%         0.763 cd/m2
    50%         50.930 cd/m2
    100%        308.534 cd/m2

Changing the ICC lumi tag to 1000 cd/m2 did not correct the curve. Changing the Wayland surface reference luminance, mastering luminance metadata, and render intent also did not correct it. The local cLUT and KWin tests used the same display, 10% patch size, HDMI full range output, meter, and patch values.

The display profile was built from 424 valid measured characterization patches and a separate 254 point neutral measurement. ArgyllCMS generated the ICC profile. Internal profile validation reported average Delta E 2000 of 0.459 and 95th percentile of 1.644.

SYSTEM INFORMATION

Operating System: Fedora Linux 44 KDE Plasma Desktop Edition
KDE Plasma Version: 6.7.4
KWin Version: 6.7.4
KDE Frameworks Version: 6.28.0
Qt Version: 6.11.1
Kernel Version: 7.1.6-201.fc44.x86_64
Mesa Version: 26.1.5
Graphics Platform: Wayland
GPU: AMD Phoenix1, amdgpu
Display: YCT Innoview AMOLED connected over HDMI
HDR: enabled
Output range: full
VRR: disabled
Patch client: PGenerator+ Patch Companion 1.4.7, Vulkan renderer, native Wayland HDR10 ST 2084 surface
Meter: Calibrite/X-Rite i1Display Pro Plus
Patch geometry: borderless fullscreen with a centered 10% window

ATTACHMENTS

1. Innoview_Fedora_Dense_256_Neutral_KDE-KDE-HDR-XYZ-cLUT-matrix.icc
2. dense256-local-clut-validation.json
3. dense256-kwin-system-validation.json
4. Innoview_Fedora_Dense_MHC2-HDR-MHC2-XYZ-cLUT-matrix.icc
5. kwin-mhc2-system-validation.json
Comment 1 nadrojcote 2026-08-08 21:28:02 UTC
Created attachment 194989 [details]
Affected HDR ICC profile
Comment 2 nadrojcote 2026-08-08 21:28:03 UTC
Created attachment 194990 [details]
Measurements with the profile BToA cLUT applied in the native HDR client
Comment 3 nadrojcote 2026-08-08 21:28:04 UTC
Created attachment 194991 [details]
Measurements with the same profile applied by KWin
Comment 4 nadrojcote 2026-08-08 21:28:06 UTC
Created attachment 194992 [details]
MHC2 comparison profile
Comment 5 nadrojcote 2026-08-08 21:28:07 UTC
Created attachment 194993 [details]
Measurements with the MHC2 comparison profile applied by KWin
Comment 6 nadrojcote 2026-08-08 22:01:20 UTC
I traced this through KWin 6.7.4 source and found three concrete problems. The same code is still present on current KWin master.

For context, we are developing PGenerator+, a display calibration and pattern generation system. These measurements use its Native Wayland patch application, which submits HDR patches through Wayland rather than X11 or an SDR compatibility path. The tested profile is an Argyll-generated ICC v2.2 display profile, not v4. Its B2A0 tag is a 16-bit mft2 transform with a 33x33x33 cLUT and 2048-entry input and output tables.

1. The DRM output checks the SDR profile source before attaching the HDR profile.

In src/backends/drm/drm_output.cpp, DrmOutput::queueChanges() tests m_nextState->colorProfileSource. When HDR is active it should test hdrColorProfileSource. On this system the HDR source was ICC while the SDR source was sRGB, so KWin called setIccProfile(nullptr). Changing only the unused SDR source to ICC changed the measured HDR result from 30%=0.76, 50%=51.4, 100%=307.9 cd/m2 to 30%=6.37, 50%=56.7, 100%=249.5 cd/m2. This confirms that the SDR setting incorrectly controls whether the HDR profile pointer reaches the shadow render path.

A minimal correction is:

const auto profileSource = hdr ? m_nextState->hdrColorProfileSource : m_nextState->colorProfileSource;

2. KWin does not fall back from B2A1 to B2A0.

The tested Argyll profile contains B2A0 but no B2A1. IccProfile::BToATag() returns null for relative and absolute colorimetric intents when B2A1 is absent. KWin then uses matrix/TRC instead of the profile cLUT. ICC transform precedence requires trying B2A0 before falling back to matrix/TRC when the intent-specific transform is unavailable. BToATag() should return B2A0 when B2A1 is absent.

3. The ICC shader rejects this Argyll mft2 BToA transform.

parseBToATag() prepends a ColorMultiplier for ICC XYZ encoding. LittleCMS removes the identity matrix stage from this Argyll mft2 tag, leaving these operations:

ColorMultiplier, ColorTransformation, ColorLUT3D, ColorTransformation

IccShader::setProfile() accepts an optional ColorTransformation or ColorMatrix first, but not ColorMultiplier. It leaves the operations unconsumed, returns false, and KWin repeatedly logs:

Could not represent ICC profile in the ICC shader!

I confirmed this with a B2A1 probe profile whose cLUT forces solid red. The displayed patch remained neutral while the journal logged the shader error every frame. KWin's existing Framework 13 LUT test does not expose this because that test profile has a non-identity matrix stage, which absorbs the initial multiplier during ColorPipeline optimization.

The shader can handle this case by consuming an initial ColorMultiplier and left-multiplying it into toXYZD50 before the B curves. setUniforms() also should not silently continue with stale or default shader state when setProfile() fails for an ordinary profile.

ICC v2.2 does not have a named ST 2084 transfer-function tag. The profile represents the device transform with sampled curves and the cLUT. That limitation does not explain this failure: applying this same B2A0 cLUT directly in the PGenerator+ Native Wayland application measured the expected PQ response, including 20%=2.42, 30%=10.03, 50%=92.54 and 100%=307.73 cd/m2. With KWin profile handling, the same patches measured 30%=0.76, 50%=51.43 and 100%=307.87 cd/m2.

I prepared a small candidate source patch for the wrong HDR source check, B2A0 fallback and initial ColorMultiplier handling. It passes git diff checks. I could not compile KWin on the development machine because it does not have the required ECM 6.26 development package.
Comment 7 nadrojcote 2026-08-08 22:08:13 UTC
One more relevant profile-format detail: the profile attached to this report is ICC v2.2, so it cannot contain the CICP tag added in ICC.1:2022 profile version 4.4.

ICC v4.4 CICP can identify a display profile's data encoding as Rec.2020 PQ RGB and specify full or narrow range. For example, 9-16-0-1 identifies full-range Rec.2020 PQ RGB. CICP identifies primaries, transfer characteristics, matrix coefficients and range. It does not define display peak luminance or replace the profile transform.

I searched KWin 6.7.4 and current master and found no CICP parsing. The HDR ICC path currently constructs the profile-side transfer function as gamma 2.2 in DrmOutput::createColorDescription(). This means that generating a valid v4.4 profile with a PQ CICP tag would not currently help because KWin would ignore the tag.

This looks like an additional missing part of HDR display-profile support, separate from the three bugs described above. KWin likely needs to read CICP for v4.4 display profiles and use it to describe the profile's device encoding. We can generate a v4.4 Rec.2020 PQ test profile for validation once there is a code path that consumes the tag.
Comment 8 nadrojcote 2026-08-08 22:10:42 UTC
Could a Bugzilla administrator please remove the final paragraph of comment 6? It contains irrelevant information about the development environment and is not needed for this report.
Comment 9 Zamundaaa 2026-08-28 11:33:56 UTC
> LittleCMS removes the identity matrix stage from this Argyll mft2 tag, leaving these operations:
I think it's our own ColorPipeline code that optimizes things there. For 6.9, I hope to finally finish https://invent.kde.org/plasma/kwin/-/merge_requests/6743, which would make it impossible to overlook such problems.

Either way, thanks for the thorough bug report.
Comment 10 Bug Janitor Service 2026-08-28 12:03:07 UTC
A possibly relevant merge request was started @ https://invent.kde.org/plasma/kwin/-/merge_requests/9824
Comment 11 Zamundaaa 2026-08-28 12:04:06 UTC
I think the commits should cherry-pick cleanly to 6.7. It would be great if you could test them; on my monitor, the ICC profile you provided still results in somewhat weird colors, but that may just be from the profile not matching the screen.
Comment 12 Zamundaaa 2026-08-28 13:03:56 UTC
Git commit eb6a57fba628f700e2046dafc3367677ee29ce6a by Xaver Hugl.
Committed on 28/08/2026 at 12:47.
Pushed by zamundaaa into branch 'master'.

core/iccprofile: fall back to BToA0 if BToA1 isn't available

The spec says to fall back to BToA0 if the tag for the used rendering
intent can't be used, and to fall back to matrix+shaper if that can't
be used either (which we do already).

M  +7    -1    src/core/iccprofile.cpp

https://invent.kde.org/plasma/kwin/-/commit/eb6a57fba628f700e2046dafc3367677ee29ce6a
Comment 13 Zamundaaa 2026-08-28 13:04:05 UTC
Git commit b23f4973e3747e00b2a8fa63a45d78142f1c7c2f by Xaver Hugl.
Committed on 28/08/2026 at 12:47.
Pushed by zamundaaa into branch 'master'.

backends/drm: fix a color profile source check

It always checked the SDR source, rather than the correct one for the
current mode.

M  +4    -4    src/backends/drm/drm_output.cpp

https://invent.kde.org/plasma/kwin/-/commit/b23f4973e3747e00b2a8fa63a45d78142f1c7c2f
Comment 14 Zamundaaa 2026-08-28 13:09:36 UTC
Git commit 59ae700f62d68483f6154354fc3661437c0bb468 by Xaver Hugl.
Committed on 28/08/2026 at 12:47.
Pushed by zamundaaa into branch 'master'.

opengl/icc_shader: handle multipliers, too

ColorPipeline automatically optimizes scaling-only matrices to a
multiplier, which is normally very helpful, but in this case caused
the shader code to break.

M  +4    -0    src/opengl/icc_shader.cpp

https://invent.kde.org/plasma/kwin/-/commit/59ae700f62d68483f6154354fc3661437c0bb468
Comment 15 Zamundaaa 2026-08-28 13:27:58 UTC
Git commit 7bd406d40b35a1c2f8d79cf6030d39089acca13c by Xaver Hugl.
Committed on 28/08/2026 at 13:04.
Pushed by zamundaaa into branch 'Plasma/6.7'.

backends/drm: fix a color profile source check

It always checked the SDR source, rather than the correct one for the
current mode.

M  +4    -4    src/backends/drm/drm_output.cpp

https://invent.kde.org/plasma/kwin/-/commit/7bd406d40b35a1c2f8d79cf6030d39089acca13c
Comment 16 Zamundaaa 2026-08-28 13:28:01 UTC
Git commit 946df15efeb1f2ebb5a679aecd5a2d36ebf8453d by Xaver Hugl.
Committed on 28/08/2026 at 13:04.
Pushed by zamundaaa into branch 'Plasma/6.7'.

opengl/icc_shader: handle multipliers, too

ColorPipeline automatically optimizes scaling-only matrices to a
multiplier, which is normally very helpful, but in this case caused
the shader code to break.

M  +4    -0    src/opengl/icc_shader.cpp

https://invent.kde.org/plasma/kwin/-/commit/946df15efeb1f2ebb5a679aecd5a2d36ebf8453d
Comment 17 Zamundaaa 2026-08-28 13:28:07 UTC
Git commit a8ee8e9f6a1873b633ba7ff5361b366573aa26a6 by Xaver Hugl.
Committed on 28/08/2026 at 13:04.
Pushed by zamundaaa into branch 'Plasma/6.7'.

core/iccprofile: fall back to BToA0 if BToA1 isn't available

The spec says to fall back to BToA0 if the tag for the used rendering
intent can't be used, and to fall back to matrix+shaper if that can't
be used either (which we do already).

M  +7    -1    src/core/iccprofile.cpp

https://invent.kde.org/plasma/kwin/-/commit/a8ee8e9f6a1873b633ba7ff5361b366573aa26a6