contrast.cpp 16.5 KB
Newer Older
1
/*
Vlad Zahorodnii's avatar
Vlad Zahorodnii committed
2
3
4
5
6
7
    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
*/
8
9
10
11
12
13

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

#include <QMatrix4x4>
14
#include <QWindow>
15

16
17
18
#include <KWaylandServer/surface_interface.h>
#include <KWaylandServer/contrast_interface.h>
#include <KWaylandServer/display.h>
19

20
21
22
namespace KWin
{

23
24
static const QByteArray s_contrastAtomName = QByteArrayLiteral("_KDE_NET_WM_BACKGROUND_CONTRAST_REGION");

25
26
27
28
29
30
31
32
ContrastEffect::ContrastEffect()
{
    shader = ContrastShader::create();

    reconfigure(ReconfigureAll);

    // ### Hackish way to announce support.
    //     Should be included in _NET_SUPPORTED instead.
33
    if (shader && shader->isValid()) {
34
        net_wm_contrast_region = effects->announceSupportProperty(s_contrastAtomName, this);
35
        KWaylandServer::Display *display = effects->waylandDisplay();
36
37
38
        if (display) {
            m_contrastManager = display->createContrastManager(this);
        }
39
    } else {
40
        net_wm_contrast_region = 0;
41
42
    }

43
44
45
46
    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);
47
48
49
50
51
52
53
    connect(effects, &EffectsHandler::xcbConnectionChanged, this,
        [this] {
            if (shader && shader->isValid()) {
                net_wm_contrast_region = effects->announceSupportProperty(s_contrastAtomName, this);
            }
        }
    );
54

55
    // Fetch the contrast regions for all windows
56
    for (EffectWindow *window: effects->stackingOrder()) {
57
        updateContrastRegion(window);
58
    }
59
60
61
62
63
64
65
66
67
}

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

void ContrastEffect::slotScreenGeometryChanged()
{
68
69
70
71
72
73
74
75
    effects->makeOpenGLContextCurrent();
    if (!supported()) {
        effects->reloadEffect(this);
        return;
    }
    for (EffectWindow *window: effects->stackingOrder()) {
        updateContrastRegion(window);
    }
76
77
78
79
80
81
}

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

Marco Martin's avatar
Marco Martin committed
82
83
84
    if (shader)
        shader->init();

85
    if (!shader || !shader->isValid()) {
86
        effects->removeSupportProperty(s_contrastAtomName, this);
87
88
89
        delete m_contrastManager;
        m_contrastManager = nullptr;
    }
90
91
}

92
void ContrastEffect::updateContrastRegion(EffectWindow *w)
93
94
{
    QRegion region;
95
    float colorTransform[16];
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
    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);
            }
112

113
114
115
            for (unsigned int j = 0; j < 16; ++j) {
                colorTransform[j] = floatCardinals[i + j];
            }
Marco Martin's avatar
Marco Martin committed
116

117
118
            QMatrix4x4 colorMatrix(colorTransform);
            m_colorMatrices[w] = colorMatrix;
119
        }
120
121
    }

122
    KWaylandServer::SurfaceInterface *surf = w->surface();
123
124
125

    if (surf && surf->contrast()) {
        region = surf->contrast()->region();
126
        m_colorMatrices[w] = colorMatrix(surf->contrast()->contrast(), surf->contrast()->intensity(), surf->contrast()->saturation());
127
128
    }

129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
    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);
        }
    }

150
151
152
    //!value.isNull() full window in X11 case, surf->contrast()
    //valid, full window in wayland case
    if (region.isEmpty() && (!value.isNull() || (surf && surf->contrast()))) {
153
154
155
        // 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.
156
        w->setData(WindowBackgroundContrastRole, 1);
157
    } else
158
        w->setData(WindowBackgroundContrastRole, region);
159
160
161
162
}

void ContrastEffect::slotWindowAdded(EffectWindow *w)
{
163
    KWaylandServer::SurfaceInterface *surf = w->surface();
164
165

    if (surf) {
166
        m_contrastChangedConnections[w] = connect(surf, &KWaylandServer::SurfaceInterface::contrastChanged, this, [this, w] () {
167
168
169
170
171
172

            if (w) {
                updateContrastRegion(w);
            }
        });
    }
173
174
175
176
177

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

178
179
180
    updateContrastRegion(w);
}

181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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;
}

198
199
200
201
202
void ContrastEffect::slotWindowDeleted(EffectWindow *w)
{
    if (m_contrastChangedConnections.contains(w)) {
        disconnect(m_contrastChangedConnections[w]);
        m_contrastChangedConnections.remove(w);
203
        m_colorMatrices.remove(w);
204
205
206
    }
}

Marco Martin's avatar
Marco Martin committed
207
208
void ContrastEffect::slotPropertyNotify(EffectWindow *w, long atom)
{
209
    if (w && atom == net_wm_contrast_region && net_wm_contrast_region != XCB_ATOM_NONE) {
Marco Martin's avatar
Marco Martin committed
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
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
        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;
}

253
254
255
256
257
258
bool ContrastEffect::enabledByDefault()
{
    GLPlatform *gl = GLPlatform::instance();

    if (gl->isIntel() && gl->chipClass() < SandyBridge)
        return false;
259
260
261
    if (gl->isSoftwareEmulation()) {
        return false;
    }
262
263
264
265
266
267

    return true;
}

bool ContrastEffect::supported()
{
268
    bool supported = effects->isOpenGLCompositing() && GLRenderTarget::supported();
269
270
271
272
273

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

274
275
        const QSize screenSize = effects->virtualScreenSize();
        if (screenSize.width() > maxTexSize || screenSize.height() > maxTexSize)
276
277
278
279
280
281
282
283
284
            supported = false;
    }
    return supported;
}

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

