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#if QT_CONFIG(opengl)
32#include <QOffscreenSurface>
33#include <QOpenGLContext>
34#include <QOpenGLFunctions>
35#endif
36
37#include <QtQuick>
38#include <QtQml>
39
40#if QT_CONFIG(opengl)
41#include <private/qopenglcontext_p.h>
42#endif
43
44#include <private/qsgcontext_p.h>
45#include <private/qsgrenderloop_p.h>
46
47#include "../../shared/util.h"
48#include "../shared/visualtestutil.h"
49
50#include <QtGui/private/qguiapplication_p.h>
51#include <QtGui/qpa/qplatformintegration.h>
52
53using namespace QQuickVisualTestUtil;
54
55class PerPixelRect : public QQuickItem
56{
57 Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)
58 Q_OBJECT
59public:
60 PerPixelRect() {
61 setFlag(flag: ItemHasContents);
62 }
63
64 void setColor(const QColor &c) {
65 if (c == m_color)
66 return;
67 m_color = c;
68 emit colorChanged(c);
69 }
70
71 QColor color() const { return m_color; }
72
73 QSGNode *updatePaintNode(QSGNode *node, UpdatePaintNodeData *)
74 {
75 delete node;
76 node = new QSGNode;
77
78 const int w = int(width());
79 const int h = int(height());
80 QQuickWindow *win = window();
81 for (int y = 0; y < h; ++y) {
82 for (int x = 0; x < w; ++x) {
83 QSGRectangleNode *rn = win->createRectangleNode();
84 rn->setRect(x, y, w: 1, h: 1);
85 rn->setColor(m_color);
86 node->appendChildNode(node: rn);
87 }
88 }
89
90 return node;
91 }
92
93Q_SIGNALS:
94 void colorChanged(const QColor &c);
95
96private:
97 QColor m_color;
98};
99
100class tst_SceneGraph : public QQmlDataTest
101{
102 Q_OBJECT
103
104private slots:
105 void initTestCase();
106
107 void manyWindows_data();
108 void manyWindows();
109
110 void render_data();
111 void render();
112#if QT_CONFIG(opengl)
113 void hideWithOtherContext();
114#endif
115 void createTextureFromImage_data();
116 void createTextureFromImage();
117
118private:
119 bool m_brokenMipmapSupport;
120 QQuickView *createView(const QString &file, QWindow *parent = nullptr, int x = -1, int y = -1, int w = -1, int h = -1);
121 bool isRunningOnOpenGLDirectly();
122 bool isRunningOnRhi();
123};
124
125template <typename T> class ScopedList : public QList<T> {
126public:
127 ~ScopedList() { qDeleteAll(*this); }
128};
129
130void tst_SceneGraph::initTestCase()
131{
132 qmlRegisterType<PerPixelRect>(uri: "SceneGraphTest", versionMajor: 1, versionMinor: 0, qmlName: "PerPixelRect");
133
134 QQmlDataTest::initTestCase();
135
136 QSGRenderLoop *loop = QSGRenderLoop::instance();
137 qDebug() << "RenderLoop: " << loop;
138
139#if QT_CONFIG(opengl)
140 if (QGuiApplicationPrivate::platformIntegration()->hasCapability(cap: QPlatformIntegration::OpenGL)) {
141 QOpenGLContext context;
142 context.setFormat(loop->sceneGraphContext()->defaultSurfaceFormat());
143 context.create();
144 QSurfaceFormat format = context.format();
145
146 QOffscreenSurface surface;
147 surface.setFormat(format);
148 surface.create();
149 if (!context.makeCurrent(surface: &surface))
150 qFatal(msg: "Failed to create a GL context...");
151
152 QOpenGLFunctions *funcs = QOpenGLContext::currentContext()->functions();
153 qDebug() << "R/G/B/A Buffers: " << format.redBufferSize() << format.greenBufferSize() << format.blueBufferSize() << format.alphaBufferSize();
154 qDebug() << "Depth Buffer: " << format.depthBufferSize();
155 qDebug() << "Stencil Buffer: " << format.stencilBufferSize();
156 qDebug() << "Samples: " << format.samples();
157 int textureSize;
158 funcs->glGetIntegerv(GL_MAX_TEXTURE_SIZE, params: &textureSize);
159 qDebug() << "Max Texture Size: " << textureSize;
160 qDebug() << "GL_VENDOR: " << (const char *) funcs->glGetString(GL_VENDOR);
161 qDebug() << "GL_RENDERER: " << (const char *) funcs->glGetString(GL_RENDERER);
162 QByteArray version = (const char *) funcs->glGetString(GL_VERSION);
163 qDebug() << "GL_VERSION: " << version.constData();
164 QSet<QByteArray> exts = context.extensions();
165 QByteArray all;
166 foreach (const QByteArray &e, exts) all += ' ' + e;
167 qDebug() << "GL_EXTENSIONS: " << all.constData();
168
169 m_brokenMipmapSupport = version.contains(c: "Mesa 10.1") || version.contains(c: "Mesa 9.");
170 qDebug() << "Broken Mipmap: " << m_brokenMipmapSupport;
171
172 context.doneCurrent();
173 }
174#endif
175}
176
177QQuickView *tst_SceneGraph::createView(const QString &file, QWindow *parent, int x, int y, int w, int h)
178{
179 QQuickView *view = new QQuickView(parent);
180 view->setSource(testFileUrl(fileName: file));
181 if (x >= 0 && y >= 0) view->setPosition(posx: x, posy: y);
182 if (w >= 0 && h >= 0) view->resize(w, h);
183 view->show();
184 return view;
185}
186
187// Assumes the images are opaque white...
188bool containsSomethingOtherThanWhite(const QImage &image)
189{
190 QImage img;
191 if (image.format() != QImage::Format_ARGB32_Premultiplied
192 || image.format() != QImage::Format_RGB32)
193 img = image.convertToFormat(f: QImage::Format_ARGB32_Premultiplied);
194 else
195 img = image;
196
197 int w = img.width();
198 int h = img.height();
199 for (int y=0; y<h; ++y) {
200 const uint *pixels = (const uint *) img.constScanLine(y);
201 for (int x=0; x<w; ++x)
202 if (pixels[x] != 0xffffffff)
203 return true;
204 }
205 return false;
206}
207
208void tst_SceneGraph::manyWindows_data()
209{
210 QTest::addColumn<QString>(name: "file");
211 QTest::addColumn<bool>(name: "toplevel");
212 QTest::addColumn<bool>(name: "shared");
213
214 QTest::newRow(dataTag: "image,toplevel") << QStringLiteral("manyWindows_image.qml") << true << false;
215 QTest::newRow(dataTag: "image,subwindow") << QStringLiteral("manyWindows_image.qml") << false << false;
216 QTest::newRow(dataTag: "dftext,toplevel") << QStringLiteral("manyWindows_dftext.qml") << true << false;
217 QTest::newRow(dataTag: "dftext,subwindow") << QStringLiteral("manyWindows_dftext.qml") << false << false;
218 QTest::newRow(dataTag: "ntext,toplevel") << QStringLiteral("manyWindows_ntext.qml") << true << false;
219 QTest::newRow(dataTag: "ntext,subwindow") << QStringLiteral("manyWindows_ntext.qml") << false << false;
220 QTest::newRow(dataTag: "rects,toplevel") << QStringLiteral("manyWindows_rects.qml") << true << false;
221 QTest::newRow(dataTag: "rects,subwindow") << QStringLiteral("manyWindows_rects.qml") << false << false;
222
223 QTest::newRow(dataTag: "image,toplevel,sharing") << QStringLiteral("manyWindows_image.qml") << true << true;
224 QTest::newRow(dataTag: "image,subwindow,sharing") << QStringLiteral("manyWindows_image.qml") << false << true;
225 QTest::newRow(dataTag: "dftext,toplevel,sharing") << QStringLiteral("manyWindows_dftext.qml") << true << true;
226 QTest::newRow(dataTag: "dftext,subwindow,sharing") << QStringLiteral("manyWindows_dftext.qml") << false << true;
227 QTest::newRow(dataTag: "ntext,toplevel,sharing") << QStringLiteral("manyWindows_ntext.qml") << true << true;
228 QTest::newRow(dataTag: "ntext,subwindow,sharing") << QStringLiteral("manyWindows_ntext.qml") << false << true;
229 QTest::newRow(dataTag: "rects,toplevel,sharing") << QStringLiteral("manyWindows_rects.qml") << true << true;
230 QTest::newRow(dataTag: "rects,subwindow,sharing") << QStringLiteral("manyWindows_rects.qml") << false << true;
231}
232
233#if QT_CONFIG(opengl)
234struct ShareContextResetter {
235public:
236 ~ShareContextResetter() { qt_gl_set_global_share_context(context: nullptr); }
237};
238#endif
239
240void tst_SceneGraph::manyWindows()
241{
242 if ((QGuiApplication::platformName() == QLatin1String("offscreen"))
243 || (QGuiApplication::platformName() == QLatin1String("minimal")))
244 QSKIP("Skipping due to grabWindow not functional on offscreen/minimal platforms");
245
246 QFETCH(QString, file);
247 QFETCH(bool, toplevel);
248 QFETCH(bool, shared);
249#if QT_CONFIG(opengl)
250 QOpenGLContext sharedGLContext;
251 ShareContextResetter cleanup; // To avoid dangling pointer in case of test-failure.
252 if (shared) {
253 QVERIFY(sharedGLContext.create());
254 qt_gl_set_global_share_context(context: &sharedGLContext);
255 }
256#endif
257 QScopedPointer<QWindow> parent;
258 if (!toplevel) {
259 parent.reset(other: new QWindow());
260 parent->resize(w: 200, h: 200);
261 parent->show();
262 }
263
264 ScopedList <QQuickView *> views;
265
266 const int COUNT = 4;
267
268 QImage baseLine;
269 QString errorMessage;
270 for (int i=0; i<COUNT; ++i) {
271 views << createView(file, parent: parent.data(), x: (i % 2) * 100, y: (i / 2) * 100, w: 100, h: 100);
272 }
273 for (int i=0; i<COUNT; ++i) {
274 QQuickView *view = views.at(i);
275 QVERIFY(QTest::qWaitForWindowExposed(view));
276 QImage content = view->grabWindow();
277 if (i == 0) {
278 baseLine = content;
279 QVERIFY(containsSomethingOtherThanWhite(baseLine));
280 } else {
281 QVERIFY2(compareImages(content, baseLine, &errorMessage),
282 qPrintable(errorMessage));
283 }
284 }
285
286 // Wipe and recreate one (scope pointer delets it...)
287 delete views.takeLast();
288 QQuickView *last = createView(file, parent: parent.data(), x: 100, y: 100, w: 100, h: 100);
289 QVERIFY(QTest::qWaitForWindowExposed(last));
290 views << last;
291 QVERIFY2(compareImages(baseLine, last->grabWindow(), &errorMessage),
292 qPrintable(errorMessage));
293
294 // Wipe and recreate all
295 qDeleteAll(c: views);
296 views.clear();
297
298 for (int i=0; i<COUNT; ++i) {
299 views << createView(file, parent: parent.data(), x: (i % 2) * 100, y: (i / 2) * 100, w: 100, h: 100);
300 }
301 for (int i=0; i<COUNT; ++i) {
302 QQuickView *view = views.at(i);
303 QVERIFY(QTest::qWaitForWindowExposed(view));
304 QImage content = view->grabWindow();
305 QVERIFY2(compareImages(content, baseLine, &errorMessage),
306 qPrintable(errorMessage));
307 }
308}
309
310struct Sample {
311 Sample(int xx, int yy, qreal rr, qreal gg, qreal bb, qreal errorMargin = 0.05)
312 : x(xx)
313 , y(yy)
314 , r(rr)
315 , g(gg)
316 , b(bb)
317 , tolerance(errorMargin)
318 {
319 }
320 Sample(const Sample &o) : x(o.x), y(o.y), r(o.r), g(o.g), b(o.b), tolerance(o.tolerance) { }
321 Sample() : x(0), y(0), r(0), g(0), b(0), tolerance(0) { }
322
323 QString toString(const QImage &image) const {
324 QColor color(image.pixel(x,y));
325 return QString::fromLatin1(str: "pixel(%1,%2), rgb(%3,%4,%5), tolerance=%6 -- image(%7,%8,%9)")
326 .arg(a: x).arg(a: y)
327 .arg(a: r).arg(a: g).arg(a: b)
328 .arg(a: tolerance)
329 .arg(a: color.redF()).arg(a: color.greenF()).arg(a: color.blueF());
330 }
331
332 bool check(const QImage &image, qreal scale) {
333 QColor color(image.pixel(x: x * scale, y: y * scale));
334 return qAbs(t: color.redF() - r) <= tolerance
335 && qAbs(t: color.greenF() - g) <= tolerance
336 && qAbs(t: color.blueF() - b) <= tolerance;
337 }
338
339
340 int x, y;
341 qreal r, g, b;
342 qreal tolerance;
343};
344
345static Sample sample_from_regexp(QRegExp *re) {
346 return Sample(re->cap(nth: 1).toInt(),
347 re->cap(nth: 2).toInt(),
348 re->cap(nth: 3).toFloat(),
349 re->cap(nth: 4).toFloat(),
350 re->cap(nth: 5).toFloat(),
351 re->cap(nth: 6).toFloat()
352 );
353}
354
355Q_DECLARE_METATYPE(Sample);
356
357/*
358 The render() test implements a small test framework for itself with
359 the purpose of testing odds and ends of the scene graph
360 rendering. Each .qml file can consist of one or two stages. The
361 first stage is when the file is first displayed. The content is
362 grabbed and matched against 'base' samples defined in the .qml file
363 itself. The samples contain a pixel position, and RGB value and a
364 margin of error. The samples are defined in the .qml file so it is
365 easy to make the connection between colors and positions on the screen.
366
367 If the base stage samples all succeed, the test emits
368 'enterFinalStage' on the root item and waits for the .qml file to
369 update the value of 'finalStageComplete' The test can set this
370 directly or run an animation and set it later. Once the
371 'finalStageComplete' variable is true, we grab and match against the
372 second set of samples 'final'
373
374 The samples in the .qml file are defined in comments on the format:
375 #base: x y r g b error-tolerance
376 #final: x y r g b error-tolerance
377 - x and y are integers
378 - r g b are floats in the range of 0.0-1.0
379 - error-tolerance is a float in the range of 0.0-1.0
380
381 We also include a
382 #samples: count
383 to sanity check that all base/final samples were matched correctly
384 as the matching regexp is a bit crude.
385
386 To add new tests, add them to the 'files' list and put #base,
387 #final, #samples tags into the .qml file
388*/
389
390void tst_SceneGraph::render_data()
391{
392 QTest::addColumn<QString>(name: "file");
393 QTest::addColumn<QList<Sample> >(name: "baseStage");
394 QTest::addColumn<QList<Sample> >(name: "finalStage");
395
396 QList<QString> files;
397 files << "render_DrawSets.qml"
398 << "render_Overlap.qml"
399 << "render_MovingOverlap.qml"
400 << "render_BreakOpacityBatch.qml"
401 << "render_OutOfFloatRange.qml"
402 << "render_StackingOrder.qml"
403 << "render_ImageFiltering.qml"
404 << "render_bug37422.qml"
405 << "render_OpacityThroughBatchRoot.qml"
406 << "render_AlphaOverlapRebuild.qml";
407
408 if (!m_brokenMipmapSupport)
409 files << "render_Mipmap.qml";
410
411 QRegExp sampleCount("#samples: *(\\d+)");
412 // X:int Y:int R:float G:float B:float Error:float
413 QRegExp baseSamples("#base: *(\\d+) *(\\d+) *(\\d\\.\\d+) *(\\d\\.\\d+) *(\\d\\.\\d+) *(\\d\\.\\d+)");
414 QRegExp finalSamples("#final: *(\\d+) *(\\d+) *(\\d\\.\\d+) *(\\d\\.\\d+) *(\\d\\.\\d+) *(\\d\\.\\d+)");
415
416 foreach (QString fileName, files) {
417 QFile file(testFile(fileName));
418 if (!file.open(flags: QFile::ReadOnly)) {
419 qFatal(msg: "render_data: QFile::open failed! file=%s, error=%s",
420 qPrintable(fileName), qPrintable(file.errorString()));
421 }
422 QStringList contents = QString::fromLatin1(str: file.readAll()).split(sep: QLatin1Char('\n'));
423
424 int samples = -1;
425 foreach (QString line, contents) {
426 if (sampleCount.indexIn(str: line) >= 0) {
427 samples = sampleCount.cap(nth: 1).toInt();
428 break;
429 }
430 }
431 if (samples == -1)
432 qFatal(msg: "render_data: failed to find string '#samples: [count], file=%s", qPrintable(fileName));
433
434 QList<Sample> baseStage, finalStage;
435 foreach (QString line, contents) {
436 if (baseSamples.indexIn(str: line) >= 0)
437 baseStage << sample_from_regexp(re: &baseSamples);
438 else if (finalSamples.indexIn(str: line) >= 0)
439 finalStage << sample_from_regexp(re: &finalSamples);
440 }
441
442 if (baseStage.size() + finalStage.size() != samples)
443 qFatal(msg: "render_data: #samples does not add up to number of counted samples, file=%s", qPrintable(fileName));
444
445 QTest::newRow(qPrintable(fileName)) << fileName << baseStage << finalStage;
446 }
447}
448
449void tst_SceneGraph::render()
450{
451 if (!isRunningOnOpenGLDirectly() && !isRunningOnRhi())
452 QSKIP("Skipping complex rendering tests due to not running with OpenGL or QRhi");
453
454 QFETCH(QString, file);
455 QFETCH(QList<Sample>, baseStage);
456 QFETCH(QList<Sample>, finalStage);
457
458 QObject suite;
459 suite.setObjectName("The Suite");
460
461 QQuickView view;
462 view.rootContext()->setContextProperty("suite", &suite);
463 view.setSource(testFileUrl(fileName: file));
464 view.setResizeMode(QQuickView::SizeViewToRootObject);
465 view.show();
466 QVERIFY(QTest::qWaitForWindowExposed(&view));
467
468 // We're checking actual pixels, so scale up the sample point to the top-left of the
469 // 2x2 pixel block and hope that this is good enough. Ideally, view and content
470 // would be in identical coordinate space, meaning pixels, but we're not in an
471 // ideal world.
472 // Just keep this in mind when writing tests.
473 qreal scale = view.devicePixelRatio();
474
475 // Grab the window and check all our base stage samples
476 QImage content = view.grabWindow();
477 for (int i=0; i<baseStage.size(); ++i) {
478 Sample sample = baseStage.at(i);
479 QVERIFY2(sample.check(content, scale), qPrintable(sample.toString(content)));
480 }
481
482 // Put the qml file into the final stage and wait for it to
483 // complete it.
484 QQuickItem *rootItem = view.rootObject();
485 QMetaObject::invokeMethod(obj: rootItem, member: "enterFinalStage");
486 QTRY_VERIFY(rootItem->property("finalStageComplete").toBool());
487
488 // The grab the results and verify the samples in the end state.
489 content = view.grabWindow();
490 for (int i=0; i<finalStage.size(); ++i) {
491 Sample sample = finalStage.at(i);
492 QVERIFY2(sample.check(content, scale), qPrintable(sample.toString(content)));
493 }
494}
495
496#if QT_CONFIG(opengl)
497// Testcase for QTBUG-34898. We make another context current on another surface
498// in the GUI thread and hide the QQuickWindow while the other context is
499// current on the other window.
500void tst_SceneGraph::hideWithOtherContext()
501{
502 if (!isRunningOnOpenGLDirectly())
503 QSKIP("Skipping OpenGL context test due to not running with OpenGL");
504
505 QWindow window;
506 window.setSurfaceType(QWindow::OpenGLSurface);
507 window.resize(w: 100, h: 100);
508 window.create();
509 QOpenGLContext context;
510 QVERIFY(context.create());
511 bool renderingOnMainThread = false;
512
513 {
514 QQuickView view;
515 view.setSource(testFileUrl(fileName: "simple.qml"));
516 view.setResizeMode(QQuickView::SizeViewToRootObject);
517 view.show();
518 QVERIFY(QTest::qWaitForWindowExposed(&view));
519
520 renderingOnMainThread = view.openglContext()->thread() == QGuiApplication::instance()->thread();
521
522 // Make the local context current on the local window...
523 context.makeCurrent(surface: &window);
524 }
525
526 // The local context should no longer be the current one. It is not
527 // rebound because all well behaving Qt/OpenGL applications are
528 // required to makeCurrent their context again before making any
529 // GL calls to a new frame (see QOpenGLContext docs).
530 QVERIFY(!renderingOnMainThread || QOpenGLContext::currentContext() != &context);
531}
532#endif
533
534void tst_SceneGraph::createTextureFromImage_data()
535{
536 QImage rgba(64, 64, QImage::Format_ARGB32_Premultiplied);
537 QImage rgb(64, 64, QImage::Format_RGB32);
538
539 QTest::addColumn<QImage>(name: "image");
540 QTest::addColumn<uint>(name: "flags");
541 QTest::addColumn<bool>(name: "expectedAlpha");
542
543 QTest::newRow(dataTag: "rgb") << rgb << uint(0) << false;
544 QTest::newRow(dataTag: "argb") << rgba << uint(0) << true;
545 QTest::newRow(dataTag: "rgb,alpha") << rgb << uint(QQuickWindow::TextureHasAlphaChannel) << false;
546 QTest::newRow(dataTag: "argb,alpha") << rgba << uint(QQuickWindow::TextureHasAlphaChannel) << true;
547 QTest::newRow(dataTag: "rgb,!alpha") << rgb << uint(QQuickWindow::TextureIsOpaque) << false;
548 QTest::newRow(dataTag: "argb,!alpha") << rgba << uint(QQuickWindow::TextureIsOpaque) << false;
549}
550
551void tst_SceneGraph::createTextureFromImage()
552{
553 QFETCH(QImage, image);
554 QFETCH(uint, flags);
555 QFETCH(bool, expectedAlpha);
556
557 QQuickView view;
558 view.show();
559 QVERIFY(QTest::qWaitForWindowExposed(&view));
560 QTRY_VERIFY(view.isSceneGraphInitialized());
561
562 QScopedPointer<QSGTexture> texture(view.createTextureFromImage(image, options: (QQuickWindow::CreateTextureOptions) flags));
563 QCOMPARE(texture->hasAlphaChannel(), expectedAlpha);
564}
565
566bool tst_SceneGraph::isRunningOnOpenGLDirectly()
567{
568 static bool retval = false;
569 static bool decided = false;
570 if (!decided) {
571 decided = true;
572 QQuickView dummy;
573 dummy.show();
574 if (QTest::qWaitForWindowExposed(window: &dummy))
575 retval = dummy.rendererInterface()->graphicsApi() == QSGRendererInterface::OpenGL;
576 dummy.hide();
577 }
578 return retval;
579}
580
581bool tst_SceneGraph::isRunningOnRhi()
582{
583 static bool retval = false;
584 static bool decided = false;
585 if (!decided) {
586 decided = true;
587 QQuickView dummy;
588 dummy.show();
589 if (!QTest::qWaitForWindowExposed(window: &dummy)) {
590 [](){ QFAIL("Could not show a QQuickView"); }();
591 return false;
592 }
593 QSGRendererInterface::GraphicsApi api = dummy.rendererInterface()->graphicsApi();
594 retval = QSGRendererInterface::isApiRhiBased(api);
595 dummy.hide();
596 }
597 return retval;
598}
599
600#include "tst_scenegraph.moc"
601
602QTEST_MAIN(tst_SceneGraph)
603
604

source code of qtdeclarative/tests/auto/quick/scenegraph/tst_scenegraph.cpp