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 <algorithm>
#include <QtQuick3D/private/qquick3dnode_p.h>
#include <QtQuick3D/private/qquick3dobject_p.h>
#include <QtQuick3D/private/qquick3dcontentlayer_p.h>
#include <QtQuick3DRuntimeRender/private/qssgrendernode_p.h>
#include "../shared/util.h"
// Regression test for particles ignoring the content-layer (Node::layers)
// camera filter. The layers property is not inherited, and the render nodes
// that represent particles are produced by internal update nodes parented to
// the system; the system value has to be forwarded to them explicitly. User
// content instantiated from a delegate is not touched by the system and
// keeps its own layers assignment.
class tst_ParticleLayers : public QQuick3DDataTest
{
Q_OBJECT
private slots:
void initTestCase() override;
void systemLayersReachParticleRenderNodes();
void layersChangePropagatesDynamically();
private:
using Layer = QQuick3DContentLayer;
QQuickView *load();
static QQuick3DNode *system(QObject *root);
static void collectTags(const QSSGRenderNode *node, QSSGRenderGraphObject::Type type, QList<quint32> &out);
static QList<quint32> tagsForType(QQuick3DNode *system, QSSGRenderGraphObject::Type type);
void verifyLayerTags(QQuick3DNode *system, quint32 expected);
};
void tst_ParticleLayers::initTestCase()
{
QQuick3DDataTest::initTestCase();
if (!initialized())
return;
}
QQuickView *tst_ParticleLayers::load()
{
QQuickView *view = createView(QStringLiteral("particlelayers.qml"), QSize(200, 200));
if (!view)
return nullptr;
if (!QTest::qWaitForWindowExposed(view)) {
delete view;
return nullptr;
}
// Let the system emit and the render nodes sync over a few frames.
waitForFrames(view, 3);
return view;
}
QQuick3DNode *tst_ParticleLayers::system(QObject *root)
{
return root->findChild<QQuick3DNode *>(QStringLiteral("psystem"));
}
void tst_ParticleLayers::collectTags(const QSSGRenderNode *node, QSSGRenderGraphObject::Type type, QList<quint32> &out)
{
if (!node)
return;
if (node->type == type)
out.append(node->tag.value());
for (const auto &child : node->children)
collectTags(&child, type, out);
}
QList<quint32> tst_ParticleLayers::tagsForType(QQuick3DNode *system, QSSGRenderGraphObject::Type type)
{
QList<quint32> tags;
if (!system)
return tags;
auto *sysNode = static_cast<QSSGRenderNode *>(QQuick3DObjectPrivate::get(system)->spatialNode);
collectTags(sysNode, type, tags);
return tags;
}
// Checks both kinds of renderables the systems' particles produce: the
// Particles node of the sprite particle follows the system's layers, while
// the Model nodes of the compound ModelParticle3D delegate are user content
// and keep their own assignment (one explicit, one the default), no matter
// what the system's layers value is.
void tst_ParticleLayers::verifyLayerTags(QQuick3DNode *system, quint32 expected)
{
QList<quint32> particleTags;
QList<quint32> modelTags;
// The nodes appear once the emitter has produced particles; give it a
// couple of extra frames if needed.
QTRY_VERIFY(!(particleTags = tagsForType(system, QSSGRenderGraphObject::Type::Particles)).isEmpty());
QTRY_VERIFY((modelTags = tagsForType(system, QSSGRenderGraphObject::Type::Model)).size() >= 2);
for (quint32 tag : std::as_const(particleTags))
QCOMPARE(tag, expected);
std::sort(modelTags.begin(), modelTags.end());
const QList<quint32> expectedModelTags { quint32(Layer::Layer0), quint32(Layer::Layer5) };
QCOMPARE(modelTags, expectedModelTags);
}
// The particle render nodes carry the system's layer mask, not the default
// Layer0, so the camera layer filter can include or exclude them.
void tst_ParticleLayers::systemLayersReachParticleRenderNodes()
{
QScopedPointer<QQuickView> view(load());
QVERIFY(view);
QQuick3DNode *psystem = system(view->rootObject());
QVERIFY(psystem);
QCOMPARE(psystem->layers(), int(Layer::Layer2));
verifyLayerTags(psystem, quint32(Layer::Layer2));
}
// Changing the system's layers at runtime re-stamps the particle render nodes.
void tst_ParticleLayers::layersChangePropagatesDynamically()
{
QScopedPointer<QQuickView> view(load());
QVERIFY(view);
QQuick3DNode *psystem = system(view->rootObject());
QVERIFY(psystem);
verifyLayerTags(psystem, quint32(Layer::Layer2));
if (QTest::currentTestFailed())
return;
psystem->setLayers(int(Layer::Layer3));
QCoreApplication::processEvents();
QVERIFY(waitForFrames(view.data(), 2));
verifyLayerTags(psystem, quint32(Layer::Layer3));
}
QTEST_MAIN(tst_ParticleLayers)
#include "tst_particlelayers.moc"