Skip to content

drm/apple: m4: Power the display down for system sleep, while blanked and while static - #830

Open
erikwb wants to merge 5 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/dcp-power
Open

erikwb wants to merge 5 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/dcp-power

Conversation

@erikwb

@erikwb erikwb commented Nov 2, 2026 •

Copy link
Copy Markdown

The J713's (MacBook Air 13" M4, T8132) display pipeline stays powered whenever the panel is on: through system sleep, while it is blanked, and while a static frame is shown. This series powers it down with the DCP's own handshake. It replaces the display half of #449, plus #501 and #503, as one drm/apple PR; the commits are unchanged.

  • System sleep (commits 1–3) is what the J713's suspend-to-idle needs from the DCP; the rest of suspend-to-idle is J713 (M4 MacBook Air): suspend-to-idle #831.
  • Blank and the idle cache (commits 4–5) work at runtime, with or without system sleep.

The handshake (commits 1–3)

On IOMFB 26.x the handshake is setPowerState (A473) on the out-of-band channel. The T8132 words carry that channel 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, and each one gets an answer of the exact shape it expects.

The three commits are:

  1. stream-1 calls in the transport (calls nested in a stream-1 callback stay on stream 1);
  2. the callback shapes and answers;
  3. 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.

Power the display down while it is blanked (commit 4)

DPMS off (screen lock, idle blank, hyprctl dispatch dpms) used to only blank the display: iomfb_m4_blank() swaps to no surface, but the pipeline stays powered until the next frame.

  • Blank: after a successful blank, setPowerState(0), the same call system sleep makes.
  • Next frame: the first present after DPMS on sends setPowerState(1) before its swap, so the compositor's first frame brings the panel and backlight back. That's the path resume already uses.
  • Suspend while powered down: suspend skips its own setPowerState(0), and the display stays down after resume until the next present.
  • Refusal: a refused or failed transition fails the session, as in the sleep path. Before, that could only happen at system sleep; now it can happen at every blank and wake. The stress run below is the evidence that it holds up.
  • debugfs: iomfb_m4 gains display_off, off_cycles, and off_us / on_us (last and longest).

Cache a static frame (commit 5)

With a static image, the panel keeps being scanned out of memory 60 times a second. This registers a DCP idle-cache surface once no flip has happened for appledrm.idle_cache_ms, and leaves the cache on the next present, brightness change or blank.

  • The default is 1000 ms; 0 disables it, and the maximum is 60000.

Testing (J713)

