From 689838fdfc47cc0b939f57aee7f57bfe55986c8a Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 05:26:47 -0500 Subject: [PATCH 01/12] ASoC: apple: aop: t8140: offer neither pause nor resume A firmware-driven suspend stops cout, cin and the speaker tap, and only their DAI prepare starts them again. The dmaengine refine still adds SNDRV_PCM_INFO_PAUSE and _RESUME, so after a wake user space resumes these PCMs instead of preparing them: headphone playback, the headset microphone and Speaker Sense run silent until reopened. Mask both flags after the refine, as aop_spkr.rs does. Without RESUME user space prepares again. PAUSE goes too: with it the suspend only pauses the ADMAC transfer, and the paused one blocks the next start. Signed-off-by: Diljit Singh --- sound/soc/apple/aop_t8140.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sound/soc/apple/aop_t8140.rs b/sound/soc/apple/aop_t8140.rs index 929aefbf54f64..751cde0c4eb8e 100644 --- a/sound/soc/apple/aop_t8140.rs +++ b/sound/soc/apple/aop_t8140.rs @@ -1614,6 +1614,9 @@ fn pcm_open_dmaengine( if hw.formats == 0 { return EINVAL.to_errno(); } + // A system sleep stops the services, and only a prepare starts them all again. Offer + // neither resume nor pause: a transfer paused by the suspend would block the next start. + hw.info &= !(bindings::SNDRV_PCM_INFO_PAUSE | bindings::SNDRV_PCM_INFO_RESUME); // SAFETY: the substream's runtime exists for the op's duration; the // dmaengine open attaches `chan` to it on success. unsafe { From 56583d4014a869a955b1448fd6334fadf640ca63 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 05:26:58 -0500 Subject: [PATCH 02/12] drm/apple: leave a DPMS-stopped DCP alone at suspend-to-idle On T8140, DPMS off stops the internal DCP's firmware with presentation paused until DPMS on restarts it. A suspend that isn't firmware-driven (s2idle, also the default without the loader's PCIe tunables) still runs dcp_hold_for_sleep(), and dcp_release_after_sleep() then unpauses presentation on the stopped firmware. A present queued after the wake is never answered, the IOMFB timeout marks the DCP crashed, and the next DPMS on fails: the panel stays dark until reboot. Skip the hold while the firmware is stopped: it sends nothing, and its presentation stays paused for DPMS on. Signed-off-by: Diljit Singh --- drivers/gpu/drm/apple/dcp.c | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index b96a06f489c14..0bcc2217aeca7 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -6718,10 +6718,13 @@ static void dcp_drain_for_sleep(struct apple_dcp *dcp) */ static void dcp_hold_for_sleep(struct apple_dcp *dcp) { - /* A failed component bind leaves no coprocessor session to hold. */ + /* + * A failed component bind leaves no coprocessor session to hold. Firmware that DPMS + * stopped sends nothing, and its presentation stays paused until DPMS restarts it. + */ if (IS_ERR_OR_NULL(dcp->rtk) || !dcp_uses_soft_dpms(dcp) || !dcp_presents_nits(dcp) || - READ_ONCE(dcp->crashed)) + READ_ONCE(dcp->crashed) || READ_ONCE(dcp->s2r_fw_down)) return; iomfb_pause_presentation(dcp, true); From 86e0b1e090704960199c5cca09b1192d11bb33a7 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 05:27:53 -0500 Subject: [PATCH 03/12] drm/apple: forget a crashed DCP's calls before starting it over A DCP that crashed, or gave no reply to A500, is stopped cold at the next sleep and started again after it, but the AP side of its transport is left as it was: the unanswered call keeps its channel depth, the transaction in flight stays iomfb.active, and while crashed is still set the new firmware's IOMFB messages and calls are dropped. The queue never runs again: DCPEXT's display stays dead, every later sleep is refused as "display updates still pending", and the internal DCP's set_power_state after the restart fails with -EIO. Before either start function brings up a firmware that stopped while crashed, release the active and pending transactions, zero the channel depths and the present state, and clear crashed. This covers the restart after a sleep, DPMS on, DCPEXT's power cycle and the end of a dark wake. A failed start marks it crashed again, as before. Signed-off-by: Diljit Singh --- drivers/gpu/drm/apple/dcp-internal.h | 1 + drivers/gpu/drm/apple/dcp.c | 15 +++++++++++++ drivers/gpu/drm/apple/iomfb.c | 32 ++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 13596af5af10c..71fb0da24d1ba 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -967,6 +967,7 @@ int iomfb_configure_backlight_h17p(struct apple_dcp *dcp, u32 maximum, bool default_valid, u32 default_nits); void iomfb_queue_init(struct apple_dcp *dcp); void iomfb_queue_stop(struct apple_dcp *dcp); +void iomfb_reset_transport(struct apple_dcp *dcp); bool iomfb_queue_drain(struct apple_dcp *dcp, unsigned long timeout); int iomfb_queue_settle(struct apple_dcp *dcp, unsigned long timeout); void iomfb_pause_presentation(struct apple_dcp *dcp, bool pause); diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 0bcc2217aeca7..c360c45596ce0 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -7483,6 +7483,19 @@ static int dcp_s2r_ext_domains_on(struct apple_dcp *dcp) return ret; } +/* + * A firmware stopped while crashed is about to start over: forget the calls and the + * transaction the old one left unanswered, and let the new one's messages through. A failed + * start marks it crashed again. + */ +static void dcp_s2r_forget_crash(struct apple_dcp *dcp) +{ + if (!READ_ONCE(dcp->crashed)) + return; + iomfb_reset_transport(dcp); + WRITE_ONCE(dcp->crashed, false); +} + /* * DCPEXT back after a real sleep, the same way in every macOS wake measured: its domains, * its DART's streams, and a warm restart, with no secure port and no display-on call. After @@ -7493,6 +7506,7 @@ static int dcp_s2r_ext_start(struct apple_dcp *dcp, bool cycled) void __iomem *control = dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL; int ret; + dcp_s2r_forget_crash(dcp); if (!dcp->s2r_dcp_dart) return -ENODEV; ret = apple_dart_gapf_ready(dcp->s2r_dcp_dart); @@ -7635,6 +7649,7 @@ static int dcp_s2r_warm_restart(struct apple_dcp *dcp) { int ret; + dcp_s2r_forget_crash(dcp); ret = dcp_s2r_domains_on(dcp); if (ret) return ret; diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index fc88a2b1f8d5c..a39068aac044e 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -582,6 +582,38 @@ void iomfb_queue_init(struct apple_dcp *dcp) atomic_set(&dcp->iomfb.opaque_x_state, IOMFB_OPAQUE_X_WAITING); } +/* + * For firmware stopped cold and started again: the calls and the transaction the old + * one never answered went with it, and would hold the channels and the queue forever. + */ +void iomfb_reset_transport(struct apple_dcp *dcp) +{ + struct iomfb_transaction *transaction; + + if (!iomfb_uses_queue(dcp)) + return; + + cancel_delayed_work_sync(&dcp->iomfb.timeout); + mutex_lock(&dcp->iomfb.lock); + transaction = dcp->iomfb.active; + dcp->iomfb.active = NULL; + if (transaction) { + if (transaction->brightness_only) + dcp->iomfb.backlight_queued = false; + transaction->release(transaction); + } + iomfb_discard_pending(dcp); + dcp->ch_cmd.depth = 0; + dcp->ch_oobcmd.depth = 0; + dcp->ch_cb.depth = 0; + dcp->ch_oobcb.depth = 0; + dcp->ch_async.depth = 0; + dcp->ch_oobasync.depth = 0; + dcp->present_state_h17p.pending = false; + dcp->present_state_h17p.accepted = false; + mutex_unlock(&dcp->iomfb.lock); +} + void iomfb_queue_stop(struct apple_dcp *dcp) { if (!iomfb_uses_queue(dcp)) From b61ed1b3839c5e775c9fe6c9c4ccbad5108a5b4a Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 05:28:08 -0500 Subject: [PATCH 04/12] arm64: smp: take parking spin-table CPUs as stuck for kexec and hibernation Once a platform registers spin-table power ops, spin_table_cpus_stuck() returns false and kexec goes ahead. But machine_shutdown() only parks the secondaries: each waits in smp_rvbar_spin_table_cpu_die(), in this kernel's text and on its idle stack, and the power ops run only at a suspend's commit. The new image overwrites that memory under the parked CPUs, and the new kernel can't start them. Before the parking ops such a kexec failed cleanly with -EBUSY. Hibernation has the same problem: cpus_are_stuck_in_kernel() is false because the ops have a cpu_die. Take parking CPUs as stuck whether or not power ops are registered, and check that before hibernating too. Hotplug and suspend-to-RAM don't look at it, and a crash kexec still goes ahead. Signed-off-by: Diljit Singh --- arch/arm64/kernel/hibernate.c | 3 ++- arch/arm64/kernel/smp_spin_table.c | 6 ++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/arch/arm64/kernel/hibernate.c b/arch/arm64/kernel/hibernate.c index 424291c547f02..9398c8d0ecb03 100644 --- a/arch/arm64/kernel/hibernate.c +++ b/arch/arm64/kernel/hibernate.c @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -336,7 +337,7 @@ int swsusp_arch_suspend(void) unsigned long flags; struct sleep_stack_data state; - if (cpus_are_stuck_in_kernel()) { + if (cpus_are_stuck_in_kernel() || spin_table_cpus_stuck()) { pr_err("Can't hibernate: no mechanism to offline secondary CPUs.\n"); return -EBUSY; } diff --git a/arch/arm64/kernel/smp_spin_table.c b/arch/arm64/kernel/smp_spin_table.c index 2d8819416a489..2d7660bd22f80 100644 --- a/arch/arm64/kernel/smp_spin_table.c +++ b/arch/arm64/kernel/smp_spin_table.c @@ -397,15 +397,13 @@ const struct cpu_operations smp_rvbar_spin_table_ops = { }; /* - * Parking CPUs leave the kernel only through a platform's power ops. Until one - * registers, kexec has to take them as stuck, as on plain spin-table. + * An offlined parking CPU waits in this kernel's text and stack, which a kexec or a + * hibernation restore overwrites: it is stuck for those, power ops or not. */ bool spin_table_cpus_stuck(void) { unsigned int cpu; - if (READ_ONCE(spin_table_power)) - return false; for_each_possible_cpu(cpu) if (get_cpu_ops(cpu) == &smp_rvbar_spin_table_ops) return true; From 72c35c1af4fe5689fc2e768ad91f42a3db69406c Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 05:28:20 -0500 Subject: [PATCH 05/12] soc: apple: rtkit: finish a sleep stop once both idles took apple_rtkit_sleep_stop() promises that on an error the firmware still runs. But when the AP and IOP idles are both acknowledged and only the 5 ms status poll misses, it returns -ETIMEDOUT with the firmware idled, and an idled firmware doesn't come back without a real sleep. SIO's suspend_late then refuses the sleep with nothing to restart the firmware: every SIO transfer fails, and every later sleep is refused until reboot. PMP and DCPEXT use the same call. Once both idles took, warn and finish the stop, so the caller's resume path brings the firmware back. The other half: when only the AP idle took, apple_rtkit_sleep_return() reports the firmware as running after a refused sleep, but its AP state is still idle and every endpoint send fails. Send AP on there. Signed-off-by: Diljit Singh --- drivers/soc/apple/rtkit.c | 17 ++++++++++++----- include/linux/soc/apple/rtkit.h | 2 +- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/drivers/soc/apple/rtkit.c b/drivers/soc/apple/rtkit.c index 864826487a49f..587be2930a60b 100644 --- a/drivers/soc/apple/rtkit.c +++ b/drivers/soc/apple/rtkit.c @@ -2189,10 +2189,11 @@ static int apple_rtkit_sleep_idle(struct apple_rtkit *rtk, void __iomem *cpu_con ret = readl_poll_timeout(cpu_control + APPLE_ASC_CPU_STATUS, status, (status & APPLE_ASC_CPU_STATUS_DOWN) == APPLE_ASC_CPU_STATUS_DOWN, 100, 5000); + /* Both idles took, so it doesn't run on: stop it as if the status had shown it. */ if (ret) - dev_err(rtk->dev, "RTKit: idle, but not stopped: status %#x\n", status); + dev_warn(rtk->dev, "RTKit: idle, but not stopped: status %#x\n", status); - return ret; + return 0; } /* With interrupts off: room in the FIFO, then the message. */ @@ -2339,9 +2340,15 @@ int apple_rtkit_sleep_return(struct apple_rtkit *rtk, void __iomem *cpu_control) * No real sleep: a firmware that idled doesn't come back from that with its power kept, * and stays down until the next sleep restarts it. One whose idle failed still runs. */ - if (!pm_resume_via_firmware()) - return (readl(cpu_control) & APPLE_ASC_CPU_CONTROL_RUN) && - (rtk->iop_power_state & 0xff) == APPLE_RTKIT_PWR_STATE_ON ? 0 : -EAGAIN; + if (!pm_resume_via_firmware()) { + if (!(readl(cpu_control) & APPLE_ASC_CPU_CONTROL_RUN) || + (rtk->iop_power_state & 0xff) != APPLE_RTKIT_PWR_STATE_ON) + return -EAGAIN; + /* Its AP idle took and its IOP idle didn't: endpoints stay shut until AP on. */ + if ((rtk->ap_power_state & 0xff) != APPLE_RTKIT_PWR_STATE_ON) + return apple_rtkit_ap_power(rtk, true); + return 0; + } ret = apple_rtkit_warm_restart(rtk); if (ret) diff --git a/include/linux/soc/apple/rtkit.h b/include/linux/soc/apple/rtkit.h index 79190e4093bd2..a0971671d172b 100644 --- a/include/linux/soc/apple/rtkit.h +++ b/include/linux/soc/apple/rtkit.h @@ -315,7 +315,7 @@ int apple_rtkit_sleep_stop(struct apple_rtkit *rtk, void __iomem *cpu_control); * then sets the AP state. Where it didn't, one that idled gets -EAGAIN: an idled * firmware doesn't wake with its power kept, so it stays down, and comes back the * same way after the next sleep that does reach the hardware. One whose idle - * failed still runs, and gets 0. + * failed still runs, and gets 0 once its AP state is on again. * * @cpu_control: the ASC's CPU control register. */ From b0f149b4fcfb1530622eb405f21debc9e184d971 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 05:28:26 -0500 Subject: [PATCH 06/12] dmaengine: apple-sio: stay asleep when the firmware doesn't come back After a real sleep, sio_resume_early() clears sio->asleep before it tries the warm restart. If that fails, SIO's firmware is down but marked awake, so the next sio_suspend_late() sends an idle notice to nothing, times out and refuses the sleep, and so does every later one until reboot. Clear asleep only once the firmware is back. A failed restart then leaves SIO as the sleep left it: the next real sleep restarts it, and a refused one takes the existing engine-reset path. Signed-off-by: Diljit Singh --- drivers/dma/apple-sio.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/drivers/dma/apple-sio.c b/drivers/dma/apple-sio.c index 1ecbec8298cdf..3b9d804f206d1 100644 --- a/drivers/dma/apple-sio.c +++ b/drivers/dma/apple-sio.c @@ -2161,14 +2161,15 @@ static __maybe_unused int sio_resume_early(struct device *dev) dev_info(dev, "sleep refused: firmware back after a reset\n"); return 0; } - sio->asleep = false; - /* Endpoints and parameters stay with the firmware: nothing is sent again. */ ret = apple_rtkit_sleep_return(sio->rtk, sio->base + REG_CPU_CONTROL); - if (ret) + if (ret) { + /* Still down: the next sleep restarts it, or resets it if refused. */ dev_err(dev, "firmware did not come back from sleep: %d\n", ret); - else - dev_dbg(dev, "firmware back from sleep\n"); + return 0; + } + sio->asleep = false; + dev_dbg(dev, "firmware back from sleep\n"); return 0; } From 9f6b5ad7a535ede02a19c564e959d3f5735067ab Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 05:29:22 -0500 Subject: [PATCH 07/12] soc: apple: rtkit-helper: end a sleep whose .prepare failed When the MTP doesn't answer the AP idle notice in time, .prepare refuses the sleep and leaves idle_unanswered for .complete, which sends AP on if the answer came late. But the PM core calls no .complete for a device whose .prepare failed. A late answer then leaves the MTP, which carries the internal keyboard and trackpad, told the AP is idle while the system runs, until a later sleep gets through. Do the same end-of-sleep step from a PM_POST_SUSPEND notifier when idle_unanswered is still set. That runs after the failed suspend has unwound, so a late answer has had time to arrive, and is a no-op after any sleep whose .complete ran. Signed-off-by: Diljit Singh --- drivers/soc/apple/rtkit-helper.c | 53 +++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/drivers/soc/apple/rtkit-helper.c b/drivers/soc/apple/rtkit-helper.c index 498d1413e14ef..59bb373e954ac 100644 --- a/drivers/soc/apple/rtkit-helper.c +++ b/drivers/soc/apple/rtkit-helper.c @@ -43,6 +43,8 @@ struct apple_rtkit_helper { bool ap_idle; /* It was told, gave no answer, and wasn't told otherwise. */ bool idle_unanswered; + /* Ends a sleep whose .prepare failed, which gets no .complete. */ + struct notifier_block pm_nb; void __iomem *asc_base; @@ -241,6 +243,8 @@ static int apple_rtkit_helper_inherit_buffers( return count; } +static int apple_rtkit_helper_pm_register(struct apple_rtkit_helper *helper); + static int apple_rtkit_helper_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; @@ -343,6 +347,9 @@ static int apple_rtkit_helper_probe(struct platform_device *pdev) helper->rtk); if (ret) return ret; + ret = apple_rtkit_helper_pm_register(helper); + if (ret) + return ret; /* There is no safe module-unload boundary for the retained session. */ __module_get(THIS_MODULE); @@ -367,7 +374,7 @@ static int apple_rtkit_helper_probe(struct platform_device *pdev) if (ret != 0) return dev_err_probe(dev, ret, "Failed to wake up coprocessor\n"); - return 0; + return apple_rtkit_helper_pm_register(helper); } static void apple_rtkit_helper_remove(struct platform_device *pdev) @@ -393,7 +400,7 @@ static int apple_rtkit_helper_ap_power(struct apple_rtkit_helper *helper, bool o on ? "on" : "idle", ret); /* * Nothing is sent to undo an unanswered idle notice, which macOS never does: - * .complete looks whether a late answer came, and only then sends AP on. + * the end of the sleep looks whether a late answer came, and only then sends AP on. */ if (ret && !on) helper->idle_unanswered = true; @@ -434,16 +441,13 @@ static int __maybe_unused apple_rtkit_helper_prepare(struct device *dev) return apple_rtkit_helper_ap_idle(dev_get_drvdata(dev)); } -/* A sleep refused before this device's .suspend never calls its .resume. */ -static void __maybe_unused apple_rtkit_helper_complete(struct device *dev) +static void apple_rtkit_helper_sleep_end(struct apple_rtkit_helper *helper) { - struct apple_rtkit_helper *helper = dev_get_drvdata(dev); - if (helper->idle_unanswered) { bool late = !apple_rtkit_is_running(helper->rtk); helper->idle_unanswered = false; - dev_info(dev, "after the unanswered idle notice: %s\n", + dev_info(helper->dev, "after the unanswered idle notice: %s\n", late ? "its answer came late, sending AP on" : "still no answer, nothing sent"); helper->ap_idle = late; @@ -451,6 +455,41 @@ static void __maybe_unused apple_rtkit_helper_complete(struct device *dev) apple_rtkit_helper_ap_on(helper); } +/* A sleep refused before this device's .suspend never calls its .resume. */ +static void __maybe_unused apple_rtkit_helper_complete(struct device *dev) +{ + apple_rtkit_helper_sleep_end(dev_get_drvdata(dev)); +} + +/* The PM core calls no .complete after a failed .prepare: end that sleep here. */ +static int apple_rtkit_helper_pm_notify(struct notifier_block *nb, unsigned long action, + void *data) +{ + struct apple_rtkit_helper *helper = container_of(nb, struct apple_rtkit_helper, pm_nb); + + if (action == PM_POST_SUSPEND && helper->idle_unanswered) + apple_rtkit_helper_sleep_end(helper); + return NOTIFY_DONE; +} + +static void apple_rtkit_helper_pm_unregister(void *data) +{ + struct apple_rtkit_helper *helper = data; + + unregister_pm_notifier(&helper->pm_nb); +} + +static int apple_rtkit_helper_pm_register(struct apple_rtkit_helper *helper) +{ + int ret; + + helper->pm_nb.notifier_call = apple_rtkit_helper_pm_notify; + ret = register_pm_notifier(&helper->pm_nb); + if (ret) + return ret; + return devm_add_action_or_reset(helper->dev, apple_rtkit_helper_pm_unregister, helper); +} + static int __maybe_unused apple_rtkit_helper_suspend(struct device *dev) { struct apple_rtkit_helper *helper = dev_get_drvdata(dev); From 8126fd36779104f80baf55d0d997e92356587290 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 05:29:44 -0500 Subject: [PATCH 08/12] pmdomain: apple: pmgr-pwrstate: don't gate a domain its driver took down The USB host's PHY lowers ATC0_USB and ATC0_COMMON itself at noirq, and the resync after a wake leaves them to it. genpd's own noirq power-off doesn't know: it keeps ATC0_USB only while a device in it is on the wakeup path and the domain has GENPD_FLAG_ACTIVE_WAKEUP, which it gets only when it was on at probe. Otherwise genpd powers ATC0_USB "off" and walks up, and ATC0_USB_AON, the always-on side the in-place sleep keeps, really goes off. At the wake, or after a refused sleep, genpd then raises USB_AON, COMMON and USB before the PHY opens the fabric bridge, the order the resync avoids. The host restarts and its devices enumerate again. During system sleep, refuse genpd's power-off of a domain its driver took down, as for a sleep-retained one. genpd then leaves it and its parents alone, and has nothing to raise. Signed-off-by: Diljit Singh --- drivers/pmdomain/apple/pmgr-pwrstate.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/drivers/pmdomain/apple/pmgr-pwrstate.c b/drivers/pmdomain/apple/pmgr-pwrstate.c index 5d39e61687d60..8ec8e3c31f48f 100644 --- a/drivers/pmdomain/apple/pmgr-pwrstate.c +++ b/drivers/pmdomain/apple/pmgr-pwrstate.c @@ -341,6 +341,12 @@ static int apple_pmgr_ps_power_off(struct generic_pm_domain *genpd) /* A live coprocessor keeps this domain through system sleep. */ if (ps->sleep_retained && ps->system_sleep) return -EBUSY; + /* + * Its driver took it down for the sleep and raises it in its own order. Gating it + * here would take its parents too, and genpd would raise them all first at the wake. + */ + if (ps->system_sleep && apple_sleep_domain_is_down(ps->dev->of_node)) + return -EBUSY; ret = apple_pmgr_ps_set(genpd, APPLE_PMGR_PS_PWRGATE, false); if (ret) From ac7a539658d1ec390bbd66e2ac02636195fe7053 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 05:29:55 -0500 Subject: [PATCH 09/12] dmaengine: apple-admac: set held channels up again after a refused sleep admac_resume_early() marks a held channel's registers lost only when the sleep reached the hardware. The ADMACs' power domains are plain leaves, though, so genpd powers them off at noirq in every system sleep, and a firmware-driven sleep refused after that (a wakeup event, or a refusal in the platform's .enter) brings them back reset. The open stream then runs with no SRAM carveout and the default channel setup until it is reopened. Mark them lost after any firmware-driven sleep. The restore waits for the next transfer start and only writes the saved values back, so it costs nothing when the registers survived. SoCs whose suspend isn't firmware-driven are unchanged. Signed-off-by: Diljit Singh --- drivers/dma/apple-admac.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/dma/apple-admac.c b/drivers/dma/apple-admac.c index ee660cc38992f..b4a757d8c69e9 100644 --- a/drivers/dma/apple-admac.c +++ b/drivers/dma/apple-admac.c @@ -1116,9 +1116,9 @@ static int admac_runtime_resume(struct device *dev) } /* - * A system sleep that reached the hardware takes the registers of a channel a - * client still holds, and its client won't configure it again: only a new - * hw_params does that. + * A firmware-driven sleep, refused or not, takes the registers of a channel a + * client still holds (genpd gates the domain at noirq), and its client won't + * configure it again: only a new hw_params does that. */ static int __maybe_unused admac_resume_early(struct device *dev) { @@ -1126,7 +1126,7 @@ static int __maybe_unused admac_resume_early(struct device *dev) unsigned long flags; int i; - if (!pm_resume_via_firmware()) + if (!pm_suspend_via_firmware()) return 0; mutex_lock(&ad->cache_alloc_lock); From f4647486fa74faed7c509809d0a8ce606ea8241c Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 05:30:10 -0500 Subject: [PATCH 10/12] usb: dwc3: apple: end a sleep from the PHY's work, not the glue's After a wake, the glue's sleep_finish work runs apple_atcphy_sleep_finish(), which may update the SuperSpeed mux, and atcphy_usb3_pm_get() takes the dwc3-apple device lock for that. An unbind of dwc3-apple holds that lock across remove() and devres, where devm_work_autocancel() waits for the same work: the unbind hangs, and the next dpm_prepare() blocks on the device lock too. Queue the finish on a work the PHY owns, with a flush for the next sleep's PM_SUSPEND_PREPARE. The PHY's unbind cancels it. An unbind of the controller no longer waits for it: the work takes the lock once the unbind is done, finds no driver bound, and gets -EAGAIN. Signed-off-by: Diljit Singh --- drivers/phy/apple/atc.c | 54 +++++++++++++++++++++++++++++++---- drivers/usb/dwc3/dwc3-apple.c | 27 ++++++------------ include/linux/phy/apple-atc.h | 12 ++++++++ 3 files changed, 69 insertions(+), 24 deletions(-) diff --git a/drivers/phy/apple/atc.c b/drivers/phy/apple/atc.c index 329178d06f850..7a5d4f660f0c4 100644 --- a/drivers/phy/apple/atc.c +++ b/drivers/phy/apple/atc.c @@ -788,6 +788,8 @@ enum atcphy_t8132_cio_tunable { * off for a PIPE or lane change and is not powered on again yet * @usb3_port_work: Retries the power-on of @usb3_port_off after a USB failure * @usb3_port_retries: Retries left for @usb3_port_work + * @sleep_finish_work: Ends a sleep for the controller, outside its device lock + * @sleep_finish_kept: Whether the controller kept its state, for @sleep_finish_work * @mode: Current PHY operating mode * @swap_lanes: True if lanes must be swapped due to cable orientation * @dp_link_rate: DisplayPort link rate @@ -919,6 +921,8 @@ struct apple_atcphy { bool usb3_port_off; struct delayed_work usb3_port_work; unsigned int usb3_port_retries; + struct work_struct sleep_finish_work; + bool sleep_finish_kept; enum atcphy_mode mode; int dp_link_rate; bool swap_lanes; @@ -6547,6 +6551,8 @@ static bool atcphy_has_t8103_usb3_workaround(const struct atcphy_hw *hw, of_device_is_compatible(root, "apple,t8103"); } +static void atcphy_sleep_finish_work(struct work_struct *work); + static int atcphy_probe(struct platform_device *pdev) { struct apple_atcphy *atcphy; @@ -6569,6 +6575,9 @@ static int atcphy_probe(struct platform_device *pdev) mutex_init(&atcphy->lock); platform_set_drvdata(pdev, atcphy); ret = devm_delayed_work_autocancel(dev, &atcphy->usb3_port_work, atcphy_usb3_port_work); + if (ret) + return ret; + ret = devm_work_autocancel(dev, &atcphy->sleep_finish_work, atcphy_sleep_finish_work); if (ret) return ret; @@ -7101,12 +7110,8 @@ EXPORT_SYMBOL_GPL(apple_atcphy_sleep_result); * again. Only the PHY's own failures stop it for the later sleeps; a sleep refused for a * USB change doesn't. */ -void apple_atcphy_sleep_finish(struct phy *usb2_phy, bool kept) +static void atcphy_sleep_finish(struct apple_atcphy *atcphy, bool kept) { - struct apple_atcphy *atcphy = atcphy_from_usb2_phy(usb2_phy); - - if (!atcphy) - return; scoped_guard(mutex, &atcphy->lock) { if (!kept && atcphy->sleep_ret < 0 && atcphy->sleep_ret != -EBUSY) atcphy->sleep_no_keep = true; @@ -7115,8 +7120,47 @@ void apple_atcphy_sleep_finish(struct phy *usb2_phy, bool kept) } atcphy_sleep_ss_up(atcphy); } + +void apple_atcphy_sleep_finish(struct phy *usb2_phy, bool kept) +{ + struct apple_atcphy *atcphy = atcphy_from_usb2_phy(usb2_phy); + + if (atcphy) + atcphy_sleep_finish(atcphy, kept); +} EXPORT_SYMBOL_GPL(apple_atcphy_sleep_finish); +static void atcphy_sleep_finish_work(struct work_struct *work) +{ + struct apple_atcphy *atcphy = container_of(work, struct apple_atcphy, sleep_finish_work); + + atcphy_sleep_finish(atcphy, READ_ONCE(atcphy->sleep_finish_kept)); +} + +/* + * The SuperSpeed side may wait for the controller's device lock, which an unbind of the + * controller holds while it cancels the controller's own work: so this work is the PHY's. + */ +void apple_atcphy_sleep_finish_async(struct phy *usb2_phy, bool kept) +{ + struct apple_atcphy *atcphy = atcphy_from_usb2_phy(usb2_phy); + + if (!atcphy) + return; + WRITE_ONCE(atcphy->sleep_finish_kept, kept); + queue_work(system_unbound_wq, &atcphy->sleep_finish_work); +} +EXPORT_SYMBOL_GPL(apple_atcphy_sleep_finish_async); + +void apple_atcphy_sleep_finish_flush(struct phy *usb2_phy) +{ + struct apple_atcphy *atcphy = atcphy_from_usb2_phy(usb2_phy); + + if (atcphy) + flush_work(&atcphy->sleep_finish_work); +} +EXPORT_SYMBOL_GPL(apple_atcphy_sleep_finish_flush); + static int atcphy_suspend(struct device *dev) { struct apple_atcphy *atcphy = dev_get_drvdata(dev); diff --git a/drivers/usb/dwc3/dwc3-apple.c b/drivers/usb/dwc3/dwc3-apple.c index f3c3abd0a9f7e..5afe5268cdf98 100644 --- a/drivers/usb/dwc3/dwc3-apple.c +++ b/drivers/usb/dwc3/dwc3-apple.c @@ -185,8 +185,6 @@ struct dwc3_apple { bool sleep_reinit; /* Looks for the fixed hub once a host restarted after a sleep has had time to find it. */ struct delayed_work hub_check; - struct work_struct sleep_finish; - bool sleep_finish_kept; /* The core didn't come back from a sleep, and nothing may touch it until a reboot. */ bool sleep_dead; /* Stopped for a deep sleep: the state to come back to, and the wakeup setting. */ @@ -1295,7 +1293,7 @@ static int dwc3_apple_pm_notify(struct notifier_block *nb, unsigned long action, * check takes the glue's lock, and has nothing to look at until the sleep is over. */ if (action == PM_SUSPEND_PREPARE) { - flush_work(&appledwc->sleep_finish); + apple_atcphy_sleep_finish_flush(appledwc->usb2_phy); cancel_delayed_work_sync(&appledwc->hub_check); } if (action == PM_SUSPEND_PREPARE && !pm_suspend_default_s2idle()) { @@ -1308,24 +1306,18 @@ static int dwc3_apple_pm_notify(struct notifier_block *nb, unsigned long action, scoped_guard(mutex, &appledwc->lock) ret = dwc3_apple_pm_notify_locked(appledwc, action, in_place, &finish, &kept); - /* After a wake, user space doesn't wait for SuperSpeed: it comes back in the background. */ - if (finish && action == PM_POST_SUSPEND) { - WRITE_ONCE(appledwc->sleep_finish_kept, kept); - queue_work(system_unbound_wq, &appledwc->sleep_finish); - } else if (finish) { + /* + * After a wake, user space doesn't wait for SuperSpeed: it comes back in the background, + * from the PHY's work, which an unbind of this device doesn't wait for under its lock. + */ + if (finish && action == PM_POST_SUSPEND) + apple_atcphy_sleep_finish_async(appledwc->usb2_phy, kept); + else if (finish) apple_atcphy_sleep_finish(appledwc->usb2_phy, kept); - } return ret; } -static void dwc3_apple_sleep_finish(struct work_struct *work) -{ - struct dwc3_apple *appledwc = container_of(work, struct dwc3_apple, sleep_finish); - - apple_atcphy_sleep_finish(appledwc->usb2_phy, READ_ONCE(appledwc->sleep_finish_kept)); -} - static void dwc3_apple_pm_notifier_remove(void *data) { struct dwc3_apple *appledwc = data; @@ -1430,9 +1422,6 @@ static int dwc3_apple_probe(struct platform_device *pdev) */ appledwc->state = DWC3_APPLE_PROBE_PENDING; ret = devm_delayed_work_autocancel(dev, &appledwc->hub_check, dwc3_apple_hub_check); - if (ret) - return ret; - ret = devm_work_autocancel(dev, &appledwc->sleep_finish, dwc3_apple_sleep_finish); if (ret) return ret; appledwc->pm_nb.notifier_call = dwc3_apple_pm_notify; diff --git a/include/linux/phy/apple-atc.h b/include/linux/phy/apple-atc.h index 156db66eb7036..bc91b4206310f 100644 --- a/include/linux/phy/apple-atc.h +++ b/include/linux/phy/apple-atc.h @@ -37,6 +37,10 @@ int apple_atcphy_sleep_result(struct phy *usb2_phy); * controller stop for later sleeps. */ void apple_atcphy_sleep_finish(struct phy *usb2_phy, bool kept); + +/* The same from the PHY's work, for after a wake; the flush waits for it. */ +void apple_atcphy_sleep_finish_async(struct phy *usb2_phy, bool kept); +void apple_atcphy_sleep_finish_flush(struct phy *usb2_phy); #else static inline int apple_atcphy_sleep_prepare(struct phy *usb2_phy) { @@ -51,6 +55,14 @@ static inline int apple_atcphy_sleep_result(struct phy *usb2_phy) static inline void apple_atcphy_sleep_finish(struct phy *usb2_phy, bool kept) { } + +static inline void apple_atcphy_sleep_finish_async(struct phy *usb2_phy, bool kept) +{ +} + +static inline void apple_atcphy_sleep_finish_flush(struct phy *usb2_phy) +{ +} #endif #endif From 8e92d54aaba98c65e9c7a8d7ccb2c63abc7a6f84 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 05:30:18 -0500 Subject: [PATCH 11/12] nvme-apple: let the last wake's reset finish before sleeping again After a T8140 sleep, apple_nvme_resume() only queues the reset work, which waits for ANS and sets the controller up again. A dark wake goes straight back to sleep, so the next apple_nvme_suspend() can arrive while the controller is still RESETTING or CONNECTING: sleep_wanted is false and the change to RESETTING fails with -EBUSY, which turns the dark wake into a full wake behind the closed lid. That .suspend also clears sleep_failed and sleep_restarted while the reset work reads them. Flush the reset work before deciding. It is idle in every other case, and its wait for ANS is bounded. Signed-off-by: Diljit Singh --- drivers/nvme/host/apple.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/nvme/host/apple.c b/drivers/nvme/host/apple.c index 48b8433395de6..9b80464657437 100644 --- a/drivers/nvme/host/apple.c +++ b/drivers/nvme/host/apple.c @@ -2942,6 +2942,8 @@ static int apple_nvme_suspend(struct device *dev) int ret; if (anv->hw->needs_ioq_registers) { + /* A sleep right after a wake, as from a dark wake: let that reset finish first. */ + flush_work(&anv->ctrl.reset_work); anv->sleep_stop = apple_nvme_sleep_wanted(anv); anv->sleep_idled = false; anv->sleep_failed = false; From cc859e5150c4acb27654fa2862eeb5d7388e6dae Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 05:30:25 -0500 Subject: [PATCH 12/12] dmaengine: apple-admac: rewind a lost paused transfer to what was reclaimed Resuming a paused cyclic transfer whose rings a sleep took restarts it with admac_start_current_tx(), but keeps submitted_pos and nperiods_outstanding from before the sleep. The descriptors the reset dropped never report, so the position stays up to the ring depth behind the DMA for the rest of the stream: playback underruns go unnoticed and capture can overwrite unread periods. Rewind the transfer to the last period reclaimed before restarting it. Signed-off-by: Diljit Singh --- drivers/dma/apple-admac.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/dma/apple-admac.c b/drivers/dma/apple-admac.c index b4a757d8c69e9..34599811779fb 100644 --- a/drivers/dma/apple-admac.c +++ b/drivers/dma/apple-admac.c @@ -715,7 +715,11 @@ static int admac_resume(struct dma_chan *chan) if (adchan->current_tx && adchan->current_tx->error) { ret = -EIO; } else if (adchan->lost && adchan->current_tx) { - /* The rings went with the registers: start from the top. */ + struct admac_tx *tx = adchan->current_tx; + + /* The rings went with the registers: start again from the last period reclaimed. */ + tx->submitted_pos = tx->reclaimed_pos; + tx->nperiods_outstanding = 0; adchan->nperiod_acks = 0; admac_start_current_tx(adchan); } else {