From f0d5128b644bc03ed8532d1771430c91d8bca932 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 06:32:18 +0100 Subject: [PATCH 1/4] drm/apple: Backlight control on the H17G (M5) panel On H17G the system endpoint announces powerlog-service on channel 1 with only ep-has-desc-mgr and interface-id, so parse_epic_service_init() returned -ENOENT and the service carrying the mNits reports never bound. Match such announces by their name, as props-less announces are (only for the H17G method profile, so H17P is unchanged). H17G publishes no NITS property, so register the backlight device from the first powerlog report instead (140 nits at boot on the J813). H17G firmware ignores the DAC words (bl_unk/bl_value/bl_power at swap+0x32f) and takes the H17P brightness encoding in the same swap bytes (activation at +0x354, binary64 nits at +0x35e, ceiling pair at +0x36e/+0x376), which the unserialized 26.6 request already spans. Factor the H17P encoder to take the swap record and use it from the flush on a brightness update. Tested on the MacBook Air M5 (J813): apple-panel-bl 0..500 nits; writes of 100/300/140 give DCP "send_brightness" at those levels and matching actual_brightness, and the panel follows. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/afk.c | 14 +++++++++-- drivers/gpu/drm/apple/iomfb_h17p.c | 35 +++++++++++++++----------- drivers/gpu/drm/apple/iomfb_h17p.h | 2 ++ drivers/gpu/drm/apple/iomfb_template.c | 16 ++++++++++++ drivers/gpu/drm/apple/systemep.c | 4 +++ 5 files changed, 55 insertions(+), 16 deletions(-) diff --git a/drivers/gpu/drm/apple/afk.c b/drivers/gpu/drm/apple/afk.c index 688b2d2f5c622..08dc5cf8a242b 100644 --- a/drivers/gpu/drm/apple/afk.c +++ b/drivers/gpu/drm/apple/afk.c @@ -1380,13 +1380,23 @@ static void afk_recv_handle_init(struct apple_dcp_afkep *ep, u32 channel, } ret = parse_epic_service_init(&ctx, &epic_name, &epic_class, &epic_unit, ep->dcp->fw_compat == DCP_FIRMWARE_H17P); - if (ret) { + if (ret == -ENOENT && + ep->dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G) { + /* + * M5 (H17G) announces powerlog-service with only + * ep-has-desc-mgr and interface-id; match it by the + * announced name, as for props-less announces. + */ + epic_name = NULL; + epic_class = NULL; + } else if (ret) { dev_err(ep->dcp->dev, "AFK[ep:%02x]: failed to extract init props: %d\n", ep->endpoint, ret); return; + } else { + service_name = epic_class; } - service_name = epic_class; } else { service_name = name; } diff --git a/drivers/gpu/drm/apple/iomfb_h17p.c b/drivers/gpu/drm/apple/iomfb_h17p.c index 4f13cee71c872..1e0b68668a5b7 100644 --- a/drivers/gpu/drm/apple/iomfb_h17p.c +++ b/drivers/gpu/drm/apple/iomfb_h17p.c @@ -168,30 +168,37 @@ static u64 iomfb_nits_binary64_h17p(u32 nits) return value; } -void iomfb_encode_backlight_h17p(struct dcp_present_h17p *wire, u32 nits, - u32 maximum, bool update) +/* @swap is the swap record of a present; H17G sends it unserialized. */ +void iomfb_encode_backlight_swap_h17p(u8 *swap, u32 nits, u32 maximum, + bool update) { u64 ceiling = iomfb_nits_binary64_h17p(maximum); u64 unity = iomfb_nits_binary64_h17p(1); /* This profile does not use the older DAC/power words. */ - memset(wire->swap + 0x32f, 0, 0xd); - memset(wire->swap + 0x354, 0, 0x9a); + memset(swap + 0x32f, 0, 0xd); + memset(swap + 0x354, 0, 0x9a); /* The ceiling pair must remain valid even on a brightness-idle frame. */ - put_unaligned_le64(ceiling, wire->swap + 0x36e); - put_unaligned_le64(ceiling, wire->swap + 0x376); + put_unaligned_le64(ceiling, swap + 0x36e); + put_unaligned_le64(ceiling, swap + 0x376); if (!update) return; /* Linux brightness changes and soft DPMS use the same activation bytes. */ - put_unaligned_le32(1, wire->swap + 0x354); - wire->swap[0x358] = 1; - wire->swap[0x359] = 1; - wire->swap[0x35a] = 1; - put_unaligned_le16(1, wire->swap + 0x35c); - put_unaligned_le64(iomfb_nits_binary64_h17p(nits), wire->swap + 0x35e); - put_unaligned_le64(unity, wire->swap + 0x37e); - put_unaligned_le64(unity, wire->swap + 0x3e6); + put_unaligned_le32(1, swap + 0x354); + swap[0x358] = 1; + swap[0x359] = 1; + swap[0x35a] = 1; + put_unaligned_le16(1, swap + 0x35c); + put_unaligned_le64(iomfb_nits_binary64_h17p(nits), swap + 0x35e); + put_unaligned_le64(unity, swap + 0x37e); + put_unaligned_le64(unity, swap + 0x3e6); +} + +void iomfb_encode_backlight_h17p(struct dcp_present_h17p *wire, u32 nits, + u32 maximum, bool update) +{ + iomfb_encode_backlight_swap_h17p(wire->swap, nits, maximum, update); } static const struct dcp_method_entry dcp_methods[dcpep_num_methods] = { diff --git a/drivers/gpu/drm/apple/iomfb_h17p.h b/drivers/gpu/drm/apple/iomfb_h17p.h index 9880e82078178..0bb74295c382f 100644 --- a/drivers/gpu/drm/apple/iomfb_h17p.h +++ b/drivers/gpu/drm/apple/iomfb_h17p.h @@ -158,6 +158,8 @@ static inline struct dcp_apply_property_h17p dcp_opaque_x_property_h17p(void) }; } +void iomfb_encode_backlight_swap_h17p(u8 *swap, u32 nits, u32 maximum, + bool update); void iomfb_encode_backlight_h17p(struct dcp_present_h17p *wire, u32 nits, u32 maximum, bool update); diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index b649590127626..a40408a67af4a 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -3633,6 +3633,21 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G; #endif if (update_brightness) { +#if DCP_FW_H17(DCP_FW_VER) + /* + * H17G firmware ignores the DAC words and takes nits in the + * same swap bytes as H17P. Without a requested or reported + * level, send nothing rather than 0 nits. + */ + int nits = dcp->brightness.bl_dev ? + dcp->brightness.bl_dev->props.brightness : + dcp->brightness.nits; + + if (nits > 0) + iomfb_encode_backlight_swap_h17p((u8 *)&req->swap, nits, + dcp->brightness.maximum, + true); +#else #if DCP_FW_LAYOUT_14_7(DCP_FW_VER) req->swap.bl_update = 1; #else @@ -3640,6 +3655,7 @@ void DCP_FW_NAME(iomfb_flush)(struct apple_dcp *dcp, struct drm_crtc *crtc, stru #endif req->swap.bl_value = dcp->brightness.dac; req->swap.bl_power = 0x40; +#endif dcp->brightness.update = false; } #endif diff --git a/drivers/gpu/drm/apple/systemep.c b/drivers/gpu/drm/apple/systemep.c index 44e239c6016b0..b729ef50189d6 100644 --- a/drivers/gpu/drm/apple/systemep.c +++ b/drivers/gpu/drm/apple/systemep.c @@ -112,6 +112,10 @@ static int powerlog_report(struct apple_epic_service *service, enum epic_subtype } dcp->brightness.nits = mnits.millinits / 1000; + /* H17G publishes no NITS property; its first report registers the device. */ + if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G && + dcp_has_panel(dcp) && !dcp->brightness.bl_dev && dcp->brightness.maximum > 0) + schedule_work(&dcp->bl_register_wq); return 0; } From badf5817d31d0bf04f73397bb7a5200a5cd0a6b9 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 19:06:09 +0100 Subject: [PATCH 2/4] drm: apple: iomfb_h17p: answer H17G CoreAnalyticsSendEvent with an empty dict The M5's DCP (H17G) panicked with "app NMI" one hour after every boot, taking the display with it. Its crash log shows "OSDictionary deserialization failed": D114 CoreAnalyticsSendEvent was answered with trampoline_zero on H17G, so the reply starts with 0 instead of an OSDictionary. H17P already returns an empty dictionary ('d', 0x1004 bytes); D114's sizes are the same on H17G (0x1044/0x1004), so use that reply there too. DJ identified the analytics callback as the one-hour trigger. Tested on J813 (M5 MacBook Air): two boots run past one hour (3799 s, 4269 s+) without a DCP crash; before, every boot crashed at about 3600 s. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/iomfb_h17p.c | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_h17p.c b/drivers/gpu/drm/apple/iomfb_h17p.c index 1e0b68668a5b7..3b217b0fcb53f 100644 --- a/drivers/gpu/drm/apple/iomfb_h17p.c +++ b/drivers/gpu/drm/apple/iomfb_h17p.c @@ -315,9 +315,12 @@ static bool trampoline_get_frequency_h17p(struct apple_dcp *dcp, int tag, static bool trampoline_analytics_h17p(struct apple_dcp *dcp, int tag, void *out, void *in) { - if (dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G) - return trampoline_zero(dcp, tag, out, in); - + /* + * H17G (M5) needs the same empty dictionary. A zeroed reply fails the + * firmware's OSDictionary deserialization ("OSDictionary + * deserialization failed" in the DCP crash log) and the DCP panics + * with "app NMI" one hour after boot. + */ trace_iomfb_callback(dcp, tag, __func__); /* The dispatcher has validated the D114 reply size. */ memset(out, 0, 0x1004); From 72fbcc2aa337607aba12369d57b6111325c168ef Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sat, 31 Oct 2026 13:11:47 +0100 Subject: [PATCH 3/4] drm/apple: Blank the H17G internal panel instead of stopping the pipe On the M5 (H17G) internal panel, DPMS off stopped the DCP pipe: setPowerState(0) times out and the following setPowerState(1) is never answered ("wait for power timed out, connector will be broken"), so the panel stays frozen after a lid close or a compositor's output power off. Keep the pipe running instead, as H17P soft DPMS does: DPMS off submits a black, surface-free clear swap that also takes the backlight to 0 nits; DPMS on only requests a brightness update, and the next frame restores scanout and level. Tested on J813: fbdev blank/unblank and sway output power off/on cycles and a lid close/open; the firmware reports brightness 0 and back to the previous level, no timeouts. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/dcp.c | 23 ++++++++++++++++ drivers/gpu/drm/apple/iomfb_h17p.h | 1 + drivers/gpu/drm/apple/iomfb_template.c | 38 ++++++++++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 2c3404593e69a..2f2660982f599 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -3866,6 +3866,18 @@ static void __maybe_unused dcp_sleep(struct apple_dcp *dcp) #define DCP_BL_FALLBACK_DELAY msecs_to_jiffies(2000) +/* + * H17G (M5) internal panel: a pipe stop never comes back (see + * iomfb_blank_h17p()), so DPMS blanks the panel and keeps the pipe running. + */ +static bool dcp_h17g_panel(struct apple_dcp *dcp) +{ + return dcp->fw_compat == DCP_FIRMWARE_H17P && + dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17G && + dcp->connector_type == DRM_MODE_CONNECTOR_eDP && + dcp_has_panel(dcp); +} + static bool dcp_uses_soft_dpms(struct apple_dcp *dcp) { return (dcp_backlight_active(dcp) && dcp_has_panel(dcp)) || @@ -3954,6 +3966,12 @@ void dcp_poweron(struct platform_device *pdev) return; } + if (dcp_h17g_panel(dcp)) { + /* The pipe never stopped; the next swap restores the level. */ + dcp->brightness.update = true; + return; + } + if (dcp_is_typec_output(dcp)) { /* This CRTC now owns the current Type-C display session. */ scoped_guard(mutex, &dcp->hpd_mutex) @@ -4081,6 +4099,11 @@ void dcp_poweroff(struct platform_device *pdev) return; } + if (dcp_h17g_panel(dcp)) { + iomfb_blank_h17p(dcp); + return; + } + if (dcp->avep) av_service_disconnect(dcp); diff --git a/drivers/gpu/drm/apple/iomfb_h17p.h b/drivers/gpu/drm/apple/iomfb_h17p.h index 0bb74295c382f..112a4e002138d 100644 --- a/drivers/gpu/drm/apple/iomfb_h17p.h +++ b/drivers/gpu/drm/apple/iomfb_h17p.h @@ -158,6 +158,7 @@ static inline struct dcp_apply_property_h17p dcp_opaque_x_property_h17p(void) }; } +void iomfb_blank_h17p(struct apple_dcp *dcp); void iomfb_encode_backlight_swap_h17p(u8 *swap, u32 nits, u32 maximum, bool update); void iomfb_encode_backlight_h17p(struct dcp_present_h17p *wire, u32 nits, diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index a40408a67af4a..7a45dba123777 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -2355,6 +2355,44 @@ void DCP_FW_NAME(iomfb_poweroff)(struct apple_dcp *dcp) dev_info(dcp->dev, "dcp_poweroff() done\n"); } +#if DCP_FW_VERSION(26, 0, 0) <= DCP_FW_VER && DCP_FW_VER < DCP_FW_VERSION(27, 0, 0) +/* + * H17G (M5) internal panel DPMS off. A pipe stop does not come back: + * setPowerState(0) times out and the next setPowerState(1) is never + * answered. Blank instead and keep the pipe running: a black, + * surface-free swap that also takes the backlight to 0 nits. The next CRTC + * enable presents the scanout and the brightness again. + */ +void DCP_FW_NAME(iomfb_blank)(struct apple_dcp *dcp) +{ + struct DCP_FW_NAME(dcp_swap_start_req) swap_req = { 0 }; + typeof(DCP_FW_UNION(dcp->swap)) *swap = &DCP_FW_UNION(dcp->swap); + struct dcp_swap_cookie *cookie; + unsigned long ret; + + cookie = kzalloc(sizeof(*cookie), GFP_KERNEL); + if (!cookie) + return; + cookie->status = ~0U; + init_completion(&cookie->done); + kref_init(&cookie->refcount); + /* increase refcount to ensure the receiver has a reference */ + kref_get(&cookie->refcount); + + dcp_prepare_clear_swap(dcp, swap); + if (dcp_has_panel(dcp)) + iomfb_encode_backlight_swap_h17p((u8 *)&swap->swap, 0, + dcp->brightness.maximum, true); + + dcp_swap_start(dcp, false, &swap_req, dcp_swap_clear_started, cookie); + ret = wait_for_completion_timeout(&cookie->done, msecs_to_jiffies(500)); + if (!ret || READ_ONCE(cookie->status)) + dev_warn(dcp->dev, "%s: blank swap %s\n", __func__, + ret ? "failed" : "timed out"); + kref_put(&cookie->refcount, release_swap_cookie); +} +#endif + static void last_client_closed_sleep(struct apple_dcp *dcp, void *out, void *cookie) { #if DCP_FW_DESKTOP_26(DCP_FW_VER) From b263c19b290e2b5067ef76ecd9751ffb0a4e6d7a Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Mon, 2 Nov 2026 07:24:43 +0100 Subject: [PATCH 4/4] drm/apple: Offer only alpha formats on the H17G primary plane The H17G (M5) DCP firmware blends the fourth byte of an XRGB surface as alpha, and it ignores the opaque-X property as H17P does. A compositor that picks XRGB8888 leaves that byte to whatever its renderer stored: the GPU-rendered sway output on J813 showed blue and cyan text as black and grey while the pixels in memory were right. Offer the overlay formats, which all carry alpha, on the H17G primary plane, so that compositors write the byte. Treat the fbdev console the same way as on H17P: an XRGB console with X = 0 stays black. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/apple_drv.c | 11 ++++++++--- drivers/gpu/drm/apple/plane.c | 12 +++++++++++- drivers/gpu/drm/apple/plane.h | 1 + 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index d71c5c0370f2e..e7d8b28ca5b48 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -136,9 +136,12 @@ static int apple_drm_fbdev_probe(struct drm_fb_helper *helper, struct apple_dcp *dcp; dcp = platform_get_drvdata(to_apple_crtc(crtc)->dcp); - if (dcp->fw_compat == DCP_FIRMWARE_H17P && - dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) { - /* H17P applies the stored fourth byte as straight alpha. */ + if (dcp->fw_compat == DCP_FIRMWARE_H17P) { + /* + * H17P applies the stored fourth byte as straight alpha, + * the H17G (M5) profile too: an XRGB console with X = 0 + * stays black. ARGB makes fbcon write it opaque. + */ sizes->surface_bpp = 32; sizes->surface_depth = 32; break; @@ -751,6 +754,8 @@ static int apple_probe_per_dcp(struct device *dev, planes[zpos] = apple_plane_init(drm, 1U << num, surf, supports_l10r, supports_xrgb2101010, + dcp_data->hw.iomfb_method_profile == + DCP_IOMFB_METHODS_H17G, plane_type, interchange, supports_premultiplied); } diff --git a/drivers/gpu/drm/apple/plane.c b/drivers/gpu/drm/apple/plane.c index 87fecce18ee20..c019cdbc9844e 100644 --- a/drivers/gpu/drm/apple/plane.c +++ b/drivers/gpu/drm/apple/plane.c @@ -743,6 +743,7 @@ struct drm_plane *apple_plane_init(struct drm_device *dev, u32 iomfb_surf, bool supports_l10r, bool supports_xrgb2101010, + bool xrgb_is_alpha, enum drm_plane_type type, bool interchange, bool premultiplied) @@ -755,7 +756,16 @@ struct drm_plane *apple_plane_init(struct drm_device *dev, switch (type) { case DRM_PLANE_TYPE_PRIMARY: - if (supports_l10r && supports_xrgb2101010) { + if (xrgb_is_alpha) { + /* + * The firmware blends the X byte as alpha and ignores the + * opaque-X property: offer only formats with alpha, so that + * compositors write it instead of leaving whatever the + * renderer stored there. + */ + fmts = dcp_overlay_formats; + num_fmts = ARRAY_SIZE(dcp_overlay_formats); + } else if (supports_l10r && supports_xrgb2101010) { fmts = dcp_primary_formats; num_fmts = ARRAY_SIZE(dcp_primary_formats); } else if (supports_l10r) { diff --git a/drivers/gpu/drm/apple/plane.h b/drivers/gpu/drm/apple/plane.h index 69229ad6a5775..32d7da10a6a4f 100644 --- a/drivers/gpu/drm/apple/plane.h +++ b/drivers/gpu/drm/apple/plane.h @@ -37,6 +37,7 @@ struct drm_plane *apple_plane_init(struct drm_device *dev, u32 iomfb_surf, bool supports_l10r, bool supports_xrgb2101010, + bool xrgb_is_alpha, enum drm_plane_type type, bool interchange, bool premultiplied);