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 <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();
void skyMaterialSwapDoesNotGrowDrawCallData();
private:
void runSwapTest(const QString &qmlFile);
#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()
{
runSwapTest(QString::fromLatin1("materialSwap.qml"));
}
void tst_MaterialSwap::skyMaterialSwapDoesNotGrowDrawCallData()
{
runSwapTest(QString::fromLatin1("skyMaterialSwap.qml"));
}
void tst_MaterialSwap::runSwapTest(const QString &qmlFile)
{
QQuick3DTestOffscreenRenderer renderer;
void *vulkanInstancePtr = nullptr;
#if QT_CONFIG(vulkan)
vulkanInstancePtr = &vulkanInstance;
#endif
QVERIFY(renderer.init(testFileUrl(qmlFile), 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"