// SPDX-License-Identifier: MIT #include "cpupage.h" #include #include #include #include QVariantMap CpuPage::Editor::value() const { QVariantMap result{{"frequencyOverride", frequency->isChecked() && bounds.isEmpty()}, {"minimum", minimum->value()}, {"maximum", maximum->value()}, {"governor", governor->currentText()}, {"preference", preference->currentText()}}; if (!bounds.isEmpty()) { QVariantMap groups; for (auto it = bounds.cbegin(); it != bounds.cend(); ++it) groups[it.key()] = QVariantMap{ {"frequencyOverride", it->enabled->isChecked()}, {"minimum", it->minimum->value()}, {"maximum", it->maximum->value()}}; result["bounds"] = groups; } return result; } void CpuPage::Editor::setValue(const QVariantMap &value) { minimum->setValue(value["minimum"].toInt()); maximum->setValue(value["maximum"].toInt()); frequency->setChecked(value.value("frequencyOverride").toBool()); minimum->setEnabled(frequency->isChecked()); maximum->setEnabled(frequency->isChecked()); governor->setCurrentText(value.value("governor", "auto").toString()); preference->setCurrentText(value.value("preference", "auto").toString()); for (auto it = bounds.cbegin(); it != bounds.cend(); ++it) { const auto p = value.contains("bounds") ? value["bounds"].toMap().value(it.key()).toMap() : value; it->minimum->setValue(p.value("minimum", it->minimum->minimum()).toInt()); it->maximum->setValue(p.value("maximum", it->maximum->maximum()).toInt()); it->enabled->setChecked(p.value("frequencyOverride").toBool()); it->minimum->setEnabled(it->enabled->isChecked()); it->maximum->setEnabled(it->enabled->isChecked()); } } CpuPage::Editor CpuPage::makeEditor(const QString &name) { Editor editor{new QGroupBox(name), new QCheckBox("Override frequency bounds"), new ValueControl, new ValueControl, new QComboBox, new QComboBox, new QLabel}; editor.frequency->setObjectName("overrideFrequency"); editor.governor->setObjectName("cpuGovernor"); editor.preference->setObjectName("cpuPreference"); editor.warning->setWordWrap(true); editor.warning->setObjectName("governorWarning"); auto *layout = new QFormLayout(editor.group); const auto limits = cpuLimits(); layout->addRow(editor.frequency); editor.frequency->setToolTip("When unchecked, this tool does not write frequency limits. Existing limits are left unchanged."); for (auto *box : {editor.minimum, editor.maximum}) { box->setRange(limits.value("low", 400).toInt(), limits.value("high", 5000).toInt()); box->setSuffix(" MHz"); box->setSingleStep(100); connect(box, &ValueControl::valueChanged, this, [this] { changed(); }); } editor.governor->addItem("auto"); editor.preference->addItem("auto"); editor.governor->addItems(cpuOptions("scaling_available_governors")); editor.preference->addItems(cpuOptions("energy_performance_available_preferences")); for (auto *combo : {editor.governor, editor.preference}) { combo->setToolTip("Auto leaves this setting to Linux or other controllers; it does not restore a preset. Save and apply commits changes."); connect(combo, &QComboBox::currentTextChanged, this, [this] { changed(); }); } layout->addRow("Minimum frequency:", editor.minimum); layout->addRow("Maximum frequency:", editor.maximum); const auto groups = cpuPolicyGroups(); if (!groups.isEmpty()) { layout->setRowVisible(editor.frequency, false); layout->setRowVisible(editor.minimum, false); layout->setRowVisible(editor.maximum, false); for (const QString &key : {QString("p"), QString("e")}) { Editor::Bounds b{new QCheckBox(key == "p" ? "Override P-core frequency bounds" : "Override E-core frequency bounds"), new ValueControl, new ValueControl}; const auto limits = cpuLimits("/sys", key); for (auto *box : {b.minimum, b.maximum}) { box->setRange(limits["low"].toInt(), limits["high"].toInt()); box->setSuffix(" MHz"); box->setSingleStep(100); connect(box, &ValueControl::valueChanged, this, [this] { changed(); }); } b.enabled->setObjectName("overrideFrequency_" + key); layout->addRow(b.enabled); layout->addRow("Minimum frequency:", b.minimum); layout->addRow("Maximum frequency:", b.maximum); connect(b.enabled, &QCheckBox::toggled, this, [this, b](bool enabled) { b.minimum->setEnabled(enabled); b.maximum->setEnabled(enabled); changed(); }); editor.bounds.insert(key, b); } } layout->addRow("Energy preference:", editor.preference); auto *row = new QWidget; auto *line = new QHBoxLayout(row); line->setContentsMargins(0, 0, 0, 0); editor.governor->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); line->addWidget(editor.governor); line->addWidget(editor.warning, 1); line->addStretch(); layout->addRow("CPU governor:", row); connect(editor.frequency, &QCheckBox::toggled, this, [this, editor](bool enabled) { editor.minimum->setEnabled(enabled); editor.maximum->setEnabled(enabled); changed(); }); return editor; } CpuPage::CpuPage(QWidget *parent) : QWidget(parent) { auto *layout = new QVBoxLayout(this); m_status = new QLabel; m_status->setWordWrap(true); layout->addWidget(m_status); m_separate = new QCheckBox("Use separate profiles for battery power and AC"); m_separate->setObjectName("separateCpuProfiles"); layout->addWidget(m_separate); m_separate->setToolTip("Splitting copies settings to both sides. Joining keeps the battery settings, committed only with Save and apply."); auto *columns = new QHBoxLayout; m_battery = makeEditor("CPU settings"); m_ac = makeEditor("AC power"); m_battery.group->setObjectName("batteryCpuProfile"); m_ac.group->setObjectName("acCpuProfile"); columns->addWidget(m_battery.group, 1); columns->addWidget(m_ac.group, 1); layout->addLayout(columns); layout->addStretch(); connect(m_separate, &QCheckBox::toggled, this, &CpuPage::split); load({}, true); } void CpuPage::changed() { if (!m_loading) { // Joining hides the AC draft; it must survive refresh until saved or undone. const auto savedAc = m_saved.value(m_saved.value("separate").toBool() ? "ac" : "battery").toMap(); m_dirty = draft() != m_saved || (m_hasAcDraft && !m_separate->isChecked() && m_ac.value() != savedAc); refreshStatus(); Q_EMIT draftChanged(); } } void CpuPage::split(bool separate) { if (separate && !m_hasAcDraft) { m_ac.setValue(m_battery.value()); m_hasAcDraft = true; } m_ac.group->setVisible(separate); m_battery.group->setTitle(separate ? "Battery power" : "CPU settings"); changed(); } QVariantMap CpuPage::draft() const { QVariantMap config{{"separate", m_separate->isChecked()}, {"battery", m_battery.value()}}; if (m_separate->isChecked()) config["ac"] = m_ac.value(); return config; } void CpuPage::load(const QVariantMap &config, bool force) { if (m_dirty && !force) return; m_loading = true; auto saved = normalizeCpuConfig(config); if (saved.isEmpty()) { const auto limits = cpuLimits(); saved = {{"separate", false}, {"battery", QVariantMap{{"frequencyOverride", false}, {"minimum", limits.value("min", 400)}, {"maximum", limits.value("max", 5000)}, {"governor", "auto"}, {"preference", "auto"}}}}; } m_saved = saved; m_battery.setValue(saved["battery"].toMap()); m_hasAcDraft = saved["separate"].toBool(); if (m_hasAcDraft) m_ac.setValue(saved["ac"].toMap()); else m_ac.setValue(saved["battery"].toMap()); m_separate->setChecked(m_hasAcDraft); split(m_hasAcDraft); m_saved = draft(); m_loading = false; m_dirty = false; refreshStatus(); Q_EMIT draftChanged(); } void CpuPage::refreshStatus() { const auto limits = cpuLimits(); const auto ac = onAcPower(); m_status->setText(limits.isEmpty() ? "Live CPU readings unavailable." : QString("Live: %1\nEnergy preference: %3\nGovernor: %2\nFrequency bounds: %4–%5 MHz (highest across policies); hardware maximum: %6 MHz.%7") .arg(ac ? (*ac ? "AC power" : "battery power") : "power source unknown", limits["governor"].toString(), limits["preference"].toString()) .arg(limits["min"].toInt()).arg(limits["max"].toInt()).arg(limits["high"].toInt()) .arg(m_dirty ? "\nUnsaved changes." : "")); auto warning = [&](Editor &editor, bool active) { QString text; const auto p = editor.value(); const QString governor = p["governor"].toString(), pref = p["preference"].toString(); if (readText("/sys/devices/system/cpu/cpufreq/policy0/scaling_driver") == "intel_pstate") { if (governor == "performance" && pref == "auto") text = "⚠ Requires performance EPP; may fall back to powersave."; else if (governor == "performance" && pref != "performance") text = "⚠ Requires performance EPP; will fall back to powersave."; else if (governor == "auto" && pref != "auto" && pref != "performance" && active && limits["governor"] == "performance") text = "⚠ Current Performance governor blocks this EPP. Select Powersave or leave EPP on Auto."; } editor.warning->setText(text); editor.warning->setVisible(!text.isEmpty()); }; warning(m_battery, !m_separate->isChecked() || (ac && !*ac)); warning(m_ac, m_separate->isChecked() && ac && *ac); }