/* KWin - the KDE window manager This file is part of the KDE project. SPDX-FileCopyrightText: 2023 Xaver Hugl SPDX-License-Identifier: GPL-2.0-or-later */ #include "outputconfigurationstore.h" #include "core/iccprofile.h" #include "core/inputdevice.h" #include "core/output.h" #include "core/outputbackend.h" #include "core/outputconfiguration.h" #include "input.h" #include "input_event.h" #include "kscreenintegration.h" #include "workspace.h" #include #include #include #include #include namespace KWin { OutputConfigurationStore::OutputConfigurationStore() { load(); } OutputConfigurationStore::~OutputConfigurationStore() { save(); } std::optional, OutputConfigurationStore::ConfigType>> OutputConfigurationStore::queryConfig(const QList &outputs, bool isLidClosed, QOrientationReading *orientation, bool isTabletMode) { QList relevantOutputs; std::copy_if(outputs.begin(), outputs.end(), std::back_inserter(relevantOutputs), [](Output *output) { return !output->isNonDesktop() && !output->isPlaceholder(); }); if (relevantOutputs.isEmpty()) { return std::nullopt; } if (const auto opt = findSetup(relevantOutputs, isLidClosed)) { const auto &[setup, outputStates] = *opt; auto [config, order] = setupToConfig(setup, outputStates); applyOrientationReading(config, relevantOutputs, orientation, isTabletMode); storeConfig(relevantOutputs, isLidClosed, config, order); return std::make_tuple(config, order, ConfigType::Preexisting); } auto [config, order] = generateConfig(relevantOutputs, isLidClosed); applyOrientationReading(config, relevantOutputs, orientation, isTabletMode); storeConfig(relevantOutputs, isLidClosed, config, order); return std::make_tuple(config, order, ConfigType::Generated); } void OutputConfigurationStore::applyOrientationReading(OutputConfiguration &config, const QList &outputs, QOrientationReading *orientation, bool isTabletMode) { const auto output = std::find_if(outputs.begin(), outputs.end(), [&config](Output *output) { return output->isInternal() && config.changeSet(output)->enabled.value_or(output->isEnabled()); }); if (output == outputs.end()) { return; } // TODO move other outputs to matching positions const auto changeset = config.changeSet(*output); if (!isAutoRotateActive(outputs, isTabletMode)) { changeset->transform = changeset->manualTransform; return; } const auto panelOrientation = (*output)->panelOrientation(); switch (orientation->orientation()) { case QOrientationReading::Orientation::TopUp: changeset->transform = panelOrientation; return; case QOrientationReading::Orientation::TopDown: changeset->transform = panelOrientation.combine(OutputTransform::Kind::Rotate180); return; case QOrientationReading::Orientation::LeftUp: changeset->transform = panelOrientation.combine(OutputTransform::Kind::Rotate90); return; case QOrientationReading::Orientation::RightUp: changeset->transform = panelOrientation.combine(OutputTransform::Kind::Rotate270); return; case QOrientationReading::Orientation::FaceUp: case QOrientationReading::Orientation::FaceDown: return; case QOrientationReading::Orientation::Undefined: changeset->transform = changeset->manualTransform; return; } } std::optional>> OutputConfigurationStore::findSetup(const QList &outputs, bool lidClosed) { std::unordered_map outputStates; for (Output *output : outputs) { if (auto opt = findOutput(output, outputs)) { outputStates[output] = *opt; } else { return std::nullopt; } } const auto setup = std::find_if(m_setups.begin(), m_setups.end(), [lidClosed, &outputStates](const auto &setup) { if (setup.lidClosed != lidClosed || size_t(setup.outputs.size()) != outputStates.size()) { return false; } return std::all_of(outputStates.begin(), outputStates.end(), [&setup](const auto &outputIt) { return std::any_of(setup.outputs.begin(), setup.outputs.end(), [&outputIt](const auto &outputInfo) { return outputInfo.outputIndex == outputIt.second; }); }); }); if (setup == m_setups.end()) { return std::nullopt; } else { return std::make_pair(&(*setup), outputStates); } } std::optional OutputConfigurationStore::findOutput(Output *output, const QList &allOutputs) const { const bool uniqueEdid = !output->edid().identifier().isEmpty() && std::none_of(allOutputs.begin(), allOutputs.end(), [output](Output *otherOutput) { return otherOutput != output && otherOutput->edid().identifier() == output->edid().identifier(); }); const bool uniqueEdidHash = !output->edid().hash().isEmpty() && std::none_of(allOutputs.begin(), allOutputs.end(), [output](Output *otherOutput) { return otherOutput != output && otherOutput->edid().hash() == output->edid().hash(); }); const bool uniqueMst = !output->mstPath().isEmpty() && std::none_of(allOutputs.begin(), allOutputs.end(), [output](Output *otherOutput) { return otherOutput != output && otherOutput->edid().identifier() == output->edid().identifier() && otherOutput->mstPath() == output->mstPath(); }); auto it = std::find_if(m_outputs.begin(), m_outputs.end(), [&](const auto &outputState) { if (output->edid().isValid()) { if (outputState.edidIdentifier != output->edid().identifier()) { return false; } else if (uniqueEdid) { return true; } } if (!output->edid().hash().isEmpty()) { if (outputState.edidHash != output->edid().hash()) { return false; } else if (uniqueEdidHash) { return true; } } if (outputState.mstPath != output->mstPath()) { return false; } else if (uniqueMst) { return true; } return outputState.connectorName == output->name(); }); if (it == m_outputs.end() && uniqueEdidHash) { // handle the edge case of EDID parsing failing in the past but not failing anymore it = std::find_if(m_outputs.begin(), m_outputs.end(), [&](const auto &outputState) { return outputState.edidHash == output->edid().hash(); }); } if (it != m_outputs.end()) { return std::distance(m_outputs.begin(), it); } else { return std::nullopt; } } void OutputConfigurationStore::storeConfig(const QList &allOutputs, bool isLidClosed, const OutputConfiguration &config, const QList &outputOrder) { QList relevantOutputs; std::copy_if(allOutputs.begin(), allOutputs.end(), std::back_inserter(relevantOutputs), [](Output *output) { return !output->isNonDesktop() && !output->isPlaceholder(); }); if (relevantOutputs.isEmpty()) { return; } const auto opt = findSetup(relevantOutputs, isLidClosed); Setup *setup = nullptr; if (opt) { setup = opt->first; } else { m_setups.push_back(Setup{}); setup = &m_setups.back(); setup->lidClosed = isLidClosed; } for (Output *output : relevantOutputs) { auto outputIndex = findOutput(output, outputOrder); if (!outputIndex) { m_outputs.push_back(OutputState{}); outputIndex = m_outputs.size() - 1; } auto outputIt = std::find_if(setup->outputs.begin(), setup->outputs.end(), [outputIndex](const auto &output) { return output.outputIndex == outputIndex; }); if (outputIt == setup->outputs.end()) { setup->outputs.push_back(SetupState{}); outputIt = setup->outputs.end() - 1; } if (const auto changeSet = config.constChangeSet(output)) { QSize modeSize = changeSet->desiredModeSize.value_or(output->desiredModeSize()); if (modeSize.isEmpty()) { modeSize = output->currentMode()->size(); } uint32_t refreshRate = changeSet->desiredModeRefreshRate.value_or(output->desiredModeRefreshRate()); if (refreshRate == 0) { refreshRate = output->currentMode()->refreshRate(); } m_outputs[*outputIndex] = OutputState{ .edidIdentifier = output->edid().identifier(), .connectorName = output->name(), .edidHash = output->edid().isValid() ? output->edid().hash() : QString{}, .mstPath = output->mstPath(), .mode = ModeData{ .size = modeSize, .refreshRate = refreshRate, }, .scale = changeSet->scale.value_or(output->scale()), .transform = changeSet->transform.value_or(output->transform()), .manualTransform = changeSet->manualTransform.value_or(output->manualTransform()), .overscan = changeSet->overscan.value_or(output->overscan()), .rgbRange = changeSet->rgbRange.value_or(output->rgbRange()), .vrrPolicy = changeSet->vrrPolicy.value_or(output->vrrPolicy()), .highDynamicRange = changeSet->highDynamicRange.value_or(output->highDynamicRange()), .referenceLuminance = changeSet->referenceLuminance.value_or(output->referenceLuminance()), .wideColorGamut = changeSet->wideColorGamut.value_or(output->wideColorGamut()), .autoRotation = changeSet->autoRotationPolicy.value_or(output->autoRotationPolicy()), .iccProfilePath = changeSet->iccProfilePath.value_or(output->iccProfilePath()), .colorProfileSource = changeSet->colorProfileSource.value_or(output->colorProfileSource()), .maxPeakBrightnessOverride = changeSet->maxPeakBrightnessOverride.value_or(output->maxPeakBrightnessOverride()), .maxAverageBrightnessOverride = changeSet->maxAverageBrightnessOverride.value_or(output->maxAverageBrightnessOverride()), .minBrightnessOverride = changeSet->minBrightnessOverride.value_or(output->minBrightnessOverride()), .sdrGamutWideness = changeSet->sdrGamutWideness.value_or(output->sdrGamutWideness()), .brightness = changeSet->brightness.value_or(output->brightnessSetting()), .allowSdrSoftwareBrightness = changeSet->allowSdrSoftwareBrightness.value_or(output->allowSdrSoftwareBrightness()), }; *outputIt = SetupState{ .outputIndex = *outputIndex, .position = changeSet->pos.value_or(output->geometry().topLeft()), .enabled = changeSet->enabled.value_or(output->isEnabled()), .priority = int(outputOrder.indexOf(output)), }; } else { QSize modeSize = output->desiredModeSize(); if (modeSize.isEmpty()) { modeSize = output->currentMode()->size(); } uint32_t refreshRate = output->desiredModeRefreshRate(); if (refreshRate == 0) { refreshRate = output->currentMode()->refreshRate(); } m_outputs[*outputIndex] = OutputState{ .edidIdentifier = output->edid().identifier(), .connectorName = output->name(), .edidHash = output->edid().isValid() ? output->edid().hash() : QString{}, .mstPath = output->mstPath(), .mode = ModeData{ .size = modeSize, .refreshRate = refreshRate, }, .scale = output->scale(), .transform = output->transform(), .manualTransform = output->manualTransform(), .overscan = output->overscan(), .rgbRange = output->rgbRange(), .vrrPolicy = output->vrrPolicy(), .highDynamicRange = output->highDynamicRange(), .referenceLuminance = output->referenceLuminance(), .wideColorGamut = output->wideColorGamut(), .autoRotation = output->autoRotationPolicy(), .iccProfilePath = output->iccProfilePath(), .colorProfileSource = output->colorProfileSource(), .maxPeakBrightnessOverride = output->maxPeakBrightnessOverride(), .maxAverageBrightnessOverride = output->maxAverageBrightnessOverride(), .minBrightnessOverride = output->minBrightnessOverride(), .sdrGamutWideness = output->sdrGamutWideness(), .brightness = output->brightnessSetting(), .allowSdrSoftwareBrightness = output->allowSdrSoftwareBrightness(), }; *outputIt = SetupState{ .outputIndex = *outputIndex, .position = output->geometry().topLeft(), .enabled = output->isEnabled(), .priority = int(outputOrder.indexOf(output)), }; } } save(); } std::pair> OutputConfigurationStore::setupToConfig(Setup *setup, const std::unordered_map &outputMap) const { OutputConfiguration ret; QList> priorities; for (const auto &[output, outputIndex] : outputMap) { const OutputState &state = m_outputs[outputIndex]; const auto &setupState = *std::find_if(setup->outputs.begin(), setup->outputs.end(), [outputIndex = outputIndex](const auto &state) { return state.outputIndex == outputIndex; }); const auto modes = output->modes(); const auto modeIt = std::find_if(modes.begin(), modes.end(), [&state](const auto &mode) { return state.mode && mode->size() == state.mode->size && mode->refreshRate() == state.mode->refreshRate; }); std::optional> mode = modeIt == modes.end() ? std::nullopt : std::optional(*modeIt); if (!mode.has_value() || !*mode || ((*mode)->flags() & OutputMode::Flag::Removed)) { mode = chooseMode(output); } *ret.changeSet(output) = OutputChangeSet{ .mode = mode, .desiredModeSize = state.mode.has_value() ? std::make_optional(state.mode->size) : std::nullopt, .desiredModeRefreshRate = state.mode.has_value() ? std::make_optional(state.mode->refreshRate) : std::nullopt, .enabled = setupState.enabled, .pos = setupState.position, .scale = state.scale, .transform = state.transform, .manualTransform = state.manualTransform, .overscan = state.overscan, .rgbRange = state.rgbRange, .vrrPolicy = state.vrrPolicy, .highDynamicRange = state.highDynamicRange, .referenceLuminance = state.referenceLuminance, .wideColorGamut = state.wideColorGamut, .autoRotationPolicy = state.autoRotation, .iccProfilePath = state.iccProfilePath, .iccProfile = state.iccProfilePath ? IccProfile::load(*state.iccProfilePath) : nullptr, .maxPeakBrightnessOverride = state.maxPeakBrightnessOverride, .maxAverageBrightnessOverride = state.maxAverageBrightnessOverride, .minBrightnessOverride = state.minBrightnessOverride, .sdrGamutWideness = state.sdrGamutWideness, .colorProfileSource = state.colorProfileSource, .brightness = state.brightness, .allowSdrSoftwareBrightness = state.allowSdrSoftwareBrightness, }; if (setupState.enabled) { priorities.push_back(std::make_pair(output, setupState.priority)); } } std::sort(priorities.begin(), priorities.end(), [](const auto &left, const auto &right) { return left.second < right.second; }); QList order; std::transform(priorities.begin(), priorities.end(), std::back_inserter(order), [](const auto &pair) { return pair.first; }); return std::make_pair(ret, order); } std::optional>> OutputConfigurationStore::generateLidClosedConfig(const QList &outputs) { const auto internalIt = std::find_if(outputs.begin(), outputs.end(), [](Output *output) { return output->isInternal(); }); if (internalIt == outputs.end()) { return std::nullopt; } const auto setup = findSetup(outputs, false); if (!setup) { return std::nullopt; } Output *const internalOutput = *internalIt; auto [config, order] = setupToConfig(setup->first, setup->second); auto internalChangeset = config.changeSet(internalOutput); internalChangeset->enabled = false; order.removeOne(internalOutput); const bool anyEnabled = std::any_of(outputs.begin(), outputs.end(), [&config = config](Output *output) { return config.changeSet(output)->enabled.value_or(output->isEnabled()); }); if (!anyEnabled) { return std::nullopt; } const auto getSize = [](OutputChangeSet *changeset, Output *output) { auto mode = changeset->mode ? changeset->mode->lock() : nullptr; if (!mode) { mode = output->currentMode(); } const auto scale = changeset->scale.value_or(output->scale()); return QSize(std::ceil(mode->size().width() / scale), std::ceil(mode->size().height() / scale)); }; const QPoint internalPos = internalChangeset->pos.value_or(internalOutput->geometry().topLeft()); const QSize internalSize = getSize(internalChangeset.get(), internalOutput); for (Output *otherOutput : outputs) { auto changeset = config.changeSet(otherOutput); QPoint otherPos = changeset->pos.value_or(otherOutput->geometry().topLeft()); if (otherPos.x() >= internalPos.x() + internalSize.width()) { otherPos.rx() -= std::floor(internalSize.width()); } if (otherPos.y() >= internalPos.y() + internalSize.height()) { otherPos.ry() -= std::floor(internalSize.height()); } // make sure this doesn't make outputs overlap, which is neither supported nor expected by users const QSize otherSize = getSize(changeset.get(), otherOutput); const bool overlap = std::any_of(outputs.begin(), outputs.end(), [&, &config = config](Output *output) { if (otherOutput == output) { return false; } const auto changeset = config.changeSet(output); const QPoint pos = changeset->pos.value_or(output->geometry().topLeft()); return QRect(pos, otherSize).intersects(QRect(otherPos, getSize(changeset.get(), output))); }); if (!overlap) { changeset->pos = otherPos; } } return std::make_pair(config, order); } std::pair> OutputConfigurationStore::generateConfig(const QList &outputs, bool isLidClosed) { if (isLidClosed) { if (const auto closedConfig = generateLidClosedConfig(outputs)) { return *closedConfig; } } const auto kscreenConfig = KScreenIntegration::readOutputConfig(outputs, KScreenIntegration::connectedOutputsHash(outputs, isLidClosed)); OutputConfiguration ret; QList outputOrder; QPoint pos(0, 0); for (const auto output : outputs) { const auto kscreenChangeSetPtr = kscreenConfig ? kscreenConfig->first.constChangeSet(output) : nullptr; const auto kscreenChangeSet = kscreenChangeSetPtr ? *kscreenChangeSetPtr : OutputChangeSet{}; const auto outputIndex = findOutput(output, outputs); const bool enable = kscreenChangeSet.enabled.value_or(!isLidClosed || !output->isInternal() || outputs.size() == 1); const OutputState existingData = outputIndex ? m_outputs[*outputIndex] : OutputState{}; const auto modes = output->modes(); const auto modeIt = std::find_if(modes.begin(), modes.end(), [&existingData](const auto &mode) { return existingData.mode && mode->size() == existingData.mode->size && mode->refreshRate() == existingData.mode->refreshRate; }); const auto mode = modeIt == modes.end() ? kscreenChangeSet.mode.value_or(output->currentMode()).lock() : *modeIt; const auto changeset = ret.changeSet(output); *changeset = { .mode = mode, .desiredModeSize = mode->size(), .desiredModeRefreshRate = mode->refreshRate(), .enabled = kscreenChangeSet.enabled.value_or(enable), .pos = pos, // kscreen scale is unreliable because it gets overwritten with the value 1 on Xorg, // and we don't know if it's from Xorg or the 5.27 Wayland session... so just ignore it .scale = existingData.scale.value_or(chooseScale(output, mode.get())), .transform = existingData.transform.value_or(kscreenChangeSet.transform.value_or(output->panelOrientation())), .manualTransform = existingData.manualTransform.value_or(kscreenChangeSet.transform.value_or(output->panelOrientation())), .overscan = existingData.overscan.value_or(kscreenChangeSet.overscan.value_or(0)), .rgbRange = existingData.rgbRange.value_or(kscreenChangeSet.rgbRange.value_or(Output::RgbRange::Automatic)), .vrrPolicy = existingData.vrrPolicy.value_or(kscreenChangeSet.vrrPolicy.value_or(VrrPolicy::Automatic)), .highDynamicRange = existingData.highDynamicRange.value_or(false), .referenceLuminance = existingData.referenceLuminance.value_or(std::clamp(output->maxAverageBrightness().value_or(200), 200.0, 500.0)), .wideColorGamut = existingData.wideColorGamut.value_or(false), .autoRotationPolicy = existingData.autoRotation.value_or(Output::AutoRotationPolicy::InTabletMode), .colorProfileSource = existingData.colorProfileSource.value_or(Output::ColorProfileSource::sRGB), .brightness = existingData.brightness.value_or(1.0), .allowSdrSoftwareBrightness = existingData.allowSdrSoftwareBrightness.value_or(output->brightnessDevice() == nullptr), }; if (enable) { const auto modeSize = changeset->transform->map(mode->size()); pos.setX(std::ceil(pos.x() + modeSize.width() / *changeset->scale)); outputOrder.push_back(output); } } if (kscreenConfig && kscreenConfig->second.size() == outputOrder.size()) { // make sure the old output order is consistent with the enablement states of the outputs const bool consistent = std::ranges::all_of(outputOrder, [&kscreenConfig](const auto output) { return kscreenConfig->second.contains(output); }); if (consistent) { outputOrder = kscreenConfig->second; } } return std::make_pair(ret, outputOrder); } std::shared_ptr OutputConfigurationStore::chooseMode(Output *output) const { const auto modes = output->modes(); // some displays advertise bigger modes than their native resolution // to avoid that, take the preferred mode into account, which is usually the native one const auto preferred = std::find_if(modes.begin(), modes.end(), [](const auto &mode) { return (mode->flags() & OutputMode::Flag::Preferred) && !(mode->flags() & OutputMode::Flag::Removed); }); if (preferred != modes.end()) { // some high refresh rate displays advertise a 60Hz mode as preferred for compatibility reasons // ignore that and choose the highest possible refresh rate by default instead std::shared_ptr highestRefresh = *preferred; for (const auto &mode : modes) { if (mode->size() == highestRefresh->size() && mode->refreshRate() > highestRefresh->refreshRate()) { highestRefresh = mode; } } // if the preferred mode size has a refresh rate that's too low for PCs, // allow falling back to a mode with lower resolution and a more usable refresh rate if (highestRefresh->refreshRate() >= 50000) { return highestRefresh; } } std::shared_ptr ret; for (auto mode : modes) { if (mode->flags() & OutputMode::Flag::Generated) { // generated modes aren't guaranteed to work, so don't choose one as the default continue; } if (!ret) { ret = mode; continue; } const bool retUsableRefreshRate = ret->refreshRate() >= 50000; const bool usableRefreshRate = mode->refreshRate() >= 50000; if (retUsableRefreshRate && !usableRefreshRate) { ret = mode; continue; } if ((usableRefreshRate && !retUsableRefreshRate) || mode->size().width() > ret->size().width() || mode->size().height() > ret->size().height() || (mode->size() == ret->size() && mode->refreshRate() > ret->refreshRate())) { ret = mode; } } return ret; } double OutputConfigurationStore::chooseScale(Output *output, OutputMode *mode) const { if (output->physicalSize().height() < 3 || output->physicalSize().width() < 3) { // A screen less than 3mm wide or tall doesn't make any sense; these are // all caused by the screen mis-reporting its size. return 1.0; } const double outputDpi = mode->size().height() / (output->physicalSize().height() / 25.4); const double desiredScale = outputDpi / targetDpi(output); // round to 25% steps return std::clamp(std::round(100.0 * desiredScale / 25.0) * 25.0 / 100.0, 1.0, 3.0); } double OutputConfigurationStore::targetDpi(Output *output) const { // The eye's ability to perceive detail diminishes with distance, so objects // that are closer can be smaller and their details remain equally // distinguishable. As a result, each device type has its own ideal physical // size of items on its screen based on how close the user's eyes are // expected to be from it on average, and its target DPI value needs to be // changed accordingly. const auto devices = input()->devices(); const bool hasLaptopLid = std::any_of(devices.begin(), devices.end(), [](const auto &device) { return device->isLidSwitch(); }); if (output->isInternal()) { if (hasLaptopLid) { // laptop screens: usually closer to the face than desktop monitors return 125; } else { // phone screens: even closer than laptops return 150; } } else { // "normal" 1x scale desktop monitor dpi return 96; } } void OutputConfigurationStore::load() { const QString jsonPath = QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("kwinoutputconfig.json")); if (jsonPath.isEmpty()) { return; } QFile f(jsonPath); if (!f.open(QIODevice::ReadOnly)) { qCWarning(KWIN_CORE) << "Could not open file" << jsonPath; return; } QJsonParseError error; const auto doc = QJsonDocument::fromJson(f.readAll(), &error); if (error.error != QJsonParseError::NoError) { qCWarning(KWIN_CORE) << "Failed to parse" << jsonPath << error.errorString(); return; } const auto array = doc.array(); std::vector objects; std::transform(array.begin(), array.end(), std::back_inserter(objects), [](const auto &json) { return json.toObject(); }); const auto outputsIt = std::find_if(objects.begin(), objects.end(), [](const auto &obj) { return obj["name"].toString() == "outputs" && obj["data"].isArray(); }); const auto setupsIt = std::find_if(objects.begin(), objects.end(), [](const auto &obj) { return obj["name"].toString() == "setups" && obj["data"].isArray(); }); if (outputsIt == objects.end() || setupsIt == objects.end()) { return; } const auto outputs = (*outputsIt)["data"].toArray(); std::vector> outputDatas; for (const auto &output : outputs) { const auto data = output.toObject(); OutputState state; bool hasIdentifier = false; if (const auto it = data.find("edidIdentifier"); it != data.end()) { if (const auto str = it->toString(); !str.isEmpty()) { state.edidIdentifier = str; hasIdentifier = true; } } if (const auto it = data.find("edidHash"); it != data.end()) { if (const auto str = it->toString(); !str.isEmpty()) { state.edidHash = str; hasIdentifier = true; } } if (const auto it = data.find("connectorName"); it != data.end()) { if (const auto str = it->toString(); !str.isEmpty()) { state.connectorName = str; hasIdentifier = true; } } if (const auto it = data.find("mstPath"); it != data.end()) { if (const auto str = it->toString(); !str.isEmpty()) { state.mstPath = str; hasIdentifier = true; } } if (!hasIdentifier) { // without an identifier the settings are useless // we still have to push something into the list so that the indices stay correct outputDatas.push_back(std::nullopt); qCWarning(KWIN_CORE, "Output in config is missing identifiers"); continue; } const bool hasDuplicate = std::any_of(outputDatas.begin(), outputDatas.end(), [&state](const auto &data) { return data && data->edidIdentifier == state.edidIdentifier && data->edidHash == state.edidHash && data->mstPath == state.mstPath && data->connectorName == state.connectorName; }); if (hasDuplicate) { qCWarning(KWIN_CORE) << "Duplicate output found in config for edidIdentifier:" << state.edidIdentifier.value_or("") << "; connectorName:" << state.connectorName.value_or("") << "; mstPath:" << state.mstPath; outputDatas.push_back(std::nullopt); continue; } if (const auto it = data.find("mode"); it != data.end()) { const auto obj = it->toObject(); const int width = obj["width"].toInt(0); const int height = obj["height"].toInt(0); const int refreshRate = obj["refreshRate"].toInt(0); if (width > 0 && height > 0 && refreshRate > 0) { state.mode = ModeData{ .size = QSize(width, height), .refreshRate = uint32_t(refreshRate), }; } } if (const auto it = data.find("scale"); it != data.end()) { const double scale = it->toDouble(0); if (scale > 0 && scale <= 3) { state.scale = scale; } } if (const auto it = data.find("transform"); it != data.end()) { const auto str = it->toString(); if (str == "Normal") { state.transform = state.manualTransform = OutputTransform::Kind::Normal; } else if (str == "Rotated90") { state.transform = state.manualTransform = OutputTransform::Kind::Rotate90; } else if (str == "Rotated180") { state.transform = state.manualTransform = OutputTransform::Kind::Rotate180; } else if (str == "Rotated270") { state.transform = state.manualTransform = OutputTransform::Kind::Rotate270; } else if (str == "Flipped") { state.transform = state.manualTransform = OutputTransform::Kind::FlipX; } else if (str == "Flipped90") { state.transform = state.manualTransform = OutputTransform::Kind::FlipX90; } else if (str == "Flipped180") { state.transform = state.manualTransform = OutputTransform::Kind::FlipX180; } else if (str == "Flipped270") { state.transform = state.manualTransform = OutputTransform::Kind::FlipX270; } } if (const auto it = data.find("overscan"); it != data.end()) { const int overscan = it->toInt(-1); if (overscan >= 0 && overscan <= 100) { state.overscan = overscan; } } if (const auto it = data.find("rgbRange"); it != data.end()) { const auto str = it->toString(); if (str == "Automatic") { state.rgbRange = Output::RgbRange::Automatic; } else if (str == "Limited") { state.rgbRange = Output::RgbRange::Limited; } else if (str == "Full") { state.rgbRange = Output::RgbRange::Full; } } if (const auto it = data.find("vrrPolicy"); it != data.end()) { const auto str = it->toString(); if (str == "Never") { state.vrrPolicy = VrrPolicy::Never; } else if (str == "Automatic") { state.vrrPolicy = VrrPolicy::Automatic; } else if (str == "Always") { state.vrrPolicy = VrrPolicy::Always; } } if (const auto it = data.find("highDynamicRange"); it != data.end() && it->isBool()) { state.highDynamicRange = it->toBool(); } if (const auto it = data.find("sdrBrightness"); it != data.end() && it->isDouble()) { state.referenceLuminance = it->toInt(200); } if (const auto it = data.find("wideColorGamut"); it != data.end() && it->isBool()) { state.wideColorGamut = it->toBool(); } if (const auto it = data.find("autoRotation"); it != data.end()) { const auto str = it->toString(); if (str == "Never") { state.autoRotation = Output::AutoRotationPolicy::Never; } else if (str == "InTabletMode") { state.autoRotation = Output::AutoRotationPolicy::InTabletMode; } else if (str == "Always") { state.autoRotation = Output::AutoRotationPolicy::Always; } } if (const auto it = data.find("iccProfilePath"); it != data.end()) { state.iccProfilePath = it->toString(); } if (const auto it = data.find("maxPeakBrightnessOverride"); it != data.end() && it->isDouble()) { state.maxPeakBrightnessOverride = it->toDouble(); } if (const auto it = data.find("maxAverageBrightnessOverride"); it != data.end() && it->isDouble()) { state.maxAverageBrightnessOverride = it->toDouble(); } if (const auto it = data.find("minBrightnessOverride"); it != data.end() && it->isDouble()) { state.minBrightnessOverride = it->toDouble(); } if (const auto it = data.find("sdrGamutWideness"); it != data.end() && it->isDouble()) { state.sdrGamutWideness = it->toDouble(); } if (const auto it = data.find("colorProfileSource"); it != data.end()) { const auto str = it->toString(); if (str == "sRGB") { state.colorProfileSource = Output::ColorProfileSource::sRGB; } else if (str == "ICC") { state.colorProfileSource = Output::ColorProfileSource::ICC; } else if (str == "EDID") { state.colorProfileSource = Output::ColorProfileSource::EDID; } } else { const bool icc = state.iccProfilePath && !state.iccProfilePath->isEmpty() && !state.highDynamicRange.value_or(false) && !state.wideColorGamut.value_or(false); if (icc) { state.colorProfileSource = Output::ColorProfileSource::ICC; } else { state.colorProfileSource = Output::ColorProfileSource::sRGB; } } if (const auto it = data.find("brightness"); it != data.end() && it->isDouble()) { state.brightness = std::clamp(it->toDouble(), 0.0, 1.0); } if (const auto it = data.find("allowSdrSoftwareBrightness"); it != data.end() && it->isBool()) { state.allowSdrSoftwareBrightness = it->toBool(); } outputDatas.push_back(state); } const auto setups = (*setupsIt)["data"].toArray(); for (const auto &s : setups) { const auto data = s.toObject(); const auto outputs = data["outputs"].toArray(); Setup setup; bool fail = false; for (const auto &output : outputs) { const auto outputData = output.toObject(); SetupState state; if (const auto it = outputData.find("enabled"); it != outputData.end() && it->isBool()) { state.enabled = it->toBool(); } else { fail = true; break; } if (const auto it = outputData.find("outputIndex"); it != outputData.end()) { const int index = it->toInt(-1); if (index <= -1 || size_t(index) >= outputDatas.size()) { fail = true; break; } // the outputs must be unique const bool unique = std::none_of(setup.outputs.begin(), setup.outputs.end(), [&index](const auto &output) { return output.outputIndex == size_t(index); }); if (!unique) { fail = true; break; } state.outputIndex = index; } if (const auto it = outputData.find("position"); it != outputData.end()) { const auto obj = it->toObject(); const auto x = obj.find("x"); const auto y = obj.find("y"); if (x == obj.end() || !x->isDouble() || y == obj.end() || !y->isDouble()) { fail = true; break; } state.position = QPoint(x->toInt(0), y->toInt(0)); } else { fail = true; break; } if (const auto it = outputData.find("priority"); it != outputData.end()) { state.priority = it->toInt(-1); if (state.priority < 0 && state.enabled) { fail = true; break; } } setup.outputs.push_back(state); } if (fail || setup.outputs.empty()) { continue; } // one of the outputs must be enabled const bool noneEnabled = std::none_of(setup.outputs.begin(), setup.outputs.end(), [](const auto &output) { return output.enabled; }); if (noneEnabled) { continue; } setup.lidClosed = data["lidClosed"].toBool(false); // there must be only one setup that refers to a given set of outputs const bool alreadyExists = std::any_of(m_setups.begin(), m_setups.end(), [&setup](const auto &other) { if (setup.lidClosed != other.lidClosed || setup.outputs.size() != other.outputs.size()) { return false; } return std::all_of(setup.outputs.begin(), setup.outputs.end(), [&other](const auto &output) { return std::any_of(other.outputs.begin(), other.outputs.end(), [&output](const auto &otherOutput) { return output.outputIndex == otherOutput.outputIndex; }); }); }); if (alreadyExists) { continue; } m_setups.push_back(setup); } // repair the outputs list in case it's broken for (size_t i = 0; i < outputDatas.size();) { if (!outputDatas[i]) { outputDatas.erase(outputDatas.begin() + i); for (auto setupIt = m_setups.begin(); setupIt != m_setups.end();) { const bool broken = std::any_of(setupIt->outputs.begin(), setupIt->outputs.end(), [i](const auto &output) { return output.outputIndex == i; }); if (broken) { setupIt = m_setups.erase(setupIt); continue; } for (auto &output : setupIt->outputs) { if (output.outputIndex > i) { output.outputIndex--; } } setupIt++; } } else { i++; } } for (const auto &o : outputDatas) { Q_ASSERT(o); m_outputs.push_back(*o); } } void OutputConfigurationStore::save() { QJsonDocument document; QJsonArray array; QJsonObject outputs; outputs["name"] = "outputs"; QJsonArray outputsData; for (const auto &output : m_outputs) { QJsonObject o; if (output.edidIdentifier) { o["edidIdentifier"] = *output.edidIdentifier; } if (!output.edidHash.isEmpty()) { o["edidHash"] = output.edidHash; } if (output.connectorName) { o["connectorName"] = *output.connectorName; } if (!output.mstPath.isEmpty()) { o["mstPath"] = output.mstPath; } if (output.mode) { QJsonObject mode; mode["width"] = output.mode->size.width(); mode["height"] = output.mode->size.height(); mode["refreshRate"] = int(output.mode->refreshRate); o["mode"] = mode; } if (output.scale) { o["scale"] = *output.scale; } if (output.manualTransform == OutputTransform::Kind::Normal) { o["transform"] = "Normal"; } else if (output.manualTransform == OutputTransform::Kind::Rotate90) { o["transform"] = "Rotated90"; } else if (output.manualTransform == OutputTransform::Kind::Rotate180) { o["transform"] = "Rotated180"; } else if (output.manualTransform == OutputTransform::Kind::Rotate270) { o["transform"] = "Rotated270"; } else if (output.manualTransform == OutputTransform::Kind::FlipX) { o["transform"] = "Flipped"; } else if (output.manualTransform == OutputTransform::Kind::FlipX90) { o["transform"] = "Flipped90"; } else if (output.manualTransform == OutputTransform::Kind::FlipX180) { o["transform"] = "Flipped180"; } else if (output.manualTransform == OutputTransform::Kind::FlipX270) { o["transform"] = "Flipped270"; } if (output.overscan) { o["overscan"] = int(*output.overscan); } if (output.rgbRange == Output::RgbRange::Automatic) { o["rgbRange"] = "Automatic"; } else if (output.rgbRange == Output::RgbRange::Limited) { o["rgbRange"] = "Limited"; } else if (output.rgbRange == Output::RgbRange::Full) { o["rgbRange"] = "Full"; } if (output.vrrPolicy == VrrPolicy::Never) { o["vrrPolicy"] = "Never"; } else if (output.vrrPolicy == VrrPolicy::Automatic) { o["vrrPolicy"] = "Automatic"; } else if (output.vrrPolicy == VrrPolicy::Always) { o["vrrPolicy"] = "Always"; } if (output.highDynamicRange) { o["highDynamicRange"] = *output.highDynamicRange; } if (output.referenceLuminance) { o["sdrBrightness"] = int(*output.referenceLuminance); } if (output.wideColorGamut) { o["wideColorGamut"] = *output.wideColorGamut; } if (output.autoRotation) { switch (*output.autoRotation) { case Output::AutoRotationPolicy::Never: o["autoRotation"] = "Never"; break; case Output::AutoRotationPolicy::InTabletMode: o["autoRotation"] = "InTabletMode"; break; case Output::AutoRotationPolicy::Always: o["autoRotation"] = "Always"; break; } } if (output.iccProfilePath) { o["iccProfilePath"] = *output.iccProfilePath; } if (output.maxPeakBrightnessOverride) { o["maxPeakBrightnessOverride"] = *output.maxPeakBrightnessOverride; } if (output.maxAverageBrightnessOverride) { o["maxAverageBrightnessOverride"] = *output.maxAverageBrightnessOverride; } if (output.minBrightnessOverride) { o["minBrightnessOverride"] = *output.minBrightnessOverride; } if (output.sdrGamutWideness) { o["sdrGamutWideness"] = *output.sdrGamutWideness; } if (output.colorProfileSource) { switch (*output.colorProfileSource) { case Output::ColorProfileSource::sRGB: o["colorProfileSource"] = "sRGB"; break; case Output::ColorProfileSource::ICC: o["colorProfileSource"] = "ICC"; break; case Output::ColorProfileSource::EDID: o["colorProfileSource"] = "EDID"; break; } } if (output.brightness) { o["brightness"] = *output.brightness; } if (output.allowSdrSoftwareBrightness) { o["allowSdrSoftwareBrightness"] = *output.allowSdrSoftwareBrightness; } outputsData.append(o); } outputs["data"] = outputsData; array.append(outputs); QJsonObject setups; setups["name"] = "setups"; QJsonArray setupData; for (const auto &setup : m_setups) { QJsonObject o; o["lidClosed"] = setup.lidClosed; QJsonArray outputs; for (ssize_t i = 0; i < setup.outputs.size(); i++) { const auto &output = setup.outputs[i]; QJsonObject o; o["enabled"] = output.enabled; o["outputIndex"] = int(output.outputIndex); o["priority"] = output.priority; QJsonObject pos; pos["x"] = output.position.x(); pos["y"] = output.position.y(); o["position"] = pos; outputs.append(o); } o["outputs"] = outputs; setupData.append(o); } setups["data"] = setupData; array.append(setups); const QString path = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation) + "/kwinoutputconfig.json"; QFile f(path); if (!f.open(QIODevice::WriteOnly)) { qCWarning(KWIN_CORE, "Couldn't open output config file %s", qPrintable(path)); return; } document.setArray(array); f.write(document.toJson()); f.flush(); } bool OutputConfigurationStore::isAutoRotateActive(const QList &outputs, bool isTabletMode) const { const auto internalIt = std::find_if(outputs.begin(), outputs.end(), [](Output *output) { return output->isInternal() && output->isEnabled(); }); if (internalIt == outputs.end()) { return false; } Output *internal = *internalIt; switch (internal->autoRotationPolicy()) { case Output::AutoRotationPolicy::Never: return false; case Output::AutoRotationPolicy::InTabletMode: return isTabletMode; case Output::AutoRotationPolicy::Always: return true; } Q_UNREACHABLE(); } }