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 }