feat: GPU切换回退保护逻辑

- switchGpu切换前写入备份文件(.gpu-switch-backup)记录之前模式
- init()检测未确认的切换,启动30秒自动回退计时器
- QML弹出'显示是否正常'确认覆盖层,支持手动确认/回退/超时自动回退
- confirmSwitchSuccess()用户确认正常后删除备份
- restoreFromBackup()自动恢复之前模式并发送通知
- primeOffloadEnabled属性检测当前PRIME offload状态
- switchGpu改用environment.d方式,不需要root/pkexec
- 翻译更新:回退确认相关8条新增
This commit is contained in:
2026-07-14 22:15:52 +08:00
parent 64408bf842
commit 9e474de717
5 changed files with 349 additions and 11 deletions
+158 -4
View File
@@ -28,6 +28,9 @@ GraphicsDriverApplet::GraphicsDriverApplet(QObject *parent)
, m_ready(false) , m_ready(false)
, m_lastAlertTime(0) , m_lastAlertTime(0)
, m_tempAlertEnabled(true) , m_tempAlertEnabled(true)
, m_switchPending(false)
, m_restoreCountdown(0)
, m_restoreTimer(nullptr)
{ {
QSettings settings("deepin", "graphics-driver-applet"); QSettings settings("deepin", "graphics-driver-applet");
m_tempAlertEnabled = settings.value("tempAlertEnabled", true).toBool(); m_tempAlertEnabled = settings.value("tempAlertEnabled", true).toBool();
@@ -45,6 +48,29 @@ bool GraphicsDriverApplet::load()
bool GraphicsDriverApplet::init() bool GraphicsDriverApplet::init()
{ {
detectGpus(); detectGpus();
// 检查是否有未确认的 GPU 切换(上次切换后可能黑屏)
QFile backup(backupFilePath());
if (backup.exists()) {
// 有备份文件,说明上次切换未确认,启动 30 秒回退计时器
m_switchPending = true;
m_restoreCountdown = 30;
emit switchPendingChanged();
emit restoreCountdownChanged();
m_restoreTimer = new QTimer(this);
connect(m_restoreTimer, &QTimer::timeout, this, [this]() {
m_restoreCountdown--;
emit restoreCountdownChanged();
if (m_restoreCountdown <= 0) {
restoreFromBackup();
}
});
m_restoreTimer->start(1000);
qDebug() << "Unconfirmed GPU switch detected, auto-restore in 30s";
}
return DApplet::init(); return DApplet::init();
} }
@@ -115,6 +141,35 @@ void GraphicsDriverApplet::setTempAlertEnabled(bool enabled)
emit tempAlertEnabledChanged(); emit tempAlertEnabledChanged();
} }
bool GraphicsDriverApplet::primeOffloadEnabled() const
{
QString configPath = QDir::homePath() + "/.config/environment.d/nvidia-prime.conf";
QFile file(configPath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return false;
QString content = QTextStream(&file).readAll();
return content.contains("__NV_PRIME_RENDER_OFFLOAD=1");
}
bool GraphicsDriverApplet::switchPending() const
{
return m_switchPending;
}
int GraphicsDriverApplet::restoreCountdown() const
{
return m_restoreCountdown;
}
QString GraphicsDriverApplet::primeConfigPath()
{
return QDir::homePath() + "/.config/environment.d/nvidia-prime.conf";
}
QString GraphicsDriverApplet::backupFilePath()
{
return QDir::homePath() + "/.config/environment.d/.gpu-switch-backup";
}
void GraphicsDriverApplet::refreshDeviceInfo() void GraphicsDriverApplet::refreshDeviceInfo()
{ {
detectGpus(); detectGpus();
@@ -649,10 +704,109 @@ void GraphicsDriverApplet::readGpuProcesses()
bool GraphicsDriverApplet::switchGpu(const QString &mode) bool GraphicsDriverApplet::switchGpu(const QString &mode)
{ {
// mode: "nvidia" / "intel" / "on-demand" // mode: "nvidia" / "intel" / "on-demand"
QProcess process; QString configPath = primeConfigPath();
process.start("pkexec", {"prime-select", "set", mode}); QString backupPath = backupFilePath();
process.waitForFinished(30000); QString configDir = QDir::homePath() + "/.config/environment.d";
return process.exitCode() == 0; QDir().mkpath(configDir);
// 保存当前模式到备份文件(用于回退)
QString prevMode = primeOffloadEnabled() ? "nvidia" : "on-demand";
QFile backupFile(backupPath);
if (backupFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream(&backupFile) << prevMode;
backupFile.close();
}
if (mode == "intel") {
QFile::remove(configPath);
} else if (mode == "nvidia") {
QFile file(configPath);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream(&file) << "__NV_PRIME_RENDER_OFFLOAD=1\n"
<< "__GLX_VENDOR_LIBRARY_NAME=nvidia\n";
file.close();
}
} else if (mode == "on-demand") {
QFile::remove(configPath);
}
sendNotification(tr("GPU Mode Switched"),
tr("Switched to %1 mode. Please re-login to take effect.").arg(mode));
detectGpuMode();
emit primeOffloadEnabledChanged();
return true;
}
void GraphicsDriverApplet::confirmSwitchSuccess()
{
// 用户确认显示正常,删除备份文件
QFile::remove(backupFilePath());
m_switchPending = false;
m_restoreCountdown = 0;
emit switchPendingChanged();
emit restoreCountdownChanged();
if (m_restoreTimer) {
m_restoreTimer->stop();
m_restoreTimer->deleteLater();
m_restoreTimer = nullptr;
}
qDebug() << "GPU switch confirmed by user";
}
void GraphicsDriverApplet::restoreSwitch()
{
// 用户手动请求回退
restoreFromBackup();
}
void GraphicsDriverApplet::restoreFromBackup()
{
// 从备份文件恢复
QFile backupFile(backupFilePath());
if (!backupFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
m_switchPending = false;
emit switchPendingChanged();
return;
}
QString prevMode = QTextStream(&backupFile).readAll().trimmed();
backupFile.close();
QFile::remove(backupFilePath());
QString configPath = primeConfigPath();
if (prevMode == "nvidia") {
QFile file(configPath);
if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
QTextStream(&file) << "__NV_PRIME_RENDER_OFFLOAD=1\n"
<< "__GLX_VENDOR_LIBRARY_NAME=nvidia\n";
file.close();
}
} else {
QFile::remove(configPath);
}
m_switchPending = false;
m_restoreCountdown = 0;
emit switchPendingChanged();
emit restoreCountdownChanged();
emit primeOffloadEnabledChanged();
if (m_restoreTimer) {
m_restoreTimer->stop();
m_restoreTimer->deleteLater();
m_restoreTimer = nullptr;
}
sendNotification(tr("GPU Mode Restored"),
tr("Reverted to %1 mode. Please re-login to take effect.").arg(prevMode));
qDebug() << "GPU mode restored to" << prevMode;
} }
D_APPLET_CLASS(GraphicsDriverApplet) D_APPLET_CLASS(GraphicsDriverApplet)
+17
View File
@@ -47,6 +47,9 @@ class GraphicsDriverApplet : public DApplet
Q_PROPERTY(QVariantList tempHistory READ tempHistory NOTIFY tempHistoryChanged) Q_PROPERTY(QVariantList tempHistory READ tempHistory NOTIFY tempHistoryChanged)
Q_PROPERTY(QStringList gpuProcesses READ gpuProcesses NOTIFY gpuProcessesChanged) Q_PROPERTY(QStringList gpuProcesses READ gpuProcesses NOTIFY gpuProcessesChanged)
Q_PROPERTY(bool tempAlertEnabled READ tempAlertEnabled WRITE setTempAlertEnabled NOTIFY tempAlertEnabledChanged) Q_PROPERTY(bool tempAlertEnabled READ tempAlertEnabled WRITE setTempAlertEnabled NOTIFY tempAlertEnabledChanged)
Q_PROPERTY(bool primeOffloadEnabled READ primeOffloadEnabled NOTIFY primeOffloadEnabledChanged)
Q_PROPERTY(bool switchPending READ switchPending NOTIFY switchPendingChanged)
Q_PROPERTY(int restoreCountdown READ restoreCountdown NOTIFY restoreCountdownChanged)
public: public:
explicit GraphicsDriverApplet(QObject *parent = nullptr); explicit GraphicsDriverApplet(QObject *parent = nullptr);
@@ -66,10 +69,15 @@ public:
QStringList gpuProcesses() const; QStringList gpuProcesses() const;
bool tempAlertEnabled() const; bool tempAlertEnabled() const;
void setTempAlertEnabled(bool enabled); void setTempAlertEnabled(bool enabled);
bool primeOffloadEnabled() const;
bool switchPending() const;
int restoreCountdown() const;
Q_INVOKABLE void refreshDeviceInfo(); Q_INVOKABLE void refreshDeviceInfo();
Q_INVOKABLE void refreshStats(); Q_INVOKABLE void refreshStats();
Q_INVOKABLE bool switchGpu(const QString &mode); Q_INVOKABLE bool switchGpu(const QString &mode);
Q_INVOKABLE void confirmSwitchSuccess();
Q_INVOKABLE void restoreSwitch();
signals: signals:
void deviceInfoChanged(); void deviceInfoChanged();
@@ -82,6 +90,9 @@ signals:
void tempHistoryChanged(); void tempHistoryChanged();
void gpuProcessesChanged(); void gpuProcessesChanged();
void tempAlertEnabledChanged(); void tempAlertEnabledChanged();
void primeOffloadEnabledChanged();
void switchPendingChanged();
void restoreCountdownChanged();
private: private:
void detectGpus(); void detectGpus();
@@ -90,6 +101,9 @@ private:
void detectGpuMode(); void detectGpuMode();
void checkTempAlert(); void checkTempAlert();
void sendNotification(const QString &summary, const QString &body); void sendNotification(const QString &summary, const QString &body);
void restoreFromBackup();
static QString primeConfigPath();
static QString backupFilePath();
static int findDrmCard(const QString &pciId); static int findDrmCard(const QString &pciId);
static QStringList runCommand(const QString &cmd, const QStringList &args); static QStringList runCommand(const QString &cmd, const QStringList &args);
static QString parseGpuName(const QString &line); static QString parseGpuName(const QString &line);
@@ -110,6 +124,9 @@ private:
// 温度告警冷却时间戳 // 温度告警冷却时间戳
qint64 m_lastAlertTime; qint64 m_lastAlertTime;
bool m_tempAlertEnabled; bool m_tempAlertEnabled;
bool m_switchPending;
int m_restoreCountdown;
QTimer *m_restoreTimer;
}; };
DS_END_NAMESPACE DS_END_NAMESPACE
+94 -7
View File
@@ -28,13 +28,8 @@ AppletItem {
// 当前 PRIME 模式(nvidia/on-demand/intel // 当前 PRIME 模式(nvidia/on-demand/intel
readonly property string currentPrimeMode: { readonly property string currentPrimeMode: {
if (root.gpuMode !== "PRIME" || !root.applet || !root.applet.gpuPrimary) return "" if (root.gpuMode !== "PRIME") return ""
var lines = root.deviceInfo.split("\n").filter(function(l) { return l.trim().length > 0 }) if (root.applet && root.applet.primeOffloadEnabled) return "nvidia"
for (var i = 0; i < lines.length; i++) {
if (/nvidia/i.test(lines[i]) && root.applet.gpuPrimary[i] === "true") {
return "nvidia"
}
}
return "on-demand" return "on-demand"
} }
@@ -1071,6 +1066,98 @@ AppletItem {
} }
} }
// GPU 切换回退确认覆盖层(启动时检测到未确认的切换)
Rectangle {
id: restoreOverlay
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.75)
radius: 12
visible: root.applet && root.applet.switchPending
z: 100
ColumnLayout {
anchors.centerIn: parent
width: parent.width - 48
spacing: 16
// 警告图标
Text {
Layout.alignment: Qt.AlignHCenter
text: "⚠"
font.pixelSize: 36
color: Qt.rgba(245/255, 158/255, 11/255, 1)
}
// 标题
Text {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Display OK?")
font.pixelSize: 18
font.bold: true
color: "#e8e8e8"
}
// 说明
Text {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
text: qsTr("GPU mode was changed. If display is abnormal, it will auto-revert in %1s.").arg(root.applet ? root.applet.restoreCountdown : 0)
font.pixelSize: 12
color: "#a0a0a0"
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
// 按钮行
RowLayout {
Layout.fillWidth: true
spacing: 12
// 回退按钮
Button {
Layout.fillWidth: true
Layout.preferredHeight: 34
font.pixelSize: 12
background: Rectangle {
color: parent.hovered ? Qt.rgba(220/255, 38/255, 38/255, 0.8) : Qt.rgba(220/255, 38/255, 38/255, 0.3)
radius: 8
border.width: 1
border.color: Qt.rgba(220/255, 38/255, 38/255, 0.6)
}
contentItem: Text {
text: qsTr("Revert")
color: "#ff6b6b"
font.pixelSize: 12
font.bold: true
horizontalAlignment: Text.AlignHCenter
}
onClicked: if (root.applet) root.applet.restoreSwitch()
}
// 确认正常按钮
Button {
Layout.fillWidth: true
Layout.preferredHeight: 34
font.pixelSize: 12
background: Rectangle {
color: parent.hovered ? Qt.darker(root.accentBlue, 1.2) : root.accentBlue
radius: 8
}
contentItem: Text {
text: qsTr("Display OK")
color: "white"
font.pixelSize: 12
font.bold: true
horizontalAlignment: Text.AlignHCenter
}
onClicked: if (root.applet) root.applet.confirmSwitchSuccess()
}
}
}
}
Component.onCompleted: { Component.onCompleted: {
DockPanelPositioner.bounding = Qt.binding(function () { DockPanelPositioner.bounding = Qt.binding(function () {
const point = root.mapToItem(null, root.width / 2, root.height / 2) const point = root.mapToItem(null, root.width / 2, root.height / 2)
@@ -193,5 +193,45 @@
<source>Integrated</source> <source>Integrated</source>
<translation></translation> <translation></translation>
</message> </message>
<message>
<location filename="../package/driverview.qml"/>
<source>Display OK?</source>
<translation></translation>
</message>
<message>
<location filename="../package/driverview.qml"/>
<source>GPU mode was changed. If display is abnormal, it will auto-revert in %1s.</source>
<translation>GPU %1 退</translation>
</message>
<message>
<location filename="../package/driverview.qml"/>
<source>Revert</source>
<translation>退</translation>
</message>
<message>
<location filename="../package/driverview.qml"/>
<source>Display OK</source>
<translation></translation>
</message>
<message>
<location filename="../package/driverview.qml"/>
<source>GPU Mode Switched</source>
<translation>GPU </translation>
</message>
<message>
<location filename="../package/driverview.qml"/>
<source>Switched to %1 mode. Please re-login to take effect.</source>
<translation> %1 </translation>
</message>
<message>
<location filename="../package/driverview.qml"/>
<source>GPU Mode Restored</source>
<translation>GPU </translation>
</message>
<message>
<location filename="../package/driverview.qml"/>
<source>Reverted to %1 mode. Please re-login to take effect.</source>
<translation> %1 </translation>
</message>
</context> </context>
</TS> </TS>
@@ -193,5 +193,45 @@
<source>Integrated</source> <source>Integrated</source>
<translation></translation> <translation></translation>
</message> </message>
<message>
<location filename="../package/driverview.qml"/>
<source>Display OK?</source>
<translation></translation>
</message>
<message>
<location filename="../package/driverview.qml"/>
<source>GPU mode was changed. If display is abnormal, it will auto-revert in %1s.</source>
<translation>GPU %1 退</translation>
</message>
<message>
<location filename="../package/driverview.qml"/>
<source>Revert</source>
<translation>退</translation>
</message>
<message>
<location filename="../package/driverview.qml"/>
<source>Display OK</source>
<translation></translation>
</message>
<message>
<location filename="../package/driverview.qml"/>
<source>GPU Mode Switched</source>
<translation>GPU </translation>
</message>
<message>
<location filename="../package/driverview.qml"/>
<source>Switched to %1 mode. Please re-login to take effect.</source>
<translation> %1 </translation>
</message>
<message>
<location filename="../package/driverview.qml"/>
<source>GPU Mode Restored</source>
<translation>GPU </translation>
</message>
<message>
<location filename="../package/driverview.qml"/>
<source>Reverted to %1 mode. Please re-login to take effect.</source>
<translation> %1 </translation>
</message>
</context> </context>
</TS> </TS>