#include "gpu.hpp" #include "sensorslib.hpp" #include #include #include #include #include #include #include #include #include namespace ZShell::services { 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"; 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"; } // namespace Gpu::Gpu(QObject* parent) : TickingService(parent) { QString configPath = QDir::homePath() + QStringLiteral("/.config/zshell/config.json"); 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())); } } }; reloadConfig(); 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.exists()) { 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); } // 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; } QString Gpu::name() const { return m_name; } qreal Gpu::percentage() const { return m_percentage; } qreal Gpu::temperature() const { return m_temperature; } qreal Gpu::memoryUsed() const { return m_memoryUsed; } 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::setAutoType(Type value) { if (value == m_userType) { return; } const Type prevDerived = type(); m_autoType = value; Q_EMIT autoTypeChanged(); if (type() != prevDerived) { Q_EMIT typeChanged(); } } void Gpu::setName(QString value) { if (value == m_name) { return; } m_name = std::move(value); Q_EMIT nameChanged(); } void Gpu::setMemoryUsed(qreal value) { if (std::abs(m_memoryUsed - value) < 0.001) return; m_memoryUsed = value; Q_EMIT memoryUsedChanged(); } void Gpu::setMemoryTotal(qreal value) { if (std::abs(m_memoryTotal - value) < 0.001) return; m_memoryTotal = value; Q_EMIT memoryTotalChanged(); } void Gpu::tick() { const Type t = type(); if (t == Generic) { readGenericUsage(); readGpuTemperature(); } else if (t == 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(); } } } void Gpu::detectTypeOnce() { if (m_typeProc) { return; } 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) { return; } m_nameProc = new QProcess(this); QObject::connect( m_nameProc, &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; }); m_nameProc->start( QStringLiteral("sh"), {QStringLiteral("-c"), QString::fromLatin1(kNameDetectScript)}); } 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; ++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(); } } void Gpu::startNvidiaUsage() { if (m_nvidiaProc) { return; } m_nvidiaProc = new QProcess(this); QObject::connect( m_nvidiaProc, &QProcess::readyReadStandardOutput, this, [this]() { while (m_nvidiaProc->canReadLine()) { const QByteArray out = m_nvidiaProc->readLine(); const QString output = QString::fromUtf8(out).trimmed(); if (output.isEmpty()) continue; const QList parts = output.split(','); if (parts.size() < 4) return; bool ok1 = false; bool ok2 = false; bool ok3 = false; bool ok4 = false; const qreal usage = parts.at(0).trimmed().toDouble(&ok1) / 100.0; const qreal temp = parts.at(1).trimmed().toDouble(&ok2); const qreal memUsed = parts.at(2).trimmed().toDouble(&ok3); const qreal memTotal = parts.at(3).trimmed().toDouble(&ok4); if (ok1 && std::abs(usage - m_percentage) > 0.0001) { m_percentage = usage; Q_EMIT percentageChanged(); } if (ok2 && std::abs(temp - m_temperature) > 0.05) { m_temperature = temp; Q_EMIT temperatureChanged(); } if (ok3) { setMemoryUsed(memUsed); } if (ok4) { setMemoryTotal(memTotal); } } }); QObject::connect(m_nvidiaProc, &QProcess::finished, this, [this]() { m_nvidiaProc->deleteLater(); m_nvidiaProc = nullptr; }); QObject::connect( m_nvidiaProc, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) { m_nvidiaProc->deleteLater(); m_nvidiaProc = nullptr; }); m_nvidiaProc->start( QStringLiteral("nvidia-smi"), {QStringLiteral( "--query-gpu=utilization.gpu,temperature.gpu," "memory.used,memory.total"), QStringLiteral("--format=csv,noheader,nounits"), QStringLiteral("-lms"), QString::number(updateInterval())}); } void Gpu::readGpuTemperature() { const auto t = sensorslib::gpuPciAverageTemp(); const qreal newTemp = t.value_or(0.0); if (std::abs(newTemp - m_temperature) > 0.05) { m_temperature = newTemp; Q_EMIT temperatureChanged(); } } Gpu::Type Gpu::parseType(const QString& s) { const QString u = s.trimmed().toUpper(); if (u.isEmpty()) { return Auto; } if (u == QStringLiteral("NVIDIA")) { return Nvidia; } 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(); } } // namespace ZShell::services