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