From 72c97feccd12059bbebec1fca1a7135ae0bfe1a6 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 14:58:14 -0500 Subject: [PATCH 1/5] media: apple: isp: T8140: run the ISP as macOS does around a sleep After the camera ran, a sleep that kept DRAM up (SLP_DDR) never came back: the SoC reset 5 s after the alarm, inside the sleep engine's own wake. The first such sleep of every boot did the same, because the driver booted the firmware at probe for the sensor detection. That wake runs a program of its own for the ISP: it powers its CPU domains up, has it run, waits for it and powers it down again. macOS traced under aura leaves the ISP for it in one state: off with no client, its firmware stopped from WFI with the CPU's run bit still set, and started only when a client opens the camera. Linux kept the firmware running between streams, booted it at probe, and cleared the run bit before the power-down. With the run bit cleared the wake stalled even with everything else as macOS has it (1 of 1); with it set, 9 of 9 SLP_DDR sleeps returned, the first of a boot and those after a camera start included. So, on T8140: start the firmware at the first open, halt it at the end of each stream, and leave the run bit set when its domains go off. The camera still doesn't start after any SLP_DDR wake, with or without camera use before it, until a deep sleep: the firmware boots but its first command times out. A failed start of firmware that starts again no longer marks it unusable until the next boot, so the camera comes back after the next deep sleep. Signed-off-by: Diljit Singh --- drivers/media/platform/apple/isp/isp-cam.c | 9 ++++++--- drivers/media/platform/apple/isp/isp-drv.c | 2 ++ drivers/media/platform/apple/isp/isp-fw.c | 19 +++++++++++++++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/drivers/media/platform/apple/isp/isp-cam.c b/drivers/media/platform/apple/isp/isp-cam.c index 0f8ae7eb2f554..d065d007a0840 100644 --- a/drivers/media/platform/apple/isp/isp-cam.c +++ b/drivers/media/platform/apple/isp/isp-cam.c @@ -587,11 +587,14 @@ int apple_isp_detect_camera(struct apple_isp *isp) err = isp_detect_camera(isp); - /* Resident firmware stays up; the channel is set up per stream. */ + /* + * Resident firmware stays up, the channel set up per stream, unless it starts + * again: then it's off until the first stream, as on macOS. + */ if (isp->hw->resident_fw) { - if (err) + if (err || isp->hw->fw_restart) apple_isp_firmware_halt(isp); - else + if (!err) isp->camera_detected = true; return err; } diff --git a/drivers/media/platform/apple/isp/isp-drv.c b/drivers/media/platform/apple/isp/isp-drv.c index 2006351c49a53..245af9fa32f35 100644 --- a/drivers/media/platform/apple/isp/isp-drv.c +++ b/drivers/media/platform/apple/isp/isp-drv.c @@ -969,6 +969,8 @@ static const struct apple_isp_hw apple_isp_hw_t8140 = { .capture_meta_size = ISP_CAPTURE_META_SIZE_T8140, .resident_fw = true, .fw_restart = true, + /* As on macOS, the firmware first starts when a client opens the camera. */ + .deferred_start = true, .meta_exposure = true, }; diff --git a/drivers/media/platform/apple/isp/isp-fw.c b/drivers/media/platform/apple/isp/isp-fw.c index 610e5c3ad72db..144c2a4ae3883 100644 --- a/drivers/media/platform/apple/isp/isp-fw.c +++ b/drivers/media/platform/apple/isp/isp-fw.c @@ -475,7 +475,13 @@ static void isp_firmware_shutdown_stage1(struct apple_isp *isp) dev_warn(isp->dev, "coproc not in WFI before power-down (status 0x%x)\n", val); - isp_coproc_write32(isp, isp_coproc_control(isp), 0x0); + /* + * macOS powers T8140's ISP off from WFI with the run bit still set. The sleep + * engine's own restore of the ISP in a wake that kept DRAM up depends on it: + * with the bit cleared that wake stalls until the SoC's watchdog resets it. + */ + if (isp->hw->gen != ISP_GEN_T8140) + isp_coproc_write32(isp, isp_coproc_control(isp), 0x0); if (started) { /* The request bit alone asks for it, and reads back once done */ @@ -1299,7 +1305,8 @@ int apple_isp_firmware_boot(struct apple_isp *isp) if (err) { dev_err(isp->dev, "failed to boot firmware: %d\n", err); pm_runtime_put_sync(isp->dev); - if (isp->hw->resident_fw) + /* Firmware that starts again can be tried again at the next open. */ + if (isp->hw->resident_fw && !isp->hw->fw_restart) isp->fw_state = ISP_FW_DEAD; return err; } @@ -1326,6 +1333,14 @@ void apple_isp_firmware_shutdown(struct apple_isp *isp) if (isp->fw_state == ISP_FW_RUNNING) synchronize_irq(isp->irq); isp_free_released_surfaces(isp); + + /* + * Firmware that starts again goes down with the stream, as macOS's camera close + * takes it, and boots again for the next one. A system sleep then finds it off: + * stopped on the way into a sleep that keeps DRAM up, the SoC doesn't wake. + */ + if (isp->hw->fw_restart) + apple_isp_firmware_halt(isp); } /* Stop the firmware; resident firmware without hw->fw_restart stays down. */ From 83e3fba2166f772a54efa77a25fccbb5f1a96971 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 14:58:31 -0500 Subject: [PATCH 2/5] arm64: dts: apple: t8140: power the ISP's domains as macOS does macOS traced under aura (stop mode, a Photo Booth capture, then a sleep) powers the camera sensor domains ISPSENS0 and ISPSENS1 with the camera, after ISP_FE and before the rest of the pipeline, and lowers them at the close. It writes the ISP-local domains exactly 0xf, never with automatic power management, which Linux turned on for the two CPU cores. List the sensor domains in the ISP's power-domains in that order, and keep automatic power management off for the ISP-local domains. Signed-off-by: Diljit Singh --- arch/arm64/boot/dts/apple/t8140-pmgr.dtsi | 16 +++++++++++++++- arch/arm64/boot/dts/apple/t8140.dtsi | 7 ++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/arch/arm64/boot/dts/apple/t8140-pmgr.dtsi b/arch/arm64/boot/dts/apple/t8140-pmgr.dtsi index eacb2caf324cc..cac4c68fd3d9f 100644 --- a/arch/arm64/boot/dts/apple/t8140-pmgr.dtsi +++ b/arch/arm64/boot/dts/apple/t8140-pmgr.dtsi @@ -274,7 +274,8 @@ * enables between ISP_FE and ISP_SECURE. Plain power * gating only: a local domain parked with DEV_DISABLE (the t8112 * apple,force-disable pattern) never reached ACTIVE again on J700 - * (isp_cpu_core0 stuck at ACTUAL=0 on the second firmware boot). + * (isp_cpu_core0 stuck at ACTUAL=0 on the second firmware boot). No + * automatic power management either: macOS writes them exactly 0xf. */ ps_sep: power-controller@c00 { compatible = "apple,t8140-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; @@ -290,6 +291,7 @@ reg = <0x4000 4>; #power-domain-cells = <0>; #reset-cells = <0>; + apple,no-auto-pm; label = "isp_cpu"; power-domains = <&ps_isp_sys>; }; @@ -299,6 +301,7 @@ reg = <0x4008 4>; #power-domain-cells = <0>; #reset-cells = <0>; + apple,no-auto-pm; label = "isp_cpu_core0"; power-domains = <&ps_isp_cpu>; }; @@ -308,6 +311,7 @@ reg = <0x4010 4>; #power-domain-cells = <0>; #reset-cells = <0>; + apple,no-auto-pm; label = "isp_cpu_core1"; power-domains = <&ps_isp_cpu>; }; @@ -317,6 +321,7 @@ reg = <0x4018 4>; #power-domain-cells = <0>; #reset-cells = <0>; + apple,no-auto-pm; label = "isp_fe"; power-domains = <&ps_isp_sys>; }; @@ -326,6 +331,7 @@ reg = <0x4020 4>; #power-domain-cells = <0>; #reset-cells = <0>; + apple,no-auto-pm; label = "isp_dprx"; power-domains = <&ps_isp_fe>; }; @@ -335,6 +341,7 @@ reg = <0x4028 4>; #power-domain-cells = <0>; #reset-cells = <0>; + apple,no-auto-pm; label = "isp_secure"; power-domains = <&ps_isp_fe>; }; @@ -344,6 +351,7 @@ reg = <0x4030 4>; #power-domain-cells = <0>; #reset-cells = <0>; + apple,no-auto-pm; label = "isp_sts0"; power-domains = <&ps_isp_fe>; }; @@ -353,6 +361,7 @@ reg = <0x4038 4>; #power-domain-cells = <0>; #reset-cells = <0>; + apple,no-auto-pm; label = "isp_sts1"; power-domains = <&ps_isp_fe>; }; @@ -362,6 +371,7 @@ reg = <0x4040 4>; #power-domain-cells = <0>; #reset-cells = <0>; + apple,no-auto-pm; label = "isp_pearl"; power-domains = <&ps_isp_fe>; }; @@ -371,6 +381,7 @@ reg = <0x4048 4>; #power-domain-cells = <0>; #reset-cells = <0>; + apple,no-auto-pm; label = "isp_be"; power-domains = <&ps_isp_fe>; }; @@ -380,6 +391,7 @@ reg = <0x4050 4>; #power-domain-cells = <0>; #reset-cells = <0>; + apple,no-auto-pm; label = "isp_clr"; power-domains = <&ps_isp_be>; }; @@ -389,6 +401,7 @@ reg = <0x4058 4>; #power-domain-cells = <0>; #reset-cells = <0>; + apple,no-auto-pm; label = "isp_cvd"; power-domains = <&ps_isp_be>; }; @@ -398,6 +411,7 @@ reg = <0x4060 4>; #power-domain-cells = <0>; #reset-cells = <0>; + apple,no-auto-pm; label = "isp_cvm"; power-domains = <&ps_isp_cvd>; }; diff --git a/arch/arm64/boot/dts/apple/t8140.dtsi b/arch/arm64/boot/dts/apple/t8140.dtsi index 79f1b67184539..6247d9c31aa47 100644 --- a/arch/arm64/boot/dts/apple/t8140.dtsi +++ b/arch/arm64/boot/dts/apple/t8140.dtsi @@ -2131,9 +2131,14 @@ interrupt-parent = <&aic>; /* ADT [323 324 325 326 336]; 323 carries the IPC mailbox */ interrupts = ; + /* + * In macOS's order: the camera sensor domains come up + * after ISP_FE and before the rest of the pipeline. + */ power-domains = <&ps_isp_sys>, <&ps_isp_cpu>, <&ps_isp_cpu_core0>, <&ps_isp_cpu_core1>, - <&ps_isp_fe>, <&ps_isp_dprx>, <&ps_isp_secure>, + <&ps_isp_fe>, <&ps_ispsens0>, <&ps_ispsens1>, + <&ps_isp_dprx>, <&ps_isp_secure>, <&ps_isp_sts0>, <&ps_isp_sts1>, <&ps_isp_pearl>, <&ps_isp_be>, <&ps_isp_clr>, <&ps_isp_cvd>, <&ps_isp_cvm>; From 20a6080ed955081c914689dad82a926a4b66c345 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 14:58:31 -0500 Subject: [PATCH 3/5] pmdomain: apple: clear a BUSY latched on a domain that went off On T8140 both ISP CPU core domains reach off with BUSY (bit 11) latched, every time Linux powers them down: 0x0f000b00. macOS's trace has them off with it clear, 0x0f000300, through to the sleep commit. Only a PS reset clears it; the power-on path already pulses one for a latch it finds. Do the same when a domain goes down: if it reads off with BUSY still set, pulse its PS reset while it is off. Not during a system sleep transition, as on the power-on path. Signed-off-by: Diljit Singh --- drivers/pmdomain/apple/pmgr-pwrstate.c | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/drivers/pmdomain/apple/pmgr-pwrstate.c b/drivers/pmdomain/apple/pmgr-pwrstate.c index 2ee6c3b850098..403161b5224d4 100644 --- a/drivers/pmdomain/apple/pmgr-pwrstate.c +++ b/drivers/pmdomain/apple/pmgr-pwrstate.c @@ -134,6 +134,22 @@ static int apple_pmgr_ps_wait_parent(struct apple_pmgr_ps *ps, u32 *cur) * power-on then fails as it did before, and genpd, which ignores errors in * that phase, keeps the domain requested on. */ +/* Pulse the PS reset of a domain that is off with BUSY latched; it stays off. */ +static void apple_pmgr_ps_unlatch_off(struct apple_pmgr_ps *ps, u32 *cur) +{ + u32 off = *cur & ~(APPLE_PMGR_AUTO_ENABLE | APPLE_PMGR_FLAGS | APPLE_PMGR_BUSY | + APPLE_PMGR_PS_RESET | APPLE_PMGR_DEV_DISABLE | APPLE_PMGR_PS_TARGET); + + regmap_write(ps->regmap, ps->offset, off | APPLE_PMGR_PS_RESET); + regmap_read_poll_timeout_atomic(ps->regmap, ps->offset, *cur, + !(*cur & APPLE_PMGR_BUSY), 1, APPLE_PMGR_PS_SET_TIMEOUT); + regmap_write(ps->regmap, ps->offset, off); + regmap_read(ps->regmap, ps->offset, cur); + if (*cur & APPLE_PMGR_BUSY) + dev_warn_ratelimited(ps->dev, "PS %s: off with BUSY latched: %#x\n", + ps->genpd.name, *cur); +} + static int apple_pmgr_ps_clear_latch(struct apple_pmgr_ps *ps, u32 reg, u32 *cur) { u32 off = (reg & ~APPLE_PMGR_PS_TARGET) | APPLE_PMGR_PS_PWRGATE; @@ -266,6 +282,19 @@ static int apple_pmgr_ps_set(struct generic_pm_domain *genpd, u32 pstate, bool a } } + /* + * A domain can reach off with BUSY latched: on T8140 both ISP CPU cores do, + * every time they go down under Linux. macOS leaves them off with it clear, + * and the sleep engine, which powers them up itself in a wake that keeps DRAM + * up, stalls on a latched one until its watchdog resets the SoC. Only a PS + * reset clears it, and the domain is off, so pulse one as on the power-on + * path (not in a system sleep transition, see apple_pmgr_ps_clear_latch()). + */ + if (!ret && pstate != APPLE_PMGR_PS_ACTIVE && (cur & APPLE_PMGR_BUSY) && + !FIELD_GET(APPLE_PMGR_PS_ACTUAL, cur) && !ps->system_sleep && + !ps->wait_parent && !ps->strict_state && !ps->externally_clocked) + apple_pmgr_ps_unlatch_off(ps, &cur); + report: if (ret < 0) dev_err_ratelimited(ps->dev, From 42b185ddcb503897fbf6556f2d5aa2137e708746 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 14:58:58 -0500 Subject: [PATCH 4/5] soc: apple: sleep: keep the deep path after an ISP start for the camera The rule that sends the next sleep deep after an ISP start came from the SLP_DDR wake stall after camera use, which the ISP driver now avoids. It still matters for the camera: on T8140 the ISP firmware doesn't start after any SLP_DDR wake until a deep sleep, whether the camera ran before it or not. Say so; the behaviour is unchanged. Signed-off-by: Diljit Singh --- drivers/soc/apple/sleep.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/drivers/soc/apple/sleep.c b/drivers/soc/apple/sleep.c index 3838cedf471ef..e20565f94d104 100644 --- a/drivers/soc/apple/sleep.c +++ b/drivers/soc/apple/sleep.c @@ -672,9 +672,10 @@ void apple_sleep_coproc_crashed(struct device *dev) EXPORT_SYMBOL_GPL(apple_sleep_coproc_crashed); /* - * After the camera ran, the next SLP_DDR wake stalled in the sleep engine, 6 of 6 times, whether - * the ISP's firmware was stopped at the sleep or about 50 s before it; a deep sleep rebuilds - * what it leaves, as after boot. So from the ISP's start, the next sleep takes the deep path. + * T8140's camera firmware doesn't start again after any SLP_DDR wake until a deep sleep: it + * boots, but its first command never completes. So from an ISP start, even a failed one, the + * next sleep takes the deep path: a camera used before a sleep works after it, and one that + * failed after a SLP_DDR wake works after the next sleep. */ void apple_sleep_next_deep(struct device *dev) { From e9e2fb1fcc2f11a5c59a079a719c11971754d6c9 Mon Sep 17 00:00:00 2001 From: Diljit Singh Date: Mon, 9 Nov 2026 14:58:58 -0500 Subject: [PATCH 5/5] soc: apple: sleep: let the first sleep of a boot keep DRAM up The first SLP_DDR sleep of a boot stalled in the sleep engine, 13 of 13, so every boot's first sleep went deep. The stall came from the ISP firmware booted at probe for the camera detection, which the ISP driver no longer does on T8140, and from the way it was stopped; forced SLP_DDR first sleeps returned after the change. Let the host's vote apply from the first sleep. Signed-off-by: Diljit Singh --- drivers/soc/apple/sleep.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/soc/apple/sleep.c b/drivers/soc/apple/sleep.c index e20565f94d104..0041ab2476a55 100644 --- a/drivers/soc/apple/sleep.c +++ b/drivers/soc/apple/sleep.c @@ -617,11 +617,11 @@ static bool sleep_slp_ddr_usb; static void __iomem *sleep_slp_ddr_word; /* - * The SLP_DDR wake runs on the SoC-side state the system left. After a Linux boot it stalls in - * the sleep engine, and once one deep sleep has rebuilt that state it gets through every time. - * So the word stays clear, whatever the host votes, until a wake shows the deep path ran. + * The SLP_DDR wake runs on the SoC-side state the system left. The first one of a boot used to + * stall in the sleep engine, which came from the ISP's firmware boot at probe (see the ISP + * driver). Cleared while a deep path is owed, and set again by a wake that shows it ran. */ -static bool sleep_slp_ddr_ready; +static bool sleep_slp_ddr_ready = true; /* * A coprocessor that runs through a sleep crashed. The AOP is the always-on master SLP_DDR keeps