Phosphor
Qt6 / Wayland library suite for window-management tools
 
Loading...
Searching...
No Matches
ScreenId.h
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 fuddlesworth
2// SPDX-License-Identifier: LGPL-2.1-or-later
3
4#pragma once
5
6// Compositor-portable EDID parsing + screen-ID format helpers.
7//
8// Header-only inline so PhosphorIdentity stays INTERFACE — the cross-
9// process consumers (KWin effect, Wayfire plugin, daemon) all share one
10// definition without anyone shipping an extra .so. The function-local
11// static caches are guaranteed unique across translation units by the
12// C++17 inline-function-static rule.
13
14#include <QByteArray>
15#include <QCoreApplication>
16#include <QDir>
17#include <QFile>
18#include <QHash>
19#include <QIODevice>
20#include <QLatin1Char>
21#include <QLatin1String>
22#include <QString>
23#include <QStringList>
24#include <QThread>
25
26#include <cstdint>
27
29
48namespace ScreenId {
49
50namespace detail {
51
52inline QHash<QString, QString>& edidSerialCache()
53{
54 static QHash<QString, QString> s_cache;
55 return s_cache;
56}
57
58inline QHash<QString, int>& edidMissCounter()
59{
60 static QHash<QString, int> s_counter;
61 return s_counter;
62}
63
68inline void assertMainThread()
69{
70 Q_ASSERT(!QCoreApplication::instance() || QThread::currentThread() == QCoreApplication::instance()->thread());
71}
72
73} // namespace detail
74
87inline QString readEdidHeaderSerial(const QString& connectorName)
88{
90 auto& cache = detail::edidSerialCache();
91 auto cacheIt = cache.constFind(connectorName);
92 if (cacheIt != cache.constEnd()) {
93 return *cacheIt;
94 }
95
96 QString result;
97
98 QDir drmDir(QStringLiteral("/sys/class/drm"));
99 if (drmDir.exists()) {
100 const QStringList entries = drmDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
101 for (const QString& entry : entries) {
102 int dashPos = entry.indexOf(QLatin1Char('-'));
103 if (dashPos < 0) {
104 continue;
105 }
106 if (entry.mid(dashPos + 1) != connectorName) {
107 continue;
108 }
109 QFile edidFile(drmDir.filePath(entry) + QStringLiteral("/edid"));
110 if (!edidFile.open(QIODevice::ReadOnly)) {
111 continue;
112 }
113 QByteArray header = edidFile.read(16);
114 if (header.size() < 16) {
115 continue;
116 }
117 const auto* data = reinterpret_cast<const uint8_t*>(header.constData());
118 if (data[0] != 0x00 || data[1] != 0xFF || data[2] != 0xFF || data[3] != 0xFF || data[4] != 0xFF
119 || data[5] != 0xFF || data[6] != 0xFF || data[7] != 0x00) {
120 continue;
121 }
122 uint32_t serial = data[12] | (static_cast<uint32_t>(data[13]) << 8)
123 | (static_cast<uint32_t>(data[14]) << 16) | (static_cast<uint32_t>(data[15]) << 24);
124 if (serial != 0) {
125 result = QString::number(serial);
126 break;
127 }
128 }
129 }
130
131 if (!result.isEmpty()) {
132 cache.insert(connectorName, result);
133 detail::edidMissCounter().remove(connectorName);
134 } else {
135 constexpr int maxRetries = 3;
136 int& misses = detail::edidMissCounter()[connectorName];
137 ++misses;
138 if (misses >= maxRetries) {
139 cache.insert(connectorName, result);
140 }
141 }
142 return result;
143}
144
149inline void invalidateEdidCache(const QString& connectorName = QString())
150{
152 if (connectorName.isEmpty()) {
153 detail::edidSerialCache().clear();
154 detail::edidMissCounter().clear();
155 } else {
156 detail::edidSerialCache().remove(connectorName);
157 detail::edidMissCounter().remove(connectorName);
158 }
159}
160
168inline QString normalizeHexSerial(const QString& serial)
169{
170 if (!serial.isEmpty() && serial.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
171 bool ok = false;
172 uint32_t numericSerial = serial.toUInt(&ok, 16);
173 if (ok && numericSerial != 0) {
174 return QString::number(numericSerial);
175 }
176 }
177 return serial;
178}
179
187inline QString buildBaseId(const QString& manufacturer, const QString& model, const QString& serial)
188{
189 if (!serial.isEmpty()) {
190 return manufacturer + QLatin1Char(':') + model + QLatin1Char(':') + serial;
191 }
192 if (!manufacturer.isEmpty() || !model.isEmpty()) {
193 return manufacturer + QLatin1Char(':') + model;
194 }
195 return QString();
196}
197
217inline QString buildScreenBaseId(const QString& manufacturer, const QString& model, const QString& serialNumber,
218 const QString& connectorName)
219{
220 QString serial = readEdidHeaderSerial(connectorName);
221 if (serial.isEmpty()) {
222 serial = normalizeHexSerial(serialNumber);
223 }
224 QString baseId = buildBaseId(manufacturer, model, serial);
225 return baseId.isEmpty() ? connectorName : baseId;
226}
227
228} // namespace ScreenId
229
230} // namespace PhosphorIdentity
QHash< QString, QString > & edidSerialCache()
Definition ScreenId.h:52
void assertMainThread()
Debug-build enforcement of the documented single-thread contract: the function-local-static caches ar...
Definition ScreenId.h:68
QHash< QString, int > & edidMissCounter()
Definition ScreenId.h:58
QString readEdidHeaderSerial(const QString &connectorName)
Read the EDID header serial from sysfs for a DRM connector.
Definition ScreenId.h:87
QString normalizeHexSerial(const QString &serial)
Coerce a possibly-hex serial ("0x0001C1A3") to decimal ("115107").
Definition ScreenId.h:168
QString buildScreenBaseId(const QString &manufacturer, const QString &model, const QString &serialNumber, const QString &connectorName)
Build a base ID from raw EDID fields, sysfs serial authoritative.
Definition ScreenId.h:217
void invalidateEdidCache(const QString &connectorName=QString())
Drop cached EDID serial data.
Definition ScreenId.h:149
QString buildBaseId(const QString &manufacturer, const QString &model, const QString &serial)
Assemble a base identifier from EDID-style fields.
Definition ScreenId.h:187
Definition ScreenId.h:28