Skip to content

drm/apple: power-cycle the J313 panel the boot loader handed over - #321

Open
malik-na wants to merge 1 commit into
omacom:integration/aurorafrom
malik-na:fix/j313-backlight-cold-poweron
Open

malik-na wants to merge 1 commit into
omacom:integration/aurorafrom
malik-na:fix/j313-backlight-cold-poweron

Conversation

@malik-na

@malik-na malik-na commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

On a MacBook Air (M1, 2020, J313) with DCP firmware 13.5, the brightness keys, OSD and backlight sysfs all change after boot, but the panel stays at the boot level. One DPMS off/on fixes it until the next boot.

Cause

The boot loader leaves the panel on, so the first dcp_poweron() finds it already enabled (set_device_enabled_gated: current state: 1, requested state: 1) and only sets a mode. The firmware never runs its own panel power-on: no VideoInterfaceIOAV::power_on, no HPD plug, no LTM block load. It still takes the brightness carried in swaps and publishes it back (actual_brightness follows), but the panel doesn't change. A DPMS cycle runs the full power-on, and every brightness change after it reaches the panel.

This is the "writes apply only after a modeset" behaviour noted in #225; the 394-nit cap from that PR is unchanged.

Change

On J313 with DCP 13.5, when the boot loader handed the panel over lit (an available simple-framebuffer whose panel points at it), the first power-on cycles the pipe first, the way dcp_poweroff() does for an internal panel: cancel the swap watchdog, disconnect the AV service, _dcp_poweroff(), then the normal power-on. It logs power-cycling the panel handed over by the boot loader.

The framebuffer node outlives the handover, so the cycle is marked done when it runs and a later bind skips it; a bind that fails before the first power-on leaves it for the retry. Reloading appledrm resets the mark, so a reload can cycle again. Other panels and firmware versions are unchanged. The panel shows black briefly during boot.

Testing

Written for this tree; measured on a MacBook Air (M1, 2020, J313, DCP firmware 13.5) running linux-aurora 7.2.11 (aurora12), with appledrm.ko from this branch:

  • Normal boot: the handover check matches, the firmware goes set_device_enabled 1 -> 0 -> 1 and runs its full power-on, and brightness keys change the panel straight after boot. No driver warnings.
  • s2idle suspend/resume: the panel comes back through the full power-on, the boot cycle doesn't repeat, brightness works after resume.
  • Boot with the lid closed: m1n1 still hands the panel over and the firmware still sees it enabled, so the cycle runs; it completes in ~120 ms without timeouts, and brightness works after opening the lid.
  • Before the change, on the same kernel: backlight writes had no visible effect until one DPMS cycle.
  • checkpatch.pl --strict and W=1 clean; the apple-dcp selftests show the same results with and without this commit.
  • Not tested: kexec into a dark panel, other 13.5 machines, upstream Asahi on J313. The boot-test runs above were on the earlier revision; the last change only moves where the cycle is marked done.

@maralcbr

maralcbr commented Nov 1, 2026

Copy link
Copy Markdown
Member

Looks correct and well scoped: J313 + DCP 13.5 only, once per bind, reusing the same _dcp_poweroff() → iomfb_poweron_v13_3() path a DPMS cycle already takes (and iomfb_poweron sets brightness.update, so the level is re-pushed on the next swap). No blockers; a few notes:

  1. Not a full mirror of DPMS off. dcp_poweroff() also does cancel_delayed_work(&dcp->swap_watchdog_wq) and av_service_disconnect() before _dcp_poweroff(). Probably moot here (no swap has run yet, internal panel likely has no avep), but cancelling the watchdog too would make it a true copy of the cycle you measured.
  2. Fires even when the panel wasn't left on (lid-closed boot, kexec, module unbind/rebind — the flag is set again at bind). With the pipe already off, the clear swap and setPowerState(0) could time out (~1.5 s plus warnings), and "left on by the boot loader" would be wrong. Is there a firmware/boot signal to gate on? Otherwise maybe soften the log message.
  3. Is it really J313-only? Every 13.5 laptop boots with the panel left on. Do J293 / J314 / J316 on 13.5 log Could not find Backlight service too? And does upstream Asahi show this on J313? If not, it may be a regression in our 13.5 start sequence that this masks.
  4. Untested: suspend/resume is fine in principle since the flag only covers boot. A DCP crash/restart that leaves the panel on would hit the original bug again — acceptable as rare.
  5. Nits: the explanation is duplicated in dcp-internal.h and dcp.c; one line in the header is enough. The PR body ends with a "Generated with Claude Code" line that would land in the commit if squash-merged with the body.

@malik-na

malik-na commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Thanks, pushed 3a1194c for 1, 2 and 5:

  1. The cycle now cancels the swap watchdog and disconnects the AV service before _dcp_poweroff(), as dcp_poweroff() does for an internal panel.
  2. It is gated on the boot loader's handover: an available simple-framebuffer whose panel points at this panel (m1n1's node on this Air does), on the first bind after boot only, since the node outlives the handover. A rebind no longer cycles. The log now says "handed over by the boot loader". I couldn't check whether m1n1 still publishes the node for a lid-closed boot; kexec reuses the DT, so a kexec into a dark panel can still hit the timeouts.
  3. Only measured on J313. I have no J293/J314/J316 to check, and haven't compared with upstream Asahi on J313, so I can't rule out a regression in our 13.5 start sequence that this masks. One data point: the external DCP logs Could not find Backlight service too, so that line alone isn't the tell; set_device_enabled_gated: current state: 1, requested state: 1 on the first power-on is. Happy to compare against an Asahi kernel on this Air.
  4. Agreed.
  5. Header comment is one line now, and I've dropped the generated-by line from the PR body.

