Move kscreen integration in Workspace

The main motivation behind moving kscreen integration to the
Workspace is to make output configuration work the same way
regardless of the backend and simplify the drm backend.
This commit is contained in:
Vlad Zahorodnii 2022-08-17 15:39:49 +03:00
parent cdf777663d
commit b5a4c08231
4 changed files with 185 additions and 177 deletions

View file

@ -32,16 +32,10 @@
#include <KCoreAddons>
#include <KLocalizedString>
// Qt
#include <QCryptographicHash>
#include <QFile>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSocketNotifier>
// system
#include <algorithm>
#include <cerrno>
#include <math.h>
#include <sys/stat.h>
#include <unistd.h>
// drm
@ -334,7 +328,6 @@ void DrmBackend::removeOutput(DrmAbstractOutput *o)
void DrmBackend::updateOutputs()
{
const auto oldOutputs = m_outputs;
for (auto it = m_gpus.begin(); it < m_gpus.end();) {
auto gpu = it->get();
gpu->updateOutputs();
@ -357,177 +350,9 @@ void DrmBackend::updateOutputs()
return false;
}
});
if (oldOutputs != m_outputs) {
readOutputsConfiguration(m_outputs);
}
Q_EMIT screensQueried();
}
namespace KWinKScreenIntegration
{
/// See KScreen::Output::hashMd5
QString outputHash(DrmAbstractOutput *output)
{
QCryptographicHash hash(QCryptographicHash::Md5);
if (!output->edid().isEmpty()) {
hash.addData(output->edid());
} else {
hash.addData(output->name().toLatin1());
}
return QString::fromLatin1(hash.result().toHex());
}
/// See KScreen::Config::connectedOutputsHash in libkscreen
QString connectedOutputsHash(const QVector<DrmAbstractOutput *> &outputs)
{
QStringList hashedOutputs;
hashedOutputs.reserve(outputs.count());
for (auto output : qAsConst(outputs)) {
if (!output->isPlaceholder() && !output->isNonDesktop()) {
hashedOutputs << outputHash(output);
}
}
std::sort(hashedOutputs.begin(), hashedOutputs.end());
const auto hash = QCryptographicHash::hash(hashedOutputs.join(QString()).toLatin1(), QCryptographicHash::Md5);
return QString::fromLatin1(hash.toHex());
}
QMap<DrmAbstractOutput *, QJsonObject> outputsConfig(const QVector<DrmAbstractOutput *> &outputs)
{
const QString kscreenJsonPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kscreen/") % connectedOutputsHash(outputs));
if (kscreenJsonPath.isEmpty()) {
return {};
}
QFile f(kscreenJsonPath);
if (!f.open(QIODevice::ReadOnly)) {
qCWarning(KWIN_DRM) << "Could not open file" << kscreenJsonPath;
return {};
}
QJsonParseError error;
const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(KWIN_DRM) << "Failed to parse" << kscreenJsonPath << error.errorString();
return {};
}
QMap<DrmAbstractOutput *, QJsonObject> ret;
const auto outputsJson = doc.array();
for (const auto &outputJson : outputsJson) {
const auto outputObject = outputJson.toObject();
for (auto it = outputs.constBegin(), itEnd = outputs.constEnd(); it != itEnd;) {
if (!ret.contains(*it) && outputObject["id"] == outputHash(*it)) {
ret[*it] = outputObject;
continue;
}
++it;
}
}
return ret;
}
/// See KScreen::Output::Rotation
enum Rotation {
None = 1,
Left = 2,
Inverted = 4,
Right = 8,
};
DrmOutput::Transform toDrmTransform(int rotation)
{
switch (Rotation(rotation)) {
case None:
return DrmOutput::Transform::Normal;
case Left:
return DrmOutput::Transform::Rotated90;
case Inverted:
return DrmOutput::Transform::Rotated180;
case Right:
return DrmOutput::Transform::Rotated270;
default:
Q_UNREACHABLE();
}
}
std::shared_ptr<OutputMode> parseMode(Output *output, const QJsonObject &modeInfo)
{
const QJsonObject size = modeInfo["size"].toObject();
const QSize modeSize = QSize(size["width"].toInt(), size["height"].toInt());
const int refreshRate = round(modeInfo["refresh"].toDouble() * 1000);
const auto modes = output->modes();
auto it = std::find_if(modes.begin(), modes.end(), [&modeSize, &refreshRate](const auto &mode) {
return mode->size() == modeSize && mode->refreshRate() == refreshRate;
});
return (it != modes.end()) ? *it : nullptr;
}
}
bool DrmBackend::readOutputsConfiguration(const QVector<DrmAbstractOutput *> &outputs)
{
Q_ASSERT(!outputs.isEmpty());
const auto outputsInfo = KWinKScreenIntegration::outputsConfig(outputs);
Output *primaryOutput = outputs.constFirst();
OutputConfiguration cfg;
// default position goes from left to right
QPoint pos(0, 0);
for (const auto &output : qAsConst(outputs)) {
if (output->isPlaceholder() || output->isNonDesktop()) {
continue;
}
auto props = cfg.changeSet(output);
const QJsonObject outputInfo = outputsInfo[output];
qCDebug(KWIN_DRM) << "Reading output configuration for " << output;
if (!outputInfo.isEmpty()) {
if (outputInfo["primary"].toBool()) {
primaryOutput = output;
}
props->enabled = outputInfo["enabled"].toBool(true);
const QJsonObject pos = outputInfo["pos"].toObject();
props->pos = QPoint(pos["x"].toInt(), pos["y"].toInt());
if (const QJsonValue scale = outputInfo["scale"]; !scale.isUndefined()) {
props->scale = scale.toDouble(1.);
}
props->transform = KWinKScreenIntegration::toDrmTransform(outputInfo["rotation"].toInt());
props->overscan = static_cast<uint32_t>(outputInfo["overscan"].toInt(props->overscan));
props->vrrPolicy = static_cast<RenderLoop::VrrPolicy>(outputInfo["vrrpolicy"].toInt(static_cast<uint32_t>(props->vrrPolicy)));
props->rgbRange = static_cast<Output::RgbRange>(outputInfo["rgbrange"].toInt(static_cast<uint32_t>(props->rgbRange)));
if (const QJsonObject modeInfo = outputInfo["mode"].toObject(); !modeInfo.isEmpty()) {
if (auto mode = KWinKScreenIntegration::parseMode(output, modeInfo)) {
props->mode = mode;
}
}
} else {
props->enabled = true;
props->pos = pos;
props->transform = DrmOutput::Transform::Normal;
}
pos.setX(pos.x() + output->geometry().width());
}
bool allDisabled = std::all_of(outputs.begin(), outputs.end(), [&cfg](const auto &output) {
return !cfg.changeSet(output)->enabled;
});
if (allDisabled) {
qCWarning(KWIN_DRM) << "KScreen config would disable all outputs!";
return false;
}
if (!cfg.changeSet(primaryOutput)->enabled) {
qCWarning(KWIN_DRM) << "KScreen config would disable the primary output!";
return false;
}
if (!applyOutputChanges(cfg)) {
qCWarning(KWIN_DRM) << "Applying KScreen config failed!";
return false;
}
setPrimaryOutput(primaryOutput);
return true;
}
void DrmBackend::enableOutput(DrmAbstractOutput *output, bool enable)
{
if (m_enabledOutputs.contains(output) == enable) {
@ -611,7 +436,6 @@ QString DrmBackend::supportInformation() const
Output *DrmBackend::createVirtualOutput(const QString &name, const QSize &size, double scale)
{
auto output = primaryGpu()->createVirtualOutput(name, size * scale, scale, DrmVirtualOutput::Type::Virtual);
readOutputsConfiguration(m_outputs);
Q_EMIT screensQueried();
return output;
}

View file

@ -95,7 +95,6 @@ private:
void activate(bool active);
void reactivate();
void deactivate();
bool readOutputsConfiguration(const QVector<DrmAbstractOutput *> &outputs);
void handleUdevEvent();
void removeGpu(DrmGpu *gpu);
DrmGpu *addGpu(const QString &fileName);

View file

@ -32,6 +32,7 @@
#include "moving_client_x11_filter.h"
#include "netinfo.h"
#include "outline.h"
#include "outputconfiguration.h"
#include "placement.h"
#include "platform.h"
#include "pluginmanager.h"
@ -206,6 +207,11 @@ void Workspace::init()
connect(options, &Options::separateScreenFocusChanged, m_focusChain.get(), &FocusChain::setSeparateScreenFocus);
m_focusChain->setSeparateScreenFocus(options->isSeparateScreenFocus());
if (waylandServer()) {
updateOutputConfiguration();
connect(kwinApp()->platform(), &Platform::screensQueried, this, &Workspace::updateOutputConfiguration);
}
Platform *platform = kwinApp()->platform();
connect(platform, &Platform::outputEnabled, this, &Workspace::slotOutputEnabled);
connect(platform, &Platform::outputDisabled, this, &Workspace::slotOutputDisabled);
@ -495,6 +501,181 @@ Workspace::~Workspace()
_self = nullptr;
}
namespace KWinKScreenIntegration
{
/// See KScreen::Output::hashMd5
QString outputHash(Output *output)
{
QCryptographicHash hash(QCryptographicHash::Md5);
if (!output->edid().isEmpty()) {
hash.addData(output->edid());
} else {
hash.addData(output->name().toLatin1());
}
return QString::fromLatin1(hash.result().toHex());
}
/// See KScreen::Config::connectedOutputsHash in libkscreen
QString connectedOutputsHash(const QVector<Output *> &outputs)
{
QStringList hashedOutputs;
hashedOutputs.reserve(outputs.count());
for (auto output : qAsConst(outputs)) {
if (!output->isPlaceholder() && !output->isNonDesktop()) {
hashedOutputs << outputHash(output);
}
}
std::sort(hashedOutputs.begin(), hashedOutputs.end());
const auto hash = QCryptographicHash::hash(hashedOutputs.join(QString()).toLatin1(), QCryptographicHash::Md5);
return QString::fromLatin1(hash.toHex());
}
QMap<Output *, QJsonObject> outputsConfig(const QVector<Output *> &outputs, const QString &hash)
{
const QString kscreenJsonPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kscreen/") % hash);
if (kscreenJsonPath.isEmpty()) {
return {};
}
QFile f(kscreenJsonPath);
if (!f.open(QIODevice::ReadOnly)) {
qCWarning(KWIN_CORE) << "Could not open file" << kscreenJsonPath;
return {};
}
QJsonParseError error;
const auto doc = QJsonDocument::fromJson(f.readAll(), &error);
if (error.error != QJsonParseError::NoError) {
qCWarning(KWIN_CORE) << "Failed to parse" << kscreenJsonPath << error.errorString();
return {};
}
QMap<Output *, QJsonObject> ret;
const auto outputsJson = doc.array();
for (const auto &outputJson : outputsJson) {
const auto outputObject = outputJson.toObject();
for (auto it = outputs.constBegin(), itEnd = outputs.constEnd(); it != itEnd;) {
if (!ret.contains(*it) && outputObject["id"] == outputHash(*it)) {
ret[*it] = outputObject;
continue;
}
++it;
}
}
return ret;
}
/// See KScreen::Output::Rotation
enum Rotation {
None = 1,
Left = 2,
Inverted = 4,
Right = 8,
};
Output::Transform toDrmTransform(int rotation)
{
switch (Rotation(rotation)) {
case None:
return Output::Transform::Normal;
case Left:
return Output::Transform::Rotated90;
case Inverted:
return Output::Transform::Rotated180;
case Right:
return Output::Transform::Rotated270;
default:
Q_UNREACHABLE();
}
}
std::shared_ptr<OutputMode> parseMode(Output *output, const QJsonObject &modeInfo)
{
const QJsonObject size = modeInfo["size"].toObject();
const QSize modeSize = QSize(size["width"].toInt(), size["height"].toInt());
const int refreshRate = std::round(modeInfo["refresh"].toDouble() * 1000);
const auto modes = output->modes();
auto it = std::find_if(modes.begin(), modes.end(), [&modeSize, &refreshRate](const auto &mode) {
return mode->size() == modeSize && mode->refreshRate() == refreshRate;
});
return (it != modes.end()) ? *it : nullptr;
}
}
void Workspace::updateOutputConfiguration()
{
// There's conflict between this code and setVirtualOutputs(), need to adjust the tests.
if (QStandardPaths::isTestModeEnabled()) {
return;
}
const auto outputs = kwinApp()->platform()->outputs();
const QString hash = KWinKScreenIntegration::connectedOutputsHash(outputs);
if (m_outputsHash == hash) {
return;
}
const auto outputsInfo = KWinKScreenIntegration::outputsConfig(outputs, hash);
m_outputsHash = hash;
Output *primaryOutput = outputs.constFirst();
OutputConfiguration cfg;
// default position goes from left to right
QPoint pos(0, 0);
for (const auto &output : qAsConst(outputs)) {
if (output->isPlaceholder() || output->isNonDesktop()) {
continue;
}
auto props = cfg.changeSet(output);
const QJsonObject outputInfo = outputsInfo[output];
qCDebug(KWIN_CORE) << "Reading output configuration for " << output;
if (!outputInfo.isEmpty()) {
if (outputInfo["primary"].toBool()) {
primaryOutput = output;
}
props->enabled = outputInfo["enabled"].toBool(true);
const QJsonObject pos = outputInfo["pos"].toObject();
props->pos = QPoint(pos["x"].toInt(), pos["y"].toInt());
if (const QJsonValue scale = outputInfo["scale"]; !scale.isUndefined()) {
props->scale = scale.toDouble(1.);
}
props->transform = KWinKScreenIntegration::toDrmTransform(outputInfo["rotation"].toInt());
props->overscan = static_cast<uint32_t>(outputInfo["overscan"].toInt(props->overscan));
props->vrrPolicy = static_cast<RenderLoop::VrrPolicy>(outputInfo["vrrpolicy"].toInt(static_cast<uint32_t>(props->vrrPolicy)));
props->rgbRange = static_cast<Output::RgbRange>(outputInfo["rgbrange"].toInt(static_cast<uint32_t>(props->rgbRange)));
if (const QJsonObject modeInfo = outputInfo["mode"].toObject(); !modeInfo.isEmpty()) {
if (auto mode = KWinKScreenIntegration::parseMode(output, modeInfo)) {
props->mode = mode;
}
}
} else {
props->enabled = true;
props->pos = pos;
props->transform = Output::Transform::Normal;
}
pos.setX(pos.x() + output->geometry().width());
}
bool allDisabled = std::all_of(outputs.begin(), outputs.end(), [&cfg](const auto &output) {
return !cfg.changeSet(output)->enabled;
});
if (allDisabled) {
qCWarning(KWIN_CORE) << "KScreen config would disable all outputs!";
return;
}
if (!cfg.changeSet(primaryOutput)->enabled) {
qCWarning(KWIN_CORE) << "KScreen config would disable the primary output!";
return;
}
if (!kwinApp()->platform()->applyOutputChanges(cfg)) {
qCWarning(KWIN_CORE) << "Applying KScreen config failed!";
return;
}
kwinApp()->platform()->setPrimaryOutput(primaryOutput);
}
void Workspace::setupWindowConnections(Window *window)
{
connect(window, &Window::desktopPresenceChanged, this, &Workspace::desktopPresenceChanged);

View file

@ -626,6 +626,8 @@ private:
Window *findWindowToActivateOnDesktop(VirtualDesktop *desktop);
void removeWindow(Window *window);
void updateOutputConfiguration();
struct Constraint
{
Window *below;
@ -648,6 +650,8 @@ private:
QList<Output *> m_outputs;
Output *m_activeOutput = nullptr;
QString m_outputsHash;
Window *m_activeWindow;
Window *m_lastActiveWindow;
Window *m_moveResizeWindow;