Revision control

Copy as Markdown

// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#include <QTest>
#include <QQuickWindow>
#include <QQuickRenderControl>
#include <QQuickRenderTarget>
#include <QQuickGraphicsDevice>
#include <QQuickItem>
#include <QQmlEngine>
#include <QQmlComponent>
#if QT_CONFIG(vulkan)
#include <QVulkanInstance>
#endif
#include "../shared/util.h"
class tst_RenderControl : public QQuick3DDataTest
{
Q_OBJECT
private slots:
void initTestCase() override;
void cube();
void textureSourceItem();
void dynamicLights();
private:
bool initRenderer(QQuick3DTestOffscreenRenderer *renderer, const char *filename);
#if QT_CONFIG(vulkan)
QVulkanInstance vulkanInstance;
#endif
};
void tst_RenderControl::initTestCase()
{
QQuick3DDataTest::initTestCase();
if (!initialized())
return;
#if QT_CONFIG(vulkan)
vulkanInstance.setLayers({ "VK_LAYER_LUNARG_standard_validation" });
vulkanInstance.create(); // may fail, which is fine is Vulkan is not used in the first place
#endif
}
bool tst_RenderControl::initRenderer(QQuick3DTestOffscreenRenderer *renderer, const char *filename)
{
const bool initSuccess = renderer->init(testFileUrl(QString::fromLatin1(filename)),
#if QT_CONFIG(vulkan)
&vulkanInstance
#else
nullptr
#endif
);
return initSuccess;
}
const int FUZZ = 5;
static inline void renderNextFrame(QQuick3DTestOffscreenRenderer *renderer,
bool *readCompleted,
QRhiReadbackResult *readResult,
QImage *result)
{
renderer->renderControl->polishItems();
renderer->renderControl->beginFrame();
renderer->renderControl->sync();
renderer->renderControl->render();
renderer->enqueueReadback(readCompleted, readResult, result);
renderer->renderControl->endFrame();
}
void tst_RenderControl::cube()
{
QQuick3DTestOffscreenRenderer renderer;
QVERIFY(initRenderer(&renderer, "cube_with_size.qml"));
bool readCompleted = false;
QRhiReadbackResult readResult;
QImage result;
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QVERIFY(!result.isNull());
QCOMPARE(result.size(), QSize(640, 480)); // no trouble with scale factors since it's all offscreen
QVERIFY(comparePixel(result, 50, 50, 1, Qt::black, FUZZ));
// front face of the cube is lighter gray-ish
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(239, 239, 239), FUZZ));
// the top is darker
QVERIFY(comparePixelNormPos(result, 0.5, 0.375, QColor::fromRgb(181, 181, 181), FUZZ));
}
void tst_RenderControl::textureSourceItem()
{
QQuick3DTestOffscreenRenderer renderer;
QVERIFY(initRenderer(&renderer, "sourceitem.qml"));
// We do expect a perfect frame, meaning that the 2D content in
// the textures must be rendered in the first frame and should not
// be deferred. Unlike with an on-screen window, we can verify
// this in a robust manner here because we are in full control of
// when exactly a new frame is generated.
bool readCompleted = false;
QRhiReadbackResult readResult;
QImage result;
// Frame 1
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
// from left to right: red, green, blue
QVERIFY(comparePixelNormPos(result, 0.275, 0.5, QColor::fromRgb(255, 0, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 128, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.725, 0.5, QColor::fromRgb(0, 0, 255), FUZZ));
result = QImage();
// make a Texture.sourceItem refer to something else now
QMetaObject::invokeMethod(renderer.rootItem, "makeThirdReferToStandaloneSourceItem");
// Frame 2
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
// from left to right: red, green, red
QVERIFY(comparePixelNormPos(result, 0.275, 0.5, QColor::fromRgb(255, 0, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 128, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.725, 0.5, QColor::fromRgb(255, 0, 0), FUZZ));
result = QImage();
// exercise resizing the render target, the root item, and the scene
QVERIFY(renderer.resize(QSize(1280, 960)));
// Frame 3
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(1280, 960));
// from left to right: red, green, red (unchanged, compared to
// frame 2, apart from the increased size, but the normalized
// positions work as-is)
QVERIFY(comparePixelNormPos(result, 0.275, 0.5, QColor::fromRgb(255, 0, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 128, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.725, 0.5, QColor::fromRgb(255, 0, 0), FUZZ));
result = QImage();
// switch the third sphere to a different sourceItem, now an Item
// tree that has an explicit layer.enabled: true on it (meaning no
// implicit QSGLayer gets created internally on Quick3D side)
QMetaObject::invokeMethod(renderer.rootItem, "makeThirdReferToExplicitLayerBasedSourceItem");
// Frame 4
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(1280, 960));
// from left to right: red, green, gray
QVERIFY(comparePixelNormPos(result, 0.275, 0.5, QColor::fromRgb(255, 0, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 128, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.725, 0.5, QColor::fromRgb(128, 128, 128), FUZZ));
result = QImage();
// switch the third sphere to with an Image (which itself is a
// textureProvider) as the sourceItem
QMetaObject::invokeMethod(renderer.rootItem, "makeThirdReferToImageSourceItem");
// Frame 5
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(1280, 960));
QVERIFY(comparePixelNormPos(result, 0.275, 0.5, QColor::fromRgb(255, 0, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 128, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.7, 0.5, QColor::fromRgb(64, 205, 82), FUZZ)); // green from Qt logo image
QVERIFY(comparePixelNormPos(result, 0.725, 0.5, QColor::fromRgb(255, 255, 255), FUZZ)); // white from Qt logo image
result = QImage();
// switch the third sphere to use a sourceItem that has non-opaque content
QMetaObject::invokeMethod(renderer.rootItem, "makeThirdReferToSemiTransparentSourceItem");
// Frame 6
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(1280, 960));
// from left to right: red, green, transparent/blue
QVERIFY(comparePixelNormPos(result, 0.275, 0.5, QColor::fromRgb(255, 0, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 128, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.725, 0.44, QColor::fromRgb(0, 0, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.725, 0.46, QColor::fromRgb(0, 0, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.725, 0.5, QColor::fromRgb(0, 0, 255), FUZZ));
result = QImage();
// make the sourceItem's size half - this leads to the blue
// rectangle occupy a larger area on the sphere's surface
QMetaObject::invokeMethod(renderer.rootItem, "makeSemiTransparentSourceItemSmaller");
// Frame 7
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(1280, 960));
QVERIFY(comparePixelNormPos(result, 0.275, 0.5, QColor::fromRgb(255, 0, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 128, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.725, 0.44, QColor::fromRgb(0, 0, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.725, 0.46, QColor::fromRgb(0, 0, 255), FUZZ)); // different from previous frame
QVERIFY(comparePixelNormPos(result, 0.725, 0.5, QColor::fromRgb(0, 0, 255), FUZZ));
}
static inline QObject *invokeAndGetObject(QObject *target, const char *func)
{
QVariant result;
QMetaObject::invokeMethod(target, func, Q_RETURN_ARG(QVariant, result));
return result.value<QObject *>();
}
void tst_RenderControl::dynamicLights()
{
QQuick3DTestOffscreenRenderer renderer;
QVERIFY(initRenderer(&renderer, "dynamic_lights.qml"));
if (renderer.quickWindow->rendererInterface()->graphicsApi() == QSGRendererInterface::OpenGL) {
#ifdef Q_OS_MACOS
QSKIP("Skipping test due to sofware OpenGL renderer problems on macOS");
#endif
}
#if defined(Q_OS_WIN)
#ifdef Q_PROCESSOR_ARM
if (qgetenv("QTEST_ENVIRONMENT") == "ci") {
QSKIP("Skipping test due to problems on windows arm64 on CI");
}
#endif
#endif
bool readCompleted = false;
QRhiReadbackResult readResult;
QImage result;
QObject *directionalLight = nullptr;
QObject *pointLight = nullptr;
QObject *spotLight = nullptr;
// Case: scene without lights
{
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 0, 0), FUZZ));
}
// Case: add a DirectionalLight with default property values
{
result = QImage();
directionalLight = invokeAndGetObject(renderer.rootItem, "addDirectionalLight");
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
// front face of the cube is lighter gray-ish
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(239, 239, 239), FUZZ));
// the top is darker
QVERIFY(comparePixelNormPos(result, 0.5, 0.375, QColor::fromRgb(181, 181, 181), FUZZ));
// the "floor" is even darker
QVERIFY(comparePixelNormPos(result, 0.5, 0.8, QColor::fromRgb(153, 153, 153), FUZZ));
}
// Case: destroy the DirectionalLight
{
result = QImage();
delete directionalLight;
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 0, 0), FUZZ));
}
// Case: add a shadow casting DirectionalLight emitting downwards
{
result = QImage();
directionalLight = invokeAndGetObject(renderer.rootItem, "addShadowCastingDirectionalLight");
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
// front face of the cube is black
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 0, 0), FUZZ));
// the top is bright
QVERIFY(comparePixelNormPos(result, 0.5, 0.375, QColor::fromRgb(234, 234, 234), FUZZ));
// the "floor" is even brighter
QVERIFY(comparePixelNormPos(result, 0.5, 0.9, QColor::fromRgb(248, 248, 248), FUZZ));
// except where it's in shadow
QVERIFY(comparePixelNormPos(result, 0.5, 0.8, QColor::fromRgb(0, 0, 0), FUZZ));
}
// Case: toggle 'visible'
{
result = QImage();
directionalLight->setProperty("visible", false);
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 0, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.375, QColor::fromRgb(0, 0, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.9, QColor::fromRgb(0, 0, 0), FUZZ));
}
// Case: move the camera up and look downwards a bit, add a (non-shadow casting) full-green PointLight
{
result = QImage();
QMetaObject::invokeMethod(renderer.rootItem, "moveCamera");
pointLight = invokeAndGetObject(renderer.rootItem, "addPointLight");
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
// top of the cube is very green
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 255, 0), FUZZ));
// the front of the cube is now black
QVERIFY(comparePixelNormPos(result, 0.5, 0.666, QColor::fromRgb(0, 0, 0), FUZZ));
// floor is darker green
QVERIFY(comparePixelNormPos(result, 0.5, 0.9, QColor::fromRgb(0, 140, 0), FUZZ));
}
// Case: make the PointLight cast shadows
{
result = QImage();
pointLight->setProperty("castsShadow", true);
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 255, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.666, QColor::fromRgb(0, 0, 0), FUZZ));
// floor is darker due to the shadow
QVERIFY(comparePixelNormPos(result, 0.5, 0.9, QColor::fromRgb(0, 72, 0), FUZZ));
}
// Case: make the DirectionalLight visible and cast shadows
{
result = QImage();
directionalLight->setProperty("visible", true);
directionalLight->setProperty("castsShadow", true);
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(255, 255, 255), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.9, QColor::fromRgb(248, 255, 248), FUZZ));
// floor in shadow, now with some green added
QVERIFY(comparePixelNormPos(result, 0.5, 0.8, QColor::fromRgb(0, 82, 0), FUZZ));
}
// Case: make the DirectionalLight not cast shadows and reduce brightness
{
result = QImage();
directionalLight->setProperty("castsShadow", false);
directionalLight->setProperty("brightness", 0.2);
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(255, 255, 255), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.8, QColor::fromRgb(173, 188, 173), FUZZ));
}
// Case: destroy the DirectionalLight
{
result = QImage();
delete directionalLight;
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 255, 0), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.666, QColor::fromRgb(0, 0, 0), FUZZ));
// floor is darker due to the shadow
QVERIFY(comparePixelNormPos(result, 0.5, 0.9, QColor::fromRgb(0, 72, 0), FUZZ));
}
// Case: re-add a shadow casting DirectionalLight emitting downwards
{
result = QImage();
directionalLight = invokeAndGetObject(renderer.rootItem, "addShadowCastingDirectionalLight");
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(255, 255, 255), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.9, QColor::fromRgb(248, 255, 248), FUZZ));
// floor in shadow, now with some green added
QVERIFY(comparePixelNormPos(result, 0.5, 0.8, QColor::fromRgb(0, 82, 0), FUZZ));
}
// Case: destroy the PointLight
{
result = QImage();
delete pointLight;
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(255, 255, 255), FUZZ));
// floor in shadow
QVERIFY(comparePixelNormPos(result, 0.5, 0.8, QColor::fromRgb(0, 0, 0), FUZZ));
}
// Case: destroy the DirectionalLight
{
result = QImage();
delete directionalLight;
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 0, 0), FUZZ));
}
// With the default forward renderer, we can have up to 4 directional lights
// and up to 16 positional lights (point/spot) at the same time. Each light
// can cast shadows as well. If a scene has more directional lights, only the
// first 4 defined will be used for lighting. The same goes for point/spot lights,
// though there is some expectation in the future we will determine the 16 closest
// positional lights to the object being drawn.
// Case: add 10 shadow casting DirectionalLights (note that only 4 of these
// will actually do anything)
QVarLengthArray<QObject *, 10> directionalLights;
{
result = QImage();
for (int i = 0; i < 10; ++i)
directionalLights.append(invokeAndGetObject(renderer.rootItem, "addShadowCastingDirectionalLight"));
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(255, 255, 255), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.8, QColor::fromRgb(0, 0, 0), FUZZ));
}
// Case: make half of the lights 32 bit
{
for (int i = 0; i < 10; i += 2)
directionalLights[i]->setProperty("use32BitShadowmap", true);
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(255, 255, 255), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.8, QColor::fromRgb(0, 0, 0), FUZZ));
}
// Case: make some invisible and destroy some
{
result = QImage();
delete directionalLights[2];
directionalLights.remove(2);
delete directionalLights[3];
directionalLights.remove(3);
directionalLights[0]->setProperty("visible", false);
directionalLights[2]->setProperty("visible", false);
directionalLights[4]->setProperty("visible", false);
directionalLights[6]->setProperty("visible", false);
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.8, QColor::fromRgb(0, 0, 0), FUZZ));
}
// Case: destroy one more DirectionalLight and add a shadow casting PointLight
{
result = QImage();
QVERIFY(directionalLights.last()->property("visible").toBool());
delete directionalLights.last();
directionalLights.removeLast();
pointLight = invokeAndGetObject(renderer.rootItem, "addPointLight");
pointLight->setProperty("castsShadow", true);
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(255, 255, 255), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.8, QColor::fromRgb(0, 82, 0), FUZZ));
}
// Case: delete all but one DirectionalLight, reduce its color and brightness, and add a red SpotLight
{
result = QImage();
for (int i = 1; i < directionalLights.size(); i++)
delete directionalLights[i];
directionalLights[0]->setProperty("visible", true);
directionalLights[0]->setProperty("brightness", 0.1);
directionalLights[0]->setProperty("color", QColor(Qt::blue));
spotLight = invokeAndGetObject(renderer.rootItem, "addSpotLight");
spotLight->setProperty("castsShadow", true);
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 255, 255), FUZZ));
QVERIFY(comparePixelNormPos(result, 0.5, 0.8, QColor::fromRgb(10, 82, 0), FUZZ));
}
// Case: add a lot of lights, to exceed the limit of 15. Should survive
// gracefully, ignoring the extra lights.
{
result = QImage();
for (int i = 0; i < 30; ++i)
invokeAndGetObject(renderer.rootItem, "addPointLight");
renderNextFrame(&renderer, &readCompleted, &readResult, &result);
QVERIFY(readCompleted);
QCOMPARE(result.size(), QSize(640, 480));
QVERIFY(comparePixelNormPos(result, 0.5, 0.5, QColor::fromRgb(0, 255, 0), FUZZ));
}
}
QTEST_MAIN(tst_RenderControl)
#include "tst_rendercontrol.moc"