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 | m_context->makeCurrent(surface: m_targetWindow); |
89 | QScopedPointer<QOpenGLFramebufferObject> fbo(new QOpenGLFramebufferObject(m_nativeTargetGeometry.size())); |
90 | renderAll(fbo: fbo.data()); |
91 | return fbo->toImage(); |
92 | } |
93 | |
94 | void QOpenGLCompositor::handleRenderAllRequest() |
95 | { |
96 | Q_ASSERT(m_context && m_targetWindow); |
97 | m_context->makeCurrent(surface: m_targetWindow); |
98 | renderAll(fbo: 0); |
99 | } |
100 | |
101 | void QOpenGLCompositor::renderAll(QOpenGLFramebufferObject *fbo) |
102 | { |
103 | if (fbo) |
104 | fbo->bind(); |
105 | |
106 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); |
107 | glViewport(x: 0, y: 0, width: m_nativeTargetGeometry.width(), height: m_nativeTargetGeometry.height()); |
108 | |
109 | if (!m_blitter.isCreated()) |
110 | m_blitter.create(); |
111 | |
112 | m_blitter.bind(); |
113 | |
114 | for (int i = 0; i < m_windows.size(); ++i) |
115 | m_windows.at(i)->beginCompositing(); |
116 | |
117 | for (int i = 0; i < m_windows.size(); ++i) |
118 | render(window: m_windows.at(i)); |
119 | |
120 | m_blitter.release(); |
121 | if (!fbo) |
122 | m_context->swapBuffers(surface: m_targetWindow); |
123 | else |
124 | fbo->release(); |
125 | |
126 | for (int i = 0; i < m_windows.size(); ++i) |
127 | m_windows.at(i)->endCompositing(); |
128 | } |
129 | |
130 | struct BlendStateBinder |
131 | { |
132 | BlendStateBinder() : m_blend(false) { |
133 | glDisable(GL_BLEND); |
134 | } |
135 | void set(bool blend) { |
136 | if (blend != m_blend) { |
137 | if (blend) { |
138 | glEnable(GL_BLEND); |
139 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); |
140 | } else { |
141 | glDisable(GL_BLEND); |
142 | } |
143 | m_blend = blend; |
144 | } |
145 | } |
146 | ~BlendStateBinder() { |
147 | if (m_blend) |
148 | glDisable(GL_BLEND); |
149 | } |
150 | bool m_blend; |
151 | }; |
152 | |
153 | static inline QRect toBottomLeftRect(const QRect &topLeftRect, int windowHeight) |
154 | { |
155 | return QRect(topLeftRect.x(), windowHeight - topLeftRect.bottomRight().y() - 1, |
156 | topLeftRect.width(), topLeftRect.height()); |
157 | } |
158 | |
159 | static void clippedBlit(const QPlatformTextureList *textures, int idx, const QRect &sourceWindowRect, |
160 | const QRect &targetWindowRect, |
161 | QOpenGLTextureBlitter *blitter, QMatrix4x4 *rotationMatrix) |
162 | { |
163 | const QRect clipRect = textures->clipRect(index: idx); |
164 | if (clipRect.isEmpty()) |
165 | return; |
166 | |
167 | const QRect rectInWindow = textures->geometry(index: idx).translated(p: sourceWindowRect.topLeft()); |
168 | const QRect clippedRectInWindow = rectInWindow & clipRect.translated(p: rectInWindow.topLeft()); |
169 | const QRect srcRect = toBottomLeftRect(topLeftRect: clipRect, windowHeight: rectInWindow.height()); |
170 | |
171 | QMatrix4x4 target = QOpenGLTextureBlitter::targetTransform(target: clippedRectInWindow, viewport: targetWindowRect); |
172 | if (rotationMatrix) |
173 | target = *rotationMatrix * target; |
174 | |
175 | const QMatrix3x3 source = QOpenGLTextureBlitter::sourceTransform(subTexture: srcRect, textureSize: rectInWindow.size(), |
176 | origin: QOpenGLTextureBlitter::OriginBottomLeft); |
177 | |
178 | const uint textureId = textures->texture(index: idx)->nativeTexture().object; |
179 | blitter->blit(texture: textureId, targetTransform: target, sourceTransform: source); |
180 | } |
181 | |
182 | void QOpenGLCompositor::render(QOpenGLCompositorWindow *window) |
183 | { |
184 | const QPlatformTextureList *textures = window->textures(); |
185 | if (!textures) |
186 | return; |
187 | |
188 | const QRect targetWindowRect(QPoint(0, 0), m_targetWindow->geometry().size()); |
189 | float currentOpacity = 1.0f; |
190 | BlendStateBinder blend; |
191 | const QRect sourceWindowRect = window->sourceWindow()->geometry(); |
192 | for (int i = 0; i < textures->count(); ++i) { |
193 | const uint textureId = textures->texture(index: i)->nativeTexture().object; |
194 | const float opacity = window->sourceWindow()->opacity(); |
195 | if (opacity != currentOpacity) { |
196 | currentOpacity = opacity; |
197 | m_blitter.setOpacity(currentOpacity); |
198 | } |
199 | |
200 | if (textures->count() > 1 && i == textures->count() - 1) { |
201 | // Backingstore for a widget with QOpenGLWidget subwidgets |
202 | blend.set(true); |
203 | QMatrix4x4 target = QOpenGLTextureBlitter::targetTransform(target: textures->geometry(index: i), viewport: targetWindowRect); |
204 | if (m_rotation) |
205 | target = m_rotationMatrix * target; |
206 | m_blitter.blit(texture: textureId, targetTransform: target, sourceOrigin: QOpenGLTextureBlitter::OriginTopLeft); |
207 | } else if (textures->count() == 1) { |
208 | // A regular QWidget window |
209 | const bool translucent = window->sourceWindow()->requestedFormat().alphaBufferSize() > 0; |
210 | blend.set(translucent); |
211 | QMatrix4x4 target = QOpenGLTextureBlitter::targetTransform(target: textures->geometry(index: i), viewport: targetWindowRect); |
212 | if (m_rotation) |
213 | target = m_rotationMatrix * target; |
214 | m_blitter.blit(texture: textureId, targetTransform: target, sourceOrigin: QOpenGLTextureBlitter::OriginTopLeft); |
215 | } else if (!textures->flags(index: i).testFlag(flag: QPlatformTextureList::StacksOnTop)) { |
216 | // Texture from an FBO belonging to a QOpenGLWidget or QQuickWidget |
217 | blend.set(false); |
218 | clippedBlit(textures, idx: i, sourceWindowRect, targetWindowRect, blitter: &m_blitter, rotationMatrix: m_rotation ? &m_rotationMatrix : nullptr); |
219 | } |
220 | } |
221 | |
222 | for (int i = 0; i < textures->count(); ++i) { |
223 | if (textures->flags(index: i).testFlag(flag: QPlatformTextureList::StacksOnTop)) { |
224 | blend.set(true); |
225 | clippedBlit(textures, idx: i, sourceWindowRect, targetWindowRect, blitter: &m_blitter, rotationMatrix: m_rotation ? &m_rotationMatrix : nullptr); |
226 | } |
227 | } |
228 | |
229 | m_blitter.setOpacity(1.0f); |
230 | } |
231 | |
232 | QOpenGLCompositor *QOpenGLCompositor::instance() |
233 | { |
234 | if (!compositor) |
235 | compositor = new QOpenGLCompositor; |
236 | return compositor; |
237 | } |
238 | |
239 | void QOpenGLCompositor::destroy() |
240 | { |
241 | delete compositor; |
242 | compositor = 0; |
243 | } |
244 | |
245 | void QOpenGLCompositor::addWindow(QOpenGLCompositorWindow *window) |
246 | { |
247 | if (!m_windows.contains(t: window)) { |
248 | m_windows.append(t: window); |
249 | ensureCorrectZOrder(); |
250 | if (window == m_windows.constLast()) |
251 | emit topWindowChanged(window); |
252 | } |
253 | } |
254 | |
255 | void QOpenGLCompositor::removeWindow(QOpenGLCompositorWindow *window) |
256 | { |
257 | m_windows.removeOne(t: window); |
258 | if (!m_windows.isEmpty()) |
259 | emit topWindowChanged(window: m_windows.last()); |
260 | } |
261 | |
262 | void QOpenGLCompositor::moveToTop(QOpenGLCompositorWindow *window) |
263 | { |
264 | if (!m_windows.isEmpty() && window == m_windows.constLast()) { |
265 | // Already on top |
266 | return; |
267 | } |
268 | |
269 | m_windows.removeOne(t: window); |
270 | m_windows.append(t: window); |
271 | ensureCorrectZOrder(); |
272 | |
273 | if (window == m_windows.constLast()) |
274 | emit topWindowChanged(window); |
275 | } |
276 | |
277 | void QOpenGLCompositor::changeWindowIndex(QOpenGLCompositorWindow *window, int newIdx) |
278 | { |
279 | int idx = m_windows.indexOf(t: window); |
280 | if (idx != -1 && idx != newIdx) { |
281 | m_windows.move(from: idx, to: newIdx); |
282 | ensureCorrectZOrder(); |
283 | if (window == m_windows.constLast()) |
284 | emit topWindowChanged(window: m_windows.last()); |
285 | } |
286 | } |
287 | |
288 | void QOpenGLCompositor::ensureCorrectZOrder() |
289 | { |
290 | const auto originalOrder = m_windows; |
291 | |
292 | std::sort(first: m_windows.begin(), last: m_windows.end(), |
293 | comp: [this, &originalOrder](QOpenGLCompositorWindow *cw1, QOpenGLCompositorWindow *cw2) { |
294 | QWindow *w1 = cw1->sourceWindow(); |
295 | QWindow *w2 = cw2->sourceWindow(); |
296 | |
297 | // Case #1: The main window needs to have less z-order. It can never be in |
298 | // front of our tool windows, popups etc, because it's fullscreen! |
299 | if (w1 == m_targetWindow || w2 == m_targetWindow) |
300 | return w1 == m_targetWindow; |
301 | |
302 | // Case #2: |
303 | if (w2->isAncestorOf(child: w1)) { |
304 | // w1 is transient child of w2. W1 goes in front then. |
305 | return false; |
306 | } |
307 | |
308 | if (w1->isAncestorOf(child: w2)) { |
309 | // Or the other way around |
310 | return true; |
311 | } |
312 | |
313 | // Case #3: Modality gets higher Z |
314 | if (w1->modality() != Qt::NonModal && w2->modality() == Qt::NonModal) |
315 | return false; |
316 | |
317 | if (w2->modality() != Qt::NonModal && w1->modality() == Qt::NonModal) |
318 | return true; |
319 | |
320 | const bool isTool1 = (w1->flags() & Qt::Tool) == Qt::Tool; |
321 | const bool isTool2 = (w2->flags() & Qt::Tool) == Qt::Tool; |
322 | const bool = !isTool1 && (w1->flags() & Qt::Popup) == Qt::Popup; |
323 | const bool = !isTool2 && (w2->flags() & Qt::Popup) == Qt::Popup; |
324 | |
325 | // Case #4: By pure-popup we mean menus and tooltips. Qt::Tool implies Qt::Popup |
326 | // and we don't want to catch QDockWidget and other tool windows just yet |
327 | if (isPurePopup1 != isPurePopup2) |
328 | return !isPurePopup1; |
329 | |
330 | // Case #5: One of the window is a Tool, that goes to front, as done in other QPAs |
331 | if (isTool1 != isTool2) |
332 | return !isTool1; |
333 | |
334 | // Case #6: Just preserve original sorting: |
335 | return originalOrder.indexOf(t: cw1) < originalOrder.indexOf(t: cw2); |
336 | }); |
337 | } |
338 | |
339 | QT_END_NAMESPACE |
340 | |
341 | #include "moc_qopenglcompositor_p.cpp" |
342 | |