core: add transfer function minimum and maximum luminance values

This redefines the transfer functions to have a custom luminance at encoded
value zero, and a custom luminance at encoded value 1, neither of which are
tied to the reference luminance, even for relative transfer functions.

The goal of that is that we can use a gamma 2.2 transfer function for the shadow
buffer, with the reference luminance being much lower than the maximum luminance.
For example, on an HDR screen you might have the reference luminance of 600 nits,
while the maximum luminance is 1000 nits. By representing this in gamma 2.2, we
can use a much smaller amount of bits per color to store the values than if we
used a linear transfer function. An additional benefit is that this way the values
in the buffer can be scaled by arbitrary amounts, for example to limit the range of
values to [0, 1], which can be represented in a normalized buffer
This commit is contained in:
Xaver Hugl 2024-07-05 20:30:15 +02:00
parent 3e4d9ce939
commit bea4d1064c
22 changed files with 248 additions and 189 deletions

View file

@ -49,7 +49,6 @@ void TestColorspaces::roundtripConversion_data()
QTest::addRow("BT709 (sRGB) <-> BT2020 (linear)") << NamedColorimetry::BT709 << TransferFunction::sRGB << NamedColorimetry::BT2020 << TransferFunction::linear << s_resolution10bit;
QTest::addRow("BT709 (gamma 2.2) <-> BT2020 (linear)") << NamedColorimetry::BT709 << TransferFunction::gamma22 << NamedColorimetry::BT2020 << TransferFunction::linear << s_resolution10bit;
QTest::addRow("BT709 (scRGB) <-> BT2020 (linear)") << NamedColorimetry::BT709 << TransferFunction::scRGB << NamedColorimetry::BT2020 << TransferFunction::linear << s_resolution10bit;
QTest::addRow("BT709 (linear) <-> BT2020 (linear)") << NamedColorimetry::BT709 << TransferFunction::linear << NamedColorimetry::BT2020 << TransferFunction::linear << s_resolution10bit;
QTest::addRow("BT709 (PQ) <-> BT2020 (linear)") << NamedColorimetry::BT709 << TransferFunction::PerceptualQuantizer << NamedColorimetry::BT2020 << TransferFunction::linear << 3 * s_resolution10bit;
}

View file

