KWin
Loading...
Searching...
No Matches
drmTest.cpp
Go to the documentation of this file.
1/*
2 KWin - the KDE window manager
3 This file is part of the KDE project.
4
5 SPDX-FileCopyrightText: 2022 Xaver Hugl <xaver.hugl@gmail.com>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8*/
9
10#include <QSize>
11#include <QTest>
12
13#include "mock_drm.h"
14
15#include "core/outputlayer.h"
16#include "core/session.h"
17#include "drm_backend.h"
18#include "drm_connector.h"
19#include "drm_crtc.h"
20#include "drm_egl_backend.h"
21#include "drm_gpu.h"
22#include "drm_output.h"
23#include "drm_pipeline.h"
24#include "drm_plane.h"
25#include "drm_pointer.h"
27
28#include <drm_fourcc.h>
29#include <fcntl.h>
30#include <sys/utsname.h>
31
32using namespace KWin;
33
34static std::unique_ptr<MockGpu> findPrimaryDevice(int crtcCount)
35{
36 const int deviceCount = drmGetDevices2(0, nullptr, 0);
37 if (deviceCount <= 0) {
38 return nullptr;
39 }
40
41 QList<drmDevice *> devices(deviceCount);
42 if (drmGetDevices2(0, devices.data(), devices.size()) < 0) {
43 return nullptr;
44 }
45 auto deviceCleanup = qScopeGuard([&devices]() {
46 drmFreeDevices(devices.data(), devices.size());
47 });
48
49 for (drmDevice *device : std::as_const(devices)) {
50 if (device->available_nodes & (1 << DRM_NODE_PRIMARY)) {
51 int fd = open(device->nodes[DRM_NODE_PRIMARY], O_RDWR | O_CLOEXEC);
52 if (fd != -1) {
53 return std::make_unique<MockGpu>(fd, device->nodes[DRM_NODE_PRIMARY], crtcCount);
54 }
55 }
56 }
57
58 return nullptr;
59}
60
61class DrmTest : public QObject
62{
63 Q_OBJECT
64private Q_SLOTS:
65 void testAmsDetection();
66 void testOutputDetection();
67 void testZeroModesHandling();
68 void testModeGeneration_data();
69 void testModeGeneration();
70 void testConnectorLifetime();
71 void testModeset_data();
72 void testModeset();
73};
74
75static void verifyCleanup(MockGpu *mockGpu)
76{
77 QVERIFY(mockGpu->drmConnectors.isEmpty());
78 QVERIFY(mockGpu->drmEncoders.isEmpty());
79 QVERIFY(mockGpu->drmCrtcs.isEmpty());
80 QVERIFY(mockGpu->drmPlanes.isEmpty());
81 QVERIFY(mockGpu->drmPlaneRes.isEmpty());
82 QVERIFY(mockGpu->fbs.isEmpty());
83 QVERIFY(mockGpu->drmProps.isEmpty());
84 QVERIFY(mockGpu->drmObjectProperties.isEmpty());
85 QVERIFY(mockGpu->drmPropertyBlobs.isEmpty());
86}
87
88void DrmTest::testAmsDetection()
89{
90 const auto session = Session::create(Session::Type::Noop);
91 const auto backend = std::make_unique<DrmBackend>(session.get());
92
93 // gpu without planes should use legacy mode
94 {
95 const auto mockGpu = findPrimaryDevice(0);
96 auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
97 QVERIFY(!gpu->atomicModeSetting());
98 }
99
100 // gpu with planes should use AMS
101 {
102 const auto mockGpu = findPrimaryDevice(0);
103 mockGpu->planes << std::make_shared<MockPlane>(mockGpu.get(), PlaneType::Primary, 0);
104 auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
105 gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
106 QVERIFY(gpu->atomicModeSetting());
107 }
108
109 // but not if the kernel doesn't allow it
110 {
111 const auto mockGpu = findPrimaryDevice(0);
112 mockGpu->deviceCaps[MOCKDRM_DEVICE_CAP_ATOMIC] = 0;
113 auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
114 QVERIFY(!gpu->atomicModeSetting());
115 gpu.reset();
116 verifyCleanup(mockGpu.get());
117 }
118}
119
120void DrmTest::testOutputDetection()
121{
122 const auto mockGpu = findPrimaryDevice(5);
123
124 const auto one = std::make_shared<MockConnector>(mockGpu.get());
125 const auto two = std::make_shared<MockConnector>(mockGpu.get());
126 const auto vr = std::make_shared<MockConnector>(mockGpu.get(), true);
127 mockGpu->connectors.push_back(one);
128 mockGpu->connectors.push_back(two);
129 mockGpu->connectors.push_back(vr);
130
131 const auto session = Session::create(Session::Type::Noop);
132 const auto backend = std::make_unique<DrmBackend>(session.get());
133 const auto renderBackend = backend->createQPainterBackend();
134 auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
135 QVERIFY(gpu->updateOutputs());
136
137 // 3 outputs should be detected, one of them non-desktop
138 const auto outputs = gpu->drmOutputs();
139 QCOMPARE(outputs.size(), 3);
140 const auto vrOutput = std::find_if(outputs.begin(), outputs.end(), [](const auto &output) {
141 return output->isNonDesktop();
142 });
143 QVERIFY(vrOutput != outputs.end());
144 QVERIFY(static_cast<DrmOutput *>(*vrOutput)->connector()->id() == vr->id);
145
146 // test hotunplugging
147 mockGpu->connectors.removeOne(one);
148 QVERIFY(gpu->updateOutputs());
149 QCOMPARE(gpu->drmOutputs().size(), 2);
150
151 // test hotplugging
152 mockGpu->connectors.push_back(one);
153 QVERIFY(gpu->updateOutputs());
154 QCOMPARE(gpu->drmOutputs().size(), 3);
155
156 // connector state changing to disconnected should count as a hotunplug
157 one->connection = DRM_MODE_DISCONNECTED;
158 QVERIFY(gpu->updateOutputs());
159 QCOMPARE(gpu->drmOutputs().size(), 2);
160
161 // don't crash if all connectors are disconnected
162 two->connection = DRM_MODE_DISCONNECTED;
163 vr->connection = DRM_MODE_DISCONNECTED;
164 QVERIFY(gpu->updateOutputs());
165 QVERIFY(gpu->drmOutputs().empty());
166
167 gpu.reset();
168 verifyCleanup(mockGpu.get());
169}
170
171void DrmTest::testZeroModesHandling()
172{
173 const auto mockGpu = findPrimaryDevice(5);
174
175 const auto conn = std::make_shared<MockConnector>(mockGpu.get());
176 mockGpu->connectors.push_back(conn);
177
178 const auto session = Session::create(Session::Type::Noop);
179 const auto backend = std::make_unique<DrmBackend>(session.get());
180 const auto renderBackend = backend->createQPainterBackend();
181 auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
182
183 // connector with zero modes should be ignored
184 conn->modes.clear();
185 QVERIFY(gpu->updateOutputs());
186 QVERIFY(gpu->drmOutputs().empty());
187
188 // once it has modes, it should be detected
189 conn->addMode(1920, 1080, 60);
190 QVERIFY(gpu->updateOutputs());
191 QCOMPARE(gpu->drmOutputs().size(), 1);
192
193 // if an update says it has no modes anymore but it's still connected, ignore that
194 conn->modes.clear();
195 QVERIFY(gpu->updateOutputs());
196 QCOMPARE(gpu->drmOutputs().size(), 1);
197 QVERIFY(!gpu->drmOutputs().constFirst()->modes().empty());
198
199 gpu.reset();
200 verifyCleanup(mockGpu.get());
201}
202
203void DrmTest::testModeGeneration_data()
204{
205 QTest::addColumn<QSize>("nativeMode");
206 QTest::addColumn<QList<QSize>>("expectedModes");
207
208 QTest::newRow("2160p") << QSize(3840, 2160) << QList<QSize>{
209 QSize(1600, 1200),
210 QSize(1280, 1024),
211 QSize(1024, 768),
212 QSize(2560, 1600),
213 QSize(1920, 1200),
214 QSize(1280, 800),
215 QSize(3840, 2160),
216 QSize(3200, 1800),
217 QSize(2880, 1620),
218 QSize(2560, 1440),
219 QSize(1920, 1080),
220 QSize(1600, 900),
221 QSize(1368, 768),
222 QSize(1280, 720),
223 };
224 QTest::newRow("1440p") << QSize(2560, 1440) << QList<QSize>{
225 QSize(1600, 1200),
226 QSize(1280, 1024),
227 QSize(1024, 768),
228 QSize(1920, 1200),
229 QSize(1280, 800),
230 QSize(2560, 1440),
231 QSize(1920, 1080),
232 QSize(1600, 900),
233 QSize(1368, 768),
234 QSize(1280, 720),
235 };
236 QTest::newRow("1080p") << QSize(1920, 1080) << QList<QSize>{
237 QSize(1280, 1024),
238 QSize(1024, 768),
239 QSize(1280, 800),
240 QSize(1920, 1080),
241 QSize(1600, 900),
242 QSize(1368, 768),
243 QSize(1280, 720),
244 };
245
246 QTest::newRow("2160p 21:9") << QSize(5120, 2160) << QList<QSize>{
247 QSize(5120, 2160),
248 QSize(1600, 1200),
249 QSize(1280, 1024),
250 QSize(1024, 768),
251 QSize(2560, 1600),
252 QSize(1920, 1200),
253 QSize(1280, 800),
254 QSize(3840, 2160),
255 QSize(3200, 1800),
256 QSize(2880, 1620),
257 QSize(2560, 1440),
258 QSize(1920, 1080),
259 QSize(1600, 900),
260 QSize(1368, 768),
261 QSize(1280, 720),
262 };
263 QTest::newRow("1440p 21:9") << QSize(3440, 1440) << QList<QSize>{
264 QSize(3440, 1440),
265 QSize(1600, 1200),
266 QSize(1280, 1024),
267 QSize(1024, 768),
268 QSize(1920, 1200),
269 QSize(1280, 800),
270 QSize(2560, 1440),
271 QSize(1920, 1080),
272 QSize(1600, 900),
273 QSize(1368, 768),
274 QSize(1280, 720),
275 };
276 QTest::newRow("1080p 21:9") << QSize(2560, 1080) << QList<QSize>{
277 QSize(2560, 1080),
278 QSize(1280, 1024),
279 QSize(1024, 768),
280 QSize(1280, 800),
281 QSize(1920, 1080),
282 QSize(1600, 900),
283 QSize(1368, 768),
284 QSize(1280, 720),
285 };
286}
287
288void DrmTest::testModeGeneration()
289{
290 const auto mockGpu = findPrimaryDevice(5);
291
292 const auto conn = std::make_shared<MockConnector>(mockGpu.get());
293 mockGpu->connectors.push_back(conn);
294
295 const auto session = Session::create(Session::Type::Noop);
296 const auto backend = std::make_unique<DrmBackend>(session.get());
297 const auto renderBackend = backend->createQPainterBackend();
298 auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
299
300 QFETCH(QSize, nativeMode);
301 QFETCH(QList<QSize>, expectedModes);
302
303 conn->modes.clear();
304 conn->addMode(nativeMode.width(), nativeMode.height(), 60);
305 QVERIFY(gpu->updateOutputs());
306 QCOMPARE(gpu->drmOutputs().size(), 1);
307 // no mode generation without the scaling property
308 QCOMPARE(gpu->drmOutputs().front()->modes().size(), 1);
309
310 mockGpu->connectors.removeAll(conn);
311 QVERIFY(gpu->updateOutputs());
312
313 conn->props.emplace_back(conn.get(), QStringLiteral("scaling mode"), 0, DRM_MODE_PROP_ENUM, QList<QByteArray>{"None", "Full", "Center", "Full aspect"});
314 mockGpu->connectors.push_back(conn);
315 QVERIFY(gpu->updateOutputs());
316
317 DrmOutput *const output = gpu->drmOutputs().front();
318 QCOMPARE(output->modes().size(), expectedModes.size());
319 for (const auto &mode : output->modes()) {
320 QVERIFY(expectedModes.contains(mode->size()));
321 QVERIFY(mode->size().width() <= nativeMode.width());
322 QVERIFY(mode->size().height() <= nativeMode.height());
323 QVERIFY(mode->refreshRate() <= 60000);
324 }
325
326 gpu.reset();
327 verifyCleanup(mockGpu.get());
328}
329
330void DrmTest::testConnectorLifetime()
331{
332 // don't crash if output lifetime is extended beyond the connector
333 const auto mockGpu = findPrimaryDevice(5);
334
335 const auto conn = std::make_shared<MockConnector>(mockGpu.get());
336 mockGpu->connectors.push_back(conn);
337
338 const auto session = Session::create(Session::Type::Noop);
339 const auto backend = std::make_unique<DrmBackend>(session.get());
340 const auto renderBackend = backend->createQPainterBackend();
341 auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
342
343 QVERIFY(gpu->updateOutputs());
344 QCOMPARE(gpu->drmOutputs().size(), 1);
345
346 DrmOutput *const output = gpu->drmOutputs().front();
347
348 output->ref();
349 mockGpu->connectors.clear();
350 QVERIFY(gpu->updateOutputs());
351 output->unref();
352
353 gpu.reset();
354 verifyCleanup(mockGpu.get());
355}
356
357void DrmTest::testModeset_data()
358{
359 QTest::addColumn<int>("AMS");
360 // TODO to uncomment this, implement page flip callbacks
361 // QTest::newRow("disabled") << 0;
362 QTest::newRow("enabled") << 1;
363}
364
365void DrmTest::testModeset()
366{
367 // test if doing a modeset would succeed
368 QFETCH(int, AMS);
369 const auto mockGpu = findPrimaryDevice(5);
370 mockGpu->deviceCaps[MOCKDRM_DEVICE_CAP_ATOMIC] = AMS;
371
372 const auto conn = std::make_shared<MockConnector>(mockGpu.get());
373 mockGpu->connectors.push_back(conn);
374
375 const auto session = Session::create(Session::Type::Noop);
376 const auto backend = std::make_unique<DrmBackend>(session.get());
377 const auto renderBackend = backend->createQPainterBackend();
378 auto gpu = std::make_unique<DrmGpu>(backend.get(), mockGpu->devNode, mockGpu->fd, 0);
379
380 QVERIFY(gpu->updateOutputs());
381 QCOMPARE(gpu->drmOutputs().size(), 1);
382 const auto output = gpu->drmOutputs().front();
383 const auto layer = renderBackend->primaryLayer(output);
384 layer->beginFrame();
385 output->renderLoop()->prepareNewFrame();
386 output->renderLoop()->beginPaint();
387 layer->endFrame(infiniteRegion(), infiniteRegion());
388 QVERIFY(output->present(std::make_shared<OutputFrame>(output->renderLoop())));
389
390 gpu.reset();
391 verifyCleanup(mockGpu.get());
392}
393
394QTEST_GUILESS_MAIN(DrmTest)
395#include "drmTest.moc"
RenderLoop * renderLoop() const override
uint32_t id() const
DrmOutputLayer * primaryLayer() const override
bool present(const std::shared_ptr< OutputFrame > &frame) override
DrmConnector * connector() const
void unref()
Definition output.cpp:350
QList< std::shared_ptr< OutputMode > > modes() const
Definition output.cpp:495
void ref()
Definition output.cpp:345
virtual std::optional< OutputLayerBeginFrameInfo > beginFrame()=0
static std::unique_ptr< Session > create()
Definition session.cpp:25
QList< drmModePlanePtr > drmPlanes
Definition mock_drm.h:180
QList< MockFb * > fbs
Definition mock_drm.h:182
QList< drmModeConnectorPtr > drmConnectors
Definition mock_drm.h:171
QList< drmModePropertyPtr > drmProps
Definition mock_drm.h:186
QList< drmModeObjectPropertiesPtr > drmObjectProperties
Definition mock_drm.h:188
QList< drmModePropertyBlobPtr > drmPropertyBlobs
Definition mock_drm.h:187
QList< drmModePlaneResPtr > drmPlaneRes
Definition mock_drm.h:189
QList< drmModeCrtcPtr > drmCrtcs
Definition mock_drm.h:177
QList< drmModeEncoderPtr > drmEncoders
Definition mock_drm.h:174
#define MOCKDRM_DEVICE_CAP_ATOMIC
Definition mock_drm.h:147
QList< KWayland::Client::Output * > outputs
KWIN_EXPORT QRect infiniteRegion()
Definition globals.h:234