From e15c496128b300a164d82b1f348b5c13328107bc Mon Sep 17 00:00:00 2001 From: Maulik Shah Date: Tue, 11 Aug 2026 11:20:01 +0530 Subject: [PATCH 1/2] FROMLIST: cpuidle: psci: Start CPU-cluster pmdomain OFF when OSI is used psci_pd_init() always calls pm_genpd_init() with is_off=false, so a CPU-cluster pmdomain is marked ON at creation regardless of whether any CPU has actually powered it on yet. The pmdomain's own status tracking (and the power-on notifier chain fired from _genpd_power_on()) is therefore wrong from the outset for OSI mode. A concrete example: hamoa has 3 CPU clusters, each with 4 CPUs, under a single parent system-level pmdomain. Passing "maxcpus=4" on the kernel command line limits boot to the first 4 CPUs, so only cluster0 is ever actually powered on. Without this fix, cluster1 and cluster2 are still marked ON at creation regardless, since is_off is always false. Because none of their CPUs ever come online, neither dt_idle_attach_cpu() nor psci_idle_cpuhp_up() ever fires for them, so nothing subsequently drives those two domains to OFF - they stay marked ON for as long as the system runs. When cluster0's CPUs go idle and its pmdomain is powered off, the parent system-level pmdomain still sees cluster1 and cluster2 as ON and therefore never selects a system-level idle state, even though no CPU in those two clusters ever executed. Starting the domain OFF is safe under OSI: - For CPUs already online by the time this driver probes, dt_idle_attach_cpu() explicitly checks cpu_online() and calls pm_runtime_get_sync() on the CPU's attach device, which resolves to this same pmdomain and drives a real power-on immediately. This runs from psci_cpuidle_probe(), a device_initcall, strictly after psci_idle_init_domains()'s core_initcall has already created and marked off every pmdomain, so there is no window where an online CPU's domain is left off with nothing left to turn it on. - For CPUs that come online later, psci_idle_cpuhp_up() (registered via cpuhp_setup_state_nocalls(), which never invokes the callback for already-online CPUs at registration time) powers the domain on through the normal cpuhp AP_ONLINE path. Gate this on use_osi rather than applying it unconditionally: in non-OSI mode psci_pd_init() sets GENPD_FLAG_ALWAYS_ON, which independently blocks genpd_power_off() regardless of is_off, so the domain can never really be off there and marking it is_off=true would be misleading noise with no effect. Link: https://lore.kernel.org/linux-arm-msm/20260811-domain_off_ss3-v1-0-6a0a0fc023f5@oss.qualcomm.com/T/#m2ca7c431b37ac9431179d8f21c3134a1823a00c7 Fixes: a65a397f2451 ("cpuidle: psci: Add support for PM domains by using genpd") Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Maulik Shah --- drivers/cpuidle/cpuidle-psci-domain.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/drivers/cpuidle/cpuidle-psci-domain.c b/drivers/cpuidle/cpuidle-psci-domain.c index b9e4ad7d43a33..fab9de648b17f 100644 --- a/drivers/cpuidle/cpuidle-psci-domain.c +++ b/drivers/cpuidle/cpuidle-psci-domain.c @@ -78,7 +78,19 @@ static int psci_pd_init(struct device_node *np, bool use_osi) /* Use governor for CPU PM domains if it has some states to manage. */ pd_gov = pd->states ? &pm_domain_cpu_gov : NULL; - ret = pm_genpd_init(pd, pd_gov, false); + /* + * Start the domain in the OFF state when OSI is in use, so that + * genpd's own status tracking (and its power-on notifier chain) + * reflects reality from the outset instead of reporting ON before + * any CPU in the domain has actually requested it. This is safe: + * dt_idle_attach_cpu() runtime-resumes the domain for every CPU + * that is already online by the time this driver probes, and the + * PSCI cpuidle cpuhp callbacks power it on/off for CPUs that come + * online/offline afterwards. When OSI isn't in use, GENPD_FLAG_ALWAYS_ON + * above keeps the domain powered regardless of is_off, so this has + * no effect there. + */ + ret = pm_genpd_init(pd, pd_gov, use_osi); if (ret) goto free_pd_prov; From 9641a4639cc55c1c1ecf5692eb551427a8252cd9 Mon Sep 17 00:00:00 2001 From: Maulik Shah Date: Tue, 11 Aug 2026 11:20:02 +0530 Subject: [PATCH 2/2] FROMLIST: pmdomain: core: Initialize state_idx to deepest state for OFF domains pm_genpd_init() sets genpd->status from is_off but never touches state_idx, which stays at its kzalloc'd value of 0 (dt_idle_pd_alloc()) regardless of is_off. A domain that starts OFF without ever having gone through an actual power-off sequence is therefore left looking like it's parked at its shallowest idle state (state_idx=0) instead of its deepest one, even though it is fully powered down. This is visible on hamoa, where cluster0/1/2 each expose two idle states, CL4 (state_idx=0) and CL5 (state_idx=1, the deepest). When CPU-cluster genpds are initialised directly into GENPD_STATE_OFF under OSI mode ("cpuidle: psci: Start CPU-cluster pmdomain OFF when OSI is used"), clusters whose CPUs are excluded at boot (e.g. maxcpus=4, leaving clusters 1 and 2 untouched) never go through a power-off path, so their state_idx remains 0 instead of the deepest 1. debugfs reports this as "off-0", meaning the domain is off but parked at idle-state 0 (CL4) rather than "off-1", idle-state 1 (CL5): genpd_power_off() and genpd_sync_power_off() both gate parent power-off on every child being at its deepest state index: if (child->state_idx < child->state_count - 1) return; so the parent's check treats these fully-off children as "not yet at deepest state" and refuses to power off, blocking the deepest system-level low-power mode (SS3) from ever being entered. Without this change: $ cat .../power-domain-cpu-cluster1/current_state off-0 $ cat /sys/kernel/debug/qcom_stats/apss Count: 0 Last Entered At: 0 Last Exited At: 0 Accumulated Duration: 0 Fix this at the source: when a domain is initialised OFF, set its state_idx to its deepest state (state_count - 1) instead of leaving it at 0. This makes genpd's reported state consistent with reality for every is_off=true caller of pm_genpd_init(), not just the PSCI CPU-cluster case, and requires no special-casing in the parent power-off checks. With this change, the domain correctly reports its deepest state and SS3 is entered normally: $ cat .../power-domain-cpu-cluster1/current_state off-1 $ cat /sys/kernel/debug/qcom_stats/apss Count: 218 Last Entered At: 726792712 Last Exited At: 726950687 Accumulated Duration: 199888773 Link: https://lore.kernel.org/linux-arm-msm/20260811-domain_off_ss3-v1-0-6a0a0fc023f5@oss.qualcomm.com/T/#m6c0e0622ef5514fba8ec7a7b41b69e5e90e21fb0 Fixes: e7d90cfac551 ("PM: domains: Prevent power off for parent unless child is in deepest state") Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Maulik Shah --- drivers/pmdomain/core.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/pmdomain/core.c b/drivers/pmdomain/core.c index 842c4169e2906..5d96d9eabc1f3 100644 --- a/drivers/pmdomain/core.c +++ b/drivers/pmdomain/core.c @@ -2409,6 +2409,8 @@ int pm_genpd_init(struct generic_pm_domain *genpd, INIT_WORK(&genpd->power_off_work, genpd_power_off_work_fn); atomic_set(&genpd->sd_count, 0); genpd->status = is_off ? GENPD_STATE_OFF : GENPD_STATE_ON; + if (is_off && genpd->state_count) + genpd->state_idx = genpd->state_count - 1; genpd_set_stay_on(genpd, is_off); genpd->sync_state = GENPD_SYNC_STATE_OFF; genpd->device_count = 0;