Skip to content

J616s (M4 Pro) at EL2: hibernation and suspend-to-RAM - #1477

Open
fbalazsy wants to merge 320 commits into
aurora-silicon:integration/aurorafrom
fbalazsy:j616s/el2-sleep
Open

fbalazsy wants to merge 320 commits into
aurora-silicon:integration/aurorafrom
fbalazsy:j616s/el2-sleep

Conversation

@fbalazsy

@fbalazsy fbalazsy commented Nov 10, 2026 •

Copy link
Copy Markdown

J616s (MacBook Pro 16", M4 Pro, T6040) running Linux at EL2 under aura: hibernation and suspend-to-RAM with the desktop session kept. Builds on #1445, which is merged.

Update 2026-11-13, night (head 6e7d5e4)

305 commits on integration/aurora a34d26c; the PR does not conflict. The base moved twice during the day (4277bdd, a34d26c). The rebase onto the second one dropped two commits of the series: the base now carries the T6041 ProRes encoder (media: apple: prores by RyanTheTide), so the ProRes part of media: apple: ProRes on T6041 and the ISP across sleep and media: apple: prores: drop a store that the suspend already made are gone; what is left of the first is media: apple: isp: ask the sleep controller whether the power is lost (a dry run keeps the power, so the ISP must not stop its firmware for it). Since the first update of the day (1ea066e) these were added:

Area What changed
Hibernation core The generic code no longer knows the platform: pm_hibernate_is_loading() for drivers, an arch hook for cleaning the pages of a loaded image (ARCH_HIBERNATION_CLEAN_IMAGE), an admission hook and two arm64 hooks (layout cookie, nosave pages) that aura-hibernate.c fills, and a record the loading kernel leaves so the restored kernel knows whether another boot loaded it (the SEP uses it instead of the PMP restart counter). A hibernation that ends before an image exists now recovers the devices (PMSG_RECOVER) instead of running their restore callbacks, and 13 drivers lost the check that guarded against that.
Sleep controller One schema for the T6040 and T8140 sleep controllers with the same register names (the T6040 names were renamed in the DT and the driver together), one test for the GP host's system domain.
Display The HDMI converter state flag is accessed with READ_ONCE/WRITE_ONCE, the once-per-machine power check is documented and atomic; the S2R warm restarts share their firmware return; unused includes dropped; apple_rtkit_idle() reports an idle that kept the firmware's memory apart from an error.
USB / PHY One dwc3 role decision during a sleep (a device role asked for on a host-only port during a sleep now leaves the port off after the wake, as the running path does); the PHY sleep provider is picked by its ops; one start of the Type-C controller's application firmware.
USB4 (Thunderbolt) The routers reach the Type-C PHY through phys and struct phy instead of a node pointer. DP IN 1 of a tunnel gets PCLK1 as well (as on T6030 and T8132). These commits were not run on a USB4/Thunderbolt device (none here); they are separate commits and carry that note, so they can be tested or dropped on their own.
Fixes of the first push The host test of the display fabric models the T6040 tunnel clock symbol; the layout and operations KUnit cases skip a SoC that has a part of its own; the tipd host test extracts the new start helper.

Run on the head of the previous rebase (8acba58, without its last commit, the Thunderbolt-only PCLK1 one), one boot, HDMI + DisplayPort in picture-by-picture, USB3 disk, no dock: cold boot (all three displays lit, ANE, ProRes, AES, Wi-Fi, BT, Touch ID, fans taken by the SMC); suspend-to-RAM four times (wake record MBSE 0x10650, noirq resume 113-120 ms, devices 560-580 ms); suspend-to-idle; dry run 2 ("a real commit would go on here (0 check(s) failed)"); a sleep refused at the enclave's request in the deep path (no SIO message, fans taken after); a hibernation aborted before the image (pm_test devices: devices recovered, SEP session kept, display and radios up); two hibernations from the desktop in reboot mode in one boot (restored, 14 CPUs, SEP attached again, fingerprint listed, Wi-Fi and BT up, RTC alarm kept) each followed by a suspend-to-RAM.
Local checks on the head: the full host test suite (the one failure it showed is fixed), KUnit g1 (1708 passed; the five apple-display-gate-t6041-max cases fail as they do on the base), the Apple device-tree check (no new warnings, apple,sleep.yaml and the USB4 router binding pass).
Run on this head (with an unrelated local series in drivers/accel/ane, which does not touch sleep): cold boot (three displays lit, ANE without timeouts, SEP, Touch ID, fans), the ProRes encoder through the base's driver (4 frames, no error; the frame header differs from the earlier driver in three bytes of colour metadata, the coded data is equal), suspend-to-RAM three times, suspend-to-idle, dry run 2, a sleep refused at the enclave, a hibernation aborted before the image (devices recovered, SEP session kept) and one hibernation in reboot mode (restored, SEP attached again, ANE restarted cold with no program replayed, fingerprint listed, radios up) followed by a suspend-to-RAM.
Not run: the dock, a KVM guest across sleeps, a disk behind a hub, a USB4/Thunderbolt device.

Base

  • 305 commits on integration/aurora at a34d26c (head 6e7d5e4); see the update above. The rest of this bullet describes the earlier rebase onto aa68b3c, whose first 258 commits it counted. The first 168 are the series as it stood at d24d697, rebased. The base's move of the SoC words into per-SoC data (sleep.c) met three of the clean-up commits: the shared header now carries the base's definitions, the PMU write helper takes the USID as the base's does, and what the base reads from its match data (the PMU write command, the boot stage, the response vector's enable for the sleep) stays with the T6040 part as its own values. The 89 on top are new in this push and are listed under "New in this push".
  • Every commit of mine carries its Signed-off-by; the two commits taken from others (RyanTheTide's phy: apple: t6040: tolerate a busy recovery domain, AxxzyWasTaken's spmi: apple: drop replies left by firmware at a firmware-driven resume) keep their authors and trailers.
  • The aura side, J616s at EL2: hib=1 fixed layout, T6040 S2R windows, the 26.7 display handoff and the S2R wake path aura#148, is merged; aura main has it.
  • Replaces J616s (M4 Pro): hibernation (suspend to disk) under aura's fixed boot layout #906.

What is in it

