From 133d453f6fd6e752b391e7c33a0e249b5afab5ad Mon Sep 17 00:00:00 2001 From: zach Date: Mon, 28 Sep 2026 12:32:36 +0200 Subject: [PATCH] fix: fix nvidia update interval on config change + more robust read path for generic --- Helpers/NetworkUsage.qml | 242 --------- Modules/Resources/Cards/NetworkCard.qml | 4 +- Plugins/ZShell/Config/services.hpp | 3 +- Plugins/ZShell/Services/gpu.cpp | 590 ++++++++++++--------- Plugins/ZShell/Services/gpu.hpp | 70 ++- Plugins/ZShell/Services/tickingservice.cpp | 1 + Plugins/ZShell/Services/tickingservice.hpp | 1 + 7 files changed, 390 insertions(+), 521 deletions(-) delete mode 100644 Helpers/NetworkUsage.qml diff --git a/Helpers/NetworkUsage.qml b/Helpers/NetworkUsage.qml deleted file mode 100644 index b78c540..0000000 --- a/Helpers/NetworkUsage.qml +++ /dev/null @@ -1,242 +0,0 @@ -pragma Singleton - -import QtQuick -import Quickshell -import Quickshell.Io -import ZShell.Internal -import ZShell.Config - -Singleton { - id: root - - property real _downloadSpeed: 0 - property real _downloadTotal: 0 - - property real _initialRxBytes: 0 - property real _initialTxBytes: 0 - property bool _initialized: false - - property real _prevRxBytes: 0 - property real _prevTimestamp: 0 - property real _prevTxBytes: 0 - property real _uploadSpeed: 0 - property real _uploadTotal: 0 - - readonly property CircularBuffer downloadBuffer: _downloadBuffer - - readonly property real downloadSpeed: _downloadSpeed - - readonly property real downloadTotal: _downloadTotal - readonly property int historyLength: Config.dashboard.performance.showVram ? 70 : 30 - property int refCount: 0 - readonly property CircularBuffer uploadBuffer: _uploadBuffer - readonly property real uploadSpeed: _uploadSpeed - readonly property real uploadTotal: _uploadTotal - -function resetSampling(): void { - netDevFile.reload(); - - const content = netDevFile.text(); - if (!content) - return; - - const data = root.parseNetDev(content); - const now = Date.now(); - - root._prevRxBytes = data.rx; - root._prevTxBytes = data.tx; - root._prevTimestamp = now; -} - - function formatBytes(bytes: real): var { - // Handle negative or invalid values - if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) { - return { - value: 0, - unit: "B/s" - }; - } - - if (bytes < 1024) { - return { - value: bytes, - unit: "B/s" - }; - } else if (bytes < 1024 * 1024) { - return { - value: bytes / 1024, - unit: "KB/s" - }; - } else if (bytes < 1024 * 1024 * 1024) { - return { - value: bytes / (1024 * 1024), - unit: "MB/s" - }; - } else { - return { - value: bytes / (1024 * 1024 * 1024), - unit: "GB/s" - }; - } - } - - function formatBytesTotal(bytes: real): var { - // Handle negative or invalid values - if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) { - return { - value: 0, - unit: "B" - }; - } - - if (bytes < 1024) { - return { - value: bytes, - unit: "B" - }; - } else if (bytes < 1024 * 1024) { - return { - value: bytes / 1024, - unit: "KB" - }; - } else if (bytes < 1024 * 1024 * 1024) { - return { - value: bytes / (1024 * 1024), - unit: "MB" - }; - } else { - return { - value: bytes / (1024 * 1024 * 1024), - unit: "GB" - }; - } - } - - function parseNetDev(content: string): var { - const lines = content.split("\n"); - let totalRx = 0; - let totalTx = 0; - - for (let i = 2; i < lines.length; i++) { - const line = lines[i].trim(); - if (!line) - continue; - - const parts = line.split(/\s+/); - if (parts.length < 10) - continue; - - const iface = parts[0].replace(":", ""); - // Skip loopback interface - if (iface === "lo") - continue; - - const rxBytes = parseFloat(parts[1]) || 0; - const txBytes = parseFloat(parts[9]) || 0; - - totalRx += rxBytes; - totalTx += txBytes; - } - - return { - rx: totalRx, - tx: totalTx - }; - } - - CircularBuffer { - id: _downloadBuffer - - capacity: root.historyLength + 1 - } - - CircularBuffer { - id: _uploadBuffer - - capacity: root.historyLength + 1 - } - - FileView { - id: netDevFile - - path: "/proc/net/dev" - } - - Timer { - interval: Config.dashboard.resourceUpdateInterval - repeat: true - running: true - triggeredOnStart: true - - onTriggered: { - if (root.refCount <= 0) { - root.resetSampling(); - return; - } - - netDevFile.reload(); - const content = netDevFile.text(); - if (!content) - return; - - const data = root.parseNetDev(content); - const now = Date.now(); - - if (!root._initialized) { - root._initialRxBytes = data.rx; - root._initialTxBytes = data.tx; - root._prevRxBytes = data.rx; - root._prevTxBytes = data.tx; - root._prevTimestamp = now; - root._initialized = true; - return; - } - - const timeDelta = (now - root._prevTimestamp) / 1000; // seconds - if (timeDelta > 0) { - // Calculate byte deltas - let rxDelta = data.rx - root._prevRxBytes; - let txDelta = data.tx - root._prevTxBytes; - - // Handle counter overflow (when counters wrap around from max to 0) - // This happens when counters exceed 32-bit or 64-bit limits - if (rxDelta < 0) { - // Counter wrapped around - assume 64-bit counter - rxDelta += Math.pow(2, 64); - } - if (txDelta < 0) { - txDelta += Math.pow(2, 64); - } - - // Calculate speeds - root._downloadSpeed = rxDelta / timeDelta; - root._uploadSpeed = txDelta / timeDelta; - - if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed)) - _downloadBuffer.push(root._downloadSpeed); - - if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) - _uploadBuffer.push(root._uploadSpeed); - } - - // Calculate totals with overflow handling - let downTotal = data.rx - root._initialRxBytes; - let upTotal = data.tx - root._initialTxBytes; - - // Handle counter overflow for totals - if (downTotal < 0) { - downTotal += Math.pow(2, 64); - } - if (upTotal < 0) { - upTotal += Math.pow(2, 64); - } - - root._downloadTotal = downTotal; - root._uploadTotal = upTotal; - - root._prevRxBytes = data.rx; - root._prevTxBytes = data.tx; - root._prevTimestamp = now; - } - } -} diff --git a/Modules/Resources/Cards/NetworkCard.qml b/Modules/Resources/Cards/NetworkCard.qml index 445ccf6..ea6474c 100644 --- a/Modules/Resources/Cards/NetworkCard.qml +++ b/Modules/Resources/Cards/NetworkCard.qml @@ -78,12 +78,12 @@ CustomRect { } Connections { - function onValuesChanged(): void { + function onUpdateTriggered(): void { sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024); slideAnim.restart(); } - target: NetworkUsage.downloadBuffer + target: NetworkUsage } NumberAnimation { diff --git a/Plugins/ZShell/Config/services.hpp b/Plugins/ZShell/Config/services.hpp index 1f341f8..8e6891c 100644 --- a/Plugins/ZShell/Config/services.hpp +++ b/Plugins/ZShell/Config/services.hpp @@ -1,4 +1,5 @@ #pragma once + #include "common.hpp" #include "enums.hpp" @@ -20,7 +21,7 @@ class Services : public settings::ObjectNode { CONFIG_PROPERTY(qreal, brightnessIncrement, 0.1) CONFIG_PROPERTY(bool, ddcutilService, true) CONFIG_PROPERTY(QString, defaultPlayer, QStringLiteral("Spotify")) - CONFIG_PROPERTY(QString, gpuType, QStringLiteral("NVIDIA")) + CONFIG_ENUM_PROPERTY(GpuType, gpuType, GpuType::Auto) CONFIG_PROPERTY(LyricsBackend::Enum, lyricsBackend, LyricsBackend::Auto) CONFIG_PROPERTY(qreal, maxVolume, 1) CONFIG_PROPERTY(qreal, minBrightness, 0.01) diff --git a/Plugins/ZShell/Services/gpu.cpp b/Plugins/ZShell/Services/gpu.cpp index c12d896..81235cb 100644 --- a/Plugins/ZShell/Services/gpu.cpp +++ b/Plugins/ZShell/Services/gpu.cpp @@ -2,97 +2,187 @@ #include "sensorslib.hpp" -#include -#include +#include "config/rootnodes.hpp" +#include "config/services.hpp" + #include #include -#include +#include #include -#include -#include -#include -#include -#include namespace ZShell::services { +using Qt::StringLiterals::operator""_s; + namespace { -constexpr const char* kTypeDetectScript = - "if command -v nvidia-smi >/dev/null 2>&1 && nvidia-smi -L >/dev/null " - "2>&1; then echo NVIDIA;" - " elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep " - "-q .; then echo GENERIC;" - " else echo NONE; fi"; +QStringList cardFiles(const QString& relative) { + static const QRegularExpression k_cardRe(u"^card\\d+$"_s); -constexpr const char* kNameDetectScript = - "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null" - " || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' " - "-f1" - " || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1"; + QStringList files; + QDirIterator it(u"/sys/class/drm"_s, QDir::Dirs | QDir::NoDotAndDotDot); + while (it.hasNext()) { + const QString path = it.next(); + if (!k_cardRe.match(it.fileName()).hasMatch()) continue; + const QString file = path + relative; + if (QFile::exists(file)) files << file; + } + return files; +} + +std::optional readNumber(const QString& path) { + QFile f(path); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return std::nullopt; + bool ok = false; + const qreal v = f.readAll().trimmed().toDouble(&ok); + if (!ok) return std::nullopt; + return v; +} + +QString cleanName(QString s) { + static const QRegularExpression k_noise( + u"\\(R\\)|\\(TM\\)|Graphics"_s, + QRegularExpression::CaseInsensitiveOption); + static const QRegularExpression k_spaces(u"\\s+"_s); + s.replace(k_noise, {}); + s.replace(k_spaces, u" "_s); + return s.trimmed(); +} + +QString parseNvidiaName(const QByteArray& out) { + const QString first = + QString::fromUtf8(out).split(u'\n').value(0).trimmed(); + return first.isEmpty() ? QString() : cleanName(first); +} + +QString parseGlxinfoName(const QByteArray& out) { + const QStringList lines = QString::fromUtf8(out).split(u'\n'); + for (const QString& line : lines) { + const qsizetype idx = line.indexOf(u"Device:"_s); + if (idx < 0) { + continue; + } + + QString rest = line.mid(idx + 7); + const qsizetype paren = rest.indexOf(u'('); + if (paren >= 0) { + rest = rest.left(paren); + } + + const QString cleaned = cleanName(rest); + if (!cleaned.isEmpty()) { + return cleaned; + } + } + + return {}; +} + +QString parseLspciName(const QByteArray& out) { + static const QRegularExpression k_lineRe( + u"vga|3d controller|display"_s, + QRegularExpression::CaseInsensitiveOption); + + const QStringList lines = QString::fromUtf8(out).split(u'\n'); + QString match; + for (const QString& line : lines) { + if (k_lineRe.match(line).hasMatch()) { + match = line; + break; + } + } + + if (match.isEmpty()) { + return {}; + } + + static const QRegularExpression k_bracketRe(u"\\[([^\\]]+)\\][^\\[]*$"_s); + const auto bracket = k_bracketRe.match(match); + if (bracket.hasMatch()) { + return cleanName(bracket.captured(1)); + } + + static const QRegularExpression k_colonRe(u":\\s+(.+)"_s); + const auto colon = k_colonRe.match(match); + if (colon.hasMatch()) { + return cleanName(colon.captured(1)); + } + + return {}; +} + +struct NameSource { + QString program; + QStringList args; + QString (*parse)(const QByteArray&); +}; + +const std::array& nameSources() { + static const std::array k_sources = {{ + { + .program = u"nvidia-smi"_s, + .args = {u"--query-gpu=name"_s, u"--format=csv,noheader"_s}, + .parse = &parseNvidiaName, + }, + { + .program = u"glxinfo"_s, + .args = {u"-B"_s}, + .parse = &parseGlxinfoName, + }, + { + .program = u"lspci"_s, + .args = {}, + .parse = &parseLspciName, + }, + }}; + return k_sources; +} + +constexpr int k_nvidiaSource = 0; +constexpr int k_firstGenericSource = 1; } // namespace Gpu::Gpu(QObject* parent) : TickingService(parent) { - QString configPath = - QDir::homePath() + QStringLiteral("/.config/zshell/config.json"); + auto* svc = config::ConfigSingleton::instance()->services(); + m_userType = svc->gpuType(); + QObject::connect(svc, &config::Services::gpuTypeChanged, this, [this, svc] { + const GpuType value = svc->gpuType(); + if (value == m_userType) return; + m_userType = value; + resolveGpu(); + }); - auto reloadConfig = [this, configPath]() { - QFile file(configPath); - if (file.open(QIODevice::ReadOnly)) { - QJsonDocument doc = QJsonDocument::fromJson(file.readAll()); - if (!doc.isNull()) { - QJsonObject services = - doc.object().value("services").toObject(); - setUserType(parseType(services.value("gpuType").toString())); - } - } - }; + resolveGpu(); - reloadConfig(); + m_restartTimer = new QTimer(this); + m_restartTimer->setSingleShot(true); + m_restartTimer->setInterval(500); + QObject::connect(m_restartTimer, &QTimer::timeout, this, [this] { + if (!m_nvidiaProc || m_type != GpuType::Nvidia) return; - static QFileSystemWatcher* watcher = new QFileSystemWatcher(); - if (!watcher->files().contains(configPath)) { - QObject::connect( - watcher, - &QFileSystemWatcher::fileChanged, - this, - [this, configPath]() { - QTimer::singleShot(100, this, [this, configPath]() { - QFile file(configPath); - if (file.open(QIODevice::ReadOnly)) { - QJsonDocument doc = - QJsonDocument::fromJson(file.readAll()); - if (!doc.isNull()) { - QJsonObject services = - doc.object().value("services").toObject(); - setUserType(parseType( - services.value("gpuType").toString())); - } - } - }); - }); - watcher->addPath(configPath); + stopNvidiaUsage(); + startNvidiaUsage(); + }); + + QObject::connect(this, &TickingService::updateIntervalChanged, this, [this] { + if (!m_nvidiaProc) return; + m_restartTimer->start(); + }); +} + +Gpu::~Gpu() { + if (!m_nvidiaProc) return; + m_nvidiaProc->disconnect(this); + m_nvidiaProc->terminate(); + if (!m_nvidiaProc->waitForFinished(500)) { + m_nvidiaProc->kill(); + m_nvidiaProc->waitForFinished(200); } - - // Detection must run before any ServiceRef appears: callers may gate the ref on - // `type !== Gpu.None`, which would otherwise deadlock the detection. - if (m_userType == Auto) { - detectTypeOnce(); - } - detectNameOnce(); } -Gpu::Type Gpu::type() const { - return m_userType == Auto ? m_autoType : m_userType; -} - -Gpu::Type Gpu::userType() const { - return m_userType; -} - -Gpu::Type Gpu::autoType() const { - return m_autoType; +GpuType Gpu::type() const { + return m_type; } QString Gpu::name() const { @@ -115,28 +205,13 @@ qreal Gpu::memoryTotal() const { return m_memoryTotal; } -void Gpu::setUserType(Type value) { - if (value == m_userType) { - return; - } - const Type prevDerived = type(); - m_userType = value; - Q_EMIT userTypeChanged(); - if (type() != prevDerived) { - Q_EMIT typeChanged(); - } -} +void Gpu::setType(GpuType value) { + if (value == m_type) return; -void Gpu::setAutoType(Type value) { - if (value == m_userType) { - return; - } - const Type prevDerived = type(); - m_autoType = value; - Q_EMIT autoTypeChanged(); - if (type() != prevDerived) { - Q_EMIT typeChanged(); - } + m_type = value; + if (m_type != GpuType::Nvidia) stopNvidiaUsage(); + if (m_type == GpuType::None) resetUsage(); + emit typeChanged(); } void Gpu::setName(QString value) { @@ -147,6 +222,12 @@ void Gpu::setName(QString value) { Q_EMIT nameChanged(); } +void Gpu::setDetecting(bool value) { + if (value == m_detecting) return; + m_detecting = value; + emit detectingChanged(); +} + void Gpu::setMemoryUsed(qreal value) { if (std::abs(m_memoryUsed - value) < 0.001) return; m_memoryUsed = value; @@ -160,168 +241,192 @@ void Gpu::setMemoryTotal(qreal value) { } void Gpu::tick() { - const Type t = type(); - if (t == Generic) { + if (m_type == GpuType::Generic) { readGenericUsage(); readGenericMemory(); readGpuTemperature(); - } else if (t == Nvidia) { + } else if (m_type == GpuType::Nvidia) { startNvidiaUsage(); } else { - if (std::abs(m_percentage) > 0.0001) { - m_percentage = 0.0; - Q_EMIT percentageChanged(); - } - if (std::abs(m_temperature) > 0.05) { - m_temperature = 0.0; - Q_EMIT temperatureChanged(); - } + resetUsage(); } } -void Gpu::detectTypeOnce() { - if (m_typeProc) { - return; +void Gpu::refreshSysfsCache() { + m_busyFiles = cardFiles(u"/device/gpu_busy_percent"_s); + + m_vramFiles.clear(); + const QStringList totals = cardFiles(u"/device/mem_info_vram_total"_s); + for (const QString& total : totals) { + QString used = total; + used.replace(u"mem_info_vram_total"_s, u"mem_info_vram_used"_s); + if (QFile::exists(used)) m_vramFiles.append({used, total}); } - m_typeProc = new QProcess(this); - QObject::connect( - m_typeProc, - &QProcess::finished, - this, - [this](int, QProcess::ExitStatus) { - const QByteArray out = - m_typeProc->readAllStandardOutput().trimmed(); - if (!out.isEmpty()) { - setAutoType(parseType(QString::fromLatin1(out))); - } - m_typeProc->deleteLater(); - m_typeProc = nullptr; - }); - m_typeProc->start( - QStringLiteral("sh"), - {QStringLiteral("-c"), QString::fromLatin1(kTypeDetectScript)}); } -void Gpu::detectNameOnce() { - if (m_nameProc) { +void Gpu::resolveGpu() { + refreshSysfsCache(); + const int generation = ++m_generation; + + if (m_userType != GpuType::Auto) setType(m_userType); + + if (m_userType == GpuType::None) { + setName({}); + setDetecting(false); return; } - m_nameProc = new QProcess(this); + + setName({}); + setDetecting(true); + tryNameSource( + m_userType == GpuType::Generic ? k_firstGenericSource : k_nvidiaSource, + generation); +} + +int Gpu::probeEnd() const { + return m_userType == GpuType::Nvidia + ? k_firstGenericSource + : static_cast(nameSources().size()); +} + +void Gpu::tryNameSource(int index, int generation) { + const NameSource& src = nameSources().at(static_cast(index)); + runProcess( + src.program, + src.args, + [this, index, generation, parse = src.parse](const QByteArray& out) { + finishNameSource(index, generation, parse(out)); + }); +} + +void Gpu::finishNameSource(int index, int generation, QString name) { + if (generation != m_generation) { + return; + } + + if (m_userType == GpuType::Auto && index == k_nvidiaSource) { + if (!name.isEmpty()) + setType(GpuType::Nvidia); + else + setType(m_busyFiles.isEmpty() ? GpuType::None : GpuType::Generic); + + if (m_type == GpuType::None) { + setName({}); + setDetecting(false); + return; + } + } + + if (!name.isEmpty()) { + setName(std::move(name)); + setDetecting(false); + return; + } + + const int next = index + 1; + if (next < probeEnd()) { + tryNameSource(next, generation); + } else { + setName({}); + setDetecting(false); + } +} + +void Gpu::runProcess( + const QString& program, + const QStringList& args, + std::function callback) { + auto* proc = new QProcess(this); + proc->setStandardErrorFile(QProcess::nullDevice()); + + const auto finish = + [proc, callback = std::move(callback)](const QByteArray& out) { + callback(out); + proc->deleteLater(); + }; + QObject::connect( - m_nameProc, + proc, &QProcess::finished, this, - [this](int, QProcess::ExitStatus) { - const QString output = - QString::fromUtf8(m_nameProc->readAllStandardOutput()).trimmed(); - if (!output.isEmpty()) { - const QString lower = output.toLower(); - if (lower.contains(QStringLiteral("nvidia")) || - lower.contains(QStringLiteral("geforce")) || - lower.contains(QStringLiteral("rtx")) || - lower.contains(QStringLiteral("gtx")) || - lower.contains(QStringLiteral("rx"))) { - setName(cleanName(output)); - } else { - static const QRegularExpression bracketRe( - QStringLiteral("\\[([^\\]]+)\\][^\\[]*$")); - const auto bracket = bracketRe.match(output); - if (bracket.hasMatch()) { - setName(cleanName(bracket.captured(1))); - } else { - static const QRegularExpression colonRe( - QStringLiteral(":\\s*(.+)")); - const auto colon = colonRe.match(output); - if (colon.hasMatch()) { - setName(cleanName(colon.captured(1))); - } - } - } - } - m_nameProc->deleteLater(); - m_nameProc = nullptr; + [finish, proc](int code, QProcess::ExitStatus status) { + const bool ok = status == QProcess::NormalExit && code == 0; + finish(ok ? proc->readAllStandardOutput() : QByteArray()); }); - m_nameProc->start( - QStringLiteral("sh"), - {QStringLiteral("-c"), QString::fromLatin1(kNameDetectScript)}); + QObject::connect( + proc, + &QProcess::errorOccurred, + this, + [finish](QProcess::ProcessError err) { + if (err == QProcess::FailedToStart) { + finish(QByteArray()); + } + }); + + proc->start(program, args); +} + +void Gpu::onStopped() { + stopNvidiaUsage(); } void Gpu::readGenericMemory() { - const QStringList paths = QDir(QStringLiteral("/sys/class/drm")) - .entryList( - QStringList() << QStringLiteral("card*"), - QDir::Dirs | QDir::NoDotAndDotDot); - - qreal totalMem = 0.0; - qreal usedMem = 0.0; - for (const QString& card : paths) { - QFile total( - QStringLiteral("/sys/class/drm/%1/device/mem_info_vram_total") - .arg(card)); - if (!total.open(QIODevice::ReadOnly | QIODevice::Text)) { - continue; - } - bool ok = false; - const qreal v = total.readAll().trimmed().toDouble(&ok); - total.close(); - if (ok) { - totalMem += v; - } - - QFile used(QStringLiteral("/sys/class/drm/%1/device/mem_info_vram_used") - .arg(card)); - if (!used.open(QIODevice::ReadOnly | QIODevice::Text)) { - continue; - } - bool ok1 = false; - const qreal v1 = used.readAll().trimmed().toDouble(&ok1); - used.close(); - if (ok1) { - usedMem += v1; - } + qreal total = 0.0; + qreal used = 0.0; + for (const VramFiles& v : std::as_const(m_vramFiles)) { + const auto t = readNumber(v.total); + const auto u = readNumber(v.used); + if (!t || !u) continue; + total += *t; + used += *u; } - - setMemoryTotal(totalMem); - setMemoryUsed(usedMem); + setMemoryTotal(total); + setMemoryUsed(used); } void Gpu::readGenericUsage() { - const QStringList paths = QDir(QStringLiteral("/sys/class/drm")) - .entryList( - QStringList() << QStringLiteral("card*"), - QDir::Dirs | QDir::NoDotAndDotDot); qreal sum = 0.0; int count = 0; - for (const QString& card : paths) { - QFile f(QStringLiteral("/sys/class/drm/%1/device/gpu_busy_percent") - .arg(card)); - if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { - continue; - } - bool ok = false; - const qreal v = f.readAll().trimmed().toDouble(&ok); - f.close(); - if (ok) { - sum += v; + for (const QString& path : std::as_const(m_busyFiles)) { + if (const auto v = readNumber(path)) { + sum += *v; ++count; } } const qreal newPerc = count > 0 ? sum / count / 100.0 : 0.0; if (std::abs(newPerc - m_percentage) > 0.0001) { m_percentage = newPerc; - Q_EMIT percentageChanged(); + emit percentageChanged(); } } -void Gpu::startNvidiaUsage() { - if (m_nvidiaProc) { +void Gpu::stopNvidiaUsage() { + m_restartTimer->stop(); + + if (!m_nvidiaProc) return; + + QProcess* proc = m_nvidiaProc; + m_nvidiaProc = nullptr; + + proc->disconnect(this); + if (proc->state() == QProcess::NotRunning) { + proc->deleteLater(); return; } - m_nvidiaProc = new QProcess(this); + QObject::connect(proc, &QProcess::finished, proc, &QObject::deleteLater); + proc->terminate(); +} + +void Gpu::startNvidiaUsage() { + if (m_nvidiaProc) return; + + auto* proc = new QProcess(this); + m_nvidiaProc = proc; + QObject::connect( - m_nvidiaProc, &QProcess::readyReadStandardOutput, this, [this]() { - while (m_nvidiaProc->canReadLine()) { - const QByteArray out = m_nvidiaProc->readLine(); + proc, &QProcess::readyReadStandardOutput, this, [this, proc]() { + while (proc->canReadLine()) { + const QByteArray out = proc->readLine(); const QString output = QString::fromUtf8(out).trimmed(); if (output.isEmpty()) continue; @@ -357,21 +462,21 @@ void Gpu::startNvidiaUsage() { } }); - QObject::connect(m_nvidiaProc, &QProcess::finished, this, [this]() { - m_nvidiaProc->deleteLater(); - m_nvidiaProc = nullptr; - }); + auto cleanup = [this, proc]() { + if (m_nvidiaProc == proc) m_nvidiaProc = nullptr; + + proc->deleteLater(); + }; + + QObject::connect(proc, &QProcess::finished, this, cleanup); QObject::connect( - m_nvidiaProc, + proc, &QProcess::errorOccurred, this, - [this](QProcess::ProcessError) { - m_nvidiaProc->deleteLater(); - m_nvidiaProc = nullptr; - }); + [cleanup](QProcess::ProcessError) { cleanup(); }); - m_nvidiaProc->start( + proc->start( QStringLiteral("nvidia-smi"), {QStringLiteral( "--query-gpu=utilization.gpu,temperature.gpu," @@ -386,32 +491,21 @@ void Gpu::readGpuTemperature() { const qreal newTemp = t.value_or(0.0); if (std::abs(newTemp - m_temperature) > 0.05) { m_temperature = newTemp; - Q_EMIT temperatureChanged(); + emit temperatureChanged(); } } -Gpu::Type Gpu::parseType(const QString& s) { - const QString u = s.trimmed().toUpper(); - if (u.isEmpty()) { - return Auto; +void Gpu::resetUsage() { + if (std::abs(m_percentage) > 0.0001) { + m_percentage = 0.0; + emit percentageChanged(); } - if (u == QStringLiteral("NVIDIA")) { - return Nvidia; + if (std::abs(m_temperature) > 0.05) { + m_temperature = 0.0; + emit temperatureChanged(); } - if (u == QStringLiteral("GENERIC")) { - return Generic; - } - return None; -} - -QString Gpu::cleanName(QString s) { - static const QRegularExpression noise( - QStringLiteral("\\(R\\)|\\(TM\\)|Graphics"), - QRegularExpression::CaseInsensitiveOption); - static const QRegularExpression spaces(QStringLiteral("\\s+")); - s.replace(noise, QString()); - s.replace(spaces, QStringLiteral(" ")); - return s.trimmed(); + setMemoryUsed(0.0); + setMemoryTotal(0.0); } } // namespace ZShell::services diff --git a/Plugins/ZShell/Services/gpu.hpp b/Plugins/ZShell/Services/gpu.hpp index dbe567c..e082d1b 100644 --- a/Plugins/ZShell/Services/gpu.hpp +++ b/Plugins/ZShell/Services/gpu.hpp @@ -1,31 +1,29 @@ #pragma once #include "tickingservice.hpp" +#include "config/enums.hpp" #include #include +#include namespace ZShell::services { +using GpuType = config::GpuType::Enum; + +struct VramFiles { + QString used; + QString total; +}; + class Gpu : public TickingService { Q_OBJECT QML_ELEMENT QML_SINGLETON - public: - enum Type { - Auto, // user override is empty (config "") — defer to detected autoType - None, // no usable GPU - Nvidia, // queried via nvidia-smi - Generic, // queried via /sys/class/drm/card*/device/gpu_busy_percent - }; - Q_ENUM(Type) - - private: - Q_PROPERTY(Type type READ type NOTIFY typeChanged) - Q_PROPERTY(Type userType READ userType NOTIFY userTypeChanged) - Q_PROPERTY(Type autoType READ autoType NOTIFY autoTypeChanged) + Q_PROPERTY(GpuType type READ type NOTIFY typeChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged) + Q_PROPERTY(bool detecting READ detecting NOTIFY detectingChanged) Q_PROPERTY(qreal percentage READ percentage NOTIFY percentageChanged) Q_PROPERTY(qreal temperature READ temperature NOTIFY temperatureChanged) Q_PROPERTY(qreal memoryUsed READ memoryUsed NOTIFY memoryUsedChanged) @@ -33,11 +31,11 @@ class Gpu : public TickingService { public: explicit Gpu(QObject* parent = nullptr); + ~Gpu() override; - [[nodiscard]] Type type() const; - [[nodiscard]] Type userType() const; - [[nodiscard]] Type autoType() const; + [[nodiscard]] GpuType type() const; [[nodiscard]] QString name() const; + [[nodiscard]] bool detecting() const; [[nodiscard]] qreal percentage() const; [[nodiscard]] qreal temperature() const; [[nodiscard]] qreal memoryUsed() const; @@ -45,9 +43,8 @@ class Gpu : public TickingService { signals: void typeChanged(); - void userTypeChanged(); - void autoTypeChanged(); void nameChanged(); + void detectingChanged(); void percentageChanged(); void temperatureChanged(); void memoryUsedChanged(); @@ -55,35 +52,52 @@ class Gpu : public TickingService { protected: void tick() override; + void onStopped() override; private: - void detectTypeOnce(); - void detectNameOnce(); + void resolveGpu(); + + [[nodiscard]] int probeEnd() const; + + void tryNameSource(int index, int generation); + void finishNameSource(int index, int generation, QString name); + void readGenericUsage(); + void stopNvidiaUsage(); void startNvidiaUsage(); void readGpuTemperature(); void readGenericMemory(); + void resetUsage(); - void setUserType(Type value); - void setAutoType(Type value); + void runProcess( + const QString& program, + const QStringList& args, + std::function callback); + + void setType(GpuType value); void setName(QString value); + void setDetecting(bool value); void setMemoryUsed(qreal value); void setMemoryTotal(qreal value); - [[nodiscard]] static Type parseType(const QString& s); - [[nodiscard]] static QString cleanName(QString s); + void refreshSysfsCache(); - Type m_userType = Auto; - Type m_autoType = None; + GpuType m_userType = GpuType::Auto; + GpuType m_type = GpuType::None; QString m_name; + bool m_detecting = false; qreal m_percentage = 0.0; qreal m_temperature = 0.0; qreal m_memoryUsed = 0.0; qreal m_memoryTotal = 0.0; - QProcess* m_typeProc = nullptr; - QProcess* m_nameProc = nullptr; + QStringList m_busyFiles; + QList m_vramFiles; + int m_generation = 0; + bool m_nvidiaQuerying = false; + QProcess* m_nvidiaProc = nullptr; + QTimer* m_restartTimer = nullptr; }; } // namespace ZShell::services diff --git a/Plugins/ZShell/Services/tickingservice.cpp b/Plugins/ZShell/Services/tickingservice.cpp index 0dc67fa..d6f585e 100644 --- a/Plugins/ZShell/Services/tickingservice.cpp +++ b/Plugins/ZShell/Services/tickingservice.cpp @@ -37,6 +37,7 @@ void TickingService::start() { void TickingService::stop() { m_running = false; m_timer->stop(); + onStopped(); } void TickingService::applyInterval(int ms) { diff --git a/Plugins/ZShell/Services/tickingservice.hpp b/Plugins/ZShell/Services/tickingservice.hpp index 5cd7fe1..87f5676 100644 --- a/Plugins/ZShell/Services/tickingservice.hpp +++ b/Plugins/ZShell/Services/tickingservice.hpp @@ -26,6 +26,7 @@ class TickingService : public Service { void stop() final; virtual void tick() = 0; + virtual void onStopped() {} private: void applyInterval(int ms);