285
    const QVariant value = w->data(WindowBackgroundContrastRole);
286
287
288
289
290
291
292
293
    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.
294
            region = w->decorationInnerRect();
295
296
297
298
299
300
301
302
        }
    }

    return region;
}

void ContrastEffect::uploadRegion(QVector2D *&map, const QRegion &region)
{
303
    for (const QRect &r : region) {
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
        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;
    }
}

321
void ContrastEffect::uploadGeometry(GLVertexBuffer *vbo, const QRegion &region)
322
{
323
    const int vertexCount = region.rectCount() * 6;
324
325
    if (!vertexCount)
        return;
326
327

    QVector2D *map = (QVector2D *) vbo->map(vertexCount * sizeof(QVector2D));
328
    uploadRegion(map, region);
329
330
331
332
333
334
335
336
337
338
    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
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
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
371
    const QRect screen = effects->virtualScreenGeometry();
Marco Martin's avatar
Marco Martin committed
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
    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;
}

396
397
bool ContrastEffect::shouldContrast(const EffectWindow *w, int mask, const WindowPaintData &data) const
{
398
    if (!shader || !shader->isValid())
399
400
        return false;

401
    if (effects->activeFullScreenEffect() && !w->data(WindowForceBackgroundContrastRole).toBool())
402
403
404
405
406
407
408
409
        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();

410
    if ((scaled || (translated || (mask & PAINT_WINDOW_TRANSFORMED))) && !w->data(WindowForceBackgroundContrastRole).toBool())
411
412
        return false;

413
    if (!w->hasAlpha())
414
415
416
417
418
        return false;

    return true;
}

Albert Astals Cid's avatar
Albert Astals Cid committed
419
void ContrastEffect::drawWindow(EffectWindow *w, int mask, const QRegion &region, WindowPaintData &data)
420
{
421
    const QRect screen = GLRenderTarget::virtualScreenGeometry();
422
423
424
425
    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
426
427
428
429
        const bool translated = data.xTranslation() || data.yTranslation();
        const bool scaled = data.xScale() != 1 || data.yScale() != 1;
        if (scaled) {
            QPoint pt = shape.boundingRect().topLeft();
430
431
            QRegion scaledShape;
            for (QRect r : shape) {
432
433
434
435
                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());
436
                scaledShape |= r;
437
            }
438
            shape = scaledShape & region;
439
440
441

        //Only translated, not scaled
        } else if (translated) {
442
443
444
445
446
            shape = shape.translated(data.xTranslation(), data.yTranslation());
            shape = shape & region;
        }

        if (!shape.isEmpty()) {
447
            doContrast(w, shape, screen, data.opacity(), data.screenProjectionMatrix());
448
449
450
        }
    }

451
    // Draw the window over the contrast area
452
453
454
    effects->drawWindow(w, mask, region, data);
}

Albert Astals Cid's avatar
Albert Astals Cid committed
455
void ContrastEffect::paintEffectFrame(EffectFrame *frame, const QRegion &region, double opacity, double frameOpacity)
456
{
457
    //FIXME: this is a no-op for now, it should figure out the right contrast, intensity, saturation
458
459
460
    effects->paintEffectFrame(frame, region, opacity, frameOpacity);
}

461
void ContrastEffect::doContrast(EffectWindow *w, const QRegion& shape, const QRect& screen, const float opacity, const QMatrix4x4 &screenProjection)
462
463
464
465
{
    const QRegion actualShape = shape & screen;
    const QRect r = actualShape.boundingRect();

466
467
    qreal scale = GLRenderTarget::virtualScreenScale();

468
469
    // Upload geometry for the horizontal and vertical passes
    GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer();
470
    vbo->reset();
471
    uploadGeometry(vbo, actualShape);
472
473
474
475
    vbo->bindArrays();

    // Create a scratch texture and copy the area in the back buffer that we're
    // going to blur into it
476
    GLTexture scratch(GL_RGBA8, r.width() * scale, r.height() * scale);
477
478
479
480
    scratch.setFilter(GL_LINEAR);
    scratch.setWrapMode(GL_CLAMP_TO_EDGE);
    scratch.bind();

481
    const QRect sg = GLRenderTarget::virtualScreenGeometry();
482
    glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, (r.x() - sg.x()) * scale, (sg.height() - (r.y() - sg.y() + r.height())) * scale,
483
                        scratch.width(), scratch.height());
484
485
486

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

487
    shader->setColorMatrix(m_colorMatrices.value(w));
488
489
    shader->bind();

490
491

    shader->setOpacity(opacity);
492
493
494
    // Set up the texture matrix to transform from screen coordinates
    // to texture coordinates.
    QMatrix4x4 textureMatrix;
495
496
    textureMatrix.scale(1.0 / r.width(), -1.0 / r.height(), 1);
    textureMatrix.translate(-r.x(), -r.height() - r.y(), 0);
497
    shader->setTextureMatrix(textureMatrix);
498
    shader->setModelViewProjectionMatrix(screenProjection);
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513

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

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

    vbo->unbindArrays();

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

    shader->unbind();
}

514
515
516
517
518
bool ContrastEffect::isActive() const
{
    return !effects->isScreenLocked();
}

519
520
} // namespace KWin