Area Change
PM core pm_hibernate_is_restored() lets restore callbacks tell a restored image from an aborted hibernation; a boot chain with a fixed layout (aura hib=1) admits hibernation.
arm64 Spin-table CPUs at EL2 are parked in a pen and released across S4 and S2R; EL2 state is restored for the hibernation resume.
soc/apple aura hibernation layout; RTKit cold restart and re-adoption of a coprocessor found running or halted after a power loss; PMP (data SRAM kept across S2R), AOP, SEP, DockChannel.
soc/apple sleep T6040 S2R as a part of the generic driver (sleep.c + sleep-t6040.c, struct apple_sleep_part in sleep-internal.h; one platform driver apple-sleep, the node enabled per board, on the J616s only). The entry takes the hardware down in a fixed order (CPU complexes, power gates, axi2af bridges, APCIe, DCS, the MasterMini S2R timer, then the commit word) and the wake brings the same blocks back, before syscore resume. The coprocessor and enclave registry stays in sleep-registry.c; the driver binds it once in probe and each SoC's part gives the two answers (T8140's rules and T6040's are the ones they had). APPLE_T6040_SLEEP stays an option, now the switch of the T6040 part under APPLE_SOC_SLEEP (with MFD_MACSMC=y). The part keeps its own suspend ops: suspend-to-idle passes through untouched and deep is offered only on a kernel booted with t6040_sleep.allow_entry=1. Four module parameters are left (allow_entry, dry_run, sleep_wd_secs, wd2_secs; 79 before); the progress marks, PMU records, probes, dumps and the experiments that were off by default are gone, the steps that were on by default are unconditional. Dry-run levels are 1 (every step that can be taken back, refused at the commit point) and 2 (also the last steps); the old level 1 never reached the commit point and is removed.
Display (drm/apple) Internal and external T6041 display processors across S4 and S2R: the stop order the firmware expects, power words, DART streams, AFK endpoints paused and resumed, backlight level sent again. External links are given back before the sleep and set up again after it. The HDMI output's link is started once per plug, from the converter's power-up, as macOS does: a second start through an HPD pulse left the display black on some cold boots. A display that the wake's link has just woken can list the modes of its standby first: a Type-C display that lists other modes than before the sleep is read again once, and the hybrid pipeline is not handed over on the first lists after a wake. A display that fits one pipe again no longer counts as waiting for a second one.
GPU (drm/asahi) G17 drain and restart around S4 and S2R, the GPU's PMGR word found through its PMP report entry.
USB / Type-C A T6040 Type-C port keeps its xHCI host through a deep sleep (PHY stopped and started under it), a pin D dock port included while DP is down; USB4 routers get their Type-C PHY in the DT; ACIO, CIO PHY and tipd across sleep.
PCIe, NVMe, radio DART locked tables re-imported; ANS/NVMe; brcmfmac survives a hibernation that was aborted or only thawed. After an S2R the internal host's two links are set up again in the noirq phase, with the port word, the tunables and the root-port link words the boot loader had set, so the PCI core finds the functions. The BCM4388's Bluetooth function comes back without its firmware: hci_bcm4377 sees that at resume (its configuration windows read reset values, boot stage 0), sends nothing and starts the transport's reset recovery at once instead of after command timeouts.
Camera (apple-isp) T6040's ISP is powered down between streams and its firmware started again from its data segment as loaded (the T8132 scheme), so the camera works after a sleep.
ANE H16 firmware 26.7.1, resident programs, system sleep and hibernation, on the base's hung-request recovery: its hung_stop and session quarantine are used unchanged, the hibernation events go through its notifier, and an engine quarantined at bring-up is powered off for a sleep (and left off at the resume) instead of refusing every sleep of that boot.
KVM Resuming from disk is a cold boot, so the physical counter starts again while a VM's counter offsets stay: the guest's counter went backwards and its clock stood still. The offsets of every VM are moved by the lost count at syscore resume.
SMC The sleep's commit writes WKTP = 2 as macOS does; after a full wake WKTP = 0 is written. Without it the SMC treats all the time awake as a power nap: it refuses manual fan control for the rest of the boot and asks for a sleep under load.
Other drivers AIC3, pmdomain, SPMI, PMU RTC, pinctrl (every AP pin and the block's global word put back after S2R), ProRes T6041 across sleep (on #1260's variant: the J616s rows are its built-in ones, the per-Mac file stays an optional override), SIO restart, DockChannel HID, AOP ALS, cs42l84 jack interrupt.
DT T6040 sleep controller, ProRes, JPEG, DP audio and USB4 PHY links on the J616s.
Clean-up (63 commits) Comments, dead code and state nobody reads, per-sleep messages at debug level (the display driver prints 96 lines per wake instead of 141), names and named constants, pm_sleep_ptr() instead of #ifdef CONFIG_HIBERNATION in PM operations, dev_* messages from the T6040 part, the sleep node's unit address, bindings for apple,t6041-display-power and aurora,s2r-mailbox. One of them fixes a build configuration: with CONFIG_HIBERNATION=n a suspend-to-RAM stopped the display firmware and could not start it again; the restart is built for every system sleep now. For the comment, rename and constant commits the objects are identical before and after; sleep.o built without the T6040 part has every symbol unchanged from f259e90.

New in this push (89 commits)

Area Change
SMC (4 commits at the end of the mfd group) A suspend that is called off no longer leaves the machine without its fans. This SMC does not take MBSE = slpw back: after slpw and then waka with no sleep between, it stopped running the fans (0 rpm with the CPU die at 100 to 113 C) and refused a fan request until the reboot; after the whole commit (WKTP, slpa, the AP idle notice) and no power loss its sensor reports stood still as well, so the thermal zones read a constant. A dry run, a device that refuses, a wake event during the entry and a refusal at the commit point all ended there, on the previous head of this series too. On T6041 the whole sequence is now sent from the sleep controller's last stop, with interrupts off and every refusal behind it (apple_rtkit_ap_idle_polled() is new for that); on the other SoCs slpw moves from .prepare to the noirq suspend, right before the commit. A suspend that ends short of the power loss sends the SMC nothing.
USB3 after a suspend-to-idle (phy-apple-t6040) The loss of USB3 until a reboot is fixed at its cause: in a suspend-to-idle genpd powered the PHY coprocessor's domains off under the running host, and the next stop of the coprocessor got AP idle not acknowledged: -62. The PHY now refuses that power-off while its firmware runs. With it: a powered-down sleep counts as the coprocessor's power cycle, a host whose USB3 is not back at a wake stays at USB2 and tries again at the next wake, and the CIO bring-up recovers the coprocessor and shuts the lanes down when it fails.
Display (drm/apple) A resume commit no longer sends a mode set to an external pipe whose sink is reported gone (the mode set failed: -5 of every suspend-to-idle with an HDMI display); the hotplug that reports the sink back sets the mode. A T6041 sleep keeps to the pipe's own power words; a firmware that was given up stays stopped across an S2R, and a dead internal processor no longer refuses every sleep; the sleep registers are mapped once at probe.
Wake time The two PCIe ports share the waits of a wake instead of taking 100 ms twice each (noirq resume 423 -> 118 ms); the dwc3 glue is asynchronous, so the USB hosts no longer start behind whatever the PM core's main thread waits for; the Type-C controllers do not wait 100 ms for a wake interrupt that has already come; a BCM4388 that lost its power is restarted from a freezable work item once the resume of every device is over (its resume callback: 619 ms -> 24 us).
Bluetooth The core is resumed after the cold function's reset, not before it (sending frame failed (-19) at the first wake of a boot).
Hibernation The jack codec, the RTC alarm, the pins, the AIC masks and the SPMI interrupt masks are put back after a resume from disk; tipd revalidates a port restored from disk itself, so the hibernation hook no longer has to write roles.
Thunderbolt / Type-C No wait for the router stop in PM_POST_*, the LSTX level at a router stop keyed on T6040, the T6040 DP IN adapters kept disabled, no watch for a router whose node is disabled. Without a USB4 device here these ran as regressions only.
NVMe, ISP, ProRes, ANE The RTKit object of a restore is configured as probe does; one predicate each for "this suspend takes the power"; timer_wakeups instead of irq_timeouts in the ANE's debugfs.
Tests macsmc_sleep_order_test expects the lid read (it failed at the base); new KUnit cases for the SMC's last stop; the host tests of the PM callbacks follow the code they extract.

Hardware results

J616s (48 GB), firmware 26.7.1, aura main (9887a6b5) at EL2, kernel built from this tree with APPLE_SOC_SLEEP and APPLE_T6040_SLEEP enabled.

