Skip to content

J713 (M4 MacBook Air): suspend-to-idle - #449

Closed
erikwb wants to merge 10 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/s2idle
Closed

erikwb wants to merge 10 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/s2idle

Conversation

@erikwb

@erikwb erikwb commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

The J713 refused system sleep in four places: the DCP (the retained T8132 session had "no qualified system-sleep handshake"), the retained display and radio DARTs, the radio's PCIe port, and genpd gating the GPU's GFX domain under the running G16 firmware. With this series it sleeps from systemctl suspend and from the lid, and wakes on the keyboard, trackpad, power button or lid, with display, GPU, Wi-Fi and audio intact.

The display handshake

On IOMFB 26.x the handshake is setPowerState (A473) on the out-of-band channel, which the T8132 words carry as stream 1 (bit 10).

  • Off: with the panel blanked, setPowerState(0) makes the firmware log power on -> off, disable its M3 helpers and switch to run mode 0.
  • On: setPowerState(1) brings it back (run mode 0 → 4, M3 firmware reloaded), and the next present scans out.
  • Callbacks: the firmware calls back about ten times during each transition. They're answered with exact shapes.

Patches 1-3 add stream-1 calls to the transport (calls nested in a stream-1 callback stay on stream 1), the callback shapes and answers, and the suspend/resume hooks. apple-drm is a device-link consumer of the DCP, so it has blanked before the DCP suspends and restores the CRTC after it resumes. The DCP keeps its domains on through the wakeup path, as the older DCPs do.

Retained DMA owners

The radio's PCIe port and DART and the display DARTs have no power domain of their own in the tree, and the DCP holds the display domains on. So suspend-to-idle leaves their live state in place, and patches 4-5 let them through for s2idle only; every other sleep state is still refused. The DARTs save their registers only so resume can check that nothing moved.

If something did move, the consumers' DMA is no longer what they set up, and a resume error alone doesn't stop the PM core from resuming them. So before failing resume, the DART quarantines them through the same paths as a translation fault:

  • the managed radio streams are marked failed and their DMA is disabled, so later maps are refused;
  • the borrowed display consumers (the DCP and its frontends) are marked failed, so their later maps are refused.

GPU

asahi-g16 has no system-sleep callbacks, so genpd's noirq power-off gated GFX, and with it the TVM gate, under the live firmware. The power-on at resume then timed out (GFX target=0xf … error=-110). A pmgr-pwrstate domain marked apple,sleep-retained declines that power-off while the system sleeps (patches 6-7). Runtime PM is unchanged.

Wakeups

Every ASC mailbox receive IRQ is IRQF_NO_SUSPEND. The G16 firmware's idle ring-service kicks (~1.9/s) and an AOP report (1/s) woke the CPUs about three times a second:

Sleep Wakeups Deep idle
Before patches 8-10 938 s 1472 512 s
After 148 s 3 146.9 s

A mailbox marked apple,hold-rx-in-sleep masks its receive IRQ from noirq suspend to noirq resume (patches 8-9). The firmware throttles once its outbox is full: after 148 s, 11 GPU and 3 AOP messages were waiting.

Patch 10 sets both flags for the GPU. The AOP mailbox flag is a one-line follow-up on the AOP node from #434:

&aop_mbox {
	status = "okay";
	apple,hold-rx-in-sleep;
};

Testing

#432 (cpuidle) is what lets the cores reach deep idle while asleep.

@RyanTheTide

RyanTheTide commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

The J713 suspend and lid results in the PR cover the normal retained-state path, including display, GPU, radio and audio after resume. The new retained-DART admission flags are already restricted to J713 by probe, and the PMGR/mailbox opt-ins are set only in its DT.

The lost-retention branch still needs a fix. apple_dart_resume() returns -EIO when the saved TCR/TTBR state changed, but leaves the managed-radio and borrowed-display admission state usable. A resume callback error does not stop the PM core from resuming consumers. Please quarantine the affected streams/session before returning, using the existing managed/borrowed failure paths as appropriate, and add a host fault case that changes a retained register and verifies later consumer operations are refused. Logging the loss alone does not establish a safe resume boundary.

The host fault case against the extracted production functions changed a live TTBR. Resume returned -EIO, managed_failed stayed false, and a later 16 KiB map succeeded. The control that sets the failure flag refused that map. This checks the managed path on the host, not physical J713 retention.

