417 lines
12 KiB
C++
417 lines
12 KiB
C++
// Copyright (C) 2016 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
|
|
|
#include <qtest.h>
|
|
|
|
#include <QtQuick/qquickitem.h>
|
|
#include <QtQuick/qquickview.h>
|
|
#include <private/qsgrendernode_p.h>
|
|
#include <rhi/qrhi.h>
|
|
|
|
#include <QtQuickTestUtils/private/qmlutils_p.h>
|
|
#include <QtQuickTestUtils/private/visualtestutils_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
|
|
|
|
private:
|
|
QQuickView *createView(const QString &file, QWindow *parent, int x, int y, int w, int h);
|
|
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);
|
|
|
|
GLint viewport[4];
|
|
f->glGetIntegerv(GL_VIEWPORT, viewport);
|
|
QCOMPARE(viewport[0], 0);
|
|
QCOMPARE(viewport[1], 0);
|
|
QCOMPARE(viewport[2], 320);
|
|
QCOMPARE(viewport[3], 200);
|
|
|
|
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()
|
|
{
|
|
SKIP_IF_NO_WINDOW_GRAB;
|
|
|
|
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");
|
|
QTest::newRow("rendernode only") << QStringLiteral("glsimple_only.qml");
|
|
}
|
|
|
|
void tst_rendernode::gltest()
|
|
{
|
|
SKIP_IF_NO_WINDOW_GRAB;
|
|
|
|
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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
dummy.hide();
|
|
}
|
|
return retval;
|
|
}
|
|
|
|
QTEST_MAIN(tst_rendernode)
|
|
|
|
#include "tst_rendernode.moc"
|