KWin
Loading...
Searching...
No Matches
drm_backend.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: 2015 Martin Gräßlin <mgraesslin@kde.org>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8*/
9#include "drm_backend.h"
10
11#include <config-kwin.h>
12
15#include "core/session.h"
16#include "drm_egl_backend.h"
17#include "drm_gpu.h"
18#include "drm_logging.h"
19#include "drm_output.h"
20#include "drm_pipeline.h"
22#include "drm_render_backend.h"
23#include "drm_virtual_output.h"
24#include "utils/udev.h"
25// KF5
26#include <KCoreAddons>
27#include <KLocalizedString>
28// Qt
29#include <QCoreApplication>
30#include <QFileInfo>
31#include <QSocketNotifier>
32#include <QStringBuilder>
33// system
34#include <algorithm>
35#include <cerrno>
36#include <sys/stat.h>
37#include <unistd.h>
38// drm
39#include <gbm.h>
40#include <libdrm/drm_mode.h>
41#include <xf86drm.h>
42
43namespace KWin
44{
45
46static QStringList splitPathList(const QString &input, const QChar delimiter)
47{
48 QStringList ret;
49 QString tmp;
50 for (int i = 0; i < input.size(); i++) {
51 if (input[i] == delimiter) {
52 if (i > 0 && input[i - 1] == '\\') {
53 tmp[tmp.size() - 1] = delimiter;
54 } else if (!tmp.isEmpty()) {
55 ret.append(tmp);
56 tmp = QString();
57 }
58 } else {
59 tmp.append(input[i]);
60 }
61 }
62 if (!tmp.isEmpty()) {
63 ret.append(tmp);
64 }
65 return ret;
66}
67
68DrmBackend::DrmBackend(Session *session, QObject *parent)
69 : OutputBackend(parent)
70 , m_udev(std::make_unique<Udev>())
71 , m_udevMonitor(m_udev->monitor())
72 , m_session(session)
73 , m_explicitGpus(splitPathList(qEnvironmentVariable("KWIN_DRM_DEVICES"), ':'))
74{
75}
76
77DrmBackend::~DrmBackend() = default;
78
80{
81 return m_session;
82}
83
85{
86 return m_outputs;
87}
88
90{
91 connect(m_session, &Session::devicePaused, this, [this](dev_t deviceId) {
92 if (const auto gpu = findGpu(deviceId)) {
93 gpu->setActive(false);
94 }
95 });
96 connect(m_session, &Session::deviceResumed, this, [this](dev_t deviceId) {
97 if (const auto gpu = findGpu(deviceId)) {
98 gpu->setActive(true);
99 }
100 });
101
102 if (!m_explicitGpus.isEmpty()) {
103 for (const QString &fileName : m_explicitGpus) {
104 addGpu(fileName);
105 }
106 } else {
107 const auto devices = m_udev->listGPUs();
108 for (const auto &device : devices) {
109 if (device->seat() == m_session->seat()) {
110 addGpu(device->devNode());
111 }
112 }
113 }
114
115 if (m_gpus.empty()) {
116 qCWarning(KWIN_DRM) << "No suitable DRM devices have been found";
117 return false;
118 }
119
120 // setup udevMonitor
121 if (m_udevMonitor) {
122 m_udevMonitor->filterSubsystemDevType("drm");
123 const int fd = m_udevMonitor->fd();
124 if (fd != -1) {
125 m_socketNotifier = std::make_unique<QSocketNotifier>(fd, QSocketNotifier::Read);
126 connect(m_socketNotifier.get(), &QSocketNotifier::activated, this, &DrmBackend::handleUdevEvent);
127 m_udevMonitor->enable();
128 }
129 }
130 return true;
131}
132
133void DrmBackend::handleUdevEvent()
134{
135 while (auto device = m_udevMonitor->getDevice()) {
136 // Ignore the device seat if the KWIN_DRM_DEVICES envvar is set.
137 if (!m_explicitGpus.isEmpty()) {
138 const auto canonicalPath = QFileInfo(device->devNode()).canonicalPath();
139 const bool foundMatch = std::any_of(m_explicitGpus.begin(), m_explicitGpus.end(), [&canonicalPath](const QString &explicitPath) {
140 return QFileInfo(explicitPath).canonicalPath() == canonicalPath;
141 });
142 if (!foundMatch) {
143 continue;
144 }
145 } else {
146 if (device->seat() != m_session->seat()) {
147 continue;
148 }
149 }
150
151 if (device->action() == QStringLiteral("add")) {
152 DrmGpu *gpu = findGpu(device->devNum());
153 if (gpu) {
154 qCWarning(KWIN_DRM) << "Received unexpected add udev event for:" << device->devNode();
155 continue;
156 }
157 if (addGpu(device->devNode())) {
159 }
160 } else if (device->action() == QStringLiteral("remove")) {
161 DrmGpu *gpu = findGpu(device->devNum());
162 if (gpu) {
163 if (primaryGpu() == gpu) {
164 qCCritical(KWIN_DRM) << "Primary gpu has been removed! Quitting...";
165 QCoreApplication::exit(1);
166 return;
167 } else {
168 gpu->setRemoved();
170 }
171 }
172 } else if (device->action() == QStringLiteral("change")) {
173 DrmGpu *gpu = findGpu(device->devNum());
174 if (!gpu) {
175 gpu = addGpu(device->devNode());
176 }
177 if (gpu && gpu->isActive()) {
178 qCDebug(KWIN_DRM) << "Received change event for monitored drm device" << gpu->devNode();
180 }
181 }
182 }
183}
184
185DrmGpu *DrmBackend::addGpu(const QString &fileName)
186{
187 int fd = m_session->openRestricted(fileName);
188 if (fd < 0) {
189 qCWarning(KWIN_DRM) << "failed to open drm device at" << fileName;
190 return nullptr;
191 }
192
193 if (!drmIsKMS(fd)) {
194 qCDebug(KWIN_DRM) << "Skipping KMS incapable drm device node at" << fileName;
195 m_session->closeRestricted(fd);
196 return nullptr;
197 }
198
199 struct stat buf;
200 if (fstat(fd, &buf) == -1) {
201 qCDebug(KWIN_DRM, "Failed to fstat %s: %s", qPrintable(fileName), strerror(errno));
202 m_session->closeRestricted(fd);
203 return nullptr;
204 }
205
206 qCDebug(KWIN_DRM, "adding GPU %s", qPrintable(fileName));
207 m_gpus.push_back(std::make_unique<DrmGpu>(this, fileName, fd, buf.st_rdev));
208 auto gpu = m_gpus.back().get();
209 connect(gpu, &DrmGpu::outputAdded, this, &DrmBackend::addOutput);
210 connect(gpu, &DrmGpu::outputRemoved, this, &DrmBackend::removeOutput);
211 Q_EMIT gpuAdded(gpu);
212 return gpu;
213}
214
215void DrmBackend::addOutput(DrmAbstractOutput *o)
216{
217 const bool allOff = std::all_of(m_outputs.begin(), m_outputs.end(), [](Output *output) {
218 return output->dpmsMode() != Output::DpmsMode::On;
219 });
220 if (allOff && m_recentlyUnpluggedDpmsOffOutputs.contains(o->uuid())) {
221 if (DrmOutput *drmOutput = qobject_cast<DrmOutput *>(o)) {
222 // When the system is in dpms power saving mode, KWin turns on all outputs if the user plugs a new output in
223 // as that's an intentional action and they expect to see the output light up.
224 // Some outputs however temporarily disconnect in some situations, most often shortly after they go into standby.
225 // To not turn on outputs in that case, restore the previous dpms state
226 drmOutput->updateDpmsMode(Output::DpmsMode::Off);
227 drmOutput->pipeline()->setActive(false);
228 drmOutput->renderLoop()->inhibit();
229 m_recentlyUnpluggedDpmsOffOutputs.removeOne(drmOutput->uuid());
230 }
231 }
232 m_outputs.append(o);
233 Q_EMIT outputAdded(o);
234 o->updateEnabled(true);
235}
236
237void DrmBackend::removeOutput(DrmAbstractOutput *o)
238{
239 if (o->dpmsMode() == Output::DpmsMode::Off) {
240 const QUuid id = o->uuid();
241 m_recentlyUnpluggedDpmsOffOutputs.push_back(id);
242 QTimer::singleShot(1000, this, [this, id]() {
243 m_recentlyUnpluggedDpmsOffOutputs.removeOne(id);
244 });
245 }
246 o->updateEnabled(false);
247 m_outputs.removeOne(o);
248 Q_EMIT outputRemoved(o);
249}
250
252{
253 for (auto it = m_gpus.begin(); it != m_gpus.end(); ++it) {
254 if ((*it)->isRemoved()) {
255 (*it)->removeOutputs();
256 } else {
257 (*it)->updateOutputs();
258 }
259 }
260
261 Q_EMIT outputsQueried();
262
263 for (auto it = m_gpus.begin(); it != m_gpus.end();) {
264 DrmGpu *gpu = it->get();
265 if (gpu->isRemoved() || (gpu != primaryGpu() && gpu->drmOutputs().isEmpty())) {
266 qCDebug(KWIN_DRM) << "Removing GPU" << (*it)->devNode();
267 const std::unique_ptr<DrmGpu> keepAlive = std::move(*it);
268 it = m_gpus.erase(it);
269 Q_EMIT gpuRemoved(keepAlive.get());
270 } else {
271 it++;
272 }
273 }
274}
275
276std::unique_ptr<InputBackend> DrmBackend::createInputBackend()
277{
278 return std::make_unique<LibinputBackend>(m_session);
279}
280
281std::unique_ptr<QPainterBackend> DrmBackend::createQPainterBackend()
282{
283 return std::make_unique<DrmQPainterBackend>(this);
284}
285
286std::unique_ptr<OpenGLBackend> DrmBackend::createOpenGLBackend()
287{
288 return std::make_unique<EglGbmBackend>(this);
289}
290
292{
293 if (m_outputs.isEmpty()) {
295 } else {
296 for (const auto &gpu : std::as_const(m_gpus)) {
297 gpu->recreateSurfaces();
298 }
299 }
300}
301
302QList<CompositingType> DrmBackend::supportedCompositors() const
303{
304 return QList<CompositingType>{OpenGLCompositing, QPainterCompositing};
305}
306
308{
309 QString supportInfo;
310 QDebug s(&supportInfo);
311 s.nospace();
312 s << "Name: "
313 << "DRM" << Qt::endl;
314 for (size_t g = 0; g < m_gpus.size(); g++) {
315 s << "Atomic Mode Setting on GPU " << g << ": " << m_gpus.at(g)->atomicModeSetting() << Qt::endl;
316 }
317 return supportInfo;
318}
319
320Output *DrmBackend::createVirtualOutput(const QString &name, const QSize &size, double scale)
321{
322 auto output = primaryGpu()->createVirtualOutput(name, size * scale, scale);
323 Q_EMIT outputsQueried();
324 return output;
325}
326
328{
329 auto virtualOutput = qobject_cast<DrmVirtualOutput *>(output);
330 if (!virtualOutput) {
331 return;
332 }
333 primaryGpu()->removeVirtualOutput(virtualOutput);
334 Q_EMIT outputsQueried();
335}
336
338{
339 return m_gpus.empty() ? nullptr : m_gpus.front().get();
340}
341
342DrmGpu *DrmBackend::findGpu(dev_t deviceId) const
343{
344 auto it = std::find_if(m_gpus.begin(), m_gpus.end(), [deviceId](const auto &gpu) {
345 return gpu->deviceId() == deviceId;
346 });
347 return it == m_gpus.end() ? nullptr : it->get();
348}
349
351{
352 return m_gpus.size();
353}
354
356{
357 QList<DrmOutput *> toBeEnabled;
358 QList<DrmOutput *> toBeDisabled;
359 for (const auto &gpu : std::as_const(m_gpus)) {
360 const auto &outputs = gpu->drmOutputs();
361 for (const auto &output : outputs) {
362 if (output->isNonDesktop()) {
363 continue;
364 }
365 if (const auto changeset = config.constChangeSet(output)) {
366 output->queueChanges(changeset);
367 if (changeset->enabled) {
368 toBeEnabled << output;
369 } else {
370 toBeDisabled << output;
371 }
372 }
373 }
374 if (gpu->testPendingConfiguration() != DrmPipeline::Error::None) {
375 for (const auto &output : std::as_const(toBeEnabled)) {
376 output->revertQueuedChanges();
377 }
378 for (const auto &output : std::as_const(toBeDisabled)) {
379 output->revertQueuedChanges();
380 }
381 return false;
382 }
383 }
384 // first, apply changes to drm outputs.
385 // This may remove the placeholder output and thus change m_outputs!
386 for (const auto &output : std::as_const(toBeEnabled)) {
387 if (const auto changeset = config.constChangeSet(output)) {
388 output->applyQueuedChanges(changeset);
389 }
390 }
391 for (const auto &output : std::as_const(toBeDisabled)) {
392 if (const auto changeset = config.constChangeSet(output)) {
393 output->applyQueuedChanges(changeset);
394 }
395 }
396 // only then apply changes to the virtual outputs
397 for (const auto &gpu : std::as_const(m_gpus)) {
398 const auto &outputs = gpu->virtualOutputs();
399 for (const auto &output : outputs) {
400 output->applyChanges(config);
401 }
402 }
403 return true;
404}
405
407{
408 m_renderBackend = backend;
409}
410
412{
413 return m_renderBackend;
414}
415
417{
418 for (const auto &gpu : std::as_const(m_gpus)) {
419 gpu->releaseBuffers();
420 }
421}
422
423const std::vector<std::unique_ptr<DrmGpu>> &DrmBackend::gpus() const
424{
425 return m_gpus;
426}
427
429{
430 return m_gpus.front()->eglDisplay();
431}
432}
433
434#include "moc_drm_backend.cpp"
Outputs outputs() const override
size_t gpuCount() const
Output * createVirtualOutput(const QString &name, const QSize &size, double scale) override
std::unique_ptr< OpenGLBackend > createOpenGLBackend() override
void setRenderBackend(DrmRenderBackend *backend)
void gpuRemoved(DrmGpu *gpu)
DrmBackend(Session *session, QObject *parent=nullptr)
QList< CompositingType > supportedCompositors() const override
void gpuAdded(DrmGpu *gpu)
bool applyOutputChanges(const OutputConfiguration &config) override
void sceneInitialized() override
Session * session() const override
QString supportInformation() const override
void removeVirtualOutput(Output *output) override
EglDisplay * sceneEglDisplayObject() const override
friend class DrmGpu
Definition drm_backend.h:81
DrmGpu * primaryGpu() const
std::unique_ptr< QPainterBackend > createQPainterBackend() override
DrmGpu * findGpu(dev_t deviceId) const
std::unique_ptr< InputBackend > createInputBackend() override
~DrmBackend() override
DrmRenderBackend * renderBackend() const
const std::vector< std::unique_ptr< DrmGpu > > & gpus() const
bool initialize() override
QList< DrmOutput * > drmOutputs() const
Definition drm_gpu.cpp:643
void outputRemoved(DrmAbstractOutput *output)
DrmVirtualOutput * createVirtualOutput(const QString &name, const QSize &size, double scale)
Definition drm_gpu.cpp:595
void outputAdded(DrmAbstractOutput *output)
bool isRemoved() const
Definition drm_gpu.cpp:703
QString devNode() const
Definition drm_gpu.cpp:663
void removeVirtualOutput(DrmVirtualOutput *output)
Definition drm_gpu.cpp:603
void outputAdded(Output *output)
void outputRemoved(Output *output)
std::shared_ptr< OutputChangeSet > constChangeSet(Output *output) const
virtual int openRestricted(const QString &fileName)=0
void devicePaused(dev_t deviceId)
void deviceResumed(dev_t deviceId)
virtual QString seat() const =0
virtual void closeRestricted(int fileDescriptor)=0
@ QPainterCompositing
Definition globals.h:39
@ OpenGLCompositing
Definition globals.h:37
InputRedirection * input()
Definition input.h:549