This head (278b7b9) is the rebase onto aa68b3c of the head the tables below were run on (4b0be95 on fdef45b), plus one commit (dmaengine: apple-sio: stop the T6041 firmware at the sleep's commit). On the rebase (3d13866) with that commit's module: cold boots, three suspends to RAM, one suspend to idle, a level 2 dry run and two sleeps refused at the enclave's request, with the panel and both external displays, Wi-Fi and Bluetooth back each time. Hibernation and the tables below were not run again.

On 4b0be95, one boot. Set-up: a Samsung G95NC in picture-by-picture with one half on the HDMI port and the other in DP alternate mode, a USB3 disk, charger; no dock. All three displays were seen lit after the boot and after every step.

Test on 4b0be95 Result
Cold boot Panel and both external displays, Wi-Fi, Bluetooth, the USB3 disk; ANE at the first attempt; the SMC registered for the sleep controller's last stop.
Suspend-to-RAM (deep, RTC wake after 25 s) 3 of 3, one of them after the hibernation: the same compositor process, three outputs with their modes, Wi-Fi associated and answering, Bluetooth, the disk. "noirq resume" 118 ms, "resume of devices" 406 ms; no mode-set error. The SMC's sequence goes out at the last stop ("sleep warning at the last stop: 0 messages in 200 ms") and the wake record is the usual one.
Suspend-to-idle with the HDMI display No mode set failed; the HDMI half is back with the firmware's hotplug, about 5 s after the wake.
Dry run level 2 Refused at the commit point; nothing is sent to the SMC, its reports move and it takes a fan request afterwards.
Hibernation from the running desktop, reboot into the resume 1 of 1: image restored, both external displays, the disk, Wi-Fi, Bluetooth, a fan request taken; ANE exact, AES equal in both directions, ProRes frames identical.

On the previous base (6970a51) the same commits ran over five kernels as they were written, most of it with a dock (hub, Ethernet, card reader), the USB3 disk and one USB-C DP display:

Test Result
Suspend-to-RAM 34 in all, none failed; with the dock 12 USB devices on 9 SuperSpeed links after each.
The USB3 failure's recipe (cold boot, suspend-to-idle 15 s, then a dry run) genpd's power-off of the PHY domains refused once per port, no -62, 9 SuperSpeed links before and after. Before the fix: 2 of 2 lost USB3.
PHY fault injection on the dock port (drop_next_power_down_ack) Dropped acknowledge and a real S2R: 9 links. Dropped acknowledge and a dry run: the host stays, at USB2, nothing latched. A real S2R right after: USB3 back, 9 links.
Fans and SMC Before the fix (the 81 commits without the SMC ones): a dry run left both fans at 0 rpm at 100-113 C under load and a fan request refused; in the same boot the SMC had run the fans before it. After it: reports live and a fan request taken after a dry run, after a refusal before the enclave's request (apple_sep sleep_refuse_before_send), and after an RTC alarm that fired during the entry.
Hibernation Two in one boot, each restored, a suspend-to-RAM after them; an RTC alarm set before the first one fired at its time after the restore, one that passed while the machine was off was gone and a new one could be set.
USB disk across a suspend-to-RAM Mounted read-only with a reader running: the same block device and SHA-256. Mounted read-write with a writer: 120 blocks of 1 MiB written with fsync across the sleep, all 120 read back with the right SHA-256, no btrfs or I/O error.
ANE, Bluetooth A held ANE session exact across a sleep; Bluetooth powered off across a sleep and on again, two sleeps back to back: no error line.
Wake time, kernel only, from "Enabling non-boot CPUs" to "PM: suspend exit" Nothing attached: 0.285 s (noirq resume 125 ms, devices 62-88 ms), the panel's firmware back at 0.37 s. HDMI + DP + disk: 0.76 s. With the dock: 2.7 s, of which 2.2 s is the dock's hub tree coming back level by level by reset-resume.

Not run on either of these two heads: anything with the dock (it was run on the previous base only), the KVM guest across the sleeps, a USB4/Thunderbolt device (none here). T8140 was not run by me on any head, and the SMC and wake-time commits change paths it shares: the noirq slpw on every SoC, device_enable_async_suspend() on the dwc3 glue, the BCM4388 restart, the skipped 100 ms in sn201202x_resume.

Review

The comments and commit messages of the 89 new commits follow the same rule as the rest since the last review: they say what the hardware or firmware does and what was measured on the J616s.

Checks

Run on 4b0be95 against its base fdef45b (not repeated after the rebase onto aa68b3c), on an aarch64 host with its own compilers (gcc 16, clang 23, rustc 1.99), not the pinned ones of the workflows.

Check Result
tools/asahi/run-host-tests.py 324 runs, all ok with the host's compiler differences shimmed away (the base's new brcmf-recovery-tests among them). Without the shims 28 runs fail on this host's compilers; that run was not repeated at this base.
KUnit, C suites of the directories the series touches (qemu arm64) 1712 tests: 1703 pass, 4 skipped, and the five apple-display-gate-t6041-max cases that fail at the base as well (base: 1686 tests, those five and macsmc_sleep_order_test, which passes with the series).
KUnit, ANE boot.kunitconfig and transport.kunitconfig 230 of 230 each.
KUnit, drivers/soc/apple/sep.kunitconfig 605 tests, 603 passed, 2 skipped; in the workflow's form (LLVM, apple_sep*) 301 of 301.
scripts/dtc/apple-dt-check.sh (CHECK_DTBS on every Apple tree, dt_binding_check on the changed bindings) No warning the base does not have (402 lines at both, 137 DTBs); the 12 changed bindings pass.
sep-source workflow: the tools/aurora-sep tests, the three DWC3/ATC compile profiles The Python tests pass (14 lines, -O ones and the ABI step included); the three apple-dwc3-atc-compile.sh profiles compile; apple-sep.o builds.
checkpatch.pl --strict on the commits written for this push Clean but for one "prefer kzalloc_obj" note in the brcmfmac commit.

Open

  • A sleep refused at the commit point left the SIO firmware stopped until the next completed sleep: fixed by the last commit. The T6041 firmware now stops in the sleep's last step, as the T8140's does, so a refusal finds it running. After two refused sleeps and a dry run the AES engine behind it still encrypts and decrypts equal to the software implementation.
  • The SMC's last stop cannot refuse. If the SMC does not answer there, each of its three writes waits its 5 s with interrupts off before the sleep goes on. An event the SMC sends during the 240 ms of the sequence is taken from its outbox and handled after the wake.
  • After hot re-cabling (a full-width DP display on both pipes, the HDMI cable plugged while it ran, then the monitor switched to picture-by-picture) the HDMI half stayed dark once: "HDMI: no sink behind the converter yet, link started again (3/3)". A cold boot had both; the same switch of the monitor worked on the next two tries, one of them after a hibernation. Not reproduced, cause not known. A WARNING at typec_altmode_update_active() came once in the same re-cabling and was not looked at.
  • A resume from disk still logs "Unable to change power state from unknown to D0, device inaccessible" for the internal host's functions; they work afterwards.
  • The ANE's H16 firmware start with ignore_loglevel: unchanged from the previous description. Independent of sleep.
  • A mounted USB disk is not carried through a hibernation (the hook that hibernates this machine takes the Type-C controllers down first).
  • With a dock most of a wake is the USB core's reset-resume of the hub tree. usbcore.old_scheme_first=Y and the ports' fast-enumeration quirk took 0.64 s of it here with the same devices and device numbers; that is a setting, not part of this series.
  • Questions the new commits leave for you: whether the T8140 SMC also stays in its dark-wake policy until WKTP is written 0, and whether it loses its fan policy after slpw + waka as this one does (then its noirq path has the same hole after the noirq phase that the last stop closes on T6041); whether sn201202x_resume may skip the 100 ms on every SoC with that controller; whether the AIC mask resync after a resume from disk should be registered for every SoC; whether a host left at USB2 by a wake should try a normal start at once instead of at the next wake. 263989e (DART in an off domain) is not in here.

How to try

Deep sleep stays opt-in, on the kernel command line:

t6040_sleep.allow_entry=1
echo deep > /sys/power/mem_sleep; systemctl suspend

With t6040_sleep.dry_run=1 (writable at run time) the same request stops at the commit point and resumes warm.

@fbalazsy
fbalazsy force-pushed the j616s/el2-sleep branch 2 times, most recently from 3926193 to abc6ee8 Compare November 11, 2026 15:57
@fbalazsy fbalazsy changed the title J616s (M4 Pro) at EL2: hibernation, and suspend-to-RAM (WIP), on #1445 J616s (M4 Pro) at EL2: hibernation and suspend-to-RAM, on #1445 Nov 11, 2026
@fbalazsy
fbalazsy force-pushed the j616s/el2-sleep branch 3 times, most recently from c99d8b7 to 6deb620 Compare November 12, 2026 02:50
@fbalazsy fbalazsy changed the title J616s (M4 Pro) at EL2: hibernation and suspend-to-RAM, on #1445 J616s (M4 Pro) at EL2: hibernation and suspend-to-RAM Nov 12, 2026

