summaryrefslogtreecommitdiff
path: root/src/helpers
diff options
context:
space:
mode:
authorVaxry <43317083+vaxerski@users.noreply.github.com>2026-03-04 19:50:28 +0000
committerGitHub <noreply@github.com>2026-03-04 19:50:28 +0000
commit10754745a97c99449ef40db58bd425f5a91f9993 (patch)
tree8aa9f57bcfb2157b9fbdc387c9021db6e6a38e46 /src/helpers
parent8271cfc97b76d573f9122032d6cf5db256d63470 (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.cpp124
-rw-r--r--src/helpers/Monitor.hpp10
-rw-r--r--src/helpers/cm/ColorManagement.cpp193
-rw-r--r--src/helpers/cm/ColorManagement.hpp347
-rw-r--r--src/helpers/cm/ICC.cpp278
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