93e0265e4e
Once in a while, we receive complaints from other fellow KDE developers about the file organization of kwin. This change addresses some of those complaints by moving all of source code in a separate directory, src/, thus making the project structure more traditional. Things such as tests are kept in their own toplevel directories. This change may wreak havoc on merge requests that add new files to kwin, but if a patch modifies an already existing file, git should be smart enough to figure out that the file has been relocated. We may potentially split the src/ directory further to make navigating the source code easier, but hopefully this is good enough already.
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
|