core: add color pipeline class

This allows encapsulating color operations in a generic way, which can then be used in KMS or shaders.
The class automatically optimizes out unnecessary color operations like identity matrices, and
combines consecutive operations like
- matrix + matrix
- multiplier + multiplier
- matrix + multiplier
- EOTF + inverse EOTF
- relative EOTF + multiplier
to improve efficiency and make KMS offloading easier
This commit is contained in:
Xaver Hugl 2024-07-02 19:05:13 +02:00
parent de85867675
commit 5aaab715b0
10 changed files with 395 additions and 25 deletions

View file

@ -44,6 +44,7 @@ target_sources(kwin PRIVATE
compositor_wayland.cpp
core/colorlut.cpp
core/colorlut3d.cpp
core/colorpipeline.cpp
core/colorpipelinestage.cpp
core/colorspace.cpp
core/colortransformation.cpp
@ -547,6 +548,7 @@ install(FILES
install(FILES
core/colorlut.h
core/colorlut3d.h
core/colorpipeline.h
core/colorpipelinestage.h
core/colorspace.h
core/colortransformation.h

View file

@ -7,6 +7,7 @@
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "drm_egl_layer.h"
#include "core/colorpipeline.h"
#include "core/iccprofile.h"
#include "drm_backend.h"
#include "drm_buffer.h"
@ -54,7 +55,7 @@ std::optional<OutputLayerBeginFrameInfo> EglGbmLayer::doBeginFrame()
// as the hardware cursor is more important than an incorrectly blended cursor edge
m_scanoutBuffer.reset();
return m_surface.startRendering(targetRect().size(), m_pipeline->output()->transform().combine(OutputTransform::FlipY), m_pipeline->formats(m_type), m_pipeline->colorDescription(), m_pipeline->output()->channelFactors(), m_pipeline->iccProfile(), m_pipeline->output()->needsColormanagement(), m_pipeline->output()->brightness());
return m_surface.startRendering(targetRect().size(), m_pipeline->output()->transform().combine(OutputTransform::FlipY), m_pipeline->formats(m_type), m_pipeline->colorDescription(), m_pipeline->output()->effectiveChannelFactors(), m_pipeline->iccProfile(), m_pipeline->output()->needsColormanagement());
}
bool EglGbmLayer::doEndFrame(const QRegion &renderedRegion, const QRegion &damagedRegion, OutputFrame *frame)
@ -90,12 +91,21 @@ bool EglGbmLayer::doAttemptScanout(GraphicsBuffer *buffer, const ColorDescriptio
if (directScanoutDisabled) {
return false;
}
if (m_pipeline->output()->channelFactors() != QVector3D(1, 1, 1) || (m_pipeline->output()->highDynamicRange() && m_pipeline->output()->brightness() != 1) || m_pipeline->iccProfile()) {
// TODO use GAMMA_LUT, CTM and DEGAMMA_LUT to allow direct scanout with HDR
if (m_pipeline->iccProfile()) {
// TODO make the icc profile output a color pipeline too?
return false;
}
const auto &targetColor = m_pipeline->colorDescription();
if (color.containerColorimetry() != targetColor.containerColorimetry() || color.transferFunction() != targetColor.transferFunction()) {
ColorPipeline pipeline = ColorPipeline::create(color, m_pipeline->colorDescription());
if (m_pipeline->output()->needsColormanagement()) {
// with color management enabled, the factors have to be applied in linear space
// the pipeline will optimize out the unnecessary transformations
pipeline.addTransferFunction(m_pipeline->colorDescription().transferFunction(), m_pipeline->colorDescription().referenceLuminance());
}
pipeline.addMultiplier(m_pipeline->output()->effectiveChannelFactors());
if (m_pipeline->output()->needsColormanagement()) {
pipeline.addInverseTransferFunction(m_pipeline->colorDescription().transferFunction(), m_pipeline->colorDescription().referenceLuminance());
}
if (!pipeline.isIdentity()) {
return false;
}
// kernel documentation says that

View file

@ -74,7 +74,7 @@ void EglGbmLayerSurface::destroyResources()
m_oldSurface = {};
}
std::optional<OutputLayerBeginFrameInfo> EglGbmLayerSurface::startRendering(const QSize &bufferSize, OutputTransform transformation, const QHash<uint32_t, QList<uint64_t>> &formats, const ColorDescription &colorDescription, const QVector3D &channelFactors, const std::shared_ptr<IccProfile> &iccProfile, bool enableColormanagement, double brightness)
std::optional<OutputLayerBeginFrameInfo> EglGbmLayerSurface::startRendering(const QSize &bufferSize, OutputTransform transformation, const QHash<uint32_t, QList<uint64_t>> &formats, const ColorDescription &colorDescription, const QVector3D &channelFactors, const std::shared_ptr<IccProfile> &iccProfile, bool enableColormanagement)
{
if (!checkSurface(bufferSize, formats)) {
return std::nullopt;
@ -96,17 +96,12 @@ std::optional<OutputLayerBeginFrameInfo> EglGbmLayerSurface::startRendering(cons
m_surface->currentSlot = slot;
if (m_surface->targetColorDescription != colorDescription || m_surface->channelFactors != channelFactors
|| m_surface->colormanagementEnabled != enableColormanagement || m_surface->iccProfile != iccProfile
|| m_surface->brightness != brightness) {
|| m_surface->colormanagementEnabled != enableColormanagement || m_surface->iccProfile != iccProfile) {
m_surface->damageJournal.clear();
m_surface->colormanagementEnabled = enableColormanagement;
m_surface->targetColorDescription = colorDescription;
m_surface->channelFactors = channelFactors;
m_surface->adaptedChannelFactors = Colorimetry::fromName(NamedColorimetry::BT709).toOther(colorDescription.containerColorimetry()) * channelFactors;
// normalize red to be the original brightness value again
m_surface->adaptedChannelFactors *= channelFactors.x() / m_surface->adaptedChannelFactors.x();
m_surface->iccProfile = iccProfile;
m_surface->brightness = brightness;
if (iccProfile) {
if (!m_surface->iccShader) {
m_surface->iccShader = std::make_unique<IccShader>();
@ -188,16 +183,12 @@ bool EglGbmLayerSurface::endRendering(const QRegion &damagedRegion, OutputFrame
GLFramebuffer::pushFramebuffer(fbo);
ShaderBinder binder = m_surface->iccShader ? ShaderBinder(m_surface->iccShader->shader()) : ShaderBinder(ShaderTrait::MapTexture | ShaderTrait::TransformColorspace);
if (m_surface->iccShader) {
m_surface->iccShader->setUniforms(m_surface->iccProfile, m_surface->intermediaryColorDescription.referenceLuminance(), m_surface->adaptedChannelFactors);
m_surface->iccShader->setUniforms(m_surface->iccProfile, m_surface->intermediaryColorDescription.referenceLuminance(), m_surface->channelFactors);
} else {
// enforce a 25 nits minimum sdr brightness
constexpr double minBrightness = 25;
const double referenceLuminance = m_surface->intermediaryColorDescription.referenceLuminance();
const double brightnessFactor = (m_surface->brightness * (1 - (minBrightness / referenceLuminance))) + (minBrightness / referenceLuminance);
QMatrix4x4 ctm;
ctm(0, 0) = m_surface->adaptedChannelFactors.x() * brightnessFactor;
ctm(1, 1) = m_surface->adaptedChannelFactors.y() * brightnessFactor;
ctm(2, 2) = m_surface->adaptedChannelFactors.z() * brightnessFactor;
ctm(0, 0) = m_surface->channelFactors.x();
ctm(1, 1) = m_surface->channelFactors.y();
ctm(2, 2) = m_surface->channelFactors.z();
binder.shader()->setUniform(GLShader::Mat4Uniform::ColorimetryTransformation, ctm);
binder.shader()->setUniform(GLShader::IntUniform::SourceNamedTransferFunction, m_surface->intermediaryColorDescription.transferFunction().type);
binder.shader()->setUniform(GLShader::IntUniform::DestinationNamedTransferFunction, m_surface->targetColorDescription.transferFunction().type);

View file

@ -56,7 +56,7 @@ public:
EglGbmLayerSurface(DrmGpu *gpu, EglGbmBackend *eglBackend, BufferTarget target = BufferTarget::Normal, FormatOption formatOption = FormatOption::PreferAlpha);
~EglGbmLayerSurface();
std::optional<OutputLayerBeginFrameInfo> startRendering(const QSize &bufferSize, OutputTransform transformation, const QHash<uint32_t, QList<uint64_t>> &formats, const ColorDescription &colorDescription, const QVector3D &channelFactors, const std::shared_ptr<IccProfile> &iccProfile, bool enableColormanagement, double brightness);
std::optional<OutputLayerBeginFrameInfo> startRendering(const QSize &bufferSize, OutputTransform transformation, const QHash<uint32_t, QList<uint64_t>> &formats, const ColorDescription &colorDescription, const QVector3D &channelFactors, const std::shared_ptr<IccProfile> &iccProfile, bool enableColormanagement);
bool endRendering(const QRegion &damagedRegion, OutputFrame *frame);
bool doesSurfaceFit(const QSize &size, const QHash<uint32_t, QList<uint64_t>> &formats) const;
@ -102,7 +102,6 @@ private:
ColorDescription intermediaryColorDescription = ColorDescription::sRGB;
QVector3D channelFactors = {1, 1, 1};
double brightness = 1.0;
QVector3D adaptedChannelFactors = {1, 1, 1};
std::unique_ptr<IccShader> iccShader;
std::shared_ptr<IccProfile> iccProfile;

View file

@ -493,9 +493,19 @@ bool DrmOutput::doSetChannelFactors(const QVector3D &rgb)
return true;
}
QVector3D DrmOutput::channelFactors() const
QVector3D DrmOutput::effectiveChannelFactors() const
{
return m_channelFactors;
QVector3D adaptedChannelFactors = Colorimetry::fromName(NamedColorimetry::BT709).toOther(m_state.colorDescription.containerColorimetry()) * m_channelFactors;
// normalize red to be the original brightness value again
adaptedChannelFactors *= m_channelFactors.x() / adaptedChannelFactors.x();
if (m_state.highDynamicRange) {
// enforce a minimum of 25 nits for the reference luminance
constexpr double minLuminance = 25;
const double brightnessFactor = (m_state.brightness * (1 - (minLuminance / m_state.referenceLuminance))) + (minLuminance / m_state.referenceLuminance);
return adaptedChannelFactors * brightnessFactor;
} else {
return adaptedChannelFactors;
}
}
bool DrmOutput::needsColormanagement() const

View file

@ -58,7 +58,10 @@ public:
void leaseEnded();
bool setChannelFactors(const QVector3D &rgb) override;
QVector3D channelFactors() const;
/**
* channel factors adapted to the target color space + brightness setting multiplied in
*/
QVector3D effectiveChannelFactors() const;
bool needsColormanagement() const;
void updateConnectorProperties();

238
src/core/colorpipeline.cpp Normal file
View file

@ -0,0 +1,238 @@
/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2024 Xaver Hugl <xaver.hugl@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "colorpipeline.h"
namespace KWin
{
ColorPipeline ColorPipeline::create(const ColorDescription &from, const ColorDescription &to)
{
const auto range1 = ValueRange(from.minLuminance(), from.maxHdrLuminance().value_or(from.referenceLuminance()));
ColorPipeline ret(ValueRange{
.min = from.transferFunction().nitsToEncoded(range1.min, from.referenceLuminance()),
.max = from.transferFunction().nitsToEncoded(range1.max, from.referenceLuminance()),
});
ret.addTransferFunction(from.transferFunction(), from.referenceLuminance());
ret.addMultiplier(to.referenceLuminance() / from.referenceLuminance());
// FIXME this assumes that the range stays the same with matrix multiplication
// that's not necessarily true, and figuring out the actual range could be complicated..
ret.addMatrix(from.containerColorimetry().toOther(to.containerColorimetry()), ret.currentOutputRange());
ret.addInverseTransferFunction(to.transferFunction(), to.referenceLuminance());
return ret;
}
ColorPipeline::ColorPipeline()
: inputRange(ValueRange{
.min = 0,
.max = 1,
})
{
}
ColorPipeline::ColorPipeline(const ValueRange &inputRange)
: inputRange(inputRange)
{
}
const ValueRange &ColorPipeline::currentOutputRange() const
{
return ops.empty() ? inputRange : ops.back().output;
}
void ColorPipeline::addMultiplier(double factor)
{
addMultiplier(QVector3D(factor, factor, factor));
}
void ColorPipeline::addMultiplier(const QVector3D &factors)
{
if (factors == QVector3D(1, 1, 1)) {
return;
}
const ValueRange output{
.min = currentOutputRange().min * std::min(factors.x(), std::min(factors.y(), factors.z())),
.max = currentOutputRange().max * std::max(factors.x(), std::max(factors.y(), factors.z())),
};
if (!ops.empty()) {
auto *lastOp = &ops.back().operation;
if (const auto mat = std::get_if<ColorMatrix>(lastOp)) {
mat->mat.scale(factors);
ops.back().output = output;
return;
} else if (const auto mult = std::get_if<ColorMultiplier>(lastOp)) {
mult->factors *= factors;
if (mult->factors == QVector3D(1, 1, 1)) {
ops.erase(ops.end() - 1);
} else {
ops.back().output = output;
}
return;
} else if (factors.x() == factors.y() && factors.y() == factors.z()) {
if (const auto tf = std::get_if<ColorTransferFunction>(lastOp); tf && tf->tf.isRelative()) {
tf->referenceLuminance *= factors.x();
ops.back().output = output;
return;
} else if (const auto tf = std::get_if<InverseColorTransferFunction>(lastOp); tf && tf->tf.isRelative()) {
tf->referenceLuminance /= factors.x();
ops.back().output = output;
return;
}
}
}
ops.push_back(ColorOp{
.input = currentOutputRange(),
.operation = ColorMultiplier(factors),
.output = output,
});
}
void ColorPipeline::addTransferFunction(TransferFunction tf, double referenceLuminance)
{
if (tf == TransferFunction::linear) {
return;
}
if (!ops.empty()) {
if (const auto otherTf = std::get_if<InverseColorTransferFunction>(&ops.back().operation)) {
if (otherTf->tf == tf) {
const double reference = otherTf->referenceLuminance;
ops.erase(ops.end() - 1);
addMultiplier(referenceLuminance / reference);
return;
}
}
}
if (tf == TransferFunction::scRGB) {
addMultiplier(80.0);
} else {
ops.push_back(ColorOp{
.input = currentOutputRange(),
.operation = ColorTransferFunction(tf, referenceLuminance),
.output = ValueRange{
.min = tf.encodedToNits(currentOutputRange().min, referenceLuminance),
.max = tf.encodedToNits(currentOutputRange().max, referenceLuminance),
},
});
}
}
void ColorPipeline::addInverseTransferFunction(TransferFunction tf, double referenceLuminance)
{
if (tf == TransferFunction::linear) {
return;
}
if (!ops.empty()) {
if (const auto otherTf = std::get_if<ColorTransferFunction>(&ops.back().operation)) {
if (otherTf->tf == tf) {
const double reference = otherTf->referenceLuminance;
ops.erase(ops.end() - 1);
addMultiplier(reference / referenceLuminance);
return;
}
}
}
if (tf == TransferFunction::scRGB) {
addMultiplier(1.0 / 80.0);
} else {
ops.push_back(ColorOp{
.input = currentOutputRange(),
.operation = InverseColorTransferFunction(tf, referenceLuminance),
.output = ValueRange{
.min = tf.nitsToEncoded(currentOutputRange().min, referenceLuminance),
.max = tf.nitsToEncoded(currentOutputRange().max, referenceLuminance),
},
});
}
}
void ColorPipeline::addMatrix(const QMatrix4x4 &mat, const ValueRange &output)
{
if (mat.isIdentity()) {
return;
}
if (!ops.empty()) {
auto *lastOp = &ops.back().operation;
if (const auto otherMat = std::get_if<ColorMatrix>(lastOp)) {
otherMat->mat *= mat;
ops.back().output = output;
return;
} else if (const auto mult = std::get_if<ColorMultiplier>(lastOp)) {
QMatrix4x4 scaled = mat;
scaled.scale(mult->factors);
ops.back() = ColorOp{
.input = currentOutputRange(),
.operation = ColorMatrix(scaled),
.output = output,
};
return;
}
}
ops.push_back(ColorOp{
.input = currentOutputRange(),
.operation = ColorMatrix(mat),
.output = output,
});
}
bool ColorPipeline::isIdentity() const
{
return ops.empty();
}
void ColorPipeline::add(const ColorOp &op)
{
if (const auto mat = std::get_if<ColorMatrix>(&op.operation)) {
addMatrix(mat->mat, op.output);
} else if (const auto mult = std::get_if<ColorMultiplier>(&op.operation)) {
addMultiplier(mult->factors);
} else if (const auto tf = std::get_if<ColorTransferFunction>(&op.operation)) {
addTransferFunction(tf->tf, tf->referenceLuminance);
} else if (const auto tf = std::get_if<InverseColorTransferFunction>(&op.operation)) {
addInverseTransferFunction(tf->tf, tf->referenceLuminance);
}
}
ColorPipeline ColorPipeline::merge(const ColorPipeline &onTop)
{
ColorPipeline ret{inputRange};
ret.ops = ops;
for (const auto &op : onTop.ops) {
ret.add(op);
}
return ret;
}
ColorTransferFunction::ColorTransferFunction(TransferFunction tf, double referenceLLuminance)
: tf(tf)
, referenceLuminance(referenceLLuminance)
{
}
InverseColorTransferFunction::InverseColorTransferFunction(TransferFunction tf, double referenceLLuminance)
: tf(tf)
, referenceLuminance(referenceLLuminance)
{
}
ColorMatrix::ColorMatrix(const QMatrix4x4 &mat)
: mat(mat)
{
}
ColorMultiplier::ColorMultiplier(const QVector3D &factors)
: factors(factors)
{
}
ColorMultiplier::ColorMultiplier(double factor)
: factors(factor, factor, factor)
{
}
}

102
src/core/colorpipeline.h Normal file
View file

@ -0,0 +1,102 @@
/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2024 Xaver Hugl <xaver.hugl@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "colorspace.h"
#include "kwin_export.h"
namespace KWin
{
class KWIN_EXPORT ValueRange
{
public:
double min = 0;
double max = 1;
bool operator==(const ValueRange &) const = default;
};
class KWIN_EXPORT ColorTransferFunction
{
public:
explicit ColorTransferFunction(TransferFunction tf, double referenceLuminance);
bool operator==(const ColorTransferFunction &) const = default;
TransferFunction tf;
double referenceLuminance;
};
class KWIN_EXPORT InverseColorTransferFunction
{
public:
explicit InverseColorTransferFunction(TransferFunction tf, double referenceLuminance);
bool operator==(const InverseColorTransferFunction &) const = default;
TransferFunction tf;
double referenceLuminance;
};
class KWIN_EXPORT ColorMatrix
{
public:
explicit ColorMatrix(const QMatrix4x4 &mat);
bool operator==(const ColorMatrix &) const = default;
QMatrix4x4 mat;
};
class KWIN_EXPORT ColorMultiplier
{
public:
explicit ColorMultiplier(double factor);
explicit ColorMultiplier(const QVector3D &factors);
bool operator==(const ColorMultiplier &) const = default;
QVector3D factors;
};
class KWIN_EXPORT ColorOp
{
public:
ValueRange input;
std::variant<ColorTransferFunction, InverseColorTransferFunction, ColorMatrix, ColorMultiplier> operation;
ValueRange output;
bool operator==(const ColorOp &) const = default;
};
class KWIN_EXPORT ColorPipeline
{
public:
explicit ColorPipeline();
explicit ColorPipeline(const ValueRange &inputRange);
static ColorPipeline create(const ColorDescription &from, const ColorDescription &to);
ColorPipeline merge(const ColorPipeline &onTop);
bool isIdentity() const;
bool operator==(const ColorPipeline &other) const = default;
const ValueRange &currentOutputRange() const;
void addMultiplier(double factor);
void addMultiplier(const QVector3D &factors);
void addTransferFunction(TransferFunction tf, double referenceLuminance);
void addInverseTransferFunction(TransferFunction tf, double referenceLuminance);
void addMatrix(const QMatrix4x4 &mat, const ValueRange &output);
void add(const ColorOp &op);
ValueRange inputRange;
std::vector<ColorOp> ops;
};
}

View file

@ -367,4 +367,18 @@ QVector3D TransferFunction::nitsToEncoded(const QVector3D &nits, double referenc
{
return QVector3D(nitsToEncoded(nits.x(), referenceLuminance), nitsToEncoded(nits.y(), referenceLuminance), nitsToEncoded(nits.z(), referenceLuminance));
}
bool TransferFunction::isRelative() const
{
switch (type) {
case TransferFunction::gamma22:
case TransferFunction::sRGB:
return true;
case TransferFunction::linear:
case TransferFunction::PerceptualQuantizer:
case TransferFunction::scRGB:
return false;
}
Q_UNREACHABLE();
}
}

View file

@ -102,6 +102,7 @@ public:
auto operator<=>(const TransferFunction &) const = default;
bool isRelative() const;
double encodedToNits(double encoded, double referenceLuminance) const;
double nitsToEncoded(double nits, double referenceLuminance) const;
QVector3D encodedToNits(const QVector3D &encoded, double referenceLuminance) const;