Files
JSwitchDisplayApplet/graphicsdriverapplet.cpp
T
Jokul f7c6801793 feat: 设置窗口、合并温度趋势图、UI优化
- 新增设置窗口(右键打开):无边框毛玻璃风格,温度告警开关,QSettings持久化
- 合并温度趋势图:多GPU多色折线,图例,Y轴刻度,填充区域,最新温度标注
- 显卡排序:核显在前独显在后(std::stable_sort by boot_vga)
- 卡片优化:删除温度行和频率/功耗行,驱动名+版本两端对齐,高度压缩至78
- 弹窗优化:宽度480,右上角刷新图标替代底部按钮
- tooltip修复:删除自定义Rectangle tooltip改用标准ToolTip
- 翻译更新:设置/常规设置/温度告警/温度趋势等
2026-07-14 21:31:00 +08:00

663 lines
22 KiB
C++
Raw 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)
{
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();
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();
}
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"
QProcess process;
process.start("pkexec", {"prime-select", "set", mode});
process.waitForFinished(30000);
return process.exitCode() == 0;
}
D_APPLET_CLASS(GraphicsDriverApplet)
DS_END_NAMESPACE
#include "graphicsdriverapplet.moc"