This head also needs a rebase onto 3b77f0f97eb0. Commit 5afbdfeb0c79 conflicts in dcp_platform_suspend() with the base's aborted-takeover guard. The local test assembly keeps that guard before the new M4 suspend call.

The stacked kernel at 5b6f5515e2f8 passed both full universal and Fedora-base WERROR builds (Image, modules and DTBs); both preserve all 131 common-config settings. ARM64 16 KiB KUnit passed 275/275. DCP host tests passed 63/63, and the PMGR and radio SID checks passed. The M4 host suite has the same five failing groups as the pinned base (18/23 on each); the AFKv2 HPD harness also fails on both. These are recorded failures, not a full host-suite pass. All six T8132 DTBs were checked: 13 schema diagnostics against 11 on the base, with exactly two new DCP dma-noncoherent binding errors from #429. The seven targeted binding checks passed. These software gates do not replace the required shared-code boots.

This also changes shared DART save-state code, PMGR and mailbox PM, so the assembled kernel needs Neo and M1 Pro or M2 regression boots with s2idle, display/GPU, radio and audio checks. Merge after #429/#432 for the qualified J713 graphics and idle combination. #450 follows this and #434.

erikwb added 10 commits October 25, 2026 20:20
IOMFB 26.x takes setPowerState (A473) on the out-of-band channel, the
older contexts' +4, which the T8132 words carry as stream 1 (bit 10): the
AP packet goes at shared-memory offset 0x8000, the firmware answers on
the same stream, and the callbacks it makes meanwhile arrive at 0x68000
with bit 10 set. The J713 backend so far admitted stream 0 only.

Let a top-level AP call name stream 1, and while it is outstanding,
decode incoming words with bit 10 as stream-1 packets. Calls nested in
one of its callbacks stay on stream 1, at the next aligned offset after
the outstanding packet, as on stream 0. Stream-0 traffic is unchanged.
The first user is system sleep.

Signed-off-by: Erik Bourget <erik@ebourget.net>
Powering the J713 display down and up with setPowerState (A473), the
26.6.2 firmware calls back into the AP: acknowledgeSetPowerState
(D106), will_power_off_signal (D002), get_calendar_time_ms (D209),
powerUpDART (D572), swap_complete_intent (D592), set_kernel_power_assert
(D700), aot_supported_peek (D203), set_power_trace_enable (D008),
set_idle_caching_state_ap (D117) and enable_backlight_message_ap_gated
(D594). The exact-shape table refused them, which quarantines the
session.

Add their shapes and answer them for this machine: the notifications
and setters with an ACK, the status-returning calls with success, AOT
support with false, and the calendar time in milliseconds. The DART
and the power assertion are the AP's to hold: the J713 keeps the
display domains on across suspend-to-idle.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The J713 DCP refused system sleep: the retained T8132 session had no
qualified handshake. setPowerState (A473) out of band is that handshake
on IOMFB 26.x: with the panel blanked, setPowerState(0) powers the
display pipeline down (the firmware logs "power on -> off" and disables
its M3 helpers) and setPowerState(1) brings it back, after which the
next present scans out again.

apple-drm is a consumer of the DCP, so its suspend handler has blanked
the panel when the DCP suspends, and its resume handler restores the
CRTC after the DCP resumed. Power the display down in between, with the
coprocessor running and its power domains kept on through the wakeup
path, as on the older DCPs; refuse sleep should the panel still scan
out. Presents while the display is down are refused.

The request is the 16-byte {u64 state; u8 flags[4]; u8 result_null;
u8 pad[3]}, the reply {u32 value; u32 status}; a nonzero status fails
the transition and the session.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The J713's retained DMA owners refuse system sleep: the radio's DART
with managed streams, and the display DARTs, which borrow the running
DCP's live page-table root. Neither has a suspend boundary of its own,
and their state cannot be restored from a copy.

Suspend-to-idle does not need one. These DARTs have no power domain of
their own in the tree, and the DCP keeps the display domains on across
s2idle, so their state stays where it is. Let them through for
suspend-to-idle only, saving their registers solely for resume to check
that nothing moved. Every other sleep state is still refused.

A DART whose retained state changed cannot be trusted with its live DMA,
and a resume error does not stop the PM core from resuming consumers.
Quarantine the managed streams and the borrowed display consumers
through the same paths as a translation fault before failing resume, so
that their later maps are refused. A host test exercises the extracted
check, quarantine and map functions.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The J713's radio port, whose streams the OS adopts from the boot
loader's handoff, refuses system sleep in its noirq phase. It has no
suspend boundary of its own, but suspend-to-idle does not need one:
the port has no power domain in the tree, so it stays up as any other
internal port does, whose resume does nothing. Let it through for
suspend-to-idle only; every other sleep state is still refused.

