From 35e5aba23fa04701805d559f7e96821c2a1b4e45 Mon Sep 17 00:00:00 2001 From: Daniel Hammer Date: Sun, 5 Jul 2026 10:51:11 +0200 Subject: [PATCH] feat: HWP scaling, C1E, closed-loop thermal governor for Intel CPUs --- docs/power.md | 117 ++++++ kernel/src/ACPI/AcpiSleep.cpp | 4 + kernel/src/ACPI/CpuIdle.cpp | 87 +++- kernel/src/ACPI/CpuIdle.hpp | 4 + kernel/src/Api/BuildNo.hpp | 2 +- kernel/src/Api/Power.hpp | 25 ++ kernel/src/Api/Syscall.cpp | 3 + kernel/src/Api/Syscall.hpp | 19 + kernel/src/Hal/Cpu.hpp | 12 +- kernel/src/Hal/CpuPower.cpp | 390 ++++++++++++++++++ kernel/src/Hal/CpuPower.hpp | 54 +++ kernel/src/Hal/SmpBoot.cpp | 6 + kernel/src/Main.cpp | 6 + kernel/src/Sched/Scheduler.cpp | 11 + programs/include/Api/Syscall.hpp | 19 + programs/include/libc/montauk.h | 1 + .../obj/src/libprintersapplet.o | Bin 19664 -> 19720 bytes .../obj/src/libtimezonesapplet.o | Bin 19808 -> 19856 bytes programs/src/power/main.cpp | 152 +++++++ 19 files changed, 903 insertions(+), 9 deletions(-) create mode 100644 docs/power.md create mode 100644 kernel/src/Hal/CpuPower.cpp create mode 100644 kernel/src/Hal/CpuPower.hpp create mode 100644 programs/src/power/main.cpp diff --git a/docs/power.md b/docs/power.md new file mode 100644 index 0000000..cd3c17c --- /dev/null +++ b/docs/power.md @@ -0,0 +1,117 @@ +# CPU Power and Thermal Management + +MontaukOS manages CPU frequency, idle states, and package temperature on +Intel hardware. Without this, a modern many-core laptop part (the reference +target is a TUXEDO Gemini Gen2 with an i9-13900HX, 24 cores / 32 threads) +runs at whatever P-state the firmware left it, never drops below C1 when +idle, and heats until the hardware throttles at TjMax - or until the +embedded controller intervenes (on Clevo boards: forced full-volume beeping, +then a hard power-off). + +Everything below is feature-gated by CPUID, so virtual machines (KVM masks +the power bits in CPUID.06H) and non-Intel CPUs skip it safely. + +## Components + +### HWP frequency scaling (`Hal/CpuPower.cpp`) + +`Hal::CpuPower::InitializeBsp()` enables Hardware P-states +(IA32_PM_ENABLE), reads the performance range from IA32_HWP_CAPABILITIES, +and programs IA32_HWP_REQUEST with: + +- min = lowest performance level, max = highest +- desired = 0 (fully autonomous hardware frequency selection) +- EPP = 0x80 (balanced; same as Linux "balance_performance") + +This is the same mechanism intel_pstate uses on Linux and is the single +biggest thermal win: the CPU idles near its minimum frequency and ramps +only while there is work. APs apply the same request in `ApEntry` via +`InitializeAp()` (HWP request MSRs are per-logical-CPU). + +CPUs with EPB but no HWP get IA32_ENERGY_PERF_BIAS = 6 (balanced) instead. + +### C1E promotion + +MSR_POWER_CTL bit 1 lets HLT-idle drop core voltage/frequency to minimum +(C1E) instead of parking at the current ratio. Set per-core on the BSP and +every AP. Gated on the DTS CPUID bit as a real-hardware heuristic, because +MSR_POWER_CTL is not architectural and hypervisors may not emulate it. + +### Thermal governor (`Hal/CpuPower.cpp`) + +A closed-loop passive-cooling controller on the BSP, stepped from +`Sched::RunBspMaintenance()` (reached from both the idle loop and the +timer tick, so it keeps running under full load). Every 500 ms it reads +the package digital thermal sensor (IA32_PACKAGE_THERM_STATUS readout +relative to TjMax from MSR_TEMPERATURE_TARGET) and adjusts the HWP +performance ceiling: + +- **>= 85 C (passive):** step the ceiling down 2 ratio units (~200 MHz) +- **>= 94 C (hot):** step down 8 units per interval +- **<= 78 C (resume):** step back up 1 unit per interval +- between resume and passive: hold (hysteresis) + +The ceiling propagates through a policy epoch counter: each CPU compares +the epoch in `Sched::Tick()` (and the AP idle loop) against the last one +it applied and rewrites its own IA32_HWP_REQUEST when it changed, since +MSR writes only affect the executing core. APs tick every 10 ms, so a +throttle decision reaches all cores within one tick. + +Throttle engage/release transitions are logged (WARNING/OK, "CpuPower"). + +### Deep AP idle (`ACPI/CpuIdle.cpp`) + +APs use deep MWAIT C-states while idle; C6 power-gates the core entirely, +which is where most of the idle-heat win lives on many-core parts. + +The hint comes from ACPI `_CST` FFH entries (the GAS address low byte is +the MWAIT hint, same convention as Linux), validated against the CPUID.05H +sub-state enumeration and capped at the C6 family (0x2F). With no usable +`_CST`, the deepest CPUID-enumerated hint (up to C6) is used. + +**The BSP stays in C1 (HLT).** On some laptops deep MWAIT states freeze +the LAPIC timer even though ARAT is advertised, and the BSP's periodic +1 ms tick is the system timekeeper (see the comment in +`Timekeeping::IdleOnce`). APs have no timekeeping duty: an idle AP is +woken by the reschedule IPI, and its coarse tick resumes on wake. + +### S3 wake + +IA32_PM_ENABLE, HWP requests, and MSR_POWER_CTL do not survive suspend. +`Hal::CpuPower::ReapplyAfterWake()` runs in the S3 resume path, redoes the +BSP setup, and bumps the policy epoch so every AP re-applies its HWP +request (and re-checks C1E) on its next tick. + +## Diagnostics + +`SYS_POWERINFO` (149) fills a `PowerInfo` struct: package temperature, +TjMax, HWP state and performance range, the governor's current ceiling, +CPUID base/turbo frequency, the measured average active frequency +(APERF/MPERF over the 500 ms governor window), and the AP idle MWAIT hint. +Returns -1 when the CPU exposes no power features (e.g. under QEMU). + +The `power` program (`programs/src/power`) prints it: + +``` +power one-shot snapshot +power watch 1 Hz samples for 60 s (temp, avg freq, ceiling) +power watch 300 same, for 300 s +``` + +Boot log lines to look for (Kernel Log app): + +``` +[CpuPower] HWP enabled: perf range 4-54, EPP=0x80 +[CpuPower] Thermal governor armed: TjMax=100C, passive=85C, hot=94C, ... +[CpuIdle] AP deep idle: MWAIT hint 0x20 +``` + +## Tuning + +The governor constants live at the top of `Hal/CpuPower.cpp` +(`PassiveTempC`, `HotTempC`, `ResumeTempC`, `GovernorIntervalMs`, step +sizes) and were chosen to keep the package well below both TjMax (100 C +on the i9-13900HX) and the Clevo EC's emergency thresholds. If the fans +still spin up aggressively under sustained load, lower `PassiveTempC` +first; if builds feel throttled while cool, raise the recovery rate +instead of the trip points. diff --git a/kernel/src/ACPI/AcpiSleep.cpp b/kernel/src/ACPI/AcpiSleep.cpp index 1781f44..d7dc380 100644 --- a/kernel/src/ACPI/AcpiSleep.cpp +++ b/kernel/src/ACPI/AcpiSleep.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -435,6 +436,9 @@ namespace Hal { } reinitProgress(0x04); // PAT Hal::InitializePAT(); + // HWP enable and POWER_CTL reset across S3; re-apply them and + // bump the policy epoch so APs re-apply on their next tick. + Hal::CpuPower::ReapplyAfterWake(); reinitProgress(0x05); // PIC Hal::DisableLegacyPic(); reinitProgress(0x06); // Local APIC diff --git a/kernel/src/ACPI/CpuIdle.cpp b/kernel/src/ACPI/CpuIdle.cpp index af46cb7..55d9503 100644 --- a/kernel/src/ACPI/CpuIdle.cpp +++ b/kernel/src/ACPI/CpuIdle.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -30,6 +31,7 @@ namespace Hal { IdleEntryKind EntryKind = IdleEntryKind::FixedHardware; uint16_t IoPort = 0; uint8_t WidthBits = 0; + uint8_t MwaitHint = 0; // FFH entries: MWAIT hint from GAS address }; static constexpr int MaxIdleStates = 8; @@ -180,6 +182,9 @@ namespace Hal { if (addressSpace == GAS_FIXED_HARDWARE) { state.EntryKind = IdleEntryKind::FixedHardware; + // Intel FFH convention: the GAS address low byte carries the + // MWAIT hint for this C-state (same as Linux acpi_idle). + state.MwaitHint = (uint8_t)(address & 0xFF); return true; } @@ -382,10 +387,81 @@ namespace Hal { return best; } + // ============================================================ + // Deep MWAIT idle for APs + // + // The BSP must stay in C1 (HLT / MWAIT hint 0): on some laptops + // deep MWAIT states freeze the LAPIC timer even though ARAT is + // advertised, and the BSP's periodic 1 ms tick is the system + // timekeeper (see ApicTimer::IdleOnce). APs have no such duty: + // an idle AP is woken by the reschedule IPI, and its coarse tick + // simply resumes on wake. Deep core C-states (C6) power-gate the + // core entirely, which is where most of the idle-heat win lives + // on many-core parts. + // ============================================================ + + static constexpr uint8_t MaxApMwaitHint = 0x2F; // cap at the C6 family + static uint8_t g_deepApHint = 0; + + // CPUID.05H:EDX enumerates the number of MWAIT sub-states per + // C-state group; the group index for hint h is (h >> 4) + 1. + static bool MwaitHintSupported(uint8_t hint) { + uint32_t a, b, c, d; + asm volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) + : "a"(0), "c"(0)); + if (a < 5) return false; + asm volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) + : "a"(5), "c"(0)); + if ((c & 1) == 0) return hint == 0; // extensions not enumerated + uint32_t group = ((uint32_t)(hint >> 4) & 0xF) + 1; + uint32_t sub = hint & 0xF; + uint32_t count = (d >> (group * 4)) & 0xF; + return sub < count; + } + + static void ComputeDeepApHint() { + g_deepApHint = 0; + if (!Hal::HasMwait()) return; + + // Prefer firmware-blessed hints from _CST FFH entries. + uint8_t best = 0; + for (int i = 0; i < g_idleStateCount; i++) { + const auto& state = g_idleStates[i]; + if (!state.Valid || state.EntryKind != IdleEntryKind::FixedHardware) continue; + if (state.MwaitHint == 0 || state.MwaitHint > MaxApMwaitHint) continue; + if (!MwaitHintSupported(state.MwaitHint)) continue; + if (state.MwaitHint > best) best = state.MwaitHint; + } + + // No usable _CST data: derive from CPUID alone (deepest core + // state, capped at C6). + if (best == 0) { + static constexpr uint8_t candidates[] = { 0x20, 0x10, 0x01 }; + for (uint8_t hint : candidates) { + if (MwaitHintSupported(hint)) { best = hint; break; } + } + } + + g_deepApHint = best; + if (g_deepApHint != 0) { + KernelLogStream(OK, "CpuIdle") << "AP deep idle: MWAIT hint 0x" + << base::hex << (uint64_t)g_deepApHint; + } + } + + static uint32_t CurrentIdleHint() { + auto* cpu = Smp::GetCurrentCpuData(); + return (cpu != nullptr && cpu->cpuIndex != 0) ? g_deepApHint : 0; + } + + uint8_t GetApIdleHint() { + return g_deepApHint; + } + static void FallbackWait(bool hasMwait, volatile uint64_t* monitorAddr) { volatile uint64_t* addr = (monitorAddr != nullptr) ? monitorAddr : &g_fallbackMonitor; if (hasMwait) { - Hal::IdleWait(addr); + Hal::IdleWait(addr, CurrentIdleHint()); } else { asm volatile("hlt"); } @@ -395,7 +471,7 @@ namespace Hal { volatile uint64_t* monitorAddr) { volatile uint64_t* addr = (monitorAddr != nullptr) ? monitorAddr : &g_fallbackMonitor; if (hasMwait) { - Hal::IdleWaitWithInterruptsDisabled(addr); + Hal::IdleWaitWithInterruptsDisabled(addr, CurrentIdleHint()); } else { Hal::HaltWithInterruptsDisabled(); } @@ -424,7 +500,7 @@ namespace Hal { } } - void Initialize(ACPI::CommonSDTHeader* xsdt) { + static void LoadIdleStates(ACPI::CommonSDTHeader* xsdt) { g_idleStateCount = 0; for (int i = 0; i < MaxIdleStates; i++) { g_idleStates[i] = {}; @@ -471,6 +547,11 @@ namespace Hal { << "ACPI _CST unavailable or unsupported - using safe MWAIT/HLT idle fallback"; } + void Initialize(ACPI::CommonSDTHeader* xsdt) { + LoadIdleStates(xsdt); + ComputeDeepApHint(); + } + void Wait(uint32_t predictedIdleMs, bool hasMwait, volatile uint64_t* monitorAddr) { const IdleState* state = SelectIdleState(predictedIdleMs, hasMwait); if (state == nullptr) { diff --git a/kernel/src/ACPI/CpuIdle.hpp b/kernel/src/ACPI/CpuIdle.hpp index 6f18412..88c7e2a 100644 --- a/kernel/src/ACPI/CpuIdle.hpp +++ b/kernel/src/ACPI/CpuIdle.hpp @@ -29,5 +29,9 @@ namespace Hal { void WaitShallowWithInterruptsDisabled(bool hasMwait, volatile uint64_t* monitorAddr = nullptr); + // The MWAIT hint APs use for deep idle (0 = plain C1). Exposed for + // SYS_POWERINFO diagnostics. + uint8_t GetApIdleHint(); + }; }; diff --git a/kernel/src/Api/BuildNo.hpp b/kernel/src/Api/BuildNo.hpp index 40ae102..b2a8a87 100644 --- a/kernel/src/Api/BuildNo.hpp +++ b/kernel/src/Api/BuildNo.hpp @@ -12,4 +12,4 @@ #pragma once -#define MONTAUK_BUILD_NUMBER 9 +#define MONTAUK_BUILD_NUMBER 14 diff --git a/kernel/src/Api/Power.hpp b/kernel/src/Api/Power.hpp index 6b4b227..bcdc3de 100644 --- a/kernel/src/Api/Power.hpp +++ b/kernel/src/Api/Power.hpp @@ -12,6 +12,8 @@ #include #include #include +#include +#include #include "Syscall.hpp" @@ -71,4 +73,27 @@ namespace montauk::abi { } return (int64_t)Hal::AcpiSleep::Suspend(); } + + // SYS_POWERINFO. Fills a CPU power/thermal snapshot. Returns -1 when the + // CPU exposes no power management features (e.g. under QEMU). + static int64_t Sys_PowerInfo(PowerInfo* out) { + Hal::CpuPower::Snapshot s{}; + if (!Hal::CpuPower::GetSnapshot(s)) { + return -1; + } + + out->hwpActive = s.HwpActive ? 1 : 0; + out->throttling = s.Throttling ? 1 : 0; + out->tempC = s.TempC; + out->tjMaxC = s.TjMaxC; + out->highestPerf = s.HighestPerf; + out->lowestPerf = s.LowestPerf; + out->curMaxPerf = s.CurMaxPerf; + out->epp = s.Epp; + out->baseMHz = s.BaseMHz; + out->maxMHz = s.MaxMHz; + out->effMHz = s.EffMHz; + out->apIdleHint = Hal::CpuIdle::GetApIdleHint(); + return 0; + } }; diff --git a/kernel/src/Api/Syscall.cpp b/kernel/src/Api/Syscall.cpp index eeb3933..a731c2f 100644 --- a/kernel/src/Api/Syscall.cpp +++ b/kernel/src/Api/Syscall.cpp @@ -378,6 +378,9 @@ namespace montauk::abi { return Sys_SdrSetParam((int)frame->arg1, (int)frame->arg2, frame->arg3); case SYS_SDR_GETPARAM: return Sys_SdrGetParam((int)frame->arg1, (int)frame->arg2); + case SYS_POWERINFO: + if (!UserMemory::Writable(frame->arg1)) return -1; + return Sys_PowerInfo((PowerInfo*)frame->arg1); case SYS_THREAD_SPAWN: return Sys_ThreadSpawn(frame->arg1, frame->arg2, frame->arg3); case SYS_THREAD_EXIT: diff --git a/kernel/src/Api/Syscall.hpp b/kernel/src/Api/Syscall.hpp index f2e7844..703e93b 100644 --- a/kernel/src/Api/Syscall.hpp +++ b/kernel/src/Api/Syscall.hpp @@ -276,6 +276,9 @@ namespace montauk::abi { static constexpr uint64_t SYS_SDR_SETPARAM = 147; // (handle, param, value) static constexpr uint64_t SYS_SDR_GETPARAM = 148; // (handle, param) -> value + /* Power.hpp -- CPU power/thermal status */ + static constexpr uint64_t SYS_POWERINFO = 149; // (PowerInfo*) -> 0, -1 unsupported + // Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM). static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz static constexpr int SDR_PARAM_SAMPLE_RATE = 1; // sample rate, Hz @@ -568,6 +571,22 @@ namespace montauk::abi { uint32_t _pad; }; + // CPU power/thermal snapshot (returned by SYS_POWERINFO) + struct PowerInfo { + uint8_t hwpActive; // hardware P-state scaling enabled + uint8_t throttling; // thermal governor currently limiting frequency + uint8_t tempC; // package temperature, degrees C (0 = unknown) + uint8_t tjMaxC; // hardware throttle temperature + uint8_t highestPerf; // HWP performance range (ratio units) + uint8_t lowestPerf; + uint8_t curMaxPerf; // thermal governor's current ceiling + uint8_t epp; // energy/perf preference (0=perf, 255=power) + uint32_t baseMHz; // nominal base frequency (0 = unknown) + uint32_t maxMHz; // max turbo frequency + uint32_t effMHz; // measured average active frequency + uint32_t apIdleHint; // MWAIT hint used for AP deep idle + }; + // Crash report (filled by kernel on process fault, returned via SYS_CRASH_REPORT) struct CrashReportInfo { int pid; diff --git a/kernel/src/Hal/Cpu.hpp b/kernel/src/Hal/Cpu.hpp index f93db3b..fba637a 100644 --- a/kernel/src/Hal/Cpu.hpp +++ b/kernel/src/Hal/Cpu.hpp @@ -24,11 +24,12 @@ namespace Hal { // power and thermal efficiency than HLT (C1 only). // The monitored address is arbitrary -- we just need MONITOR // to arm the wake trigger; any interrupt wakes MWAIT. - inline void IdleWait(volatile uint64_t* monitorAddr) { + // `hint` selects the target C-state (0x00 = C1, 0x01 = C1E, + // 0x20 = C6, ...); callers must validate it against CPUID.05H. + inline void IdleWait(volatile uint64_t* monitorAddr, uint32_t hint = 0) { // MONITOR: set up the address monitoring range asm volatile("monitor" :: "a"(monitorAddr), "c"(0), "d"(0)); - // MWAIT: hint=0x00 (C1 state, platform-dependent deeper states) - asm volatile("mwait" :: "a"(0x00), "c"(0)); + asm volatile("mwait" :: "a"(hint), "c"(0)); } // Atomic idle entry for paths that have already disabled interrupts. @@ -39,9 +40,10 @@ namespace Hal { asm volatile("sti\n\thlt\n\tcli" ::: "memory"); } - inline void IdleWaitWithInterruptsDisabled(volatile uint64_t* monitorAddr) { + inline void IdleWaitWithInterruptsDisabled(volatile uint64_t* monitorAddr, + uint32_t hint = 0) { asm volatile("monitor" :: "a"(monitorAddr), "c"(0), "d"(0) : "memory"); - asm volatile("sti\n\tmwait\n\tcli" :: "a"(0x00), "c"(0) : "memory"); + asm volatile("sti\n\tmwait\n\tcli" :: "a"(hint), "c"(0) : "memory"); } inline void EnableSSE() { diff --git a/kernel/src/Hal/CpuPower.cpp b/kernel/src/Hal/CpuPower.cpp new file mode 100644 index 0000000..d1cc975 --- /dev/null +++ b/kernel/src/Hal/CpuPower.cpp @@ -0,0 +1,390 @@ +/* + * CpuPower.cpp + * Intel CPU power management: HWP frequency scaling, C1E promotion, + * and the package thermal governor. + * + * Without this module the CPU runs at whatever P-state the firmware left + * it in, forever, and idle cores never drop below C1. On a modern laptop + * part (tested target: i9-13900HX, 24 cores) that means constant high + * package power, spinning fans, and eventually the EC's thermal + * protection (forced beeping / hard power-off on Clevo boards). + * + * Three mechanisms, all feature-gated by CPUID so virtual machines and + * non-Intel parts skip them safely: + * + * 1. HWP (Hardware P-states, CPUID.06H:EAX[7]): the CPU autonomously + * picks its frequency between lowest and highest performance level, + * biased by the Energy/Performance Preference byte. This is what + * intel_pstate does on Linux and is the single biggest thermal win. + * + * 2. C1E promotion (MSR_POWER_CTL[1]): lets HLT idle drop core voltage + * and frequency to minimum instead of parking at the current ratio. + * + * 3. Thermal governor: reads the package digital thermal sensor every + * 500 ms on the BSP and steps the HWP performance ceiling down when + * the package crosses PassiveTempC, hard-clamps toward lowest above + * HotTempC, and slowly recovers below ResumeTempC. This keeps the + * package away from TjMax (hardware PROCHOT) and far away from the + * EC's emergency responses. + * + * Copyright (c) 2026 Daniel Hammer +*/ + +#include "CpuPower.hpp" +#include "MSR.hpp" +#include "SmpBoot.hpp" +#include +#include + +using namespace Kt; + +namespace Hal { + namespace CpuPower { + + // ============================================================ + // MSR addresses (power/thermal; used only in this module) + // ============================================================ + + static constexpr uint32_t IA32_MPERF = 0x0E7; + static constexpr uint32_t IA32_APERF = 0x0E8; + static constexpr uint32_t IA32_THERM_STATUS = 0x19C; + static constexpr uint32_t MSR_TEMPERATURE_TARGET = 0x1A2; + static constexpr uint32_t IA32_ENERGY_PERF_BIAS = 0x1B0; + static constexpr uint32_t IA32_PACKAGE_THERM_STATUS = 0x1B1; + static constexpr uint32_t MSR_POWER_CTL = 0x1FC; + static constexpr uint32_t IA32_PM_ENABLE = 0x770; + static constexpr uint32_t IA32_HWP_CAPABILITIES = 0x771; + static constexpr uint32_t IA32_HWP_REQUEST = 0x774; + + // ============================================================ + // Governor tuning + // ============================================================ + + // Balanced energy/performance preference (0 = max performance, + // 255 = max power saving). 0x80 matches Linux "balance_performance". + static constexpr uint8_t BalancedEpp = 0x80; + + static constexpr uint8_t PassiveTempC = 85; // start stepping down + static constexpr uint8_t HotTempC = 94; // step down aggressively + static constexpr uint8_t ResumeTempC = 78; // start stepping back up + static constexpr uint32_t GovernorIntervalMs = 500; + static constexpr uint8_t PassiveStepDown = 2; // ratio units (~200 MHz) + static constexpr uint8_t HotStepDown = 8; // ratio units (~800 MHz) + + // ============================================================ + // Detected features and shared policy state + // ============================================================ + + struct Features { + bool Intel = false; + bool Dts = false; // CPUID.06H:EAX[0] digital thermal sensor + bool Ptm = false; // CPUID.06H:EAX[6] package thermal sensor + bool Hwp = false; // CPUID.06H:EAX[7] hardware P-states + bool HwpEpp = false; // CPUID.06H:EAX[10] HWP energy/perf pref + bool Epb = false; // CPUID.06H:ECX[3] energy/perf bias MSR + bool AperfMperf = false; // CPUID.06H:ECX[0] freq feedback counters + uint32_t MaxLeaf = 0; + }; + + static Features g_feat{}; + static bool g_hwpActive = false; + static bool g_thermalReady = false; + static uint8_t g_highestPerf = 0; + static uint8_t g_lowestPerf = 0; + static uint8_t g_epp = 0; + static uint8_t g_tjMax = 100; + static uint32_t g_baseMHz = 0; + static uint32_t g_maxMHz = 0; + + // Governor output: the current performance ceiling plus an epoch + // counter. Each CPU compares the epoch against the last one it + // applied and rewrites its own IA32_HWP_REQUEST when they differ + // (MSR writes only take effect on the core that executes them). + static std::atomic g_curMaxPerf{0}; + static std::atomic g_policyEpoch{0}; + static uint32_t g_appliedEpoch[Smp::MaxCPUs]{}; + + static bool g_throttling = false; + static std::atomic g_lastTempC{0}; + static std::atomic g_effMHz{0}; + static uint64_t g_lastGovernorMs = 0; + static uint64_t g_lastAperf = 0; + static uint64_t g_lastMperf = 0; + + // ============================================================ + // Helpers + // ============================================================ + + static void Cpuid(uint32_t leaf, uint32_t subleaf, + uint32_t& a, uint32_t& b, uint32_t& c, uint32_t& d) { + asm volatile("cpuid" + : "=a"(a), "=b"(b), "=c"(c), "=d"(d) + : "a"(leaf), "c"(subleaf)); + } + + static void DetectFeatures() { + uint32_t a, b, c, d; + Cpuid(0, 0, a, b, c, d); + g_feat.MaxLeaf = a; + g_feat.Intel = (b == 0x756E6547 && d == 0x49656E69 && c == 0x6C65746E); + if (!g_feat.Intel || g_feat.MaxLeaf < 6) { + return; + } + + Cpuid(6, 0, a, b, c, d); + g_feat.Dts = (a & (1u << 0)) != 0; + g_feat.Ptm = (a & (1u << 6)) != 0; + g_feat.Hwp = (a & (1u << 7)) != 0; + g_feat.HwpEpp = (a & (1u << 10)) != 0; + g_feat.AperfMperf = (c & (1u << 0)) != 0; + g_feat.Epb = (c & (1u << 3)) != 0; + + if (g_feat.MaxLeaf >= 0x16) { + Cpuid(0x16, 0, a, b, c, d); + g_baseMHz = a & 0xFFFF; + g_maxMHz = b & 0xFFFF; + } + } + + static uint64_t ComposeHwpRequest(uint8_t minPerf, uint8_t maxPerf, uint8_t epp) { + // Desired = 0 selects fully autonomous frequency selection. + return (uint64_t)minPerf + | ((uint64_t)maxPerf << 8) + | ((uint64_t)epp << 24); + } + + // MSR_POWER_CTL is not architectural, so gate it on the digital + // thermal sensor bit: hypervisors (KVM, TCG) mask the CPUID.06H + // power bits, so DTS present means real Intel hardware where the + // MSR exists. + static void EnableC1ePromotionOnThisCore() { + if (!g_feat.Dts) return; + uint64_t v = ReadMSR(MSR_POWER_CTL); + if ((v & (1ull << 1)) == 0) { + WriteMSR(MSR_POWER_CTL, v | (1ull << 1)); + } + } + + static uint8_t ReadPackageTempC() { + if (g_feat.Ptm) { + uint64_t s = ReadMSR(IA32_PACKAGE_THERM_STATUS); + uint32_t readout = (uint32_t)((s >> 16) & 0x7F); + return (readout <= g_tjMax) ? (uint8_t)(g_tjMax - readout) : 0; + } + if (g_feat.Dts) { + uint64_t s = ReadMSR(IA32_THERM_STATUS); + if (s & (1ull << 31)) { // reading valid + uint32_t readout = (uint32_t)((s >> 16) & 0x7F); + return (readout <= g_tjMax) ? (uint8_t)(g_tjMax - readout) : 0; + } + } + return 0; + } + + static void UpdateEffectiveFrequency() { + if (!g_feat.AperfMperf || g_baseMHz == 0) return; + + uint64_t aperf = ReadMSR(IA32_APERF); + uint64_t mperf = ReadMSR(IA32_MPERF); + uint64_t dA = aperf - g_lastAperf; + uint64_t dM = mperf - g_lastMperf; + bool primed = (g_lastAperf | g_lastMperf) != 0; + g_lastAperf = aperf; + g_lastMperf = mperf; + + if (primed && dM != 0) { + g_effMHz.store((uint32_t)((uint64_t)g_baseMHz * dA / dM), + std::memory_order_relaxed); + } + } + + static void ApplyHwpRequestOnThisCpu(uint32_t epoch) { + auto* cpu = Smp::GetCurrentCpuData(); + if (cpu == nullptr || cpu->cpuIndex < 0 || cpu->cpuIndex >= Smp::MaxCPUs) { + return; + } + g_appliedEpoch[cpu->cpuIndex] = epoch; + WriteMSR(IA32_HWP_REQUEST, + ComposeHwpRequest(g_lowestPerf, + g_curMaxPerf.load(std::memory_order_relaxed), + g_epp)); + } + + // ============================================================ + // Public interface + // ============================================================ + + void InitializeBsp() { + DetectFeatures(); + if (!g_feat.Intel) { + KernelLogStream(INFO, "CpuPower") + << "Not an Intel CPU - power management left to firmware defaults"; + return; + } + + if (g_feat.Dts || g_feat.Ptm) { + uint64_t tt = ReadMSR(MSR_TEMPERATURE_TARGET); + uint8_t tj = (uint8_t)((tt >> 16) & 0xFF); + if (tj >= 50 && tj <= 115) { + g_tjMax = tj; + } + g_thermalReady = true; + } + + EnableC1ePromotionOnThisCore(); + + if (g_feat.Hwp) { + // One-way until reset; harmless if firmware already set it. + WriteMSR(IA32_PM_ENABLE, 1); + if (ReadMSR(IA32_PM_ENABLE) & 1) { + uint64_t caps = ReadMSR(IA32_HWP_CAPABILITIES); + g_highestPerf = (uint8_t)(caps & 0xFF); + g_lowestPerf = (uint8_t)((caps >> 24) & 0xFF); + // The EPP byte is reserved when CPUID does not + // advertise it; 0 (max performance bias) is the only + // safe value there. + g_epp = g_feat.HwpEpp ? BalancedEpp : 0; + + g_curMaxPerf.store(g_highestPerf, std::memory_order_relaxed); + uint32_t epoch = g_policyEpoch.fetch_add(1, + std::memory_order_acq_rel) + 1; + g_hwpActive = true; + ApplyHwpRequestOnThisCpu(epoch); + + KernelLogStream(OK, "CpuPower") << "HWP enabled: perf range " + << base::dec << (uint64_t)g_lowestPerf << "-" + << (uint64_t)g_highestPerf << ", EPP=0x" + << base::hex << (uint64_t)g_epp; + } else { + KernelLogStream(WARNING, "CpuPower") + << "HWP advertised but IA32_PM_ENABLE did not stick"; + } + } else if (g_feat.Epb) { + // Legacy knob: bias hardware decisions toward balance. + WriteMSR(IA32_ENERGY_PERF_BIAS, 6); + KernelLogStream(OK, "CpuPower") << "No HWP - set EPB to balanced"; + } else if (!g_thermalReady) { + KernelLogStream(INFO, "CpuPower") + << "No power features exposed (hypervisor or old CPU)"; + } + + if (g_thermalReady) { + UpdateEffectiveFrequency(); // prime APERF/MPERF baseline + g_lastTempC.store(ReadPackageTempC(), std::memory_order_relaxed); + KernelLogStream(OK, "CpuPower") << "Thermal governor armed: TjMax=" + << base::dec << (uint64_t)g_tjMax << "C, passive=" + << (uint64_t)PassiveTempC << "C, hot=" << (uint64_t)HotTempC + << "C, package now " << (uint64_t)g_lastTempC.load() << "C"; + } + } + + void InitializeAp() { + if (!g_feat.Intel) return; + + EnableC1ePromotionOnThisCore(); + + if (g_hwpActive) { + // IA32_PM_ENABLE is package-scoped on most parts, but the + // SDM allows logical scope; setting it again is harmless. + WriteMSR(IA32_PM_ENABLE, 1); + ApplyHwpRequestOnThisCpu(g_policyEpoch.load(std::memory_order_acquire)); + } + } + + void ApplyPolicyIfChanged() { + if (!g_hwpActive) return; + auto* cpu = Smp::GetCurrentCpuData(); + if (cpu == nullptr || cpu->cpuIndex < 0 || cpu->cpuIndex >= Smp::MaxCPUs) { + return; + } + uint32_t epoch = g_policyEpoch.load(std::memory_order_acquire); + if (g_appliedEpoch[cpu->cpuIndex] == epoch) return; + // Also re-check C1E promotion here: POWER_CTL is per-core and + // loses its contents across S3, and ReapplyAfterWake only runs + // on the BSP. The epoch bump it does routes every AP through + // this path. + EnableC1ePromotionOnThisCore(); + ApplyHwpRequestOnThisCpu(epoch); + } + + void ThermalTick(uint64_t nowMs) { + if (!g_thermalReady) return; + if (nowMs - g_lastGovernorMs < GovernorIntervalMs) return; + g_lastGovernorMs = nowMs; + + UpdateEffectiveFrequency(); + + uint8_t temp = ReadPackageTempC(); + g_lastTempC.store(temp, std::memory_order_relaxed); + if (temp == 0 || !g_hwpActive) return; + + uint8_t cur = g_curMaxPerf.load(std::memory_order_relaxed); + uint8_t next = cur; + + if (temp >= HotTempC) { + next = (cur > (uint8_t)(g_lowestPerf + HotStepDown)) + ? (uint8_t)(cur - HotStepDown) : g_lowestPerf; + } else if (temp >= PassiveTempC) { + next = (cur > (uint8_t)(g_lowestPerf + PassiveStepDown)) + ? (uint8_t)(cur - PassiveStepDown) : g_lowestPerf; + } else if (temp <= ResumeTempC && cur < g_highestPerf) { + next = (uint8_t)(cur + 1); + } + + if (next == cur) return; + + bool wasThrottling = g_throttling; + g_throttling = (next < g_highestPerf); + g_curMaxPerf.store(next, std::memory_order_relaxed); + uint32_t epoch = g_policyEpoch.fetch_add(1, std::memory_order_acq_rel) + 1; + ApplyHwpRequestOnThisCpu(epoch); // BSP applies immediately; APs on tick + + if (g_throttling && !wasThrottling) { + KernelLogStream(WARNING, "CpuPower") << "Package at " << base::dec + << (uint64_t)temp << "C - thermal throttle engaged"; + } else if (!g_throttling && wasThrottling) { + KernelLogStream(OK, "CpuPower") << "Package cooled to " << base::dec + << (uint64_t)temp << "C - thermal throttle released"; + } + } + + void ReapplyAfterWake() { + if (!g_feat.Intel) return; + + EnableC1ePromotionOnThisCore(); + + if (g_hwpActive) { + WriteMSR(IA32_PM_ENABLE, 1); + // Force every CPU (including this one) to re-apply the HWP + // request: the MSRs lost their contents across S3. + uint32_t epoch = g_policyEpoch.fetch_add(1, std::memory_order_acq_rel) + 1; + ApplyHwpRequestOnThisCpu(epoch); + } + + // Reset the frequency-feedback baseline (counters restarted). + g_lastAperf = 0; + g_lastMperf = 0; + } + + bool GetSnapshot(Snapshot& out) { + out = {}; + if (!g_feat.Intel || (!g_thermalReady && !g_hwpActive)) { + return false; + } + + out.HwpActive = g_hwpActive; + out.Throttling = g_throttling; + out.TempC = g_lastTempC.load(std::memory_order_relaxed); + out.TjMaxC = g_tjMax; + out.HighestPerf = g_highestPerf; + out.LowestPerf = g_lowestPerf; + out.CurMaxPerf = g_curMaxPerf.load(std::memory_order_relaxed); + out.Epp = g_epp; + out.BaseMHz = g_baseMHz; + out.MaxMHz = g_maxMHz; + out.EffMHz = g_effMHz.load(std::memory_order_relaxed); + return true; + } + + }; +}; diff --git a/kernel/src/Hal/CpuPower.hpp b/kernel/src/Hal/CpuPower.hpp new file mode 100644 index 0000000..757f5c3 --- /dev/null +++ b/kernel/src/Hal/CpuPower.hpp @@ -0,0 +1,54 @@ +/* + * CpuPower.hpp + * Intel CPU power management: HWP frequency scaling, C1E promotion, + * and the package thermal governor. + * Copyright (c) 2026 Daniel Hammer +*/ + +#pragma once +#include + +namespace Hal { + namespace CpuPower { + + // Snapshot of the power/thermal state for SYS_POWERINFO and logging. + struct Snapshot { + bool HwpActive; + bool Throttling; + uint8_t TempC; // package temperature (0 = unknown) + uint8_t TjMaxC; // hardware throttle temperature + uint8_t HighestPerf; // HWP performance range (ratio units) + uint8_t LowestPerf; + uint8_t CurMaxPerf; // governor's current performance ceiling + uint8_t Epp; // energy/performance preference in effect + uint32_t BaseMHz; // CPUID base frequency (0 = unknown) + uint32_t MaxMHz; // CPUID max turbo frequency + uint32_t EffMHz; // measured average active frequency (BSP) + }; + + // BSP: detect features, enable HWP with a balanced energy preference, + // enable C1E promotion, and prime the thermal governor. Must run + // before the APs boot so they can pick up the shared policy. + void InitializeBsp(); + + // AP: per-CPU MSR setup (C1E promotion + initial HWP request). + void InitializeAp(); + + // Re-apply the HWP request on the calling CPU if the governor changed + // the performance ceiling since this CPU last applied it. Cheap (one + // relaxed atomic compare when nothing changed) - safe in tick paths. + void ApplyPolicyIfChanged(); + + // Thermal governor step. BSP-only; rate-limited internally, so it can + // be called from every maintenance pass. + void ThermalTick(uint64_t nowMs); + + // Redo the BSP MSR setup after S3 wake (HWP enable and POWER_CTL do + // not survive suspend). + void ReapplyAfterWake(); + + // Fill a snapshot for SYS_POWERINFO. Returns false when the CPU + // exposes none of the relevant features (e.g. under QEMU). + bool GetSnapshot(Snapshot& out); + }; +}; diff --git a/kernel/src/Hal/SmpBoot.cpp b/kernel/src/Hal/SmpBoot.cpp index 1f1cd4d..b1a529d 100644 --- a/kernel/src/Hal/SmpBoot.cpp +++ b/kernel/src/Hal/SmpBoot.cpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -204,6 +205,9 @@ namespace Smp { // --- Check MWAIT support --- cpu->hasMwait = Hal::HasMwait(); + // --- Per-CPU power setup (C1E promotion, HWP request) --- + Hal::CpuPower::InitializeAp(); + // --- Signal that we are online --- cpu->started = true; @@ -212,6 +216,8 @@ namespace Smp { static volatile uint64_t s_idleMonitor = 0; for (;;) { + // Pick up thermal-governor frequency changes decided by the BSP. + Hal::CpuPower::ApplyPolicyIfChanged(); Hal::CpuIdle::Wait(10, cpu->hasMwait, &s_idleMonitor); } } diff --git a/kernel/src/Main.cpp b/kernel/src/Main.cpp index a5ff48c..c1267a8 100644 --- a/kernel/src/Main.cpp +++ b/kernel/src/Main.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -148,6 +149,11 @@ extern "C" void kmain() { // ISR stubs use SWAPGS which requires GS base to point to CpuData. Smp::InitBsp(); + // Enable hardware P-state scaling and the thermal governor. + // Needs GS base (per-CPU data) set up, and must run before the + // APs boot so they inherit the shared policy in ApEntry. + Hal::CpuPower::InitializeBsp(); + // Now safe to enable interrupts (SWAPGS-aware ISR stubs are installed) asm volatile("sti"); diff --git a/kernel/src/Sched/Scheduler.cpp b/kernel/src/Sched/Scheduler.cpp index df93d6f..ffc4f73 100644 --- a/kernel/src/Sched/Scheduler.cpp +++ b/kernel/src/Sched/Scheduler.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -811,6 +812,11 @@ namespace Sched { } ReclaimTerminated(); + + // Thermal governor step (rate-limited internally to 500 ms). + // Runs here because BSP maintenance is reached from both the idle + // loop and the timer tick, so it keeps running under full load. + Hal::CpuPower::ThermalTick(Timekeeping::GetMilliseconds()); } uint64_t GetNextDeadlineTick() { @@ -922,6 +928,11 @@ namespace Sched { void Tick(uint32_t elapsedMs) { auto* cpu = Smp::GetCurrentCpuData(); + // Pick up thermal-governor frequency changes (HWP requests are + // per-core MSRs, so every CPU applies them itself). No-op unless + // the governor bumped the policy epoch since our last tick. + Hal::CpuPower::ApplyPolicyIfChanged(); + // BSP: wake sleeping processes and reclaim terminated slots if (cpu->cpuIndex == 0) { RunBspMaintenance(); diff --git a/programs/include/Api/Syscall.hpp b/programs/include/Api/Syscall.hpp index ebca163..c6f31b6 100644 --- a/programs/include/Api/Syscall.hpp +++ b/programs/include/Api/Syscall.hpp @@ -200,6 +200,9 @@ namespace montauk::abi { static constexpr uint64_t SYS_SDR_SETPARAM = 147; // (handle, param, value) static constexpr uint64_t SYS_SDR_GETPARAM = 148; // (handle, param) -> value + // CPU power/thermal status + static constexpr uint64_t SYS_POWERINFO = 149; // (PowerInfo*) -> 0, -1 unsupported + // Tunable parameters (for SYS_SDR_SETPARAM / SYS_SDR_GETPARAM). static constexpr int SDR_PARAM_FREQ = 0; // center frequency, Hz static constexpr int SDR_PARAM_SAMPLE_RATE = 1; // sample rate, Hz @@ -493,6 +496,22 @@ namespace montauk::abi { uint32_t _pad; }; + // CPU power/thermal snapshot (returned by SYS_POWERINFO) + struct PowerInfo { + uint8_t hwpActive; // hardware P-state scaling enabled + uint8_t throttling; // thermal governor currently limiting frequency + uint8_t tempC; // package temperature, degrees C (0 = unknown) + uint8_t tjMaxC; // hardware throttle temperature + uint8_t highestPerf; // HWP performance range (ratio units) + uint8_t lowestPerf; + uint8_t curMaxPerf; // thermal governor's current ceiling + uint8_t epp; // energy/perf preference (0=perf, 255=power) + uint32_t baseMHz; // nominal base frequency (0 = unknown) + uint32_t maxMHz; // max turbo frequency + uint32_t effMHz; // measured average active frequency + uint32_t apIdleHint; // MWAIT hint used for AP deep idle + }; + struct ProcInfo { int32_t pid; int32_t parentPid; diff --git a/programs/include/libc/montauk.h b/programs/include/libc/montauk.h index 538859b..29a51fd 100644 --- a/programs/include/libc/montauk.h +++ b/programs/include/libc/montauk.h @@ -172,6 +172,7 @@ extern "C" { #define MTK_SYS_SDR_READ 146 #define MTK_SYS_SDR_SETPARAM 147 #define MTK_SYS_SDR_GETPARAM 148 +#define MTK_SYS_POWERINFO 149 /* @SYSCALLS-END */ #define MTK_SOCK_TCP 1 diff --git a/programs/libs/libprintersapplet/obj/src/libprintersapplet.o b/programs/libs/libprintersapplet/obj/src/libprintersapplet.o index a497a88f266312442054ed06200adbf403da052b..00fa9831603768c23ac90ed94dedf96c625d4acf 100644 GIT binary patch delta 2065 zcmZ{kU2IfE6vyY>(k@Fs*frpCm)K1|=_+EjXs1V{^iw5y^ z*pWBZ^Q?jO0&9!)Mb=fUFR@<3`ZDWk*59zMVZGRxmNgsY;QJwxmv^y7&G93JWT;7g zVx}g!##$Brne|#OR$?7seVr#!g>JA`b;_(&otvyxv%j)dHGgBx&F01L9H?%WS^Ic` zKUk}YS6KVm|C9AP*0)&Kvi^&89qWHstA_q%ZPm(!KM%YD;9v%bG}HPyD#}Q{{D^$e zdNm+#_tWpBySKK#3exy#!&q&a_7C>T;WcboKalTQFGP!c66`LiD_8IdPf#UY*c|pf z?J<2Xc!J=#C#aT>Te-V@uX{}2+uqAusFVoO26>MzWgTcV&>Iv$5 z9Al(TlKRL^1={ZLKQ_E)$K!*$`iI?YbKY{Vw@%c+y>9NFRm*O!m+3;h!3z+Al7zGl zVqx0|bi3tkhb`EJx!4JK4x2hdu;1cwMONRV$=4J{h~oH@c9wq z0H%l@G3aIeCXV7EqQ?ySq|W^qi$u>E^fVtcAwI_?q8AOi2&${A7KogWN-CQ+09{QLTpEH zLiP?#<&Nm1$^03N5zTsX(%mjymTsZB?h;D2U$ik1Th}&-?rjrociRTiL@_>SxfL54;zt`3 zT9UHiK}382a-vvL#U)0<0}Y#NVgi^_A2gVNEr~>5l&xq`6k0!OgC_ONnc3uKCh7El z&Tr1geC$1Z5@sjhSR+&%Z~7Fz$`^it@Mo)qSmO>1iQxKi?}k+x%*Gu-!;;fg*1-B2 zYm4>QtV>wWvR=h{j&&*P>#PH;zo~X*x@tKEZ*I!U|6-M@G0&!|@gr%1s>m${D)CRO z%Q)H3tSeam#zj=3-&w0fcUh}E_gJfD7g(#Di>y_%f3W6ev*J%Sf?VJ))~fK|tV8Ty zV!fL6Kdj4HFS8D_zRy}U^e=0xTyA^_cn847IZ&(Em?8srmsi20x4*oz1YW|K$ezFe zT@k#B$+cbC+O_WO+JUY}7ams?{SgRlFX!EwKF zNE>`{b#i_EQ6HC%&=O4zQbAR9rw^7%9}SocJq1Ekj?->r`E}AcT9YN8l1V8=X z)DMkbbjzC-wBtnc8F(H$T5K5fo@;R|=<_C$UjmGy+qwl>z2mJbR;V^8Gtq%yeybge zgp^gx_^94Z91BDn40=b`Z^7teb}VI3^(chs#tx!84Vu%jgE&ld#Guogj z1FFk+agOL|gKpLK6v8$;cEO-ydPiSiGtuh?y`|~*=x(z!zZ&!eKUyJ{41ckPC6A3p z@$@$GQxhVBGgK>X&_P|R0T+q381$5U95hmxc-)S48+3txa6;_FKB6xgw4hJ#H9Sc4 zfI%;7`wh$!eczyQ9s3a$h&~%pUC2f9h3(ZRzZ;`Z*s+%lI;(SyVh7Qq23^rNeiDa? ze&C}rEUXHNGaP%~N9Cax=_Q<_*vmdDV>j#AD+nn&cGaN#d8ZXOIP(uaDl^yU*gNQ^ z?94qMm6>;Fx@`De?f=j4_iKMC&QK>I{aKKiF95qR z*EtQN=ypxRF-)Yp;2I94+d|*!n|K({rrTf&3uy-)z(mG@GIVL4!d%9&ZnoomCIxpf z(d|GZx-|D;j^+=s(Cvg?s8R=U1e(367iFxTT)3w1b8#vV-cI*>-U*9k52 VH&}=P9Iju7mwM~qJuLKY{vQBQ2}J+^ diff --git a/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o b/programs/libs/libtimezonesapplet/obj/src/libtimezonesapplet.o index ad5af58cb4f4beea5c53f6dc59a71b999cc1ceec..06995a6c507d6b637f877e2b98b4bfd494a73a20 100644 GIT binary patch delta 2066 zcmZ{kU1(fY5XaBlG`ZR4qq~xNd$%-8(i%r#N;zyh_=PX_h^uo^l z&-~8Jne%b?yLVyYJ(y^OiCoh+@T~`M3o5@{FGN*(@L6HkjC;#gshy3S6hgdhkr7{q z6Y|A+o;9#8u(nuVVqL=eGV67$udpsxJ61tXV6K{X--x-(sDb<41BSpeFf= znVMvgwJLm*bvYNi#X89P7oJ2Fy3JbESz@i~++nSn{gt(<`5S9)HYnV1~-?EQ{N+) zB6XJ3N8V7d{+>X8-@(04^z85K^Kx}ZEpJo(R2e+rzWm-(Y36AtoVPe zZMO2Ojre?}iifcfbKC}lrerJhG$XV-Zrq@EH0{MG(E|qEQKoVSF-`PkgU<2&5#k8u zh#oiS6}=NDahB+5gFdBmKgK1ZpBnTWUo#;-$E^=L?j?gRfa>ZxrilJ*&~9CM8Apj) zdVR(7D9%)kGepA%eP8cn1Ab4m*`TNSwh0l(&?Ao9YtW13s;dLoL3GfdGn&4LLqtam zx~kWE98VF=8FW(Tet-p{(*|wQ{vm`%9rqP^>FAMLq|?|xf>EO5KB^bPbPCf%&uJ

