From 2037f350528853f26c5d5521158629768f1906fd Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Tue, 20 Oct 2026 15:12:40 +0200 Subject: [PATCH 1/7] spmi: apple: Let peripheral interrupts wake the system The controller sits in an always-on block and its parent interrupt is a chained handler, so it keeps running through suspend. A wake-armed peripheral interrupt therefore needs nothing more from the chip, but without irq_set_wake or IRQCHIP_SKIP_SET_WAKE enable_irq_wake() fails and no SPMI device can wake the system. Set IRQCHIP_SKIP_SET_WAKE. Signed-off-by: Eryk Wieliczko --- drivers/spmi/spmi-apple-controller.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/drivers/spmi/spmi-apple-controller.c b/drivers/spmi/spmi-apple-controller.c index 172899d2bb219f..1cfaecdce65efd 100644 --- a/drivers/spmi/spmi-apple-controller.c +++ b/drivers/spmi/spmi-apple-controller.c @@ -308,7 +308,12 @@ static struct irq_chip apple_spmi_irq_chip = { .irq_unmask = apple_spmi_irq_unmask, .irq_ack = apple_spmi_irq_ack, .irq_set_type = apple_spmi_irq_set_type, - .flags = IRQCHIP_ONESHOT_SAFE, + /* + * The controller sits in an always-on block and its parent interrupt is + * chained, so it stays live through suspend: a wake-armed peripheral + * interrupt needs nothing more from the chip to wake the system. + */ + .flags = IRQCHIP_ONESHOT_SAFE | IRQCHIP_SKIP_SET_WAKE, }; static int apple_spmi_irq_domain_map(struct irq_domain *irqd, From 7099dacc3b8f1fd6e8bcf06ee286c4ac8c597459 Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Thu, 29 Oct 2026 07:36:08 +0100 Subject: [PATCH 2/7] dt-bindings: rtc: apple,smc-rtc: Describe the PMU alarm The PMU that keeps the RTC counter also has a one-shot alarm, reached through three more PMU registers and an SPMI peripheral interrupt that can wake the system. Allow the alarm control, compare and mask cells after the offset cell, all three or none, and the alarm interrupt and wakeup-source. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Eryk Wieliczko --- .../bindings/rtc/apple,smc-rtc.yaml | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/Documentation/devicetree/bindings/rtc/apple,smc-rtc.yaml b/Documentation/devicetree/bindings/rtc/apple,smc-rtc.yaml index 2d468e3c302c66..f0581393dbcd9b 100644 --- a/Documentation/devicetree/bindings/rtc/apple,smc-rtc.yaml +++ b/Documentation/devicetree/bindings/rtc/apple,smc-rtc.yaml @@ -20,15 +20,36 @@ properties: const: apple,smc-rtc nvmem-cells: + minItems: 1 items: - description: RTC offset. By default a 48bit value, specified in 32768 (2^15) Hz clock ticks. With apple,rtc-offset-seconds, a 64bit little-endian count of whole seconds. + - description: + PMU alarm control byte, with the alarm enable, the monitor bit and + the latched alarm event. + - description: + PMU alarm compare value, a 32-bit count of whole seconds of the RTC + counter without the offset. + - description: PMU alarm mask byte. nvmem-cell-names: - items: - - const: rtc_offset + oneOf: + - items: + - const: rtc_offset + - items: + - const: rtc_offset + - const: rtc_ctrl + - const: rtc_alarm + - const: rtc_alarm_mask + + interrupts: + maxItems: 1 + description: + The PMU's RTC alarm interrupt. Only used with the three alarm cells. + + wakeup-source: true apple,rtc-offset-seconds: type: boolean From 0d8a55c70a26199b3b69e68d665c8a6d3ad9503e Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Tue, 20 Oct 2026 15:12:40 +0200 Subject: [PATCH 3/7] rtc: macsmc: Add the PMU alarm The PMU that keeps the RTC counter also has a one-shot alarm: a 32-bit compare value in whole seconds of the free-running counter (CLKM >> 15, without the wall-clock offset), a mask, and a control byte with an enable, a monitor bit and a latched event that is cleared by writing it back. It raises an SPMI peripheral interrupt. When the device tree describes the alarm registers as nvmem cells and the interrupt, implement read_alarm, set_alarm and alarm_irq_enable, report the alarm from a threaded handler, and make it a wake source if the node says so. Without them the RTC stays alarm-less as before. Updates of the control byte keep its other bits. Firmware can leave the alarm armed or its event latched, so the probe disarms it before it requests the interrupt. Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 --- drivers/rtc/rtc-macsmc.c | 282 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 280 insertions(+), 2 deletions(-) diff --git a/drivers/rtc/rtc-macsmc.c b/drivers/rtc/rtc-macsmc.c index a28dd7c577bded..aed6940cc61c09 100644 --- a/drivers/rtc/rtc-macsmc.c +++ b/drivers/rtc/rtc-macsmc.c @@ -5,11 +5,14 @@ */ #include +#include #include #include +#include #include #include #include +#include #include #include #include @@ -24,12 +27,26 @@ /* Offset kept as a 64-bit little-endian count of whole seconds */ #define RTC_OFFSET_SECS_BYTES 8 +/* + * PMU alarm control. The alarm compares a 32-bit count of whole seconds of + * the free-running counter, i.e. CLKM >> RTC_SEC_SHIFT, without the offset. + */ +#define RTC_CTRL_MONITOR BIT(0) +#define RTC_CTRL_ALARM_EVENT BIT(1) +#define RTC_CTRL_ALARM_EN BIT(6) + struct macsmc_rtc { struct device *dev; struct apple_smc *smc; struct rtc_device *rtc_dev; struct nvmem_cell *rtc_offset; bool offset_in_seconds; + /* Alarm, if the PMU's alarm registers and interrupt are described */ + struct nvmem_cell *alarm_ctrl; + struct nvmem_cell *alarm_value; + struct nvmem_cell *alarm_mask; + struct mutex alarm_lock; + u32 alarm_secs; }; static int macsmc_rtc_get_time_secs(struct macsmc_rtc *rtc, u64 ctr, @@ -131,15 +148,268 @@ static int macsmc_rtc_set_time(struct device *dev, struct rtc_time *tm) return nvmem_cell_write(rtc->rtc_offset, &off, RTC_BYTES); } + +static int macsmc_rtc_read_u8(struct nvmem_cell *cell, u8 *val) +{ + size_t len; + void *p; + + p = nvmem_cell_read(cell, &len); + if (IS_ERR(p)) + return PTR_ERR(p); + if (len < 1) { + kfree(p); + return -EIO; + } + *val = *(u8 *)p; + kfree(p); + + return 0; +} + +static int macsmc_rtc_write_u8(struct nvmem_cell *cell, u8 val) +{ + int ret = nvmem_cell_write(cell, &val, sizeof(val)); + + return ret < 0 ? ret : 0; +} + +/* The wall-clock offset in whole seconds: wall time = counter seconds + offset */ +static int macsmc_rtc_offset_secs(struct macsmc_rtc *rtc, s64 *secs) +{ + u64 off = 0; + size_t len; + void *p; + + p = nvmem_cell_read(rtc->rtc_offset, &len); + if (IS_ERR(p)) + return PTR_ERR(p); + + if (rtc->offset_in_seconds) { + if (len < RTC_OFFSET_SECS_BYTES) { + kfree(p); + return -EIO; + } + *secs = (s64)get_unaligned_le64(p); + } else { + if (len < RTC_BYTES) { + kfree(p); + return -EIO; + } + memcpy(&off, p, RTC_BYTES); + *secs = sign_extend64(off, RTC_BITS - 1) >> RTC_SEC_SHIFT; + } + kfree(p); + + return 0; +} + +/* + * Arm the alarm at counter second @secs, or disarm it if @secs is 0. The + * other bits of the control byte are kept. A latched event is cleared by + * writing it back, so the event bit is written as 1 only to clear it. + */ +static int macsmc_rtc_program_alarm(struct macsmc_rtc *rtc, u32 secs) +{ + __le32 val = cpu_to_le32(secs); + u8 ctrl; + int ret; + + ret = macsmc_rtc_read_u8(rtc->alarm_ctrl, &ctrl); + if (ret) + return ret; + if (ctrl & RTC_CTRL_ALARM_EVENT) { + ret = macsmc_rtc_write_u8(rtc->alarm_ctrl, ctrl); + if (ret) + return ret; + } + ctrl &= ~RTC_CTRL_ALARM_EVENT; + + if (!secs) { + ret = macsmc_rtc_write_u8(rtc->alarm_mask, 1); + if (ret) + return ret; + return macsmc_rtc_write_u8(rtc->alarm_ctrl, + (ctrl & ~RTC_CTRL_ALARM_EN) | + RTC_CTRL_MONITOR); + } + + ret = nvmem_cell_write(rtc->alarm_value, &val, sizeof(val)); + if (ret < 0) + return ret; + ret = macsmc_rtc_write_u8(rtc->alarm_mask, 0); + if (ret) + return ret; + return macsmc_rtc_write_u8(rtc->alarm_ctrl, ctrl | RTC_CTRL_ALARM_EN | + RTC_CTRL_MONITOR); +} + +static int macsmc_rtc_read_alarm(struct device *dev, struct rtc_wkalrm *alrm) +{ + struct macsmc_rtc *rtc = dev_get_drvdata(dev); + size_t len; + s64 off; + u32 secs; + void *p; + u8 ctrl; + int ret; + + ret = macsmc_rtc_offset_secs(rtc, &off); + if (ret) + return ret; + + mutex_lock(&rtc->alarm_lock); + ret = macsmc_rtc_read_u8(rtc->alarm_ctrl, &ctrl); + if (ret) + goto out; + p = nvmem_cell_read(rtc->alarm_value, &len); + if (IS_ERR(p)) { + ret = PTR_ERR(p); + goto out; + } + secs = len >= sizeof(u32) ? get_unaligned_le32(p) : 0; + kfree(p); + + rtc_time64_to_tm((time64_t)secs + off, &alrm->time); + alrm->enabled = !!(ctrl & RTC_CTRL_ALARM_EN); + alrm->pending = !!(ctrl & RTC_CTRL_ALARM_EVENT); +out: + mutex_unlock(&rtc->alarm_lock); + return ret; +} + +static int macsmc_rtc_set_alarm(struct device *dev, struct rtc_wkalrm *alrm) +{ + struct macsmc_rtc *rtc = dev_get_drvdata(dev); + s64 off, secs; + int ret; + + ret = macsmc_rtc_offset_secs(rtc, &off); + if (ret) + return ret; + + /* + * With a tick offset, off is rounded down to whole seconds but the + * time is floor((ctr + off) / 2^15), so the alarm fires at the set + * second or up to one second after it. + */ + secs = rtc_tm_to_time64(&alrm->time) - off; + if (secs <= 0 || secs > U32_MAX) + return -ERANGE; + + mutex_lock(&rtc->alarm_lock); + rtc->alarm_secs = secs; + ret = macsmc_rtc_program_alarm(rtc, alrm->enabled ? rtc->alarm_secs : 0); + mutex_unlock(&rtc->alarm_lock); + + return ret; +} + +static int macsmc_rtc_alarm_irq_enable(struct device *dev, unsigned int enabled) +{ + struct macsmc_rtc *rtc = dev_get_drvdata(dev); + int ret; + + mutex_lock(&rtc->alarm_lock); + ret = macsmc_rtc_program_alarm(rtc, enabled ? rtc->alarm_secs : 0); + mutex_unlock(&rtc->alarm_lock); + + return ret; +} + +static irqreturn_t macsmc_rtc_alarm_irq(int irq, void *data) +{ + struct macsmc_rtc *rtc = data; + u8 ctrl; + + mutex_lock(&rtc->alarm_lock); + if (macsmc_rtc_read_u8(rtc->alarm_ctrl, &ctrl) || + !(ctrl & RTC_CTRL_ALARM_EVENT)) { + mutex_unlock(&rtc->alarm_lock); + return IRQ_NONE; + } + /* One-shot: clear the event and disarm until the alarm is set again */ + macsmc_rtc_program_alarm(rtc, 0); + mutex_unlock(&rtc->alarm_lock); + + rtc_update_irq(rtc->rtc_dev, 1, RTC_AF | RTC_IRQF); + + return IRQ_HANDLED; +} + static const struct rtc_class_ops macsmc_rtc_ops = { .read_time = macsmc_rtc_get_time, .set_time = macsmc_rtc_set_time, + .read_alarm = macsmc_rtc_read_alarm, + .set_alarm = macsmc_rtc_set_alarm, + .alarm_irq_enable = macsmc_rtc_alarm_irq_enable, }; +/* A missing cell means no alarm, not an error */ +static struct nvmem_cell *macsmc_rtc_cell_optional(struct device *dev, + const char *id) +{ + struct nvmem_cell *cell = devm_nvmem_cell_get(dev, id); + + if (IS_ERR(cell) && PTR_ERR(cell) == -ENOENT) + return NULL; + return cell; +} + +/* The alarm is optional: it needs the PMU's alarm registers and interrupt. */ +static int macsmc_rtc_init_alarm(struct platform_device *pdev, + struct macsmc_rtc *rtc) +{ + struct device *dev = &pdev->dev; + int irq, ret; + + rtc->alarm_ctrl = macsmc_rtc_cell_optional(dev, "rtc_ctrl"); + rtc->alarm_value = macsmc_rtc_cell_optional(dev, "rtc_alarm"); + rtc->alarm_mask = macsmc_rtc_cell_optional(dev, "rtc_alarm_mask"); + if (IS_ERR(rtc->alarm_ctrl)) + return PTR_ERR(rtc->alarm_ctrl); + if (IS_ERR(rtc->alarm_value)) + return PTR_ERR(rtc->alarm_value); + if (IS_ERR(rtc->alarm_mask)) + return PTR_ERR(rtc->alarm_mask); + if (!rtc->alarm_ctrl || !rtc->alarm_value || !rtc->alarm_mask) + return -ENODEV; + + irq = platform_get_irq_optional(pdev, 0); + if (irq == -EPROBE_DEFER) + return irq; + if (irq <= 0) + return -ENODEV; + + /* + * Firmware can leave the alarm armed or its event latched. Neither is + * an alarm Linux set, so disarm it before the interrupt is requested. + */ + ret = macsmc_rtc_program_alarm(rtc, 0); + if (ret) + return ret; + + ret = devm_request_threaded_irq(dev, irq, NULL, macsmc_rtc_alarm_irq, + IRQF_ONESHOT, dev_name(dev), rtc); + if (ret) + return ret; + + if (of_property_read_bool(dev->of_node, "wakeup-source")) { + ret = devm_device_init_wakeup(dev); + if (!ret) + ret = devm_pm_set_wake_irq(dev, irq); + if (ret) + dev_warn(dev, "alarm cannot wake the system: %d\n", ret); + } + + return 0; +} + static int macsmc_rtc_probe(struct platform_device *pdev) { struct apple_smc *smc = dev_get_drvdata(pdev->dev.parent); struct macsmc_rtc *rtc; + int ret; /* * MFD will probe this device even without a node in the device tree, @@ -155,6 +425,8 @@ static int macsmc_rtc_probe(struct platform_device *pdev) rtc->dev = &pdev->dev; rtc->smc = smc; + mutex_init(&rtc->alarm_lock); + platform_set_drvdata(pdev, rtc); rtc->rtc_offset = devm_nvmem_cell_get(&pdev->dev, "rtc_offset"); if (IS_ERR(rtc->rtc_offset)) @@ -169,6 +441,14 @@ static int macsmc_rtc_probe(struct platform_device *pdev) "apple,rtc-offset-seconds"); rtc->rtc_dev->ops = &macsmc_rtc_ops; + ret = macsmc_rtc_init_alarm(pdev, rtc); + if (ret == -EPROBE_DEFER) + return ret; + if (ret) { + if (ret != -ENODEV) + dev_warn(&pdev->dev, "no alarm: %d\n", ret); + clear_bit(RTC_FEATURE_ALARM, rtc->rtc_dev->features); + } if (rtc->offset_in_seconds) { rtc->rtc_dev->range_min = 0; rtc->rtc_dev->range_max = RTC_TIMESTAMP_END_9999; @@ -177,8 +457,6 @@ static int macsmc_rtc_probe(struct platform_device *pdev) rtc->rtc_dev->range_max = S64_MAX >> (RTC_SEC_SHIFT + (64 - RTC_BITS)); } - platform_set_drvdata(pdev, rtc); - return devm_rtc_register_device(rtc->rtc_dev); } From 1d55fad4d3550c1ae7c69e095a0a0a112c83eedd Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Tue, 20 Oct 2026 15:12:40 +0200 Subject: [PATCH 4/7] arm64: dts: apple: t6030: Describe the RTC alarm The Chapel PMIC's RTC alarm uses registers 0x2000 (control and event), 0x2008 (compare value) and 0x200e (interrupt mask) and raises peripheral interrupt 2 of nub-spmi0. Make the controller an interrupt controller, add the alarm cells to the PMIC layout and give them, the interrupt and wakeup-source to the SMC RTC, so a timer can wake the system. Signed-off-by: Eryk Wieliczko --- arch/arm64/boot/dts/apple/t6030.dtsi | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t6030.dtsi b/arch/arm64/boot/dts/apple/t6030.dtsi index 52ad06a0668893..56e3da67947061 100644 --- a/arch/arm64/boot/dts/apple/t6030.dtsi +++ b/arch/arm64/boot/dts/apple/t6030.dtsi @@ -1060,6 +1060,8 @@ #size-cells = <0>; interrupt-parent = <&aic>; interrupts = ; + interrupt-controller; + #interrupt-cells = <2>; pmic1: pmic@b { compatible = "apple,chapel-pmic", "apple,spmi-nvmem"; @@ -1070,10 +1072,22 @@ #address-cells = <1>; #size-cells = <1>; + rtc_ctrl: rtc-ctrl@2000 { + reg = <0x2000 0x1>; + }; + pm_setting: pm-setting@2001 { reg = <0x2001 0x1>; }; + rtc_alarm: rtc-alarm@2008 { + reg = <0x2008 0x4>; + }; + + rtc_alarm_mask: rtc-alarm-mask@200e { + reg = <0x200e 0x1>; + }; + rtc_offset: rtc-offset@2100 { reg = <0x2100 0x6>; }; @@ -1182,8 +1196,13 @@ rtc { compatible = "apple,smc-rtc"; - nvmem-cells = <&rtc_offset>; - nvmem-cell-names = "rtc_offset"; + nvmem-cells = <&rtc_offset>, <&rtc_ctrl>, <&rtc_alarm>, + <&rtc_alarm_mask>; + nvmem-cell-names = "rtc_offset", "rtc_ctrl", "rtc_alarm", + "rtc_alarm_mask"; + /* The PMIC's RTC alarm, peripheral interrupt 2 of nub-spmi0 */ + interrupts-extended = <&nub_spmi 2 IRQ_TYPE_EDGE_RISING>; + wakeup-source; }; }; From b28cb3a4bb171450a6bfbb84db21bfa0335dac7b Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Wed, 28 Oct 2026 19:12:34 +0100 Subject: [PATCH 5/7] pmdomain: apple: pmp-report: withdraw external display requests in sleep An acknowledged DISPEXT request keeps the PMP's external display state up while no display is shown. The requests are asserted once at probe (apple,always-on) and never withdrawn, so they also hold through suspend-to-idle. Give report entries marked apple,release-for-sleep noirq sleep callbacks: by then every display processor has suspended, with its displays off and its links released or idle. Withdraw such a request if it is acknowledged, with one replacement write of the request entry as macOS releases its display requests, and assert it again in resume_noirq, before the processors resume. Without the request the external display stays dark until reboot, so a re-assert the PMP does not acknowledge in time is written again, three times at most. The T6030 display gate marks the DISPEXT requests of the processors it enables. The PMP graph host test runs the new callbacks against its fake PMP: an acknowledged request is withdrawn and asserted again, an unacknowledged or unmarked one is left alone, and a re-assert acknowledged late is written again. Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 --- drivers/pmdomain/apple/pmp-report.c | 79 ++++++++++++++++++++++++++ drivers/soc/apple/t6030-display-gate.c | 3 + tools/asahi/t6030-pmp-graph-tests.c | 56 +++++++++++++++++- tools/asahi/t6030-pmp-graph-tests.py | 16 +++++- 4 files changed, 152 insertions(+), 2 deletions(-) diff --git a/drivers/pmdomain/apple/pmp-report.c b/drivers/pmdomain/apple/pmp-report.c index 98d96d08f8a434..9aac3ced17cda1 100644 --- a/drivers/pmdomain/apple/pmp-report.c +++ b/drivers/pmdomain/apple/pmp-report.c @@ -1261,6 +1261,9 @@ struct apple_pmp_report_entry { u32 id; /* The PMP does not acknowledge this request (a started PMP only). */ bool no_ack; + /* Withdrawn for system sleep (apple,release-for-sleep), and now so. */ + bool sleep_release; + bool released; bool removed; }; @@ -1368,6 +1371,80 @@ static int apple_pmp_report_entry_set_state(struct generic_pm_domain *genpd, boo return ret; } +/* + * System sleep. An external display processor's request keeps the PMP's + * external display state up while nothing is shown, about 0.25 W in + * suspend-to-idle on J514S. By the noirq phase every display processor has + * suspended, its displays off and its links released or idle: withdraw such a + * request if it is acknowledged, with one replacement write as macOS releases + * its display requests, and assert it again before the processors resume. + */ +static int apple_pmp_report_entry_sleep(struct apple_pmp_report_entry *ent, bool release) +{ + struct apple_pmp_report *rep; + int ret = -ENODEV; + + rcu_read_lock(); + list_for_each_entry_rcu(rep, &apple_pmp_reports, list) { + if (rep->dev->of_node != ent->report_node) + continue; + if (release && (!(readq(rep->base + rep->offsets->status) & PMP_REPORT_READY) || + !(readq(rep->base + rep->offsets->actual) & BIT_ULL(ent->id)))) + ret = -EALREADY; + else + ret = apple_pmp_report_set_state(rep, ent->id, !release, ent->no_ack); + break; + } + rcu_read_unlock(); + return ret; +} + +static int apple_pmp_report_entry_suspend_noirq(struct device *dev) +{ + struct apple_pmp_report_entry *ent = dev_get_drvdata(dev); + int ret; + + if (!ent || !ent->sleep_release || READ_ONCE(ent->removed)) + return 0; + ret = apple_pmp_report_entry_sleep(ent, true); + if (ret == -EALREADY) + return 0; + /* A withdrawal that was not acknowledged is asserted again all the same. */ + if (ret) + dev_warn(dev, "request %u not withdrawn for sleep: %d\n", ent->id, ret); + ent->released = true; + return 0; +} + +static int apple_pmp_report_entry_resume_noirq(struct device *dev) +{ + struct apple_pmp_report_entry *ent = dev_get_drvdata(dev); + int ret, tries = 0; + + if (!ent || !ent->released) + return 0; + ent->released = false; + /* + * Without the request the external display stays dark until reboot: + * write it again, three times at most, while the PMP does not + * acknowledge it in time. + */ + do { + ret = apple_pmp_report_entry_sleep(ent, false); + } while (ret == -ETIMEDOUT && ++tries < 3); + if (ret) + dev_err(dev, "request %u not asserted after sleep: %d\n", ent->id, ret); + else if (tries) + dev_warn(dev, "request %u asserted after sleep on write %d\n", ent->id, + tries + 1); + return ret; +} + +static const struct dev_pm_ops apple_pmp_report_entry_pm_ops = { + NOIRQ_SYSTEM_SLEEP_PM_OPS(apple_pmp_report_entry_suspend_noirq, + apple_pmp_report_entry_resume_noirq) +}; + static int apple_pmp_report_entry_power_on(struct generic_pm_domain *genpd) { return apple_pmp_report_entry_set_state(genpd, true); @@ -1440,6 +1517,7 @@ static int apple_pmp_report_entry_probe(struct platform_device *pdev) } ent->no_ack = of_property_read_bool(node, "apple,no-ack"); } + ent->sleep_release = of_property_read_bool(node, "apple,release-for-sleep"); ret = of_property_read_string(node, "label", &name); if (ret < 0) { @@ -1521,6 +1599,7 @@ static struct platform_driver apple_pmp_report_entry_driver = { .driver = { .name = "apple-pmp-report-entry", .of_match_table = apple_pmp_report_entry_of_match, + .pm = pm_sleep_ptr(&apple_pmp_report_entry_pm_ops), /* Power-domain providers are not intended for manual unbinding. */ .suppress_bind_attrs = true, }, diff --git a/drivers/soc/apple/t6030-display-gate.c b/drivers/soc/apple/t6030-display-gate.c index c153a4d7cae0fd..7ee02087c969b8 100644 --- a/drivers/soc/apple/t6030-display-gate.c +++ b/drivers/soc/apple/t6030-display-gate.c @@ -1461,6 +1461,9 @@ static int __init gate_ext_apply(struct gate_ext *ext, bool native) /* The driver must wait for this request's acknowledgement before RUN. */ if (!ret) ret = gate_set_u32(cs, ext->dcp, "apple,pmp-report", ext->entry->phandle); + /* No external display is attached in system sleep: the PMP may drop it. */ + if (!ret) + ret = gate_add_bool(cs, ext->entry, "apple,release-for-sleep"); if (!ret) ret = of_changeset_update_prop_string(cs, ext->entry, "status", "okay"); if (!ret) diff --git a/tools/asahi/t6030-pmp-graph-tests.c b/tools/asahi/t6030-pmp-graph-tests.c index e8a6bac35a9a50..d7d079a9673a64 100644 --- a/tools/asahi/t6030-pmp-graph-tests.c +++ b/tools/asahi/t6030-pmp-graph-tests.c @@ -45,7 +45,7 @@ static void host_log(const char *format, ...) #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) struct device_node { const char *name, *compatible; - bool available, always_on, no_ack, deferred, uuid, soc_record; + bool available, always_on, no_ack, deferred, uuid, soc_record, release_sleep; u32 reg; int refs; struct device_node *parent, *child, *sibling, *pmp; @@ -142,6 +142,7 @@ struct apple_pmp_report_entry { struct device_node *node, *report_node; u32 id; bool no_ack, removed; + bool sleep_release, released; }; #define genpd_to_apple_pmp_report_entry(p) \ container_of(p, struct apple_pmp_report_entry, genpd) @@ -297,11 +298,18 @@ static u64 readq(const void *a) { return *(const u64 *)a; } +/* The sleep cases: the PMP acknowledges the requests it is given, ack_late writes late. */ +static bool ack_follows; +static unsigned int ack_late; static void writeq(u64 v, void *a) { assert(a == (char *)regs + 0x108c0); *(u64 *)a = v; regs[0x1180 / 8] = v; + if (ack_follows && ack_late) + ack_late--; + else if (ack_follows) + regs[0x11c0 / 8] = v; } #define readq_poll_timeout(address, value, condition, delay, timeout) \ ({ \ @@ -364,6 +372,8 @@ static bool of_property_read_bool(const struct device_node *n, const char *p) return n->deferred; if (!strcmp(p, "apple,soc-device-record")) return n->soc_record; + if (!strcmp(p, "apple,release-for-sleep")) + return n->release_sleep; return false; } static bool of_property_present(const struct device_node *n, const char *p) @@ -666,6 +676,8 @@ static struct apple_pmp_report *setup(void) put32(devices + i * 0x7c + 8, 2); } ack_polls = 0; + ack_follows = false; + ack_late = 0; check_error = enable_error = 0; hw_binds = hw_ready = true; hw_ack = BIT_ULL(7); @@ -798,6 +810,48 @@ int main(void) assert(g15_vote() == -EINVAL && commands == 0 && !enables); cases++; cleanup(); +#ifdef HAVE_SLEEP_RELEASE + /* System sleep withdraws an acknowledged request marked for it, then asserts it again. */ + setup(); + entries[1].release_sleep = true; + assert(apple_pmp_report_probe(&report_dev) == 0); + assert(apple_pmp_report_wait_ready(&entries[0], 30000) == 0); + struct apple_pmp_report_entry *disp = entry_devs[0].dev.data; + struct apple_pmp_report_entry *ext = entry_devs[1].dev.data; + + assert(ext->sleep_release && !disp->sleep_release); + ack_follows = true; + regs[0x11c0 / 8] = regs[0x1180 / 8]; + u64 target = regs[0x1180 / 8]; + + assert(target & BIT_ULL(8)); + assert(apple_pmp_report_entry_suspend_noirq(&entry_devs[0].dev) == 0 && + !disp->released && regs[0x1180 / 8] == target); + assert(apple_pmp_report_entry_suspend_noirq(&entry_devs[1].dev) == 0); + assert(ext->released && regs[0x1180 / 8] == (target & ~BIT_ULL(8))); + assert(apple_pmp_report_entry_resume_noirq(&entry_devs[1].dev) == 0); + assert(!ext->released && regs[0x1180 / 8] == target); + cases++; + /* A request the PMP has not acknowledged is left as it is. */ + regs[0x11c0 / 8] &= ~BIT_ULL(8); + assert(apple_pmp_report_entry_suspend_noirq(&entry_devs[1].dev) == 0); + assert(!ext->released && regs[0x1180 / 8] == target); + assert(apple_pmp_report_entry_resume_noirq(&entry_devs[1].dev) == 0 && + regs[0x1180 / 8] == target); + cases++; + /* A re-assert the PMP acknowledges late is written again, three times at most. */ + regs[0x11c0 / 8] = target; + assert(apple_pmp_report_entry_suspend_noirq(&entry_devs[1].dev) == 0 && ext->released); + ack_late = 2; + assert(apple_pmp_report_entry_resume_noirq(&entry_devs[1].dev) == 0 && + !ack_late && regs[0x11c0 / 8] == target); + assert(apple_pmp_report_entry_suspend_noirq(&entry_devs[1].dev) == 0 && ext->released); + ack_late = 5; + assert(apple_pmp_report_entry_resume_noirq(&entry_devs[1].dev) == -ETIMEDOUT && + ack_late == 2 && regs[0x1180 / 8] == target); + cases++; + cleanup(); +#endif printf("PASS: %u actual overlay/probe/entry/DCP7/managed-start/supplier graph cases\n", cases); return 0; diff --git a/tools/asahi/t6030-pmp-graph-tests.py b/tools/asahi/t6030-pmp-graph-tests.py index 50cea55ee727ac..8757ee04cde1d6 100644 --- a/tools/asahi/t6030-pmp-graph-tests.py +++ b/tools/asahi/t6030-pmp-graph-tests.py @@ -45,6 +45,13 @@ def main(): 'static int apple_pmp_report_wait_t6030_14(', 'int apple_pmp_report_wait_supplier_ready('] if 'static int apple_pmp_report_wait_t6030_bound(' in source: names.insert(names.index('static struct device_node *apple_pmp_report_t6030_entry('), 'static int apple_pmp_report_wait_t6030_bound(') + # System sleep (apple,release-for-sleep), where the source has it. + sleep = 'static int apple_pmp_report_entry_sleep(' in source + if sleep: + at = names.index('static int apple_pmp_report_entry_power_on(') + names[at:at] = ['static int apple_pmp_report_entry_sleep(', + 'static int apple_pmp_report_entry_suspend_noirq(', + 'static int apple_pmp_report_entry_resume_noirq('] offsets = re.search(r'static const struct apple_pmp_report_offsets apple_pmp_offsets_t6030 = \{.*?\n\};', source, re.S)[0] functions = '\n'.join(extract.function(source, n) for n in names) # The T8122 14.x wait (CONFIG_APPLE_T8122_GPU_PMP_LINK=0 here) precedes the dispatch; it is @@ -54,7 +61,7 @@ def main(): def with_t8122(text): return text.replace(dispatch, t8122 + dispatch, 1) validation = re.sub(r'^#include .*\n', '', (ROOT / 'drivers/pmdomain/apple/pmp-report-validation.h').read_text(), flags=re.M) - code = offsets + '\n' + validation + '\n' + functions + code = ('#define HAVE_SLEEP_RELEASE 1\n' if sleep else '') + offsets + '\n' + validation + '\n' + functions # The actual overlay must still describe a halted supplier and disabled DMA/mailbox. assert overlay.count('status = \"disabled\";') >= 3 (args.output / 'production.h').write_text(with_t8122(code)) @@ -87,6 +94,13 @@ def with_t8122(text): ('storage-ack-layout', 'st->ack & ~st->record_seed,', 'st->seed & ~st->record_seed,'), ('skip-supplier-wait', 'apple_pmp_report_wait_ready(entry, msecs_to_jiffies(timeout_ms))', '((void)timeout_ms, 0)'), ] + if sleep: + controls += [ + ('sleep-releases-unacked', '!(readq(rep->base + rep->offsets->actual) & BIT_ULL(ent->id))', 'false'), + ('sleep-not-reasserted', 'ret = apple_pmp_report_entry_sleep(ent, false);', 'ret = 0;'), + ('sleep-ignores-mark', '!ent->sleep_release ||', ''), + ('sleep-not-rewritten', '++tries < 3', '++tries < 1'), + ] for name, old, new in controls: assert code.count(old) == 1, (name, code.count(old)) (args.output / 'production.h').write_text(with_t8122(code.replace(old, new))) From 4e87561ce3ea7253c5a6ee23d079b53a4e92a1b6 Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Wed, 28 Oct 2026 19:12:34 +0100 Subject: [PATCH 6/7] drm/apple: stop the J514S panel's display controller on DPMS off DPMS off only blanked the 14.7 internal panel, leaving its display controller scanning out. Once the blank has been presented, send A472 with the hardware-power selector (+8) and a null output word (+11), which stops scanout and the panel; power on sends the same with state 1 before the backlight is restored. The IOMFB client, the RTKit session and every mapping are kept. On J514S this takes screen-off power from 2.86 W to 1.83 W. System sleep still only blanks the panel: suspend-to-idle cuts that power itself (1.354 W asleep with the controller stopped, 1.313 W without), so an A472 off and on for every suspend would buy nothing. A refused power on is tried once more. If the controller stays off, the backlight stays off too rather than light a dark panel, and the next power on tries again. Answer the callbacks of those transitions on the panel: will_power_off (D2) and the intent swap (D591) as notifications, powerUpDART (D574) and set_kernel_power_assert (D700) with status 0, as the host keeps the display powered, and enable_backlight_message_ap_gated (D593), which the backlight restore after power on covers. Only the J514S/J516S panel sends A472 so; other sessions handle these callbacks as before. appledrm.panel_hw_off=0 restores the blank-only behaviour. Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 --- drivers/gpu/drm/apple/iomfb_v14_7.c | 96 ++++++++++++++- .../iomfb_v14_7_backlight_lifetime_test.c | 112 ++++++++++++++++++ 2 files changed, 205 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index d55f96b2ba123f..abeb7847e91aad 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -304,8 +305,14 @@ struct apple_dcp_v14 { enum dcp_v14_bl_gate bl_gate; u32 nits_sent; u64 nits_presents, brightness_presents, nits_refused; + /* Panel: its display controller is stopped (A472 hardware power off). */ + bool panel_off; }; +static bool panel_hw_off = true; +module_param(panel_hw_off, bool, 0644); +MODULE_PARM_DESC(panel_hw_off, "Internal 14.7 panel: stop its display controller on DPMS off, not only blank it [default: true]"); + /* * The panel's DCP session; one per boot, RTKit is never started twice. The * manual external start waits for it, as the PMP runs once the panel's @@ -1150,6 +1157,24 @@ static int dcp_v14_callback(void *cookie, u32 tag, const void *input, u32 in_siz /* Swap information for a completed swap; the link checks its id. */ if (tag == D(589) && SHAPE(0x6f0, 0)) return 0; + /* + * The panel's display-controller power transitions (A472 with the + * hardware selector): will power off and an intent swap, both + * notifications; display DART power and a kernel power assertion, both + * held by the host throughout, so status 0; and the backlight message + * at power on, which the backlight restore after A472(1) answers. Only + * the J514S/J516S panel sends A472 so; elsewhere these stay unhandled. + */ + if (dcp_v14_board_nits(v14) && + ((tag == D(2) && SHAPE(0, 0)) || (tag == D(591) && SHAPE(20, 0)) || + (tag == D(593) && SHAPE(4, 0)))) + return 0; + if (dcp_v14_board_nits(v14) && (tag == D(574) || tag == D(700)) && SHAPE(4, 4)) { + if (tag == D(574) && in[0] > 1) + return -EINVAL; + put_unaligned_le32(0, out); + return 0; + } if ((tag == D(588) && SHAPE(8, 0)) || (tag == D(598) && SHAPE(0, 0))) return 0; /* Main-display query, answered by asking the firmware. */ @@ -3108,6 +3133,32 @@ static int dcp_v14_set_power(struct apple_dcp_v14 *v14, bool on) return ret; } +/* + * A472 with the hardware selector (+8) and a null output word (+11): the + * panel's display controller off or on. Off stops scanout and the panel once + * a blank has been presented, on restarts them; the soft form (+8 clear) + * leaves the controller running. The client, the RTKit session and every + * mapping are kept. + */ +static int dcp_v14_panel_power(struct apple_dcp_v14 *v14, bool on) +{ + u8 in[12] = {}, out[8] = {}; + int ret; + + put_unaligned_le64(on, in); + in[8] = 1; + in[11] = 1; + ret = dcp_v14_call(v14, A(472), in, sizeof(in), out, sizeof(out), 0); + if (!ret && get_unaligned_le32(out + 4)) { + dev_err(v14->dev, "display controller %s refused: %#x\n", on ? "on" : "off", + get_unaligned_le32(out + 4)); + ret = -EIO; + } + if (!ret) + v14->panel_off = !on; + return ret; +} + /* The display is fully described, and no description is in transfer. */ static bool dcp_v14_external_described(struct apple_dcp_v14 *v14) { @@ -3344,8 +3395,35 @@ void iomfb_v14_7_flush(struct apple_dcp *dcp, struct drm_crtc *crtc, dcp_v14_cancel_event(dcp); } +/* A system sleep is in progress, as opposed to a DPMS change. */ +static bool dcp_v14_system_sleep(void) +{ + KUNIT_STATIC_STUB_REDIRECT(dcp_v14_system_sleep); + return pm_suspend_in_progress(); +} + void iomfb_v14_7_poweron(struct apple_dcp *dcp) { + struct apple_dcp_v14 *v14 = dcp->v14; + bool dark; + + /* + * The display controller first, then the backlight, as at boot. A + * refused power on is tried once more. If the controller stays off, + * so does the backlight, rather than light a dark panel; the next + * power on tries again. + */ + if (!dcp->external && v14 && v14->panel_off) { + mutex_lock(&v14->lock); + if (v14->panel_off && dcp_v14_panel_power(v14, true)) + dcp_v14_panel_power(v14, true); + dark = v14->panel_off; + mutex_unlock(&v14->lock); + if (dark) { + dev_err(dcp->dev, "display controller did not power on: backlight left off\n"); + return; + } + } /* The next present restores a level only after Linux has set one. */ if (dcp->v14 && dcp_v14_bl_dpms_applies(dcp->v14, dcp)) dcp_backlight_dpms(dcp, true); @@ -3369,9 +3447,21 @@ void iomfb_v14_7_poweroff(struct apple_dcp *dcp) dcp_mode_invalidate(&dcp->mode_state); return; } - /* Blank to black; the panel and the DCP stay powered. */ - if (v14 && v14->started) - dcp_v14_blank(v14, dcp); + /* Blank to black, then stop the display controller; the DCP stays powered. */ + if (!v14 || !v14->started) + return; + dcp_v14_blank(v14, dcp); + /* + * Only the J514S/J516S panel, the one this was measured on, and only + * on DPMS off: suspend-to-idle cuts that power itself. + */ + if (!panel_hw_off || !dcp_v14_board_nits(v14) || dcp_v14_system_sleep()) + return; + mutex_lock(&v14->lock); + if (!v14->panel_off && !READ_ONCE(v14->failed) && !READ_ONCE(dcp->crashed) && + !v14->active_fb && dcp_v14_panel_power(v14, false)) + dev_err(dcp->dev, "display controller did not power off\n"); + mutex_unlock(&v14->lock); } #if IS_ENABLED(CONFIG_DRM_APPLE_DISPLAY_KUNIT_TEST) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c index 29acb9d5fe9e99..52eac9b835979b 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c @@ -31,6 +31,9 @@ struct v14_bl_rpc { u64 iova[5], nits[5]; bool nits_present[5]; struct drm_framebuffer *owner[5]; + /* A472 hardware power states, in order. */ + u64 power[4]; + unsigned int power_calls, power_refusals; bool block_first, refuse; }; @@ -49,6 +52,23 @@ static int v14_bl_rpc_call(struct apple_dcp_v14 *v14, u32 tag, const void *in, u put_unaligned_le32(1, out); return 0; } + /* The panel's display controller: the hardware selector, a null output. */ + if (tag == A(472)) { + KUNIT_EXPECT_EQ(c->test, in_size, 12U); + KUNIT_EXPECT_EQ(c->test, out_size, 8U); + KUNIT_EXPECT_EQ(c->test, ((const u8 *)in)[8], 1); + KUNIT_EXPECT_EQ(c->test, ((const u8 *)in)[11], 1); + /* Off only once the blank has retired the frame. */ + KUNIT_EXPECT_PTR_EQ(c->test, v14->active_fb, NULL); + if (c->power_calls < ARRAY_SIZE(c->power)) + c->power[c->power_calls] = get_unaligned_le64(in); + c->power_calls++; + if (c->power_refusals) { + c->power_refusals--; + put_unaligned_le32(1, (u8 *)out + 4); + } + return 0; + } KUNIT_EXPECT_EQ(c->test, tag, A(407)); KUNIT_EXPECT_EQ(c->test, in_size, (u32)DCP_V14_SWAP_SIZE); n = atomic_inc_return(&c->submits) - 1; @@ -285,8 +305,15 @@ static void v14_bl_dpms_dispatch(struct kunit *test) KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 2); KUNIT_EXPECT_FALSE(test, c->nits_present[1]); KUNIT_EXPECT_PTR_EQ(test, c->f.v14->active_fb, NULL); + /* The blank, then the display controller off. */ + KUNIT_EXPECT_EQ(test, c->power_calls, 1U); + KUNIT_EXPECT_EQ(test, c->power[0], 0ULL); + KUNIT_EXPECT_TRUE(test, c->f.v14->panel_off); /* Even the advertised default must be an explicit first request. */ dcp_poweron(pdev); + KUNIT_EXPECT_EQ(test, c->power_calls, 2U); + KUNIT_EXPECT_EQ(test, c->power[1], 1ULL); + KUNIT_EXPECT_FALSE(test, c->f.v14->panel_off); v14_bl_request(test, c->f.dcp, DCP_V14_NITS_DEFAULT); KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); KUNIT_EXPECT_TRUE(test, c->nits_present[2]); @@ -296,7 +323,10 @@ static void v14_bl_dpms_dispatch(struct kunit *test) KUNIT_EXPECT_TRUE(test, c->nits_present[3]); KUNIT_EXPECT_EQ(test, c->nits[3], 0ULL); KUNIT_EXPECT_PTR_EQ(test, c->f.v14->active_fb, NULL); + KUNIT_EXPECT_EQ(test, c->power_calls, 3U); dcp_poweron(pdev); + KUNIT_EXPECT_EQ(test, c->power_calls, 4U); + KUNIT_EXPECT_EQ(test, c->power[3], 1ULL); KUNIT_EXPECT_TRUE(test, dcp_backlight_pending(c->f.dcp)); KUNIT_EXPECT_FALSE(test, c->f.dcp->backlight.state.dpms_off); KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); @@ -306,6 +336,85 @@ static void v14_bl_dpms_dispatch(struct kunit *test) KUNIT_EXPECT_FALSE(test, dcp_backlight_pending(c->f.dcp)); } +/* appledrm.panel_hw_off=0: DPMS off only blanks the panel. */ +static void v14_bl_dpms_blank_only(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + struct platform_device *pdev = kunit_kzalloc(test, sizeof(*pdev), GFP_KERNEL); + bool saved = panel_hw_off; + + KUNIT_ASSERT_NOT_NULL(test, pdev); + platform_set_drvdata(pdev, c->f.dcp); + KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); + panel_hw_off = false; + dcp_poweroff(pdev); + dcp_poweron(pdev); + panel_hw_off = saved; + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 2); + KUNIT_EXPECT_PTR_EQ(test, c->f.v14->active_fb, NULL); + KUNIT_EXPECT_EQ(test, c->power_calls, 0U); + KUNIT_EXPECT_FALSE(test, c->f.v14->panel_off); +} + +static bool v14_bl_in_system_sleep(void) +{ + return true; +} + +/* System sleep only blanks the panel; s2idle cuts the controller's power. */ +static void v14_bl_sleep_blank_only(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + struct platform_device *pdev = kunit_kzalloc(test, sizeof(*pdev), GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, pdev); + platform_set_drvdata(pdev, c->f.dcp); + KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); + kunit_activate_static_stub(test, dcp_v14_system_sleep, v14_bl_in_system_sleep); + dcp_poweroff(pdev); + dcp_poweron(pdev); + kunit_deactivate_static_stub(test, dcp_v14_system_sleep); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 2); + KUNIT_EXPECT_PTR_EQ(test, c->f.v14->active_fb, NULL); + KUNIT_EXPECT_EQ(test, c->power_calls, 0U); + KUNIT_EXPECT_FALSE(test, c->f.v14->panel_off); +} + +/* + * A refused power on is tried once more; a controller that stays off keeps + * the backlight off, and the next power on tries again. + */ +static void v14_bl_power_on_refused(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + struct platform_device *pdev = kunit_kzalloc(test, sizeof(*pdev), GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, pdev); + platform_set_drvdata(pdev, c->f.dcp); + v14_bl_request(test, c->f.dcp, DCP_V14_NITS_DEFAULT); + KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); + dcp_poweroff(pdev); + KUNIT_ASSERT_TRUE(test, c->f.v14->panel_off); + /* Refused once: the second try powers it on. */ + c->power_refusals = 1; + dcp_poweron(pdev); + KUNIT_EXPECT_EQ(test, c->power_calls, 3U); + KUNIT_EXPECT_FALSE(test, c->f.v14->panel_off); + KUNIT_EXPECT_FALSE(test, c->f.dcp->backlight.state.dpms_off); + dcp_poweroff(pdev); + /* Refused twice: dark, with the backlight still off. */ + c->power_refusals = 2; + dcp_poweron(pdev); + KUNIT_EXPECT_EQ(test, c->power_calls, 6U); + KUNIT_EXPECT_TRUE(test, c->f.v14->panel_off); + KUNIT_EXPECT_TRUE(test, c->f.dcp->backlight.state.dpms_off); + /* The next power on tries again and restores the backlight. */ + dcp_poweron(pdev); + KUNIT_EXPECT_EQ(test, c->power_calls, 7U); + KUNIT_EXPECT_FALSE(test, c->f.v14->panel_off); + KUNIT_EXPECT_FALSE(test, c->f.dcp->backlight.state.dpms_off); +} + /* An actual failed submit must leave the loader's level unknown. */ static void v14_bl_failed_submit_unknown(struct kunit *test) { @@ -406,6 +515,9 @@ static struct kunit_case v14_bl_lifetime_cases[] = { KUNIT_CASE(v14_bl_present_publication_serialized), KUNIT_CASE(v14_bl_running_work_suspend), KUNIT_CASE(v14_bl_dpms_dispatch), + KUNIT_CASE(v14_bl_dpms_blank_only), + KUNIT_CASE(v14_bl_sleep_blank_only), + KUNIT_CASE(v14_bl_power_on_refused), KUNIT_CASE(v14_bl_failed_submit_unknown), KUNIT_CASE(v14_bl_registration_unbind_race), KUNIT_CASE(v14_bl_class_unregistered), From 71019b83a624333847a083ed0855f6d496def67d Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Thu, 29 Oct 2026 01:11:14 +0100 Subject: [PATCH 7/7] drm/apple: let the system sleep with a dark external display attached A native external display processor (M3 Pro HDMI and USB-C) refused system sleep while a display was attached at all, lit or not, and while its link was up, on the grounds that resume would not bring the display back. Unplugging the display was the only way to suspend. Resume does bring it back. User space turns the displays off before sleep. Suspend then releases the HDMI link (as an unplug would) and keeps a Type-C tunnel. Resume samples HPD again on HDMI and powers the Type-C CRTC back up as after DPMS off. On a J514S, with a 4K120 TV attached over HDMI (4 cycles) and over USB-C (3 cycles), every resume brought back the picture, the mode and the DP audio sink. Refuse sleep only while the firmware is starting or a display is lit. A lit display at prepare time means user space did not turn it off. Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 --- drivers/gpu/drm/apple/dcp.c | 22 ++++++++-------------- drivers/gpu/drm/apple/iomfb_v14_7.c | 10 ++++------ drivers/soc/apple/t6030-display-gate.c | 2 +- 3 files changed, 13 insertions(+), 21 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 14d3bdf6403357..cefe696b8a7522 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -4823,11 +4823,12 @@ static void dcp_release_after_sleep(struct apple_dcp *dcp) /* * Why an external processor must keep the system awake, or NULL. The manual * path refuses sleep once its firmware was ever started. A native processor - * refuses while its firmware starts or holds a display link, and while a - * display is attached to it at all, lit or not: resume does not bring an - * attached display back. Idle with nothing attached, its firmware stays - * powered across s2idle like the panel's (the gate holds its CPU domain at - * the active floor) and the next attach reconnects through it. + * refuses while its firmware starts or a display is lit (user space turns + * displays off before sleep). A dark attached display sleeps with it: + * suspend releases an HDMI link and keeps a Type-C tunnel, and resume finds + * the display again as after DPMS off. Its firmware stays powered across + * s2idle like the panel's (the gate holds its CPU domain at the active + * floor), and the next attach reconnects through it. * * A processor that failed (its start, or its session later) never refuses * sleep: it drives no display until reboot, attached or not, and its power @@ -4845,15 +4846,8 @@ static const char *dcp_external_sleep_vetoed(struct apple_dcp *dcp) why = iomfb_v14_7_external_busy(dcp); if (why) return why; - if (iomfb_v14_7_external_failed(dcp)) { - if (dcp->v14) - dev_info(dcp->dev, "external display processor failed earlier: sleeping with it as it is\n"); - return NULL; - } - if (READ_ONCE(dcp->active_typec_route) || READ_ONCE(dcp->typec_cable_connected)) - return "a USB-C display is attached"; - if (dcp->hdmi_hpd && gpiod_get_value_cansleep(dcp->hdmi_hpd) > 0) - return "an HDMI display is attached"; + if (iomfb_v14_7_external_failed(dcp) && dcp->v14) + dev_info(dcp->dev, "external display processor failed earlier: sleeping with it as it is\n"); return NULL; } diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index abeb7847e91aad..7b095aabe5ed1e 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -2107,8 +2107,8 @@ bool iomfb_v14_7_external_ready(struct apple_dcp *dcp) /* * What this processor's firmware holds that system sleep would not hand - * back, or NULL: a start in progress, or a display link or a powered - * display. The caller also refuses sleep while a display is attached. + * back, or NULL: a start in progress, or a powered display. Suspend + * releases a dark display's link itself. */ const char *iomfb_v14_7_external_busy(struct apple_dcp *dcp) { @@ -2119,10 +2119,8 @@ const char *iomfb_v14_7_external_busy(struct apple_dcp *dcp) /* A stopped session drives nothing, whatever it held last. */ if (dcp->v14 && READ_ONCE(dcp->v14->failed)) return NULL; - if (READ_ONCE(dcp->dptxport[0].connected) || - READ_ONCE(dcp->external_link_ready) || - (dcp->v14 && READ_ONCE(dcp->v14->powered))) - return "a display link is up"; + if (dcp->v14 && READ_ONCE(dcp->v14->powered)) + return "a display is lit"; return NULL; default: return NULL; diff --git a/drivers/soc/apple/t6030-display-gate.c b/drivers/soc/apple/t6030-display-gate.c index 7ee02087c969b8..edd69d642ac0d5 100644 --- a/drivers/soc/apple/t6030-display-gate.c +++ b/drivers/soc/apple/t6030-display-gate.c @@ -1461,7 +1461,7 @@ static int __init gate_ext_apply(struct gate_ext *ext, bool native) /* The driver must wait for this request's acknowledgement before RUN. */ if (!ret) ret = gate_set_u32(cs, ext->dcp, "apple,pmp-report", ext->entry->phandle); - /* No external display is attached in system sleep: the PMP may drop it. */ + /* In system sleep its displays are dark or detached: the PMP may drop it. */ if (!ret) ret = gate_add_bool(cs, ext->entry, "apple,release-for-sleep"); if (!ret)