MythTV master
mythdrmdevice.cpp
Go to the documentation of this file.
1#include "libmythbase/mythconfig.h"
2
3// Qt
4#include <QtGlobal>
5#if QT_VERSION >= QT_VERSION_CHECK(6,5,0)
6#include <QtSystemDetection>
7#endif
8#include <QDir>
9#include <QMutex>
10#include <QScreen>
11#include <QGuiApplication>
12
13#if CONFIG_QTPRIVATEHEADERS
14#include <qpa/qplatformnativeinterface.h>
15#endif
16
17// MythTV
19#include "mythedid.h"
24
25// Std
26#include <unistd.h>
27#include <fcntl.h>
28
29// libdrm
30extern "C" {
31#include <drm_fourcc.h>
32}
33
34#define LOC (QString("%1: ").arg(m_deviceName))
35
36/* A DRM based display is only useable if neither X nor Wayland are running; as
37 * X or Wayland will hold the master/privileged DRM connection and without it
38 * there is little we can do (no video mode setting etc) - and both X and Wayland
39 * provide their own relevant, higher level API's.
40 *
41 * If X or Wayland aren't running we may have privileged access *if* MythTV was
42 * compiled with Qt private header support; this is the only way to retrieve
43 * the master DRM file descriptor and use DRM atomic operations.
44 *
45 * Having master privileges allows us to:-
46 *
47 * 1. Set the video mode
48 * 2. Improve performance on SoCs by rendering YUV video directly to the framebuffer
49 * 3. Implement HDR support and/or setup 10bit output
50 * 4. Enable/disable FreeSync
51 *
52 * There are a variety of use cases, depending on hardware, user preferences and
53 * compile time support (and assuming neither X or Wayland are running):-
54 *
55 * 1. No master DRM privileges. Use DRM as a last resort and for information only.
56 * 2. Have master privileges but only need video mode switching (judder free) and maybe
57 * HDR support. This is likely with more modern graphics, and e.g. VAAPI decoding
58 * support, where we do not need the extra performance of rendering YUV frames
59 * directly, it is potentially unstable and the additional complexity of
60 * setting up Qt is not needed.
61 * 3. We require direct YUV rendering to video planes for performance. This requires
62 * us to configure Qt in various ways to ensure we can use the correct plane
63 * for video and Qt uses an appropriate plane for OpenGL/Vulkan, correctly
64 * configured to ensure alpha blending works.
65 * 4. Option 3 plus potential optimisation of 4K rendering (Qt allows us to configure
66 * our display with a 1080P GUI framebuffer but a 4K video framebuffer) and/or
67 * forcing of full video mode switching (we are currently limited to switching
68 * the refresh rate only but if we force the first modeswitch to the maximum
69 * (e.g. 4K) we could then manipulate windowing to allow smaller modes).
70 *
71 * Options 1 and 2 require no additional configuration; if we have a privileged
72 * connection then we can use mode switching, if not we have a 'dumb' connection.
73 *
74 * Option 4 is not yet implemented.
75 *
76 * Option 3 requires us to 'configure' Qt by way of environment variables and a
77 * configuration file *before* we start the QGuiApplication instance *and* before
78 * we have a database connection. Given the complexity of this setup and the
79 * possibility for it to go horribly wrong, this option must be explicitly enabled
80 * via an environment variable.
81 *
82 * Typically, DRM drivers provide 3 types of plane; primary, overlay and cursor.
83 * A typical implementation provides one of each for each CRTC; with the primary
84 * plane usually providing video support. Different vendors do however approach
85 * setup differently; so there may be multiple primary planes, multiple overlay
86 * planes, no cursor planes (not relevant to us) and video support may only be
87 * provided in the overlay plane(s) - in which case we need 'zpos' support so
88 * that we can manipulate the rendering order...
89 *
90 * Qt's eglfs_kms implementation will typically grab the first suitable primary plane
91 * for its own use but that is often the only plane with YUV format support (which we
92 * need for video playback support) and we want to overlay the UI on top of the video.
93 *
94 * Fortunately QKmsDevice respects the QT_QPA_EGLFS_KMS_PLANE_INDEX environment
95 * variable (since Qt 5.9) which allows us to tell QKmsDevice to use the overlay
96 * plane for its OpenGL implementation; hence allowing us to grab the primary plane later.
97 * With Qt5.15 and later, we can also use QT_QPA_EGLFS_KMS_PLANES_FOR_CRTCS which
98 * is much more flexible. We set both here and if Qt supports QT_QPA_EGLFS_KMS_PLANES_FOR_CRTCS
99 * it will override the older version and be ignored otherwise.
100 *
101 * So that hopefully gets the video and UI in the correct planes but...
102 *
103 * Furthermore, we must tell Qt to use a transparent format for the overlay plane
104 * so that it does not obscure the video plane *and* ensure that we clear our
105 * OpenGL/Vulkan overlay framebuffer with an alpha of zero. This is however
106 * handled not by the Qt environment variables but by the KMS json configuration
107 * file which is pointed to by QT_QPA_EGLFS_KMS_CONFIG. So we create or modifiy
108 * that file here and direct Qt to it.
109 *
110 * Finally, if the video and GUI planes are of the same type, we need to tell Qt
111 * to set the zpos for the GUI plane to ensure it is on top of the video.
112 *
113 * So we need to set 4 environment variables and create one config file...
114 *
115 * \note If *any* of the 4 environment variables have been set by the user then
116 * we assume the user has a custom solution and do nothing.
117 *
118 * \note This is called immediately after application startup; all we have for
119 * reference is the MythCommandLineParsers instance and any environment variables.
120*/
121#if CONFIG_QTPRIVATEHEADERS
122MythDRMPtr MythDRMDevice::FindDevice(bool NeedPlanes)
123{
124 // Retrieve possible devices and analyse them.
125 // We are only interested in authenticated devices with a connected connector.
126 // We can only use one device, so if there are multiple devices (RPI4 only?) then
127 // take the first with a connected connector (which on the RPI4 at least is
128 // usually the better choice anyway).
129 auto [root, devices] = GetDeviceList();
130
131 // Allow the user to specify the device
132 if (!s_mythDRMDevice.isEmpty())
133 {
134 LOG(VB_GENERAL, LOG_INFO, QString("Forcing '%1' as DRM device").arg(s_mythDRMDevice));
135 root.clear();
136 devices.clear();
137 devices.append(s_mythDRMDevice);
138 }
139
140 for (const auto & dev : std::as_const(devices))
141 if (auto device = MythDRMDevice::Create(nullptr, root + dev, NeedPlanes); device && device->Authenticated())
142 return device;
143
144 return nullptr;
145}
146
147void MythDRMDevice::SetupDRM(const MythCommandLineParser& CmdLine)
148{
149 // Try and enable/disable FreeSync if requested by the user
150 if (CmdLine.toBool("vrr"))
151 MythDRMVRR::ForceFreeSync(FindDevice(false), CmdLine.toUInt("vrr") > 0);
152
153 // Return early if eglfs is not *explicitly* requested via the command line or environment.
154 // Note: On some setups it is not necessary to explicitly request eglfs for Qt to use it.
155 // Note: Not sure which takes precedent in Qt or what happens if they are different.
156 auto platform = CmdLine.toString("platform");
157 if (platform.isEmpty())
158 platform = qEnvironmentVariable("QT_QPA_PLATFORM");
159 if (!platform.contains("eglfs", Qt::CaseInsensitive))
160 {
161 // Log something just in case it reminds someone to enable eglfs
162 LOG(VB_GENERAL, LOG_INFO, "'eglfs' not explicitly requested. Not configuring DRM.");
163 return;
164 }
165
166 // Qt environment variables
167 static const char * s_kmsPlaneIndex = "QT_QPA_EGLFS_KMS_PLANE_INDEX"; // Qt 5.9
168 static const char * s_kmsPlaneCRTCS = "QT_QPA_EGLFS_KMS_PLANES_FOR_CRTCS"; // Qt 5.15
169 static const char * s_kmsPlaneZpos = "QT_QPA_EGLFS_KMS_ZPOS"; // Qt 5.12
170 static const char * s_kmsConfigFile = "QT_QPA_EGLFS_KMS_CONFIG";
171 static const char * s_kmsAtomic = "QT_QPA_EGLFS_KMS_ATOMIC"; // Qt 5.12
172 static const char * s_kmsSetMode = "QT_QPA_EGLFS_ALWAYS_SET_MODE";
173
174 // The following 2 environment variables are forced regardless of any existing
175 // environment settings etc. They are just needed and should have no adverse
176 // impacts
177
178 // If we are using eglfs_kms we want atomic operations. No effect on other plugins.
179 LOG(VB_GENERAL, LOG_INFO, QString("Exporting '%1=1'").arg(s_kmsAtomic));
180 setenv(s_kmsAtomic, "1", 0);
181
182 // Seems to fix occasional issues. Again no impact on other plugins.
183 LOG(VB_GENERAL, LOG_INFO, QString("Exporting '%1=1'").arg(s_kmsSetMode));
184 setenv(s_kmsSetMode, "1", 0);
185
186 bool plane = qEnvironmentVariableIsSet(s_kmsPlaneIndex) ||
187 qEnvironmentVariableIsSet(s_kmsPlaneCRTCS);
188 bool config = qEnvironmentVariableIsSet(s_kmsConfigFile);
189 bool zpos = qEnvironmentVariableIsSet(s_kmsPlaneZpos);
190 bool custom = plane || config || zpos;
191
192 // Don't attempt to override any custom user configuration
193 if (custom)
194 {
195 LOG(VB_GENERAL, LOG_INFO, "QT_QPA_EGLFS_KMS user overrides detected");
196
197 if (!s_mythDRMVideo)
198 {
199 // It is likely the user is customising planar video; so warn if planar
200 // video has not been enabled
201 LOG(VB_GENERAL, LOG_WARNING, "Qt eglfs_kms custom plane settings detected"
202 " but planar support not requested.");
203 }
204 else
205 {
206 // Planar support requested so we must signal to our future self
207 s_planarRequested = true;
208
209 // We don't know whether zpos support is required at this point
210 if (!zpos)
211 {
212 LOG(VB_GENERAL, LOG_WARNING, QString("%1 not detected - assuming not required")
213 .arg(s_kmsPlaneZpos));
214 }
215
216 // Warn if we do no see all of the known required config
217 if (!(plane && config))
218 {
219 LOG(VB_GENERAL, LOG_WARNING, "Warning: DRM planar support requested but "
220 "it looks like not all environment variables have been set.");
221 LOG(VB_GENERAL, LOG_INFO,
222 QString("Minimum required: %1 and/or %2 for plane index and %3 for alpha blending")
223 .arg(s_kmsPlaneIndex, s_kmsPlaneCRTCS, s_kmsConfigFile));
224 }
225 else
226 {
227 LOG(VB_GENERAL, LOG_INFO, "DRM planar support enabled for custom user settings");
228 }
229 }
230 return;
231 }
232
233 if (!s_mythDRMVideo)
234 {
235 LOG(VB_GENERAL, LOG_INFO, "Qt eglfs_kms planar video not requested");
236 return;
237 }
238
239 MythDRMPtr device = FindDevice();
240 if (!device)
241 {
242 LOG(VB_GENERAL, LOG_WARNING, "Failed to open any suitable DRM devices with privileges");
243 return;
244 }
245
246 if (!(device->m_guiPlane.get() && device->m_guiPlane->m_id &&
247 device->m_videoPlane.get() && device->m_videoPlane->m_id))
248 {
249 LOG(VB_GENERAL, LOG_WARNING, QString("Failed to deduce correct planes for device '%1'")
250 .arg(drmGetDeviceNameFromFd2(device->GetFD())));
251 return;
252 }
253
254 // We have a valid, authenticated device with a connected display and validated planes
255 auto guiplane = device->m_guiPlane;
256 auto format = MythDRMPlane::GetAlphaFormat(guiplane->m_formats);
257 if (format == DRM_FORMAT_INVALID)
258 {
259 LOG(VB_GENERAL, LOG_WARNING, "Failed to find alpha format for GUI. Quitting DRM setup.");
260 return;
261 }
262
263 // N.B. No MythDirs setup yet so mimic the conf dir setup
264 QString confdir = qEnvironmentVariable("MYTHCONFDIR");
265 if (confdir.isEmpty())
266 confdir = QDir::homePath() + "/.mythtv";
267
268 auto filename = confdir + "/eglfs_kms_config.json";
269 QFile file(filename);
270 if (!file.open(QIODevice::WriteOnly))
271 {
272 LOG(VB_GENERAL, LOG_WARNING, QString("Failed to open '%1' for writing. Quitting DRM setup.")
273 .arg(filename));
274 return;
275 }
276
277 static const QString s_json =
278 "{\n"
279 " \"device\": \"%1\",\n"
280 " \"outputs\": [ { \"name\": \"%2\", \"format\": \"%3\", \"mode\": \"%4\" } ]\n"
281 "}\n";
282
283 // Note: mode is not sanitised
284 QString wrote = s_json.arg(drmGetDeviceNameFromFd2(device->GetFD()),
285 device->m_connector->m_name, MythDRMPlane::FormatToString(format).toLower(),
286 s_mythDRMVideoMode.isEmpty() ? "current" : s_mythDRMVideoMode);
287
288 if (file.write(qPrintable(wrote)))
289 {
290 LOG(VB_GENERAL, LOG_INFO, QString("Wrote %1:\r\n%2").arg(filename, wrote));
291 LOG(VB_GENERAL, LOG_INFO, QString("Exporting '%1=%2'").arg(s_kmsConfigFile, filename));
292 setenv(s_kmsConfigFile, qPrintable(filename), 1);
293 }
294 file.close();
295
296 auto planeindex = QString::number(guiplane->m_index);
297 auto crtcplane = QString("%1,%2").arg(device->m_crtc->m_id).arg(guiplane->m_id);
298 LOG(VB_GENERAL, LOG_INFO, QString("Exporting '%1=%2'").arg(s_kmsPlaneIndex, planeindex));
299 LOG(VB_GENERAL, LOG_INFO, QString("Exporting '%1=%2'").arg(s_kmsPlaneCRTCS, crtcplane));
300 setenv(s_kmsPlaneIndex, qPrintable(planeindex), 1);
301 setenv(s_kmsPlaneCRTCS, qPrintable(crtcplane), 1);
302
303 // Set the zpos if supported
304 if (auto zposp = MythDRMProperty::GetProperty("zpos", guiplane->m_properties); zposp.get())
305 {
306 if (auto *range = dynamic_cast<MythDRMRangeProperty*>(zposp.get()); range)
307 {
308 auto val = QString::number(std::min(range->m_min + 1, range->m_max));
309 LOG(VB_GENERAL, LOG_INFO, QString("Exporting '%1=%2'").arg(s_kmsPlaneZpos, val));
310 setenv(s_kmsPlaneZpos, qPrintable(val), 1);
311 }
312 }
313
314 // Signal to our future self that we did request some Qt DRM configuration
315 s_planarRequested = true;
316}
317#endif
318
322MythDRMPtr MythDRMDevice::Create(QScreen *qScreen, const QString &Device,
323 [[maybe_unused]] bool NeedPlanes)
324{
325#if CONFIG_QTPRIVATEHEADERS
326 auto * app = dynamic_cast<QGuiApplication *>(QCoreApplication::instance());
327 if (qScreen && app && QGuiApplication::platformName().contains("eglfs", Qt::CaseInsensitive))
328 {
329 int fd = 0;
330 uint32_t crtc = 0;
331 uint32_t connector = 0;
332 bool useatomic = false;
333 auto * pni = QGuiApplication::platformNativeInterface();
334 if (auto * drifd = pni->nativeResourceForIntegration("dri_fd"); drifd)
335 fd = static_cast<int>(reinterpret_cast<qintptr>(drifd));
336 if (auto * crtcid = pni->nativeResourceForScreen("dri_crtcid", qScreen); crtcid)
337 crtc = static_cast<uint32_t>(reinterpret_cast<qintptr>(crtcid));
338 if (auto * connid = pni->nativeResourceForScreen("dri_connectorid", qScreen); connid)
339 connector = static_cast<uint32_t>(reinterpret_cast<qintptr>(connid));
340 if (auto * atomic = pni->nativeResourceForIntegration("dri_atomic_request"); atomic)
341 if (auto * request = reinterpret_cast<drmModeAtomicReq*>(atomic); request != nullptr)
342 useatomic = true;
343
344 LOG(VB_GENERAL, LOG_INFO, QString("%1 Qt EGLFS/KMS Fd:%2 Crtc id:%3 Connector id:%4 Atomic: %5")
345 .arg(drmGetDeviceNameFromFd2(fd)).arg(fd).arg(crtc).arg(connector).arg(useatomic));
346
347 // We have all the details we need from Qt
348 if (fd && crtc && connector)
349 {
350 if (auto result = std::shared_ptr<MythDRMDevice>(new MythDRMDevice(fd, crtc, connector, useatomic));
351 result.get() && result->m_valid)
352 {
353 return result;
354 }
355 }
356 }
357#endif
358
359 if (qScreen)
360 {
361 if (auto result = std::shared_ptr<MythDRMDevice>(new MythDRMDevice(qScreen, Device));
362 result.get() && result->m_valid)
363 {
364 return result;
365 }
366 // N.B. Don't fall through here.
367 return nullptr;
368 }
369
370#if CONFIG_QTPRIVATEHEADERS
371 if (auto result = std::shared_ptr<MythDRMDevice>(new MythDRMDevice(Device, NeedPlanes)); result && result->m_valid)
372 return result;
373#endif
374 return nullptr;
375}
376
377std::tuple<QString, QStringList> MythDRMDevice::GetDeviceList()
378{
379 // Iterate over /dev/dri/card*
380 const QString root(QString(DRM_DIR_NAME) + "/");
381 QDir dir(root);
382 QStringList namefilters;
383#ifdef Q_OS_OPENBSD
384 namefilters.append("drm*");
385#else
386 namefilters.append("card*");
387#endif
388 return { root, dir.entryList(namefilters, QDir::Files | QDir::System) };
389}
390
397MythDRMDevice::MythDRMDevice(QScreen* qScreen, const QString& Device)
398 : m_screen(qScreen),
399 m_deviceName(Device),
400 m_verbose(Device.isEmpty() ? LOG_INFO : LOG_DEBUG)
401{
402 // This is hackish workaround to suppress logging when it isn't required
403 if (m_deviceName == DRM_QUIET)
404 {
405 m_deviceName.clear();
406 m_verbose = LOG_DEBUG;
407 }
408
409 if (!Open())
410 {
411 LOG(VB_GENERAL, m_verbose, LOC + "Failed to open");
412 return;
413 }
414
415 if (!Initialise())
416 return;
417
418 m_valid = true;
419
420 // Will almost certainly fail
421 Authenticate();
422}
423
424#if CONFIG_QTPRIVATEHEADERS
433MythDRMDevice::MythDRMDevice(int Fd, uint32_t CrtcId, uint32_t ConnectorId, bool Atomic)
434 : m_openedDevice(false),
435 m_fd(Fd),
436 m_atomic(Atomic)
437{
438 if (m_fd < 1)
439 return;
440
441 // Get the device name for debugging
442 m_deviceName = drmGetDeviceNameFromFd2(m_fd);
443
444 // This should always succeed here...
445 Authenticate();
446
447 // Retrieve all objects
448 Load();
449
450 // Get correct connector and Crtc
453 m_valid = m_connector.get() && m_crtc.get();
454
455 if (m_valid)
456 {
457 // Get physical size
458 m_physicalSize = QSize(static_cast<int>(m_connector->m_mmWidth),
459 static_cast<int>(m_connector->m_mmHeight));
460 // Get EDID
461 auto prop = MythDRMProperty::GetProperty("EDID", m_connector->m_properties);
462 if (auto *blob = dynamic_cast<MythDRMBlobProperty*>(prop.get()); blob)
463 {
464 MythEDID edid(blob->m_blob);
465 if (edid.Valid())
466 m_edid = edid;
467 }
468
469 // Get resolution and rate
470 m_resolution = QSize(static_cast<int>(m_crtc->m_width), static_cast<int>(m_crtc->m_height));
471 if (m_crtc->m_mode.get())
472 m_refreshRate = m_crtc->m_mode->m_rate;
473
474 // Only setup video and gui planes if requested
475 if (s_planarRequested)
476 {
477 AnalysePlanes();
478 if (m_videoPlane.get() && m_guiPlane.get() && m_videoPlane->m_id && m_guiPlane->m_id)
479 s_planarSetup = true;
480 }
481 LOG(VB_GENERAL, LOG_INFO, LOC + "DRM device retrieved from Qt");
482 }
483 else
484 {
485 LOG(VB_GENERAL, LOG_ERR, LOC + "Device setup failed");
486 }
487
488 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Multi-plane setup: Requested: %1 Setup: %2")
489 .arg(s_planarRequested).arg(s_planarSetup));
490}
491
498MythDRMDevice::MythDRMDevice(QString Device, bool NeedPlanes)
499 : m_deviceName(std::move(Device)),
500 m_atomic(true) // Just squashes some logging
501{
502 if (!Open())
503 return;
504 Authenticate();
505 if (!m_authenticated)
506 return;
507 m_valid = drmSetClientCap(m_fd, DRM_CLIENT_CAP_UNIVERSAL_PLANES, 1) == 0;
508 if (!m_valid)
509 {
510 LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to request universal planes");
511 return;
512 }
513 Load();
514 m_valid = false;
515
516 // Find a user suggested connector or the first connected. Oddly
517 // clang-tidy-16 thinks the "if" and "else" clauses are the same.
518 // NOLINTNEXTLINE(bugprone-branch-clone)
519 if (!s_mythDRMConnector.isEmpty())
520 {
522 }
523 else
524 {
525 for (const auto & connector : m_connectors)
526 {
527 if (connector->m_state == DRM_MODE_CONNECTED)
528 {
529 m_connector = connector;
530 break;
531 }
532 }
533 }
534
535 if (!m_connector)
536 {
537 LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to find connector");
538 return;
539 }
540
541 auto encoder = MythDRMEncoder::GetEncoder(m_encoders, m_connector->m_encoderId);
542 if (!encoder.get())
543 return;
544
545 m_crtc = MythDRMCrtc::GetCrtc(m_crtcs, encoder->m_crtcId);
546 if (!m_crtc)
547 return;
548
549 if (NeedPlanes)
550 {
551 AnalysePlanes();
552 m_valid = m_videoPlane.get() && m_guiPlane.get();
553 }
554 else
555 {
556 m_valid = true;
557 }
558}
559#endif
560
562{
563 if (m_fd && m_openedDevice)
564 {
565 close(m_fd);
566 LOG(VB_GENERAL, m_verbose, LOC + "Closed");
567 }
568}
569
571{
572 if (m_deviceName.isEmpty())
574 if (m_deviceName.isEmpty())
575 return false;
576 m_fd = open(m_deviceName.toLocal8Bit().constData(), O_RDWR);
577 return m_fd > 0;
578}
579
581{
582 return m_valid && m_authenticated;
583}
584
586{
587 return m_atomic;
588}
589
591{
592 return m_fd;
593}
594
596{
597 return m_serialNumber;
598}
599
601{
602 return m_screen;
603}
604
606{
607 return m_resolution;
608}
609
611{
612 return m_physicalSize;
613}
614
616{
617 return m_edid;
618}
619
631{
632 if (m_adjustedRefreshRate > 1.0)
634 return m_refreshRate;
635}
636
638{
639 return m_valid && m_authenticated && m_atomic;
640}
641
642// NOLINTNEXTLINE(readability-convert-member-functions-to-static)
644{
645 static const DRMModes empty;
647 return m_connector->m_modes;
648 return empty;
649}
650
657bool MythDRMDevice::SwitchMode(int ModeIndex)
658{
659 if (!(m_authenticated && m_atomic && m_connector.get() && m_crtc.get()))
660 return false;
661
662 auto index = static_cast<size_t>(ModeIndex);
663
664 if (ModeIndex < 0 || index >= m_connector->m_modes.size())
665 return false;
666
667 bool result = false;
668#if CONFIG_QTPRIVATEHEADERS
669 auto crtcid = MythDRMProperty::GetProperty("crtc_id", m_connector->m_properties);
670 auto modeid = MythDRMProperty::GetProperty("mode_id", m_crtc->m_properties);
671 if (crtcid.get() && modeid.get())
672 {
673 uint32_t blobid = 0;
674 // Presumably blobid does not need to be released? Can't find any documentation but
675 // there is the matching drmModeDestroyPropertyBlob...
676 if (drmModeCreatePropertyBlob(m_fd, &m_connector->m_modes[index], sizeof(drmModeModeInfo), &blobid) == 0)
677 {
678 QueueAtomics( {{ m_connector->m_id, crtcid->m_id, m_crtc->m_id },
679 { m_crtc->m_id, modeid->m_id, blobid }} );
680 m_adjustedRefreshRate = m_connector->m_modes[index]->m_rate;
681 result = true;
682 }
683 else
684 {
685 LOG(VB_GENERAL, LOG_ERR, LOC + "Failed to create mode blob");
686 }
687 }
688#endif
689 return result;
690}
691
699{
700 if (!m_fd || m_authenticated)
701 return;
702
703 int ret = drmSetMaster(m_fd);
704 m_authenticated = ret >= 0;
705
706 if (!m_authenticated)
707 {
708 drm_magic_t magic = 0;
709 m_authenticated = drmGetMagic(m_fd, &magic) == 0 && drmAuthMagic(m_fd, magic) == 0;
710 }
711
712 if (m_authenticated)
713 {
714 const auto * extra = m_atomic ? "" : " but atomic operations required for mode switching";
715 LOG(VB_GENERAL, m_verbose, LOC + "Authenticated" + extra);
716 }
717 else
718 {
719 LOG(VB_GENERAL, m_verbose, LOC + "Not authenticated - mode switching not available");
720 }
721}
722
724{
728}
729
731{
732 if (!m_fd)
733 return false;
734
735 // Find the serial number of the display we are connected to
736 auto serial = m_screen ? m_screen->serialNumber() : "";
737 if (m_screen && serial.isEmpty())
738 {
739 // No serial number either means an older version of Qt or the EDID
740 // is not available for some reason - in which case there is no point
741 // in trying to use it anyway.
742 LOG(VB_GENERAL, m_verbose, LOC + "QScreen has no serial number.");
743 LOG(VB_GENERAL, m_verbose, LOC + "Will use first suitable connected device");
744 }
745
746 // Retrieve full details for the device
747 Load();
748
749 // Find connector
750 for (const auto & connector : m_connectors)
751 {
752 if (connector->m_state == DRM_MODE_CONNECTED)
753 {
754 if (serial.isEmpty())
755 {
756 m_connector = connector;
757 break;
758 }
759
760 // Does the connected display have the serial number we are looking for?
761 if (const auto edidprop = MythDRMProperty::GetProperty("EDID", connector->m_properties); edidprop.get())
762 {
763 MythEDID edid;
764 if (auto * blob = dynamic_cast<MythDRMBlobProperty*>(edidprop.get()); blob)
765 edid = MythEDID(blob->m_blob);
766
767 if (edid.Valid() && edid.SerialNumbers().contains(serial))
768 {
769 LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Matched connector with serial '%1'")
770 .arg(serial));
771 m_connector = connector;
772 m_physicalSize = QSize(static_cast<int>(connector->m_mmWidth),
773 static_cast<int>(connector->m_mmHeight));
774 m_serialNumber = serial;
775 m_edid = edid;
776 break;
777 }
778
779 if (!edid.Valid())
780 LOG(VB_GENERAL, m_verbose, LOC + "Connected device has invalid EDID");
781
782 if (m_connector && !m_serialNumber.isEmpty())
783 break;
784 }
785 else
786 {
787 LOG(VB_GENERAL, m_verbose, LOC + "Connected device has no EDID");
788 }
789 }
790 LOG(VB_GENERAL, LOG_DEBUG, LOC + QString("Ignoring disconnected connector %1")
791 .arg(connector->m_name));
792 }
793
794 if (!m_connector.get())
795 {
796 LOG(VB_GENERAL, LOG_DEBUG, LOC + "No connected connectors");
797 return false;
798 }
799
800 LOG(VB_GENERAL, m_verbose, LOC + QString("Selected connector %1").arg(m_connector->m_name));
801
802 // Find the encoder for the connector
803 auto encoder = MythDRMEncoder::GetEncoder(m_encoders, m_connector->m_encoderId);
804 if (!encoder)
805 {
806 LOG(VB_GENERAL, m_verbose, LOC + QString("Failed to find encoder for %1").arg(m_connector->m_name));
807 return false;
808 }
809
810 // Find the CRTC for the encoder
811 m_crtc = MythDRMCrtc::GetCrtc(m_crtcs, encoder->m_crtcId);
812 if (!m_crtc)
813 {
814 LOG(VB_GENERAL, LOG_DEBUG, LOC + "Failed to find crtc for encoder");
815 return false;
816 }
817
818 m_resolution = QSize(static_cast<int>(m_crtc->m_width), static_cast<int>(m_crtc->m_height));
819 if (m_crtc->m_mode.get())
820 m_refreshRate = m_crtc->m_mode->m_rate;
821
822 LOG(VB_GENERAL, LOG_DEBUG, LOC + "Initialised");
823 return true;
824}
825
827{
828 if (!m_screen)
829 return {};
830
831 auto [root, devices] = GetDeviceList();
832 if (devices.isEmpty())
833 return {};
834
835 // Only one device - return it
836 if (devices.size() == 1)
837 return root + devices.first();
838
839 // Use the serial number from the current QScreen to select a suitable device
840 auto serial = m_screen->serialNumber();
841 if (serial.isEmpty())
842 {
843 LOG(VB_GENERAL, m_verbose, LOC + "No serial number to search for");
844 return {};
845 }
846
847 for (const auto& dev : std::as_const(devices))
848 {
849 QString device = root + dev;
850 if (!ConfirmDevice(device))
851 {
852 LOG(VB_GENERAL, m_verbose, LOC + "Failed to confirm device");
853 continue;
854 }
855 MythDRMDevice drmdevice(m_screen, device);
856 if (drmdevice.GetSerialNumber() == serial)
857 return device;
858 }
859 return {};
860}
861
863{
864 bool result = false;
865 int fd = open(Device.toLocal8Bit().constData(), O_RDWR);
866 if (fd < 0)
867 return result;
868 drmVersionPtr version = drmGetVersion(fd);
869 if (version)
870 {
871 drmFreeVersion(version);
872 result = true;
873 }
874 close(fd);
875 return result;
876}
877
879{
880 return m_crtc;
881}
882
884{
885 return m_connector;
886}
887
888#if CONFIG_QTPRIVATEHEADERS
889void MythDRMDevice::MainWindowReady()
890{
891 // This is causing issues - disabled for now
892 //DisableVideoPlane();
893
894 // Temporarily disabled - this is informational only
895 /*
896 // Confirm GUI plane format now that Qt is setup
897 if (m_guiPlane.get())
898 {
899 // TODO Add methods to retrieve up to date property values rather than
900 // create new objects
901 if (auto plane = MythDRMPlane::Create(m_fd, m_guiPlane->m_id, 0); plane)
902 {
903 if (auto guifb = MythDRMFramebuffer::Create(m_fd, plane->m_fbId); guifb)
904 {
905 if (MythDRMPlane::HasOverlayFormat({ guifb->m_format }))
906 {
907 LOG(VB_GENERAL, LOG_INFO, LOC + QString("GUI alpha format confirmed (%1)")
908 .arg(MythDRMPlane::FormatToString(guifb->m_format)));
909 }
910 else
911 {
912 LOG(VB_GENERAL, LOG_WARNING, LOC + QString("GUI plane has no alpha (%1)")
913 .arg(MythDRMPlane::FormatToString(guifb->m_format)));
914 }
915 }
916 }
917 }
918 */
919}
920
921bool MythDRMDevice::QueueAtomics(const MythAtomics& Atomics) const
922{
923 auto * app = dynamic_cast<QGuiApplication *>(QCoreApplication::instance());
924 if (!(m_atomic && m_authenticated && app))
925 return false;
926
927 auto * pni = QGuiApplication::platformNativeInterface();
928 if (auto * dri = pni->nativeResourceForIntegration("dri_atomic_request"); dri)
929 {
930 if (auto * request = reinterpret_cast<drmModeAtomicReq*>(dri); request != nullptr)
931 {
932 for (const auto & a : Atomics)
933 drmModeAtomicAddProperty(request, std::get<0>(a), std::get<1>(a), std::get<2>(a));
934 return true;
935 }
936 }
937 return false;
938}
939
940void MythDRMDevice::DisableVideoPlane()
941{
942 if (m_videoPlane.get())
943 {
944 LOG(VB_GENERAL, LOG_INFO, LOC + "Disabling video plane");
945 QueueAtomics( {{ m_videoPlane->m_id, m_videoPlane->m_fbIdProp->m_id, 0 },
946 { m_videoPlane->m_id, m_videoPlane->m_crtcIdProp->m_id, 0 }} );
947 }
948}
949
950DRMPlane MythDRMDevice::GetVideoPlane() const
951{
952 return m_videoPlane;
953}
954
955DRMPlane MythDRMDevice::GetGUIPlane() const
956{
957 return m_guiPlane;
958}
959
979void MythDRMDevice::AnalysePlanes()
980{
981 if (!m_fd || !m_crtc || m_crtc->m_index <= -1)
982 return;
983
984 // Find our planes
985 auto allplanes = MythDRMPlane::GetPlanes(m_fd);
987
988 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Found %1 planes; %2 for this CRTC")
989 .arg(allplanes.size()).arg(m_planes.size()));
990
991 // NOLINTBEGIN(cppcoreguidelines-init-variables)
992 DRMPlanes primaryVideo;
993 DRMPlanes overlayVideo;
994 DRMPlanes primaryGUI;
995 DRMPlanes overlayGUI;
996 // NOLINTEND(cppcoreguidelines-init-variables)
997
998 for (const auto & plane : m_planes)
999 {
1000 if (plane->m_type == DRM_PLANE_TYPE_PRIMARY)
1001 {
1002 if (!plane->m_videoFormats.empty())
1003 primaryVideo.emplace_back(plane);
1004 if (MythDRMPlane::HasOverlayFormat(plane->m_formats))
1005 primaryGUI.emplace_back(plane);
1006 }
1007 else if (plane->m_type == DRM_PLANE_TYPE_OVERLAY)
1008 {
1009 if (!plane->m_videoFormats.empty())
1010 overlayVideo.emplace_back(plane);
1011 if (MythDRMPlane::HasOverlayFormat(plane->m_formats))
1012 overlayGUI.emplace_back(plane);
1013 }
1014
1015 if (VERBOSE_LEVEL_CHECK(VB_PLAYBACK, LOG_INFO))
1016 LOG(VB_PLAYBACK, LOG_INFO, LOC + plane->Description());
1017 }
1018
1019 // This *should not happen*
1020 if (primaryGUI.empty() && overlayGUI.empty())
1021 return;
1022
1023 // Neither should this really...
1024 if (primaryVideo.empty() && overlayVideo.empty())
1025 {
1026 LOG(VB_GENERAL, LOG_WARNING, LOC + "Found no planes with video support");
1027 return;
1028 }
1029
1030 // Need to ensure we don't pick the same plane for video and GUI
1031 auto nodupe = [](const auto & Planes, const auto & Plane)
1032 {
1033 for (const auto & plane : Planes)
1034 if (plane->m_id != Plane->m_id)
1035 return plane;
1036 return DRMPlane { nullptr };
1037 };
1038
1039 // Note: If video is an overlay or both planes are of the same type then
1040 // video composition will likely fail if there is no zpos support. Oddly
1041 // clang-tidy-16 thinks the "if" and "else" clauses are the same.
1042 // NOLINTNEXTLINE(bugprone-branch-clone)
1043 if (primaryVideo.empty())
1044 {
1045 m_videoPlane = overlayVideo.front();
1046 if (overlayGUI.empty())
1047 m_guiPlane = primaryGUI.front();
1048 else
1049 m_guiPlane = nodupe(overlayGUI, m_videoPlane);
1050 }
1051 else
1052 {
1053 m_videoPlane = primaryVideo.front();
1054 if (overlayGUI.empty())
1055 m_guiPlane = nodupe(primaryGUI, m_videoPlane);
1056 else
1057 m_guiPlane = overlayGUI.front(); // Simple primary video and overlay GUI
1058 }
1059
1060 if (!m_videoPlane.get())
1061 {
1062 LOG(VB_GENERAL, LOG_ERR, LOC + "No video plane");
1063 }
1064 else
1065 {
1066 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Selected Plane #%1 %2 for video")
1067 .arg(m_videoPlane->m_id).arg(MythDRMPlane::PlaneTypeToString(m_videoPlane->m_type)));
1068 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Supported DRM video formats: %1")
1069 .arg(MythDRMPlane::FormatsToString(m_videoPlane->m_videoFormats)));
1070 }
1071
1072 if (!m_guiPlane.get())
1073 {
1074 LOG(VB_GENERAL, LOG_ERR, LOC + "No GUI plane");
1075 }
1076 else
1077 {
1078 LOG(VB_GENERAL, LOG_INFO, LOC + QString("Selected Plane #%1 %2 for GUI")
1079 .arg(m_guiPlane->m_id).arg(MythDRMPlane::PlaneTypeToString(m_guiPlane->m_type)));
1080 }
1081}
1082#endif
A device containing images (ie. USB stick, CD, storage group etc)
Parent class for defining application command line parsers.
bool toBool(const QString &key) const
Returns stored QVariant as a boolean.
QString toString(const QString &key) const
Returns stored QVariant as a QString, falling to default if not provided.
uint toUInt(const QString &key) const
Returns stored QVariant as an unsigned integer, falling to default if not provided.
static DRMConns GetConnectors(int FD)
static DRMConn GetConnectorByName(const DRMConns &Connectors, const QString &Name)
static DRMConn GetConnector(const DRMConns &Connectors, uint32_t Id)
static DRMCrtcs GetCrtcs(int FD)
Definition: mythdrmcrtc.cpp:29
static DRMCrtc GetCrtc(const DRMCrtcs &Crtcs, uint32_t Id)
Definition: mythdrmcrtc.cpp:21
static MythDRMPtr Create(QScreen *qScreen, const QString &Device=QString(), bool NeedPlanes=true)
Create a MythDRMDevice instance.
QSize GetPhysicalSize() const
MythEDID GetEDID() const
DRMCrtcs m_crtcs
Definition: mythdrmdevice.h:93
LogLevel_t m_verbose
double m_adjustedRefreshRate
MythDRMDevice(QScreen *qScreen, const QString &Device=QString())
Constructor used when we have no DRM handles from Qt.
QScreen * GetScreen() const
DRMPlanes m_planes
Definition: mythdrmdevice.h:94
static std::tuple< QString, QStringList > GetDeviceList()
DRMCrtc m_crtc
Definition: mythdrmdevice.h:96
static bool ConfirmDevice(const QString &Device)
QSize m_physicalSize
Definition: mythdrmdevice.h:98
QSize m_resolution
Definition: mythdrmdevice.h:97
bool Authenticated() const
QString GetSerialNumber() const
double m_refreshRate
Definition: mythdrmdevice.h:99
void Authenticate()
Attempt to acquire privileged DRM access.
bool CanSwitchModes() const
double GetRefreshRate() const
Return the refresh rate we think is in use.
QString m_deviceName
Definition: mythdrmdevice.h:86
DRMCrtc GetCrtc() const
bool SwitchMode(int ModeIndex)
Set the required video mode.
bool m_authenticated
Definition: mythdrmdevice.h:90
QScreen * m_screen
Definition: mythdrmdevice.h:85
QString FindBestDevice()
DRMConns m_connectors
Definition: mythdrmdevice.h:91
MythEDID m_edid
QSize GetResolution() const
DRMConn GetConnector() const
DRMConn m_connector
Definition: mythdrmdevice.h:95
int GetFD() const
DRMEncs m_encoders
Definition: mythdrmdevice.h:92
const DRMModes & GetModes() const
QString m_serialNumber
bool Atomic() const
static DRMEncs GetEncoders(int FD)
static DRMEnc GetEncoder(const DRMEncs &Encoders, uint32_t Id)
static QString FormatsToString(const FOURCCVec &Formats)
static DRMPlanes GetPlanes(int FD, int CRTCFilter=-1)
static bool HasOverlayFormat(const FOURCCVec &Formats)
Enusure list of supplied formats contains a format that is suitable for OpenGL/Vulkan.
static QString FormatToString(uint32_t Format)
static QString PlaneTypeToString(uint64_t Type)
static uint32_t GetAlphaFormat(const FOURCCVec &Formats)
static DRMProp GetProperty(const QString &Name, const DRMProps &Properties)
static void ForceFreeSync(const MythDRMPtr &Device, bool Enable)
Force FreeSync on or off before the main app is started.
Definition: mythdrmvrr.cpp:11
QStringList SerialNumbers() const
Definition: mythedid.cpp:45
bool Valid() const
Definition: mythedid.cpp:40
#define close
Definition: compat.h:28
#define setenv(x, y, z)
Definition: compat.h:62
static QString confdir
Definition: mythdirs.cpp:25
std::shared_ptr< class MythDRMConnector > DRMConn
std::shared_ptr< class MythDRMCrtc > DRMCrtc
Definition: mythdrmcrtc.h:8
#define LOC
static constexpr const char * DRM_QUIET
Definition: mythdrmdevice.h:23
std::shared_ptr< class MythDRMDevice > MythDRMPtr
Definition: mythdrmdevice.h:19
std::vector< MythAtomic > MythAtomics
Definition: mythdrmdevice.h:21
std::vector< DRMMode > DRMModes
Definition: mythdrmmode.h:8
#define DRM_FORMAT_INVALID
Definition: mythdrmplane.h:13
std::vector< DRMPlane > DRMPlanes
Definition: mythdrmplane.h:51
std::shared_ptr< class MythDRMPlane > DRMPlane
Definition: mythdrmplane.h:50
static bool VERBOSE_LEVEL_CHECK(uint64_t mask, LogLevel_t level)
Definition: mythlogging.h:29
#define LOG(_MASK_, _LEVEL_, _QSTRING_)
Definition: mythlogging.h:39
string version
Definition: giantbomb.py:185
VERBOSE_PREAMBLE Most true
Definition: verbosedefs.h:86
VERBOSE_PREAMBLE false
Definition: verbosedefs.h:80