Changeset View
Standalone View
effects/blur/blur.cpp
1 | /* | 1 | /* | ||
---|---|---|---|---|---|
2 | * Copyright © 2010 Fredrik Höglund <fredrik@kde.org> | 2 | * Copyright © 2010 Fredrik Höglund <fredrik@kde.org> | ||
3 | * Copyright © 2011 Philipp Knechtges <philipp-dev@knechtges.com> | 3 | * Copyright © 2011 Philipp Knechtges <philipp-dev@knechtges.com> | ||
4 | * Copyright © 2018 Alex Nemeth <alex.nemeth329@gmail.com> | ||||
4 | * | 5 | * | ||
5 | * This program is free software; you can redistribute it and/or modify | 6 | * This program is free software; you can redistribute it and/or modify | ||
6 | * it under the terms of the GNU General Public License as published by | 7 | * it under the terms of the GNU General Public License as published by | ||
7 | * the Free Software Foundation; either version 2 of the License, or | 8 | * the Free Software Foundation; either version 2 of the License, or | ||
8 | * (at your option) any later version. | 9 | * (at your option) any later version. | ||
9 | * | 10 | * | ||
10 | * This program is distributed in the hope that it will be useful, | 11 | * This program is distributed in the hope that it will be useful, | ||
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
Show All 9 Lines | |||||
21 | #include "blur.h" | 22 | #include "blur.h" | ||
22 | #include "effects.h" | 23 | #include "effects.h" | ||
23 | #include "blurshader.h" | 24 | #include "blurshader.h" | ||
24 | // KConfigSkeleton | 25 | // KConfigSkeleton | ||
25 | #include "blurconfig.h" | 26 | #include "blurconfig.h" | ||
26 | 27 | | |||
27 | #include <QMatrix4x4> | 28 | #include <QMatrix4x4> | ||
28 | #include <QLinkedList> | 29 | #include <QLinkedList> | ||
30 | #include <math.h> // for ceil() | ||||
fredrik: Is this still needed when qPow() is not used? | |||||
fredrik: Nitpick, but it should be <cmath> and std::ceil() in C++. | |||||
29 | 31 | | |||
30 | #include <KWayland/Server/surface_interface.h> | 32 | #include <KWayland/Server/surface_interface.h> | ||
31 | #include <KWayland/Server/blur_interface.h> | 33 | #include <KWayland/Server/blur_interface.h> | ||
32 | #include <KWayland/Server/shadow_interface.h> | 34 | #include <KWayland/Server/shadow_interface.h> | ||
33 | #include <KWayland/Server/display.h> | 35 | #include <KWayland/Server/display.h> | ||
34 | 36 | | |||
35 | namespace KWin | 37 | namespace KWin | ||
romangg: `static const` and in the KWin namespace please. Also camel case then. | |||||
36 | { | 38 | { | ||
37 | 39 | | |||
38 | static const QByteArray s_blurAtomName = QByteArrayLiteral("_KDE_NET_WM_BLUR_BEHIND_REGION"); | 40 | static const QByteArray s_blurAtomName = QByteArrayLiteral("_KDE_NET_WM_BLUR_BEHIND_REGION"); | ||
39 | 41 | | |||
40 | BlurEffect::BlurEffect() | 42 | BlurEffect::BlurEffect() | ||
41 | { | 43 | { | ||
42 | initConfig<BlurConfig>(); | 44 | initConfig<BlurConfig>(); | ||
43 | shader = BlurShader::create(); | 45 | m_shader = BlurShader::create(); | ||
44 | m_simpleShader = ShaderManager::instance()->generateShaderFromResources(ShaderTrait::MapTexture, QString(), QStringLiteral("logout-blur.frag")); | 46 | m_simpleShader = ShaderManager::instance()->generateShaderFromResources(ShaderTrait::MapTexture, QString(), QStringLiteral("logout-blur.frag")); | ||
47 | m_simpleTarget = new GLRenderTarget(); | ||||
You should add a GLRenderTarget constructor that takes no arguments. fredrik: You should add a GLRenderTarget constructor that takes no arguments.
This is working around… | |||||
48 | | ||||
45 | if (!m_simpleShader->isValid()) { | 49 | if (!m_simpleShader->isValid()) { | ||
46 | qCDebug(KWINEFFECTS) << "Simple blur shader failed to load"; | 50 | qCDebug(KWINEFFECTS) << "Simple blur shader failed to load"; | ||
47 | } | 51 | } | ||
48 | 52 | | |||
49 | updateTexture(); | 53 | initBlurStrengthValues(); | ||
50 | reconfigure(ReconfigureAll); | 54 | reconfigure(ReconfigureAll); | ||
51 | 55 | | |||
52 | // ### Hackish way to announce support. | 56 | // ### Hackish way to announce support. | ||
53 | // Should be included in _NET_SUPPORTED instead. | 57 | // Should be included in _NET_SUPPORTED instead. | ||
54 | if (shader && shader->isValid() && target->valid()) { | 58 | if (m_shader && m_shader->isValid() && m_renderTargetsValid) { | ||
55 | net_wm_blur_region = effects->announceSupportProperty(s_blurAtomName, this); | 59 | net_wm_blur_region = effects->announceSupportProperty(s_blurAtomName, this); | ||
56 | KWayland::Server::Display *display = effects->waylandDisplay(); | 60 | KWayland::Server::Display *display = effects->waylandDisplay(); | ||
57 | if (display) { | 61 | if (display) { | ||
58 | m_blurManager = display->createBlurManager(this); | 62 | m_blurManager = display->createBlurManager(this); | ||
59 | m_blurManager->create(); | 63 | m_blurManager->create(); | ||
60 | } | 64 | } | ||
61 | } else { | 65 | } else { | ||
62 | net_wm_blur_region = 0; | 66 | net_wm_blur_region = 0; | ||
63 | } | 67 | } | ||
64 | 68 | | |||
65 | connect(effects, SIGNAL(windowAdded(KWin::EffectWindow*)), this, SLOT(slotWindowAdded(KWin::EffectWindow*))); | 69 | connect(effects, SIGNAL(windowAdded(KWin::EffectWindow*)), this, SLOT(slotWindowAdded(KWin::EffectWindow*))); | ||
66 | connect(effects, SIGNAL(windowDeleted(KWin::EffectWindow*)), this, SLOT(slotWindowDeleted(KWin::EffectWindow*))); | 70 | connect(effects, SIGNAL(windowDeleted(KWin::EffectWindow*)), this, SLOT(slotWindowDeleted(KWin::EffectWindow*))); | ||
67 | connect(effects, SIGNAL(propertyNotify(KWin::EffectWindow*,long)), this, SLOT(slotPropertyNotify(KWin::EffectWindow*,long))); | 71 | connect(effects, SIGNAL(propertyNotify(KWin::EffectWindow*,long)), this, SLOT(slotPropertyNotify(KWin::EffectWindow*,long))); | ||
68 | connect(effects, SIGNAL(screenGeometryChanged(QSize)), this, SLOT(slotScreenGeometryChanged())); | 72 | connect(effects, SIGNAL(screenGeometryChanged(QSize)), this, SLOT(slotScreenGeometryChanged())); | ||
69 | connect(effects, &EffectsHandler::xcbConnectionChanged, this, | 73 | connect(effects, &EffectsHandler::xcbConnectionChanged, this, | ||
70 | [this] { | 74 | [this] { | ||
71 | if (shader && shader->isValid() && target->valid()) { | 75 | if (m_shader && m_shader->isValid() && m_renderTargetsValid) { | ||
72 | net_wm_blur_region = effects->announceSupportProperty(s_blurAtomName, this); | 76 | net_wm_blur_region = effects->announceSupportProperty(s_blurAtomName, this); | ||
73 | } | 77 | } | ||
74 | } | 78 | } | ||
75 | ); | 79 | ); | ||
76 | 80 | | |||
77 | // Fetch the blur regions for all windows | 81 | // Fetch the blur regions for all windows | ||
78 | foreach (EffectWindow *window, effects->stackingOrder()) | 82 | foreach (EffectWindow *window, effects->stackingOrder()) | ||
79 | updateBlurRegion(window); | 83 | updateBlurRegion(window); | ||
80 | } | 84 | } | ||
81 | 85 | | |||
82 | BlurEffect::~BlurEffect() | 86 | BlurEffect::~BlurEffect() | ||
83 | { | 87 | { | ||
84 | windows.clear(); | 88 | deleteFBOs(); | ||
89 | | ||||
90 | delete m_simpleTarget; | ||||
91 | m_simpleTarget = nullptr; | ||||
85 | 92 | | |||
86 | delete m_simpleShader; | 93 | delete m_simpleShader; | ||
87 | delete shader; | 94 | m_simpleShader = nullptr; | ||
davidedmundson: m_simpleTarget needs deleting | |||||
88 | delete target; | 95 | | ||
zzag: Please use `nullptr`. | |||||
96 | delete m_shader; | ||||
97 | m_shader = nullptr; | ||||
anthonyfieroni: m_simpleTarget should be deleted too | |||||
zzag: Please use `nullptr`. | |||||
zzag: Please use `nullptr`. | |||||
89 | } | 98 | } | ||
90 | 99 | | |||
91 | void BlurEffect::slotScreenGeometryChanged() | 100 | void BlurEffect::slotScreenGeometryChanged() | ||
92 | { | 101 | { | ||
93 | effects->makeOpenGLContextCurrent(); | 102 | effects->makeOpenGLContextCurrent(); | ||
94 | updateTexture(); | 103 | updateTexture(); | ||
104 | | ||||
anthonyfieroni: Make a function that clears them. | |||||
95 | // Fetch the blur regions for all windows | 105 | // Fetch the blur regions for all windows | ||
96 | foreach (EffectWindow *window, effects->stackingOrder()) | 106 | foreach (EffectWindow *window, effects->stackingOrder()) | ||
97 | updateBlurRegion(window); | 107 | updateBlurRegion(window); | ||
98 | effects->doneOpenGLContextCurrent(); | 108 | effects->doneOpenGLContextCurrent(); | ||
99 | } | 109 | } | ||
100 | 110 | | |||
101 | void BlurEffect::updateTexture() { | 111 | bool BlurEffect::renderTargetsValid() const | ||
102 | delete target; | 112 | { | ||
103 | // Offscreen texture that's used as the target for the horizontal blur pass | 113 | return !m_renderTargets.isEmpty() && std::find_if(m_renderTargets.cbegin(), m_renderTargets.cend(), | ||
104 | // and the source for the vertical pass. | 114 | [](const GLRenderTarget *target) { | ||
105 | tex = GLTexture(GL_RGBA8, effects->virtualScreenSize()); | 115 | return !target->valid(); | ||
106 | tex.setFilter(GL_LINEAR); | 116 | }) == m_renderTargets.cend(); | ||
I suggest doing this check after creating the render targets, and caching the result. fredrik: I suggest doing this check after creating the render targets, and caching the result. | |||||
107 | tex.setWrapMode(GL_CLAMP_TO_EDGE); | 117 | } | ||
118 | | ||||
119 | void BlurEffect::deleteFBOs() | ||||
120 | { | ||||
121 | qDeleteAll(m_renderTargets); | ||||
fredrik: You can use qDeleteAll() here. | |||||
122 | | ||||
123 | m_renderTargets.clear(); | ||||
You can minimize it return !renderTargets.isEmpty() && std::find_if(renderTargets.cbegin(), renderTargets.cend(), [](const GLRenderTarget *target) { return !target->valid(); }) == renderTarget.cend(); anthonyfieroni: You can minimize it
```
return !renderTargets.isEmpty()
&& std::find_if(renderTargets. | |||||
zzag: `nullptr` | |||||
124 | m_renderTextures.clear(); | ||||
125 | } | ||||
fredrik: There is no need to call discard on the textures - just clear the vector. | |||||
126 | | ||||
127 | void BlurEffect::updateTexture() | ||||
128 | { | ||||
129 | deleteFBOs(); | ||||
130 | | ||||
131 | /* Reserve memory for: | ||||
132 | * - The original sized texture (1) | ||||
133 | * - The downsized textures (m_downSampleIterations) | ||||
134 | * - The helper texture (1) | ||||
135 | */ | ||||
fredrik: Call reserve() on m_renderTextures and m_renderTargets here. | |||||
136 | m_renderTargets.reserve(m_downSampleIterations + 2); | ||||
137 | m_renderTextures.reserve(m_downSampleIterations + 2); | ||||
romangg: The texture blur amount depends | |||||
fredrik: 1 << i | |||||
138 | | ||||
139 | for (int i = 0; i <= m_downSampleIterations; i++) { | ||||
140 | m_renderTextures.append(GLTexture(GL_RGBA8, effects->virtualScreenSize() / (1 << i))); | ||||
141 | m_renderTextures.last().setFilter(GL_LINEAR); | ||||
142 | m_renderTextures.last().setWrapMode(GL_CLAMP_TO_EDGE); | ||||
143 | | ||||
144 | m_renderTargets.append(new GLRenderTarget(m_renderTextures.last())); | ||||
145 | } | ||||
Why is this needed? I'm probably missing something here, but it looks to me as if the effect copies the contents of the framebuffer to the helper texture, then copies the contents of that texture to m_renderTextures[0], after which the contents of helper texture is not used again. Can't copyScreenSampleTexture() copy directly from the framebuffer to m_renderTextures[0]? fredrik: Why is this needed?
I'm probably missing something here, but it looks to me as if the effect… | |||||
This texture is only used by copyScreenSampleTexture() For example if we want to blur a window (red) we have to blur a bigger area. To avoid extended blur I use the copySample shader to create a GL_CLAMP_TO_EDGE effect (blue) when copying the texture from m_renderTextures.last() to m_renderTextures[0] Ideally we could specify to only disable extended blur on the taskbar, but I don't see a way to identify a window as the taskbar. anemeth: This texture is only used by `copyScreenSampleTexture()`
It could very well be a separate… | |||||
Okay, I see what you mean. I think this is a trade-off between two undesirable effects though, because the clamping causes abrupt changes near the edges of the blurred region when a window is moved. You can see this effect pretty clearly in the video you attached when the blur strength is set to strong. It really comes down to what you think is worse though. But my concern here is that a fullscreen texture consumes at least 8 MB of VRAM with an HD monitor, and 32 MB with a 4K monitor. I think it would be better to copy the framebuffer to m_renderTexture[0], and apply the clamping as a post-processing effect, using m_renderTexture[0] as both the source and destination, and targeting only the region outside the red rectangle for rendering. Using the same texture as both the source and destination is allowed in OpenGL when GL_ARB_texture_barrier or GL_NV_texture_barrier is supported, but unfortunately not in OpenGL ES. The taskbar (and other panels) can be identified by calling EffectWindow::isDock(). fredrik: Okay, I see what you mean. I think this is a trade-off between two undesirable effects though… | |||||
Now only the taskbar doesn't have the extended blur effect. GL_ARB_texture_barrier is supported since OpenGL 4.4, but KWin uses 3.0 I guess it's an unpopular opinion, but it's "only" 8 MB and since cards these days have at least like 1 GB of VRAM it's not a big concern. How do you suggest we handle this? anemeth: Now only the taskbar doesn't have the extended blur effect.
GL_ARB_texture_barrier is… | |||||
I checked if I have these extensions. Just to make sure it works I included another extension I know my card supports. qCDebug(KWINEFFECTS) << "NV_texture_barrier:" << hasGLExtension("NV_texture_barrier"); qCDebug(KWINEFFECTS) << "ARB_texture_barrier:" << hasGLExtension("ARB_texture_barrier"); qCDebug(KWINEFFECTS) << "GL_NV_texgen_reflection:" << hasGLExtension("GL_NV_texgen_reflection"); And the output: kwineffects: NV_texture_barrier: false kwineffects: ARB_texture_barrier: false kwineffects: GL_NV_texgen_reflection: true If this works so unreliably I don't think we should use this. anemeth: I checked if I have these extensions. Just to make sure it works I included another extension I… | |||||
Ok I realize now that the extension name begins with GL_ and this is the reason it didn't find them. The red arrow is where they should be but they aren't present. So my point still stands. anemeth: Ok I realize now that the extension name begins with GL_ and this is the reason it didn't find… | |||||
The 3.1 backend can and does use some GL 4 extensions, but it must have a fallback path for when they are not available. The NV prefix only tells you which vendor created the extension, not which vendors support it. The AMD drivers in Mesa have supported texture_barrier since 2011, and the i965 driver has supported it since 2015. But as I said on IRC, don't worry about it if your driver doesn't support it. We can add a path that uses texture_barrier after this has landed. fredrik: > GL_ARB_texture_barrier is supported since OpenGL 4.4, but KWin uses 3.0
>… | |||||
146 | | ||||
147 | // This last set is used as a temporary helper texture | ||||
148 | m_renderTextures.append(GLTexture(GL_RGBA8, effects->virtualScreenSize())); | ||||
149 | m_renderTextures.last().setFilter(GL_LINEAR); | ||||
150 | m_renderTextures.last().setWrapMode(GL_CLAMP_TO_EDGE); | ||||
I think 5 blur strength steps to select from is fine, if that means we can get rid off the magic numbers. Other opinions? romangg: I think 5 blur strength steps to select from is fine, if that means we can get rid off the… | |||||
anemeth: I'm working on a nicer solution with less magical numbers. | |||||
108 | 151 | | |||
109 | target = new GLRenderTarget(tex); | 152 | m_renderTargets.append(new GLRenderTarget(m_renderTextures.last())); | ||
153 | | ||||
154 | m_renderTargetsValid = renderTargetsValid(); | ||||
155 | | ||||
156 | // Prepare the stack for the rendering | ||||
157 | m_renderTargetStack.clear(); | ||||
158 | m_renderTargets.reserve(m_downSampleIterations * 2 - 1); | ||||
159 | | ||||
160 | // Upsample | ||||
161 | for (int i = 1; i < m_downSampleIterations; i++) { | ||||
162 | m_renderTargetStack.push(m_renderTargets[i]); | ||||
163 | } | ||||
164 | | ||||
165 | // Downsample | ||||
166 | for (int i = m_downSampleIterations; i > 0; i--) { | ||||
167 | m_renderTargetStack.push(m_renderTargets[i]); | ||||
168 | } | ||||
169 | | ||||
170 | // Copysample | ||||
171 | m_renderTargetStack.push(m_renderTargets[0]); | ||||
172 | } | ||||
173 | | ||||
174 | void BlurEffect::initBlurStrengthValues() | ||||
175 | { | ||||
176 | // This function creates an array of blur strength values that are evenly distributed | ||||
177 | | ||||
178 | // The range of the slider on the blur settings UI | ||||
179 | int numOfBlurSteps = 15; | ||||
180 | int remainingSteps = numOfBlurSteps; | ||||
181 | | ||||
182 | /* | ||||
183 | * Explanation for these numbers: | ||||
184 | * | ||||
185 | * The texture blur amount depends on the downsampling iterations and the offset value. | ||||
186 | * By changing the offset we can alter the blur amount without relying on further downsampling. | ||||
187 | * But there is a minimum and maximum value of offset per downsample iteration before we | ||||
188 | * get artifacts. | ||||
189 | * | ||||
190 | * The minOffset variable is the minimum offset value for an iteratoin before we | ||||
fredrik: Typo: iteratoin | |||||
191 | * get blocky artifacts because of the downsampling. | ||||
192 | * | ||||
193 | * The maxOffset value is the maximum offset value for an iteration before we | ||||
194 | * get diagonal line artifacts because of the nature of the dual kawase blur algorithm. | ||||
195 | * | ||||
196 | * The expandSize value is the minimum value for an iteration before we reach the end | ||||
197 | * of a texture in the shader and sample outside of the area that was copied into the | ||||
198 | * texture from the screen. | ||||
199 | */ | ||||
200 | | ||||
201 | // {minOffset, maxOffset, expandSize} | ||||
202 | blurOffsets.append({1.0, 2.0, 10}); // Down sample size / 2 | ||||
203 | blurOffsets.append({2.0, 3.0, 20}); // Down sample size / 4 | ||||
204 | blurOffsets.append({2.0, 5.0, 50}); // Down sample size / 8 | ||||
205 | blurOffsets.append({3.0, 8.0, 150}); // Down sample size / 16 | ||||
206 | //blurOffsets.append({5.0, 10.0, 400}); // Down sample size / 32 - For really high resolutions | ||||
207 | | ||||
208 | float offsetSum = 0; | ||||
209 | | ||||
210 | for (int i = 0; i < blurOffsets.size(); i++) { | ||||
211 | offsetSum += blurOffsets[i].maxOffset - blurOffsets[i].minOffset; | ||||
212 | } | ||||
213 | | ||||
214 | for (int i = 0; i < blurOffsets.size(); i++) { | ||||
215 | int iterationNumber = ceil((blurOffsets[i].maxOffset - blurOffsets[i].minOffset) / offsetSum * numOfBlurSteps); | ||||
216 | remainingSteps -= iterationNumber; | ||||
217 | | ||||
218 | if (remainingSteps < 0) { | ||||
219 | iterationNumber += remainingSteps; | ||||
220 | } | ||||
221 | | ||||
222 | float offsetDifference = blurOffsets[i].maxOffset - blurOffsets[i].minOffset; | ||||
anthonyfieroni: Make it const. | |||||
223 | | ||||
224 | for (int j = 1; j <= iterationNumber; j++) { | ||||
225 | // {iteration, offset} | ||||
226 | blurStrengthValues.append({i + 1, blurOffsets[i].minOffset + (offsetDifference / iterationNumber) * j}); | ||||
227 | } | ||||
228 | } | ||||
110 | } | 229 | } | ||
111 | 230 | | |||
112 | void BlurEffect::reconfigure(ReconfigureFlags flags) | 231 | void BlurEffect::reconfigure(ReconfigureFlags flags) | ||
113 | { | 232 | { | ||
davidedmundson: m_simpleTarget no longer gets updated on screen changes. | |||||
What do you mean by this? anemeth: What do you mean by this?
m_simpleTarget works independently form the vector of rendertargets. | |||||
114 | Q_UNUSED(flags) | 233 | Q_UNUSED(flags) | ||
115 | 234 | | |||
anthonyfieroni: Use same function from above. | |||||
116 | BlurConfig::self()->read(); | 235 | BlurConfig::self()->read(); | ||
117 | int radius = qBound(2, BlurConfig::blurRadius(), 14); | | |||
118 | if (shader) | | |||
119 | shader->setRadius(radius); | | |||
120 | 236 | | |||
121 | m_shouldCache = BlurConfig::cacheTexture(); | 237 | m_useSimpleBlur = BlurConfig::useSimpleBlur(); | ||
122 | 238 | | |||
123 | windows.clear(); | 239 | int blurStrength = BlurConfig::blurStrength() - 1; | ||
240 | m_downSampleIterations = blurStrengthValues[blurStrength].iteration; | ||||
zzag: Where did they come from? | |||||
I came up with these numbers by testing different iteration and offset values. anemeth: I came up with these numbers by testing different iteration and offset values. | |||||
Just a naming thing: maybe it should be blurRadius.. So, for given blur radius blurRadius lookup corresponding "Dual filter" params in a map (BlurRadius -> BlurParams). zzag: Just a naming thing: maybe it should be `blurRadius`.. So, for given blur radius `blurRadius`… | |||||
romangg: You need to guard against out of bounds values. | |||||
241 | m_offset = blurStrengthValues[blurStrength].offset; | ||||
242 | m_expandSize = blurOffsets[m_downSampleIterations - 1].expandSize; | ||||
All this data is constant, so it doesn't make sense to reinitialize it on every Blur instantiation and it is only a helper for reconfigure. Maybe instead as static const in the file? See here how to do it for a QVector: https://forum.qt.io/topic/32353/solved-creating-a-const-qvector-with-values romangg: All this data is constant, so it doesn't make sense to reinitialize it on every Blur… | |||||
124 | 243 | | |||
125 | if (!shader || !shader->isValid()) { | 244 | updateTexture(); | ||
245 | | ||||
246 | if (!m_shader || !m_shader->isValid()) { | ||||
126 | effects->removeSupportProperty(s_blurAtomName, this); | 247 | effects->removeSupportProperty(s_blurAtomName, this); | ||
127 | delete m_blurManager; | 248 | delete m_blurManager; | ||
128 | m_blurManager = nullptr; | 249 | m_blurManager = nullptr; | ||
129 | } | 250 | } | ||
130 | } | 251 | } | ||
131 | 252 | | |||
132 | void BlurEffect::updateBlurRegion(EffectWindow *w) const | 253 | void BlurEffect::updateBlurRegion(EffectWindow *w) const | ||
133 | { | 254 | { | ||
fredrik: Does calling effects->addRepaintFull() also work? | |||||
anemeth: It does.
Changed to that. | |||||
134 | QRegion region; | 255 | QRegion region; | ||
135 | QByteArray value; | 256 | QByteArray value; | ||
136 | 257 | | |||
137 | if (net_wm_blur_region != XCB_ATOM_NONE) { | 258 | if (net_wm_blur_region != XCB_ATOM_NONE) { | ||
138 | value = w->readProperty(net_wm_blur_region, XCB_ATOM_CARDINAL, 32); | 259 | value = w->readProperty(net_wm_blur_region, XCB_ATOM_CARDINAL, 32); | ||
139 | if (value.size() > 0 && !(value.size() % (4 * sizeof(uint32_t)))) { | 260 | if (value.size() > 0 && !(value.size() % (4 * sizeof(uint32_t)))) { | ||
140 | const uint32_t *cardinals = reinterpret_cast<const uint32_t*>(value.constData()); | 261 | const uint32_t *cardinals = reinterpret_cast<const uint32_t*>(value.constData()); | ||
141 | for (unsigned int i = 0; i < value.size() / sizeof(uint32_t);) { | 262 | for (unsigned int i = 0; i < value.size() / sizeof(uint32_t);) { | ||
142 | int x = cardinals[i++]; | 263 | int x = cardinals[i++]; | ||
143 | int y = cardinals[i++]; | 264 | int y = cardinals[i++]; | ||
144 | int w = cardinals[i++]; | 265 | int w = cardinals[i++]; | ||
Please add a comment explaining each number(e.g. the first number is the number of downsampling and upsampling iterations, and so on). Just for readability. zzag: Please add a comment explaining each number(e.g. the first number is the number of downsampling… | |||||
145 | int h = cardinals[i++]; | 266 | int h = cardinals[i++]; | ||
146 | region += QRect(x, y, w, h); | 267 | region += QRect(x, y, w, h); | ||
147 | } | 268 | } | ||
148 | } | 269 | } | ||
149 | } | 270 | } | ||
150 | 271 | | |||
151 | KWayland::Server::SurfaceInterface *surf = w->surface(); | 272 | KWayland::Server::SurfaceInterface *surf = w->surface(); | ||
152 | 273 | | |||
Show All 12 Lines | 285 | } else | |||
165 | w->setData(WindowBlurBehindRole, region); | 286 | w->setData(WindowBlurBehindRole, region); | ||
166 | } | 287 | } | ||
167 | 288 | | |||
168 | void BlurEffect::slotWindowAdded(EffectWindow *w) | 289 | void BlurEffect::slotWindowAdded(EffectWindow *w) | ||
169 | { | 290 | { | ||
170 | KWayland::Server::SurfaceInterface *surf = w->surface(); | 291 | KWayland::Server::SurfaceInterface *surf = w->surface(); | ||
171 | 292 | | |||
172 | if (surf) { | 293 | if (surf) { | ||
173 | windows[w].blurChangedConnection = connect(surf, &KWayland::Server::SurfaceInterface::blurChanged, this, [this, w] () { | 294 | windowBlurChangedConnections[w] = connect(surf, &KWayland::Server::SurfaceInterface::blurChanged, this, [this, w] () { | ||
174 | | ||||
175 | if (w) { | 295 | if (w) { | ||
176 | updateBlurRegion(w); | 296 | updateBlurRegion(w); | ||
177 | } | 297 | } | ||
178 | }); | 298 | }); | ||
179 | } | 299 | } | ||
300 | | ||||
180 | updateBlurRegion(w); | 301 | updateBlurRegion(w); | ||
181 | } | 302 | } | ||
182 | 303 | | |||
183 | void BlurEffect::slotWindowDeleted(EffectWindow *w) | 304 | void BlurEffect::slotWindowDeleted(EffectWindow *w) | ||
184 | { | 305 | { | ||
185 | if (windows.contains(w)) { | 306 | if (windowBlurChangedConnections.contains(w)) { | ||
186 | disconnect(windows[w].blurChangedConnection); | 307 | disconnect(windowBlurChangedConnections[w]); | ||
187 | windows.remove(w); | 308 | windowBlurChangedConnections.remove(w); | ||
188 | } | 309 | } | ||
189 | } | 310 | } | ||
190 | 311 | | |||
191 | void BlurEffect::slotPropertyNotify(EffectWindow *w, long atom) | 312 | void BlurEffect::slotPropertyNotify(EffectWindow *w, long atom) | ||
192 | { | 313 | { | ||
193 | if (w && atom == net_wm_blur_region && net_wm_blur_region != XCB_ATOM_NONE) { | 314 | if (w && atom == net_wm_blur_region && net_wm_blur_region != XCB_ATOM_NONE) { | ||
194 | updateBlurRegion(w); | 315 | updateBlurRegion(w); | ||
195 | CacheEntry it = windows.find(w); | | |||
196 | if (it != windows.end()) { | | |||
197 | const QRect screen = effects->virtualScreenGeometry(); | | |||
198 | it->damagedRegion = expand(blurRegion(w).translated(w->pos())) & screen; | | |||
199 | } | | |||
200 | } | 316 | } | ||
201 | } | 317 | } | ||
202 | 318 | | |||
203 | bool BlurEffect::enabledByDefault() | 319 | bool BlurEffect::enabledByDefault() | ||
204 | { | 320 | { | ||
205 | GLPlatform *gl = GLPlatform::instance(); | 321 | GLPlatform *gl = GLPlatform::instance(); | ||
206 | 322 | | |||
207 | if (gl->isIntel() && gl->chipClass() < SandyBridge) | 323 | if (gl->isIntel() && gl->chipClass() < SandyBridge) | ||
Show All 17 Lines | 336 | if (supported) { | |||
225 | if (screenSize.width() > maxTexSize || screenSize.height() > maxTexSize) | 341 | if (screenSize.width() > maxTexSize || screenSize.height() > maxTexSize) | ||
226 | supported = false; | 342 | supported = false; | ||
227 | } | 343 | } | ||
228 | return supported; | 344 | return supported; | ||
229 | } | 345 | } | ||
230 | 346 | | |||
231 | QRect BlurEffect::expand(const QRect &rect) const | 347 | QRect BlurEffect::expand(const QRect &rect) const | ||
232 | { | 348 | { | ||
233 | const int radius = shader->radius(); | 349 | return rect.adjusted(-m_expandSize, -m_expandSize, m_expandSize, m_expandSize); | ||
234 | return rect.adjusted(-radius, -radius, radius, radius); | | |||
235 | } | 350 | } | ||
236 | 351 | | |||
237 | QRegion BlurEffect::expand(const QRegion ®ion) const | 352 | QRegion BlurEffect::expand(const QRegion ®ion) const | ||
238 | { | 353 | { | ||
239 | QRegion expanded; | 354 | QRegion expanded; | ||
240 | 355 | | |||
241 | for (const QRect &rect : region) { | 356 | for (const QRect &rect : region) { | ||
242 | expanded += expand(rect); | 357 | expanded += expand(rect); | ||
Show All 26 Lines | 382 | } else if (w->decorationHasAlpha() && effects->decorationSupportsBlurBehind()) { | |||
269 | // the effect behind the decoration. | 384 | // the effect behind the decoration. | ||
270 | region = w->shape(); | 385 | region = w->shape(); | ||
271 | region -= w->decorationInnerRect(); | 386 | region -= w->decorationInnerRect(); | ||
272 | } | 387 | } | ||
273 | 388 | | |||
274 | return region; | 389 | return region; | ||
275 | } | 390 | } | ||
276 | 391 | | |||
277 | void BlurEffect::uploadRegion(QVector2D *&map, const QRegion ®ion) | 392 | void BlurEffect::uploadRegion(QVector2D *&map, const QRegion ®ion, const int downSampleIterations) | ||
278 | { | 393 | { | ||
394 | for (int i = 0; i <= downSampleIterations; i++) { | ||||
395 | const int divisionRatio = (1 << i); | ||||
fredrik: 1 << i | |||||
396 | | ||||
279 | for (const QRect &r : region) { | 397 | for (const QRect &r : region) { | ||
280 | const QVector2D topLeft(r.x(), r.y()); | 398 | const QVector2D topLeft( r.x() / divisionRatio, r.y() / divisionRatio); | ||
281 | const QVector2D topRight(r.x() + r.width(), r.y()); | 399 | const QVector2D topRight( (r.x() + r.width()) / divisionRatio, r.y() / divisionRatio); | ||
282 | const QVector2D bottomLeft(r.x(), r.y() + r.height()); | 400 | const QVector2D bottomLeft( r.x() / divisionRatio, (r.y() + r.height()) / divisionRatio); | ||
283 | const QVector2D bottomRight(r.x() + r.width(), r.y() + r.height()); | 401 | const QVector2D bottomRight((r.x() + r.width()) / divisionRatio, (r.y() + r.height()) / divisionRatio); | ||
284 | 402 | | |||
285 | // First triangle | 403 | // First triangle | ||
286 | *(map++) = topRight; | 404 | *(map++) = topRight; | ||
287 | *(map++) = topLeft; | 405 | *(map++) = topLeft; | ||
288 | *(map++) = bottomLeft; | 406 | *(map++) = bottomLeft; | ||
289 | 407 | | |||
290 | // Second triangle | 408 | // Second triangle | ||
291 | *(map++) = bottomLeft; | 409 | *(map++) = bottomLeft; | ||
292 | *(map++) = bottomRight; | 410 | *(map++) = bottomRight; | ||
293 | *(map++) = topRight; | 411 | *(map++) = topRight; | ||
294 | } | 412 | } | ||
295 | } | 413 | } | ||
414 | } | ||||
296 | 415 | | |||
297 | void BlurEffect::uploadGeometry(GLVertexBuffer *vbo, const QRegion &horizontal, const QRegion &vertical) | 416 | void BlurEffect::uploadGeometry(GLVertexBuffer *vbo, const QRegion &blurRegion, const QRegion &windowRegion) | ||
298 | { | 417 | { | ||
299 | const int vertexCount = (horizontal.rectCount() + vertical.rectCount()) * 6; | 418 | const int vertexCount = ((blurRegion.rectCount() * (m_downSampleIterations + 1)) + windowRegion.rectCount()) * 6; | ||
419 | | ||||
300 | if (!vertexCount) | 420 | if (!vertexCount) | ||
301 | return; | 421 | return; | ||
302 | 422 | | |||
303 | QVector2D *map = (QVector2D *) vbo->map(vertexCount * sizeof(QVector2D)); | 423 | QVector2D *map = (QVector2D *) vbo->map(vertexCount * sizeof(QVector2D)); | ||
304 | uploadRegion(map, horizontal); | 424 | | ||
305 | uploadRegion(map, vertical); | 425 | uploadRegion(map, blurRegion, m_downSampleIterations); | ||
426 | uploadRegion(map, windowRegion, 0); | ||||
427 | | ||||
306 | vbo->unmap(); | 428 | vbo->unmap(); | ||
307 | 429 | | |||
308 | const GLVertexAttrib layout[] = { | 430 | const GLVertexAttrib layout[] = { | ||
309 | { VA_Position, 2, GL_FLOAT, 0 }, | 431 | { VA_Position, 2, GL_FLOAT, 0 }, | ||
310 | { VA_TexCoord, 2, GL_FLOAT, 0 } | 432 | { VA_TexCoord, 2, GL_FLOAT, 0 } | ||
311 | }; | 433 | }; | ||
312 | 434 | | |||
313 | vbo->setAttribLayout(layout, 2, sizeof(QVector2D)); | 435 | vbo->setAttribLayout(layout, 2, sizeof(QVector2D)); | ||
Show All 12 Lines | |||||
326 | { | 448 | { | ||
327 | // this effect relies on prePaintWindow being called in the bottom to top order | 449 | // this effect relies on prePaintWindow being called in the bottom to top order | ||
328 | 450 | | |||
329 | effects->prePaintWindow(w, data, time); | 451 | effects->prePaintWindow(w, data, time); | ||
330 | 452 | | |||
331 | if (!w->isPaintingEnabled()) { | 453 | if (!w->isPaintingEnabled()) { | ||
332 | return; | 454 | return; | ||
333 | } | 455 | } | ||
334 | if (!shader || !shader->isValid()) { | 456 | if (!m_shader || !m_shader->isValid()) { | ||
335 | return; | 457 | return; | ||
336 | } | 458 | } | ||
337 | 459 | | |||
338 | // to blur an area partially we have to shrink the opaque area of a window | 460 | // to blur an area partially we have to shrink the opaque area of a window | ||
339 | QRegion newClip; | 461 | QRegion newClip; | ||
340 | const QRegion oldClip = data.clip; | 462 | const QRegion oldClip = data.clip; | ||
341 | const int radius = shader->radius(); | | |||
342 | for (const QRect &rect : data.clip) { | 463 | for (const QRect &rect : data.clip) { | ||
343 | newClip |= rect.adjusted(radius,radius,-radius,-radius); | 464 | newClip |= rect.adjusted(m_expandSize, m_expandSize, -m_expandSize, -m_expandSize); | ||
344 | } | 465 | } | ||
345 | data.clip = newClip; | 466 | data.clip = newClip; | ||
346 | 467 | | |||
347 | const QRegion oldPaint = data.paint; | 468 | const QRegion oldPaint = data.paint; | ||
348 | 469 | | |||
349 | // we don't have to blur a region we don't see | 470 | // we don't have to blur a region we don't see | ||
350 | m_currentBlur -= newClip; | 471 | m_currentBlur -= newClip; | ||
351 | // if we have to paint a non-opaque part of this window that intersects with the | 472 | // if we have to paint a non-opaque part of this window that intersects with the | ||
352 | // currently blurred region (which is not cached) we have to redraw the whole region | 473 | // currently blurred region we have to redraw the whole region | ||
353 | if ((data.paint-oldClip).intersects(m_currentBlur)) { | 474 | if ((data.paint - oldClip).intersects(m_currentBlur)) { | ||
354 | data.paint |= m_currentBlur; | 475 | data.paint |= m_currentBlur; | ||
355 | } | 476 | } | ||
356 | 477 | | |||
357 | // in case this window has regions to be blurred | 478 | // in case this window has regions to be blurred | ||
358 | const QRect screen = effects->virtualScreenGeometry(); | 479 | const QRect screen = effects->virtualScreenGeometry(); | ||
359 | const QRegion blurArea = blurRegion(w).translated(w->pos()) & screen; | 480 | const QRegion blurArea = blurRegion(w).translated(w->pos()) & screen; | ||
360 | const QRegion expandedBlur = expand(blurArea) & screen; | 481 | const QRegion expandedBlur = (w->isDock() ? blurArea : expand(blurArea)) & screen; | ||
361 | 482 | | |||
362 | if (m_shouldCache && !w->isDeleted()) { | 483 | // if this window or a window underneath the blurred area is painted again we have to | ||
363 | // we are caching the horizontally blurred background texture | | |||
364 | | ||||
365 | // if a window underneath the blurred area is damaged we have to | | |||
366 | // update the cached texture | | |||
367 | QRegion damagedCache; | | |||
368 | CacheEntry it = windows.find(w); | | |||
369 | if (it != windows.end() && !it->dropCache && | | |||
370 | it->windowPos == w->pos() && | | |||
371 | it->blurredBackground.size() == expandedBlur.boundingRect().size()) { | | |||
372 | damagedCache = (expand(expandedBlur & m_damagedArea) | | | |||
373 | (it->damagedRegion & data.paint)) & expandedBlur; | | |||
374 | } else { | | |||
375 | damagedCache = expandedBlur; | | |||
376 | } | | |||
377 | if (!damagedCache.isEmpty()) { | | |||
378 | // This is the area of the blurry window which really can change. | | |||
379 | const QRegion damagedArea = damagedCache & blurArea; | | |||
380 | // In order to be able to recalculate this area we have to make sure the | | |||
381 | // background area is painted before. | | |||
382 | data.paint |= expand(damagedArea); | | |||
383 | if (it != windows.end()) { | | |||
384 | // In case we already have a texture cache mark the dirty regions invalid. | | |||
385 | it->damagedRegion &= expandedBlur; | | |||
386 | it->damagedRegion |= damagedCache; | | |||
387 | // The valid part of the cache can be considered as being opaque | | |||
388 | // as long as we don't need to update a bordering part | | |||
389 | data.clip |= blurArea - expand(it->damagedRegion); | | |||
390 | it->dropCache = false; | | |||
391 | } | | |||
392 | // we keep track of the "damage propagation" | | |||
393 | m_damagedArea |= damagedArea; | | |||
394 | // we have to check again whether we do not damage a blurred area | | |||
395 | // of a window we do not cache | | |||
396 | if (expandedBlur.intersects(m_currentBlur)) { | | |||
397 | data.paint |= m_currentBlur; | | |||
398 | } | | |||
399 | } | | |||
400 | } else { | | |||
401 | // we are not caching the window | | |||
402 | | ||||
403 | // if this window or an window underneath the blurred area is painted again we have to | | |||
404 | // blur everything | 484 | // blur everything | ||
405 | if (m_paintedArea.intersects(expandedBlur) || data.paint.intersects(blurArea)) { | 485 | if (m_paintedArea.intersects(expandedBlur) || data.paint.intersects(blurArea)) { | ||
406 | data.paint |= expandedBlur; | 486 | data.paint |= expandedBlur; | ||
407 | // we keep track of the "damage propagation" | 487 | // we keep track of the "damage propagation" | ||
408 | m_damagedArea |= expand(expandedBlur & m_damagedArea) & blurArea; | 488 | m_damagedArea |= (w->isDock() ? (expandedBlur & m_damagedArea) : expand(expandedBlur & m_damagedArea)) & blurArea; | ||
409 | // we have to check again whether we do not damage a blurred area | 489 | // we have to check again whether we do not damage a blurred area | ||
410 | // of a window we do not cache | 490 | // of a window | ||
411 | if (expandedBlur.intersects(m_currentBlur)) { | 491 | if (expandedBlur.intersects(m_currentBlur)) { | ||
412 | data.paint |= m_currentBlur; | 492 | data.paint |= m_currentBlur; | ||
413 | } | 493 | } | ||
414 | } | 494 | } | ||
415 | 495 | | |||
416 | m_currentBlur |= expandedBlur; | 496 | m_currentBlur |= expandedBlur; | ||
417 | } | | |||
418 | 497 | | |||
419 | // we don't consider damaged areas which are occluded and are not | 498 | // we don't consider damaged areas which are occluded and are not | ||
420 | // explicitly damaged by this window | 499 | // explicitly damaged by this window | ||
421 | m_damagedArea -= data.clip; | 500 | m_damagedArea -= data.clip; | ||
422 | m_damagedArea |= oldPaint; | 501 | m_damagedArea |= oldPaint; | ||
423 | 502 | | |||
424 | // in contrast to m_damagedArea does m_paintedArea keep track of all repainted areas | 503 | // in contrast to m_damagedArea does m_paintedArea keep track of all repainted areas | ||
425 | m_paintedArea -= data.clip; | 504 | m_paintedArea -= data.clip; | ||
426 | m_paintedArea |= data.paint; | 505 | m_paintedArea |= data.paint; | ||
427 | } | 506 | } | ||
428 | 507 | | |||
429 | bool BlurEffect::shouldBlur(const EffectWindow *w, int mask, const WindowPaintData &data) const | 508 | bool BlurEffect::shouldBlur(const EffectWindow *w, int mask, const WindowPaintData &data) const | ||
430 | { | 509 | { | ||
431 | if (!target->valid() || !shader || !shader->isValid()) | 510 | if (!m_renderTargetsValid || !m_shader || !m_shader->isValid()) | ||
432 | return false; | 511 | return false; | ||
433 | 512 | | |||
434 | if (effects->activeFullScreenEffect() && !w->data(WindowForceBlurRole).toBool()) | 513 | if (effects->activeFullScreenEffect() && !w->data(WindowForceBlurRole).toBool()) | ||
435 | return false; | 514 | return false; | ||
436 | 515 | | |||
437 | if (w->isDesktop()) | 516 | if (w->isDesktop()) | ||
438 | return false; | 517 | return false; | ||
439 | 518 | | |||
Show All 36 Lines | 537 | if (shouldBlur(w, mask, data)) { | |||
476 | 555 | | |||
477 | //Only translated, not scaled | 556 | //Only translated, not scaled | ||
478 | } else if (translated) { | 557 | } else if (translated) { | ||
479 | shape = shape.translated(data.xTranslation(), data.yTranslation()); | 558 | shape = shape.translated(data.xTranslation(), data.yTranslation()); | ||
480 | shape = shape & region; | 559 | shape = shape & region; | ||
481 | } | 560 | } | ||
482 | 561 | | |||
483 | if (!shape.isEmpty()) { | 562 | if (!shape.isEmpty()) { | ||
484 | if (w->isFullScreen() && GLRenderTarget::blitSupported() && m_simpleShader->isValid() | 563 | if (m_useSimpleBlur && | ||
485 | && !GLPlatform::instance()->supports(LimitedNPOT) && shape.boundingRect() == w->geometry()) { | 564 | w->isFullScreen() && | ||
565 | GLRenderTarget::blitSupported() && | ||||
566 | m_simpleShader->isValid() && | ||||
567 | !GLPlatform::instance()->supports(LimitedNPOT) && | ||||
568 | shape.boundingRect() == w->geometry()) { | ||||
486 | doSimpleBlur(w, data.opacity(), data.screenProjectionMatrix()); | 569 | doSimpleBlur(w, data.opacity(), data.screenProjectionMatrix()); | ||
487 | } else if (m_shouldCache && !translated && !w->isDeleted()) { | | |||
488 | doCachedBlur(w, region, data.opacity(), data.screenProjectionMatrix()); | | |||
489 | } else { | 570 | } else { | ||
490 | doBlur(shape, screen, data.opacity(), data.screenProjectionMatrix()); | 571 | doBlur(shape, screen, data.opacity(), data.screenProjectionMatrix(), w->isDock()); | ||
491 | } | 572 | } | ||
492 | } | 573 | } | ||
493 | } | 574 | } | ||
494 | 575 | | |||
495 | // Draw the window over the blurred area | 576 | // Draw the window over the blurred area | ||
496 | effects->drawWindow(w, mask, region, data); | 577 | effects->drawWindow(w, mask, region, data); | ||
497 | } | 578 | } | ||
498 | 579 | | |||
499 | void BlurEffect::paintEffectFrame(EffectFrame *frame, QRegion region, double opacity, double frameOpacity) | 580 | void BlurEffect::paintEffectFrame(EffectFrame *frame, QRegion region, double opacity, double frameOpacity) | ||
500 | { | 581 | { | ||
501 | const QRect screen = effects->virtualScreenGeometry(); | 582 | const QRect screen = effects->virtualScreenGeometry(); | ||
502 | bool valid = target->valid() && shader && shader->isValid(); | 583 | bool valid = m_renderTargetsValid && m_shader && m_shader->isValid(); | ||
503 | QRegion shape = frame->geometry().adjusted(-5, -5, 5, 5) & screen; | 584 | | ||
585 | QRegion shape = frame->geometry().adjusted(-borderSize, -borderSize, borderSize, borderSize) & screen; | ||||
586 | | ||||
504 | if (valid && !shape.isEmpty() && region.intersects(shape.boundingRect()) && frame->style() != EffectFrameNone) { | 587 | if (valid && !shape.isEmpty() && region.intersects(shape.boundingRect()) && frame->style() != EffectFrameNone) { | ||
505 | doBlur(shape, screen, opacity * frameOpacity, frame->screenProjectionMatrix()); | 588 | doBlur(shape, screen, opacity * frameOpacity, frame->screenProjectionMatrix(), false); | ||
506 | } | 589 | } | ||
507 | effects->paintEffectFrame(frame, region, opacity, frameOpacity); | 590 | effects->paintEffectFrame(frame, region, opacity, frameOpacity); | ||
508 | } | 591 | } | ||
509 | 592 | | |||
510 | void BlurEffect::doSimpleBlur(EffectWindow *w, const float opacity, const QMatrix4x4 &screenProjection) | 593 | void BlurEffect::doSimpleBlur(EffectWindow *w, const float opacity, const QMatrix4x4 &screenProjection) | ||
511 | { | 594 | { | ||
512 | // The fragment shader uses a LOD bias of 1.75, so we need 3 mipmap levels. | 595 | // The fragment shader uses a LOD bias of 1.75, so we need 3 mipmap levels. | ||
513 | GLTexture blurTexture = GLTexture(GL_RGBA8, w->size(), 3); | 596 | GLTexture blurTexture = GLTexture(GL_RGBA8, w->size(), 3); | ||
514 | blurTexture.setFilter(GL_LINEAR_MIPMAP_LINEAR); | 597 | blurTexture.setFilter(GL_LINEAR_MIPMAP_LINEAR); | ||
515 | blurTexture.setWrapMode(GL_CLAMP_TO_EDGE); | 598 | blurTexture.setWrapMode(GL_CLAMP_TO_EDGE); | ||
516 | 599 | | |||
517 | target->attachTexture(blurTexture); | 600 | m_simpleTarget->attachTexture(blurTexture); | ||
518 | target->blitFromFramebuffer(w->geometry(), QRect(QPoint(0, 0), w->size())); | 601 | m_simpleTarget->blitFromFramebuffer(w->geometry(), QRect(QPoint(0, 0), w->size())); | ||
Detach the texture from the render target before you return from this method - otherwise the FBO continues to hold a reference to the texture, preventing it from being deleted. fredrik: Detach the texture from the render target before you return from this method - otherwise the… | |||||
602 | m_simpleTarget->detachTexture(); | ||||
519 | 603 | | |||
520 | // Unmodified base image | 604 | // Unmodified base image | ||
521 | ShaderBinder binder(m_simpleShader); | 605 | ShaderBinder binder(m_simpleShader); | ||
522 | QMatrix4x4 mvp = screenProjection; | 606 | QMatrix4x4 mvp = screenProjection; | ||
523 | mvp.translate(w->x(), w->y()); | 607 | mvp.translate(w->x(), w->y()); | ||
524 | m_simpleShader->setUniform(GLShader::ModelViewProjectionMatrix, mvp); | 608 | m_simpleShader->setUniform(GLShader::ModelViewProjectionMatrix, mvp); | ||
525 | m_simpleShader->setUniform("u_alphaProgress", opacity); | 609 | m_simpleShader->setUniform("u_alphaProgress", opacity); | ||
526 | glEnable(GL_BLEND); | 610 | glEnable(GL_BLEND); | ||
527 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); | 611 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); | ||
528 | blurTexture.bind(); | 612 | blurTexture.bind(); | ||
529 | blurTexture.generateMipmaps(); | 613 | blurTexture.generateMipmaps(); | ||
530 | blurTexture.render(infiniteRegion(), w->geometry()); | 614 | blurTexture.render(infiniteRegion(), w->geometry()); | ||
531 | blurTexture.unbind(); | 615 | blurTexture.unbind(); | ||
532 | glDisable(GL_BLEND); | 616 | glDisable(GL_BLEND); | ||
533 | } | 617 | } | ||
534 | 618 | | |||
535 | void BlurEffect::doBlur(const QRegion& shape, const QRect& screen, const float opacity, const QMatrix4x4 &screenProjection) | 619 | void BlurEffect::doBlur(const QRegion& shape, const QRect& screen, const float opacity, const QMatrix4x4 &screenProjection, bool isDock) | ||
536 | { | 620 | { | ||
537 | const QRegion expanded = expand(shape) & screen; | 621 | QRegion expandedBlurRegion = expand(shape) & expand(screen); | ||
538 | const QRect r = expanded.boundingRect(); | | |||
539 | 622 | | |||
540 | // Upload geometry for the horizontal and vertical passes | 623 | // Upload geometry for the down and upsample iterations | ||
541 | GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); | 624 | GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); | ||
542 | uploadGeometry(vbo, expanded, shape); | 625 | uploadGeometry(vbo, expandedBlurRegion, shape); | ||
543 | vbo->bindArrays(); | 626 | vbo->bindArrays(); | ||
544 | 627 | | |||
545 | const qreal scale = GLRenderTarget::virtualScreenScale(); | 628 | /* | ||
546 | 629 | * If the window is a dock or panel we avoid the "extended blur" effect. | |||
547 | // Create a scratch texture and copy the area in the back buffer that we're | 630 | * Extended blur is when windows that are not under the blurred area affect | ||
548 | // going to blur into it | 631 | * the final blur result. | ||
549 | // for HIGH DPI scratch is captured in native resolution, it is then implicitly downsampled | 632 | * We want to avoid this on panels, because it looks really weird and ugly | ||
550 | // when rendering into tex | 633 | * when maximized windows or windows near the panel affect the dock blur. | ||
551 | GLTexture scratch(GL_RGBA8, r.width() * scale, r.height() * scale); | 634 | */ | ||
552 | scratch.setFilter(GL_LINEAR); | 635 | isDock ? m_renderTextures.last().bind() : m_renderTextures[0].bind(); | ||
553 | scratch.setWrapMode(GL_CLAMP_TO_EDGE); | | |||
554 | scratch.bind(); | | |||
555 | | ||||
556 | const QRect sg = GLRenderTarget::virtualScreenGeometry(); | | |||
557 | glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, (r.x() - sg.x()) * scale, (sg.height() - sg.y() - r.y() - r.height()) * scale, | | |||
558 | scratch.width(), scratch.height()); | | |||
559 | | ||||
560 | // Draw the texture on the offscreen framebuffer object, while blurring it horizontally | | |||
561 | target->attachTexture(tex); | | |||
562 | GLRenderTarget::pushRenderTarget(target); | | |||
563 | | ||||
564 | shader->bind(); | | |||
565 | shader->setDirection(Qt::Horizontal); | | |||
566 | shader->setPixelDistance(1.0 / r.width()); | | |||
567 | | ||||
568 | QMatrix4x4 modelViewProjectionMatrix; | | |||
569 | modelViewProjectionMatrix.ortho(0, tex.width(), tex.height(), 0 , 0, 65535); | | |||
570 | shader->setModelViewProjectionMatrix(modelViewProjectionMatrix); | | |||
571 | 636 | | |||
572 | // Set up the texture matrix to transform from screen coordinates | 637 | QRect copyRect = expandedBlurRegion.boundingRect() & screen; | ||
573 | // to texture coordinates. | 638 | glCopyTexSubImage2D( | ||
574 | QMatrix4x4 textureMatrix; | 639 | GL_TEXTURE_2D, | ||
575 | textureMatrix.scale(1.0 / r.width(), -1.0 / r.height(), 1); | 640 | 0, | ||
576 | textureMatrix.translate(-r.x(), (-r.height() - r.y()), 0); | 641 | copyRect.x(), | ||
577 | shader->setTextureMatrix(textureMatrix); | 642 | effects->virtualScreenSize().height() - copyRect.y() - copyRect.height(), | ||
643 | copyRect.x(), | ||||
644 | effects->virtualScreenSize().height() - copyRect.y() - copyRect.height(), | ||||
645 | copyRect.width(), | ||||
646 | copyRect.height() | ||||
647 | ); | ||||
578 | 648 | | |||
579 | vbo->draw(GL_TRIANGLES, 0, expanded.rectCount() * 6); | 649 | GLRenderTarget::pushRenderTargets(m_renderTargetStack); | ||
650 | int blurRectCount = expandedBlurRegion.rectCount() * 6; | ||||
580 | 651 | | |||
652 | if (isDock) { | ||||
653 | copyScreenSampleTexture(vbo, blurRectCount, shape, screen.size(), screenProjection); | ||||
654 | } else { | ||||
655 | // Remove the m_renderTargets[0] from the top of the stack that we will not use | ||||
581 | GLRenderTarget::popRenderTarget(); | 656 | GLRenderTarget::popRenderTarget(); | ||
582 | scratch.unbind(); | 657 | } | ||
583 | scratch.discard(); | | |||
584 | | ||||
585 | // Now draw the horizontally blurred area back to the backbuffer, while | | |||
586 | // blurring it vertically and clipping it to the window shape. | | |||
587 | tex.bind(); | | |||
588 | 658 | | |||
589 | shader->setDirection(Qt::Vertical); | 659 | downSampleTexture(vbo, blurRectCount); | ||
590 | shader->setPixelDistance(1.0 / tex.height()); | 660 | upSampleTexture(vbo, blurRectCount); | ||
591 | 661 | | |||
592 | // Modulate the blurred texture with the window opacity if the window isn't opaque | 662 | // Modulate the blurred texture with the window opacity if the window isn't opaque | ||
593 | if (opacity < 1.0) { | 663 | if (opacity < 1.0) { | ||
594 | glEnable(GL_BLEND); | 664 | glEnable(GL_BLEND); | ||
595 | #if 1 // bow shape, always above y = x | 665 | #if 1 // bow shape, always above y = x | ||
596 | float o = 1.0f-opacity; | 666 | float o = 1.0f-opacity; | ||
597 | o = 1.0f - o*o; | 667 | o = 1.0f - o*o; | ||
598 | #else // sigmoid shape, above y = x for x > 0.5, below y = x for x < 0.5 | 668 | #else // sigmoid shape, above y = x for x > 0.5, below y = x for x < 0.5 | ||
599 | float o = 2.0f*opacity - 1.0f; | 669 | float o = 2.0f*opacity - 1.0f; | ||
600 | o = 0.5f + o / (1.0f + qAbs(o)); | 670 | o = 0.5f + o / (1.0f + qAbs(o)); | ||
601 | #endif | 671 | #endif | ||
602 | glBlendColor(0, 0, 0, o); | 672 | glBlendColor(0, 0, 0, o); | ||
603 | glBlendFunc(GL_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_ALPHA); | 673 | glBlendFunc(GL_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_ALPHA); | ||
604 | } | 674 | } | ||
605 | 675 | | |||
606 | // Set the up the texture matrix to transform from screen coordinates | 676 | //Final upscale to the screen | ||
607 | // to texture coordinates. | 677 | m_shader->bind(BlurShader::UpSampleType); | ||
608 | textureMatrix.setToIdentity(); | 678 | m_shader->setOffset(m_offset); | ||
609 | textureMatrix.scale(1.0 / tex.width(), -1.0 / tex.height(), 1); | | |||
610 | textureMatrix.translate(0, -tex.height(), 0); | | |||
611 | shader->setTextureMatrix(textureMatrix); | | |||
612 | shader->setModelViewProjectionMatrix(screenProjection); | | |||
613 | 679 | | |||
614 | vbo->draw(GL_TRIANGLES, expanded.rectCount() * 6, shape.rectCount() * 6); | 680 | m_shader->setModelViewProjectionMatrix(screenProjection); | ||
615 | vbo->unbindArrays(); | 681 | m_shader->setTargetSize(m_renderTextures[0].size()); | ||
682 | | ||||
683 | //Copy the image from this texture | ||||
684 | m_renderTextures[1].bind(); | ||||
685 | | ||||
686 | //Render to the screen | ||||
687 | vbo->draw(GL_TRIANGLES, blurRectCount * (m_downSampleIterations + 1), shape.rectCount() * 6); | ||||
616 | 688 | | |||
617 | if (opacity < 1.0) { | 689 | if (opacity < 1.0) { | ||
618 | glDisable(GL_BLEND); | 690 | glDisable(GL_BLEND); | ||
619 | } | 691 | } | ||
620 | 692 | | |||
621 | tex.unbind(); | 693 | vbo->unbindArrays(); | ||
622 | shader->unbind(); | 694 | m_shader->unbind(); | ||
623 | } | 695 | } | ||
624 | 696 | | |||
625 | void BlurEffect::doCachedBlur(EffectWindow *w, const QRegion& region, const float opacity, const QMatrix4x4 &screenProjection) | 697 | void BlurEffect::downSampleTexture(GLVertexBuffer *vbo, int blurRectCount) | ||
626 | { | 698 | { | ||
627 | const QRect screen = effects->virtualScreenGeometry(); | | |||
628 | const QRegion blurredRegion = blurRegion(w).translated(w->pos()) & screen; | | |||
629 | const QRegion expanded = expand(blurredRegion) & screen; | | |||
630 | const QRect r = expanded.boundingRect(); | | |||
631 | | ||||
632 | // The background texture we get is only partially valid. | | |||
633 | | ||||
634 | CacheEntry it = windows.find(w); | | |||
635 | if (it == windows.end()) { | | |||
636 | BlurWindowInfo bwi; | | |||
637 | bwi.blurredBackground = GLTexture(GL_RGBA8, r.width(),r.height()); | | |||
638 | bwi.damagedRegion = expanded; | | |||
639 | bwi.dropCache = false; | | |||
640 | bwi.windowPos = w->pos(); | | |||
641 | it = windows.insert(w, bwi); | | |||
642 | } else if (it->blurredBackground.size() != r.size()) { | | |||
643 | it->blurredBackground = GLTexture(GL_RGBA8, r.width(),r.height()); | | |||
644 | it->dropCache = false; | | |||
645 | it->windowPos = w->pos(); | | |||
646 | } else if (it->windowPos != w->pos()) { | | |||
647 | it->dropCache = false; | | |||
648 | it->windowPos = w->pos(); | | |||
649 | } | | |||
650 | | ||||
651 | GLTexture targetTexture = it->blurredBackground; | | |||
652 | targetTexture.setFilter(GL_LINEAR); | | |||
653 | targetTexture.setWrapMode(GL_CLAMP_TO_EDGE); | | |||
654 | shader->bind(); | | |||
655 | QMatrix4x4 textureMatrix; | | |||
656 | QMatrix4x4 modelViewProjectionMatrix; | 699 | QMatrix4x4 modelViewProjectionMatrix; | ||
657 | 700 | | |||
658 | /** | 701 | m_shader->bind(BlurShader::DownSampleType); | ||
659 | * Which part of the background texture can be updated ? | 702 | m_shader->setOffset(m_offset); | ||
660 | * | | |||
661 | * Well this is a rather difficult question. We kind of rely on the fact, that | | |||
662 | * we need a bigger background region being painted before, more precisely if we want to | | |||
663 | * blur region A we need the background region expand(A). This business logic is basically | | |||
664 | * done in prePaintWindow: | | |||
665 | * data.paint |= expand(damagedArea); | | |||
666 | * | | |||
667 | * Now "data.paint" gets clipped and becomes what we receive as the "region" variable | | |||
668 | * in this function. In theory there is now only one function that does this clipping | | |||
669 | * and this is paintSimpleScreen. The clipping has the effect that "damagedRegion" | | |||
670 | * is no longer a subset of "region" and we cannot fully validate the cache within one | | |||
671 | * rendering pass. If we would now update the "damageRegion & region" part of the cache | | |||
672 | * we would wrongly update the part of the cache that is next to the "region" border and | | |||
673 | * which lies within "damagedRegion", just because we cannot assume that the framebuffer | | |||
674 | * outside of "region" is valid. Therefore the maximal damaged region of the cache that can | | |||
675 | * be repainted is given by: | | |||
676 | * validUpdate = damagedRegion - expand(damagedRegion - region); | | |||
677 | * | | |||
678 | * Now you may ask what is with the rest of "damagedRegion & region" that is not part | | |||
679 | * of "validUpdate" but also might end up on the screen. Well under the assumption | | |||
680 | * that only the occlusion culling can shrink "data.paint", we can control this by reducing | | |||
681 | * the opaque area of every window by a margin of the blurring radius (c.f. prePaintWindow). | | |||
682 | * This way we are sure that this area is overpainted by a higher opaque window. | | |||
683 | * | | |||
684 | * Apparently paintSimpleScreen is not the only function that can influence "region". | | |||
685 | * In fact every effect's paintWindow that is called before Blur::paintWindow | | |||
686 | * can do so (e.g. SlidingPopups). Hence we have to make the compromise that we update | | |||
687 | * "damagedRegion & region" of the cache but only mark "validUpdate" as valid. | | |||
688 | **/ | | |||
689 | const QRegion damagedRegion = it->damagedRegion; | | |||
690 | const QRegion updateBackground = damagedRegion & region; | | |||
691 | const QRegion validUpdate = damagedRegion - expand(damagedRegion - region); | | |||
692 | | ||||
693 | const QRegion horizontal = validUpdate.isEmpty() ? QRegion() : (updateBackground & screen); | | |||
694 | const QRegion vertical = blurredRegion & region; | | |||
695 | 703 | | |||
696 | const int horizontalOffset = 0; | 704 | for (int i = 1; i <= m_downSampleIterations; i++) { | ||
697 | const int horizontalCount = horizontal.rectCount() * 6; | 705 | modelViewProjectionMatrix.setToIdentity(); | ||
706 | modelViewProjectionMatrix.ortho(0, m_renderTextures[i].width(), m_renderTextures[i].height(), 0 , 0, 65535); | ||||
698 | 707 | | |||
699 | const int verticalOffset = horizontalCount; | 708 | m_shader->setModelViewProjectionMatrix(modelViewProjectionMatrix); | ||
700 | const int verticalCount = vertical.rectCount() * 6; | 709 | m_shader->setTargetSize(m_renderTextures[i].size()); | ||
701 | | ||||
702 | GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); | | |||
703 | uploadGeometry(vbo, horizontal, vertical); | | |||
704 | | ||||
705 | vbo->bindArrays(); | | |||
706 | 710 | | |||
707 | if (!validUpdate.isEmpty()) { | 711 | //Copy the image from this texture | ||
708 | const QRect updateRect = (expand(updateBackground) & expanded).boundingRect(); | 712 | m_renderTextures[i - 1].bind(); | ||
709 | // First we have to copy the background from the frontbuffer | | |||
710 | // into a scratch texture (in this case "tex"). | | |||
711 | tex.bind(); | | |||
712 | | ||||
713 | glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, updateRect.x(), effects->virtualScreenSize().height() - updateRect.y() - updateRect.height(), | | |||
714 | updateRect.width(), updateRect.height()); | | |||
715 | | ||||
716 | // Draw the texture on the offscreen framebuffer object, while blurring it horizontally | | |||
717 | target->attachTexture(targetTexture); | | |||
718 | GLRenderTarget::pushRenderTarget(target); | | |||
719 | | ||||
720 | shader->setDirection(Qt::Horizontal); | | |||
721 | shader->setPixelDistance(1.0 / tex.width()); | | |||
722 | | ||||
723 | modelViewProjectionMatrix.ortho(0, r.width(), r.height(), 0 , 0, 65535); | | |||
724 | modelViewProjectionMatrix.translate(-r.x(), -r.y(), 0); | | |||
725 | shader->setModelViewProjectionMatrix(modelViewProjectionMatrix); | | |||
726 | | ||||
727 | // Set up the texture matrix to transform from screen coordinates | | |||
728 | // to texture coordinates. | | |||
729 | textureMatrix.scale(1.0 / tex.width(), -1.0 / tex.height(), 1); | | |||
730 | textureMatrix.translate(-updateRect.x(), -updateRect.height() - updateRect.y(), 0); | | |||
731 | shader->setTextureMatrix(textureMatrix); | | |||
732 | | ||||
733 | vbo->draw(GL_TRIANGLES, horizontalOffset, horizontalCount); | | |||
734 | 713 | | |||
714 | vbo->draw(GL_TRIANGLES, blurRectCount * i, blurRectCount); | ||||
735 | GLRenderTarget::popRenderTarget(); | 715 | GLRenderTarget::popRenderTarget(); | ||
736 | tex.unbind(); | | |||
737 | // mark the updated region as valid | | |||
738 | it->damagedRegion -= validUpdate; | | |||
739 | } | 716 | } | ||
740 | 717 | | |||
These three lines of code will expand to an unfortunate sequence of GL calls: // Iteration N glGetIntegerv(GL_VIEWPORT, ...); glBindFramebuffer(GL_FRAMEBUFFER, ...); glViewport(...); glDrawArrays(...); glBindFramebuffer(GL_FRAMEBUFFER, 0); glViewport(...); // Iteration N+1 glGetIntegerv(GL_VIEWPORT, ..); glBindFramebuffer(GL_FRAMEBUFFER, ...); glViewport(...); glDrawArrays(...); glBindFramebuffer(GL_FRAMEBUFFER, 0); glViewport(...); Note the redundant calls to glViewport() and glBindFramebuffer(). The worst offender here is glGetIntegerv() however, because it forces serialization of the internal driver threads. Ideally the call sequence should look like this: // Iteration N glBindFramebuffer(GL_FRAMEBUFFER, ...); glViewport(...); glDrawArrays(...); // Iteration N+1 glBindFramebuffer(GL_FRAMEBUFFER, ...); glViewport(...); glDrawArrays(...); This can be fixed in a followup patch though. fredrik: These three lines of code will expand to an unfortunate sequence of GL calls:
```
// Iteration… | |||||
741 | // Now draw the horizontally blurred area back to the backbuffer, while | 718 | m_shader->unbind(); | ||
Make a 3 function copySampleType, downSampleType, upSampleType with needed parameters. Call them here and in doCachedBlur anthonyfieroni: Make a 3 function copySampleType, downSampleType, upSampleType with needed parameters. Call… | |||||
742 | // blurring it vertically and clipping it to the window shape. | 719 | } | ||
743 | targetTexture.bind(); | | |||
744 | 720 | | |||
745 | shader->setDirection(Qt::Vertical); | 721 | void BlurEffect::upSampleTexture(GLVertexBuffer *vbo, int blurRectCount) | ||
746 | shader->setPixelDistance(1.0 / targetTexture.height()); | 722 | { | ||
723 | QMatrix4x4 modelViewProjectionMatrix; | ||||
747 | 724 | | |||
748 | // Modulate the blurred texture with the window opacity if the window isn't opaque | 725 | m_shader->bind(BlurShader::UpSampleType); | ||
749 | if (opacity < 1.0) { | 726 | m_shader->setOffset(m_offset); | ||
750 | glEnable(GL_BLEND); | | |||
751 | glBlendColor(0, 0, 0, opacity); | | |||
752 | glBlendFunc(GL_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_ALPHA); | | |||
753 | } | | |||
754 | 727 | | |||
755 | shader->setModelViewProjectionMatrix(screenProjection); | 728 | for (int i = m_downSampleIterations - 1; i > 0; i--) { | ||
729 | modelViewProjectionMatrix.setToIdentity(); | ||||
730 | modelViewProjectionMatrix.ortho(0, m_renderTextures[i].width(), m_renderTextures[i].height(), 0 , 0, 65535); | ||||
756 | 731 | | |||
757 | // Set the up the texture matrix to transform from screen coordinates | 732 | m_shader->setModelViewProjectionMatrix(modelViewProjectionMatrix); | ||
758 | // to texture coordinates. | 733 | m_shader->setTargetSize(m_renderTextures[i].size()); | ||
759 | textureMatrix.setToIdentity(); | | |||
760 | textureMatrix.scale(1.0 / targetTexture.width(), -1.0 / targetTexture.height(), 1); | | |||
761 | textureMatrix.translate(-r.x(), -targetTexture.height() - r.y(), 0); | | |||
762 | shader->setTextureMatrix(textureMatrix); | | |||
763 | 734 | | |||
764 | vbo->draw(GL_TRIANGLES, verticalOffset, verticalCount); | 735 | //Copy the image from this texture | ||
765 | vbo->unbindArrays(); | 736 | m_renderTextures[i + 1].bind(); | ||
766 | 737 | | |||
767 | if (opacity < 1.0) { | 738 | vbo->draw(GL_TRIANGLES, blurRectCount * i, blurRectCount); | ||
768 | glDisable(GL_BLEND); | 739 | GLRenderTarget::popRenderTarget(); | ||
769 | } | 740 | } | ||
770 | 741 | | |||
771 | targetTexture.unbind(); | 742 | m_shader->unbind(); | ||
772 | shader->unbind(); | | |||
773 | } | 743 | } | ||
774 | 744 | | |||
775 | int BlurEffect::blurRadius() const | 745 | void BlurEffect::copyScreenSampleTexture(GLVertexBuffer *vbo, int blurRectCount, QRegion blurShape, QSize screenSize, QMatrix4x4 screenProjection) | ||
776 | { | 746 | { | ||
777 | if (!shader) { | 747 | m_shader->bind(BlurShader::CopySampleType); | ||
778 | return 0; | 748 | | ||
779 | } | 749 | m_shader->setModelViewProjectionMatrix(screenProjection); | ||
780 | return shader->radius(); | 750 | m_shader->setTargetSize(screenSize); | ||
751 | | ||||
752 | /* | ||||
753 | * This '1' sized adjustment is necessary do avoid windows affecting the blur that are | ||||
754 | * right next to this window. | ||||
755 | */ | ||||
756 | m_shader->setBlurRect(blurShape.boundingRect().adjusted(1, 1, -1, -1), screenSize); | ||||
757 | | ||||
758 | vbo->draw(GL_TRIANGLES, 0, blurRectCount); | ||||
759 | GLRenderTarget::popRenderTarget(); | ||||
760 | | ||||
761 | m_shader->unbind(); | ||||
781 | } | 762 | } | ||
782 | 763 | | |||
783 | } // namespace KWin | 764 | } // namespace KWin | ||
784 | 765 | |
Is this still needed when qPow() is not used?