From 62243b7f6484e2bb63f04cc38e4303c28f64b371 Mon Sep 17 00:00:00 2001 From: Marco Allegretti Date: Tue, 21 Apr 2026 09:08:03 +0200 Subject: [PATCH] Add gaming session control backends Add PowerProfileControl and GameModeControl singletons and wire them into gaming mode lifecycle handling. When gaming mode turns on, keep DND on, switch to the performance profile when available, and request GameMode. Restore previous state when gaming mode turns off. Add an overlayEnabled property in GameLauncherProvider so the launcher can toggle MangoHud environment variables from QML. --- components/gamingshellplugin/CMakeLists.txt | 5 +- .../gamelauncherprovider.cpp | 23 ++++ .../gamingshellplugin/gamelauncherprovider.h | 5 + .../gamingshellplugin/gamemodecontrol.cpp | 64 +++++++++ .../gamingshellplugin/gamemodecontrol.h | 46 +++++++ .../gamingshellplugin/powerprofilecontrol.cpp | 127 ++++++++++++++++++ .../gamingshellplugin/powerprofilecontrol.h | 44 ++++++ containments/homescreens/folio/qml/main.qml | 44 +++++- 8 files changed, 353 insertions(+), 5 deletions(-) create mode 100644 components/gamingshellplugin/gamemodecontrol.cpp create mode 100644 components/gamingshellplugin/gamemodecontrol.h create mode 100644 components/gamingshellplugin/powerprofilecontrol.cpp create mode 100644 components/gamingshellplugin/powerprofilecontrol.h diff --git a/components/gamingshellplugin/CMakeLists.txt b/components/gamingshellplugin/CMakeLists.txt index 97d75dc8..d9846ad6 100644 --- a/components/gamingshellplugin/CMakeLists.txt +++ b/components/gamingshellplugin/CMakeLists.txt @@ -2,7 +2,7 @@ # SPDX-License-Identifier: EUPL-1.2 find_package(SDL3 REQUIRED) -find_package(Qt6 REQUIRED COMPONENTS Sql) +find_package(Qt6 REQUIRED COMPONENTS Sql DBus) ecm_add_qml_module(gamingshellplugin URI org.kde.plasma.private.mobileshell.gamingshellplugin GENERATE_PLUGIN_SOURCE) @@ -10,10 +10,13 @@ target_sources(gamingshellplugin PRIVATE gamepadmanager.cpp gamepaddevice.cpp gamelauncherprovider.cpp + powerprofilecontrol.cpp + gamemodecontrol.cpp ) target_link_libraries(gamingshellplugin PRIVATE Qt::Core + Qt::DBus Qt::Qml Qt::Quick Qt::Sql diff --git a/components/gamingshellplugin/gamelauncherprovider.cpp b/components/gamingshellplugin/gamelauncherprovider.cpp index 91915600..5aede406 100644 --- a/components/gamingshellplugin/gamelauncherprovider.cpp +++ b/components/gamingshellplugin/gamelauncherprovider.cpp @@ -533,6 +533,29 @@ void GameLauncherProvider::setSourceFilter(const QString &source) applyFilter(); } +bool GameLauncherProvider::overlayEnabled() const +{ + return m_overlayEnabled; +} + +void GameLauncherProvider::setOverlayEnabled(bool enabled) +{ + if (m_overlayEnabled == enabled) { + return; + } + m_overlayEnabled = enabled; + Q_EMIT overlayEnabledChanged(); + + // Set/unset MangoHud environment variables for child processes + if (enabled) { + qputenv("MANGOHUD", "1"); + qputenv("MANGOHUD_DLSYM", "1"); + } else { + qunsetenv("MANGOHUD"); + qunsetenv("MANGOHUD_DLSYM"); + } +} + void GameLauncherProvider::applyFilter() { beginResetModel(); diff --git a/components/gamingshellplugin/gamelauncherprovider.h b/components/gamingshellplugin/gamelauncherprovider.h index 56c76626..62a95463 100644 --- a/components/gamingshellplugin/gamelauncherprovider.h +++ b/components/gamingshellplugin/gamelauncherprovider.h @@ -21,6 +21,7 @@ class GameLauncherProvider : public QAbstractListModel Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged) Q_PROPERTY(QString filterString READ filterString WRITE setFilterString NOTIFY filterStringChanged) Q_PROPERTY(QString sourceFilter READ sourceFilter WRITE setSourceFilter NOTIFY sourceFilterChanged) + Q_PROPERTY(bool overlayEnabled READ overlayEnabled WRITE setOverlayEnabled NOTIFY overlayEnabledChanged) public: explicit GameLauncherProvider(QObject *parent = nullptr); @@ -46,6 +47,8 @@ public: void setFilterString(const QString &filter); QString sourceFilter() const; void setSourceFilter(const QString &source); + bool overlayEnabled() const; + void setOverlayEnabled(bool enabled); Q_INVOKABLE void refresh(); Q_INVOKABLE void launch(int index); @@ -57,6 +60,7 @@ Q_SIGNALS: void loadingChanged(); void filterStringChanged(); void sourceFilterChanged(); + void overlayEnabledChanged(); void gameLaunched(const QString &name); private: @@ -88,4 +92,5 @@ private: QString m_sourceFilter; // empty = all, or "desktop"/"steam"/"flatpak" KSharedConfigPtr m_config; bool m_loading = false; + bool m_overlayEnabled = false; }; diff --git a/components/gamingshellplugin/gamemodecontrol.cpp b/components/gamingshellplugin/gamemodecontrol.cpp new file mode 100644 index 00000000..20778cf2 --- /dev/null +++ b/components/gamingshellplugin/gamemodecontrol.cpp @@ -0,0 +1,64 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +#include "gamemodecontrol.h" + +#include +#include +#include + +#include // getpid() + +static const QString s_service = QStringLiteral("com.feralinteractive.GameMode"); +static const QString s_path = QStringLiteral("/com/feralinteractive/GameMode"); +static const QString s_iface = QStringLiteral("com.feralinteractive.GameMode"); + +GameModeControl::GameModeControl(QObject *parent) + : QObject(parent) + , m_iface(new QDBusInterface(s_service, s_path, s_iface, QDBusConnection::sessionBus(), this)) +{ + m_available = m_iface->isValid(); + if (!m_available) { + qDebug() << "GameModeControl: Feral GameMode not available"; + } +} + +bool GameModeControl::available() const +{ + return m_available; +} + +bool GameModeControl::active() const +{ + return m_active; +} + +void GameModeControl::requestStart() +{ + if (!m_available || m_active) { + return; + } + + QDBusReply reply = m_iface->call(QStringLiteral("RegisterGame"), static_cast(getpid())); + if (reply.isValid() && reply.value() == 0) { + m_active = true; + Q_EMIT activeChanged(); + } else { + qWarning() << "GameModeControl: RegisterGame failed:" << reply.error().message(); + } +} + +void GameModeControl::requestEnd() +{ + if (!m_available || !m_active) { + return; + } + + QDBusReply reply = m_iface->call(QStringLiteral("UnregisterGame"), static_cast(getpid())); + if (reply.isValid() && reply.value() == 0) { + m_active = false; + Q_EMIT activeChanged(); + } else { + qWarning() << "GameModeControl: UnregisterGame failed:" << reply.error().message(); + } +} diff --git a/components/gamingshellplugin/gamemodecontrol.h b/components/gamingshellplugin/gamemodecontrol.h new file mode 100644 index 00000000..ff17c716 --- /dev/null +++ b/components/gamingshellplugin/gamemodecontrol.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +#pragma once + +#include +#include +#include + +/** + * D-Bus client for Feral GameMode (com.feralinteractive.GameMode). + * + * Calling requestStart() tells the daemon to apply performance + * optimizations (CPU governor, I/O priority, GPU perf mode, etc.) + * for the calling process. requestEnd() reverses them. + * + * GameMode is optional — if the daemon is not installed the calls + * are silently ignored. + */ +class GameModeControl : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(bool available READ available NOTIFY availableChanged) + Q_PROPERTY(bool active READ active NOTIFY activeChanged) + +public: + explicit GameModeControl(QObject *parent = nullptr); + + bool available() const; + bool active() const; + + Q_INVOKABLE void requestStart(); + Q_INVOKABLE void requestEnd(); + +Q_SIGNALS: + void availableChanged(); + void activeChanged(); + +private: + QDBusInterface *m_iface = nullptr; + bool m_available = false; + bool m_active = false; +}; diff --git a/components/gamingshellplugin/powerprofilecontrol.cpp b/components/gamingshellplugin/powerprofilecontrol.cpp new file mode 100644 index 00000000..dc42cdb0 --- /dev/null +++ b/components/gamingshellplugin/powerprofilecontrol.cpp @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +#include "powerprofilecontrol.h" + +#include +#include +#include +#include +#include + +static const QString s_service = QStringLiteral("net.hadess.PowerProfiles"); +static const QString s_path = QStringLiteral("/net/hadess/PowerProfiles"); +static const QString s_iface = QStringLiteral("net.hadess.PowerProfiles"); +static const QString s_propIface = QStringLiteral("org.freedesktop.DBus.Properties"); + +PowerProfileControl::PowerProfileControl(QObject *parent) + : QObject(parent) + , m_iface(new QDBusInterface(s_service, s_path, s_iface, QDBusConnection::systemBus(), this)) +{ + if (m_iface->isValid()) { + m_available = true; + fetchState(); + + // Subscribe to property changes + QDBusConnection::systemBus() + .connect(s_service, s_path, s_propIface, QStringLiteral("PropertiesChanged"), this, SLOT(onPropertiesChanged(QString, QVariantMap, QStringList))); + } else { + qDebug() << "PowerProfileControl: power-profiles-daemon not available"; + } +} + +QString PowerProfileControl::activeProfile() const +{ + return m_activeProfile; +} + +void PowerProfileControl::setActiveProfile(const QString &profile) +{ + if (!m_available || profile == m_activeProfile) { + return; + } + if (!m_profiles.contains(profile)) { + return; + } + + // Write via org.freedesktop.DBus.Properties.Set + QDBusInterface propIface(s_service, s_path, s_propIface, QDBusConnection::systemBus()); + propIface.call(QStringLiteral("Set"), s_iface, QStringLiteral("ActiveProfile"), QVariant::fromValue(QDBusVariant(profile))); +} + +QStringList PowerProfileControl::profiles() const +{ + return m_profiles; +} + +bool PowerProfileControl::available() const +{ + return m_available; +} + +void PowerProfileControl::fetchState() +{ + // Read ActiveProfile + QDBusInterface propIface(s_service, s_path, s_propIface, QDBusConnection::systemBus()); + + QDBusReply profileReply = propIface.call(QStringLiteral("Get"), s_iface, QStringLiteral("ActiveProfile")); + if (profileReply.isValid()) { + const QString profile = profileReply.value().variant().toString(); + if (profile != m_activeProfile) { + m_activeProfile = profile; + Q_EMIT activeProfileChanged(); + } + } + + // Read Profiles — array of dicts, each with a "Profile" key + QDBusReply profilesReply = propIface.call(QStringLiteral("Get"), s_iface, QStringLiteral("Profiles")); + if (profilesReply.isValid()) { + QStringList profiles; + const QVariant profilesVariant = profilesReply.value().variant(); + if (profilesVariant.canConvert()) { + const QVariantList list = profilesVariant.toList(); + for (const QVariant &item : list) { + const QVariantMap map = item.toMap(); + QString profileName; + if (map.contains(QStringLiteral("Profile"))) { + QVariant value = map.value(QStringLiteral("Profile")); + if (value.canConvert()) { + value = value.value().variant(); + } + profileName = value.toString(); + } + if (!profileName.isEmpty()) { + profiles.append(profileName); + } + } + } + if (profiles != m_profiles) { + m_profiles = profiles; + Q_EMIT profilesChanged(); + } + } +} + +void PowerProfileControl::onPropertiesChanged(const QString &interface, const QVariantMap &changed, const QStringList &invalidated) +{ + Q_UNUSED(invalidated) + if (interface != s_iface) { + return; + } + + if (changed.contains(QStringLiteral("ActiveProfile"))) { + QVariant value = changed.value(QStringLiteral("ActiveProfile")); + if (value.canConvert()) { + value = value.value().variant(); + } + const QString profile = value.toString(); + if (profile != m_activeProfile) { + m_activeProfile = profile; + Q_EMIT activeProfileChanged(); + } + } + + if (changed.contains(QStringLiteral("Profiles"))) { + fetchState(); + } +} diff --git a/components/gamingshellplugin/powerprofilecontrol.h b/components/gamingshellplugin/powerprofilecontrol.h new file mode 100644 index 00000000..42b56c0d --- /dev/null +++ b/components/gamingshellplugin/powerprofilecontrol.h @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2026 Marco Allegretti +// SPDX-License-Identifier: EUPL-1.2 + +#pragma once + +#include +#include +#include +#include + +class PowerProfileControl : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + Q_PROPERTY(QString activeProfile READ activeProfile WRITE setActiveProfile NOTIFY activeProfileChanged) + Q_PROPERTY(QStringList profiles READ profiles NOTIFY profilesChanged) + Q_PROPERTY(bool available READ available NOTIFY availableChanged) + +public: + explicit PowerProfileControl(QObject *parent = nullptr); + + QString activeProfile() const; + void setActiveProfile(const QString &profile); + QStringList profiles() const; + bool available() const; + +Q_SIGNALS: + void activeProfileChanged(); + void profilesChanged(); + void availableChanged(); + +private Q_SLOTS: + void onPropertiesChanged(const QString &interface, const QVariantMap &changed, const QStringList &invalidated); + +private: + void fetchState(); + + QDBusInterface *m_iface = nullptr; + QString m_activeProfile; + QStringList m_profiles; + bool m_available = false; +}; diff --git a/containments/homescreens/folio/qml/main.qml b/containments/homescreens/folio/qml/main.qml index 2e0744f9..cfa4b45f 100644 --- a/containments/homescreens/folio/qml/main.qml +++ b/containments/homescreens/folio/qml/main.qml @@ -40,6 +40,44 @@ ContainmentItem { property bool gameCenterOpen: ShellSettings.Settings.gamingModeEnabled property bool showGameCenterHint: false + // State saved when gaming mode activates, restored when it deactivates + property string _savedPowerProfile: "" + property bool _savedDnd: false + property bool _gamingSessionActive: false + + function _applyGamingModeState(enabled) { + root.gameCenterOpen = enabled + GamingShell.GamepadManager.active = enabled + + if (enabled === root._gamingSessionActive) { + return + } + + if (enabled) { + // Save current state and apply gaming optimizations + root._savedDnd = MobileShellState.ShellDBusClient.doNotDisturb + MobileShellState.ShellDBusClient.doNotDisturb = true + + if (GamingShell.PowerProfileControl.available) { + root._savedPowerProfile = GamingShell.PowerProfileControl.activeProfile + GamingShell.PowerProfileControl.activeProfile = "performance" + } + + GamingShell.GameModeControl.requestStart() + root._gamingSessionActive = true + } else { + // Restore previous state + MobileShellState.ShellDBusClient.doNotDisturb = root._savedDnd + + if (GamingShell.PowerProfileControl.available && root._savedPowerProfile.length > 0) { + GamingShell.PowerProfileControl.activeProfile = root._savedPowerProfile + } + + GamingShell.GameModeControl.requestEnd() + root._gamingSessionActive = false + } + } + Timer { id: gameCenterHintTimer interval: 2600 @@ -49,8 +87,7 @@ ContainmentItem { Connections { target: ShellSettings.Settings function onGamingModeEnabledChanged() { - root.gameCenterOpen = ShellSettings.Settings.gamingModeEnabled - GamingShell.GamepadManager.active = ShellSettings.Settings.gamingModeEnabled + root._applyGamingModeState(ShellSettings.Settings.gamingModeEnabled) } } @@ -66,8 +103,7 @@ ContainmentItem { } Component.onCompleted: { - root.gameCenterOpen = ShellSettings.Settings.gamingModeEnabled - GamingShell.GamepadManager.active = ShellSettings.Settings.gamingModeEnabled + root._applyGamingModeState(ShellSettings.Settings.gamingModeEnabled) folio.FolioSettings.load(); folio.FavouritesModel.load(); folio.PageListModel.load();