File indexing completed on 2024-11-10 04:55:35
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 QList<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) 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 (readInGlobal(output)) { 0335 // output data read from global output file 0336 return; 0337 } 0338 // if global data isn't available, use per-output-setup data as a fallback 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); 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 }