From 1e4701309f882b152ebc437761df51592d0bdff1 Mon Sep 17 00:00:00 2001 From: Xaver Hugl Date: Mon, 9 Oct 2023 16:40:26 +0200 Subject: [PATCH] core/iccprofile: read colorimetry, BToA1 and BToA0 tags --- src/CMakeLists.txt | 1 + src/core/colorlut3d.cpp | 45 +++++ src/core/colorlut3d.h | 41 +++++ src/core/iccprofile.cpp | 285 +++++++++++++++++++++++++++++- src/core/iccprofile.h | 33 +++- src/libkwineffects/colorspace.cpp | 23 ++- src/libkwineffects/colorspace.h | 5 +- 7 files changed, 425 insertions(+), 8 deletions(-) create mode 100644 src/core/colorlut3d.cpp create mode 100644 src/core/colorlut3d.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ea6b8a323a..44de24635f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -43,6 +43,7 @@ target_sources(kwin PRIVATE compositor_wayland.cpp compositor_x11.cpp core/colorlut.cpp + core/colorlut3d.cpp core/colorpipelinestage.cpp core/colortransformation.cpp core/gbmgraphicsbufferallocator.cpp diff --git a/src/core/colorlut3d.cpp b/src/core/colorlut3d.cpp new file mode 100644 index 0000000000..a9988b106a --- /dev/null +++ b/src/core/colorlut3d.cpp @@ -0,0 +1,45 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#include "colorlut3d.h" +#include "colortransformation.h" + +#include + +namespace KWin +{ + +ColorLUT3D::ColorLUT3D(std::unique_ptr &&transformation, size_t xSize, size_t ySize, size_t zSize) + : m_transformation(std::move(transformation)) + , m_xSize(xSize) + , m_ySize(ySize) + , m_zSize(zSize) +{ +} + +size_t ColorLUT3D::xSize() const +{ + return m_xSize; +} + +size_t ColorLUT3D::ySize() const +{ + return m_ySize; +} + +size_t ColorLUT3D::zSize() const +{ + return m_zSize; +} + +QVector3D ColorLUT3D::sample(size_t x, size_t y, size_t z) +{ + return m_transformation->transform(QVector3D(x / double(m_xSize - 1), y / double(m_ySize - 1), z / double(m_zSize - 1))); +} + +} diff --git a/src/core/colorlut3d.h b/src/core/colorlut3d.h new file mode 100644 index 0000000000..98791e3de3 --- /dev/null +++ b/src/core/colorlut3d.h @@ -0,0 +1,41 @@ +/* + KWin - the KDE window manager + This file is part of the KDE project. + + SPDX-FileCopyrightText: 2023 Xaver Hugl + + SPDX-License-Identifier: GPL-2.0-or-later +*/ +#pragma once + +#include +#include + +#include "kwin_export.h" + +class QVector3D; + +namespace KWin +{ + +class ColorTransformation; + +class KWIN_EXPORT ColorLUT3D +{ +public: + ColorLUT3D(std::unique_ptr &&transformation, size_t xSize, size_t ySize, size_t zSize); + + size_t xSize() const; + size_t ySize() const; + size_t zSize() const; + + QVector3D sample(size_t x, size_t y, size_t z); + +private: + const std::unique_ptr m_transformation; + const size_t m_xSize; + const size_t m_ySize; + const size_t m_zSize; +}; + +} diff --git a/src/core/iccprofile.cpp b/src/core/iccprofile.cpp index eed88d5e40..9cec7b2127 100644 --- a/src/core/iccprofile.cpp +++ b/src/core/iccprofile.cpp @@ -5,17 +5,30 @@ */ #include "iccprofile.h" #include "colorlut.h" +#include "colorlut3d.h" #include "colorpipelinestage.h" #include "colortransformation.h" #include "utils/common.h" #include +#include +#include namespace KWin { -IccProfile::IccProfile(cmsHPROFILE handle, const std::shared_ptr &vcgt) +IccProfile::IccProfile(cmsHPROFILE handle, const Colorimetry &colorimetry, BToATagData &&bToATag, const std::shared_ptr &vcgt) : m_handle(handle) + , m_colorimetry(colorimetry) + , m_bToATag(std::move(bToATag)) + , m_vcgt(vcgt) +{ +} + +IccProfile::IccProfile(cmsHPROFILE handle, const Colorimetry &colorimetry, const std::shared_ptr &inverseEOTF, const std::shared_ptr &vcgt) + : m_handle(handle) + , m_colorimetry(colorimetry) + , m_inverseEOTF(inverseEOTF) , m_vcgt(vcgt) { } @@ -25,11 +38,174 @@ IccProfile::~IccProfile() cmsCloseProfile(m_handle); } +const Colorimetry &IccProfile::colorimetry() const +{ + return m_colorimetry; +} + +std::shared_ptr IccProfile::inverseEOTF() const +{ + return m_inverseEOTF; +} + std::shared_ptr IccProfile::vcgt() const { return m_vcgt; } +const IccProfile::BToATagData *IccProfile::BtToATag() const +{ + return m_bToATag ? &m_bToATag.value() : nullptr; +} + +static std::vector readTagRaw(cmsHPROFILE profile, cmsTagSignature tag) +{ + const auto numBytes = cmsReadRawTag(profile, tag, nullptr, 0); + std::vector data(numBytes); + cmsReadRawTag(profile, tag, data.data(), numBytes); + return data; +} + +template +static T read(std::span data, size_t index) +{ + // ICC profile data is big-endian + T ret; + for (size_t i = 0; i < sizeof(T); i++) { + *(reinterpret_cast(&ret) + i) = data[index + sizeof(T) - i - 1]; + } + return ret; +} + +static float readS15Fixed16(std::span data, size_t index) +{ + return read(data, index) / 65536.0; +} + +static std::optional> parseBToACLUTSize(std::span data) +{ + const uint32_t tagType = read(data, 0); + const bool isLutTag = tagType == cmsSigLut8Type || tagType == cmsSigLut16Type; + if (isLutTag) { + const uint8_t size = data[10]; + return std::make_tuple(size, size, size); + } else { + const uint32_t clutOffset = read(data, 24); + if (data.size() < clutOffset + 19) { + qCWarning(KWIN_CORE, "CLut offset points to invalid position %u", clutOffset); + return std::nullopt; + } + return std::make_tuple(data[clutOffset + 0], data[clutOffset + 1], data[clutOffset + 2]); + } +} + +static std::optional parseMatrix(std::span data, bool hasOffset) +{ + const size_t matrixSize = hasOffset ? 12 : 9; + std::vector floats; + floats.reserve(matrixSize); + for (size_t i = 0; i < matrixSize; i++) { + floats.push_back(readS15Fixed16(data, i * 4)); + } + constexpr double xyzEncodingFactor = 65536.0 / (2 * 65535.0); + QMatrix4x4 ret; + ret(0, 0) = floats[0] * xyzEncodingFactor; + ret(0, 1) = floats[1] * xyzEncodingFactor; + ret(0, 2) = floats[2] * xyzEncodingFactor; + ret(1, 0) = floats[3] * xyzEncodingFactor; + ret(1, 1) = floats[4] * xyzEncodingFactor; + ret(1, 2) = floats[5] * xyzEncodingFactor; + ret(2, 0) = floats[6] * xyzEncodingFactor; + ret(2, 1) = floats[7] * xyzEncodingFactor; + ret(2, 2) = floats[8] * xyzEncodingFactor; + if (hasOffset) { + ret(0, 3) = floats[9] * xyzEncodingFactor; + ret(1, 3) = floats[10] * xyzEncodingFactor; + ret(2, 3) = floats[11] * xyzEncodingFactor; + } + return ret; +} + +static std::optional parseBToATag(cmsHPROFILE profile, cmsTagSignature tag) +{ + cmsPipeline *bToAPipeline = static_cast(cmsReadTag(profile, tag)); + if (!bToAPipeline) { + return std::nullopt; + } + IccProfile::BToATagData ret; + auto data = readTagRaw(profile, tag); + const uint32_t tagType = read(data, 0); + switch (tagType) { + case cmsSigLut8Type: + case cmsSigLut16Type: + if (data.size() < 48) { + qCWarning(KWIN_CORE) << "ICC profile tag is too small" << data.size(); + return std::nullopt; + } + break; + case cmsSigLutBtoAType: + if (data.size() < 32) { + qCWarning(KWIN_CORE) << "ICC profile tag is too small" << data.size(); + return std::nullopt; + } + break; + default: + qCWarning(KWIN_CORE).nospace() << "unknown lut type " << (char)data[0] << (char)data[1] << (char)data[2] << (char)data[3]; + return std::nullopt; + } + for (auto stage = cmsPipelineGetPtrToFirstStage(bToAPipeline); stage != nullptr; stage = cmsStageNext(stage)) { + switch (const cmsStageSignature stageType = cmsStageType(stage)) { + case cmsStageSignature::cmsSigCurveSetElemType: { + // TODO read the actual functions and apply them in the shader instead + // of using LUTs for more accuracy + std::vector> stages; + stages.push_back(std::make_unique(cmsStageDup(stage))); + auto transformation = std::make_unique(std::move(stages)); + // the order of operations is fixed, so just sort the LUTs into the appropriate places + // depending on the stages that have already been added + if (!ret.matrix) { + ret.B = std::move(transformation); + } else if (!ret.CLut) { + ret.M = std::move(transformation); + } else if (!ret.A) { + ret.A = std::move(transformation); + } else { + qCWarning(KWIN_CORE, "unexpected amount of curve elements in BToA tag"); + return std::nullopt; + } + } break; + case cmsStageSignature::cmsSigMatrixElemType: { + const bool isLutTag = tagType == cmsSigLut8Type || tagType == cmsSigLut16Type; + const uint32_t matrixOffset = isLutTag ? 12 : read(data, 16); + const uint32_t matrixSize = isLutTag ? 9 : 12; + if (data.size() < matrixOffset + matrixSize * 4) { + qCWarning(KWIN_CORE, "matrix offset points to invalid position %u", matrixOffset); + return std::nullopt; + } + const auto mat = parseMatrix(std::span(data).subspan(matrixOffset), !isLutTag); + if (!mat) { + return std::nullopt; + } + ret.matrix = mat; + }; break; + case cmsStageSignature::cmsSigCLutElemType: { + const auto size = parseBToACLUTSize(data); + if (!size) { + return std::nullopt; + } + const auto [x, y, z] = *size; + std::vector> stages; + stages.push_back(std::make_unique(cmsStageDup(stage))); + ret.CLut = std::make_unique(std::make_unique(std::move(stages)), x, y, z); + } break; + default: + qCWarning(KWIN_CORE, "unknown stage type %u", stageType); + return std::nullopt; + } + } + return ret; +} + std::unique_ptr IccProfile::load(const QString &path) { if (path.isEmpty()) { @@ -56,7 +232,7 @@ std::unique_ptr IccProfile::load(const QString &path) std::shared_ptr vcgt; cmsToneCurve **vcgtTag = static_cast(cmsReadTag(handle, cmsSigVcgtTag)); if (!vcgtTag || !vcgtTag[0]) { - qCWarning(KWIN_CORE) << "Profile" << path << "has no VCGT tag"; + qCDebug(KWIN_CORE) << "Profile" << path << "has no VCGT tag"; } else { // Need to duplicate the VCGT tone curves as they are owned by the profile. cmsToneCurve *toneCurves[] = { @@ -69,7 +245,110 @@ std::unique_ptr IccProfile::load(const QString &path) vcgt = std::make_shared(std::move(stages)); } - return std::make_unique(handle, vcgt); + const cmsCIEXYZ *whitepoint = static_cast(cmsReadTag(handle, cmsSigMediaWhitePointTag)); + if (!whitepoint) { + qCWarning(KWIN_CORE, "profile is missing the wtpt tag"); + return nullptr; + } + + QVector3D red; + QVector3D green; + QVector3D blue; + QVector3D white(whitepoint->X, whitepoint->Y, whitepoint->Z); + std::optional chromaticAdaptationMatrix; + if (cmsIsTag(handle, cmsSigChromaticAdaptationTag)) { + // the chromatic adaptation tag is a 3x3 matrix that converts from the actual whitepoint to D50 + const auto data = readTagRaw(handle, cmsSigChromaticAdaptationTag); + const auto mat = parseMatrix(std::span(data).subspan(8), false); + if (!mat) { + qCWarning(KWIN_CORE, "Parsing chromatic adaptation matrix failed"); + return nullptr; + } + bool invertable = false; + chromaticAdaptationMatrix = mat->inverted(&invertable); + if (!invertable) { + qCWarning(KWIN_CORE, "Inverting chromatic adaptation matrix failed"); + return nullptr; + } + const QVector3D D50(0.9642, 1.0, 0.8249); + white = *chromaticAdaptationMatrix * D50; + } + if (cmsCIExyYTRIPLE *chrmTag = static_cast(cmsReadTag(handle, cmsSigChromaticityTag))) { + red = Colorimetry::xyToXYZ(QVector2D(chrmTag->Red.x, chrmTag->Red.y)) * chrmTag->Red.Y; + green = Colorimetry::xyToXYZ(QVector2D(chrmTag->Green.x, chrmTag->Green.y)) * chrmTag->Green.Y; + blue = Colorimetry::xyToXYZ(QVector2D(chrmTag->Blue.x, chrmTag->Blue.y)) * chrmTag->Blue.Y; + } else { + const cmsCIEXYZ *r = static_cast(cmsReadTag(handle, cmsSigRedColorantTag)); + const cmsCIEXYZ *g = static_cast(cmsReadTag(handle, cmsSigGreenColorantTag)); + const cmsCIEXYZ *b = static_cast(cmsReadTag(handle, cmsSigBlueColorantTag)); + if (!r || !g || !b) { + qCWarning(KWIN_CORE, "rXYZ, gXYZ or bXYZ tag is missing"); + return nullptr; + } + if (chromaticAdaptationMatrix) { + red = *chromaticAdaptationMatrix * QVector3D(r->X, r->Y, r->Z); + green = *chromaticAdaptationMatrix * QVector3D(g->X, g->Y, g->Z); + blue = *chromaticAdaptationMatrix * QVector3D(b->X, b->Y, b->Z); + } else { + // if the chromatic adaptation tag isn't available, fall back to using the media whitepoint instead + cmsCIEXYZ adaptedR{}; + cmsCIEXYZ adaptedG{}; + cmsCIEXYZ adaptedB{}; + bool success = cmsAdaptToIlluminant(&adaptedR, cmsD50_XYZ(), whitepoint, r); + success &= cmsAdaptToIlluminant(&adaptedG, cmsD50_XYZ(), whitepoint, g); + success &= cmsAdaptToIlluminant(&adaptedB, cmsD50_XYZ(), whitepoint, b); + if (!success) { + return nullptr; + } + red = QVector3D(adaptedR.X, adaptedR.Y, adaptedR.Z); + green = QVector3D(adaptedG.X, adaptedG.Y, adaptedG.Z); + blue = QVector3D(adaptedB.X, adaptedB.Y, adaptedB.Z); + } + } + + BToATagData lutData; + if (cmsIsTag(handle, cmsSigBToD1Tag) && !cmsIsTag(handle, cmsSigBToA1Tag) && !cmsIsTag(handle, cmsSigBToA0Tag)) { + qCWarning(KWIN_CORE, "Profiles with only BToD tags aren't supported yet"); + return nullptr; + } + if (cmsIsTag(handle, cmsSigBToA1Tag)) { + // lut based profile, with relative colorimetric intent supported + auto data = parseBToATag(handle, cmsSigBToA1Tag); + if (data) { + return std::make_unique(handle, Colorimetry::fromXYZ(red, green, blue, white), std::move(*data), vcgt); + } else { + qCWarning(KWIN_CORE, "Parsing BToA1 tag failed"); + return nullptr; + } + } + if (cmsIsTag(handle, cmsSigBToA0Tag)) { + // lut based profile, with perceptual intent. The ICC docs say to use this as a fallback + auto data = parseBToATag(handle, cmsSigBToA0Tag); + if (data) { + return std::make_unique(handle, Colorimetry::fromXYZ(red, green, blue, white), std::move(*data), vcgt); + } else { + qCWarning(KWIN_CORE, "Parsing BToA0 tag failed"); + return nullptr; + } + } + // matrix based profile. The matrix is already read out for the colorimetry above + // All that's missing is the EOTF, which is stored in the rTRC, gTRC and bTRC tags + cmsToneCurve *r = static_cast(cmsReadTag(handle, cmsSigRedTRCTag)); + cmsToneCurve *g = static_cast(cmsReadTag(handle, cmsSigGreenTRCTag)); + cmsToneCurve *b = static_cast(cmsReadTag(handle, cmsSigBlueTRCTag)); + if (!r || !g || !b) { + qCWarning(KWIN_CORE) << "ICC profile is missing at least one TRC tag"; + return nullptr; + } + cmsToneCurve *toneCurves[] = { + cmsReverseToneCurveEx(4096, r), + cmsReverseToneCurveEx(4096, g), + cmsReverseToneCurveEx(4096, b), + }; + std::vector> stages; + stages.push_back(std::make_unique(cmsStageAllocToneCurves(nullptr, 3, toneCurves))); + const auto inverseEOTF = std::make_shared(std::move(stages)); + return std::make_unique(handle, Colorimetry::fromXYZ(red, green, blue, white), inverseEOTF, vcgt); } } diff --git a/src/core/iccprofile.h b/src/core/iccprofile.h index be5c919ecc..003014a1a0 100644 --- a/src/core/iccprofile.h +++ b/src/core/iccprofile.h @@ -6,9 +6,12 @@ #pragma once #include "kwin_export.h" +#include "libkwineffects/colorspace.h" +#include #include #include +#include typedef void *cmsHPROFILE; @@ -16,19 +19,47 @@ namespace KWin { class ColorTransformation; +class ColorLUT3D; class KWIN_EXPORT IccProfile { public: - explicit IccProfile(cmsHPROFILE handle, const std::shared_ptr &vcgt); + struct BToATagData + { + std::unique_ptr B; + std::optional matrix; + std::unique_ptr M; + std::unique_ptr CLut; + std::unique_ptr A; + }; + + explicit IccProfile(cmsHPROFILE handle, const Colorimetry &colorimetry, BToATagData &&bToATag, const std::shared_ptr &vcgt); + explicit IccProfile(cmsHPROFILE handle, const Colorimetry &colorimetry, const std::shared_ptr &inverseEOTF, const std::shared_ptr &vcgt); ~IccProfile(); + /** + * the BToA tag describes a transformation from XYZ with D50 whitepoint + * to the display color space. May be nullptr! + */ + const BToATagData *BtToATag() const; + /** + * Contains the inverse of the TRC tags. May be nullptr! + */ + std::shared_ptr inverseEOTF() const; + /** + * The VCGT is a non-standard tag that needs to be applied before + * pixels are sent to the display. May be nullptr! + */ std::shared_ptr vcgt() const; + const Colorimetry &colorimetry() const; static std::unique_ptr load(const QString &path); private: cmsHPROFILE const m_handle; + const Colorimetry m_colorimetry; + const std::optional m_bToATag; + const std::shared_ptr m_inverseEOTF; const std::shared_ptr m_vcgt; }; diff --git a/src/libkwineffects/colorspace.cpp b/src/libkwineffects/colorspace.cpp index 21299a2bf2..7fb3bfc476 100644 --- a/src/libkwineffects/colorspace.cpp +++ b/src/libkwineffects/colorspace.cpp @@ -49,11 +49,17 @@ static QVector3D operator*(const QMatrix3x3 &mat, const QVector3D &v) mat(2, 0) * v.x() + mat(2, 1) * v.y() + mat(2, 2) * v.z()); } -static QVector3D xyToXYZ(QVector2D xy) +QVector3D Colorimetry::xyToXYZ(QVector2D xy) { return QVector3D(xy.x() / xy.y(), 1, (1 - xy.x() - xy.y()) / xy.y()); } +QVector2D Colorimetry::xyzToXY(QVector3D xyz) +{ + xyz /= xyz.y(); + return QVector2D(xyz.x() / (xyz.x() + xyz.y() + xyz.z()), xyz.y() / (xyz.x() + xyz.y() + xyz.z())); +} + QMatrix3x3 Colorimetry::toXYZ() const { const auto r_xyz = xyToXYZ(red); @@ -75,7 +81,7 @@ bool Colorimetry::operator==(const Colorimetry &other) const : (red == other.red && green == other.green && blue == other.blue && white == other.white); } -constexpr Colorimetry Colorimetry::createFromName(NamedColorimetry name) +constexpr Colorimetry Colorimetry::fromName(NamedColorimetry name) { switch (name) { case NamedColorimetry::BT709: @@ -98,6 +104,17 @@ constexpr Colorimetry Colorimetry::createFromName(NamedColorimetry name) Q_UNREACHABLE(); } +Colorimetry Colorimetry::fromXYZ(QVector3D red, QVector3D green, QVector3D blue, QVector3D white) +{ + return Colorimetry{ + .red = xyzToXY(red), + .green = xyzToXY(green), + .blue = xyzToXY(blue), + .white = xyzToXY(white), + .name = std::nullopt, + }; +} + const ColorDescription ColorDescription::sRGB = ColorDescription(NamedColorimetry::BT709, NamedTransferFunction::sRGB, 100, 0, 100, 100); ColorDescription::ColorDescription(const Colorimetry &colorimety, NamedTransferFunction tf, double sdrBrightness, double minHdrBrightness, double maxFrameAverageBrightness, double maxHdrHighlightBrightness) @@ -111,7 +128,7 @@ ColorDescription::ColorDescription(const Colorimetry &colorimety, NamedTransferF } ColorDescription::ColorDescription(NamedColorimetry colorimetry, NamedTransferFunction tf, double sdrBrightness, double minHdrBrightness, double maxFrameAverageBrightness, double maxHdrHighlightBrightness) - : m_colorimetry(Colorimetry::createFromName(colorimetry)) + : m_colorimetry(Colorimetry::fromName(colorimetry)) , m_transferFunction(tf) , m_sdrBrightness(sdrBrightness) , m_minHdrBrightness(minHdrBrightness) diff --git a/src/libkwineffects/colorspace.h b/src/libkwineffects/colorspace.h index 92d0f60318..78e722fa68 100644 --- a/src/libkwineffects/colorspace.h +++ b/src/libkwineffects/colorspace.h @@ -27,7 +27,10 @@ enum class NamedColorimetry { class KWIN_EXPORT Colorimetry { public: - static constexpr Colorimetry createFromName(NamedColorimetry name); + static constexpr Colorimetry fromName(NamedColorimetry name); + static Colorimetry fromXYZ(QVector3D red, QVector3D green, QVector3D blue, QVector3D white); + static QVector3D xyToXYZ(QVector2D xy); + static QVector2D xyzToXY(QVector3D xyz); QMatrix3x3 toXYZ() const; QMatrix3x3 toOther(const Colorimetry &colorimetry) const;