@RyanTheTide RyanTheTide left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before this can merge, please reword the comments that name macOS driver classes and methods, mark steps as RE or decompile-verified, or say where a value was read. That covers mainly t6040-sleep.c, plus t6040-usb2.c, dcp.c and macsmc.c. Each comment should say what the hardware or firmware does: the register, the bits, the order, and what you measured on the J616s. Values and code can stay as they are.

@fbalazsy
fbalazsy force-pushed the j616s/el2-sleep branch 3 times, most recently from 2ce0d80 to 7aefde0 Compare November 12, 2026 07:05
@fbalazsy

Copy link
Copy Markdown
Author

Done in 7aefde0, thanks for the pointer to #1625.

  • The comments that named host-driver classes or routines, marked a step as reverse-engineered, or said where a value was read (a trace, a recording, a run, a source file) now describe the hardware or firmware: register, bits, order, and what was measured on the J616s. Mainly t6040-sleep.c and dcp.c, also t6040-usb2.c, macsmc.c, nvme/host/apple.c, pmp.rs, t6040.dtsi and apple-ane.rst. The private labels of my bring-up notes went with them.
  • Values and code are as they were: with comments stripped the files are identical, apart from six strings (five parameter descriptions and one log line) that carried the same labels.
  • One real change came out of it: t6040-sleep.c still printed a list of steps that "had never run on hardware" at every suspend and needed i_know_untested to get past it, although all of them run. The list and the parameter are removed in the last commit; the commit is reached with no live blocker and t6040_sleep.allow_entry=1, and a requested dry run comes first.

Rebased onto aea2deb on the way. Tested on the J616s on this head (suspend-to-RAM, hibernation, suspend-to-RAM again; details in the description). That round was on battery without the dock, so the external displays have not run on this head yet; I will add that run.

What is left in t6040-sleep.c are the module parameters and records marked "Bring-up"; they go when the T6040 becomes match data in sleep.c.

@RyanTheTide

Copy link
Copy Markdown
Contributor

Tested on a second J616s (M4 Pro, 24 GB, firmware of a macOS 27.0 install), Linux at EL2 under aura main 27298d21 (includes aura#148), kernel = integration + this series:

  • Deep sleep, RTC wake after 30 s: 25 of 25 sleeps resumed, each a real commit and mailbox return (wake: resumes now +1 per cycle): 10/10 on c99d8b7, then 4 + 10 on 7aefde0 with the four commits below (two boots). After every wake: Wi-Fi reassociated, gateway ping, an 8 MiB NVMe write + fsync + read, internal panel and the USB-C DP monitor, 7 input devices, CPUs 0-13. Wall time 32.3-33.5 s for a 30 s alarm.
  • One miss: on the first sleep of one boot, the dock port's USB3 side did not come back. phy-apple-t6040-usb2 392a90000.phy: RTKit: AP idle not acknowledged: -62 at the suspend, then PHY not back after the sleep (-5), dwc3-apple 392280000.usb: not resuming xHCI ... -5; the port came back USB2-only (hub on bus 3, the USB3 device gone) and stayed that way, with phymxwrap suspend failed: -11 on each later sleep. 24 of the 25 wakes had all 8 USB devices.
  • One 603 s deep sleep on battery: about 0.34 W average from the fuel gauge (rough: the battery was full and the gauge corrects itself after a wake). Awake with the panels off: 3.9-4.1 W.
  • Kernel log after each wake: the hci_bcm4377 -19 lines you list under Open, plus 1-5 apple_soc_edac 'event-log source 9' reports in the first ~10 s after the wake.

Four commits on top of 7aefde0 are on aurora-silicon/linux branch j6s2r/el2-sleep-fixups. Please pick them into this branch (or say if you want them changed):

  1. soc: apple: t6040-sleep: leave suspend-to-idle alone: the PM core calls suspend_ops->begin/end for s2idle when there are no s2idle_ops, so t6040_sleep_begin() refused every s2idle with -EOPNOTSUPP once the controller probed. .begin now returns 0 for anything but PM_SUSPEND_MEM, .end only undoes a MEM .begin, and .valid offers MEM only with t6040_sleep.allow_entry=1, so mem_sleep keeps s2idle as the default.
  2. soc: apple: share the sleep registry so one kernel builds both controllers: sleep.c and t6040-sleep.c both exported the nine apple_sleep_coproc_*/apple_sleep_enclave_* symbols, hence depends on !APPLE_SOC_SLEEP. The registry moves to sleep-registry.c; each controller binds to it in its probe. asahi.config gets CONFIG_APPLE_T6040_SLEEP=y, so one config builds the T8140 and T6040 controllers.
  3. soc: apple: t6040-sleep: depend on a built-in SMC driver: built in, it calls apple_smc_read_u32/write_u32.
  4. soc: apple: t6040-sleep: name the T8140 driver instead of its pull request: comment wording.

Checks on that branch: universal config builds (both controllers =y, and each one off), sleep KUnit 6 pass / 1 skip, checkpatch --strict clean, s2idle 3/3 with no kernel errors and the deep rows above on the J616s, s2idle 3/3 on an M1 Pro and an M2 with no new kernel errors.

AxxzyWasTaken added a commit to AxxzyWasTaken/linux that referenced this pull request Nov 12, 2026
…uspend

The T6041 (M4 Max) has the AICv3 of T8122 and now a suspend-to-RAM
driver of its own. Give "apple,t6041-aic3" its own info, the T8122 one
with fw_suspend, so the AIC state the sleep loses is saved and put
back as on T8140. Nothing else in the driver looks at fw_suspend.

Also in aurora-silicon#1477 (T6040 sleep), which admits apple,t6041-aic3 the same
way; drop this one when that lands.

Signed-off-by: AxxzyWasTaken <axxzytte@gmail.com>
AxxzyWasTaken added a commit to AxxzyWasTaken/linux that referenced this pull request Nov 12, 2026
apple_nvme_t6040_hw is only matched by "apple,t6041-nvme-ans2". Give it
the sleep fields T8140 uses: register ANS with the sleep core
(sleep_traced), flush namespace 1 only, the OS's own, and send the
full shutdown notification at suspend. The T6041 sleep driver stops
ANS through the sleep core's coprocessor stop.

On T6041 the sleep core's last check (the shutdown notification has
completed) does not run: the T6041 driver calls only the coprocessor
stop.

Also in aurora-silicon#1477, which sets the same fields with sleep_untraced on the
same struct; drop this one when that lands.

Signed-off-by: AxxzyWasTaken <axxzytte@gmail.com>
AxxzyWasTaken added a commit to AxxzyWasTaken/linux that referenced this pull request Nov 12, 2026
The PMP's SRAM loses its contents in a real sleep. On the J616C's
26.6.2 firmware its __DATA segment is at 0x500761000, 0x7d000 bytes:
back up (0x61000, 0x7d000) of the SRAM, as T8140 does for its own.

Also in aurora-silicon#1477, whose HW_CFG_T6041 has the same range; drop this one
when that lands.

Signed-off-by: AxxzyWasTaken <axxzytte@gmail.com>
AxxzyWasTaken added a commit to AxxzyWasTaken/linux that referenced this pull request Nov 12, 2026
On apple,t6041, write the keys macOS writes last before MBSE slpa in a
trace of its sleep: WKTP = 2 (AppleSMCEmbedded, going to sleep), then
pmVC. The value written is 0x101, as the bring-up kernel runs it; the
comment, taken over from it, says the trace has 0x100. That difference
is open.

Also in aurora-silicon#1477, which writes WKTP through a sleep_wktp parameter;
drop this one when that lands.

