File indexing completed on 2024-12-08 13:21:55
0001 /* 0002 KWin - the KDE window manager 0003 This file is part of the KDE project. 0004 0005 SPDX-FileCopyrightText: 2021 Aleix Pol Gonzalez <aleixpol@kde.org> 0006 SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com> 0007 0008 SPDX-License-Identifier: GPL-2.0-or-later 0009 */ 0010 #include "kscreenintegration.h" 0011 #include "utils/common.h" 0012 0013 #include <QCryptographicHash> 0014 #include <QFile> 0015 #include <QJsonArray> 0016 #include <QJsonDocument> 0017 #include <QJsonObject> 0018 0019 #include <algorithm> 0020 #include <cmath> 0021 0022 namespace KWin 0023 { 0024 namespace KScreenIntegration 0025 { 0026 /// See KScreen::Output::hashMd5 0027 static QString outputHash(Output *output) 0028 { 0029 if (!output->edid().isEmpty()) { 0030 QCryptographicHash hash(QCryptographicHash::Md5); 0031 hash.addData(output->edid()); 0032 return QString::fromLatin1(hash.result().toHex()); 0033 } else { 0034 return output->name(); 0035 } 0036 } 0037 0038 /// See KScreen::Config::connectedOutputsHash in libkscreen 0039 QString connectedOutputsHash(const QVector<Output *> &outputs) 0040 { 0041 QStringList hashedOutputs; 0042 hashedOutputs.reserve(outputs.count()); 0043 for (auto output : std::as_const(outputs)) { 0044 if (!output->isPlaceholder() && !output->isNonDesktop()) { 0045 hashedOutputs << outputHash(output); 0046 } 0047 } 0048 std::sort(hashedOutputs.begin(), hashedOutputs.end()); 0049 const auto hash = QCryptographicHash::hash(hashedOutputs.join(QString()).toLatin1(), QCryptographicHash::Md5); 0050 return QString::fromLatin1(hash.toHex()); 0051 } 0052 0053 static QMap<Output *, QJsonObject> outputsConfig(const QVector<Output *> &outputs, const QString &hash) 0054 { 0055 const QString kscreenJsonPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kscreen/") % hash); 0056 if (kscreenJsonPath.isEmpty()) { 0057 return {}; 0058 } 0059 0060 QFile f(kscreenJsonPath); 0061 if (!f.open(QIODevice::ReadOnly)) { 0062 qCWarning(KWIN_CORE) << "Could not open file" << kscreenJsonPath; 0063 return {}; 0064 } 0065 0066 QJsonParseError error; 0067 const auto doc = QJsonDocument::fromJson(f.readAll(), &error); 0068 if (error.error != QJsonParseError::NoError) { 0069 qCWarning(KWIN_CORE) << "Failed to parse" << kscreenJsonPath << error.errorString(); 0070 return {}; 0071 } 0072 0073 QHash<Output *, bool> duplicate; 0074 QHash<Output *, QString> outputHashes; 0075 for (Output *output : outputs) { 0076 const QString hash = outputHash(output); 0077 const auto it = std::find_if(outputHashes.cbegin(), outputHashes.cend(), [hash](const auto &value) { 0078 return value == hash; 0079 }); 0080 if (it == outputHashes.cend()) { 0081 duplicate[output] = false; 0082 } else { 0083 duplicate[output] = true; 0084 duplicate[it.key()] = true; 0085 } 0086 outputHashes[output] = hash; 0087 } 0088 0089 QMap<Output *, QJsonObject> ret; 0090 const auto outputsJson = doc.array(); 0091 for (const auto &outputJson : outputsJson) { 0092 const auto outputObject = outputJson.toObject(); 0093 const auto id = outputObject["id"]; 0094 const auto output = std::find_if(outputs.begin(), outputs.end(), [&duplicate, &id, &outputObject](Output *output) { 0095 if (outputHash(output) != id.toString()) { 0096 return false; 0097 } 0098 if (duplicate[output]) { 0099 // can't distinguish between outputs by hash alone, need to look at connector names 0100 const auto metadata = outputObject[QStringLiteral("metadata")]; 0101 const auto outputName = metadata[QStringLiteral("name")].toString(); 0102 return outputName == output->name(); 0103 } else { 0104 return true; 0105 } 0106 }); 0107 if (output != outputs.end()) { 0108 ret[*output] = outputObject; 0109 } 0110 } 0111 return ret; 0112 } 0113 0114 static std::optional<QJsonObject> globalOutputConfig(Output *output) 0115 { 0116 const QString kscreenPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kscreen/")); 0117 if (kscreenPath.isEmpty()) { 0118 return std::nullopt; 0119 } 0120 const auto hash = outputHash(output); 0121 // use connector specific data if available, unspecific data if not 0122 QFile f(kscreenPath % hash % output->name()); 0123 if (!f.open(QIODevice::ReadOnly)) { 0124 f.setFileName(kscreenPath % hash); 0125 if (!f.open(QIODevice::ReadOnly)) { 0126 qCWarning(KWIN_CORE) << "Could not open file" << f.fileName(); 0127 return std::nullopt; 0128 } 0129 } 0130 0131 QJsonParseError error; 0132 const auto doc = QJsonDocument::fromJson(f.readAll(), &error); 0133 if (error.error != QJsonParseError::NoError) { 0134 qCWarning(KWIN_CORE) << "Failed to parse" << f.fileName() << error.errorString(); 0135 return std::nullopt; 0136 } 0137 return doc.object(); 0138 } 0139 0140 /// See KScreen::Output::Rotation 0141 enum Rotation { 0142 None = 1, 0143 Left = 2, 0144 Inverted = 4, 0145 Right = 8, 0146 }; 0147 0148 Output::Transform toDrmTransform(int rotation) 0149 { 0150 switch (Rotation(rotation)) { 0151 case None: 0152 return Output::Transform::Normal; 0153 case Left: 0154 return Output::Transform::Rotated90; 0155 case Inverted: 0156 return Output::Transform::Rotated180; 0157 case Right: 0158 return Output::Transform::Rotated270; 0159 default: 0160 Q_UNREACHABLE(); 0161 } 0162 } 0163 0164 std::shared_ptr<OutputMode> parseMode(Output *output, const QJsonObject &modeInfo) 0165 { 0166 const QJsonObject size = modeInfo["size"].toObject(); 0167 const QSize modeSize = QSize(size["width"].toInt(), size["height"].toInt()); 0168 const uint32_t refreshRate = std::round(modeInfo["refresh"].toDouble() * 1000); 0169 0170 const auto modes = output->modes(); 0171 auto it = std::find_if(modes.begin(), modes.end(), [&modeSize, &refreshRate](const auto &mode) { 0172 return mode->size() == modeSize && mode->refreshRate() == refreshRate; 0173 }); 0174 return (it != modes.end()) ? *it : nullptr; 0175 } 0176 0177 std::optional<std::pair<OutputConfiguration, QVector<Output *>>> readOutputConfig(const QVector<Output *> &outputs, const QString &hash) 0178 { 0179 const auto outputsInfo = outputsConfig(outputs, hash); 0180 std::vector<std::pair<uint32_t, Output *>> outputOrder; 0181 OutputConfiguration cfg; 0182 // default position goes from left to right 0183 QPoint pos(0, 0); 0184 for (const auto &output : std::as_const(outputs)) { 0185 if (output->isPlaceholder() || output->isNonDesktop()) { 0186 continue; 0187 } 0188 auto props = cfg.changeSet(output); 0189 const QJsonObject outputInfo = outputsInfo[output]; 0190 const auto globalOutputInfo = globalOutputConfig(output); 0191 qCDebug(KWIN_CORE) << "Reading output configuration for " << output; 0192 if (!outputInfo.isEmpty() || globalOutputInfo.has_value()) { 0193 // settings that are per output setup: 0194 props->enabled = outputInfo["enabled"].toBool(true); 0195 if (outputInfo["primary"].toBool()) { 0196 outputOrder.push_back(std::make_pair(1, output)); 0197 if (!props->enabled) { 0198 qCWarning(KWIN_CORE) << "KScreen config would disable the primary output!"; 0199 return std::nullopt; 0200 } 0201 } else if (int prio = outputInfo["priority"].toInt(); prio > 0) { 0202 outputOrder.push_back(std::make_pair(prio, output)); 0203 if (!props->enabled) { 0204 qCWarning(KWIN_CORE) << "KScreen config would disable an output with priority!"; 0205 return std::nullopt; 0206 } 0207 } else { 0208 outputOrder.push_back(std::make_pair(0, output)); 0209 } 0210 const QJsonObject pos = outputInfo["pos"].toObject(); 0211 props->pos = QPoint(pos["x"].toInt(), pos["y"].toInt()); 0212 0213 // settings that are independent of per output setups: 0214 const auto &globalInfo = globalOutputInfo ? globalOutputInfo.value() : outputInfo; 0215 if (const QJsonValue scale = globalInfo["scale"]; !scale.isUndefined()) { 0216 props->scale = scale.toDouble(1.); 0217 } 0218 props->transform = KScreenIntegration::toDrmTransform(globalInfo["rotation"].toInt()); 0219 props->overscan = static_cast<uint32_t>(globalInfo["overscan"].toInt(props->overscan)); 0220 props->vrrPolicy = static_cast<RenderLoop::VrrPolicy>(globalInfo["vrrpolicy"].toInt(static_cast<uint32_t>(props->vrrPolicy))); 0221 props->rgbRange = static_cast<Output::RgbRange>(globalInfo["rgbrange"].toInt(static_cast<uint32_t>(props->rgbRange))); 0222 0223 if (const QJsonObject modeInfo = globalInfo["mode"].toObject(); !modeInfo.isEmpty()) { 0224 if (auto mode = KScreenIntegration::parseMode(output, modeInfo)) { 0225 props->mode = mode; 0226 } 0227 } 0228 } else { 0229 props->enabled = true; 0230 props->pos = pos; 0231 props->transform = output->panelOrientation(); 0232 outputOrder.push_back(std::make_pair(0, output)); 0233 } 0234 pos.setX(pos.x() + output->geometry().width()); 0235 } 0236 0237 bool allDisabled = std::all_of(outputs.begin(), outputs.end(), [&cfg](const auto &output) { 0238 return !cfg.changeSet(output)->enabled; 0239 }); 0240 if (allDisabled) { 0241 qCWarning(KWIN_CORE) << "KScreen config would disable all outputs!"; 0242 return std::nullopt; 0243 } 0244 std::erase_if(outputOrder, [&cfg](const auto &pair) { 0245 return !cfg.constChangeSet(pair.second)->enabled; 0246 }); 0247 std::sort(outputOrder.begin(), outputOrder.end(), [](const auto &left, const auto &right) { 0248 if (left.first == right.first) { 0249 // sort alphabetically as a fallback 0250 return left.second->name() < right.second->name(); 0251 } else if (left.first == 0) { 0252 return false; 0253 } else { 0254 return left.first < right.first; 0255 } 0256 }); 0257 0258 QVector<Output *> order; 0259 order.reserve(outputOrder.size()); 0260 std::transform(outputOrder.begin(), outputOrder.end(), std::back_inserter(order), [](const auto &pair) { 0261 return pair.second; 0262 }); 0263 return std::make_pair(cfg, order); 0264 } 0265 } 0266 }