@ -145,9 +145,9 @@ void LegacyLutColorOp::program(DrmAtomicCommit *commit, std::span<const ColorOp>
QVector3D output(scaledInput, scaledInput, scaledInput);
for (const auto &op : operations) {
if (auto tf = std::get_if<ColorTransferFunction>(&op.operation)) {
output = tf->tf.encodedToNits(output, tf->referenceLuminance);
output = tf->tf.encodedToNits(output);
} else if (auto tf = std::get_if<InverseColorTransferFunction>(&op.operation)) {
output = tf->tf.nitsToEncoded(output, tf->referenceLuminance);
output = tf->tf.nitsToEncoded(output);
} else if (auto mult = std::get_if<ColorMultiplier>(&op.operation)) {
output *= mult->factors;
} else {

View file

@ -100,11 +100,11 @@ bool EglGbmLayer::doAttemptScanout(GraphicsBuffer *buffer, const ColorDescriptio
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.addTransferFunction(m_pipeline->colorDescription().transferFunction());
}
pipeline.addMultiplier(m_pipeline->output()->effectiveChannelFactors());
if (m_pipeline->output()->needsColormanagement()) {
pipeline.addInverseTransferFunction(m_pipeline->colorDescription().transferFunction(), m_pipeline->colorDescription().referenceLuminance());
pipeline.addInverseTransferFunction(m_pipeline->colorDescription().transferFunction());
}
m_colorPipeline = pipeline;
// kernel documentation says that

View file

@ -185,16 +185,12 @@ bool EglGbmLayerSurface::endRendering(const QRegion &damagedRegion, OutputFrame
if (m_surface->iccShader) {
m_surface->iccShader->setUniforms(m_surface->iccProfile, m_surface->intermediaryColorDescription, m_surface->channelFactors);
} else {
binder.shader()->setColorspaceUniforms(m_surface->intermediaryColorDescription, m_surface->targetColorDescription);
QMatrix4x4 ctm;
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);
binder.shader()->setUniform(GLShader::FloatUniform::SourceReferenceLuminance, m_surface->intermediaryColorDescription.referenceLuminance());
binder.shader()->setUniform(GLShader::FloatUniform::DestinationReferenceLuminance, m_surface->intermediaryColorDescription.referenceLuminance());
binder.shader()->setUniform(GLShader::FloatUniform::MaxDestinationLuminance, m_surface->intermediaryColorDescription.maxHdrLuminance().value_or(800));
}
QMatrix4x4 mat;
mat.scale(1, -1);

View file

@ -481,9 +481,9 @@ bool DrmOutput::doSetChannelFactors(const QVector3D &rgb)
// TODO this doesn't allow using only a CTM for night light offloading
// maybe relax correctness in that case and apply night light in non-linear space?
ColorPipeline pipeline{ValueRange{}};
pipeline.addTransferFunction(m_state.colorDescription.transferFunction(), m_state.colorDescription.referenceLuminance());
pipeline.addTransferFunction(m_state.colorDescription.transferFunction());
pipeline.addMultiplier(rgb);
pipeline.addInverseTransferFunction(m_state.colorDescription.transferFunction(), m_state.colorDescription.referenceLuminance());
pipeline.addInverseTransferFunction(m_state.colorDescription.transferFunction());
m_pipeline->setCrtcColorPipeline(pipeline);
if (DrmPipeline::commitPipelines({m_pipeline}, DrmPipeline::CommitMode::Test) == DrmPipeline::Error::None) {
m_pipeline->applyPendingChanges();

View file

@ -172,9 +172,9 @@ DrmPipeline::Error DrmPipeline::setLegacyGamma()
QVector3D output = QVector3D(input, input, input);
for (const auto &op : m_pending.crtcColorPipeline.ops) {
if (auto tf = std::get_if<ColorTransferFunction>(&op.operation)) {
output = tf->tf.encodedToNits(output, tf->referenceLuminance);
output = tf->tf.encodedToNits(output);
} else if (auto tf = std::get_if<InverseColorTransferFunction>(&op.operation)) {
output = tf->tf.nitsToEncoded(output, tf->referenceLuminance);
output = tf->tf.nitsToEncoded(output);
} else if (auto mult = std::get_if<ColorMultiplier>(&op.operation)) {
output *= mult->factors;
} else {

View file

@ -26,8 +26,6 @@ uniform sampler3D Csampler;
uniform int Asize;
uniform sampler2D Asampler;
uniform float referenceLuminance;
vec3 sample1DLut(vec3 input, sampler2D lut, int lutSize) {
float lutOffset = 0.5 / float(lutSize);
float lutScale = 1.0 - lutOffset * 2.0;
@ -40,10 +38,7 @@ vec3 sample1DLut(vec3 input, sampler2D lut, int lutSize) {
void main()
{
vec4 tex = texture2D(src, texcoord0);
tex = encodingToNits(tex, sourceNamedTransferFunction, referenceLuminance);
tex.rgb /= max(tex.a, 0.001);
tex.rgb /= referenceLuminance;
tex.rgb = (toXYZD50 * vec4(tex.rgb, 1.0)).rgb;
tex = sourceEncodingToNitsInDestinationColorspace(tex) / destinationReferenceLuminance;
if (Bsize > 0) {
tex.rgb = sample1DLut(tex.rgb, Bsampler, Bsize);
}

View file

@ -29,8 +29,6 @@ uniform sampler3D Csampler;
uniform int Asize;
uniform sampler2D Asampler;
uniform float referenceLuminance;
vec3 sample1DLut(in vec3 srcColor, in sampler2D lut, in int lutSize) {
float lutOffset = 0.5 / float(lutSize);
float lutScale = 1.0 - lutOffset * 2.0;
@ -43,10 +41,7 @@ vec3 sample1DLut(in vec3 srcColor, in sampler2D lut, in int lutSize) {
void main()
{
vec4 tex = texture(src, texcoord0);
tex = encodingToNits(tex, sourceNamedTransferFunction,referenceLuminance);
tex.rgb /= max(tex.a, 0.001);
tex.rgb /= referenceLuminance;
tex.rgb = (toXYZD50 * vec4(tex.rgb, 1.0)).rgb;
tex = sourceEncodingToNitsInDestinationColorspace(tex) / destinationReferenceLuminance;
if (Bsize > 0) {
tex.rgb = sample1DLut(tex.rgb, Bsampler, Bsize);
}

View file

@ -22,8 +22,6 @@ IccShader::IccShader()
{
m_locations = {
.src = m_shader->uniformLocation("src"),
.sourceNamedTransferFunction = m_shader->uniformLocation("sourceNamedTransferFunction"),
.referenceLuminance = m_shader->uniformLocation("referenceLuminance"),
.toXYZD50 = m_shader->uniformLocation("toXYZD50"),
.bsize = m_shader->uniformLocation("Bsize"),
.bsampler = m_shader->uniformLocation("Bsampler"),
@ -156,8 +154,11 @@ void IccShader::setUniforms(const std::shared_ptr<IccProfile> &profile, const Co
nightColor(1, 1) = channelFactors.y();
nightColor(2, 2) = channelFactors.z();
m_shader->setUniform(m_locations.toXYZD50, m_toXYZD50 * nightColor);
m_shader->setUniform(m_locations.sourceNamedTransferFunction, inputColor.transferFunction().type);
m_shader->setUniform(m_locations.referenceLuminance, inputColor.referenceLuminance());
m_shader->setUniform(GLShader::IntUniform::SourceNamedTransferFunction, inputColor.transferFunction().type);
m_shader->setUniform(GLShader::Vec2Uniform::SourceTransferFunctionParams, QVector2D(inputColor.transferFunction().minLuminance, inputColor.transferFunction().maxLuminance - inputColor.transferFunction().minLuminance));
m_shader->setUniform(GLShader::FloatUniform::SourceReferenceLuminance, inputColor.referenceLuminance());
m_shader->setUniform(GLShader::FloatUniform::DestinationReferenceLuminance, inputColor.referenceLuminance());
m_shader->setUniform(GLShader::FloatUniform::MaxDestinationLuminance, inputColor.referenceLuminance());
glActiveTexture(GL_TEXTURE1);
if (m_B) {

View file

@ -43,8 +43,6 @@ private:
struct Locations
{
int src;
int sourceNamedTransferFunction;
int referenceLuminance;
int toXYZD50;
int bsize;
int bsampler;

View file

@ -47,9 +47,9 @@ bool X11Output::setChannelFactors(const QVector3D &rgb)
return true;
}
ColorPipeline pipeline;
pipeline.addTransferFunction(TransferFunction::gamma22, 1);
pipeline.addTransferFunction(TransferFunction::gamma22);
pipeline.addMultiplier(rgb);
pipeline.addInverseTransferFunction(TransferFunction::gamma22, 1);
pipeline.addInverseTransferFunction(TransferFunction::gamma22);
std::vector<uint16_t> red(m_gammaRampSize);
std::vector<uint16_t> green(m_gammaRampSize);
std::vector<uint16_t> blue(m_gammaRampSize);

View file

@ -55,7 +55,7 @@ void ColorDevicePrivate::recalculateFactors()
blackbodyColor[blackBodyColorIndex + 5],
blendFactor);
// the values in the blackbodyColor array are "gamma corrected", but we need a linear value
temperatureFactors = TransferFunction(TransferFunction::gamma22).encodedToNits(QVector3D(xWhitePoint, yWhitePoint, zWhitePoint), 1);
temperatureFactors = TransferFunction(TransferFunction::gamma22, 0, 1).encodedToNits(QVector3D(xWhitePoint, yWhitePoint, zWhitePoint));
}
}

View file

@ -15,17 +15,17 @@ ColorPipeline ColorPipeline::create(const ColorDescription &from, const ColorDes
{
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()),
.min = from.transferFunction().nitsToEncoded(range1.min),
.max = from.transferFunction().nitsToEncoded(range1.max),
});
ret.addTransferFunction(from.transferFunction(), from.referenceLuminance());
ret.addTransferFunction(from.transferFunction());
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());
ret.addInverseTransferFunction(to.transferFunction());
return ret;
}
@ -77,11 +77,13 @@ void ColorPipeline::addMultiplier(const QVector3D &factors)
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();
tf->tf.minLuminance *= factors.x();
tf->tf.maxLuminance *= 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();
tf->tf.minLuminance /= factors.x();
tf->tf.maxLuminance /= factors.x();
ops.back().output = output;
return;
}
@ -94,59 +96,61 @@ void ColorPipeline::addMultiplier(const QVector3D &factors)
});
}
void ColorPipeline::addTransferFunction(TransferFunction tf, double referenceLuminance)
void ColorPipeline::addTransferFunction(TransferFunction tf)
{
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;
if (const auto invTf = std::get_if<InverseColorTransferFunction>(&ops.back().operation)) {
if (invTf->tf == tf) {
ops.erase(ops.end() - 1);
addMultiplier(referenceLuminance / reference);
return;
}
}
}
if (tf == TransferFunction::scRGB) {
addMultiplier(80.0);
if (tf == TransferFunction::linear) {
QMatrix4x4 mat;
mat.translate(tf.minLuminance, tf.minLuminance, tf.minLuminance);
mat.scale(tf.maxLuminance - tf.minLuminance);
addMatrix(mat, ValueRange{
.min = (mat * QVector3D(currentOutputRange().min, 0, 0)).x(),
.max = (mat * QVector3D(currentOutputRange().max, 0, 0)).x(),
});
} else {
ops.push_back(ColorOp{
.input = currentOutputRange(),
.operation = ColorTransferFunction(tf, referenceLuminance),
.operation = ColorTransferFunction(tf),
.output = ValueRange{
.min = tf.encodedToNits(currentOutputRange().min, referenceLuminance),
.max = tf.encodedToNits(currentOutputRange().max, referenceLuminance),
.min = tf.encodedToNits(currentOutputRange().min),
.max = tf.encodedToNits(currentOutputRange().max),
},
});
}
}
void ColorPipeline::addInverseTransferFunction(TransferFunction tf, double referenceLuminance)
void ColorPipeline::addInverseTransferFunction(TransferFunction tf)
{
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);
if (tf == TransferFunction::linear) {
QMatrix4x4 mat;
mat.scale(1.0 / (tf.maxLuminance - tf.minLuminance));
mat.translate(-tf.minLuminance, -tf.minLuminance, -tf.minLuminance);
addMatrix(mat, ValueRange{
.min = (mat * QVector3D(currentOutputRange().min, 0, 0)).x(),
.max = (mat * QVector3D(currentOutputRange().max, 0, 0)).x(),
});
} else {
ops.push_back(ColorOp{
.input = currentOutputRange(),
.operation = InverseColorTransferFunction(tf, referenceLuminance),
.operation = InverseColorTransferFunction(tf),
.output = ValueRange{
.min = tf.nitsToEncoded(currentOutputRange().min, referenceLuminance),
.max = tf.nitsToEncoded(currentOutputRange().max, referenceLuminance),
.min = tf.nitsToEncoded(currentOutputRange().min),
.max = tf.nitsToEncoded(currentOutputRange().max),
},
});
}
@ -168,6 +172,24 @@ static bool isFuzzyIdentity(const QMatrix4x4 &mat)
return true;
}
static bool isFuzzyScalingOnly(const QMatrix4x4 &mat)
{
// matrix calculations with floating point numbers can result in very small errors
// -> ignore them, as that just causes inefficiencies and more rounding errors
constexpr float maxResolution = 0.0000001;
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (i < 3 && i == j) {
continue;
}
if (std::abs(mat(i, j)) > maxResolution) {
return false;
}
}
}
return true;
}
void ColorPipeline::addMatrix(const QMatrix4x4 &mat, const ValueRange &output)
{
if (isFuzzyIdentity(mat)) {
@ -190,6 +212,11 @@ void ColorPipeline::addMatrix(const QMatrix4x4 &mat, const ValueRange &output)
return;
}
}
if (isFuzzyScalingOnly(mat)) {
// pure scaling, this can be simplified
addMultiplier(QVector3D(mat(0, 0), mat(1, 1), mat(2, 2)));
return;
}
ops.push_back(ColorOp{
.input = currentOutputRange(),
.operation = ColorMatrix(mat),
@ -209,9 +236,9 @@ void ColorPipeline::add(const ColorOp &op)
} 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);
addTransferFunction(tf->tf);
} else if (const auto tf = std::get_if<InverseColorTransferFunction>(&op.operation)) {
addInverseTransferFunction(tf->tf, tf->referenceLuminance);
addInverseTransferFunction(tf->tf);
}
}
@ -234,23 +261,21 @@ QVector3D ColorPipeline::evaluate(const QVector3D &input) const
} else if (const auto mult = std::get_if<ColorMultiplier>(&op.operation)) {
ret *= mult->factors;
} else if (const auto tf = std::get_if<ColorTransferFunction>(&op.operation)) {
ret = tf->tf.encodedToNits(ret, tf->referenceLuminance);
ret = tf->tf.encodedToNits(ret);
} else if (const auto tf = std::get_if<InverseColorTransferFunction>(&op.operation)) {
ret = tf->tf.nitsToEncoded(ret, tf->referenceLuminance);
ret = tf->tf.nitsToEncoded(ret);
}
}
return ret;
}
ColorTransferFunction::ColorTransferFunction(TransferFunction tf, double referenceLLuminance)
ColorTransferFunction::ColorTransferFunction(TransferFunction tf)
: tf(tf)
, referenceLuminance(referenceLLuminance)
{
}
InverseColorTransferFunction::InverseColorTransferFunction(TransferFunction tf, double referenceLLuminance)
InverseColorTransferFunction::InverseColorTransferFunction(TransferFunction tf)
: tf(tf)
, referenceLuminance(referenceLLuminance)
{
}

