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 <QDir>
16#include <QFile>
17#include <QHash>
18#include <QIODevice>
19#include <QLatin1Char>
20#include <QLatin1String>
21#include <QString>
22#include <QStringList>
23
24#include <cstdint>
25
27
46namespace ScreenId {
47
48namespace detail {
49
50inline QHash<QString, QString>& edidSerialCache()
51{
52 static QHash<QString, QString> s_cache;
53 return s_cache;
54}
55
56inline QHash<QString, int>& edidMissCounter()
57{
58 static QHash<QString, int> s_counter;
59 return s_counter;
60}
61
62} // namespace detail
63
76inline QString readEdidHeaderSerial(const QString& connectorName)
77{
78 auto& cache = detail::edidSerialCache();
79 auto cacheIt = cache.constFind(connectorName);
80 if (cacheIt != cache.constEnd()) {
81 return *cacheIt;
82 }
83
84 QString result;
85
86 QDir drmDir(QStringLiteral("/sys/class/drm"));
87 if (drmDir.exists()) {
88 const QStringList entries = drmDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
89 for (const QString& entry : entries) {
90 int dashPos = entry.indexOf(QLatin1Char('-'));
91 if (dashPos < 0) {
92 continue;
93 }
94 if (entry.mid(dashPos + 1) != connectorName) {
95 continue;
96 }
97 QFile edidFile(drmDir.filePath(entry) + QStringLiteral("/edid"));
98 if (!edidFile.open(QIODevice::ReadOnly)) {
99 continue;
100 }
101 QByteArray header = edidFile.read(16);
102 if (header.size() < 16) {
103 continue;
104 }
105 const auto* data = reinterpret_cast<const uint8_t*>(header.constData());
106 if (data[0] != 0x00 || data[1] != 0xFF || data[2] != 0xFF || data[3] != 0xFF || data[4] != 0xFF
107 || data[5] != 0xFF || data[6] != 0xFF || data[7] != 0x00) {
108 continue;
109 }
110 uint32_t serial = data[12] | (static_cast<uint32_t>(data[13]) << 8)
111 | (static_cast<uint32_t>(data[14]) << 16) | (static_cast<uint32_t>(data[15]) << 24);
112 if (serial != 0) {
113 result = QString::number(serial);
114 break;
115 }
116 }
117 }
118
119 if (!result.isEmpty()) {
120 cache.insert(connectorName, result);
121 detail::edidMissCounter().remove(connectorName);
122 } else {
123 constexpr int maxRetries = 3;
124 int& misses = detail::edidMissCounter()[connectorName];
125 ++misses;
126 if (misses >= maxRetries) {
127 cache.insert(connectorName, result);
128 }
129 }
130 return result;
131}
132
137inline void invalidateEdidCache(const QString& connectorName = QString())
138{
139 if (connectorName.isEmpty()) {
140 detail::edidSerialCache().clear();
141 detail::edidMissCounter().clear();
142 } else {
143 detail::edidSerialCache().remove(connectorName);
144 detail::edidMissCounter().remove(connectorName);
145 }
146}
147
155inline QString normalizeHexSerial(const QString& serial)
156{
157 if (!serial.isEmpty() && serial.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
158 bool ok = false;
159 uint32_t numericSerial = serial.toUInt(&ok, 16);
160 if (ok && numericSerial != 0) {
161 return QString::number(numericSerial);
162 }
163 }
164 return serial;
165}
166
174inline QString buildBaseId(const QString& manufacturer, const QString& model, const QString& serial)
175{
176 if (!serial.isEmpty()) {
177 return manufacturer + QLatin1Char(':') + model + QLatin1Char(':') + serial;
178 }
179 if (!manufacturer.isEmpty() || !model.isEmpty()) {
180 return manufacturer + QLatin1Char(':') + model;
181 }
182 return QString();
183}
184
192inline QString buildScreenBaseId(const QString& manufacturer, const QString& model, const QString& serialNumber,
193 const QString& connectorName)
194{
195 QString serial = normalizeHexSerial(serialNumber);
196 if (serial.isEmpty()) {
197 serial = readEdidHeaderSerial(connectorName);
198 }
199 QString baseId = buildBaseId(manufacturer, model, serial);
200 return baseId.isEmpty() ? connectorName : baseId;
201}
202
203} // namespace ScreenId
204
205} // namespace PhosphorIdentity
QHash< QString, QString > & edidSerialCache()
Definition ScreenId.h:50
QHash< QString, int > & edidMissCounter()
Definition ScreenId.h:56
QString readEdidHeaderSerial(const QString &connectorName)
Read the EDID header serial from sysfs for a DRM connector.
Definition ScreenId.h:76
QString normalizeHexSerial(const QString &serial)
Coerce a possibly-hex serial ("0x0001C1A3") to decimal ("115107").
Definition ScreenId.h:155
QString buildScreenBaseId(const QString &manufacturer, const QString &model, const QString &serialNumber, const QString &connectorName)
Build a base ID from raw EDID fields with sysfs serial fallback.
Definition ScreenId.h:192
void invalidateEdidCache(const QString &connectorName=QString())
Drop cached EDID serial data.
Definition ScreenId.h:137
QString buildBaseId(const QString &manufacturer, const QString &model, const QString &serial)
Assemble a base identifier from EDID-style fields.
Definition ScreenId.h:174
Definition ScreenId.h:26