Signed-off-by: AxxzyWasTaken <axxzytte@gmail.com>
AxxzyWasTaken added a commit to AxxzyWasTaken/linux that referenced this pull request Nov 12, 2026
A conventional host (internal ports with the radio and the SD reader)
loses its power in a real sleep: the endpoints sit unpowered or in
reset behind a link that is down. On a firmware suspend, save what
only the port registers hold; on the noirq resume, note which ports
lost their link and keep a copy of their functions saved state before
the PCI core spends it on the down link; in the normal resume phase
(after the SMC, which owns the PWREN GPIOs) bring each such port up
again as probe does, then restore its functions.

Only for hosts that are not tunneled, have no managed SIDs, no PIO DMA
supplier and more than a root bus; s2r_conventional turns it off. The
suspend-to-idle paths (domain hold, adopted links) are unchanged.

Also in aurora-silicon#1477 (0ab1a66), which this follows; drop this one when that
lands.

Signed-off-by: AxxzyWasTaken <axxzytte@gmail.com>
@fbalazsy
fbalazsy force-pushed the j616s/el2-sleep branch 2 times, most recently from 1599341 to 9bee928 Compare November 12, 2026 13:35
@fbalazsy

fbalazsy commented Nov 12, 2026 •

Copy link
Copy Markdown
Author

Picked up, thanks for the second machine and the four commits. Head is f259e90 on integration/aurora 107b361 (the brcmfmac .thaw commit dropped out, the base has it now). In order:

  1. 7aefde0 as you tested it, then your four from j6s2r/el2-sleep-fixups, unchanged.
  2. One fix for them: the registry commit moved apple_sleep_commit_stops_coprocs() and the enclave registration out of sleep.c, and tools/asahi/sep-suspend-errors-tests.py reads them from there, so it and its 21 mutation runs failed ("missing function"). The test now reads the three functions from sleep-registry.c.
  3. The follow-up I had announced as a separate pull request, 35 commits; it was finished and it removes what you would otherwise have to review twice:
    • t6040-sleep's bring-up parameters, marks, records, probes and dumps are gone: 79 module parameters down to 4 (allow_entry, dry_run, sleep_wd_secs, wd2_secs); the same in the display, GPU, PCIe, pinctrl, PHY, dwc3 and PMP parts of the series.
    • dry_run=1 never reached the commit point (the PMU RTC refuses a deep sleep that is not firmware-driven in .prepare); it is removed and the levels are 1 and 2 now.
    • The T6040 controller is a part of the generic driver: sleep.c + sleep-t6040.c, one apple-sleep platform driver. Your sleep-registry.c and struct apple_sleep_ctl stay as they are; the driver binds the registry once in probe and the SoC's part supplies the two answers, each SoC with the rules it had. APPLE_T6040_SLEEP stays a real option (the switch of the T6040 part, with your MFD_MACSMC=y), so the line in asahi.config stays valid. Your suspend-to-idle change is kept as written: the part keeps its own suspend ops, .valid offers deep only with allow_entry=1.
    • Two comments that still cited pull request numbers are reworded.

If you would rather merge 9bee928 first and take the 35 commits as a second pull request, say so and I will split it there; they sit on top and nothing below them changed.

On the J616s here, kernel from this head with APPLE_SOC_SLEEP and APPLE_T6040_SLEEP (lid closed, no dock): cold boot, suspend-to-idle, dry run 1 and 2, suspend-to-RAM x3, hibernation, suspend-to-RAM after it; KVM guest, SIO AES and ProRes checked after each. The same list passed on 9bee928 earlier today. Host tests 292 -> 306 ok, KUnit with the same six failures as the base, Rust KUnit 573 of 575, sep-source, the three DWC3/ATC profiles and the dt check pass; details in the description.

Not verified: T8140 on the merged driver (its object differs from before only in the functions that now go through the part; a sleep on a T8140 would be welcome), and external displays, the dock and the kept USB hosts on this head. The USB3 miss on your dock port ("AP idle not acknowledged: -62") is under Open; I could not try for it without a dock.

sithglan added a commit to sithglan/aurora-linux that referenced this pull request Nov 12, 2026
… K116s deep-sleep base

Base: Jens Axboe's K116s series of 2026-11-12 (40 patches on
integration/aurora 2b4523c), the tree his K116s runs at EL2.
aurora-silicon#1477 brings the generic Apple SoC sleep driver with per-SoC parts
(sleep.c, sleep-t6040.c) to start a T6050 part from.

Signed-off-by: Thomas Glanzmann <thomas@glanzmann.de>
@RyanTheTide

Copy link
Copy Markdown
Contributor

@fbalazsy thanks for picking up the fixups and folding the T6040 controller into sleep.c. To merge this we need:

  1. A rebase onto the current integration/aurora (028d762); the branch conflicts now.
  2. Signed-off-by on 9bee928, c92a639, 9794539 and f259e90.
  3. Your external-display run on the rebased head, and whether you have seen the dock port come back USB2-only after a wake (phy-apple-t6040-usb2 "AP idle not acknowledged: -62") on your machine.
  4. Please reword "follows the order of macOS's quiesce and commit" in the description to describe the hardware order itself.
    One PR is fine with us; there is no need to split off the 35 follow-up commits. Once the rebased head is up we will run the J616s sleep rows and a regression pass on our other Macs.

The probe and the restore after a resume from disk both put the
controller into system power state S0, check that application firmware
runs and load the patch if the controller waits for one. The restore
repeated the probe's sequence step by step.

Move the sequence into one helper that both call. The order of the
steps, their error returns and what the probe does with the mode it
found are unchanged.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The T6040 USB4 router runs only over its port's Type-C PHY and tells
the PHY when it starts and stops. The PHY was named in a vendor phandle,
which the router resolved to a device node and looked up by hand for
each call.

Describe it with the standard phys property, as the USB controller of
the same port does, so the PHY framework resolves it and orders the
router after the PHY.

Not run on a USB4/Thunderbolt device.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
Name each router's Type-C PHY with phys, as the binding now describes,
instead of the vendor phandle. The routers stay disabled here, so no
default board changes. The built J616s device tree passes the router
schema.

Not run on a USB4/Thunderbolt device.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The router called the T6040 Type-C PHY through four exported functions
that took the PHY's device node, found the platform device by that node
on every call and kept the driver bound with the device lock. Nothing
ordered the router's probe or power management after the PHY.

Get the PHY at probe with devm_phy_get() from the router's phys
property and pass the struct phy to the same four calls, as the USB
controller does for its sleep steps. The PHY framework links the router
to the PHY as its consumer, and a router whose PHY is not bound yet
defers its probe instead of failing its first start. The PHY side
checks that the PHY is a T6040 Type-C PHY by its ops, takes its lock and
runs the same steps. The check for an enabled router in the PHY driver
reads the same property.

Not run on a USB4/Thunderbolt device.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
A driver that must treat the kernel loading a hibernation image
differently from a kernel that freezes for an image of its own has no
way to ask: the freeze callbacks run the same in both. The SMC driver
and the RTKit hand-off each registered a PM notifier that sets a flag on
PM_RESTORE_PREPARE and clears it on PM_POST_RESTORE, and the hand-off
copy is read by two more drivers.

Keep the flag in the PM core, next to the restored flag, and have the
two notifier helpers set it around the PM_RESTORE_PREPARE and
PM_POST_RESTORE notifications, whether the load starts from the resume
path or from the snapshot device. pm_hibernate_is_loading() reads it.
It is true from before the first notifier runs (the copies became true
only when their own notifier was reached) until PM_POST_RESTORE has been
delivered, and it is cleared again when a notifier refuses the load,
where the copies of the notifiers not reached never became true. It is
saved in an image as false, like the notifier copies, so the restored
kernel reads false.

The SMC driver reads the core flag in its freeze callback and drops its
notifier, which also removes the registration at probe. The RTKit
hand-off keeps apple_rtkit_hib_restoring() for its callers and answers
it from the core flag.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
swsusp cleans every page of a loaded image to the point of coherency
before the jump into it, for devices that read memory without snooping
the CPU caches, and does so under #ifdef CONFIG_ARM64 in the generic
snapshot code, with the justification of one SoC family in its comment.

