qtdeclarative/tests/auto/quick/rendernode/tst_rendernode.cpp

411 lines
12 KiB
C++
Raw Normal View History

// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <qtest.h>
#include <QtQuick/qquickitem.h>
#include <QtQuick/qquickview.h>
#include <private/qsgrendernode_p.h>
#include <QtGui/private/qrhi_p.h>
#include <QtQuickTestUtils/private/qmlutils_p.h>
#if QT_CONFIG(opengl)
#include <QOpenGLContext>
#include <QOpenGLFunctions>
#endif
class tst_rendernode: public QQmlDataTest
{
Q_OBJECT
public:
tst_rendernode();
private slots:
void test_data();
void test();
#if QT_CONFIG(opengl)
void gltest_data();
void gltest();
#endif
Add the graphics api independent scenegraph port Opt in via environment variables: QSG_RHI=1 -> enable using QRhi instead of GL QSG_RHI_BACKEND -> set to vulkan, metal, d3d11, gl to override the default (the default is d3d11 on Windows, metal on Mac, gl elsewhere) Or force a given rhi backend via the existing QQuickWindow::setSceneGraphBackend(). Otherwise the default behavior is the same as before, the rhi code path is never active by default. -no-opengl builds are supported in the sense that they work and default to the software backend. However, the rhi code path cannot currently be used in such builds, even though QRhi from qtbase is fully functional with Vulkan, D3D, or Metal even when qtbase was configured with -no-opengl. This cannot be utilized by Quick atm due to OpenGL usage being all over the place in the sources corresponding to the default backend, and those host the rhi code path as well. This will be cleaned up hopefully in Qt 6, with the removal all direct OpenGL usage. Other env.vars.: QSG_RHI_DEBUG_LAYER=1 -> enable D3D debug or Vulkan validation layer (assuming the system is set up for this) QSG_RHI_SHADEREFFECT_DEBUG=1 -> print stuff from ShaderEffect QSG_SAMPLES=1,2,4,... -> MSAA sample count (but QSurfaceFormat works too) QT_D3D_ADAPTER_INDEX=0,1,... -> D3D adapter index QT_VK_PHYSICAL_DEVICE_INDEX=0,1,... -> Vulkan physical device index QSG_RHI_UINT32_INDEX=1 -> always use uint index data (both merged/unmerged, convert when needed - with some rhi backends this is implicit) QSG_RENDER_LOOP -> to override the render loop as usual. The default with RHI is threaded for Metal, threaded for Vulkan on Windows, basic for Vulkan on Linux and Android (to be checked later), while the existing rules apply for OpenGL. Not supported when running with QRhi: - particles - compressed atlases (though this is transparent to the apps) - QSGRenderNode - QQuickRenderControl - QQuickFramebufferObject - certain QQuickWindow functionality that depends directly on OpenGL - anisotropic filtering for textures - native text may lack some gamma correction - QSGEngine applicability unclear - some QML profiler logs may be incorrect or irrelevant Change-Id: I7822e99ad79e342e4166275da6e9e66498d76521 Reviewed-by: Lars Knoll <lars.knoll@qt.io>
2019-04-23 07:40:59 +00:00
private:
QQuickView *createView(const QString &file, QWindow *parent, int x, int y, int w, int h);
Add the graphics api independent scenegraph port Opt in via environment variables: QSG_RHI=1 -> enable using QRhi instead of GL QSG_RHI_BACKEND -> set to vulkan, metal, d3d11, gl to override the default (the default is d3d11 on Windows, metal on Mac, gl elsewhere) Or force a given rhi backend via the existing QQuickWindow::setSceneGraphBackend(). Otherwise the default behavior is the same as before, the rhi code path is never active by default. -no-opengl builds are supported in the sense that they work and default to the software backend. However, the rhi code path cannot currently be used in such builds, even though QRhi from qtbase is fully functional with Vulkan, D3D, or Metal even when qtbase was configured with -no-opengl. This cannot be utilized by Quick atm due to OpenGL usage being all over the place in the sources corresponding to the default backend, and those host the rhi code path as well. This will be cleaned up hopefully in Qt 6, with the removal all direct OpenGL usage. Other env.vars.: QSG_RHI_DEBUG_LAYER=1 -> enable D3D debug or Vulkan validation layer (assuming the system is set up for this) QSG_RHI_SHADEREFFECT_DEBUG=1 -> print stuff from ShaderEffect QSG_SAMPLES=1,2,4,... -> MSAA sample count (but QSurfaceFormat works too) QT_D3D_ADAPTER_INDEX=0,1,... -> D3D adapter index QT_VK_PHYSICAL_DEVICE_INDEX=0,1,... -> Vulkan physical device index QSG_RHI_UINT32_INDEX=1 -> always use uint index data (both merged/unmerged, convert when needed - with some rhi backends this is implicit) QSG_RENDER_LOOP -> to override the render loop as usual. The default with RHI is threaded for Metal, threaded for Vulkan on Windows, basic for Vulkan on Linux and Android (to be checked later), while the existing rules apply for OpenGL. Not supported when running with QRhi: - particles - compressed atlases (though this is transparent to the apps) - QSGRenderNode - QQuickRenderControl - QQuickFramebufferObject - certain QQuickWindow functionality that depends directly on OpenGL - anisotropic filtering for textures - native text may lack some gamma correction - QSGEngine applicability unclear - some QML profiler logs may be incorrect or irrelevant Change-Id: I7822e99ad79e342e4166275da6e9e66498d76521 Reviewed-by: Lars Knoll <lars.knoll@qt.io>
2019-04-23 07:40:59 +00:00
bool isRunningOnRhi() const;
};
static QShader getShader(const QString &name)
{
QFile f(name);
if (f.open(QIODevice::ReadOnly))
return QShader::fromSerialized(f.readAll());
return QShader();
}
// *****
// * *
// * *
// *
// assumes the top-left scenegraph coordinate system, will be scaled to the item size
static float vertexData[] = {
0, 0, 0, 0, 1, // blue
0, 1, 0, 0, 1,
1, 0, 0, 0, 1
};
class SimpleNode : public QSGRenderNode
{
public:
SimpleNode(QQuickWindow *window)
: m_window(window)
{
}
StateFlags changedStates() const override
{
return ViewportState; // nothing else matters in Qt 6
}
RenderingFlags flags() const override
{
// this node uses QRhi directly and is also well behaving depth-wise
return NoExternalRendering | DepthAwareRendering;
}
void prepare() override
{
QSGRendererInterface *rif = m_window->rendererInterface();
QRhi *rhi = static_cast<QRhi *>(rif->getResource(m_window, QSGRendererInterface::RhiResource));
QVERIFY(rhi);
QSGRenderNodePrivate *d = QSGRenderNodePrivate::get(this);
QRhiRenderTarget *rt = d->m_rt.rt;
QRhiCommandBuffer *cb = d->m_rt.cb;
QRhiResourceUpdateBatch *u = rhi->nextResourceUpdateBatch();
if (!m_vbuf) {
m_vbuf.reset(rhi->newBuffer(QRhiBuffer::Immutable, QRhiBuffer::VertexBuffer, sizeof(vertexData)));
QVERIFY(m_vbuf->create());
u->uploadStaticBuffer(m_vbuf.data(), vertexData);
}
if (!m_ubuf) {
m_ubuf.reset(rhi->newBuffer(QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, 68));
QVERIFY(m_ubuf->create());
}
if (!m_srb) {
m_srb.reset(rhi->newShaderResourceBindings());
m_srb->setBindings({
QRhiShaderResourceBinding::uniformBuffer(0,
QRhiShaderResourceBinding::VertexStage | QRhiShaderResourceBinding::FragmentStage,
m_ubuf.get())
});
m_srb->create();
}
if (!m_ps) {
m_ps.reset(rhi->newGraphicsPipeline());
const QShader vs = getShader(QLatin1String(":/shaders/color.vert.qsb"));
if (!vs.isValid())
qFatal("Failed to load shader pack (vertex)");
const QShader fs = getShader(QLatin1String(":/shaders/color.frag.qsb"));
if (!fs.isValid())
qFatal("Failed to load shader pack (fragment)");
m_ps->setShaderStages({
{ QRhiShaderStage::Vertex, vs },
{ QRhiShaderStage::Fragment, fs }
});
m_ps->setCullMode(QRhiGraphicsPipeline::Back);
// important to test against what's already in the depth buffer from the opaque pass
m_ps->setDepthTest(true);
m_ps->setDepthOp(QRhiGraphicsPipeline::LessOrEqual);
// we are in the alpha pass always so not writing out the depth
m_ps->setDepthWrite(false);
QRhiVertexInputLayout inputLayout;
inputLayout.setBindings({
{ 5 * sizeof(float) }
});
inputLayout.setAttributes({
{ 0, 0, QRhiVertexInputAttribute::Float2, 0 },
{ 0, 1, QRhiVertexInputAttribute::Float3, 2 * sizeof(float) }
});
m_ps->setVertexInputLayout(inputLayout);
m_ps->setShaderResourceBindings(m_srb.data());
m_ps->setRenderPassDescriptor(rt->renderPassDescriptor());
QVERIFY(m_ps->create());
}
// follow what the scenegraph tells us, hence DepthAwareRendering from
// flags, the downside is that we are stuck with the scenegraph
// coordinate system but that's enough for this test.
QMatrix4x4 mvp = *projectionMatrix() * *matrix();
mvp.scale(m_itemSize.width(), m_itemSize.height());
u->updateDynamicBuffer(m_ubuf.data(), 0, 64, mvp.constData());
const float opacity = inheritedOpacity();
u->updateDynamicBuffer(m_ubuf.data(), 64, 4, &opacity);
cb->resourceUpdate(u);
}
void render(const RenderState *) override
{
QSGRenderNodePrivate *d = QSGRenderNodePrivate::get(this);
QRhiRenderTarget *rt = d->m_rt.rt;
QRhiCommandBuffer *cb = d->m_rt.cb;
cb->setGraphicsPipeline(m_ps.data());
cb->setViewport({ 0, 0, float(rt->pixelSize().width()), float(rt->pixelSize().height()) });
cb->setShaderResources();
const QRhiCommandBuffer::VertexInput vbufBinding(m_vbuf.get(), 0);
cb->setVertexInput(0, 1, &vbufBinding);
cb->draw(3);
}
QQuickWindow *m_window;
QScopedPointer<QRhiBuffer> m_vbuf;
QScopedPointer<QRhiBuffer> m_ubuf;
QScopedPointer<QRhiShaderResourceBindings> m_srb;
QScopedPointer<QRhiGraphicsPipeline> m_ps;
QSizeF m_itemSize;
};
class SimpleItem : public QQuickItem
{
Q_OBJECT
public:
SimpleItem()
{
setFlag(ItemHasContents, true);
}
protected:
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override
{
if (size().isEmpty()) {
delete oldNode;
return nullptr;
}
SimpleNode *node = static_cast<SimpleNode *>(oldNode);
if (!node)
node = new SimpleNode(window());
node->m_itemSize = size();
return node;
}
};
#if QT_CONFIG(opengl)
class GLNode : public QSGRenderNode
{
public:
StateFlags changedStates() const override
{
return ViewportState; // nothing else matters in Qt 6
}
RenderingFlags flags() const override
{
return {};
}
void prepare() override
{
QVERIFY(QOpenGLContext::currentContext());
}
void render(const RenderState *) override
{
QVERIFY(QOpenGLContext::currentContext());
QOpenGLFunctions *f = QOpenGLContext::currentContext()->functions();
QVERIFY(f);
f->glClearColor(0.0f, 1.0f, 0.0f, 1.0f);
f->glClear(GL_COLOR_BUFFER_BIT);
}
};
class GLItem : public QQuickItem
{
Q_OBJECT
public:
GLItem()
{
setFlag(ItemHasContents, true);
}
protected:
QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override
{
if (size().isEmpty()) {
delete oldNode;
return nullptr;
}
GLNode *node = static_cast<GLNode *>(oldNode);
if (!node)
node = new GLNode;
return node;
}
};
#endif // QT_CONFIG(opengl)
tst_rendernode::tst_rendernode()
: QQmlDataTest(QT_QMLTEST_DATADIR)
{
qmlRegisterType<SimpleItem>("Test", 1, 0, "SimpleItem");
#if QT_CONFIG(opengl)
qmlRegisterType<GLItem>("Test", 1, 0, "GLSimpleItem");
#endif
}
void tst_rendernode::test_data()
{
QTest::addColumn<QString>("file");
QTest::newRow("simple") << QStringLiteral("simple.qml");
}
void tst_rendernode::test()
{
if ((QGuiApplication::platformName() == QLatin1String("offscreen"))
|| (QGuiApplication::platformName() == QLatin1String("minimal")))
QSKIP("Skipping due to grabWindow not functional on offscreen/minimal platforms");
if (!isRunningOnRhi())
QSKIP("Skipping QSGRenderNode test due to not running with QRhi");
QFETCH(QString, file);
QScopedPointer<QQuickView> view(createView(file, nullptr, 100, 100, 320, 200));
QVERIFY(QTest::qWaitForWindowExposed(view.data()));
QImage result = view->grabWindow();
const int maxFuzz = 5;
// red background
int x = 10, y = 10;
QVERIFY(qAbs(qRed(result.pixel(x, y)) - 255) < maxFuzz);
QVERIFY(qAbs(qGreen(result.pixel(x, y))) < maxFuzz);
QVERIFY(qAbs(qBlue(result.pixel(x, y))) < maxFuzz);
// gray rectangle in the middle
x = result.width() / 2;
y = result.height() / 2;
QVERIFY(qAbs(qRed(result.pixel(x, y)) - 128) < maxFuzz);
QVERIFY(qAbs(qGreen(result.pixel(x, y)) - 128) < maxFuzz);
QVERIFY(qAbs(qBlue(result.pixel(x, y)) - 128) < maxFuzz);
// check a bit up and left, this catches if the triangle is not depth
// tested correctly and so appears above the gray rect, not below as it should
x = result.width() / 2 - 5;
y = result.height() / 2 - 5;
QVERIFY(qAbs(qRed(result.pixel(x, y)) - 128) < maxFuzz);
QVERIFY(qAbs(qGreen(result.pixel(x, y)) - 128) < maxFuzz);
QVERIFY(qAbs(qBlue(result.pixel(x, y)) - 128) < maxFuzz);
// in search for blue pixels
int blueCount = 0;
for (y = 0; y < result.height(); ++y) {
for (x = 0; x < result.width(); ++x) {
if (qAbs(qRed(result.pixel(x, y))) < maxFuzz
&& qAbs(qGreen(result.pixel(x, y))) < maxFuzz
&& qAbs(qBlue(result.pixel(x, y)) - 255) < maxFuzz)
{
++blueCount;
}
}
}
// if the blue triangle is rendered by SimpleNode, there should be lots of
// blue pixels present
QVERIFY(blueCount > 5000);
}
#if QT_CONFIG(opengl)
void tst_rendernode::gltest_data()
{
QTest::addColumn<QString>("file");
QTest::newRow("simple") << QStringLiteral("glsimple.qml");
}
void tst_rendernode::gltest()
{
if ((QGuiApplication::platformName() == QLatin1String("offscreen"))
|| (QGuiApplication::platformName() == QLatin1String("minimal")))
QSKIP("Skipping due to grabWindow not functional on offscreen/minimal platforms");
if (!isRunningOnRhi())
QSKIP("Skipping QSGRenderNode test due to not running with QRhi");
if (QQuickWindow::graphicsApi() != QSGRendererInterface::OpenGL)
QSKIP("Skipping test due to not using OpenGL");
QFETCH(QString, file);
QScopedPointer<QQuickView> view(createView(file, nullptr, 100, 100, 320, 200));
QVERIFY(QTest::qWaitForWindowExposed(view.data()));
QImage result = view->grabWindow();
const int maxFuzz = 5;
// green
int x = 10, y = 10;
QVERIFY(qAbs(qRed(result.pixel(x, y))) < maxFuzz);
QVERIFY(qAbs(qGreen(result.pixel(x, y)) - 255) < maxFuzz);
QVERIFY(qAbs(qBlue(result.pixel(x, y))) < maxFuzz);
// gray rectangle in the middle
x = result.width() / 2;
y = result.height() / 2;
QVERIFY(qAbs(qRed(result.pixel(x, y)) - 128) < maxFuzz);
QVERIFY(qAbs(qGreen(result.pixel(x, y)) - 128) < maxFuzz);
QVERIFY(qAbs(qBlue(result.pixel(x, y)) - 128) < maxFuzz);
}
#endif // QT_CONFIG(opengl)
QQuickView *tst_rendernode::createView(const QString &file, QWindow *parent, int x, int y, int w, int h)
{
QQuickView *view = new QQuickView(parent);
view->setResizeMode(QQuickView::SizeRootObjectToView);
view->setSource(testFileUrl(file));
if (x >= 0 && y >= 0)
view->setPosition(x, y);
if (w >= 0 && h >= 0)
view->resize(w, h);
view->show();
return view;
}
Add the graphics api independent scenegraph port Opt in via environment variables: QSG_RHI=1 -> enable using QRhi instead of GL QSG_RHI_BACKEND -> set to vulkan, metal, d3d11, gl to override the default (the default is d3d11 on Windows, metal on Mac, gl elsewhere) Or force a given rhi backend via the existing QQuickWindow::setSceneGraphBackend(). Otherwise the default behavior is the same as before, the rhi code path is never active by default. -no-opengl builds are supported in the sense that they work and default to the software backend. However, the rhi code path cannot currently be used in such builds, even though QRhi from qtbase is fully functional with Vulkan, D3D, or Metal even when qtbase was configured with -no-opengl. This cannot be utilized by Quick atm due to OpenGL usage being all over the place in the sources corresponding to the default backend, and those host the rhi code path as well. This will be cleaned up hopefully in Qt 6, with the removal all direct OpenGL usage. Other env.vars.: QSG_RHI_DEBUG_LAYER=1 -> enable D3D debug or Vulkan validation layer (assuming the system is set up for this) QSG_RHI_SHADEREFFECT_DEBUG=1 -> print stuff from ShaderEffect QSG_SAMPLES=1,2,4,... -> MSAA sample count (but QSurfaceFormat works too) QT_D3D_ADAPTER_INDEX=0,1,... -> D3D adapter index QT_VK_PHYSICAL_DEVICE_INDEX=0,1,... -> Vulkan physical device index QSG_RHI_UINT32_INDEX=1 -> always use uint index data (both merged/unmerged, convert when needed - with some rhi backends this is implicit) QSG_RENDER_LOOP -> to override the render loop as usual. The default with RHI is threaded for Metal, threaded for Vulkan on Windows, basic for Vulkan on Linux and Android (to be checked later), while the existing rules apply for OpenGL. Not supported when running with QRhi: - particles - compressed atlases (though this is transparent to the apps) - QSGRenderNode - QQuickRenderControl - QQuickFramebufferObject - certain QQuickWindow functionality that depends directly on OpenGL - anisotropic filtering for textures - native text may lack some gamma correction - QSGEngine applicability unclear - some QML profiler logs may be incorrect or irrelevant Change-Id: I7822e99ad79e342e4166275da6e9e66498d76521 Reviewed-by: Lars Knoll <lars.knoll@qt.io>
2019-04-23 07:40:59 +00:00
bool tst_rendernode::isRunningOnRhi() const
{
static bool retval = false;
static bool decided = false;
if (!decided) {
decided = true;
QQuickView dummy;
dummy.show();
if (QTest::qWaitForWindowExposed(&dummy)) {
QSGRendererInterface::GraphicsApi api = dummy.rendererInterface()->graphicsApi();
retval = QSGRendererInterface::isApiRhiBased(api);
}
Add the graphics api independent scenegraph port Opt in via environment variables: QSG_RHI=1 -> enable using QRhi instead of GL QSG_RHI_BACKEND -> set to vulkan, metal, d3d11, gl to override the default (the default is d3d11 on Windows, metal on Mac, gl elsewhere) Or force a given rhi backend via the existing QQuickWindow::setSceneGraphBackend(). Otherwise the default behavior is the same as before, the rhi code path is never active by default. -no-opengl builds are supported in the sense that they work and default to the software backend. However, the rhi code path cannot currently be used in such builds, even though QRhi from qtbase is fully functional with Vulkan, D3D, or Metal even when qtbase was configured with -no-opengl. This cannot be utilized by Quick atm due to OpenGL usage being all over the place in the sources corresponding to the default backend, and those host the rhi code path as well. This will be cleaned up hopefully in Qt 6, with the removal all direct OpenGL usage. Other env.vars.: QSG_RHI_DEBUG_LAYER=1 -> enable D3D debug or Vulkan validation layer (assuming the system is set up for this) QSG_RHI_SHADEREFFECT_DEBUG=1 -> print stuff from ShaderEffect QSG_SAMPLES=1,2,4,... -> MSAA sample count (but QSurfaceFormat works too) QT_D3D_ADAPTER_INDEX=0,1,... -> D3D adapter index QT_VK_PHYSICAL_DEVICE_INDEX=0,1,... -> Vulkan physical device index QSG_RHI_UINT32_INDEX=1 -> always use uint index data (both merged/unmerged, convert when needed - with some rhi backends this is implicit) QSG_RENDER_LOOP -> to override the render loop as usual. The default with RHI is threaded for Metal, threaded for Vulkan on Windows, basic for Vulkan on Linux and Android (to be checked later), while the existing rules apply for OpenGL. Not supported when running with QRhi: - particles - compressed atlases (though this is transparent to the apps) - QSGRenderNode - QQuickRenderControl - QQuickFramebufferObject - certain QQuickWindow functionality that depends directly on OpenGL - anisotropic filtering for textures - native text may lack some gamma correction - QSGEngine applicability unclear - some QML profiler logs may be incorrect or irrelevant Change-Id: I7822e99ad79e342e4166275da6e9e66498d76521 Reviewed-by: Lars Knoll <lars.knoll@qt.io>
2019-04-23 07:40:59 +00:00
dummy.hide();
}
return retval;
}
QTEST_MAIN(tst_rendernode)
#include "tst_rendernode.moc"