diff --git a/backends/kwayland/plugins/CMakeLists.txt b/backends/kwayland/plugins/CMakeLists.txt --- a/backends/kwayland/plugins/CMakeLists.txt +++ b/backends/kwayland/plugins/CMakeLists.txt @@ -7,5 +7,12 @@ ) if (Wrapland_FOUND) + message("Wrapland found with version ${Wrapland_VERSION}.") + if (${Wrapland_VERSION} VERSION_GREATER_EQUAL "0.518.80") + message("Selecting Wrapland KWinFT and wlroots plugins.") + add_subdirectory(wrapland-wlr) + else() + message("Selecting Wrapland KWinFT plugin (Wrapland version too low for wlroots plugin).") + endif() add_subdirectory(wrapland) endif() diff --git a/backends/kwayland/plugins/wrapland-wlr/CMakeLists.txt b/backends/kwayland/plugins/wrapland-wlr/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/backends/kwayland/plugins/wrapland-wlr/CMakeLists.txt @@ -0,0 +1,29 @@ +set(wrapland_SRCS + wrapland_interface.cpp + wrapland_output.cpp + ../../wayland_interface.cpp + ../../waylandoutput.cpp + ../../wayland_logging.cpp + ../../../utils.cpp +) + +include_directories(${CMAKE_SOURCE_DIR}/backends/kwayland) + +add_library(LibkscreenWaylandPluginWraplandWlr MODULE ${wrapland_SRCS}) + +set_target_properties(LibkscreenWaylandPluginWraplandWlr + PROPERTIES LIBRARY_OUTPUT_DIRECTORY + "${CMAKE_BINARY_DIR}/bin/org.kde.libkscreen.backends/wayland/" +) + +target_link_libraries(LibkscreenWaylandPluginWraplandWlr + Qt5::Core + Qt5::Gui + KF5::Screen + WraplandClient +) + +install( + TARGETS LibkscreenWaylandPluginWraplandWlr + DESTINATION ${PLUGIN_INSTALL_DIR}/org.kde.libkscreen.backends/wayland/ +) diff --git a/backends/kwayland/plugins/wrapland-wlr/wrapland-wlr.json b/backends/kwayland/plugins/wrapland-wlr/wrapland-wlr.json new file mode 100644 --- /dev/null +++ b/backends/kwayland/plugins/wrapland-wlr/wrapland-wlr.json @@ -0,0 +1,7 @@ +{ + "KPlugin": { + "Description": "Wayland backend using Wrapland library's output management interface for wlroots based compositors.", + "Id": "LibkscreenWaylandPluginWraplandWlr", + "Name": "wrapland-wlr" + } +} diff --git a/backends/kwayland/plugins/wrapland-wlr/wrapland_interface.h b/backends/kwayland/plugins/wrapland-wlr/wrapland_interface.h new file mode 100644 --- /dev/null +++ b/backends/kwayland/plugins/wrapland-wlr/wrapland_interface.h @@ -0,0 +1,98 @@ +/************************************************************************* +Copyright © 2020 Roman Gilg + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +**************************************************************************/ +#pragma once + +#include "wayland_interface.h" +#include "config.h" + +namespace Wrapland +{ +namespace Client +{ +class ConnectionThread; +class EventQueue; +class Registry; +class WlrOutputManagerV1; +class WlrOutputHeadV1; +} +} + +namespace KScreen +{ +class Output; +class WraplandOutput; +class WaylandOutput; +class WaylandScreen; + +class WraplandFactory : public WaylandFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID "org.kde.libkscreen.waylandinterface" FILE "wrapland-wlr.json") + +public: + WaylandInterface* createInterface(QObject *parent = nullptr) override; +}; + +class WraplandInterface : public WaylandInterface +{ + Q_OBJECT + +public: + explicit WraplandInterface(QObject *parent = nullptr); + ~WraplandInterface() override = default; + + void initConnection(QThread *thread) override; + bool isInitialized() const override; + + QMap outputMap() const override; + + void applyConfig(const KScreen::ConfigPtr &newConfig) override; + void updateConfig(KScreen::ConfigPtr &config) override; + + Wrapland::Client::WlrOutputManagerV1* outputManager() const; + +protected: + void insertOutput(WaylandOutput *output) override; + WaylandOutput* takeOutput(WaylandOutput *output) override; + void handleDisconnect() override; + +private: + void setupRegistry(); + void addHead(Wrapland::Client::WlrOutputHeadV1 *head); + void tryPendingConfig(); + + Wrapland::Client::ConnectionThread *m_connection; + Wrapland::Client::EventQueue *m_queue; + + Wrapland::Client::Registry *m_registry; + Wrapland::Client::WlrOutputManagerV1 *m_outputManager; + + // Wrapland names as keys + QMap m_outputMap; + + // Wrapland names + int m_lastOutputId = -1; + + bool m_registryInitialized; + bool m_blockSignals; + KScreen::ConfigPtr m_kscreenPendingConfig; + + int m_outputId = 0; +}; + +} diff --git a/backends/kwayland/plugins/wrapland-wlr/wrapland_interface.cpp b/backends/kwayland/plugins/wrapland-wlr/wrapland_interface.cpp new file mode 100644 --- /dev/null +++ b/backends/kwayland/plugins/wrapland-wlr/wrapland_interface.cpp @@ -0,0 +1,261 @@ +/************************************************************************* +Copyright © 2020 Roman Gilg + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +**************************************************************************/ +#include "wrapland_interface.h" + +#include "waylandbackend.h" +#include "wrapland_output.h" +#include "waylandscreen.h" + +#include "../../wayland_logging.h" + +#include +#include +#include +#include +#include + +#include + +using namespace KScreen; + +WaylandInterface* WraplandFactory::createInterface(QObject *parent) +{ + return new WraplandInterface(parent); +} + +WraplandInterface::WraplandInterface(QObject *parent) + : WaylandInterface(parent) + , m_outputManager(nullptr) + , m_registryInitialized(false) + , m_kscreenPendingConfig(nullptr) +{ +} + +void WraplandInterface::initConnection(QThread *thread) +{ + m_connection = new Wrapland::Client::ConnectionThread; + + connect(m_connection, &Wrapland::Client::ConnectionThread::establishedChanged, + this, [this](bool established) { + if (established) { + setupRegistry(); + } else { + handleDisconnect(); + } + }, Qt::QueuedConnection); + + connect(m_connection, &Wrapland::Client::ConnectionThread::failed, this, [this] { + qCWarning(KSCREEN_WAYLAND) << "Failed to connect to Wayland server at socket:" + << m_connection->socketName(); + Q_EMIT connectionFailed(m_connection->socketName()); + }); + + thread->start(); + m_connection->moveToThread(thread); + m_connection->establishConnection(); +} + +Wrapland::Client::WlrOutputManagerV1* WraplandInterface::outputManager() const +{ + return m_outputManager; +} + +bool WraplandInterface::isInitialized() const +{ + return m_registryInitialized && m_outputManager != nullptr + && WaylandInterface::isInitialized(); +} + +void WraplandInterface::handleDisconnect() +{ + qDeleteAll(m_outputMap); + m_outputMap.clear(); + + // Clean up + if (m_queue) { + delete m_queue; + m_queue = nullptr; + } + + m_connection->deleteLater(); + m_connection = nullptr; + + WaylandInterface::handleDisconnect(); +} + +void WraplandInterface::setupRegistry() +{ + m_queue = new Wrapland::Client::EventQueue(this); + m_queue->setup(m_connection); + + m_registry = new Wrapland::Client::Registry(this); + + connect(m_registry, &Wrapland::Client::Registry::wlrOutputManagerV1Announced, + this, [this](quint32 name, quint32 version) { + m_outputManager = m_registry->createWlrOutputManagerV1(name, version, m_registry); + + connect(m_outputManager, &Wrapland::Client::WlrOutputManagerV1::head, + this, &WraplandInterface::addHead); + + connect(m_outputManager, &Wrapland::Client::WlrOutputManagerV1::done, + this, [this] { + // We only need to process this once in the beginning. + disconnect(m_outputManager, + &Wrapland::Client::WlrOutputManagerV1::done, this, nullptr); + unblockSignals(); + checkInitialized(); + }); + m_outputManager->setEventQueue(m_queue); + } + ); + + connect(m_registry, &Wrapland::Client::Registry::interfacesAnnounced, + this, [this] { + m_registryInitialized = true; + checkInitialized(); + } + ); + + m_registry->setEventQueue(m_queue); + m_registry->create(m_connection); + m_registry->setup(); +} + +void WraplandInterface::addHead(Wrapland::Client::WlrOutputHeadV1 *head) +{ + auto output = new WraplandOutput(++m_outputId, head, this); + addOutput(output); +} + +void WraplandInterface::insertOutput(WaylandOutput *output) +{ + auto *out = static_cast(output); + m_outputMap.insert(out->id(), out); +} + +WaylandOutput* WraplandInterface::takeOutput(WaylandOutput *output) +{ + auto *out = static_cast(output); + return m_outputMap.take(out->id()); +} + +void WraplandInterface::updateConfig(KScreen::ConfigPtr &config) +{ + config->setSupportedFeatures(Config::Feature::Writable | Config::Feature::PerOutputScaling); + config->setValid(m_connection->display()); + + //Removing removed outputs + const KScreen::OutputList outputs = config->outputs(); + for (const auto &output : outputs) { + if (!m_outputMap.contains(output->id())) { + config->removeOutput(output->id()); + } + } + + // Add KScreen::Outputs that aren't in the list yet, handle primaryOutput + KScreen::OutputList kscreenOutputs = config->outputs(); + for (const auto &output : m_outputMap) { + KScreen::OutputPtr kscreenOutput = kscreenOutputs[output->id()]; + if (!kscreenOutput) { + kscreenOutput = output->toKScreenOutput(); + kscreenOutputs.insert(kscreenOutput->id(), kscreenOutput); + } else { + output->updateKScreenOutput(kscreenOutput); + } + if (kscreenOutput && m_outputMap.count() == 1) { + kscreenOutput->setPrimary(true); + } else if (m_outputMap.count() > 1) { + // primaryScreen concept doesn't exist in Wayland, so we don't set one + } + } + config->setOutputs(kscreenOutputs); +} + +QMap WraplandInterface::outputMap() const +{ + QMap ret; + + auto it = m_outputMap.constBegin(); + while (it != m_outputMap.constEnd()) { + ret[it.key()] = it.value(); + ++it; + } + return ret; +} + +void WraplandInterface::tryPendingConfig() +{ + if (!m_kscreenPendingConfig) { + return; + } + applyConfig(m_kscreenPendingConfig); + m_kscreenPendingConfig = nullptr; +} + +void WraplandInterface::applyConfig(const KScreen::ConfigPtr &newConfig) +{ + using namespace Wrapland::Client; + + // Create a new configuration object + auto *wlConfig = m_outputManager->createConfiguration(); + wlConfig->setEventQueue(m_queue); + + bool changed = false; + + if (signalsBlocked()) { + /* Last apply still pending, remember new changes and apply afterwards */ + m_kscreenPendingConfig = newConfig; + return; + } + + for (const auto &output : newConfig->outputs()) { + changed |= m_outputMap[output->id()]->setWlConfig(wlConfig, output); + } + + if (!changed) { + return; + } + + // We now block changes in order to compress events while the compositor is doing its thing + // once it's done or failed, we'll trigger configChanged() only once, and not per individual + // property change. + connect(wlConfig, &WlrOutputConfigurationV1::succeeded, this, [this, wlConfig] { + wlConfig->deleteLater(); + unblockSignals(); + Q_EMIT configChanged(); + tryPendingConfig(); + }); + connect(wlConfig, &WlrOutputConfigurationV1::failed, this, [this, wlConfig] { + wlConfig->deleteLater(); + unblockSignals(); + Q_EMIT configChanged(); + tryPendingConfig(); + }); + connect(wlConfig, &WlrOutputConfigurationV1::cancelled, this, [this, wlConfig] { + // This should never be received since we apply the new config directly. But in case we just + // do the same as on failed. + wlConfig->deleteLater(); + unblockSignals(); + Q_EMIT configChanged(); + tryPendingConfig(); + }); + + // Now block signals and ask the compositor to apply the changes. + blockSignals(); + wlConfig->apply(); +} diff --git a/backends/kwayland/plugins/wrapland-wlr/wrapland_output.h b/backends/kwayland/plugins/wrapland-wlr/wrapland_output.h new file mode 100644 --- /dev/null +++ b/backends/kwayland/plugins/wrapland-wlr/wrapland_output.h @@ -0,0 +1,72 @@ +/************************************************************************* +Copyright © 2020 Roman Gilg + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +**************************************************************************/ +#pragma once + +#include "waylandoutput.h" + +#include "output.h" + +#include +#include + +namespace Wrapland +{ +namespace Client +{ +class WlrOutputConfigurationV1; +class WlrOutputHeadV1; +class WlrOutputModeV1; +} +} + +namespace KScreen +{ +class WraplandInterface; + +class WraplandOutput : public WaylandOutput +{ + Q_OBJECT + +public: + explicit WraplandOutput(quint32 id, Wrapland::Client::WlrOutputHeadV1 *head, + WraplandInterface *parent); + ~WraplandOutput() override = default; + + void updateKScreenOutput(KScreen::OutputPtr &output) override; + + QString name() const; + QByteArray edid() const override; + bool enabled() const override; + QRectF geometry() const override; + + Wrapland::Client::WlrOutputHeadV1* outputHead() const; + + bool setWlConfig(Wrapland::Client::WlrOutputConfigurationV1 *wlConfig, + const KScreen::OutputPtr &output); + +private: + void showOutput(); + + Wrapland::Client::WlrOutputHeadV1 *m_head; + Wrapland::Client::Registry *m_registry; + + // left-hand-side: KScreen::Mode, right-hand-side: Wrapland's WlrOutputModeV1 + QMap m_modeIdMap; +}; + +} diff --git a/backends/kwayland/plugins/wrapland-wlr/wrapland_output.cpp b/backends/kwayland/plugins/wrapland-wlr/wrapland_output.cpp new file mode 100644 --- /dev/null +++ b/backends/kwayland/plugins/wrapland-wlr/wrapland_output.cpp @@ -0,0 +1,237 @@ +/************************************************************************* +Copyright © 2020 Roman Gilg + +This library is free software; you can redistribute it and/or +modify it under the terms of the GNU Lesser General Public +License as published by the Free Software Foundation; either +version 2.1 of the License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public +License along with this library; if not, write to the Free Software +Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +**************************************************************************/ +#include "wrapland_output.h" + +#include "wrapland_interface.h" + +#include "waylandbackend.h" +#include "waylandconfig.h" +#include "../utils.h" + +#include +#include + +#include "../../wayland_logging.h" + +#include + +using namespace KScreen; +namespace Wl = Wrapland::Client; + +const QMap +s_rotationMap = { + {Wl::WlrOutputHeadV1::Transform::Normal, Output::None}, + {Wl::WlrOutputHeadV1::Transform::Rotated90, Output::Right}, + {Wl::WlrOutputHeadV1::Transform::Rotated180, Output::Inverted}, + {Wl::WlrOutputHeadV1::Transform::Rotated270, Output::Left}, + {Wl::WlrOutputHeadV1::Transform::Flipped, Output::None}, + {Wl::WlrOutputHeadV1::Transform::Flipped90, Output::Right}, + {Wl::WlrOutputHeadV1::Transform::Flipped180, Output::Inverted}, + {Wl::WlrOutputHeadV1::Transform::Flipped270, Output::Left} +}; + +Output::Rotation toKScreenRotation(const Wl::WlrOutputHeadV1::Transform transform) +{ + auto it = s_rotationMap.constFind(transform); + return it.value(); +} + +Wl::WlrOutputHeadV1::Transform toWraplandTransform(const Output::Rotation rotation) +{ + return s_rotationMap.key(rotation); +} + +WraplandOutput::WraplandOutput(quint32 id, Wrapland::Client::WlrOutputHeadV1 *head, + WraplandInterface *parent) + : WaylandOutput(id, parent) + , m_head(head) +{ + connect(m_head, &Wl::WlrOutputHeadV1::changed, this, &WraplandOutput::changed); + connect(m_head, &Wl::WlrOutputHeadV1::removed, this, &WraplandOutput::removed); + + + auto manager = parent->outputManager(); + connect(manager, &Wl::WlrOutputManagerV1::done, + this, [this, manager]() { + disconnect(manager, &Wl::WlrOutputManagerV1::done, this, nullptr); + Q_EMIT dataReceived(); + } + ); +} + +bool WraplandOutput::enabled() const +{ + return m_head != nullptr; +} + +QByteArray WraplandOutput::edid() const +{ + // wlroots protocol does not provide edid information. + return QByteArray(); +} + +bool portraitMode(Wrapland::Client::WlrOutputHeadV1 *head) +{ + auto transform = head->transform(); + return transform == Wl::WlrOutputHeadV1::Transform::Rotated90 + || transform == Wl::WlrOutputHeadV1::Transform::Rotated270 + || transform == Wl::WlrOutputHeadV1::Transform::Flipped90 + || transform == Wl::WlrOutputHeadV1::Transform::Flipped270; +} + +QRectF WraplandOutput::geometry() const +{ + auto modeSize = m_head->currentMode()->size(); + + // Rotate and scale. + if (portraitMode(m_head)) { + modeSize.transpose(); + } + + modeSize = modeSize / m_head->scale(); + + return QRectF(m_head->position(), modeSize); +} + +Wrapland::Client::WlrOutputHeadV1 *WraplandOutput::outputHead() const +{ + return m_head; +} + +QString modeName(const Wl::WlrOutputModeV1 *mode) +{ + return QString::number(mode->size().width()) + QLatin1Char('x') + + QString::number(mode->size().height()) + QLatin1Char('@') + + QString::number( qRound(mode->refresh() / 1000.0) ); +} + +void WraplandOutput::updateKScreenOutput(OutputPtr &output) +{ + // Initialize primary output + output->setEnabled(m_head->enabled()); + output->setConnected(true); + output->setPrimary(true); // FIXME: wayland doesn't have the concept of a primary display + output->setName(name()); + output->setSizeMm(m_head->physicalSize()); + output->setPos(m_head->position()); + output->setRotation(s_rotationMap[m_head->transform()]); + + ModeList modeList; + QStringList preferredModeIds; + m_modeIdMap.clear(); + QString currentModeId = QStringLiteral("-1"); + + auto currentMode = m_head->currentMode(); + + int modeCounter = 0; + for (auto wlMode : m_head->modes()) { + const auto modeId = QString::number(++modeCounter); + + ModePtr mode(new Mode()); + + mode->setId(modeId); + + // Wrapland gives the refresh rate as int in mHz. + mode->setRefreshRate(wlMode->refresh() / 1000.0); + mode->setSize(wlMode->size()); + mode->setName(modeName(wlMode)); + + if (wlMode->preferred()) { + preferredModeIds << modeId; + } + if (currentMode == wlMode) { + currentModeId = modeId; + } + + // Update the KScreen => Wrapland mode id translation map. + m_modeIdMap.insert(modeId, wlMode); + + // Add to the modelist which gets set on the output. + modeList[modeId] = mode; + } + + if (currentModeId == QLatin1String("-1")) { + qCWarning(KSCREEN_WAYLAND) << "Could not find the current mode id" << modeList; + } + + output->setCurrentModeId(currentModeId); + output->setPreferredModes(preferredModeIds); + output->setModes(modeList); + output->setScale(m_head->scale()); + output->setType(Utils::guessOutputType(m_head->name(), m_head->name())); +} + +bool WraplandOutput::setWlConfig(Wl::WlrOutputConfigurationV1 *wlConfig, + const KScreen::OutputPtr &output) +{ + bool changed = false; + + // enabled? + if (m_head->enabled() != output->isEnabled()) { + changed = true; + } + + // In any case set the enabled state to initialize the output's native handle. + wlConfig->setEnabled(m_head, output->isEnabled()); + + // position + if (m_head->position() != output->pos()) { + changed = true; + wlConfig->setPosition(m_head, output->pos()); + } + + // scale + if (!qFuzzyCompare(m_head->scale(), output->scale())) { + changed = true; + wlConfig->setScale(m_head, output->scale()); + } + + // rotation + if (toKScreenRotation(m_head->transform()) != output->rotation()) { + changed = true; + wlConfig->setTransform(m_head, toWraplandTransform(output->rotation())); + } + + // mode + if (m_modeIdMap.contains(output->currentModeId())) { + auto newMode = m_modeIdMap.value(output->currentModeId(), nullptr); + if (newMode != m_head->currentMode()) { + changed = true; + wlConfig->setMode(m_head, newMode); + } + } else { + qCWarning(KSCREEN_WAYLAND) << "Invalid kscreen mode id:" << output->currentModeId() + << "\n\n" << m_modeIdMap; + } + + return changed; +} + +QString WraplandOutput::name() const +{ + Q_ASSERT(m_head); + return m_head->description(); +} + +QDebug operator<<(QDebug dbg, const WraplandOutput *output) +{ + dbg << "WraplandOutput(Id:" << output->id() <<", Name:" << \ + QString(output->outputHead()->name() + QLatin1Char(' ') + \ + output->outputHead()->description()) << ")"; + return dbg; +}