1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2017 The Qt Company Ltd. |
4 | ** Copyright (C) 2016 Pelagicore AG |
5 | ** Copyright (C) 2015 Pier Luigi Fiorini <pierluigi.fiorini@gmail.com> |
6 | ** Contact: https://www.qt.io/licensing/ |
7 | ** |
8 | ** This file is part of the plugins of the Qt Toolkit. |
9 | ** |
10 | ** $QT_BEGIN_LICENSE:LGPL$ |
11 | ** Commercial License Usage |
12 | ** Licensees holding valid commercial Qt licenses may use this file in |
13 | ** accordance with the commercial license agreement provided with the |
14 | ** Software or, alternatively, in accordance with the terms contained in |
15 | ** a written agreement between you and The Qt Company. For licensing terms |
16 | ** and conditions see https://www.qt.io/terms-conditions. For further |
17 | ** information use the contact form at https://www.qt.io/contact-us. |
18 | ** |
19 | ** GNU Lesser General Public License Usage |
20 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
21 | ** General Public License version 3 as published by the Free Software |
22 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
23 | ** packaging of this file. Please review the following information to |
24 | ** ensure the GNU Lesser General Public License version 3 requirements |
25 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
26 | ** |
27 | ** GNU General Public License Usage |
28 | ** Alternatively, this file may be used under the terms of the GNU |
29 | ** General Public License version 2.0 or (at your option) the GNU General |
30 | ** Public license version 3 or any later version approved by the KDE Free |
31 | ** Qt Foundation. The licenses are as published by the Free Software |
32 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
33 | ** included in the packaging of this file. Please review the following |
34 | ** information to ensure the GNU General Public License requirements will |
35 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
36 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
37 | ** |
38 | ** $QT_END_LICENSE$ |
39 | ** |
40 | ****************************************************************************/ |
41 | |
42 | #include "qkmsdevice_p.h" |
43 | |
44 | #include <QtCore/QJsonDocument> |
45 | #include <QtCore/QJsonObject> |
46 | #include <QtCore/QJsonArray> |
47 | #include <QtCore/QFile> |
48 | #include <QtCore/QLoggingCategory> |
49 | |
50 | #include <errno.h> |
51 | |
52 | #define ARRAY_LENGTH(a) (sizeof (a) / sizeof (a)[0]) |
53 | |
54 | QT_BEGIN_NAMESPACE |
55 | |
56 | Q_LOGGING_CATEGORY(qLcKmsDebug, "qt.qpa.eglfs.kms" ) |
57 | |
58 | enum OutputConfiguration { |
59 | OutputConfigOff, |
60 | OutputConfigPreferred, |
61 | OutputConfigCurrent, |
62 | OutputConfigSkip, |
63 | OutputConfigMode, |
64 | OutputConfigModeline |
65 | }; |
66 | |
67 | int QKmsDevice::crtcForConnector(drmModeResPtr resources, drmModeConnectorPtr connector) |
68 | { |
69 | int candidate = -1; |
70 | |
71 | for (int i = 0; i < connector->count_encoders; i++) { |
72 | drmModeEncoderPtr encoder = drmModeGetEncoder(fd: m_dri_fd, encoder_id: connector->encoders[i]); |
73 | if (!encoder) { |
74 | qWarning(msg: "Failed to get encoder" ); |
75 | continue; |
76 | } |
77 | |
78 | quint32 encoderId = encoder->encoder_id; |
79 | quint32 crtcId = encoder->crtc_id; |
80 | quint32 possibleCrtcs = encoder->possible_crtcs; |
81 | drmModeFreeEncoder(ptr: encoder); |
82 | |
83 | for (int j = 0; j < resources->count_crtcs; j++) { |
84 | bool isPossible = possibleCrtcs & (1 << j); |
85 | bool isAvailable = !(m_crtc_allocator & (1 << j)); |
86 | // Preserve the existing CRTC -> encoder -> connector routing if |
87 | // any. It makes the initialization faster, and may be better |
88 | // since we have a very dumb picking algorithm. |
89 | bool isBestChoice = (!connector->encoder_id || |
90 | (connector->encoder_id == encoderId && |
91 | resources->crtcs[j] == crtcId)); |
92 | |
93 | if (isPossible && isAvailable && isBestChoice) { |
94 | return j; |
95 | } else if (isPossible && isAvailable) { |
96 | candidate = j; |
97 | } |
98 | } |
99 | } |
100 | |
101 | return candidate; |
102 | } |
103 | |
104 | static const char * const connector_type_names[] = { // must match DRM_MODE_CONNECTOR_* |
105 | "None" , |
106 | "VGA" , |
107 | "DVI" , |
108 | "DVI" , |
109 | "DVI" , |
110 | "Composite" , |
111 | "TV" , |
112 | "LVDS" , |
113 | "CTV" , |
114 | "DIN" , |
115 | "DP" , |
116 | "HDMI" , |
117 | "HDMI" , |
118 | "TV" , |
119 | "eDP" , |
120 | "Virtual" , |
121 | "DSI" |
122 | }; |
123 | |
124 | static QByteArray nameForConnector(const drmModeConnectorPtr connector) |
125 | { |
126 | QByteArray connectorName("UNKNOWN" ); |
127 | |
128 | if (connector->connector_type < ARRAY_LENGTH(connector_type_names)) |
129 | connectorName = connector_type_names[connector->connector_type]; |
130 | |
131 | connectorName += QByteArray::number(connector->connector_type_id); |
132 | |
133 | return connectorName; |
134 | } |
135 | |
136 | static bool parseModeline(const QByteArray &text, drmModeModeInfoPtr mode) |
137 | { |
138 | char hsync[16]; |
139 | char vsync[16]; |
140 | float fclock; |
141 | |
142 | mode->type = DRM_MODE_TYPE_USERDEF; |
143 | mode->hskew = 0; |
144 | mode->vscan = 0; |
145 | mode->vrefresh = 0; |
146 | mode->flags = 0; |
147 | |
148 | if (sscanf(s: text.constData(), format: "%f %hd %hd %hd %hd %hd %hd %hd %hd %15s %15s" , |
149 | &fclock, |
150 | &mode->hdisplay, |
151 | &mode->hsync_start, |
152 | &mode->hsync_end, |
153 | &mode->htotal, |
154 | &mode->vdisplay, |
155 | &mode->vsync_start, |
156 | &mode->vsync_end, |
157 | &mode->vtotal, hsync, vsync) != 11) |
158 | return false; |
159 | |
160 | mode->clock = fclock * 1000; |
161 | |
162 | if (strcmp(s1: hsync, s2: "+hsync" ) == 0) |
163 | mode->flags |= DRM_MODE_FLAG_PHSYNC; |
164 | else if (strcmp(s1: hsync, s2: "-hsync" ) == 0) |
165 | mode->flags |= DRM_MODE_FLAG_NHSYNC; |
166 | else |
167 | return false; |
168 | |
169 | if (strcmp(s1: vsync, s2: "+vsync" ) == 0) |
170 | mode->flags |= DRM_MODE_FLAG_PVSYNC; |
171 | else if (strcmp(s1: vsync, s2: "-vsync" ) == 0) |
172 | mode->flags |= DRM_MODE_FLAG_NVSYNC; |
173 | else |
174 | return false; |
175 | |
176 | return true; |
177 | } |
178 | |
179 | static inline void assignPlane(QKmsOutput *output, QKmsPlane *plane) |
180 | { |
181 | if (output->eglfs_plane) |
182 | output->eglfs_plane->activeCrtcId = 0; |
183 | |
184 | plane->activeCrtcId = output->crtc_id; |
185 | output->eglfs_plane = plane; |
186 | } |
187 | |
188 | QPlatformScreen *QKmsDevice::createScreenForConnector(drmModeResPtr resources, |
189 | drmModeConnectorPtr connector, |
190 | ScreenInfo *vinfo) |
191 | { |
192 | Q_ASSERT(vinfo); |
193 | const QByteArray connectorName = nameForConnector(connector); |
194 | |
195 | const int crtc = crtcForConnector(resources, connector); |
196 | if (crtc < 0) { |
197 | qWarning() << "No usable crtc/encoder pair for connector" << connectorName; |
198 | return nullptr; |
199 | } |
200 | |
201 | OutputConfiguration configuration; |
202 | QSize configurationSize; |
203 | int configurationRefresh = 0; |
204 | drmModeModeInfo configurationModeline; |
205 | |
206 | auto userConfig = m_screenConfig->outputSettings(); |
207 | QVariantMap userConnectorConfig = userConfig.value(akey: QString::fromUtf8(str: connectorName)); |
208 | // default to the preferred mode unless overridden in the config |
209 | const QByteArray mode = userConnectorConfig.value(QStringLiteral("mode" ), QStringLiteral("preferred" )) |
210 | .toByteArray().toLower(); |
211 | if (mode == "off" ) { |
212 | configuration = OutputConfigOff; |
213 | } else if (mode == "preferred" ) { |
214 | configuration = OutputConfigPreferred; |
215 | } else if (mode == "current" ) { |
216 | configuration = OutputConfigCurrent; |
217 | } else if (mode == "skip" ) { |
218 | configuration = OutputConfigSkip; |
219 | } else if (sscanf(s: mode.constData(), format: "%dx%d@%d" , &configurationSize.rwidth(), &configurationSize.rheight(), |
220 | &configurationRefresh) == 3) |
221 | { |
222 | configuration = OutputConfigMode; |
223 | } else if (sscanf(s: mode.constData(), format: "%dx%d" , &configurationSize.rwidth(), &configurationSize.rheight()) == 2) { |
224 | configuration = OutputConfigMode; |
225 | } else if (parseModeline(text: mode, mode: &configurationModeline)) { |
226 | configuration = OutputConfigModeline; |
227 | } else { |
228 | qWarning(msg: "Invalid mode \"%s\" for output %s" , mode.constData(), connectorName.constData()); |
229 | configuration = OutputConfigPreferred; |
230 | } |
231 | |
232 | *vinfo = ScreenInfo(); |
233 | vinfo->virtualIndex = userConnectorConfig.value(QStringLiteral("virtualIndex" ), INT_MAX).toInt(); |
234 | if (userConnectorConfig.contains(QStringLiteral("virtualPos" ))) { |
235 | const QByteArray vpos = userConnectorConfig.value(QStringLiteral("virtualPos" )).toByteArray(); |
236 | const QByteArrayList vposComp = vpos.split(sep: ','); |
237 | if (vposComp.count() == 2) |
238 | vinfo->virtualPos = QPoint(vposComp[0].trimmed().toInt(), vposComp[1].trimmed().toInt()); |
239 | } |
240 | if (userConnectorConfig.value(QStringLiteral("primary" )).toBool()) |
241 | vinfo->isPrimary = true; |
242 | |
243 | const uint32_t crtc_id = resources->crtcs[crtc]; |
244 | |
245 | if (configuration == OutputConfigOff) { |
246 | qCDebug(qLcKmsDebug) << "Turning off output" << connectorName; |
247 | drmModeSetCrtc(fd: m_dri_fd, crtcId: crtc_id, bufferId: 0, x: 0, y: 0, connectors: 0, count: 0, mode: nullptr); |
248 | return nullptr; |
249 | } |
250 | |
251 | // Skip disconnected output |
252 | if (configuration == OutputConfigPreferred && connector->connection == DRM_MODE_DISCONNECTED) { |
253 | qCDebug(qLcKmsDebug) << "Skipping disconnected output" << connectorName; |
254 | return nullptr; |
255 | } |
256 | |
257 | if (configuration == OutputConfigSkip) { |
258 | qCDebug(qLcKmsDebug) << "Skipping output" << connectorName; |
259 | return nullptr; |
260 | } |
261 | |
262 | // Get the current mode on the current crtc |
263 | drmModeModeInfo crtc_mode; |
264 | memset(s: &crtc_mode, c: 0, n: sizeof crtc_mode); |
265 | if (drmModeEncoderPtr encoder = drmModeGetEncoder(fd: m_dri_fd, encoder_id: connector->encoder_id)) { |
266 | drmModeCrtcPtr crtc = drmModeGetCrtc(fd: m_dri_fd, crtcId: encoder->crtc_id); |
267 | drmModeFreeEncoder(ptr: encoder); |
268 | |
269 | if (!crtc) |
270 | return nullptr; |
271 | |
272 | if (crtc->mode_valid) |
273 | crtc_mode = crtc->mode; |
274 | |
275 | drmModeFreeCrtc(ptr: crtc); |
276 | } |
277 | |
278 | QList<drmModeModeInfo> modes; |
279 | modes.reserve(alloc: connector->count_modes); |
280 | qCDebug(qLcKmsDebug) << connectorName << "mode count:" << connector->count_modes |
281 | << "crtc index:" << crtc << "crtc id:" << crtc_id; |
282 | for (int i = 0; i < connector->count_modes; i++) { |
283 | const drmModeModeInfo &mode = connector->modes[i]; |
284 | qCDebug(qLcKmsDebug) << "mode" << i << mode.hdisplay << "x" << mode.vdisplay |
285 | << '@' << mode.vrefresh << "hz" ; |
286 | modes << connector->modes[i]; |
287 | } |
288 | |
289 | int preferred = -1; |
290 | int current = -1; |
291 | int configured = -1; |
292 | int best = -1; |
293 | |
294 | for (int i = modes.size() - 1; i >= 0; i--) { |
295 | const drmModeModeInfo &m = modes.at(i); |
296 | |
297 | if (configuration == OutputConfigMode |
298 | && m.hdisplay == configurationSize.width() |
299 | && m.vdisplay == configurationSize.height() |
300 | && (!configurationRefresh || m.vrefresh == uint32_t(configurationRefresh))) |
301 | { |
302 | configured = i; |
303 | } |
304 | |
305 | if (!memcmp(s1: &crtc_mode, s2: &m, n: sizeof m)) |
306 | current = i; |
307 | |
308 | if (m.type & DRM_MODE_TYPE_PREFERRED) |
309 | preferred = i; |
310 | |
311 | best = i; |
312 | } |
313 | |
314 | if (configuration == OutputConfigModeline) { |
315 | modes << configurationModeline; |
316 | configured = modes.size() - 1; |
317 | } |
318 | |
319 | if (current < 0 && crtc_mode.clock != 0) { |
320 | modes << crtc_mode; |
321 | current = mode.size() - 1; |
322 | } |
323 | |
324 | if (configuration == OutputConfigCurrent) |
325 | configured = current; |
326 | |
327 | int selected_mode = -1; |
328 | |
329 | if (configured >= 0) |
330 | selected_mode = configured; |
331 | else if (preferred >= 0) |
332 | selected_mode = preferred; |
333 | else if (current >= 0) |
334 | selected_mode = current; |
335 | else if (best >= 0) |
336 | selected_mode = best; |
337 | |
338 | if (selected_mode < 0) { |
339 | qWarning() << "No modes available for output" << connectorName; |
340 | return nullptr; |
341 | } else { |
342 | int width = modes[selected_mode].hdisplay; |
343 | int height = modes[selected_mode].vdisplay; |
344 | int refresh = modes[selected_mode].vrefresh; |
345 | qCDebug(qLcKmsDebug) << "Selected mode" << selected_mode << ":" << width << "x" << height |
346 | << '@' << refresh << "hz for output" << connectorName; |
347 | } |
348 | |
349 | // physical size from connector < config values < env vars |
350 | int pwidth = qEnvironmentVariableIntValue(varName: "QT_QPA_EGLFS_PHYSICAL_WIDTH" ); |
351 | if (!pwidth) |
352 | pwidth = qEnvironmentVariableIntValue(varName: "QT_QPA_PHYSICAL_WIDTH" ); |
353 | int pheight = qEnvironmentVariableIntValue(varName: "QT_QPA_EGLFS_PHYSICAL_HEIGHT" ); |
354 | if (!pheight) |
355 | pheight = qEnvironmentVariableIntValue(varName: "QT_QPA_PHYSICAL_HEIGHT" ); |
356 | QSizeF physSize(pwidth, pheight); |
357 | if (physSize.isEmpty()) { |
358 | physSize = QSize(userConnectorConfig.value(QStringLiteral("physicalWidth" )).toInt(), |
359 | userConnectorConfig.value(QStringLiteral("physicalHeight" )).toInt()); |
360 | if (physSize.isEmpty()) { |
361 | physSize.setWidth(connector->mmWidth); |
362 | physSize.setHeight(connector->mmHeight); |
363 | } |
364 | } |
365 | qCDebug(qLcKmsDebug) << "Physical size is" << physSize << "mm" << "for output" << connectorName; |
366 | |
367 | const QByteArray formatStr = userConnectorConfig.value(QStringLiteral("format" ), adefaultValue: QString()) |
368 | .toByteArray().toLower(); |
369 | uint32_t drmFormat; |
370 | bool drmFormatExplicit = true; |
371 | if (formatStr.isEmpty()) { |
372 | drmFormat = DRM_FORMAT_XRGB8888; |
373 | drmFormatExplicit = false; |
374 | } else if (formatStr == "xrgb8888" ) { |
375 | drmFormat = DRM_FORMAT_XRGB8888; |
376 | } else if (formatStr == "xbgr8888" ) { |
377 | drmFormat = DRM_FORMAT_XBGR8888; |
378 | } else if (formatStr == "argb8888" ) { |
379 | drmFormat = DRM_FORMAT_ARGB8888; |
380 | } else if (formatStr == "abgr8888" ) { |
381 | drmFormat = DRM_FORMAT_ABGR8888; |
382 | } else if (formatStr == "rgb565" ) { |
383 | drmFormat = DRM_FORMAT_RGB565; |
384 | } else if (formatStr == "bgr565" ) { |
385 | drmFormat = DRM_FORMAT_BGR565; |
386 | } else if (formatStr == "xrgb2101010" ) { |
387 | drmFormat = DRM_FORMAT_XRGB2101010; |
388 | } else if (formatStr == "xbgr2101010" ) { |
389 | drmFormat = DRM_FORMAT_XBGR2101010; |
390 | } else if (formatStr == "argb2101010" ) { |
391 | drmFormat = DRM_FORMAT_ARGB2101010; |
392 | } else if (formatStr == "abgr2101010" ) { |
393 | drmFormat = DRM_FORMAT_ABGR2101010; |
394 | } else { |
395 | qWarning(msg: "Invalid pixel format \"%s\" for output %s" , formatStr.constData(), connectorName.constData()); |
396 | drmFormat = DRM_FORMAT_XRGB8888; |
397 | drmFormatExplicit = false; |
398 | } |
399 | qCDebug(qLcKmsDebug) << "Format is" << Qt::hex << drmFormat << Qt::dec << "requested_by_user =" << drmFormatExplicit |
400 | << "for output" << connectorName; |
401 | |
402 | const QString cloneSource = userConnectorConfig.value(QStringLiteral("clones" )).toString(); |
403 | if (!cloneSource.isEmpty()) |
404 | qCDebug(qLcKmsDebug) << "Output" << connectorName << " clones output " << cloneSource; |
405 | |
406 | QSize framebufferSize; |
407 | bool framebufferSizeSet = false; |
408 | const QByteArray fbsize = userConnectorConfig.value(QStringLiteral("size" )).toByteArray().toLower(); |
409 | if (!fbsize.isEmpty()) { |
410 | if (sscanf(s: fbsize.constData(), format: "%dx%d" , &framebufferSize.rwidth(), &framebufferSize.rheight()) == 2) { |
411 | #if QT_CONFIG(drm_atomic) |
412 | if (hasAtomicSupport()) |
413 | framebufferSizeSet = true; |
414 | #endif |
415 | if (!framebufferSizeSet) |
416 | qWarning(msg: "Setting framebuffer size is only available with DRM atomic API" ); |
417 | } else { |
418 | qWarning(msg: "Invalid framebuffer size '%s'" , fbsize.constData()); |
419 | } |
420 | } |
421 | if (!framebufferSizeSet) { |
422 | framebufferSize.setWidth(modes[selected_mode].hdisplay); |
423 | framebufferSize.setHeight(modes[selected_mode].vdisplay); |
424 | } |
425 | |
426 | qCDebug(qLcKmsDebug) << "Output" << connectorName << "framebuffer size is " << framebufferSize; |
427 | |
428 | QKmsOutput output; |
429 | output.name = QString::fromUtf8(str: connectorName); |
430 | output.connector_id = connector->connector_id; |
431 | output.crtc_index = crtc; |
432 | output.crtc_id = crtc_id; |
433 | output.physical_size = physSize; |
434 | output.preferred_mode = preferred >= 0 ? preferred : selected_mode; |
435 | output.mode = selected_mode; |
436 | output.mode_set = false; |
437 | output.saved_crtc = drmModeGetCrtc(fd: m_dri_fd, crtcId: crtc_id); |
438 | output.modes = modes; |
439 | output.subpixel = connector->subpixel; |
440 | output.dpms_prop = connectorProperty(connector, QByteArrayLiteral("DPMS" )); |
441 | output.edid_blob = connectorPropertyBlob(connector, QByteArrayLiteral("EDID" )); |
442 | output.wants_forced_plane = false; |
443 | output.forced_plane_id = 0; |
444 | output.forced_plane_set = false; |
445 | output.drm_format = drmFormat; |
446 | output.drm_format_requested_by_user = drmFormatExplicit; |
447 | output.clone_source = cloneSource; |
448 | output.size = framebufferSize; |
449 | |
450 | #if QT_CONFIG(drm_atomic) |
451 | if (drmModeCreatePropertyBlob(fd: m_dri_fd, data: &modes[selected_mode], size: sizeof(drmModeModeInfo), |
452 | id: &output.mode_blob_id) != 0) { |
453 | qCDebug(qLcKmsDebug) << "Failed to create mode blob for mode" << selected_mode; |
454 | } |
455 | |
456 | parseConnectorProperties(connectorId: output.connector_id, output: &output); |
457 | parseCrtcProperties(crtcId: output.crtc_id, output: &output); |
458 | #endif |
459 | |
460 | QString planeListStr; |
461 | for (QKmsPlane &plane : m_planes) { |
462 | if (plane.possibleCrtcs & (1 << output.crtc_index)) { |
463 | output.available_planes.append(t: plane); |
464 | planeListStr.append(s: QString::number(plane.id)); |
465 | planeListStr.append(c: QLatin1Char(' ')); |
466 | |
467 | // Choose the first primary plane that is not already assigned to |
468 | // another screen's associated crtc. |
469 | if (!output.eglfs_plane && plane.type == QKmsPlane::PrimaryPlane && !plane.activeCrtcId) |
470 | assignPlane(output: &output, plane: &plane); |
471 | } |
472 | } |
473 | qCDebug(qLcKmsDebug, "Output %s can use %d planes: %s" , |
474 | connectorName.constData(), output.available_planes.count(), qPrintable(planeListStr)); |
475 | |
476 | // This is for the EGLDevice/EGLStream backend. On some of those devices one |
477 | // may want to target a pre-configured plane. It is probably useless for |
478 | // eglfs_kms and others. Do not confuse with generic plane support (available_planes). |
479 | bool ok; |
480 | int idx = qEnvironmentVariableIntValue(varName: "QT_QPA_EGLFS_KMS_PLANE_INDEX" , ok: &ok); |
481 | if (ok) { |
482 | drmModePlaneRes *planeResources = drmModeGetPlaneResources(fd: m_dri_fd); |
483 | if (planeResources) { |
484 | if (idx >= 0 && idx < int(planeResources->count_planes)) { |
485 | drmModePlane *plane = drmModeGetPlane(fd: m_dri_fd, plane_id: planeResources->planes[idx]); |
486 | if (plane) { |
487 | output.wants_forced_plane = true; |
488 | output.forced_plane_id = plane->plane_id; |
489 | qCDebug(qLcKmsDebug, "Forcing plane index %d, plane id %u (belongs to crtc id %u)" , |
490 | idx, plane->plane_id, plane->crtc_id); |
491 | |
492 | for (QKmsPlane &kmsplane : m_planes) { |
493 | if (kmsplane.id == output.forced_plane_id) { |
494 | assignPlane(output: &output, plane: &kmsplane); |
495 | break; |
496 | } |
497 | } |
498 | |
499 | drmModeFreePlane(ptr: plane); |
500 | } |
501 | } else { |
502 | qWarning(msg: "Invalid plane index %d, must be between 0 and %u" , idx, planeResources->count_planes - 1); |
503 | } |
504 | } |
505 | } |
506 | |
507 | // A more useful version: allows specifying "crtc_id,plane_id:crtc_id,plane_id:..." |
508 | // in order to allow overriding the plane used for a given crtc. |
509 | if (qEnvironmentVariableIsSet(varName: "QT_QPA_EGLFS_KMS_PLANES_FOR_CRTCS" )) { |
510 | const QString val = qEnvironmentVariable(varName: "QT_QPA_EGLFS_KMS_PLANES_FOR_CRTCS" ); |
511 | qCDebug(qLcKmsDebug, "crtc_id:plane_id override list: %s" , qPrintable(val)); |
512 | const QStringList crtcPlanePairs = val.split(sep: QLatin1Char(':')); |
513 | for (const QString &crtcPlanePair : crtcPlanePairs) { |
514 | const QStringList values = crtcPlanePair.split(sep: QLatin1Char(',')); |
515 | if (values.count() == 2 && uint(values[0].toInt()) == output.crtc_id) { |
516 | uint planeId = values[1].toInt(); |
517 | for (QKmsPlane &kmsplane : m_planes) { |
518 | if (kmsplane.id == planeId) { |
519 | assignPlane(output: &output, plane: &kmsplane); |
520 | break; |
521 | } |
522 | } |
523 | } |
524 | } |
525 | } |
526 | |
527 | if (output.eglfs_plane) { |
528 | qCDebug(qLcKmsDebug, "Chose plane %u for output %s (crtc id %u) (may not be applicable)" , |
529 | output.eglfs_plane->id, connectorName.constData(), output.crtc_id); |
530 | } |
531 | |
532 | #if QT_CONFIG(drm_atomic) |
533 | if (hasAtomicSupport() && !output.eglfs_plane) { |
534 | qCDebug(qLcKmsDebug, "No plane associated with output %s (crtc id %u) and atomic modesetting is enabled. This is bad." , |
535 | connectorName.constData(), output.crtc_id); |
536 | } |
537 | #endif |
538 | |
539 | m_crtc_allocator |= (1 << output.crtc_index); |
540 | |
541 | vinfo->output = output; |
542 | |
543 | return createScreen(output); |
544 | } |
545 | |
546 | drmModePropertyPtr QKmsDevice::connectorProperty(drmModeConnectorPtr connector, const QByteArray &name) |
547 | { |
548 | drmModePropertyPtr prop; |
549 | |
550 | for (int i = 0; i < connector->count_props; i++) { |
551 | prop = drmModeGetProperty(fd: m_dri_fd, propertyId: connector->props[i]); |
552 | if (!prop) |
553 | continue; |
554 | if (strcmp(s1: prop->name, s2: name.constData()) == 0) |
555 | return prop; |
556 | drmModeFreeProperty(ptr: prop); |
557 | } |
558 | |
559 | return nullptr; |
560 | } |
561 | |
562 | drmModePropertyBlobPtr QKmsDevice::connectorPropertyBlob(drmModeConnectorPtr connector, const QByteArray &name) |
563 | { |
564 | drmModePropertyPtr prop; |
565 | drmModePropertyBlobPtr blob = nullptr; |
566 | |
567 | for (int i = 0; i < connector->count_props && !blob; i++) { |
568 | prop = drmModeGetProperty(fd: m_dri_fd, propertyId: connector->props[i]); |
569 | if (!prop) |
570 | continue; |
571 | if ((prop->flags & DRM_MODE_PROP_BLOB) && (strcmp(s1: prop->name, s2: name.constData()) == 0)) |
572 | blob = drmModeGetPropertyBlob(fd: m_dri_fd, blob_id: connector->prop_values[i]); |
573 | drmModeFreeProperty(ptr: prop); |
574 | } |
575 | |
576 | return blob; |
577 | } |
578 | |
579 | QKmsDevice::QKmsDevice(QKmsScreenConfig *screenConfig, const QString &path) |
580 | : m_screenConfig(screenConfig) |
581 | , m_path(path) |
582 | , m_dri_fd(-1) |
583 | , m_has_atomic_support(false) |
584 | , m_crtc_allocator(0) |
585 | { |
586 | if (m_path.isEmpty()) { |
587 | m_path = m_screenConfig->devicePath(); |
588 | qCDebug(qLcKmsDebug, "Using DRM device %s specified in config file" , qPrintable(m_path)); |
589 | if (m_path.isEmpty()) |
590 | qFatal(msg: "No DRM device given" ); |
591 | } else { |
592 | qCDebug(qLcKmsDebug, "Using backend-provided DRM device %s" , qPrintable(m_path)); |
593 | } |
594 | } |
595 | |
596 | QKmsDevice::~QKmsDevice() |
597 | { |
598 | #if QT_CONFIG(drm_atomic) |
599 | threadLocalAtomicReset(); |
600 | #endif |
601 | } |
602 | |
603 | struct OrderedScreen |
604 | { |
605 | OrderedScreen() : screen(nullptr) { } |
606 | OrderedScreen(QPlatformScreen *screen, const QKmsDevice::ScreenInfo &vinfo) |
607 | : screen(screen), vinfo(vinfo) { } |
608 | QPlatformScreen *screen; |
609 | QKmsDevice::ScreenInfo vinfo; |
610 | }; |
611 | |
612 | QDebug operator<<(QDebug dbg, const OrderedScreen &s) |
613 | { |
614 | QDebugStateSaver saver(dbg); |
615 | dbg.nospace() << "OrderedScreen(QPlatformScreen=" << s.screen << " (" << s.screen->name() << ") : " |
616 | << s.vinfo.virtualIndex |
617 | << " / " << s.vinfo.virtualPos |
618 | << " / primary: " << s.vinfo.isPrimary |
619 | << ")" ; |
620 | return dbg; |
621 | } |
622 | |
623 | static bool orderedScreenLessThan(const OrderedScreen &a, const OrderedScreen &b) |
624 | { |
625 | return a.vinfo.virtualIndex < b.vinfo.virtualIndex; |
626 | } |
627 | |
628 | void QKmsDevice::createScreens() |
629 | { |
630 | // Headless mode using a render node: cannot do any output related DRM |
631 | // stuff. Skip it all and register a dummy screen. |
632 | if (m_screenConfig->headless()) { |
633 | QPlatformScreen *screen = createHeadlessScreen(); |
634 | if (screen) { |
635 | qCDebug(qLcKmsDebug, "Headless mode enabled" ); |
636 | registerScreen(screen, isPrimary: true, virtualPos: QPoint(0, 0), virtualSiblings: QList<QPlatformScreen *>()); |
637 | return; |
638 | } else { |
639 | qWarning(msg: "QKmsDevice: Requested headless mode without support in the backend. Request is ignored." ); |
640 | } |
641 | } |
642 | |
643 | drmSetClientCap(fd: m_dri_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, value: 1); |
644 | |
645 | #if QT_CONFIG(drm_atomic) |
646 | // check atomic support |
647 | m_has_atomic_support = !drmSetClientCap(fd: m_dri_fd, DRM_CLIENT_CAP_ATOMIC, value: 1); |
648 | if (m_has_atomic_support) { |
649 | qCDebug(qLcKmsDebug, "Atomic reported as supported" ); |
650 | if (qEnvironmentVariableIntValue(varName: "QT_QPA_EGLFS_KMS_ATOMIC" )) { |
651 | qCDebug(qLcKmsDebug, "Atomic enabled" ); |
652 | } else { |
653 | qCDebug(qLcKmsDebug, "Atomic disabled" ); |
654 | m_has_atomic_support = false; |
655 | } |
656 | } |
657 | #endif |
658 | |
659 | drmModeResPtr resources = drmModeGetResources(fd: m_dri_fd); |
660 | if (!resources) { |
661 | qErrnoWarning(errno, msg: "drmModeGetResources failed" ); |
662 | return; |
663 | } |
664 | |
665 | discoverPlanes(); |
666 | |
667 | QVector<OrderedScreen> screens; |
668 | |
669 | int wantedConnectorIndex = -1; |
670 | bool ok; |
671 | int idx = qEnvironmentVariableIntValue(varName: "QT_QPA_EGLFS_KMS_CONNECTOR_INDEX" , ok: &ok); |
672 | if (ok) { |
673 | if (idx >= 0 && idx < resources->count_connectors) |
674 | wantedConnectorIndex = idx; |
675 | else |
676 | qWarning(msg: "Invalid connector index %d, must be between 0 and %u" , idx, resources->count_connectors - 1); |
677 | } |
678 | |
679 | for (int i = 0; i < resources->count_connectors; i++) { |
680 | if (wantedConnectorIndex >= 0 && i != wantedConnectorIndex) |
681 | continue; |
682 | |
683 | drmModeConnectorPtr connector = drmModeGetConnector(fd: m_dri_fd, connectorId: resources->connectors[i]); |
684 | if (!connector) |
685 | continue; |
686 | |
687 | ScreenInfo vinfo; |
688 | QPlatformScreen *screen = createScreenForConnector(resources, connector, vinfo: &vinfo); |
689 | if (screen) |
690 | screens.append(t: OrderedScreen(screen, vinfo)); |
691 | |
692 | drmModeFreeConnector(ptr: connector); |
693 | } |
694 | |
695 | drmModeFreeResources(ptr: resources); |
696 | |
697 | // Use stable sort to preserve the original (DRM connector) order |
698 | // for outputs with unspecified indices. |
699 | std::stable_sort(first: screens.begin(), last: screens.end(), comp: orderedScreenLessThan); |
700 | qCDebug(qLcKmsDebug) << "Sorted screen list:" << screens; |
701 | |
702 | // The final list of screens is available, so do the second phase setup. |
703 | // Hook up clone sources and targets. |
704 | for (const OrderedScreen &orderedScreen : screens) { |
705 | QVector<QPlatformScreen *> screensCloningThisScreen; |
706 | for (const OrderedScreen &s : screens) { |
707 | if (s.vinfo.output.clone_source == orderedScreen.vinfo.output.name) |
708 | screensCloningThisScreen.append(t: s.screen); |
709 | } |
710 | QPlatformScreen *screenThisScreenClones = nullptr; |
711 | if (!orderedScreen.vinfo.output.clone_source.isEmpty()) { |
712 | for (const OrderedScreen &s : screens) { |
713 | if (s.vinfo.output.name == orderedScreen.vinfo.output.clone_source) { |
714 | screenThisScreenClones = s.screen; |
715 | break; |
716 | } |
717 | } |
718 | } |
719 | if (screenThisScreenClones) |
720 | qCDebug(qLcKmsDebug) << orderedScreen.screen->name() << "clones" << screenThisScreenClones; |
721 | if (!screensCloningThisScreen.isEmpty()) |
722 | qCDebug(qLcKmsDebug) << orderedScreen.screen->name() << "is cloned by" << screensCloningThisScreen; |
723 | |
724 | registerScreenCloning(screen: orderedScreen.screen, screenThisScreenClones, screensCloningThisScreen); |
725 | } |
726 | |
727 | // Figure out the virtual desktop and register the screens to QPA/QGuiApplication. |
728 | QPoint pos(0, 0); |
729 | QList<QPlatformScreen *> siblings; |
730 | QVector<QPoint> virtualPositions; |
731 | int primarySiblingIdx = -1; |
732 | |
733 | for (const OrderedScreen &orderedScreen : screens) { |
734 | QPlatformScreen *s = orderedScreen.screen; |
735 | QPoint virtualPos(0, 0); |
736 | // set up a horizontal or vertical virtual desktop |
737 | if (orderedScreen.vinfo.virtualPos.isNull()) { |
738 | virtualPos = pos; |
739 | if (m_screenConfig->virtualDesktopLayout() == QKmsScreenConfig::VirtualDesktopLayoutVertical) |
740 | pos.ry() += s->geometry().height(); |
741 | else |
742 | pos.rx() += s->geometry().width(); |
743 | } else { |
744 | virtualPos = orderedScreen.vinfo.virtualPos; |
745 | } |
746 | qCDebug(qLcKmsDebug) << "Adding QPlatformScreen" << s << "(" << s->name() << ")" |
747 | << "to QPA with geometry" << s->geometry() |
748 | << "and isPrimary=" << orderedScreen.vinfo.isPrimary; |
749 | // The order in qguiapp's screens list will match the order set by |
750 | // virtualIndex. This is not only handy but also required since for instance |
751 | // evdevtouch relies on it when performing touch device - screen mapping. |
752 | if (!m_screenConfig->separateScreens()) { |
753 | siblings.append(t: s); |
754 | virtualPositions.append(t: virtualPos); |
755 | if (orderedScreen.vinfo.isPrimary) |
756 | primarySiblingIdx = siblings.count() - 1; |
757 | } else { |
758 | registerScreen(screen: s, isPrimary: orderedScreen.vinfo.isPrimary, virtualPos, virtualSiblings: QList<QPlatformScreen *>() << s); |
759 | } |
760 | } |
761 | |
762 | if (!m_screenConfig->separateScreens()) { |
763 | // enable the virtual desktop |
764 | for (int i = 0; i < siblings.count(); ++i) |
765 | registerScreen(screen: siblings[i], isPrimary: i == primarySiblingIdx, virtualPos: virtualPositions[i], virtualSiblings: siblings); |
766 | } |
767 | } |
768 | |
769 | QPlatformScreen *QKmsDevice::createHeadlessScreen() |
770 | { |
771 | // headless mode not supported by default |
772 | return nullptr; |
773 | } |
774 | |
775 | // not all subclasses support screen cloning |
776 | void QKmsDevice::registerScreenCloning(QPlatformScreen *screen, |
777 | QPlatformScreen *screenThisScreenClones, |
778 | const QVector<QPlatformScreen *> &screensCloningThisScreen) |
779 | { |
780 | Q_UNUSED(screen); |
781 | Q_UNUSED(screenThisScreenClones); |
782 | Q_UNUSED(screensCloningThisScreen); |
783 | } |
784 | |
785 | // drm_property_type_is is not available in old headers |
786 | static inline bool propTypeIs(drmModePropertyPtr prop, uint32_t type) |
787 | { |
788 | if (prop->flags & DRM_MODE_PROP_EXTENDED_TYPE) |
789 | return (prop->flags & DRM_MODE_PROP_EXTENDED_TYPE) == type; |
790 | return prop->flags & type; |
791 | } |
792 | |
793 | void QKmsDevice::enumerateProperties(drmModeObjectPropertiesPtr objProps, PropCallback callback) |
794 | { |
795 | for (uint32_t propIdx = 0; propIdx < objProps->count_props; ++propIdx) { |
796 | drmModePropertyPtr prop = drmModeGetProperty(fd: m_dri_fd, propertyId: objProps->props[propIdx]); |
797 | if (!prop) |
798 | continue; |
799 | |
800 | const quint64 value = objProps->prop_values[propIdx]; |
801 | qCDebug(qLcKmsDebug, " property %d: id = %u name = '%s'" , propIdx, prop->prop_id, prop->name); |
802 | |
803 | if (propTypeIs(prop, DRM_MODE_PROP_SIGNED_RANGE)) { |
804 | qCDebug(qLcKmsDebug, " type is SIGNED_RANGE, value is %lld, possible values are:" , qint64(value)); |
805 | for (int i = 0; i < prop->count_values; ++i) |
806 | qCDebug(qLcKmsDebug, " %lld" , qint64(prop->values[i])); |
807 | } else if (propTypeIs(prop, DRM_MODE_PROP_RANGE)) { |
808 | qCDebug(qLcKmsDebug, " type is RANGE, value is %llu, possible values are:" , value); |
809 | for (int i = 0; i < prop->count_values; ++i) |
810 | qCDebug(qLcKmsDebug, " %llu" , quint64(prop->values[i])); |
811 | } else if (propTypeIs(prop, DRM_MODE_PROP_ENUM)) { |
812 | qCDebug(qLcKmsDebug, " type is ENUM, value is %llu, possible values are:" , value); |
813 | for (int i = 0; i < prop->count_enums; ++i) |
814 | qCDebug(qLcKmsDebug, " enum %d: %s - %llu" , i, prop->enums[i].name, quint64(prop->enums[i].value)); |
815 | } else if (propTypeIs(prop, DRM_MODE_PROP_BITMASK)) { |
816 | qCDebug(qLcKmsDebug, " type is BITMASK, value is %llu, possible bits are:" , value); |
817 | for (int i = 0; i < prop->count_enums; ++i) |
818 | qCDebug(qLcKmsDebug, " bitmask %d: %s - %u" , i, prop->enums[i].name, 1 << prop->enums[i].value); |
819 | } else if (propTypeIs(prop, DRM_MODE_PROP_BLOB)) { |
820 | qCDebug(qLcKmsDebug, " type is BLOB" ); |
821 | } else if (propTypeIs(prop, DRM_MODE_PROP_OBJECT)) { |
822 | qCDebug(qLcKmsDebug, " type is OBJECT" ); |
823 | } |
824 | |
825 | callback(prop, value); |
826 | |
827 | drmModeFreeProperty(ptr: prop); |
828 | } |
829 | } |
830 | |
831 | void QKmsDevice::discoverPlanes() |
832 | { |
833 | m_planes.clear(); |
834 | |
835 | drmModePlaneResPtr planeResources = drmModeGetPlaneResources(fd: m_dri_fd); |
836 | if (!planeResources) |
837 | return; |
838 | |
839 | const int countPlanes = planeResources->count_planes; |
840 | qCDebug(qLcKmsDebug, "Found %d planes" , countPlanes); |
841 | for (int planeIdx = 0; planeIdx < countPlanes; ++planeIdx) { |
842 | drmModePlanePtr drmplane = drmModeGetPlane(fd: m_dri_fd, plane_id: planeResources->planes[planeIdx]); |
843 | if (!drmplane) { |
844 | qCDebug(qLcKmsDebug, "Failed to query plane %d, ignoring" , planeIdx); |
845 | continue; |
846 | } |
847 | |
848 | QKmsPlane plane; |
849 | plane.id = drmplane->plane_id; |
850 | plane.possibleCrtcs = drmplane->possible_crtcs; |
851 | |
852 | const int countFormats = drmplane->count_formats; |
853 | QString formatStr; |
854 | for (int i = 0; i < countFormats; ++i) { |
855 | uint32_t f = drmplane->formats[i]; |
856 | plane.supportedFormats.append(t: f); |
857 | formatStr += QString::asprintf(format: "%c%c%c%c " , f, f >> 8, f >> 16, f >> 24); |
858 | } |
859 | |
860 | qCDebug(qLcKmsDebug, "plane %d: id = %u countFormats = %d possibleCrtcs = 0x%x supported formats = %s" , |
861 | planeIdx, plane.id, countFormats, plane.possibleCrtcs, qPrintable(formatStr)); |
862 | |
863 | drmModeFreePlane(ptr: drmplane); |
864 | |
865 | drmModeObjectPropertiesPtr objProps = drmModeObjectGetProperties(fd: m_dri_fd, object_id: plane.id, DRM_MODE_OBJECT_PLANE); |
866 | if (!objProps) { |
867 | qCDebug(qLcKmsDebug, "Failed to query plane %d object properties, ignoring" , planeIdx); |
868 | continue; |
869 | } |
870 | |
871 | enumerateProperties(objProps, callback: [&plane](drmModePropertyPtr prop, quint64 value) { |
872 | if (!strcmp(s1: prop->name, s2: "type" )) { |
873 | plane.type = QKmsPlane::Type(value); |
874 | } else if (!strcmp(s1: prop->name, s2: "rotation" )) { |
875 | plane.initialRotation = QKmsPlane::Rotations(int(value)); |
876 | plane.availableRotations = { }; |
877 | if (propTypeIs(prop, DRM_MODE_PROP_BITMASK)) { |
878 | for (int i = 0; i < prop->count_enums; ++i) |
879 | plane.availableRotations |= QKmsPlane::Rotation(1 << prop->enums[i].value); |
880 | } |
881 | plane.rotationPropertyId = prop->prop_id; |
882 | } else if (!strcasecmp(s1: prop->name, s2: "crtc_id" )) { |
883 | plane.crtcPropertyId = prop->prop_id; |
884 | } else if (!strcasecmp(s1: prop->name, s2: "fb_id" )) { |
885 | plane.framebufferPropertyId = prop->prop_id; |
886 | } else if (!strcasecmp(s1: prop->name, s2: "src_w" )) { |
887 | plane.srcwidthPropertyId = prop->prop_id; |
888 | } else if (!strcasecmp(s1: prop->name, s2: "src_h" )) { |
889 | plane.srcheightPropertyId = prop->prop_id; |
890 | } else if (!strcasecmp(s1: prop->name, s2: "crtc_w" )) { |
891 | plane.crtcwidthPropertyId = prop->prop_id; |
892 | } else if (!strcasecmp(s1: prop->name, s2: "crtc_h" )) { |
893 | plane.crtcheightPropertyId = prop->prop_id; |
894 | } else if (!strcasecmp(s1: prop->name, s2: "src_x" )) { |
895 | plane.srcXPropertyId = prop->prop_id; |
896 | } else if (!strcasecmp(s1: prop->name, s2: "src_y" )) { |
897 | plane.srcYPropertyId = prop->prop_id; |
898 | } else if (!strcasecmp(s1: prop->name, s2: "crtc_x" )) { |
899 | plane.crtcXPropertyId = prop->prop_id; |
900 | } else if (!strcasecmp(s1: prop->name, s2: "crtc_y" )) { |
901 | plane.crtcYPropertyId = prop->prop_id; |
902 | } else if (!strcasecmp(s1: prop->name, s2: "zpos" )) { |
903 | plane.zposPropertyId = prop->prop_id; |
904 | } else if (!strcasecmp(s1: prop->name, s2: "blend_op" )) { |
905 | plane.blendOpPropertyId = prop->prop_id; |
906 | } |
907 | }); |
908 | |
909 | m_planes.append(t: plane); |
910 | |
911 | drmModeFreeObjectProperties(ptr: objProps); |
912 | } |
913 | |
914 | drmModeFreePlaneResources(ptr: planeResources); |
915 | } |
916 | |
917 | int QKmsDevice::fd() const |
918 | { |
919 | return m_dri_fd; |
920 | } |
921 | |
922 | QString QKmsDevice::devicePath() const |
923 | { |
924 | return m_path; |
925 | } |
926 | |
927 | void QKmsDevice::setFd(int fd) |
928 | { |
929 | m_dri_fd = fd; |
930 | } |
931 | |
932 | |
933 | bool QKmsDevice::hasAtomicSupport() |
934 | { |
935 | return m_has_atomic_support; |
936 | } |
937 | |
938 | #if QT_CONFIG(drm_atomic) |
939 | drmModeAtomicReq *QKmsDevice::threadLocalAtomicRequest() |
940 | { |
941 | if (!m_has_atomic_support) |
942 | return nullptr; |
943 | |
944 | AtomicReqs &a(m_atomicReqs.localData()); |
945 | if (!a.request) |
946 | a.request = drmModeAtomicAlloc(); |
947 | |
948 | return a.request; |
949 | } |
950 | |
951 | bool QKmsDevice::threadLocalAtomicCommit(void *user_data) |
952 | { |
953 | if (!m_has_atomic_support) |
954 | return false; |
955 | |
956 | AtomicReqs &a(m_atomicReqs.localData()); |
957 | if (!a.request) |
958 | return false; |
959 | |
960 | int ret = drmModeAtomicCommit(fd: m_dri_fd, req: a.request, |
961 | DRM_MODE_ATOMIC_NONBLOCK | DRM_MODE_PAGE_FLIP_EVENT | DRM_MODE_ATOMIC_ALLOW_MODESET, |
962 | user_data); |
963 | |
964 | if (ret) { |
965 | qWarning(msg: "Failed to commit atomic request (code=%d)" , ret); |
966 | return false; |
967 | } |
968 | |
969 | a.previous_request = a.request; |
970 | a.request = nullptr; |
971 | |
972 | return true; |
973 | } |
974 | |
975 | void QKmsDevice::threadLocalAtomicReset() |
976 | { |
977 | if (!m_has_atomic_support) |
978 | return; |
979 | |
980 | AtomicReqs &a(m_atomicReqs.localData()); |
981 | if (a.previous_request) { |
982 | drmModeAtomicFree(req: a.previous_request); |
983 | a.previous_request = nullptr; |
984 | } |
985 | } |
986 | #endif |
987 | |
988 | void QKmsDevice::parseConnectorProperties(uint32_t connectorId, QKmsOutput *output) |
989 | { |
990 | drmModeObjectPropertiesPtr objProps = drmModeObjectGetProperties(fd: m_dri_fd, object_id: connectorId, DRM_MODE_OBJECT_CONNECTOR); |
991 | if (!objProps) { |
992 | qCDebug(qLcKmsDebug, "Failed to query connector %d object properties" , connectorId); |
993 | return; |
994 | } |
995 | |
996 | enumerateProperties(objProps, callback: [output](drmModePropertyPtr prop, quint64 value) { |
997 | Q_UNUSED(value); |
998 | if (!strcasecmp(s1: prop->name, s2: "crtc_id" )) |
999 | output->crtcIdPropertyId = prop->prop_id; |
1000 | }); |
1001 | |
1002 | drmModeFreeObjectProperties(ptr: objProps); |
1003 | } |
1004 | |
1005 | void QKmsDevice::parseCrtcProperties(uint32_t crtcId, QKmsOutput *output) |
1006 | { |
1007 | drmModeObjectPropertiesPtr objProps = drmModeObjectGetProperties(fd: m_dri_fd, object_id: crtcId, DRM_MODE_OBJECT_CRTC); |
1008 | if (!objProps) { |
1009 | qCDebug(qLcKmsDebug, "Failed to query crtc %d object properties" , crtcId); |
1010 | return; |
1011 | } |
1012 | |
1013 | enumerateProperties(objProps, callback: [output](drmModePropertyPtr prop, quint64 value) { |
1014 | Q_UNUSED(value) |
1015 | if (!strcasecmp(s1: prop->name, s2: "mode_id" )) |
1016 | output->modeIdPropertyId = prop->prop_id; |
1017 | else if (!strcasecmp(s1: prop->name, s2: "active" )) |
1018 | output->activePropertyId = prop->prop_id; |
1019 | }); |
1020 | |
1021 | drmModeFreeObjectProperties(ptr: objProps); |
1022 | } |
1023 | |
1024 | QKmsScreenConfig *QKmsDevice::screenConfig() const |
1025 | { |
1026 | return m_screenConfig; |
1027 | } |
1028 | |
1029 | QKmsScreenConfig::QKmsScreenConfig() |
1030 | : m_headless(false) |
1031 | , m_hwCursor(true) |
1032 | , m_separateScreens(false) |
1033 | , m_pbuffers(false) |
1034 | , m_virtualDesktopLayout(VirtualDesktopLayoutHorizontal) |
1035 | { |
1036 | loadConfig(); |
1037 | } |
1038 | |
1039 | void QKmsScreenConfig::loadConfig() |
1040 | { |
1041 | QByteArray json = qgetenv(varName: "QT_QPA_EGLFS_KMS_CONFIG" ); |
1042 | if (json.isEmpty()) { |
1043 | json = qgetenv(varName: "QT_QPA_KMS_CONFIG" ); |
1044 | if (json.isEmpty()) |
1045 | return; |
1046 | } |
1047 | |
1048 | qCDebug(qLcKmsDebug) << "Loading KMS setup from" << json; |
1049 | |
1050 | QFile file(QString::fromUtf8(str: json)); |
1051 | if (!file.open(flags: QFile::ReadOnly)) { |
1052 | qCWarning(qLcKmsDebug) << "Could not open config file" |
1053 | << json << "for reading" ; |
1054 | return; |
1055 | } |
1056 | |
1057 | const QJsonDocument doc = QJsonDocument::fromJson(json: file.readAll()); |
1058 | if (!doc.isObject()) { |
1059 | qCWarning(qLcKmsDebug) << "Invalid config file" << json |
1060 | << "- no top-level JSON object" ; |
1061 | return; |
1062 | } |
1063 | |
1064 | const QJsonObject object = doc.object(); |
1065 | |
1066 | const QString headlessStr = object.value(key: QLatin1String("headless" )).toString(); |
1067 | const QByteArray headless = headlessStr.toUtf8(); |
1068 | QSize headlessSize; |
1069 | if (sscanf(s: headless.constData(), format: "%dx%d" , &headlessSize.rwidth(), &headlessSize.rheight()) == 2) { |
1070 | m_headless = true; |
1071 | m_headlessSize = headlessSize; |
1072 | } else { |
1073 | m_headless = false; |
1074 | } |
1075 | |
1076 | m_hwCursor = object.value(key: QLatin1String("hwcursor" )).toBool(defaultValue: m_hwCursor); |
1077 | m_pbuffers = object.value(key: QLatin1String("pbuffers" )).toBool(defaultValue: m_pbuffers); |
1078 | m_devicePath = object.value(key: QLatin1String("device" )).toString(); |
1079 | m_separateScreens = object.value(key: QLatin1String("separateScreens" )).toBool(defaultValue: m_separateScreens); |
1080 | |
1081 | const QString vdOriString = object.value(key: QLatin1String("virtualDesktopLayout" )).toString(); |
1082 | if (!vdOriString.isEmpty()) { |
1083 | if (vdOriString == QLatin1String("horizontal" )) |
1084 | m_virtualDesktopLayout = VirtualDesktopLayoutHorizontal; |
1085 | else if (vdOriString == QLatin1String("vertical" )) |
1086 | m_virtualDesktopLayout = VirtualDesktopLayoutVertical; |
1087 | else |
1088 | qCWarning(qLcKmsDebug) << "Unknown virtualDesktopOrientation value" << vdOriString; |
1089 | } |
1090 | |
1091 | const QJsonArray outputs = object.value(key: QLatin1String("outputs" )).toArray(); |
1092 | for (int i = 0; i < outputs.size(); i++) { |
1093 | const QVariantMap outputSettings = outputs.at(i).toObject().toVariantMap(); |
1094 | |
1095 | if (outputSettings.contains(QStringLiteral("name" ))) { |
1096 | const QString name = outputSettings.value(QStringLiteral("name" )).toString(); |
1097 | |
1098 | if (m_outputSettings.contains(akey: name)) { |
1099 | qCDebug(qLcKmsDebug) << "Output" << name << "configured multiple times!" ; |
1100 | } |
1101 | |
1102 | m_outputSettings.insert(akey: name, avalue: outputSettings); |
1103 | } |
1104 | } |
1105 | |
1106 | qCDebug(qLcKmsDebug) << "Requested configuration (some settings may be ignored):\n" |
1107 | << "\theadless:" << m_headless << "\n" |
1108 | << "\thwcursor:" << m_hwCursor << "\n" |
1109 | << "\tpbuffers:" << m_pbuffers << "\n" |
1110 | << "\tseparateScreens:" << m_separateScreens << "\n" |
1111 | << "\tvirtualDesktopLayout:" << m_virtualDesktopLayout << "\n" |
1112 | << "\toutputs:" << m_outputSettings; |
1113 | } |
1114 | |
1115 | void QKmsOutput::restoreMode(QKmsDevice *device) |
1116 | { |
1117 | if (mode_set && saved_crtc) { |
1118 | drmModeSetCrtc(fd: device->fd(), |
1119 | crtcId: saved_crtc->crtc_id, |
1120 | bufferId: saved_crtc->buffer_id, |
1121 | x: 0, y: 0, |
1122 | connectors: &connector_id, count: 1, |
1123 | mode: &saved_crtc->mode); |
1124 | mode_set = false; |
1125 | } |
1126 | } |
1127 | |
1128 | void QKmsOutput::cleanup(QKmsDevice *device) |
1129 | { |
1130 | if (dpms_prop) { |
1131 | drmModeFreeProperty(ptr: dpms_prop); |
1132 | dpms_prop = nullptr; |
1133 | } |
1134 | |
1135 | if (edid_blob) { |
1136 | drmModeFreePropertyBlob(ptr: edid_blob); |
1137 | edid_blob = nullptr; |
1138 | } |
1139 | |
1140 | restoreMode(device); |
1141 | |
1142 | if (saved_crtc) { |
1143 | drmModeFreeCrtc(ptr: saved_crtc); |
1144 | saved_crtc = nullptr; |
1145 | } |
1146 | } |
1147 | |
1148 | QPlatformScreen::SubpixelAntialiasingType QKmsOutput::subpixelAntialiasingTypeHint() const |
1149 | { |
1150 | switch (subpixel) { |
1151 | default: |
1152 | case DRM_MODE_SUBPIXEL_UNKNOWN: |
1153 | case DRM_MODE_SUBPIXEL_NONE: |
1154 | return QPlatformScreen::Subpixel_None; |
1155 | case DRM_MODE_SUBPIXEL_HORIZONTAL_RGB: |
1156 | return QPlatformScreen::Subpixel_RGB; |
1157 | case DRM_MODE_SUBPIXEL_HORIZONTAL_BGR: |
1158 | return QPlatformScreen::Subpixel_BGR; |
1159 | case DRM_MODE_SUBPIXEL_VERTICAL_RGB: |
1160 | return QPlatformScreen::Subpixel_VRGB; |
1161 | case DRM_MODE_SUBPIXEL_VERTICAL_BGR: |
1162 | return QPlatformScreen::Subpixel_VBGR; |
1163 | } |
1164 | } |
1165 | |
1166 | void QKmsOutput::setPowerState(QKmsDevice *device, QPlatformScreen::PowerState state) |
1167 | { |
1168 | if (dpms_prop) |
1169 | drmModeConnectorSetProperty(fd: device->fd(), connector_id, |
1170 | property_id: dpms_prop->prop_id, value: (int) state); |
1171 | } |
1172 | |
1173 | QT_END_NAMESPACE |
1174 | |