contrast.cpp 17.2 KB
Newer Older
1 2 3
/*
 *   Copyright © 2010 Fredrik Höglund <fredrik@kde.org>
 *   Copyright © 2011 Philipp Knechtges <philipp-dev@knechtges.com>
4
 *   Copyright 2014 Marco Martin <mart@kde.org>
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
 *
 *   This program is free software; you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation; either version 2 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *   General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; see the file COPYING.  if not, write to
 *   the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 *   Boston, MA 02110-1301, USA.
 */

#include "contrast.h"
#include "contrastshader.h"
// KConfigSkeleton

#include <QMatrix4x4>
#include <QLinkedList>
28
#include <QWindow>
29

30 31 32 33
#include <KWayland/Server/surface_interface.h>
#include <KWayland/Server/contrast_interface.h>
#include <KWayland/Server/display.h>

34 35 36
namespace KWin
{

37 38
static const QByteArray s_contrastAtomName = QByteArrayLiteral("_KDE_NET_WM_BACKGROUND_CONTRAST_REGION");

39 40 41 42 43 44 45 46
ContrastEffect::ContrastEffect()
{
    shader = ContrastShader::create();

    reconfigure(ReconfigureAll);

    // ### Hackish way to announce support.
    //     Should be included in _NET_SUPPORTED instead.
47
    if (shader && shader->isValid()) {
48
        net_wm_contrast_region = effects->announceSupportProperty(s_contrastAtomName, this);
49 50 51 52 53
        KWayland::Server::Display *display = effects->waylandDisplay();
        if (display) {
            m_contrastManager = display->createContrastManager(this);
            m_contrastManager->create();
        }
54
    } else {
55
        net_wm_contrast_region = 0;
56 57
    }

58 59 60 61
    connect(effects, &EffectsHandler::windowAdded, this, &ContrastEffect::slotWindowAdded);
    connect(effects, &EffectsHandler::windowDeleted, this, &ContrastEffect::slotWindowDeleted);
    connect(effects, &EffectsHandler::propertyNotify, this, &ContrastEffect::slotPropertyNotify);
    connect(effects, &EffectsHandler::screenGeometryChanged, this, &ContrastEffect::slotScreenGeometryChanged);
62 63 64 65 66 67 68
    connect(effects, &EffectsHandler::xcbConnectionChanged, this,
        [this] {
            if (shader && shader->isValid()) {
                net_wm_contrast_region = effects->announceSupportProperty(s_contrastAtomName, this);
            }
        }
    );
69

70
    // Fetch the contrast regions for all windows
71
    for (EffectWindow *window: effects->stackingOrder()) {
72
        updateContrastRegion(window);
73
    }
74 75 76 77 78 79 80 81 82
}

ContrastEffect::~ContrastEffect()
{
    delete shader;
}

void ContrastEffect::slotScreenGeometryChanged()
{
83 84 85 86 87 88 89 90
    effects->makeOpenGLContextCurrent();
    if (!supported()) {
        effects->reloadEffect(this);
        return;
    }
    for (EffectWindow *window: effects->stackingOrder()) {
        updateContrastRegion(window);
    }
91 92 93 94 95 96
}

void ContrastEffect::reconfigure(ReconfigureFlags flags)
{
    Q_UNUSED(flags)

Marco Martin's avatar
Marco Martin committed
97 98 99
    if (shader)
        shader->init();

100
    if (!shader || !shader->isValid()) {
101
        effects->removeSupportProperty(s_contrastAtomName, this);
102 103 104
        delete m_contrastManager;
        m_contrastManager = nullptr;
    }
105 106
}

107
void ContrastEffect::updateContrastRegion(EffectWindow *w)
108 109
{
    QRegion region;
110
    float colorTransform[16];
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126
    QByteArray value;

    if (net_wm_contrast_region != XCB_ATOM_NONE) {
        value = w->readProperty(net_wm_contrast_region, 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 += QRect(x, y, w, h);
            }
127

128 129 130
            for (unsigned int j = 0; j < 16; ++j) {
                colorTransform[j] = floatCardinals[i + j];
            }
Marco Martin's avatar
Marco Martin committed
131

132 133
            QMatrix4x4 colorMatrix(colorTransform);
            m_colorMatrices[w] = colorMatrix;
134
        }
135 136
    }

137 138 139 140
    KWayland::Server::SurfaceInterface *surf = w->surface();

