Add OutputManager for taking care of output settings in KWin

As a first step to move away from having an external service remember output settings
for KWin, this commit introduces an output manager that can load configuration files
and generate new output configurations.
This commit is contained in:
Xaver Hugl 2023-03-05 14:59:49 +01:00
parent 6b8e08dfa9
commit c51824b535
15 changed files with 214 additions and 33 deletions

View file

@ -108,6 +108,7 @@ target_sources(kwin PRIVATE
options.cpp
osd.cpp
outline.cpp
outputconfigurationstore.cpp
placeholderinputeventfilter.cpp
placeholderoutput.cpp
placement.cpp

View file

@ -473,13 +473,15 @@ bool DrmBackend::applyOutputChanges(const OutputConfiguration &config)
if (output->isNonDesktop()) {
continue;
}
if (const auto changeset = config.constChangeSet(output)) {
output->queueChanges(config);
if (config.constChangeSet(output)->enabled) {
if (changeset->enabled) {
toBeEnabled << output;
} else {
toBeDisabled << output;
}
}
}
if (gpu->testPendingConfiguration() != DrmPipeline::Error::None) {
for (const auto &output : std::as_const(toBeEnabled)) {
output->revertQueuedChanges();

View file

@ -77,7 +77,7 @@ DrmOutput::DrmOutput(const std::shared_ptr<DrmConnector> &conn)
.serialNumber = edid->serialNumber(),
.eisaId = edid->eisaId(),
.physicalSize = conn->physicalSize(),
.edid = edid->raw(),
.edid = *edid,
.subPixel = conn->subpixel(),
.capabilities = capabilities,
.panelOrientation = DrmConnector::toKWinTransform(conn->panelOrientation()),

View file

@ -377,7 +377,7 @@ void X11StandaloneBackend::doUpdateOutputs()
information.manufacturer = edid.manufacturerString();
information.model = edid.monitorName();
information.serialNumber = edid.serialNumber();
information.edid = data;
information.edid = edid;
}
}

View file

@ -199,7 +199,7 @@ QSize Output::pixelSize() const
return orientateSize(modeSize());
}
QByteArray Output::edid() const
const Edid &Output::edid() const
{
return m_information.edid;
}
@ -222,6 +222,9 @@ Output::SubPixel Output::subPixel() const
void Output::applyChanges(const OutputConfiguration &config)
{
auto props = config.constChangeSet(this);
if (!props) {
return;
}
Q_EMIT aboutToChange();
State next = m_state;

View file

@ -11,6 +11,7 @@
#include <kwin_export.h>
#include "renderloop.h"
#include "utils/edid.h"
#include <QDebug>
#include <QMatrix3x3>
@ -235,7 +236,7 @@ public:
SubPixel subPixel() const;
QString description() const;
Capabilities capabilities() const;
QByteArray edid() const;
const Edid &edid() const;
QList<std::shared_ptr<OutputMode>> modes() const;
std::shared_ptr<OutputMode> currentMode() const;
DpmsMode dpmsMode() const;
@ -328,7 +329,7 @@ protected:
QString serialNumber;
QString eisaId;
QSize physicalSize;
QByteArray edid;
Edid edid;
SubPixel subPixel = SubPixel::Unknown;
Capabilities capabilities;
Transform panelOrientation = Transform::Normal;

View file

@ -65,12 +65,14 @@ bool OutputBackend::applyOutputChanges(const OutputConfiguration &config)
QVector<Output *> toBeEnabledOutputs;
QVector<Output *> toBeDisabledOutputs;
for (const auto &output : availableOutputs) {
if (config.constChangeSet(output)->enabled) {
if (const auto changeset = config.constChangeSet(output)) {
if (changeset->enabled) {
toBeEnabledOutputs << output;
} else {
toBeDisabledOutputs << output;
}
}
}
for (const auto &output : toBeEnabledOutputs) {
output->applyChanges(config);
}

View file

@ -26,10 +26,8 @@ namespace KScreenIntegration
/// See KScreen::Output::hashMd5
static QString outputHash(Output *output)
{
if (!output->edid().isEmpty()) {
QCryptographicHash hash(QCryptographicHash::Md5);
hash.addData(output->edid());
return QString::fromLatin1(hash.result().toHex());
if (output->edid().isValid()) {
return output->edid().hash();
} else {
return output->name();
}

View file

@ -0,0 +1,132 @@
/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "outputconfigurationstore.h"
#include "core/inputdevice.h"
#include "core/output.h"
#include "core/outputconfiguration.h"
#include "input.h"
#include "kscreenintegration.h"
namespace KWin
{
std::pair<OutputConfiguration, QVector<Output *>> OutputConfigurationStore::queryConfig(const QVector<Output *> &outputs)
{
const auto kscreenConfig = KScreenIntegration::readOutputConfig(outputs, KScreenIntegration::connectedOutputsHash(outputs));
if (kscreenConfig) {
return kscreenConfig.value();
} else {
// no config file atm -> generate a new one
return generateConfig(outputs);
}
}
std::pair<OutputConfiguration, QVector<Output *>> OutputConfigurationStore::generateConfig(const QVector<Output *> &outputs) const
{
OutputConfiguration ret;
QPoint pos(0, 0);
for (const auto output : outputs) {
const auto mode = chooseMode(output);
const double scale = chooseScale(output, mode.get());
*ret.changeSet(output) = {
.mode = mode,
.enabled = true,
.pos = pos,
.scale = scale,
.transform = output->panelOrientation(),
.overscan = 0,
.rgbRange = Output::RgbRange::Automatic,
.vrrPolicy = RenderLoop::VrrPolicy::Automatic,
};
pos.setX(pos.x() + mode->size().width() / scale);
}
return std::make_pair(ret, outputs);
}
std::shared_ptr<OutputMode> 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;
});
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<OutputMode> 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<OutputMode> ret;
for (auto mode : modes) {
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() <= 0) {
// invalid size, can't do anything with this
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::round(100.0 * desiredScale / 25.0) * 100.0 / 25.0;
}
double OutputConfigurationStore::targetDpi(Output *output) const
{
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 136;
}
} else {
// "normal" 1x scale desktop monitor dpi
return 96;
}
}
}

