|
|
|
@@ -0,0 +1,373 @@
|
|
|
|
|
/*
|
|
|
|
|
* main.cpp
|
|
|
|
|
* MontaukOS Power Manager - GUI equivalent of the `power` command.
|
|
|
|
|
*
|
|
|
|
|
* Polls the kernel's CPU power/thermal snapshot (SYS_POWERINFO) once per
|
|
|
|
|
* second and plots package temperature, average active frequency, and the
|
|
|
|
|
* thermal governor's frequency ceiling as scrolling line graphs, plus a
|
|
|
|
|
* details panel with the HWP/EPP/idle configuration.
|
|
|
|
|
* Copyright (c) 2026 Daniel Hammer
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <montauk/syscall.h>
|
|
|
|
|
#include <montauk/string.h>
|
|
|
|
|
#include <montauk/heap.h>
|
|
|
|
|
#include <gui/mtk.hpp>
|
|
|
|
|
|
|
|
|
|
extern "C" {
|
|
|
|
|
#include <stdio.h>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
using namespace gui;
|
|
|
|
|
|
|
|
|
|
static constexpr int INIT_W = 640;
|
|
|
|
|
static constexpr int INIT_H = 520;
|
|
|
|
|
static constexpr int PW_TOOLBAR_H = 36;
|
|
|
|
|
static constexpr int PW_POLL_MS = 1000;
|
|
|
|
|
static constexpr int PW_GRAPH_CAP = 120;
|
|
|
|
|
|
|
|
|
|
static constexpr Color PW_TOOLBAR_BG = Color::from_rgb(0xF5, 0xF5, 0xF5);
|
|
|
|
|
static constexpr Color PW_HEADER_BG = Color::from_rgb(0xF0, 0xF0, 0xF0);
|
|
|
|
|
static constexpr Color PW_BORDER = Color::from_rgb(0xD0, 0xD0, 0xD0);
|
|
|
|
|
static constexpr Color PW_DIM_TEXT = Color::from_rgb(0x66, 0x66, 0x66);
|
|
|
|
|
static constexpr Color PW_SUCCESS = Color::from_rgb(0x22, 0x88, 0x22);
|
|
|
|
|
static constexpr Color PW_WARNING = Color::from_rgb(0xCC, 0x88, 0x00);
|
|
|
|
|
static constexpr Color PW_DANGER = Color::from_rgb(0xCC, 0x33, 0x33);
|
|
|
|
|
static constexpr Color PW_INFO = Color::from_rgb(0x33, 0x66, 0xCC);
|
|
|
|
|
static constexpr Color PW_CEILING = Color::from_rgb(0x00, 0x88, 0x88);
|
|
|
|
|
|
|
|
|
|
struct PowerMgrState {
|
|
|
|
|
montauk::abi::PowerInfo info;
|
|
|
|
|
bool supported;
|
|
|
|
|
int mouse_x;
|
|
|
|
|
int mouse_y;
|
|
|
|
|
uint64_t last_poll_ms;
|
|
|
|
|
|
|
|
|
|
uint64_t temp_samples[PW_GRAPH_CAP];
|
|
|
|
|
uint64_t freq_samples[PW_GRAPH_CAP];
|
|
|
|
|
uint64_t ceiling_samples[PW_GRAPH_CAP];
|
|
|
|
|
mtk::GraphHistory temp_history;
|
|
|
|
|
mtk::GraphHistory freq_history;
|
|
|
|
|
mtk::GraphHistory ceiling_history;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static WsWindow g_win;
|
|
|
|
|
static PowerMgrState g_pw = {};
|
|
|
|
|
|
|
|
|
|
static mtk::Theme pw_theme() {
|
|
|
|
|
mtk::Theme theme = mtk::make_theme();
|
|
|
|
|
theme.surface = PW_TOOLBAR_BG;
|
|
|
|
|
theme.surface_alt = PW_HEADER_BG;
|
|
|
|
|
theme.border = PW_BORDER;
|
|
|
|
|
theme.text_muted = PW_DIM_TEXT;
|
|
|
|
|
theme.text_subtle = PW_DIM_TEXT;
|
|
|
|
|
return theme;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void init_graph_histories() {
|
|
|
|
|
mtk::graph_history_init(&g_pw.temp_history, g_pw.temp_samples, PW_GRAPH_CAP);
|
|
|
|
|
mtk::graph_history_init(&g_pw.freq_history, g_pw.freq_samples, PW_GRAPH_CAP);
|
|
|
|
|
mtk::graph_history_init(&g_pw.ceiling_history, g_pw.ceiling_samples, PW_GRAPH_CAP);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool read_info() {
|
|
|
|
|
return montauk::syscall1(montauk::abi::SYS_POWERINFO, (uint64_t)&g_pw.info) == 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool refresh_state(bool force) {
|
|
|
|
|
if (!g_pw.supported)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
uint64_t now = montauk::get_milliseconds();
|
|
|
|
|
if (!force && now - g_pw.last_poll_ms < PW_POLL_MS)
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
g_pw.last_poll_ms = now;
|
|
|
|
|
if (!read_info())
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
if (g_pw.info.tempC != 0)
|
|
|
|
|
mtk::graph_history_push(&g_pw.temp_history, g_pw.info.tempC);
|
|
|
|
|
mtk::graph_history_push(&g_pw.freq_history, g_pw.info.effMHz);
|
|
|
|
|
mtk::graph_history_push(&g_pw.ceiling_history, g_pw.info.curMaxPerf);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Rect toolbar_rect() {
|
|
|
|
|
return {0, 0, g_win.width, PW_TOOLBAR_H};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static Rect refresh_button_rect() {
|
|
|
|
|
int btn_h = 26;
|
|
|
|
|
int btn_y = (PW_TOOLBAR_H - btn_h) / 2;
|
|
|
|
|
return {8, btn_y, 82, btn_h};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool mouse_in_rect(const Rect& rect) {
|
|
|
|
|
return rect.contains(g_pw.mouse_x, g_pw.mouse_y);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void render_toolbar(Canvas& c, const mtk::Theme& theme, int fh) {
|
|
|
|
|
Rect bar = toolbar_rect();
|
|
|
|
|
c.fill_rect(bar.x, bar.y, bar.w, bar.h, theme.surface);
|
|
|
|
|
c.hline(bar.x, bar.y + bar.h - 1, bar.w, theme.border);
|
|
|
|
|
|
|
|
|
|
Rect refresh_rect = refresh_button_rect();
|
|
|
|
|
mtk::draw_button(c, refresh_rect, "Refresh", mtk::BUTTON_TONAL,
|
|
|
|
|
mtk::widget_state(false, mouse_in_rect(refresh_rect),
|
|
|
|
|
g_pw.supported), theme);
|
|
|
|
|
|
|
|
|
|
char status[96];
|
|
|
|
|
if (!g_pw.supported) {
|
|
|
|
|
snprintf(status, sizeof(status), "No power management");
|
|
|
|
|
} else if (g_pw.info.throttling) {
|
|
|
|
|
snprintf(status, sizeof(status), "%u C, avg %u MHz - THROTTLING",
|
|
|
|
|
(unsigned)g_pw.info.tempC, (unsigned)g_pw.info.effMHz);
|
|
|
|
|
} else if (g_pw.info.tempC != 0) {
|
|
|
|
|
snprintf(status, sizeof(status), "%u C, avg %u MHz",
|
|
|
|
|
(unsigned)g_pw.info.tempC, (unsigned)g_pw.info.effMHz);
|
|
|
|
|
} else {
|
|
|
|
|
snprintf(status, sizeof(status), "avg %u MHz", (unsigned)g_pw.info.effMHz);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int status_w = text_width(status);
|
|
|
|
|
Color status_color = (g_pw.supported && g_pw.info.throttling)
|
|
|
|
|
? PW_DANGER : theme.text;
|
|
|
|
|
c.text(g_win.width - status_w - 12, (PW_TOOLBAR_H - fh) / 2, status, status_color);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void render_graph(Canvas& c,
|
|
|
|
|
const Rect& rect,
|
|
|
|
|
const mtk::GraphHistory& history,
|
|
|
|
|
uint64_t max_value,
|
|
|
|
|
const char* title,
|
|
|
|
|
const char* value,
|
|
|
|
|
Color line,
|
|
|
|
|
const mtk::Theme& theme) {
|
|
|
|
|
mtk::GraphStyle style = mtk::make_graph_style(theme, line);
|
|
|
|
|
style.background = theme.window_bg;
|
|
|
|
|
style.plot_bg = Color::from_rgb(0xFA, 0xFA, 0xFA);
|
|
|
|
|
style.grid = mtk::mix(style.plot_bg, line, 16);
|
|
|
|
|
style.fill = mtk::lighten(line, 225);
|
|
|
|
|
mtk::draw_line_graph(c, rect, history, max_value, title, value, theme, style);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static constexpr int PW_DETAIL_ROWS = 4;
|
|
|
|
|
|
|
|
|
|
static int details_row_h(int fh) {
|
|
|
|
|
return fh + 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static int details_panel_h(int fh) {
|
|
|
|
|
// Mirrors the graph cards: padding, title line, 7px gap, then rows.
|
|
|
|
|
return 8 * 2 + fh + 7 + PW_DETAIL_ROWS * details_row_h(fh);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void render_details(Canvas& c, const Rect& rect,
|
|
|
|
|
const mtk::Theme& theme, int fh) {
|
|
|
|
|
mtk::draw_rounded_frame(c, rect, theme.radius_md, theme.window_bg, theme.border);
|
|
|
|
|
|
|
|
|
|
const montauk::abi::PowerInfo& info = g_pw.info;
|
|
|
|
|
int pad = 8;
|
|
|
|
|
int x = rect.x + pad;
|
|
|
|
|
int y = rect.y + pad;
|
|
|
|
|
int w = rect.w - pad * 2;
|
|
|
|
|
int row_h = details_row_h(fh);
|
|
|
|
|
char buf[96];
|
|
|
|
|
|
|
|
|
|
c.text(x, y, "Details", theme.text_subtle);
|
|
|
|
|
y += fh + 7;
|
|
|
|
|
|
|
|
|
|
struct Row {
|
|
|
|
|
const char* label;
|
|
|
|
|
const char* value;
|
|
|
|
|
Color color;
|
|
|
|
|
} rows[PW_DETAIL_ROWS];
|
|
|
|
|
|
|
|
|
|
char hwp[64];
|
|
|
|
|
if (info.hwpActive) {
|
|
|
|
|
snprintf(hwp, sizeof(hwp), "active (EPP 0x%02X, perf range %u-%u)",
|
|
|
|
|
(unsigned)info.epp, (unsigned)info.lowestPerf,
|
|
|
|
|
(unsigned)info.highestPerf);
|
|
|
|
|
} else {
|
|
|
|
|
snprintf(hwp, sizeof(hwp), "not active");
|
|
|
|
|
}
|
|
|
|
|
rows[0] = {"HWP", hwp, theme.text};
|
|
|
|
|
|
|
|
|
|
snprintf(buf, sizeof(buf), "base %u MHz, turbo %u MHz",
|
|
|
|
|
(unsigned)info.baseMHz, (unsigned)info.maxMHz);
|
|
|
|
|
rows[1] = {"Frequency", buf, theme.text};
|
|
|
|
|
|
|
|
|
|
char gov[64];
|
|
|
|
|
Color gov_color = theme.text;
|
|
|
|
|
if (!info.hwpActive) {
|
|
|
|
|
snprintf(gov, sizeof(gov), "inactive");
|
|
|
|
|
} else if (info.throttling) {
|
|
|
|
|
snprintf(gov, sizeof(gov), "THROTTLING - ceiling %u of %u",
|
|
|
|
|
(unsigned)info.curMaxPerf, (unsigned)info.highestPerf);
|
|
|
|
|
gov_color = PW_DANGER;
|
|
|
|
|
} else {
|
|
|
|
|
snprintf(gov, sizeof(gov), "not throttling (ceiling %u)",
|
|
|
|
|
(unsigned)info.curMaxPerf);
|
|
|
|
|
gov_color = PW_SUCCESS;
|
|
|
|
|
}
|
|
|
|
|
rows[2] = {"Governor", gov, gov_color};
|
|
|
|
|
|
|
|
|
|
char idle[48];
|
|
|
|
|
snprintf(idle, sizeof(idle), "MWAIT hint 0x%02X%s",
|
|
|
|
|
(unsigned)(info.apIdleHint & 0xFF),
|
|
|
|
|
info.apIdleHint == 0 ? " (C1 only)" : "");
|
|
|
|
|
rows[3] = {"AP idle", idle, theme.text};
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < PW_DETAIL_ROWS; i++) {
|
|
|
|
|
c.text(x, y, rows[i].label, theme.text);
|
|
|
|
|
int value_w = text_width(rows[i].value);
|
|
|
|
|
c.text(x + w - value_w, y, rows[i].value, rows[i].color);
|
|
|
|
|
y += row_h;
|
|
|
|
|
if (i < PW_DETAIL_ROWS - 1)
|
|
|
|
|
mtk::draw_separator(c, x, y - 5, w, theme);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void render_unsupported(Canvas& c, const mtk::Theme& theme, int fh) {
|
|
|
|
|
const char* line1 = "No CPU power management available";
|
|
|
|
|
const char* line2 = "(non-Intel CPU or running under emulation)";
|
|
|
|
|
int y = gui_max((g_win.height - fh * 2 - 8) / 2, PW_TOOLBAR_H + 12);
|
|
|
|
|
c.text(gui_max((g_win.width - text_width(line1)) / 2, 12), y,
|
|
|
|
|
line1, theme.text);
|
|
|
|
|
c.text(gui_max((g_win.width - text_width(line2)) / 2, 12), y + fh + 8,
|
|
|
|
|
line2, theme.text_muted);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void render_body(Canvas& c, const mtk::Theme& theme, int fh) {
|
|
|
|
|
const montauk::abi::PowerInfo& info = g_pw.info;
|
|
|
|
|
|
|
|
|
|
Rect body = {0, PW_TOOLBAR_H, g_win.width,
|
|
|
|
|
gui_max(g_win.height - PW_TOOLBAR_H, 0)};
|
|
|
|
|
int margin = 12;
|
|
|
|
|
int gap = 10;
|
|
|
|
|
int details_h = details_panel_h(fh);
|
|
|
|
|
int graph_area_h = gui_max(body.h - margin * 2 - gap * 2 - details_h, 96);
|
|
|
|
|
int temp_h = (graph_area_h * 3) / 5;
|
|
|
|
|
int row_h = graph_area_h - temp_h - gap;
|
|
|
|
|
int col_w = gui_max((body.w - margin * 2 - gap) / 2, 80);
|
|
|
|
|
int x0 = body.x + margin;
|
|
|
|
|
int y0 = body.y + margin;
|
|
|
|
|
int y1 = y0 + temp_h + gap;
|
|
|
|
|
int y2 = y1 + row_h + gap;
|
|
|
|
|
|
|
|
|
|
Rect temp_rect = {x0, y0, body.w - margin * 2, temp_h};
|
|
|
|
|
Rect freq_rect = {x0, y1, col_w, row_h};
|
|
|
|
|
Rect ceiling_rect = {x0 + col_w + gap, y1, col_w, row_h};
|
|
|
|
|
Rect details_rect = {x0, y2, body.w - margin * 2, details_h};
|
|
|
|
|
|
|
|
|
|
// Scale the plot to TjMax so the top edge is the hardware throttle
|
|
|
|
|
// point; the line turns red while the governor is actively limiting.
|
|
|
|
|
char temp_value[48];
|
|
|
|
|
if (info.tempC != 0) {
|
|
|
|
|
snprintf(temp_value, sizeof(temp_value), "%u C (throttle at %u C)",
|
|
|
|
|
(unsigned)info.tempC, (unsigned)info.tjMaxC);
|
|
|
|
|
} else {
|
|
|
|
|
snprintf(temp_value, sizeof(temp_value), "unavailable");
|
|
|
|
|
}
|
|
|
|
|
render_graph(c, temp_rect, g_pw.temp_history,
|
|
|
|
|
info.tjMaxC > 0 ? info.tjMaxC : 100,
|
|
|
|
|
"Package Temperature", temp_value,
|
|
|
|
|
info.throttling ? PW_DANGER : PW_WARNING, theme);
|
|
|
|
|
|
|
|
|
|
char freq_value[32];
|
|
|
|
|
snprintf(freq_value, sizeof(freq_value), "%u MHz", (unsigned)info.effMHz);
|
|
|
|
|
render_graph(c, freq_rect, g_pw.freq_history,
|
|
|
|
|
info.maxMHz > 0 ? info.maxMHz : 1,
|
|
|
|
|
"Avg Frequency", freq_value, PW_INFO, theme);
|
|
|
|
|
|
|
|
|
|
char ceiling_value[32];
|
|
|
|
|
snprintf(ceiling_value, sizeof(ceiling_value), "%u / %u",
|
|
|
|
|
(unsigned)info.curMaxPerf, (unsigned)info.highestPerf);
|
|
|
|
|
render_graph(c, ceiling_rect, g_pw.ceiling_history,
|
|
|
|
|
info.highestPerf > 0 ? info.highestPerf : 1,
|
|
|
|
|
"Frequency Ceiling", ceiling_value, PW_CEILING, theme);
|
|
|
|
|
|
|
|
|
|
render_details(c, details_rect, theme, fh);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void render() {
|
|
|
|
|
mtk::StandaloneHost host(&g_win);
|
|
|
|
|
Canvas c = host.canvas();
|
|
|
|
|
mtk::Theme theme = pw_theme();
|
|
|
|
|
c.fill(theme.window_bg);
|
|
|
|
|
|
|
|
|
|
int fh = system_font_height();
|
|
|
|
|
render_toolbar(c, theme, fh);
|
|
|
|
|
|
|
|
|
|
if (g_pw.supported)
|
|
|
|
|
render_body(c, theme, fh);
|
|
|
|
|
else
|
|
|
|
|
render_unsupported(c, theme, fh);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool handle_mouse(const montauk::abi::WinEvent& ev) {
|
|
|
|
|
bool refresh_hover_before = mouse_in_rect(refresh_button_rect());
|
|
|
|
|
|
|
|
|
|
g_pw.mouse_x = ev.mouse.x;
|
|
|
|
|
g_pw.mouse_y = ev.mouse.y;
|
|
|
|
|
|
|
|
|
|
bool redraw = refresh_hover_before != mouse_in_rect(refresh_button_rect());
|
|
|
|
|
|
|
|
|
|
bool left_pressed = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1);
|
|
|
|
|
if (!left_pressed)
|
|
|
|
|
return redraw;
|
|
|
|
|
|
|
|
|
|
if (refresh_button_rect().contains(ev.mouse.x, ev.mouse.y))
|
|
|
|
|
return refresh_state(true) || redraw;
|
|
|
|
|
return redraw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
extern "C" void _start() {
|
|
|
|
|
if (!fonts::init())
|
|
|
|
|
montauk::exit(1);
|
|
|
|
|
|
|
|
|
|
if (!g_win.create("Power", INIT_W, INIT_H))
|
|
|
|
|
montauk::exit(1);
|
|
|
|
|
|
|
|
|
|
montauk::memset(&g_pw, 0, sizeof(g_pw));
|
|
|
|
|
init_graph_histories();
|
|
|
|
|
g_pw.mouse_x = -1;
|
|
|
|
|
g_pw.mouse_y = -1;
|
|
|
|
|
g_pw.supported = read_info();
|
|
|
|
|
|
|
|
|
|
refresh_state(true);
|
|
|
|
|
render();
|
|
|
|
|
g_win.present();
|
|
|
|
|
|
|
|
|
|
while (!g_win.closed) {
|
|
|
|
|
bool redraw = refresh_state(false);
|
|
|
|
|
|
|
|
|
|
montauk::abi::WinEvent ev;
|
|
|
|
|
int r = g_win.poll(&ev);
|
|
|
|
|
if (r < 0) break;
|
|
|
|
|
|
|
|
|
|
if (r == 0) {
|
|
|
|
|
if (redraw) {
|
|
|
|
|
render();
|
|
|
|
|
g_win.present();
|
|
|
|
|
}
|
|
|
|
|
montauk::sleep_ms(16);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ev.type == 3)
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
if (ev.type == 1) redraw |= handle_mouse(ev);
|
|
|
|
|
else if (ev.type == 2 || ev.type == 4) redraw = true;
|
|
|
|
|
|
|
|
|
|
if (redraw) {
|
|
|
|
|
render();
|
|
|
|
|
g_win.present();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
g_win.destroy();
|
|
|
|
|
montauk::exit(0);
|
|
|
|
|
}
|