From be289d9ca14dc4cc8c52b2b06906d9c19cf636f5 Mon Sep 17 00:00:00 2001 From: Wes Grimes Date: Sat, 3 Oct 2026 17:42:37 -0400 Subject: [PATCH 1/4] drm/apple: read SupportsBacklightControl from the display attributes 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 --- drivers/gpu/drm/apple/dcp-internal.h | 2 ++ drivers/gpu/drm/apple/iomfb_template.c | 6 +++++- drivers/gpu/drm/apple/parser.c | 6 +++++- drivers/gpu/drm/apple/parser.h | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 93b421ea3b0d22..019daa126c29d0 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -250,6 +250,8 @@ struct apple_dcp { /* Attributes of the connected display */ int width_mm, height_mm; + /* an external display whose backlight DCP can drive */ + bool ext_backlight; unsigned notch_height; diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index f05b601de1324b..7ff2f57946d273 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -592,14 +592,18 @@ static bool dcpep_process_chunks(struct apple_dcp *dcp, if (dcp->nr_modes == 0) dev_warn(dcp->dev, "TimingElements without valid modes!\n"); } else if (!strcmp(req->key, "DisplayAttributes")) { + bool backlight_control; + ret = parse_display_attributes(&ctx, &dcp->width_mm, - &dcp->height_mm); + &dcp->height_mm, &backlight_control); if (ret) { dev_warn(dcp->dev, "failed to parse display attribs\n"); return false; } + WRITE_ONCE(dcp->ext_backlight, + backlight_control && !dcp_has_panel(dcp)); dcp_set_dimensions(dcp); } diff --git a/drivers/gpu/drm/apple/parser.c b/drivers/gpu/drm/apple/parser.c index 3d520938a2900f..94fb1b486f127b 100644 --- a/drivers/gpu/drm/apple/parser.c +++ b/drivers/gpu/drm/apple/parser.c @@ -615,12 +615,14 @@ struct dcp_display_mode *enumerate_modes(struct dcp_parse_ctx *handle, } int parse_display_attributes(struct dcp_parse_ctx *handle, int *width_mm, - int *height_mm) + int *height_mm, bool *backlight_control) { int ret = 0; struct iterator it; s64 width_cm = 0, height_cm = 0; + *backlight_control = false; + dcp_parse_foreach_in_dict(handle, it) { char *key = parse_string(it.handle); @@ -630,6 +632,8 @@ int parse_display_attributes(struct dcp_parse_ctx *handle, int *width_mm, ret = parse_int(it.handle, &width_cm); else if (!strcmp(key, "MaxVerticalImageSize")) ret = parse_int(it.handle, &height_cm); + else if (!strcmp(key, "SupportsBacklightControl")) + ret = parse_bool(it.handle, backlight_control); else skip(it.handle); diff --git a/drivers/gpu/drm/apple/parser.h b/drivers/gpu/drm/apple/parser.h index 35f4e81e762125..60d69ab39b6322 100644 --- a/drivers/gpu/drm/apple/parser.h +++ b/drivers/gpu/drm/apple/parser.h @@ -107,7 +107,7 @@ struct dcp_display_mode *enumerate_modes(struct dcp_parse_ctx *handle, int height_mm, unsigned notch_height, bool internal); int parse_display_attributes(struct dcp_parse_ctx *handle, int *width_mm, - int *height_mm); + int *height_mm, bool *backlight_control); int parse_epic_service_init(struct dcp_parse_ctx *handle, const char **name, const char **class, s64 *unit); From b0f9e8d2eac247ef1164cb3e261211f22158be07 Mon Sep 17 00:00:00 2001 From: Wes Grimes Date: Sat, 3 Oct 2026 17:43:02 -0400 Subject: [PATCH 2/4] drm/apple: drive the backlight of external displays 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--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 --- drivers/gpu/drm/apple/apple_drv.c | 14 +++ drivers/gpu/drm/apple/connector.h | 15 +++ drivers/gpu/drm/apple/dcp-internal.h | 1 + drivers/gpu/drm/apple/dcp.c | 2 + drivers/gpu/drm/apple/dcp_backlight.c | 130 +++++++++++++++++++++++++ drivers/gpu/drm/apple/iomfb.c | 2 + drivers/gpu/drm/apple/iomfb_template.c | 9 ++ 7 files changed, 173 insertions(+) diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index d6a544dd1bac27..d0d18b3c0b8f8e 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -259,6 +259,16 @@ static void appledrm_connector_cleanup(struct drm_connector *connector) kfree(apple_connector); } +static int apple_connector_late_register(struct drm_connector *connector) +{ + return apple_connector_backlight_late_register(to_apple_connector(connector)); +} + +static void apple_connector_early_unregister(struct drm_connector *connector) +{ + apple_connector_backlight_early_unregister(to_apple_connector(connector)); +} + static const struct drm_connector_funcs apple_connector_funcs = { .fill_modes = drm_helper_probe_single_connector_modes, .destroy = appledrm_connector_cleanup, @@ -268,6 +278,8 @@ static const struct drm_connector_funcs apple_connector_funcs = { .detect = apple_connector_detect, .debugfs_init = apple_connector_debugfs_init, .oob_hotplug_event = apple_connector_oob_hotplug, + .late_register = apple_connector_late_register, + .early_unregister = apple_connector_early_unregister, }; /* @@ -325,6 +337,7 @@ static int apple_connector_create(struct drm_device *drm, return -ENOMEM; mutex_init(&connector->chunk_lock); + apple_connector_backlight_init(connector); drm_connector_helper_add(&connector->base, &apple_connector_helper_funcs); if (attach_fwnode) @@ -466,6 +479,7 @@ static int apple_probe_typec_ports(struct drm_device *drm, return -ENOMEM; mutex_init(&connector->chunk_lock); + apple_connector_backlight_init(connector); drm_connector_helper_add(&connector->base, &apple_connector_helper_funcs); diff --git a/drivers/gpu/drm/apple/connector.h b/drivers/gpu/drm/apple/connector.h index 4cb58502c1ef8c..1885527217c549 100644 --- a/drivers/gpu/drm/apple/connector.h +++ b/drivers/gpu/drm/apple/connector.h @@ -17,6 +17,11 @@ struct apple_connector; void dcp_hotplug(struct work_struct *work); void dcp_retrain_oob(struct apple_connector *connector); +void apple_connector_backlight_init(struct apple_connector *connector); +void apple_connector_backlight_sync(struct apple_connector *connector); +int apple_connector_backlight_late_register(struct apple_connector *connector); +void apple_connector_backlight_early_unregister(struct apple_connector *connector); + /* * How many DCP pipelines may offer a route to one Type-C port. The Type-C mux * class caps the mode-switch providers per connector (TYPEC_MUX_MAX_DEVS) and @@ -53,6 +58,16 @@ struct apple_connector { struct dcp_chunks timing_elements; struct dcp_chunks display_attributes; struct dcp_chunks transport; + + /* + * Backlight of an external display that DCP can drive, registered + * while such a display is connected. bl_lock serialises + * registration against the connector's sysfs lifetime. + */ + struct mutex bl_lock; + struct backlight_device *bl_dev; + bool bl_allowed; /* between late_register and early_unregister */ + int bl_level; /* kept across replug, applied on every power-on */ }; #define to_apple_connector(x) container_of(x, struct apple_connector, base) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 019daa126c29d0..3a2cad29373c09 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -347,6 +347,7 @@ void dcp_handle_hotplug_actions(struct apple_dcp *dcp, unsigned int action); int dcp_backlight_register(struct apple_dcp *dcp); int dcp_backlight_update(struct apple_dcp *dcp); +s32 dcp_ext_backlight_value(struct apple_dcp *dcp); bool dcp_has_panel(struct apple_dcp *dcp); #define DCP_AUDIO_MAX_CHANS 15 diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 4c1a6f85eb4d68..622ca21753efdd 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -2193,6 +2193,8 @@ static void disconnected_hpd_event(struct apple_connector *con) drm_edid_free(con->drm_edid); con->drm_edid = NULL; drm_kms_helper_connector_hotplug_event(&con->base); + /* drops the display's backlight, outside the caller's locks */ + schedule_work(&con->hotplug_wq); } } diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index a4a261c4c3c079..8e6a4cb681de84 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -5,12 +5,15 @@ #include #include #include +#include #include +#include #include #include #include "linux/jiffies.h" +#include "connector.h" #include "dcp.h" #include "dcp-internal.h" @@ -256,3 +259,130 @@ int dcp_backlight_register(struct apple_dcp *dcp) return 0; } + +/* + * External displays that report SupportsBacklightControl, such as the Apple + * Studio Display, take the same swap backlight fields as an integrated + * panel. bl_value is then a signed 32-bit level from 0 (dimmest) to S32_MAX + * (brightest); DCP converts it for the display. The backlight device belongs + * to the connector, so userspace can tell which output it dims. + */ +#define DCP_EXT_BL_MAX 1000 + +/* The bl_value for the display on this DCP, or -1 if it has no backlight. */ +s32 dcp_ext_backlight_value(struct apple_dcp *dcp) +{ + struct apple_connector *connector = READ_ONCE(dcp->connector); + + if (!connector || !READ_ONCE(dcp->ext_backlight)) + return -1; + + return div_u64((u64)READ_ONCE(connector->bl_level) * S32_MAX, + DCP_EXT_BL_MAX); +} + +static int dcp_ext_backlight_update_status(struct backlight_device *bd) +{ + struct apple_connector *connector = bl_get_data(bd); + struct platform_device *pdev; + struct apple_dcp *dcp; + + WRITE_ONCE(connector->bl_level, backlight_get_brightness(bd)); + + /* An unrouted port picks the level up when it powers on again. */ + pdev = READ_ONCE(connector->dcp); + if (!pdev) + return 0; + + dcp = platform_get_drvdata(pdev); + /* pairs with the xchg() in iomfb_flush: it sees the level or leaves update set */ + smp_store_release(&dcp->brightness.update, true); + + return dcp_backlight_update(dcp); +} + +static const struct backlight_ops dcp_ext_backlight_ops = { + .update_status = dcp_ext_backlight_update_status, +}; + +static void apple_connector_backlight_register(struct apple_connector *connector) +{ + struct backlight_properties props = { + .type = BACKLIGHT_RAW, + .max_brightness = DCP_EXT_BL_MAX, + .brightness = connector->bl_level, + }; + struct backlight_device *bd; + char name[32]; + + snprintf(name, sizeof(name), "apple-%s-bl", connector->base.name); + bd = backlight_device_register(name, connector->base.kdev, connector, + &dcp_ext_backlight_ops, &props); + if (IS_ERR(bd)) { + drm_warn(connector->base.dev, + "%s: failed to register backlight: %pe\n", + connector->base.name, bd); + return; + } + + connector->bl_dev = bd; +} + +static void apple_connector_backlight_unregister(struct apple_connector *connector) +{ + backlight_device_unregister(connector->bl_dev); + connector->bl_dev = NULL; +} + +void apple_connector_backlight_init(struct apple_connector *connector) +{ + mutex_init(&connector->bl_lock); + connector->bl_level = DCP_EXT_BL_MAX; +} + +/* + * Register the backlight while a display that supports it is connected, and + * drop it otherwise. Called from the connector's hotplug work. + */ +void apple_connector_backlight_sync(struct apple_connector *connector) +{ + struct platform_device *pdev; + bool want = false; + + mutex_lock(&connector->bl_lock); + + pdev = READ_ONCE(connector->dcp); + if (connector->bl_allowed && READ_ONCE(connector->connected) && pdev) { + struct apple_dcp *dcp = platform_get_drvdata(pdev); + + want = READ_ONCE(dcp->ext_backlight); + } + + if (want && !connector->bl_dev) + apple_connector_backlight_register(connector); + else if (!want && connector->bl_dev) + apple_connector_backlight_unregister(connector); + + mutex_unlock(&connector->bl_lock); +} + +int apple_connector_backlight_late_register(struct apple_connector *connector) +{ + mutex_lock(&connector->bl_lock); + connector->bl_allowed = true; + mutex_unlock(&connector->bl_lock); + + /* a display may have connected before the connector was registered */ + apple_connector_backlight_sync(connector); + + return 0; +} + +void apple_connector_backlight_early_unregister(struct apple_connector *connector) +{ + mutex_lock(&connector->bl_lock); + connector->bl_allowed = false; + if (connector->bl_dev) + apple_connector_backlight_unregister(connector); + mutex_unlock(&connector->bl_lock); +} diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index a56f9ee5c7d4ee..cb2477bf62a55a 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -292,6 +292,8 @@ void dcp_hotplug(struct work_struct *work) connector = container_of(work, struct apple_connector, hotplug_wq); + apple_connector_backlight_sync(connector); + pdev = READ_ONCE(connector->dcp); if (!pdev) { /* a Type-C port unrouted after this was queued */ drm_kms_helper_connector_hotplug_event(&connector->base); diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 7ff2f57946d273..900728e4cdc8a1 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -1490,6 +1490,15 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru req->swap.bl_value = dcp->brightness.dac; req->swap.bl_power = 0x40; dcp->brightness.update = false; + } else if (!dcp_has_panel(dcp) && xchg(&dcp->brightness.update, false)) { + /* read after clearing: a newer level sets update again and commits */ + s32 bl = dcp_ext_backlight_value(dcp); + + if (bl >= 0) { + req->swap.bl_unk = 1; + req->swap.bl_value = bl; + req->swap.bl_power = 0x40; + } } if (crtc_state->color_mgmt_changed) { From 07ac4e9daac0ff63c42bf9498ebeac08ec47d9d8 Mon Sep 17 00:00:00 2001 From: Wes Grimes Date: Sat, 3 Oct 2026 18:07:35 -0400 Subject: [PATCH 3/4] PCI/ASPM: Export pcie_aspm_remove_cap() 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 --- drivers/pci/pcie/aspm.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/pci/pcie/aspm.c b/drivers/pci/pcie/aspm.c index 925373b98dff0c..a69e4f525a88e0 100644 --- a/drivers/pci/pcie/aspm.c +++ b/drivers/pci/pcie/aspm.c @@ -1545,6 +1545,7 @@ void pcie_aspm_remove_cap(struct pci_dev *pdev, u32 lnkcap) lnkcap & PCI_EXP_LNKCAP_ASPM_L1 ? " L1" : ""); } +EXPORT_SYMBOL_GPL(pcie_aspm_remove_cap); static int pcie_aspm_set_policy(const char *val, const struct kernel_param *kp) From 67cd2c70ee49071197c88a4e79800c767555dc43 Mon Sep 17 00:00:00 2001 From: Wes Grimes Date: Sat, 3 Oct 2026 18:07:35 -0400 Subject: [PATCH 4/4] PCI: apple: Keep the PCIe-C tunnel link out of ASPM L1 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 --- drivers/pci/controller/pcie-apple.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/pci/controller/pcie-apple.c b/drivers/pci/controller/pcie-apple.c index 6a6e0e3785d8a7..0197b2cf0c2410 100644 --- a/drivers/pci/controller/pcie-apple.c +++ b/drivers/pci/controller/pcie-apple.c @@ -2191,6 +2191,11 @@ static int apple_pcie_tunnel_add_link(struct apple_pcie *pcie, * that suspends before a power-cycled display's hub reconnects never sees * the hub. Hold a runtime PM reference for as long as each function exists, * so that neither its driver nor power/control can suspend it. + * + * Keep the root port's link out of ASPM L1, too. Pulling the cable while + * the link idles in L1 wedges the root port: every later cold init leaves + * it unable to train, until the SoC is reset. The root port is added before + * the bus behind it is scanned, so ASPM never enables L1 on its link. */ static int apple_pcie_tunnel_pci_notify(struct notifier_block *nb, unsigned long action, void *data) @@ -2205,6 +2210,8 @@ static int apple_pcie_tunnel_pci_notify(struct notifier_block *nb, switch (action) { case BUS_NOTIFY_ADD_DEVICE: pm_runtime_get_noresume(&pdev->dev); + if (pci_pcie_type(pdev) == PCI_EXP_TYPE_ROOT_PORT) + pcie_aspm_remove_cap(pdev, PCI_EXP_LNKCAP_ASPM_L1); break; case BUS_NOTIFY_DEL_DEVICE: pm_runtime_put_noidle(&pdev->dev);