diff --git a/graphicsdriverapplet.cpp b/graphicsdriverapplet.cpp index 42d55ea..34cdc45 100644 --- a/graphicsdriverapplet.cpp +++ b/graphicsdriverapplet.cpp @@ -11,6 +11,7 @@ #include #include #include +#include DCORE_USE_NAMESPACE @@ -57,11 +58,21 @@ bool GraphicsDriverApplet::ready() const return m_ready; } +QStringList GraphicsDriverApplet::gpuStats() const +{ + return m_gpuStats; +} + void GraphicsDriverApplet::refreshDeviceInfo() { detectGpus(); } +void GraphicsDriverApplet::refreshStats() +{ + readGpuStats(); +} + void GraphicsDriverApplet::detectGpus() { m_gpus.clear(); @@ -74,6 +85,14 @@ void GraphicsDriverApplet::detectGpus() line.contains("Display controller", Qt::CaseInsensitive)) { GpuInfo gpu; + // 初始化统计字段 + gpu.drmCard = -1; + 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(' '); @@ -97,6 +116,9 @@ void GraphicsDriverApplet::detectGpus() gpu.driver = "unknown"; } + // 查找 DRM 卡号 + gpu.drmCard = findDrmCard(gpu.pciId); + m_gpus.append(gpu); } } @@ -131,6 +153,9 @@ void GraphicsDriverApplet::detectGpus() emit currentDriverChanged(); emit gpuSummaryChanged(); emit readyChanged(); + + // 读取实时统计信息 + readGpuStats(); } QStringList GraphicsDriverApplet::runCommand(const QString &cmd, const QStringList &args) @@ -213,6 +238,113 @@ QString GraphicsDriverApplet::readDriverVersion(const QString &driverName) return QString(); } +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 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 diff --git a/graphicsdriverapplet.h b/graphicsdriverapplet.h index dc00776..8c67d34 100644 --- a/graphicsdriverapplet.h +++ b/graphicsdriverapplet.h @@ -17,6 +17,14 @@ struct GpuInfo { QString driver; // 驱动模块名,如 "nvidia" QString driverVersion; // 驱动版本 QString pciId; // PCI ID,如 "01:00.0" + int drmCard; // DRM 卡号,如 0, 1(-1 表示未找到) + + // 实时统计 + int temperature; // 温度 (°C),-1 表示不可用 + int gpuUsage; // GPU 使用率 (%),-1 表示不可用 + int memUsage; // 显存使用率 (%),-1 表示不可用 + int memTotal; // 显存总量 (MB),-1 表示不可用 + int memUsed; // 显存已用 (MB),-1 表示不可用 }; class GraphicsDriverApplet : public DApplet @@ -26,6 +34,7 @@ class GraphicsDriverApplet : public DApplet Q_PROPERTY(QString deviceInfo READ deviceInfo NOTIFY deviceInfoChanged) Q_PROPERTY(QString currentDriver READ currentDriver NOTIFY currentDriverChanged) Q_PROPERTY(QString gpuSummary READ gpuSummary NOTIFY gpuSummaryChanged) + Q_PROPERTY(QStringList gpuStats READ gpuStats NOTIFY gpuStatsChanged) Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) public: @@ -38,18 +47,23 @@ public: QString deviceInfo() const; QString currentDriver() const; QString gpuSummary() const; + QStringList gpuStats() const; bool ready() const; Q_INVOKABLE void refreshDeviceInfo(); + Q_INVOKABLE void refreshStats(); signals: void deviceInfoChanged(); void currentDriverChanged(); void gpuSummaryChanged(); + void gpuStatsChanged(); void readyChanged(); private: void detectGpus(); + void readGpuStats(); + static int findDrmCard(const QString &pciId); static QStringList runCommand(const QString &cmd, const QStringList &args); static QString parseGpuName(const QString &line); static QString readDriverVersion(const QString &driverName); @@ -58,6 +72,7 @@ private: QString m_deviceInfo; QString m_currentDriver; QString m_gpuSummary; + QStringList m_gpuStats; bool m_ready; }; diff --git a/package/driverview.qml b/package/driverview.qml index a1e1363..1945943 100644 --- a/package/driverview.qml +++ b/package/driverview.qml @@ -55,11 +55,23 @@ AppletItem { // 悬停提示 PanelToolTip { id: toolTip - text: root.ready ? root.deviceInfo.replace(/\n/g, "\n") : qsTr("No GPU detected") + text: root.ready ? buildToolTipText() : qsTr("No GPU detected") toolTipX: DockPanelPositioner.x toolTipY: DockPanelPositioner.y } + // 统计数据刷新定时器 + Timer { + id: statsRefreshTimer + interval: 2000 + repeat: true + onTriggered: { + if (root.applet) { + root.applet.refreshStats() + } + } + } + Timer { id: toolTipShowTimer interval: 50 @@ -74,11 +86,18 @@ AppletItem { onHoveredChanged: { if (hovered && !driverPopup.popupVisible) { toolTipShowTimer.start() + if (root.applet) { + root.applet.refreshStats() + } + statsRefreshTimer.start() } else { if (toolTipShowTimer.running) { toolTipShowTimer.stop() } toolTip.close() + if (!driverPopup.popupVisible) { + statsRefreshTimer.stop() + } } } } @@ -87,13 +106,19 @@ AppletItem { PanelPopup { id: driverPopup width: 360 - height: 320 + height: 420 popupX: DockPanelPositioner.x popupY: DockPanelPositioner.y onPopupVisibleChanged: { if (popupVisible) { toolTip.close() + statsRefreshTimer.start() + if (root.applet) { + root.applet.refreshStats() + } + } else { + statsRefreshTimer.stop() } } @@ -188,12 +213,53 @@ AppletItem { delegate: Rectangle { id: gpuCard width: gpuColumn.width - height: 72 + height: 98 color: root.cardBackground radius: 10 border.width: 1 border.color: root.cardBorder + property string statsLine: root.applet && root.applet.gpuStats ? (root.applet.gpuStats[index] || "") : "" + property string tempStr: { + var parts = statsLine.split("|") + var temp = parts[0] + return temp === "-1" || temp === "" ? "—" : temp + } + property string gpuUsageStr: { + var parts = statsLine.split("|") + var usage = parts[1] + return usage === "-1" || usage === "" ? "—" : usage + } + property string memUsageStr: { + var parts = statsLine.split("|") + var usage = parts[2] + return usage === "-1" || usage === "" ? "—" : usage + } + property string memUsedStr: { + var parts = statsLine.split("|") + var used = parts[3] + return used === "-1" || used === "" ? "—" : used + } + property string memTotalStr: { + var parts = statsLine.split("|") + var total = parts[4] + return total === "-1" || total === "" ? "—" : total + } + property real gpuUsagePercent: { + var parts = statsLine.split("|") + var usage = parts[1] + if (usage === "-1" || usage === "") return 0 + return parseFloat(usage) + } + property color tempColor: { + var parts = statsLine.split("|") + var temp = parseInt(parts[0]) + if (isNaN(temp) || temp === -1) return root.primaryText + if (temp >= 80) return Qt.rgba(220/255, 38/255, 38/255, 1) + if (temp >= 60) return Qt.rgba(245/255, 158/255, 11/255, 1) + return Qt.rgba(22/255, 163/255, 74/255, 1) + } + ToolTip.text: qsTr("GPU: ") + parseGpuName(modelData) + "\n" + qsTr("Driver: ") + parseDriverName(modelData) + "\n" + qsTr("Version: ") + (parseDriverVersion(modelData) || qsTr("Unknown")) @@ -204,47 +270,113 @@ AppletItem { id: cardHover } - RowLayout { + ColumnLayout { anchors.fill: parent - anchors.margins: 12 - spacing: 12 + anchors.margins: 10 + spacing: 6 - // GPU 图标 - Rectangle { - width: 44 - height: 44 - color: root.accentBlueLight - radius: 8 + RowLayout { + Layout.fillWidth: true + spacing: 12 - Text { - anchors.centerIn: parent - text: parseGpuVendorShort(modelData) - font.pixelSize: 14 - font.bold: true - color: root.accentBlue + // GPU 图标 + Rectangle { + width: 36 + height: 36 + color: root.accentBlueLight + radius: 8 + + Text { + anchors.centerIn: parent + text: parseGpuVendorShort(modelData) + font.pixelSize: 14 + font.bold: true + color: root.accentBlue + } + } + + // GPU 信息 + ColumnLayout { + spacing: 4 + Layout.fillWidth: true + + Text { + text: parseGpuName(modelData) + font.pixelSize: 13 + font.bold: true + color: root.primaryText + Layout.fillWidth: true + elide: Text.ElideRight + } + + Text { + text: parseDriverInfo(modelData) + font.pixelSize: 11 + color: root.secondaryText + Layout.fillWidth: true + elide: Text.ElideRight + } } } - // GPU 信息 + // 统计数据区域 ColumnLayout { - spacing: 4 Layout.fillWidth: true + spacing: 6 - Text { - text: parseGpuName(modelData) - font.pixelSize: 13 - font.bold: true - color: root.primaryText + RowLayout { Layout.fillWidth: true - elide: Text.ElideRight + spacing: 12 + + // 温度 + Text { + text: tempStr !== "—" ? tempStr + "°C" : "—" + font.pixelSize: 12 + color: gpuCard.tempColor + font.bold: tempStr !== "—" + } + + Item { Layout.fillWidth: true } + + // 显存信息 + Text { + text: (memUsedStr !== "—" && memTotalStr !== "—") ? memUsedStr + "/" + memTotalStr + " MB" : "—" + font.pixelSize: 11 + color: root.secondaryText + } } - Text { - text: parseDriverInfo(modelData) - font.pixelSize: 11 - color: root.secondaryText + // GPU 使用率进度条 + RowLayout { Layout.fillWidth: true - elide: Text.ElideRight + spacing: 8 + + Text { + text: "GPU" + font.pixelSize: 11 + color: root.tertiaryText + } + + Rectangle { + Layout.fillWidth: true + height: 4 + color: root.cardBorder + radius: 2 + + Rectangle { + width: Math.min(gpuCard.gpuUsagePercent, 100) * parent.width / 100 + height: parent.height + color: root.accentBlue + radius: 2 + } + } + + Text { + text: gpuUsageStr !== "—" ? gpuUsageStr + "%" : "—" + font.pixelSize: 11 + color: root.secondaryText + font.bold: gpuUsageStr !== "—" + } } } } @@ -366,6 +498,32 @@ AppletItem { return "GPU" } + // 构建 tooltip 文本 + function buildToolTipText() { + if (!root.applet || !root.applet.gpuStats) { + return root.currentDriver || qsTr("No GPU detected") + } + + var lines = root.deviceInfo.split("\n").filter(function(line) { return line.trim().length > 0 }) + var result = [] + + for (var i = 0; i < lines.length; i++) { + var vendor = parseGpuVendorShort(lines[i]) + var statsLine = root.applet.gpuStats[i] || "" + var parts = statsLine.split("|") + + var temp = parts[0] + var gpuUsage = parts[1] + + var tempStr = (temp === "-1" || temp === "") ? "" : temp + "°C" + var gpuUsageStr = (gpuUsage === "-1" || gpuUsage === "") ? "" : gpuUsage + "%" + + result.push(vendor + " " + tempStr + " " + gpuUsageStr) + } + + return result.join(" | ") + } + // 点击处理 TapHandler { acceptedButtons: Qt.LeftButton