1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2017 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the examples of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:BSD$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** BSD License Usage |
18 | ** Alternatively, you may use this file under the terms of the BSD license |
19 | ** as follows: |
20 | ** |
21 | ** "Redistribution and use in source and binary forms, with or without |
22 | ** modification, are permitted provided that the following conditions are |
23 | ** met: |
24 | ** * Redistributions of source code must retain the above copyright |
25 | ** notice, this list of conditions and the following disclaimer. |
26 | ** * Redistributions in binary form must reproduce the above copyright |
27 | ** notice, this list of conditions and the following disclaimer in |
28 | ** the documentation and/or other materials provided with the |
29 | ** distribution. |
30 | ** * Neither the name of The Qt Company Ltd nor the names of its |
31 | ** contributors may be used to endorse or promote products derived |
32 | ** from this software without specific prior written permission. |
33 | ** |
34 | ** |
35 | ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
36 | ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
37 | ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
38 | ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
39 | ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
40 | ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
41 | ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
42 | ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
43 | ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
44 | ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
45 | ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." |
46 | ** |
47 | ** $QT_END_LICENSE$ |
48 | ** |
49 | ****************************************************************************/ |
50 | |
51 | #include "window_multithreaded.h" |
52 | #include "cuberenderer.h" |
53 | #include <QOpenGLContext> |
54 | #include <QOpenGLFunctions> |
55 | #include <QOpenGLFramebufferObject> |
56 | #include <QOpenGLShaderProgram> |
57 | #include <QOpenGLVertexArrayObject> |
58 | #include <QOpenGLBuffer> |
59 | #include <QOpenGLVertexArrayObject> |
60 | #include <QOffscreenSurface> |
61 | #include <QQmlEngine> |
62 | #include <QQmlComponent> |
63 | #include <QQuickItem> |
64 | #include <QQuickWindow> |
65 | #include <QQuickRenderControl> |
66 | #include <QCoreApplication> |
67 | |
68 | /* |
69 | This implementation runs the Qt Quick scenegraph's sync and render phases on a |
70 | separate, dedicated thread. Rendering the cube using our custom OpenGL engine |
71 | happens on that thread as well. This is similar to the built-in threaded |
72 | render loop, but does not support all the features. There is no support for |
73 | getting Animators running on the render thread for example. |
74 | |
75 | We choose to use QObject's event mechanism to communicate with the QObject |
76 | living on the render thread. An alternative would be to subclass QThread and |
77 | reimplement run() with a custom event handling approach, like |
78 | QSGThreadedRenderLoop does. That would potentially lead to better results but |
79 | is also more complex. |
80 | */ |
81 | |
82 | static const QEvent::Type INIT = QEvent::Type(QEvent::User + 1); |
83 | static const QEvent::Type RENDER = QEvent::Type(QEvent::User + 2); |
84 | static const QEvent::Type RESIZE = QEvent::Type(QEvent::User + 3); |
85 | static const QEvent::Type STOP = QEvent::Type(QEvent::User + 4); |
86 | |
87 | static const QEvent::Type UPDATE = QEvent::Type(QEvent::User + 5); |
88 | |
89 | QuickRenderer::QuickRenderer() |
90 | : m_context(nullptr), |
91 | m_surface(nullptr), |
92 | m_fbo(nullptr), |
93 | m_window(nullptr), |
94 | m_quickWindow(nullptr), |
95 | m_renderControl(nullptr), |
96 | m_cubeRenderer(nullptr), |
97 | m_quit(false) |
98 | { |
99 | } |
100 | |
101 | void QuickRenderer::requestInit() |
102 | { |
103 | QCoreApplication::postEvent(receiver: this, event: new QEvent(INIT)); |
104 | } |
105 | |
106 | void QuickRenderer::requestRender() |
107 | { |
108 | QCoreApplication::postEvent(receiver: this, event: new QEvent(RENDER)); |
109 | } |
110 | |
111 | void QuickRenderer::requestResize() |
112 | { |
113 | QCoreApplication::postEvent(receiver: this, event: new QEvent(RESIZE)); |
114 | } |
115 | |
116 | void QuickRenderer::requestStop() |
117 | { |
118 | QCoreApplication::postEvent(receiver: this, event: new QEvent(STOP)); |
119 | } |
120 | |
121 | bool QuickRenderer::event(QEvent *e) |
122 | { |
123 | QMutexLocker lock(&m_mutex); |
124 | |
125 | switch (int(e->type())) { |
126 | case INIT: |
127 | init(); |
128 | return true; |
129 | case RENDER: |
130 | render(lock: &lock); |
131 | return true; |
132 | case RESIZE: |
133 | if (m_cubeRenderer) |
134 | m_cubeRenderer->resize(w: m_window->width(), h: m_window->height()); |
135 | return true; |
136 | case STOP: |
137 | cleanup(); |
138 | return true; |
139 | default: |
140 | return QObject::event(event: e); |
141 | } |
142 | } |
143 | |
144 | void QuickRenderer::init() |
145 | { |
146 | m_context->makeCurrent(surface: m_surface); |
147 | |
148 | // Pass our offscreen surface to the cube renderer just so that it will |
149 | // have something is can make current during cleanup. QOffscreenSurface, |
150 | // just like QWindow, must always be created on the gui thread (as it might |
151 | // be backed by an actual QWindow). |
152 | m_cubeRenderer = new CubeRenderer(m_surface); |
153 | m_cubeRenderer->resize(w: m_window->width(), h: m_window->height()); |
154 | |
155 | m_renderControl->initialize(gl: m_context); |
156 | } |
157 | |
158 | void QuickRenderer::cleanup() |
159 | { |
160 | m_context->makeCurrent(surface: m_surface); |
161 | |
162 | m_renderControl->invalidate(); |
163 | |
164 | delete m_fbo; |
165 | m_fbo = nullptr; |
166 | |
167 | delete m_cubeRenderer; |
168 | m_cubeRenderer = nullptr; |
169 | |
170 | m_context->doneCurrent(); |
171 | m_context->moveToThread(thread: QCoreApplication::instance()->thread()); |
172 | |
173 | m_cond.wakeOne(); |
174 | } |
175 | |
176 | void QuickRenderer::ensureFbo() |
177 | { |
178 | if (m_fbo && m_fbo->size() != m_window->size() * m_window->devicePixelRatio()) { |
179 | delete m_fbo; |
180 | m_fbo = nullptr; |
181 | } |
182 | |
183 | if (!m_fbo) { |
184 | m_fbo = new QOpenGLFramebufferObject(m_window->size() * m_window->devicePixelRatio(), |
185 | QOpenGLFramebufferObject::CombinedDepthStencil); |
186 | m_quickWindow->setRenderTarget(m_fbo); |
187 | } |
188 | } |
189 | |
190 | void QuickRenderer::render(QMutexLocker *lock) |
191 | { |
192 | Q_ASSERT(QThread::currentThread() != m_window->thread()); |
193 | |
194 | if (!m_context->makeCurrent(surface: m_surface)) { |
195 | qWarning(msg: "Failed to make context current on render thread" ); |
196 | return; |
197 | } |
198 | |
199 | ensureFbo(); |
200 | |
201 | // Synchronization and rendering happens here on the render thread. |
202 | m_renderControl->sync(); |
203 | |
204 | // The gui thread can now continue. |
205 | m_cond.wakeOne(); |
206 | lock->unlock(); |
207 | |
208 | // Meanwhile on this thread continue with the actual rendering (into the FBO first). |
209 | m_renderControl->render(); |
210 | m_context->functions()->glFlush(); |
211 | |
212 | // The cube renderer uses its own context, no need to bother with the state here. |
213 | |
214 | // Get something onto the screen using our custom OpenGL engine. |
215 | QMutexLocker quitLock(&m_quitMutex); |
216 | if (!m_quit) |
217 | m_cubeRenderer->render(w: m_window, share: m_context, texture: m_fbo->texture()); |
218 | } |
219 | |
220 | void QuickRenderer::aboutToQuit() |
221 | { |
222 | QMutexLocker lock(&m_quitMutex); |
223 | m_quit = true; |
224 | } |
225 | |
226 | class RenderControl : public QQuickRenderControl |
227 | { |
228 | public: |
229 | RenderControl(QWindow *w) : m_window(w) { } |
230 | QWindow *renderWindow(QPoint *offset) override; |
231 | |
232 | private: |
233 | QWindow *m_window; |
234 | }; |
235 | |
236 | WindowMultiThreaded::WindowMultiThreaded() |
237 | : m_qmlComponent(nullptr), |
238 | m_rootItem(nullptr), |
239 | m_quickInitialized(false), |
240 | m_psrRequested(false) |
241 | { |
242 | setSurfaceType(QSurface::OpenGLSurface); |
243 | |
244 | QSurfaceFormat format; |
245 | // Qt Quick may need a depth and stencil buffer. Always make sure these are available. |
246 | format.setDepthBufferSize(16); |
247 | format.setStencilBufferSize(8); |
248 | setFormat(format); |
249 | |
250 | m_context = new QOpenGLContext; |
251 | m_context->setFormat(format); |
252 | m_context->create(); |
253 | |
254 | m_offscreenSurface = new QOffscreenSurface; |
255 | // Pass m_context->format(), not format. Format does not specify and color buffer |
256 | // sizes, while the context, that has just been created, reports a format that has |
257 | // these values filled in. Pass this to the offscreen surface to make sure it will be |
258 | // compatible with the context's configuration. |
259 | m_offscreenSurface->setFormat(m_context->format()); |
260 | m_offscreenSurface->create(); |
261 | |
262 | m_renderControl = new RenderControl(this); |
263 | |
264 | // Create a QQuickWindow that is associated with out render control. Note that this |
265 | // window never gets created or shown, meaning that it will never get an underlying |
266 | // native (platform) window. |
267 | m_quickWindow = new QQuickWindow(m_renderControl); |
268 | |
269 | // Create a QML engine. |
270 | m_qmlEngine = new QQmlEngine; |
271 | if (!m_qmlEngine->incubationController()) |
272 | m_qmlEngine->setIncubationController(m_quickWindow->incubationController()); |
273 | |
274 | m_quickRenderer = new QuickRenderer; |
275 | m_quickRenderer->setContext(m_context); |
276 | |
277 | // These live on the gui thread. Just give access to them on the render thread. |
278 | m_quickRenderer->setSurface(m_offscreenSurface); |
279 | m_quickRenderer->setWindow(this); |
280 | m_quickRenderer->setQuickWindow(m_quickWindow); |
281 | m_quickRenderer->setRenderControl(m_renderControl); |
282 | |
283 | m_quickRendererThread = new QThread; |
284 | |
285 | // Notify the render control that some scenegraph internals have to live on |
286 | // m_quickRenderThread. |
287 | m_renderControl->prepareThread(targetThread: m_quickRendererThread); |
288 | |
289 | // The QOpenGLContext and the QObject representing the rendering logic on |
290 | // the render thread must live on that thread. |
291 | m_context->moveToThread(thread: m_quickRendererThread); |
292 | m_quickRenderer->moveToThread(thread: m_quickRendererThread); |
293 | |
294 | m_quickRendererThread->start(); |
295 | |
296 | // Now hook up the signals. For simplicy we don't differentiate |
297 | // between renderRequested (only render is needed, no sync) and |
298 | // sceneChanged (polish and sync is needed too). |
299 | connect(sender: m_renderControl, signal: &QQuickRenderControl::renderRequested, receiver: this, slot: &WindowMultiThreaded::requestUpdate); |
300 | connect(sender: m_renderControl, signal: &QQuickRenderControl::sceneChanged, receiver: this, slot: &WindowMultiThreaded::requestUpdate); |
301 | } |
302 | |
303 | WindowMultiThreaded::~WindowMultiThreaded() |
304 | { |
305 | // Release resources and move the context ownership back to this thread. |
306 | m_quickRenderer->mutex()->lock(); |
307 | m_quickRenderer->requestStop(); |
308 | m_quickRenderer->cond()->wait(lockedMutex: m_quickRenderer->mutex()); |
309 | m_quickRenderer->mutex()->unlock(); |
310 | |
311 | m_quickRendererThread->quit(); |
312 | m_quickRendererThread->wait(); |
313 | |
314 | delete m_renderControl; |
315 | delete m_qmlComponent; |
316 | delete m_quickWindow; |
317 | delete m_qmlEngine; |
318 | |
319 | delete m_offscreenSurface; |
320 | delete m_context; |
321 | } |
322 | |
323 | void WindowMultiThreaded::requestUpdate() |
324 | { |
325 | if (m_quickInitialized && !m_psrRequested) { |
326 | m_psrRequested = true; |
327 | QCoreApplication::postEvent(receiver: this, event: new QEvent(UPDATE)); |
328 | } |
329 | } |
330 | |
331 | bool WindowMultiThreaded::event(QEvent *e) |
332 | { |
333 | if (e->type() == UPDATE) { |
334 | polishSyncAndRender(); |
335 | m_psrRequested = false; |
336 | return true; |
337 | } else if (e->type() == QEvent::Close) { |
338 | // Avoid rendering on the render thread when the window is about to |
339 | // close. Once a QWindow is closed, the underlying platform window will |
340 | // go away, even though the QWindow instance itself is still |
341 | // valid. Operations like swapBuffers() are futile and only result in |
342 | // warnings afterwards. Prevent this. |
343 | m_quickRenderer->aboutToQuit(); |
344 | } |
345 | return QWindow::event(e); |
346 | } |
347 | |
348 | void WindowMultiThreaded::polishSyncAndRender() |
349 | { |
350 | Q_ASSERT(QThread::currentThread() == thread()); |
351 | |
352 | // Polishing happens on the gui thread. |
353 | m_renderControl->polishItems(); |
354 | // Sync happens on the render thread with the gui thread (this one) blocked. |
355 | QMutexLocker lock(m_quickRenderer->mutex()); |
356 | m_quickRenderer->requestRender(); |
357 | // Wait until sync is complete. |
358 | m_quickRenderer->cond()->wait(lockedMutex: m_quickRenderer->mutex()); |
359 | // Rendering happens on the render thread without blocking the gui (main) |
360 | // thread. This is good because the blocking swap (waiting for vsync) |
361 | // happens on the render thread, not blocking other work. |
362 | } |
363 | |
364 | void WindowMultiThreaded::run() |
365 | { |
366 | disconnect(sender: m_qmlComponent, signal: &QQmlComponent::statusChanged, receiver: this, slot: &WindowMultiThreaded::run); |
367 | |
368 | if (m_qmlComponent->isError()) { |
369 | const QList<QQmlError> errorList = m_qmlComponent->errors(); |
370 | for (const QQmlError &error : errorList) |
371 | qWarning() << error.url() << error.line() << error; |
372 | return; |
373 | } |
374 | |
375 | QObject *rootObject = m_qmlComponent->create(); |
376 | if (m_qmlComponent->isError()) { |
377 | const QList<QQmlError> errorList = m_qmlComponent->errors(); |
378 | for (const QQmlError &error : errorList) |
379 | qWarning() << error.url() << error.line() << error; |
380 | return; |
381 | } |
382 | |
383 | m_rootItem = qobject_cast<QQuickItem *>(object: rootObject); |
384 | if (!m_rootItem) { |
385 | qWarning(msg: "run: Not a QQuickItem" ); |
386 | delete rootObject; |
387 | return; |
388 | } |
389 | |
390 | // The root item is ready. Associate it with the window. |
391 | m_rootItem->setParentItem(m_quickWindow->contentItem()); |
392 | |
393 | // Update item and rendering related geometries. |
394 | updateSizes(); |
395 | |
396 | m_quickInitialized = true; |
397 | |
398 | // Initialize the render thread and perform the first polish/sync/render. |
399 | m_quickRenderer->requestInit(); |
400 | polishSyncAndRender(); |
401 | } |
402 | |
403 | void WindowMultiThreaded::updateSizes() |
404 | { |
405 | // Behave like SizeRootObjectToView. |
406 | m_rootItem->setWidth(width()); |
407 | m_rootItem->setHeight(height()); |
408 | |
409 | m_quickWindow->setGeometry(posx: 0, posy: 0, w: width(), h: height()); |
410 | } |
411 | |
412 | void WindowMultiThreaded::startQuick(const QString &filename) |
413 | { |
414 | m_qmlComponent = new QQmlComponent(m_qmlEngine, QUrl(filename)); |
415 | if (m_qmlComponent->isLoading()) |
416 | connect(sender: m_qmlComponent, signal: &QQmlComponent::statusChanged, receiver: this, slot: &WindowMultiThreaded::run); |
417 | else |
418 | run(); |
419 | } |
420 | |
421 | void WindowMultiThreaded::exposeEvent(QExposeEvent *) |
422 | { |
423 | if (isExposed()) { |
424 | if (!m_quickInitialized) |
425 | startQuick(QStringLiteral("qrc:/rendercontrol/demo.qml" )); |
426 | } |
427 | } |
428 | |
429 | void WindowMultiThreaded::resizeEvent(QResizeEvent *) |
430 | { |
431 | // If this is a resize after the scene is up and running, recreate the fbo and the |
432 | // Quick item and scene. |
433 | if (m_rootItem) { |
434 | updateSizes(); |
435 | m_quickRenderer->requestResize(); |
436 | polishSyncAndRender(); |
437 | } |
438 | } |
439 | |
440 | void WindowMultiThreaded::mousePressEvent(QMouseEvent *e) |
441 | { |
442 | // Use the constructor taking localPos and screenPos. That puts localPos into the |
443 | // event's localPos and windowPos, and screenPos into the event's screenPos. This way |
444 | // the windowPos in e is ignored and is replaced by localPos. This is necessary |
445 | // because QQuickWindow thinks of itself as a top-level window always. |
446 | QMouseEvent mappedEvent(e->type(), e->localPos(), e->screenPos(), e->button(), e->buttons(), e->modifiers()); |
447 | QCoreApplication::sendEvent(receiver: m_quickWindow, event: &mappedEvent); |
448 | } |
449 | |
450 | void WindowMultiThreaded::mouseReleaseEvent(QMouseEvent *e) |
451 | { |
452 | QMouseEvent mappedEvent(e->type(), e->localPos(), e->screenPos(), e->button(), e->buttons(), e->modifiers()); |
453 | QCoreApplication::sendEvent(receiver: m_quickWindow, event: &mappedEvent); |
454 | } |
455 | |