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 <qtest.h>
30
31#include <QtQuick/qquickitem.h>
32#include <QtQuick/qquickview.h>
33#include <QtGui/qopenglcontext.h>
34#include <QtGui/qopenglfunctions.h>
35#include <QtGui/qscreen.h>
36#include <private/qsgrendernode_p.h>
37
38#include "../../shared/util.h"
39
40class tst_rendernode: public QQmlDataTest
41{
42 Q_OBJECT
43public:
44 tst_rendernode();
45
46 QImage runTest(const QString &fileName)
47 {
48 QQuickView view(&outerWindow);
49 view.setResizeMode(QQuickView::SizeViewToRootObject);
50 view.setSource(testFileUrl(fileName));
51 view.setVisible(true);
52 return QTest::qWaitForWindowExposed(window: &view) ? view.grabWindow() : QImage();
53 }
54
55 //It is important for platforms that only are able to show fullscreen windows
56 //to have a container for the window that is painted on.
57 QQuickWindow outerWindow;
58
59private slots:
60 void renderOrder();
61 void messUpState();
62 void matrix();
63
64private:
65 bool isRunningOnRhi() const;
66};
67
68class ClearNode : public QSGRenderNode
69{
70public:
71 StateFlags changedStates() const override
72 {
73 return ColorState;
74 }
75
76 void render(const RenderState *) override
77 {
78 // If clip has been set, scissoring will make sure the right area is cleared.
79 QOpenGLContext::currentContext()->functions()->glClearColor(red: color.redF(), green: color.greenF(), blue: color.blueF(), alpha: 1.0f);
80 QOpenGLContext::currentContext()->functions()->glClear(GL_COLOR_BUFFER_BIT);
81 }
82
83 QColor color;
84};
85
86class ClearItem : public QQuickItem
87{
88 Q_OBJECT
89 Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
90public:
91 ClearItem() : m_color(Qt::black)
92 {
93 setFlag(flag: ItemHasContents, enabled: true);
94 }
95
96 QColor color() const { return m_color; }
97 void setColor(const QColor &color)
98 {
99 if (color == m_color)
100 return;
101 m_color = color;
102 emit colorChanged();
103 }
104
105protected:
106 virtual QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
107 {
108 ClearNode *node = static_cast<ClearNode *>(oldNode);
109 if (!node)
110 node = new ClearNode;
111 node->color = m_color;
112 return node;
113 }
114
115Q_SIGNALS:
116 void colorChanged();
117
118private:
119 QColor m_color;
120};
121
122class MessUpNode : public QSGRenderNode, protected QOpenGLFunctions
123{
124public:
125 MessUpNode() {}
126
127 StateFlags changedStates() const override
128 {
129 return StateFlags(DepthState) | StencilState | ScissorState | ColorState | BlendState
130 | CullState | ViewportState | RenderTargetState;
131 }
132
133 void render(const RenderState *) override
134 {
135 if (!initialized) {
136 initializeOpenGLFunctions();
137 initialized = true;
138 }
139 // Don't draw anything, just mess up the state
140 glViewport(x: 10, y: 10, width: 10, height: 10);
141 glDisable(GL_SCISSOR_TEST);
142 glDepthMask(flag: true);
143 glEnable(GL_DEPTH_TEST);
144 glDepthFunc(GL_EQUAL);
145 glClearDepthf(depth: 1);
146 glClearStencil(s: 42);
147 glClearColor(red: 1.0f, green: 0.5f, blue: 1.0f, alpha: 0.0f);
148 glClear(GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
149 glEnable(GL_SCISSOR_TEST);
150 glScissor(x: 190, y: 190, width: 10, height: 10);
151 glStencilFunc(GL_EQUAL, ref: 28, mask: 0xff);
152 glBlendFunc(GL_ZERO, GL_ZERO);
153 GLint frontFace;
154 glGetIntegerv(GL_FRONT_FACE, params: &frontFace);
155 glFrontFace(mode: frontFace == GL_CW ? GL_CCW : GL_CW);
156 glEnable(GL_CULL_FACE);
157 GLuint fbo;
158 glGenFramebuffers(n: 1, framebuffers: &fbo);
159 glBindFramebuffer(GL_FRAMEBUFFER, framebuffer: fbo);
160 }
161
162 bool initialized = false;
163};
164
165class MessUpItem : public QQuickItem
166{
167 Q_OBJECT
168public:
169 MessUpItem()
170 {
171 setFlag(flag: ItemHasContents, enabled: true);
172 }
173
174protected:
175 virtual QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *)
176 {
177 MessUpNode *node = static_cast<MessUpNode *>(oldNode);
178 if (!node)
179 node = new MessUpNode;
180 return node;
181 }
182};
183
184tst_rendernode::tst_rendernode()
185{
186 qmlRegisterType<ClearItem>(uri: "Test", versionMajor: 1, versionMinor: 0, qmlName: "ClearItem");
187 qmlRegisterType<MessUpItem>(uri: "Test", versionMajor: 1, versionMinor: 0, qmlName: "MessUpItem");
188 outerWindow.showNormal();
189 outerWindow.setGeometry(posx: 0,posy: 0,w: 400,h: 400);
190}
191
192static bool fuzzyCompareColor(QRgb x, QRgb y, QByteArray *errorMessage)
193{
194 enum { fuzz = 4 };
195 if (qAbs(t: qRed(rgb: x) - qRed(rgb: y)) >= fuzz || qAbs(t: qGreen(rgb: x) - qGreen(rgb: y)) >= fuzz || qAbs(t: qBlue(rgb: x) - qBlue(rgb: y)) >= fuzz) {
196 QString s;
197 QDebug(&s).nospace() << hex << "Color mismatch 0x" << x << " 0x" << y << dec << " (fuzz=" << fuzz << ").";
198 *errorMessage = s.toLocal8Bit();
199 return false;
200 }
201 return true;
202}
203
204static inline QByteArray msgColorMismatchAt(const QByteArray &colorMsg, int x, int y)
205{
206 return colorMsg + QByteArrayLiteral(" at ") + QByteArray::number(x) +',' + QByteArray::number(y);
207}
208
209/* The test draws four rects, each 100x100 and verifies
210 * that a rendernode which calls glClear() is stacked
211 * correctly. The red rectangles come under the white
212 * and are obscured.
213 */
214void tst_rendernode::renderOrder()
215{
216 if (QGuiApplication::primaryScreen()->depth() < 24)
217 QSKIP("This test does not work at display depths < 24");
218
219 if ((QGuiApplication::platformName() == QLatin1String("offscreen"))
220 || (QGuiApplication::platformName() == QLatin1String("minimal")))
221 QSKIP("Skipping due to grabWindow not functional on offscreen/minimal platforms");
222
223 if (isRunningOnRhi())
224 QSKIP("Render nodes not yet supported with QRhi");
225
226 QImage fb = runTest(fileName: "RenderOrder.qml");
227 QVERIFY(!fb.isNull());
228
229 const qreal scaleFactor = QGuiApplication::primaryScreen()->devicePixelRatio();
230 QCOMPARE(fb.width(), qRound(200 * scaleFactor));
231 QCOMPARE(fb.height(), qRound(200 * scaleFactor));
232
233 QCOMPARE(fb.pixel(50 * scaleFactor, 50 * scaleFactor), qRgb(0xff, 0xff, 0xff));
234 QCOMPARE(fb.pixel(50 * scaleFactor, 150 * scaleFactor), qRgb(0xff, 0xff, 0xff));
235 QCOMPARE(fb.pixel(150 * scaleFactor, 50 * scaleFactor), qRgb(0x00, 0x00, 0xff));
236
237 QByteArray errorMessage;
238 const qreal coordinate = 150 * scaleFactor;
239 QVERIFY2(fuzzyCompareColor(fb.pixel(coordinate, coordinate), qRgb(0x7f, 0x7f, 0xff), &errorMessage),
240 msgColorMismatchAt(errorMessage, coordinate, coordinate).constData());
241}
242
243/* The test uses a number of nested rectangles with clipping
244 * and rotation to verify that using a render node which messes
245 * with the state does not break rendering that comes after it.
246 */
247void tst_rendernode::messUpState()
248{
249 if (QGuiApplication::primaryScreen()->depth() < 24)
250 QSKIP("This test does not work at display depths < 24");
251
252 if ((QGuiApplication::platformName() == QLatin1String("offscreen"))
253 || (QGuiApplication::platformName() == QLatin1String("minimal")))
254 QSKIP("Skipping due to grabWindow not functional on offscreen/minimal platforms");
255
256 if (isRunningOnRhi())
257 QSKIP("Render nodes not yet supported with QRhi");
258
259 QImage fb = runTest(fileName: "MessUpState.qml");
260 QVERIFY(!fb.isNull());
261 int x1 = 0;
262 int x2 = fb.width() / 2;
263 int x3 = fb.width() - 1;
264 int y1 = 0;
265 int y2 = fb.height() * 3 / 16;
266 int y3 = fb.height() / 2;
267 int y4 = fb.height() * 13 / 16;
268 int y5 = fb.height() - 1;
269
270 QCOMPARE(fb.pixel(x1, y3), qRgb(0xff, 0xff, 0xff));
271 QCOMPARE(fb.pixel(x3, y3), qRgb(0xff, 0xff, 0xff));
272
273 QCOMPARE(fb.pixel(x2, y1), qRgb(0x00, 0x00, 0x00));
274 QCOMPARE(fb.pixel(x2, y2), qRgb(0x00, 0x00, 0x00));
275 QByteArray errorMessage;
276 QVERIFY2(fuzzyCompareColor(fb.pixel(x2, y3), qRgb(0x7f, 0x00, 0x7f), &errorMessage),
277 msgColorMismatchAt(errorMessage, x2, y3).constData());
278 QCOMPARE(fb.pixel(x2, y4), qRgb(0x00, 0x00, 0x00));
279 QCOMPARE(fb.pixel(x2, y5), qRgb(0x00, 0x00, 0x00));
280}
281
282class StateRecordingRenderNode : public QSGRenderNode
283{
284public:
285 StateFlags changedStates() const override { return StateFlags(-1); }
286 void render(const RenderState *) override {
287 matrices[name] = *matrix();
288
289 }
290
291 QString name;
292 static QHash<QString, QMatrix4x4> matrices;
293};
294
295QHash<QString, QMatrix4x4> StateRecordingRenderNode::matrices;
296
297class StateRecordingRenderNodeItem : public QQuickItem
298{
299 Q_OBJECT
300public:
301 StateRecordingRenderNodeItem() { setFlag(flag: ItemHasContents, enabled: true); }
302 QSGNode *updatePaintNode(QSGNode *r, UpdatePaintNodeData *) {
303 if (r)
304 return r;
305 StateRecordingRenderNode *rn = new StateRecordingRenderNode();
306 rn->name = objectName();
307 return rn;
308 }
309};
310
311void tst_rendernode::matrix()
312{
313 if ((QGuiApplication::platformName() == QLatin1String("offscreen"))
314 || (QGuiApplication::platformName() == QLatin1String("minimal")))
315 QSKIP("Skipping due to grabWindow not functional on offscreen/minimal platforms");
316
317 if (isRunningOnRhi())
318 QSKIP("Render nodes not yet supported with QRhi");
319
320 qmlRegisterType<StateRecordingRenderNodeItem>(uri: "RenderNode", versionMajor: 1, versionMinor: 0, qmlName: "StateRecorder");
321 StateRecordingRenderNode::matrices.clear();
322 QVERIFY(!runTest("matrix.qml").isNull());
323
324 QMatrix4x4 noRotateOffset;
325 noRotateOffset.translate(x: 20, y: 20);
326 { QMatrix4x4 result = StateRecordingRenderNode::matrices.value(QStringLiteral("no-clip; no-rotation"));
327 QCOMPARE(result, noRotateOffset);
328 }
329 { QMatrix4x4 result = StateRecordingRenderNode::matrices.value(QStringLiteral("parent-clip; no-rotation"));
330 QCOMPARE(result, noRotateOffset);
331 }
332 { QMatrix4x4 result = StateRecordingRenderNode::matrices.value(QStringLiteral("self-clip; no-rotation"));
333 QCOMPARE(result, noRotateOffset);
334 }
335
336 QMatrix4x4 parentRotation;
337 parentRotation.translate(x: 10, y: 10); // parent at x/y: 10
338 parentRotation.translate(x: 5, y: 5); // rotate 90 around center (width/height: 10)
339 parentRotation.rotate(angle: 90, x: 0, y: 0, z: 1);
340 parentRotation.translate(x: -5, y: -5);
341 parentRotation.translate(x: 10, y: 10); // StateRecorder at: x/y: 10
342 { QMatrix4x4 result = StateRecordingRenderNode::matrices.value(QStringLiteral("no-clip; parent-rotation"));
343 QCOMPARE(result, parentRotation);
344 }
345 { QMatrix4x4 result = StateRecordingRenderNode::matrices.value(QStringLiteral("parent-clip; parent-rotation"));
346 QCOMPARE(result, parentRotation);
347 }
348 { QMatrix4x4 result = StateRecordingRenderNode::matrices.value(QStringLiteral("self-clip; parent-rotation"));
349 QCOMPARE(result, parentRotation);
350 }
351
352 QMatrix4x4 selfRotation;
353 selfRotation.translate(x: 10, y: 10); // parent at x/y: 10
354 selfRotation.translate(x: 10, y: 10); // StateRecorder at: x/y: 10
355 selfRotation.rotate(angle: 90, x: 0, y: 0, z: 1); // rotate 90, width/height: 0
356 { QMatrix4x4 result = StateRecordingRenderNode::matrices.value(QStringLiteral("no-clip; self-rotation"));
357 QCOMPARE(result, selfRotation);
358 }
359 { QMatrix4x4 result = StateRecordingRenderNode::matrices.value(QStringLiteral("parent-clip; self-rotation"));
360 QCOMPARE(result, selfRotation);
361 }
362 { QMatrix4x4 result = StateRecordingRenderNode::matrices.value(QStringLiteral("self-clip; self-rotation"));
363 QCOMPARE(result, selfRotation);
364 }
365}
366
367bool tst_rendernode::isRunningOnRhi() const
368{
369 static bool retval = false;
370 static bool decided = false;
371 if (!decided) {
372 decided = true;
373 QQuickView dummy;
374 dummy.show();
375 if (QTest::qWaitForWindowExposed(window: &dummy)) {
376 QSGRendererInterface::GraphicsApi api = dummy.rendererInterface()->graphicsApi();
377 retval = QSGRendererInterface::isApiRhiBased(api);
378 }
379 dummy.hide();
380 }
381 return retval;
382}
383
384QTEST_MAIN(tst_rendernode)
385
386#include "tst_rendernode.moc"
387

source code of qtdeclarative/tests/auto/quick/rendernode/tst_rendernode.cpp