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