6K`i@=m zqPxe+aJZ`m=Q=-xdCYXJ!A;C`cfk#u?QRR~YE*T)5qjDv7VAks4`#>?W1j2{Tp`(m`^6GE%msXjKeOB?MuJ{X2`yQ Ud9wYuvM&)R-=k8FAtWRJ0X<;|h5!Hn delta 2040 zcmZ{kYiL|m5XaBlG<$cO?KX)CH#fzkY0{8YY1|l+;A1z7nAcV$P%8)}*{Y?hs7>gH z7D<+ZHBdja%YfJzABn{pN=w7lFSXJ}P>E>F79ahjsV`iu)>Mt9f_2WEvv4`k13ULW z^E)%=HSCdBV0jXbw7_sK`Zavw0Tdwo{#qf{rFUk8T{Y&dTBUZjHX&$O(tO4mSTC}+ zSbxsCg!K~ZHLUZjOIcrF9bo-sU0Rl_lg3`I&&sb@qt^J2UA4ydqzS4;eqf*q|HwMX z#eQO4&iXPhq6+=SS{3@8wW@Q4wc6|-tX0hw)@rj?S@UMI;!k#LUf?g*YT>_Ghd92< zdM)dJSeLQB#yZUUI%~C|e_2~)^57ZZ6979WLCs?46gl{OSv5>~lVv?6@C06p><;Xt zCxT~j+xp&Y<9Z<)WesrKr7n(&l+P)VQP@yxZ}SE1A)iz6l+P)Z)2;ot+IxLLd(7{= zpdEg=COLoeVIP-)&?=c`si3;9#|MKlMgyio4}cJrV{{t1{4FxLZ&Q|HO14WWDyN7~ z5f48zDw^Io-*hO?mQvR;2_bD40=(%35^SQfao^{?bDTi!8xL<20f#F z3oj9k=+hT(@=bk{wODbl<2D;~Mn6>>CW*!k`k`*B0URN^-Jo-tK8Djo_ZalLKJPf@ zi5@iQ%R2W}g!>%#m_ei3e*|MhAD2BuLtas_%ccJ`rit$JQGFN*#bL}5ozzqolp89S zh{kI;PxSara!=z5(X&QwRHcgZ*bsLr77Ut1h&wnO54vC7r0TbLAnqi8HSz{^)hkB) zAsxSF#M79IJMNICrTQPBc)tBOPrKNd2)Yp;kuA`rGaO7N9Cw3{%6pm6G>Ric+clLp zu3L}!!#GWJ*q19e*Q6iib<7ie%b+v5J5M5XJMKFMt+hskrau8)I7_w<3uG5DI*@>km?0a% zS+cvaKz0P9sf4wh#3xhn(0H{fatP;Aoe;zVq5qojwyklf!OYeKyp6MD_h5l+7e*gU Ogf8B%QVwD)RsTQJwF#O4 diff --git a/programs/src/power/main.cpp b/programs/src/power/main.cpp new file mode 100644 index 0000000..dcb77d9 --- /dev/null +++ b/programs/src/power/main.cpp @@ -0,0 +1,152 @@ +/* + * main.cpp + * power - CPU power/thermal status tool. + * + * Reads the kernel's power management snapshot (SYS_POWERINFO): package + * temperature, HWP frequency-scaling state, the thermal governor's + * current frequency ceiling, and the measured average active frequency. + * Useful for verifying that HWP and the thermal governor are doing their + * job on real hardware. + * + * Usage: + * power print the current snapshot + * power watch sample once per second until 60 s elapse + * power watch sample once per second for seconds + * Copyright (c) 2026 Daniel Hammer +*/ + +#include + +using namespace montauk; + +static void put_u64(uint64_t n) { + char buf[24]; + int i = 0; + if (n == 0) { putchar('0'); return; } + while (n) { buf[i++] = (char)('0' + (n % 10)); n /= 10; } + while (i) putchar(buf[--i]); +} + +static void put_hex8(uint8_t v) { + static const char* digits = "0123456789ABCDEF"; + print("0x"); + putchar(digits[(v >> 4) & 0xF]); + putchar(digits[v & 0xF]); +} + +static bool read_info(montauk::abi::PowerInfo& info) { + return syscall1(montauk::abi::SYS_POWERINFO, (uint64_t)&info) == 0; +} + +static void print_snapshot(const montauk::abi::PowerInfo& info) { + print("CPU power/thermal status:\n"); + + print(" HWP: "); + if (info.hwpActive) { + print("active (EPP "); + put_hex8(info.epp); + print(", perf range "); + put_u64(info.lowestPerf); + print("-"); + put_u64(info.highestPerf); + print(")\n"); + } else { + print("not active\n"); + } + + print(" Frequency: base "); + put_u64(info.baseMHz); + print(" MHz, turbo "); + put_u64(info.maxMHz); + print(" MHz, avg active "); + put_u64(info.effMHz); + print(" MHz\n"); + + print(" Package: "); + if (info.tempC != 0) { + put_u64(info.tempC); + print(" C (hardware throttles at "); + put_u64(info.tjMaxC); + print(" C)\n"); + } else { + print("temperature unavailable\n"); + } + + print(" Governor: "); + if (!info.hwpActive) { + print("inactive\n"); + } else if (info.throttling) { + print("THROTTLING - ceiling "); + put_u64(info.curMaxPerf); + print(" of "); + put_u64(info.highestPerf); + print("\n"); + } else { + print("not throttling (ceiling "); + put_u64(info.curMaxPerf); + print(")\n"); + } + + print(" AP idle: MWAIT hint "); + put_hex8((uint8_t)info.apIdleHint); + if (info.apIdleHint == 0) { + print(" (C1 only)"); + } + print("\n"); +} + +static void print_watch_line(const montauk::abi::PowerInfo& info, uint64_t t) { + put_u64(t); + print("s "); + put_u64(info.tempC); + print(" C avg "); + put_u64(info.effMHz); + print(" MHz ceiling "); + put_u64(info.curMaxPerf); + print("/"); + put_u64(info.highestPerf); + print(info.throttling ? " THROTTLING\n" : "\n"); +} + +static uint64_t parse_u64(const char* s, uint64_t fallback) { + uint64_t v = 0; + bool any = false; + while (*s == ' ') s++; + while (*s >= '0' && *s <= '9') { v = v * 10 + (uint64_t)(*s - '0'); s++; any = true; } + return any ? v : fallback; +} + +extern "C" void _start() { + char args[64]; + int alen = montauk::getargs(args, sizeof(args)); + const char* rest = (alen > 0) ? args : ""; + + montauk::abi::PowerInfo info{}; + if (!read_info(info)) { + print("power: no CPU power management available\n"); + print("(non-Intel CPU or running under emulation)\n"); + montauk::exit(1); + } + + bool watch = rest[0] == 'w'; + if (!watch) { + print_snapshot(info); + montauk::exit(0); + } + + const char* p = rest; + while (*p && *p != ' ') p++; + uint64_t seconds = parse_u64(p, 60); + if (seconds == 0) seconds = 60; + + print("time temp freq ceiling (1 sample/s, "); + put_u64(seconds); + print(" s)\n"); + + for (uint64_t t = 0; t < seconds; t++) { + if (!read_info(info)) break; + print_watch_line(info, t); + montauk::sleep_ms(1000); + } + montauk::exit(0); +}