View file

@ -25,23 +25,21 @@ public:
class KWIN_EXPORT ColorTransferFunction
{
public:
explicit ColorTransferFunction(TransferFunction tf, double referenceLuminance);
explicit ColorTransferFunction(TransferFunction tf);
bool operator==(const ColorTransferFunction &) const = default;
TransferFunction tf;
double referenceLuminance;
};
class KWIN_EXPORT InverseColorTransferFunction
{
public:
explicit InverseColorTransferFunction(TransferFunction tf, double referenceLuminance);
explicit InverseColorTransferFunction(TransferFunction tf);
bool operator==(const InverseColorTransferFunction &) const = default;
TransferFunction tf;
double referenceLuminance;
};
class KWIN_EXPORT ColorMatrix
@ -92,8 +90,8 @@ public:
void addMultiplier(double factor);
void addMultiplier(const QVector3D &factors);
void addTransferFunction(TransferFunction tf, double referenceLuminance);
void addInverseTransferFunction(TransferFunction tf, double referenceLuminance);
void addTransferFunction(TransferFunction tf);
void addInverseTransferFunction(TransferFunction tf);
void addMatrix(const QMatrix4x4 &mat, const ValueRange &output);
void add(const ColorOp &op);

View file

@ -198,7 +198,7 @@ const Colorimetry &Colorimetry::fromName(NamedColorimetry name)
Q_UNREACHABLE();
}
const ColorDescription ColorDescription::sRGB = ColorDescription(NamedColorimetry::BT709, TransferFunction::gamma22, 100, 0, 100, 100);
const ColorDescription ColorDescription::sRGB = ColorDescription(NamedColorimetry::BT709, TransferFunction::gamma22, TransferFunction::defaultReferenceLuminanceFor(TransferFunction::gamma22), TransferFunction::defaultMinLuminanceFor(TransferFunction::gamma22), TransferFunction::defaultMaxLuminanceFor(TransferFunction::gamma22), TransferFunction::defaultMaxLuminanceFor(TransferFunction::gamma22));
ColorDescription::ColorDescription(const Colorimetry &containerColorimetry, TransferFunction tf, double referenceLuminance, double minLuminance, std::optional<double> maxAverageLuminance, std::optional<double> maxHdrLuminance)
: ColorDescription(containerColorimetry, tf, referenceLuminance, minLuminance, maxAverageLuminance, maxHdrLuminance, std::nullopt, Colorimetry::fromName(NamedColorimetry::BT709))
@ -267,105 +267,135 @@ std::optional<double> ColorDescription::maxHdrLuminance() const
return m_maxHdrLuminance;
}
static float srgbToLinear(float sRGB)
{
if (sRGB < 0.04045) {
return std::max(sRGB / 12.92, 0.0);
} else {
return std::clamp(std::pow((sRGB + 0.055) / 1.055, 12.0 / 5.0), 0.0, 1.0);
}
}
static float linearToSRGB(float linear)
{
if (linear < 0.0031308) {
return std::max(linear / 12.92, 0.0);
} else {
return std::clamp(std::pow(linear, 5.0 / 12.0) * 1.055 - 0.055, 0.0, 1.0);
}
}
static float nitsToPQ(float nits)
{
const float normalized = std::clamp(nits / 10000.0f, 0.0f, 1.0f);
const float c1 = 0.8359375;
const float c2 = 18.8515625;
const float c3 = 18.6875;
const float m1 = 0.1593017578125;
const float m2 = 78.84375;
const float powed = std::pow(normalized, m1);
const float num = c1 + c2 * powed;
const float denum = 1 + c3 * powed;
return std::pow(num / denum, m2);
}
static float pqToNits(float pq)
{
const float c1 = 0.8359375;
const float c2 = 18.8515625;
const float c3 = 18.6875;
const float m1_inv = 1.0 / 0.1593017578125;
const float m2_inv = 1.0 / 78.84375;
const float powed = std::pow(pq, m2_inv);
const float num = std::max(powed - c1, 0.0f);
const float den = c2 - c3 * powed;
return 10000.0f * std::pow(num / den, m1_inv);
}
QVector3D ColorDescription::mapTo(QVector3D rgb, const ColorDescription &dst) const
{
rgb = m_transferFunction.encodedToNits(rgb, m_referenceLuminance);
rgb = m_transferFunction.encodedToNits(rgb);
rgb *= dst.referenceLuminance() / m_referenceLuminance;
rgb = m_containerColorimetry.toOther(dst.containerColorimetry()) * rgb;
return dst.transferFunction().nitsToEncoded(rgb, dst.referenceLuminance());
return dst.transferFunction().nitsToEncoded(rgb);
}
double TransferFunction::defaultMinLuminanceFor(Type type)
{
switch (type) {
case Type::sRGB:
case Type::gamma22:
return 0.01;
case Type::linear:
return 0;
case Type::PerceptualQuantizer:
return 0.005;
}
Q_UNREACHABLE();
}
double TransferFunction::defaultMaxLuminanceFor(Type type)
{
switch (type) {
case Type::sRGB:
case Type::gamma22:
return 100;
case Type::linear:
return 1;
case Type::PerceptualQuantizer:
return 10'000;
}
Q_UNREACHABLE();
}
double TransferFunction::defaultReferenceLuminanceFor(Type type)
{
switch (type) {
case Type::PerceptualQuantizer:
return 203;
case Type::linear:
return 80;
case Type::sRGB:
case Type::gamma22:
return 100;
}
Q_UNREACHABLE();
}
TransferFunction::TransferFunction(Type tf)
: TransferFunction(tf, defaultMinLuminanceFor(tf), defaultMaxLuminanceFor(tf))
{
}
TransferFunction::TransferFunction(Type tf, double minLuminance, double maxLuminance)
: type(tf)
, minLuminance(minLuminance)
, maxLuminance(maxLuminance)
{
}
double TransferFunction::encodedToNits(double encoded, double referenceLuminance) const
double TransferFunction::encodedToNits(double encoded) const
{
switch (type) {
case TransferFunction::sRGB:
return referenceLuminance * srgbToLinear(encoded);
case TransferFunction::sRGB: {
if (encoded < 0.04045) {
return std::max(encoded / 12.92, 0.0) * (maxLuminance - minLuminance) + minLuminance;
} else {
return std::clamp(std::pow((encoded + 0.055) / 1.055, 12.0 / 5.0), 0.0, 1.0) * (maxLuminance - minLuminance) + minLuminance;
}
}
case TransferFunction::gamma22:
return referenceLuminance * std::pow(encoded, 2.2);
return std::pow(encoded, 2.2) * (maxLuminance - minLuminance) + minLuminance;
case TransferFunction::linear:
return encoded;
case TransferFunction::scRGB:
return encoded * 80.0f;
case TransferFunction::PerceptualQuantizer:
return pqToNits(encoded);
return encoded * (maxLuminance - minLuminance) + minLuminance;
case TransferFunction::PerceptualQuantizer: {
const double c1 = 0.8359375;
const double c2 = 18.8515625;
const double c3 = 18.6875;
const double m1_inv = 1.0 / 0.1593017578125;
const double m2_inv = 1.0 / 78.84375;
const double powed = std::pow(encoded, m2_inv);
const double num = std::max(powed - c1, 0.0);
const double den = c2 - c3 * powed;
return std::pow(num / den, m1_inv) * (maxLuminance - minLuminance) + minLuminance;
}
}
Q_UNREACHABLE();
}
QVector3D TransferFunction::encodedToNits(const QVector3D &encoded, double referenceLuminance) const
QVector3D TransferFunction::encodedToNits(const QVector3D &encoded) const
{
return QVector3D(encodedToNits(encoded.x(), referenceLuminance), encodedToNits(encoded.y(), referenceLuminance), encodedToNits(encoded.z(), referenceLuminance));
return QVector3D(encodedToNits(encoded.x()), encodedToNits(encoded.y()), encodedToNits(encoded.z()));
}
double TransferFunction::nitsToEncoded(double nits, double referenceLuminance) const
double TransferFunction::nitsToEncoded(double nits) const
{
const double normalized = (nits - minLuminance) / (maxLuminance - minLuminance);
switch (type) {
case TransferFunction::sRGB:
return linearToSRGB(std::clamp(nits / referenceLuminance, 0.0, 1.0));
case TransferFunction::sRGB: {
if (normalized < 0.0031308) {
return std::max(normalized / 12.92, 0.0);
} else {
return std::clamp(std::pow(normalized, 5.0 / 12.0) * 1.055 - 0.055, 0.0, 1.0);
}
}
case TransferFunction::gamma22:
return std::pow(std::clamp(nits / referenceLuminance, 0.0, 1.0), 1.0 / 2.2);
return std::pow(std::clamp(normalized, 0.0, 1.0), 1.0 / 2.2);
case TransferFunction::linear:
return nits;
case TransferFunction::scRGB:
return nits / 80.0f;
case TransferFunction::PerceptualQuantizer:
return nitsToPQ(nits);
return normalized;
case TransferFunction::PerceptualQuantizer: {
const double c1 = 0.8359375;
const double c2 = 18.8515625;
const double c3 = 18.6875;
const double m1 = 0.1593017578125;
const double m2 = 78.84375;
const double powed = std::pow(std::clamp(normalized, 0.0, 1.0), m1);
const double num = c1 + c2 * powed;
const double denum = 1 + c3 * powed;
return std::pow(num / denum, m2);
}
}
Q_UNREACHABLE();
}
QVector3D TransferFunction::nitsToEncoded(const QVector3D &nits, double referenceLuminance) const
QVector3D TransferFunction::nitsToEncoded(const QVector3D &nits) const
{
return QVector3D(nitsToEncoded(nits.x(), referenceLuminance), nitsToEncoded(nits.y(), referenceLuminance), nitsToEncoded(nits.z(), referenceLuminance));
return QVector3D(nitsToEncoded(nits.x()), nitsToEncoded(nits.y()), nitsToEncoded(nits.z()));
}
bool TransferFunction::isRelative() const
@ -376,7 +406,6 @@ bool TransferFunction::isRelative() const
return true;
case TransferFunction::linear:
case TransferFunction::PerceptualQuantizer:
case TransferFunction::scRGB:
return false;
}
Q_UNREACHABLE();

