feat: add AMD ADL sensor augmentation on Windows via PMLog (#349)
## Summary Closes #347. Builds on #346. The DXGI and PDH layer from #346 covers everything WDDM publishes. It cannot cover temperature, board power, fan speed, or clocks, because Windows does not expose them at all. This adds `src/device/readers/amd_adl.rs`, which reads them from AMD's own library. `atiadlxx.dll` is loaded at runtime via `libloading` from the absolute path `C:\Windows\System32\atiadlxx.dll`, never by bare name, matching the DLL-hijacking stance already documented in `windows_temp/amd_ryzen.rs`. **No import library is referenced**, so the executable carries no `atiadlxx` entry in its import table and a machine without AMD's driver starts normally and simply reports no ADL data. That is deliberately the same shape #345 asks for on Linux, where a missing `libdrm` is a loader error before `main`. Precedence is WMI < DXGI/PDH < ADL. ADL reads the hardware's own telemetry rather than the OS's accounting of it, so it overwrites PDH utilization and is the sole source for temperature, power, fan, and clocks. ## Two deliberate scope limits, both about refusing to declare an ABI blind **Legacy Overdrive paths are not implemented.** Sensors come only from `ADL2_New_QueryPMLogData_Get`, gated on `ADL2_Overdrive_Caps`. OD5/6/7 would be three more ABI surfaces to get right with no way to test them, for hardware predating the cards all-smi targets. A pre-Vega card keeps the baseline. **`AdapterInfo` is not declared, so augmentation requires exactly one AMD GPU.** This is the important one. `AdapterInfo` is the 1568-byte struct that carries the PCI bus/device/function and PNP string that would let an ADL adapter index be tied to a specific card. ADL sizes its write by *its own* `sizeof`, so a layout mistake overflows our buffer rather than failing cleanly. Worse, a single card exposes several adapter indices (one per display output) reporting identical telemetry, which cannot be deduplicated without that struct either. Rather than guess, `can_attribute()` requires a single AMD GPU. A multi-AMD-GPU host gets the honest DXGI/PDH baseline instead of one card's temperature reported against another. This is the same conclusion the #346 review reached about adapter matching: declining to attribute beats attributing wrongly. Multi-GPU support is a follow-up that would need `AdapterInfo` and real hardware to validate against. ## The sensor indices are the weakest point, and are treated as such `ADLSensorType` indices are transcribed from AMD's public `adl_structures.h`. Nothing in CI compiles all-smi for Windows and no test can call the real library, so if AMD renumbered an entry this would read the wrong sensor and report a *plausible but wrong* number, which is worse than reporting nothing. Two mitigations: 1. **Range guards.** Every value is checked against a physically sensible band (temperature 0-150 C, power 0-1000 W, activity 0-100%, and so on). A misindexed read almost always lands outside its target band, so the guard turns a silent wrong number into an absent one. Tested with a case that feeds a clock value into the temperature slot and asserts the whole readout comes back empty. 2. **`amd.adl.sensors` doctor check dumps the raw `index=value` table**, unfiltered and unnamed. That makes the mapping confirmable from real hardware without a code change shipping first, and if every sensor fails the range guard the check says explicitly that this is what a shifted enum looks like and asks for the dump. `amd.adl.library` separately distinguishes: DLL absent, DLL present but context creation failed, loaded but no PMLog-capable adapter, and healthy with the selected adapter index. ## Also in this PR: DXGI factory caching Prompted by a question about polling overhead. `CreateDXGIFactory1` is COM object creation that can pull in graphics driver DLLs, and #346 ran it on every poll (as often as once per second). It is now created once and rebuilt only when `IDXGIFactory1::IsCurrent` reports the adapter set changed, which also keeps hot-plug correct. For the record on cost, since it came up: **nothing in either layer submits GPU work.** DXGI reads adapter descriptors, PDH reads counters the OS already maintains, ADL reads a telemetry block the driver already collects. The load is CPU-side and small; the library load, ADL context creation, and the PMLog capability scan each happen once per process, so a steady-state poll is a single ADL call. One caveat now documented in the module: very aggressive sensor polling (the 100 ms rates desktop monitoring tools use) can hold an AMD GPU out of its deepest idle state. all-smi's intervals start at one second, well clear of that. ## Verification Same constraint as #346: **no CI job compiles all-smi for Windows.** The module is gated `cfg(any(target_os = "windows", test))` so the sensor mapping, the range guards, and the field application all run on the Linux test runner. 34 new tests. | Gate | Result | |---|---| | `cargo fmt --check` | pass | | `cargo clippy --all-targets -- -D warnings` | pass | | `cargo test` | 3162 pass, 0 fail (was 3128 on main) | | `cargo xwin check --target x86_64-pc-windows-msvc` | pass, 0 warnings | | `cargo xwin clippy --target x86_64-pc-windows-msvc -- -D warnings` | pass for this change | Layout is pinned by compile-time assertions on `ADLSingleSensorData` (8 bytes, 4-aligned) and `ADLPMLogDataOutput` (2052 bytes), plus a test asserting the sensor array actually strides by one 8-byte record from offset 4. Those assertions are the only automated check this ABI can have, which is why they are there rather than being decorative. ## Not verified, needs real hardware - Temperature, power, fan, and clocks populated on a PMLog-capable card - The sensor index mapping itself (`amd.adl.sensors` exists to produce this evidence) - `dumpbin /imports` showing no `atiadlxx` entry. The design guarantees it (nothing is linked, `libloading` only), but it has not been observed on a built exe. ## Pre-existing, still not fixed The same three Windows clippy lints noted in #348, in files neither PR touches: `cpu_windows.rs:49`, `:63`, `amd_ryzen.rs:228`. Worth a follow-up together with a CI job that compiles the Windows target, which `windows-latest` runners would do for free on this public repo.
J
Jeongkyu Shin committed
7201d6c21b436699510d8737a6235a4f5d6303ab
Parent: 55c6a1a
Committed by GitHub <noreply@github.com>
on 8/7/2026, 3:24:15 AM