Files
JSwitchDisplayApplet/graphicsdriverapplet.cpp
T
Jokul d5a2c380ab feat: 添加GPU切换模式检测和主副显标识
- C++后端:GpuInfo添加isPrimary字段,读取boot_vga判断主副显
- C++后端:detectGpuMode()检测PRIME/Bumblebee/Solo/Hybrid模式
- C++后端:新增gpuMode和gpuPrimary属性
- QML前端:卡片显示绿色主显标签(boot_vga=1的GPU)
- QML前端:弹窗显示GPU模式标签(PRIME等)和帮助图标
- 帮助图标悬停显示模式说明文字
- 翻译更新:主显/主顯 + 四种模式的中文说明
2026-07-14 00:19:56 +08:00

419 lines
13 KiB
C++

// 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>
DCORE_USE_NAMESPACE
DS_BEGIN_NAMESPACE
GraphicsDriverApplet::GraphicsDriverApplet(QObject *parent)
: DApplet(parent)
, m_ready(false)
{
}
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;
}
void GraphicsDriverApplet::refreshDeviceInfo()
{
detectGpus();
}
void GraphicsDriverApplet::refreshStats()
{
readGpuStats();
}
void GraphicsDriverApplet::detectGpus()
{
m_gpus.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;
// 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/<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
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",
"--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) {
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();
} 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") {
QFile tempFile(hwmonDir.absoluteFilePath(hwmon + "/temp1_input"));
if (tempFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
int tempMilli = QTextStream(&tempFile).readAll().trimmed().toInt();
gpu.temperature = tempMilli / 1000;
}
break;
}
}
}
}
}
}
// 生成统计数据 (格式: temp|gpuUsage|memUsage|memUsed|memTotal)
m_gpuStats.clear();
for (const GpuInfo &gpu : m_gpus) {
m_gpuStats << QString("%1|%2|%3|%4|%5")
.arg(gpu.temperature)
.arg(gpu.gpuUsage)
.arg(gpu.memUsage)
.arg(gpu.memUsed)
.arg(gpu.memTotal);
}
emit gpuStatsChanged();
}
D_APPLET_CLASS(GraphicsDriverApplet)
DS_END_NAMESPACE
#include "graphicsdriverapplet.moc"