From bae5456f295956373786b8c6ed080568a553651d Mon Sep 17 00:00:00 2001
From: Oksion
Date: Wed, 29 Jul 2026 17:37:51 +0500
Subject: [PATCH 1/4] =?UTF-8?q?feat(monitor):=20=D1=80=D1=8F=D0=B4=20GPU?=
=?UTF-8?q?=20=E2=80=94=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=BA=D0=B0?=
=?UTF-8?q?,=20=D0=B2=D0=B0=D1=82=D1=82=D1=8B=20=D0=B8=20=D1=87=D0=B0?=
=?UTF-8?q?=D1=81=D1=82=D0=BE=D1=82=D0=B0=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7?=
=?UTF-8?q?=20Intel=20IGCL?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Виджет «Монитор» получил ряд GPU между CPU и RAM: процент + график, под ним
частота и мощность («2450 МГц · 21.4 Вт»); в мини-виде только процент.
Канал — Intel IGCL, user-mode API графического драйвера: ControlLib.dll лежит
в System32 и приезжает со штатным драйвером Intel, поэтому ничего не
устанавливаем и прав администратора для чтения не нужно — тот же driver-free
принцип, что и WMI. Всё берётся одним ctlPowerTelemetryGet; загрузка и ватты
считаются как приращения накопительных счётчиков (секунды активности, джоули).
Проверено на TM2424 бенчмарком (829 выборок / 216 с): загрузка 0.9 → 99.7 %,
мощность до 24 Вт, частота 900 → 2500 МГц, корреляция загрузки и мощности
r = 0.988; загрузка сверена с \GPU Engine(*)\Utilization Percentage — сходится.
Опрос стоит 47 мкс (медиана), поэтому живёт в том же таймере 1 Гц без воркера.
Границы канала: температуру, обороты вентиляторов и напряжение IGCL на
встроенном GPU не отдаёт — все ctlEnum* отвечают отказом, это пути для
дискретных Arc. Температуры как раньше через DPTF. Инициализация ленивая:
пока виджет не открыт, IGCL в процесс не подгружается. Не Intel (в т.ч. AMD) —
ctlInit не поднимется или DllImport бросит DllNotFoundException, ряд просто не
появится, виджет остаётся прежним.
Цвет ряда — Amber 300 #FFD54F: у GPU оба соседа по рядам холодные (CPU синий,
RAM сиреневая), а тёплые оранжевый разряд и коралловая температура стоят через
ряд; cyan сливался с CPU, розово-фиолетовые — с RAM, плюс красно-розовая зона
уже занята смыслом «горячо».
Раскладка ctl_power_telemetry_t, замеры и оговорки — в docs/09; в ROADMAP
поправлен пункт «🚫 Не берём»: «GPU не отдаётся» больше не верно целиком,
без вентиляторов и GPU-режима, но с телеметрией.
Co-Authored-By: Claude Opus 5
---
CHANGELOG.md | 10 ++
CLAUDE.md | 7 +-
ROADMAP.md | 11 +-
docs/09-power-monitoring.md | 53 +++++++++
docs/10-colors.md | 6 +
src/Localization/lang/en.json | 1 +
src/Localization/lang/ru.json | 1 +
src/Localization/lang/zh.json | 1 +
src/SystemIntegration/GpuTelemetry.cs | 160 ++++++++++++++++++++++++++
src/Ui/MonitorForm.cs | 62 ++++++++--
10 files changed, 299 insertions(+), 13 deletions(-)
create mode 100644 src/SystemIntegration/GpuTelemetry.cs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c417d82..8203a5a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,16 @@
## [Unreleased]
### Добавлено
+- **Загрузка GPU в «Мониторе»** — новый ряд между CPU и RAM: процент + график, под ним частота и
+ мощность (`2450 МГц · 21.4 Вт`), в мини-виде только процент. Данные — через **Intel IGCL**,
+ user-mode API графического драйвера (`ControlLib.dll` приезжает со штатным драйвером Intel и лежит
+ в System32): driver-free, прав администратора для чтения не нужно, опрос стоит 47 мкс и живёт в том
+ же таймере 1 Гц, что и остальные графики. Загрузка сверена с собственным счётчиком Windows
+ (`\GPU Engine(*)\Utilization Percentage`) — сходится. Температуру и обороты вентиляторов IGCL на
+ встроенном GPU не даёт (пути только для дискретных Arc), поэтому в ряду их нет. Инициализация
+ ленивая: пока виджет не открыт, IGCL в процесс не подгружается. Не Intel (в т.ч. AMD-модели) —
+ ряд просто не появляется, виджет остаётся прежним. Разбор раскладки телеметрии и замеры —
+ [docs/09-power-monitoring.md](docs/09-power-monitoring.md).
- **`OwlIgnoreDisplay` в config.json** — «Режим совы» умеет держать бодрой только систему, отпуская
экран: с `true` он больше не выставляет `ES_DISPLAY_REQUIRED`, и экран гаснет по обычному таймауту
схемы питания. Для сценария «комп не должен уснуть ради удалённого доступа», где подсвеченная
diff --git a/CLAUDE.md b/CLAUDE.md
index 2e00cb3..985fdf6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -141,7 +141,7 @@ WMI-событий). `Program.cs`: single-instance mutex → DI-контейне
`TravelChargeMonitor` (SystemIntegration).
- `src/Ui/` остальное: `QuickPanelForm` (панель по удержанию Mi / клику по трею — чистый view
над контроллером, навигация с клавиатуры; ширина фиксированная — при скрытых режимах
- растягиваются ячейки), `OsdForm` (всплывашки), `MonitorForm` (виджет Вт/CPU/RAM/°C),
+ растягиваются ячейки), `OsdForm` (всплывашки), `MonitorForm` (виджет Вт/CPU/GPU/RAM/°C),
`FlyoutForm` + `FlyoutPalette` (общая база флайаутов: borderless tool-window, Region, Esc;
палитра — единственный источник тёмных цветов флайаутов), `FormChrome` (DWM-тёмный
заголовок + WM_SETREDRAW), `ModeUi` (режим → ключ локализации / вид OSD / акцент),
@@ -170,7 +170,10 @@ WMI-событий). `Program.cs`: single-instance mutex → DI-контейне
`AutoStart` (задача планировщика + самопочинка пути), `AwakeMode` («режим совы»: всегда
`ES_SYSTEM_REQUIRED` + крышка на AC, экран держится `ES_DISPLAY_REQUIRED` — кроме
`OwlIgnoreDisplay: true` в config.json, тогда только «не спать»),
- `MicControl`, `KeyActions`, `Sound` (WAV-джинглы), `BatteryInfo`, `PowerDraw`;
+ `MicControl`, `KeyActions`, `Sound` (WAV-джинглы), `BatteryInfo`, `PowerDraw`,
+ `GpuTelemetry` (загрузка/ватты/частота iGPU через **Intel IGCL** — user-mode API драйвера Intel,
+ `ControlLib.dll` из System32; driver-free, без админа, ленивая инициализация; не Intel → ряд GPU
+ в «Мониторе» просто не появляется, см. `docs/09`);
**HTTP API (XIC-13, opt-in)** — `HttpApi` (хост на `HttpListener`/http.sys, без ASP.NET Core;
создаётся только при включённой фиче → выключено = 0 CPU), `ApiRouter` (авторизация Bearer+SHA-256
constant-time и белый список маршрутов — тестируется на фейках), `ApiSettings`/`ApiSettingsStore`
diff --git a/ROADMAP.md b/ROADMAP.md
index 98f11c2..e993893 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -55,6 +55,11 @@
через Intel DPTF: WMI `root\wmi\EsifDeviceInformation`, `.Temperature` уже в °C, берём максимум
среди активных доменов. Ранее числился закрытым как «невозможно» — переоткрыт на проверенном
сигнале (см. ниже). Горячая зона графика подсвечивается вишнёвым (порог от ACPI `CriticalTripPoint`) (unreleased).
+- **Загрузка GPU в «Мониторе»** — ряд между CPU и RAM: процент + график, под ним частота и мощность
+ (`2450 МГц · 21.4 Вт`); в мини-виде только процент. Driver-free через **Intel IGCL** — user-mode API
+ графического драйвера (`ControlLib.dll` из System32, прав администратора не нужно даже на чтение):
+ один `ctlPowerTelemetryGet`, загрузка и ватты считаются как приращения накопительных счётчиков.
+ Не Intel → ряда просто нет. Разбор и замеры — [docs/09-power-monitoring.md](docs/09-power-monitoring.md) (unreleased).
- **Сенсорный экран вкл/выкл** — то же, что тачпад, для тач-экрана ноутбука: действие для клавиш +
ячейка в панели (левее тачпада, только если тачскрин есть) + OSD. Общая база `HidNodeToggle`
(тачпад `U:0005`, тачскрин `U:0004`). Вклад контрибьютора (PR #4) (unreleased).
@@ -120,7 +125,11 @@
- Ручной цикл частоты экрана по клавише — не нужна: висела «если попросят», запросов не было,
авто-герцовка по питанию покрывает сценарий (XIC-2 отменена; вернёмся, если попросят тестеры).
- ICC-профили, splash-скины, служба + named pipe + protobuf (архитектура MIControl).
-- GPU-режим, обороты вентиляторов — WMI на нашей модели не отдаёт (температуры — берём через DPTF, см. «Сделано»).
+- **Обороты вентиляторов** — WMI на нашей модели не отдаёт, EC закрыт своим драйвером; в Intel IGCL
+ `ctlEnumFans` на встроенном GPU тоже отказ (это путь для дискретных Arc, как и `ctlEnumTemperatureSensors`).
+ GPU-**режим** (переключение/профили самого GPU) — тоже нет. При этом **телеметрия** iGPU
+ ✅ **сделана** driver-free через IGCL (загрузка/ватты/частота, см. «Сделано»), а температуры — через
+ DPTF; так что «GPU не отдаётся» больше не верно целиком.
- WinRing0 / прямой доступ к EC — цель проекта в том, чтобы обойтись без них.
- Lottie-анимации — заменены алгоритмическими (+10 МБ и второй рендер-стек не окупаются).
- NFC-фичи (метки → действия) — модуль есть в железе, но в Windows как устройство не выведен
diff --git a/docs/09-power-monitoring.md b/docs/09-power-monitoring.md
index 5b6864e..bba9c96 100644
--- a/docs/09-power-monitoring.md
+++ b/docs/09-power-monitoring.md
@@ -83,3 +83,56 @@ Driver-free (только чтение), на нём построены:
Строка-график «горячей точки» в «Мониторе» — driver-free через **Intel DPTF** (WMI
`root\wmi\EsifDeviceInformation`, `.Temperature` в °C, максимум активных доменов). Тема раньше
числилась закрытой как «невозможно», переоткрыта на этом сигнале — см. ROADMAP и `05-open-questions.md`.
+
+## GPU: загрузка, мощность, частота (Intel IGCL)
+
+Ряд GPU берётся из **Intel IGCL** — user-mode API графического драйвера. `ControlLib.dll` лежит в
+`System32` и приезжает со штатным драйвером Intel: **мы ничего не устанавливаем, и права
+администратора для чтения не нужны** — тот же driver-free принцип, что и WMI. Реализация —
+`src/SystemIntegration/GpuTelemetry.cs`, инициализация ленивая (пока виджет не открыли, IGCL в
+процесс не подгружается).
+
+Всё берётся одним вызовом `ctlPowerTelemetryGet`. Раскладка `ctl_power_telemetry_t` (проверено на
+TM2424, `ControlLib 1.2.291.0`, адаптер `0x8086:0xB080`): структура 512 байт, внутри цепочка
+элементов `ctl_oc_telemetry_item_t` = `uint bSupported; uint units; uint type; pad; double value`.
+
+| смещение | поле | единицы | есть? |
+|---|---|---|---|
+| 8 | `timeStamp` | секунды | да |
+| 32 | `gpuEnergyCounter` | джоули | да |
+| 56 | `gpuVoltage` | — | **нет** |
+| 80 | `gpuCurrentClockFrequency` | МГц | да |
+| 104 | `gpuCurrentTemperature` | — | **нет** |
+| 128 | `globalActivityCounter` | секунды активности | да |
+| 152 | `renderComputeActivityCounter` | секунды активности | да |
+| 176 | `mediaActivityCounter` | секунды активности | да |
+
+Счётчики **накопительные**, поэтому загрузка и ватты — приращения:
+`загрузка % = Δactivity / Δtime · 100`, `Вт = Δjoules / Δtime`. Первая выборка после открытия
+виджета только набирает базу и даёт `NaN` — график начинается со второй точки. При повторном
+открытии зовём `Reset()`, иначе первое значение размажется по времени, что виджет был закрыт.
+
+Тонкости и границы:
+
+- `ctl_version_info_t` — это **простой `uint32`** (`major << 16 | minor`), а не структура из двух
+ `uint16`. `ctl_init_args_t` = 36 байт, `AppVersion = 1.0`; реализация рапортует 1.1.
+- На старте сверяем `bSupported`/`units`/`type` по всем четырём используемым смещениям. Не совпало —
+ выключаемся навсегда: лучше не показать ряд, чем показать мусор.
+- **Температуры и оборотов вентиляторов IGCL на встроенном GPU не даёт**: все `ctlEnum*`
+ (`TemperatureSensors`, `Fans`, `PowerDomains`, `EngineGroups`, `FrequencyDomains`) отвечают
+ отказом — это пути для дискретных Arc. Температуры мы и так берём через DPTF (выше).
+- `renderCompute` почти дублирует `global` (разница ~1.3 п.п. под нагрузкой), `media` под 3D-нагрузкой
+ всегда 0 — в UI выведен только `global`.
+- AMD-модели: IGCL там нет (`ctlInit` не поднимется, либо `ControlLib.dll` вообще отсутствует и
+ P/Invoke бросит `DllNotFoundException`) — ряд просто не появится. Аналог у AMD — `atiadlxx.dll`,
+ отдельная история.
+
+### Проверено на железе (TM2424, 2026-07-29)
+
+Прогон бенчмарка, 829 выборок / 216 с: загрузка 0.9 → 99.7 %, мощность 0.01 → **24 Вт**,
+частота 900 → **2500 МГц** (под длительной нагрузкой оседает на 2390–2440 — троттлинг),
+корреляция загрузки и мощности **r = 0.988**. Сверка загрузки с собственным счётчиком Windows
+`\GPU Engine(*)\Utilization Percentage`: IGCL ~42.5 % против Windows ~45.4 % — сходится.
+Цена вызова: **медиана 47 мкс**, p95 77 мкс (кадр при 60 Гц — 16666 мкс), поэтому опрос живёт
+в том же UI-таймере 1 Гц, без воркера. `ctlInit` + enumerate — 8.9 мс однократно при первом показе;
+приватная память не растёт (`ControlLib.dll` 296 КБ и `IntelControlLib.dll` 712 КБ мапятся как образы).
diff --git a/docs/10-colors.md b/docs/10-colors.md
index 2efc7b1..ea71405 100644
--- a/docs/10-colors.md
+++ b/docs/10-colors.md
@@ -90,6 +90,12 @@
|---|---|
| OSD карточка / рамка | `#1C1C1E` / `#46464A` |
| Монитор — CPU | `#5AAAFF` |
+| Монитор — GPU (Amber 300) | `#FFD54F` |
| Монитор — RAM | `#B39DDB` |
| Монитор — Температура (норма/горячо) | `#FF6F61` / `#CE203E` |
| Монитор — заряд / разряд | `#34C759` / `#FF9500` |
+
+В «Мониторе» цвет выбирается **с оглядкой на соседние ряды**, а не сам по себе. Порядок рядов —
+питание → CPU → GPU → RAM → температура, поэтому у GPU оба соседа холодные (синий, сиреневый), и
+тёплый Amber 300 читается там лучше всего; холодные варианты (cyan) сливались с синим CPU, а
+розово-фиолетовые — с сиреневой RAM, плюс красно-розовая зона уже занята смыслом «горячо» (`#CE203E`).
diff --git a/src/Localization/lang/en.json b/src/Localization/lang/en.json
index c641711..24bbf0d 100644
--- a/src/Localization/lang/en.json
+++ b/src/Localization/lang/en.json
@@ -184,6 +184,7 @@
"monitor.ram.of": "{0:0.0} / {1:0.0} GB",
"monitor.adapter": "Adapter {0} W",
"monitor.watts.scale": "{0:0} W",
+ "monitor.gpu.sub": "{0:0} MHz · {1:0.0} W",
"monitor.temp": "Temperature",
"monitor.temp.c": "{0} °C",
"osd.charging": "Charging",
diff --git a/src/Localization/lang/ru.json b/src/Localization/lang/ru.json
index 633e13f..585c3a6 100644
--- a/src/Localization/lang/ru.json
+++ b/src/Localization/lang/ru.json
@@ -184,6 +184,7 @@
"monitor.ram.of": "{0:0.0} / {1:0.0} ГБ",
"monitor.adapter": "Адаптер {0} Вт",
"monitor.watts.scale": "{0:0} Вт",
+ "monitor.gpu.sub": "{0:0} МГц · {1:0.0} Вт",
"monitor.temp": "Температура",
"monitor.temp.c": "{0} °C",
"osd.charging": "Зарядка",
diff --git a/src/Localization/lang/zh.json b/src/Localization/lang/zh.json
index e3ce1e0..8c88614 100644
--- a/src/Localization/lang/zh.json
+++ b/src/Localization/lang/zh.json
@@ -184,6 +184,7 @@
"monitor.ram.of": "{0:0.0} / {1:0.0} GB",
"monitor.adapter": "适配器 {0} 瓦",
"monitor.watts.scale": "{0:0} 瓦",
+ "monitor.gpu.sub": "{0:0} MHz · {1:0.0} 瓦",
"monitor.temp": "温度",
"monitor.temp.c": "{0} °C",
"osd.charging": "充电中",
diff --git a/src/SystemIntegration/GpuTelemetry.cs b/src/SystemIntegration/GpuTelemetry.cs
new file mode 100644
index 0000000..0eac65c
--- /dev/null
+++ b/src/SystemIntegration/GpuTelemetry.cs
@@ -0,0 +1,160 @@
+using System.Runtime.InteropServices;
+
+namespace XiControl.SystemIntegration;
+
+///
+/// Телеметрия встроенного GPU через Intel IGCL — user-mode API графического драйвера
+/// (ControlLib.dll приезжает со штатным драйвером Intel и лежит в System32). Мы ничего не
+/// устанавливаем и не требуем прав администратора — это тот же driver-free принцип, что и WMI.
+/// Отдаём загрузку %, мощность Вт и частоту МГц: температуры и обороты вентиляторов IGCL на
+/// встроенном GPU не даёт (все ctlEnum* отвечают отказом — это пути для дискретных Arc).
+///
+/// Загрузка и мощность — приращения накопительных счётчиков (секунды активности и джоули),
+/// поэтому первый вызов только запоминает базу и возвращает false. Инициализация ленивая: пока
+/// «Монитор» не открыли, IGCL в процесс не подгружается вообще.
+///
+/// Нет драйвера Intel (в т.ч. AMD-модели) или структура телеметрии разложена иначе →
+/// выключаемся навсегда, остаётся false, «Монитор» просто не рисует ряд.
+///
+public sealed class GpuTelemetry : IDisposable
+{
+ // ctl_power_telemetry_t на ControlLib 1.2.291.0; проверено на TM2424 (Intel, 0x8086:0xB080).
+ // Внутри — цепочка ctl_oc_telemetry_item_t: uint bSupported; uint units; uint type; pad; double value.
+ private const int TelemetrySize = 512;
+ private const int OffTime = 8; // timeStamp, секунды
+ private const int OffEnergy = 32; // gpuEnergyCounter, джоули
+ private const int OffClock = 80; // gpuCurrentClockFrequency, МГц
+ private const int OffActive = 128; // globalActivityCounter, секунды активности
+ private const int ValueOffset = 16; // double внутри элемента
+
+ // ctl_units_t / ctl_data_type_t — сверяем на старте, чтобы не считать мусор при другой раскладке
+ private const uint UnitsMHz = 0, UnitsJoules = 6, UnitsSeconds = 7, TypeDouble = 9;
+
+ private const int InitArgsSize = 36; // ctl_init_args_t: Size, Version, AppVersion, flags, SupportedVersion, GUID
+ private const int AppVersion = 1 << 16; // ctl_version_info_t — это uint32 (major << 16 | minor), т.е. 1.0
+
+ private IntPtr _api, _dev, _buf;
+ private bool _off; // IGCL не поднялся / раскладка не та — больше не пытаемся
+ private bool _tried; // инициализацию уже делали (успешно или нет)
+
+ private double _prevTime, _prevEnergy, _prevActive;
+ private bool _hasPrev;
+
+ /// IGCL поднялся и телеметрия отдаёт нужные поля (ряд GPU в «Мониторе» имеет смысл).
+ public bool Available => !_off && _tried;
+
+ /// Забыть базу — следующий вызов начнёт отсчёт заново (при повторном открытии виджета,
+ /// иначе первое значение размажется по всему времени, что «Монитор» был закрыт).
+ public void Reset() => _hasPrev = false;
+
+ ///
+ /// Загрузка GPU (0..100), мощность (Вт) и текущая частота (МГц). false — телеметрия недоступна
+ /// либо это первая выборка и разностей ещё нет; значения в этом случае не осмысленны.
+ ///
+ public bool TryRead(out float loadPct, out float watts, out float mhz)
+ {
+ loadPct = watts = mhz = float.NaN;
+ if (!_tried) Init();
+ if (_off) return false;
+
+ try
+ {
+ if (!Fetch()) { _off = true; return false; }
+
+ double t = Value(OffTime), energy = Value(OffEnergy), active = Value(OffActive);
+ mhz = (float)Value(OffClock);
+
+ if (!_hasPrev)
+ {
+ (_prevTime, _prevEnergy, _prevActive) = (t, energy, active);
+ _hasPrev = true;
+ return false; // база есть, разности будут со следующего тика
+ }
+
+ double dt = t - _prevTime;
+ if (dt <= 0) return false; // счётчик времени не двинулся (или перезапустился) — тик пропускаем
+
+ loadPct = (float)Math.Clamp((active - _prevActive) / dt * 100.0, 0.0, 100.0);
+ watts = (float)Math.Max((energy - _prevEnergy) / dt, 0.0);
+ (_prevTime, _prevEnergy, _prevActive) = (t, energy, active);
+ return true;
+ }
+ catch (Exception ex) { Log.Ex("GpuTelemetry.Read", ex); _off = true; return false; }
+ }
+
+ private void Init()
+ {
+ _tried = true;
+ try
+ {
+ IntPtr args = Marshal.AllocHGlobal(InitArgsSize);
+ try
+ {
+ Zero(args, InitArgsSize);
+ Marshal.WriteInt32(args, 0, InitArgsSize);
+ Marshal.WriteInt32(args, 8, AppVersion);
+ if (ctlInit(args, out _api) != 0) { _off = true; return; }
+ }
+ finally { Marshal.FreeHGlobal(args); }
+
+ uint count = 0;
+ if (ctlEnumerateDevices(_api, ref count, null) != 0 || count == 0) { Close(); return; }
+ var devices = new IntPtr[count];
+ if (ctlEnumerateDevices(_api, ref count, devices) != 0) { Close(); return; }
+ _dev = devices[0]; // встроенный GPU — единственный адаптер на наших моделях
+
+ _buf = Marshal.AllocHGlobal(TelemetrySize);
+ if (!Fetch() || !LayoutMatches()) { Close(); return; }
+ }
+ catch (Exception ex) { Log.Ex("GpuTelemetry.Init", ex); Close(); } // нет ControlLib.dll (не Intel) и т.п.
+ }
+
+ /// Поля лежат там, где мы их ждём, и поддержаны железом — иначе цифрам верить нельзя.
+ private bool LayoutMatches() =>
+ Item(OffTime, UnitsSeconds) && Item(OffEnergy, UnitsJoules) &&
+ Item(OffClock, UnitsMHz) && Item(OffActive, UnitsSeconds);
+
+ private bool Item(int off, uint units) =>
+ (uint)Marshal.ReadInt32(_buf, off) == 1 &&
+ (uint)Marshal.ReadInt32(_buf, off + 4) == units &&
+ (uint)Marshal.ReadInt32(_buf, off + 8) == TypeDouble;
+
+ private bool Fetch()
+ {
+ Zero(_buf, TelemetrySize);
+ Marshal.WriteInt32(_buf, 0, TelemetrySize);
+ return ctlPowerTelemetryGet(_dev, _buf) == 0;
+ }
+
+ private double Value(int off) => BitConverter.Int64BitsToDouble(Marshal.ReadInt64(_buf, off + ValueOffset));
+
+ private static void Zero(IntPtr p, int len)
+ {
+ for (int i = 0; i < len; i++) Marshal.WriteByte(p, i, 0);
+ }
+
+ private void Close()
+ {
+ _off = true;
+ if (_buf != IntPtr.Zero) { Marshal.FreeHGlobal(_buf); _buf = IntPtr.Zero; }
+ if (_api != IntPtr.Zero)
+ {
+ try
+ {
+ int r = ctlClose(_api);
+ if (r != 0) Log.Write($"GpuTelemetry: ctlClose -> 0x{r:X8}");
+ }
+ catch (Exception ex) { Log.Ex("GpuTelemetry.Close", ex); }
+ _api = IntPtr.Zero;
+ }
+ _dev = IntPtr.Zero;
+ }
+
+ public void Dispose() => Close();
+
+ private const string Lib = "ControlLib.dll";
+ [DllImport(Lib)] private static extern int ctlInit(IntPtr args, out IntPtr api);
+ [DllImport(Lib)] private static extern int ctlClose(IntPtr api);
+ [DllImport(Lib)] private static extern int ctlEnumerateDevices(IntPtr api, ref uint count, IntPtr[]? devices);
+ [DllImport(Lib)] private static extern int ctlPowerTelemetryGet(IntPtr device, IntPtr telemetry);
+}
diff --git a/src/Ui/MonitorForm.cs b/src/Ui/MonitorForm.cs
index 3163757..96fcb6a 100644
--- a/src/Ui/MonitorForm.cs
+++ b/src/Ui/MonitorForm.cs
@@ -9,7 +9,7 @@ namespace XiControl.Ui;
///
/// «Монитор»: живые графики с момента открытия — потребление (Вт, датчик батареи),
-/// CPU % и RAM %. Семплирование (1 Гц) работает только пока окно видно.
+/// CPU %, GPU % (Intel IGCL, если есть) и RAM %. Семплирование (1 Гц) работает только пока окно видно.
/// Ватты честные только от батареи/на зарядке: на питании от сети датчика нет — «—».
/// Три вида — полный (графики), мини (строка Power/CPU/RAM), только ватты;
/// переключаются кнопкой «вид» или двойным кликом по виджету, выбор запоминается.
@@ -20,6 +20,7 @@ public sealed class MonitorForm : FlyoutForm
private static readonly Color DischargeCol = FlyoutPalette.Orange; // разряд батареи (вниз)
private static readonly Color ChargeCol = FlyoutPalette.Green; // заряд в батарею (вверх)
private static readonly Color CpuCol = FlyoutPalette.Blue;
+ private static readonly Color GpuCol = Color.FromArgb(255, 213, 79); // Amber 300: соседи по рядам холодные (CPU, RAM), а тёплые — через ряд
private static readonly Color RamCol = Color.FromArgb(179, 157, 219); // сиреневый (зелёный ушёл под заряд)
private static readonly Color TempCol = Color.FromArgb(255, 111, 97); // коралловый — температура (норма)
private static readonly Color TempHotCol = Color.FromArgb(206, 32, 62); // вишнёвый — горячая/крит-зона (сочно на OLED)
@@ -35,6 +36,7 @@ public sealed class MonitorForm : FlyoutForm
private readonly System.Windows.Forms.Timer _tick = new() { Interval = 1000 };
private readonly List _power = new(); // Вт со знаком: + заряд в батарею, − разряд; NaN = от сети без заряда
private readonly List _cpu = new(); // 0..100
+ private readonly List _gpu = new(); // 0..100; NaN = нет данных
private readonly List _ram = new(); // 0..100
private readonly List _temp = new(); // °C горячей точки (Intel DPTF); NaN = нет данных
@@ -43,9 +45,12 @@ public sealed class MonitorForm : FlyoutForm
private ManagementObjectSearcher? _battery;
private ManagementObjectSearcher? _thermal;
private readonly SystemIntegration.PowerDraw _powerDraw = new(); // живая мощность через Battery IOCTL
+ private readonly SystemIntegration.GpuTelemetry _gpuTel = new(); // iGPU через Intel IGCL (ленивая инициализация)
private long _prevIdle, _prevKernel, _prevUser;
private float _ramUsedGb, _ramTotalGb;
private int _adapterWatts; // ватты подключённого PD-БП (0 — нет/не PD); MIFS, driver-free
+ private float _gpuMhz, _gpuWatts; // текущая частота и мощность GPU — в подстроку ряда
+ private bool _hasGpu; // IGCL поднялся → резервируем ряд GPU (иначе виджет как раньше)
private bool _hasTemp; // на этой модели DPTF отдаёт температуры → резервируем строку/высоту
private bool _tempOff; // класс DPTF отсутствует — больше не опрашиваем
private float _critC; // критический порог, °C (0 = неизвестен); из ACPI-термозоны, best-effort
@@ -89,9 +94,10 @@ public void Popup()
{
if (Visible) { Hide(); return; }
- _power.Clear(); _cpu.Clear(); _ram.Clear(); _temp.Clear();
+ _power.Clear(); _cpu.Clear(); _gpu.Clear(); _ram.Clear(); _temp.Clear();
_prevIdle = _prevKernel = _prevUser = 0;
- Sample(); // первая точка сразу (заодно определит наличие DPTF-температур до ApplyView)
+ _gpuTel.Reset(); // иначе первая загрузка GPU размажется по времени, что виджет был закрыт
+ Sample(); // первая точка сразу (заодно определит наличие DPTF-температур и IGCL до ApplyView)
ApplyView();
@@ -116,8 +122,9 @@ private void ApplyView()
int w, h;
switch (_view)
{
- case ViewKind.Mini: // Power | CPU | RAM + [развернуть]/[вид]/[крестик] справа
- w = Sc(16) + Sc(104) + Sc(8) + Sc(72) + Sc(8) + Sc(72) + Sc(8) + Sc(18) + Sc(6) + Sc(18) + Sc(6) + Sc(18) + Sc(10);
+ case ViewKind.Mini: // Power | CPU | [GPU] | RAM + [развернуть]/[вид]/[крестик] справа
+ w = Sc(16) + Sc(104) + Sc(8) + Sc(72) + Sc(8) + (_hasGpu ? Sc(72) + Sc(8) : 0)
+ + Sc(72) + Sc(8) + Sc(18) + Sc(6) + Sc(18) + Sc(6) + Sc(18) + Sc(10);
h = Sc(56);
_corner = Sc(14);
_close = new Rectangle(w - Sc(10) - Sc(18), (h - Sc(18)) / 2, Sc(18), Sc(18));
@@ -132,7 +139,8 @@ private void ApplyView()
_expandBtn = Rectangle.Empty;
break;
default:
- w = Sc(400); h = Sc(96) * (_hasTemp ? 4 : 3) + Sc(52); // + строка температуры, если DPTF отдаёт
+ // базовые три ряда (питание, CPU, RAM) + GPU, если есть IGCL, + температура, если есть DPTF
+ w = Sc(400); h = Sc(96) * (3 + (_hasGpu ? 1 : 0) + (_hasTemp ? 1 : 0)) + Sc(52);
_corner = Sc(18);
_close = new Rectangle(w - Sc(16) - Sc(22), Sc(14), Sc(22), Sc(22)); // как в панели
_viewBtn = new Rectangle(_close.X - Sc(28), _close.Y, Sc(22), Sc(22));
@@ -241,12 +249,30 @@ protected override void OnMouseLeave(EventArgs e)
private void Sample()
{
Push(_cpu, SampleCpu());
+ Push(_gpu, SampleGpu());
Push(_ram, SampleRam());
Push(_power, SamplePowerWatts());
Push(_temp, SampleTempC());
try { _adapterWatts = _mifs.GetAdapterWatts(); } catch (Exception ex) { Log.Ex("Monitor.Adapter", ex); _adapterWatts = 0; }
}
+ ///
+ /// Загрузка встроенного GPU через Intel IGCL — driver-free, права администратора не нужны
+ /// (детали и раскладка телеметрии — в ). Заодно
+ /// запоминаем частоту и мощность для подстроки ряда. Первая выборка после открытия виджета
+ /// только набирает базу счётчиков — она даёт NaN, график начинается со второй точки.
+ /// IGCL нет (не Intel) → ряд GPU не показываем вовсе.
+ ///
+ private float SampleGpu()
+ {
+ bool ok = _gpuTel.TryRead(out float load, out float watts, out float mhz);
+ if (!_gpuTel.Available) return float.NaN; // не Intel / телеметрия не та — ряда не будет
+ _hasGpu = true;
+ if (!ok) return float.NaN;
+ (_gpuMhz, _gpuWatts) = (mhz, watts);
+ return load;
+ }
+
///
/// Температура «горячей точки» через Intel DPTF (WMI EsifDeviceInformation) — driver-free:
/// провайдер идёт со штатными драйверами Intel. Берём максимум среди активных доменов — это
@@ -391,15 +417,28 @@ protected override void OnPaint(PaintEventArgs e)
? (pw >= 0 ? "+" : "") + Loc.T("monitor.watts", MathF.Abs(pw)) // всегда положительное число; направление — цветом
: Loc.T("monitor.na");
+ // ряды идут подряд, а GPU и температура опциональны → позицию считаем счётчиком, а не множителем
+ int row = 0;
+ Rectangle Next() => new(Sc(16), top + rowH * row++, Width - Sc(32), rowH);
+
float powerMax = NiceMax(_power);
- DrawRow(g, new Rectangle(Sc(16), top, Width - Sc(32), rowH),
+ DrawRow(g, Next(),
Loc.T("monitor.power"), powerText, pColor, _power, powerMax,
sub: _adapterWatts > 0 ? Loc.T("monitor.adapter", _adapterWatts) : null, // рейтинг подключённого БП
scaleLabel: Loc.T("monitor.watts.scale", powerMax),
pick: v => v >= 0f ? ChargeCol : DischargeCol); // цвет по направлению тока
- DrawRow(g, new Rectangle(Sc(16), top + rowH, Width - Sc(32), rowH),
+ DrawRow(g, Next(),
"CPU", _cpu.Count > 0 && !float.IsNaN(_cpu[^1]) ? $"{_cpu[^1]:0}%" : "—", CpuCol, _cpu, 100f);
- DrawRow(g, new Rectangle(Sc(16), top + rowH * 2, Width - Sc(32), rowH),
+
+ if (_hasGpu)
+ {
+ float gl = _gpu.Count > 0 ? _gpu[^1] : float.NaN;
+ DrawRow(g, Next(),
+ "GPU", float.IsNaN(gl) ? "—" : $"{gl:0}%", GpuCol, _gpu, 100f,
+ _gpuMhz > 0 ? Loc.T("monitor.gpu.sub", _gpuMhz, _gpuWatts) : null); // «2450 МГц · 21 Вт»
+ }
+
+ DrawRow(g, Next(),
"RAM", _ram.Count > 0 ? $"{_ram[^1]:0}%" : "—", RamCol, _ram, 100f,
_ramTotalGb > 0 ? Loc.T("monitor.ram.of", _ramUsedGb, _ramTotalGb) : null);
@@ -407,7 +446,7 @@ protected override void OnPaint(PaintEventArgs e)
{
float tc = _temp.Count > 0 ? _temp[^1] : float.NaN;
Color now = !float.IsNaN(tc) && tc >= HotAt ? TempHotCol : TempCol; // текущее значение — вишнёвым, если горячо
- DrawRow(g, new Rectangle(Sc(16), top + rowH * 3, Width - Sc(32), rowH),
+ DrawRow(g, Next(),
Loc.T("monitor.temp"), float.IsNaN(tc) ? "—" : Loc.T("monitor.temp.c", (int)tc), now, _temp, TempMax,
scaleLabel: Loc.T("monitor.temp.c", (int)TempMax),
pick: v => v >= HotAt ? TempHotCol : TempCol); // горячая зона на графике — вишнёвая
@@ -421,6 +460,8 @@ private void PaintMini(Graphics g, float pw, Color pColor)
int x = Sc(16);
x = MiniCell(g, x, Sc(104), "Power", powerVal, pColor);
x = MiniCell(g, x, Sc(72), "CPU", _cpu.Count > 0 && !float.IsNaN(_cpu[^1]) ? $"{_cpu[^1]:0}%" : "—", CpuCol);
+ if (_hasGpu) // в мини-виде у GPU только процент — частота и ватты живут в полном виде
+ x = MiniCell(g, x, Sc(72), "GPU", _gpu.Count > 0 && !float.IsNaN(_gpu[^1]) ? $"{_gpu[^1]:0}%" : "—", GpuCol);
MiniCell(g, x, Sc(72), "RAM", _ram.Count > 0 ? $"{_ram[^1]:0}%" : "—", RamCol);
Draw.ExpandButton(g, _expandBtn, _expandHover); // развернуть в полный
@@ -546,6 +587,7 @@ protected override void Dispose(bool disposing)
_battery?.Dispose();
_thermal?.Dispose();
_powerDraw.Dispose();
+ _gpuTel.Dispose();
}
base.Dispose(disposing);
}
From c6d90f493b7c296da09e5279cb98d746d6b4c98a Mon Sep 17 00:00:00 2001
From: Oksion
Date: Wed, 29 Jul 2026 17:54:55 +0500
Subject: [PATCH 2/4] =?UTF-8?q?docs:=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA?=
=?UTF-8?q?=D0=B8=20=D0=BD=D0=B0=20XIC-18=20=D0=B2=20ROADMAP=20=D0=B8=20CH?=
=?UTF-8?q?ANGELOG?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Задача в Linear заведена после реализации, поэтому буллет в «✅ Сделано» и запись
в «[Unreleased]» номера не несли. Дописан «(XIC-18)» — как у XIC-4, чтобы связь
кода с трекером читалась из файлов, а не только из истории ветки.
Co-Authored-By: Claude Opus 5
---
CHANGELOG.md | 2 +-
ROADMAP.md | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8203a5a..d5e0151 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,7 +15,7 @@
встроенном GPU не даёт (пути только для дискретных Arc), поэтому в ряду их нет. Инициализация
ленивая: пока виджет не открыт, IGCL в процесс не подгружается. Не Intel (в т.ч. AMD-модели) —
ряд просто не появляется, виджет остаётся прежним. Разбор раскладки телеметрии и замеры —
- [docs/09-power-monitoring.md](docs/09-power-monitoring.md).
+ [docs/09-power-monitoring.md](docs/09-power-monitoring.md) (XIC-18).
- **`OwlIgnoreDisplay` в config.json** — «Режим совы» умеет держать бодрой только систему, отпуская
экран: с `true` он больше не выставляет `ES_DISPLAY_REQUIRED`, и экран гаснет по обычному таймауту
схемы питания. Для сценария «комп не должен уснуть ради удалённого доступа», где подсвеченная
diff --git a/ROADMAP.md b/ROADMAP.md
index e993893..9fdb2ba 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -59,7 +59,7 @@
(`2450 МГц · 21.4 Вт`); в мини-виде только процент. Driver-free через **Intel IGCL** — user-mode API
графического драйвера (`ControlLib.dll` из System32, прав администратора не нужно даже на чтение):
один `ctlPowerTelemetryGet`, загрузка и ватты считаются как приращения накопительных счётчиков.
- Не Intel → ряда просто нет. Разбор и замеры — [docs/09-power-monitoring.md](docs/09-power-monitoring.md) (unreleased).
+ Не Intel → ряда просто нет. Разбор и замеры — [docs/09-power-monitoring.md](docs/09-power-monitoring.md) (XIC-18, unreleased).
- **Сенсорный экран вкл/выкл** — то же, что тачпад, для тач-экрана ноутбука: действие для клавиш +
ячейка в панели (левее тачпада, только если тачскрин есть) + OSD. Общая база `HidNodeToggle`
(тачпад `U:0005`, тачскрин `U:0004`). Вклад контрибьютора (PR #4) (unreleased).
From 6fc6dec1484c4b870c3458efbf954349fff8ac44 Mon Sep 17 00:00:00 2001
From: Oksion
Date: Wed, 29 Jul 2026 18:01:20 +0500
Subject: [PATCH 3/4] =?UTF-8?q?docs(readme):=20=D1=80=D1=8F=D0=B4=20GPU=20?=
=?UTF-8?q?=D0=B2=20=D0=BE=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D0=B8=20?=
=?UTF-8?q?=C2=AB=D0=9C=D0=BE=D0=BD=D0=B8=D1=82=D0=BE=D1=80=D0=B0=C2=BB=20?=
=?UTF-8?q?(RU/EN)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Подписи к скриншотам и раздел «Ограничения» перечисляли показатели виджета,
но про GPU не знали. Дописано: ряд GPU с частотой и ваттами в подстроке,
GPU в компактной строке, и отдельный пункт ограничений — канал Intel IGCL,
без графики Intel ряда нет, температуру и вентиляторы он на iGPU не отдаёт.
Скриншоты docs/img/monitor*.png сняты до фичи и ряда GPU не показывают —
перезаписать их можно только с живого виджета, оставлено владельцу.
Co-Authored-By: Claude Opus 5
---
README.md | 14 +++++++++-----
README.ru.md | 14 +++++++++-----
2 files changed, 18 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md
index 0ca5045..8dfdcb6 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ auto refresh rate and "owl mode" (stay awake).*
-*Monitor window: live graphs of power draw (W), CPU and RAM usage.*
+*Monitor window: live graphs of power draw (W), CPU, GPU and RAM usage.*
@@ -35,10 +35,10 @@ auto refresh rate and "owl mode" (stay awake).*
-*In full view — live graphs: power draw (W), CPU, RAM, **hotspot temperature**
-(hot zone in cherry red) and the connected adapter's wattage. Collapses into a compact line
-(Power / CPU / RAM) or a single watts readout — via the "view" button or a double-click;
-current direction is shown by color (charging green / discharging orange).*
+*In full view — live graphs: power draw (W), CPU, **GPU** (clock and watts under the percentage),
+RAM, **hotspot temperature** (hot zone in cherry red) and the connected adapter's wattage. Collapses
+into a compact line (Power / CPU / GPU / RAM) or a single watts readout — via the "view" button or a
+double-click; current direction is shown by color (charging green / discharging orange).*
*The tray icon changes with the active mode:*
@@ -411,6 +411,10 @@ None of this runs or spends resources while the API is off (the server simply is
- The feature set depends on the model: firmware telemetry (fan RPM) is unsupported on the tested
machine. **Temperature** is shown anyway — not from the firmware, but via Intel DPTF
(WMI `EsifDeviceInformation`), as a line graph in the Monitor.
+- **GPU usage** comes from Intel IGCL — the graphics driver's user-mode API (`ControlLib.dll` in
+ System32, installed with the Intel driver; no administrator rights needed). On machines without
+ Intel graphics the GPU row simply does not appear. This channel exposes no temperature or fan RPM
+ for integrated GPUs — only usage, power and clock.
## How it works
diff --git a/README.ru.md b/README.ru.md
index 6d35b2c..c740c69 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -28,7 +28,7 @@
-*Окно «Монитор»: живые графики потребления (Вт), загрузки CPU и RAM.*
+*Окно «Монитор»: живые графики потребления (Вт), загрузки CPU, GPU и RAM.*
@@ -36,10 +36,10 @@
-*В полном виде — живые графики: потребление (Вт), CPU, RAM, **температура** «горячей точки»
-(горячая зона — вишнёвым) и мощность подключённого адаптера. Сворачивается в компактную строку
-(Power / CPU / RAM) или в один показатель ватт — кнопкой «вид» или двойным кликом; направление
-тока — цветом (заряд зелёный / разряд оранжевый).*
+*В полном виде — живые графики: потребление (Вт), CPU, **GPU** (под процентом — частота и ватты),
+RAM, **температура** «горячей точки» (горячая зона — вишнёвым) и мощность подключённого адаптера.
+Сворачивается в компактную строку (Power / CPU / GPU / RAM) или в один показатель ватт — кнопкой
+«вид» или двойным кликом; направление тока — цветом (заряд зелёный / разряд оранжевый).*
*Значок в трее меняется по активному режиму:*
@@ -412,6 +412,10 @@ curl -X POST http://192.168.1.50:58125/travel \
- Набор функций зависит от модели: прошивочная телеметрия (обороты вентиляторов) на проверенной
машине не поддерживается. **Температуру** при этом показываем — не из прошивки, а через Intel DPTF
(WMI `EsifDeviceInformation`), строкой-графиком в «Мониторе».
+- **Загрузка GPU** идёт через Intel IGCL — user-mode API графического драйвера (`ControlLib.dll`
+ из System32, ставится вместе с драйвером Intel; прав администратора не требует). На машинах без
+ графики Intel ряд GPU в «Мониторе» просто не появляется. Температуру и обороты вентиляторов этот
+ канал на встроенном GPU не отдаёт — только загрузку, мощность и частоту.
## Как это работает
From 2f35bf6815be791f45c267ac0992886d42665049 Mon Sep 17 00:00:00 2001
From: Oksion
Date: Wed, 29 Jul 2026 19:07:44 +0500
Subject: [PATCH 4/4] =?UTF-8?q?docs(readme):=20=D1=81=D0=B2=D0=B5=D0=B6?=
=?UTF-8?q?=D0=B8=D0=B5=20=D1=81=D0=BA=D1=80=D0=B8=D0=BD=D1=88=D0=BE=D1=82?=
=?UTF-8?q?=D1=8B=20=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB=D0=B8=20=D0=B8=20=C2=AB?=
=?UTF-8?q?=D0=9C=D0=BE=D0=BD=D0=B8=D1=82=D0=BE=D1=80=D0=B0=C2=BB,=20?=
=?UTF-8?q?=D0=BF=D0=BE=20=D1=8F=D0=B7=D1=8B=D0=BA=D0=B0=D0=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Скриншоты в репозитории были русскими и общими для обоих README — то есть
английская версия показывала русский интерфейс. Заодно они отставали от кода:
в «Мониторе» не было ни GPU, ни температуры, RAM был зелёным (цвет давно ушёл
под заряд), а на панели Эко рисовалось луной (XIC-14), не было шестерёнки
(XIC-12), сенсорного экрана и нового порога заряда.
Введена схема по языку, как у самих README: без суффикса — английские
(README.md), с «.ru» — русские (README.ru.md). Существующие русские ватты
переименованы через git mv, остальное — новые снимки:
monitor.png / monitor.ru.png полный вид, 5 рядов с GPU
monitor-mini.png / monitor-mini.ru.png мини: Power / CPU / GPU / RAM
monitor-watts.png / monitor-watts.ru.png только ватты
quick-panel.png / quick-panel.ru.png панель
tray.png не тронут — он языко-нейтральный.
Co-Authored-By: Claude Opus 5
---
README.ru.md | 8 ++++----
docs/img/monitor-mini.png | Bin 9783 -> 12175 bytes
docs/img/monitor-mini.ru.png | Bin 0 -> 12517 bytes
docs/img/monitor-watts.png | Bin 2986 -> 3714 bytes
docs/img/monitor-watts.ru.png | Bin 0 -> 2986 bytes
docs/img/monitor.png | Bin 33846 -> 72639 bytes
docs/img/monitor.ru.png | Bin 0 -> 78504 bytes
docs/img/quick-panel.png | Bin 48380 -> 51002 bytes
docs/img/quick-panel.ru.png | Bin 0 -> 52460 bytes
9 files changed, 4 insertions(+), 4 deletions(-)
create mode 100644 docs/img/monitor-mini.ru.png
create mode 100644 docs/img/monitor-watts.ru.png
create mode 100644 docs/img/monitor.ru.png
create mode 100644 docs/img/quick-panel.ru.png
diff --git a/README.ru.md b/README.ru.md
index c740c69..293d905 100644
--- a/README.ru.md
+++ b/README.ru.md
@@ -17,7 +17,7 @@
**Никакого WinRing0**, никаких сторонних драйверов и прямого доступа к EC.
-
+
*Панель быстрых настроек (удержание кнопки Mi): пять режимов производительности,
@@ -25,15 +25,15 @@
авто-герцовка и «режим совы» (не спать).*
-
+
*Окно «Монитор»: живые графики потребления (Вт), загрузки CPU, GPU и RAM.*
-
+
-
+
*В полном виде — живые графики: потребление (Вт), CPU, **GPU** (под процентом — частота и ватты),
diff --git a/docs/img/monitor-mini.png b/docs/img/monitor-mini.png
index d4184a89c5149cf7a14c92b182365e5837d7046f..647e41f517a259682925f3fd56476f6c10f78043 100644
GIT binary patch
literal 12175
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
U@bfWX;CTR0l2el{mo@?a53+L|;s5{u
literal 9783
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