diff options
| author | Vaxry <43317083+vaxerski@users.noreply.github.com> | 2026-03-04 19:50:28 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-04 19:50:28 +0000 |
| commit | 10754745a97c99449ef40db58bd425f5a91f9993 (patch) | |
| tree | 8aa9f57bcfb2157b9fbdc387c9021db6e6a38e46 /src/helpers | |
| parent | 8271cfc97b76d573f9122032d6cf5db256d63470 (diff) | |
render/cm: add ICC profile pipeline (#12711)
Adds an ICC profile pipeline, loading via config and applying via 3D LUTs.
Diffstat (limited to 'src/helpers')
| -rw-r--r-- | src/helpers/Monitor.cpp | 124 | ||||
| -rw-r--r-- | src/helpers/Monitor.hpp | 10 | ||||
| -rw-r--r-- | src/helpers/cm/ColorManagement.cpp | 193 | ||||
| -rw-r--r-- | src/helpers/cm/ColorManagement.hpp | 347 | ||||
| -rw-r--r-- | src/helpers/cm/ICC.cpp | 278 |
5 files changed, 929 insertions, 23 deletions
diff --git a/src/helpers/Monitor.cpp b/src/helpers/Monitor.cpp index 3be5be40..626c3bce 100644 --- a/src/helpers/Monitor.cpp +++ b/src/helpers/Monitor.cpp @@ -30,7 +30,7 @@ #include "../hyprerror/HyprError.hpp" #include "../layout/LayoutManager.hpp" #include "../i18n/Engine.hpp" -#include "../protocols/types/ColorManagement.hpp" +#include "../helpers/cm/ColorManagement.hpp" #include "sync/SyncTimeline.hpp" #include "time/Time.hpp" #include "../desktop/view/LayerSurface.hpp" @@ -933,28 +933,45 @@ bool CMonitor::applyMonitorRule(SMonitorRule* pMonitorRule, bool force) { m_supportsWideColor = RULE->supportsHDR; m_supportsHDR = RULE->supportsHDR; - m_cmType = RULE->cmType; - switch (m_cmType) { - case NCMType::CM_AUTO: m_cmType = m_enabled10bit && supportsWideColor() ? NCMType::CM_WIDE : NCMType::CM_SRGB; break; - case NCMType::CM_EDID: m_cmType = m_output->parsedEDID.chromaticityCoords.has_value() ? NCMType::CM_EDID : NCMType::CM_SRGB; break; - case NCMType::CM_HDR: - case NCMType::CM_HDR_EDID: m_cmType = supportsHDR() ? m_cmType : NCMType::CM_SRGB; break; - default: break; - } + if (RULE->iccFile.empty()) { + // only apply explicit cm settings if we have no icc file + + m_cmType = RULE->cmType; + switch (m_cmType) { + case NCMType::CM_AUTO: m_cmType = m_enabled10bit && supportsWideColor() ? NCMType::CM_WIDE : NCMType::CM_SRGB; break; + case NCMType::CM_EDID: m_cmType = m_output->parsedEDID.chromaticityCoords.has_value() ? NCMType::CM_EDID : NCMType::CM_SRGB; break; + case NCMType::CM_HDR: + case NCMType::CM_HDR_EDID: m_cmType = supportsHDR() ? m_cmType : NCMType::CM_SRGB; break; + default: break; + } - m_sdrEotf = RULE->sdrEotf; + m_sdrEotf = RULE->sdrEotf; - m_sdrMinLuminance = RULE->sdrMinLuminance; - m_sdrMaxLuminance = RULE->sdrMaxLuminance; + m_sdrMinLuminance = RULE->sdrMinLuminance; + m_sdrMaxLuminance = RULE->sdrMaxLuminance; - m_minLuminance = RULE->minLuminance; - m_maxLuminance = RULE->maxLuminance; - m_maxAvgLuminance = RULE->maxAvgLuminance; + m_minLuminance = RULE->minLuminance; + m_maxLuminance = RULE->maxLuminance; + m_maxAvgLuminance = RULE->maxAvgLuminance; - applyCMType(m_cmType, m_sdrEotf); + applyCMType(m_cmType, m_sdrEotf); - m_sdrSaturation = RULE->sdrSaturation; - m_sdrBrightness = RULE->sdrBrightness; + m_sdrSaturation = RULE->sdrSaturation; + m_sdrBrightness = RULE->sdrBrightness; + } else { + auto image = NColorManagement::SImageDescription::fromICC(RULE->iccFile); + if (!image) { + Log::logger->log(Log::ERR, "icc for {} ({}) failed: {}", m_name, RULE->iccFile, image.error()); + g_pConfigManager->addParseError(std::format("failed to apply icc {} to {}: {}", RULE->iccFile, m_name, image.error())); + } else { + m_imageDescription = CImageDescription::from(*image); + if (!m_imageDescription) { + Log::logger->log(Log::ERR, "icc for {} ({}) failed 2: {}", m_name, RULE->iccFile, image.error()); + g_pConfigManager->addParseError(std::format("failed to apply icc {} to {}: {}", RULE->iccFile, m_name, image.error())); + m_imageDescription = CImageDescription::from(SImageDescription{}); + } + } + } Vector2D logicalSize = m_pixelSize / m_scale; if (!*PDISABLESCALECHECKS && (logicalSize.x != std::round(logicalSize.x) || logicalSize.y != std::round(logicalSize.y))) { @@ -1037,6 +1054,8 @@ bool CMonitor::applyMonitorRule(SMonitorRule* pMonitorRule, bool force) { m_damage.setSize(m_transformedSize); + updateVCGTRamps(); + // Set scale for all surfaces on this monitor, needed for some clients // but not on unsafe state to avoid crashes if (!g_pCompositor->m_unsafeState) { @@ -2187,8 +2206,8 @@ bool CMonitor::canNoShaderCM() { const auto SRC_DESC_VALUE = SRC_DESC.value()->value(); - if (SRC_DESC_VALUE.icc.fd >= 0 || m_imageDescription->value().icc.fd >= 0) - return false; // no ICC support + if (m_imageDescription->value().icc.present) + return false; const auto sdrEOTF = NTransferFunction::fromConfig(); // only primaries differ @@ -2207,6 +2226,71 @@ bool CMonitor::doesNoShaderCM() { return m_noShaderCTM; } +static std::vector<uint16_t> resampleInterleavedToKms(const SVCGTTable16& t, size_t gammaSize) { + std::vector<uint16_t> out; + out.resize(gammaSize * 3); + + // + auto sample = [&](int c, float x) -> uint16_t { + const float maxX = t.entries - 1; + x = std::clamp(x, 0.F, maxX); + + const size_t i0 = (size_t)std::floor(x); + const size_t i1 = std::min(i0 + 1, (size_t)t.entries - 1); + const float f = x - sc<float>(i0); + + const float v0 = sc<float>(t.ch[c][i0]); + const float v1 = sc<float>(t.ch[c][i1]); + const float v = v0 + ((v1 - v0) * f); + + int64_t vi = std::round(v); + vi = std::clamp(vi, sc<int64_t>(0), sc<int64_t>(65535)); + return sc<uint16_t>(vi); + }; + + for (size_t i = 0; i < gammaSize; ++i) { + float x = sc<float>(i) * sc<float>(t.entries - 1) / sc<float>(gammaSize - 1); + + const uint16_t r = sample(0, x); + const uint16_t g = sample(1, x); + const uint16_t b = sample(2, x); + + out[i * 3 + 0] = r; + out[i * 3 + 1] = g; + out[i * 3 + 2] = b; + } + + return out; +} + +void CMonitor::updateVCGTRamps() { + auto gammaSize = m_output->getGammaSize(); + + if (gammaSize <= 10) { + Log::logger->log(Log::DEBUG, "CMonitor::updateVCGTRamps: skipping, no gamma ramp for output"); + return; + } + + if (!m_imageDescription->value().icc.vcgt) { + if (m_vcgtRampsSet) + m_output->state->setGammaLut({}); + + m_vcgtRampsSet = false; + return; + } + + // build table + auto table = resampleInterleavedToKms(*m_imageDescription->value().icc.vcgt, gammaSize); + + m_output->state->setGammaLut(table); + + m_vcgtRampsSet = true; +} + +bool CMonitor::gammaRampsInUse() { + return m_vcgtRampsSet; +} + CMonitorState::CMonitorState(CMonitor* owner) : m_owner(owner) { ; } diff --git a/src/helpers/Monitor.hpp b/src/helpers/Monitor.hpp index 72e0cf66..dd14a19b 100644 --- a/src/helpers/Monitor.hpp +++ b/src/helpers/Monitor.hpp @@ -16,7 +16,7 @@ #include "math/Math.hpp" #include "../desktop/reserved/ReservedArea.hpp" #include <optional> -#include "../protocols/types/ColorManagement.hpp" +#include "cm/ColorManagement.hpp" #include "signal/Signal.hpp" #include "DamageRing.hpp" #include <aquamarine/output/Output.hpp> @@ -56,6 +56,7 @@ struct SMonitorRule { float sdrSaturation = 1.0f; // SDR -> HDR float sdrBrightness = 1.0f; // SDR -> HDR Desktop::CReservedArea reservedArea; + std::string iccFile; int supportsWideColor = 0; // 0 - auto, 1 - force enable, -1 - force disable int supportsHDR = 0; // 0 - auto, 1 - force enable, -1 - force disable @@ -332,6 +333,7 @@ class CMonitor { bool wantsHDR(); bool inHDR(); + bool gammaRampsInUse(); /// Has an active workspace with a real fullscreen window (includes special workspace) bool inFullscreenMode(); @@ -353,8 +355,8 @@ class CMonitor { PHLWINDOWREF m_previousFSWindow; bool m_needsHDRupdate = false; - NColorManagement::PImageDescription m_imageDescription; - bool m_noShaderCTM = false; // sets drm CTM, restore needed + NColorManagement::PImageDescription m_imageDescription = NColorManagement::CImageDescription::from(NColorManagement::SImageDescription{}); + bool m_noShaderCTM = false; // sets drm CTM, restore needed // For the list lookup @@ -366,8 +368,10 @@ class CMonitor { void setupDefaultWS(const SMonitorRule&); WORKSPACEID findAvailableDefaultWS(); void commitDPMSState(bool state); + void updateVCGTRamps(); bool m_doneScheduled = false; + bool m_vcgtRampsSet = false; std::stack<WORKSPACEID> m_prevWorkSpaces; struct { diff --git a/src/helpers/cm/ColorManagement.cpp b/src/helpers/cm/ColorManagement.cpp new file mode 100644 index 00000000..bac9f25a --- /dev/null +++ b/src/helpers/cm/ColorManagement.cpp @@ -0,0 +1,193 @@ +#include "ColorManagement.hpp" +#include "../../macros.hpp" +#include <hyprutils/memory/UniquePtr.hpp> +#include <map> +#include <vector> + +using namespace NColorManagement; + +namespace NColorManagement { + // expected to be small + static std::vector<UP<const CPrimaries>> knownPrimaries; + static std::vector<UP<const CImageDescription>> knownDescriptions; + static std::map<std::pair<uint, uint>, Hyprgraphics::CMatrix3> primariesConversion; +} + +const SPCPRimaries& NColorManagement::getPrimaries(ePrimaries name) { + switch (name) { + case CM_PRIMARIES_SRGB: return NColorPrimaries::BT709; + case CM_PRIMARIES_BT2020: return NColorPrimaries::BT2020; + case CM_PRIMARIES_PAL_M: return NColorPrimaries::PAL_M; + case CM_PRIMARIES_PAL: return NColorPrimaries::PAL; + case CM_PRIMARIES_NTSC: return NColorPrimaries::NTSC; + case CM_PRIMARIES_GENERIC_FILM: return NColorPrimaries::GENERIC_FILM; + case CM_PRIMARIES_CIE1931_XYZ: return NColorPrimaries::CIE1931_XYZ; + case CM_PRIMARIES_DCI_P3: return NColorPrimaries::DCI_P3; + case CM_PRIMARIES_DISPLAY_P3: return NColorPrimaries::DISPLAY_P3; + case CM_PRIMARIES_ADOBE_RGB: return NColorPrimaries::ADOBE_RGB; + default: return NColorPrimaries::DEFAULT_PRIMARIES; + } +} + +CPrimaries::CPrimaries(const SPCPRimaries& primaries, const uint32_t primariesId) : m_id(primariesId), m_primaries(primaries) { + m_primaries2XYZ = m_primaries.toXYZ(); +} + +WP<const CPrimaries> CPrimaries::from(const SPCPRimaries& primaries) { + for (const auto& known : knownPrimaries) { + if (known->value() == primaries) + return known; + } + + knownPrimaries.emplace_back(CUniquePointer(new CPrimaries(primaries, knownPrimaries.size() + 1))); + return knownPrimaries.back(); +} + +WP<const CPrimaries> CPrimaries::from(const ePrimaries name) { + return from(getPrimaries(name)); +} + +WP<const CPrimaries> CPrimaries::from(const uint32_t primariesId) { + ASSERT(primariesId <= knownPrimaries.size()); + return knownPrimaries[primariesId - 1]; +} + +const SPCPRimaries& CPrimaries::value() const { + return m_primaries; +} + +uint CPrimaries::id() const { + return m_id; +} + +const Hyprgraphics::CMatrix3& CPrimaries::toXYZ() const { + return m_primaries2XYZ; +} + +const Hyprgraphics::CMatrix3& CPrimaries::convertMatrix(const WP<const CPrimaries> dst) const { + const auto cacheKey = std::make_pair(m_id, dst->m_id); + if (!primariesConversion.contains(cacheKey)) + primariesConversion.insert(std::make_pair(cacheKey, m_primaries.convertMatrix(dst->m_primaries))); + + return primariesConversion[cacheKey]; +} + +CImageDescription::CImageDescription(const SImageDescription& imageDescription, const uint32_t imageDescriptionId) : + m_id(imageDescriptionId), m_imageDescription(imageDescription) { + m_primariesId = CPrimaries::from(m_imageDescription.getPrimaries())->id(); +} + +PImageDescription CImageDescription::from(const SImageDescription& imageDescription) { + for (const auto& known : knownDescriptions) { + if (known->value() == imageDescription) + return known; + } + + knownDescriptions.emplace_back(UP<CImageDescription>(new CImageDescription(imageDescription, knownDescriptions.size() + 1))); + return knownDescriptions.back(); +} + +PImageDescription CImageDescription::from(const uint32_t imageDescriptionId) { + ASSERT(imageDescriptionId <= knownDescriptions.size()); + return knownDescriptions[imageDescriptionId - 1]; +} + +PImageDescription CImageDescription::with(const SImageDescription::SPCLuminances& luminances) const { + auto desc = m_imageDescription; + desc.luminances = luminances; + return CImageDescription::from(desc); +} + +const SImageDescription& CImageDescription::value() const { + return m_imageDescription; +} + +uint CImageDescription::id() const { + return m_id; +} + +WP<const CPrimaries> CImageDescription::getPrimaries() const { + return CPrimaries::from(m_primariesId); +} + +static Mat3x3 diag3(const std::array<float, 3>& s) { + return Mat3x3{std::array<float, 9>{s[0], 0, 0, 0, s[1], 0, 0, 0, s[2]}}; +} + +static std::optional<Mat3x3> invertMat3(const Mat3x3& m) { + const auto ARR = m.getMatrix(); + const double a = ARR[0], b = ARR[1], c = ARR[2]; + const double d = ARR[3], e = ARR[4], f = ARR[5]; + const double g = ARR[6], h = ARR[7], i = ARR[8]; + + const double A = (e * i - f * h); + const double B = -(d * i - f * g); + const double C = (d * h - e * g); + const double D = -(b * i - c * h); + const double E = (a * i - c * g); + const double F = -(a * h - b * g); + const double G = (b * f - c * e); + const double H = -(a * f - c * d); + const double I = (a * e - b * d); + + const double det = a * A + b * B + c * C; + if (std::abs(det) < 1e-18) + return std::nullopt; + + const double invDet = 1.0 / det; + Mat3x3 inv{std::array<float, 9>{ + A * invDet, + D * invDet, + G * invDet, // + B * invDet, + E * invDet, + H * invDet, // + C * invDet, + F * invDet, + I * invDet, // + }}; + return inv; +} + +static std::array<float, 3> matByVec(const Mat3x3& M, const std::array<float, 3>& v) { + const auto ARR = M.getMatrix(); + return {ARR[0] * v[0] + ARR[1] * v[1] + ARR[2] * v[2], ARR[3] * v[0] + ARR[4] * v[1] + ARR[5] * v[2], ARR[6] * v[0] + ARR[7] * v[1] + ARR[8] * v[2]}; +} + +std::optional<Mat3x3> NColorManagement::rgbToXYZFromPrimaries(SPCPRimaries pr) { + const auto R = Hyprgraphics::xy2xyz(pr.red); + const auto G = Hyprgraphics::xy2xyz(pr.green); + const auto B = Hyprgraphics::xy2xyz(pr.blue); + const auto W = Hyprgraphics::xy2xyz(pr.white); + + // P has columns R,G,B + Mat3x3 P{std::array<float, 9>{R.x, G.x, B.x, R.y, G.y, B.y, R.z, G.z, B.z}}; + + auto invP = invertMat3(P); + if (!invP) + return std::nullopt; + + const auto S = matByVec(*invP, {W.x, W.y, W.z}); + + P.multiply(diag3(S)); // RGB->XYZ + + return P; +} + +Mat3x3 NColorManagement::adaptBradford(Hyprgraphics::CColor::xy srcW, Hyprgraphics::CColor::xy dstW) { + static const Mat3x3 Bradford{std::array<float, 9>{0.8951f, 0.2664f, -0.1614f, -0.7502f, 1.7135f, 0.0367f, 0.0389f, -0.0685f, 1.0296f}}; + static const Mat3x3 BradfordInv = invertMat3(Bradford).value(); + + const auto srcXYZ = Hyprgraphics::xy2xyz(srcW); + const auto dstXYZ = Hyprgraphics::xy2xyz(dstW); + + const auto srcLMS = matByVec(Bradford, {srcXYZ.x, srcXYZ.y, srcXYZ.z}); + const auto dstLMS = matByVec(Bradford, {dstXYZ.x, dstXYZ.y, dstXYZ.z}); + + const std::array<float, 3> scale{dstLMS[0] / srcLMS[0], dstLMS[1] / srcLMS[1], dstLMS[2] / srcLMS[2]}; + + Mat3x3 result = BradfordInv; + result.multiply(diag3(scale)).multiply(Bradford); + + return result; +}
\ No newline at end of file diff --git a/src/helpers/cm/ColorManagement.hpp b/src/helpers/cm/ColorManagement.hpp new file mode 100644 index 00000000..e8d47fae --- /dev/null +++ b/src/helpers/cm/ColorManagement.hpp @@ -0,0 +1,347 @@ +#pragma once + +#include "color-management-v1.hpp" +#include <hyprgraphics/color/Color.hpp> +#include "../../helpers/memory/Memory.hpp" +#include "../../helpers/math/Math.hpp" + +#include <filesystem> +#include <vector> +#include <expected> + +#define SDR_MIN_LUMINANCE 0.2 +#define SDR_MAX_LUMINANCE 80.0 +#define SDR_REF_LUMINANCE 80.0 +#define HDR_MIN_LUMINANCE 0.005 +#define HDR_MAX_LUMINANCE 10000.0 +#define HDR_REF_LUMINANCE 203.0 +#define HLG_MAX_LUMINANCE 1000.0 + +class CTexture; + +namespace NColorManagement { + enum eNoShader : uint8_t { + CM_NS_DISABLE = 0, + CM_NS_ALWAYS = 1, + CM_NS_ONDEMAND = 2, + CM_NS_IGNORE = 3, + }; + + enum ePrimaries : uint8_t { + CM_PRIMARIES_SRGB = 1, + CM_PRIMARIES_PAL_M = 2, + CM_PRIMARIES_PAL = 3, + CM_PRIMARIES_NTSC = 4, + CM_PRIMARIES_GENERIC_FILM = 5, + CM_PRIMARIES_BT2020 = 6, + CM_PRIMARIES_CIE1931_XYZ = 7, + CM_PRIMARIES_DCI_P3 = 8, + CM_PRIMARIES_DISPLAY_P3 = 9, + CM_PRIMARIES_ADOBE_RGB = 10, + }; + + enum eTransferFunction : uint8_t { + CM_TRANSFER_FUNCTION_BT1886 = 1, + CM_TRANSFER_FUNCTION_GAMMA22 = 2, + CM_TRANSFER_FUNCTION_GAMMA28 = 3, + CM_TRANSFER_FUNCTION_ST240 = 4, + CM_TRANSFER_FUNCTION_EXT_LINEAR = 5, + CM_TRANSFER_FUNCTION_LOG_100 = 6, + CM_TRANSFER_FUNCTION_LOG_316 = 7, + CM_TRANSFER_FUNCTION_XVYCC = 8, + CM_TRANSFER_FUNCTION_SRGB = 9, + CM_TRANSFER_FUNCTION_EXT_SRGB = 10, + CM_TRANSFER_FUNCTION_ST2084_PQ = 11, + CM_TRANSFER_FUNCTION_ST428 = 12, + CM_TRANSFER_FUNCTION_HLG = 13, + }; + + // NOTE should be ok this way. unsupported primaries/tfs must be rejected earlier. supported enum values should be in sync with proto. + // might need a proper switch-case and additional INVALID enum value. + inline wpColorManagerV1Primaries convertPrimaries(ePrimaries primaries) { + return sc<wpColorManagerV1Primaries>(primaries); + } + inline ePrimaries convertPrimaries(wpColorManagerV1Primaries primaries) { + return sc<ePrimaries>(primaries); + } + inline wpColorManagerV1TransferFunction convertTransferFunction(eTransferFunction tf) { + return sc<wpColorManagerV1TransferFunction>(tf); + } + inline eTransferFunction convertTransferFunction(wpColorManagerV1TransferFunction tf) { + return sc<eTransferFunction>(tf); + } + + using SPCPRimaries = Hyprgraphics::SPCPRimaries; + + namespace NColorPrimaries { + + static const auto BT709 = SPCPRimaries{ + .red = {.x = 0.64, .y = 0.33}, + .green = {.x = 0.30, .y = 0.60}, + .blue = {.x = 0.15, .y = 0.06}, + .white = {.x = 0.3127, .y = 0.3290}, + }; + + static const auto DEFAULT_PRIMARIES = BT709; + + static const auto PAL_M = SPCPRimaries{ + .red = {.x = 0.67, .y = 0.33}, + .green = {.x = 0.21, .y = 0.71}, + .blue = {.x = 0.14, .y = 0.08}, + .white = {.x = 0.310, .y = 0.316}, + }; + + static const auto PAL = SPCPRimaries{ + .red = {.x = 0.640, .y = 0.330}, + .green = {.x = 0.290, .y = 0.600}, + .blue = {.x = 0.150, .y = 0.060}, + .white = {.x = 0.3127, .y = 0.3290}, + }; + + static const auto NTSC = SPCPRimaries{ + .red = {.x = 0.630, .y = 0.340}, + .green = {.x = 0.310, .y = 0.595}, + .blue = {.x = 0.155, .y = 0.070}, + .white = {.x = 0.3127, .y = 0.3290}, + }; + + static const auto GENERIC_FILM = SPCPRimaries{ + .red = {.x = 0.243, .y = 0.692}, + .green = {.x = 0.145, .y = 0.049}, + .blue = {.x = 0.681, .y = 0.319}, // NOLINT(modernize-use-std-numbers) + .white = {.x = 0.310, .y = 0.316}, + }; + + static const auto BT2020 = SPCPRimaries{ + .red = {.x = 0.708, .y = 0.292}, + .green = {.x = 0.170, .y = 0.797}, + .blue = {.x = 0.131, .y = 0.046}, + .white = {.x = 0.3127, .y = 0.3290}, + }; + + static const auto CIE1931_XYZ = SPCPRimaries{ + .red = {.x = 1.0, .y = 0.0}, + .green = {.x = 0.0, .y = 1.0}, + .blue = {.x = 0.0, .y = 0.0}, + .white = {.x = 1.0 / 3.0, .y = 1.0 / 3.0}, + }; + + static const auto DCI_P3 = SPCPRimaries{ + .red = {.x = 0.680, .y = 0.320}, + .green = {.x = 0.265, .y = 0.690}, + .blue = {.x = 0.150, .y = 0.060}, + .white = {.x = 0.314, .y = 0.351}, + }; + + static const auto DISPLAY_P3 = SPCPRimaries{ + .red = {.x = 0.680, .y = 0.320}, + .green = {.x = 0.265, .y = 0.690}, + .blue = {.x = 0.150, .y = 0.060}, + .white = {.x = 0.3127, .y = 0.3290}, + }; + + static const auto ADOBE_RGB = SPCPRimaries{ + .red = {.x = 0.6400, .y = 0.3300}, + .green = {.x = 0.2100, .y = 0.7100}, + .blue = {.x = 0.1500, .y = 0.0600}, + .white = {.x = 0.3127, .y = 0.3290}, + }; + } + + struct SVCGTTable16 { + uint16_t channels = 0; + uint16_t entries = 0; + uint16_t entrySize = 0; + std::array<std::vector<uint16_t>, 3> ch; + }; + + const SPCPRimaries& getPrimaries(ePrimaries name); + std::optional<Mat3x3> rgbToXYZFromPrimaries(SPCPRimaries pr); + Mat3x3 adaptBradford(Hyprgraphics::CColor::xy srcW, Hyprgraphics::CColor::xy dstW); + + class CPrimaries { + public: + static WP<const CPrimaries> from(const SPCPRimaries& primaries); + static WP<const CPrimaries> from(const ePrimaries name); + static WP<const CPrimaries> from(const uint primariesId); + + const SPCPRimaries& value() const; + uint id() const; + + const Hyprgraphics::CMatrix3& toXYZ() const; // toXYZ() * rgb -> xyz + const Hyprgraphics::CMatrix3& convertMatrix(const WP<const CPrimaries> dst) const; // convertMatrix(dst) * rgb with "this" primaries -> rgb with dst primaries + + private: + CPrimaries(const SPCPRimaries& primaries, const uint primariesId); + uint m_id; + SPCPRimaries m_primaries; + + Hyprgraphics::CMatrix3 m_primaries2XYZ; + }; + + struct SImageDescription { + static std::expected<SImageDescription, std::string> fromICC(const std::filesystem::path& file); + + // + std::vector<uint8_t> rawICC; + + eTransferFunction transferFunction = CM_TRANSFER_FUNCTION_GAMMA22; + float transferFunctionPower = 1.0f; + bool windowsScRGB = false; + + bool primariesNameSet = false; + ePrimaries primariesNamed = CM_PRIMARIES_SRGB; + // primaries are stored as FP values with the same scale as standard defines (0.0 - 1.0) + // wayland protocol expects int32_t values multiplied by 1000000 + // drm expects uint16_t values multiplied by 50000 + SPCPRimaries primaries, masteringPrimaries; + + // luminances in cd/m² + // protos and drm expect min * 10000 + struct SPCLuminances { + float min = 0.2; // 0.2 cd/m² + uint32_t max = 80; // 80 cd/m² + uint32_t reference = 80; // 80 cd/m² + bool operator==(const SPCLuminances& l2) const { + return min == l2.min && max == l2.max && reference == l2.reference; + } + } luminances; + struct SPCMasteringLuminances { + float min = 0; + uint32_t max = 0; + bool operator==(const SPCMasteringLuminances& l2) const { + return min == l2.min && max == l2.max; + } + } masteringLuminances; + + // Matrix data from ICC + struct SICCData { + bool present = false; + size_t lutSize = 33; + std::vector<float> lutDataPacked; + SP<CTexture> lutTexture; + std::optional<SVCGTTable16> vcgt; + } icc; + + uint32_t maxCLL = 0; + uint32_t maxFALL = 0; + + bool operator==(const SImageDescription& d2) const { + if (icc.present || d2.icc.present) + return false; + + return windowsScRGB == d2.windowsScRGB && transferFunction == d2.transferFunction && transferFunctionPower == d2.transferFunctionPower && + (primariesNameSet == d2.primariesNameSet && (primariesNameSet ? primariesNamed == d2.primariesNamed : primaries == d2.primaries)) && + masteringPrimaries == d2.masteringPrimaries && luminances == d2.luminances && masteringLuminances == d2.masteringLuminances && maxCLL == d2.maxCLL && + maxFALL == d2.maxFALL; + } + + const SPCPRimaries& getPrimaries() const { + if (primariesNameSet || primaries == SPCPRimaries{}) + return NColorManagement::getPrimaries(primariesNamed); + return primaries; + } + + float getTFMinLuminance(float sdrMinLuminance = -1.0f) const { + switch (transferFunction) { + case CM_TRANSFER_FUNCTION_EXT_LINEAR: return 0; + case CM_TRANSFER_FUNCTION_ST2084_PQ: + case CM_TRANSFER_FUNCTION_HLG: return HDR_MIN_LUMINANCE; + case CM_TRANSFER_FUNCTION_BT1886: return 0.01; + case CM_TRANSFER_FUNCTION_GAMMA22: + case CM_TRANSFER_FUNCTION_GAMMA28: + case CM_TRANSFER_FUNCTION_ST240: + case CM_TRANSFER_FUNCTION_LOG_100: + case CM_TRANSFER_FUNCTION_LOG_316: + case CM_TRANSFER_FUNCTION_XVYCC: + case CM_TRANSFER_FUNCTION_EXT_SRGB: + case CM_TRANSFER_FUNCTION_ST428: + case CM_TRANSFER_FUNCTION_SRGB: + default: return sdrMinLuminance >= 0 ? sdrMinLuminance : SDR_MIN_LUMINANCE; + } + }; + + float getTFMaxLuminance(int sdrMaxLuminance = -1) const { + switch (transferFunction) { + case CM_TRANSFER_FUNCTION_EXT_LINEAR: + return SDR_MAX_LUMINANCE; // assume Windows scRGB. white color range 1.0 - 125.0 maps to SDR_MAX_LUMINANCE (80) - HDR_MAX_LUMINANCE (10000) + case CM_TRANSFER_FUNCTION_ST2084_PQ: return HDR_MAX_LUMINANCE; + case CM_TRANSFER_FUNCTION_HLG: return HLG_MAX_LUMINANCE; + case CM_TRANSFER_FUNCTION_BT1886: return 100; + case CM_TRANSFER_FUNCTION_GAMMA22: + case CM_TRANSFER_FUNCTION_GAMMA28: + case CM_TRANSFER_FUNCTION_ST240: + case CM_TRANSFER_FUNCTION_LOG_100: + case CM_TRANSFER_FUNCTION_LOG_316: + case CM_TRANSFER_FUNCTION_XVYCC: + case CM_TRANSFER_FUNCTION_EXT_SRGB: + case CM_TRANSFER_FUNCTION_ST428: + case CM_TRANSFER_FUNCTION_SRGB: + default: return sdrMaxLuminance >= 0 ? sdrMaxLuminance : SDR_MAX_LUMINANCE; + } + }; + + float getTFRefLuminance(int sdrRefLuminance = -1) const { + switch (transferFunction) { + case CM_TRANSFER_FUNCTION_EXT_LINEAR: + case CM_TRANSFER_FUNCTION_ST2084_PQ: + case CM_TRANSFER_FUNCTION_HLG: return HDR_REF_LUMINANCE; + case CM_TRANSFER_FUNCTION_BT1886: return 100; + case CM_TRANSFER_FUNCTION_GAMMA22: + case CM_TRANSFER_FUNCTION_GAMMA28: + case CM_TRANSFER_FUNCTION_ST240: + case CM_TRANSFER_FUNCTION_LOG_100: + case CM_TRANSFER_FUNCTION_LOG_316: + case CM_TRANSFER_FUNCTION_XVYCC: + case CM_TRANSFER_FUNCTION_EXT_SRGB: + case CM_TRANSFER_FUNCTION_ST428: + case CM_TRANSFER_FUNCTION_SRGB: + default: return sdrRefLuminance >= 0 ? sdrRefLuminance : SDR_REF_LUMINANCE; + } + }; + }; + + class CImageDescription { + public: + static WP<const CImageDescription> from(const SImageDescription& imageDescription); + static WP<const CImageDescription> from(const uint32_t imageDescriptionId); + + WP<const CImageDescription> with(const SImageDescription::SPCLuminances& luminances) const; + + const SImageDescription& value() const; + uint32_t id() const; + + WP<const CPrimaries> getPrimaries() const; + + private: + CImageDescription(const SImageDescription& imageDescription, const uint imageDescriptionId); + uint32_t m_id = 0; + uint32_t m_primariesId = 0; + SImageDescription m_imageDescription; + }; + + using PImageDescription = WP<const CImageDescription>; + + static const auto DEFAULT_IMAGE_DESCRIPTION = CImageDescription::from(SImageDescription{.transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_GAMMA22, + .primariesNameSet = true, + .primariesNamed = NColorManagement::CM_PRIMARIES_SRGB, + .primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_SRGB), + .luminances = {.min = SDR_MIN_LUMINANCE, .max = 80, .reference = 80}}); + + static const auto DEFAULT_HDR_IMAGE_DESCRIPTION = CImageDescription::from(SImageDescription{.transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_ST2084_PQ, + .primariesNameSet = true, + .primariesNamed = NColorManagement::CM_PRIMARIES_BT2020, + .primaries = NColorManagement::getPrimaries(NColorManagement::CM_PRIMARIES_BT2020), + .luminances = {.min = HDR_MIN_LUMINANCE, .max = 10000, .reference = 203}}); + ; + static const auto SCRGB_IMAGE_DESCRIPTION = CImageDescription::from(SImageDescription{ + .transferFunction = NColorManagement::CM_TRANSFER_FUNCTION_EXT_LINEAR, + .windowsScRGB = true, + .primariesNameSet = true, + .primariesNamed = NColorManagement::CM_PRIMARIES_SRGB, + .primaries = NColorPrimaries::BT709, + .luminances = {.reference = 203}, + }); + ; + +}
\ No newline at end of file diff --git a/src/helpers/cm/ICC.cpp b/src/helpers/cm/ICC.cpp new file mode 100644 index 00000000..34045543 --- /dev/null +++ b/src/helpers/cm/ICC.cpp @@ -0,0 +1,278 @@ +#include "ColorManagement.hpp" +#include "../math/Math.hpp" +#include <cstddef> +#include <fstream> + +#include "../../debug/log/Logger.hpp" +#include "../../render/Texture.hpp" +#include "../../render/Renderer.hpp" + +#include <hyprutils/utils/ScopeGuard.hpp> +using namespace Hyprutils::Utils; + +#include <lcms2.h> + +using namespace NColorManagement; + +static std::vector<uint8_t> readBinary(const std::filesystem::path& file) { + std::ifstream ifs(file, std::ios::binary); + if (!ifs.good()) + return {}; + + ifs.seekg(0, std::ios::end); + size_t len = ifs.tellg(); + ifs.seekg(0, std::ios::beg); + + if (len <= 0) + return {}; + + std::vector<uint8_t> buf; + buf.resize(len); + ifs.read(reinterpret_cast<char*>(buf.data()), len); + + return buf; +} + +static uint16_t bigEndianU16(const uint8_t* p) { + return (uint16_t)((uint16_t)p[0] << 8 | (uint16_t)p[1]); +} + +static uint32_t bigEndianU32(const uint8_t* p) { + return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | (uint32_t)p[3]; +} + +static constexpr cmsTagSignature makeSig(char a, char b, char c, char d) { + return sc<cmsTagSignature>(sc<uint32_t>(a) << 24 | sc<uint32_t>(b) << 16 | sc<uint32_t>(c) << 8 | sc<uint32_t>(d)); +} + +static constexpr cmsTagSignature VCGT_SIG = makeSig('v', 'c', 'g', 't'); + +// + +static std::expected<std::optional<SVCGTTable16>, std::string> readVCGT16(cmsHPROFILE prof) { + if (!cmsIsTag(prof, VCGT_SIG)) + return std::nullopt; + + cmsUInt32Number n = cmsReadRawTag(prof, VCGT_SIG, nullptr, 0); + if (n < 8 + 4 + 2 + 2 + 2 + 2) // header + type + table header + return std::unexpected("Malformed vcgt tag"); + + std::vector<uint8_t> raw(n); + if (cmsReadRawTag(prof, VCGT_SIG, raw.data(), n) != n) + return std::unexpected("Malformed vcgt tag"); + + // raw layout: + // 0 ... 3: 'vcgt' + // 4 ... 7: reserved + // 8 ... 11: gammaType (0 = table) + uint32_t gammaType = bigEndianU32(raw.data() + 8); + if (gammaType != 0) + return std::unexpected("VCGT formula type is not supported by Hyprland"); + + SVCGTTable16 table; + table.channels = bigEndianU16(raw.data() + 12); + table.entries = bigEndianU16(raw.data() + 14); + table.entrySize = bigEndianU16(raw.data() + 16); + // raw+18: reserved u16 + + Log::logger->log(Log::DEBUG, "readVCGT16: table has {} channels, {} entries, and entry size of {}", table.channels, table.entries, table.entrySize); + + if (table.channels != 3 || table.entrySize != 2 || table.entries == 0) + return std::unexpected("invalid vcgt table size"); + + size_t tableBytes = (size_t)table.channels * (size_t)table.entries * (size_t)table.entrySize; + + // VCGT is a piece of shit and some absolute fucking mongoloid idiots + // decided it'd be great to have both 18 and 20 + // FUCK YOU + size_t tableOff = 20; + + auto readTable = [&] -> void { + for (int c = 0; c < 3; ++c) { + table.ch[c].resize(table.entries); + for (uint16_t i = 0; i < table.entries; ++i) { + const uint8_t* p = raw.data() + tableOff + static_cast<ptrdiff_t>((c * table.entries + i) * 2); + table.ch[c][i] = bigEndianU16(p); // 0 ... 65535 + } + } + }; + + if (raw.size() < tableOff + tableBytes) { + tableOff = 18; + + if (raw.size() < tableOff + tableBytes) { + Log::logger->log(Log::ERR, "readVCGT16: table is too short, tag is invalid"); + return std::unexpected("table is too short"); + } + + Log::logger->log(Log::DEBUG, "readVCGT16: table is too short, but off = 18 fits. Attempting offset = 18"); + + readTable(); + } else { + readTable(); + + // if the table's last entry is suspiciously low, we more than likely read an 18 as a 20. + if (table.ch[0][table.entries - 1] < 30000) { + Log::logger->log(Log::DEBUG, "readVCGT16: table is likely offset 18 not 20, re-reading"); + + tableOff = 18; + + readTable(); + } + } + + if (table.ch[0][table.entries - 1] < 30000) { + Log::logger->log(Log::ERR, "readVCGT16: table is malformed, last value of a gamma ramp can't be {}", table.ch[0][table.entries - 1]); + return std::unexpected("invalid table values"); + } + + Log::logger->log(Log::DEBUG, "readVCGT16: red channel: [{}, {}, ... {}, {}]", table.ch[0][0], table.ch[0][1], table.ch[0][table.entries - 2], table.ch[0][table.entries - 1]); + Log::logger->log(Log::DEBUG, "readVCGT16: green channel: [{}, {}, ... {}, {}]", table.ch[1][0], table.ch[1][1], table.ch[1][table.entries - 2], table.ch[1][table.entries - 1]); + Log::logger->log(Log::DEBUG, "readVCGT16: blue channel: [{}, {}, ... {}, {}]", table.ch[2][0], table.ch[2][1], table.ch[2][table.entries - 2], table.ch[2][table.entries - 1]); + + return table; +} + +struct CmsProfileDeleter { + void operator()(cmsHPROFILE p) const { + if (p) + cmsCloseProfile(p); + } +}; +struct CmsTransformDeleter { + void operator()(cmsHTRANSFORM t) const { + if (t) + cmsDeleteTransform(t); + } +}; + +using UniqueProfile = std::unique_ptr<std::remove_pointer_t<cmsHPROFILE>, CmsProfileDeleter>; +using UniqueTransform = std::unique_ptr<std::remove_pointer_t<cmsHTRANSFORM>, CmsTransformDeleter>; + +static UniqueProfile createLinearSRGBProfile() { + cmsCIExyYTRIPLE prim{}; + // sRGB / Rec.709 primaries + prim.Red.x = 0.6400; + prim.Red.y = 0.3300; + prim.Red.Y = 1.0; + prim.Green.x = 0.3000; + prim.Green.y = 0.6000; + prim.Green.Y = 1.0; + prim.Blue.x = 0.1500; + prim.Blue.y = 0.0600; + prim.Blue.Y = 1.0; + + cmsCIExyY wp{}; + wp.x = 0.3127; + wp.y = 0.3290; + wp.Y = 1.0; // D65 + + cmsToneCurve* lin = cmsBuildGamma(nullptr, 1.0); + cmsToneCurve* curves[3] = {lin, lin, lin}; + + cmsHPROFILE p = cmsCreateRGBProfile(&wp, &prim, curves); + + cmsFreeToneCurve(lin); + return UniqueProfile{p}; +} + +static std::expected<void, std::string> buildIcc3DLut(cmsHPROFILE profile, SImageDescription& image) { + UniqueProfile src = createLinearSRGBProfile(); + if (!src) + return std::unexpected("Failed to create linear sRGB profile"); + + // Rendering intent: RELATIVE_COLORIMETRIC is common for displays; add BPC to be safe. + const int intent = INTENT_RELATIVE_COLORIMETRIC; + const cmsUInt32Number flags = cmsFLAGS_BLACKPOINTCOMPENSATION | cmsFLAGS_HIGHRESPRECALC; // good quality precalc in LCMS + + // float->float transform (linear input, encoded output in dst device space) + UniqueTransform xform{cmsCreateTransform(src.get(), TYPE_RGB_FLT, profile, TYPE_RGB_FLT, intent, flags)}; + if (!xform) + return std::unexpected("Failed to create ICC transform"); + + Log::logger->log(Log::DEBUG, "Building a {}³ 3D LUT", image.icc.lutSize); + + image.icc.present = true; + image.icc.lutDataPacked.resize(image.icc.lutSize * image.icc.lutSize * image.icc.lutSize * 3); + + auto idx = [&image](int r, int g, int b) -> size_t { + // + return ((size_t)b * image.icc.lutSize * image.icc.lutSize + (size_t)g * image.icc.lutSize + (size_t)r) * 3; + }; + + for (size_t bz = 0; bz < image.icc.lutSize; ++bz) { + for (size_t gy = 0; gy < image.icc.lutSize; ++gy) { + for (size_t rx = 0; rx < image.icc.lutSize; ++rx) { + float in[3] = { + rx / float(image.icc.lutSize - 1), + gy / float(image.icc.lutSize - 1), + bz / float(image.icc.lutSize - 1), + }; + float outRGB[3]; + cmsDoTransform(xform.get(), in, outRGB, 1); + + outRGB[0] = std::clamp(outRGB[0], 0.F, 1.F); + outRGB[1] = std::clamp(outRGB[1], 0.F, 1.F); + outRGB[2] = std::clamp(outRGB[2], 0.F, 1.F); + + const size_t o = idx(rx, gy, bz); + image.icc.lutDataPacked[o + 0] = outRGB[0]; + image.icc.lutDataPacked[o + 1] = outRGB[1]; + image.icc.lutDataPacked[o + 2] = outRGB[2]; + } + } + } + + Log::logger->log(Log::DEBUG, "3D LUT constructed, size {}", image.icc.lutDataPacked.size()); + + // upload + image.icc.lutTexture = makeShared<CTexture>(image.icc.lutDataPacked, image.icc.lutSize); + + return {}; +} + +std::expected<SImageDescription, std::string> SImageDescription::fromICC(const std::filesystem::path& file) { + static auto PVCGTENABLED = CConfigValue<Hyprlang::INT>("render:icc_vcgt_enabled"); + + std::error_code ec; + if (!std::filesystem::exists(file, ec) || ec) + return std::unexpected("Invalid file"); + + SImageDescription image; + image.rawICC = readBinary(file); + + if (image.rawICC.empty()) + return std::unexpected("Failed to read file"); + + cmsHPROFILE prof = cmsOpenProfileFromFile(file.string().c_str(), "r"); + if (!prof) + return std::unexpected("CMS failed to open icc file"); + + CScopeGuard x([&prof] { cmsCloseProfile(prof); }); + + // only handle RGB (typical display profiles) + if (cmsGetColorSpace(prof) != cmsSigRgbData) + return std::unexpected("Only RGB display profiles are supported"); + + Log::logger->log(Log::DEBUG, "============= Begin ICC load ============="); + Log::logger->log(Log::DEBUG, "ICC size: {} bytes", image.rawICC.size()); + + if (const auto RET = buildIcc3DLut(prof, image); !RET) + return std::unexpected(RET.error()); + + if (*PVCGTENABLED) { + auto vcgtRes = readVCGT16(prof); + if (!vcgtRes) + return std::unexpected(vcgtRes.error()); + + image.icc.vcgt = *vcgtRes; + + if (!*vcgtRes) + Log::logger->log(Log::DEBUG, "ICC profile has no VCGT data"); + } else + Log::logger->log(Log::DEBUG, "Skipping VCGT load, disabled by config"); + + Log::logger->log(Log::DEBUG, "============= End ICC load ============="); + + return image; +}
\ No newline at end of file |