Signed-off-by: Erik Bourget <erik@ebourget.net>
A coprocessor can keep running in its power domain across system sleep
without system-sleep callbacks of its own, as the J713's GPU firmware
does. Describe such a domain, which the OS keeps on during system
sleep; runtime power management is unaffected.

Signed-off-by: Erik Bourget <erik@ebourget.net>
…tem sleep

The J713's G16 GPU has no system-sleep callbacks, so in the noirq
phase genpd powers its GFX domain off, and the TVM gate above it, under
the running GPU firmware; the power-on at resume then times out. Let a
domain marked apple,sleep-retained decline its power-off while the
system sleeps: genpd then keeps it and its parents on. Runtime power
management is unchanged.

Signed-off-by: Erik Bourget <erik@ebourget.net>
Some coprocessors message the AP periodically while idle: the J713's
GPU firmware asks for ring service about twice a second, and its AOP
sends a report every second. Describe a mailbox whose messages need not
wake the system, so the OS can leave them in the FIFO while it sleeps
and handle them at resume.

Signed-off-by: Erik Bourget <erik@ebourget.net>
…uest

The receive IRQ of the ASC mailboxes is IRQF_NO_SUSPEND, so every
coprocessor message wakes a CPU out of suspend-to-idle. On the J713
the GPU firmware's idle ring-service kicks and an AOP report every
second wake it about three times a second: in 938 s of s2idle, 1472
wakeups and only 512 s in deep idle.

For a mailbox marked apple,hold-rx-in-sleep, mask the receive IRQ from
noirq suspend to noirq resume, so its messages wait in the FIFO and the
consumer handles them on resume. The mailbox is a supplier of its
consumer, so this runs after the consumer suspended and before it
resumes. The firmware throttles once its outbox fills: after 148 s of
s2idle, 11 GPU and 3 AOP messages were waiting, with 3 wakeups and
146.9 s in deep idle.

Signed-off-by: Erik Bourget <erik@ebourget.net>
The G16 firmware keeps running across suspend-to-idle: keep its GFX
domain on through system sleep, and hold its idle ring-service kicks in
the mailbox instead of waking the system about twice a second.

Signed-off-by: Erik Bourget <erik@ebourget.net>
@erikwb

erikwb commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Updated (4fe8d2f), rebased onto 3b77f0f97eb0.

Lost retention: apple_dart_resume() now goes through apple_dart_resume_retained() for a retained DART. If the saved TCR/TTBR state changed, it quarantines through the existing translation-fault paths before returning -EIO:

  • apple_dart_managed_quarantine() for the managed radio streams: managed_failed is set and their DMA disabled;
  • apple_dart_quarantine_borrowed() for the DCP and its frontends.

Host fault case: tools/testing/selftests/drivers/apple/dart-s2idle-retention.py runs on the extracted production apple_dart_state_retained(), the two quarantine helpers, apple_dart_resume_retained() and apple_dart_map_pages().

  • Each of these makes resume return -EIO, with managed_failed and every borrowed failed set, the radio SIDs disabled, and a later 16 KiB map refused:
    • a changed managed TTBR;
    • a changed TCR on another stream of the same DART;
    • a DART that reads back as reset.
  • The retained case still resumes and maps.
  • A negative control with the quarantine removed fails at runtime.
  • On the display side, m4-dcp-errors already checks that a failed DCP domain refuses apple_dart_dcp_map_owned() with -EIO.

As you said, this checks the failure path on the host, not physical retention.

Rebase: commit 3 now keeps the aborted-takeover guard before the M4 suspend call. Range-diff against the old head: commits 1-2 and 5-10 are identical, commit 3 differs only by that context, and commit 4 has the quarantine and test.

Checks:

  • apple-dart.o builds with W=1 and no warnings; checkpatch --strict is clean.
  • The apple-dcp host suite is 57/63, with the same six failures as the bare base; m4-dcp-errors and the radio SID checks pass.

Noted on the Neo and M1 Pro/M2 regression boots; I don't have those machines.

@erikwb

erikwb commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #830 and #831, both on integration/aurora, with the commits unchanged:

@erikwb erikwb closed this Nov 2, 2026
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.

2 participants