View file

@ -94,21 +94,32 @@ public:
sRGB = 0,
linear = 1,
PerceptualQuantizer = 2,
scRGB = 3,
gamma22 = 4,
gamma22 = 3,
};
TransferFunction(Type tf);
explicit TransferFunction(Type tf, double minLuminance, double maxLuminance);
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;
QVector3D nitsToEncoded(const QVector3D &nits, double referenceLuminance) const;
double encodedToNits(double encoded) const;
double nitsToEncoded(double nits) const;
QVector3D encodedToNits(const QVector3D &encoded) const;
QVector3D nitsToEncoded(const QVector3D &nits) const;
Type type;
/**
* the luminance at encoded value zero
*/
double minLuminance;
/**
* the luminance at encoded value 1
*/
double maxLuminance;
static double defaultMinLuminanceFor(Type type);
static double defaultMaxLuminanceFor(Type type);
static double defaultReferenceLuminanceFor(Type type);
};
/**

View file

@ -1,39 +1,50 @@
const int sRGB_EOTF = 0;
const int linear_EOTF = 1;
const int PQ_EOTF = 2;
const int scRGB_EOTF = 3;
const int gamma22_EOTF = 4;
const int gamma22_EOTF = 3;
uniform mat4 colorimetryTransform;
uniform int sourceNamedTransferFunction;
/**
* x: min luminance
* y: max luminance - min luminance
*/
uniform vec2 sourceTransferFunctionParams;
uniform int destinationNamedTransferFunction;
/**
* x: min luminance
* y: max luminance - min luminance
*/
uniform vec2 destinationTransferFunctionParams;
// in nits
uniform float sourceReferenceLuminance;
uniform float destinationReferenceLuminance;
uniform float maxDestinationLuminance;
vec3 nitsToPq(vec3 nits) {
vec3 normalized = clamp(nits / 10000.0, vec3(0), vec3(1));
vec3 linearToPq(vec3 linear) {
const float c1 = 0.8359375;
const float c2 = 18.8515625;
const float c3 = 18.6875;
const float m1 = 0.1593017578125;
const float m2 = 78.84375;
vec3 powed = pow(normalized, vec3(m1));
vec3 powed = pow(clamp(linear, vec3(0), vec3(1)), vec3(m1));
vec3 num = vec3(c1) + c2 * powed;
vec3 denum = vec3(1.0) + c3 * powed;
return pow(num / denum, vec3(m2));
}
vec3 pqToNits(vec3 pq) {
vec3 pqToLinear(vec3 pq) {
const float c1 = 0.8359375;
const float c2 = 18.8515625;
const float c3 = 18.6875;
const float m1_inv = 1.0 / 0.1593017578125;
const float m2_inv = 1.0 / 78.84375;
vec3 powed = pow(pq, vec3(m2_inv));
vec3 powed = pow(clamp(pq, vec3(0.0), vec3(1.0)), vec3(m2_inv));
vec3 num = max(powed - c1, vec3(0.0));
vec3 den = c2 - c3 * powed;
return 10000.0 * pow(num / den, vec3(m1_inv));
return pow(num / den, vec3(m1_inv));
}
vec3 srgbToLinear(vec3 color) {
bvec3 isLow = lessThanEqual(color, vec3(0.04045f));
@ -62,51 +73,51 @@ vec3 doTonemapping(vec3 color, float maxBrightness) {
return clamp(color.rgb, vec3(0.0), vec3(maxBrightness));
}
vec4 encodingToNits(vec4 color, int sourceTransferFunction, float referenceLuminance) {
vec4 encodingToNits(vec4 color, int sourceTransferFunction, float luminanceOffset, float luminanceScale) {
if (sourceTransferFunction == sRGB_EOTF) {
color.rgb /= max(color.a, 0.001);
color.rgb = referenceLuminance * srgbToLinear(color.rgb);
color.rgb = srgbToLinear(color.rgb) * luminanceScale + vec3(luminanceOffset);
color.rgb *= color.a;
} else if (sourceTransferFunction == linear_EOTF) {
color.rgb = color.rgb * luminanceScale + vec3(luminanceOffset);
} else if (sourceTransferFunction == PQ_EOTF) {
color.rgb /= max(color.a, 0.001);
color.rgb = pqToNits(color.rgb);
color.rgb = pqToLinear(color.rgb) * luminanceScale + vec3(luminanceOffset);
color.rgb *= color.a;
} else if (sourceTransferFunction == scRGB_EOTF) {
color.rgb *= 80.0;
} else if (sourceTransferFunction == gamma22_EOTF) {
color.rgb /= max(color.a, 0.001);
color.rgb = referenceLuminance * pow(color.rgb, vec3(2.2));
color.rgb = pow(max(color.rgb, vec3(0.0)), vec3(2.2)) * luminanceScale + vec3(luminanceOffset);
color.rgb *= color.a;
}
return color;
}
vec4 sourceEncodingToNitsInDestinationColorspace(vec4 color) {
color = encodingToNits(color, sourceNamedTransferFunction, sourceReferenceLuminance);
color = encodingToNits(color, sourceNamedTransferFunction, sourceTransferFunctionParams.x, sourceTransferFunctionParams.y);
color.rgb = color.rgb * (destinationReferenceLuminance / sourceReferenceLuminance);
color.rgb = (colorimetryTransform * vec4(color.rgb, 1.0)).rgb;
return vec4(doTonemapping(color.rgb, maxDestinationLuminance), color.a);
}
vec4 nitsToEncoding(vec4 color, int destinationTransferFunction, float referenceLuminance) {
vec4 nitsToEncoding(vec4 color, int destinationTransferFunction, float luminanceOffset, float luminanceScale) {
if (destinationTransferFunction == sRGB_EOTF) {
color.rgb /= max(color.a, 0.001);
color.rgb = linearToSrgb(doTonemapping(color.rgb, referenceLuminance) / referenceLuminance);
color.rgb = linearToSrgb((color.rgb - vec3(luminanceOffset)) / luminanceScale);
color.rgb *= color.a;
} else if (destinationTransferFunction == linear_EOTF) {
color.rgb = (color.rgb - vec3(luminanceOffset)) / luminanceScale;
} else if (destinationTransferFunction == PQ_EOTF) {
color.rgb /= max(color.a, 0.001);
color.rgb = nitsToPq(color.rgb);
color.rgb = linearToPq((color.rgb - vec3(luminanceOffset)) / luminanceScale);
color.rgb *= color.a;
} else if (destinationTransferFunction == scRGB_EOTF) {
color.rgb /= 80.0;
} else if (destinationTransferFunction == gamma22_EOTF) {
color.rgb /= max(color.a, 0.001);
color.rgb = pow(color.rgb / referenceLuminance, vec3(1.0 / 2.2));
color.rgb = pow(max((color.rgb - vec3(luminanceOffset)) / luminanceScale, vec3(0.0)), vec3(1.0 / 2.2));
color.rgb *= color.a;
}
return color;
}
vec4 nitsToDestinationEncoding(vec4 color) {
return nitsToEncoding(color, destinationNamedTransferFunction, destinationReferenceLuminance);
return nitsToEncoding(color, destinationNamedTransferFunction, destinationTransferFunctionParams.x, destinationTransferFunctionParams.y);
}

View file

@ -219,6 +219,8 @@ void GLShader::resolveLocations()
m_matrix4Locations[Mat4Uniform::ColorimetryTransformation] = uniformLocation("colorimetryTransform");
m_vec2Locations[Vec2Uniform::Offset] = uniformLocation("offset");
m_vec2Locations[Vec2Uniform::SourceTransferFunctionParams] = uniformLocation("sourceTransferFunctionParams");
m_vec2Locations[Vec2Uniform::DestinationTransferFunctionParams] = uniformLocation("destinationTransferFunctionParams");
m_vec3Locations[Vec3Uniform::PrimaryBrightness] = uniformLocation("primaryBrightness");
@ -470,10 +472,12 @@ QMatrix4x4 GLShader::getUniformMatrix4x4(const char *name)
bool GLShader::setColorspaceUniforms(const ColorDescription &src, const ColorDescription &dst)
{
const auto &srcColorimetry = src.containerColorimetry() == NamedColorimetry::BT709 ? dst.sdrColorimetry() : src.containerColorimetry();
return setUniform(GLShader::Mat4Uniform::ColorimetryTransformation, srcColorimetry.toOther(dst.containerColorimetry()))
&& setUniform(GLShader::IntUniform::SourceNamedTransferFunction, src.transferFunction().type)
&& setUniform(GLShader::IntUniform::DestinationNamedTransferFunction, dst.transferFunction().type)
return setUniform(Mat4Uniform::ColorimetryTransformation, srcColorimetry.toOther(dst.containerColorimetry()))
&& setUniform(IntUniform::SourceNamedTransferFunction, src.transferFunction().type)
&& setUniform(Vec2Uniform::SourceTransferFunctionParams, QVector2D(src.transferFunction().minLuminance, src.transferFunction().maxLuminance - src.transferFunction().minLuminance))
&& setUniform(FloatUniform::SourceReferenceLuminance, src.referenceLuminance())
&& setUniform(IntUniform::DestinationNamedTransferFunction, dst.transferFunction().type)
&& setUniform(Vec2Uniform::DestinationTransferFunctionParams, QVector2D(dst.transferFunction().minLuminance, dst.transferFunction().maxLuminance - dst.transferFunction().minLuminance))
&& setUniform(FloatUniform::DestinationReferenceLuminance, dst.referenceLuminance())
&& setUniform(FloatUniform::MaxDestinationLuminance, dst.maxHdrLuminance().value_or(10'000));
}

View file

@ -90,6 +90,8 @@ public:
enum class Vec2Uniform {
Offset,
SourceTransferFunctionParams,
DestinationTransferFunctionParams,
Vec2UniformCount
};

View file

@ -65,10 +65,9 @@ void ColorPickerEffect::paintScreen(const RenderTarget &renderTarget, const Rend
std::array<float, 4> data;
constexpr GLsizei PIXEL_SIZE = 1;
const QPoint texturePosition = viewport.mapToRenderTarget(m_scheduledPosition).toPoint();
const ColorDescription sRGBencoding(Colorimetry::fromName(NamedColorimetry::BT709), TransferFunction::gamma22, renderTarget.colorDescription().referenceLuminance(), 0, renderTarget.colorDescription().referenceLuminance(), renderTarget.colorDescription().referenceLuminance());
glReadPixels(texturePosition.x(), renderTarget.size().height() - texturePosition.y() - PIXEL_SIZE, PIXEL_SIZE, PIXEL_SIZE, GL_RGBA, GL_FLOAT, data.data());
QVector3D sRGB = 255 * renderTarget.colorDescription().mapTo(QVector3D(data[0], data[1], data[2]), sRGBencoding);
QVector3D sRGB = 255 * renderTarget.colorDescription().mapTo(QVector3D(data[0], data[1], data[2]), ColorDescription::sRGB);
QDBusConnection::sessionBus().send(m_replyMessage.createReply(QColor(sRGB.x(), sRGB.y(), sRGB.z())));
setPicking(false);
m_scheduledPosition = QPoint(-1, -1);

View file

@ -59,8 +59,6 @@ static QtWaylandServer::frog_color_managed_surface::transfer_function kwinToFrog
return QtWaylandServer::frog_color_managed_surface::transfer_function_gamma_22;
case TransferFunction::PerceptualQuantizer:
return QtWaylandServer::frog_color_managed_surface::transfer_function_st2084_pq;
case TransferFunction::scRGB:
return QtWaylandServer::frog_color_managed_surface::transfer_function_scrgb_linear;
case TransferFunction::linear:
return QtWaylandServer::frog_color_managed_surface::transfer_function_scrgb_linear;
}
@ -97,7 +95,7 @@ void FrogColorManagementSurfaceV1::frog_color_managed_surface_set_known_transfer
m_transferFunction = TransferFunction::PerceptualQuantizer;
break;
case transfer_function_scrgb_linear:
m_transferFunction = TransferFunction::scRGB;
m_transferFunction = TransferFunction(TransferFunction::linear, 0.0, 80.0);
break;
}
updateColorDescription();

View file

@ -332,8 +332,6 @@ static uint32_t kwinTFtoProtoTF(TransferFunction tf)
return xx_color_manager_v4_transfer_function::XX_COLOR_MANAGER_V4_TRANSFER_FUNCTION_LINEAR;
case TransferFunction::PerceptualQuantizer:
return xx_color_manager_v4_transfer_function::XX_COLOR_MANAGER_V4_TRANSFER_FUNCTION_ST2084_PQ;
case TransferFunction::scRGB:
return xx_color_manager_v4_transfer_function::XX_COLOR_MANAGER_V4_TRANSFER_FUNCTION_LINEAR;
case TransferFunction::gamma22:
return xx_color_manager_v4_transfer_function::XX_COLOR_MANAGER_V4_TRANSFER_FUNCTION_GAMMA22;
}