Revision control
Copy as Markdown
// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QTest>
#include <QQuickView>
#include <QQuickItem>
#include <private/qquick3dscenemanager_p.h>
#include <private/qquick3dobject_p.h>
#include <QtQuick3DRuntimeRender/private/qssgrenderroot_p.h>
#include "../shared/util.h"
class tst_ImportSceneLifetime : public QQuick3DDataTest
{
Q_OBJECT
private slots:
void initTestCase() override;
void sharedSceneSurvivesViewRecreation();
void sharedSceneReQueuedAfterHomeViewDestroyed();
};
void tst_ImportSceneLifetime::initTestCase()
{
QQuick3DDataTest::initTestCase();
if (!initialized())
return;
}
// A scene imported into a View3D is owned by the QML tree, not the view, so it can
// outlive the view that imports it. The view's render root lives on the per-window
// QQuick3DWindowAttachment, and imported nodes hold a back-reference to that root
// through rootNodeRef. If the attachment (and its root) is destroyed when the last
// view in a window goes away, that back-reference dangles, and recreating a view that
// imports the same scene reads the freed root in setImportScene() -> a use-after-free.
//
// Destroy the importing view while the window (and the shared scene) live on, and
// verify the attachment and its render root are kept alive and reused. This is checked
// by pointer identity only, the freed root is never dereferenced, so it is a
// deterministic check rather than relying on hitting the use-after-free read.
void tst_ImportSceneLifetime::sharedSceneSurvivesViewRecreation()
{
QScopedPointer<QQuickView> view(createView(QStringLiteral("importscenelifetime.qml"), QSize(200, 200)));
QVERIFY(view);
QVERIFY(QTest::qWaitForWindowExposed(view.data()));
QObject *root = view->rootObject();
QVERIFY(root);
QVERIFY(waitForFrames(view.data(), 2));
// The view is present and importing the shared scene; capture the window attachment
// and the render root the shared scene's nodes point at.
auto *attachment = QQuick3DSceneManager::getOrSetWindowAttachment(*view);
QVERIFY(attachment);
const QSSGRenderRoot *rootBefore = attachment->rootNode();
QVERIFY(rootBefore);
// Destroy the importing view. The shared scene lives on, so the attachment and its
// root must not be torn down while the window is alive.
QVERIFY(root->setProperty("loadView", false));
QVERIFY(waitForFrames(view.data(), 3));
QCoreApplication::processEvents();
QVERIFY(waitForFrames(view.data(), 1));
// Same window -> same attachment and same render root. getOrSetWindowAttachment()
// would hand back a fresh attachment (with a new root) had the old one been deleted.
// Pointer comparison only; the old root is never dereferenced.
auto *attachmentAfter = QQuick3DSceneManager::getOrSetWindowAttachment(*view);
// Compare as raw pointers: a freed attachment must not be dereferenced (which QCOMPARE's
// QObject formatting would do) when reporting a mismatch.
QCOMPARE(static_cast<const void *>(attachmentAfter), static_cast<const void *>(attachment));
QCOMPARE(attachmentAfter->rootNode(), rootBefore);
// Recreate the view importing the same shared scene; setImportScene() runs again and
// must read a still-valid root. Reaching here without crashing (ASan) is part of the
// assertion.
QVERIFY(root->setProperty("loadView", true));
QVERIFY(waitForFrames(view.data(), 2));
QCOMPARE(QQuick3DSceneManager::getOrSetWindowAttachment(*view)->rootNode(), rootBefore);
}
// Test and verify the re-scheduling contract that keeps a shared imported scene alive when the transient view is destroyed.
// If that contract breaks, the shared scene is orphaned (sceneManager == null) and this will fail.
// See comments in the test body for details.
void tst_ImportSceneLifetime::sharedSceneReQueuedAfterHomeViewDestroyed()
{
QScopedPointer<QQuickView> view(createView(QStringLiteral("sharedimportreadopt.qml"), QSize(400, 200)));
QVERIFY(view);
QVERIFY(QTest::qWaitForWindowExposed(view.data()));
QObject *root = view->rootObject();
QVERIFY(root);
QVERIFY(waitForFrames(view.data(), 2));
auto *sharedModel = root->findChild<QQuick3DObject *>(QStringLiteral("sharedModel"));
QVERIFY(sharedModel);
auto *transientViewCamera = root->findChild<QQuick3DObject *>(QStringLiteral("transientViewCamera"));
QVERIFY(transientViewCamera);
auto *staticViewCamera = root->findChild<QQuick3DObject *>(QStringLiteral("staticViewCamera"));
QVERIFY(staticViewCamera);
auto *modelPriv = QQuick3DObjectPrivate::get(sharedModel);
// A camera's scene manager is its View3D's scene manager.
QQuick3DSceneManager *transientSM = QQuick3DObjectPrivate::get(transientViewCamera)->sceneManager;
QQuick3DSceneManager *staticSM = QQuick3DObjectPrivate::get(staticViewCamera)->sceneManager;
QVERIFY(transientSM);
QVERIFY(staticSM);
QVERIFY(transientSM != staticSM);
// Only the transient view imports the shared scene so far, so its objects are adopted by it.
QVERIFY(modelPriv->sceneManager);
QCOMPARE(static_cast<const void *>(modelPriv->sceneManager.data()),
static_cast<const void *>(transientSM));
// The static view now imports it too. The transient stays as the main caretaker.
QVERIFY(root->setProperty("staticViewImports", true));
QVERIFY(waitForFrames(view.data(), 2));
QCOMPARE(static_cast<const void *>(modelPriv->sceneManager.data()),
static_cast<const void *>(transientSM));
// Destroy the transient view. Its scene manager's destroyed() must drive
// updateSceneManagerForImportScene() on the surviving view and re-adopt the shared scene.
QVERIFY(root->setProperty("loadTransientView", false));
QVERIFY(waitForFrames(view.data(), 3));
QCoreApplication::processEvents();
QVERIFY(waitForFrames(view.data(), 1));
// The shared scene must be re-homed on the surviving view rather than left orphaned. If the
// destroyed()->updateSceneManagerForImportScene contract breaks, sceneManager stays null here.
// (void* comparison so a failure does not dereference a possibly-freed manager when formatting).
QVERIFY(modelPriv->sceneManager);
QCOMPARE(static_cast<const void *>(modelPriv->sceneManager.data()),
static_cast<const void *>(staticSM));
}
QTEST_MAIN(tst_ImportSceneLifetime)
#include "tst_importscenelifetime.moc"