// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd. // // SPDX-License-Identifier: GPL-3.0-or-later #include "graphicsdriverapplet.h" #include #include #include #include #include #include #include #include #include #include #include #include DCORE_USE_NAMESPACE DS_BEGIN_NAMESPACE GraphicsDriverApplet::GraphicsDriverApplet(QObject *parent) : DApplet(parent) , m_ready(false) , m_lastAlertTime(0) { } 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 &history : m_tempHistory) { QVariantList gpuHistory; for (int temp : history) { gpuHistory << temp; } result << QVariant(gpuHistory); } return result; } QStringList GraphicsDriverApplet::gpuProcesses() const { return m_gpuProcesses; } 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); } } // 生成摘要信息 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//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 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_mhz,AMD 使用 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(>FreqFile).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() { 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"