Skip to content

M3 Pro: panel off on DPMS, sleep with a dark external display - #283

Open
erykwieliczko wants to merge 7 commits into
integration/aurorafrom
feat/m3-pro-panel-dark-sleep
Open

erykwieliczko wants to merge 7 commits into
integration/aurorafrom
feat/m3-pro-panel-dark-sleep

Conversation

@erykwieliczko

Copy link
Copy Markdown
Collaborator

Split out of #274, as suggested in the review there: the panel's display-controller power-off and sleep with a dark external display. Stacked on #274 (its five commits come first here); only the last two are new.

Patches

  1. drm/apple: stop the J514S panel's display controller on DPMS off.
    • After the blank, A472 with the hardware-power selector turns off scanout and the panel; power on reverses it before the backlight is restored. The RTKit session, the IOMFB client and the mappings stay up.
    • DPMS only. System sleep still only blanks: s2idle cuts that power itself (1.354 W asleep with the controller stopped, 1.313 W without).
    • A refused power on is tried once more. If the controller stays off, the backlight stays off too, rather than lit over a dark panel, and the next power on tries again.
    • The new callback answers (D2, D591, D593, D574, D700) apply to the J514S/J516S panel only; other sessions handle them as before.
    • appledrm.panel_hw_off=0 restores blank-only.
  2. drm/apple: let the system sleep with a dark external display attached. Sleep is refused only while the external firmware is starting or a display is lit. Suspend already releases the HDMI link and keeps a Type-C tunnel, and resume brings the display back.

Power (J514S):