    if (surf && surf->contrast()) {
        region = surf->contrast()->region();
141
        m_colorMatrices[w] = colorMatrix(surf->contrast()->contrast(), surf->contrast()->intensity(), surf->contrast()->saturation());
142 143
    }

144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
    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;
            }
            m_colorMatrices[w] = colorMatrix(contrast, intensity, saturation);
        }
    }

165 166 167
    //!value.isNull() full window in X11 case, surf->contrast()
    //valid, full window in wayland case
    if (region.isEmpty() && (!value.isNull() || (surf && surf->contrast()))) {
168 169 170
        // Set the data to a dummy value.
        // This is needed to be able to distinguish between the value not
        // being set, and being set to an empty region.
171
        w->setData(WindowBackgroundContrastRole, 1);
172
    } else
173
        w->setData(WindowBackgroundContrastRole, region);
174 175 176 177
}

void ContrastEffect::slotWindowAdded(EffectWindow *w)
{
178 179 180 181 182 183 184 185 186 187
    KWayland::Server::SurfaceInterface *surf = w->surface();

    if (surf) {
        m_contrastChangedConnections[w] = connect(surf, &KWayland::Server::SurfaceInterface::contrastChanged, this, [this, w] () {

            if (w) {
                updateContrastRegion(w);
            }
        });
    }
188 189 190 191 192

    if (auto internal = w->internalWindow()) {
        internal->installEventFilter(this);
    }

193 194 195
    updateContrastRegion(w);
}

196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
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;
}

213 214 215 216 217
void ContrastEffect::slotWindowDeleted(EffectWindow *w)
{
    if (m_contrastChangedConnections.contains(w)) {
        disconnect(m_contrastChangedConnections[w]);
        m_contrastChangedConnections.remove(w);
218
        m_colorMatrices.remove(w);
219 220 221
    }
}

Marco Martin's avatar
Marco Martin committed
222 223
void ContrastEffect::slotPropertyNotify(EffectWindow *w, long atom)
{
224
    if (w && atom == net_wm_contrast_region && net_wm_contrast_region != XCB_ATOM_NONE) {
Marco Martin's avatar
Marco Martin committed
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
        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;
}

268 269 270 271 272 273
bool ContrastEffect::enabledByDefault()
{
    GLPlatform *gl = GLPlatform::instance();

    if (gl->isIntel() && gl->chipClass() < SandyBridge)
        return false;
274 275 276
    if (gl->isSoftwareEmulation()) {
        return false;
    }
277 278 279 280 281 282

    return true;
}

bool ContrastEffect::supported()
{
283
    bool supported = effects->isOpenGLCompositing() && GLRenderTarget::supported();
284 285 286 287 288

    if (supported) {
        int maxTexSize;
        glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTexSize);

289 290
        const QSize screenSize = effects->virtualScreenSize();
        if (screenSize.width() > maxTexSize || screenSize.height() > maxTexSize)
291 292 293 294 295 296 297 298 299
            supported = false;
    }
    return supported;
}

QRegion ContrastEffect::contrastRegion(const EffectWindow *w) const
{
    QRegion region;

300
    const QVariant value = w->data(WindowBackgroundContrastRole);
301 302 303 304 305 306 307 308
    if (value.isValid()) {
        const QRegion appRegion = qvariant_cast<QRegion>(value);
        if (!appRegion.isEmpty()) {
            region |= appRegion.translated(w->contentsRect().topLeft()) &
                      w->decorationInnerRect();
        } else {
            // An empty region means that the blur effect should be enabled
            // for the whole window.
309
            region = w->decorationInnerRect();
310 311 312 313 314 315 316 317
        }
    }

    return region;
}

