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_DrawCallData : public QQuick3DDataTest
{
Q_OBJECT
private slots:
void initTestCase() override;
void viewChurnReleasesDrawCallData();
void viewChurnReleasesShaderResourceBindings();
void resizeDoesNotGrowPipelineCache();
void resizeDoesNotGrowSrbCacheForQuadRenderer();
private:
#if QT_CONFIG(vulkan)
QVulkanInstance vulkanInstance;
#endif
};
void tst_DrawCallData::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
}
// Draw call data for rendered content is keyed on the pass objects of the
// layer it was rendered by. When a View3D is destroyed while its content
// lives on, e.g. a shared import scene, the entries keyed on the destroyed
// layer's passes must be released; otherwise every destroyed view leaks its
// draw call data (uniform buffers and cached srbs) for as long as the content
// is alive.
void tst_DrawCallData::viewChurnReleasesDrawCallData()
{
QQuick3DTestOffscreenRenderer renderer;
void *vulkanInstancePtr = nullptr;
#if QT_CONFIG(vulkan)
vulkanInstancePtr = &vulkanInstance;
#endif
QVERIFY(renderer.init(testFileUrl(QString::fromLatin1("viewChurn.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 setViewLoadedAndRenderFrames = [&](bool loaded) {
renderer.rootItem->setProperty("loadView", loaded);
// The view is destroyed on unload during the first sync; the layer and
// its render data are queued for cleanup and deleted during the next
// one, so render a few frames to let the deferred cleanup run.
for (int frame = 0; frame < 3; ++frame)
renderer.renderNextFrame();
};
const qsizetype loadedSize = rhiCtxD->m_drawCallData.size();
QVERIFY(loadedSize > 0);
setViewLoadedAndRenderFrames(false);
const qsizetype unloadedSize = rhiCtxD->m_drawCallData.size();
constexpr int ChurnCount = 5;
for (int i = 0; i < ChurnCount; ++i) {
setViewLoadedAndRenderFrames(true);
setViewLoadedAndRenderFrames(false);
}
// Without the per-pass cleanup each cycle leaks the draw call data of the
// shared content; with it the size returns to the unloaded baseline.
const qsizetype finalUnloadedSize = rhiCtxD->m_drawCallData.size();
QVERIFY2(finalUnloadedSize <= unloadedSize,
qPrintable(QString::fromLatin1("m_drawCallData grew from %1 to %2 entries over %3 view load/unload cycles")
.arg(unloadedSize).arg(finalUnloadedSize).arg(ChurnCount)));
// And loading again must not accumulate on top of leaked entries either.
setViewLoadedAndRenderFrames(true);
const qsizetype reloadedSize = rhiCtxD->m_drawCallData.size();
QVERIFY2(reloadedSize <= loadedSize,
qPrintable(QString::fromLatin1("m_drawCallData grew from %1 to %2 entries after view churn")
.arg(loadedSize).arg(reloadedSize)));
}
// The depth pre-pass, shadow, normal and user passes build their srbs around the
// uniform buffer of their draw call data. Unless the srb is owned by the draw call
// data, releasing the draw call data with a destroyed view leaves the srb in the
// cache, still referencing the deleted uniform buffer, so the cache grows with
// every destroyed view (or window) for as long as the content is alive.
void tst_DrawCallData::viewChurnReleasesShaderResourceBindings()
{
QQuick3DTestOffscreenRenderer renderer;
void *vulkanInstancePtr = nullptr;
#if QT_CONFIG(vulkan)
vulkanInstancePtr = &vulkanInstance;
#endif
QVERIFY(renderer.init(testFileUrl(QString::fromLatin1("srbChurn.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 setViewLoadedAndRenderFrames = [&](bool loaded) {
renderer.rootItem->setProperty("loadView", loaded);
// See viewChurnReleasesDrawCallData() for why more than one frame is needed.
for (int frame = 0; frame < 3; ++frame)
renderer.renderNextFrame();
};
const qsizetype loadedSize = rhiCtxD->m_srbCache.size();
QVERIFY(loadedSize > 0);
setViewLoadedAndRenderFrames(false);
const qsizetype unloadedSize = rhiCtxD->m_srbCache.size();
constexpr int ChurnCount = 5;
for (int i = 0; i < ChurnCount; ++i) {
setViewLoadedAndRenderFrames(true);
setViewLoadedAndRenderFrames(false);
}
const qsizetype finalUnloadedSize = rhiCtxD->m_srbCache.size();
QVERIFY2(finalUnloadedSize <= unloadedSize,
qPrintable(QString::fromLatin1("m_srbCache grew from %1 to %2 entries over %3 view load/unload cycles")
.arg(unloadedSize).arg(finalUnloadedSize).arg(ChurnCount)));
setViewLoadedAndRenderFrames(true);
const qsizetype reloadedSize = rhiCtxD->m_srbCache.size();
QVERIFY2(reloadedSize <= loadedSize,
qPrintable(QString::fromLatin1("m_srbCache grew from %1 to %2 entries after view churn")
.arg(loadedSize).arg(reloadedSize)));
}
// The viewport and scissor rects are dynamic state and must not take part in the
// graphics pipeline cache key. When they did, every window resize created a new set
// of pipelines (a full program link on OpenGL) that stayed in the cache forever.
void tst_DrawCallData::resizeDoesNotGrowPipelineCache()
{
QQuick3DTestOffscreenRenderer renderer;
void *vulkanInstancePtr = nullptr;
#if QT_CONFIG(vulkan)
vulkanInstancePtr = &vulkanInstance;
#endif
QVERIFY(renderer.init(testFileUrl(QString::fromLatin1("viewChurn.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 qsizetype baselinePipelines = rhiCtxD->m_pipelines.size();
QVERIFY(baselinePipelines > 0);
constexpr int ResizeCount = 20;
for (int i = 0; i < ResizeCount; ++i) {
QVERIFY(renderer.resize(QSize(500 + i * 7, 400 + i * 5)));
renderer.renderNextFrame();
}
const qsizetype finalPipelines = rhiCtxD->m_pipelines.size();
QVERIFY2(finalPipelines <= baselinePipelines,
qPrintable(QString::fromLatin1("m_pipelines grew from %1 to %2 entries over %3 resizes")
.arg(baselinePipelines).arg(finalPipelines).arg(ResizeCount)));
}
// The SimpleQuadRenderer binds a texture to output, which is recreated when the view is resized.
// The srb built for the previous texture must therefore be released then, otherwise every resize
// leaves an srb in the cache that references a deleted texture.
void tst_DrawCallData::resizeDoesNotGrowSrbCacheForQuadRenderer()
{
QQuick3DTestOffscreenRenderer renderer;
void *vulkanInstancePtr = nullptr;
#if QT_CONFIG(vulkan)
vulkanInstancePtr = &vulkanInstance;
#endif
QVERIFY(renderer.init(testFileUrl(QString::fromLatin1("quadResize.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();
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 qsizetype baselineSrbs = rhiCtxD->m_srbCache.size();
QVERIFY(baselineSrbs > 0);
constexpr int ResizeCount = 10;
for (int i = 0; i < ResizeCount; ++i) {
QVERIFY(renderer.resize(QSize(500 + i * 7, 400 + i * 5)));
renderer.renderNextFrame();
renderer.renderNextFrame();
}
const qsizetype finalSrbs = rhiCtxD->m_srbCache.size();
QVERIFY2(finalSrbs <= baselineSrbs,
qPrintable(QString::fromLatin1("m_srbCache grew from %1 to %2 entries over %3 resizes")
.arg(baselineSrbs).arg(finalSrbs).arg(ResizeCount)));
}
QTEST_MAIN(tst_DrawCallData)
#include "tst_drawcalldata.moc"