File indexing completed on 2024-04-28 16:45:09

0001 /*
0002     SPDX-FileCopyrightText: 2019 Roman Gilg <subdiff@gmail.com>
0003     SPDX-FileCopyrightText: 2021 David Redondo <kde@david-redondo.de>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 #include "output.h"
0008 #include "config.h"
0009 
0010 #include "generator.h"
0011 #include "kscreen_daemon_debug.h"
0012 
0013 #include <QDir>
0014 #include <QFile>
0015 #include <QJsonDocument>
0016 #include <QLoggingCategory>
0017 #include <QRect>
0018 #include <QStringBuilder>
0019 #include <QStringList>
0020 
0021 #include <kscreen/edid.h>
0022 
0023 QString Output::s_dirName = QStringLiteral("outputs/");
0024 
0025 QString Output::dirPath()
0026 {
0027     return Globals::dirPath() % s_dirName;
0028 }
0029 
0030 static Output::GlobalConfig fromInfo(const KScreen::OutputPtr output, const QVariantMap &info)
0031 {
0032     Output::GlobalConfig config;
0033     bool ok = false;
0034     if (int rotation = info.value(QStringLiteral("rotation")).toInt(&ok); ok) {
0035         config.rotation = static_cast<KScreen::Output::Rotation>(rotation);
0036     }
0037 
0038     if (qreal scale = info.value(QStringLiteral("scale")).toDouble(&ok); ok) {
0039         config.scale = scale;
0040     }
0041 
0042     if (auto vrr = static_cast<KScreen::Output::VrrPolicy>(info.value(QStringLiteral("vrrpolicy")).toUInt(&ok)); ok) {
0043         config.vrrPolicy = vrr;
0044     }
0045 
0046     if (auto overscan = info.value(QStringLiteral("overscan")).toUInt(&ok); ok) {
0047         config.overscan = overscan;
0048     }
0049 
0050     if (auto rgbRange = static_cast<KScreen::Output::RgbRange>(info.value(QStringLiteral("rgbrange")).toUInt(&ok)); ok) {
0051         config.rgbRange = rgbRange;
0052     }
0053 
0054     const QVariantMap modeInfo = info[QStringLiteral("mode")].toMap();
0055     const QVariantMap modeSize = modeInfo[QStringLiteral("size")].toMap();
0056     const QSize size = QSize(modeSize[QStringLiteral("width")].toInt(), modeSize[QStringLiteral("height")].toInt());
0057 
0058     qCDebug(KSCREEN_KDED) << "Finding a mode for" << size << "@" << modeInfo[QStringLiteral("refresh")].toFloat();
0059 
0060     const KScreen::ModeList modes = output->modes();
0061     for (const KScreen::ModePtr &mode : modes) {
0062         if (mode->size() != size) {
0063             continue;
0064         }
0065         if (!qFuzzyCompare(mode->refreshRate(), modeInfo[QStringLiteral("refresh")].toFloat())) {
0066             continue;
0067         }
0068 
0069         qCDebug(KSCREEN_KDED) << "\tFound: " << mode->id() << " " << mode->size() << "@" << mode->refreshRate();
0070         config.modeId = mode->id();
0071         break;
0072     }
0073     return config;
0074 }
0075 
0076 void Output::readInGlobalPartFromInfo(KScreen::OutputPtr output, const QVariantMap &info)
0077 {
0078     GlobalConfig config = fromInfo(output, info);
0079     output->setRotation(config.rotation.value_or(KScreen::Output::Rotation::None));
0080     output->setScale(config.scale.value_or(1.0));
0081     output->setVrrPolicy(config.vrrPolicy.value_or(KScreen::Output::VrrPolicy::Automatic));
0082     output->setOverscan(config.overscan.value_or(0));
0083     output->setRgbRange(config.rgbRange.value_or(KScreen::Output::RgbRange::Automatic));
0084 
0085     KScreen::ModePtr matchingMode;
0086     if (config.modeId) {
0087         matchingMode = output->mode(config.modeId.value());
0088     }
0089     if (!matchingMode) {
0090         qCWarning(KSCREEN_KDED) << "\tFailed to find a matching mode - this means that our config is corrupted"
0091                                    " or a different device with the same serial number has been connected (very unlikely)."
0092                                    " Falling back to preferred modes.";
0093         matchingMode = output->preferredMode();
0094     }
0095     if (!matchingMode) {
0096         qCWarning(KSCREEN_KDED) << "\tFailed to get a preferred mode, falling back to biggest mode.";
0097         matchingMode = Generator::biggestMode(output->modes());
0098     }
0099     if (!matchingMode) {
0100         qCWarning(KSCREEN_KDED) << "\tFailed to get biggest mode. Which means there are no modes. Turning off the screen.";
0101         output->setEnabled(false);
0102         return;
0103     }
0104 
0105     output->setCurrentModeId(matchingMode->id());
0106 }
0107 
0108 QVariantMap Output::getGlobalData(KScreen::OutputPtr output)
0109 {
0110     const auto tryFile = [output](const auto &name) {
0111         QString fileName = Globals::findFile(name);
0112         if (fileName.isEmpty()) {
0113             qCDebug(KSCREEN_KDED) << "No file for" << name;
0114             return QVariantMap();
0115         }
0116         QFile file(fileName);
0117         if (!file.open(QIODevice::ReadOnly)) {
0118             qCDebug(KSCREEN_KDED) << "Failed to open file" << file.fileName();
0119             return QVariantMap();
0120         }
0121         qCDebug(KSCREEN_KDED) << "Found global data at" << file.fileName();
0122         QJsonDocument parser;
0123         return parser.fromJson(file.readAll()).toVariant().toMap();
0124     };
0125     auto specific = tryFile(s_dirName % output->hashMd5() % output->name());
0126     if (!specific.isEmpty()) {
0127         return specific;
0128     }
0129     return tryFile(s_dirName % output->hashMd5());
0130 }
0131 
0132 bool Output::readInGlobal(KScreen::OutputPtr output)
0133 {
0134     const QVariantMap info = getGlobalData(output);
0135     if (info.empty()) {
0136         // if info is empty, the global file does not exists, or is in an unreadable state
0137         return false;
0138     }
0139     readInGlobalPartFromInfo(output, info);
0140     return true;
0141 }
0142 
0143 Output::GlobalConfig Output::readGlobal(const KScreen::OutputPtr &output)
0144 {
0145     return fromInfo(output, getGlobalData(output));
0146 }
0147 
0148 KScreen::Output::Rotation orientationToRotation(QOrientationReading::Orientation orientation, KScreen::Output::Rotation fallback)
0149 {
0150     using Orientation = QOrientationReading::Orientation;
0151 
0152     switch (orientation) {
0153     case Orientation::TopUp:
0154         return KScreen::Output::Rotation::None;
0155     case Orientation::TopDown:
0156         return KScreen::Output::Rotation::Inverted;
0157     case Orientation::LeftUp:
0158         return KScreen::Output::Rotation::Left;
0159     case Orientation::RightUp:
0160         return KScreen::Output::Rotation::Right;
0161     case Orientation::Undefined:
0162     case Orientation::FaceUp:
0163     case Orientation::FaceDown:
0164         return fallback;
0165     default:
0166         Q_UNREACHABLE();
0167     }
0168 }
0169 
0170 bool Output::updateOrientation(KScreen::OutputPtr &output, QOrientationReading::Orientation orientation)
0171 {
0172     if (output->type() != KScreen::Output::Type::Panel) {
0173         return false;
0174     }
0175     const auto currentRotation = output->rotation();
0176     const auto rotation = orientationToRotation(orientation, currentRotation);
0177     if (rotation == currentRotation) {
0178         return true;
0179     }
0180     output->setRotation(rotation);
0181     return true;
0182 }
0183 
0184 // TODO: move this into the Layouter class.
0185 void Output::adjustPositions(KScreen::ConfigPtr config, const QVariantList &outputsInfo)
0186 {
0187     typedef QPair<int, QPoint> Out;
0188 
0189     KScreen::OutputList outputs = config->outputs();
0190     QVector<Out> sortedOutputs; // <id, pos>
0191     for (const KScreen::OutputPtr &output : outputs) {
0192         sortedOutputs.append(Out(output->id(), output->pos()));
0193     }
0194 
0195     // go from left to right, top to bottom
0196     std::sort(sortedOutputs.begin(), sortedOutputs.end(), [](const Out &o1, const Out &o2) {
0197         const int x1 = o1.second.x();
0198         const int x2 = o2.second.x();
0199         return x1 < x2 || (x1 == x2 && o1.second.y() < o2.second.y());
0200     });
0201 
0202     for (int cnt = 1; cnt < sortedOutputs.length(); cnt++) {
0203         auto getOutputInfoProperties = [outputsInfo](KScreen::OutputPtr output, QRect &geo) -> bool {
0204             if (!output) {
0205                 return false;
0206             }
0207             const auto hash = output->hash();
0208 
0209             auto it = std::find_if(outputsInfo.begin(), outputsInfo.end(), [hash](QVariant v) {
0210                 const QVariantMap info = v.toMap();
0211                 return info[QStringLiteral("id")].toString() == hash;
0212             });
0213             if (it == outputsInfo.end()) {
0214                 return false;
0215             }
0216 
0217             auto isPortrait = [](const QVariant &info) {
0218                 bool ok;
0219                 const int rot = info.toInt(&ok);
0220                 if (!ok) {
0221                     return false;
0222                 }
0223                 return rot & KScreen::Output::Rotation::Left || rot & KScreen::Output::Rotation::Right;
0224             };
0225 
0226             const QVariantMap outputInfo = it->toMap();
0227 
0228             const QVariantMap posInfo = outputInfo[QStringLiteral("pos")].toMap();
0229             const QVariant scaleInfo = outputInfo[QStringLiteral("scale")];
0230             const QVariantMap modeInfo = outputInfo[QStringLiteral("mode")].toMap();
0231             const QVariantMap modeSize = modeInfo[QStringLiteral("size")].toMap();
0232             const bool portrait = isPortrait(outputInfo[QStringLiteral("rotation")]);
0233 
0234             if (posInfo.isEmpty() || modeSize.isEmpty() || !scaleInfo.canConvert<int>()) {
0235                 return false;
0236             }
0237 
0238             const qreal scale = scaleInfo.toDouble();
0239             if (scale <= 0) {
0240                 return false;
0241             }
0242             const QPoint pos = QPoint(posInfo[QStringLiteral("x")].toInt(), posInfo[QStringLiteral("y")].toInt());
0243             QSize size = QSize(modeSize[QStringLiteral("width")].toInt() / scale, modeSize[QStringLiteral("height")].toInt() / scale);
0244             if (portrait) {
0245                 size.transpose();
0246             }
0247             geo = QRect(pos, size);
0248 
0249             return true;
0250         };
0251 
0252         // it's guaranteed that we find the following values in the QMap
0253         KScreen::OutputPtr prevPtr = outputs.find(sortedOutputs[cnt - 1].first).value();
0254         KScreen::OutputPtr curPtr = outputs.find(sortedOutputs[cnt].first).value();
0255 
0256         QRect prevInfoGeo, curInfoGeo;
0257         if (!getOutputInfoProperties(prevPtr, prevInfoGeo) || !getOutputInfoProperties(curPtr, curInfoGeo)) {
0258             // no info found, nothing can be adjusted for the next output
0259             continue;
0260         }
0261 
0262         const QRect prevGeo = prevPtr->geometry();
0263         const QRect curGeo = curPtr->geometry();
0264 
0265         // the old difference between previous and current output read from the config file
0266         const int xInfoDiff = curInfoGeo.x() - (prevInfoGeo.x() + prevInfoGeo.width());
0267 
0268         // the proposed new difference
0269         const int prevRight = prevGeo.x() + prevGeo.width();
0270         const int xCorrected = prevRight + prevGeo.width() * xInfoDiff / (double)prevInfoGeo.width();
0271         const int xDiff = curGeo.x() - prevRight;
0272 
0273         // In the following calculate the y-correction. This is more involved since we
0274         // differentiate between overlapping and non-overlapping pairs and align either
0275         // top to top/bottom or bottom to top/bottom
0276         const bool yOverlap = prevInfoGeo.y() + prevInfoGeo.height() > curInfoGeo.y() && prevInfoGeo.y() < curInfoGeo.y() + curInfoGeo.height();
0277 
0278         // these values determine which horizontal edge of previous output we align with
0279         const int topToTopDiffAbs = qAbs(prevInfoGeo.y() - curInfoGeo.y());
0280         const int topToBottomDiffAbs = qAbs(prevInfoGeo.y() - curInfoGeo.y() - curInfoGeo.height());
0281         const int bottomToBottomDiffAbs = qAbs(prevInfoGeo.y() + prevInfoGeo.height() - curInfoGeo.y() - curInfoGeo.height());
0282         const int bottomToTopDiffAbs = qAbs(prevInfoGeo.y() + prevInfoGeo.height() - curInfoGeo.y());
0283 
0284         const bool yTopAligned = (topToTopDiffAbs < bottomToBottomDiffAbs && topToTopDiffAbs <= bottomToTopDiffAbs) //
0285             || topToBottomDiffAbs < bottomToBottomDiffAbs;
0286 
0287         int yInfoDiff = curInfoGeo.y() - prevInfoGeo.y();
0288         int yDiff = curGeo.y() - prevGeo.y();
0289         int yCorrected;
0290 
0291         if (yTopAligned) {
0292             // align to previous top
0293             if (!yOverlap) {
0294                 // align previous top with current bottom
0295                 yInfoDiff += curInfoGeo.height();
0296                 yDiff += curGeo.height();
0297             }
0298             // When we align with previous top we are interested in the changes to the
0299             // current geometry and not in the ones of the previous one.
0300             const double yInfoRel = yInfoDiff / (double)curInfoGeo.height();
0301             yCorrected = prevGeo.y() + yInfoRel * curGeo.height();
0302         } else {
0303             // align previous bottom...
0304             yInfoDiff -= prevInfoGeo.height();
0305             yDiff -= prevGeo.height();
0306             yCorrected = prevGeo.y() + prevGeo.height();
0307 
0308             if (yOverlap) {
0309                 // ... with current bottom
0310                 yInfoDiff += curInfoGeo.height();
0311                 yDiff += curGeo.height();
0312                 yCorrected -= curGeo.height();
0313             } // ... else with current top
0314 
0315             // When we align with previous bottom we are interested in changes to the
0316             // previous geometry.
0317             const double yInfoRel = yInfoDiff / (double)prevInfoGeo.height();
0318             yCorrected += yInfoRel * prevGeo.height();
0319         }
0320 
0321         const int x = xDiff == xInfoDiff ? curGeo.x() : xCorrected;
0322         const int y = yDiff == yInfoDiff ? curGeo.y() : yCorrected;
0323         curPtr->setPos(QPoint(x, y));
0324     }
0325 }
0326 
0327 void Output::readIn(KScreen::OutputPtr output, const QVariantMap &info, Control::OutputRetention retention)
0328 {
0329     const QVariantMap posInfo = info[QStringLiteral("pos")].toMap();
0330     QPoint point(posInfo[QStringLiteral("x")].toInt(), posInfo[QStringLiteral("y")].toInt());
0331     output->setPos(point);
0332     output->setEnabled(info[QStringLiteral("enabled")].toBool());
0333 
0334     if (retention != Control::OutputRetention::Individual && readInGlobal(output)) {
0335         // output data read from global output file
0336         return;
0337     }
0338     // output data read directly from info
0339     readInGlobalPartFromInfo(output, info);
0340 }
0341 
0342 void Output::readInOutputs(KScreen::ConfigPtr config, const QVariantList &outputsInfo)
0343 {
0344     const KScreen::OutputList outputs = config->outputs();
0345     ControlConfig control(config);
0346     // As global outputs are indexed by a hash of their edid, which is not unique,
0347     // to be able to tell apart multiple identical outputs, these need special treatment
0348     QStringList duplicateIds;
0349     {
0350         QStringList allConnectedIds;
0351         allConnectedIds.reserve(outputs.count());
0352         for (const KScreen::OutputPtr &output : outputs) {
0353             if (!output->isConnected()) {
0354                 // Duplicated IDs only matter if the duplicates are actually connected. Duplicates may also be transient.
0355                 continue;
0356             }
0357             const auto outputId = output->hash();
0358             if (allConnectedIds.contains(outputId) && !duplicateIds.contains(outputId)) {
0359                 duplicateIds << outputId;
0360             }
0361             allConnectedIds << outputId;
0362         }
0363     }
0364 
0365     QMap<KScreen::OutputPtr, uint32_t> priorities;
0366 
0367     for (const KScreen::OutputPtr &output : outputs) {
0368         if (!output->isConnected()) {
0369             output->setEnabled(false);
0370             continue;
0371         }
0372         const auto outputId = output->hash();
0373         bool infoFound = false;
0374         for (const auto &variantInfo : outputsInfo) {
0375             const QVariantMap info = variantInfo.toMap();
0376             if (outputId != info[QStringLiteral("id")].toString()) {
0377                 continue;
0378             }
0379             if (!output->name().isEmpty() && duplicateIds.contains(outputId)) {
0380                 // We may have identical outputs connected, these will have the same id in the config
0381                 // in order to find the right one, also check the output's name (usually the connector)
0382                 const auto metadata = info[QStringLiteral("metadata")].toMap();
0383                 const auto outputName = metadata[QStringLiteral("name")].toString();
0384                 if (output->name() != outputName) {
0385                     // was a duplicate id, but info not for this output
0386                     continue;
0387                 }
0388             }
0389             infoFound = true;
0390             readIn(output, info, control.getOutputRetention(output));
0391 
0392             // the deprecated "primary" property may exist for compatibility, but "priority" should override it whenever present.
0393             uint32_t priority = 0;
0394             if (info.contains(QStringLiteral("priority"))) {
0395                 priority = info[QStringLiteral("priority")].toUInt();
0396             } else if (info.contains(QStringLiteral("primary"))) {
0397                 priority = info[QStringLiteral("primary")].toBool() ? 1 : 2;
0398             }
0399             priorities[output] = priority;
0400             break;
0401         }
0402         if (!infoFound) {
0403             // no info in info for this output, try reading in global output info at least or set some default values
0404 
0405             qCWarning(KSCREEN_KDED) << "\tFailed to find a matching output in the current info data - this means that our info is corrupted"
0406                                        " or a different device with the same serial number has been connected (very unlikely).";
0407             if (!readInGlobal(output)) {
0408                 // set some default values instead
0409                 output->setEnabled(true);
0410                 readInGlobalPartFromInfo(output, QVariantMap());
0411             }
0412         }
0413     }
0414 
0415     config->setOutputPriorities(priorities);
0416 
0417     for (KScreen::OutputPtr output : outputs) {
0418         auto replicationSource = control.getReplicationSource(output);
0419         if (replicationSource) {
0420             output->setPos(replicationSource->pos());
0421             output->setExplicitLogicalSize(config->logicalSizeForOutput(*replicationSource));
0422         } else {
0423             output->setExplicitLogicalSize(QSizeF());
0424         }
0425     }
0426 
0427     // TODO: this does not work at the moment with logical size replication. Deactivate for now.
0428     // correct positional config regressions on global output data changes
0429 #if 0
0430     adjustPositions(config, outputsInfo);
0431 #endif
0432 }
0433 
0434 static QVariantMap metadata(const KScreen::OutputPtr &output)
0435 {
0436     QVariantMap metadata;
0437     metadata[QStringLiteral("name")] = output->name();
0438     if (!output->edid() || !output->edid()->isValid()) {
0439         return metadata;
0440     }
0441 
0442     metadata[QStringLiteral("fullname")] = output->edid()->deviceId();
0443     return metadata;
0444 }
0445 
0446 bool Output::writeGlobalPart(const KScreen::OutputPtr &output, QVariantMap &info, const KScreen::OutputPtr &fallback)
0447 {
0448     info[QStringLiteral("id")] = output->hash();
0449     info[QStringLiteral("metadata")] = metadata(output);
0450     info[QStringLiteral("rotation")] = output->rotation();
0451 
0452     // Round scale to four digits
0453     info[QStringLiteral("scale")] = int(output->scale() * 10000 + 0.5) / 10000.;
0454 
0455     QVariantMap modeInfo;
0456     float refreshRate = -1.;
0457     QSize modeSize;
0458     if (output->currentMode() && output->isEnabled()) {
0459         refreshRate = output->currentMode()->refreshRate();
0460         modeSize = output->currentMode()->size();
0461     } else if (fallback && fallback->currentMode()) {
0462         refreshRate = fallback->currentMode()->refreshRate();
0463         modeSize = fallback->currentMode()->size();
0464     }
0465 
0466     if (refreshRate < 0 || !modeSize.isValid()) {
0467         return false;
0468     }
0469 
0470     modeInfo[QStringLiteral("refresh")] = refreshRate;
0471 
0472     QVariantMap modeSizeMap;
0473     modeSizeMap[QStringLiteral("width")] = modeSize.width();
0474     modeSizeMap[QStringLiteral("height")] = modeSize.height();
0475     modeInfo[QStringLiteral("size")] = modeSizeMap;
0476 
0477     info[QStringLiteral("mode")] = modeInfo;
0478     info[QStringLiteral("vrrpolicy")] = static_cast<uint32_t>(output->vrrPolicy());
0479     info[QStringLiteral("overscan")] = output->overscan();
0480     info[QStringLiteral("rgbrange")] = static_cast<uint32_t>(output->rgbRange());
0481 
0482     return true;
0483 }
0484 
0485 void Output::writeGlobal(const KScreen::OutputPtr &output, bool hasDuplicate)
0486 {
0487     // get old values and subsequently override
0488     QVariantMap info = getGlobalData(output);
0489     if (!writeGlobalPart(output, info, nullptr)) {
0490         return;
0491     }
0492 
0493     if (!QDir().mkpath(dirPath())) {
0494         return;
0495     }
0496     QString fileName = dirPath() % output->hashMd5() % output->name();
0497     if (!hasDuplicate && !QFile(fileName).exists()) {
0498         // connector-specific file doesn't exist yet, use the non-specific one instead
0499         fileName = dirPath() % output->hashMd5();
0500     }
0501     QFile file(fileName);
0502     if (!file.open(QIODevice::WriteOnly)) {
0503         qCWarning(KSCREEN_KDED) << "Failed to open global output file for writing! " << file.errorString();
0504         return;
0505     }
0506 
0507     file.write(QJsonDocument::fromVariant(info).toJson());
0508     return;
0509 }