void ContrastEffect::uploadRegion(QVector2D *&map, const QRegion &region)
{
318
    for (const QRect &r : region) {
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335
        const QVector2D topLeft(r.x(), r.y());
        const QVector2D topRight(r.x() + r.width(), r.y());
        const QVector2D bottomLeft(r.x(), r.y() + r.height());
        const QVector2D bottomRight(r.x() + r.width(), r.y() + r.height());

        // First triangle
        *(map++) = topRight;
        *(map++) = topLeft;
        *(map++) = bottomLeft;

        // Second triangle
        *(map++) = bottomLeft;
        *(map++) = bottomRight;
        *(map++) = topRight;
    }
}

336
void ContrastEffect::uploadGeometry(GLVertexBuffer *vbo, const QRegion &region)
337
{
338
    const int vertexCount = region.rectCount() * 6;
339 340
    if (!vertexCount)
        return;
341 342

    QVector2D *map = (QVector2D *) vbo->map(vertexCount * sizeof(QVector2D));
343
    uploadRegion(map, region);
344 345 346 347 348 349 350 351 352 353
    vbo->unmap();

    const GLVertexAttrib layout[] = {
        { VA_Position, 2, GL_FLOAT, 0 },
        { VA_TexCoord, 2, GL_FLOAT, 0 }
    };

    vbo->setAttribLayout(layout, 2, sizeof(QVector2D));
}

Marco Martin's avatar
Marco Martin committed
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385
void ContrastEffect::prePaintScreen(ScreenPrePaintData &data, int time)
{
    m_paintedArea = QRegion();
    m_currentContrast = QRegion();

    effects->prePaintScreen(data, time);
}

void ContrastEffect::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time)
{
    // this effect relies on prePaintWindow being called in the bottom to top order

    effects->prePaintWindow(w, data, time);

    if (!w->isPaintingEnabled()) {
        return;
    }
    if (!shader || !shader->isValid()) {
        return;
    }

    const QRegion oldPaint = data.paint;

    // we don't have to blur a region we don't see
    m_currentContrast -= data.clip;
    // if we have to paint a non-opaque part of this window that intersects with the
    // currently blurred region (which is not cached) we have to redraw the whole region
    if ((data.paint-data.clip).intersects(m_currentContrast)) {
        data.paint |= m_currentContrast;
    }

    // in case this window has regions to be blurred
386
    const QRect screen = effects->virtualScreenGeometry();
Marco Martin's avatar
Marco Martin committed
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
    const QRegion contrastArea = contrastRegion(w).translated(w->pos()) & screen;

    // we are not caching the window

    // if this window or an window underneath the modified area is painted again we have to
    // do everything
    if (m_paintedArea.intersects(contrastArea) || data.paint.intersects(contrastArea)) {
        data.paint |= contrastArea;

        // we have to check again whether we do not damage a blurred area
        // of a window we do not cache
        if (contrastArea.intersects(m_currentContrast)) {
            data.paint |= m_currentContrast;
        }
    }

    m_currentContrast |= contrastArea;


    // m_paintedArea keep track of all repainted areas
    m_paintedArea -= data.clip;
    m_paintedArea |= data.paint;
}

411 412
bool ContrastEffect::shouldContrast(const EffectWindow *w, int mask, const WindowPaintData &data) const
{
413
    if (!shader || !shader->isValid())
414 415
        return false;

416
    if (effects->activeFullScreenEffect() && !w->data(WindowForceBackgroundContrastRole).toBool())
417 418 419 420 421 422 423 424
        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();

425
    if ((scaled || (translated || (mask & PAINT_WINDOW_TRANSFORMED))) && !w->data(WindowForceBackgroundContrastRole).toBool())
426 427
        return false;

428
    if (!w->hasAlpha())
429 430 431 432 433 434 435
        return false;

    return true;
}

