/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2016 Martin Gräßlin SPDX-License-Identifier: GPL-2.0-or-later */ #include "kwin_wayland_test.h" #include "composite.h" #include "cursor.h" #include "effectloader.h" #include "effects.h" #include "platform.h" #include "wayland/shmclientbuffer.h" #include "wayland/surface_interface.h" #include "wayland_server.h" #include "workspace.h" #include "x11window.h" #include #include #include #include #include #include #include using namespace KWin; static const QString s_socketName = QStringLiteral("wayland_test_kwin_scene_qpainter-0"); class SceneQPainterTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanup(); void testStartFrame(); void testCursorMoving(); void testWindow(); void testWindowScaled(); void testCompositorRestart(); void testX11Window(); private: QImage grab(Output *output); }; void SceneQPainterTest::cleanup() { Test::destroyWaylandConnection(); } void SceneQPainterTest::initTestCase() { qRegisterMetaType(); QSignalSpy applicationStartedSpy(kwinApp(), &Application::started); QVERIFY(applicationStartedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName)); // disable all effects - we don't want to have it interact with the rendering auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); KConfigGroup plugins(config, QStringLiteral("Plugins")); const auto builtinNames = EffectLoader().listOfKnownEffects(); for (QString name : builtinNames) { plugins.writeEntry(name + QStringLiteral("Enabled"), false); } config->sync(); kwinApp()->setConfig(config); if (!QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("icons/DMZ-White/index.theme")).isEmpty()) { qputenv("XCURSOR_THEME", QByteArrayLiteral("DMZ-White")); } else { // might be vanilla-dmz (e.g. Arch, FreeBSD) qputenv("XCURSOR_THEME", QByteArrayLiteral("Vanilla-DMZ")); } qputenv("XCURSOR_SIZE", QByteArrayLiteral("24")); qputenv("KWIN_COMPOSE", QByteArrayLiteral("Q")); kwinApp()->start(); QVERIFY(applicationStartedSpy.wait()); QVERIFY(Compositor::self()); } QImage SceneQPainterTest::grab(Output *output) { QImage image; QMetaObject::invokeMethod(kwinApp()->platform(), "captureOutput", Qt::DirectConnection, Q_RETURN_ARG(QImage, image), Q_ARG(Output *, output)); return image; } void SceneQPainterTest::testStartFrame() { // this test verifies that the initial rendering is correct Compositor::self()->scene()->addRepaintFull(); auto scene = Compositor::self()->scene(); QVERIFY(scene); QCOMPARE(kwinApp()->platform()->selectedCompositor(), QPainterCompositing); QSignalSpy frameRenderedSpy(scene, &Scene::frameRendered); QVERIFY(frameRenderedSpy.isValid()); QVERIFY(frameRenderedSpy.wait()); // now let's render a reference image for comparison QImage referenceImage(QSize(1280, 1024), QImage::Format_RGB32); referenceImage.fill(Qt::black); QPainter p(&referenceImage); auto cursor = KWin::Cursors::self()->mouse(); const QImage cursorImage = cursor->image(); QVERIFY(!cursorImage.isNull()); p.drawImage(cursor->pos() - cursor->hotspot(), cursorImage); const auto outputs = kwinApp()->platform()->enabledOutputs(); QCOMPARE(referenceImage, grab(outputs.constFirst())); } void SceneQPainterTest::testCursorMoving() { // this test verifies that rendering is correct also after moving the cursor a few times auto scene = Compositor::self()->scene(); QVERIFY(scene); QSignalSpy frameRenderedSpy(scene, &Scene::frameRendered); QVERIFY(frameRenderedSpy.isValid()); KWin::Cursors::self()->mouse()->setPos(0, 0); QVERIFY(frameRenderedSpy.wait()); KWin::Cursors::self()->mouse()->setPos(10, 0); QVERIFY(frameRenderedSpy.wait()); KWin::Cursors::self()->mouse()->setPos(10, 12); QVERIFY(frameRenderedSpy.wait()); KWin::Cursors::self()->mouse()->setPos(12, 14); QVERIFY(frameRenderedSpy.wait()); KWin::Cursors::self()->mouse()->setPos(50, 60); QVERIFY(frameRenderedSpy.wait()); KWin::Cursors::self()->mouse()->setPos(45, 45); QVERIFY(frameRenderedSpy.wait()); // now let's render a reference image for comparison QImage referenceImage(QSize(1280, 1024), QImage::Format_RGB32); referenceImage.fill(Qt::black); QPainter p(&referenceImage); auto cursor = Cursors::self()->currentCursor(); const QImage cursorImage = cursor->image(); QVERIFY(!cursorImage.isNull()); p.drawImage(QPoint(45, 45) - cursor->hotspot(), cursorImage); const auto outputs = kwinApp()->platform()->enabledOutputs(); QCOMPARE(referenceImage, grab(outputs.constFirst())); } void SceneQPainterTest::testWindow() { KWin::Cursors::self()->mouse()->setPos(45, 45); // this test verifies that a window is rendered correctly using namespace KWayland::Client; QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); QVERIFY(Test::waitForWaylandPointer()); QScopedPointer s(Test::createSurface()); QScopedPointer ss(Test::createXdgToplevelSurface(s.data())); QScopedPointer p(Test::waylandSeat()->createPointer()); auto scene = KWin::Compositor::self()->scene(); QVERIFY(scene); QSignalSpy frameRenderedSpy(scene, &Scene::frameRendered); QVERIFY(frameRenderedSpy.isValid()); // now let's map the window QVERIFY(Test::renderAndWaitForShown(s.data(), QSize(200, 300), Qt::blue)); // which should trigger a frame if (frameRenderedSpy.isEmpty()) { QVERIFY(frameRenderedSpy.wait()); } // we didn't set a cursor image on the surface yet, so it should be just black + window and previous cursor QImage referenceImage(QSize(1280, 1024), QImage::Format_RGB32); referenceImage.fill(Qt::black); QPainter painter(&referenceImage); painter.fillRect(0, 0, 200, 300, Qt::blue); // now let's set a cursor image QScopedPointer cs(Test::createSurface()); QVERIFY(!cs.isNull()); Test::render(cs.data(), QSize(10, 10), Qt::red); p->setCursor(cs.data(), QPoint(5, 5)); QVERIFY(frameRenderedSpy.wait()); painter.fillRect(KWin::Cursors::self()->mouse()->pos().x() - 5, KWin::Cursors::self()->mouse()->pos().y() - 5, 10, 10, Qt::red); const auto outputs = kwinApp()->platform()->enabledOutputs(); QCOMPARE(referenceImage, grab(outputs.constFirst())); // let's move the cursor again KWin::Cursors::self()->mouse()->setPos(10, 10); QVERIFY(frameRenderedSpy.wait()); painter.fillRect(0, 0, 200, 300, Qt::blue); painter.fillRect(5, 5, 10, 10, Qt::red); QCOMPARE(referenceImage, grab(outputs.constFirst())); } void SceneQPainterTest::testWindowScaled() { KWin::Cursors::self()->mouse()->setPos(10, 10); // this test verifies that a window is rendered correctly using namespace KWayland::Client; QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); QVERIFY(Test::waitForWaylandPointer()); QScopedPointer s(Test::createSurface()); QScopedPointer ss(Test::createXdgToplevelSurface(s.data())); QScopedPointer p(Test::waylandSeat()->createPointer()); QSignalSpy pointerEnteredSpy(p.data(), &Pointer::entered); QVERIFY(pointerEnteredSpy.isValid()); auto scene = KWin::Compositor::self()->scene(); QVERIFY(scene); QSignalSpy frameRenderedSpy(scene, &Scene::frameRendered); QVERIFY(frameRenderedSpy.isValid()); // now let's set a cursor image QScopedPointer cs(Test::createSurface()); QVERIFY(!cs.isNull()); Test::render(cs.data(), QSize(10, 10), Qt::red); // now let's map the window s->setScale(2); // draw a blue square@400x600 with red rectangle@200x200 in the middle const QSize size(400, 600); QImage img(size, QImage::Format_ARGB32_Premultiplied); img.fill(Qt::blue); QPainter surfacePainter(&img); surfacePainter.fillRect(200, 300, 200, 200, Qt::red); // add buffer Test::render(s.data(), img); QVERIFY(pointerEnteredSpy.wait()); p->setCursor(cs.data(), QPoint(5, 5)); // which should trigger a frame QVERIFY(frameRenderedSpy.wait()); QImage referenceImage(QSize(1280, 1024), QImage::Format_RGB32); referenceImage.fill(Qt::black); QPainter painter(&referenceImage); painter.fillRect(0, 0, 200, 300, Qt::blue); painter.fillRect(100, 150, 100, 100, Qt::red); painter.fillRect(5, 5, 10, 10, Qt::red); // cursor const auto outputs = kwinApp()->platform()->enabledOutputs(); QCOMPARE(referenceImage, grab(outputs.constFirst())); } void SceneQPainterTest::testCompositorRestart() { // this test verifies that the compositor/SceneQPainter survive a restart of the compositor and still render correctly KWin::Cursors::self()->mouse()->setPos(400, 400); // first create a window using namespace KWayland::Client; QVERIFY(Test::setupWaylandConnection()); QScopedPointer s(Test::createSurface()); QScopedPointer ss(Test::createXdgToplevelSurface(s.data())); QVERIFY(Test::renderAndWaitForShown(s.data(), QSize(200, 300), Qt::blue)); // now let's try to reinitialize the compositing scene auto oldScene = KWin::Compositor::self()->scene(); QVERIFY(oldScene); QSignalSpy sceneCreatedSpy(KWin::Compositor::self(), &KWin::Compositor::sceneCreated); QVERIFY(sceneCreatedSpy.isValid()); KWin::Compositor::self()->reinitialize(); if (sceneCreatedSpy.isEmpty()) { QVERIFY(sceneCreatedSpy.wait()); } QCOMPARE(sceneCreatedSpy.count(), 1); auto scene = KWin::Compositor::self()->scene(); QVERIFY(scene); // this should directly trigger a frame KWin::Compositor::self()->scene()->addRepaintFull(); QSignalSpy frameRenderedSpy(scene, &Scene::frameRendered); QVERIFY(frameRenderedSpy.isValid()); QVERIFY(frameRenderedSpy.wait()); // render reference image QImage referenceImage(QSize(1280, 1024), QImage::Format_RGB32); referenceImage.fill(Qt::black); QPainter painter(&referenceImage); painter.fillRect(0, 0, 200, 300, Qt::blue); auto cursor = Cursors::self()->mouse(); const QImage cursorImage = cursor->image(); QVERIFY(!cursorImage.isNull()); painter.drawImage(QPoint(400, 400) - cursor->hotspot(), cursorImage); const auto outputs = kwinApp()->platform()->enabledOutputs(); QCOMPARE(referenceImage, grab(outputs.constFirst())); } struct XcbConnectionDeleter { static inline void cleanup(xcb_connection_t *pointer) { xcb_disconnect(pointer); } }; static bool waitForXwaylandBuffer(Window *window, const QSize &size) { // Usually, when an Xwayland surface is created, it has a buffer of size 1x1, // a buffer with the correct size will be committed a bit later. KWaylandServer::SurfaceInterface *surface = window->surface(); int attemptCount = 0; do { if (surface->buffer() && surface->buffer()->size() == size) { return true; } QSignalSpy committedSpy(surface, &KWaylandServer::SurfaceInterface::committed); if (!committedSpy.wait()) { return false; } ++attemptCount; } while (attemptCount <= 3); return false; } void SceneQPainterTest::testX11Window() { // this test verifies the condition of BUG: 382748 // create X11 window QSignalSpy windowAddedSpy(effects, &EffectsHandler::windowAdded); QVERIFY(windowAddedSpy.isValid()); // create an xcb window QScopedPointer c(xcb_connect(nullptr, nullptr)); QVERIFY(!xcb_connection_has_error(c.data())); const QRect windowGeometry(0, 0, 100, 200); xcb_window_t w = xcb_generate_id(c.data()); uint32_t value = kwinApp()->x11DefaultScreen()->white_pixel; xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), windowGeometry.x(), windowGeometry.y(), windowGeometry.width(), windowGeometry.height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, XCB_CW_BACK_PIXEL, &value); xcb_size_hints_t hints; memset(&hints, 0, sizeof(hints)); xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); xcb_map_window(c.data(), w); xcb_flush(c.data()); // we should get a client for it QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); QVERIFY(windowCreatedSpy.isValid()); QVERIFY(windowCreatedSpy.wait()); X11Window *client = windowCreatedSpy.first().first().value(); QVERIFY(client); QCOMPARE(client->window(), w); QCOMPARE(client->clientSize(), QSize(100, 200)); QVERIFY(Test::waitForWaylandSurface(client)); QVERIFY(waitForXwaylandBuffer(client, client->size())); QImage compareImage(client->clientSize(), QImage::Format_RGB32); compareImage.fill(Qt::white); auto buffer = qobject_cast(client->surface()->buffer()); QCOMPARE(buffer->data().copy(QRect(client->clientPos(), client->clientSize())), compareImage); // enough time for rendering the window QTest::qWait(100); auto scene = KWin::Compositor::self()->scene(); QVERIFY(scene); // this should directly trigger a frame KWin::Compositor::self()->scene()->addRepaintFull(); QSignalSpy frameRenderedSpy(scene, &Scene::frameRendered); QVERIFY(frameRenderedSpy.isValid()); QVERIFY(frameRenderedSpy.wait()); const QPoint startPos = client->pos() + client->clientPos(); auto image = grab(kwinApp()->platform()->enabledOutputs().constFirst()); QCOMPARE(image.copy(QRect(startPos, client->clientSize())), compareImage); // and destroy the window again xcb_unmap_window(c.data(), w); xcb_flush(c.data()); QSignalSpy windowClosedSpy(client, &X11Window::windowClosed); QVERIFY(windowClosedSpy.isValid()); QVERIFY(windowClosedSpy.wait()); xcb_destroy_window(c.data(), w); c.reset(); } WAYLANDTEST_MAIN(SceneQPainterTest) #include "scene_qpainter_test.moc"