Revision control

Copy as Markdown

// 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 <QtQuick/qsgrendererinterface.h>
#include <QtQuick/private/qquickitem_p.h>
#include <qopenglcontext.h>
#include <qopenglfunctions.h>
#include <QtQuickTestUtils/private/qmlutils_p.h>
#include <QtQuickTestUtils/private/viewtestutils_p.h>
#include <QtQuick/qsgtexture.h>
#include <QtQuick/qsgtextureprovider.h>
#include <QtCore/qscopeguard.h>
#include <QtGui/qscreen.h>
#include <QtGui/private/qguiapplication_p.h>
#include <QtGui/private/qhighdpiscaling_p.h>
#include <QtGui/qpa/qplatformintegration.h>
class tst_QQuickItemLayer: public QQmlDataTest
{
Q_OBJECT
public:
tst_QQuickItemLayer();
QImage runTest(const QString &fileName)
{
QQuickView view;
view.setSource(testFileUrl(fileName));
view.showNormal();
return QTest::qWaitForWindowExposed(&view)
? view.grabWindow() : QImage();
}
private slots:
void initTestCase() override;
void layerEnabled();
void layerSmooth();
#if QT_CONFIG(opengl)
void layerMipmap();
void layerEffect();
#endif
void layerVisibility_data();
void layerVisibility();
void layerSourceRect();
void layerZOrder_data();
void layerZOrder();
void layerIsTextureProvider();
void changeZOrder_data();
void changeZOrder();
void toggleLayerAndEffect();
void disableLayer();
void changeSamplerName();
void itemEffect();
void rectangleEffect();
void textureMirroring_data();
void textureMirroring();
void effectSourceResizeToItem();
void layerIsItemSizeInDevicePixels();
void layerBlitsTextureOneToOne_data();
void layerBlitsTextureOneToOne();
private:
void mirroringCheck(int mirroring, int x, bool shouldMirror, const QImage &fb);
};
bool isOffscreen()
{
return (QGuiApplication::platformName() == QLatin1String("offscreen"))
|| (QGuiApplication::platformName() == QLatin1String("minimal"));
}
const char skipOffscreenMsg[] =
"Skipping due to grabWindow not functional on offscreen/minimal platformsi (QTBUG-63185)";
tst_QQuickItemLayer::tst_QQuickItemLayer() : QQmlDataTest(QT_QMLTEST_DATADIR) { }
void tst_QQuickItemLayer::initTestCase()
{
QQmlDataTest::initTestCase();
QQuickView view;
view.showNormal();
QVERIFY(QTest::qWaitForWindowExposed(&view));
}
// The test draws a red and a blue box next to each other and tests that the
// output is still red and blue on the left and right and a combination of
// the two in the middle.
void tst_QQuickItemLayer::layerSmooth()
{
if (isOffscreen())
QSKIP(skipOffscreenMsg);
QImage fb = runTest("Smooth.qml");
QVERIFY(!fb.size().isEmpty());
QCOMPARE(fb.pixel(0, 0), qRgb(0xff, 0, 0));
QCOMPARE(fb.pixel(fb.width() - 1, 0), qRgb(0, 0, 0xff));
uint pixel = fb.pixel(fb.width() / 2, 0);
QVERIFY(qRed(pixel) > 0);
QVERIFY(qBlue(pixel) > 0);
}
// The test draws a gradient at a small size into a layer and scales the
// layer. If the layer is enabled there should be very visible bands in
// the gradient.
void tst_QQuickItemLayer::layerEnabled()
{
if (isOffscreen())
QSKIP(skipOffscreenMsg);
QImage fb = runTest("Enabled.qml");
QVERIFY(!fb.size().isEmpty());
// Verify the banding
QCOMPARE(fb.pixel(0, 0), fb.pixel(0, 1));
// Verify the gradient
QVERIFY(fb.pixel(0, 0) != fb.pixel(0, fb.height() - 1));
}
#if QT_CONFIG(opengl)
// The test draws a one pixel wide line and scales it down by more than a a factor 2
// If mipmpping works, the pixels should be gray, not white or black
void tst_QQuickItemLayer::layerMipmap()
{
if (isOffscreen())
QSKIP(skipOffscreenMsg);
QImage fb = runTest("Mipmap.qml");
QVERIFY(fb.pixel(0, 0) != 0xff000000);
QVERIFY(fb.pixel(0, 0) != 0xffffffff);
}
// The test implements an rgb swapping effect sourced from a blue rectangle. The
// resulting pixel should be red
void tst_QQuickItemLayer::layerEffect()
{
if (isOffscreen())
QSKIP(skipOffscreenMsg);
QImage fb = runTest("Effect.qml");
QVERIFY(!fb.size().isEmpty());
QCOMPARE(fb.pixel(0, 0), qRgb(0xff, 0, 0));
QCOMPARE(fb.pixel(fb.width() - 1, 0), qRgb(0, 0xff, 0));
}
#endif
// The test draws a rectangle and verifies that there is padding on each side
// as the source rect spans outside the item. The padding is verified using
// a shader that pads transparent to blue. Everything else is red.
void tst_QQuickItemLayer::layerSourceRect()
{
if (isOffscreen())
QSKIP(skipOffscreenMsg);
QImage fb = runTest("SourceRect.qml");
QVERIFY(!fb.size().isEmpty());
// Check that the edges are converted to blue
QCOMPARE(fb.pixel(0, 0), qRgb(0, 0, 0xff));
QCOMPARE(fb.pixel(fb.width() - 1, 0), qRgb(0, 0, 0xff));
QCOMPARE(fb.pixel(0, fb.height() - 1), qRgb(0, 0, 0xff));
QCOMPARE(fb.pixel(fb.width() - 1, fb.height() - 1), qRgb(0, 0, 0xff));
// The center pixel should be red
QCOMPARE(fb.pixel(fb.width() / 2, fb.height() / 2), qRgb(0xff, 0, 0));
}
// Same as the effect test up above, but this time use the item
// directly in a stand alone ShaderEffect
void tst_QQuickItemLayer::layerIsTextureProvider()
{
if (isOffscreen())
QSKIP(skipOffscreenMsg);
QImage fb = runTest("TextureProvider.qml");
QVERIFY(!fb.size().isEmpty());
QCOMPARE(fb.pixel(0, 0), qRgb(0xff, 0, 0));
QCOMPARE(fb.pixel(fb.width() - 1, 0), qRgb(0, 0xff, 0));
}
void tst_QQuickItemLayer::layerVisibility_data()
{
QTest::addColumn<bool>("visible");
QTest::addColumn<bool>("effect");
QTest::addColumn<qreal>("opacity");
QTest::newRow("!effect, !visible, a=1") << false << false << 1.;
QTest::newRow("!effect, visible, a=1") << false << true << 1.;
QTest::newRow("effect, !visible, a=1") << true << false << 1.;
QTest::newRow("effect, visible, a=1") << true << true << 1.;
QTest::newRow("!effect, !visible, a=.5") << false << false << .5;
QTest::newRow("!effect, visible, a=.5") << false << true << .5;
QTest::newRow("effect, !visible, a=.5") << true << false << .5;
QTest::newRow("effect, visible, a=.5") << true << true << .5;
QTest::newRow("!effect, !visible, a=0") << false << false << 0.;
QTest::newRow("!effect, visible, a=0") << false << true << 0.;
QTest::newRow("effect, !visible, a=0") << true << false << 0.;
QTest::newRow("effect, visible, a=0") << true << true << 0.;
}
void tst_QQuickItemLayer::layerVisibility()
{
if (isOffscreen())
QSKIP(skipOffscreenMsg);
QFETCH(bool, visible);
QFETCH(bool, effect);
QFETCH(qreal, opacity);
QQuickView view;
view.setSource(testFileUrl("Visible.qml"));
QQuickItem *child = view.contentItem()->childItems().at(0);
child->setProperty("layerVisible", visible);
child->setProperty("layerEffect", effect);
child->setProperty("layerOpacity", opacity);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QImage fb = view.grabWindow();
uint pixel = fb.pixel(0, 0);
if (!visible || opacity == 0) {
QCOMPARE(pixel, qRgb(0xff, 0xff, 0xff));
} else if (effect) {
QCOMPARE(qRed(pixel), 0xff);
QVERIFY(qGreen(pixel) < 0xff);
QVERIFY(qBlue(pixel) < 0xff);
} else { // no effect
QCOMPARE(qBlue(pixel), 0xff);
QVERIFY(qGreen(pixel) < 0xff);
QVERIFY(qRed(pixel) < 0xff);
}
}
void tst_QQuickItemLayer::layerZOrder_data()
{
QTest::addColumn<bool>("effect");
QTest::newRow("!effect") << false;
QTest::newRow("effect") << true;
}
void tst_QQuickItemLayer::layerZOrder()
{
if (isOffscreen())
QSKIP(skipOffscreenMsg);
QFETCH(bool, effect);
QQuickView view;
view.setSource(testFileUrl("ZOrder.qml"));
QQuickItem *child = view.contentItem()->childItems().at(0);
child->setProperty("layerEffect", effect);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QImage fb = view.grabWindow();
QCOMPARE(fb.pixel(50, 50), qRgb(0, 0, 0xff));
QCOMPARE(fb.pixel(150, 150), qRgb(0, 0xff, 00));
}
void tst_QQuickItemLayer::changeZOrder_data()
{
QTest::addColumn<bool>("layered");
QTest::addColumn<bool>("effect");
QTest::newRow("layered, effect") << true << true;
QTest::newRow("layered, !effect") << true << false;
QTest::newRow("!layered") << false << false;
}
void tst_QQuickItemLayer::changeZOrder()
{
if (isOffscreen())
QSKIP(skipOffscreenMsg);
QFETCH(bool, layered);
QFETCH(bool, effect);
QQuickView view;
view.setSource(testFileUrl("ZOrderChange.qml"));
QQuickItem *child = view.contentItem()->childItems().at(0);
child->setProperty("layerEnabled", layered);
child->setProperty("layerEffect", effect);
child->setProperty("layerZ", 1);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QImage fb = view.grabWindow();
QRgb topLeft = fb.pixel(50, 50);
QRgb topRight = fb.pixel(150, 50);
QRgb bottomLeft = fb.pixel(50, 150);
QRgb bottomRight = fb.pixel(150, 150);
QCOMPARE(bottomLeft, qRgb(0, 0, 0xff));
if (layered) {
QCOMPARE(topLeft, qRgb(0, 0xff, 0xff));
} else {
QCOMPARE(qGreen(topLeft), 0xff);
QVERIFY(qAbs(qRed(topLeft) - 0x3f) < 4);
QVERIFY(qAbs(qBlue(topLeft) - 0xbf) < 4);
}
if (layered && effect) {
QCOMPARE(qRed(topRight), 0xff);
QCOMPARE(qGreen(topRight), 0x00);
QVERIFY(qAbs(qBlue(topRight) - 0x7f) < 4);
QVERIFY(qAbs(qRed(bottomRight) - 0x7f) < 4);
QCOMPARE(qBlue(bottomRight), 0xff);
QVERIFY(qAbs(qGreen(bottomRight) - 0x7f) < 4);
} else {
QCOMPARE(qRed(topRight), 0xff);
QCOMPARE(qBlue(topRight), 0x00);
QVERIFY(qAbs(qGreen(topRight) - 0x7f) < 4);
QVERIFY(qAbs(qRed(bottomRight) - 0x7f) < 4);
QCOMPARE(qGreen(bottomRight), 0xff);
QVERIFY(qAbs(qBlue(bottomRight) - 0x7f) < 4);
}
}
void tst_QQuickItemLayer::toggleLayerAndEffect()
{
if (isOffscreen())
QSKIP(skipOffscreenMsg);
// This test passes if it doesn't crash.
runTest("ToggleLayerAndEffect.qml");
}
void tst_QQuickItemLayer::disableLayer()
{
if (isOffscreen())
QSKIP(skipOffscreenMsg);
// This test passes if it doesn't crash.
runTest("DisableLayer.qml");
}
void tst_QQuickItemLayer::changeSamplerName()
{
if (isOffscreen())
QSKIP(skipOffscreenMsg);
QImage fb = runTest("SamplerNameChange.qml");
QVERIFY(!fb.size().isEmpty());
QCOMPARE(fb.pixel(0, 0), qRgb(0, 0, 0xff));
}
void tst_QQuickItemLayer::itemEffect()
{
if (isOffscreen())
QSKIP(skipOffscreenMsg);
QImage fb = runTest("ItemEffect.qml");
QVERIFY(!fb.size().isEmpty());
QCOMPARE(fb.pixel(0, 0), qRgb(0xff, 0, 0));
QCOMPARE(fb.pixel(199, 0), qRgb(0xff, 0, 0));
QCOMPARE(fb.pixel(0, 199), qRgb(0, 0, 0xff));
QCOMPARE(fb.pixel(199, 199), qRgb(0, 0, 0xff));
}
void tst_QQuickItemLayer::rectangleEffect()
{
if (isOffscreen())
QSKIP(skipOffscreenMsg);
QImage fb = runTest("RectangleEffect.qml");
QVERIFY(!fb.size().isEmpty());
QCOMPARE(fb.pixel(0, 0), qRgb(0, 0xff, 0));
QCOMPARE(fb.pixel(199, 0), qRgb(0, 0xff, 0));
QCOMPARE(fb.pixel(0, 199), qRgb(0, 0xff, 0));
QCOMPARE(fb.pixel(199, 199), qRgb(0, 0xff, 0));
QCOMPARE(fb.pixel(100, 0), qRgb(0, 0, 0xff));
QCOMPARE(fb.pixel(199, 100), qRgb(0, 0, 0xff));
QCOMPARE(fb.pixel(100, 199), qRgb(0, 0, 0xff));
QCOMPARE(fb.pixel(0, 100), qRgb(0, 0, 0xff));
}
void tst_QQuickItemLayer::textureMirroring_data()
{
QTest::addColumn<int>("mirroring");
QTest::newRow("no mirroring") << 0;
QTest::newRow("horizontal") << 1;
QTest::newRow("vertical") << 2;
QTest::newRow("horizontal | vertical") << 3;
}
void tst_QQuickItemLayer::textureMirroring()
{
if (isOffscreen())
QSKIP(skipOffscreenMsg);
QFETCH(int, mirroring);
QQuickView view;
view.setSource(testFileUrl("TextureMirroring.qml"));
QQuickItem *child = view.contentItem()->childItems().at(0);
child->setProperty("mirroring", mirroring);
view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QImage fb = view.grabWindow();
// Mirroring should have no visual effect on layered item without shader effect
mirroringCheck(mirroring, 0, false, fb);
// Mirroring should have visual effect on layered item with shader effect
mirroringCheck(mirroring, 50, true, fb);
// Mirroring should have no visual effect on source item for ShaderEffectSource
mirroringCheck(mirroring, 100, false, fb);
// Mirroring should have no visual effect on ShaderEffectSource item
mirroringCheck(mirroring, 150, false, fb);
// Mirroring should have visual effect on ShaderEffect item itself
mirroringCheck(mirroring, 200, true, fb);
}
void tst_QQuickItemLayer::effectSourceResizeToItem() // QTBUG-104442
{
QQuickView window;
QVERIFY(QQuickTest::showView(window, testFileUrl("itemImageLayer.qml")));
window.setResizeMode(QQuickView::SizeRootObjectToView);
QQuickItem *image = window.rootObject()->findChild<QQuickItem*>("image");
QVERIFY(image);
QCOMPARE(image->size(), window.rootObject()->size());
QQuickItemLayer *layer = QQuickItemPrivate::get(image)->layer();
QVERIFY(layer);
auto *effectSource = layer->effectSource();
QVERIFY(effectSource);
QCOMPARE(effectSource->size(), image->size());
window.resize(200, 200); // shrink it a bit
QTRY_COMPARE(image->size().toSize(), QSize(200, 200)); // wait for the window system
QCOMPARE(effectSource->size(), image->size());
}
void tst_QQuickItemLayer::layerIsItemSizeInDevicePixels()
{
#if !QT_CONFIG(highdpiscaling)
QSKIP("This test requires high-DPI scaling support");
#endif
if (isOffscreen())
QSKIP(skipOffscreenMsg);
constexpr qreal dpr = 1.5;
constexpr int pixelWidth = 595;
constexpr int pixelHeight = 511;
QVERIFY2(QGuiApplication::allWindows().isEmpty(),
"setGlobalFactor() only takes effect while no window exists");
QScreen *screen = QGuiApplication::primaryScreen();
QHighDpiScaling::setGlobalFactor(dpr / screen->devicePixelRatio());
QGuiApplicationPrivate::resetCachedDevicePixelRatio();
const auto restoreScaling = qScopeGuard([]{
QHighDpiScaling::setGlobalFactor(1);
QGuiApplicationPrivate::resetCachedDevicePixelRatio();
});
QCOMPARE(screen->devicePixelRatio(), dpr);
QSize textureSize;
QQuickView view;
view.setInitialProperties({{ "dpr", dpr },
{ "pixelWidth", pixelWidth },
{ "pixelHeight", pixelHeight }});
view.setSource(testFileUrl("DevicePixelStripes.qml"));
view.showNormal();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QCOMPARE(view.effectiveDevicePixelRatio(), dpr);
QQuickItem *target = view.rootObject()->findChild<QQuickItem *>("target");
QVERIFY(target);
QQuickItemLayer *layer = QQuickItemPrivate::get(target)->layer();
QVERIFY(layer);
QQuickShaderEffectSource *effectSource = layer->effectSource();
QVERIFY(effectSource);
connect(&view, &QQuickWindow::afterRendering, &view, [&]{
if (QSGTextureProvider *provider = effectSource->textureProvider()) {
if (QSGTexture *texture = provider->texture())
textureSize = texture->textureSize();
}
}, Qt::DirectConnection);
view.grabWindow();
QCOMPARE(textureSize, QSize(pixelWidth, pixelHeight));
}
void tst_QQuickItemLayer::layerBlitsTextureOneToOne_data()
{
QTest::addColumn<qreal>("position");
QTest::newRow("aligned") << 0.0;
QTest::newRow("one device pixel right") << 0.5;
QTest::newRow("one device pixel left") << -0.5;
}
void tst_QQuickItemLayer::layerBlitsTextureOneToOne()
{
QFETCH(qreal, position);
#if !QT_CONFIG(highdpiscaling)
QSKIP("This test requires high-DPI scaling support");
#endif
if (isOffscreen())
QSKIP(skipOffscreenMsg);
constexpr qreal dpr = 2.0;
constexpr int pixelWidth = 201;
constexpr int pixelHeight = 101;
QVERIFY2(QGuiApplication::allWindows().isEmpty(),
"setGlobalFactor() only takes effect while no window exists");
QScreen *screen = QGuiApplication::primaryScreen();
QHighDpiScaling::setGlobalFactor(dpr / screen->devicePixelRatio());
QGuiApplicationPrivate::resetCachedDevicePixelRatio();
const auto restoreScaling = qScopeGuard([]{
QHighDpiScaling::setGlobalFactor(1);
QGuiApplicationPrivate::resetCachedDevicePixelRatio();
});
QCOMPARE(screen->devicePixelRatio(), dpr);
QQuickView view;
view.setInitialProperties({{ "dpr", dpr },
{ "pixelWidth", pixelWidth },
{ "pixelHeight", pixelHeight },
{ "position", position }});
view.setSource(testFileUrl("DevicePixelStripes.qml"));
view.showNormal();
QVERIFY(QTest::qWaitForWindowExposed(&view));
QCOMPARE(view.effectiveDevicePixelRatio(), dpr);
if (view.rendererInterface()->graphicsApi() == QSGRendererInterface::Software)
QSKIP("The software backend maps the target rect through a different path");
const QImage fb = view.grabWindow();
if (fb.width() < pixelWidth || fb.height() < pixelHeight)
QSKIP("Window is smaller than the item under test");
const int offset = qRound(position * dpr);
const int firstColumn = qMax(0, offset);
const int lastColumn = qMin(fb.width(), offset + pixelWidth);
const int row = pixelHeight / 2;
int misplaced = 0;
int firstMisplaced = -1;
for (int x = firstColumn; x < lastColumn; ++x) {
const QRgb expected = ((x - offset) % 2) ? qRgb(0xff, 0xff, 0xff) : qRgb(0, 0, 0);
if (fb.pixel(x, row) != expected) {
++misplaced;
if (firstMisplaced < 0)
firstMisplaced = x;
}
}
QVERIFY2(misplaced == 0,
qPrintable(QStringLiteral("%1 of %2 pixels differ, first at x=%3")
.arg(misplaced).arg(lastColumn - firstColumn)
.arg(firstMisplaced)));
}
void tst_QQuickItemLayer::mirroringCheck(int mirroring, int x, bool shouldMirror, const QImage &fb)
{
int offset = 10;
int spacing = 25;
if (shouldMirror) {
switch (mirroring) {
case 0: { // No mirroring -> Visually Y gets swapped, X is default
QCOMPARE(fb.pixel(x + offset, offset), qRgb(0, 0xff, 0));
QCOMPARE(fb.pixel(x + offset + spacing, offset), qRgb(0, 0, 0xff));
QCOMPARE(fb.pixel(x + offset, offset + spacing), qRgb(0, 0, 0));
QCOMPARE(fb.pixel(x + offset + spacing, offset + spacing), qRgb(0xff, 0, 0));
break;
}
case 1: { // Horizontal mirroring -> Visually both X and Y get swapped, as neither is default
QCOMPARE(fb.pixel(x + offset, offset), qRgb(0, 0, 0xff));
QCOMPARE(fb.pixel(x + offset + spacing, offset), qRgb(0, 0xff, 0));
QCOMPARE(fb.pixel(x + offset, offset + spacing), qRgb(0xff, 0, 0));
QCOMPARE(fb.pixel(x + offset + spacing, offset + spacing), qRgb(0, 0, 0));
break;
}
case 2: { // Vertical mirroring -> The default case, nothing gets swapped
QCOMPARE(fb.pixel(x + offset, offset), qRgb(0, 0, 0));
QCOMPARE(fb.pixel(x + offset + spacing, offset), qRgb(0xff, 0, 0));
QCOMPARE(fb.pixel(x + offset, offset + spacing), qRgb(0, 0xff, 0));
QCOMPARE(fb.pixel(x + offset + spacing, offset + spacing), qRgb(0, 0, 0xff));
break;
}
case 3: { // Both axes mirrored -> Visually X gets swapped, Y is default
QCOMPARE(fb.pixel(x + offset, offset), qRgb(0xff, 0, 0));
QCOMPARE(fb.pixel(x + offset + spacing, offset), qRgb(0, 0, 0));
QCOMPARE(fb.pixel(x + offset, offset + spacing), qRgb(0, 0, 0xff));
QCOMPARE(fb.pixel(x + offset + spacing, offset + spacing), qRgb(0, 0xff, 0));
break;
}
default:
qWarning() << "Invalid case!";
break;
}
} else {
QCOMPARE(fb.pixel(x + offset, offset), qRgb(0, 0, 0));
QCOMPARE(fb.pixel(x + offset + spacing, offset), qRgb(0xff, 0, 0));
QCOMPARE(fb.pixel(x + offset, offset + spacing), qRgb(0, 0xff, 0));
QCOMPARE(fb.pixel(x + offset + spacing, offset + spacing), qRgb(0, 0, 0xff));
}
}
QTEST_MAIN(tst_QQuickItemLayer)
#include "tst_qquickitemlayer.moc"