# 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.