The revised module builds W=1 clean; I haven't boot-tested this revision yet and will post once I have.

@malik-na

malik-na commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Boot-tested 3a1194c on the J313 (DCP 13.5, linux-aurora 7.2.11): the handover check matched and logged power-cycling the panel handed over by the boot loader; the firmware went set_device_enabled 1 -> 0 -> 1 and ran its full power-on (IOAV power_on, HPD plug, LTM load, handle_stage1_enable), with no driver warnings. Brightness keys change the panel straight after boot.

@malik-na

malik-na commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Two more runs of 3a1194c on the J313:

  • Suspend/resume (s2idle, lid close): the DPMS off before suspend and the power-on after resume both run the firmware's full panel sequence; the cold-power-on cycle doesn't repeat. Brightness keys work after resume, no driver warnings.
  • Boot with the lid closed (logind: lid opened at 35 s): m1n1 still published its simple-framebuffer for the panel and the firmware still saw it enabled (set_device_enabled 1 -> 1), so the cycle ran. The power-off took ~120 ms with no clear-swap or setPowerState timeouts, and brightness works after opening the lid. So on this machine a lid-closed boot doesn't reach the pipe-already-off case; that case remains possible for kexec into a dark panel, untested.

@malik-na

malik-na commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

On point 3: I read m1n1 src/display.c. It power-cycles the internal display on t8103/t8112 machines without a notch when the system firmware is 15.0 or newer and the macOS stub firmware is older (the "Sequoia SFW" case), but only in the installed stage-1 m1n1, not a chainloaded one.

I checked what this Air reports in the device tree: asahi,system-fw-version 27.0, asahi,os-fw-version 13.5, asahi,m1n1-stage1-version v1.6.1-dirty (stage2 bc79acc4-dirty). By the source condition that is the case m1n1 cycles. The m1n1 boot log isn't kept anywhere I can read, so I haven't confirmed that stage 1 actually ran the cycle on this boot, nor that this build of v1.6.1 contains that code. So m1n1's own power cycle probably doesn't help here: Linux's DCP firmware starts fresh and still sees the panel already on, which is what the new cycle fixes. I have no J293/J314/J316 to compare against.

@maralcbr

maralcbr commented Nov 1, 2026

Copy link
Copy Markdown
Member

Thanks, the DPMS match and the handover check look right, and the extra test runs help. Four things on 3a1194c:

  1. A failed bind can disable the fix; please fix before merge. dcp_panel_handed_over() sets taken during dcp_comp_bind_pipe(), but later steps in that function can still fail, including -EPROBE_DEFER for the PIODMA iommu domain, the clocks and RTKit init. A later component failing the master bind should have the same effect. The retry then sees taken set, leaves panel_cold_poweron false, and the original brightness bug comes back with no log message. Setting the flag in dcp_poweron(), when the cycle actually runs, would fix it.
  2. Module reload (optional). taken is a static in the module, so reloading appledrm resets it while the framebuffer node stays, and the cycle runs again, possibly on a panel that's already off. At worst that's the bounded timeouts, so I'm fine leaving it, but "a rebind no longer cycles" only holds without a reload.
  3. PR body is out of date. It still quotes the old left on by the boot loader log line, lists suspend/resume as untested, and doesn't mention the simple-framebuffer check.
  4. Please squash. The second commit corrects the first, and merges here keep the branch commits, so they should land as one.

On a MacBook Air (M1, 2020, J313) with DCP 13.5 the boot loader leaves
the panel on.  The first dcp_poweron() then finds it enabled
(set_device_enabled 1 -> 1) and only sets a mode: the firmware does not
power up its video interface, assert HPD or load its LTM block, and logs
that it could not find the Backlight service.  Brightness swaps are
taken and the published level follows them, but the panel stays at the
boot level.  A DPMS off/on runs the firmware's full power-on sequence,
and every brightness change after it reaches the panel.

Give the J313 panel that cycle once, on DCP 13.5, when the boot loader
handed it over lit: an available simple-framebuffer whose "panel" points
at it.  The first power-on then cancels the swap watchdog, disconnects
the AV service and powers the pipe off, as dcp_poweroff() does for an
internal panel, before powering on.  The node outlives the handover, so
the cycle is marked done when it runs, and a later bind does not repeat
it; a bind that fails before the first power-on leaves it for the retry.
Other panels and firmware are unchanged.

Measured on a MacBook Air (M1, 2020, J313, DCP firmware 13.5) running
linux-aurora 7.2.11: brightness changes reach the panel straight after
boot, after s2idle suspend/resume, and after a boot with the lid closed,
where the boot loader still hands the panel over and the cycle completes
without timeouts.  Before, backlight writes had no visible effect until
one DPMS cycle.

Assisted-by: Claude:claude-opus-5-5
Signed-off-by: Naeem Malik <malik2015naeem@gmail.com>
@malik-na malik-na changed the title drm/apple: power-cycle the J313 panel on its first power-on drm/apple: power-cycle the J313 panel the boot loader handed over Nov 1, 2026
@malik-na
malik-na force-pushed the fix/j313-backlight-cold-poweron branch from 3a1194c to 36bef1a Compare November 1, 2026 11:50
@malik-na

malik-na commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Thanks, force-pushed as one commit, 36bef1a:

  1. Fixed: the mark is now set in dcp_poweron() when the cycle runs, not in dcp_comp_bind_pipe(), so a bind that fails or defers after the handover check leaves it for the retry.
  2. Left as is, and you're right that "a rebind no longer cycles" only holds without a reload; the PR body now says a reload can cycle again.
  3. PR body updated: the new log line, the simple-framebuffer check, and the suspend/resume and lid-closed results.
  4. Squashed. The earlier boot tests ran on 3a1194c; this revision only moves where the cycle is marked done.

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