File indexing completed on 2024-05-12 05:30:26

0001 /*
0002     KWin - the KDE window manager
0003     This file is part of the KDE project.
0004 
0005     SPDX-FileCopyrightText: 2022 Xaver Hugl <xaver.hugl@gmail.com>
0006 
0007     SPDX-License-Identifier: GPL-2.0-or-later
0008 */
0009 
0010 #include <QSize>
0011 #include <QTest>
0012 
0013 #include "mock_drm.h"
0014 
0015 #include "core/outputlayer.h"
0016 #include "core/session.h"
0017 #include "drm_backend.h"
0018 #include "drm_connector.h"
0019 #include "drm_crtc.h"
0020 #include "drm_egl_backend.h"
0021 #include "drm_gpu.h"
0022 #include "drm_output.h"
0023 #include "drm_pipeline.h"
0024 #include "drm_plane.h"
0025 #include "drm_pointer.h"
0026 #include "platformsupport/scenes/qpainter/qpainterbackend.h"
0027 
0028 #include <drm_fourcc.h>
0029 #include <fcntl.h>
0030 #include <sys/utsname.h>
0031 
0032 using namespace KWin;
0033 
0034 static std::unique_ptr<MockGpu> findPrimaryDevice(int crtcCount)
0035 {
0036     const int deviceCount = drmGetDevices2(0, nullptr, 0);
0037     if (deviceCount <= 0) {
0038         return nullptr;
0039     }
0040 
0041     QList<drmDevice *> devices(deviceCount);
0042     if (drmGetDevices2(0, devices.data(), devices.size()) < 0) {
0043         return nullptr;
0044     }
0045     auto deviceCleanup = qScopeGuard([&devices]() {
0046         drmFreeDevices(devices.data(), devices.size());
0047     });
0048 
0049     for (drmDevice *device : std::as_const(devices)) {
0050         if (device->available_nodes & (1 << DRM_NODE_PRIMARY)) {
0051             int fd = open(device->nodes[DRM_NODE_PRIMARY], O_RDWR | O_CLOEXEC);
0052             if (fd != -1) {
0053                 return std::make_unique<MockGpu>(fd, device->nodes[DRM_NODE_PRIMARY], crtcCount);
0054             }
0055         }
0056     }
0057 
0058     return nullptr;
0059 }
0060 
0061 class DrmTest : public QObject
0062 {
0063     Q_OBJECT
0064 private Q_SLOTS:
0065     void testAmsDetection();
0066     void testOutputDetection();
0067     void testZeroModesHandling();
0068     void testModeGeneration_data();
0069     void testModeGeneration();
0070     void testConnectorLifetime();
0071     void testModeset_data();
0072     void testModeset();
0073 };
0074 
0075 static void verifyCleanup(MockGpu *mockGpu)
0076 {
0077     QVERIFY(mockGpu->drmConnectors.isEmpty());
0078     QVERIFY(mockGpu->drmEncoders.isEmpty());
0079     QVERIFY(mockGpu->drmCrtcs.isEmpty());
0080     QVERIFY(mockGpu->drmPlanes.isEmpty());
0081     QVERIFY(mockGpu->drmPlaneRes.isEmpty());
0082     QVERIFY(mockGpu->fbs.isEmpty());
0083     QVERIFY(mockGpu->drmProps.isEmpty());
0084     QVERIFY(mockGpu->drmObjectProperties.isEmpty());
0085     QVERIFY(mockGpu->drmPropertyBlobs.isEmpty());
0086 }
0087 
0088 void DrmTest::testAmsDetection()
0089 {
0090     const auto session = Session::create(Session::Type::Noop);
0091     const auto backend = std::make_unique<DrmBackend>(session.get());
0092 
0093     // gpu without planes should use legacy mode
0094     {
0095         const auto mockGpu = findPrimaryDevice(0);
0096         auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
0097         QVERIFY(!gpu->atomicModeSetting());
0098     }
0099 
0100     // gpu with planes should use AMS
0101     {
0102         const auto mockGpu = findPrimaryDevice(0);
0103         mockGpu->planes << std::make_shared<MockPlane>(mockGpu.get(), PlaneType::Primary, 0);
0104         auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
0105         gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
0106         QVERIFY(gpu->atomicModeSetting());
0107     }
0108 
0109     // but not if the kernel doesn't allow it
0110     {
0111         const auto mockGpu = findPrimaryDevice(0);
0112         mockGpu->deviceCaps[MOCKDRM_DEVICE_CAP_ATOMIC] = 0;
0113         auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
0114         QVERIFY(!gpu->atomicModeSetting());
0115         gpu.reset();
0116         verifyCleanup(mockGpu.get());
0117     }
0118 }
0119 
0120 void DrmTest::testOutputDetection()
0121 {
0122     const auto mockGpu = findPrimaryDevice(5);
0123 
0124     const auto one = std::make_shared<MockConnector>(mockGpu.get());
0125     const auto two = std::make_shared<MockConnector>(mockGpu.get());
0126     const auto vr = std::make_shared<MockConnector>(mockGpu.get(), true);
0127     mockGpu->connectors.push_back(one);
0128     mockGpu->connectors.push_back(two);
0129     mockGpu->connectors.push_back(vr);
0130 
0131     const auto session = Session::create(Session::Type::Noop);
0132     const auto backend = std::make_unique<DrmBackend>(session.get());
0133     const auto renderBackend = backend->createQPainterBackend();
0134     auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
0135     QVERIFY(gpu->updateOutputs());
0136 
0137     // 3 outputs should be detected, one of them non-desktop
0138     const auto outputs = gpu->drmOutputs();
0139     QCOMPARE(outputs.size(), 3);
0140     const auto vrOutput = std::find_if(outputs.begin(), outputs.end(), [](const auto &output) {
0141         return output->isNonDesktop();
0142     });
0143     QVERIFY(vrOutput != outputs.end());
0144     QVERIFY(static_cast<DrmOutput *>(*vrOutput)->connector()->id() == vr->id);
0145 
0146     // test hotunplugging
0147     mockGpu->connectors.removeOne(one);
0148     QVERIFY(gpu->updateOutputs());
0149     QCOMPARE(gpu->drmOutputs().size(), 2);
0150 
0151     // test hotplugging
0152     mockGpu->connectors.push_back(one);
0153     QVERIFY(gpu->updateOutputs());
0154     QCOMPARE(gpu->drmOutputs().size(), 3);
0155 
0156     // connector state changing to disconnected should count as a hotunplug
0157     one->connection = DRM_MODE_DISCONNECTED;
0158     QVERIFY(gpu->updateOutputs());
0159     QCOMPARE(gpu->drmOutputs().size(), 2);
0160 
0161     // don't crash if all connectors are disconnected
0162     two->connection = DRM_MODE_DISCONNECTED;
0163     vr->connection = DRM_MODE_DISCONNECTED;
0164     QVERIFY(gpu->updateOutputs());
0165     QVERIFY(gpu->drmOutputs().empty());
0166 
0167     gpu.reset();
0168     verifyCleanup(mockGpu.get());
0169 }
0170 
0171 void DrmTest::testZeroModesHandling()
0172 {
0173     const auto mockGpu = findPrimaryDevice(5);
0174 
0175     const auto conn = std::make_shared<MockConnector>(mockGpu.get());
0176     mockGpu->connectors.push_back(conn);
0177 
0178     const auto session = Session::create(Session::Type::Noop);
0179     const auto backend = std::make_unique<DrmBackend>(session.get());
0180     const auto renderBackend = backend->createQPainterBackend();
0181     auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
0182 
0183     // connector with zero modes should be ignored
0184     conn->modes.clear();
0185     QVERIFY(gpu->updateOutputs());
0186     QVERIFY(gpu->drmOutputs().empty());
0187 
0188     // once it has modes, it should be detected
0189     conn->addMode(1920, 1080, 60);
0190     QVERIFY(gpu->updateOutputs());
0191     QCOMPARE(gpu->drmOutputs().size(), 1);
0192 
0193     // if an update says it has no modes anymore but it's still connected, ignore that
0194     conn->modes.clear();
0195     QVERIFY(gpu->updateOutputs());
0196     QCOMPARE(gpu->drmOutputs().size(), 1);
0197     QVERIFY(!gpu->drmOutputs().constFirst()->modes().empty());
0198 
0199     gpu.reset();
0200     verifyCleanup(mockGpu.get());
0201 }
0202 
0203 void DrmTest::testModeGeneration_data()
0204 {
0205     QTest::addColumn<QSize>("nativeMode");
0206     QTest::addColumn<QList<QSize>>("expectedModes");
0207 
0208     QTest::newRow("2160p") << QSize(3840, 2160) << QList<QSize>{
0209         QSize(1600, 1200),
0210         QSize(1280, 1024),
0211         QSize(1024, 768),
0212         QSize(2560, 1600),
0213         QSize(1920, 1200),
0214         QSize(1280, 800),
0215         QSize(3840, 2160),
0216         QSize(3200, 1800),
0217         QSize(2880, 1620),
0218         QSize(2560, 1440),
0219         QSize(1920, 1080),
0220         QSize(1600, 900),
0221         QSize(1368, 768),
0222         QSize(1280, 720),
0223     };
0224     QTest::newRow("1440p") << QSize(2560, 1440) << QList<QSize>{
0225         QSize(1600, 1200),
0226         QSize(1280, 1024),
0227         QSize(1024, 768),
0228         QSize(1920, 1200),
0229         QSize(1280, 800),
0230         QSize(2560, 1440),
0231         QSize(1920, 1080),
0232         QSize(1600, 900),
0233         QSize(1368, 768),
0234         QSize(1280, 720),
0235     };
0236     QTest::newRow("1080p") << QSize(1920, 1080) << QList<QSize>{
0237         QSize(1280, 1024),
0238         QSize(1024, 768),
0239         QSize(1280, 800),
0240         QSize(1920, 1080),
0241         QSize(1600, 900),
0242         QSize(1368, 768),
0243         QSize(1280, 720),
0244     };
0245 
0246     QTest::newRow("2160p 21:9") << QSize(5120, 2160) << QList<QSize>{
0247         QSize(5120, 2160),
0248         QSize(1600, 1200),
0249         QSize(1280, 1024),
0250         QSize(1024, 768),
0251         QSize(2560, 1600),
0252         QSize(1920, 1200),
0253         QSize(1280, 800),
0254         QSize(3840, 2160),
0255         QSize(3200, 1800),
0256         QSize(2880, 1620),
0257         QSize(2560, 1440),
0258         QSize(1920, 1080),
0259         QSize(1600, 900),
0260         QSize(1368, 768),
0261         QSize(1280, 720),
0262     };
0263     QTest::newRow("1440p 21:9") << QSize(3440, 1440) << QList<QSize>{
0264         QSize(3440, 1440),
0265         QSize(1600, 1200),
0266         QSize(1280, 1024),
0267         QSize(1024, 768),
0268         QSize(1920, 1200),
0269         QSize(1280, 800),
0270         QSize(2560, 1440),
0271         QSize(1920, 1080),
0272         QSize(1600, 900),
0273         QSize(1368, 768),
0274         QSize(1280, 720),
0275     };
0276     QTest::newRow("1080p 21:9") << QSize(2560, 1080) << QList<QSize>{
0277         QSize(2560, 1080),
0278         QSize(1280, 1024),
0279         QSize(1024, 768),
0280         QSize(1280, 800),
0281         QSize(1920, 1080),
0282         QSize(1600, 900),
0283         QSize(1368, 768),
0284         QSize(1280, 720),
0285     };
0286 }
0287 
0288 void DrmTest::testModeGeneration()
0289 {
0290     const auto mockGpu = findPrimaryDevice(5);
0291 
0292     const auto conn = std::make_shared<MockConnector>(mockGpu.get());
0293     mockGpu->connectors.push_back(conn);
0294 
0295     const auto session = Session::create(Session::Type::Noop);
0296     const auto backend = std::make_unique<DrmBackend>(session.get());
0297     const auto renderBackend = backend->createQPainterBackend();
0298     auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
0299 
0300     QFETCH(QSize, nativeMode);
0301     QFETCH(QList<QSize>, expectedModes);
0302 
0303     conn->modes.clear();
0304     conn->addMode(nativeMode.width(), nativeMode.height(), 60);
0305     QVERIFY(gpu->updateOutputs());
0306     QCOMPARE(gpu->drmOutputs().size(), 1);
0307     // no mode generation without the scaling property
0308     QCOMPARE(gpu->drmOutputs().front()->modes().size(), 1);
0309 
0310     mockGpu->connectors.removeAll(conn);
0311     QVERIFY(gpu->updateOutputs());
0312 
0313     conn->props.emplace_back(conn.get(), QStringLiteral("scaling mode"), 0, DRM_MODE_PROP_ENUM, QList<QByteArray>{"None", "Full", "Center", "Full aspect"});
0314     mockGpu->connectors.push_back(conn);
0315     QVERIFY(gpu->updateOutputs());
0316 
0317     DrmOutput *const output = gpu->drmOutputs().front();
0318     QCOMPARE(output->modes().size(), expectedModes.size());
0319     for (const auto &mode : output->modes()) {
0320         QVERIFY(expectedModes.contains(mode->size()));
0321         QVERIFY(mode->size().width() <= nativeMode.width());
0322         QVERIFY(mode->size().height() <= nativeMode.height());
0323         QVERIFY(mode->refreshRate() <= 60000);
0324     }
0325 
0326     gpu.reset();
0327     verifyCleanup(mockGpu.get());
0328 }
0329 
0330 void DrmTest::testConnectorLifetime()
0331 {
0332     // don't crash if output lifetime is extended beyond the connector
0333     const auto mockGpu = findPrimaryDevice(5);
0334 
0335     const auto conn = std::make_shared<MockConnector>(mockGpu.get());
0336     mockGpu->connectors.push_back(conn);
0337 
0338     const auto session = Session::create(Session::Type::Noop);
0339     const auto backend = std::make_unique<DrmBackend>(session.get());
0340     const auto renderBackend = backend->createQPainterBackend();
0341     auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
0342 
0343     QVERIFY(gpu->updateOutputs());
0344     QCOMPARE(gpu->drmOutputs().size(), 1);
0345 
0346     DrmOutput *const output = gpu->drmOutputs().front();
0347 
0348     output->ref();
0349     mockGpu->connectors.clear();
0350     QVERIFY(gpu->updateOutputs());
0351     output->unref();
0352 
0353     gpu.reset();
0354     verifyCleanup(mockGpu.get());
0355 }
0356 
0357 void DrmTest::testModeset_data()
0358 {
0359     QTest::addColumn<int>("AMS");
0360     // TODO to uncomment this, implement page flip callbacks
0361     // QTest::newRow("disabled") << 0;
0362     QTest::newRow("enabled") << 1;
0363 }
0364 
0365 void DrmTest::testModeset()
0366 {
0367     // test if doing a modeset would succeed
0368     QFETCH(int, AMS);
0369     const auto mockGpu = findPrimaryDevice(5);
0370     mockGpu->deviceCaps[MOCKDRM_DEVICE_CAP_ATOMIC] = AMS;
0371 
0372     const auto conn = std::make_shared<MockConnector>(mockGpu.get());
0373     mockGpu->connectors.push_back(conn);
0374 
0375     const auto session = Session::create(Session::Type::Noop);
0376     const auto backend = std::make_unique<DrmBackend>(session.get());
0377     const auto renderBackend = backend->createQPainterBackend();
0378     auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
0379 
0380     QVERIFY(gpu->updateOutputs());
0381     QCOMPARE(gpu->drmOutputs().size(), 1);
0382     const auto output = gpu->drmOutputs().front();
0383     const auto layer = renderBackend->primaryLayer(output);
0384     layer->beginFrame();
0385     output->renderLoop()->prepareNewFrame();
0386     output->renderLoop()->beginPaint();
0387     layer->endFrame(infiniteRegion(), infiniteRegion());
0388     QVERIFY(output->present(std::make_shared<OutputFrame>(output->renderLoop())));
0389 
0390     gpu.reset();
0391     verifyCleanup(mockGpu.get());
0392 }
0393 
0394 QTEST_GUILESS_MAIN(DrmTest)
0395 #include "drmTest.moc"