Keep the walk over the image's page frames in snapshot.c and ask the
architecture, per frame, through arch_hibernate_clean_image_page(),
enabled by ARCH_HIBERNATION_CLEAN_IMAGE. arm64 sets the symbol and
provides the function with the body the walk had: a frame that is not in
the boot kernel's linear map is left to the restore_pblist copy, any
other frame is cleaned and invalidated to the PoC. The frames, their
order, the cond_resched() cadence and the clean operations are the same
as before; the disassembly of the arm64 function matches the removed
loop body. Other architectures compile the walk out, as they did.

The comment of the copy loop in hibernate-asm.S no longer names a SoC.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
hibernation_available() asked of_machine_is_compatible("apple,arm-platform")
and aura_hibernate_layout_fixed() in the generic hibernation code, and the
log line carried the reason of one platform.

Add hibernation_set_platform_check(): platform code registers a function
that is true while hibernation must stay refused, with the reason that is
logged at the end of the boot when it is. hibernation_available() asks it
where it asked the two Apple conditions. The aura hibernation code
registers aura_hibernate_blocked(), which is the same expression
(Apple platform and no fixed layout), evaluated at the same moments, and
the same reason text; the explanation of why the machines refuse moves
with it.

The check is registered from the init of that code on every boot of a
kernel that has it, where the Apple condition used to be compiled into the
core unconditionally. To keep that fail-closed, APPLE_AURA_HIBERNATE is no
longer a prompt: it is always on with ARCH_APPLE and HIBERNATION, as the
refusal belongs to it. Nothing registers before that init runs, which
precedes the first possible hibernation (the resume check is a later
initcall level and user space starts after all of them).

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
arm64's hibernation code read aura's /chosen properties, called aura's
nosave functions and kept aura's layout rules in the boot layout check
that is stored with an image and compared before it is restored.

Add struct arm64_hibernate_platform with two hooks, registered once at
boot: pfn_nosave(), asked by pfn_is_nosave(), and layout(), which adds
the platform's facts to the layout cookie. The part of the check that
belongs to every arm64 machine stays: the CPU release addresses (as
offsets from a base the platform may name, together with whether the
platform keeps the page out of the image) and the memory map less the
ranges the platform names. The aura hibernation code registers the hooks
and carries the rest: the /chosen properties, the S2R mailbox and the
split of aura's layout around the exec window, moved as they were, with
the comment that explains them. arm64_hibernate_layout_mix() is the
mixing function, made visible for the hook.

The nosave ranges stay a table in aura-hibernate.c and are not handed to
register_nosave_region(): that function allocates from memblock and runs
at setup_arch() time, and the table is filled from a late initcall. The
aura functions that nothing outside the file calls any more become
static, and aura_hibernate_layout_fixed() is dropped.

The cookie is bit for bit the same: the old function and the new pair were
compiled against the same stubbed device tree and memblock in 200000
randomised boots (properties present or absent, mailbox, window, nosave
tables, CPUs with and without release addresses, memory maps); every
cookie is identical, and a change of one mixed value in the new code is
caught. The set of pages kept out of the image is the table's, asked at
the same place.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
A driver whose device does not survive a boot cannot tell, in the kernel
restored from a hibernation image, whether the machine went through one.
pm_hibernate_is_restored() is also true after a test_resume, where the
kernel that wrote the image loads it itself and nothing rebooted, and
drivers fell back on hardware side channels to tell the two apart.

The kernel that loads an image now leaves a record for the restored
kernel just before the jump: whether it is the kernel that wrote the
image, which is the case only when hibernate() loads the image it has
just written for test_resume, or one that was started to resume it. The
record lives in the no-save section, which the image neither saves nor
overwrites, and carries a magic and its own physical address, so a record
from a kernel loaded elsewhere, or none, reads as absent. It is the same
requirement that in_suspend and other no-save hand-offs already have: both
kernels are the same build at the same physical address. The restored
kernel copies it where it sets the restored flag, and
pm_hibernate_restored_cold() returns 1 when another boot loaded the image,
0 when this kernel did (or was not restored) and -ENODATA when no record
was found. The record is cleared when it is read and when a restore fails,
and the value has the lifetime of pm_hibernate_is_restored().

The flag says that the machine booted, not that a given device lost its
state: a kexec-style resume would read as another boot with firmware that
kept running, and a device that needs more than the boot has to check it
itself. Nothing uses it yet.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
iBoot boots sepOS anew on every boot and the SEP has no run bit the AP
can read, so its resume from disk asked the PMP: the PMP's restore counted
the resumes that found the PMP halted, and the SEP's notifier, one
priority below the PMP's, compared the count taken at registration and at
each hibernation's prepare and end. That tied the SEP to the PMP driver
(a Kconfig dependency, a count, an export and a call from the PMP's
restore) and to the order of two notifiers.

pm_hibernate_restored_cold() answers the same question in the one place
that knows it: whether another boot loaded the image. The notifier asks it
where it compared the counts. Not restored (aborted hibernation, wake of a
hybrid sleep): warm, as before. Restored and loaded by another boot: the
cold path, the session is dropped and the device probed again. Restored by
the kernel that wrote the image (test_resume): warm; the PMP found itself
running there too. No record: the session is dropped without a probe,
which is what a missing PMP count did.

On the J616s the boot that loads an image always restarts the PMP, so
the two signals agree on a resume, a test resume and an aborted
hibernation. They differ where a boot that loaded an image did not restart
the firmware (a kexec-style resume), and for a machine without a bound
PMP, which can now tell. The PMP's own restore is unchanged: it still
reads the ASC run bit and only stops reporting a count.

The host test now drives the end of a hibernation through the notifier
for an aborted hibernation, a resume after another boot, a test resume and
a missing record, with three mutations that each fail it.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
When hibernation is abandoned before the image exists, hibernation_snapshot()
and create_image() resume the frozen devices with PMSG_RESTORE, the event of
a kernel restored from an image: in_suspend is clear in both cases, and
only the creating kernel with an image sets it. The cases are a device that
fails to freeze, a wakeup event, the pm_test levels devices, platform and
core, and a failure in the noirq phase before in_suspend is set. The
hardware did not boot in any of them, but the drivers get their restore
callbacks, run them for a state they cannot have, and some have to be told
by pm_hibernate_is_restored() that nothing happened.

Pick the event in one function: in_suspend set keeps what it was (PMSG_THAW,
PMSG_RECOVER after an error); in_suspend clear is PMSG_RESTORE only when
pm_hibernate_is_restored() says this kernel was restored, and PMSG_RECOVER
otherwise, which is what the PM core sends after every other failed freeze
(dpm_prepare() failing here, dpm_suspend_start()/end() through
resume_event()). The restored flag is reset at the start of every
hibernation_snapshot() and set right where the restored kernel leaves
swsusp_arch_suspend(), before either resume call.

What changes: only the calls of an abandoned hibernation, from the restore
callbacks to the thaw callbacks (pm_op() maps PM_EVENT_RECOVER to the thaw
callbacks as PM_EVENT_THAW), and, for a device with
DPM_FLAG_MAY_SKIP_RESUME, dev_pm_skip_resume() now follows must_resume as it
does for every other failed freeze. pm_hibernate_is_recovering() is now true
there as well, as its documentation says. Nothing changes for a created
image, a restored kernel or a test_resume, nor after a failed power-off in
platform mode (PMSG_RESTORE from hibernation_platform_enter(); only ACPI sets
platform hibernation ops).

For the Apple drivers the abandoned case was already a thaw: every restore
callback that checks the flag returns or forwards to its thaw (macsmc, the
RTKit helper, apple-dart, sn201202x, DCP, NVMe, PCIe, cs42l84) or does
nothing where there is no thaw (AOP, t6040 USB2 PHY, RTC, GPIO and SPMI
early restores); PCIe's restore_noirq and thaw_noirq agree on the ports.
So they now get the thaw directly. The checks in those callbacks stay for
now.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
…nation

