Revision control

+ // Copyright (C) 2026 The Qt Company Ltd.
+ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+ #include <QTest>
+
+ #include <private/qquick3dviewport_p.h>
+ #include <ssg/qssgrendercontextcore.h>
+ #include <QtQuick3DRuntimeRender/private/qssgrhicontext_p.h>
+
+ #if QT_CONFIG(vulkan)
+ #include <QVulkanInstance>
+ #endif
+
+ #include "../shared/util.h"
+
+ class tst_MaterialSwap : public QQuick3DDataTest
+ {
+ Q_OBJECT
+
+ private slots:
+ void initTestCase() override;
+ void swapDoesNotGrowDrawCallData();
+
+ private:
+ #if QT_CONFIG(vulkan)
+ QVulkanInstance vulkanInstance;
+ #endif
+ };
+
+ void tst_MaterialSwap::initTestCase()
+ {
+ QQuick3DDataTest::initTestCase();
+ if (!initialized())
+ return;
+
+ #if QT_CONFIG(vulkan)
+ vulkanInstance.setLayers({ "VK_LAYER_LUNARG_standard_validation" });
+ vulkanInstance.create(); // may fail, which is fine if Vulkan is not used in the first place
+ #endif
+ }
+
+ void tst_MaterialSwap::swapDoesNotGrowDrawCallData()
+ {
+ QQuick3DTestOffscreenRenderer renderer;
+ void *vulkanInstancePtr = nullptr;
+ #if QT_CONFIG(vulkan)
+ vulkanInstancePtr = &vulkanInstance;
+ #endif
+ QVERIFY(renderer.init(testFileUrl(QString::fromLatin1("materialSwap.qml")), vulkanInstancePtr));
+
+ #ifdef Q_OS_MACOS
+ if (renderer.quickWindow->rendererInterface()->graphicsApi() == QSGRendererInterface::OpenGL)
+ QSKIP("Skipping test due to software OpenGL renderer problems on macOS");
+ #endif
+
+ renderer.renderNextFrame();
+
+ const auto &context = QQuick3DSceneManager::getOrSetWindowAttachment(*renderer.quickWindow)->rci();
+ QVERIFY(context);
+
+ const auto &rhiContext = context->rhiContext();
+ QVERIFY(rhiContext);
+ const QSSGRhiContextPrivate *rhiCtxD = QSSGRhiContextPrivate::get(rhiContext.get());
+
+ const auto swapAndRenderFrame = [&](bool swapped) {
+ renderer.rootItem->setProperty("swapped", swapped);
+ renderer.renderNextFrame();
+ };
+
+ // Warm up so both materials have been through creation, destruction and
+ // re-creation at least once; the sizes after this represent the expected
+ // steady state (entries for the currently assigned material, plus at most
+ // one swap's worth of not-yet-cleaned-up entries).
+ for (int i = 1; i <= 4; ++i)
+ swapAndRenderFrame(i % 2 != 0);
+
+ const qsizetype baselineDrawCallData = rhiCtxD->m_drawCallData.size();
+ const qsizetype baselineSrbCache = rhiCtxD->m_srbCache.size();
+ QVERIFY(baselineDrawCallData > 0);
+
+ constexpr int SwapCount = 20;
+ for (int i = 1; i <= SwapCount; ++i)
+ swapAndRenderFrame(i % 2 == 0);
+
+ // Without the cleanup of material-keyed draw call data the caches grow by
+ // at least one entry per swap; with it the sizes stay at the steady state.
+ // Allow a little slack so the test does not depend on the exact frame at
+ // which the deferred cleanup of the previous material runs.
+ constexpr qsizetype Slack = 4;
+ const qsizetype finalDrawCallData = rhiCtxD->m_drawCallData.size();
+ const qsizetype finalSrbCache = rhiCtxD->m_srbCache.size();
+ QVERIFY2(finalDrawCallData <= baselineDrawCallData + Slack,
+ qPrintable(QString::fromLatin1("m_drawCallData grew from %1 to %2 entries over %3 material swaps")
+ .arg(baselineDrawCallData).arg(finalDrawCallData).arg(SwapCount)));
+ QVERIFY2(finalSrbCache <= baselineSrbCache + Slack,
+ qPrintable(QString::fromLatin1("m_srbCache grew from %1 to %2 entries over %3 material swaps")
+ .arg(baselineSrbCache).arg(finalSrbCache).arg(SwapCount)));
+ }
+
+ QTEST_MAIN(tst_MaterialSwap)
+ #include "tst_materialswap.moc"