377 lines
10 KiB
C++
377 lines
10 KiB
C++
|
/*
|
||
|
SPDX-FileCopyrightText: 2020 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
|
||
|
|
||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||
|
*/
|
||
|
|
||
|
#include "colordevice.h"
|
||
|
#include "abstract_output.h"
|
||
|
#include "utils.h"
|
||
|
|
||
|
#include "3rdparty/colortemperature.h"
|
||
|
|
||
|
#include <QTimer>
|
||
|
|
||
|
#include <lcms2.h>
|
||
|
|
||
|
namespace KWin
|
||
|
{
|
||
|
|
||
|
template <typename T>
|
||
|
struct CmsDeleter;
|
||
|
|
||
|
template <typename T>
|
||
|
using CmsScopedPointer = QScopedPointer<T, CmsDeleter<T>>;
|
||
|
|
||
|
template <>
|
||
|
struct CmsDeleter<cmsPipeline>
|
||
|
{
|
||
|
static inline void cleanup(cmsPipeline *pipeline)
|
||
|
{
|
||
|
if (pipeline) {
|
||
|
cmsPipelineFree(pipeline);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
template <>
|
||
|
struct CmsDeleter<cmsStage>
|
||
|
{
|
||
|
static inline void cleanup(cmsStage *stage)
|
||
|
{
|
||
|
if (stage) {
|
||
|
cmsStageFree(stage);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
template <>
|
||
|
struct CmsDeleter<cmsToneCurve>
|
||
|
{
|
||
|
static inline void cleanup(cmsToneCurve *toneCurve)
|
||
|
{
|
||
|
if (toneCurve) {
|
||
|
cmsFreeToneCurve(toneCurve);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
class ColorDevicePrivate
|
||
|
{
|
||
|
public:
|
||
|
enum DirtyToneCurveBit {
|
||
|
DirtyTemperatureToneCurve = 0x1,
|
||
|
DirtyBrightnessToneCurve = 0x2,
|
||
|
DirtyCalibrationToneCurve = 0x4,
|
||
|
};
|
||
|
Q_DECLARE_FLAGS(DirtyToneCurves, DirtyToneCurveBit)
|
||
|
|
||
|
void rebuildPipeline();
|
||
|
void unlinkPipeline();
|
||
|
|
||
|
void updateTemperatureToneCurves();
|
||
|
void updateBrightnessToneCurves();
|
||
|
void updateCalibrationToneCurves();
|
||
|
|
||
|
AbstractOutput *output;
|
||
|
DirtyToneCurves dirtyCurves;
|
||
|
QTimer *updateTimer;
|
||
|
QString profile;
|
||
|
uint brightness = 100;
|
||
|
uint temperature = 6500;
|
||
|
|
||
|
CmsScopedPointer<cmsStage> temperatureStage;
|
||
|
CmsScopedPointer<cmsStage> brightnessStage;
|
||
|
CmsScopedPointer<cmsStage> calibrationStage;
|
||
|
|
||
|
CmsScopedPointer<cmsPipeline> pipeline;
|
||
|
};
|
||
|
|
||
|
void ColorDevicePrivate::rebuildPipeline()
|
||
|
{
|
||
|
if (!pipeline) {
|
||
|
pipeline.reset(cmsPipelineAlloc(nullptr, 3, 3));
|
||
|
}
|
||
|
|
||
|
unlinkPipeline();
|
||
|
|
||
|
if (dirtyCurves & DirtyCalibrationToneCurve) {
|
||
|
updateCalibrationToneCurves();
|
||
|
}
|
||
|
if (dirtyCurves & DirtyBrightnessToneCurve) {
|
||
|
updateBrightnessToneCurves();
|
||
|
}
|
||
|
if (dirtyCurves & DirtyTemperatureToneCurve) {
|
||
|
updateTemperatureToneCurves();
|
||
|
}
|
||
|
|
||
|
dirtyCurves = DirtyToneCurves();
|
||
|
|
||
|
if (calibrationStage) {
|
||
|
if (!cmsPipelineInsertStage(pipeline.data(), cmsAT_END, calibrationStage.data())) {
|
||
|
qCWarning(KWIN_CORE) << "Failed to insert the color calibration pipeline stage";
|
||
|
}
|
||
|
}
|
||
|
if (temperatureStage) {
|
||
|
if (!cmsPipelineInsertStage(pipeline.data(), cmsAT_END, temperatureStage.data())) {
|
||
|
qCWarning(KWIN_CORE) << "Failed to insert the color temperature pipeline stage";
|
||
|
}
|
||
|
}
|
||
|
if (brightnessStage) {
|
||
|
if (!cmsPipelineInsertStage(pipeline.data(), cmsAT_END, brightnessStage.data())) {
|
||
|
qCWarning(KWIN_CORE) << "Failed to insert the color brightness pipeline stage";
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void ColorDevicePrivate::unlinkPipeline()
|
||
|
{
|
||
|
while (true) {
|
||
|
cmsStage *last = nullptr;
|
||
|
cmsPipelineUnlinkStage(pipeline.data(), cmsAT_END, &last);
|
||
|
|
||
|
if (!last) {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static qreal interpolate(qreal a, qreal b, qreal blendFactor)
|
||
|
{
|
||
|
return (1 - blendFactor) * a + blendFactor * b;
|
||
|
}
|
||
|
|
||
|
void ColorDevicePrivate::updateTemperatureToneCurves()
|
||
|
{
|
||
|
temperatureStage.reset();
|
||
|
|
||
|
if (temperature == 6500) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Note that cmsWhitePointFromTemp() returns a slightly green-ish white point.
|
||
|
const int blackBodyColorIndex = ((temperature - 1000) / 100) * 3;
|
||
|
const qreal blendFactor = (temperature % 100) / 100.0;
|
||
|
|
||
|
const qreal xWhitePoint = interpolate(blackbodyColor[blackBodyColorIndex + 0],
|
||
|
blackbodyColor[blackBodyColorIndex + 3],
|
||
|
blendFactor);
|
||
|
const qreal yWhitePoint = interpolate(blackbodyColor[blackBodyColorIndex + 1],
|
||
|
blackbodyColor[blackBodyColorIndex + 4],
|
||
|
blendFactor);
|
||
|
const qreal zWhitePoint = interpolate(blackbodyColor[blackBodyColorIndex + 2],
|
||
|
blackbodyColor[blackBodyColorIndex + 5],
|
||
|
blendFactor);
|
||
|
|
||
|
const double redCurveParams[] = { 1.0, xWhitePoint, 0.0 };
|
||
|
const double greenCurveParams[] = { 1.0, yWhitePoint, 0.0 };
|
||
|
const double blueCurveParams[] = { 1.0, zWhitePoint, 0.0 };
|
||
|
|
||
|
CmsScopedPointer<cmsToneCurve> redCurve(cmsBuildParametricToneCurve(nullptr, 2, redCurveParams));
|
||
|
if (!redCurve) {
|
||
|
qCWarning(KWIN_CORE) << "Failed to build the temperature tone curve for the red channel";
|
||
|
return;
|
||
|
}
|
||
|
CmsScopedPointer<cmsToneCurve> greenCurve(cmsBuildParametricToneCurve(nullptr, 2, greenCurveParams));
|
||
|
if (!greenCurve) {
|
||
|
qCWarning(KWIN_CORE) << "Failed to build the temperature tone curve for the green channel";
|
||
|
return;
|
||
|
}
|
||
|
CmsScopedPointer<cmsToneCurve> blueCurve(cmsBuildParametricToneCurve(nullptr, 2, blueCurveParams));
|
||
|
if (!blueCurve) {
|
||
|
qCWarning(KWIN_CORE) << "Failed to build the temperature tone curve for the blue channel";
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// The ownership of the tone curves will be moved to the pipeline stage.
|
||
|
cmsToneCurve *toneCurves[] = { redCurve.take(), greenCurve.take(), blueCurve.take() };
|
||
|
|
||
|
temperatureStage.reset(cmsStageAllocToneCurves(nullptr, 3, toneCurves));
|
||
|
if (!temperatureStage) {
|
||
|
qCWarning(KWIN_CORE) << "Failed to create the color temperature pipeline stage";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void ColorDevicePrivate::updateBrightnessToneCurves()
|
||
|
{
|
||
|
brightnessStage.reset();
|
||
|
|
||
|
if (brightness == 100) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const double curveParams[] = { 1.0, brightness / 100.0, 0.0 };
|
||
|
|
||
|
CmsScopedPointer<cmsToneCurve> redCurve(cmsBuildParametricToneCurve(nullptr, 2, curveParams));
|
||
|
if (!redCurve) {
|
||
|
qCWarning(KWIN_CORE) << "Failed to build the brightness tone curve for the red channel";
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
CmsScopedPointer<cmsToneCurve> greenCurve(cmsBuildParametricToneCurve(nullptr, 2, curveParams));
|
||
|
if (!greenCurve) {
|
||
|
qCWarning(KWIN_CORE) << "Failed to build the brightness tone curve for the green channel";
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
CmsScopedPointer<cmsToneCurve> blueCurve(cmsBuildParametricToneCurve(nullptr, 2, curveParams));
|
||
|
if (!blueCurve) {
|
||
|
qCWarning(KWIN_CORE) << "Failed to build the brightness tone curve for the blue channel";
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// The ownership of the tone curves will be moved to the pipeline stage.
|
||
|
cmsToneCurve *toneCurves[] = { redCurve.take(), greenCurve.take(), blueCurve.take() };
|
||
|
|
||
|
brightnessStage.reset(cmsStageAllocToneCurves(nullptr, 3, toneCurves));
|
||
|
if (!brightnessStage) {
|
||
|
qCWarning(KWIN_CORE) << "Failed to create the color brightness pipeline stage";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void ColorDevicePrivate::updateCalibrationToneCurves()
|
||
|
{
|
||
|
calibrationStage.reset();
|
||
|
|
||
|
if (profile.isNull()) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
cmsHPROFILE handle = cmsOpenProfileFromFile(profile.toUtf8(), "r");
|
||
|
if (!handle) {
|
||
|
qCWarning(KWIN_CORE) << "Failed to open color profile file:" << profile;
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
cmsToneCurve **vcgt = static_cast<cmsToneCurve **>(cmsReadTag(handle, cmsSigVcgtTag));
|
||
|
if (!vcgt || !vcgt[0]) {
|
||
|
qCWarning(KWIN_CORE) << "Profile" << profile << "has no VCGT tag";
|
||
|
} else {
|
||
|
// Need to duplicate the VCGT tone curves as they are owned by the profile.
|
||
|
cmsToneCurve *toneCurves[] = {
|
||
|
cmsDupToneCurve(vcgt[0]),
|
||
|
cmsDupToneCurve(vcgt[1]),
|
||
|
cmsDupToneCurve(vcgt[2]),
|
||
|
};
|
||
|
|
||
|
calibrationStage.reset(cmsStageAllocToneCurves(nullptr, 3, toneCurves));
|
||
|
if (!calibrationStage) {
|
||
|
qCWarning(KWIN_CORE) << "Failed to create the color calibration pipeline stage";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
cmsCloseProfile(handle);
|
||
|
}
|
||
|
|
||
|
ColorDevice::ColorDevice(AbstractOutput *output, QObject *parent)
|
||
|
: QObject(parent)
|
||
|
, d(new ColorDevicePrivate)
|
||
|
{
|
||
|
d->updateTimer = new QTimer(this);
|
||
|
d->updateTimer->setSingleShot(true);
|
||
|
connect(d->updateTimer, &QTimer::timeout, this, &ColorDevice::update);
|
||
|
|
||
|
d->output = output;
|
||
|
}
|
||
|
|
||
|
ColorDevice::~ColorDevice()
|
||
|
{
|
||
|
if (d->pipeline) {
|
||
|
d->unlinkPipeline();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
AbstractOutput *ColorDevice::output() const
|
||
|
{
|
||
|
return d->output;
|
||
|
}
|
||
|
|
||
|
uint ColorDevice::brightness() const
|
||
|
{
|
||
|
return d->brightness;
|
||
|
}
|
||
|
|
||
|
void ColorDevice::setBrightness(uint brightness)
|
||
|
{
|
||
|
if (brightness > 100) {
|
||
|
qCWarning(KWIN_CORE) << "Got invalid brightness value:" << brightness;
|
||
|
brightness = 100;
|
||
|
}
|
||
|
if (d->brightness == brightness) {
|
||
|
return;
|
||
|
}
|
||
|
d->brightness = brightness;
|
||
|
d->dirtyCurves |= ColorDevicePrivate::DirtyBrightnessToneCurve;
|
||
|
scheduleUpdate();
|
||
|
emit brightnessChanged();
|
||
|
}
|
||
|
|
||
|
uint ColorDevice::temperature() const
|
||
|
{
|
||
|
return d->temperature;
|
||
|
}
|
||
|
|
||
|
void ColorDevice::setTemperature(uint temperature)
|
||
|
{
|
||
|
if (temperature > 6500) {
|
||
|
qCWarning(KWIN_CORE) << "Got invalid temperature value:" << temperature;
|
||
|
temperature = 6500;
|
||
|
}
|
||
|
if (d->temperature == temperature) {
|
||
|
return;
|
||
|
}
|
||
|
d->temperature = temperature;
|
||
|
d->dirtyCurves |= ColorDevicePrivate::DirtyTemperatureToneCurve;
|
||
|
scheduleUpdate();
|
||
|
emit temperatureChanged();
|
||
|
}
|
||
|
|
||
|
QString ColorDevice::profile() const
|
||
|
{
|
||
|
return d->profile;
|
||
|
}
|
||
|
|
||
|
void ColorDevice::setProfile(const QString &profile)
|
||
|
{
|
||
|
if (d->profile == profile) {
|
||
|
return;
|
||
|
}
|
||
|
d->profile = profile;
|
||
|
d->dirtyCurves |= ColorDevicePrivate::DirtyCalibrationToneCurve;
|
||
|
scheduleUpdate();
|
||
|
emit profileChanged();
|
||
|
}
|
||
|
|
||
|
void ColorDevice::update()
|
||
|
{
|
||
|
d->rebuildPipeline();
|
||
|
|
||
|
GammaRamp gammaRamp(d->output->gammaRampSize());
|
||
|
uint16_t *redChannel = gammaRamp.red();
|
||
|
uint16_t *greenChannel = gammaRamp.green();
|
||
|
uint16_t *blueChannel = gammaRamp.blue();
|
||
|
|
||
|
for (uint32_t i = 0; i < gammaRamp.size(); ++i) {
|
||
|
const uint16_t index = (i * 0xffff) / (gammaRamp.size() - 1);
|
||
|
|
||
|
const uint16_t in[3] = { index, index, index };
|
||
|
uint16_t out[3] = { 0 };
|
||
|
cmsPipelineEval16(in, out, d->pipeline.data());
|
||
|
|
||
|
redChannel[i] = out[0];
|
||
|
greenChannel[i] = out[1];
|
||
|
blueChannel[i] = out[2];
|
||
|
}
|
||
|
|
||
|
if (!d->output->setGammaRamp(gammaRamp)) {
|
||
|
qCWarning(KWIN_CORE) << "Failed to update gamma ramp for output" << d->output;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void ColorDevice::scheduleUpdate()
|
||
|
{
|
||
|
d->updateTimer->start();
|
||
|
}
|
||
|
|
||
|
} // namespace KWin
|