An abandoned hibernation resumed its devices with PMSG_RESTORE, so every
restore callback that must not act on a state it never lost began with a
pm_hibernate_is_restored() check, and most forwarded to their thaw. The PM
core now sends PMSG_RECOVER there, which reaches the thaw callbacks, so the
check and the forward go from macsmc, the RTKit helper, apple-dart, the
sn201202x Type-C controller, the SPMI, GPIO and NVMe restore paths, both
PCIe restore callbacks, the DCP and AOP restores, the t6040 USB2 PHY's two
and the RTC; cs42l84's restore passes that the codec lost its state.

Each callback runs for a restored kernel exactly as before. For an abandoned
hibernation the old path was already the thaw or nothing, and the thaw now
runs on its own: macsmc, the RTKit helper, apple-dart, sn201202x, NVMe and
cs42l84 forwarded to the same function (cs42l84's resume asks
pm_resume_via_firmware(), which hibernation_snapshot() has cleared); PCIe's
restore_noirq did for a bridge without hibernation tunnelling what its
thaw_noirq does, and the same call with it; the AOP, t6040, RTC, GPIO and SPMI
restores did nothing and have no thaw. DCP keeps its power-off-failed branch
(PMSG_RESTORE after a failed platform power-off, which only a platform with
hibernation ops reaches) and loses the abandoned one, whose only other
effect was a log line.

One path changes: macsmc's thaw, after the SMC was handed over and the image
could not be entered, calls apple_smc_restore() to start a new session, and
its own check sent that back to the thaw, which did nothing. It now
starts the session as the comment says it should.

The flag keeps its other users, which are not restore callbacks: the AIC
syscore resume, the dockchannel receive interrupt, the SEP's PM notifier.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
DP IN 1 of a Thunderbolt tunnel runs on PCLK2, and on T6030 and T8132 its
video stops unless PCLK1 runs as well, at the same rate when DP IN 0 does
not use it. The T6040 tunnel clock enabled PCLK2 alone for DP IN 1, so the
second stream of a two-stream display (a tiled one) got no video.

Enable PCLK1 whenever either adapter runs, and write DP IN 1's rate into the
PCLK1 select while DP IN 0 is off. Hold clock switch 1 for it, and release
the switch and the enable when DP IN 1 stops, or hand PCLK1 over to DP IN 1's
rate when DP IN 0 stops first. A stop of the whole tunnel releases the held
switch.

Not run on a Thunderbolt display; this follows the two SoCs where the
requirement was measured.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
…lace

The render and the compute kick queue return their ring to the state of
a freshly installed queue after the GPU firmware was cold-restarted
across a hibernation by two functions with the same body: they differ
only in a check that the queue has the engine of its type.

That check can not fail: the engine of a kick queue is fixed when it is
built, and the constructors for the two types refuse the other engine.
Replace both functions by one that clears the ring, restarts the
timestamp and drops the parent, in the same order as before.

Hardware test: resume from disk (S4 round trip) with a render and a
compute client holding queues across the hibernation; both must submit
their first job after the resume without a timeout.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The wait for the PMP to take the held power state requests after a
resume from hibernation is a one-line wrapper around the PMP report
call. It sat in the CLPC wrapper file, next to the performance
controller calls that it has nothing to do with. Move it to the file
that already holds the GPU's PMP request and the sleep-time power state
word. The symbol keeps its name and its behaviour.

Hardware test: none for behaviour (same call, same arguments); a resume
from disk shows the "PMP requests acknowledged" line as before.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The T6040 sleep controller wrote the SMC key pmVC itself, with the key,
the value 0x101 (the J616s enable mask | 0x100) and the failure text
all living in sleep-t6040.c. The SMC key space belongs to the SMC
driver, so move that knowledge here: apple_smc_request_pmvc() performs
the same u32 write of 0x101 to pmVC and warns with the same text when
it fails. include/linux/mfd/macsmc.h declares it, with a -ENODEV stub
when the SMC driver is not reachable.

The moment of the write is unchanged. It is deliberately not made a
suspend callback of the SMC driver: that would run relative to the
USB-C controller's prepare step, which hands the port controller to the
S3 state, in a device-list order rather than the order the sleep
controller has today. sleep-t6040 still looks the SMC up by its
compatible as before and calls the helper from the same point of its
.begin, after the AOP island save and before the DCS power setup, with
the same warning when no SMC is there (now logged without the value).
The write reaches the SMC with the same key, size and value.

Hardware test: one suspend to RAM with a USB-C device attached and
powered; the log shows no "pmVC:" warning, the ATC port hand-over
completes, and the device is still present after the wake.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The statistics sysfs shim also owned the hibernation gate, the G17
drain notifier and the G17 statistics and thaw notifier, registered
from the probe of each device. Move them into g17/hibernate.c, next to
the other G17 sleep code, and register the gate once at module load
instead of when the first device binds.

The chain is the same for every event:

 - gate, priority 30: refuses hibernation with an unrestorable GPU
   bound and refuses restoring an image with any GPU bound. With no
   device bound it does nothing, so keeping it registered from module
   load to unload changes no outcome. Registering it can no longer fail
   at probe time.
 - G17 drain, priority 20, registered per device ahead of the display
   and PMP notifiers.
 - G17 statistics epoch and thaw, priority -10, below the PMP restore.

Per device they are still registered before the gate counts the device
and unregistered after it stops counting it, and the notifier bodies
are unchanged. The priorities are named in one place.

Hardware test: S4 round trip (drain at hibernate, cold republish after
the resume, "GPU firmware republished after resume" in the log), a
refused hibernation path (a PM_HIBERNATION_PREPARE failure after the
GPU drain, for example by a failing later notifier, must reopen
submission), S2R (statistics resume), and unloading and reloading the
aurora module.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The H16G HWX that Apple's compiler writes carries one LC_NOTE (0x31)
command, owner "src model info", which names the source model and the
compiler component. The validator refused the container on the unknown
command.

Accept one note as an annotation with no authority over mappings. The
compiler stores the payload inside the command, right after the 40-byte
note header, and counts the offset from the start of the command. Require
exactly that: a terminated owner name, a payload that starts after the
header and ends inside the command, and zero bytes after it.

A KUnit case covers the accepted note (the region table is unchanged)
and the refusals: payload past the command, overlapping the header,
wrapping length, nonzero padding and an unterminated or empty owner.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The validator tied the __TEXT segment flags to the scratch: SG_NORELOC
(0x4) with a __DATA scratch, 0 without one. That is one compiler's habit,
not a property of the program. Apple's compiler sets SG_NORELOC when the
task stream has no relocations: its add [1,1,128,128] program has no
scratch and flags 0x4, and its Parakeet encoder has a scratch, 1596
relocations and flags 0.

The flag only says that a loader relocates nothing in the segment. The
validator checks every relocation of the task stream itself and no
mapping depends on the flag, so accept 0 or SG_NORELOC on __TEXT in
every form. Other segments keep their exact flags.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
Apple's compiler appends STAB entries to the symbol table of an H16G HWX:
N_LSYM (0x80) descriptions of the element types (float16, int8, ...) and
one N_GSYM (0x20) per tensor with its array type. They have section 0 and
value 0 and describe the source, not the image; their names repeat the
tensor names. The validator required every entry to be a defined
section symbol and refused the container.

Skip entries with any N_STAB bit (type & 0xe0) after bounding their name
offset by the string table, and leave them out of the duplicate-name
check. Section symbols keep every existing check, and a STAB entry never
stands in for a surface symbol.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
A whole-model H16G HWX names every kernel blob in its symbol table. The
Parakeet encoder that Apple's compiler writes has 24809 entries (24783
section symbols, 26 STAB entries) and 1.77 MB of names, against a limit
of 4096 entries and 256 KiB. The duplicate-name check also compared
every pair, about 3e8 string compares for that table, and the validator
runs several times per LOAD.

