1 /*
2 SPDX-FileCopyrightText: 2019 Roman Gilg <subdiff@gmail.com>
3 SPDX-FileCopyrightText: 2021 David Redondo <kde@david-redondo.de>
4
5 SPDX-License-Identifier: GPL-2.0-or-later
6 */
7 #include "output.h"
8 #include "config.h"
9
10 #include "generator.h"
11 #include "kscreen_daemon_debug.h"
12
13 #include <QDir>
14 #include <QFile>
15 #include <QJsonDocument>
16 #include <QLoggingCategory>
17 #include <QRect>
18 #include <QStringBuilder>
19 #include <QStringList>
20
21 #include <kscreen/edid.h>
22 #include <kscreen/output.h>
23
24 QString Output::s_dirName = QStringLiteral("outputs/");
25
dirPath()26 QString Output::dirPath()
27 {
28 return Globals::dirPath() % s_dirName;
29 }
30
globalFileName(const QString & hash)31 QString Output::globalFileName(const QString &hash)
32 {
33 const auto dir = dirPath();
34 if (!QDir().mkpath(dir)) {
35 return QString();
36 }
37 return dir % hash;
38 }
39
fromInfo(const KScreen::OutputPtr output,const QVariantMap & info)40 static Output::GlobalConfig fromInfo(const KScreen::OutputPtr output, const QVariantMap &info)
41 {
42 Output::GlobalConfig config;
43 bool ok = false;
44 if (int rotation = info.value(QStringLiteral("rotation")).toInt(&ok); ok) {
45 config.rotation = static_cast<KScreen::Output::Rotation>(rotation);
46 }
47
48 if (qreal scale = info.value(QStringLiteral("scale")).toDouble(&ok); ok) {
49 config.scale = scale;
50 }
51
52 if (auto vrr = static_cast<KScreen::Output::VrrPolicy>(info.value(QStringLiteral("vrrpolicy")).toUInt(&ok)); ok) {
53 config.vrrPolicy = vrr;
54 }
55
56 if (auto overscan = info.value(QStringLiteral("overscan")).toUInt(&ok); ok) {
57 config.overscan = overscan;
58 }
59
60 if (auto rgbRange = static_cast<KScreen::Output::RgbRange>(info.value(QStringLiteral("rgbrange")).toUInt(&ok)); ok) {
61 config.rgbRange = rgbRange;
62 }
63
64 const QVariantMap modeInfo = info[QStringLiteral("mode")].toMap();
65 const QVariantMap modeSize = modeInfo[QStringLiteral("size")].toMap();
66 const QSize size = QSize(modeSize[QStringLiteral("width")].toInt(), modeSize[QStringLiteral("height")].toInt());
67
68 qCDebug(KSCREEN_KDED) << "Finding a mode for" << size << "@" << modeInfo[QStringLiteral("refresh")].toFloat();
69
70 const KScreen::ModeList modes = output->modes();
71 for (const KScreen::ModePtr &mode : modes) {
72 if (mode->size() != size) {
73 continue;
74 }
75 if (!qFuzzyCompare(mode->refreshRate(), modeInfo[QStringLiteral("refresh")].toFloat())) {
76 continue;
77 }
78
79 qCDebug(KSCREEN_KDED) << "\tFound: " << mode->id() << " " << mode->size() << "@" << mode->refreshRate();
80 config.modeId = mode->id();
81 break;
82 }
83 return config;
84 }
85
readInGlobalPartFromInfo(KScreen::OutputPtr output,const QVariantMap & info)86 void Output::readInGlobalPartFromInfo(KScreen::OutputPtr output, const QVariantMap &info)
87 {
88 GlobalConfig config = fromInfo(output, info);
89 output->setRotation(config.rotation.value_or(KScreen::Output::Rotation::None));
90 output->setScale(config.scale.value_or(1.0));
91 output->setVrrPolicy(config.vrrPolicy.value_or(KScreen::Output::VrrPolicy::Automatic));
92 output->setOverscan(config.overscan.value_or(0));
93 output->setRgbRange(config.rgbRange.value_or(KScreen::Output::RgbRange::Automatic));
94
95 KScreen::ModePtr matchingMode;
96 if (config.modeId) {
97 matchingMode = output->mode(config.modeId.value());
98 }
99 if (!matchingMode) {
100 qCWarning(KSCREEN_KDED) << "\tFailed to find a matching mode - this means that our config is corrupted"
101 "or a different device with the same serial number has been connected (very unlikely)."
102 "Falling back to preferred modes.";
103 matchingMode = output->preferredMode();
104 }
105 if (!matchingMode) {
106 qCWarning(KSCREEN_KDED) << "\tFailed to get a preferred mode, falling back to biggest mode.";
107 matchingMode = Generator::biggestMode(output->modes());
108 }
109 if (!matchingMode) {
110 qCWarning(KSCREEN_KDED) << "\tFailed to get biggest mode. Which means there are no modes. Turning off the screen.";
111 output->setEnabled(false);
112 return;
113 }
114
115 output->setCurrentModeId(matchingMode->id());
116 }
117
getGlobalData(KScreen::OutputPtr output)118 QVariantMap Output::getGlobalData(KScreen::OutputPtr output)
119 {
120 QString fileName = Globals::findFile(s_dirName % output->hashMd5());
121 if (fileName.isEmpty()) {
122 qCDebug(KSCREEN_KDED) << "No file for" << s_dirName % output->hashMd5();
123 return QVariantMap();
124 }
125 QFile file(fileName);
126 if (!file.open(QIODevice::ReadOnly)) {
127 qCDebug(KSCREEN_KDED) << "Failed to open file" << file.fileName();
128 return QVariantMap();
129 }
130 qCDebug(KSCREEN_KDED) << "Found global data at" << file.fileName();
131 QJsonDocument parser;
132 return parser.fromJson(file.readAll()).toVariant().toMap();
133 }
134
readInGlobal(KScreen::OutputPtr output)135 bool Output::readInGlobal(KScreen::OutputPtr output)
136 {
137 const QVariantMap info = getGlobalData(output);
138 if (info.empty()) {
139 // if info is empty, the global file does not exists, or is in an unreadable state
140 return false;
141 }
142 readInGlobalPartFromInfo(output, info);
143 return true;
144 }
145
readGlobal(const KScreen::OutputPtr & output)146 Output::GlobalConfig Output::readGlobal(const KScreen::OutputPtr &output)
147 {
148 return fromInfo(output, getGlobalData(output));
149 }
150
orientationToRotation(QOrientationReading::Orientation orientation,KScreen::Output::Rotation fallback)151 KScreen::Output::Rotation orientationToRotation(QOrientationReading::Orientation orientation, KScreen::Output::Rotation fallback)
152 {
153 using Orientation = QOrientationReading::Orientation;
154
155 switch (orientation) {
156 case Orientation::TopUp:
157 return KScreen::Output::Rotation::None;
158 case Orientation::TopDown:
159 return KScreen::Output::Rotation::Inverted;
160 case Orientation::LeftUp:
161 return KScreen::Output::Rotation::Left;
162 case Orientation::RightUp:
163 return KScreen::Output::Rotation::Right;
164 case Orientation::Undefined:
165 case Orientation::FaceUp:
166 case Orientation::FaceDown:
167 return fallback;
168 default:
169 Q_UNREACHABLE();
170 }
171 }
172
updateOrientation(KScreen::OutputPtr & output,QOrientationReading::Orientation orientation)173 bool Output::updateOrientation(KScreen::OutputPtr &output, QOrientationReading::Orientation orientation)
174 {
175 if (output->type() != KScreen::Output::Type::Panel) {
176 return false;
177 }
178 const auto currentRotation = output->rotation();
179 const auto rotation = orientationToRotation(orientation, currentRotation);
180 if (rotation == currentRotation) {
181 return true;
182 }
183 output->setRotation(rotation);
184 return true;
185 }
186
187 // TODO: move this into the Layouter class.
adjustPositions(KScreen::ConfigPtr config,const QVariantList & outputsInfo)188 void Output::adjustPositions(KScreen::ConfigPtr config, const QVariantList &outputsInfo)
189 {
190 typedef QPair<int, QPoint> Out;
191
192 KScreen::OutputList outputs = config->outputs();
193 QVector<Out> sortedOutputs; // <id, pos>
194 for (const KScreen::OutputPtr &output : outputs) {
195 sortedOutputs.append(Out(output->id(), output->pos()));
196 }
197
198 // go from left to right, top to bottom
199 std::sort(sortedOutputs.begin(), sortedOutputs.end(), [](const Out &o1, const Out &o2) {
200 const int x1 = o1.second.x();
201 const int x2 = o2.second.x();
202 return x1 < x2 || (x1 == x2 && o1.second.y() < o2.second.y());
203 });
204
205 for (int cnt = 1; cnt < sortedOutputs.length(); cnt++) {
206 auto getOutputInfoProperties = [outputsInfo](KScreen::OutputPtr output, QRect &geo) -> bool {
207 if (!output) {
208 return false;
209 }
210 const auto hash = output->hash();
211
212 auto it = std::find_if(outputsInfo.begin(), outputsInfo.end(), [hash](QVariant v) {
213 const QVariantMap info = v.toMap();
214 return info[QStringLiteral("id")].toString() == hash;
215 });
216 if (it == outputsInfo.end()) {
217 return false;
218 }
219
220 auto isPortrait = [](const QVariant &info) {
221 bool ok;
222 const int rot = info.toInt(&ok);
223 if (!ok) {
224 return false;
225 }
226 return rot & KScreen::Output::Rotation::Left || rot & KScreen::Output::Rotation::Right;
227 };
228
229 const QVariantMap outputInfo = it->toMap();
230
231 const QVariantMap posInfo = outputInfo[QStringLiteral("pos")].toMap();
232 const QVariant scaleInfo = outputInfo[QStringLiteral("scale")];
233 const QVariantMap modeInfo = outputInfo[QStringLiteral("mode")].toMap();
234 const QVariantMap modeSize = modeInfo[QStringLiteral("size")].toMap();
235 const bool portrait = isPortrait(outputInfo[QStringLiteral("rotation")]);
236
237 if (posInfo.isEmpty() || modeSize.isEmpty() || !scaleInfo.canConvert<int>()) {
238 return false;
239 }
240
241 const qreal scale = scaleInfo.toDouble();
242 if (scale <= 0) {
243 return false;
244 }
245 const QPoint pos = QPoint(posInfo[QStringLiteral("x")].toInt(), posInfo[QStringLiteral("y")].toInt());
246 QSize size = QSize(modeSize[QStringLiteral("width")].toInt() / scale, modeSize[QStringLiteral("height")].toInt() / scale);
247 if (portrait) {
248 size.transpose();
249 }
250 geo = QRect(pos, size);
251
252 return true;
253 };
254
255 // it's guaranteed that we find the following values in the QMap
256 KScreen::OutputPtr prevPtr = outputs.find(sortedOutputs[cnt - 1].first).value();
257 KScreen::OutputPtr curPtr = outputs.find(sortedOutputs[cnt].first).value();
258
259 QRect prevInfoGeo, curInfoGeo;
260 if (!getOutputInfoProperties(prevPtr, prevInfoGeo) || !getOutputInfoProperties(curPtr, curInfoGeo)) {
261 // no info found, nothing can be adjusted for the next output
262 continue;
263 }
264
265 const QRect prevGeo = prevPtr->geometry();
266 const QRect curGeo = curPtr->geometry();
267
268 // the old difference between previous and current output read from the config file
269 const int xInfoDiff = curInfoGeo.x() - (prevInfoGeo.x() + prevInfoGeo.width());
270
271 // the proposed new difference
272 const int prevRight = prevGeo.x() + prevGeo.width();
273 const int xCorrected = prevRight + prevGeo.width() * xInfoDiff / (double)prevInfoGeo.width();
274 const int xDiff = curGeo.x() - prevRight;
275
276 // In the following calculate the y-correction. This is more involved since we
277 // differentiate between overlapping and non-overlapping pairs and align either
278 // top to top/bottom or bottom to top/bottom
279 const bool yOverlap = prevInfoGeo.y() + prevInfoGeo.height() > curInfoGeo.y() && prevInfoGeo.y() < curInfoGeo.y() + curInfoGeo.height();
280
281 // these values determine which horizontal edge of previous output we align with
282 const int topToTopDiffAbs = qAbs(prevInfoGeo.y() - curInfoGeo.y());
283 const int topToBottomDiffAbs = qAbs(prevInfoGeo.y() - curInfoGeo.y() - curInfoGeo.height());
284 const int bottomToBottomDiffAbs = qAbs(prevInfoGeo.y() + prevInfoGeo.height() - curInfoGeo.y() - curInfoGeo.height());
285 const int bottomToTopDiffAbs = qAbs(prevInfoGeo.y() + prevInfoGeo.height() - curInfoGeo.y());
286
287 const bool yTopAligned = (topToTopDiffAbs < bottomToBottomDiffAbs && topToTopDiffAbs <= bottomToTopDiffAbs) //
288 || topToBottomDiffAbs < bottomToBottomDiffAbs;
289
290 int yInfoDiff = curInfoGeo.y() - prevInfoGeo.y();
291 int yDiff = curGeo.y() - prevGeo.y();
292 int yCorrected;
293
294 if (yTopAligned) {
295 // align to previous top
296 if (!yOverlap) {
297 // align previous top with current bottom
298 yInfoDiff += curInfoGeo.height();
299 yDiff += curGeo.height();
300 }
301 // When we align with previous top we are interested in the changes to the
302 // current geometry and not in the ones of the previous one.
303 const double yInfoRel = yInfoDiff / (double)curInfoGeo.height();
304 yCorrected = prevGeo.y() + yInfoRel * curGeo.height();
305 } else {
306 // align previous bottom...
307 yInfoDiff -= prevInfoGeo.height();
308 yDiff -= prevGeo.height();
309 yCorrected = prevGeo.y() + prevGeo.height();
310
311 if (yOverlap) {
312 // ... with current bottom
313 yInfoDiff += curInfoGeo.height();
314 yDiff += curGeo.height();
315 yCorrected -= curGeo.height();
316 } // ... else with current top
317
318 // When we align with previous bottom we are interested in changes to the
319 // previous geometry.
320 const double yInfoRel = yInfoDiff / (double)prevInfoGeo.height();
321 yCorrected += yInfoRel * prevGeo.height();
322 }
323
324 const int x = xDiff == xInfoDiff ? curGeo.x() : xCorrected;
325 const int y = yDiff == yInfoDiff ? curGeo.y() : yCorrected;
326 curPtr->setPos(QPoint(x, y));
327 }
328 }
329
readIn(KScreen::OutputPtr output,const QVariantMap & info,Control::OutputRetention retention)330 void Output::readIn(KScreen::OutputPtr output, const QVariantMap &info, Control::OutputRetention retention)
331 {
332 const QVariantMap posInfo = info[QStringLiteral("pos")].toMap();
333 QPoint point(posInfo[QStringLiteral("x")].toInt(), posInfo[QStringLiteral("y")].toInt());
334 output->setPos(point);
335 output->setPrimary(info[QStringLiteral("primary")].toBool());
336 output->setEnabled(info[QStringLiteral("enabled")].toBool());
337
338 if (retention != Control::OutputRetention::Individual && readInGlobal(output)) {
339 // output data read from global output file
340 return;
341 }
342 // output data read directly from info
343 readInGlobalPartFromInfo(output, info);
344 }
345
readInOutputs(KScreen::ConfigPtr config,const QVariantList & outputsInfo)346 void Output::readInOutputs(KScreen::ConfigPtr config, const QVariantList &outputsInfo)
347 {
348 const KScreen::OutputList outputs = config->outputs();
349 ControlConfig control(config);
350 // As global outputs are indexed by a hash of their edid, which is not unique,
351 // to be able to tell apart multiple identical outputs, these need special treatment
352 QStringList duplicateIds;
353 {
354 QStringList allIds;
355 allIds.reserve(outputs.count());
356 for (const KScreen::OutputPtr &output : outputs) {
357 const auto outputId = output->hash();
358 if (allIds.contains(outputId) && !duplicateIds.contains(outputId)) {
359 duplicateIds << outputId;
360 }
361 allIds << outputId;
362 }
363 }
364
365 for (const KScreen::OutputPtr &output : outputs) {
366 if (!output->isConnected()) {
367 output->setEnabled(false);
368 continue;
369 }
370 const auto outputId = output->hash();
371 bool infoFound = false;
372 for (const auto &variantInfo : outputsInfo) {
373 const QVariantMap info = variantInfo.toMap();
374 if (outputId != info[QStringLiteral("id")].toString()) {
375 continue;
376 }
377 if (!output->name().isEmpty() && duplicateIds.contains(outputId)) {
378 // We may have identical outputs connected, these will have the same id in the config
379 // in order to find the right one, also check the output's name (usually the connector)
380 const auto metadata = info[QStringLiteral("metadata")].toMap();
381 const auto outputName = metadata[QStringLiteral("name")].toString();
382 if (output->name() != outputName) {
383 // was a duplicate id, but info not for this output
384 continue;
385 }
386 }
387 infoFound = true;
388 readIn(output, info, control.getOutputRetention(output));
389 break;
390 }
391 if (!infoFound) {
392 // no info in info for this output, try reading in global output info at least or set some default values
393
394 qCWarning(KSCREEN_KDED) << "\tFailed to find a matching output in the current info data - this means that our info is corrupted"
395 "or a different device with the same serial number has been connected (very unlikely).";
396 if (!readInGlobal(output)) {
397 // set some default values instead
398 readInGlobalPartFromInfo(output, QVariantMap());
399 }
400 }
401 }
402
403 for (KScreen::OutputPtr output : outputs) {
404 auto replicationSource = control.getReplicationSource(output);
405 if (replicationSource) {
406 output->setPos(replicationSource->pos());
407 output->setLogicalSize(replicationSource->logicalSize());
408 } else {
409 output->setLogicalSize(QSizeF());
410 }
411 }
412
413 // TODO: this does not work at the moment with logical size replication. Deactivate for now.
414 // correct positional config regressions on global output data changes
415 #if 0
416 adjustPositions(config, outputsInfo);
417 #endif
418 }
419
metadata(const KScreen::OutputPtr & output)420 static QVariantMap metadata(const KScreen::OutputPtr &output)
421 {
422 QVariantMap metadata;
423 metadata[QStringLiteral("name")] = output->name();
424 if (!output->edid() || !output->edid()->isValid()) {
425 return metadata;
426 }
427
428 metadata[QStringLiteral("fullname")] = output->edid()->deviceId();
429 return metadata;
430 }
431
writeGlobalPart(const KScreen::OutputPtr & output,QVariantMap & info,const KScreen::OutputPtr & fallback)432 bool Output::writeGlobalPart(const KScreen::OutputPtr &output, QVariantMap &info, const KScreen::OutputPtr &fallback)
433 {
434 info[QStringLiteral("id")] = output->hash();
435 info[QStringLiteral("metadata")] = metadata(output);
436 info[QStringLiteral("rotation")] = output->rotation();
437
438 // Round scale to four digits
439 info[QStringLiteral("scale")] = int(output->scale() * 10000 + 0.5) / 10000.;
440
441 QVariantMap modeInfo;
442 float refreshRate = -1.;
443 QSize modeSize;
444 if (output->currentMode() && output->isEnabled()) {
445 refreshRate = output->currentMode()->refreshRate();
446 modeSize = output->currentMode()->size();
447 } else if (fallback && fallback->currentMode()) {
448 refreshRate = fallback->currentMode()->refreshRate();
449 modeSize = fallback->currentMode()->size();
450 }
451
452 if (refreshRate < 0 || !modeSize.isValid()) {
453 return false;
454 }
455
456 modeInfo[QStringLiteral("refresh")] = refreshRate;
457
458 QVariantMap modeSizeMap;
459 modeSizeMap[QStringLiteral("width")] = modeSize.width();
460 modeSizeMap[QStringLiteral("height")] = modeSize.height();
461 modeInfo[QStringLiteral("size")] = modeSizeMap;
462
463 info[QStringLiteral("mode")] = modeInfo;
464 info[QStringLiteral("vrrpolicy")] = static_cast<uint32_t>(output->vrrPolicy());
465 info[QStringLiteral("overscan")] = output->overscan();
466 info[QStringLiteral("rgbrange")] = static_cast<uint32_t>(output->rgbRange());
467
468 return true;
469 }
470
writeGlobal(const KScreen::OutputPtr & output)471 void Output::writeGlobal(const KScreen::OutputPtr &output)
472 {
473 // get old values and subsequently override
474 QVariantMap info = getGlobalData(output);
475 if (!writeGlobalPart(output, info, nullptr)) {
476 return;
477 }
478
479 QFile file(globalFileName(output->hashMd5()));
480 if (!file.open(QIODevice::WriteOnly)) {
481 qCWarning(KSCREEN_KDED) << "Failed to open global output file for writing! " << file.errorString();
482 return;
483 }
484
485 file.write(QJsonDocument::fromVariant(info).toJson());
486 return;
487 }
488