1 | // Copyright (C) 2016 The Qt Company Ltd. |
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
3 | |
4 | #include <QtOpenGL/QOpenGLFramebufferObject> |
5 | #include <QtGui/QOpenGLContext> |
6 | #include <QtGui/QWindow> |
7 | #include <rhi/qrhi.h> |
8 | #include <qpa/qplatformbackingstore.h> |
9 | |
10 | #include "qopenglcompositor_p.h" |
11 | |
12 | QT_BEGIN_NAMESPACE |
13 | |
14 | /*! |
15 | \class QOpenGLCompositor |
16 | \brief A generic OpenGL-based compositor |
17 | \since 5.4 |
18 | \internal |
19 | \ingroup qpa |
20 | |
21 | This class provides a lightweight compositor that maintains the |
22 | basic stacking order of windows and composites them by drawing |
23 | textured quads via OpenGL. |
24 | |
25 | It it meant to be used by platform plugins that run without a |
26 | windowing system. |
27 | |
28 | It is up to the platform plugin to manage the lifetime of the |
29 | compositor (instance(), destroy()), set the correct destination |
30 | context and window as early as possible (setTarget()), |
31 | register the composited windows as they are shown, activated, |
32 | raised and lowered (addWindow(), moveToTop(), etc.), and to |
33 | schedule repaints (update()). |
34 | |
35 | \note To get support for QWidget-based windows, just use |
36 | QOpenGLCompositorBackingStore. It will automatically create |
37 | textures from the raster-rendered content and trigger the |
38 | necessary repaints. |
39 | */ |
40 | |
41 | static QOpenGLCompositor *compositor = 0; |
42 | |
43 | QOpenGLCompositor::QOpenGLCompositor() |
44 | : m_context(0), |
45 | m_targetWindow(0), |
46 | m_rotation(0) |
47 | { |
48 | Q_ASSERT(!compositor); |
49 | m_updateTimer.setSingleShot(true); |
50 | m_updateTimer.setInterval(0); |
51 | connect(asender: &m_updateTimer, SIGNAL(timeout()), SLOT(handleRenderAllRequest())); |
52 | } |
53 | |
54 | QOpenGLCompositor::~QOpenGLCompositor() |
55 | { |
56 | Q_ASSERT(compositor == this); |
57 | m_blitter.destroy(); |
58 | compositor = 0; |
59 | } |
60 | |
61 | void QOpenGLCompositor::setTargetWindow(QWindow *targetWindow, const QRect &nativeTargetGeometry) |
62 | { |
63 | m_targetWindow = targetWindow; |
64 | m_nativeTargetGeometry = nativeTargetGeometry; |
65 | } |
66 | |
67 | void QOpenGLCompositor::setTargetContext(QOpenGLContext *context) |
68 | { |
69 | m_context = context; |
70 | } |
71 | |
72 | void QOpenGLCompositor::setRotation(int degrees) |
73 | { |
74 | m_rotation = degrees; |
75 | m_rotationMatrix.setToIdentity(); |
76 | m_rotationMatrix.rotate(angle: degrees, x: 0, y: 0, z: 1); |
77 | } |
78 | |
79 | void QOpenGLCompositor::update() |
80 | { |
81 | if (!m_updateTimer.isActive()) |
82 | m_updateTimer.start(); |
83 | } |
84 | |
85 | QImage QOpenGLCompositor::grab() |
86 | { |
87 | Q_ASSERT(m_context && m_targetWindow); |
88 | QOpenGLFramebufferObject fbo(m_nativeTargetGeometry.size()); |
89 | grabToFrameBufferObject(fbo: &fbo); |
90 | return fbo.toImage(); |
91 | } |
92 | |
93 | bool QOpenGLCompositor::grabToFrameBufferObject(QOpenGLFramebufferObject *fbo, GrabOrientation orientation) |
94 | { |
95 | Q_ASSERT(fbo); |
96 | if (fbo->size() != m_nativeTargetGeometry.size() |
97 | || fbo->format().textureTarget() != GL_TEXTURE_2D) |
98 | return false; |
99 | |
100 | m_context->makeCurrent(surface: m_targetWindow); |
101 | renderAll(fbo, |
102 | origin: orientation == Flipped ? QOpenGLTextureBlitter::OriginTopLeft |
103 | : QOpenGLTextureBlitter::OriginBottomLeft); |
104 | return true; |
105 | } |
106 | |
107 | void QOpenGLCompositor::handleRenderAllRequest() |
108 | { |
109 | Q_ASSERT(m_context && m_targetWindow); |
110 | m_context->makeCurrent(surface: m_targetWindow); |
111 | renderAll(fbo: 0); |
112 | } |
113 | |
114 | void QOpenGLCompositor::renderAll(QOpenGLFramebufferObject *fbo, QOpenGLTextureBlitter::Origin origin) |
115 | { |
116 | if (fbo) |
117 | fbo->bind(); |
118 | |
119 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); |
120 | glViewport(x: 0, y: 0, width: m_nativeTargetGeometry.width(), height: m_nativeTargetGeometry.height()); |
121 | |
122 | if (!m_blitter.isCreated()) |
123 | m_blitter.create(); |
124 | |
125 | m_blitter.bind(); |
126 | |
127 | for (int i = 0; i < m_windows.size(); ++i) |
128 | m_windows.at(i)->beginCompositing(); |
129 | |
130 | for (int i = 0; i < m_windows.size(); ++i) |
131 | render(window: m_windows.at(i), origin); |
132 | |
133 | m_blitter.release(); |
134 | if (!fbo) |
135 | m_context->swapBuffers(surface: m_targetWindow); |
136 | else |
137 | fbo->release(); |
138 | |
139 | for (int i = 0; i < m_windows.size(); ++i) |
140 | m_windows.at(i)->endCompositing(); |
141 | } |
142 | |
143 | struct BlendStateBinder |
144 | { |
145 | BlendStateBinder() : m_blend(false) { |
146 | glDisable(GL_BLEND); |
147 | } |
148 | void set(bool blend) { |
149 | if (blend != m_blend) { |
150 | if (blend) { |
151 | glEnable(GL_BLEND); |
152 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); |
153 | } else { |
154 | glDisable(GL_BLEND); |
155 | } |
156 | m_blend = blend; |
157 | } |
158 | } |
159 | ~BlendStateBinder() { |
160 | if (m_blend) |
161 | glDisable(GL_BLEND); |
162 | } |
163 | bool m_blend; |
164 | }; |
165 | |
166 | static inline QRect toBottomLeftRect(const QRect &topLeftRect, int windowHeight) |
167 | { |
168 | return QRect(topLeftRect.x(), windowHeight - topLeftRect.bottomRight().y() - 1, |
169 | topLeftRect.width(), topLeftRect.height()); |
170 | } |
171 | |
172 | static void clippedBlit(const QPlatformTextureList *textures, int idx, |
173 | const QRect &sourceWindowRect, const QRect &targetWindowRect, |
174 | QOpenGLTextureBlitter *blitter, QMatrix4x4 *rotationMatrix, |
175 | QOpenGLTextureBlitter::Origin sourceOrigin) |
176 | { |
177 | const QRect clipRect = textures->clipRect(index: idx); |
178 | if (clipRect.isEmpty()) |
179 | return; |
180 | |
181 | const QRect rectInWindow = textures->geometry(index: idx).translated(p: sourceWindowRect.topLeft()); |
182 | const QRect clippedRectInWindow = rectInWindow & clipRect.translated(p: rectInWindow.topLeft()); |
183 | const QRect srcRect = toBottomLeftRect(topLeftRect: clipRect, windowHeight: rectInWindow.height()); |
184 | |
185 | QMatrix4x4 target = QOpenGLTextureBlitter::targetTransform(target: clippedRectInWindow, viewport: targetWindowRect); |
186 | if (rotationMatrix) |
187 | target = *rotationMatrix * target; |
188 | |
189 | const QMatrix3x3 source = QOpenGLTextureBlitter::sourceTransform(subTexture: srcRect, textureSize: rectInWindow.size(), |
190 | origin: sourceOrigin); |
191 | |
192 | const uint textureId = textures->texture(index: idx)->nativeTexture().object; |
193 | blitter->blit(texture: textureId, targetTransform: target, sourceTransform: source); |
194 | } |
195 | |
196 | void QOpenGLCompositor::render(QOpenGLCompositorWindow *window, QOpenGLTextureBlitter::Origin origin) |
197 | { |
198 | const QPlatformTextureList *textures = window->textures(); |
199 | if (!textures) |
200 | return; |
201 | |
202 | const QRect targetWindowRect(QPoint(0, 0), m_targetWindow->geometry().size()); |
203 | float currentOpacity = 1.0f; |
204 | BlendStateBinder blend; |
205 | const QRect sourceWindowRect = window->sourceWindow()->geometry(); |
206 | auto clippedBlitSourceOrigin = origin == QOpenGLTextureBlitter::OriginTopLeft |
207 | ? QOpenGLTextureBlitter::OriginBottomLeft |
208 | : QOpenGLTextureBlitter::OriginTopLeft; |
209 | for (int i = 0; i < textures->count(); ++i) { |
210 | const uint textureId = textures->texture(index: i)->nativeTexture().object; |
211 | const float opacity = window->sourceWindow()->opacity(); |
212 | if (opacity != currentOpacity) { |
213 | currentOpacity = opacity; |
214 | m_blitter.setOpacity(currentOpacity); |
215 | } |
216 | |
217 | if (textures->count() > 1 && i == textures->count() - 1) { |
218 | // Backingstore for a widget with QOpenGLWidget subwidgets |
219 | blend.set(true); |
220 | QMatrix4x4 target = QOpenGLTextureBlitter::targetTransform(target: textures->geometry(index: i), viewport: targetWindowRect); |
221 | if (m_rotation) |
222 | target = m_rotationMatrix * target; |
223 | m_blitter.blit(texture: textureId, targetTransform: target, sourceOrigin: origin); |
224 | } else if (textures->count() == 1) { |
225 | // A regular QWidget window |
226 | const bool translucent = window->sourceWindow()->requestedFormat().alphaBufferSize() > 0; |
227 | blend.set(translucent); |
228 | QMatrix4x4 target = QOpenGLTextureBlitter::targetTransform(target: textures->geometry(index: i), viewport: targetWindowRect); |
229 | if (m_rotation) |
230 | target = m_rotationMatrix * target; |
231 | m_blitter.blit(texture: textureId, targetTransform: target, sourceOrigin: origin); |
232 | } else if (!textures->flags(index: i).testFlag(flag: QPlatformTextureList::StacksOnTop)) { |
233 | // Texture from an FBO belonging to a QOpenGLWidget or QQuickWidget |
234 | blend.set(false); |
235 | clippedBlit(textures, idx: i, sourceWindowRect, targetWindowRect, blitter: &m_blitter, |
236 | rotationMatrix: m_rotation ? &m_rotationMatrix : nullptr, sourceOrigin: clippedBlitSourceOrigin); |
237 | } |
238 | } |
239 | |
240 | for (int i = 0; i < textures->count(); ++i) { |
241 | if (textures->flags(index: i).testFlag(flag: QPlatformTextureList::StacksOnTop)) { |
242 | blend.set(true); |
243 | clippedBlit(textures, idx: i, sourceWindowRect, targetWindowRect, blitter: &m_blitter, |
244 | rotationMatrix: m_rotation ? &m_rotationMatrix : nullptr, sourceOrigin: clippedBlitSourceOrigin); |
245 | } |
246 | } |
247 | |
248 | m_blitter.setOpacity(1.0f); |
249 | } |
250 | |
251 | QOpenGLCompositor *QOpenGLCompositor::instance() |
252 | { |
253 | if (!compositor) |
254 | compositor = new QOpenGLCompositor; |
255 | return compositor; |
256 | } |
257 | |
258 | void QOpenGLCompositor::destroy() |
259 | { |
260 | delete compositor; |
261 | compositor = 0; |
262 | } |
263 | |
264 | void QOpenGLCompositor::addWindow(QOpenGLCompositorWindow *window) |
265 | { |
266 | if (!m_windows.contains(t: window)) { |
267 | m_windows.append(t: window); |
268 | ensureCorrectZOrder(); |
269 | if (window == m_windows.constLast()) |
270 | emit topWindowChanged(window); |
271 | } |
272 | } |
273 | |
274 | void QOpenGLCompositor::removeWindow(QOpenGLCompositorWindow *window) |
275 | { |
276 | m_windows.removeOne(t: window); |
277 | if (!m_windows.isEmpty()) |
278 | emit topWindowChanged(window: m_windows.last()); |
279 | } |
280 | |
281 | void QOpenGLCompositor::moveToTop(QOpenGLCompositorWindow *window) |
282 | { |
283 | if (!m_windows.isEmpty() && window == m_windows.constLast()) { |
284 | // Already on top |
285 | return; |
286 | } |
287 | |
288 | m_windows.removeOne(t: window); |
289 | m_windows.append(t: window); |
290 | ensureCorrectZOrder(); |
291 | |
292 | if (window == m_windows.constLast()) |
293 | emit topWindowChanged(window); |
294 | } |
295 | |
296 | void QOpenGLCompositor::changeWindowIndex(QOpenGLCompositorWindow *window, int newIdx) |
297 | { |
298 | int idx = m_windows.indexOf(t: window); |
299 | if (idx != -1 && idx != newIdx) { |
300 | m_windows.move(from: idx, to: newIdx); |
301 | ensureCorrectZOrder(); |
302 | if (window == m_windows.constLast()) |
303 | emit topWindowChanged(window: m_windows.last()); |
304 | } |
305 | } |
306 | |
307 | void QOpenGLCompositor::ensureCorrectZOrder() |
308 | { |
309 | const auto originalOrder = m_windows; |
310 | |
311 | std::sort(first: m_windows.begin(), last: m_windows.end(), |
312 | comp: [this, &originalOrder](QOpenGLCompositorWindow *cw1, QOpenGLCompositorWindow *cw2) { |
313 | QWindow *w1 = cw1->sourceWindow(); |
314 | QWindow *w2 = cw2->sourceWindow(); |
315 | |
316 | // Case #1: The main window needs to have less z-order. It can never be in |
317 | // front of our tool windows, popups etc, because it's fullscreen! |
318 | if (w1 == m_targetWindow || w2 == m_targetWindow) |
319 | return w1 == m_targetWindow; |
320 | |
321 | // Case #2: |
322 | if (w2->isAncestorOf(child: w1)) { |
323 | // w1 is transient child of w2. W1 goes in front then. |
324 | return false; |
325 | } |
326 | |
327 | if (w1->isAncestorOf(child: w2)) { |
328 | // Or the other way around |
329 | return true; |
330 | } |
331 | |
332 | // Case #3: Modality gets higher Z |
333 | if (w1->modality() != Qt::NonModal && w2->modality() == Qt::NonModal) |
334 | return false; |
335 | |
336 | if (w2->modality() != Qt::NonModal && w1->modality() == Qt::NonModal) |
337 | return true; |
338 | |
339 | const bool isTool1 = (w1->flags() & Qt::Tool) == Qt::Tool; |
340 | const bool isTool2 = (w2->flags() & Qt::Tool) == Qt::Tool; |
341 | const bool = !isTool1 && (w1->flags() & Qt::Popup) == Qt::Popup; |
342 | const bool = !isTool2 && (w2->flags() & Qt::Popup) == Qt::Popup; |
343 | |
344 | // Case #4: By pure-popup we mean menus and tooltips. Qt::Tool implies Qt::Popup |
345 | // and we don't want to catch QDockWidget and other tool windows just yet |
346 | if (isPurePopup1 != isPurePopup2) |
347 | return !isPurePopup1; |
348 | |
349 | // Case #5: One of the window is a Tool, that goes to front, as done in other QPAs |
350 | if (isTool1 != isTool2) |
351 | return !isTool1; |
352 | |
353 | // Case #6: Just preserve original sorting: |
354 | return originalOrder.indexOf(t: cw1) < originalOrder.indexOf(t: cw2); |
355 | }); |
356 | } |
357 | |
358 | QT_END_NAMESPACE |
359 | |
360 | #include "moc_qopenglcompositor_p.cpp" |
361 | |