Raise the bounds to 64K entries and 8 MiB of names, which covers a
kernel region at the 1 GiB segment limit at the encoder's density, and
find duplicate names by sorting the section symbol names and comparing
neighbours. The table and its strings stay bounded by the file and must
not overlap any other file range, as before.

A KUnit case builds 30000 constant symbols, accepts them, and refuses
two equal names far apart, a constant that reuses a surface name and a
count above the bound.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
A large H16G model does not fit its kernel data in one __KERN_0 segment.
The compiler splits it into __KERN_0, __KERN_1, ... of at most 128 MiB
each (the Parakeet encoder: four segments, 390 MiB). The segments are
contiguous in VM and in the file, each holds one section __kern_n, the
program descriptor lists every section base after the surface bases,
and the task stream addresses each section through its own BAR:
relocations name the section's ordinal and carry an offset into that
section.

Accept up to 16 kernel segments in order. Each further segment must
start where the previous one ends, in VM and in the file, and follow a
wholly file-backed one, so that together they form one kernel region
that maps exactly like the file and stays within the segment limit.
The validated info keeps every kernel section, and @kernel spans them
all with the ordinal of __kern_0. The base records of the descriptor
must be the surface bases, then every kernel section base, then zero.
Relocations may name any kernel section and are bounded by it.

The section, segment and file-range tables grow with the kernel
segments; the region table does not.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
A buffer reference (load command 0x40) names its surface in the bytes
after +0x18. With names of up to seven bytes the command is 32 bytes
long; Apple's compiler writes 0x28-byte commands with a 16-byte field
for longer names such as "attention_mask" and "encoder_hidden". The
validator took only 32-byte references and compared at most eight bytes
of the name.

Accept references of 32 bytes up to a 32-byte name field, with the name
terminated inside the command and zero after it. A surface pairs with
the tensor whose whole short name equals the reference's name, and the
surface keeps names of up to 31 bytes.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The engine reads every BAR at the base the operation section gives it,
and the firmware programs a BAR only from those (slot, tag) pairs. Tag
1007 names what LOAD slot 1 carries: the __KERN_0 section of a program
that has one, otherwise __TEXT,__const, the constants that every program
places at BAR slot 1 of its descriptor. One slot gives one base.

Apple's whole-model programs need more: the Parakeet encoder reads its
38 MB of constants through BAR 1 and its kernel data through BARs 9 to
12, one per __kern_n section, with offsets into that section. No slot
assignment of the kernel and the constants gives five distinct bases.

Bind such a program the way a scratch is bound: the generic section
lists every kernel section after the surfaces and the scratch, typed as
a read-only input of the section size (up to its 16 KiB pages), each
with its own ID, and the operation section binds every kernel base to
that ID. EXECUTE then carries one buffer per kernel section at its place
in the one kernel region mapping. LOAD slot 1 carries the constants at
their address in the __TEXT mapping, as for a program without kernel
data, and 1007 binds BAR slot 1.

A program whose generic section has no kernel entries keeps the
existing form: slot 1 carries its one kernel section, which it must
bind through 1007. That form refuses more than one kernel section.
In every form a 1007 pair must name the resource slot 1 really
carries, so the constants base binds to 1007 only when slot 1 holds
them.

KUnit covers both forms: the slot table and EXECUTE descriptors of a
four-section program with a scratch, the refusal of four sections in
the slot-1 form, unbound or mismatched kernel IDs, kernel entry sizes,
types and duplicate IDs, and which base 1007 may name.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
The load planner capped the sum of all mapped regions of one program at
512M, although each segment may map 1G. Apple's Parakeet encoder maps
488M (390M of kernel data in one region, 37M of __TEXT, a 59M scratch,
1.2M of surfaces): it fits with 24M to spare, and a slightly larger
model or a longer input would fail the plan with -E2BIG while every one
of its segments is far below its own limit.

Raise the sum to 2G, twice the segment limit. The bound stays: every
region is rounded to the 16K mapping size and added with an overflow
check before any memory is taken, and a program beyond it is refused
before any firmware-visible mapping exists. The test plans 1.5G,
exactly 2G and one page more, and checks that a segment above its own
limit still refuses.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
Teach the host section builder the forms the driver now takes:

- kernel segments __KERN_0..__KERN_n in order;
- BAR slot 1 at __TEXT,__const binds index 1007, which names the
  constants whenever LOAD slot 1 carries them (no kernel section, or
  kernel sections bound as buffers). A program without kernel data used
  to leave that slot unbound; --no-constants-ref keeps that behaviour
  for comparison;
- a program with several kernel sections, or with a kernel section and
  a constant operand, lists each kernel section in the generic section
  after the surfaces and the scratch, as an input of the section size
  with the next input ID, and binds every kernel base to its ID.

Buffer references with names longer than seven bytes already parse.
The sections of the add-128 fixture, the conv/relu program and Apple's
add [1,1,128,128] program are byte-identical to before.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
Describe what the H16 validator now takes from Apple's compiler: the
provenance note, STAB entries, SG_NORELOC on __TEXT, longer buffer
reference names and kernel data split over __KERN_0..__KERN_15, with
the aperture records and relocations that go with them. Describe what
index 1007 names, the kernel-buffer form of a program with split kernel
data or constants next to kernel data, and the 2 GiB budget, and say
that the buffer form has not been run on hardware.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
…ller

The H16 firmware checks every EXECUTE buffer descriptor against the
buffer size the program's generic section declared at LOAD, like H17
does: a descriptor smaller than the declared buffer makes the firmware
refuse the request, and the engine hangs until it is booted again.

H16 descriptors carry the HWX section size. That covers programs whose
surface section spans the whole buffer, but Apple-compiled HWX keep only
the tensor in the section: a [1, 512] add has a 0x400 section in a
0x4000 buffer, and the Parakeet encoder mask has 0x1780 of 0x4000. Both
hung on every EXECUTE.

Send the larger of the declared size and the section size on H16. A
section that already covers the declared buffer sends the same size as
before; the declared size never exceeds the mapping, which LOAD checked.
With the sections grown to the buffer size in the file, the [1, 512] add
and the whole encoder run on the J616s, so the size is the only change
they need. The test sends a section holding only the span and expects
the declared size.

Signed-off-by: František Balázsy <frantisekbalazsy@gmail.com>
@niteria

niteria commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Test report from Claude (Opus 5.5) for niteria.

The light sensor's sleep handling here also fixes the J700 (MacBook Neo, T8140, CT817 sensor), and we're dropping our own fix in favour of it.

What we tested. integration/aurora 1d2ef218 plus only this PR's aop_als.rs changes (0ff54175 + ac34523a; the file is unchanged at the current head f96cb630), built into the kernel. The DockChannel HID part was left out to keep the test to the sensor. Aura 1.0.0b9.

Without a fix, after the first deep sleep the sensor stops reporting: the mailbox interrupt rate drops from about 51 per 10 s to 0 and the reading stays at its last value for the rest of the boot.

With this PR, after a deep sleep woken by the RTC alarm:

  • Interrupts: about 51 per 10 s before the sleep, 50 after it.
  • Readings while covering the sensor, lighting it with a torch, then leaving it alone, 15 s each, one reading per second:
Phase min (lux) max (lux) mean (lux) distinct readings
Covered 0.84 10.75 1.77 3/15
Lit 9.60 1434.14 704.63 15/15
Left alone 9.60 12.29 11.29 5/15

(The 10.75 under "covered" is the reading before the finger landed.) An earlier module-swap test on 2026-11-10 gave the same interrupt-rate result over two more deep sleeps.

Our fix, for comparison. We had a fix with the same structure: interval 0 before the sleep, the probe's full arming sequence after it. We never opened it because this PR covers the J700 too. Its one extra is leaving the interval at 0 on unbind, so a later probe starts from the state enable_als() expects. That's optional and not needed for this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants