Skip to content

drm/apple, PCI: apple: Studio Display brightness through DCP, and a PCIe-C replug fix - #15

Merged
iconidentify merged 4 commits into
iconidentify:custom/sepfrom
wesleygrimes:studio-display-backlight
Oct 4, 2026
Merged

iconidentify merged 4 commits into
iconidentify:custom/sepfrom
wesleygrimes:studio-display-backlight

Conversation

@wesleygrimes

@wesleygrimes wesleygrimes commented Oct 3, 2026 •

Copy link
Copy Markdown

Two fixes for the Apple Studio Display on T602x: its brightness, driven through DCP, and a PCIe-C tunnel that was left dead after some replugs (#8, comment).

Studio Display brightness

The Studio Display's USB HID brightness control (feature report 1, the one asdbctl/asdcontrol use) doesn't drive the panel on firmware 17.0. Writes are accepted and read back, but nothing changes, from Linux and from macOS. macOS sets brightness through DCP instead: with all five of the display's HID interfaces seized, DisplayServices still dims the panel, and the HID report stays frozen. The external framebuffer's IOMFBBrightnessLevel carries the level in nits.

DCP accepts the same swap backlight fields for the external display as for an integrated panel. bl_value is a signed 32-bit level, 0 = dimmest, 0x7fffffff = brightest; negative values clamp to the dimmest. The display reports the capability itself, in DisplayAttributes: SupportsBacklightControl, plus BacklightLuminanceMin/Max = 4/600 nits.

  • drm/apple: read SupportsBacklightControl from the display attributes: parses the flag per pipeline. It's reset for each new display and never set for an integrated panel.
  • drm/apple: drive the backlight of external displays:
    • Registers a BACKLIGHT_RAW device apple-<connector>-bl (e.g. apple-DP-1-bl) as a child of the DRM connector (/sys/class/drm/card2-DP-1/apple-DP-1-bl), so userspace can tell which output it dims.
    • It's registered while a capable display is connected and dropped on unplug, Type-C reroute or DCP crash, all from the connector's hotplug work. late_register/early_unregister tie it to the connector's sysfs lifetime.
    • The level is kept per connector, sent on every power-on (resume, DPMS) and saved/restored across reboots by systemd-backlight like any other backlight.
    • A new level is stored before brightness.update is set, and the swap clears update with xchg() before reading the level. A level written while a swap is being built goes out either with that swap or with the commit the write triggers.

No model list: any external display whose DCP attributes set SupportsBacklightControl gets a backlight. The Pro Display XDR and the 2026 Studio Display models probably qualify, but I have no way to test them.

Using it

Omarchy's brightness keys don't drive this backlight yet: for a Studio Display they go to asdcontrol and the USB HID report, which this firmware ignores. Until omacom/omarchy-mac#687 lands (see the follow-up below), use brightnessctl. No root is needed. The device is named after the connector the display lands on (apple-DP-1-bl, apple-DP-2-bl or apple-DP-3-bl, depending on the port), and only exists while the display is connected. With one external display, the pattern apple-DP-* matches it on any port:

brightnessctl -l -c backlight                  # list: apple-panel-bl (built-in) and apple-DP-N-bl
brightnessctl -d 'apple-DP-*' get              # current level, 0-1000
brightnessctl -d 'apple-DP-*' set 50%          # absolute
brightnessctl -d 'apple-DP-*' set 10%+         # brighter
brightnessctl -d 'apple-DP-*' set 10%-         # dimmer

The level is restored on replug and across reboots (systemd-backlight).

Testing (j416s, MacBook Pro 16" M2 Pro, Studio Display 2022, firmware 17.0)

Test Result
brightnessctl -d apple-DP-1-bl set N% as a normal user panel follows, dim to full
Integrated panel (apple-panel-bl) unaffected
Unplug device removed
Replug device back with the previous level
s2idle with the display attached display back instantly, level held, device not re-created
Unbind the DRM device while a loop writes brightness device removed cleanly, writer stops, no warnings
Ports right and "left 1" exercised; "left 2" registers, level not exercised

appledrm itself can't be rmmoded on this machine: the Type-C port drivers hold module references on the DCP Type-C muxes. Rebinding after an unbind fails (389c00000.dcp: Failed to boot RTKit: -62), independent of this change.

Open questions

  • Whether bl_value is linear in luminance is unknown, hence the unitless 0–1000 scale rather than nits.
  • enable_backlight_message_ap_gated schedules bl_update_wq, which is only initialised for DCPs with an integrated panel. It doesn't check for one, so an external DCP sending that callback would hit an uninitialised work item. I haven't seen it happen; it's pre-existing and not touched here.

Follow-up (separate)

Omarchy's brightness keys call omarchy-brightness-display, which picks asdcontrol (HID) for Apple displays. With this, it can use the focused connector's kernel backlight via brightnessctl instead. That's omacom/omarchy-mac#687, tested on this kernel: the brightness keys step the Studio Display with the OSD and no sudo prompt.

PCIe-C tunnel dead after a replug

Unplugging the display while its USB devices were runtime-suspended left that port's tunnel dead until reboot. The next cold init reported PORT_LINKSTS 0x81000200 instead of 0xa9000200, before link training had even started, and then link didn't come up. Power-cycling the display didn't help.

Cause: on devicetree platforms the ASPM core enables L1 by default on every link that supports it. That includes the link from the PCIe-C root port through the tunnel (0001:01:00.0: ASPM: default states L1). With USB idle, the link sits in L1, and pulling the cable then wedges the root port in a way neither the reset table nor cold init clears. PCI D-states aren't involved: the tunnel functions are already held in D0.

  • PCI/ASPM: Export pcie_aspm_remove_cap(): lets a modular host driver drop an ASPM state before the root port's link is configured. pcie-apple is tristate.
  • PCI: apple: Keep the PCIe-C tunnel link out of ASPM L1: in the existing per-function notifier, which is registered only for cold-initialized tunnels, the root port's L1 support is removed when the root port is added. That happens before the bus behind it is scanned, so L1 is never enabled on that link. Links inside the display or dock keep their ASPM states.

Same as macOS: in the Apple device tree macOS boots with, only internal ports carry pci-aspm-default (2 = L1 for the Wi-Fi/Bluetooth bridge, 0 for the SD card reader). The PCIe-C root ports (apciec* / pcic*-bridge) carry none, so macOS doesn't put these links in L1 either. Checked on a J716c running macOS 26.5.1. I haven't read the j416s's own device tree, and I haven't watched the link state on a running Mac.

Testing (j416s, Studio Display)

Test Before After
Replug after an unplug with all five USB devices runtime-suspended wedged, 6 of 6 link 0xa9000200, USB back, on all 3 ports
Same, with ASPM off at runtime (pcie_aspm.policy=performance) on the old kernel passes, 2 of 2 n/a
Replug while the camera streams passes
s2idle with the display attached link kept, no USB disconnect
Replug after that resume passes
Backlight after the fix (dim, full, level kept across replug) works

The cost: the root port's link through the tunnel no longer enters L1 while a display or dock is connected.

Open question: base M1 and M2. The fix covers the ports the kernel cold-initializes: M1 Pro/Max (T6000/T6001, untested on hardware) and M2 Pro/Max (T6020/T6021). The base M1 (T8103, apple,pciec-kernel-init) and base M2 (T8112) bring up their tunnels differently and aren't covered. ASPM enables L1 on their tunnel links too, so they might wedge the same way. If you have one: plug a Thunderbolt display or dock, let its USB devices runtime-suspend, unplug, and replug. If the cold init then reports a different PORT_LINKSTS and link didn't come up, the fix could be extended to every tunneled host.

DCP describes a connected display in its DisplayAttributes dictionary.
For external displays whose brightness DCP can set, such as the Apple
Studio Display, that includes SupportsBacklightControl alongside
BacklightLuminanceMin/Max (4 and 600 nits there).

Parse the flag and record it per pipeline, cleared for every new
display and never set for an integrated panel, which has its own
backlight path.  Nothing uses it yet.

Signed-off-by: Wes Grimes <wesgrimes@hey.com>
@wesleygrimes wesleygrimes changed the title drm/apple: Studio Display brightness through DCP (+ replug fix, WIP) drm/apple, PCI: apple: Studio Display brightness through DCP, and a PCIe-C replug fix Oct 3, 2026
@wesleygrimes
wesleygrimes force-pushed the studio-display-backlight branch from d868949 to 691f995 Compare October 3, 2026 22:09
The Apple Studio Display has a USB HID brightness control (VESA usage
page 0x82, feature report 1), but on firmware 17.0 the panel ignores
it, from Linux and from macOS alike.  macOS sets the brightness through
DCP instead: with every HID interface of the display seized, the
DisplayServices brightness API still dims the panel.

DCP accepts the same swap backlight fields for such a display as for an
integrated panel.  bl_value is then a signed 32-bit level, 0 for the
dimmest and S32_MAX for the brightest; negative values clamp to the
dimmest.  Whether the steps in between are linear in luminance is not
known, so the level is exposed on a unitless 0-1000 scale.

Register a BACKLIGHT_RAW device, "apple-<connector>-bl", for the
connector of a display that reports SupportsBacklightControl.  It is a
child of the connector's sysfs device, so userspace can tell which
output it belongs to.  The connector's hotplug work registers it when
such a display connects and drops it otherwise.  Disconnects reported
outside that work queue it, so registration always runs without the
caller's locks.  The late_register and early_unregister hooks bound it
to the connector's own lifetime.  The backlight core stops calling
update_status once unregistration returns, so a brightness write that
races an unplug is safe.

The level is kept per connector across replug and sent with the first
swap after every power-on.  A new level is stored before
brightness.update is set, and the swap clears update with xchg() before
reading the level.  A level written while a swap is being built thus
either goes out with that swap or leaves update set, so the commit that
the write triggers sends it.
Integrated panels keep their existing path.

Tested on j416s (MacBook Pro 16" M2 Pro) with a Studio Display on the
right and "left 1" ports: brightnessctl without root, unplug and replug
(level restored), s2idle, and unbinding the DRM device while brightness
writes race the teardown.  On "left 2" the device registers, but
the level was not exercised.  The integrated panel's backlight is
unaffected.

Signed-off-by: Wes Grimes <wesgrimes@hey.com>
Host controller drivers may know that a root port's link cannot use an
ASPM state safely even though the root port advertises it.  Export
pcie_aspm_remove_cap() so a modular controller driver can drop that
state before the link below the root port is configured, as quirks do
for built-in fixups.

Signed-off-by: Wes Grimes <wesgrimes@hey.com>
On devicetree platforms the ASPM core enables L1 by default on every
link that supports it, including the link from a PCIe-C root port
through the USB4 tunnel.  With the functions behind the tunnel idle, for
instance a Studio Display whose USB devices are runtime-suspended, that
link sits in L1.

Unplugging the cable while the link is in L1 wedges the root port.  Each
later cold init of that port reports PORT_LINKSTS 0x81000200 instead of
0xa9000200 before link training is even started, the link never comes
up, and only a reboot recovers the port.  Power-cycling the display does
not help, so the state is on the host.  Neither the port reset table nor
the rest of the cold-init sequence clears it.

Treat L1 as unsupported on the root port of a cold-initialized tunnel.
The root port is added before the bus behind it is scanned, so ASPM
never enables L1 on its link.  Links further down, inside the dock or
display, keep their ASPM states.

This matches the Apple device tree that macOS boots with: there, only
internal ports carry "pci-aspm-default" (L1 for the WLAN bridge, 0 for
the SD card reader), and the PCIe-C root ports carry none.  Checked on a
J716c running macOS 26.5.1.

Tested on j416s (MacBook Pro 16" M2 Pro) with a Studio Display:
- With L1 enabled, each of six replugs after an unplug with the
  display's USB devices runtime-suspended wedged the port.
- With ASPM disabled at runtime, two such replugs on the same port
  passed.
- With this change, the same replug passed on all three ports, as did a
  replug while the camera was streaming, s2idle with the display
  attached, and a replug after that resume.

Signed-off-by: Wes Grimes <wesgrimes@hey.com>
@wesleygrimes
wesleygrimes force-pushed the studio-display-backlight branch from 691f995 to 67cd2c7 Compare October 3, 2026 22:16
@wesleygrimes
wesleygrimes marked this pull request as ready for review October 3, 2026 22:21
@iconidentify
iconidentify merged commit 0436c3b into iconidentify:custom/sep Oct 4, 2026
iconidentify added a commit that referenced this pull request Oct 4, 2026
linux-aurora 7.1.12.aurora2-11.34, built from e508e10:
 - Apple Studio Display brightness through DCP
   (#15, Wesley Grimes), as dcp-<connector>-bl,
   sending nothing until a level is chosen;
 - the PCIe-C root port's link kept out of ASPM L1 on cold-initialized
   tunnels (M1/M2 Pro and Max), so unplugging an idle dock no longer
   leaves the port dead until reboot (#15);
 - review fixes: one hotplug event per unplug, the bl_update_wq guard,
   lenient attribute parsing, connector and DCP teardown lifetimes, and
   a parser allocation-failure fix.

Tested on a J293 (MacBook Pro 13" M1) in the lab: a monitor through a
USB-C to HDMI adapter (plug, unplug with one hotplug event, replug, DPMS
off/on, s2idle), a CalDigit TS4's display and USB, a Kensington dock with
a TV, and three reboots with panel brightness writes in flight, each with
no oops, warning or hung task at shutdown. The Studio Display itself and
the M1 Pro/Max dock path are untested here. Device trees and the kernel
config are unchanged from 11.33.

libfprint, aurora-touchid and m1n1-aurora are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FwdG63dRaMrRbYrUBkSa1i
Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com>
@cb2206

cb2206 commented Oct 4, 2026

Copy link
Copy Markdown

M1 Pro report for the PCIe-C replug fix: j314s (MacBook Pro 14" M1 Pro), LG UltraFine 5K on Thunderbolt 3, left port (701ac0000.cio). It works, with one caveat (last paragraph).

Build: 11.34 (custom/sep aac9f2360b03) built from source. My boot.bin still carries the 11.6 device tree, so I add the 11.32 t600x-pciec-dart-tunables.dtsi / apple,dma-range values at runtime when they're missing. No other PCIe changes.

Boot with the LG attached:

pci 0001:00:00.0: ASPM: Link Capabilities L1 treated as unsupported to avoid device defect
pci 0001:03:00.0: ASPM: default states L0s L1
port /soc/usb4-pcie-tunnel-0/pcie@730000000/pci@0,0 cold init done, status 0x3 link 0xa9000200

That gives 4K@60, the LG USB tree (hubs, USB Audio, camera, brightness controls), and no DART faults. 0001:03:00.0 is the Fresco xHCI behind the LG's own bridge, so its link inside the display keeps L1.

Unplug and replug with the USB3 side idle: before the unplug, the LG's USB3 hub, its downstream hub and the camera were runtime-suspended. The USB2 side (audio, HID controls) stayed active.

Step Time
Unplug (set cable state: 0) 72.63 s
tunnel reset acknowledgment timed out 73.16 s
Replug (set cable state: 1) 79.55 s
cold init done, status 0x3 link 0xa9000200, Link up 80.84 s (+1.3 s)
L1 removed from the root port again 81.90 s
Picture (modeset done) 86.94 s (+7.4 s)

USB and audio were back too. The reset-acknowledgment timeout on unplug shows up on every build here and is harmless.

Caveat: I never hit the 0x81000200 / link didn't come up failure on this M1 Pro before, including on builds without this fix and in idle replugs. So this shows the fix runs cleanly on t600x, not that t600x needed it. Not tested: the right and front-left ports, a dock, or s2idle on 11.34.

iconidentify pushed a commit that referenced this pull request Oct 8, 2026
ACIO returns -EAGAIN while its PCIe host is being populated or quiesced.
CD321x treats this as a failed cable and forces a complete disconnect on
the next status snapshot, resetting an otherwise unchanged USB4 session.

Keep the bounded fresh-read retry budget, but only request a forced
reconnect for non-deferred errors. Preserve any earlier genuine failure.
Notify the Type-C consumer after asynchronous PCIe activation succeeds so
an exhausted busy retry can read the current cable state again. Return
-EAGAIN when an allegedly populated host is not yet available, rather
than reporting false activation success.

Propagate partner-registration errors and add regression tests for busy
retries, completion after budget exhaustion and a busy retry following a
real failure.

[Chris Kearney: Ported onto custom/sep 1caa9fd (11.35) without code changes.
 The PR #10 cold-init acceptance and PR #15 follow-ups are retained.
 Review trial only: readiness and retry-budget concerns remain in the report.]
Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com>

(cherry picked from commit cd2525c)
(cherry picked from commit 39176aa921904549e1eeead5a2a36bc3bac6e67f)
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.

3 participants