Files
JSwitchDisplayApplet/graphicsdriverapplet.cpp
Jokul 9e474de717 feat: GPU切换回退保护逻辑
- switchGpu切换前写入备份文件(.gpu-switch-backup)记录之前模式
- init()检测未确认的切换,启动30秒自动回退计时器
- QML弹出'显示是否正常'确认覆盖层,支持手动确认/回退/超时自动回退
- confirmSwitchSuccess()用户确认正常后删除备份
- restoreFromBackup()自动恢复之前模式并发送通知
- primeOffloadEnabled属性检测当前PRIME offload状态
- switchGpu改用environment.d方式,不需要root/pkexec
- 翻译更新:回退确认相关8条新增
2026-07-14 22:15:52 +08:00

817 lines
27 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "graphicsdriverapplet.h"
#include <pluginfactory.h>
#include <QProcess>
#include <QDir>
#include <QDebug>
#include <QFile>
#include <QTextStream>
#include <QRegularExpression>
#include <QFileInfo>
#include <QSettings>
#include <algorithm>
#include <QDateTime>
#include <QDBusInterface>
#include <QDBusMessage>
#include <QVariantList>
DCORE_USE_NAMESPACE
DS_BEGIN_NAMESPACE
GraphicsDriverApplet::GraphicsDriverApplet(QObject *parent)
: DApplet(parent)
, m_ready(false)
, m_lastAlertTime(0)
, m_tempAlertEnabled(true)
, m_switchPending(false)
, m_restoreCountdown(0)
, m_restoreTimer(nullptr)
{
QSettings settings("deepin", "graphics-driver-applet");
m_tempAlertEnabled = settings.value("tempAlertEnabled", true).toBool();
}
GraphicsDriverApplet::~GraphicsDriverApplet()
{
}
bool GraphicsDriverApplet::load()
{
return DApplet::load();
}
bool GraphicsDriverApplet::init()
{
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();
}
QString GraphicsDriverApplet::deviceInfo() const
{
return m_deviceInfo;
}
QString GraphicsDriverApplet::currentDriver() const
{
return m_currentDriver;
}
QString GraphicsDriverApplet::gpuSummary() const
{
return m_gpuSummary;
}
bool GraphicsDriverApplet::ready() const
{
return m_ready;
}
QStringList GraphicsDriverApplet::gpuStats() const
{
return m_gpuStats;
}
QString GraphicsDriverApplet::gpuMode() const
{
return m_gpuMode;
}
QStringList GraphicsDriverApplet::gpuPrimary() const
{
return m_gpuPrimary;
}
QVariantList GraphicsDriverApplet::tempHistory() const
{
QVariantList result;
for (const QList<int> &history : m_tempHistory) {
QVariantList gpuHistory;
for (int temp : history) {
gpuHistory << temp;
}
result << QVariant(gpuHistory);
}
return result;
}
QStringList GraphicsDriverApplet::gpuProcesses() const
{
return m_gpuProcesses;
}
bool GraphicsDriverApplet::tempAlertEnabled() const
{
return m_tempAlertEnabled;
}
void GraphicsDriverApplet::setTempAlertEnabled(bool enabled)
{
if (m_tempAlertEnabled == enabled) return;
m_tempAlertEnabled = enabled;
QSettings settings("deepin", "graphics-driver-applet");
settings.setValue("tempAlertEnabled", enabled);
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()
{
detectGpus();
}
void GraphicsDriverApplet::refreshStats()
{
readGpuStats();
}
void GraphicsDriverApplet::detectGpus()
{
m_gpus.clear();
m_tempHistory.clear();
// 通过 lspci 检测显卡
QStringList lines = runCommand("lspci", {"-mm"});
for (const QString &line : lines) {
if (line.contains("VGA compatible controller", Qt::CaseInsensitive) ||
line.contains("3D controller", Qt::CaseInsensitive) ||
line.contains("Display controller", Qt::CaseInsensitive)) {
GpuInfo gpu;
// 初始化统计字段
gpu.drmCard = -1;
gpu.isPrimary = false;
gpu.temperature = -1;
gpu.gpuUsage = -1;
gpu.memUsage = -1;
gpu.memTotal = -1;
gpu.memUsed = -1;
gpu.gpuClock = -1;
gpu.memClock = -1;
gpu.powerDraw = -1;
gpu.fanSpeed = -1;
// lspci -mm 格式: "Class" "Vendor" "Device" ...
// 提取 PCI ID(行首的 01:00.0 等)
int spaceIdx = line.indexOf(' ');
if (spaceIdx > 0) {
gpu.pciId = line.left(spaceIdx);
}
gpu.name = parseGpuName(line);
// 检测驱动:通过 symlink 目标获取真实驱动名
QString sysPath = "/sys/bus/pci/devices/0000:" + gpu.pciId + "/driver";
QFileInfo driverLink(sysPath);
if (driverLink.exists() && driverLink.isSymLink()) {
QString target = driverLink.symLinkTarget();
// symLinkTarget 返回如 ".../drivers/nvidia",提取最后一段
gpu.driver = target.section('/', -1);
// 读取驱动版本
gpu.driverVersion = readDriverVersion(gpu.driver);
} else {
gpu.driver = "unknown";
}
// 查找 DRM 卡号
gpu.drmCard = findDrmCard(gpu.pciId);
// 读取 boot_vga 判断主副显
QFile bootVgaFile("/sys/bus/pci/devices/0000:" + gpu.pciId + "/boot_vga");
if (bootVgaFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
gpu.isPrimary = QTextStream(&bootVgaFile).readAll().trimmed() == "1";
}
m_gpus.append(gpu);
}
}
// 排序:核显(isPrimary)排在前面
std::stable_sort(m_gpus.begin(), m_gpus.end(), [](const GpuInfo &a, const GpuInfo &b) {
return a.isPrimary > b.isPrimary;
});
// 生成摘要信息
QStringList parts;
QStringList driverParts;
for (const GpuInfo &gpu : m_gpus) {
QString info = gpu.name;
if (!gpu.driver.isEmpty() && gpu.driver != "unknown") {
info += " (" + gpu.driver;
if (!gpu.driverVersion.isEmpty()) {
info += " " + gpu.driverVersion;
}
info += ")";
}
parts << info;
if (!gpu.driver.isEmpty() && !driverParts.contains(gpu.driver)) {
driverParts << gpu.driver;
}
}
m_deviceInfo = parts.join("\n");
m_currentDriver = driverParts.join(", ");
m_gpuSummary = parts.join(" | ");
m_ready = !m_gpus.isEmpty();
// 生成主副显标识
m_gpuPrimary.clear();
for (const GpuInfo &gpu : m_gpus) {
m_gpuPrimary << (gpu.isPrimary ? "true" : "false");
}
qDebug() << "Detected GPUs:" << m_deviceInfo;
qDebug() << "Current drivers:" << m_currentDriver;
emit deviceInfoChanged();
emit currentDriverChanged();
emit gpuSummaryChanged();
emit gpuStatsChanged();
emit gpuPrimaryChanged();
emit readyChanged();
// 读取实时统计信息
readGpuStats();
// 检测 GPU 切换模式
detectGpuMode();
}
QStringList GraphicsDriverApplet::runCommand(const QString &cmd, const QStringList &args)
{
QProcess process;
process.start(cmd, args);
process.waitForFinished(5000);
QString output = QString::fromUtf8(process.readAllStandardOutput());
return output.split('\n', Qt::SkipEmptyParts);
}
QString GraphicsDriverApplet::parseGpuName(const QString &line)
{
// lspci -mm 格式示例:
// 01:00.0 "VGA compatible controller" "NVIDIA Corporation" "GA106M [GeForce RTX 3060 Mobile / Max-Q]" "ASUSTeK Computer Inc." "Device 10a2"
// 提取引号中的内容
QStringList parts;
int pos = 0;
while (pos < line.length()) {
int start = line.indexOf('"', pos);
if (start < 0) break;
int end = line.indexOf('"', start + 1);
if (end < 0) break;
parts << line.mid(start + 1, end - start - 1);
pos = end + 1;
}
if (parts.size() >= 3) {
// parts[0] = class, parts[1] = vendor, parts[2] = device
QString vendor = parts[1];
QString device = parts[2];
// 去掉方括号内的别名,保留主要名称
int bracket = device.indexOf('[');
if (bracket > 0) {
// 保留完整名称,方括号内是子系统信息
}
return vendor + " " + device;
}
return line;
}
QString GraphicsDriverApplet::readDriverVersion(const QString &driverName)
{
// 尝试从 /sys/module/<driver>/version 读取版本
QString path = "/sys/module/" + driverName + "/version";
QFile file(path);
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString version = QTextStream(&file).readAll().trimmed();
if (!version.isEmpty()) {
return version;
}
}
// nvidia 特殊处理:从 /proc/driver/nvidia/version 读取
if (driverName == "nvidia") {
QFile nvFile("/proc/driver/nvidia/version");
if (nvFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString content = QTextStream(&nvFile).readAll();
// 格式: NVRM version: NVIDIA UNIX x86_64 Kernel Module 535.183.01 ...
QRegularExpression re("Kernel Module\\s+(\\d+\\.\\d+\\.\\d+)");
auto match = re.match(content);
if (match.hasMatch()) {
return match.captured(1);
}
}
}
// 开源内核驱动(如 amdgpu, radeon, nouveau, i915)无独立版本号,版本跟随内核
static const QStringList kernelDrivers = {
"amdgpu", "radeon", "nouveau", "i915", "i965", "mgag200", "ast", "vmwgfx"
};
if (kernelDrivers.contains(driverName)) {
QFile osrelease("/proc/sys/kernel/osrelease");
if (osrelease.open(QIODevice::ReadOnly | QIODevice::Text)) {
return "Linux " + QTextStream(&osrelease).readAll().trimmed();
}
}
return QString();
}
void GraphicsDriverApplet::detectGpuMode()
{
// 单 GPU
if (m_gpus.size() <= 1) {
m_gpuMode = "Solo";
emit gpuModeChanged();
return;
}
// 多 GPU:检测 PRIME / Bumblebee
// PRIME: nvidia-drm-outputclass.conf 存在
if (QFile::exists("/usr/share/X11/xorg.conf.d/nvidia-drm-outputclass.conf") ||
QFile::exists("/etc/X11/xorg.conf.d/70-nvidia.conf")) {
m_gpuMode = "PRIME";
emit gpuModeChanged();
return;
}
// Bumblebee: 检查 bumblebeed 服务
QStringList lsmod = runCommand("lsmod", {});
bool bumblebeeLoaded = false;
for (const QString &line : lsmod) {
if (line.contains("bumblebee", Qt::CaseInsensitive)) {
bumblebeeLoaded = true;
break;
}
}
if (bumblebeeLoaded || QFile::exists("/usr/sbin/bumblebeed")) {
m_gpuMode = "Bumblebee";
emit gpuModeChanged();
return;
}
// 多 GPU 但无已知切换方案
m_gpuMode = "Hybrid";
emit gpuModeChanged();
}
int GraphicsDriverApplet::findDrmCard(const QString &pciId)
{
// 扫描 /sys/bus/pci/devices/0000:XX:XX.X/drm/ 目录,找到 cardN 条目
QString drmPath = "/sys/bus/pci/devices/0000:" + pciId + "/drm";
QDir dir(drmPath);
if (!dir.exists()) return -1;
QStringList entries = dir.entryList(QStringList() << "card*", QDir::Dirs | QDir::NoDotAndDotDot);
for (const QString &entry : entries) {
// 匹配 "card0" "card1" 等,排除 "card0-DP-1" 等连接器
QRegularExpression re("^card(\\d+)$");
auto match = re.match(entry);
if (match.hasMatch()) {
return match.captured(1).toInt();
}
}
return -1;
}
void GraphicsDriverApplet::readGpuStats()
{
// 先处理 NVIDIA GPU(单次 nvidia-smi 调用获取所有 NVIDIA GPU 数据)
// nvidia-smi 输出格式: pci.bus_id,temperature.gpu,utilization.gpu,utilization.memory,
// memory.total,memory.used,clocks.gr,clocks.mem,power.draw,fan.speed
QMap<QString, QStringList> nvidiaData; // pciId -> stats
QStringList nvLines = runCommand("nvidia-smi", {
"--query-gpu=pci.bus_id,temperature.gpu,utilization.gpu,utilization.memory,memory.total,memory.used,clocks.gr,clocks.mem,power.draw,fan.speed",
"--format=csv,noheader,nounits"
});
for (const QString &line : nvLines) {
QStringList parts = line.split(",");
if (parts.size() >= 6) {
// nvidia-smi pci.bus_id 格式: 00000000:01:00.0,提取后半部分匹配
QString busId = parts[0].trimmed();
QString shortId = busId.section(':', -2); // "01:00.0"
nvidiaData[shortId] = parts;
}
}
// 逐个 GPU 读取统计
for (GpuInfo &gpu : m_gpus) {
// 每次刷新前重置实时统计字段,确保数据源不可用时不会残留旧值
gpu.temperature = -1;
gpu.gpuUsage = -1;
gpu.memUsage = -1;
gpu.memTotal = -1;
gpu.memUsed = -1;
gpu.gpuClock = -1;
gpu.memClock = -1;
gpu.powerDraw = -1;
gpu.fanSpeed = -1;
if (gpu.driver == "nvidia" && nvidiaData.contains(gpu.pciId)) {
// NVIDIA: 使用 nvidia-smi 数据
QStringList &parts = nvidiaData[gpu.pciId];
gpu.temperature = parts[1].trimmed().toInt();
gpu.gpuUsage = parts[2].trimmed().toInt();
gpu.memUsage = parts[3].trimmed().toInt();
gpu.memTotal = parts[4].trimmed().toInt();
gpu.memUsed = parts[5].trimmed().toInt();
// 频率 (parts[6]=GPU, parts[7]=显存)
if (parts.size() > 6) gpu.gpuClock = parts[6].trimmed().toInt();
if (parts.size() > 7) gpu.memClock = parts[7].trimmed().toInt();
// 功耗 (parts[8])[N/A] 时保持 -1
if (parts.size() > 8) {
QString pwr = parts[8].trimmed();
if (pwr != "[N/A]" && !pwr.isEmpty()) gpu.powerDraw = pwr.toDouble();
}
// 风扇转速 (parts[9])[N/A] 时保持 -1
if (parts.size() > 9) {
QString fan = parts[9].trimmed();
if (fan != "[N/A]" && !fan.isEmpty()) gpu.fanSpeed = fan.toInt();
}
} else if (gpu.drmCard >= 0) {
// AMD / Intel / 其他: 使用 sysfs
QString cardPath = QString("/sys/class/drm/card%1/device").arg(gpu.drmCard);
// GPU 使用率
QFile busyFile(cardPath + "/gpu_busy_percent");
if (busyFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
gpu.gpuUsage = QTextStream(&busyFile).readAll().trimmed().toInt();
}
// 显存 (字节,转换为 MB)
QFile vramTotal(cardPath + "/mem_info_vram_total");
QFile vramUsed(cardPath + "/mem_info_vram_used");
if (vramTotal.open(QIODevice::ReadOnly | QIODevice::Text)) {
gpu.memTotal = QTextStream(&vramTotal).readAll().trimmed().toLongLong() / 1024 / 1024;
}
if (vramUsed.open(QIODevice::ReadOnly | QIODevice::Text)) {
gpu.memUsed = QTextStream(&vramUsed).readAll().trimmed().toLongLong() / 1024 / 1024;
}
if (gpu.memTotal > 0 && gpu.memUsed >= 0) {
gpu.memUsage = gpu.memUsed * 100 / gpu.memTotal;
}
// 温度: 查找 hwmon 子目录
QDir hwmonDir(cardPath + "/hwmon");
if (hwmonDir.exists()) {
QStringList hwmons = hwmonDir.entryList(QStringList() << "hwmon*", QDir::Dirs | QDir::NoDotAndDotDot);
for (const QString &hwmon : hwmons) {
QFile nameFile(hwmonDir.absoluteFilePath(hwmon + "/name"));
if (nameFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString name = QTextStream(&nameFile).readAll().trimmed();
if (name == gpu.driver || name == "amdgpu" || name == "radeon") {
QString hwmonPath = hwmonDir.absoluteFilePath(hwmon);
// 温度
QFile tempFile(hwmonPath + "/temp1_input");
if (tempFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
int tempMilli = QTextStream(&tempFile).readAll().trimmed().toInt();
gpu.temperature = tempMilli / 1000;
}
// 功耗 (µW -> W)
QFile powerFile(hwmonPath + "/power1_average");
if (powerFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qint64 microWatts = QTextStream(&powerFile).readAll().trimmed().toLongLong();
gpu.powerDraw = microWatts / 1000000.0;
}
// 风扇转速 (RPM)
QFile fanFile(hwmonPath + "/fan1_input");
if (fanFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
gpu.fanSpeed = QTextStream(&fanFile).readAll().trimmed().toInt();
}
break;
}
}
}
}
// GPU 频率: Intel i915 使用 gt/gt0/rps_act_freq_mhzAMD 使用 hwmon freq1_input
QString gtPath = QString("/sys/class/drm/card%1/gt/gt0/rps_act_freq_mhz").arg(gpu.drmCard);
QFile gtFreqFile(gtPath);
if (gtFreqFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
gpu.gpuClock = QTextStream(&gtFreqFile).readAll().trimmed().toInt();
}
}
// --- 以下为 Intel 核显等无 hwmon/vram sysfs 接口时的回退逻辑 ---
// 温度回退:扫描 thermal_zone,匹配 Intel GPU 的 ACPI 标识 (B0D4)
// B0D4 是 Intel GPU 在 ACPI 规范中的标准热区标识
if (gpu.temperature < 0) {
QDir thermalDir("/sys/class/thermal");
QStringList zones = thermalDir.entryList(QStringList() << "thermal_zone*", QDir::Dirs | QDir::NoDotAndDotDot);
for (const QString &zone : zones) {
QFile typeFile(thermalDir.absoluteFilePath(zone + "/type"));
if (typeFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString type = QTextStream(&typeFile).readAll().trimmed();
if (type == "B0D4") {
QFile tempFile(thermalDir.absoluteFilePath(zone + "/temp"));
if (tempFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
int tempMilli = QTextStream(&tempFile).readAll().trimmed().toInt();
gpu.temperature = tempMilli / 1000;
}
break;
}
}
}
}
// 显存回退:Intel 核显使用共享系统内存,读取 /proc/meminfo 作为参考
// 注意:这是系统内存使用量,非核显独占,仅作参考
if (gpu.memTotal < 0) {
QFile meminfo("/proc/meminfo");
if (meminfo.open(QIODevice::ReadOnly | QIODevice::Text)) {
QString content = QTextStream(&meminfo).readAll();
QRegularExpression totalRe("MemTotal:\\s*(\\d+)\\s*kB");
QRegularExpression availRe("MemAvailable:\\s*(\\d+)\\s*kB");
auto totalMatch = totalRe.match(content);
auto availMatch = availRe.match(content);
if (totalMatch.hasMatch()) {
gpu.memTotal = totalMatch.captured(1).toLongLong() / 1024; // kB -> MB
}
if (availMatch.hasMatch() && gpu.memTotal > 0) {
qint64 availMB = availMatch.captured(1).toLongLong() / 1024;
gpu.memUsed = gpu.memTotal - availMB;
if (gpu.memUsed >= 0) {
gpu.memUsage = gpu.memUsed * 100 / gpu.memTotal;
}
}
}
}
}
// 生成统计数据 (格式: temp|gpuUsage|memUsage|memUsed|memTotal|gpuClock|memClock|powerDraw|fanSpeed)
m_gpuStats.clear();
for (const GpuInfo &gpu : m_gpus) {
m_gpuStats << QString("%1|%2|%3|%4|%5|%6|%7|%8|%9")
.arg(gpu.temperature)
.arg(gpu.gpuUsage)
.arg(gpu.memUsage)
.arg(gpu.memUsed)
.arg(gpu.memTotal)
.arg(gpu.gpuClock)
.arg(gpu.memClock)
.arg(gpu.powerDraw, 0, 'f', 1)
.arg(gpu.fanSpeed);
}
emit gpuStatsChanged();
// 更新温度历史(保留最近 60 个采样点)
if (m_tempHistory.size() != m_gpus.size()) {
m_tempHistory.resize(m_gpus.size());
}
for (int i = 0; i < m_gpus.size(); ++i) {
m_tempHistory[i].append(m_gpus[i].temperature);
if (m_tempHistory[i].size() > 60) {
m_tempHistory[i].removeFirst();
}
}
emit tempHistoryChanged();
// 温度告警检测
checkTempAlert();
// 读取 NVIDIA 进程列表
readGpuProcesses();
}
// 温度告警:检测是否有 GPU 温度超过 80°C,30 秒内不重复告警
void GraphicsDriverApplet::checkTempAlert()
{
if (!m_tempAlertEnabled) return;
qint64 now = QDateTime::currentSecsSinceEpoch();
if (now - m_lastAlertTime < 30) return;
for (const GpuInfo &gpu : m_gpus) {
if (gpu.temperature >= 80) {
sendNotification(
tr("GPU Temperature Alert"),
tr("%1 temperature reached %2°C, please check cooling")
.arg(gpu.name)
.arg(gpu.temperature)
);
m_lastAlertTime = now;
break;
}
}
}
// 通过 D-Bus 发送桌面通知
void GraphicsDriverApplet::sendNotification(const QString &summary, const QString &body)
{
QDBusInterface notify("org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications",
QDBusConnection::sessionBus());
if (notify.isValid()) {
notify.call("Notify", "dde-shell-graphics-driver", 0u,
"dialog-warning", summary, body,
QStringList(), QVariantMap(), 5000);
}
}
// 读取 NVIDIA GPU 上运行的进程列表
void GraphicsDriverApplet::readGpuProcesses()
{
m_gpuProcesses.clear();
// 仅当存在 NVIDIA GPU 时查询
bool hasNvidia = false;
for (const GpuInfo &gpu : m_gpus) {
if (gpu.driver == "nvidia") { hasNvidia = true; break; }
}
if (!hasNvidia) {
emit gpuProcessesChanged();
return;
}
// nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv,noheader,nounits
QStringList lines = runCommand("nvidia-smi", {
"--query-compute-apps=pid,process_name,used_memory",
"--format=csv,noheader,nounits"
});
for (const QString &line : lines) {
QStringList parts = line.split(",");
if (parts.size() >= 3) {
m_gpuProcesses << QString("%1|%2|%3")
.arg(parts[0].trimmed())
.arg(parts[1].trimmed())
.arg(parts[2].trimmed());
}
}
emit gpuProcessesChanged();
}
// GPU 切换:调用 prime-select 切换 PRIME 模式(需要 polkit 提权)
bool GraphicsDriverApplet::switchGpu(const QString &mode)
{
// mode: "nvidia" / "intel" / "on-demand"
QString configPath = primeConfigPath();
QString backupPath = backupFilePath();
QString configDir = QDir::homePath() + "/.config/environment.d";
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)
DS_END_NAMESPACE
#include "graphicsdriverapplet.moc"