1/****************************************************************************
2**
3** Copyright (C) 2016 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the test suite of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:GPL-EXCEPT$
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** GNU General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU
19** General Public License version 3 as published by the Free Software
20** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
21** included in the packaging of this file. Please review the following
22** information to ensure the GNU General Public License requirements will
23** be met: https://www.gnu.org/licenses/gpl-3.0.html.
24**
25** $QT_END_LICENSE$
26**
27****************************************************************************/
28
29#include <QtTest/QtTest>
30#include <QtCore/QtCore>
31#include <QtGui/QtGui>
32#include <private/qguiapplication_p.h>
33#include <qpa/qplatformintegration.h>
34#include <QtWidgets/QApplication>
35#include <QtOpenGL/QtOpenGL>
36#include <qelapsedtimer.h>
37#include "tst_qglthreads.h"
38
39#ifndef QT_OPENGL_ES_2
40#include <QtGui/QOpenGLFunctions_1_0>
41#endif
42
43#define RUNNING_TIME 5000
44
45tst_QGLThreads::tst_QGLThreads(QObject *parent)
46 : QObject(parent)
47{
48}
49
50/*
51
52 swapInThread
53
54 The purpose of this testcase is to verify that it is possible to do rendering into
55 a GL context from the GUI thread, then swap the contents in from a background thread.
56
57 The usecase for this is to have the background thread do the waiting for vertical
58 sync while the GUI thread is idle.
59
60 Currently the locking is handled directly in the paintEvent(). For the actual usecase
61 in Qt, the locking is done in the windowsurface before starting any drawing while
62 unlocking is done after all drawing has been done.
63 */
64
65
66class SwapThread : public QThread
67{
68 Q_OBJECT
69public:
70 SwapThread(QGLWidget *widget)
71 : m_context(widget->context())
72 , m_swapTriggered(false)
73 {
74 moveToThread(thread: this);
75 }
76
77 void run() {
78 QElapsedTimer timer;
79 timer.start();
80 while (timer.elapsed() < RUNNING_TIME) {
81 lock();
82 waitForReadyToSwap();
83
84 m_context->makeCurrent();
85 m_context->swapBuffers();
86 m_context->doneCurrent();
87
88 m_context->moveToThread(qApp->thread());
89
90 signalSwapDone();
91 unlock();
92 }
93
94 m_swapTriggered = false;
95 }
96
97 void lock() { m_mutex.lock(); }
98 void unlock() { m_mutex.unlock(); }
99
100 void waitForSwapDone() { if (m_swapTriggered) m_swapDone.wait(lockedMutex: &m_mutex); }
101 void waitForReadyToSwap() { if (!m_swapTriggered) m_readyToSwap.wait(lockedMutex: &m_mutex); }
102
103 void signalReadyToSwap()
104 {
105 if (!isRunning())
106 return;
107 m_readyToSwap.wakeAll();
108 m_swapTriggered = true;
109 }
110
111 void signalSwapDone()
112 {
113 m_swapTriggered = false;
114 m_swapDone.wakeAll();
115 }
116
117private:
118 QGLContext *m_context;
119 QMutex m_mutex;
120 QWaitCondition m_readyToSwap;
121 QWaitCondition m_swapDone;
122
123 bool m_swapTriggered;
124};
125
126class ForegroundWidget : public QGLWidget
127{
128public:
129 ForegroundWidget(const QGLFormat &format)
130 : QGLWidget(format), m_thread(0)
131 {
132 setAutoBufferSwap(false);
133 }
134
135 void resizeEvent(QResizeEvent *e)
136 {
137 m_thread->lock();
138 QGLWidget::resizeEvent(e);
139 m_thread->unlock();
140 }
141
142 void paintEvent(QPaintEvent *)
143 {
144 m_thread->lock();
145 m_thread->waitForSwapDone();
146
147 makeCurrent();
148 QPainter p(this);
149 p.fillRect(rect(), color: QColor(QRandomGenerator::global()->bounded(highest: 256), QRandomGenerator::global()->bounded(highest: 256), QRandomGenerator::global()->bounded(highest: 256)));
150 p.setPen(Qt::red);
151 p.setFont(QFont("SansSerif", 24));
152 p.drawText(r: rect(), flags: Qt::AlignCenter, text: "This is an autotest");
153 p.end();
154 doneCurrent();
155
156 if (m_thread->isRunning()) {
157 context()->moveToThread(thread: m_thread);
158 m_thread->signalReadyToSwap();
159 }
160
161 m_thread->unlock();
162
163 update();
164 }
165
166 void setThread(SwapThread *thread) {
167 m_thread = thread;
168 }
169
170 SwapThread *m_thread;
171};
172
173void tst_QGLThreads::swapInThread()
174{
175 if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(cap: QPlatformIntegration::ThreadedOpenGL))
176 QSKIP("No platformsupport for ThreadedOpenGL");
177 QGLFormat format;
178 format.setSwapInterval(1);
179 ForegroundWidget widget(format);
180 SwapThread thread(&widget);
181 widget.setThread(&thread);
182 widget.show();
183
184 QVERIFY(QTest::qWaitForWindowExposed(&widget));
185 thread.start();
186
187 while (thread.isRunning()) {
188 qApp->processEvents();
189 }
190
191 widget.hide();
192
193 QVERIFY(true);
194}
195
196/*
197 renderInThread
198
199 This test sets up a scene and renders it in a different thread.
200 For simplicity, the scene is simply a bunch of rectangles, but
201 if that works, we're in good shape..
202 */
203
204static inline float qrandom() { return (QRandomGenerator::global()->bounded(highest: 100)) / 100.f; }
205
206void renderAScene(int w, int h)
207{
208 QOpenGLFunctions *funcs = QOpenGLContext::currentContext()->functions();
209
210 if (QOpenGLContext::currentContext()->isOpenGLES()) {
211 Q_UNUSED(w);
212 Q_UNUSED(h);
213 QGLShaderProgram program;
214 program.addShaderFromSourceCode(type: QGLShader::Vertex, source: "attribute highp vec2 pos; void main() { gl_Position = vec4(pos.xy, 1.0, 1.0); }");
215 program.addShaderFromSourceCode(type: QGLShader::Fragment, source: "uniform lowp vec4 color; void main() { gl_FragColor = color; }");
216 program.bindAttributeLocation(name: "pos", location: 0);
217 program.bind();
218
219 funcs->glEnableVertexAttribArray(index: 0);
220
221 for (int i=0; i<1000; ++i) {
222 GLfloat pos[] = {
223 (QRandomGenerator::global()->bounded(highest: 100)) / 100.f,
224 (QRandomGenerator::global()->bounded(highest: 100)) / 100.f,
225 (QRandomGenerator::global()->bounded(highest: 100)) / 100.f,
226 (QRandomGenerator::global()->bounded(highest: 100)) / 100.f,
227 (QRandomGenerator::global()->bounded(highest: 100)) / 100.f,
228 (QRandomGenerator::global()->bounded(highest: 100)) / 100.f
229 };
230
231 funcs->glVertexAttribPointer(indx: 0, size: 2, GL_FLOAT, GL_FALSE, stride: 0, ptr: pos);
232 funcs->glDrawArrays(GL_TRIANGLE_STRIP, first: 0, count: 3);
233 }
234 } else {
235#ifndef QT_OPENGL_ES_2
236 QOpenGLFunctions_1_0 *gl1funcs = QOpenGLContext::currentContext()->versionFunctions<QOpenGLFunctions_1_0>();
237 gl1funcs->initializeOpenGLFunctions();
238
239 gl1funcs->glViewport(x: 0, y: 0, width: w, height: h);
240
241 gl1funcs->glMatrixMode(GL_PROJECTION);
242 gl1funcs->glLoadIdentity();
243 gl1funcs->glFrustum(left: 0, right: w, bottom: h, top: 0, zNear: 1, zFar: 100);
244 gl1funcs->glTranslated(x: 0, y: 0, z: -1);
245
246 gl1funcs->glMatrixMode(GL_MODELVIEW);
247 gl1funcs->glLoadIdentity();
248
249 for (int i=0;i<1000; ++i) {
250 gl1funcs->glBegin(GL_TRIANGLES);
251 gl1funcs->glColor3f(red: qrandom(), green: qrandom(), blue: qrandom());
252 gl1funcs->glVertex2f(x: qrandom() * w, y: qrandom() * h);
253 gl1funcs->glColor3f(red: qrandom(), green: qrandom(), blue: qrandom());
254 gl1funcs->glVertex2f(x: qrandom() * w, y: qrandom() * h);
255 gl1funcs->glColor3f(red: qrandom(), green: qrandom(), blue: qrandom());
256 gl1funcs->glVertex2f(x: qrandom() * w, y: qrandom() * h);
257 gl1funcs->glEnd();
258 }
259#endif
260 }
261}
262
263class ThreadSafeGLWidget : public QGLWidget
264{
265public:
266 ThreadSafeGLWidget(QWidget *parent = 0) : QGLWidget(parent) {}
267 void paintEvent(QPaintEvent *)
268 {
269 // ignored as we're anyway swapping as fast as we can
270 };
271
272 void resizeEvent(QResizeEvent *e)
273 {
274 mutex.lock();
275 newSize = e->size();
276 mutex.unlock();
277 };
278
279 QMutex mutex;
280 QSize newSize;
281};
282
283class SceneRenderingThread : public QThread
284{
285 Q_OBJECT
286public:
287 SceneRenderingThread(ThreadSafeGLWidget *widget)
288 : m_widget(widget)
289 {
290 moveToThread(thread: this);
291 m_size = widget->size();
292 }
293
294 void run() {
295 QElapsedTimer timer;
296 timer.start();
297 failure = false;
298
299 while (timer.elapsed() < RUNNING_TIME && !failure) {
300
301 m_widget->makeCurrent();
302
303 m_widget->mutex.lock();
304 QSize s = m_widget->newSize;
305 m_widget->mutex.unlock();
306
307 QOpenGLFunctions *funcs = QOpenGLContext::currentContext()->functions();
308 if (s != m_size) {
309 funcs->glViewport(x: 0, y: 0, width: s.width(), height: s.height());
310 }
311
312 if (QGLContext::currentContext() != m_widget->context()) {
313 failure = true;
314 break;
315 }
316
317 funcs->glClear(GL_COLOR_BUFFER_BIT);
318
319 int w = m_widget->width();
320 int h = m_widget->height();
321
322 renderAScene(w, h);
323
324 int color;
325 funcs->glReadPixels(x: w / 2, y: h / 2, width: 1, height: 1, GL_RGBA, GL_UNSIGNED_BYTE, pixels: &color);
326
327 m_widget->swapBuffers();
328 }
329
330 m_widget->doneCurrent();
331 }
332
333 bool failure;
334
335private:
336 ThreadSafeGLWidget *m_widget;
337 QSize m_size;
338};
339
340void tst_QGLThreads::renderInThread_data()
341{
342 QTest::addColumn<bool>(name: "resize");
343 QTest::addColumn<bool>(name: "update");
344
345 QTest::newRow(dataTag: "basic") << false << false;
346 QTest::newRow(dataTag: "with-resize") << true << false;
347 QTest::newRow(dataTag: "with-update") << false << true;
348 QTest::newRow(dataTag: "with-resize-and-update") << true << true;
349}
350
351void tst_QGLThreads::renderInThread()
352{
353 if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(cap: QPlatformIntegration::ThreadedOpenGL))
354 QSKIP("No platformsupport for ThreadedOpenGL");
355
356 QFETCH(bool, resize);
357 QFETCH(bool, update);
358
359#if defined(Q_OS_MACOS)
360 if (resize)
361 QSKIP("gldSetZero crashes in render thread, QTBUG-68524");
362#endif
363
364 ThreadSafeGLWidget widget;
365 widget.resize(w: 200, h: 200);
366 SceneRenderingThread thread(&widget);
367
368 widget.show();
369 QVERIFY(QTest::qWaitForWindowExposed(&widget));
370 widget.doneCurrent();
371
372 widget.context()->moveToThread(thread: &thread);
373
374 thread.start();
375
376 int value = 10;
377 while (thread.isRunning()) {
378 if (resize)
379 widget.resize(w: 200 + value, h: 200 + value);
380 if (update)
381 widget.update(ax: 100 + value, ay: 100 + value, aw: 20, ah: 20);
382 qApp->processEvents();
383 value = -value;
384
385 QThread::msleep(100);
386 }
387
388 QVERIFY(!thread.failure);
389}
390
391class Device
392{
393public:
394 virtual ~Device() {}
395 virtual QPaintDevice *realPaintDevice() = 0;
396 virtual void prepareDevice() {}
397 virtual void moveToThread(QThread *) {}
398};
399
400class GLWidgetWrapper : public Device
401{
402public:
403 GLWidgetWrapper() {
404 widget.resize(w: 150, h: 150);
405 widget.show();
406 QVERIFY(QTest::qWaitForWindowExposed(&widget));
407 widget.doneCurrent();
408 }
409 QPaintDevice *realPaintDevice() { return &widget; }
410 void moveToThread(QThread *thread) { widget.context()->moveToThread(thread); }
411
412 ThreadSafeGLWidget widget;
413};
414
415class PixmapWrapper : public Device
416{
417public:
418 PixmapWrapper() { pixmap = new QPixmap(512, 512); }
419 ~PixmapWrapper() { delete pixmap; }
420 QPaintDevice *realPaintDevice() { return pixmap; }
421
422 QPixmap *pixmap;
423};
424
425class PixelBufferWrapper : public Device
426{
427public:
428 PixelBufferWrapper() { pbuffer = new QGLPixelBuffer(512, 512); }
429 ~PixelBufferWrapper() { delete pbuffer; }
430 QPaintDevice *realPaintDevice() { return pbuffer; }
431 void moveToThread(QThread *thread) { pbuffer->context()->moveToThread(thread); }
432
433 QGLPixelBuffer *pbuffer;
434};
435
436
437class FrameBufferObjectWrapper : public Device
438{
439public:
440 FrameBufferObjectWrapper() {
441 widget.makeCurrent();
442 fbo = new QGLFramebufferObject(512, 512);
443 widget.doneCurrent();
444 }
445 ~FrameBufferObjectWrapper() { delete fbo; }
446 QPaintDevice *realPaintDevice() { return fbo; }
447 void prepareDevice() { widget.makeCurrent(); }
448 void moveToThread(QThread *thread) { widget.context()->moveToThread(thread); }
449
450 ThreadSafeGLWidget widget;
451 QGLFramebufferObject *fbo;
452};
453
454
455class ThreadPainter : public QObject
456{
457 Q_OBJECT
458public:
459 ThreadPainter(Device *pd) : device(pd), fail(true) {
460 pixmap = QPixmap(40, 40);
461 pixmap.fill(fillColor: Qt::green);
462 QPainter p(&pixmap);
463 p.drawLine(x1: 0, y1: 0, x2: 40, y2: 40);
464 p.drawLine(x1: 0, y1: 40, x2: 40, y2: 0);
465 }
466
467public slots:
468 void draw() {
469 bool beginFailed = false;
470 QElapsedTimer timer;
471 timer.start();
472 int rotAngle = 10;
473 device->prepareDevice();
474 QPaintDevice *paintDevice = device->realPaintDevice();
475 QSize s(paintDevice->width(), paintDevice->height());
476 while (timer.elapsed() < RUNNING_TIME) {
477 QPainter p;
478 if (!p.begin(paintDevice)) {
479 beginFailed = true;
480 break;
481 }
482 p.translate(dx: s.width()/2, dy: s.height()/2);
483 p.rotate(a: rotAngle);
484 p.translate(dx: -s.width()/2, dy: -s.height()/2);
485 p.fillRect(x: 0, y: 0, w: s.width(), h: s.height(), c: Qt::red);
486 QRect rect(QPoint(0, 0), s);
487 p.drawPixmap(x: 10, y: 10, pm: pixmap);
488 p.drawTiledPixmap(x: 50, y: 50, w: 100, h: 100, pm: pixmap);
489 p.drawText(p: rect.center(), s: "This is a piece of text");
490 p.end();
491 rotAngle += 2;
492 QThread::msleep(20);
493 }
494
495 device->moveToThread(qApp->thread());
496
497 fail = beginFailed;
498 QThread::currentThread()->quit();
499 }
500
501 bool failed() { return fail; }
502
503private:
504 QPixmap pixmap;
505 Device *device;
506 bool fail;
507};
508
509template <class T>
510class PaintThreadManager
511{
512public:
513 PaintThreadManager(int count) : numThreads(count)
514 {
515 for (int i=0; i<numThreads; ++i)
516 devices.append(new T);
517 // Wait until resize events are processed on the internal
518 // QGLWidgets of the buffers to suppress errors
519 // about makeCurrent() from the wrong thread.
520 QCoreApplication::processEvents();
521 for (int i=0; i<numThreads; ++i) {
522 devices.append(new T);
523 threads.append(t: new QThread);
524 painters.append(t: new ThreadPainter(devices.at(i)));
525 painters.at(i)->moveToThread(thread: threads.at(i));
526 painters.at(i)->connect(sender: threads.at(i), SIGNAL(started()), receiver: painters.at(i), SLOT(draw()));
527 devices.at(i)->moveToThread(threads.at(i));
528 }
529 }
530
531 ~PaintThreadManager() {
532 qDeleteAll(c: threads);
533 qDeleteAll(c: painters);
534 qDeleteAll(c: devices);
535 }
536
537
538 void start() {
539 for (int i=0; i<numThreads; ++i)
540 threads.at(i)->start();
541 }
542
543 bool areRunning() {
544 bool running = false;
545 for (int i=0; i<numThreads; ++i){
546 if (threads.at(i)->isRunning())
547 running = true;
548 }
549
550 return running;
551 }
552
553 bool failed() {
554 for (int i=0; i<numThreads; ++i) {
555 if (painters.at(i)->failed())
556 return true;
557 }
558
559 return false;
560 }
561
562private:
563 QList<QThread *> threads;
564 QList<Device *> devices;
565 QList<ThreadPainter *> painters;
566 int numThreads;
567};
568
569/*
570 This test uses QPainter to draw onto different QGLWidgets in
571 different threads at the same time. The ThreadSafeGLWidget is
572 necessary to handle paint and resize events that might come from
573 the main thread at any time while the test is running. The resize
574 and paint events would cause makeCurrent() calls to be issued from
575 within the QGLWidget while the widget's context was current in
576 another thread, which would cause errors.
577*/
578void tst_QGLThreads::painterOnGLWidgetInThread()
579{
580 //QTBUG-46446 tst_qglthreads is unstable on windows 7
581 if (QGuiApplication::platformName().compare(s: "windows 7", cs: Qt::CaseInsensitive))
582 QSKIP("Doesn't work on this platform. QTBUG-46446");
583 if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(cap: QPlatformIntegration::ThreadedOpenGL))
584 QSKIP("No platformsupport for ThreadedOpenGL");
585 if (!((QGLFormat::openGLVersionFlags() & QGLFormat::OpenGL_Version_2_0) ||
586 (QGLFormat::openGLVersionFlags() & QGLFormat::OpenGL_ES_Version_2_0))) {
587 QSKIP("The OpenGL based threaded QPainter tests requires OpenGL/ES 2.0.");
588 }
589
590 PaintThreadManager<GLWidgetWrapper> painterThreads(5);
591 painterThreads.start();
592
593 while (painterThreads.areRunning()) {
594 qApp->processEvents();
595 QThread::msleep(100);
596 }
597 QVERIFY(!painterThreads.failed());
598}
599
600/*
601 This test uses QPainter to draw onto different QPixmaps in
602 different threads at the same time.
603*/
604void tst_QGLThreads::painterOnPixmapInThread()
605{
606 if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(cap: QPlatformIntegration::ThreadedOpenGL)
607 || !QGuiApplicationPrivate::platformIntegration()->hasCapability(cap: QPlatformIntegration::ThreadedPixmaps))
608 QSKIP("No platformsupport for ThreadedOpenGL or ThreadedPixmaps");
609 PaintThreadManager<PixmapWrapper> painterThreads(5);
610 painterThreads.start();
611
612 while (painterThreads.areRunning()) {
613 qApp->processEvents();
614 QThread::msleep(100);
615 }
616 QVERIFY(!painterThreads.failed());
617}
618
619/* This test uses QPainter to draw onto different QGLPixelBuffer
620 objects in different threads at the same time.
621*/
622void tst_QGLThreads::painterOnPboInThread()
623{
624 //QTBUG-46446 tst_qglthreads is unstable on windows 7
625 if (QGuiApplication::platformName().compare(s: "windows 7", cs: Qt::CaseInsensitive))
626 QSKIP("Doesn't work on this platform. QTBUG-46446");
627 if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(cap: QPlatformIntegration::ThreadedOpenGL))
628 QSKIP("No platformsupport for ThreadedOpenGL");
629 if (!((QGLFormat::openGLVersionFlags() & QGLFormat::OpenGL_Version_2_0) ||
630 (QGLFormat::openGLVersionFlags() & QGLFormat::OpenGL_ES_Version_2_0))) {
631 QSKIP("The OpenGL based threaded QPainter tests requires OpenGL/ES 2.0.");
632 }
633
634 if (!QGLPixelBuffer::hasOpenGLPbuffers()) {
635 QSKIP("This system doesn't support pbuffers.");
636 }
637
638 PaintThreadManager<PixelBufferWrapper> painterThreads(5);
639 painterThreads.start();
640
641 while (painterThreads.areRunning()) {
642 qApp->processEvents();
643 QThread::msleep(100);
644 }
645 QVERIFY(!painterThreads.failed());
646}
647
648/* This test uses QPainter to draw onto different
649 QGLFramebufferObjects (bound in a QGLWidget's context) in different
650 threads at the same time.
651*/
652void tst_QGLThreads::painterOnFboInThread()
653{
654 //QTBUG-46446 tst_qglthreads is unstable on windows 7
655 if (QGuiApplication::platformName().compare(s: "windows 7", cs: Qt::CaseInsensitive))
656 QSKIP("Doesn't work on this platform. QTBUG-46446");
657 if (!QGuiApplicationPrivate::platformIntegration()->hasCapability(cap: QPlatformIntegration::ThreadedOpenGL))
658 QSKIP("No platformsupport for ThreadedOpenGL");
659 if (!((QGLFormat::openGLVersionFlags() & QGLFormat::OpenGL_Version_2_0) ||
660 (QGLFormat::openGLVersionFlags() & QGLFormat::OpenGL_ES_Version_2_0))) {
661 QSKIP("The OpenGL based threaded QPainter tests requires OpenGL/ES 2.0.");
662 }
663
664 if (!QGLFramebufferObject::hasOpenGLFramebufferObjects()) {
665 QSKIP("This system doesn't support framebuffer objects.");
666 }
667
668 PaintThreadManager<FrameBufferObjectWrapper> painterThreads(5);
669 painterThreads.start();
670
671 while (painterThreads.areRunning()) {
672 qApp->processEvents();
673 QThread::msleep(100);
674 }
675 QVERIFY(!painterThreads.failed());
676}
677
678int main(int argc, char **argv)
679{
680 QApplication app(argc, argv);
681 QTEST_DISABLE_KEYPAD_NAVIGATION \
682
683 tst_QGLThreads tc;
684 return QTest::qExec(testObject: &tc, argc, argv);
685}
686
687#include "tst_qglthreads.moc"
688

source code of qtbase/tests/auto/opengl/qglthreads/tst_qglthreads.cpp