/* SPDX-FileCopyrightText: 2024 Xaver Hugl SPDX-License-Identifier: GPL-2.0-or-later */ #include "kwin_wayland_test.h" #include "core/colorpipeline.h" #include "core/output.h" #include "core/outputbackend.h" #include "core/outputconfiguration.h" #include "outputconfigurationstore.h" #include "pointer_input.h" #include "tiles/tilemanager.h" #include "wayland/surface.h" #include "wayland_server.h" #include "window.h" #include "workspace.h" #include #include #include #include using namespace std::chrono_literals; namespace KWin { static const QString s_socketName = QStringLiteral("wayland_test_fifo-0"); class FifoTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testFifo_data(); void testFifo(); void testFifoInitiallyHidden(); void testFifoBarriersOnly(); void testFifoWaitOnly(); void testFifoOnSubsurfaces(); void testBarrierNotClearedByEmptyCommit(); }; class FifoV1Surface : public QObject, public QtWayland::wp_fifo_v1 { Q_OBJECT public: explicit FifoV1Surface(::wp_fifo_v1 *obj) : QtWayland::wp_fifo_v1(obj) { } ~FifoV1Surface() override { wp_fifo_v1_destroy(object()); } }; void FifoTest::initTestCase() { qRegisterMetaType(); QVERIFY(waylandServer()->init(s_socketName)); kwinApp()->start(); } void FifoTest::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::FifoV1 | Test::AdditionalWaylandInterface::PresentationTime)); Test::setupWaylandConnection(); workspace()->setActiveOutput(QPoint(640, 512)); input()->pointer()->warp(QPoint(640, 512)); } void FifoTest::cleanup() { Test::destroyWaylandConnection(); } void FifoTest::testFifo_data() { QTest::addColumn("refreshRate"); QTest::addRow("50Hz") << 50'000u; QTest::addRow("24Hz") << 24'000u; } void FifoTest::testFifo() { QFETCH(uint32_t, refreshRate); Test::setOutputConfig({ Test::OutputInfo{ .geometry = Rect(0, 0, 200, 200), .modes = { std::make_tuple(QSize(200, 200), refreshRate, OutputMode::Flag::Preferred), }, }, }); std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); QVERIFY(window); auto fifo = std::make_unique(Test::fifoManager()->get_fifo(*surface)); std::array, 3> frames; // commit 3 frames in quick succession: without fifo, the first two should be discarded, and the last should be presented for (auto &frame : frames) { frame = std::make_unique(Test::presentationTime()->feedback(*surface)); surface->commit(KWayland::Client::Surface::CommitFlag::None); } { QSignalSpy discarded1(frames[0].get(), &Test::WpPresentationFeedback::discarded); QSignalSpy discarded2(frames[1].get(), &Test::WpPresentationFeedback::discarded); QSignalSpy presented(frames[2].get(), &Test::WpPresentationFeedback::presented); QVERIFY(presented.wait(100)); QVERIFY(discarded1.count()); QVERIFY(discarded2.count()); } // do it again; this time with fifo, all frames should be presented fifo->set_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); for (auto &frame : frames) { frame = std::make_unique(Test::presentationTime()->feedback(*surface)); fifo->set_barrier(); fifo->wait_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); } { std::array spies = { QSignalSpy(frames[0].get(), &Test::WpPresentationFeedback::presented), QSignalSpy(frames[1].get(), &Test::WpPresentationFeedback::presented), QSignalSpy(frames[2].get(), &Test::WpPresentationFeedback::presented), }; for (size_t i = 0; i < frames.size(); i++) { QVERIFY(spies[i].wait(100)); if (i > 0) { // each frame should be presented in the refresh cycle after the last one const auto thisTimestamp = spies[i].last().at(0).value(); const auto lastTimestamp = spies[i - 1].last().at(0).value(); const auto refreshDuration = spies[i].last().at(1).value(); const auto diff = thisTimestamp - lastTimestamp; QCOMPARE_GT(diff, refreshDuration / 2); // sometimes, the compositor seems to drop a frame -> don't test for this // the only requirement for fifo is that the commit is shown for at least one frame // so this doesn't technically change the test result // QCOMPARE_LT(diff, refreshDuration * 3 / 2); } } } // even if the window is hidden, forward progress must be guaranteed window->setMinimized(true); for (auto &frame : frames) { frame = std::make_unique(Test::presentationTime()->feedback(*surface)); fifo->set_barrier(); fifo->wait_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); } // another commit, so that the last one will be discarded as well fifo->set_barrier(); fifo->wait_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); { const std::chrono::nanoseconds targetRefreshDuration = std::chrono::nanoseconds(1'000'000'000'000 / std::min(30'000u, refreshRate)); std::array spies = { QSignalSpy(frames[0].get(), &Test::WpPresentationFeedback::discarded), QSignalSpy(frames[1].get(), &Test::WpPresentationFeedback::discarded), QSignalSpy(frames[2].get(), &Test::WpPresentationFeedback::discarded), }; for (size_t i = 0; i < frames.size(); i++) { const auto before = std::chrono::steady_clock::now(); QVERIFY(spies[i].wait(100)); const auto diff = std::chrono::steady_clock::now() - before; QCOMPARE_GT(diff, targetRefreshDuration * 2 / 3); QCOMPARE_LT(diff, targetRefreshDuration * 2); } } } void FifoTest::testFifoInitiallyHidden() { // this test verifies that even when the window has never been presented once, // we still ensure forward progress std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); QVERIFY(window); window->setMinimized(true); auto fifo = std::make_unique(Test::fifoManager()->get_fifo(*surface)); std::array, 3> frames; for (auto &frame : frames) { frame = std::make_unique(Test::presentationTime()->feedback(*surface)); fifo->set_barrier(); fifo->wait_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); } // another commit, so that the last one will be discarded as well fifo->set_barrier(); fifo->wait_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); { std::array spies = { QSignalSpy(frames[0].get(), &Test::WpPresentationFeedback::discarded), QSignalSpy(frames[1].get(), &Test::WpPresentationFeedback::discarded), QSignalSpy(frames[2].get(), &Test::WpPresentationFeedback::discarded), }; for (size_t i = 0; i < frames.size(); i++) { const auto before = std::chrono::steady_clock::now(); QVERIFY(spies[i].wait(100)); const auto diff = std::chrono::steady_clock::now() - before; QCOMPARE_GT(diff, std::chrono::milliseconds(30)); QCOMPARE_LT(diff, std::chrono::milliseconds(100)); } } } void FifoTest::testFifoBarriersOnly() { // this test verifies that if you only set barriers but don't wait on them, // commits still get applied immediately std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); QVERIFY(window); auto fifo = std::make_unique(Test::fifoManager()->get_fifo(*surface)); std::array, 3> frames; for (auto &frame : frames) { frame = std::make_unique(Test::presentationTime()->feedback(*surface)); fifo->set_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); } // another commit, so that the last one will be discarded as well fifo->set_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); { std::array spies = { QSignalSpy(frames[0].get(), &Test::WpPresentationFeedback::discarded), QSignalSpy(frames[1].get(), &Test::WpPresentationFeedback::discarded), QSignalSpy(frames[2].get(), &Test::WpPresentationFeedback::discarded), }; for (auto &spy : spies) { QVERIFY(spy.count() || spy.wait(100)); } } } void FifoTest::testFifoWaitOnly() { // this test verifies that if you only wait on barriers but don't set any, // commits still get applied immediately std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); QVERIFY(window); auto fifo = std::make_unique(Test::fifoManager()->get_fifo(*surface)); std::array, 3> frames; for (auto &frame : frames) { frame = std::make_unique(Test::presentationTime()->feedback(*surface)); fifo->wait_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); } // another commit, so that the last one will be discarded as well fifo->wait_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); { std::array spies = { QSignalSpy(frames[0].get(), &Test::WpPresentationFeedback::discarded), QSignalSpy(frames[1].get(), &Test::WpPresentationFeedback::discarded), QSignalSpy(frames[2].get(), &Test::WpPresentationFeedback::discarded), }; for (auto &spy : spies) { QVERIFY(spy.count() || spy.wait(100)); } } } void FifoTest::testFifoOnSubsurfaces() { // this test verifies that fifo works correctly with subsurfaces Test::setOutputConfig({ Test::OutputInfo{ .geometry = Rect(0, 0, 200, 200), .modes = { std::make_tuple(QSize(200, 200), 60'000, OutputMode::Flag::Preferred), }, }, }); std::unique_ptr parentSurface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(parentSurface.get())); auto window = Test::renderAndWaitForShown(parentSurface.get(), QSize(100, 50), Qt::blue); QVERIFY(window); std::unique_ptr surface(Test::createSurface()); auto subsurface = Test::createSubSurface(surface.get(), parentSurface.get()); Test::render(surface.get(), QSize(100, 50), Qt::red); auto fifo = std::make_unique(Test::fifoManager()->get_fifo(*surface)); std::array, 3> frames; // when the surface is desync, fifo should work like on a toplevel surface subsurface->setMode(KWayland::Client::SubSurface::Mode::Desynchronized); for (auto &frame : frames) { frame = std::make_unique(Test::presentationTime()->feedback(*surface)); fifo->set_barrier(); fifo->wait_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); } // another commit, so that the last one will be discarded as well fifo->set_barrier(); fifo->wait_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); { std::array spies = { QSignalSpy(frames[0].get(), &Test::WpPresentationFeedback::presented), QSignalSpy(frames[1].get(), &Test::WpPresentationFeedback::presented), QSignalSpy(frames[2].get(), &Test::WpPresentationFeedback::presented), }; for (auto &spy : spies) { QVERIFY(spy.wait(100)); } } // when the surface is synchronized though, fifo should be ignored subsurface->setMode(KWayland::Client::SubSurface::Mode::Synchronized); for (auto &frame : frames) { frame = std::make_unique(Test::presentationTime()->feedback(*surface)); fifo->set_barrier(); fifo->wait_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); } // another commit, so that the last one will be discarded as well fifo->set_barrier(); fifo->wait_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); { std::array spies = { QSignalSpy(frames[0].get(), &Test::WpPresentationFeedback::discarded), QSignalSpy(frames[1].get(), &Test::WpPresentationFeedback::discarded), QSignalSpy(frames[2].get(), &Test::WpPresentationFeedback::discarded), }; for (auto &spy : spies) { QVERIFY(spy.count() || spy.wait(100)); } } } void FifoTest::testBarrierNotClearedByEmptyCommit() { std::unique_ptr surface(Test::createSurface()); std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue); QVERIFY(window); auto fifo = std::make_unique(Test::fifoManager()->get_fifo(*surface)); std::array, 2> frames; fifo->set_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); // this additional commit must not clear the barrier auto firstFrame = std::make_unique(Test::presentationTime()->feedback(*surface)); surface->commit(KWayland::Client::Surface::CommitFlag::None); auto secondFrame = std::make_unique(Test::presentationTime()->feedback(*surface)); fifo->wait_barrier(); surface->commit(KWayland::Client::Surface::CommitFlag::None); QSignalSpy firstSpy(firstFrame.get(), &Test::WpPresentationFeedback::presented); QVERIFY(firstSpy.wait(100)); QSignalSpy secondSpy(secondFrame.get(), &Test::WpPresentationFeedback::presented); QVERIFY(secondSpy.wait(100)); // the second frame should be presented in the refresh cycle after the first one const auto thisTimestamp = secondSpy.last().at(0).value(); const auto lastTimestamp = firstSpy.last().at(0).value(); const auto refreshDuration = secondSpy.last().at(1).value(); const auto diff = thisTimestamp - lastTimestamp; QCOMPARE_GT(diff, refreshDuration / 2); } } WAYLANDTEST_MAIN(KWin::FifoTest) #include "test_fifo.moc"