You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
516 lines
18 KiB
516 lines
18 KiB
/* |
|
SPDX-FileCopyrightText: 2010 Fredrik Höglund <fredrik@kde.org> |
|
SPDX-FileCopyrightText: 2011 Philipp Knechtges <philipp-dev@knechtges.com> |
|
SPDX-FileCopyrightText: 2014 Marco Martin <mart@kde.org> |
|
|
|
SPDX-License-Identifier: GPL-2.0-or-later |
|
*/ |
|
|
|
#include "contrast.h" |
|
#include "contrastshader.h" |
|
// KConfigSkeleton |
|
|
|
#include "core/rendertarget.h" |
|
#include "core/renderviewport.h" |
|
#include "effect/effects.h" |
|
#include "utils/xcbutils.h" |
|
#include "wayland/contrast.h" |
|
#include "wayland/display.h" |
|
#include "wayland/surface.h" |
|
|
|
#include <QCoreApplication> |
|
#include <QMatrix4x4> |
|
#include <QTimer> |
|
#include <QWindow> |
|
#include <cmath> // for ceil() |
|
|
|
namespace KWin |
|
{ |
|
|
|
static const QByteArray s_contrastAtomName = QByteArrayLiteral("_KDE_NET_WM_BACKGROUND_CONTRAST_REGION"); |
|
|
|
ContrastManagerInterface *ContrastEffect::s_contrastManager = nullptr; |
|
QTimer *ContrastEffect::s_contrastManagerRemoveTimer = nullptr; |
|
|
|
ContrastEffect::ContrastEffect() |
|
{ |
|
m_shader = std::make_unique<ContrastShader>(); |
|
m_shader->init(); |
|
|
|
// ### Hackish way to announce support. |
|
// Should be included in _NET_SUPPORTED instead. |
|
if (m_shader && m_shader->isValid()) { |
|
if (effects->xcbConnection()) { |
|
m_net_wm_contrast_region = effects->announceSupportProperty(s_contrastAtomName, this); |
|
} |
|
if (effects->waylandDisplay()) { |
|
if (!s_contrastManagerRemoveTimer) { |
|
s_contrastManagerRemoveTimer = new QTimer(QCoreApplication::instance()); |
|
s_contrastManagerRemoveTimer->setSingleShot(true); |
|
s_contrastManagerRemoveTimer->callOnTimeout([]() { |
|
s_contrastManager->remove(); |
|
s_contrastManager = nullptr; |
|
}); |
|
} |
|
s_contrastManagerRemoveTimer->stop(); |
|
if (!s_contrastManager) { |
|
s_contrastManager = new ContrastManagerInterface(effects->waylandDisplay(), s_contrastManagerRemoveTimer); |
|
} |
|
} |
|
} |
|
|
|
connect(effects, &EffectsHandler::windowAdded, this, &ContrastEffect::slotWindowAdded); |
|
connect(effects, &EffectsHandler::windowDeleted, this, &ContrastEffect::slotWindowDeleted); |
|
connect(effects, &EffectsHandler::propertyNotify, this, &ContrastEffect::slotPropertyNotify); |
|
connect(effects, &EffectsHandler::virtualScreenGeometryChanged, this, &ContrastEffect::slotScreenGeometryChanged); |
|
connect(effects, &EffectsHandler::xcbConnectionChanged, this, [this]() { |
|
if (m_shader && m_shader->isValid()) { |
|
m_net_wm_contrast_region = effects->announceSupportProperty(s_contrastAtomName, this); |
|
} |
|
}); |
|
|
|
// Fetch the contrast regions for all windows |
|
const QList<EffectWindow *> windowList = effects->stackingOrder(); |
|
for (EffectWindow *window : windowList) { |
|
slotWindowAdded(window); |
|
} |
|
} |
|
|
|
ContrastEffect::~ContrastEffect() |
|
{ |
|
// When compositing is restarted, avoid removing the manager immediately. |
|
if (s_contrastManager) { |
|
s_contrastManagerRemoveTimer->start(1000); |
|
} |
|
} |
|
|
|
void ContrastEffect::slotScreenGeometryChanged() |
|
{ |
|
effects->makeOpenGLContextCurrent(); |
|
if (!supported()) { |
|
effects->reloadEffect(this); |
|
return; |
|
} |
|
|
|
const QList<EffectWindow *> windowList = effects->stackingOrder(); |
|
for (EffectWindow *window : windowList) { |
|
updateContrastRegion(window); |
|
} |
|
} |
|
|
|
void ContrastEffect::updateContrastRegion(EffectWindow *w) |
|
{ |
|
QRegion region; |
|
QMatrix4x4 matrix; |
|
float colorTransform[16]; |
|
bool valid = false; |
|
|
|
if (m_net_wm_contrast_region != XCB_ATOM_NONE) { |
|
const QByteArray value = w->readProperty(m_net_wm_contrast_region, m_net_wm_contrast_region, 32); |
|
|
|
if (value.size() > 0 && !((value.size() - (16 * sizeof(uint32_t))) % ((4 * sizeof(uint32_t))))) { |
|
const uint32_t *cardinals = reinterpret_cast<const uint32_t *>(value.constData()); |
|
const float *floatCardinals = reinterpret_cast<const float *>(value.constData()); |
|
unsigned int i = 0; |
|
for (; i < ((value.size() - (16 * sizeof(uint32_t)))) / sizeof(uint32_t);) { |
|
int x = cardinals[i++]; |
|
int y = cardinals[i++]; |
|
int w = cardinals[i++]; |
|
int h = cardinals[i++]; |
|
region += Xcb::fromXNative(QRect(x, y, w, h)).toRect(); |
|
} |
|
|
|
for (unsigned int j = 0; j < 16; ++j) { |
|
colorTransform[j] = floatCardinals[i + j]; |
|
} |
|
|
|
matrix = QMatrix4x4(colorTransform); |
|
} |
|
|
|
valid = !value.isNull(); |
|
} |
|
|
|
SurfaceInterface *surf = w->surface(); |
|
|
|
if (surf && surf->contrast()) { |
|
region = surf->contrast()->region(); |
|
matrix = colorMatrix(surf->contrast()->contrast(), surf->contrast()->intensity(), surf->contrast()->saturation()); |
|
valid = true; |
|
} |
|
|
|
if (auto internal = w->internalWindow()) { |
|
const auto property = internal->property("kwin_background_region"); |
|
if (property.isValid()) { |
|
region = property.value<QRegion>(); |
|
bool ok = false; |
|
qreal contrast = internal->property("kwin_background_contrast").toReal(&ok); |
|
if (!ok) { |
|
contrast = 1.0; |
|
} |
|
qreal intensity = internal->property("kwin_background_intensity").toReal(&ok); |
|
if (!ok) { |
|
intensity = 1.0; |
|
} |
|
qreal saturation = internal->property("kwin_background_saturation").toReal(&ok); |
|
if (!ok) { |
|
saturation = 1.0; |
|
} |
|
matrix = colorMatrix(contrast, intensity, saturation); |
|
valid = true; |
|
} |
|
} |
|
|
|
if (valid) { |
|
Data &data = m_windowData[w]; |
|
data.colorMatrix = matrix; |
|
data.contrastRegion = region; |
|
} else { |
|
if (auto it = m_windowData.find(w); it != m_windowData.end()) { |
|
effects->makeOpenGLContextCurrent(); |
|
m_windowData.erase(it); |
|
} |
|
} |
|
} |
|
|
|
void ContrastEffect::slotWindowAdded(EffectWindow *w) |
|
{ |
|
SurfaceInterface *surf = w->surface(); |
|
|
|
if (surf) { |
|
m_contrastChangedConnections[w] = connect(surf, &SurfaceInterface::contrastChanged, this, [this, w]() { |
|
if (w) { |
|
updateContrastRegion(w); |
|
} |
|
}); |
|
} |
|
|
|
if (auto internal = w->internalWindow()) { |
|
internal->installEventFilter(this); |
|
} |
|
|
|
updateContrastRegion(w); |
|
} |
|
|
|
bool ContrastEffect::eventFilter(QObject *watched, QEvent *event) |
|
{ |
|
auto internal = qobject_cast<QWindow *>(watched); |
|
if (internal && event->type() == QEvent::DynamicPropertyChange) { |
|
QDynamicPropertyChangeEvent *pe = static_cast<QDynamicPropertyChangeEvent *>(event); |
|
if (pe->propertyName() == "kwin_background_region" || pe->propertyName() == "kwin_background_contrast" || pe->propertyName() == "kwin_background_intensity" || pe->propertyName() == "kwin_background_saturation") { |
|
if (auto w = effects->findWindow(internal)) { |
|
updateContrastRegion(w); |
|
} |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
void ContrastEffect::slotWindowDeleted(EffectWindow *w) |
|
{ |
|
if (m_contrastChangedConnections.contains(w)) { |
|
disconnect(m_contrastChangedConnections[w]); |
|
m_contrastChangedConnections.remove(w); |
|
} |
|
if (auto it = m_windowData.find(w); it != m_windowData.end()) { |
|
effects->makeOpenGLContextCurrent(); |
|
m_windowData.erase(it); |
|
} |
|
} |
|
|
|
void ContrastEffect::slotPropertyNotify(EffectWindow *w, long atom) |
|
{ |
|
if (w && atom == m_net_wm_contrast_region && m_net_wm_contrast_region != XCB_ATOM_NONE) { |
|
updateContrastRegion(w); |
|
} |
|
} |
|
|
|
QMatrix4x4 ContrastEffect::colorMatrix(qreal contrast, qreal intensity, qreal saturation) |
|
{ |
|
QMatrix4x4 satMatrix; // saturation |
|
QMatrix4x4 intMatrix; // intensity |
|
QMatrix4x4 contMatrix; // contrast |
|
|
|
// Saturation matrix |
|
if (!qFuzzyCompare(saturation, 1.0)) { |
|
const qreal rval = (1.0 - saturation) * .2126; |
|
const qreal gval = (1.0 - saturation) * .7152; |
|
const qreal bval = (1.0 - saturation) * .0722; |
|
|
|
satMatrix = QMatrix4x4(rval + saturation, rval, rval, 0.0, |
|
gval, gval + saturation, gval, 0.0, |
|
bval, bval, bval + saturation, 0.0, |
|
0, 0, 0, 1.0); |
|
} |
|
|
|
// IntensityMatrix |
|
if (!qFuzzyCompare(intensity, 1.0)) { |
|
intMatrix.scale(intensity, intensity, intensity); |
|
} |
|
|
|
// Contrast Matrix |
|
if (!qFuzzyCompare(contrast, 1.0)) { |
|
const float transl = (1.0 - contrast) / 2.0; |
|
|
|
contMatrix = QMatrix4x4(contrast, 0, 0, 0.0, |
|
0, contrast, 0, 0.0, |
|
0, 0, contrast, 0.0, |
|
transl, transl, transl, 1.0); |
|
} |
|
|
|
QMatrix4x4 colorMatrix = contMatrix * satMatrix * intMatrix; |
|
// colorMatrix = colorMatrix.transposed(); |
|
|
|
return colorMatrix; |
|
} |
|
|
|
bool ContrastEffect::enabledByDefault() |
|
{ |
|
GLPlatform *gl = GLPlatform::instance(); |
|
|
|
if (gl->isIntel() && gl->chipClass() < SandyBridge) { |
|
return false; |
|
} |
|
if (gl->isPanfrost() && gl->chipClass() <= MaliT8XX) { |
|
return false; |
|
} |
|
if (gl->isLima() || gl->isVideoCore4() || gl->isVideoCore3D()) { |
|
return false; |
|
} |
|
if (gl->isSoftwareEmulation()) { |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
bool ContrastEffect::supported() |
|
{ |
|
bool supported = effects->isOpenGLCompositing() && GLFramebuffer::supported(); |
|
|
|
if (supported) { |
|
int maxTexSize; |
|
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTexSize); |
|
|
|
const QSize screenSize = effects->virtualScreenSize(); |
|
if (screenSize.width() > maxTexSize || screenSize.height() > maxTexSize) { |
|
supported = false; |
|
} |
|
} |
|
return supported; |
|
} |
|
|
|
QRegion ContrastEffect::contrastRegion(const EffectWindow *w) const |
|
{ |
|
QRegion region; |
|
if (const auto it = m_windowData.find(w); it != m_windowData.end()) { |
|
const QRegion &appRegion = it->second.contrastRegion; |
|
if (!appRegion.isEmpty()) { |
|
region |= appRegion.translated(w->contentsRect().topLeft().toPoint()) & w->decorationInnerRect().toRect(); |
|
} else { |
|
// An empty region means that the blur effect should be enabled |
|
// for the whole window. |
|
region = w->decorationInnerRect().toRect(); |
|
} |
|
} |
|
|
|
return region; |
|
} |
|
|
|
void ContrastEffect::uploadRegion(std::span<QVector2D> map, const QRegion ®ion, qreal scale) |
|
{ |
|
size_t index = 0; |
|
for (const QRect &r : region) { |
|
const auto deviceRect = scaledRect(r, scale); |
|
const QVector2D topLeft = roundVector(QVector2D(deviceRect.x(), deviceRect.y())); |
|
const QVector2D topRight = roundVector(QVector2D(deviceRect.x() + deviceRect.width(), deviceRect.y())); |
|
const QVector2D bottomLeft = roundVector(QVector2D(deviceRect.x(), deviceRect.y() + deviceRect.height())); |
|
const QVector2D bottomRight = roundVector(QVector2D(deviceRect.x() + deviceRect.width(), deviceRect.y() + deviceRect.height())); |
|
|
|
// First triangle |
|
map[index++] = topRight; |
|
map[index++] = topLeft; |
|
map[index++] = bottomLeft; |
|
|
|
// Second triangle |
|
map[index++] = bottomLeft; |
|
map[index++] = bottomRight; |
|
map[index++] = topRight; |
|
} |
|
} |
|
|
|
bool ContrastEffect::uploadGeometry(GLVertexBuffer *vbo, const QRegion ®ion, qreal scale) |
|
{ |
|
const int vertexCount = region.rectCount() * 6; |
|
if (!vertexCount) { |
|
return false; |
|
} |
|
|
|
const auto map = vbo->map<QVector2D>(vertexCount); |
|
if (!map) { |
|
return false; |
|
} |
|
uploadRegion(*map, region, scale); |
|
vbo->unmap(); |
|
|
|
constexpr std::array layout{ |
|
GLVertexAttrib{ |
|
.attributeIndex = VA_Position, |
|
.componentCount = 2, |
|
.type = GL_FLOAT, |
|
.relativeOffset = 0, |
|
}, |
|
GLVertexAttrib{ |
|
.attributeIndex = VA_TexCoord, |
|
.componentCount = 2, |
|
.type = GL_FLOAT, |
|
.relativeOffset = 0, |
|
}, |
|
}; |
|
vbo->setAttribLayout(std::span(layout), sizeof(QVector2D)); |
|
return true; |
|
} |
|
|
|
bool ContrastEffect::shouldContrast(const EffectWindow *w, int mask, const WindowPaintData &data) const |
|
{ |
|
if (!m_shader || !m_shader->isValid()) { |
|
return false; |
|
} |
|
|
|
if (effects->activeFullScreenEffect() && !w->data(WindowForceBackgroundContrastRole).toBool()) { |
|
return false; |
|
} |
|
|
|
if (w->isDesktop()) { |
|
return false; |
|
} |
|
|
|
bool scaled = !qFuzzyCompare(data.xScale(), 1.0) && !qFuzzyCompare(data.yScale(), 1.0); |
|
bool translated = data.xTranslation() || data.yTranslation(); |
|
|
|
if ((scaled || (translated || (mask & PAINT_WINDOW_TRANSFORMED))) && !w->data(WindowForceBackgroundContrastRole).toBool()) { |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
void ContrastEffect::drawWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const QRegion ®ion, WindowPaintData &data) |
|
{ |
|
if (shouldContrast(w, mask, data)) { |
|
const QRect screen = viewport.renderRect().toRect(); |
|
QRegion shape = region & contrastRegion(w).translated(w->pos().toPoint()) & screen; |
|
|
|
// let's do the evil parts - someone wants to contrast behind a transformed window |
|
const bool translated = data.xTranslation() || data.yTranslation(); |
|
const bool scaled = data.xScale() != 1 || data.yScale() != 1; |
|
if (scaled) { |
|
QPoint pt = shape.boundingRect().topLeft(); |
|
QRegion scaledShape; |
|
for (QRect r : shape) { |
|
const QPointF topLeft(pt.x() + (r.x() - pt.x()) * data.xScale() + data.xTranslation(), |
|
pt.y() + (r.y() - pt.y()) * data.yScale() + data.yTranslation()); |
|
const QPoint bottomRight(std::floor(topLeft.x() + r.width() * data.xScale()) - 1, |
|
std::floor(topLeft.y() + r.height() * data.yScale()) - 1); |
|
scaledShape |= QRect(QPoint(std::floor(topLeft.x()), std::floor(topLeft.y())), bottomRight); |
|
} |
|
shape = scaledShape & region; |
|
|
|
// Only translated, not scaled |
|
} else if (translated) { |
|
QRegion translated; |
|
for (QRect r : shape) { |
|
const QRectF t = QRectF(r).translated(data.xTranslation(), data.yTranslation()); |
|
const QPoint topLeft(std::ceil(t.x()), std::ceil(t.y())); |
|
const QPoint bottomRight(std::floor(t.x() + t.width() - 1), std::floor(t.y() + t.height() - 1)); |
|
translated |= QRect(topLeft, bottomRight); |
|
} |
|
shape = translated & region; |
|
} |
|
|
|
if (!shape.isEmpty()) { |
|
doContrast(renderTarget, viewport, w, shape, screen, w->opacity() * data.opacity(), data.projectionMatrix()); |
|
} |
|
} |
|
|
|
// Draw the window over the contrast area |
|
effects->drawWindow(renderTarget, viewport, w, mask, region, data); |
|
} |
|
|
|
void ContrastEffect::doContrast(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, const QRegion &shape, const QRect &screen, const float opacity, const QMatrix4x4 &screenProjection) |
|
{ |
|
const qreal scale = viewport.scale(); |
|
const QRegion actualShape = shape & screen; |
|
const QRectF r = viewport.mapToRenderTarget(actualShape.boundingRect()); |
|
|
|
// Upload geometry for the horizontal and vertical passes |
|
GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); |
|
vbo->reset(); |
|
if (!uploadGeometry(vbo, actualShape, scale)) { |
|
return; |
|
} |
|
vbo->bindArrays(); |
|
|
|
Q_ASSERT(m_windowData.contains(w)); |
|
auto &windowData = m_windowData[w]; |
|
if (!windowData.texture || (renderTarget.texture() && windowData.texture->internalFormat() != renderTarget.texture()->internalFormat()) || windowData.texture->size() != r.size()) { |
|
windowData.texture = GLTexture::allocate(renderTarget.texture() ? renderTarget.texture()->internalFormat() : GL_RGBA8, r.size().toSize()); |
|
if (!windowData.texture) { |
|
return; |
|
} |
|
windowData.fbo = std::make_unique<GLFramebuffer>(windowData.texture.get()); |
|
windowData.texture->setFilter(GL_LINEAR); |
|
windowData.texture->setWrapMode(GL_CLAMP_TO_EDGE); |
|
} |
|
GLTexture *contrastTexture = windowData.texture.get(); |
|
contrastTexture->bind(); |
|
|
|
windowData.fbo->blitFromFramebuffer(r.toRect(), QRect(QPoint(), contrastTexture->size())); |
|
|
|
m_shader->setColorMatrix(m_windowData[w].colorMatrix); |
|
m_shader->bind(); |
|
|
|
m_shader->setOpacity(opacity); |
|
// Set up the texture matrix to transform from screen coordinates |
|
// to texture coordinates. |
|
const QRectF boundingRect = actualShape.boundingRect(); |
|
QMatrix4x4 textureMatrix; |
|
// apply texture->buffer transformation |
|
textureMatrix.translate(0.5, 0.5); |
|
textureMatrix *= renderTarget.transformation(); |
|
textureMatrix.translate(-0.5, -0.5); |
|
// scaled logical to texture coordinates |
|
textureMatrix.scale(1, -1); |
|
textureMatrix.translate(0, -1); |
|
textureMatrix.scale(1.0 / boundingRect.width(), 1.0 / boundingRect.height(), 1); |
|
textureMatrix.translate(-boundingRect.x(), -boundingRect.y(), 0); |
|
textureMatrix.scale(1.0 / viewport.scale(), 1.0 / viewport.scale()); |
|
|
|
m_shader->setTextureMatrix(textureMatrix); |
|
m_shader->setModelViewProjectionMatrix(screenProjection); |
|
|
|
vbo->draw(GL_TRIANGLES, 0, actualShape.rectCount() * 6); |
|
|
|
contrastTexture->unbind(); |
|
|
|
vbo->unbindArrays(); |
|
|
|
if (opacity < 1.0) { |
|
glDisable(GL_BLEND); |
|
} |
|
|
|
m_shader->unbind(); |
|
} |
|
|
|
bool ContrastEffect::isActive() const |
|
{ |
|
return !effects->isScreenLocked(); |
|
} |
|
|
|
bool ContrastEffect::blocksDirectScanout() const |
|
{ |
|
return false; |
|
} |
|
|
|
} // namespace KWin |
|
|
|
#include "moc_contrast.cpp"
|
|
|