From 4410204050c7871046e1894ab97311d3f3a08d0b Mon Sep 17 00:00:00 2001 From: Eric Juta Date: Sat, 7 Nov 2026 23:51:55 +0000 Subject: [PATCH 1/3] hwmon: macsmc: expose complete J615 SoC die temperature set --- drivers/hwmon/macsmc-hwmon.c | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/drivers/hwmon/macsmc-hwmon.c b/drivers/hwmon/macsmc-hwmon.c index 7e0a2312d2d292..b75c9958cc29d4 100644 --- a/drivers/hwmon/macsmc-hwmon.c +++ b/drivers/hwmon/macsmc-hwmon.c @@ -77,6 +77,7 @@ static const struct macsmc_hwmon_board { const char *compatible; const char *die_keys; const char *pressure_key; + bool require_all_die_keys; } macsmc_hwmon_boards[] = { /* * MacBook Pro (16-inch, M3 Pro): key names used for the CPU @@ -119,6 +120,18 @@ static const struct macsmc_hwmon_board { .die_keys = "Tg0D,Tg0P,Tg0X,Tg0b,Tg0j,Tg0v,Te05,Te0P,Tp05,Tp0L,Tp0b,Tp0r", .pressure_key = "mTPL", }, + /* + * MacBook Air (15-inch, M3): all twelve keys are readable floats on + * J615. The Tg readings rose during a GPU workload and fell after + * it stopped. Require the complete CPU/GPU set, not a CPU-only zone + * if discovery of a GPU key fails. + */ + { + .compatible = "apple,j615", + .die_keys = "Tg0D,Tg0P,Tg0X,Tg0b,Tg0j,Tg0v,Te05,Te0P,Tp05,Tp0L,Tp0b,Tp0r", + .pressure_key = "mTPL", + .require_all_die_keys = true, + }, }; static bool macsmc_hwmon_is_m3(void) @@ -1139,6 +1152,7 @@ static void macsmc_hwmon_die_unregister(void *data) static int macsmc_hwmon_die_register(struct macsmc_hwmon *hwmon) { + const struct macsmc_hwmon_board *board = macsmc_hwmon_board(); struct thermal_zone_device *tz; char keys[16 * 5] = ""; size_t len = 0; @@ -1151,6 +1165,12 @@ static int macsmc_hwmon_die_register(struct macsmc_hwmon *hwmon) hwmon->die_listed); return 0; } + if (board && board->require_all_die_keys && + hwmon->die_count != hwmon->die_listed) { + dev_warn(hwmon->dev, "no SoC die temperature zone: only %u of %u required keys is present\n", + hwmon->die_count, hwmon->die_listed); + return 0; + } for (i = 0; i < hwmon->temp.count && len + 6 <= sizeof(keys); i++) { smc_key key = hwmon->temp.sensors[i].macsmc_key; From 33bec533acc9d952d8b51dddc61371fd5fd179b5 Mon Sep 17 00:00:00 2001 From: Eric Juta Date: Sat, 7 Nov 2026 23:51:55 +0000 Subject: [PATCH 2/3] drm/asahi: preserve state-2 fallback for T8122 thermal boost --- drivers/gpu/drm/asahi/m3/m3_adt_config.rs | 47 +++--- drivers/gpu/drm/asahi/m3/m3_params.rs | 17 +-- drivers/gpu/drm/asahi/m3/m3_thermal.rs | 11 +- drivers/gpu/drm/asahi/m3/m3_thermal_policy.rs | 144 +++++++++++++++++- drivers/gpu/drm/asahi/m3/t8122_knobs.rs | 5 +- drivers/gpu/drm/asahi/m3/t8122_start.rs | 14 +- 6 files changed, 172 insertions(+), 66 deletions(-) diff --git a/drivers/gpu/drm/asahi/m3/m3_adt_config.rs b/drivers/gpu/drm/asahi/m3/m3_adt_config.rs index 1893c113cf2a7d..7b4bc82f9a65dd 100644 --- a/drivers/gpu/drm/asahi/m3/m3_adt_config.rs +++ b/drivers/gpu/drm/asahi/m3/m3_adt_config.rs @@ -21,7 +21,8 @@ use crate::{ m3::m3_init_layout as init, m3::m3_init_storage as storage, m3::m3_memory::Buffer, - m3::m3_params::{self, G15Debug, InitDataParam, ThermalMode}, + m3::m3_params::{self, G15Debug, InitDataParam}, + m3::m3_thermal_policy::ThermalMode, m3::m3_soc::{IoMapping, PstateTable, Soc}, m3::t8122_hwdata, m3::t8122_start::Experiment, @@ -137,12 +138,6 @@ const _: () = assert!(storage::same_iomaps( &storage::T6030_IOMAPS )); -/// Highest performance state device-tree InitData may use without the driver's thermal limit -/// (`asahi.m3_thermal=off`), and the runtime cap the thermal limit starts at and falls back to -/// (the "safe cap"). The InitData's fast-die temperature controller words stay 0, so the -/// firmware does not limit the die temperature by itself. -pub(crate) const ADT_MAX_PSTATE_LIMIT: u32 = 5; - /// The performance states the runtime backend lets the firmware use. #[derive(Copy, Clone, Debug)] pub(crate) struct PstatePolicy { @@ -200,42 +195,34 @@ impl PstatePolicy { ThermalMode::Hold | ThermalMode::On => u64::from(table_max), ThermalMode::Off => m3_params::MAX_PSTATE_DEFAULT, }); - let mut max = requested.clamp(1, u64::from(table_max)) as u32; - if u64::from(max) != requested { + let table_cap = requested.clamp(1, u64::from(table_max)) as u32; + let (max, safe) = thermal.pstate_caps(table_cap, ceiling); + if u64::from(table_cap) != requested { dev_info!( dev, "M3: asahi.m3_max_pstate={} clamped to {} (the table has states 1..={})\n", requested, - max, + table_cap, table_max ); } - if thermal == ThermalMode::Off && max > ADT_MAX_PSTATE_LIMIT { - dev_info!( - dev, - "M3: performance cap {} lowered to {}: device-tree InitData leaves the fast-die temperature controller off and the thermal limit is off (asahi.m3_thermal=off)\n", - max, - ADT_MAX_PSTATE_LIMIT - ); - max = ADT_MAX_PSTATE_LIMIT; - } - if let Some(ceiling) = ceiling { - if max > ceiling { + if max < table_cap { + if thermal == ThermalMode::Off { + dev_info!( + dev, + "M3: performance cap {} lowered to {}: device-tree InitData leaves the fast-die temperature controller off and the thermal limit is off (asahi.m3_thermal=off)\n", + table_cap, + max + ); + } else { dev_info!( dev, "M3 G15G start: performance cap {} lowered to {} (asahi.t8122_pstate_cap)\n", - max, - ceiling + table_cap, + max ); - max = ceiling; } } - // Without the thermal limit the runtime cap is the ceiling; with it, the runtime cap - // starts at (and never goes below) the cap used without it. - let safe = match thermal { - ThermalMode::Off => max, - ThermalMode::Hold | ThermalMode::On => max.min(ADT_MAX_PSTATE_LIMIT), - }; let boot = m3_params::boot_pstate_param() .unwrap_or(u64::from(safe)) .clamp(1, u64::from(safe)) as u32; diff --git a/drivers/gpu/drm/asahi/m3/m3_params.rs b/drivers/gpu/drm/asahi/m3/m3_params.rs index b6dbb6726a004f..a75f762a39d7f7 100644 --- a/drivers/gpu/drm/asahi/m3/m3_params.rs +++ b/drivers/gpu/drm/asahi/m3/m3_params.rs @@ -22,6 +22,8 @@ use kernel::{ prelude::*, // }; +use crate::m3::m3_thermal_policy::ThermalMode; + /// Parse a decimal or `0x`-prefixed hexadecimal `u64`. fn parse_u64(text: &str) -> Option { let text = text.trim(); @@ -545,21 +547,6 @@ fn parse_thermal(text: &str) -> Option { static M3_THERMAL: AtomicU64 = AtomicU64::new(THERMAL_UNSET); m3_param!("m3_thermal", M3_THERMAL, parse_thermal); -/// How the runtime backend limits the GPU performance state by temperature. -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub(crate) enum ThermalMode { - /// Today's fixed cap: the firmware is given the cap as its highest state, and nothing - /// changes it at runtime. - Off, - /// The firmware is given the whole table as its highest state, and the runtime cap words - /// hold the fixed cap for the whole boot. The same performance as `Off`, if the firmware - /// follows the runtime cap: this checks that it does. - Hold, - /// As `Hold`, and the runtime cap follows the SoC die temperature, up to the whole table - /// while the die is cool. - On, -} - /// The `asahi.m3_thermal` setting, if given. pub(crate) fn thermal_param() -> Option { match M3_THERMAL.load(Ordering::Relaxed) { diff --git a/drivers/gpu/drm/asahi/m3/m3_thermal.rs b/drivers/gpu/drm/asahi/m3/m3_thermal.rs index e30f8fe011332d..4a19861420bcc5 100644 --- a/drivers/gpu/drm/asahi/m3/m3_thermal.rs +++ b/drivers/gpu/drm/asahi/m3/m3_thermal.rs @@ -7,9 +7,12 @@ //! J516S) and nothing watches temperatures. The firmware's own die temperature controller is //! off in the InitData this driver builds. With device-tree InitData the default is `on`; //! `asahi.m3_thermal=off` restores the fixed cap exactly. +//! On T8122 the fixed cap is state 2 (or a lower requested ceiling). A higher +//! `asahi.t8122_pstate_cap` changes only the monitored ceiling, never this fallback. //! -//! With it, the InitData gives the firmware the whole table as its highest state (the -//! "ceiling"), and what bounds the firmware is the runtime cap: the Globals performance-state +//! With it, the InitData gives the firmware the requested, table-clamped highest state (the +//! "ceiling", also bounded by the T8122 experiment), and what bounds the firmware is the +//! runtime cap: the Globals performance-state //! cap and power-interface state targets, which the firmware keeps reading while it runs (a //! host moves the cap with a single store, with no message). The runtime cap starts at the //! fixed cap of `off` (the "safe cap") and never goes below it, so at worst the GPU runs as it @@ -59,8 +62,8 @@ use kernel::{ use crate::{ m3::m3_adt_config::PstatePolicy, - m3::m3_params::{self, ThermalMode}, - m3::m3_thermal_policy::{Change, Policy, Reading, Source, Why, S}, + m3::m3_params, + m3::m3_thermal_policy::{Change, Policy, Reading, Source, ThermalMode, Why, S}, }; /// The thermal zones read, each the hottest of a set of SoC die sensors: the SMC's die sensors diff --git a/drivers/gpu/drm/asahi/m3/m3_thermal_policy.rs b/drivers/gpu/drm/asahi/m3/m3_thermal_policy.rs index f7aa8120b25aaf..317be6e458fa6f 100644 --- a/drivers/gpu/drm/asahi/m3/m3_thermal_policy.rs +++ b/drivers/gpu/drm/asahi/m3/m3_thermal_policy.rs @@ -1,11 +1,42 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT //! Decisions of the M3 thermal limit (see `m3_thermal`), kept free of kernel dependencies so -//! that host tests drive exactly this code (`tools/asahi/m3-thermal-policy-tests.rs`). +//! that host tests drive exactly this code (`tools/asahi/m3-runtime-tests.rs`). //! //! Times are monotonic nanoseconds from any fixed origin, temperatures millidegrees Celsius, and //! states indices of the published performance-state table. +/// Highest fixed cap for non-T8122 device-tree InitData, whose firmware die-temperature +/// controller is disabled. T8122 retains its lower default cap without temperature feedback. +const ADT_MAX_PSTATE_LIMIT: u32 = 5; +/// T8122's unmonitored cap, independent of any experiment ceiling or future default change. +const T8122_SAFE_PSTATE: u32 = 2; + +/// How the runtime backend limits the GPU performance state by temperature. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub(crate) enum ThermalMode { + /// A fixed ceiling no higher than the safe cap, without temperature feedback. + Off, + /// Publish the requested ceiling, but hold the runtime cap at the safe cap. + Hold, + /// Allow the runtime cap to follow the die temperature up to the requested ceiling. + On, +} + +impl ThermalMode { + /// Return (firmware ceiling, boot/failure cap) for a table-clamped request. A T8122 + /// experiment ceiling permits higher states only with working temperature feedback; + /// it must not also raise the cap used before a reading or after a sensor failure. + pub(crate) fn pstate_caps(self, requested: u32, t8122_ceiling: Option) -> (u32, u32) { + let (max, limit) = match t8122_ceiling { + Some(ceiling) => (requested.min(ceiling), T8122_SAFE_PSTATE), + None => (requested, ADT_MAX_PSTATE_LIMIT), + }; + let safe = max.min(limit); + (if self == Self::Off { safe } else { max }, safe) + } +} + /// Required thermal-zone bits: SMC die (bit 0), PMP hotspot (bit 1). /// A missing or malformed compatible list requires both zones. pub(crate) fn required_zones(compatible: &[u8]) -> u32 { @@ -257,6 +288,117 @@ impl Policy { } } +#[cfg(test)] +mod cap_tests { + use super::*; + + fn temp(mc: i32, at: i64) -> Reading { + Reading::Temp { mc, at, zones: 1 } + } + + fn boosted_air() -> Policy { + let (ceiling, safe) = ThermalMode::On.pstate_caps(8, Some(5)); + let mut policy = Policy::new(true, safe, ceiling, 80_000); + assert_eq!(policy.cap(), 2); + for (at, mc, cap) in [(0, 60_000, 3), (2 * S, 60_001, 4), (4 * S, 60_002, 5)] { + policy.update(at, temp(mc, at)).0.unwrap(); + assert_eq!(policy.cap(), cap); + } + policy + } + + #[test] + fn air_ceiling_never_raises_the_unmonitored_cap() { + assert_eq!(ThermalMode::On.pstate_caps(8, Some(5)), (5, 2)); + assert_eq!(ThermalMode::Hold.pstate_caps(8, Some(5)), (5, 2)); + assert_eq!(ThermalMode::Off.pstate_caps(8, Some(5)), (2, 2)); + for mode in [ThermalMode::On, ThermalMode::Hold, ThermalMode::Off] { + assert_eq!(mode.pstate_caps(1, Some(5)), (1, 1)); + assert_eq!(mode.pstate_caps(8, Some(1)), (1, 1)); + } + } + + #[test] + fn non_air_caps_keep_the_existing_safe_limit() { + assert_eq!(ThermalMode::On.pstate_caps(8, None), (8, 5)); + assert_eq!(ThermalMode::Hold.pstate_caps(8, None), (8, 5)); + assert_eq!(ThermalMode::Off.pstate_caps(8, None), (5, 5)); + } + + #[test] + fn failed_missing_stale_and_implausible_readings_drop_air_to_two() { + for (reading, source) in [ + (Reading::None, Source::NoReading), + (Reading::Failed { errno: -5, at: 5 * S }, Source::Failed(-5)), + (temp(60_000, S), Source::Stale), + (temp(9_999, 5 * S), Source::Implausible), + (temp(125_001, 5 * S), Source::Implausible), + ] { + let mut policy = boosted_air(); + let (usable, change) = policy.update(5 * S, reading); + assert_eq!(usable, Err(source)); + assert_eq!(change, Some(Change { from: 5, to: 2, why: Why::NoTemperature(source) })); + policy.update(7 * S, temp(60_003, 7 * S)).0.unwrap(); + assert_eq!(policy.cap(), 3); + } + } + + #[test] + fn air_boost_requires_fresh_paced_readings_and_respects_the_ceiling() { + let (ceiling, safe) = ThermalMode::On.pstate_caps(8, Some(5)); + let mut policy = Policy::new(true, safe, ceiling, 80_000); + policy.update(0, temp(60_000, 0)).0.unwrap(); + assert_eq!(policy.cap(), 3); + policy.update(S, temp(60_001, S)).0.unwrap(); + assert_eq!(policy.cap(), 3); + policy.update(2 * S, temp(60_000, 0)).0.unwrap(); + assert_eq!(policy.cap(), 3); + policy.update(2 * S, temp(60_002, 2 * S)).0.unwrap(); + assert_eq!(policy.cap(), 4); + policy.update(4 * S, temp(60_003, 4 * S)).0.unwrap(); + policy.update(6 * S, temp(60_004, 6 * S)).0.unwrap(); + assert_eq!(policy.cap(), 5); + } + + #[test] + fn air_hot_and_critical_readings_reduce_the_cap() { + let mut policy = boosted_air(); + policy.update(4 * S + 500 * MS, temp(80_000, 4 * S + 500 * MS)).0.unwrap(); + assert_eq!(policy.cap(), 4); + policy.update(5 * S, temp(80_000, 4 * S + 500 * MS)).0.unwrap(); + assert_eq!(policy.cap(), 4); + policy.update(5 * S, temp(80_001, 5 * S)).0.unwrap(); + assert_eq!(policy.cap(), 3); + policy.update(5 * S + MS, temp(88_000, 5 * S + MS)).0.unwrap(); + assert_eq!(policy.cap(), 2); + assert_eq!(policy.allowed(5 * S + 2 * MS), 5); + assert_eq!(policy.allowed(6 * S + MS), 2); + } + + #[test] + fn unchanged_air_temperature_cannot_sustain_boost_forever() { + let (ceiling, safe) = ThermalMode::On.pstate_caps(8, Some(5)); + let mut policy = Policy::new(true, safe, ceiling, 80_000); + for at in [0, 2 * S, 4 * S] { + policy.update(at, temp(60_000, at)).0.unwrap(); + } + assert_eq!(policy.cap(), 5); + let (usable, _) = policy.update(STUCK_NS, temp(60_000, STUCK_NS)); + assert_eq!(usable, Err(Source::Stuck)); + assert_eq!(policy.cap(), 2); + } + + #[test] + fn hold_mode_never_boosts_the_air() { + let (ceiling, safe) = ThermalMode::Hold.pstate_caps(8, Some(5)); + let mut policy = Policy::new(false, safe, ceiling, 80_000); + for at in [0, 2 * S, 4 * S] { + policy.update(at, temp(60_000, at)).0.unwrap(); + } + assert_eq!(policy.cap(), 2); + } +} + #[cfg(test)] mod zone_tests { use super::required_zones; diff --git a/drivers/gpu/drm/asahi/m3/t8122_knobs.rs b/drivers/gpu/drm/asahi/m3/t8122_knobs.rs index ca0f8a6d499ccd..0e3deb07b33b2b 100644 --- a/drivers/gpu/drm/asahi/m3/t8122_knobs.rs +++ b/drivers/gpu/drm/asahi/m3/t8122_knobs.rs @@ -27,9 +27,8 @@ pub(crate) const UNIT_MASK_A_LIMIT: u64 = 0x7_0000_0003; pub(crate) const UNIT_MASK_B_LIMIT: u64 = 0x7; /// The default performance-state ceiling: the lowest two states. pub(crate) const PSTATE_CAP: u32 = 2; -/// The highest performance-state ceiling the experiment accepts. The same as the driver's own -/// limit when the fast-die temperature controller is off (`m3_adt_config::ADT_MAX_PSTATE_LIMIT`): -/// J613/J615 have no SoC die temperature zone yet, so there is no thermal feedback above it. +/// The highest performance-state ceiling the experiment accepts. States above the default +/// require the thermal governor and usable die-temperature readings, not just a higher ceiling. pub(crate) const PSTATE_CAP_MAX: u32 = 5; /// Parse a decimal or `0x`-prefixed hexadecimal `u64`. A leading sign is not accepted. diff --git a/drivers/gpu/drm/asahi/m3/t8122_start.rs b/drivers/gpu/drm/asahi/m3/t8122_start.rs index f4fe2bae341aa5..cba818d611a215 100644 --- a/drivers/gpu/drm/asahi/m3/t8122_start.rs +++ b/drivers/gpu/drm/asahi/m3/t8122_start.rs @@ -301,19 +301,7 @@ pub(crate) fn arm(dev: &device::Device, soc: &Soc) -> Result> e.mapping_count(), storage::IOMAP_BASE ); - // J615 and J504 have no SoC die temperature zone yet, so a cap above the no-feedback limit - // runs the GPU without any temperature feedback (`m3_thermal` holds the cap but gets no - // reading). The runtime cap and the firmware ceiling stay equal, because the cap is at most - // the no-feedback limit. - if v.pstate_cap > knobs::PSTATE_CAP { - dev_warn!( - dev, - "M3 G15G start: pstate_cap={} is above {}: no SoC die temperature zone on this board, so states up to {} run with no temperature feedback\n", - v.pstate_cap, - knobs::PSTATE_CAP, - v.pstate_cap - ); - } + Ok(Some(e)) } From da758c56a7aa4ab19d823c1aec63d3ae965b6159 Mon Sep 17 00:00:00 2001 From: Eric Juta Date: Sat, 7 Nov 2026 23:51:55 +0000 Subject: [PATCH 3/3] docs: asahi: document guarded T8122 performance ceilings --- tools/aurora-sep/M3-GPU.md | 26 +++++++++++++++++++++ tools/aurora-sep/air-gpu/air-gpu-oneshot.sh | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/tools/aurora-sep/M3-GPU.md b/tools/aurora-sep/M3-GPU.md index 821d37b66f984a..b93c09954ac43a 100644 --- a/tools/aurora-sep/M3-GPU.md +++ b/tools/aurora-sep/M3-GPU.md @@ -109,3 +109,29 @@ to this Mac; neither the installer nor this option migrates firmware or stage1. J615 hardware qualification is pending. The retained **Aurora previous (GPU off)** entry remains available. After reboot and desktop login, run `aurora-m3-gpu-check --details` and collect the installer's `--m3-report`. + +## J613/J615 temperature-guarded performance ceiling + +The T8122 default ceiling remains state 2. With the state-2 fallback repair in +this driver, an explicit `asahi.t8122_pstate_cap=5 asahi.m3_thermal=on` permits +states up to 5 only while the governor has usable die-temperature readings. +Do not apply this higher ceiling to an older kernel: its implementation can +also raise the boot and sensor-failure cap. + +The repaired driver starts at state 2 and immediately returns there for missing, +failed, stale, stuck or implausible temperature readings, or at the critical +temperature. A lower requested ceiling still wins. `m3_thermal=off` and `hold` +do not permit states above 2. Non-T8122 cap selection is unchanged. + +J615 now supplies `macsmc_soc_die` from the six Tg GPU-family keys and the six +CPU die keys also used on J613. All twelve must be discovered before the J615 +zone is registered; a later read failure in any sensor fails the whole reading. +Do not substitute a partial `macsmc_hwmon.soc_die_keys` list. These are raw sensor +readings, not a calibration or a hardware safety certification. + +First boot with `asahi.t8122_pstate_cap=5 asahi.m3_thermal=hold` and confirm that +the firmware respects the runtime cap of 2 under GPU load. Before relying on +`m3_thermal=on`, check the boot log for all twelve keys, a runtime range of 2..5 +and usable temperatures during GPU work. Keep a bootable previous kernel. +Verify rendering and temperature response on the running kernel; passing the +host policy tests alone does not qualify hardware behavior. diff --git a/tools/aurora-sep/air-gpu/air-gpu-oneshot.sh b/tools/aurora-sep/air-gpu/air-gpu-oneshot.sh index 2231a78b2a3465..b460c151ea1fb8 100755 --- a/tools/aurora-sep/air-gpu/air-gpu-oneshot.sh +++ b/tools/aurora-sep/air-gpu/air-gpu-oneshot.sh @@ -40,7 +40,7 @@ t8122_clkgen e1c|e5c|none clock-generator slot: t8122_sgx_setup none|t6030 SGX write before the firmware starts: none (default) or T6030 0x70001 t8122_unit_mask_a 0x[0-9a-fA-F]{1,9}|[1-9][0-9]{0,10} HwDataB unit mask A: nonzero, within 0x700000001 (default) t8122_unit_mask_b 0x[1-7]|[1-7] HwDataB unit mask B: nonzero, within 0x7 (default 0x3) -t8122_pstate_cap [1-5] highest GPU performance state, 1 to 5 (default 2; above 2 has no temperature feedback yet) +t8122_pstate_cap [1-5] highest GPU performance state, 1 to 5 (default 2; higher states require thermal feedback and the state-2 fallback repair) m3_expose -1|0|1 render node: -1 auto (on for J613/J615, the default), 0 off, 1 on m3_timeout_nohang 0|1 on a job timeout, fail the job without the hang path '