void ContrastEffect::drawWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data)
{
436
    const QRect screen = GLRenderTarget::virtualScreenGeometry();
437 438 439 440
    if (shouldContrast(w, mask, data)) {
        QRegion shape = region & contrastRegion(w).translated(w->pos()) & screen;

        // let's do the evil parts - someone wants to blur behind a transformed window
441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457
        const bool translated = data.xTranslation() || data.yTranslation();
        const bool scaled = data.xScale() != 1 || data.yScale() != 1;
        if (scaled) {
            QPoint pt = shape.boundingRect().topLeft();
            QVector<QRect> shapeRects = shape.rects();
            shape = QRegion(); // clear
            foreach (QRect r, shapeRects) {
                r.moveTo(pt.x() + (r.x() - pt.x()) * data.xScale() + data.xTranslation(),
                            pt.y() + (r.y() - pt.y()) * data.yScale() + data.yTranslation());
                r.setWidth(r.width() * data.xScale());
                r.setHeight(r.height() * data.yScale());
                shape |= r;
            }
            shape = shape & region;

        //Only translated, not scaled
        } else if (translated) {
458 459 460 461 462
            shape = shape.translated(data.xTranslation(), data.yTranslation());
            shape = shape & region;
        }

        if (!shape.isEmpty()) {
463
            doContrast(w, shape, screen, data.opacity(), data.screenProjectionMatrix());
464 465 466
        }
    }

467
    // Draw the window over the contrast area
468 469 470 471 472
    effects->drawWindow(w, mask, region, data);
}

void ContrastEffect::paintEffectFrame(EffectFrame *frame, QRegion region, double opacity, double frameOpacity)
{
473
    //FIXME: this is a no-op for now, it should figure out the right contrast, intensity, saturation
474 475 476
    effects->paintEffectFrame(frame, region, opacity, frameOpacity);
}

477
void ContrastEffect::doContrast(EffectWindow *w, const QRegion& shape, const QRect& screen, const float opacity, const QMatrix4x4 &screenProjection)
478 479 480 481
{
    const QRegion actualShape = shape & screen;
    const QRect r = actualShape.boundingRect();

482 483
    qreal scale = GLRenderTarget::virtualScreenScale();

484 485
    // Upload geometry for the horizontal and vertical passes
    GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer();
486
    uploadGeometry(vbo, actualShape);
487 488 489 490
    vbo->bindArrays();

    // Create a scratch texture and copy the area in the back buffer that we're
    // going to blur into it
491
    GLTexture scratch(GL_RGBA8, r.width() * scale, r.height() * scale);
492 493 494 495
    scratch.setFilter(GL_LINEAR);
    scratch.setWrapMode(GL_CLAMP_TO_EDGE);
    scratch.bind();

496
    const QRect sg = GLRenderTarget::virtualScreenGeometry();
497
    glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, (r.x() - sg.x()) * scale, (sg.height() - (r.y() - sg.y() + r.height())) * scale,
498
                        scratch.width(), scratch.height());
499 500 501

    // Draw the texture on the offscreen framebuffer object, while blurring it horizontally

502
    shader->setColorMatrix(m_colorMatrices.value(w));
503 504
    shader->bind();

505 506

    shader->setOpacity(opacity);
507 508 509
    // Set up the texture matrix to transform from screen coordinates
    // to texture coordinates.
    QMatrix4x4 textureMatrix;
510 511
    textureMatrix.scale(1.0 / r.width(), -1.0 / r.height(), 1);
    textureMatrix.translate(-r.x(), -r.height() - r.y(), 0);
512
    shader->setTextureMatrix(textureMatrix);
513
    shader->setModelViewProjectionMatrix(screenProjection);
514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530

    vbo->draw(GL_TRIANGLES, 0, actualShape.rectCount() * 6);

    scratch.unbind();
    scratch.discard();

    vbo->unbindArrays();

    if (opacity < 1.0) {
        glDisable(GL_BLEND);
    }

    shader->unbind();
}

} // namespace KWin