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