On integration/aurora 41c4055 with the rest of the J713 PR stack (this series, the suspend-to-idle and USB series, #429, #432, #819/#820, #450, #531, #825):

  • DPMS off: the display powers down in 129 ms; DPMS on powers it up in 55 ms; error=0.
  • Idle cache: it keeps entering on a static screen.
  • Real suspend-to-idle (82 s): the display comes back, with frames flowing and the cache re-entering.

Earlier, on our J713 tree:

The device tests ran on our J713 tree, which has the J713 series this branch alone lacks (the display doesn't come up on integration/aurora alone yet). The commits are identical to the ones tested. Today, on integration/aurora 41c4055 with the rest of the J713 stack, it went through several real suspend-to-idle cycles of up to 352 s, each resuming to a working desktop.

System sleep:

  • Real s2idle from systemctl suspend (up to about 16 min asleep) and from the lid. Display and GPU come back (glmark2 630–642, unchanged), and so do Wi-Fi and audio.
  • pm_test=devices and pm_test=platform pass.

DPMS off, power: idle on battery, 2 s samples over 120 s per phase, medians. Each column is one run on one boot, so compare within a column.

Phase Without commit 4 With it
Display on, backlight off 3038 mW 2926 mW
DPMS off 2692 mW 1728 mW
Display on, backlight off 3017 mW 2914 mW
Saved by DPMS off 346 mW 1198 mW

DPMS stress: 200 blank/wake cycles through Omarchy's DPMS commands, checking debugfs after every step. All 200 passed, with no errors.

Last Longest
setPowerState(0) 128 ms 132 ms
setPowerState(1) 55 ms 56 ms

The power-down runs inside the commit that disables the CRTC, so that commit takes about 130 ms longer. Hyprland's DPMS dispatch still returns in 10–12 ms.

DPMS, other cases:

  • Brightness changed while blanked: takes effect with the first frame after wake.
  • Blank, then system sleep: suspend skips its own power-down, resumes with the display still down, and one key press brings it back.
  • Lid suspend: the display comes back on resume; Touch ID works before and after.

Idle cache: J713, battery, backlight 0, 180 s per phase, same boot.

  • Power: idle_cache_ms=0 draws 3266 mW, idle_cache_ms=1000 draws 2931 mW: −335 mW.
  • Entry and exit: it cached about 93 % of a 10 min static run. The frame stays on screen, and the first swap after leaving the cache takes 5–10 ms (ordinary swaps take 12–14 ms).
  • Re-entry: brightness changes, DPMS off/on and suspend-to-idle all exit the cache and re-enter it.
  • fbcon: the text console, which writes the scanout buffer without flips, updates live while cached.
  • Stress: a full-screen random-noise image caches without DART faults.
  • Errors: fence refusals roll back cleanly and are retried, and out-of-range values are rejected.

Host tests:

  • apple-dcp matches integration/aurora, with the same failures on the bare base. New cases cover stream-1 decoding and routing, and a present refused while powered off.
  • m4-dcp-errors passes, including its case where a failed display domain refuses maps.
  • m4-dpms-retirement, the host test of the production blank and present paths, gets stubs for the new calls in commits 4 and 5. It now also asserts that a blank powers the display down once, the next frame powers it back up, and every shown frame restarts the idle-cache countdown. It passes at both commits.

Checks (on integration/aurora 41c4055):

  • all commits are signed off;
  • checkpatch --strict is clean;
  • full arm64 build (Image, modules, DTBs) with the J713 config, with no warnings in the touched files;
  • CHECK_DTBS W=1 on all six T8132 DTBs gives the same output as the base;
  • run-host-tests: 132/133. The one failing group, apple-dcp, fails the same nine cases on the bare base and nothing more.

Known limits

  • A CRTC-only commit after DPMS on (no new framebuffer) leaves the display down until the next frame. Before commit 4 the same case left it blanked, so nothing that used to show a picture now doesn't.
  • Unbind, shutdown or kexec while the display is down leave it down. A reboot through iBoot resets it.
  • The firmware logs IOAVVideoInterface open failed after each power-up, with no effect.
  • Other T8132 machines take the same path, but only the J713 device tree enables this DCP today.

This series was developed and tested by Claude (Anthropic), working with me on the J713.

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>
With DPMS off, the J713 display was only blanked: the swap to no
surface stops presenting, but the display pipeline stays powered. A
blanked panel is the state in which system sleep powers the display
down with setPowerState(0), and the same works here.

After a successful blank, power the display down, and power it back up
with setPowerState(1) before the next present, so the first frame after
DPMS on brings the panel back. System sleep with the display already
down leaves it down and skips its own setPowerState(0); after resume the
next present powers it up. A refused transition fails the session, as
in system sleep. debugfs reports whether the display is down, how often
it went down, and how long the last and longest power-down and power-up
took.

On the J713, idle on battery with the backlight off, DPMS off saved
346 mW before this change (3.04 -> 2.69 W) and saves 1.20 W with it
(2.93 -> 1.73 W), each measured within one run. Over 200 blank/wake
cycles, power-down took 128-132 ms and power-up 55-56 ms.

Signed-off-by: Erik Bourget <erik@ebourget.net>
With a static image on the J713 panel, the display keeps scanning the
frame out of memory 60 times a second. The DCP can instead cache it
(IdleCachingMethod 2), but only after the host registers a cache surface:
a swap with intent 8 whose sole surface is a separate surface object.
Setting the method alone does nothing.

Once no flip has followed for idle_cache_ms (default 1000, 0 disables),
set the method and submit that swap with a dedicated, retained surface
of the panel geometry, which the host never writes. The DCP keeps
presenting the frame on screen, steps through its idle states (D117) and
asks for a fence (D578) on entry and on each later refresh of the cache,
granted for the surface this host registered; the next present,
brightness change or blank leaves the cache (D579, then state 0). The
surface stays registered across exits, so later entries resubmit it.
After a rollback without a flip, entry is retried after ten quiet
periods. The surface is allocated at startup; without it, or if the DCP
refuses the method, the cache stays off. debugfs reports the idle state,
open fences and the number of entries.

On the J713, idle on battery with the backlight off, this saves 335 mW
(3.27 -> 2.93 W) within one run. Brightness changes, DPMS off/on and
suspend-to-idle leave and re-enter the cache.

Signed-off-by: Erik Bourget <erik@ebourget.net>
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.

1 participant