a53100dd55
ColorDevice gets recreated when outputs get removed and added back. This means the output can be in an unknown state that does not match the values in ColorDevice. So ensure we update after creating the device to set it to a known proper state.
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;
|
|
scheduleUpdate();
|
|
}
|
|
|
|
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
|