/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2022 Xaver Hugl SPDX-License-Identifier: GPL-2.0-or-later */ #include #include #include #include "mock_drm.h" #include "core/outputlayer.h" #include "core/session.h" #include "drm_backend.h" #include "drm_connector.h" #include "drm_crtc.h" #include "drm_egl_backend.h" #include "drm_gpu.h" #include "drm_output.h" #include "drm_pipeline.h" #include "drm_plane.h" #include "drm_pointer.h" #include "qpainter/qpainterbackend.h" #include #include #include using namespace KWin; static std::unique_ptr findPrimaryDevice(int crtcCount) { #if !HAVE_LIBDRM_FAUX #if defined(Q_OS_LINUX) // Workaround for libdrm being unaware of faux bus. if (qEnvironmentVariableIsSet("CI")) { int fd = open("/dev/dri/card1", O_RDWR | O_CLOEXEC); if (fd == -1) { return nullptr; } else { return std::make_unique(fd, "/dev/dri/card1", crtcCount); } } #endif #endif const int deviceCount = drmGetDevices2(0, nullptr, 0); if (deviceCount <= 0) { return nullptr; } QList devices(deviceCount); if (drmGetDevices2(0, devices.data(), devices.size()) < 0) { return nullptr; } auto deviceCleanup = qScopeGuard([&devices]() { drmFreeDevices(devices.data(), devices.size()); }); for (drmDevice *device : std::as_const(devices)) { if (device->available_nodes & (1 << DRM_NODE_PRIMARY)) { int fd = open(device->nodes[DRM_NODE_PRIMARY], O_RDWR | O_CLOEXEC); if (fd != -1) { return std::make_unique(fd, device->nodes[DRM_NODE_PRIMARY], crtcCount); } } } return nullptr; } class DrmTest : public QObject { Q_OBJECT private Q_SLOTS: void testAmsDetection(); void testOutputDetection_data(); void testOutputDetection(); void testZeroModesHandling(); void testModeGeneration_data(); void testModeGeneration(); void testConnectorLifetime(); void testModeset_data(); void testModeset(); void testVrrChange(); }; static void verifyCleanup(MockGpu *mockGpu) { QVERIFY(mockGpu->drmConnectors.isEmpty()); QVERIFY(mockGpu->drmEncoders.isEmpty()); QVERIFY(mockGpu->drmCrtcs.isEmpty()); QVERIFY(mockGpu->drmPlanes.isEmpty()); QVERIFY(mockGpu->drmPlaneRes.isEmpty()); QVERIFY(mockGpu->fbs.isEmpty()); QVERIFY(mockGpu->drmProps.isEmpty()); QVERIFY(mockGpu->drmObjectProperties.isEmpty()); QVERIFY(mockGpu->drmPropertyBlobs.isEmpty()); } void DrmTest::testAmsDetection() { const auto session = Session::create(Session::Type::Noop); const auto backend = std::make_unique(session.get()); // gpu without planes should use legacy mode { const auto mockGpu = findPrimaryDevice(0); auto gpu = std::make_unique(backend.get(), mockGpu->fd, DrmDevice::open(mockGpu->devNode)); QVERIFY(!gpu->atomicModeSetting()); } // gpu with planes should use AMS { const auto mockGpu = findPrimaryDevice(0); mockGpu->planes << std::make_shared(mockGpu.get(), PlaneType::Primary, 0); auto gpu = std::make_unique(backend.get(), mockGpu->fd, DrmDevice::open(mockGpu->devNode)); gpu = std::make_unique(backend.get(), mockGpu->fd, DrmDevice::open(mockGpu->devNode)); QVERIFY(gpu->atomicModeSetting()); } // but not if the kernel doesn't allow it { const auto mockGpu = findPrimaryDevice(0); mockGpu->deviceCaps[MOCKDRM_DEVICE_CAP_ATOMIC] = 0; auto gpu = std::make_unique(backend.get(), mockGpu->fd, DrmDevice::open(mockGpu->devNode)); QVERIFY(!gpu->atomicModeSetting()); gpu.reset(); verifyCleanup(mockGpu.get()); } } void DrmTest::testOutputDetection_data() { QTest::addColumn("atomicModeSetting"); QTest::addRow("AMS") << true; QTest::addRow("legacy") << false; } void DrmTest::testOutputDetection() { const auto mockGpu = findPrimaryDevice(5); QFETCH(bool, atomicModeSetting); mockGpu->deviceCaps[MOCKDRM_DEVICE_CAP_ATOMIC] = atomicModeSetting; const auto one = std::make_shared(mockGpu.get()); const auto two = std::make_shared(mockGpu.get()); const auto vr = std::make_shared(mockGpu.get(), true); mockGpu->connectors.push_back(one); mockGpu->connectors.push_back(two); mockGpu->connectors.push_back(vr); const auto session = Session::create(Session::Type::Noop); const auto backend = std::make_unique(session.get()); const auto renderBackend = backend->createQPainterBackend(); auto gpu = std::make_unique(backend.get(), mockGpu->fd, DrmDevice::open(mockGpu->devNode)); QVERIFY(gpu->updateOutputs()); // 3 outputs should be detected, one of them non-desktop const auto outputs = gpu->drmOutputs(); QCOMPARE(outputs.size(), 3); const auto vrOutput = std::find_if(outputs.begin(), outputs.end(), [](const auto &output) { return output->isNonDesktop(); }); QVERIFY(vrOutput != outputs.end()); QVERIFY(static_cast(*vrOutput)->connector()->id() == vr->id); // test hotunplugging mockGpu->connectors.removeOne(one); QVERIFY(gpu->updateOutputs()); QCOMPARE(gpu->drmOutputs().size(), 2); // test hotplugging mockGpu->connectors.push_back(one); QVERIFY(gpu->updateOutputs()); QCOMPARE(gpu->drmOutputs().size(), 3); // connector state changing to disconnected should count as a hotunplug one->connection = DRM_MODE_DISCONNECTED; QVERIFY(gpu->updateOutputs()); QCOMPARE(gpu->drmOutputs().size(), 2); // don't crash if all connectors are disconnected two->connection = DRM_MODE_DISCONNECTED; vr->connection = DRM_MODE_DISCONNECTED; QVERIFY(gpu->updateOutputs()); QVERIFY(gpu->drmOutputs().empty()); gpu.reset(); verifyCleanup(mockGpu.get()); } void DrmTest::testZeroModesHandling() { const auto mockGpu = findPrimaryDevice(5); const auto conn = std::make_shared(mockGpu.get()); mockGpu->connectors.push_back(conn); const auto session = Session::create(Session::Type::Noop); const auto backend = std::make_unique(session.get()); const auto renderBackend = backend->createQPainterBackend(); auto gpu = std::make_unique(backend.get(), mockGpu->fd, DrmDevice::open(mockGpu->devNode)); // connector with zero modes should be ignored conn->modes.clear(); QVERIFY(gpu->updateOutputs()); QVERIFY(gpu->drmOutputs().empty()); // once it has modes, it should be detected conn->addMode(1920, 1080, 60); QVERIFY(gpu->updateOutputs()); QCOMPARE(gpu->drmOutputs().size(), 1); // if an update says it has no modes anymore but it's still connected, ignore that conn->modes.clear(); QVERIFY(gpu->updateOutputs()); QCOMPARE(gpu->drmOutputs().size(), 1); QVERIFY(!gpu->drmOutputs().constFirst()->modes().empty()); gpu.reset(); verifyCleanup(mockGpu.get()); } // because QFETCH as a macro doesn't like commas in the type name using Mode = std::pair; void DrmTest::testModeGeneration_data() { QTest::addColumn("nativeMode"); QTest::addColumn>("expectedModes"); QTest::newRow("2160p") << std::make_pair(QSize(3840, 2160), 60u) << QList{ std::make_pair(QSize(1600, 1200), 60), std::make_pair(QSize(1280, 1024), 60), std::make_pair(QSize(1024, 768), 60), std::make_pair(QSize(2560, 1600), 60), std::make_pair(QSize(1920, 1200), 60), std::make_pair(QSize(1280, 800), 60), std::make_pair(QSize(3840, 2160), 60), std::make_pair(QSize(3200, 1800), 60), std::make_pair(QSize(2880, 1620), 60), std::make_pair(QSize(2560, 1440), 60), std::make_pair(QSize(1920, 1080), 60), std::make_pair(QSize(1600, 900), 60), std::make_pair(QSize(1368, 768), 60), std::make_pair(QSize(1280, 720), 60), }; QTest::newRow("2160p") << std::make_pair(QSize(3840, 2160), 120u) << QList{ std::make_pair(QSize(1600, 1200), 120), std::make_pair(QSize(1280, 1024), 120), std::make_pair(QSize(1024, 768), 120), std::make_pair(QSize(2560, 1600), 120), std::make_pair(QSize(1920, 1200), 120), std::make_pair(QSize(1280, 800), 120), std::make_pair(QSize(3840, 2160), 120), std::make_pair(QSize(3200, 1800), 120), std::make_pair(QSize(2880, 1620), 120), std::make_pair(QSize(2560, 1440), 120), std::make_pair(QSize(1920, 1080), 120), std::make_pair(QSize(1600, 900), 120), std::make_pair(QSize(1368, 768), 120), std::make_pair(QSize(1280, 720), 120), std::make_pair(QSize(1600, 1200), 60), std::make_pair(QSize(1280, 1024), 60), std::make_pair(QSize(1024, 768), 60), std::make_pair(QSize(2560, 1600), 60), std::make_pair(QSize(1920, 1200), 60), std::make_pair(QSize(1280, 800), 60), std::make_pair(QSize(3840, 2160), 60), std::make_pair(QSize(3200, 1800), 60), std::make_pair(QSize(2880, 1620), 60), std::make_pair(QSize(2560, 1440), 60), std::make_pair(QSize(1920, 1080), 60), std::make_pair(QSize(1600, 900), 60), std::make_pair(QSize(1368, 768), 60), std::make_pair(QSize(1280, 720), 60), }; QTest::newRow("1440p") << std::make_pair(QSize(2560, 1440), 60u) << QList{ std::make_pair(QSize(1600, 1200), 60), std::make_pair(QSize(1280, 1024), 60), std::make_pair(QSize(1024, 768), 60), std::make_pair(QSize(1920, 1200), 60), std::make_pair(QSize(1280, 800), 60), std::make_pair(QSize(2560, 1440), 60), std::make_pair(QSize(1920, 1080), 60), std::make_pair(QSize(1600, 900), 60), std::make_pair(QSize(1368, 768), 60), std::make_pair(QSize(1280, 720), 60), }; QTest::newRow("1080p") << std::make_pair(QSize(1920, 1080), 60u) << QList{ std::make_pair(QSize(1280, 1024), 60), std::make_pair(QSize(1024, 768), 60), std::make_pair(QSize(1280, 800), 60), std::make_pair(QSize(1920, 1080), 60), std::make_pair(QSize(1600, 900), 60), std::make_pair(QSize(1368, 768), 60), std::make_pair(QSize(1280, 720), 60), }; QTest::newRow("2160p 21:9") << std::make_pair(QSize(5120, 2160), 60u) << QList{ std::make_pair(QSize(5120, 2160), 60), std::make_pair(QSize(1600, 1200), 60), std::make_pair(QSize(1280, 1024), 60), std::make_pair(QSize(1024, 768), 60), std::make_pair(QSize(2560, 1600), 60), std::make_pair(QSize(1920, 1200), 60), std::make_pair(QSize(1280, 800), 60), std::make_pair(QSize(3840, 2160), 60), std::make_pair(QSize(3200, 1800), 60), std::make_pair(QSize(2880, 1620), 60), std::make_pair(QSize(2560, 1440), 60), std::make_pair(QSize(1920, 1080), 60), std::make_pair(QSize(1600, 900), 60), std::make_pair(QSize(1368, 768), 60), std::make_pair(QSize(1280, 720), 60), }; QTest::newRow("1440p 21:9") << std::make_pair(QSize(3440, 1440), 60u) << QList{ std::make_pair(QSize(3440, 1440), 60), std::make_pair(QSize(1600, 1200), 60), std::make_pair(QSize(1280, 1024), 60), std::make_pair(QSize(1024, 768), 60), std::make_pair(QSize(1920, 1200), 60), std::make_pair(QSize(1280, 800), 60), std::make_pair(QSize(2560, 1440), 60), std::make_pair(QSize(1920, 1080), 60), std::make_pair(QSize(1600, 900), 60), std::make_pair(QSize(1368, 768), 60), std::make_pair(QSize(1280, 720), 60), }; QTest::newRow("1080p 21:9") << std::make_pair(QSize(2560, 1080), 60u) << QList{ std::make_pair(QSize(2560, 1080), 60), std::make_pair(QSize(1280, 1024), 60), std::make_pair(QSize(1024, 768), 60), std::make_pair(QSize(1280, 800), 60), std::make_pair(QSize(1920, 1080), 60), std::make_pair(QSize(1600, 900), 60), std::make_pair(QSize(1368, 768), 60), std::make_pair(QSize(1280, 720), 60), }; } void DrmTest::testModeGeneration() { const auto mockGpu = findPrimaryDevice(5); const auto conn = std::make_shared(mockGpu.get()); mockGpu->connectors.push_back(conn); const auto session = Session::create(Session::Type::Noop); const auto backend = std::make_unique(session.get()); const auto renderBackend = backend->createQPainterBackend(); auto gpu = std::make_unique(backend.get(), mockGpu->fd, DrmDevice::open(mockGpu->devNode)); QFETCH(Mode, nativeMode); QFETCH(QList, expectedModes); const auto &[nativeSize, nativeRefreshHz] = nativeMode; conn->modes.clear(); conn->addMode(nativeSize.width(), nativeSize.height(), nativeRefreshHz); QVERIFY(gpu->updateOutputs()); QCOMPARE(gpu->drmOutputs().size(), 1); // no mode generation without the scaling property QCOMPARE(gpu->drmOutputs().front()->modes().size(), 1); mockGpu->connectors.removeAll(conn); QVERIFY(gpu->updateOutputs()); conn->props.emplace_back(conn.get(), QStringLiteral("scaling mode"), 0, DRM_MODE_PROP_ENUM, QList{"None", "Full", "Center", "Full aspect"}); mockGpu->connectors.push_back(conn); QVERIFY(gpu->updateOutputs()); DrmOutput *const output = gpu->drmOutputs().front(); QCOMPARE(output->modes().size(), expectedModes.size()); for (const auto &mode : output->modes()) { const auto it = std::ranges::find_if(expectedModes, [mode](const auto &expectedMode) { return mode->size() == expectedMode.first && std::round(mode->refreshRate() / 1000.0) == expectedMode.second; }); QVERIFY(it != expectedModes.end()); QVERIFY(mode->size().width() <= nativeSize.width()); QVERIFY(mode->size().height() <= nativeSize.height()); QCOMPARE_LE(mode->refreshRate(), nativeRefreshHz * 1000); } gpu.reset(); verifyCleanup(mockGpu.get()); } void DrmTest::testConnectorLifetime() { // don't crash if output lifetime is extended beyond the connector const auto mockGpu = findPrimaryDevice(5); const auto conn = std::make_shared(mockGpu.get()); mockGpu->connectors.push_back(conn); const auto session = Session::create(Session::Type::Noop); const auto backend = std::make_unique(session.get()); const auto renderBackend = backend->createQPainterBackend(); auto gpu = std::make_unique(backend.get(), mockGpu->fd, DrmDevice::open(mockGpu->devNode)); QVERIFY(gpu->updateOutputs()); QCOMPARE(gpu->drmOutputs().size(), 1); DrmOutput *const output = gpu->drmOutputs().front(); output->ref(); mockGpu->connectors.clear(); QVERIFY(gpu->updateOutputs()); output->unref(); gpu.reset(); verifyCleanup(mockGpu.get()); } void DrmTest::testModeset_data() { QTest::addColumn("AMS"); // TODO to uncomment this, implement page flip callbacks // QTest::newRow("disabled") << 0; QTest::newRow("enabled") << 1; } void DrmTest::testModeset() { // to reenable, make this part of an integration test, so that kwinApp() isn't nullptr QSKIP("this test needs output pipelines to be enabled by default, which is no longer the case"); // test if doing a modeset would succeed QFETCH(int, AMS); const auto mockGpu = findPrimaryDevice(5); mockGpu->deviceCaps[MOCKDRM_DEVICE_CAP_ATOMIC] = AMS; const auto conn = std::make_shared(mockGpu.get()); mockGpu->connectors.push_back(conn); const auto session = Session::create(Session::Type::Noop); const auto backend = std::make_unique(session.get()); const auto renderBackend = backend->createQPainterBackend(); auto gpu = std::make_unique(backend.get(), mockGpu->fd, DrmDevice::open(mockGpu->devNode)); QVERIFY(gpu->updateOutputs()); QCOMPARE(gpu->drmOutputs().size(), 1); const auto output = gpu->drmOutputs().front(); const auto layer = renderBackend->compatibleOutputLayers(output).front(); layer->beginFrame(); output->renderLoop()->prepareNewFrame(); const auto frame = std::make_shared(output->renderLoop(), std::chrono::nanoseconds(1'000'000'000'000 / output->refreshRate())); layer->endFrame(Region::infinite(), Region::infinite(), frame.get()); QVERIFY(output->present({layer}, frame)); gpu.reset(); verifyCleanup(mockGpu.get()); } void DrmTest::testVrrChange() { const auto mockGpu = findPrimaryDevice(5); mockGpu->deviceCaps[MOCKDRM_DEVICE_CAP_ATOMIC] = 1; const auto conn = std::make_shared(mockGpu.get()); conn->setVrrCapable(false); mockGpu->connectors.push_back(conn); const auto session = Session::create(Session::Type::Noop); const auto backend = std::make_unique(session.get()); const auto renderBackend = backend->createQPainterBackend(); auto gpu = std::make_unique(backend.get(), mockGpu->fd, DrmDevice::open(mockGpu->devNode)); QVERIFY(gpu->updateOutputs()); const auto output = gpu->drmOutputs().front(); QVERIFY(!(output->capabilities() & BackendOutput::Capability::Vrr)); QSignalSpy capsChanged(output, &BackendOutput::capabilitiesChanged); conn->setVrrCapable(true); QVERIFY(gpu->updateOutputs()); QCOMPARE(gpu->drmOutputs().front(), output); QCOMPARE(capsChanged.count(), 1); QVERIFY(output->capabilities() & BackendOutput::Capability::Vrr); } QTEST_GUILESS_MAIN(DrmTest) #include "mockDrmTest.moc"