Testing (J514S, a kernel with #265 v3, #274 and this PR, on 63a0580a)

  • KUnit (aurora.kunitconfig): 1591 run, 1588 pass, 3 expected skips. New cases:
    • system sleep only blanks;
    • a power on refused once is retried;
    • a power on refused twice leaves the backlight off, and the next power on restores both.
  • 3/3 s2idle cycles with a USB-C TV attached (RTC wake, 25 s). The TV and the panel came back each time.
  • With call logging on, the DRM suspend path sent no A472. On Plasma, KWin turns the displays off before sleep and on after it, so the panel still gets one A472 off and on around each suspend, through DPMS.
  • check-commits.py and checkpatch are clean, and every commit builds alone with W=1.
  • Earlier, v3 of the same code:
    • HDMI 4/4 and USB-C 3/3 resumes with the TV attached brought back the picture, the mode and the DP audio sink;
    • HDMI's first link attempt after resume fails, and the existing retry brings it up about 2 s later.

Known, outside the kernel. KWin 6.8.1 can hold sleep for logind's whole InhibitDelayMaxSec if the pointer moves while it fades the screens out. It then suspends with the external display still lit, which patch 2 refuses. Fixed upstream in KWin 6a7b78b.

Not covered yet (wanted before patch 2 lands, per the #274 review):

  • unplug and replug while asleep;
  • two external displays;
  • J516S;
  • on Omarchy, hypridle locks before sleep but doesn't turn the displays off, so patch 2 changes nothing there until it does.

🤖 Generated with Claude Code

The controller sits in an always-on block and its parent interrupt is a
chained handler, so it keeps running through suspend. A wake-armed
peripheral interrupt therefore needs nothing more from the chip, but
without irq_set_wake or IRQCHIP_SKIP_SET_WAKE enable_irq_wake() fails
and no SPMI device can wake the system. Set IRQCHIP_SKIP_SET_WAKE.

Signed-off-by: Eryk Wieliczko <eryk@wieliczko.ninja>
The PMU that keeps the RTC counter also has a one-shot alarm, reached
through three more PMU registers and an SPMI peripheral interrupt that
can wake the system. Allow the alarm control, compare and mask cells
after the offset cell, all three or none, and the alarm interrupt and
wakeup-source.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Eryk Wieliczko <eryk@wieliczko.ninja>
The PMU that keeps the RTC counter also has a one-shot alarm: a 32-bit
compare value in whole seconds of the free-running counter (CLKM >> 15,
without the wall-clock offset), a mask, and a control byte with an
enable, a monitor bit and a latched event that is cleared by writing it
back. It raises an SPMI peripheral interrupt.

When the device tree describes the alarm registers as nvmem cells and
the interrupt, implement read_alarm, set_alarm and alarm_irq_enable,
report the alarm from a threaded handler, and make it a wake source if
the node says so. Without them the RTC stays alarm-less as before.

Updates of the control byte keep its other bits. Firmware can leave the
alarm armed or its event latched, so the probe disarms it before it
requests the interrupt.

Signed-off-by: Eryk Wieliczko <eryk@wieliczko.ninja>
Assisted-by: Claude:claude-opus-5-5
The Chapel PMIC's RTC alarm uses registers 0x2000 (control and event),
0x2008 (compare value) and 0x200e (interrupt mask) and raises peripheral
interrupt 2 of nub-spmi0. Make the controller an interrupt controller,
add the alarm cells to the PMIC layout and give them, the interrupt and
wakeup-source to the SMC RTC, so a timer can wake the system.

Signed-off-by: Eryk Wieliczko <eryk@wieliczko.ninja>
An acknowledged DISPEXT request keeps the PMP's external display state
up while no display is shown. The requests are asserted once at probe
(apple,always-on) and never withdrawn, so they also hold through
suspend-to-idle.

Give report entries marked apple,release-for-sleep noirq sleep
callbacks: by then every display processor has suspended, with its
displays off and its links released or idle. Withdraw such a request if
it is acknowledged, with one replacement write of the request entry as
macOS releases its display requests, and assert it again in
resume_noirq, before the processors resume. Without the request the
external display stays dark until reboot, so a re-assert the PMP does
not acknowledge in time is written again, three times at most. The
T6030 display gate marks the DISPEXT requests of the processors it
enables.

The PMP graph host test runs the new callbacks against its fake PMP:
an acknowledged request is withdrawn and asserted again, an
unacknowledged or unmarked one is left alone, and a re-assert
acknowledged late is written again.

Signed-off-by: Eryk Wieliczko <eryk@wieliczko.ninja>
Assisted-by: Claude:claude-opus-5-5
DPMS off only blanked the 14.7 internal panel, leaving its display
controller scanning out. Once the blank has been presented, send A472
with the hardware-power selector (+8) and a null output word (+11),
which stops scanout and the panel; power on sends the same with state 1
before the backlight is restored. The IOMFB client, the RTKit session
and every mapping are kept. On J514S this takes screen-off power from
2.86 W to 1.83 W.

System sleep still only blanks the panel: suspend-to-idle cuts that
power itself (1.354 W asleep with the controller stopped, 1.313 W
without), so an A472 off and on for every suspend would buy nothing.

A refused power on is tried once more. If the controller stays off, the
backlight stays off too rather than light a dark panel, and the next
power on tries again.

Answer the callbacks of those transitions on the panel: will_power_off
(D2) and the intent swap (D591) as notifications, powerUpDART (D574)
and set_kernel_power_assert (D700) with status 0, as the host keeps the
display powered, and enable_backlight_message_ap_gated (D593), which
the backlight restore after power on covers. Only the J514S/J516S panel
sends A472 so; other sessions handle these callbacks as before.

appledrm.panel_hw_off=0 restores the blank-only behaviour.

Signed-off-by: Eryk Wieliczko <eryk@wieliczko.ninja>
Assisted-by: Claude:claude-opus-5-5
A native external display processor (M3 Pro HDMI and USB-C) refused
system sleep while a display was attached at all, lit or not, and while
its link was up, on the grounds that resume would not bring the display
back. Unplugging the display was the only way to suspend.

Resume does bring it back. User space turns the displays off before
sleep. Suspend then releases the HDMI link (as an unplug would) and
keeps a Type-C tunnel. Resume samples HPD again on HDMI and powers the
Type-C CRTC back up as after DPMS off. On a J514S, with a 4K120 TV
attached over HDMI (4 cycles) and over USB-C (3 cycles), every resume
brought back the picture, the mode and the DP audio sink.

Refuse sleep only while the firmware is starting or a display is lit.
A lit display at prepare time means user space did not turn it off.

Signed-off-by: Eryk Wieliczko <eryk@wieliczko.ninja>
Assisted-by: Claude:claude-opus-5-5

@scottjones scottjones left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for splitting these out, it made the two patches much easier to read on their own. We went through 4e87561 and 71019b8 against our #274 notes and checked what reaches our other Macs (J613, J700, J414c).

Our #274 points:

  • Refused power on: answered. iomfb_v14_7.c:3414-3423 tries A472(1) once more, keeps the backlight off if it stays refused, and the next power on tries again, with v14_bl_power_on_refused covering it. What's left is that DRM thinks the CRTC is on while the panel is dark until something does another DPMS cycle. That's fine for now since it's logged.
  • Callback gating: answered. The D2/D591/D593/D574/D700 answers at iomfb_v14_7.c:1160-1178 are behind dcp_v14_board_nits(), which only J514S/J516S get (:329-332, :1714-1718). The external handler still runs first (:1106-1110), so DCPEXT doesn't change. A correction to our own note: the D574 -EINVAL on DCPEXT (:981) came in with 2210268 and is already in integration. It isn't from this PR.
  • A472 on every suspend: answered. :3456 skips it when pm_suspend_in_progress(), and v14_bl_sleep_blank_only covers it. pm_suspend_target_state is set before dpm_suspend_start (kernel/power/suspend.c:512), so the DRM helper's disable sees it.
  • Dark-display veto: partly. dcp.c:4846-4851 and iomfb_v14_7.c:2122 now refuse only a lit display, and prepare still refuses a lit one (dcp.c:4858-4876). The unplug/replug, two-display and J516S runs are still open, as your "Not covered yet" says.
  • t6030-display-gate.c:1464: answered.

A few new things:

  • On Omarchy, patch 1 runs at every idle screen-off: stock hypridle runs hyprctl dispatch dpms off at 330 s and dpms on in after_sleep_cmd. So the dark-panel case above is reachable there many times a day.
  • Patch 2 changes little on stock Omarchy today, but not quite for the reason in the description. With a display attached, logind's default HandleLidSwitchDocked=ignore means a lid close doesn't ask for sleep at all, and a Suspend from the menu happens with the display lit, which is still refused. We'd like Omarchy to behave like macOS here: on battery, closing the lid with a display attached sleeps; on the charger, it keeps driving the display. Omarchy would turn the displays off and then suspend, which is exactly the path patch 2 opens. So we're keen on it, and the cycle we'd most like covered is dpms off, suspend, resume, dpms on with a display attached, plus unplugging during sleep.
  • Patch 1 is on for J516S too, but :3452-3455 says "the one this was measured on" and only J514S was measured. Could you either run a DPMS cycle on a J516S or gate it to j514s until someone does?
  • In v14_bl_power_on_refused the retry is two dcp_poweron() calls in a row. Real DRM does off then on. A dcp_poweroff() in between, checking it only blanks and sends no A472 off since panel_off is already set (:3459), would test what actually happens.
  • Three DPMS cycles is thin for something Omarchy will hit many times a day. A longer hyprctl dispatch dpms off/on loop on J514S, plus a few dpms off, suspend, resume cycles on Hyprland with a display attached, would cover it.
  • Optional: the panel's D2/D591/D574 answers copy the external ones at :977-981, and gating them on the backlight-nits flag ties two unrelated features together. A small shared helper or a named predicate would read better.

Nothing changes on our other Macs. Patch 1 is behind dcp_v14_board_nits(), so J613/J615/J514c/J516c take the old blank-only path. Patch 2 only touches the native branch (external_native is T6030 only, dcp.c:4241), and J700 and J414c don't use the 14.7 boards.

For the RC: patch 2 should wait until after it. Patch 1 by itself looks RC-safe to us. It's narrow, can be turned off with appledrm.panel_hw_off=0, and has KUnit coverage, so it could go in once the longer DPMS run and the J516S check are done. If those don't happen in time, we'd rather take it with panel_hw_off defaulting to 0.

@chriskearney chriskearney left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed exact head 71019b8, treating 4e87561 and 71019b8 separately from the already-merged #274 commits. Both new patches apply with zero fuzz to current integration ba46b62; all four resulting changed files equal the PR head.

Panel DPMS: the sequencing is sensible. Blank first; require the old framebuffer retired and no recorded failure/crash before hardware off. Only acknowledged A472 updates panel_off. Power-on retries once, keeps the backlight off after two refusals, and a subsequent off/on cycle can retry. System suspend bypasses hardware off, and changing panel_hw_off to 0 after a successful off still permits the necessary power-on recovery. The added callback responses are limited to the J514S/J516S panel; the external callback handler runs first.

Independent bounded CPU checks: 22 controls passed using six exact extracted production functions with mocked RPC/blanking/kernel boundaries, including the refusal recovery through an intervening off/on cycle, opt-out, unsupported board, unretired framebuffer, crash/failure gates, and starting/lit/dark external sleep decisions. This checks control flow, not real firmware or DRM/PM concurrency. I did not boot a Mac or repeat KUnit locally.

I also checked the raw CI logs for run 40974853043: KUnit reports 1718 pass / 3 expected skips / 0 fail, including all 10 backlight-lifetime cases; compile checks both new commits. That run used the older synthetic merge 413af74 onto 2f72554, so it is not current-integration CI qualification.

No additional source blocker found. I agree with Scott's remaining qualification requests: the default-on panel path also reaches J516S without a reported J516S DPMS run; the repeated-DPMS and actual off/on refusal recovery checks still matter. Restricting enablement or defaulting off remains an alternative if those checks are unavailable. Keep the dark-external sleep change separate and pending the declared unplug/replug, two-display and J516S coverage and Scott's RC sequencing request. Existing J514S measurements are not J516S qualification, and the CPU controls do not close these hardware gaps. I am leaving this as a comment rather than approving the combined PR.

@aronvandepol

Copy link
Copy Markdown

Tested on a J516S (16" M3 Pro, apple,j516s, T6030, DCP 14.7): the DPMS-off panel power-down works here and saves 1.2 W with the screen off.

Build: packaged linux-aurora 7.2.11.aurora16 recipe at bed544b, plus this PR's two new commits (4e87561, 71019b8, with #274 already in the base) and #358, #370/#371, #389. 4e87561 needed fuzz 1 in iomfb_v14_7.c against bed544b (the context after iomfb_v14_7_flush() changed). The hunk lands right after that function. mesa-aurora 26.3.0_devel.20261107, Hyprland 0.57.2, Omarchy 4.5.0-3.

DPMS cycle (hl.dsp.dpms disable, then enable): the DCP log shows IOMFB: power on -> off, HPD removed and power_down_M3: disabling M3, then power off -> on and the backlight re-enabled. The desktop comes back and no kernel errors are logged. display controller did not power on never appears.

Power, session locked, backlight 120/500, wluma and Syncthing stopped, 3 × 60 s per state, SMC Total System Power / Heatpipe Power (on AC, so no battery-charge noise in the reading):

aurora16 (stock) + this PR Δ
Screen off, total 4.33 W 3.10 W −1.23 W
Screen off, SoC 1.21 W 1.14 W ≈ 0
Lock screen on, total 7.31 W 6.83 W −0.5 W (within noise)

Runs per state were within 0.05 W of each other with the screen off.

Not tested yet: system sleep with this kernel, and the dark-external-display path (no external display here).

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