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.
This commit is contained in:
Marco Allegretti 2026-04-21 09:08:03 +02:00
parent bcb985c652
commit 62243b7f64
8 changed files with 353 additions and 5 deletions

View file

@ -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

View file

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

View file

@ -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;
};

View file

@ -0,0 +1,64 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
#include "gamemodecontrol.h"
#include <QDBusConnection>
#include <QDBusReply>
#include <QDebug>
#include <unistd.h> // 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<int> reply = m_iface->call(QStringLiteral("RegisterGame"), static_cast<int>(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<int> reply = m_iface->call(QStringLiteral("UnregisterGame"), static_cast<int>(getpid()));
if (reply.isValid() && reply.value() == 0) {
m_active = false;
Q_EMIT activeChanged();
} else {
qWarning() << "GameModeControl: UnregisterGame failed:" << reply.error().message();
}
}

View file

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
#pragma once
#include <QDBusInterface>
#include <QObject>
#include <qqmlregistration.h>
/**
* 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;
};

View file

@ -0,0 +1,127 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
#include "powerprofilecontrol.h"
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusReply>
#include <QDBusVariant>
#include <QDebug>
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<QDBusVariant> 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<QDBusVariant> profilesReply = propIface.call(QStringLiteral("Get"), s_iface, QStringLiteral("Profiles"));
if (profilesReply.isValid()) {
QStringList profiles;
const QVariant profilesVariant = profilesReply.value().variant();
if (profilesVariant.canConvert<QVariantList>()) {
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<QDBusVariant>()) {
value = value.value<QDBusVariant>().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<QDBusVariant>()) {
value = value.value<QDBusVariant>().variant();
}
const QString profile = value.toString();
if (profile != m_activeProfile) {
m_activeProfile = profile;
Q_EMIT activeProfileChanged();
}
}
if (changed.contains(QStringLiteral("Profiles"))) {
fetchState();
}
}

View file

@ -0,0 +1,44 @@
// SPDX-FileCopyrightText: 2026 Marco Allegretti
// SPDX-License-Identifier: EUPL-1.2
#pragma once
#include <QDBusInterface>
#include <QObject>
#include <QStringList>
#include <qqmlregistration.h>
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;
};

View file

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