View file

@ -0,0 +1,33 @@
/*
KWin - the KDE window manager
This file is part of the KDE project.
SPDX-FileCopyrightText: 2023 Xaver Hugl <xaver.hugl@gmail.com>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include <QVector>
#include <memory>
namespace KWin
{
class OutputConfiguration;
class Output;
class OutputMode;
class OutputConfigurationStore
{
public:
std::pair<OutputConfiguration, QVector<Output *>> queryConfig(const QVector<Output *> &outputs);
private:
std::pair<OutputConfiguration, QVector<Output *>> generateConfig(const QVector<Output *> &outputs) const;
std::shared_ptr<OutputMode> chooseMode(Output *output) const;
double chooseScale(Output *output, OutputMode *mode) const;
double targetDpi(Output *output) const;
};
}

View file

@ -15,6 +15,7 @@
#include <QStandardPaths>
#include <KLocalizedString>
#include <QCryptographicHash>
namespace KWin
{
@ -175,6 +176,9 @@ Edid::Edid(const void *data, uint32_t size)
m_monitorName = parseMonitorName(bytes);
m_serialNumber = parseSerialNumber(bytes);
m_vendor = parseVendor(bytes);
QCryptographicHash hash(QCryptographicHash::Md5);
hash.addData(m_raw);
m_hash = QString::fromLatin1(hash.result().toHex());
m_isValid = true;
}
@ -241,4 +245,9 @@ QString Edid::nameString() const
}
}
QString Edid::hash() const
{
return m_hash;
}
} // namespace KWin

View file

@ -74,12 +74,15 @@ public:
*/
QString nameString() const;
QString hash() const;
private:
QSize m_physicalSize;
QByteArray m_vendor;
QByteArray m_eisaId;
QByteArray m_monitorName;
QByteArray m_serialNumber;
QString m_hash;
QByteArray m_raw;
bool m_isValid = false;

View file

@ -484,7 +484,7 @@ void OutputDeviceV2Interface::updateCurrentMode()
void OutputDeviceV2Interface::updateEdid()
{
d->m_edid = d->m_handle->edid();
d->m_edid = d->m_handle->edid().raw();
const auto clientResources = d->resourceMap();
for (const auto &resource : clientResources) {
d->sendEdid(resource);

View file

@ -46,8 +46,8 @@
#include "tabbox/tabbox.h"
#endif
#include "decorations/decorationbridge.h"
#include "kscreenintegration.h"
#include "main.h"
#include "outputconfigurationstore.h"
#include "placeholderinputeventfilter.h"
#include "placeholderoutput.h"
#include "placementtracker.h"
@ -130,6 +130,7 @@ Workspace::Workspace()
, m_focusChain(std::make_unique<FocusChain>())
, m_applicationMenu(std::make_unique<ApplicationMenu>())
, m_placementTracker(std::make_unique<PlacementTracker>(this))
, m_outputConfigStore(std::make_unique<OutputConfigurationStore>())
{
// If KWin was already running it saved its configuration after loosing the selection -> Reread
QFuture<void> reparseConfigFuture = QtConcurrent::run(&Options::reparseConfiguration, options);
@ -277,8 +278,8 @@ QString Workspace::getPlacementTrackerHash()
QStringList hashes;
for (const auto &output : std::as_const(m_outputs)) {
QCryptographicHash hash(QCryptographicHash::Md5);
if (!output->edid().isEmpty()) {
hash.addData(output->edid());
if (output->edid().isValid()) {
hash.addData(output->edid().raw());
} else {
hash.addData(output->name().toLatin1());
}
@ -537,18 +538,13 @@ void Workspace::updateOutputConfiguration()
setOutputOrder(newOrder);
};
m_outputsHash = KScreenIntegration::connectedOutputsHash(outputs);
if (const auto config = KScreenIntegration::readOutputConfig(outputs, m_outputsHash)) {
const auto &[cfg, order] = config.value();
const auto &[cfg, order] = m_outputConfigStore->queryConfig(outputs);
if (!kwinApp()->outputBackend()->applyOutputChanges(cfg)) {
qCWarning(KWIN_CORE) << "Applying KScreen config failed!";
qCWarning(KWIN_CORE) << "Applying output config failed!";
setFallbackOutputOrder();
return;
}
setOutputOrder(order);
} else {
setFallbackOutputOrder();
}
}
void Workspace::setupWindowConnections(Window *window)

View file

@ -78,6 +78,7 @@ class PlaceholderOutput;
class Placement;
class OutputConfiguration;
class TileManager;
class OutputConfigurationStore;
class KWIN_EXPORT Workspace : public QObject
{
@ -632,7 +633,6 @@ private:
QList<Output *> m_outputs;
Output *m_activeOutput = nullptr;
Output *m_activeCursorOutput = nullptr;
QString m_outputsHash;
QVector<Output *> m_outputOrder;
Window *m_activeWindow;
@ -723,6 +723,7 @@ private:
PlaceholderOutput *m_placeholderOutput = nullptr;
std::unique_ptr<PlaceholderInputEventFilter> m_placeholderFilter;
std::map<Output *, std::unique_ptr<TileManager>> m_tileManagers;
std::unique_ptr<OutputConfigurationStore> m_outputConfigStore;
private:
friend bool performTransiencyCheck();