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();