From 07a949f1170e18a9da547eae987d8936092047c3 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:13 -0400 Subject: [PATCH 1/7] iommu/apple-dart: Keep the retained J713 DARTs through suspend-to-idle The J713's retained DMA owners refuse system sleep: the radio's DART with managed streams, and the display DARTs, which borrow the running DCP's live page-table root. Neither has a suspend boundary of its own, and their state cannot be restored from a copy. Suspend-to-idle does not need one. These DARTs have no power domain of their own in the tree, and the DCP keeps the display domains on across s2idle, so their state stays where it is. Let them through for suspend-to-idle only, saving their registers solely for resume to check that nothing moved. Every other sleep state is still refused. A DART whose retained state changed cannot be trusted with its live DMA, and a resume error does not stop the PM core from resuming consumers. Quarantine the managed streams and the borrowed display consumers through the same paths as a translation fault before failing resume, so that their later maps are refused. A host test exercises the extracted check, quarantine and map functions. Signed-off-by: Erik Bourget --- drivers/iommu/apple-dart.c | 68 ++++-- .../drivers/apple/dart-s2idle-retention.py | 218 ++++++++++++++++++ 2 files changed, 272 insertions(+), 14 deletions(-) create mode 100755 tools/testing/selftests/drivers/apple/dart-s2idle-retention.py diff --git a/drivers/iommu/apple-dart.c b/drivers/iommu/apple-dart.c index fbe1ba33089b4..81fd469a6c5c3 100644 --- a/drivers/iommu/apple-dart.c +++ b/drivers/iommu/apple-dart.c @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -311,6 +312,8 @@ struct apple_dart { struct apple_dart_dcp *dcp; struct apple_dart_dcp *frontend[2]; bool preserve_display; + /* Retained radio/display state left in place across suspend-to-idle. */ + bool s2idle_kept; /* Resources reachable by a retained domain outlive device removal. */ bool retained; bool retained_mtp; @@ -4718,10 +4721,23 @@ static const struct apple_dart_hw apple_dart_hw_t8110 = { .ttbr_count = 1, }; +static void apple_dart_save_state(struct apple_dart *dart) +{ + unsigned int sid, idx; + + for (sid = 0; sid < dart->num_streams; sid++) { + dart->save_tcr[sid] = apple_dart_readl(dart, + DART_TCR(dart, sid)); + for (idx = 0; idx < dart->hw->ttbr_count; idx++) + dart->save_ttbr[sid][idx] = + apple_dart_readl(dart, + DART_TTBR(dart, sid, idx)); + } +} + static __maybe_unused int apple_dart_suspend(struct device *dev) { struct apple_dart *dart = dev_get_drvdata(dev); - unsigned int sid, idx; dart->tunnel_state_restored = false; if (READ_ONCE(dart->commands_gated)) @@ -4733,9 +4749,19 @@ static __maybe_unused int apple_dart_suspend(struct device *dev) */ if (dart->cold_ane) return 0; - /* Neither retained radio nor display has a qualified suspend boundary. */ - if (dart->managed_sids || dart->preserve_display || apple_dart_has_borrowed(dart)) - return -EBUSY; + /* + * Retained radio and display DMA has no suspend boundary of its own. + * Suspend-to-idle leaves it running in place: these DARTs have no + * power domain here, and the DCP keeps the display domains on. The + * registers are saved only for resume to check. + */ + if (dart->managed_sids || dart->preserve_display || apple_dart_has_borrowed(dart)) { + if (pm_suspend_target_state != PM_SUSPEND_TO_IDLE) + return -EBUSY; + apple_dart_save_state(dart); + dart->s2idle_kept = true; + return 0; + } /* The tunneled DART has no state to save when its domain stays on. */ if (dart->power_retained || dart->retained_mtp) return 0; @@ -4744,15 +4770,7 @@ static __maybe_unused int apple_dart_suspend(struct device *dev) if (dart->locked) return 0; - for (sid = 0; sid < dart->num_streams; sid++) { - dart->save_tcr[sid] = apple_dart_readl(dart, - DART_TCR(dart, sid)); - for (idx = 0; idx < dart->hw->ttbr_count; idx++) - dart->save_ttbr[sid][idx] = - apple_dart_readl(dart, - DART_TTBR(dart, sid, idx)); - } - + apple_dart_save_state(dart); return 0; } @@ -4816,6 +4834,28 @@ static int apple_dart_invalidate_each_stream(struct apple_dart *dart) return 0; } +/* + * Resume of a DART whose live DMA stayed in place through suspend-to-idle. It + * cannot be restored from the saved copy. If the translations changed, the + * consumers' DMA is no longer what they set up: refuse their further use + * through the same paths as a translation fault before failing resume, since + * a resume error does not stop the PM core from resuming them. + */ +static int apple_dart_resume_retained(struct apple_dart *dart) +{ + if (!dart->s2idle_kept) + return -EBUSY; + dart->s2idle_kept = false; + if (apple_dart_state_retained(dart)) + return 0; + + dev_err(dart->dev, "retained DMA state lost in suspend-to-idle\n"); + if (dart->managed_sids) + apple_dart_managed_quarantine(dart); + apple_dart_quarantine_borrowed(dart); + return -EIO; +} + static __maybe_unused int apple_dart_resume(struct device *dev) { struct apple_dart *dart = dev_get_drvdata(dev); @@ -4826,7 +4866,7 @@ static __maybe_unused int apple_dart_resume(struct device *dev) if (dart->cold_ane) return 0; if (dart->managed_sids || dart->preserve_display || apple_dart_has_borrowed(dart)) - return -EBUSY; + return apple_dart_resume_retained(dart); /* PCIe-C restored these mappings before starting its downstream link. */ if (dart->tunnel_state_restored) { diff --git a/tools/testing/selftests/drivers/apple/dart-s2idle-retention.py b/tools/testing/selftests/drivers/apple/dart-s2idle-retention.py new file mode 100755 index 0000000000000..44682a977f716 --- /dev/null +++ b/tools/testing/selftests/drivers/apple/dart-s2idle-retention.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-only OR MIT +"""Exercise the suspend-to-idle resume check of retained DARTs on the host. + +The actual state check, quarantine and map functions are extracted from +apple-dart.c. A retained DART whose translation registers changed must fail +resume and quarantine its consumers, so that later maps are refused. These CPU +tests do not qualify physical retention on a J713. +""" +from pathlib import Path +import subprocess +import tempfile + + +def definition(source, name): + start = source.index(name) + start = source.rfind('\n', 0, start) + 1 + brace = source.index('{', start) + depth, end = 1, brace + 1 + while depth: + depth += (source[end] == '{') - (source[end] == '}') + end += 1 + return source[start:end] + + +PRELUDE = r''' +#include +#include +#include +#include +#include +#include +#include +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +typedef u64 phys_addr_t; +typedef unsigned int gfp_t; +#define BIT(n) (1UL << (n)) +#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) +#define WRITE_ONCE(x, v) ((x) = (v)) +#define READ_ONCE(x) (x) +#define DART_MAX_STREAMS 32 +#define DART_MAX_TTBR 4 +#define DART_TCR(d, sid) (0x100 + 4 * (sid)) +#define DART_TTBR(d, sid, idx) (0x200 + 16 * (sid) + 4 * (idx)) +#define container_of(p, t, m) ((t *)((char *)(p) - offsetof(t, m))) +#define lockdep_assert_held(l) ((void)(l)) +#define spin_lock_irqsave(l, f) ((void)(l), (f) = 0) +#define spin_unlock_irqrestore(l, f) ((void)(l), (void)(f)) +static int errors; +#define dev_err(dev, ...) (errors++) +static void __set_bit(unsigned int n, unsigned long *p) { p[n / 64] |= 1UL << (n % 64); } + +struct apple_dart_hw { u32 tcr_bypass, ttbr_valid; unsigned int ttbr_count; }; +struct apple_dart; +struct apple_dart_dcp { bool failed; struct apple_dart *dart; }; +struct apple_dart { + const struct apple_dart_hw *hw; + u16 managed_sids; + bool managed_failed, supports_bypass, s2idle_kept; + int managed_lock; + struct { bool ready; } iommu; + struct apple_dart_dcp *dcp, *frontend[2]; + void *dev; + unsigned int num_streams; + u32 save_tcr[DART_MAX_STREAMS], save_ttbr[DART_MAX_STREAMS][DART_MAX_TTBR]; + u32 regs[0x400 / 4]; +}; +struct apple_dart_stream_map { struct apple_dart *dart; unsigned long sidmap[4]; }; +static u32 apple_dart_readl(struct apple_dart *d, unsigned int o) { return d->regs[o / 4]; } +static unsigned long disabled; +static void apple_dart_hw_disable_dma(struct apple_dart_stream_map *m) { disabled |= m->sidmap[0]; } + +struct iommu_domain { int unused; }; +struct io_pgtable_ops { + int (*map_pages)(struct io_pgtable_ops *, unsigned long, phys_addr_t, size_t, size_t, int, + gfp_t, size_t *); +}; +struct apple_dart_domain { + struct iommu_domain domain; + struct io_pgtable_ops *pgtbl_ops; + struct apple_dart_dcp *dcp; + struct apple_dart_stream_map stream_maps[1]; + u64 dma_offset, mask; +}; +#define to_dart_domain(d) container_of(d, struct apple_dart_domain, domain) +static int apple_dart_check_fw_map(struct apple_dart_domain *d, u64 dva, phys_addr_t pa, + size_t size, int prot) { return 0; } +static int apple_dart_dcp_map(struct apple_dart_dcp *dcp, unsigned long iova, phys_addr_t pa, + size_t pgsize, size_t count, int prot, gfp_t gfp, size_t *mapped) +{ assert(!"the managed radio domain is not a borrowed display domain"); return 0; } +static int maps; +static int test_map(struct io_pgtable_ops *ops, unsigned long iova, phys_addr_t pa, size_t pgsize, + size_t count, int prot, gfp_t gfp, size_t *mapped) +{ maps++; *mapped = pgsize * count; return 0; } +''' + +TEST = r''' +static const struct apple_dart_hw hw = { .tcr_bypass = 0, .ttbr_valid = 1, .ttbr_count = 1 }; +static struct apple_dart_dcp dcp, front0, front1; +static struct apple_dart d; +static struct io_pgtable_ops ops = { .map_pages = test_map }; +static struct apple_dart_domain radio; + +static void setup(void) +{ + memset(&d, 0, sizeof(d)); + dcp.failed = front0.failed = front1.failed = false; + d.hw = &hw; + d.num_streams = 16; + d.managed_sids = BIT(1) | BIT(2); + d.dcp = &dcp; + d.frontend[0] = &front0; + d.frontend[1] = &front1; + d.iommu.ready = true; + /* Live translations on the radio's streams and on an ordinary one. */ + d.regs[DART_TCR(&d, 1) / 4] = d.regs[DART_TCR(&d, 2) / 4] = d.regs[DART_TCR(&d, 5) / 4] = 1; + d.regs[DART_TTBR(&d, 1, 0) / 4] = 0x1001; + d.regs[DART_TTBR(&d, 2, 0) / 4] = 0x2001; + d.regs[DART_TTBR(&d, 5, 0) / 4] = 0x5001; + for (unsigned int sid = 0; sid < d.num_streams; sid++) { + d.save_tcr[sid] = d.regs[DART_TCR(&d, sid) / 4]; + d.save_ttbr[sid][0] = d.regs[DART_TTBR(&d, sid, 0) / 4]; + } + d.s2idle_kept = true; + disabled = 0; + maps = 0; + errors = 0; + radio = (struct apple_dart_domain){ .pgtbl_ops = &ops, .mask = ~0ULL, + .stream_maps = {{ .dart = &d }} }; +} + +static int map_16k(void) +{ + size_t mapped = 0; + return apple_dart_map_pages(&radio.domain, 0x10000, 0x800000000ULL, 0x4000, 1, 3, 0, &mapped); +} + +static void expect_quarantined(int ret) +{ + assert(ret == -EIO && errors == 1); + assert(d.managed_failed && disabled == d.managed_sids); + assert(dcp.failed && front0.failed && front1.failed); + assert(map_16k() == -EIO && maps == 0); +} + +int main(void) +{ + /* Retained: resume succeeds and the radio maps as before. */ + setup(); + assert(apple_dart_resume_retained(&d) == 0 && !d.s2idle_kept); + assert(!d.managed_failed && !disabled && !dcp.failed && !errors); + assert(map_16k() == 0 && maps == 1); + + /* A changed TTBR on a managed stream. */ + setup(); + d.regs[DART_TTBR(&d, 1, 0) / 4] = 0x9001; + expect_quarantined(apple_dart_resume_retained(&d)); + + /* A changed TCR on an ordinary stream of the same DART. */ + setup(); + d.regs[DART_TCR(&d, 5) / 4] = 0; + expect_quarantined(apple_dart_resume_retained(&d)); + + /* A DART that lost power reads back as reset. */ + setup(); + memset(d.regs, 0, sizeof(d.regs)); + memset(d.save_tcr, 0, sizeof(d.save_tcr)); + memset(d.save_ttbr, 0, sizeof(d.save_ttbr)); + expect_quarantined(apple_dart_resume_retained(&d)); + + /* No suspend-to-idle suspend: refused, nothing quarantined. */ + setup(); + d.s2idle_kept = false; + assert(apple_dart_resume_retained(&d) == -EBUSY); + assert(!d.managed_failed && !dcp.failed && map_16k() == 0); + + puts("PASS retained DART resume: kept state resumes; lost state quarantines and refuses maps"); + return 0; +} +''' + + +def main(): + root = Path(__file__).resolve().parents[5] + dart = (root / 'drivers/iommu/apple-dart.c').read_text() + code = PRELUDE + for name in ('static bool apple_dart_state_retained(', + 'static void apple_dart_managed_quarantine_locked(', + 'static void apple_dart_managed_quarantine(', + 'static void apple_dart_quarantine_borrowed(', + 'static int apple_dart_resume_retained(', + 'static int apple_dart_map_pages('): + code += definition(dart, name) + '\n' + code += TEST + quarantine = ('\tif (dart->managed_sids)\n\t\tapple_dart_managed_quarantine(dart);\n' + '\tapple_dart_quarantine_borrowed(dart);\n') + assert code.count(quarantine) == 1, 'the resume quarantine was not found' + with tempfile.TemporaryDirectory() as directory: + c, exe = Path(directory) / 'resume.c', Path(directory) / 'resume' + for control in ('production', 'missing-quarantine'): + tested = code if control == 'production' else code.replace(quarantine, '') + c.write_text(tested) + subprocess.run(['cc', '-std=c11', '-Wall', '-Wextra', '-Werror', + '-Wno-unused-parameter', '-Wno-unused-function', + str(c), '-o', str(exe)], check=True) + result = subprocess.run([str(exe)], capture_output=True, text=True) + if control == 'production': + assert result.returncode == 0, result.stderr + print(result.stdout.strip()) + else: + assert result.returncode and 'Assertion' in result.stderr, result + print('Retained DART resume ' + control + ' negative control: detected at runtime') + + +if __name__ == '__main__': + main() From e6e4cde3bdeee05868b7bbf71b1338f89a2d74ef Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:13 -0400 Subject: [PATCH 2/7] PCI: apple: Let the managed J713 radio through suspend-to-idle The J713's radio port, whose streams the OS adopts from the boot loader's handoff, refuses system sleep in its noirq phase. It has no suspend boundary of its own, but suspend-to-idle does not need one: the port has no power domain in the tree, so it stays up as any other internal port does, whose resume does nothing. Let it through for suspend-to-idle only; every other sleep state is still refused. Signed-off-by: Erik Bourget --- drivers/pci/controller/pcie-apple.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/drivers/pci/controller/pcie-apple.c b/drivers/pci/controller/pcie-apple.c index 9ad89bba8415c..614b2577ad258 100644 --- a/drivers/pci/controller/pcie-apple.c +++ b/drivers/pci/controller/pcie-apple.c @@ -3638,8 +3638,12 @@ static int apple_pcie_suspend_noirq(struct device *dev) { struct apple_pcie *pcie = apple_pcie_lookup(dev); + /* + * The managed radio has no suspend boundary of its own; suspend-to-idle + * leaves it in place like any other internal port (no power domain). + */ if (pcie->managed_sids) - return -EBUSY; + return pm_suspend_target_state == PM_SUSPEND_TO_IDLE ? 0 : -EBUSY; if (!pcie->hw->tunneled) return 0; pcie->reset_on_resume = false; From 7c714f5f145f09e55769c11f2deb3a1dcb355491 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:14 -0400 Subject: [PATCH 3/7] dt-bindings: power: apple,pmgr-pwrstate: Add apple,sleep-retained A coprocessor can keep running in its power domain across system sleep without system-sleep callbacks of its own, as the J713's GPU firmware does. Describe such a domain, which the OS keeps on during system sleep; runtime power management is unaffected. Signed-off-by: Erik Bourget --- .../devicetree/bindings/power/apple,pmgr-pwrstate.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml b/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml index 919e53113fd2a..6ba123fbeba95 100644 --- a/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml +++ b/Documentation/devicetree/bindings/power/apple,pmgr-pwrstate.yaml @@ -22,6 +22,7 @@ allOf: then: properties: apple,firmware-owned: false + apple,sleep-retained: false apple,always-on: false apple,min-state: false apple,force-disable: false @@ -106,6 +107,13 @@ properties: coprocessors' CPU domains. type: boolean + apple,sleep-retained: + description: + A coprocessor keeps running in this domain across system sleep with no + system-sleep callbacks of its own. The OS keeps the domain on during + system sleep. Runtime power management is unaffected. + type: boolean + apple,firmware-owned: description: The boot loader hands over active hardware whose power domain remains From 8fd377b7d93eea9d1951c9c915c827c5ad80cc30 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:14 -0400 Subject: [PATCH 4/7] pmdomain: apple: pmgr-pwrstate: Keep sleep-retained domains on in system sleep The J713's G16 GPU has no system-sleep callbacks, so in the noirq phase genpd powers its GFX domain off, and the TVM gate above it, under the running GPU firmware; the power-on at resume then times out. Let a domain marked apple,sleep-retained decline its power-off while the system sleeps: genpd then keeps it and its parents on. Runtime power management is unchanged. Signed-off-by: Erik Bourget --- drivers/pmdomain/apple/pmgr-pwrstate.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/drivers/pmdomain/apple/pmgr-pwrstate.c b/drivers/pmdomain/apple/pmgr-pwrstate.c index a0350f831b6f2..5be4731606d9a 100644 --- a/drivers/pmdomain/apple/pmgr-pwrstate.c +++ b/drivers/pmdomain/apple/pmgr-pwrstate.c @@ -54,6 +54,8 @@ struct apple_pmgr_ps { bool externally_clocked; bool system_sleep; bool firmware_owned; + /* apple,sleep-retained: kept on while the system sleeps */ + bool sleep_retained; /* Never enable automatic power management (apple,no-auto-pm). */ bool no_auto_pm; bool inherited_on; @@ -263,6 +265,9 @@ static int apple_pmgr_ps_power_off(struct generic_pm_domain *genpd) return 0; if (ps->inherited_on) return -EBUSY; + /* A live coprocessor keeps this domain through system sleep. */ + if (ps->sleep_retained && ps->system_sleep) + return -EBUSY; ret = apple_pmgr_ps_set(genpd, APPLE_PMGR_PS_PWRGATE, false); if (ret) @@ -524,6 +529,7 @@ static int apple_pmgr_ps_probe(struct platform_device *pdev) * vote and never read, reset or change the hardware state. */ ps->firmware_owned = of_property_read_bool(node, "apple,firmware-owned"); + ps->sleep_retained = of_property_read_bool(node, "apple,sleep-retained"); /* * Only T8132 hands over inherited-on domains with this contract. Older * M3 device trees carry the property with another meaning; keep them @@ -531,7 +537,8 @@ static int apple_pmgr_ps_probe(struct platform_device *pdev) */ ps->inherited_on = t8132 && of_property_read_bool(node, "apple,inherited-on"); if (ps->inherited_on && - (ps->firmware_owned || of_property_present(node, "apple,min-state") || + (ps->firmware_owned || ps->sleep_retained || + of_property_present(node, "apple,min-state") || of_property_present(node, "apple,always-on") || of_property_present(node, "apple,force-disable") || of_property_present(node, "apple,force-reset") || From 36743381282d9b9319c65571858ec8bc63fe6955 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:14 -0400 Subject: [PATCH 5/7] dt-bindings: mailbox: apple,mailbox: Add apple,hold-rx-in-sleep Some coprocessors message the AP periodically while idle: the J713's GPU firmware asks for ring service about twice a second, and its AOP sends a report every second. Describe a mailbox whose messages need not wake the system, so the OS can leave them in the FIFO while it sleeps and handle them at resume. Signed-off-by: Erik Bourget --- .../devicetree/bindings/mailbox/apple,mailbox.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml index a1dfa81a65014..071ba0f3a6fed 100644 --- a/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml +++ b/Documentation/devicetree/bindings/mailbox/apple,mailbox.yaml @@ -114,6 +114,13 @@ properties: resets: maxItems: 1 + apple,hold-rx-in-sleep: + description: + Messages from this coprocessor need not wake the system. The OS may + leave them in the receive FIFO while the system sleeps and handle + them at resume. + type: boolean + power-domains: minItems: 1 maxItems: 2 From 7c5a1efc5f04b4904f94aafa7b9ade890c1e4dd1 Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:14 -0400 Subject: [PATCH 6/7] soc: apple: mailbox: Hold the receive side across system sleep on request The receive IRQ of the ASC mailboxes is IRQF_NO_SUSPEND, so every coprocessor message wakes a CPU out of suspend-to-idle. On the J713 the GPU firmware's idle ring-service kicks and an AOP report every second wake it about three times a second: in 938 s of s2idle, 1472 wakeups and only 512 s in deep idle. For a mailbox marked apple,hold-rx-in-sleep, mask the receive IRQ from noirq suspend to noirq resume, so its messages wait in the FIFO and the consumer handles them on resume. The mailbox is a supplier of its consumer, so this runs after the consumer suspended and before it resumes. The firmware throttles once its outbox fills: after 148 s of s2idle, 11 GPU and 3 AOP messages were waiting, with 3 wakeups and 146.9 s in deep idle. Signed-off-by: Erik Bourget --- drivers/soc/apple/mailbox.c | 30 ++++++++++++++++++++++++++++++ include/linux/soc/apple/mailbox.h | 2 ++ 2 files changed, 32 insertions(+) diff --git a/drivers/soc/apple/mailbox.c b/drivers/soc/apple/mailbox.c index 2591560a9b716..6f0d1f7957ec0 100644 --- a/drivers/soc/apple/mailbox.c +++ b/drivers/soc/apple/mailbox.c @@ -780,6 +780,7 @@ static int apple_mbox_probe(struct platform_device *pdev) return -ENOMEM; mbox->dev = &pdev->dev; + mbox->hold_rx_in_sleep = of_property_read_bool(dev->of_node, "apple,hold-rx-in-sleep"); mbox->hw = of_device_get_match_data(dev); if (!mbox->hw) return -EINVAL; @@ -1133,10 +1134,39 @@ static const struct of_device_id apple_mbox_of_match[] = { }; MODULE_DEVICE_TABLE(of, apple_mbox_of_match); +/* + * The receive IRQ is IRQF_NO_SUSPEND, so each message wakes a CPU out of + * suspend-to-idle. A coprocessor whose messages need not wake the system + * keeps them in the FIFO until resume. The mailbox is a supplier of its + * consumer, so this runs after the consumer suspended and before it resumes. + */ +static int apple_mbox_suspend_noirq(struct device *dev) +{ + struct apple_mbox *mbox = dev_get_drvdata(dev); + + if (mbox && mbox->hold_rx_in_sleep) + apple_mbox_hold_rx(mbox); + return 0; +} + +static int apple_mbox_resume_noirq(struct device *dev) +{ + struct apple_mbox *mbox = dev_get_drvdata(dev); + + if (mbox && mbox->hold_rx_in_sleep) + apple_mbox_release_rx(mbox); + return 0; +} + +static const struct dev_pm_ops apple_mbox_pm_ops = { + NOIRQ_SYSTEM_SLEEP_PM_OPS(apple_mbox_suspend_noirq, apple_mbox_resume_noirq) +}; + static struct platform_driver apple_mbox_driver = { .driver = { .name = "apple-mailbox", .of_match_table = apple_mbox_of_match, + .pm = pm_sleep_ptr(&apple_mbox_pm_ops), }, .probe = apple_mbox_probe, .remove = apple_mbox_remove, diff --git a/include/linux/soc/apple/mailbox.h b/include/linux/soc/apple/mailbox.h index 4c27621f22e0a..7a07772cc5967 100644 --- a/include/linux/soc/apple/mailbox.h +++ b/include/linux/soc/apple/mailbox.h @@ -64,6 +64,8 @@ struct apple_mbox { bool active; bool ascwrap_v6_cpu_running; bool removing; + /* apple,hold-rx-in-sleep: receive IRQ masked across system sleep */ + bool hold_rx_in_sleep; int irq_recv_not_empty; int irq_send_empty; /* < 0: no send-empty IRQ, TX polls */ From 4b2b3de62d6b944991c38939a475dbf6b842eabc Mon Sep 17 00:00:00 2001 From: Erik Bourget Date: Sun, 25 Oct 2026 20:20:14 -0400 Subject: [PATCH 7/7] arm64: dts: apple: t8132-j713: Keep the GPU on and quiet in system sleep The G16 firmware keeps running across suspend-to-idle: keep its GFX domain on through system sleep, and hold its idle ring-service kicks in the mailbox instead of waking the system about twice a second. Signed-off-by: Erik Bourget --- arch/arm64/boot/dts/apple/t8132-j713-gpu.dtsi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t8132-j713-gpu.dtsi b/arch/arm64/boot/dts/apple/t8132-j713-gpu.dtsi index 7cddbaf1e5b56..bd6bf689e84e7 100644 --- a/arch/arm64/boot/dts/apple/t8132-j713-gpu.dtsi +++ b/arch/arm64/boot/dts/apple/t8132-j713-gpu.dtsi @@ -80,6 +80,8 @@ /* ASCWrap v6 contains the same mailbox aperture used by ASC v4. */ compatible = "apple,t8132-asc-mailbox", "apple,asc-mailbox-v4"; reg = <0x3 0x02608000 0x0 0x4000>; + /* Idle ring-service kicks need not wake the system. */ + apple,hold-rx-in-sleep; interrupt-parent = <&aic>; interrupts = , , @@ -95,6 +97,8 @@ &ps_gfx { status = "okay"; power-domains = <&gfx_tvm>; + /* The G16 firmware stays live across suspend-to-idle. */ + apple,sleep-retained; }; /* The existing report provider owns readiness for shared PMP commands. */