From 47d53ce423030e351f5dd26100065766c07b53e2 Mon Sep 17 00:00:00 2001 From: Naeem Malik Date: Fri, 23 Oct 2026 02:12:09 +0530 Subject: [PATCH 01/28] drm/apple: stamp the J516C panel's 14.7 swaps On the J516C the 120 Hz panel presented at 60 fps: the 14.7 swap record is sent with its timestamps zeroed, and the firmware then completes at most one swap per 16.7 ms. Counter values (1 GHz on T6031) in the timestamp slots make the firmware time out on the first swap. Write the constant 120 in timestamp slots 0, 1 and 6 instead, a time that is never in the future, after the swap record is encoded. Add a swap_timestamps flag to the 14.x board records, set only in the J516C record, and stamp a swap only for the internal panel of such a record. The J514C (untested), the T6030 (J514S, J516S) and T8122 panels and the external processors, which have no board record, send zero timestamps, byte for byte as before. The counter timestamps of the IOMFB template path (VRR modes on a main display) are not used by the native 14.7 path and are unchanged. Measured on the J516C on this branch: kmscube (atomic) presents at 120 fps (120.03 on two boots) on the panel's 120 Hz mode, 3456x2160 with the 74 notch rows hidden, and the first swaps of a boot complete in 2.9 to 8.1 ms instead of 16.7 ms. KUnit (apple-dcp-v14-board): the J516C record stamps its internal panel's swaps; the external role, the J514C, T6030 and J613 records and no record do not; a stamped swap holds 120 in slots 0, 1 and 6 and is otherwise the encoded record, with and without a surface. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Naeem Malik [ryan: the J516C base EDID (d77c55666a80) is not carried, so the board record and header gain only swap_timestamps] (cherry picked from commit e3f69b773c8fa6711b41fdc3f96dbe382322b6b9) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/iomfb_v14_7.c | 8 +++ drivers/gpu/drm/apple/iomfb_v14_7_board.h | 14 +++++ drivers/gpu/drm/apple/iomfb_v14_7_swap.h | 17 +++++ drivers/gpu/drm/apple/v14-board-test.c | 77 ++++++++++++++++++++++- 4 files changed, 115 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 474841ff8d0ae..349410962127a 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -159,6 +159,7 @@ static const struct dcp_v14_board dcp_v14_board_j516c = { .panel_height = 2234, .promotion = true, .log_bringup = true, + .swap_timestamps = true, }; /* @@ -1541,6 +1542,11 @@ const char *iomfb_v14_7_board_name(const struct dcp_v14_board *board) return board->name; } +bool dcp_v14_board_stamps_swaps(const struct dcp_v14_board *board, bool external) +{ + return board && !external && board->swap_timestamps; +} + int iomfb_v14_7_probe(struct apple_dcp *dcp) { struct device *dev = dcp->dev; @@ -2609,6 +2615,8 @@ static int dcp_v14_swap(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova, if (!ret) { dcp_v14_encode_swap(swap, id, DCP_V14_BLACK, surface, iova, width, height, dst_y, DCP_V14_LAYER); + if (dcp_v14_board_stamps_swaps(v14->board, v14->external)) + dcp_v14_swap_timestamps(swap); ret = dcp_v14_call_guarded(v14, A(407), swap, DCP_V14_SWAP_SIZE, result, sizeof(result), id, dcp_v14_swap_admit, &admission); /* The swap status sits at byte 5 of the reply. */ diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_board.h b/drivers/gpu/drm/apple/iomfb_v14_7_board.h index 368dab49c2944..89d6a26ecb06c 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_board.h +++ b/drivers/gpu/drm/apple/iomfb_v14_7_board.h @@ -39,6 +39,13 @@ struct dcp_v14_board { * releases, so they are only set where tested. */ u32 ctm_set, ctm_get; + /* + * The panel's swaps carry the constant timestamp + * DCP_V14_SWAP_TIMESTAMP_VALUE in slots 0, 1 and 6. With zero + * timestamps this board's firmware completes at most one swap per + * 16.7 ms, so a 120 Hz panel presents at 60 fps. + */ + bool swap_timestamps; }; /* Whether the DCP node or the machine (as @ctx gives them) has @compat. */ @@ -59,4 +66,11 @@ const struct dcp_v14_board *dcp_v14_board_select(dcp_v14_compat_fn dcp_is, */ bool dcp_v14_board_admits(const struct dcp_v14_board *board, const char *uuid); +/* + * Whether a swap carries timestamps: only the internal panel of a board whose + * record asks for them. External processors (@external, no record) and every + * other board send zero timestamps. + */ +bool dcp_v14_board_stamps_swaps(const struct dcp_v14_board *board, bool external); + #endif /* __APPLE_IOMFB_V14_7_BOARD_H__ */ diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_swap.h b/drivers/gpu/drm/apple/iomfb_v14_7_swap.h index aab3351dd9eea..7fe32cabfae65 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_swap.h +++ b/drivers/gpu/drm/apple/iomfb_v14_7_swap.h @@ -11,6 +11,8 @@ #define DCP_V14_SURFACE_SIZE 0x22c /* Offsets in the swap record. Rectangles are x, y, w, h (u32 each). */ +#define DCP_V14_SWAP_TIMESTAMP(n) ((n) * 8) /* u64, slots 0 to 6 */ +#define DCP_V14_SWAP_TIMESTAMPS 7 #define DCP_V14_SWAP_ID 0x98 #define DCP_V14_SWAP_SURF_ID(p) (0x9c + (p) * 4) #define DCP_V14_SWAP_SRC(p) (0xac + (p) * 16) /* in the surface */ @@ -115,4 +117,19 @@ static inline void dcp_v14_encode_swap(u8 *swap, u32 id, u32 background, const u } } +/* + * The swap timestamps of a board that asks for them: the constant 120 in + * slots 0, 1 and 6, a time that is never in the future. Counter values + * (1 GHz on T6031) make the firmware time out on the first swap. Call after + * dcp_v14_encode_swap(), which zeroes the record. + */ +#define DCP_V14_SWAP_TIMESTAMP_VALUE 120 + +static inline void dcp_v14_swap_timestamps(u8 *swap) +{ + put_unaligned_le64(DCP_V14_SWAP_TIMESTAMP_VALUE, swap + DCP_V14_SWAP_TIMESTAMP(0)); + put_unaligned_le64(DCP_V14_SWAP_TIMESTAMP_VALUE, swap + DCP_V14_SWAP_TIMESTAMP(1)); + put_unaligned_le64(DCP_V14_SWAP_TIMESTAMP_VALUE, swap + DCP_V14_SWAP_TIMESTAMP(6)); +} + #endif /* __APPLE_IOMFB_V14_7_SWAP_H__ */ diff --git a/drivers/gpu/drm/apple/v14-board-test.c b/drivers/gpu/drm/apple/v14-board-test.c index 801ed2f623a4c..f9b84ab36a7cc 100644 --- a/drivers/gpu/drm/apple/v14-board-test.c +++ b/drivers/gpu/drm/apple/v14-board-test.c @@ -3,12 +3,16 @@ * The 14.x IOMFB board records: which record a DCP gets on which machine, * and which DCP firmware image a record admits. The J516C (T6031) record * admits only the T6030 image, whose IOMFB layouts were validated, and - * refuses every other; the existing records are unchanged. + * refuses every other; the existing records are unchanged. Which DCPs stamp + * their swaps: the internal panel of a T6031 record; the T6030 and T8122 + * panels and external processors keep zero timestamps, byte for byte. */ #include #include +#include #include "iomfb_v14_7_board.h" +#include "iomfb_v14_7_swap.h" #define T6030_DCP_IMAGE "DDF38191-93B3-324A-BC8F-643006F5AC82" #define J613_DCP_IMAGE "90F849E1-B422-367E-B389-50246F8DEC47" @@ -174,12 +178,83 @@ static void v14_board_unchanged(struct kunit *test) KUNIT_EXPECT_TRUE(test, known); } +/* Only the internal panel of the J516C record stamps its swaps. */ +static void v14_board_swap_timestamps(struct kunit *test) +{ + const struct dcp_v14_board *j516c_b, *j514c_b, *t6030_b, *j613_b; + bool known; + + j516c_b = board_for("apple,t6031-dcp", j516c, &known); + j514c_b = board_for("apple,t6031-dcp", j514c, &known); + t6030_b = board_for("apple,t6030-dcp", j516s, &known); + j613_b = board_for("apple,t8122-dcp", j613, &known); + KUNIT_ASSERT_TRUE(test, j516c_b && j514c_b && t6030_b && j613_b); + + KUNIT_EXPECT_TRUE(test, j516c_b->swap_timestamps); + KUNIT_EXPECT_FALSE(test, j514c_b->swap_timestamps); + KUNIT_EXPECT_FALSE(test, t6030_b->swap_timestamps); + KUNIT_EXPECT_FALSE(test, j613_b->swap_timestamps); + + KUNIT_EXPECT_TRUE(test, dcp_v14_board_stamps_swaps(j516c_b, false)); + /* The J514C, the external role, the T6030 (J514S, J516S) and T8122 panels, no record. */ + KUNIT_EXPECT_FALSE(test, dcp_v14_board_stamps_swaps(j514c_b, false)); + KUNIT_EXPECT_FALSE(test, dcp_v14_board_stamps_swaps(j516c_b, true)); + KUNIT_EXPECT_FALSE(test, dcp_v14_board_stamps_swaps(t6030_b, false)); + KUNIT_EXPECT_FALSE(test, dcp_v14_board_stamps_swaps(j613_b, false)); + KUNIT_EXPECT_FALSE(test, dcp_v14_board_stamps_swaps(NULL, false)); + KUNIT_EXPECT_FALSE(test, dcp_v14_board_stamps_swaps(NULL, true)); +} + +/* + * A stamped swap holds 120 in timestamp slots 0, 1 and 6 and is otherwise the + * encoded record, with or without a surface; an unstamped one keeps every + * slot zero. + */ +static void v14_swap_timestamps_encoding(struct kunit *test) +{ + const size_t end = DCP_V14_SWAP_TIMESTAMP(DCP_V14_SWAP_TIMESTAMPS); + u8 surface[DCP_V14_SURFACE_SIZE]; + unsigned int i, pass; + u8 *plain, *stamped; + + plain = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + stamped = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + KUNIT_ASSERT_TRUE(test, plain && stamped); + dcp_v14_encode_surface(surface, 3456 * 4, 3456, 2160, true); + + for (pass = 0; pass < 2; pass++) { + const u8 *s = pass ? surface : NULL; + + dcp_v14_encode_swap(plain, 0x2a, 0xff000000, s, 0x10000, pass ? 3456 : 0, + pass ? 2160 : 0, 74, 0); + dcp_v14_encode_swap(stamped, 0x2a, 0xff000000, s, 0x10000, pass ? 3456 : 0, + pass ? 2160 : 0, 74, 0); + dcp_v14_swap_timestamps(stamped); + + for (i = 0; i < DCP_V14_SWAP_TIMESTAMPS; i++) { + size_t at = DCP_V14_SWAP_TIMESTAMP(i); + u64 want = i == 0 || i == 1 || i == 6 ? 120 : 0; + + KUNIT_EXPECT_EQ_MSG(test, get_unaligned_le64(plain + at), 0, + "surface %u: unstamped slot %u", pass, i); + KUNIT_EXPECT_EQ_MSG(test, get_unaligned_le64(stamped + at), want, + "surface %u: slot %u", pass, i); + } + /* The rest of the record, from the word after slot 6, is unchanged. */ + KUNIT_EXPECT_EQ(test, + memcmp(plain + end, stamped + end, DCP_V14_SWAP_SIZE - end), 0); + KUNIT_EXPECT_EQ(test, get_unaligned_le32(stamped + DCP_V14_SWAP_ID), 0x2a); + } +} + static struct kunit_case v14_board_cases[] = { KUNIT_CASE(v14_board_j516c), KUNIT_CASE(v14_board_j514c), KUNIT_CASE(v14_board_t6031_other_boards), KUNIT_CASE(v14_board_j516c_image), KUNIT_CASE(v14_board_unchanged), + KUNIT_CASE(v14_board_swap_timestamps), + KUNIT_CASE(v14_swap_timestamps_encoding), {} }; From 279f5f641cc1bcc1284cfabc8a377aba4416cfcd Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+iconidentify@users.noreply.github.com> Date: Thu, 22 Oct 2026 16:04:47 -0700 Subject: [PATCH 02/28] drm/apple: port the scoped T6030 14.7 panel backlight Port the SF 44fe00e5eb58 panel-level implementation and its 4b318c41 fixture-lifetime correction onto PR177's current shared backlight API. Keep panel_nits on the T6030 board record, with separate immutable root machine admission for J514S and J516S. Other 14.7 boards and external pipes keep their existing level encoding and policy behavior. Register the requested default without sending it. Keep the loader's unknown level until userspace requests brightness, and report actual brightness as unknown until a successful present carries a level. Preserve the H17P fade/default behavior and its soft-DPMS dispatch; the 14.7 panel uses its own known-level-only blank and restore hooks. Use the existing v14 RPC lock to serialize a swap with publication of its active framebuffer and copied surface. Brightness work holds that lock and the active framebuffer reference throughout its present. Close the queue gate under that lock, then cancel outside it. Permanent teardown also publishes quiescing, drains shared registration/update work, and unregisters the class device while its CRTC is still valid. Retain the original KUnit suite and add actual present/publication thread interleavings, running-work suspend, real DPMS dispatch, failed-submit actual-level checks, and shared registration/unbind controls. Static stubs replace only the RPC boundary and a one-shot publication pause; the latter continues through the real publication body. Based-on: 44fe00e5eb58d54baf1cc9ad607293e774bfed44 Based-on: 4b318c4140e83e44033168836fe8cf4f76e16d80 Based-on: omacom/linux-aurora PR148 commit 02524534c6b8 (DjDeveloperr) Generated-by: LLM Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com> [ryan: resolved onto the 7.3 shared backlight: the T8132 (iomfb_is_m4) panel stays active, configurable and registrable as before, the desktop 26.x readback is kept, and soft DPMS excludes only the 14.7 panel (which has its own hooks) instead of every non-H17P panel; the swap keeps the J516C timestamps before the level block] (cherry picked from commit 378d88cdee47eff60eca2782c06d132a7011c2af) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp.c | 10 +- drivers/gpu/drm/apple/dcp_backlight.c | 54 +- drivers/gpu/drm/apple/dcp_backlight.h | 8 + drivers/gpu/drm/apple/iomfb_v14_7.c | 341 ++++++++++- drivers/gpu/drm/apple/iomfb_v14_7.h | 11 + drivers/gpu/drm/apple/iomfb_v14_7_backlight.h | 98 +++ .../iomfb_v14_7_backlight_lifetime_test.c | 365 ++++++++++++ .../drm/apple/iomfb_v14_7_backlight_test.c | 562 ++++++++++++++++++ drivers/gpu/drm/apple/iomfb_v14_7_board.h | 2 + drivers/gpu/drm/apple/iomfb_v14_7_swap.h | 51 ++ 10 files changed, 1471 insertions(+), 31 deletions(-) create mode 100644 drivers/gpu/drm/apple/iomfb_v14_7_backlight.h create mode 100644 drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c create mode 100644 drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 7e4f83b7240b1..2685a136710fc 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -3875,7 +3875,9 @@ static void __maybe_unused dcp_sleep(struct apple_dcp *dcp) static bool dcp_uses_soft_dpms(struct apple_dcp *dcp) { - return (dcp_backlight_active(dcp) && dcp_has_panel(dcp)) || + /* The 14.7 panel has its own known-level blank and restore hooks. */ + return (!iomfb_v14_7_panel_nits(dcp) && + dcp_backlight_active(dcp) && dcp_has_panel(dcp)) || (dcp->fw_compat == DCP_FIRMWARE_H17P && dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G && dcp->connector_type == DRM_MODE_CONNECTOR_eDP); @@ -4183,7 +4185,7 @@ void dcp_backlight_takeover_registered(struct platform_device *pdev) schedule_work(&dcp->bl_register_wq); } -static void dcp_work_register_backlight(struct work_struct *work) +VISIBLE_IF_KUNIT void dcp_work_register_backlight(struct work_struct *work) { int ret; struct apple_dcp *dcp; @@ -4191,7 +4193,7 @@ static void dcp_work_register_backlight(struct work_struct *work) dcp = container_of(work, struct apple_dcp, bl_register_wq); mutex_lock(&dcp->bl_register_mutex); - if (dcp->brightness.bl_dev) + if (READ_ONCE(dcp->quiescing) || dcp->brightness.bl_dev) goto out_unlock; /* try to register backlight device, */ @@ -6292,6 +6294,7 @@ static int dcp_platform_suspend(struct device *dev) return -EBUSY; if (READ_ONCE(dcp->ext_dead)) return 0; + iomfb_v14_7_backlight_suspend(dcp); /* Serialize PM against explicit bring-up, including queued startup work. * Before a startup attempt the device remains suspendable. After any @@ -6375,6 +6378,7 @@ static int dcp_platform_resume(struct device *dev) dcp->link_recovery_tries = 0; dcp_release_after_sleep(dcp); + iomfb_v14_7_backlight_resume(dcp); dcp_enable_typec_work(dcp); /* The HDMI output's PHY and crossbar may have lost their setup in sleep. */ if (dcp->external_native) diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index 36f3ed466185d..5c808438d5df6 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -16,6 +16,7 @@ #include "connector.h" #include "dcp.h" #include "dcp-internal.h" +#include "iomfb_v14_7.h" #define MIN_BRIGHTNESS_PART1 2U #define MAX_BRIGHTNESS_PART1 99U @@ -93,14 +94,33 @@ static u32 brightness_part2[] = { #define DCP_BL_FADE_MS 250U #define DCP_BL_FADE_STEP_MS 16U +static bool dcp_backlight_presents_nits(struct apple_dcp *dcp) +{ + return (dcp->fw_compat == DCP_FIRMWARE_H17P && + dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) || + iomfb_v14_7_panel_nits(dcp); +} + bool dcp_backlight_active(struct apple_dcp *dcp) { - return ((dcp->fw_compat == DCP_FIRMWARE_H17P && - dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) || - iomfb_is_m4(dcp)) && + return (dcp_backlight_presents_nits(dcp) || iomfb_is_m4(dcp)) && READ_ONCE(dcp->backlight.state.ready); } +bool dcp_backlight_level_known(struct apple_dcp *dcp) +{ + unsigned long flags; + bool known; + + /* Only a panel running the policy initialised its lock and state. */ + if (!dcp_backlight_active(dcp)) + return false; + spin_lock_irqsave(&dcp->backlight.lock, flags); + known = dcp->backlight.state.ready && dcp->backlight.state.level_known; + spin_unlock_irqrestore(&dcp->backlight.lock, flags); + return known; +} + static bool dcp_backlight_desktop(struct apple_dcp *dcp) { return dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && @@ -228,11 +248,17 @@ static int dcp_get_brightness(struct backlight_device *bd) return -ENODEV; if (dcp_backlight_active(dcp) || dcp_backlight_desktop(dcp)) { + /* + * The 14.7 panel reports no actual level until a present + * carried one: before that it shows the boot loader's level, + * which Linux does not know. + */ spin_lock_irqsave(&dcp->backlight.lock, flags); if (dcp_backlight_desktop(dcp)) actual = dcp_bl_actual(&dcp->backlight.state); else - actual = dcp->backlight.state.actual; + actual = dcp_bl_reported_actual(&dcp->backlight.state, + iomfb_v14_7_panel_nits(dcp)); spin_unlock_irqrestore(&dcp->backlight.lock, flags); return actual; } @@ -249,7 +275,7 @@ int dcp_backlight_configure(struct apple_dcp *dcp, u32 maximum, int ret; if ((dcp->fw_compat != DCP_FIRMWARE_H17P && - !iomfb_is_m4(dcp)) || + !iomfb_is_m4(dcp) && !iomfb_v14_7_panel_nits(dcp)) || !dcp_has_panel(dcp) || !dcp->crtc || !kick) return -EINVAL; @@ -269,7 +295,7 @@ int dcp_backlight_configure(struct apple_dcp *dcp, u32 maximum, dcp_bl_jump(&dcp->backlight.state); dcp->backlight.kick = kick; } else if (!ret) { - dcp->backlight.state.fade = true; + dcp->backlight.state.fade = !iomfb_v14_7_panel_nits(dcp); dcp->backlight.kick = kick; } } @@ -655,8 +681,7 @@ int dcp_backlight_update(struct apple_dcp *dcp) dcp_backlight_kick(dcp); return 0; } - if (dcp->fw_compat == DCP_FIRMWARE_H17P && - dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) + if (dcp_backlight_presents_nits(dcp)) return -ENODATA; /* @@ -744,6 +769,12 @@ static const struct backlight_ops dcp_backlight_ops = { .controls_device = dcp_backlight_controls_device, }; +static const struct backlight_ops dcp_backlight_ops_v14 = { + .get_brightness = dcp_get_brightness, + .update_status = dcp_set_brightness, + .controls_device = dcp_backlight_controls_device, +}; + int dcp_backlight_register(struct apple_dcp *dcp) { struct device *dev = dcp->dev; @@ -756,9 +787,7 @@ int dcp_backlight_register(struct apple_dcp *dcp) .scale = BACKLIGHT_SCALE_LINEAR, }; props.max_brightness = min(dcp->brightness.maximum, MAX_BRIGHTNESS_PART2 - 1); - if (((dcp->fw_compat == DCP_FIRMWARE_H17P && - dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G) || - iomfb_is_m4(dcp)) && + if ((dcp_backlight_presents_nits(dcp) || iomfb_is_m4(dcp)) && !dcp_backlight_active(dcp)) return -ENODATA; if (dcp_backlight_active(dcp)) { @@ -804,7 +833,8 @@ int dcp_backlight_register(struct apple_dcp *dcp) } bl_dev = devm_backlight_device_register(dev, "apple-panel-bl", dev, dcp, - &dcp_backlight_ops, &props); + iomfb_v14_7_panel_nits(dcp) ? + &dcp_backlight_ops_v14 : &dcp_backlight_ops, &props); if (IS_ERR(bl_dev)) return PTR_ERR(bl_dev); diff --git a/drivers/gpu/drm/apple/dcp_backlight.h b/drivers/gpu/drm/apple/dcp_backlight.h index 0540c87d28108..cc2d321fb1396 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.h +++ b/drivers/gpu/drm/apple/dcp_backlight.h @@ -309,6 +309,13 @@ static inline bool dcp_bl_complete(struct dcp_backlight_state *state, return true; } +static inline int dcp_bl_reported_actual(const struct dcp_backlight_state *state, bool strict) +{ + if (strict && !state->level_known) + return -ENODATA; + return state->actual; +} + /* Three deferred retries per user request; a permanent rejection stays dirty. */ static inline unsigned int dcp_bl_retry_delay(const struct dcp_backlight_state *state) { @@ -376,6 +383,7 @@ void dcp_backlight_desktop_finish(struct apple_dcp *dcp, u32 id, * Configure before registration and keep the callback valid until teardown. */ bool dcp_backlight_active(struct apple_dcp *dcp); +bool dcp_backlight_level_known(struct apple_dcp *dcp); int dcp_backlight_takeover(struct apple_dcp *dcp, u32 millinits); int dcp_backlight_configure(struct apple_dcp *dcp, u32 maximum, bool inherited_valid, u32 inherited, diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 349410962127a..e8ec2f265ff88 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -40,6 +40,7 @@ #include "dcp-lifecycle.h" #include "iomfb_internal.h" #include "iomfb_v14_7.h" +#include "iomfb_v14_7_backlight.h" #include "iomfb_v14_7_board.h" #include "iomfb_v14_7_link.h" #include "iomfb_v14_7_swap.h" @@ -102,6 +103,7 @@ static const struct dcp_v14_board dcp_v14_board_t6030 = { .firmware_uuid = DCP_V14_FIRMWARE_UUID, .handoff = "apple,t6030-handoff", .promotion = true, + .panel_nits = true, }; /* @@ -288,6 +290,18 @@ struct apple_dcp_v14 { struct drm_framebuffer *active_fb; u64 swaps; u64 swap_ns_max; + + /* Immutable probe admission: only the J514S/J516S internal panel. */ + bool panel_nits; + /* Copy of the last accepted surface; active_fb pins its DMA under lock. */ + u8 shown_surface[DCP_V14_SURFACE_SIZE]; + u64 shown_iova; + u32 shown_width, shown_height, shown_dst_y; + struct delayed_work backlight_work; + spinlock_t bl_gate_lock; + enum dcp_v14_bl_gate bl_gate; + u32 nits_sent; + u64 nits_presents, brightness_presents, nits_refused; }; /* @@ -300,6 +314,134 @@ static bool dcp_v14_panel_session; static int dcp_v14_callback(void *cookie, u32 tag, const void *input, u32 in_size, void *output, u32 out_size); +static void dcp_v14_backlight_work(struct work_struct *work); + +/* The internal panel of a board whose swap level block was validated (T6030). */ +static bool dcp_v14_board_nits(const struct apple_dcp_v14 *v14) +{ + return !v14->external && v14->panel_nits && v14->board && v14->board->panel_nits; +} + +bool iomfb_v14_7_panel_nits(struct apple_dcp *dcp) +{ + return dcp_is_v14_7_panel(dcp) && dcp->v14 && dcp_v14_board_nits(dcp->v14); +} + +static enum dcp_v14_bl_gate dcp_v14_bl_gate_get(struct apple_dcp_v14 *v14) +{ + enum dcp_v14_bl_gate gate; + unsigned long flags; + + spin_lock_irqsave(&v14->bl_gate_lock, flags); + gate = v14->bl_gate; + spin_unlock_irqrestore(&v14->bl_gate_lock, flags); + return gate; +} + +/* Queues a brightness-only present, unless the gate is shut. */ +static void dcp_v14_bl_queue(struct apple_dcp_v14 *v14, unsigned long delay) +{ + unsigned long flags; + + spin_lock_irqsave(&v14->bl_gate_lock, flags); + if (dcp_v14_bl_may_queue(v14->bl_gate)) + mod_delayed_work(system_dfl_wq, &v14->backlight_work, delay); + spin_unlock_irqrestore(&v14->bl_gate_lock, flags); +} + +/* + * Shuts the gate, until resume or (@for_good, unbind and remove) for good, + * then drains the work. The RPC lock waits for a running present; the + * gate lock excludes late queueing. Cancellation runs outside the RPC lock. + */ +static void dcp_v14_bl_shut(struct apple_dcp_v14 *v14, bool for_good) +{ + unsigned long flags; + + mutex_lock(&v14->lock); + spin_lock_irqsave(&v14->bl_gate_lock, flags); + v14->bl_gate = for_good ? DCP_V14_BL_CLOSED : dcp_v14_bl_gate_suspend(v14->bl_gate); + spin_unlock_irqrestore(&v14->bl_gate_lock, flags); + mutex_unlock(&v14->lock); + cancel_delayed_work_sync(&v14->backlight_work); +} + +/* The class device may be platform devres, outside the component bind group. */ +static void dcp_v14_stop_backlight(struct apple_dcp_v14 *v14) +{ + struct apple_dcp *dcp = READ_ONCE(v14->dcp); + + if (dcp) + WRITE_ONCE(dcp->quiescing, true); + dcp_v14_bl_shut(v14, true); + if (!dcp) + return; + dcp_backlight_stop(dcp); + cancel_work_sync(&dcp->bl_register_wq); + cancel_work_sync(&dcp->bl_update_wq); + /* Never hold this mutex while waiting for its registration worker. */ + guard(mutex)(&dcp->bl_register_mutex); + if (dcp->brightness.bl_dev) { + devm_backlight_device_unregister(dcp->dev, dcp->brightness.bl_dev); + dcp->brightness.bl_dev = NULL; + } +} + +/* The policy's kick (dcp_backlight.c): only queues; the RPC runs in the work. */ +static void dcp_v14_backlight_kick(struct apple_dcp *dcp) +{ + if (iomfb_v14_7_panel_nits(dcp) && !READ_ONCE(dcp->quiescing)) + dcp_v14_bl_queue(dcp->v14, 0); +} + +/* + * The panel's level policy, set up once the panel runs: at most 500 nits + * (less if the panel's DT record says so), and no fades. No boot loader + * level is known, so none is presented until userspace sets one; until + * then the backlight reports DCP_V14_NITS_DEFAULT as its requested level + * and no actual level (dcp_backlight.c). dcp_backlight_configure() + * registers the device from bl_register_wq. + */ +static void dcp_v14_configure_backlight(struct apple_dcp *dcp) +{ + struct apple_dcp_v14 *v14 = dcp->v14; + u32 maximum = DCP_V14_NITS_MAX; + int ret; + + if (!iomfb_v14_7_panel_nits(dcp) || READ_ONCE(dcp->quiescing) || + !v14->started || dcp_backlight_active(dcp) || + !dcp_has_panel(dcp) || !dcp->crtc) + return; + /* A described panel maximum may only lower the wire bound. */ + if (dcp->brightness.maximum) + maximum = min(maximum, dcp->brightness.maximum); + ret = dcp_backlight_configure(dcp, maximum, false, 0, true, + min_t(u32, maximum, DCP_V14_NITS_DEFAULT), + dcp_v14_backlight_kick); + if (ret && ret != -EBUSY) + dev_err_once(dcp->dev, "%s backlight unavailable: %d\n", v14->board->name, ret); +} + +void iomfb_v14_7_backlight_suspend(struct apple_dcp *dcp) +{ + if (iomfb_v14_7_panel_nits(dcp)) + dcp_v14_bl_shut(dcp->v14, false); +} + +void iomfb_v14_7_backlight_resume(struct apple_dcp *dcp) +{ + struct apple_dcp_v14 *v14 = dcp->v14; + unsigned long flags; + + if (!iomfb_v14_7_panel_nits(dcp)) + return; + mutex_lock(&v14->lock); + spin_lock_irqsave(&v14->bl_gate_lock, flags); + v14->bl_gate = dcp_v14_bl_gate_resume(v14->bl_gate); + spin_unlock_irqrestore(&v14->bl_gate_lock, flags); + mutex_unlock(&v14->lock); +} + /* * An external session stopped on a link error, not a crash: its display is * gone until reboot. Report that as the crash path does, from process @@ -337,6 +479,8 @@ static int dcp_v14_call_guarded(struct apple_dcp_v14 *v14, u32 tag, const void * bool vetoed; int ret; + KUNIT_STATIC_STUB_REDIRECT(dcp_v14_call_guarded, v14, tag, in, in_size, + out, out_size, completion, admit, admit_cookie); if (v14->failed) return -EIO; dev_dbg(v14->dev, "call %#x in %u out %u\n", tag, in_size, out_size); @@ -1456,6 +1600,8 @@ static void dcp_v14_release(void *data) { struct apple_dcp_v14 *v14 = data; + if (dcp_v14_board_nits(v14)) + dcp_v14_stop_backlight(v14); if (!v14->rtk) kfree(v14); } @@ -1537,6 +1683,13 @@ const struct dcp_v14_board *iomfb_v14_7_board(struct device *dev) return NULL; } +static bool dcp_v14_panel_nits_admit(const struct dcp_v14_board *board, + dcp_v14_compat_fn machine_is, const void *ctx) +{ + return board && board->panel_nits && + (machine_is(ctx, "apple,j514s") || machine_is(ctx, "apple,j516s")); +} + const char *iomfb_v14_7_board_name(const struct dcp_v14_board *board) { return board->name; @@ -1595,10 +1748,13 @@ int iomfb_v14_7_probe(struct apple_dcp *dcp) return -ENOMEM; v14->dev = dev; v14->board = board; + v14->panel_nits = dcp_v14_panel_nits_admit(board, dcp_v14_machine_is, np); v14->dcp = dcp; mutex_init(&v14->lock); init_waitqueue_head(&v14->described_wait); INIT_WORK(&v14->idle_work, dcp_v14_idle); + INIT_DELAYED_WORK(&v14->backlight_work, dcp_v14_backlight_work); + spin_lock_init(&v14->bl_gate_lock); ret = devm_add_action_or_reset(dev, dcp_v14_release, v14); if (ret) return ret; @@ -2051,6 +2207,8 @@ static int dcpext_boot(struct apple_dcp *dcp) mutex_init(&v14->lock); init_waitqueue_head(&v14->described_wait); INIT_WORK(&v14->idle_work, dcp_v14_idle); + INIT_DELAYED_WORK(&v14->backlight_work, dcp_v14_backlight_work); + spin_lock_init(&v14->bl_gate_lock); INIT_WORK(&v14->stopped_work, dcp_v14_stopped_work); dcp->v14 = v14; dcp_v14_link_init(&v14->link, dev, NULL); @@ -2281,6 +2439,8 @@ void iomfb_v14_7_unbind(struct apple_dcp *dcp) flush_work(&v14->idle_work); cancel_work_sync(&v14->stopped_work); } else { + if (dcp_v14_board_nits(v14)) + dcp_v14_stop_backlight(v14); WRITE_ONCE(v14->dcp, NULL); } if (v14->rtk) @@ -2320,6 +2480,8 @@ void iomfb_v14_7_remove(struct apple_dcp *dcp) * Callbacks that already loaded the old pointer finish before the * flush returns; later ones see NULL. */ + if (dcp_v14_board_nits(v14)) + dcp_v14_stop_backlight(v14); WRITE_ONCE(v14->dcp, NULL); if (v14->rtk) apple_rtkit_flush_rx(v14->rtk); @@ -2472,6 +2634,7 @@ int iomfb_v14_7_start(struct apple_dcp *dcp) dcp->mode_state.valid = true; dcp->active = true; complete(&dcp->start_done); + dcp_v14_configure_backlight(dcp); mutex_lock(&dcp->modes_lock); selected = dcp->modes[0].mode; @@ -2577,20 +2740,67 @@ static bool dcp_v14_swap_admit(void *cookie) return connector && READ_ONCE(connector->connected); } +static bool dcp_v14_swap_nits(struct apple_dcp_v14 *v14, u8 *swap, + struct dcp_backlight_present *present) +{ + struct apple_dcp *dcp = READ_ONCE(v14->dcp); + + lockdep_assert_held(&v14->lock); + /* The policy exists only on the T6030 panel: nothing else is asked. */ + if (!dcp || !dcp_v14_board_nits(v14) || !dcp_backlight_active(dcp)) + return false; + if (!dcp_v14_swap_takes_nits(true, v14->external, dcp_is_typec_output(dcp), true)) + return false; + /* -EALREADY: no change; -ENODATA/-EBUSY: nothing to present now. */ + if (dcp_backlight_prepare(dcp, true, present)) + return false; + /* The policy's maximum is at most the wire bound; never above it. */ + if (WARN_ON_ONCE(dcp_v14_encode_nits(swap, true, present->nits))) { + dcp_backlight_complete(dcp, present->sequence, false); + v14->nits_refused++; + return false; + } + return true; +} + +static void dcp_v14_swap_nits_done(struct apple_dcp_v14 *v14, + const struct dcp_backlight_present *present, bool accepted) +{ + struct apple_dcp *dcp = READ_ONCE(v14->dcp); + unsigned int delay; + + if (!dcp) + return; + if (!dcp_backlight_complete(dcp, present->sequence, accepted)) + return; + if (accepted) { + v14->nits_sent = present->nits; + v14->nits_presents++; + return; + } + v14->nits_refused++; + delay = dcp_backlight_retry_delay(dcp); + if (delay && !READ_ONCE(v14->failed)) + dcp_v14_bl_queue(v14, msecs_to_jiffies(delay)); +} + /* * Swap start, then the swap; returns once the firmware completed that swap. * @ctm_state, if set, carries the colour matrix to send first. */ -static int dcp_v14_swap(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova, +static int dcp_v14_swap_locked(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova, u32 width, u32 height, u32 dst_y, const struct drm_crtc_state *ctm_state) { struct dcp_v14_swap_admission admission = { .v14 = v14 }; __le32 start[4] = {}, started[2] = {}, result[3]; + struct dcp_backlight_present present; + bool nits; u8 *swap; u32 id; int ret; + lockdep_assert_held(&v14->lock); if (surface && (!iova || !width || width > v14->panel_width || !height || dst_y > v14->panel_height || height > v14->panel_height - dst_y)) return -EINVAL; @@ -2598,7 +2808,6 @@ static int dcp_v14_swap(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova, if (!swap) return -ENOMEM; - mutex_lock(&v14->lock); admission.generation = v14->description_generation; /* * A colour matrix the firmware refuses costs night light, not the @@ -2617,11 +2826,14 @@ static int dcp_v14_swap(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova, dst_y, DCP_V14_LAYER); if (dcp_v14_board_stamps_swaps(v14->board, v14->external)) dcp_v14_swap_timestamps(swap); + nits = dcp_v14_swap_nits(v14, swap, &present); ret = dcp_v14_call_guarded(v14, A(407), swap, DCP_V14_SWAP_SIZE, result, sizeof(result), id, dcp_v14_swap_admit, &admission); /* The swap status sits at byte 5 of the reply. */ if (!ret && get_unaligned_le32((u8 *)result + 5)) ret = -EIO; + if (nits) + dcp_v14_swap_nits_done(v14, &present, !ret); } /* * An external display can go away under a swap: the firmware then @@ -2630,26 +2842,49 @@ static int dcp_v14_swap(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova, */ if (ret && !v14->external) v14->failed = true; - mutex_unlock(&v14->lock); kfree(swap); return ret; } +/* Publish a successful swap while the RPC lock still pins both surfaces. */ +static struct drm_framebuffer *dcp_v14_publish_surface(struct apple_dcp_v14 *v14, + struct drm_framebuffer *fb, const u8 *surface, u64 iova, + u32 width, u32 height, u32 dst_y) +{ + struct drm_framebuffer *old; + + KUNIT_STATIC_STUB_REDIRECT(dcp_v14_publish_surface, v14, fb, surface, iova, + width, height, dst_y); + lockdep_assert_held(&v14->lock); + old = v14->active_fb; + v14->active_fb = fb; + if (fb) { + memcpy(v14->shown_surface, surface, sizeof(v14->shown_surface)); + v14->shown_iova = iova; + v14->shown_width = width; + v14->shown_height = height; + v14->shown_dst_y = dst_y; + } + return old; +} + /* * Shows the top-left @width x @height of @fb (or only the black background), * after the colour matrix of @ctm_state if that is set, * and keeps @fb until the next swap. */ -static bool dcp_v14_present(struct apple_dcp_v14 *v14, struct drm_framebuffer *fb, +static bool dcp_v14_present_locked(struct apple_dcp_v14 *v14, struct drm_framebuffer *fb, u32 width, u32 height, u32 dst_y, - const struct drm_crtc_state *ctm_state) + const struct drm_crtc_state *ctm_state, + struct drm_framebuffer **retired) { u8 surface[DCP_V14_SURFACE_SIZE]; struct drm_framebuffer *old; u64 iova = 0, start, elapsed, swaps; int ret; + lockdep_assert_held(&v14->lock); if (READ_ONCE(v14->failed)) return false; if (fb) { @@ -2659,13 +2894,12 @@ static bool dcp_v14_present(struct apple_dcp_v14 *v14, struct drm_framebuffer *f iova = drm_fb_dma_get_gem_obj(fb, 0)->dma_addr; } start = ktime_get_ns(); - ret = dcp_v14_swap(v14, fb ? surface : NULL, iova, fb ? width : 0, + ret = dcp_v14_swap_locked(v14, fb ? surface : NULL, iova, fb ? width : 0, fb ? height : 0, dst_y, ctm_state); elapsed = ktime_get_ns() - start; if (ret && v14->external && !READ_ONCE(v14->failed)) { /* Refused before it was taken: the old framebuffer stays on screen. */ - if (fb) - drm_framebuffer_put(fb); + *retired = fb; WRITE_ONCE(v14->mode_swaps, 0); dev_warn_ratelimited(v14->dev, "display refused a swap: %d\n", ret); return false; @@ -2675,22 +2909,91 @@ static bool dcp_v14_present(struct apple_dcp_v14 *v14, struct drm_framebuffer *f dev_err(v14->dev, "native DCP flip failed %d; buffers pinned until reboot\n", ret); return false; } - mutex_lock(&v14->lock); - old = v14->active_fb; - v14->active_fb = fb; + old = dcp_v14_publish_surface(v14, fb, surface, iova, width, height, dst_y); swaps = ++v14->swaps; if (fb) v14->mode_swaps++; v14->swap_ns_max = max(v14->swap_ns_max, elapsed); - mutex_unlock(&v14->lock); - if (old) - drm_framebuffer_put(old); + *retired = old; if (swaps <= 3) dev_info(v14->dev, "swap %llu complete: surface %d, imported %d, %llu us\n", swaps, !!fb, fb && fb->obj[0]->import_attach, elapsed / NSEC_PER_USEC); return true; } +static bool dcp_v14_present(struct apple_dcp_v14 *v14, struct drm_framebuffer *fb, + u32 width, u32 height, u32 dst_y, + const struct drm_crtc_state *ctm_state) +{ + struct drm_framebuffer *retired = NULL; + bool shown; + + mutex_lock(&v14->lock); + shown = dcp_v14_present_locked(v14, fb, width, height, dst_y, ctm_state, &retired); + mutex_unlock(&v14->lock); + if (retired) + drm_framebuffer_put(retired); + return shown; +} + +static void dcp_v14_backlight_work(struct work_struct *work) +{ + struct apple_dcp_v14 *v14 = container_of(to_delayed_work(work), struct apple_dcp_v14, + backlight_work); + struct dcp_v14_bl_facts facts; + struct drm_framebuffer *fb; + struct apple_dcp *dcp; + int ret; + + guard(mutex)(&v14->lock); + dcp = READ_ONCE(v14->dcp); + if (!dcp) + return; + fb = v14->active_fb; + facts = (struct dcp_v14_bl_facts) { + .gate = dcp_v14_bl_gate_get(v14), + .panel_nits = dcp_v14_board_nits(v14), + .external = v14->external, + .typec = dcp_is_typec_output(dcp), + .started = v14->started, + .failed = READ_ONCE(v14->failed), + .crashed = READ_ONCE(dcp->crashed), + .quiescing = READ_ONCE(dcp->quiescing), + .pending = dcp_v14_board_nits(v14) && dcp_backlight_active(dcp) && + dcp_backlight_pending(dcp), + .frame = !!fb, + .mode_valid = READ_ONCE(dcp->mode_state.valid), + }; + if (dcp_v14_bl_plan(&facts) != DCP_V14_BL_REPRESENT) + return; + ret = dcp_v14_swap_locked(v14, v14->shown_surface, v14->shown_iova, v14->shown_width, v14->shown_height, + v14->shown_dst_y, NULL); + if (!ret) + v14->brightness_presents++; + else if (ret != -EAGAIN) + dev_err(v14->dev, "brightness present failed: %d\n", ret); +} + +static bool dcp_v14_bl_dpms_applies(struct apple_dcp_v14 *v14, struct apple_dcp *dcp) +{ + if (!dcp_v14_board_nits(v14) || !dcp_backlight_active(dcp)) + return false; + return dcp_v14_bl_follow_dpms(true, true, dcp_backlight_level_known(dcp)); +} + +static void dcp_v14_blank(struct apple_dcp_v14 *v14, struct apple_dcp *dcp) +{ + struct drm_framebuffer *retired = NULL; + + mutex_lock(&v14->lock); + if (dcp_v14_bl_dpms_applies(v14, dcp)) + dcp_backlight_dpms(dcp, false); + dcp_v14_present_locked(v14, NULL, 0, 0, 0, NULL, &retired); + mutex_unlock(&v14->lock); + if (retired) + drm_framebuffer_put(retired); +} + /* Releases helper waiters without reporting a completed flip. */ static void dcp_v14_cancel_event(struct apple_dcp *dcp) { @@ -3023,6 +3326,8 @@ void iomfb_v14_7_flush(struct apple_dcp *dcp, struct drm_crtc *crtc, dcp_drm_crtc_vblank(dcp->crtc); return; } + if (!dcp->external) + dcp_v14_configure_backlight(dcp); dcp->swap_start = ktime_get(); if (dcp->v14 && dcp_v14_present(dcp->v14, fb, fb ? (dcp->external ? p->src_w >> 16 : fb->width) : 0, @@ -3035,7 +3340,9 @@ void iomfb_v14_7_flush(struct apple_dcp *dcp, struct drm_crtc *crtc, void iomfb_v14_7_poweron(struct apple_dcp *dcp) { - /* The panel stays on; the next swap shows the plane again. */ + /* The next present restores a level only after Linux has set one. */ + if (dcp->v14 && dcp_v14_bl_dpms_applies(dcp->v14, dcp)) + dcp_backlight_dpms(dcp, true); } void iomfb_v14_7_poweroff(struct apple_dcp *dcp) @@ -3058,9 +3365,11 @@ void iomfb_v14_7_poweroff(struct apple_dcp *dcp) } /* Blank to black; the panel and the DCP stay powered. */ if (v14 && v14->started) - dcp_v14_present(v14, NULL, 0, 0, 0, NULL); + dcp_v14_blank(v14, dcp); } #if IS_ENABLED(CONFIG_DRM_APPLE_DISPLAY_KUNIT_TEST) #include "iomfb_v14_7_external_audio_test.c" +#include "iomfb_v14_7_backlight_test.c" +#include "iomfb_v14_7_backlight_lifetime_test.c" #endif diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.h b/drivers/gpu/drm/apple/iomfb_v14_7.h index f9bef6ed0f109..8aa4eeda4be7a 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.h +++ b/drivers/gpu/drm/apple/iomfb_v14_7.h @@ -31,6 +31,17 @@ const char *iomfb_v14_7_board_name(const struct dcp_v14_board *board); /* Platform probe: firmware identity, boot loader handoff, PMP acknowledgment. */ int iomfb_v14_7_probe(struct apple_dcp *dcp); +/* + * Whether @dcp is a 14.x internal panel whose swaps carry its level (the + * T6030 board record): its backlight is the nits policy in dcp_backlight.c. + */ +bool iomfb_v14_7_panel_nits(struct apple_dcp *dcp); +/* + * System sleep: no brightness-only present may be queued or run from + * _suspend() until _resume(). + */ +void iomfb_v14_7_backlight_suspend(struct apple_dcp *dcp); +void iomfb_v14_7_backlight_resume(struct apple_dcp *dcp); /* Component bind: the running DCP's RTKit session. */ int iomfb_v14_7_bind(struct apple_dcp *dcp); int iomfb_v14_7_external_start(struct apple_dcp *dcp); diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight.h b/drivers/gpu/drm/apple/iomfb_v14_7_backlight.h new file mode 100644 index 0000000000000..f2b71653664eb --- /dev/null +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight.h @@ -0,0 +1,98 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ + +#ifndef __APPLE_IOMFB_V14_7_BACKLIGHT_H__ +#define __APPLE_IOMFB_V14_7_BACKLIGHT_H__ + +#include + +/* + * Decisions of the 14.7 panel's level path (iomfb_v14_7.c), kept free of + * driver state so that KUnit runs the same code. + */ + +/* + * Whether brightness-only presents may be queued and run. Set under the + * gate lock before the work is cancelled: system sleep suspends it until + * resume, unbind and remove close it for good. + */ +enum dcp_v14_bl_gate { + DCP_V14_BL_OPEN, + DCP_V14_BL_SUSPENDED, + DCP_V14_BL_CLOSED, +}; + +/* Whether a brightness-only present may be queued, or run once queued. */ +static inline bool dcp_v14_bl_may_queue(enum dcp_v14_bl_gate gate) +{ + return gate == DCP_V14_BL_OPEN; +} + +static inline enum dcp_v14_bl_gate dcp_v14_bl_gate_suspend(enum dcp_v14_bl_gate gate) +{ + return gate == DCP_V14_BL_OPEN ? DCP_V14_BL_SUSPENDED : gate; +} + +static inline enum dcp_v14_bl_gate dcp_v14_bl_gate_resume(enum dcp_v14_bl_gate gate) +{ + return gate == DCP_V14_BL_SUSPENDED ? DCP_V14_BL_OPEN : gate; +} + +/* + * Whether a swap carries the level the backlight policy reserves: only the + * internal panel of a board whose level block was validated (T6030), only + * while it shows its own panel (not a J613 internal pipe lent to a Type-C + * display), and only with an active policy. + */ +static inline bool dcp_v14_swap_takes_nits(bool panel_nits, bool external, bool typec, + bool policy_active) +{ + return panel_nits && !external && !typec && policy_active; +} + +/* + * Blanking follows DPMS only once Linux has set a level: before that the + * panel shows the boot loader's level, and a DPMS cycle must not replace it + * with the reported default. + */ +static inline bool dcp_v14_bl_follow_dpms(bool panel_nits, bool policy_active, + bool level_known) +{ + return panel_nits && policy_active && level_known; +} + +struct dcp_v14_bl_facts { + enum dcp_v14_bl_gate gate; + bool panel_nits; + bool external; + bool typec; + bool started; + bool failed; + bool crashed; + bool quiescing; + /* A level is reserved-able: dirty and not in flight. */ + bool pending; + /* The last present showed a frame (active_fb), not the background. */ + bool frame; + bool mode_valid; +}; + +enum dcp_v14_bl_action { + /* Nothing to do, or not allowed to: no RPC. */ + DCP_V14_BL_SKIP, + /* Blanked or no frame yet: the next atomic present carries the level. */ + DCP_V14_BL_WAIT, + /* Present the frame shown last again, with the level. */ + DCP_V14_BL_REPRESENT, +}; + +static inline enum dcp_v14_bl_action dcp_v14_bl_plan(const struct dcp_v14_bl_facts *f) +{ + if (!dcp_v14_bl_may_queue(f->gate) || !f->panel_nits || f->external || f->typec || + !f->started || f->failed || f->crashed || f->quiescing || !f->pending) + return DCP_V14_BL_SKIP; + if (!f->frame || !f->mode_valid) + return DCP_V14_BL_WAIT; + return DCP_V14_BL_REPRESENT; +} + +#endif /* __APPLE_IOMFB_V14_7_BACKLIGHT_H__ */ diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c new file mode 100644 index 0000000000000..291aecd0e277c --- /dev/null +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c @@ -0,0 +1,365 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * Port-specific controls: run the real swap/present, worker, DPMS dispatch + * and class teardown. Only the RPC transport is replaced. Threads and a + * workqueue context bridge retain KUnit's per-task static-stub context. + */ +#include +#include +#include +#include + +/* The real shared registration worker, visible only with CONFIG_KUNIT. */ +void dcp_work_register_backlight(struct work_struct *work); + +struct v14_bl_rpc; +struct v14_bl_actor { + struct v14_bl_rpc *ctx; + struct task_struct *task; + struct completion entered, done; + unsigned int op; + bool shown; +}; + +struct v14_bl_rpc { + struct kunit *test; + struct v14_bl_fixture f; + struct drm_device *drm; + struct drm_framebuffer fb[2]; + struct drm_gem_dma_object gem[2]; + unsigned int initialized; + struct v14_bl_actor actors[2]; + struct completion rpc_entered, rpc_release; + atomic_t submits; + u64 iova[4], nits[4]; + bool nits_present[4]; + struct drm_framebuffer *owner[4]; + bool block_first, refuse; +}; + +static struct v14_bl_rpc *v14_bl_rpc_current; + +static int v14_bl_rpc_call(struct apple_dcp_v14 *v14, u32 tag, const void *in, u32 in_size, + void *out, u32 out_size, u32 completion, + dcp_v14_admit_fn admit, void *admit_cookie) +{ + struct v14_bl_rpc *c = v14_bl_rpc_current; + unsigned int n; + + KUNIT_EXPECT_TRUE(c->test, mutex_is_locked(&v14->lock)); + memset(out, 0, out_size); + if (tag == A(406)) { + put_unaligned_le32(1, out); + return 0; + } + KUNIT_EXPECT_EQ(c->test, tag, A(407)); + KUNIT_EXPECT_EQ(c->test, in_size, (u32)DCP_V14_SWAP_SIZE); + n = atomic_inc_return(&c->submits) - 1; + if (n < ARRAY_SIZE(c->iova)) { + c->iova[n] = get_unaligned_le64((const u8 *)in + DCP_V14_SWAP_IOVA(0)); + c->nits_present[n] = ((const u8 *)in)[DCP_V14_SWAP_NITS_PRESENT]; + c->nits[n] = get_unaligned_le64((const u8 *)in + DCP_V14_SWAP_NITS); + c->owner[n] = v14->active_fb; + } + if (!n && c->block_first) { + complete(&c->rpc_entered); + wait_for_completion(&c->rpc_release); + } + if (c->refuse) + put_unaligned_le32(1, (u8 *)out + 5); + return 0; +} + +static void v14_bl_rpc_work(struct work_struct *work) +{ + struct kunit *previous = current->kunit_test; + + current->kunit_test = v14_bl_rpc_current->test; + dcp_v14_backlight_work(work); + current->kunit_test = previous; +} + +static int v14_bl_actor_run(void *data) +{ + struct v14_bl_actor *a = data; + struct v14_bl_rpc *c = a->ctx; + + current->kunit_test = c->test; + complete(&a->entered); + if (a->op < 2) + a->shown = dcp_v14_present(c->f.v14, &c->fb[a->op], 64, 64, 0, NULL); + else if (a->op == 2) + iomfb_v14_7_backlight_suspend(c->f.dcp); + else + iomfb_v14_7_unbind(c->f.dcp); + complete(&a->done); + current->kunit_test = NULL; + return 0; +} + +static void v14_bl_rpc_cleanup(void *data) +{ + struct v14_bl_rpc *c = data; + unsigned int i; + + complete_all(&c->rpc_release); + for (i = 0; i < ARRAY_SIZE(c->actors); i++) + if (!IS_ERR_OR_NULL(c->actors[i].task)) + kthread_stop_put(c->actors[i].task); + dcp_v14_stop_backlight(c->f.v14); + if (c->f.v14->active_fb) { + drm_framebuffer_put(c->f.v14->active_fb); + c->f.v14->active_fb = NULL; + } + for (i = 0; i < c->initialized; i++) + drm_framebuffer_put(&c->fb[i]); + v14_bl_rpc_current = NULL; +} + +static void v14_bl_fb_destroy(struct drm_framebuffer *fb) +{ + drm_framebuffer_cleanup(fb); +} + +static const struct drm_framebuffer_funcs v14_bl_fb_funcs = { + .destroy = v14_bl_fb_destroy, +}; + +static struct v14_bl_rpc *v14_bl_rpc_setup(struct kunit *test) +{ + struct v14_bl_rpc *c = kunit_kzalloc(test, sizeof(*c), GFP_KERNEL); + struct device *dev; + unsigned int i; + + KUNIT_ASSERT_NOT_NULL(test, c); + c->test = test; + c->f = v14_bl_fixture(test, &dcp_v14_board_t6030, true); + dev = drm_kunit_helper_alloc_device(test); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev); + c->drm = __drm_kunit_helper_alloc_drm_device(test, dev, sizeof(*c->drm), 0, + DRIVER_MODESET); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, c->drm); + init_completion(&c->rpc_entered); + init_completion(&c->rpc_release); + atomic_set(&c->submits, 0); + v14_bl_rpc_current = c; + kunit_activate_static_stub(test, dcp_v14_call_guarded, v14_bl_rpc_call); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, v14_bl_rpc_cleanup, c), 0); + for (i = 0; i < ARRAY_SIZE(c->fb); i++) { + c->fb[i].dev = c->drm; + c->fb[i].format = drm_format_info(DRM_FORMAT_XRGB8888); + c->fb[i].width = c->fb[i].height = 64; + c->fb[i].pitches[0] = 256; + c->gem[i].dma_addr = 0x100000 + i * 0x10000; + c->fb[i].obj[0] = &c->gem[i].base; + KUNIT_ASSERT_EQ(test, drm_framebuffer_init(c->drm, &c->fb[i], &v14_bl_fb_funcs), 0); + c->initialized++; + } + c->f.v14->panel_width = c->f.v14->panel_height = 64; + c->f.dcp->fixed_connector_type = DRM_MODE_CONNECTOR_eDP; + c->f.dcp->connector_type = DRM_MODE_CONNECTOR_eDP; + c->f.dcp->mode_state.valid = true; + c->f.dcp->crtc = devm_kzalloc(c->f.dcp->dev, sizeof(*c->f.dcp->crtc), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, c->f.dcp->crtc); + c->f.dcp->crtc->base.dev = c->drm; + INIT_DELAYED_WORK(&c->f.v14->backlight_work, v14_bl_rpc_work); + return c; +} + +static struct v14_bl_actor *v14_bl_actor_start(struct kunit *test, + struct v14_bl_rpc *c, unsigned int slot, + unsigned int op) +{ + struct v14_bl_actor *a = &c->actors[slot]; + + a->ctx = c; + a->op = op; + init_completion(&a->entered); + init_completion(&a->done); + a->task = kthread_create(v14_bl_actor_run, a, "v14-backlight-test"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, a->task); + get_task_struct(a->task); + wake_up_process(a->task); + KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&a->entered, HZ), 0UL); + return a; +} + +static struct drm_framebuffer *v14_bl_pause_publication(struct apple_dcp_v14 *v14, + struct drm_framebuffer *fb, const u8 *surface, u64 iova, + u32 width, u32 height, u32 dst_y) +{ + struct v14_bl_rpc *c = v14_bl_rpc_current; + + /* Only the first publication pauses; its eventual call runs the real body. */ + kunit_deactivate_static_stub(c->test, dcp_v14_publish_surface); + complete(&c->rpc_entered); + wait_for_completion(&c->rpc_release); + return dcp_v14_publish_surface(v14, fb, surface, iova, width, height, dst_y); +} + +/* Swap and framebuffer publication cannot be reordered by another present. */ +static void v14_bl_present_publication_serialized(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + struct v14_bl_actor *first, *second; + + kunit_activate_static_stub(test, dcp_v14_publish_surface, v14_bl_pause_publication); + first = v14_bl_actor_start(test, c, 0, 0); + KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&c->rpc_entered, HZ), 0UL); + second = v14_bl_actor_start(test, c, 1, 1); + KUNIT_EXPECT_EQ(test, wait_for_completion_timeout(&second->done, msecs_to_jiffies(20)), 0UL); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 1); + complete_all(&c->rpc_release); + KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&first->done, HZ), 0UL); + KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&second->done, HZ), 0UL); + KUNIT_EXPECT_TRUE(test, first->shown); + KUNIT_EXPECT_TRUE(test, second->shown); + KUNIT_EXPECT_PTR_EQ(test, c->owner[1], &c->fb[0]); + KUNIT_EXPECT_PTR_EQ(test, c->f.v14->active_fb, &c->fb[1]); + KUNIT_EXPECT_EQ(test, c->f.v14->shown_iova, (u64)c->gem[1].dma_addr); + KUNIT_EXPECT_EQ(test, kref_read(&c->fb[0].base.refcount), 1U); + KUNIT_EXPECT_EQ(test, kref_read(&c->fb[1].base.refcount), 2U); +} + +/* A running brightness RPC pins its copied surface until suspend drains it. */ +static void v14_bl_running_work_suspend(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + struct v14_bl_actor *stop; + + KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); + atomic_set(&c->submits, 0); + c->block_first = true; + v14_bl_request(test, c->f.dcp, 200); + dcp_v14_backlight_kick(c->f.dcp); + KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&c->rpc_entered, HZ), 0UL); + stop = v14_bl_actor_start(test, c, 0, 2); + KUNIT_EXPECT_EQ(test, wait_for_completion_timeout(&stop->done, msecs_to_jiffies(20)), 0UL); + KUNIT_EXPECT_PTR_EQ(test, c->owner[0], &c->fb[0]); + KUNIT_EXPECT_EQ(test, kref_read(&c->fb[0].base.refcount), 2U); + complete_all(&c->rpc_release); + KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&stop->done, HZ), 0UL); + KUNIT_EXPECT_EQ(test, c->iova[0], (u64)c->gem[0].dma_addr); + KUNIT_EXPECT_EQ(test, c->f.v14->bl_gate, DCP_V14_BL_SUSPENDED); + v14_bl_request(test, c->f.dcp, 250); + dcp_v14_backlight_kick(c->f.dcp); + KUNIT_EXPECT_FALSE(test, work_busy(&c->f.v14->backlight_work.work)); +} + +/* Actual dcp_poweroff/on must reach the 14.7 blank/restore hooks. */ +static void v14_bl_dpms_dispatch(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + struct platform_device *pdev = kunit_kzalloc(test, sizeof(*pdev), GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, pdev); + platform_set_drvdata(pdev, c->f.dcp); + KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); + KUNIT_EXPECT_FALSE(test, c->nits_present[0]); + KUNIT_EXPECT_FALSE(test, dcp_backlight_level_known(c->f.dcp)); + dcp_poweroff(pdev); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 2); + KUNIT_EXPECT_FALSE(test, c->nits_present[1]); + KUNIT_EXPECT_PTR_EQ(test, c->f.v14->active_fb, NULL); + /* Even the advertised default must be an explicit first request. */ + dcp_poweron(pdev); + v14_bl_request(test, c->f.dcp, DCP_V14_NITS_DEFAULT); + KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); + KUNIT_EXPECT_TRUE(test, c->nits_present[2]); + KUNIT_EXPECT_TRUE(test, dcp_backlight_level_known(c->f.dcp)); + dcp_poweroff(pdev); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 4); + KUNIT_EXPECT_TRUE(test, c->nits_present[3]); + KUNIT_EXPECT_EQ(test, c->nits[3], 0ULL); + KUNIT_EXPECT_PTR_EQ(test, c->f.v14->active_fb, NULL); + dcp_poweron(pdev); + KUNIT_EXPECT_TRUE(test, dcp_backlight_pending(c->f.dcp)); + KUNIT_EXPECT_FALSE(test, c->f.dcp->backlight.state.dpms_off); +} + +/* An actual failed submit must leave the loader's level unknown. */ +static void v14_bl_failed_submit_unknown(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + + c->refuse = true; + v14_bl_request(test, c->f.dcp, 240); + dcp_v14_bl_shut(c->f.v14, true); + mutex_lock(&c->f.v14->lock); + KUNIT_EXPECT_EQ(test, dcp_v14_swap_locked(c->f.v14, NULL, 0, 0, 0, 0, NULL), -EIO); + mutex_unlock(&c->f.v14->lock); + KUNIT_EXPECT_FALSE(test, dcp_backlight_level_known(c->f.dcp)); + KUNIT_EXPECT_EQ(test, c->f.v14->nits_presents, 0ULL); + KUNIT_EXPECT_TRUE(test, c->f.v14->failed); + KUNIT_EXPECT_FALSE(test, work_busy(&c->f.v14->backlight_work.work)); +} + +static void v14_bl_mutex_unlock(void *data) +{ + mutex_unlock(data); +} + +/* Unbind closes class admission before draining a blocked real registrar. */ +static void v14_bl_registration_unbind_race(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + struct v14_bl_actor *stop; + unsigned long deadline; + bool running = false; + + mutex_lock(&c->f.dcp->bl_register_mutex); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, v14_bl_mutex_unlock, + &c->f.dcp->bl_register_mutex), 0); + KUNIT_EXPECT_TRUE(test, schedule_work(&c->f.dcp->bl_register_wq)); + deadline = jiffies + HZ; + do { + running = work_busy(&c->f.dcp->bl_register_wq) & WORK_BUSY_RUNNING; + if (running) + break; + usleep_range(50, 100); + } while (time_before(jiffies, deadline)); + KUNIT_EXPECT_TRUE(test, running); + stop = v14_bl_actor_start(test, c, 0, 3); + KUNIT_EXPECT_EQ(test, wait_for_completion_timeout(&stop->done, msecs_to_jiffies(20)), 0UL); + KUNIT_EXPECT_TRUE(test, READ_ONCE(c->f.dcp->quiescing)); + kunit_release_action(test, v14_bl_mutex_unlock, &c->f.dcp->bl_register_mutex); + KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&stop->done, HZ), 0UL); + KUNIT_EXPECT_PTR_EQ(test, c->f.dcp->brightness.bl_dev, NULL); + KUNIT_EXPECT_PTR_EQ(test, c->f.v14->dcp, NULL); + KUNIT_EXPECT_EQ(test, c->f.v14->bl_gate, DCP_V14_BL_CLOSED); + /* A late shared producer is refused as well. */ + schedule_work(&c->f.dcp->bl_register_wq); + flush_work(&c->f.dcp->bl_register_wq); + KUNIT_EXPECT_PTR_EQ(test, c->f.dcp->brightness.bl_dev, NULL); +} + +static void v14_bl_class_unregistered(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + + schedule_work(&c->f.dcp->bl_register_wq); + flush_work(&c->f.dcp->bl_register_wq); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, c->f.dcp->brightness.bl_dev); + iomfb_v14_7_unbind(c->f.dcp); + KUNIT_EXPECT_PTR_EQ(test, c->f.dcp->brightness.bl_dev, NULL); + KUNIT_EXPECT_TRUE(test, c->f.dcp->quiescing); + iomfb_v14_7_backlight_resume(c->f.dcp); + KUNIT_EXPECT_EQ(test, c->f.v14->bl_gate, DCP_V14_BL_CLOSED); +} + +static struct kunit_case v14_bl_lifetime_cases[] = { + KUNIT_CASE(v14_bl_present_publication_serialized), + KUNIT_CASE(v14_bl_running_work_suspend), + KUNIT_CASE(v14_bl_dpms_dispatch), + KUNIT_CASE(v14_bl_failed_submit_unknown), + KUNIT_CASE(v14_bl_registration_unbind_race), + KUNIT_CASE(v14_bl_class_unregistered), + {} +}; + +static struct kunit_suite v14_bl_lifetime_suite = { + .name = "dcp_v14_backlight_lifetime", + .test_cases = v14_bl_lifetime_cases, +}; + +kunit_test_suite(v14_bl_lifetime_suite); diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c new file mode 100644 index 0000000000000..7a2aba44e4857 --- /dev/null +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c @@ -0,0 +1,562 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * The 14.7 panel level path, included at the end of iomfb_v14_7.c so that + * the production functions run: the swap's level block, which boards carry + * it, the gate that keeps brightness-only presents out of system sleep and + * after unbind, and the policy as this panel uses it (no level presented + * until one is requested, no actual level reported until one was presented, + * a refused level never becomes the actual one). + * + * The objects are built here, with no DCP: nothing in a case may reach an + * RPC. A J613, J615 or J516C panel never initialises the backlight policy; + * its state is set to answer "yes" so that a case fails if anything on + * those boards looks at it. + */ +#include +#include + +void dcp_work_register_backlight(struct work_struct *work); + +static void v14_bl_unused_work(struct work_struct *work) +{ +} + + +static void v14_bl_binary64(struct kunit *test) +{ + static const struct { + u32 nits; + u64 bits; + } known[] = { + { 0, 0 }, + { 1, 0x3ff0000000000000ULL }, + { 2, 0x4000000000000000ULL }, + { 3, 0x4008000000000000ULL }, + { 140, 0x4061800000000000ULL }, + { 255, 0x406fe00000000000ULL }, + { 256, 0x4070000000000000ULL }, + { 500, 0x407f400000000000ULL }, + }; + unsigned int i; + u32 nits; + + for (i = 0; i < ARRAY_SIZE(known); i++) + KUNIT_EXPECT_EQ_MSG(test, dcp_v14_nits_binary64(known[i].nits), known[i].bits, + "%u nits", known[i].nits); + /* Every level up to the bound is an exact integral binary64. */ + for (nits = 1; nits <= DCP_V14_NITS_MAX; nits++) { + u64 bits = dcp_v14_nits_binary64(nits); + unsigned int e = (bits >> 52) - 1023; + u64 mantissa = bits & GENMASK_ULL(51, 0); + + KUNIT_ASSERT_LT(test, e, 32U); + KUNIT_EXPECT_EQ(test, mantissa & GENMASK_ULL(51 - e, 0), 0ULL); + KUNIT_EXPECT_EQ(test, (1ULL << e) + (mantissa >> (52 - e)), (u64)nits); + KUNIT_EXPECT_FALSE(test, bits >> 63); + } +} + +static void v14_bl_level_block(struct kunit *test) +{ + u8 *swap = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + u8 *before = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + unsigned int i; + + KUNIT_ASSERT_NOT_NULL(test, swap); + KUNIT_ASSERT_NOT_NULL(test, before); + memset(swap, 0xa5, DCP_V14_SWAP_SIZE); + memcpy(before, swap, DCP_V14_SWAP_SIZE); + + /* Above the wire bound: refused, the record untouched. */ + KUNIT_EXPECT_EQ(test, dcp_v14_encode_nits(swap, true, DCP_V14_NITS_MAX + 1), -EINVAL); + KUNIT_EXPECT_EQ(test, memcmp(swap, before, DCP_V14_SWAP_SIZE), 0); + + /* A level touches only the present byte and the binary64. */ + KUNIT_EXPECT_EQ(test, dcp_v14_encode_nits(swap, true, 140), 0); + for (i = 0; i < DCP_V14_SWAP_SIZE; i++) { + if (i == DCP_V14_SWAP_NITS_PRESENT || + (i >= DCP_V14_SWAP_NITS && i < DCP_V14_SWAP_NITS + 8)) + continue; + KUNIT_EXPECT_EQ_MSG(test, swap[i], before[i], "byte %#x", i); + } + KUNIT_EXPECT_EQ(test, swap[DCP_V14_SWAP_NITS_PRESENT], 1); + KUNIT_EXPECT_EQ(test, get_unaligned_le64(swap + DCP_V14_SWAP_NITS), + 0x4061800000000000ULL); + + /* No level: zero, as the record the session sent before. */ + KUNIT_EXPECT_EQ(test, dcp_v14_encode_nits(swap, false, 0), 0); + KUNIT_EXPECT_EQ(test, swap[DCP_V14_SWAP_NITS_PRESENT], 0); + KUNIT_EXPECT_EQ(test, get_unaligned_le64(swap + DCP_V14_SWAP_NITS), 0ULL); +} + +/* A swap without a level is byte for byte the one encoded before this. */ +static void v14_bl_swap_unchanged_without_level(struct kunit *test) +{ + u8 *swap = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + u8 *plain = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + u8 surface[DCP_V14_SURFACE_SIZE]; + + KUNIT_ASSERT_NOT_NULL(test, swap); + KUNIT_ASSERT_NOT_NULL(test, plain); + dcp_v14_encode_surface(surface, 3456 * 4, 3456, 2160, true); + dcp_v14_encode_swap(plain, 7, DCP_V14_BLACK, surface, 0x100000000ULL, 3456, 2160, 74, + DCP_V14_LAYER); + dcp_v14_encode_swap(swap, 7, DCP_V14_BLACK, surface, 0x100000000ULL, 3456, 2160, 74, + DCP_V14_LAYER); + dcp_v14_encode_nits(swap, false, 0); + KUNIT_EXPECT_EQ(test, memcmp(swap, plain, DCP_V14_SWAP_SIZE), 0); + /* The level block lies in the zeroed prefix, clear of every field. */ + KUNIT_EXPECT_EQ(test, plain[DCP_V14_SWAP_NITS_PRESENT], 0); + KUNIT_EXPECT_EQ(test, get_unaligned_le64(plain + DCP_V14_SWAP_NITS), 0ULL); + + dcp_v14_encode_swap(plain, 8, DCP_V14_BLACK, NULL, 0, 0, 0, 0, DCP_V14_LAYER); + dcp_v14_encode_swap(swap, 8, DCP_V14_BLACK, NULL, 0, 0, 0, 0, DCP_V14_LAYER); + dcp_v14_encode_nits(swap, false, 0); + KUNIT_EXPECT_EQ(test, memcmp(swap, plain, DCP_V14_SWAP_SIZE), 0); +} + +static bool v14_bl_ctx_dcp_is(const void *ctx, const char *compat) +{ + return !strcmp(((const char *const *)ctx)[0], compat); +} + +static bool v14_bl_ctx_machine_is(const void *ctx, const char *compat) +{ + return !strcmp(((const char *const *)ctx)[1], compat); +} + +/* Only the T6030 record (J514S, J516S) carries levels. */ +static void v14_bl_boards(struct kunit *test) +{ + static const char *const t6030_j516s[] = { "apple,t6030-dcp", "apple,j516s" }; + static const char *const t6030_j514s[] = { "apple,t6030-dcp", "apple,j514s" }; + static const char *const j613[] = { "apple,t8122-dcp", "apple,j613" }; + static const char *const j615[] = { "apple,t8122-dcp", "apple,j615" }; + static const char *const j516c[] = { "apple,t6031-dcp", "apple,j516c" }; + static const char *const other_t6030[] = { "apple,t6030-dcp", "apple,other" }; + const struct dcp_v14_board *board; + bool known; + + board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, t6030_j516s, &known); + KUNIT_ASSERT_NOT_NULL(test, board); + KUNIT_EXPECT_TRUE(test, board->panel_nits); + KUNIT_EXPECT_TRUE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, t6030_j516s)); + board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, t6030_j514s, &known); + KUNIT_ASSERT_NOT_NULL(test, board); + KUNIT_EXPECT_TRUE(test, board->panel_nits); + board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, other_t6030, &known); + KUNIT_ASSERT_NOT_NULL(test, board); + KUNIT_EXPECT_FALSE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, other_t6030)); + board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, j613, &known); + KUNIT_ASSERT_NOT_NULL(test, board); + KUNIT_EXPECT_FALSE(test, board->panel_nits); + board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, j615, &known); + KUNIT_ASSERT_NOT_NULL(test, board); + KUNIT_EXPECT_FALSE(test, board->panel_nits); + board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, j516c, &known); + KUNIT_ASSERT_NOT_NULL(test, board); + KUNIT_EXPECT_FALSE(test, board->panel_nits); +} + +static void v14_bl_gate_transitions(struct kunit *test) +{ + KUNIT_EXPECT_TRUE(test, dcp_v14_bl_may_queue(DCP_V14_BL_OPEN)); + KUNIT_EXPECT_FALSE(test, dcp_v14_bl_may_queue(DCP_V14_BL_SUSPENDED)); + KUNIT_EXPECT_FALSE(test, dcp_v14_bl_may_queue(DCP_V14_BL_CLOSED)); + KUNIT_EXPECT_EQ(test, dcp_v14_bl_gate_suspend(DCP_V14_BL_OPEN), DCP_V14_BL_SUSPENDED); + KUNIT_EXPECT_EQ(test, dcp_v14_bl_gate_suspend(DCP_V14_BL_SUSPENDED), DCP_V14_BL_SUSPENDED); + KUNIT_EXPECT_EQ(test, dcp_v14_bl_gate_suspend(DCP_V14_BL_CLOSED), DCP_V14_BL_CLOSED); + KUNIT_EXPECT_EQ(test, dcp_v14_bl_gate_resume(DCP_V14_BL_SUSPENDED), DCP_V14_BL_OPEN); + KUNIT_EXPECT_EQ(test, dcp_v14_bl_gate_resume(DCP_V14_BL_OPEN), DCP_V14_BL_OPEN); + /* Unbind is for good: no resume opens it again. */ + KUNIT_EXPECT_EQ(test, dcp_v14_bl_gate_resume(DCP_V14_BL_CLOSED), DCP_V14_BL_CLOSED); +} + +static void v14_bl_plan(struct kunit *test) +{ + const struct dcp_v14_bl_facts ready = { + .gate = DCP_V14_BL_OPEN, .panel_nits = true, .started = true, + .pending = true, .frame = true, .mode_valid = true, + }; + struct dcp_v14_bl_facts f; + + KUNIT_EXPECT_EQ(test, dcp_v14_bl_plan(&ready), DCP_V14_BL_REPRESENT); +#define V14_BL_EXPECT(field, value, action) do { \ + f = ready; \ + f.field = value; \ + KUNIT_EXPECT_EQ_MSG(test, dcp_v14_bl_plan(&f), action, \ + #field " = " #value); \ + } while (0) + V14_BL_EXPECT(gate, DCP_V14_BL_SUSPENDED, DCP_V14_BL_SKIP); + V14_BL_EXPECT(gate, DCP_V14_BL_CLOSED, DCP_V14_BL_SKIP); + V14_BL_EXPECT(panel_nits, false, DCP_V14_BL_SKIP); + V14_BL_EXPECT(external, true, DCP_V14_BL_SKIP); + V14_BL_EXPECT(typec, true, DCP_V14_BL_SKIP); + V14_BL_EXPECT(started, false, DCP_V14_BL_SKIP); + V14_BL_EXPECT(failed, true, DCP_V14_BL_SKIP); + V14_BL_EXPECT(crashed, true, DCP_V14_BL_SKIP); + V14_BL_EXPECT(quiescing, true, DCP_V14_BL_SKIP); + V14_BL_EXPECT(pending, false, DCP_V14_BL_SKIP); + V14_BL_EXPECT(frame, false, DCP_V14_BL_WAIT); + V14_BL_EXPECT(mode_valid, false, DCP_V14_BL_WAIT); +#undef V14_BL_EXPECT + KUNIT_EXPECT_TRUE(test, dcp_v14_swap_takes_nits(true, false, false, true)); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_takes_nits(false, false, false, true)); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_takes_nits(true, true, false, true)); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_takes_nits(true, false, true, true)); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_takes_nits(true, false, false, false)); +} + +/* The policy as this panel configures it: a default that is never presented. */ +static void v14_bl_policy(struct kunit *test) +{ + struct dcp_backlight_state s; + struct dcp_backlight_present p; + u64 seq; + + KUNIT_ASSERT_EQ(test, dcp_bl_init(&s, DCP_V14_NITS_MAX, false, 0, true, + DCP_V14_NITS_DEFAULT), 0); + /* Nothing to present at takeover: the panel keeps the loader's level. */ + KUNIT_EXPECT_EQ(test, dcp_bl_prepare(&s, true, &p), -EALREADY); + KUNIT_EXPECT_EQ(test, dcp_bl_reported_actual(&s, true), -ENODATA); + /* The H17P path keeps reporting its default. */ + KUNIT_EXPECT_EQ(test, dcp_bl_reported_actual(&s, false), DCP_V14_NITS_DEFAULT); + KUNIT_EXPECT_FALSE(test, dcp_v14_bl_follow_dpms(true, true, s.level_known)); + + /* A request is presented; a refusal never becomes the actual level. */ + KUNIT_ASSERT_EQ(test, dcp_bl_request(&s, 250, false, false), 0); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&s, true, &p), 0); + KUNIT_EXPECT_EQ(test, p.nits, 250U); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&s, p.sequence, false)); + KUNIT_EXPECT_EQ(test, dcp_bl_reported_actual(&s, true), -ENODATA); + KUNIT_EXPECT_TRUE(test, s.dirty); + KUNIT_EXPECT_EQ(test, dcp_bl_retry_delay(&s), 100U); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&s, true, &p), 0); + seq = p.sequence; + /* A stale completion does not complete the newer reservation. */ + KUNIT_EXPECT_FALSE(test, dcp_bl_complete(&s, seq - 1, true)); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&s, seq, true)); + KUNIT_EXPECT_EQ(test, dcp_bl_reported_actual(&s, true), 250); + KUNIT_EXPECT_TRUE(test, dcp_v14_bl_follow_dpms(true, true, s.level_known)); + + /* A later refusal keeps the last accepted level. */ + KUNIT_ASSERT_EQ(test, dcp_bl_request(&s, 300, false, false), 0); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&s, true, &p), 0); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&s, p.sequence, false)); + KUNIT_EXPECT_EQ(test, dcp_bl_reported_actual(&s, true), 250); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&s, true, &p), 0); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&s, p.sequence, true)); + + /* DPMS off blanks at 0 nits, on presents the level again. */ + KUNIT_ASSERT_EQ(test, dcp_bl_dpms(&s, false), 0); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&s, true, &p), 0); + KUNIT_EXPECT_EQ(test, p.nits, 0U); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&s, p.sequence, true)); + KUNIT_ASSERT_EQ(test, dcp_bl_dpms(&s, true), 0); + KUNIT_ASSERT_EQ(test, dcp_bl_prepare(&s, true, &p), 0); + KUNIT_EXPECT_EQ(test, p.nits, 300U); + KUNIT_EXPECT_TRUE(test, dcp_bl_complete(&s, p.sequence, true)); +} + +struct v14_bl_fixture { + struct apple_dcp *dcp; + struct apple_dcp_v14 *v14; +}; + +static void v14_bl_fixture_stop(void *data) +{ + struct apple_dcp_v14 *v14 = data; + + cancel_delayed_work_sync(&v14->backlight_work); +} + +/* + * An internal 14.7 panel on @board, started, with no DCP behind it. With + * @policy, the T6030 panel's policy as dcp_v14_configure_backlight() sets it + * up; without, a policy that was never initialised (other boards), whose + * state would answer "yes" if anything looked at it. + */ +static struct v14_bl_fixture v14_bl_fixture(struct kunit *test, + const struct dcp_v14_board *board, bool policy) +{ + static atomic_t serial = ATOMIC_INIT(0); + struct v14_bl_fixture f; + struct device *dev; + char name[32]; + + snprintf(name, sizeof(name), "dcp-v14-bl.%d", atomic_inc_return(&serial)); + dev = kunit_device_register(test, name); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev); + /* The device's backlight cleanup must run before these objects are freed. */ + f.dcp = devm_kzalloc(dev, sizeof(*f.dcp), GFP_KERNEL); + f.v14 = devm_kzalloc(dev, sizeof(*f.v14), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, f.dcp); + KUNIT_ASSERT_NOT_NULL(test, f.v14); + f.dcp->dev = dev; + mutex_init(&f.dcp->bl_register_mutex); + INIT_WORK(&f.dcp->bl_register_wq, dcp_work_register_backlight); + INIT_WORK(&f.dcp->bl_update_wq, v14_bl_unused_work); + INIT_DELAYED_WORK(&f.dcp->swap_watchdog_wq, v14_bl_unused_work); + f.dcp->fw_compat = DCP_FIRMWARE_V_14_7; + f.dcp->v14 = f.v14; + f.v14->dev = dev; + f.v14->board = board; + f.v14->panel_nits = board && board->panel_nits; + f.v14->dcp = f.dcp; + f.v14->started = true; + mutex_init(&f.v14->lock); + INIT_DELAYED_WORK(&f.v14->backlight_work, dcp_v14_backlight_work); + spin_lock_init(&f.v14->bl_gate_lock); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, v14_bl_fixture_stop, f.v14), 0); + if (policy) { + KUNIT_ASSERT_EQ(test, dcp_backlight_init(f.dcp, dev), 0); + KUNIT_ASSERT_EQ(test, dcp_bl_init(&f.dcp->backlight.state, DCP_V14_NITS_MAX, + false, 0, true, DCP_V14_NITS_DEFAULT), 0); + f.dcp->backlight.kick = dcp_v14_backlight_kick; + } else { + f.dcp->backlight.state.ready = true; + f.dcp->backlight.state.level_known = true; + f.dcp->backlight.state.dirty = true; + } + return f; +} + +/* Through the policy's own entry point, under its lock. */ +static void v14_bl_request(struct kunit *test, struct apple_dcp *dcp, u32 nits) +{ + unsigned long flags; + int ret; + + spin_lock_irqsave(&dcp->backlight.lock, flags); + ret = dcp_bl_request(&dcp->backlight.state, nits, false, false); + spin_unlock_irqrestore(&dcp->backlight.lock, flags); + KUNIT_ASSERT_EQ(test, ret, 0); +} + +/* Whether the work is queued or running; the caller holds lock. */ +static bool v14_bl_queued(struct apple_dcp_v14 *v14) +{ + return work_busy(&v14->backlight_work.work); +} + +/* + * The production paths on J613, J615 and J516C: no policy is consulted, no + * level is encoded, the work does nothing, sleep leaves the gate alone. + */ +static void v14_bl_other_boards_untouched(struct kunit *test) +{ + const struct dcp_v14_board *boards[] = { + &dcp_v14_board_j613, &dcp_v14_board_j615, &dcp_v14_board_j516c, + &dcp_v14_board_j514c, + }; + struct dcp_backlight_present present; + unsigned int i; + + for (i = 0; i < ARRAY_SIZE(boards); i++) { + struct v14_bl_fixture f = v14_bl_fixture(test, boards[i], false); + u8 *swap = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, swap); + KUNIT_EXPECT_FALSE(test, iomfb_v14_7_panel_nits(f.dcp)); + KUNIT_EXPECT_FALSE(test, dcp_backlight_active(f.dcp)); + KUNIT_EXPECT_FALSE(test, dcp_backlight_level_known(f.dcp)); + KUNIT_EXPECT_FALSE(test, dcp_v14_bl_dpms_applies(f.v14, f.dcp)); + + mutex_lock(&f.v14->lock); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_nits(f.v14, swap, &present)); + mutex_unlock(&f.v14->lock); + KUNIT_EXPECT_EQ(test, swap[DCP_V14_SWAP_NITS_PRESENT], 0); + KUNIT_EXPECT_EQ(test, get_unaligned_le64(swap + DCP_V14_SWAP_NITS), 0ULL); + + /* Run directly: it must return without an RPC (there is no DCP). */ + dcp_v14_backlight_work(&f.v14->backlight_work.work); + KUNIT_EXPECT_EQ(test, f.v14->brightness_presents, 0ULL); + KUNIT_EXPECT_EQ(test, f.v14->nits_refused, 0ULL); + + iomfb_v14_7_backlight_suspend(f.dcp); + KUNIT_EXPECT_EQ(test, f.v14->bl_gate, DCP_V14_BL_OPEN); + dcp_v14_configure_backlight(f.dcp); + KUNIT_EXPECT_TRUE(test, f.dcp->backlight.kick == NULL); + } +} + +/* An external T6030 processor is no panel: nothing either. */ +static void v14_bl_external_untouched(struct kunit *test) +{ + struct v14_bl_fixture f = v14_bl_fixture(test, &dcp_v14_board_t6030, false); + struct dcp_backlight_present present; + u8 *swap = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, swap); + f.dcp->external = true; + f.v14->external = true; + KUNIT_EXPECT_FALSE(test, iomfb_v14_7_panel_nits(f.dcp)); + KUNIT_EXPECT_FALSE(test, dcp_backlight_level_known(f.dcp)); + KUNIT_EXPECT_FALSE(test, dcp_v14_bl_dpms_applies(f.v14, f.dcp)); + mutex_lock(&f.v14->lock); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_nits(f.v14, swap, &present)); + mutex_unlock(&f.v14->lock); + KUNIT_EXPECT_EQ(test, swap[DCP_V14_SWAP_NITS_PRESENT], 0); + dcp_v14_backlight_work(&f.v14->backlight_work.work); + KUNIT_EXPECT_EQ(test, f.v14->brightness_presents, 0ULL); +} + +/* + * The T6030 panel: no level until one is requested; the requested level is + * encoded; a refusal keeps the actual level and is retried; an accepted one + * is the actual level, and only then does blanking follow DPMS. + */ +static void v14_bl_t6030_swap_levels(struct kunit *test) +{ + struct v14_bl_fixture f = v14_bl_fixture(test, &dcp_v14_board_t6030, true); + struct dcp_backlight_present present; + u8 *swap = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, swap); + KUNIT_EXPECT_TRUE(test, iomfb_v14_7_panel_nits(f.dcp)); + KUNIT_EXPECT_TRUE(test, dcp_backlight_active(f.dcp)); + KUNIT_EXPECT_FALSE(test, dcp_backlight_level_known(f.dcp)); + KUNIT_EXPECT_FALSE(test, dcp_v14_bl_dpms_applies(f.v14, f.dcp)); + + mutex_lock(&f.v14->lock); + /* Nothing requested: the swap carries no level. */ + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_nits(f.v14, swap, &present)); + KUNIT_EXPECT_EQ(test, swap[DCP_V14_SWAP_NITS_PRESENT], 0); + mutex_unlock(&f.v14->lock); + + v14_bl_request(test, f.dcp, 250); + mutex_lock(&f.v14->lock); + KUNIT_EXPECT_TRUE(test, dcp_v14_swap_nits(f.v14, swap, &present)); + KUNIT_EXPECT_EQ(test, swap[DCP_V14_SWAP_NITS_PRESENT], 1); + KUNIT_EXPECT_EQ(test, get_unaligned_le64(swap + DCP_V14_SWAP_NITS), + dcp_v14_nits_binary64(250)); + /* Refused: no actual level, a retry queued. */ + dcp_v14_swap_nits_done(f.v14, &present, false); + mutex_unlock(&f.v14->lock); + KUNIT_EXPECT_FALSE(test, dcp_backlight_level_known(f.dcp)); + KUNIT_EXPECT_EQ(test, f.v14->nits_refused, 1ULL); + KUNIT_EXPECT_TRUE(test, delayed_work_pending(&f.v14->backlight_work)); + cancel_delayed_work_sync(&f.v14->backlight_work); + + /* While a Type-C display has the pipe, no level either. */ + f.dcp->fixed_connector_type = DRM_MODE_CONNECTOR_USB; + memset(swap, 0, DCP_V14_SWAP_SIZE); + mutex_lock(&f.v14->lock); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_nits(f.v14, swap, &present)); + mutex_unlock(&f.v14->lock); + KUNIT_EXPECT_EQ(test, swap[DCP_V14_SWAP_NITS_PRESENT], 0); + f.dcp->fixed_connector_type = DRM_MODE_CONNECTOR_eDP; + + mutex_lock(&f.v14->lock); + KUNIT_EXPECT_TRUE(test, dcp_v14_swap_nits(f.v14, swap, &present)); + dcp_v14_swap_nits_done(f.v14, &present, true); + mutex_unlock(&f.v14->lock); + KUNIT_EXPECT_TRUE(test, dcp_backlight_level_known(f.dcp)); + KUNIT_EXPECT_EQ(test, f.v14->nits_sent, 250U); + KUNIT_EXPECT_TRUE(test, dcp_v14_bl_dpms_applies(f.v14, f.dcp)); + + /* Unchanged: not sent again. */ + mutex_lock(&f.v14->lock); + KUNIT_EXPECT_FALSE(test, dcp_v14_swap_nits(f.v14, swap, &present)); + mutex_unlock(&f.v14->lock); +} + +/* + * The gate, through the production queue path: open, a kick queues the work; + * suspended, nothing queues until resume; closed, nothing ever does. The + * caller holds lock, so a queued work stays queued or running. + */ +static void v14_bl_t6030_gate(struct kunit *test) +{ + struct v14_bl_fixture f = v14_bl_fixture(test, &dcp_v14_board_t6030, true); + + mutex_lock(&f.v14->lock); + dcp_v14_backlight_kick(f.dcp); + KUNIT_EXPECT_TRUE(test, v14_bl_queued(f.v14)); + mutex_unlock(&f.v14->lock); + cancel_delayed_work_sync(&f.v14->backlight_work); + + iomfb_v14_7_backlight_suspend(f.dcp); + KUNIT_EXPECT_EQ(test, f.v14->bl_gate, DCP_V14_BL_SUSPENDED); + mutex_lock(&f.v14->lock); + dcp_v14_backlight_kick(f.dcp); + dcp_v14_bl_queue(f.v14, 0); + KUNIT_EXPECT_FALSE(test, v14_bl_queued(f.v14)); + mutex_unlock(&f.v14->lock); + + iomfb_v14_7_backlight_resume(f.dcp); + KUNIT_EXPECT_EQ(test, f.v14->bl_gate, DCP_V14_BL_OPEN); + mutex_lock(&f.v14->lock); + dcp_v14_backlight_kick(f.dcp); + KUNIT_EXPECT_TRUE(test, v14_bl_queued(f.v14)); + mutex_unlock(&f.v14->lock); + cancel_delayed_work_sync(&f.v14->backlight_work); + + /* A quiescing DCP takes no kick either. */ + WRITE_ONCE(f.dcp->quiescing, true); + mutex_lock(&f.v14->lock); + dcp_v14_backlight_kick(f.dcp); + KUNIT_EXPECT_FALSE(test, v14_bl_queued(f.v14)); + mutex_unlock(&f.v14->lock); + WRITE_ONCE(f.dcp->quiescing, false); + + dcp_v14_bl_shut(f.v14, true); + KUNIT_EXPECT_EQ(test, f.v14->bl_gate, DCP_V14_BL_CLOSED); + iomfb_v14_7_backlight_resume(f.dcp); + KUNIT_EXPECT_EQ(test, f.v14->bl_gate, DCP_V14_BL_CLOSED); + mutex_lock(&f.v14->lock); + dcp_v14_backlight_kick(f.dcp); + dcp_v14_bl_queue(f.v14, 0); + KUNIT_EXPECT_FALSE(test, v14_bl_queued(f.v14)); + mutex_unlock(&f.v14->lock); +} + +/* + * The work itself on the T6030 panel, with no DCP: blanked (no frame) it + * leaves the level to the next present; with the gate shut, or while + * suspended, it does nothing. None of these may reach an RPC. + */ +static void v14_bl_t6030_work(struct kunit *test) +{ + struct v14_bl_fixture f = v14_bl_fixture(test, &dcp_v14_board_t6030, true); + + v14_bl_request(test, f.dcp, 250); + WRITE_ONCE(f.dcp->mode_state.valid, true); + /* No frame shown: the level stays pending for the next present. */ + dcp_v14_backlight_work(&f.v14->backlight_work.work); + KUNIT_EXPECT_EQ(test, f.v14->brightness_presents, 0ULL); + KUNIT_EXPECT_TRUE(test, dcp_backlight_pending(f.dcp)); + + iomfb_v14_7_backlight_suspend(f.dcp); + dcp_v14_backlight_work(&f.v14->backlight_work.work); + KUNIT_EXPECT_TRUE(test, dcp_backlight_pending(f.dcp)); + iomfb_v14_7_backlight_resume(f.dcp); + + dcp_v14_bl_shut(f.v14, true); + dcp_v14_backlight_work(&f.v14->backlight_work.work); + KUNIT_EXPECT_EQ(test, f.v14->brightness_presents, 0ULL); + KUNIT_EXPECT_TRUE(test, dcp_backlight_pending(f.dcp)); +} + +static struct kunit_case v14_backlight_cases[] = { + KUNIT_CASE(v14_bl_binary64), + KUNIT_CASE(v14_bl_level_block), + KUNIT_CASE(v14_bl_swap_unchanged_without_level), + KUNIT_CASE(v14_bl_boards), + KUNIT_CASE(v14_bl_gate_transitions), + KUNIT_CASE(v14_bl_plan), + KUNIT_CASE(v14_bl_policy), + KUNIT_CASE(v14_bl_other_boards_untouched), + KUNIT_CASE(v14_bl_external_untouched), + KUNIT_CASE(v14_bl_t6030_swap_levels), + KUNIT_CASE(v14_bl_t6030_gate), + KUNIT_CASE(v14_bl_t6030_work), + {} +}; + +static struct kunit_suite v14_backlight_suite = { + .name = "dcp_v14_backlight", + .test_cases = v14_backlight_cases, +}; + +kunit_test_suite(v14_backlight_suite); diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_board.h b/drivers/gpu/drm/apple/iomfb_v14_7_board.h index 89d6a26ecb06c..6de1ae9004122 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_board.h +++ b/drivers/gpu/drm/apple/iomfb_v14_7_board.h @@ -46,6 +46,8 @@ struct dcp_v14_board { * 16.7 ms, so a 120 Hz panel presents at 60 fps. */ bool swap_timestamps; + /* This board has a qualified panel level block; machine admission is separate. */ + bool panel_nits; }; /* Whether the DCP node or the machine (as @ctx gives them) has @compat. */ diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_swap.h b/drivers/gpu/drm/apple/iomfb_v14_7_swap.h index 7fe32cabfae65..382e73ee99e61 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_swap.h +++ b/drivers/gpu/drm/apple/iomfb_v14_7_swap.h @@ -3,6 +3,9 @@ #ifndef __APPLE_IOMFB_V14_7_SWAP_H__ #define __APPLE_IOMFB_V14_7_SWAP_H__ +#include +#include +#include #include #include #include @@ -28,6 +31,54 @@ #define DCP_V14_SWAP_LAYERS 0x7 #define DCP_V14_SWAP_SET_BACKGROUND BIT(31) +/* + * The panel level block of the swap record (the 14.7 J514S/J516S panel): a + * present byte at +0x354 and the level in nits as an unaligned IEEE 754 + * binary64 at +0x35c, both zero when the level does not change. The same + * layout as drivers/gpu/drm/apple-m3/iomfb_v14_7.h (bl_nits_present, + * bl_nits) and omacom/linux-aurora PR148. + */ +#define DCP_V14_SWAP_NITS_PRESENT 0x354 +#define DCP_V14_SWAP_NITS 0x35c +/* The nits field takes integral levels up to the panel maximum. */ +#define DCP_V14_NITS_MAX 500 +/* + * The level userspace is shown before Linux has set one. It is never + * presented on its own: until a level is requested the panel keeps the boot + * loader's level, which Linux does not know. + */ +#define DCP_V14_NITS_DEFAULT 140 + +static_assert(DCP_V14_SWAP_NITS_PRESENT >= DCP_V14_SWAP_BACKGROUND + 4); +static_assert(DCP_V14_SWAP_NITS >= DCP_V14_SWAP_NITS_PRESENT + 1); +static_assert(DCP_V14_SWAP_NITS + 8 <= DCP_V14_SWAP_SURFACE(0)); + +/* Integral nits as IEEE 754 binary64, without kernel floating point. */ +static inline u64 dcp_v14_nits_binary64(u32 nits) +{ + unsigned int bit; + + if (!nits) + return 0; + bit = fls(nits) - 1; + return ((u64)(1023 + bit) << 52) | + ((u64)(nits - (1U << bit)) << (52 - bit)); +} + +/* + * The level block of an encoded swap: @nits when @change, else none. A level + * above the wire bound leaves the record untouched. + */ +static inline int dcp_v14_encode_nits(u8 *swap, bool change, u32 nits) +{ + if (change && nits > DCP_V14_NITS_MAX) + return -EINVAL; + swap[DCP_V14_SWAP_NITS_PRESENT] = change; + put_unaligned_le64(change ? dcp_v14_nits_binary64(nits) : 0, + swap + DCP_V14_SWAP_NITS); + return 0; +} + /* Offsets in a surface record. */ #define DCP_V14_SURF_OPAQUE 0x02 #define DCP_V14_SURF_FORMAT 0x0b From 8cb8074ad4ff55c88f7a180d5e324205932adb1b Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+iconidentify@users.noreply.github.com> Date: Thu, 22 Oct 2026 16:07:41 -0700 Subject: [PATCH 03/28] drm/apple: initialize backlight KUnit GEM and mode fixtures Give the framebuffer fixtures real private GEM objects on a GEM-capable DRM device. Release their initial references after the framebuffer and active-scanout references, with the matching public object release. Initialize the mode-state spinlock used by the actual unbind path. Set the fixture panel dimensions so real DPMS dispatch also detects a regression that routes this panel through H17P soft DPMS. Exercise the J514S admission branch as well as J516S and the other-machine refusal. Production code is unchanged. Generated-by: LLM Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com> (cherry picked from commit fb8f91beaed57fa5b1a5e730d197f43b23e1d6de) Assisted-by: LLM Signed-off-by: Ryan Murray --- .../iomfb_v14_7_backlight_lifetime_test.c | 19 +++++++++++++++++-- .../drm/apple/iomfb_v14_7_backlight_test.c | 3 +++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c index 291aecd0e277c..4eb640a19a7e2 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c @@ -27,7 +27,7 @@ struct v14_bl_rpc { struct drm_device *drm; struct drm_framebuffer fb[2]; struct drm_gem_dma_object gem[2]; - unsigned int initialized; + unsigned int initialized, gems_initialized; struct v14_bl_actor actors[2]; struct completion rpc_entered, rpc_release; atomic_t submits; @@ -113,6 +113,8 @@ static void v14_bl_rpc_cleanup(void *data) } for (i = 0; i < c->initialized; i++) drm_framebuffer_put(&c->fb[i]); + for (i = 0; i < c->gems_initialized; i++) + drm_gem_object_put(&c->gem[i].base); v14_bl_rpc_current = NULL; } @@ -121,6 +123,15 @@ static void v14_bl_fb_destroy(struct drm_framebuffer *fb) drm_framebuffer_cleanup(fb); } +static void v14_bl_gem_free(struct drm_gem_object *obj) +{ + drm_gem_object_release(obj); +} + +static const struct drm_gem_object_funcs v14_bl_gem_funcs = { + .free = v14_bl_gem_free, +}; + static const struct drm_framebuffer_funcs v14_bl_fb_funcs = { .destroy = v14_bl_fb_destroy, }; @@ -137,7 +148,7 @@ static struct v14_bl_rpc *v14_bl_rpc_setup(struct kunit *test) dev = drm_kunit_helper_alloc_device(test); KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev); c->drm = __drm_kunit_helper_alloc_drm_device(test, dev, sizeof(*c->drm), 0, - DRIVER_MODESET); + DRIVER_MODESET | DRIVER_GEM); KUNIT_ASSERT_NOT_ERR_OR_NULL(test, c->drm); init_completion(&c->rpc_entered); init_completion(&c->rpc_release); @@ -150,6 +161,9 @@ static struct v14_bl_rpc *v14_bl_rpc_setup(struct kunit *test) c->fb[i].format = drm_format_info(DRM_FORMAT_XRGB8888); c->fb[i].width = c->fb[i].height = 64; c->fb[i].pitches[0] = 256; + drm_gem_private_object_init(c->drm, &c->gem[i].base, PAGE_SIZE); + c->gem[i].base.funcs = &v14_bl_gem_funcs; + c->gems_initialized++; c->gem[i].dma_addr = 0x100000 + i * 0x10000; c->fb[i].obj[0] = &c->gem[i].base; KUNIT_ASSERT_EQ(test, drm_framebuffer_init(c->drm, &c->fb[i], &v14_bl_fb_funcs), 0); @@ -158,6 +172,7 @@ static struct v14_bl_rpc *v14_bl_rpc_setup(struct kunit *test) c->f.v14->panel_width = c->f.v14->panel_height = 64; c->f.dcp->fixed_connector_type = DRM_MODE_CONNECTOR_eDP; c->f.dcp->connector_type = DRM_MODE_CONNECTOR_eDP; + c->f.dcp->panel.width_mm = 200; c->f.dcp->mode_state.valid = true; c->f.dcp->crtc = devm_kzalloc(c->f.dcp->dev, sizeof(*c->f.dcp->crtc), GFP_KERNEL); KUNIT_ASSERT_NOT_NULL(test, c->f.dcp->crtc); diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c index 7a2aba44e4857..e60d45991ec5a 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c @@ -144,6 +144,8 @@ static void v14_bl_boards(struct kunit *test) board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, t6030_j514s, &known); KUNIT_ASSERT_NOT_NULL(test, board); KUNIT_EXPECT_TRUE(test, board->panel_nits); + KUNIT_EXPECT_TRUE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, + t6030_j514s)); board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, other_t6030, &known); KUNIT_ASSERT_NOT_NULL(test, board); KUNIT_EXPECT_FALSE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, other_t6030)); @@ -293,6 +295,7 @@ static struct v14_bl_fixture v14_bl_fixture(struct kunit *test, KUNIT_ASSERT_NOT_NULL(test, f.dcp); KUNIT_ASSERT_NOT_NULL(test, f.v14); f.dcp->dev = dev; + spin_lock_init(&f.dcp->mode_state.lock); mutex_init(&f.dcp->bl_register_mutex); INIT_WORK(&f.dcp->bl_register_wq, dcp_work_register_backlight); INIT_WORK(&f.dcp->bl_update_wq, v14_bl_unused_work); From 572c9be85ea19cb3c763a3e24dd5ac21a92b4a8a Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+iconidentify@users.noreply.github.com> Date: Thu, 22 Oct 2026 16:11:58 -0700 Subject: [PATCH 04/28] drm/apple: tidy the panel backlight port formatting Wrap and align the new declarations and calls, describe the gate spinlock, and remove an extra blank line. Split two fixture field assignments and write its null-callback assertion as a boolean expectation. Document that resume reopens admission and leaves a pending level for the next normal CRTC restore present. The production C token stream is unchanged after removing comments and whitespace. Keep the qualified encoder's constant-only layout assertions; their checkpatch comparison warning does not indicate an operand-order bug. New files are already covered by the Apple DRM maintainer path. Generated-by: LLM Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com> (cherry picked from commit 964323ebf32a21656f63cd27bde72610d4eccb09) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp_backlight.c | 3 +- drivers/gpu/drm/apple/iomfb_v14_7.c | 29 ++++++++++--------- .../iomfb_v14_7_backlight_lifetime_test.c | 22 ++++++++------ .../drm/apple/iomfb_v14_7_backlight_test.c | 11 +++---- 4 files changed, 37 insertions(+), 28 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index 5c808438d5df6..27ef6f370391b 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -834,7 +834,8 @@ int dcp_backlight_register(struct apple_dcp *dcp) bl_dev = devm_backlight_device_register(dev, "apple-panel-bl", dev, dcp, iomfb_v14_7_panel_nits(dcp) ? - &dcp_backlight_ops_v14 : &dcp_backlight_ops, &props); + &dcp_backlight_ops_v14 : &dcp_backlight_ops, + &props); if (IS_ERR(bl_dev)) return PTR_ERR(bl_dev); diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index e8ec2f265ff88..56b08ebc03487 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -298,6 +298,7 @@ struct apple_dcp_v14 { u64 shown_iova; u32 shown_width, shown_height, shown_dst_y; struct delayed_work backlight_work; + /* Serializes gate transitions with queue admission. */ spinlock_t bl_gate_lock; enum dcp_v14_bl_gate bl_gate; u32 nits_sent; @@ -428,6 +429,7 @@ void iomfb_v14_7_backlight_suspend(struct apple_dcp *dcp) dcp_v14_bl_shut(dcp->v14, false); } +/* Pending levels ride the next normal CRTC restore present after admission opens. */ void iomfb_v14_7_backlight_resume(struct apple_dcp *dcp) { struct apple_dcp_v14 *v14 = dcp->v14; @@ -480,7 +482,7 @@ static int dcp_v14_call_guarded(struct apple_dcp_v14 *v14, u32 tag, const void * int ret; KUNIT_STATIC_STUB_REDIRECT(dcp_v14_call_guarded, v14, tag, in, in_size, - out, out_size, completion, admit, admit_cookie); + out, out_size, completion, admit, admit_cookie); if (v14->failed) return -EIO; dev_dbg(v14->dev, "call %#x in %u out %u\n", tag, in_size, out_size); @@ -1684,7 +1686,7 @@ const struct dcp_v14_board *iomfb_v14_7_board(struct device *dev) } static bool dcp_v14_panel_nits_admit(const struct dcp_v14_board *board, - dcp_v14_compat_fn machine_is, const void *ctx) + dcp_v14_compat_fn machine_is, const void *ctx) { return board && board->panel_nits && (machine_is(ctx, "apple,j514s") || machine_is(ctx, "apple,j516s")); @@ -2789,8 +2791,8 @@ static void dcp_v14_swap_nits_done(struct apple_dcp_v14 *v14, * @ctm_state, if set, carries the colour matrix to send first. */ static int dcp_v14_swap_locked(struct apple_dcp_v14 *v14, const u8 *surface, u64 iova, - u32 width, u32 height, u32 dst_y, - const struct drm_crtc_state *ctm_state) + u32 width, u32 height, u32 dst_y, + const struct drm_crtc_state *ctm_state) { struct dcp_v14_swap_admission admission = { .v14 = v14 }; __le32 start[4] = {}, started[2] = {}, result[3]; @@ -2849,13 +2851,14 @@ static int dcp_v14_swap_locked(struct apple_dcp_v14 *v14, const u8 *surface, u64 /* Publish a successful swap while the RPC lock still pins both surfaces. */ static struct drm_framebuffer *dcp_v14_publish_surface(struct apple_dcp_v14 *v14, - struct drm_framebuffer *fb, const u8 *surface, u64 iova, - u32 width, u32 height, u32 dst_y) + struct drm_framebuffer *fb, + const u8 *surface, u64 iova, + u32 width, u32 height, u32 dst_y) { struct drm_framebuffer *old; KUNIT_STATIC_STUB_REDIRECT(dcp_v14_publish_surface, v14, fb, surface, iova, - width, height, dst_y); + width, height, dst_y); lockdep_assert_held(&v14->lock); old = v14->active_fb; v14->active_fb = fb; @@ -2875,9 +2878,9 @@ static struct drm_framebuffer *dcp_v14_publish_surface(struct apple_dcp_v14 *v14 * and keeps @fb until the next swap. */ static bool dcp_v14_present_locked(struct apple_dcp_v14 *v14, struct drm_framebuffer *fb, - u32 width, u32 height, u32 dst_y, - const struct drm_crtc_state *ctm_state, - struct drm_framebuffer **retired) + u32 width, u32 height, u32 dst_y, + const struct drm_crtc_state *ctm_state, + struct drm_framebuffer **retired) { u8 surface[DCP_V14_SURFACE_SIZE]; struct drm_framebuffer *old; @@ -2895,7 +2898,7 @@ static bool dcp_v14_present_locked(struct apple_dcp_v14 *v14, struct drm_framebu } start = ktime_get_ns(); ret = dcp_v14_swap_locked(v14, fb ? surface : NULL, iova, fb ? width : 0, - fb ? height : 0, dst_y, ctm_state); + fb ? height : 0, dst_y, ctm_state); elapsed = ktime_get_ns() - start; if (ret && v14->external && !READ_ONCE(v14->failed)) { /* Refused before it was taken: the old framebuffer stays on screen. */ @@ -2966,8 +2969,8 @@ static void dcp_v14_backlight_work(struct work_struct *work) }; if (dcp_v14_bl_plan(&facts) != DCP_V14_BL_REPRESENT) return; - ret = dcp_v14_swap_locked(v14, v14->shown_surface, v14->shown_iova, v14->shown_width, v14->shown_height, - v14->shown_dst_y, NULL); + ret = dcp_v14_swap_locked(v14, v14->shown_surface, v14->shown_iova, + v14->shown_width, v14->shown_height, v14->shown_dst_y, NULL); if (!ret) v14->brightness_presents++; else if (ret != -EAGAIN) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c index 4eb640a19a7e2..046c05b3043ca 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c @@ -148,7 +148,7 @@ static struct v14_bl_rpc *v14_bl_rpc_setup(struct kunit *test) dev = drm_kunit_helper_alloc_device(test); KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev); c->drm = __drm_kunit_helper_alloc_drm_device(test, dev, sizeof(*c->drm), 0, - DRIVER_MODESET | DRIVER_GEM); + DRIVER_MODESET | DRIVER_GEM); KUNIT_ASSERT_NOT_ERR_OR_NULL(test, c->drm); init_completion(&c->rpc_entered); init_completion(&c->rpc_release); @@ -159,7 +159,8 @@ static struct v14_bl_rpc *v14_bl_rpc_setup(struct kunit *test) for (i = 0; i < ARRAY_SIZE(c->fb); i++) { c->fb[i].dev = c->drm; c->fb[i].format = drm_format_info(DRM_FORMAT_XRGB8888); - c->fb[i].width = c->fb[i].height = 64; + c->fb[i].width = 64; + c->fb[i].height = 64; c->fb[i].pitches[0] = 256; drm_gem_private_object_init(c->drm, &c->gem[i].base, PAGE_SIZE); c->gem[i].base.funcs = &v14_bl_gem_funcs; @@ -169,7 +170,8 @@ static struct v14_bl_rpc *v14_bl_rpc_setup(struct kunit *test) KUNIT_ASSERT_EQ(test, drm_framebuffer_init(c->drm, &c->fb[i], &v14_bl_fb_funcs), 0); c->initialized++; } - c->f.v14->panel_width = c->f.v14->panel_height = 64; + c->f.v14->panel_width = 64; + c->f.v14->panel_height = 64; c->f.dcp->fixed_connector_type = DRM_MODE_CONNECTOR_eDP; c->f.dcp->connector_type = DRM_MODE_CONNECTOR_eDP; c->f.dcp->panel.width_mm = 200; @@ -182,8 +184,8 @@ static struct v14_bl_rpc *v14_bl_rpc_setup(struct kunit *test) } static struct v14_bl_actor *v14_bl_actor_start(struct kunit *test, - struct v14_bl_rpc *c, unsigned int slot, - unsigned int op) + struct v14_bl_rpc *c, unsigned int slot, + unsigned int op) { struct v14_bl_actor *a = &c->actors[slot]; @@ -200,8 +202,9 @@ static struct v14_bl_actor *v14_bl_actor_start(struct kunit *test, } static struct drm_framebuffer *v14_bl_pause_publication(struct apple_dcp_v14 *v14, - struct drm_framebuffer *fb, const u8 *surface, u64 iova, - u32 width, u32 height, u32 dst_y) + struct drm_framebuffer *fb, + const u8 *surface, u64 iova, + u32 width, u32 height, u32 dst_y) { struct v14_bl_rpc *c = v14_bl_rpc_current; @@ -222,7 +225,8 @@ static void v14_bl_present_publication_serialized(struct kunit *test) first = v14_bl_actor_start(test, c, 0, 0); KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&c->rpc_entered, HZ), 0UL); second = v14_bl_actor_start(test, c, 1, 1); - KUNIT_EXPECT_EQ(test, wait_for_completion_timeout(&second->done, msecs_to_jiffies(20)), 0UL); + KUNIT_EXPECT_EQ(test, + wait_for_completion_timeout(&second->done, msecs_to_jiffies(20)), 0UL); KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 1); complete_all(&c->rpc_release); KUNIT_ASSERT_NE(test, wait_for_completion_timeout(&first->done, HZ), 0UL); @@ -324,7 +328,7 @@ static void v14_bl_registration_unbind_race(struct kunit *test) mutex_lock(&c->f.dcp->bl_register_mutex); KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, v14_bl_mutex_unlock, - &c->f.dcp->bl_register_mutex), 0); + &c->f.dcp->bl_register_mutex), 0); KUNIT_EXPECT_TRUE(test, schedule_work(&c->f.dcp->bl_register_wq)); deadline = jiffies + HZ; do { diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c index e60d45991ec5a..b594da4c0b455 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c @@ -21,7 +21,6 @@ static void v14_bl_unused_work(struct work_struct *work) { } - static void v14_bl_binary64(struct kunit *test) { static const struct { @@ -140,15 +139,17 @@ static void v14_bl_boards(struct kunit *test) board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, t6030_j516s, &known); KUNIT_ASSERT_NOT_NULL(test, board); KUNIT_EXPECT_TRUE(test, board->panel_nits); - KUNIT_EXPECT_TRUE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, t6030_j516s)); + KUNIT_EXPECT_TRUE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, + t6030_j516s)); board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, t6030_j514s, &known); KUNIT_ASSERT_NOT_NULL(test, board); KUNIT_EXPECT_TRUE(test, board->panel_nits); KUNIT_EXPECT_TRUE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, - t6030_j514s)); + t6030_j514s)); board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, other_t6030, &known); KUNIT_ASSERT_NOT_NULL(test, board); - KUNIT_EXPECT_FALSE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, other_t6030)); + KUNIT_EXPECT_FALSE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, + other_t6030)); board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, j613, &known); KUNIT_ASSERT_NOT_NULL(test, board); KUNIT_EXPECT_FALSE(test, board->panel_nits); @@ -379,7 +380,7 @@ static void v14_bl_other_boards_untouched(struct kunit *test) iomfb_v14_7_backlight_suspend(f.dcp); KUNIT_EXPECT_EQ(test, f.v14->bl_gate, DCP_V14_BL_OPEN); dcp_v14_configure_backlight(f.dcp); - KUNIT_EXPECT_TRUE(test, f.dcp->backlight.kick == NULL); + KUNIT_EXPECT_FALSE(test, f.dcp->backlight.kick); } } From 985a20a0bf881e7395b574a51238a54201b436b3 Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+iconidentify@users.noreply.github.com> Date: Thu, 22 Oct 2026 16:11:58 -0700 Subject: [PATCH 05/28] drm/apple: check the suspended backlight worker admission After the running brightness present and suspend have completed, leave a new level pending and invoke the real callback as a previously queued callback could arrive. It must refuse the level without another RPC even though the framebuffer is still pinned. This separately checks callback admission as well as the queue-side gate. Generated-by: LLM Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com> (cherry picked from commit f4ef473afffcd52075db09bdd13a71d482b39421) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c index 046c05b3043ca..62ee5eedbbb57 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c @@ -263,6 +263,9 @@ static void v14_bl_running_work_suspend(struct kunit *test) v14_bl_request(test, c->f.dcp, 250); dcp_v14_backlight_kick(c->f.dcp); KUNIT_EXPECT_FALSE(test, work_busy(&c->f.v14->backlight_work.work)); + /* A callback queued before the gate shut must also refuse the pending level. */ + dcp_v14_backlight_work(&c->f.v14->backlight_work.work); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 1); } /* Actual dcp_poweroff/on must reach the 14.7 blank/restore hooks. */ From 63123a1ae1ea34e51319fe1581b330e9bdd3a737 Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+iconidentify@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:15:56 -0700 Subject: [PATCH 06/28] drm/apple: address the panel backlight review A refused internal swap latches the panel failure state. Do not queue a brightness retry that cannot be admitted after that failure; exercise the real refused swap and subsequent work admission in the lifecycle test. Declare the KUnit-visible registration worker in the shared internal header so its definition has a prototype in KUnit builds. Replace the nonexistent variant-tree reference with the public source commit. Keep fbdev unblank notifications from sending the advertised default before a level is known. Exercise the registered class operations before and after a real present, and take the DPMS and suspend controls through the restore present and its encoded level. Generated-by: LLM Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com> [ryan: dropped the Link: trailer to the review on the omacom tree] (cherry picked from commit 40eb318ba3db66e1702427f1d51892b760d30a28) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/dcp-internal.h | 1 + drivers/gpu/drm/apple/dcp_backlight.c | 12 ++++- drivers/gpu/drm/apple/iomfb_v14_7.c | 5 +- .../iomfb_v14_7_backlight_lifetime_test.c | 47 ++++++++++++++++--- .../drm/apple/iomfb_v14_7_backlight_test.c | 9 ++-- drivers/gpu/drm/apple/iomfb_v14_7_swap.h | 4 +- 6 files changed, 58 insertions(+), 20 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 32b39de3c7fb4..b5a85bc29708e 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -884,6 +884,7 @@ static inline void dcp_test_point(struct apple_dcp *dcp, enum dcp_test_point poi } #if IS_ENABLED(CONFIG_KUNIT) +void dcp_work_register_backlight(struct work_struct *work); void dcp_comp_unbind(struct device *dev, struct device *main, void *data); void dcp_platform_shutdown(struct platform_device *pdev); void dcp_disable_typec_work(struct apple_dcp *dcp, bool release_cable); diff --git a/drivers/gpu/drm/apple/dcp_backlight.c b/drivers/gpu/drm/apple/dcp_backlight.c index 27ef6f370391b..223a462afced8 100644 --- a/drivers/gpu/drm/apple/dcp_backlight.c +++ b/drivers/gpu/drm/apple/dcp_backlight.c @@ -769,10 +769,20 @@ static const struct backlight_ops dcp_backlight_ops = { .controls_device = dcp_backlight_controls_device, }; +static bool dcp_backlight_controls_device_v14(struct backlight_device *bd, + struct device *display_dev) +{ + struct apple_dcp *dcp = bl_get_data(bd); + + /* An fbdev unblank must not present the unrequested default level. */ + return dcp_backlight_level_known(dcp) && + dcp_backlight_controls_device(bd, display_dev); +} + static const struct backlight_ops dcp_backlight_ops_v14 = { .get_brightness = dcp_get_brightness, .update_status = dcp_set_brightness, - .controls_device = dcp_backlight_controls_device, + .controls_device = dcp_backlight_controls_device_v14, }; int dcp_backlight_register(struct apple_dcp *dcp) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 56b08ebc03487..681c50972dc3a 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -2769,7 +2769,6 @@ static void dcp_v14_swap_nits_done(struct apple_dcp_v14 *v14, const struct dcp_backlight_present *present, bool accepted) { struct apple_dcp *dcp = READ_ONCE(v14->dcp); - unsigned int delay; if (!dcp) return; @@ -2781,9 +2780,7 @@ static void dcp_v14_swap_nits_done(struct apple_dcp_v14 *v14, return; } v14->nits_refused++; - delay = dcp_backlight_retry_delay(dcp); - if (delay && !READ_ONCE(v14->failed)) - dcp_v14_bl_queue(v14, msecs_to_jiffies(delay)); + /* The internal swap failure is final: swap_locked latches failed. */ } /* diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c index 62ee5eedbbb57..f993c4c1c4f65 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c @@ -9,9 +9,6 @@ #include #include -/* The real shared registration worker, visible only with CONFIG_KUNIT. */ -void dcp_work_register_backlight(struct work_struct *work); - struct v14_bl_rpc; struct v14_bl_actor { struct v14_bl_rpc *ctx; @@ -31,9 +28,9 @@ struct v14_bl_rpc { struct v14_bl_actor actors[2]; struct completion rpc_entered, rpc_release; atomic_t submits; - u64 iova[4], nits[4]; - bool nits_present[4]; - struct drm_framebuffer *owner[4]; + u64 iova[5], nits[5]; + bool nits_present[5]; + struct drm_framebuffer *owner[5]; bool block_first, refuse; }; @@ -266,6 +263,11 @@ static void v14_bl_running_work_suspend(struct kunit *test) /* A callback queued before the gate shut must also refuse the pending level. */ dcp_v14_backlight_work(&c->f.v14->backlight_work.work); KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 1); + iomfb_v14_7_backlight_resume(c->f.dcp); + KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 2); + KUNIT_EXPECT_TRUE(test, c->nits_present[1]); + KUNIT_EXPECT_EQ(test, c->nits[1], dcp_v14_nits_binary64(250)); } /* Actual dcp_poweroff/on must reach the 14.7 blank/restore hooks. */ @@ -297,6 +299,11 @@ static void v14_bl_dpms_dispatch(struct kunit *test) dcp_poweron(pdev); KUNIT_EXPECT_TRUE(test, dcp_backlight_pending(c->f.dcp)); KUNIT_EXPECT_FALSE(test, c->f.dcp->backlight.state.dpms_off); + KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 5); + KUNIT_EXPECT_TRUE(test, c->nits_present[4]); + KUNIT_EXPECT_EQ(test, c->nits[4], dcp_v14_nits_binary64(DCP_V14_NITS_DEFAULT)); + KUNIT_EXPECT_FALSE(test, dcp_backlight_pending(c->f.dcp)); } /* An actual failed submit must leave the loader's level unknown. */ @@ -306,7 +313,6 @@ static void v14_bl_failed_submit_unknown(struct kunit *test) c->refuse = true; v14_bl_request(test, c->f.dcp, 240); - dcp_v14_bl_shut(c->f.v14, true); mutex_lock(&c->f.v14->lock); KUNIT_EXPECT_EQ(test, dcp_v14_swap_locked(c->f.v14, NULL, 0, 0, 0, 0, NULL), -EIO); mutex_unlock(&c->f.v14->lock); @@ -314,6 +320,11 @@ static void v14_bl_failed_submit_unknown(struct kunit *test) KUNIT_EXPECT_EQ(test, c->f.v14->nits_presents, 0ULL); KUNIT_EXPECT_TRUE(test, c->f.v14->failed); KUNIT_EXPECT_FALSE(test, work_busy(&c->f.v14->backlight_work.work)); + /* Neither a later kick nor a work callback may retry the failed panel. */ + dcp_v14_backlight_kick(c->f.dcp); + dcp_v14_backlight_work(&c->f.v14->backlight_work.work); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 1); + KUNIT_EXPECT_FALSE(test, work_busy(&c->f.v14->backlight_work.work)); } static void v14_bl_mutex_unlock(void *data) @@ -369,6 +380,27 @@ static void v14_bl_class_unregistered(struct kunit *test) KUNIT_EXPECT_EQ(test, c->f.v14->bl_gate, DCP_V14_BL_CLOSED); } +/* fbdev cannot turn the advertised default into the first brightness request. */ +static void v14_bl_fbdev_unknown_level(struct kunit *test) +{ + struct v14_bl_rpc *c = v14_bl_rpc_setup(test); + struct device unrelated = {}; + struct backlight_device *bd; + + schedule_work(&c->f.dcp->bl_register_wq); + flush_work(&c->f.dcp->bl_register_wq); + bd = c->f.dcp->brightness.bl_dev; + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, bd); + KUNIT_EXPECT_FALSE(test, bd->ops->controls_device(bd, NULL)); + KUNIT_EXPECT_FALSE(test, dcp_backlight_pending(c->f.dcp)); + KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 0); + v14_bl_request(test, c->f.dcp, 250); + KUNIT_ASSERT_TRUE(test, dcp_v14_present(c->f.v14, &c->fb[0], 64, 64, 0, NULL)); + KUNIT_EXPECT_TRUE(test, bd->ops->controls_device(bd, NULL)); + KUNIT_EXPECT_TRUE(test, bd->ops->controls_device(bd, c->f.dcp->dev)); + KUNIT_EXPECT_FALSE(test, bd->ops->controls_device(bd, &unrelated)); +} + static struct kunit_case v14_bl_lifetime_cases[] = { KUNIT_CASE(v14_bl_present_publication_serialized), KUNIT_CASE(v14_bl_running_work_suspend), @@ -376,6 +408,7 @@ static struct kunit_case v14_bl_lifetime_cases[] = { KUNIT_CASE(v14_bl_failed_submit_unknown), KUNIT_CASE(v14_bl_registration_unbind_race), KUNIT_CASE(v14_bl_class_unregistered), + KUNIT_CASE(v14_bl_fbdev_unknown_level), {} }; diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c index b594da4c0b455..d2e56c427f368 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c @@ -15,8 +15,6 @@ #include #include -void dcp_work_register_backlight(struct work_struct *work); - static void v14_bl_unused_work(struct work_struct *work) { } @@ -407,7 +405,7 @@ static void v14_bl_external_untouched(struct kunit *test) /* * The T6030 panel: no level until one is requested; the requested level is - * encoded; a refusal keeps the actual level and is retried; an accepted one + * encoded; a refusal keeps the actual level without a retry; an accepted one * is the actual level, and only then does blanking follow DPMS. */ static void v14_bl_t6030_swap_levels(struct kunit *test) @@ -434,13 +432,12 @@ static void v14_bl_t6030_swap_levels(struct kunit *test) KUNIT_EXPECT_EQ(test, swap[DCP_V14_SWAP_NITS_PRESENT], 1); KUNIT_EXPECT_EQ(test, get_unaligned_le64(swap + DCP_V14_SWAP_NITS), dcp_v14_nits_binary64(250)); - /* Refused: no actual level, a retry queued. */ + /* Refused: no actual level, and the panel must not queue a retry. */ dcp_v14_swap_nits_done(f.v14, &present, false); mutex_unlock(&f.v14->lock); KUNIT_EXPECT_FALSE(test, dcp_backlight_level_known(f.dcp)); KUNIT_EXPECT_EQ(test, f.v14->nits_refused, 1ULL); - KUNIT_EXPECT_TRUE(test, delayed_work_pending(&f.v14->backlight_work)); - cancel_delayed_work_sync(&f.v14->backlight_work); + KUNIT_EXPECT_FALSE(test, delayed_work_pending(&f.v14->backlight_work)); /* While a Type-C display has the pipe, no level either. */ f.dcp->fixed_connector_type = DRM_MODE_CONNECTOR_USB; diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_swap.h b/drivers/gpu/drm/apple/iomfb_v14_7_swap.h index 382e73ee99e61..c361103d06525 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_swap.h +++ b/drivers/gpu/drm/apple/iomfb_v14_7_swap.h @@ -35,8 +35,8 @@ * The panel level block of the swap record (the 14.7 J514S/J516S panel): a * present byte at +0x354 and the level in nits as an unaligned IEEE 754 * binary64 at +0x35c, both zero when the level does not change. The same - * layout as drivers/gpu/drm/apple-m3/iomfb_v14_7.h (bl_nits_present, - * bl_nits) and omacom/linux-aurora PR148. + * layout as SiliconForest commit 44fe00e5eb58d54baf1cc9ad607293e774bfed44 + * and omacom/linux-aurora PR148 (02524534c6b8). */ #define DCP_V14_SWAP_NITS_PRESENT 0x354 #define DCP_V14_SWAP_NITS 0x35c From b2cb665d307e90323bd17746e0228e24bdccdfd3 Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+iconidentify@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:21:33 -0700 Subject: [PATCH 07/28] drm/apple: drain the failed-panel test kick before checking idle A late class kick may queue a worker even after the panel failure is latched; the worker refuses to send the swap. Drain that asynchronous callback before checking that no work remains, and keep the submit-count assertion on both the queued and directly invoked callback paths. Generated-by: LLM Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com> (cherry picked from commit fa224e85a01adbee386542aef7438b6f7e2f2d8c) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c index f993c4c1c4f65..29acb9d5fe9e9 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_lifetime_test.c @@ -322,6 +322,7 @@ static void v14_bl_failed_submit_unknown(struct kunit *test) KUNIT_EXPECT_FALSE(test, work_busy(&c->f.v14->backlight_work.work)); /* Neither a later kick nor a work callback may retry the failed panel. */ dcp_v14_backlight_kick(c->f.dcp); + flush_delayed_work(&c->f.v14->backlight_work); dcp_v14_backlight_work(&c->f.v14->backlight_work.work); KUNIT_EXPECT_EQ(test, atomic_read(&c->submits), 1); KUNIT_EXPECT_FALSE(test, work_busy(&c->f.v14->backlight_work.work)); From bb0c381424d9827a3241b393d3a621c3466915e6 Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Mon, 26 Oct 2026 12:46:41 +0100 Subject: [PATCH 08/28] drm/apple: restore panel dimensions after mode probes EDID invalidation clears display_info after the panel's startup dimensions were set. Without an EDID, later probes leave a 0x0 mm connector and KWin falls back to 1x scaling. Restore the internal panel's dimensions after publishing its modes. Cover repeated EDID clears, firmware sizes, DT fallback and external EDID sizes with the production get_modes path under ASan/UBSan. Generated-by: LLM Signed-off-by: Eryk Wieliczko (cherry picked from commit b7c75a932a9c527b693842331b647720a553cf88) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/iomfb.c | 4 ++ .../selftests/drivers/gpu/apple-dcp/rate.py | 54 +++++++++++++++++++ .../selftests/drivers/gpu/apple-dcp/run.py | 1 + 3 files changed, 59 insertions(+) diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 76a53d1f91556..fc87253401d10 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -2139,6 +2139,10 @@ int dcp_get_modes(struct drm_connector *connector) if (apple_connector->drm_edid) dcp_retry_placeholder_edid(dcp, apple_connector->drm_edid); + /* EDID updates clear display_info even for panels without an EDID. */ + if (published && dcp_has_panel(dcp)) + dcp_update_connector_dimensions(dcp, connector); + /* * An internal panel has no EDID, so nothing fills in the refresh range * that userspace needs before it will drive VRR. Supply the range DCP diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index 52d7125f0c295..8598e9ad7a7e1 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -1105,6 +1105,60 @@ def publication(root): unit=re.sub(r'\blookup_mode\((&d,&[\w.\[\]]+)\)', r'lookup_mode(\1,NULL)', unit) return {**locals(), "block": block} + +def panel_dimensions(root): + n = publication(root) + unit, block = n['unit'], n['block'] + unit = unit.replace('bool modes_lock,modes_admitted,', + 'bool external_physically_absent,modes_lock,modes_admitted,', 1) + dcp = (root / 'drivers/gpu/drm/apple/dcp.c').read_text() + unit = unit.replace(block(unit, 'static bool __attribute__((unused)) dcp_has_panel('), + block(dcp, 'bool dcp_has_panel(')) + unit = unit.replace(block(unit, 'static void dcp_update_connector_dimensions('), + block(dcp, 'void dcp_update_connector_dimensions(')) + # Ordinary panel modes do not take the 180 Hz exception's metadata lock. + unit = unit.replace('probing->dcpavserv.lock && probing->metadata.lock', + 'probing->dcpavserv.lock && ' + '(!probing->modes[0].automatic_rgb8_quirk || probing->metadata.lock)') + # Model DRM's display_info reset and the absence of an internal AV EDID. + unit = unit.replace(block(unit, 'static int drm_edid_connector_update('), r""" +static int drm_edid_connector_update(struct drm_connector *c,const struct drm_edid *e) { + assert(c->dev->mode_config.mutex); + c->display_info.width_mm=e ? 600 : 0; + c->display_info.height_mm=e ? 330 : 0; + return fail_update ? -EIO : 0; +} +""") + unit = unit.replace('if(!d->dcpavserv.enabled || !d->dcpavserv.service', + 'if(dcp_has_panel(d)) return ERR_PTR(-ENODEV);\n' + ' if(!d->dcpavserv.enabled || !d->dcpavserv.service') + checks = r""" +static void panel_dimension_probes(void) { + reset();callbacks(false);callbacks(true);timings();admit(0);probe(true); + assert(con.base.display_info.width_mm==600 && con.base.display_info.height_mm==330); + d.panel.width_mm=302;d.panel.height_mm=196;d.width_mm=0;d.height_mm=0; + d.modes[0].automatic_rgb8_quirk=false; + con.edid_invalidation_pending=true;probe(true); + assert(!con.drm_edid); + assert(con.base.display_info.width_mm==302 && con.base.display_info.height_mm==196); + con.edid_invalidation_pending=true;probe(true); + assert(con.base.display_info.width_mm==302 && con.base.display_info.height_mm==196); + d.width_mm=300;d.height_mm=190;con.edid_invalidation_pending=true;probe(true); + assert(con.base.display_info.width_mm==300 && con.base.display_info.height_mm==190); + d.width_mm=0;d.height_mm=190;con.edid_invalidation_pending=true;probe(true); + assert(con.base.display_info.width_mm==302 && con.base.display_info.height_mm==196); + cleanup(); + reset();callbacks(false);callbacks(true);timings();admit(0);probe(true); + assert(con.base.display_info.width_mm==600 && con.base.display_info.height_mm==330); + cleanup(); + puts("PASS: internal panel dimensions survive repeated EDID clears; firmware size and DT fallback; external EDID size preserved"); +} +""" + unit = unit.replace('int main(int argc,char **argv) {', + checks + 'int main(int argc,char **argv) {') + return unit.replace(' transport_edid=raw;', + ' transport_edid=raw;panel_dimension_probes();', 1) + def hotplug(root): n = publication(root) unit = n['unit'] diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index a2a8332a30269..9fb5815566a63 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -90,6 +90,7 @@ def main(): ('edid', 'ordered-rx', edid.rx_progress, ((),), ('-pthread',)), ('rate', 'parser-policy', lambda r: rate.policy(r)['unit'], ((),), ()), ('rate', 'publication-hpd', rate.hotplug, ((),), ()), + ('rate', 'panel-dimensions', rate.panel_dimensions, ((),), ()), ('rate', 'metadata-decoder', lambda r: rate.metadata(r, output), ((),), ('-Wno-sign-compare',)), ('rate', 'bounded-retry', rate.retry, ((),), ()), ('phy', 'pm-ownership', pm.ownership, ((),), ()), From 4db122d9804d50f905786456ff333cefd5c81499 Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Tue, 27 Oct 2026 03:33:53 +0100 Subject: [PATCH 09/28] drm/apple: skip AFK debugfs setup for unrouted connectors Type-C connectors have no DCP until a pipeline is routed. With DRM_APPLE_DEBUG enabled, registering one dereferences the null platform device. Skip the pipeline debug directory until a DCP is available. Fixes: 2314090e59f0 ("drm: apple: route Type-C displays through a shared DCP fabric") Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 (cherry picked from commit c92eb3943810d746ac3f97d0ad81f4eee4974ad1) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/connector.c | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/connector.c b/drivers/gpu/drm/apple/connector.c index 282fc4d1295f5..2434eba0656ed 100644 --- a/drivers/gpu/drm/apple/connector.c +++ b/drivers/gpu/drm/apple/connector.c @@ -117,7 +117,14 @@ static void dcp_afk_debugfs_root(struct platform_device *pdev, int ep, struct de { #if IS_ENABLED(CONFIG_DRM_APPLE_DEBUG) struct dentry *entry = NULL; - struct apple_dcp *dcp = platform_get_drvdata(pdev); + struct apple_dcp *dcp; + + /* Type-C connectors can be registered before a pipeline is routed. */ + if (!pdev) + return; + dcp = platform_get_drvdata(pdev); + if (!dcp) + return; switch (ep) { case AV_ENDPOINT: @@ -153,7 +160,7 @@ void apple_connector_debugfs_init(struct drm_connector *connector, struct dentry switch (connector->connector_type) { case DRM_MODE_CONNECTOR_DisplayPort: case DRM_MODE_CONNECTOR_HDMIA: - dcp_afk_debugfs_root(apple_con->dcp, AV_ENDPOINT, root); + dcp_afk_debugfs_root(READ_ONCE(apple_con->dcp), AV_ENDPOINT, root); break; default: break; From ae21253a44e6eeb167180013b2d6c79b148a37bb Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Tue, 27 Oct 2026 16:39:52 +0100 Subject: [PATCH 10/28] drm/apple: reject stale native audio prepares Check the published instance before Prepare and after its reply. Record the link owner only after the firmware accepts it. Fixes: f627d7748df5 ("drm/apple: av: carry a Terminate's instance through its whole disconnect") Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 [ryan: the 7.3 test runs every case on the 14.7 and desktop 26.6 roles and answers link status through its link_status field, so the new cases are parameterised and use it; the reply status check was already inside the open lock here] (cherry picked from commit c806e16bbdd3e148034dca3e854226b77bb1cc9a) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/av-v14-test.c | 69 ++++++++++++++++++++++++++++- drivers/gpu/drm/apple/av.c | 19 +++++++- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/apple/av-v14-test.c b/drivers/gpu/drm/apple/av-v14-test.c index 362bb77db6465..f178a8aec2521 100644 --- a/drivers/gpu/drm/apple/av-v14-test.c +++ b/drivers/gpu/drm/apple/av-v14-test.c @@ -29,9 +29,11 @@ struct av_v14_test { bool successor_during_release; u32 link_status; u8 link_body[64]; + bool replace_during_prepare; }; static struct av_v14_test *av_v14_current; +static void av_v14_replace(struct av_v14_test *t); static int av_v14_call(struct apple_epic_service *srv, u32 command, const void *body, size_t body_len, size_t body_pad, void *reply, size_t reply_len) @@ -50,6 +52,10 @@ static int av_v14_call(struct apple_epic_service *srv, u32 command, const void * if (body_len == sizeof(t->link_body)) { memcpy(t->link_body, body, body_len); put_unaligned_le32(t->link_status, reply + 48); + if (command == t->asrv->cmds.prepare && t->replace_during_prepare) { + t->replace_during_prepare = false; + av_v14_replace(t); + } } return 0; } @@ -59,8 +65,6 @@ static void av_v14_card_disconnect(struct platform_device *pdev) av_v14_current->disconnects++; } -static void av_v14_replace(struct av_v14_test *t); - static void av_v14_release_link(struct platform_device *pdev, const u64 *gen) { struct av_v14_test *t = av_v14_current; @@ -319,6 +323,63 @@ static void av_v14_link_instance(struct kunit *test) KUNIT_EXPECT_EQ(test, t->ncalls, 5U); } +static void av_v14_prepare_rejects_unpublished_open(struct kunit *test) +{ + struct av_v14_test *t = av_v14_setup(test); + struct dcp_sound_cookie cookie = {}; + + KUNIT_ASSERT_EQ(test, dcp_audiosrv_open(t->dcp->dev), 0); + av_v14_replace(t); + /* Same slot and pointer, but the PCM still owns the old instance. */ + KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_PREPARE, &cookie), + -ENODEV); + KUNIT_EXPECT_EQ(test, t->ncalls, 1U); + KUNIT_EXPECT_EQ(test, t->asrv->link_gen, 0ULL); +} + +static void av_v14_prepare_replaced_during_reply(struct kunit *test) +{ + struct av_v14_test *t = av_v14_setup(test); + struct dcp_sound_cookie cookie = {}; + + KUNIT_ASSERT_EQ(test, dcp_audiosrv_open(t->dcp->dev), 0); + t->replace_during_prepare = true; + KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_PREPARE, &cookie), + -ENODEV); + KUNIT_EXPECT_EQ(test, t->ncalls, 2U); + KUNIT_EXPECT_EQ(test, t->asrv->link_gen, 0ULL); +} + +static void av_v14_failed_prepare_does_not_own_link(struct kunit *test) +{ + struct av_v14_test *t = av_v14_setup(test); + struct dcp_sound_cookie cookie = {}; + + KUNIT_ASSERT_EQ(test, dcp_audiosrv_open(t->dcp->dev), 0); + t->link_status = 0xe00002ea; + KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_PREPARE, &cookie), + -EIO); + KUNIT_EXPECT_EQ(test, t->ncalls, 2U); + KUNIT_EXPECT_EQ(test, t->asrv->link_gen, 0ULL); +} + +static void av_v14_failed_reprepare_drops_owner(struct kunit *test) +{ + struct av_v14_test *t = av_v14_setup(test); + struct dcp_sound_cookie cookie = {}; + u64 gen; + + KUNIT_ASSERT_EQ(test, dcp_audiosrv_open(t->dcp->dev), 0); + gen = t->asrv->gen; + KUNIT_ASSERT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_PREPARE, &cookie), 0); + KUNIT_ASSERT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_COMPLETE, &cookie), 0); + t->link_status = 0xe00002ea; + KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_PREPARE, &cookie), + -EIO); + KUNIT_EXPECT_FALSE(test, dcp_audiosrv_link_on(t->dcp->dev, gen)); + KUNIT_EXPECT_EQ(test, t->asrv->link_gen, 0ULL); +} + /* A Terminate from an instance that is not the published one does nothing. */ static void av_v14_terminate_unpublished(struct kunit *test) { @@ -403,6 +464,10 @@ static struct kunit_case av_v14_cases[] = { KUNIT_CASE_PARAM(av_v14_successor_during_release, av_native_gen_params), KUNIT_CASE_PARAM(av_v14_terminate_after_successor, av_native_gen_params), KUNIT_CASE_PARAM(av_v14_link_instance, av_native_gen_params), + KUNIT_CASE_PARAM(av_v14_prepare_rejects_unpublished_open, av_native_gen_params), + KUNIT_CASE_PARAM(av_v14_prepare_replaced_during_reply, av_native_gen_params), + KUNIT_CASE_PARAM(av_v14_failed_prepare_does_not_own_link, av_native_gen_params), + KUNIT_CASE_PARAM(av_v14_failed_reprepare_drops_owner, av_native_gen_params), KUNIT_CASE_PARAM(av_v14_terminate_unpublished, av_native_gen_params), KUNIT_CASE_PARAM(av_native_link_payload_status, av_native_gen_params), KUNIT_CASE_PARAM(av_desktop_service_admission, av_native_gen_params), diff --git a/drivers/gpu/drm/apple/av.c b/drivers/gpu/drm/apple/av.c index afb918fb58f89..6820bab24315a 100644 --- a/drivers/gpu/drm/apple/av.c +++ b/drivers/gpu/drm/apple/av.c @@ -388,6 +388,15 @@ bool dcp_audiosrv_link_on(struct device *dev, u64 gen) return asrv->open_srv && asrv->open_gen == gen && asrv->link_gen == gen; } +/* open_lock pins the client; the endpoint can replace its published instance. */ +static bool av_audiosrv_open_published(struct audiosrv_data *asrv) +{ + lockdep_assert_held(&asrv->open_lock); + guard(rwsem_read)(&asrv->srv_rwsem); + return asrv->open_srv && asrv->srv == asrv->open_srv && + asrv->gen == asrv->open_gen; +} + /* * The link commands of the 14.7 external role's audio service, the native * audio driver's: prepare, start, stop and complete all take the selected @@ -433,6 +442,11 @@ int dcp_audiosrv_link(struct device *dev, enum dcp_audio_link op, scoped_guard(mutex, &asrv->open_lock) { if (!asrv->open_srv) return -ENODEV; + if (op == DCP_AUDIO_LINK_PREPARE) { + asrv->link_gen = 0; + if (!av_audiosrv_open_published(asrv)) + return -ENODEV; + } /* A link ends on the instance it was prepared on; one gone took it along. */ if (op != DCP_AUDIO_LINK_PREPARE && asrv->link_gen != asrv->open_gen) return -ENODEV; @@ -445,8 +459,11 @@ int dcp_audiosrv_link(struct device *dev, enum dcp_audio_link op, dev_err(dev, "audiosrv: link method %u answered %#x\n", command, status); return -EIO; } - if (op == DCP_AUDIO_LINK_PREPARE) + if (op == DCP_AUDIO_LINK_PREPARE) { + if (!av_audiosrv_open_published(asrv)) + return -ENODEV; asrv->link_gen = asrv->open_gen; + } } return 0; } From 3300eb46d126282732160e621609d6fc9b80a02c Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Tue, 27 Oct 2026 16:40:37 +0100 Subject: [PATCH 11/28] drm/apple: complete live audio links after disconnect A disconnected PCM can still own a prepared firmware link. Complete that link before closing its client, including when sync_stop wins the race against the deferred owner release. Fixes: 3fe0ef05013a ("drm: apple: Add the DCP 14.7 external role's HDMI audio") Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 (cherry picked from commit d00568eaae5b5925cf88087eb31312d0fc27cc4a) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/audio-link-test.c | 107 +++++++++++++++++++++++- drivers/gpu/drm/apple/audio.c | 22 +++-- drivers/gpu/drm/apple/audio.h | 2 + drivers/gpu/drm/apple/av-v14-test.c | 3 + drivers/gpu/drm/apple/av.c | 14 ++++ 5 files changed, 137 insertions(+), 11 deletions(-) diff --git a/drivers/gpu/drm/apple/audio-link-test.c b/drivers/gpu/drm/apple/audio-link-test.c index a90d85f221ead..b69d38de25e73 100644 --- a/drivers/gpu/drm/apple/audio-link-test.c +++ b/drivers/gpu/drm/apple/audio-link-test.c @@ -11,13 +11,47 @@ struct dcpaud_link_test { bool link_on; + bool link_live; u64 asked; unsigned int completes; unsigned int stops; + struct dma_device dma; + struct dma_chan channel; + char sequence[16]; + unsigned int nsequence; }; static struct dcpaud_link_test *dcpaud_link_current; +static void dcpaud_link_record(char step) +{ + struct dcpaud_link_test *t = dcpaud_link_current; + + if (t->nsequence < sizeof(t->sequence) - 1) + t->sequence[t->nsequence++] = step; +} + +static bool dcpaud_link_test_live(struct device *dev) +{ + return dcpaud_link_current->link_live; +} + +static int dcpaud_link_test_terminate(struct dma_chan *chan) +{ + return 0; +} + +static void dcpaud_link_test_drain(struct dma_chan *chan) +{ + dcpaud_link_record('D'); +} + +static enum dma_status dcpaud_link_test_status(struct dma_chan *chan, + dma_cookie_t cookie, struct dma_tx_state *state) +{ + return DMA_COMPLETE; +} + static bool dcpaud_link_test_on(struct device *dev, u64 gen) { dcpaud_link_current->asked = gen; @@ -27,10 +61,13 @@ static bool dcpaud_link_test_on(struct device *dev, u64 gen) static int dcpaud_link_test_call(struct device *dev, enum dcp_audio_link op, const struct dcp_sound_cookie *cookie) { - if (op == DCP_AUDIO_LINK_COMPLETE) + if (op == DCP_AUDIO_LINK_COMPLETE) { dcpaud_link_current->completes++; - else if (op == DCP_AUDIO_LINK_STOP) + dcpaud_link_record('C'); + } else if (op == DCP_AUDIO_LINK_STOP) { dcpaud_link_current->stops++; + dcpaud_link_record('S'); + } return 0; } @@ -49,7 +86,13 @@ static struct dcp_audio *dcpaud_link_setup(struct kunit *test, struct platform_d dcpaud->native_link = true; mutex_init(&dcpaud->link_lock); dcpaud_link_current = t; + t->link_live = true; + t->channel.device = &t->dma; + t->dma.device_terminate_all = dcpaud_link_test_terminate; + t->dma.device_synchronize = dcpaud_link_test_drain; + t->dma.device_tx_status = dcpaud_link_test_status; kunit_activate_static_stub(test, dcp_audiosrv_link_on, dcpaud_link_test_on); + kunit_activate_static_stub(test, dcp_audiosrv_link_live, dcpaud_link_test_live); kunit_activate_static_stub(test, dcp_audiosrv_link, dcpaud_link_test_call); return dcpaud; } @@ -85,8 +128,68 @@ static void dcpaud_link_release_instance(struct kunit *test) KUNIT_EXPECT_EQ(test, dcpaud_link_current->completes, 2U); } +static void dcpaud_link_test_close(void *data) +{ + snd_dmaengine_pcm_close(data); +} + +static struct snd_pcm_substream *dcpaud_link_test_open(struct kunit *test, + struct dcp_audio *dcpaud) +{ + struct snd_pcm_substream *sub = kunit_kzalloc(test, sizeof(*sub), GFP_KERNEL); + struct snd_pcm_runtime *runtime = kunit_kzalloc(test, sizeof(*runtime), GFP_KERNEL); + struct snd_pcm *pcm = kunit_kzalloc(test, sizeof(*pcm), GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, sub); + KUNIT_ASSERT_NOT_NULL(test, runtime); + KUNIT_ASSERT_NOT_NULL(test, pcm); + pcm->private_data = dcpaud; + pcm->nonatomic = true; + sub->pcm = pcm; + sub->runtime = runtime; + KUNIT_ASSERT_EQ(test, snd_dmaengine_pcm_open(sub, &dcpaud_link_current->channel), 0); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, dcpaud_link_test_close, sub), 0); + return sub; +} + +static void dcpaud_disconnect_sync_completes_live_owner(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_open(test, audio); + + /* Disconnect cleared connected; the deferred owner release has not run. */ + audio->connected = false; + audio->link_prepared = true; + audio->link_started = true; + KUNIT_ASSERT_EQ(test, dcp_pcm_native_sync_stop(sub), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "DSC"); + KUNIT_EXPECT_FALSE(test, audio->link_prepared); + KUNIT_EXPECT_FALSE(test, audio->link_started); + dcpaud_release_link(pdev, NULL); + KUNIT_EXPECT_EQ(test, dcpaud_link_current->stops, 1U); + KUNIT_EXPECT_EQ(test, dcpaud_link_current->completes, 1U); +} + +static void dcpaud_gone_service_drops_link(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_open(test, audio); + + audio->link_prepared = true; + audio->link_started = true; + dcpaud_link_current->link_live = false; + KUNIT_ASSERT_EQ(test, dcp_pcm_native_sync_stop(sub), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "D"); + KUNIT_EXPECT_FALSE(test, audio->link_prepared); + KUNIT_EXPECT_FALSE(test, audio->link_started); +} + static struct kunit_case dcpaud_link_cases[] = { KUNIT_CASE(dcpaud_link_release_instance), + KUNIT_CASE(dcpaud_disconnect_sync_completes_live_owner), + KUNIT_CASE(dcpaud_gone_service_drops_link), {} }; diff --git a/drivers/gpu/drm/apple/audio.c b/drivers/gpu/drm/apple/audio.c index ccb10dcf3768c..7c14b61098f94 100644 --- a/drivers/gpu/drm/apple/audio.c +++ b/drivers/gpu/drm/apple/audio.c @@ -639,25 +639,29 @@ static int dcp_pcm_native_close(struct snd_pcm_substream *substream) } /* - * Stop and complete the link held, on the open audio service (@live), or - * drop both unsent with a service that went away. + * A PCM disconnect does not end its firmware service. Complete the link on + * that instance while it is published; a removed instance took it along. + * The service can disappear after the check, so -ENODEV during teardown is normal. */ -static void dcpaud_link_teardown(struct dcp_audio *dcpaud, bool live) +static void dcpaud_link_teardown(struct dcp_audio *dcpaud) { + bool live; int ret; lockdep_assert_held(&dcpaud->link_lock); + live = (dcpaud->link_prepared || dcpaud->link_started) && + dcp_audiosrv_link_live(dcpaud->dcp_dev); if (dcpaud->link_started && live) { ret = dcp_audiosrv_link(dcpaud->dcp_dev, DCP_AUDIO_LINK_STOP, &dcpaud->selected_cookie); - if (ret) + if (ret && ret != -ENODEV) dev_err(dcpaud->dev, "audio link stop failed: %d\n", ret); } dcpaud->link_started = false; if (dcpaud->link_prepared && live) { ret = dcp_audiosrv_link(dcpaud->dcp_dev, DCP_AUDIO_LINK_COMPLETE, &dcpaud->selected_cookie); - if (ret) + if (ret && ret != -ENODEV) dev_err(dcpaud->dev, "audio link complete failed: %d\n", ret); } dcpaud->link_prepared = false; @@ -670,7 +674,7 @@ static int dcp_pcm_native_hw_params(struct snd_pcm_substream *substream, struct dcp_audio *dcpaud = substream->pcm->private_data; scoped_guard(mutex, &dcpaud->link_lock) - dcpaud_link_teardown(dcpaud, dcpaud_connection_up(dcpaud)); + dcpaud_link_teardown(dcpaud); return dcp_pcm_hw_params(substream, params); } @@ -680,7 +684,7 @@ static int dcp_pcm_native_hw_free(struct snd_pcm_substream *substream) struct dcp_audio *dcpaud = substream->pcm->private_data; guard(mutex)(&dcpaud->link_lock); - dcpaud_link_teardown(dcpaud, dcpaud_connection_up(dcpaud)); + dcpaud_link_teardown(dcpaud); return 0; } @@ -781,7 +785,7 @@ static int dcp_pcm_native_sync_stop(struct snd_pcm_substream *substream) ret = snd_dmaengine_pcm_sync_stop(substream); guard(mutex)(&dcpaud->link_lock); - dcpaud_link_teardown(dcpaud, dcpaud_connection_up(dcpaud)); + dcpaud_link_teardown(dcpaud); return ret; } @@ -810,7 +814,7 @@ void dcpaud_release_link(struct platform_device *pdev, const u64 *gen) return; if (dcpaud->chan) dmaengine_synchronize(dcpaud->chan); - dcpaud_link_teardown(dcpaud, true); + dcpaud_link_teardown(dcpaud); } static const struct snd_pcm_ops dcp_native_playback_ops = { diff --git a/drivers/gpu/drm/apple/audio.h b/drivers/gpu/drm/apple/audio.h index 88e9610363234..79bee44bcefbc 100644 --- a/drivers/gpu/drm/apple/audio.h +++ b/drivers/gpu/drm/apple/audio.h @@ -35,6 +35,8 @@ int dcp_audiosrv_link(struct device *dev, enum dcp_audio_link op, void dcpaud_release_link(struct platform_device *pdev, const u64 *gen); /* Whether the link prepared last is on instance @gen, still the open one */ bool dcp_audiosrv_link_on(struct device *dev, u64 gen); +/* The prepared link still belongs to the client of the published instance. */ +bool dcp_audiosrv_link_live(struct device *dev); #endif #endif /* __AUDIO_H__ */ diff --git a/drivers/gpu/drm/apple/av-v14-test.c b/drivers/gpu/drm/apple/av-v14-test.c index f178a8aec2521..433530763af7f 100644 --- a/drivers/gpu/drm/apple/av-v14-test.c +++ b/drivers/gpu/drm/apple/av-v14-test.c @@ -308,7 +308,9 @@ static void av_v14_link_instance(struct kunit *test) old = t->asrv->gen; KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_PREPARE, &cookie), 0); KUNIT_EXPECT_TRUE(test, dcp_audiosrv_link_on(t->dcp->dev, old)); + KUNIT_EXPECT_TRUE(test, dcp_audiosrv_link_live(t->dcp->dev)); av_v14_replace(t); + KUNIT_EXPECT_FALSE(test, dcp_audiosrv_link_live(t->dcp->dev)); KUNIT_EXPECT_EQ(test, dcp_audiosrv_open(t->dcp->dev), 0); KUNIT_ASSERT_EQ(test, t->ncalls, 3U); KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_STOP, &cookie), -ENODEV); @@ -319,6 +321,7 @@ static void av_v14_link_instance(struct kunit *test) KUNIT_EXPECT_FALSE(test, dcp_audiosrv_link_on(t->dcp->dev, t->asrv->gen)); KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_PREPARE, &cookie), 0); KUNIT_EXPECT_TRUE(test, dcp_audiosrv_link_on(t->dcp->dev, t->asrv->gen)); + KUNIT_EXPECT_TRUE(test, dcp_audiosrv_link_live(t->dcp->dev)); KUNIT_EXPECT_EQ(test, dcp_audiosrv_link(t->dcp->dev, DCP_AUDIO_LINK_COMPLETE, &cookie), 0); KUNIT_EXPECT_EQ(test, t->ncalls, 5U); } diff --git a/drivers/gpu/drm/apple/av.c b/drivers/gpu/drm/apple/av.c index 6820bab24315a..fad5fe6c43d1a 100644 --- a/drivers/gpu/drm/apple/av.c +++ b/drivers/gpu/drm/apple/av.c @@ -397,6 +397,20 @@ static bool av_audiosrv_open_published(struct audiosrv_data *asrv) asrv->gen == asrv->open_gen; } +bool dcp_audiosrv_link_live(struct device *dev) +{ + struct apple_dcp *dcp; + struct audiosrv_data *asrv; + + KUNIT_STATIC_STUB_REDIRECT(dcp_audiosrv_link_live, dev); + dcp = dev_get_drvdata(dev); + asrv = dcp->audiosrv; + if (!asrv) + return false; + guard(mutex)(&asrv->open_lock); + return asrv->link_gen == asrv->open_gen && av_audiosrv_open_published(asrv); +} + /* * The link commands of the 14.7 external role's audio service, the native * audio driver's: prepare, start, stop and complete all take the selected From 630faace180c287ea218c48ac1c7f6ad4c8ee85a Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Tue, 27 Oct 2026 08:57:12 +0100 Subject: [PATCH 12/28] dmaengine: apple-sio: allocate the full descriptor bitmap The 64-slot descriptor bitmap is only allocated two bytes. Use the bitmap allocator so unsigned-long bitmap operations have enough storage. Fixes: 5fdca02134fa ("dmaengine: apple-sio: Add Apple SIO driver") Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 (cherry picked from commit 69e431f5787d1825ccbfaea9b8975b79eede21fd) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/dma/apple-sio.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/drivers/dma/apple-sio.c b/drivers/dma/apple-sio.c index 65ba4511501c7..0b20b5b2b74bc 100644 --- a/drivers/dma/apple-sio.c +++ b/drivers/dma/apple-sio.c @@ -6,6 +6,7 @@ */ #include +#include #include #include #include @@ -908,8 +909,8 @@ static int sio_alloc_shmem(struct sio_data *sio) return -ENOMEM; sio->shmem_desc_base = (struct sio_coproc_desc *) (sio->shmem + 56); - sio->desc_allocated = devm_kzalloc(sio->dev, SIO_NO_DESC_SLOTS / 32, - GFP_KERNEL); + sio->desc_allocated = devm_bitmap_zalloc(sio->dev, SIO_NO_DESC_SLOTS, + GFP_KERNEL); if (!sio->desc_allocated) return -ENOMEM; From f9d4b9ae1878d3553f47e4afa12710b644aec326 Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Tue, 27 Oct 2026 22:56:33 +0100 Subject: [PATCH 13/28] drm/apple: hold the native HDMI audio link between streams The sink behind the J514S HDMI port (PS190 converter, TV) mutes the audio of a link stopped and started again with an unchanged format, until the DP link trains again. macOS never does that: it starts the link once and, between streams, keeps it running on a cleared buffer until the display sleeps or the format changes. Do the same for stereo on the HDMI port: the stopped stream's DMA is replaced by silence and the link is held. The mute was measured behind the HDMI port only, so a Type-C output keeps one link per stream unless hold_link=2; hold_link=0 restores one link per stream everywhere. While links are held, a link is not started in the first 1.5 s after the sink connects: once, at boot, a link started 0.2 s after the display came on was not heard, and holding it kept it silent. A link prepared at START configures the SIO channel first, as the PCM's prepare does. Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 (cherry picked from commit 459ba2444ce53ea30792e3b7ac868c9d8c20313d) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/audio-link-test.c | 237 +++++++++++++++- drivers/gpu/drm/apple/audio.c | 349 +++++++++++++++++++++--- drivers/gpu/drm/apple/audio.h | 2 + drivers/gpu/drm/apple/av.c | 6 + 4 files changed, 557 insertions(+), 37 deletions(-) diff --git a/drivers/gpu/drm/apple/audio-link-test.c b/drivers/gpu/drm/apple/audio-link-test.c index b69d38de25e73..ef897f94d7f1c 100644 --- a/drivers/gpu/drm/apple/audio-link-test.c +++ b/drivers/gpu/drm/apple/audio-link-test.c @@ -1,9 +1,9 @@ // SPDX-License-Identifier: GPL-2.0-only OR MIT /* * Included by audio.c: a Terminate's link release ends only the link of the - * instance it is for. The production dcpaud_release_link() runs; the audio - * service's answer to which instance the link is on, and its link calls, - * are stubbed. + * instance it is for, and a stopped stereo link is held between streams. + * The production link lifecycle runs; the audio service's answer to which + * instance the link is on, its link calls and the DMA channel are stubbed. */ #include #include @@ -12,11 +12,13 @@ struct dcpaud_link_test { bool link_on; bool link_live; + bool typec; u64 asked; unsigned int completes; unsigned int stops; struct dma_device dma; struct dma_chan channel; + struct dma_async_tx_descriptor desc; char sequence[16]; unsigned int nsequence; }; @@ -38,9 +40,43 @@ static bool dcpaud_link_test_live(struct device *dev) static int dcpaud_link_test_terminate(struct dma_chan *chan) { + dcpaud_link_record('X'); return 0; } +static dma_cookie_t dcpaud_link_test_submit(struct dma_async_tx_descriptor *tx) +{ + return 1; +} + +static struct dma_async_tx_descriptor * +dcpaud_link_test_prep(struct dma_chan *chan, dma_addr_t buf, size_t len, size_t period, + enum dma_transfer_direction dir, unsigned long flags) +{ + struct dcpaud_link_test *t = dcpaud_link_current; + + dcpaud_link_record('Q'); + t->desc.chan = chan; + t->desc.tx_submit = dcpaud_link_test_submit; + return &t->desc; +} + +/* G the channel configured */ +static int dcpaud_link_test_config(struct dma_chan *chan, struct dma_slave_config *config) +{ + dcpaud_link_record('G'); + return 0; +} + +static bool dcpaud_link_test_typec(struct device *dev) +{ + return dcpaud_link_current->typec; +} + +static void dcpaud_link_test_issue(struct dma_chan *chan) +{ +} + static void dcpaud_link_test_drain(struct dma_chan *chan) { dcpaud_link_record('D'); @@ -67,6 +103,10 @@ static int dcpaud_link_test_call(struct device *dev, enum dcp_audio_link op, } else if (op == DCP_AUDIO_LINK_STOP) { dcpaud_link_current->stops++; dcpaud_link_record('S'); + } else if (op == DCP_AUDIO_LINK_PREPARE) { + dcpaud_link_record('P'); + } else if (op == DCP_AUDIO_LINK_START) { + dcpaud_link_record('T'); } return 0; } @@ -91,9 +131,13 @@ static struct dcp_audio *dcpaud_link_setup(struct kunit *test, struct platform_d t->dma.device_terminate_all = dcpaud_link_test_terminate; t->dma.device_synchronize = dcpaud_link_test_drain; t->dma.device_tx_status = dcpaud_link_test_status; + t->dma.device_prep_dma_cyclic = dcpaud_link_test_prep; + t->dma.device_issue_pending = dcpaud_link_test_issue; + t->dma.device_config = dcpaud_link_test_config; kunit_activate_static_stub(test, dcp_audiosrv_link_on, dcpaud_link_test_on); kunit_activate_static_stub(test, dcp_audiosrv_link_live, dcpaud_link_test_live); kunit_activate_static_stub(test, dcp_audiosrv_link, dcpaud_link_test_call); + kunit_activate_static_stub(test, dcp_audiosrv_typec, dcpaud_link_test_typec); return dcpaud; } @@ -186,10 +230,197 @@ static void dcpaud_gone_service_drops_link(struct kunit *test) KUNIT_EXPECT_FALSE(test, audio->link_started); } +/* A stream stopped by STOP with its DMA playing on, on the PCM's channel */ +static void dcpaud_link_test_held_stop(struct kunit *test, struct dcp_audio *audio) +{ + audio->chan = &dcpaud_link_current->channel; + audio->silence = kunit_kzalloc(test, DCPAUD_SILENCE_BYTES, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, audio->silence); + audio->link_prepared = true; + audio->link_started = true; + audio->dma_cleared = true; + /* The stream's PM reference, carried by the cleared DMA */ + pm_runtime_get_noresume(audio->dev); +} + +static void dcpaud_stop_holds_link(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_open(test, audio); + + dcpaud_link_test_held_stop(test, audio); + /* The stream's DMA ends, the silence follows it; no link call */ + KUNIT_ASSERT_EQ(test, dcp_pcm_native_sync_stop(sub), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "XQD"); + KUNIT_EXPECT_TRUE(test, audio->silence_running); + KUNIT_EXPECT_TRUE(test, audio->link_started); + + KUNIT_ASSERT_EQ(test, dcp_pcm_native_hw_free(sub), 0); + KUNIT_EXPECT_TRUE(test, audio->link_prepared); + KUNIT_EXPECT_EQ(test, dcpaud_link_current->stops + dcpaud_link_current->completes, 0U); + + /* The display goes off: the silence ends, then the link */ + dcpaud_release_link(pdev, NULL); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "XQDXDDSC"); + KUNIT_EXPECT_FALSE(test, audio->silence_running); + KUNIT_EXPECT_FALSE(test, audio->link_prepared); + /* The stream's reference went with the silence */ + KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 0); +} + +static void dcpaud_held_stop_without_service(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_open(test, audio); + + dcpaud_link_test_held_stop(test, audio); + dcpaud_link_current->link_live = false; + /* No service to hold it for: the DMA ends and the link is dropped */ + KUNIT_ASSERT_EQ(test, dcp_pcm_native_sync_stop(sub), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "XD"); + KUNIT_EXPECT_FALSE(test, audio->silence_running); + KUNIT_EXPECT_FALSE(test, audio->link_prepared); + KUNIT_EXPECT_FALSE(test, audio->link_started); + KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 0); +} + +/* A held link and its silence, and a prepared stream to start on it */ +static struct snd_pcm_substream *dcpaud_link_test_held(struct kunit *test, struct dcp_audio *audio) +{ + struct snd_pcm_substream *sub = dcpaud_link_test_open(test, audio); + + mutex_init(&audio->data_lock); + audio->connected = true; + audio->chan = &dcpaud_link_current->channel; + audio->link_prepared = true; + audio->link_started = true; + audio->silence_running = true; + /* The held link's PM reference, then the stream's from its START */ + pm_runtime_get_noresume(audio->dev); + pm_runtime_get_noresume(audio->dev); + sub->runtime->frame_bits = 64; + sub->runtime->buffer_size = 8192; + sub->runtime->period_size = 512; + return sub; +} + +static void dcpaud_start_on_held_link(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_held(test, audio); + + /* Its element: the stream's DMA replaces the silence, no link call */ + KUNIT_ASSERT_EQ(test, dcp_pcm_native_start(sub, SNDRV_PCM_TRIGGER_START), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "XQ"); + KUNIT_EXPECT_FALSE(test, audio->silence_running); + KUNIT_EXPECT_TRUE(test, audio->link_started); + pm_runtime_put_noidle(audio->dev); +} + +static void dcpaud_start_replaces_held_link(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_held(test, audio); + + /* Another element: the held link ends, the stream's is configured, prepared and started */ + memset(&audio->link_cookie, 1, sizeof(audio->link_cookie)); + memset(&audio->selected_cookie, 2, sizeof(audio->selected_cookie)); + KUNIT_ASSERT_EQ(test, dcp_pcm_native_start(sub, SNDRV_PCM_TRIGGER_START), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "XDSCGPQT"); + KUNIT_EXPECT_EQ(test, memcmp(&audio->link_cookie, &audio->selected_cookie, + sizeof(audio->link_cookie)), 0); + KUNIT_EXPECT_TRUE(test, audio->link_started); + pm_runtime_put_noidle(audio->dev); +} + +static void dcpaud_start_after_held_link_went(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_held(test, audio); + + /* The held link's service went (unplug): the stream gets a link of its own */ + dcpaud_link_current->link_live = false; + KUNIT_ASSERT_EQ(test, dcp_pcm_native_start(sub, SNDRV_PCM_TRIGGER_START), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "XDGPQT"); + KUNIT_EXPECT_FALSE(test, audio->silence_running); + KUNIT_EXPECT_TRUE(test, audio->link_started); + pm_runtime_put_noidle(audio->dev); + KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 0); +} + +static void dcpaud_failed_start_keeps_held_link(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_held(test, audio); + + /* Unplugged before the swap: START fails, its STOP leaves the silence */ + audio->connected = false; + sub->runtime->channels = 2; + sub->runtime->dma_area = kunit_kzalloc(test, 64, GFP_KERNEL); + sub->runtime->dma_bytes = 64; + KUNIT_ASSERT_EQ(test, dcp_pcm_native_start(sub, SNDRV_PCM_TRIGGER_START), -ENXIO); + KUNIT_ASSERT_EQ(test, dcp_pcm_native_trigger(sub, SNDRV_PCM_TRIGGER_STOP), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, ""); + KUNIT_EXPECT_FALSE(test, audio->dma_cleared); + KUNIT_EXPECT_TRUE(test, audio->silence_running); + /* Only the held silence's reference is left */ + KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 1); + pm_runtime_put_noidle(audio->dev); +} + +static void dcpaud_link_test_hold_default(void *data) +{ + WRITE_ONCE(hold_link, 1); +} + +static void dcpaud_typec_stop_ends_stream(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_open(test, audio); + + KUNIT_ASSERT_EQ(test, + kunit_add_action_or_reset(test, dcpaud_link_test_hold_default, NULL), 0); + sub->runtime->channels = 2; + sub->runtime->dma_area = kunit_kzalloc(test, 64, GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, sub->runtime->dma_area); + sub->runtime->dma_bytes = 64; + dcpaud_link_current->typec = true; + + /* USB-C by default: STOP ends the stream's DMA, no link is held */ + audio->stream_dma = true; + pm_runtime_get_noresume(audio->dev); + KUNIT_ASSERT_EQ(test, dcp_pcm_native_trigger(sub, SNDRV_PCM_TRIGGER_STOP), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "X"); + KUNIT_EXPECT_FALSE(test, audio->dma_cleared); + KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 0); + + /* hold_link=2: the DMA plays the cleared buffer on for the held link */ + WRITE_ONCE(hold_link, 2); + pm_runtime_get_noresume(audio->dev); + KUNIT_ASSERT_EQ(test, dcp_pcm_native_trigger(sub, SNDRV_PCM_TRIGGER_STOP), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "X"); + KUNIT_EXPECT_TRUE(test, audio->dma_cleared); + pm_runtime_put_noidle(audio->dev); +} + static struct kunit_case dcpaud_link_cases[] = { KUNIT_CASE(dcpaud_link_release_instance), KUNIT_CASE(dcpaud_disconnect_sync_completes_live_owner), KUNIT_CASE(dcpaud_gone_service_drops_link), + KUNIT_CASE(dcpaud_stop_holds_link), + KUNIT_CASE(dcpaud_held_stop_without_service), + KUNIT_CASE(dcpaud_start_on_held_link), + KUNIT_CASE(dcpaud_start_replaces_held_link), + KUNIT_CASE(dcpaud_start_after_held_link_went), + KUNIT_CASE(dcpaud_failed_start_keeps_held_link), + KUNIT_CASE(dcpaud_typec_stop_ends_stream), {} }; diff --git a/drivers/gpu/drm/apple/audio.c b/drivers/gpu/drm/apple/audio.c index 7c14b61098f94..472e653f46d41 100644 --- a/drivers/gpu/drm/apple/audio.c +++ b/drivers/gpu/drm/apple/audio.c @@ -14,7 +14,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -43,6 +45,13 @@ */ #define DCPAUD_PERIODS_MAX 16 +/* + * The silence a held link plays between streams. SIO keeps up to four + * periods of a transfer in flight, so it gets more than that. + */ +#define DCPAUD_SILENCE_PERIOD SZ_16K +#define DCPAUD_SILENCE_BYTES (8 * DCPAUD_SILENCE_PERIOD) + struct dcp_audio { struct device *dev; struct device *dcp_dev; @@ -59,6 +68,8 @@ struct dcp_audio { bool dcp_connected; /// dcp status keep for delayed initialization bool connected; unsigned int connection_cookie; + /* When the sink was last connected */ + ktime_t connected_at; struct snd_pcm_chmap_elem selected_chmap; struct dcp_sound_cookie selected_cookie; @@ -81,6 +92,18 @@ struct dcp_audio { struct mutex link_lock; bool link_prepared; bool link_started; + /* The element the link was prepared with */ + struct dcp_sound_cookie link_cookie; + /* START issued the stream's DMA */ + bool stream_dma; + /* STOP left the DMA playing the cleared buffer, see dcp_pcm_native_sync_stop() */ + bool dma_cleared; + /* Drops a held link whose service went, see dcpaud_disconnect() */ + struct work_struct stale_work; + /* The held link's silence, see dcpaud_silence_start() */ + void *silence; + dma_addr_t silence_addr; + bool silence_running; #endif }; @@ -291,7 +314,7 @@ static int dcpaud_init_dma(struct dcp_audio *dcpaud) { struct dma_chan *chan; if (dcpaud->chan) - return 0; + goto silence; chan = of_dma_request_slave_channel(dcpaud->dev->of_node, "tx"); /* squelch dma channel request errors, the driver will try again alter */ @@ -311,6 +334,14 @@ static int dcpaud_init_dma(struct dcp_audio *dcpaud) dcpaud->chan->device->dev, 1024 * 1024, SIZE_MAX); +silence: +#if IS_ENABLED(CONFIG_DRM_APPLE_DCP_T6030_AUDIO) + /* Without it a stopped link is not held, see dcpaud_silence_start(). */ + if (dcpaud_native_link(dcpaud) && !dcpaud->silence) + dcpaud->silence = dma_alloc_coherent(dcpaud->chan->device->dev, + DCPAUD_SILENCE_BYTES, + &dcpaud->silence_addr, GFP_KERNEL); +#endif return 0; } @@ -599,14 +630,114 @@ struct snd_pcm_ops dcp_playback_ops = { * * prepare: the DMA channel configured, then the link prepared, once; * START: the cyclic DMA issued, then the link started; - * STOP: the DMA terminated; - * sync_stop: the DMA drained, then the link stopped and completed; + * STOP: the DMA terminated, or for a held link (below) left playing + * the cleared buffer; + * sync_stop: the DMA drained, then the link stopped and completed, or the + * held link's silence queued in place of the DMA; * hw_free: a link prepared but never started, completed. * - * Every link method carries the selected element's link data. A link of an - * earlier connection went with its service: its state is dropped unsent. - * The audio service itself is open only while the PCM is. + * Every link method carries its element's link data. A link of an earlier + * connection went with its service: its state is dropped unsent. The audio + * service itself is open while the PCM is, or while its link is held. + */ +/* + * The sink behind the J514S HDMI port (PS190 converter and TV) mutes the + * audio of a link that stops and starts again with an unchanged format, + * until the DP link trains again: only a different channel layout brings + * it back. So a stereo link, once started, is held between streams, as + * macOS holds it: the stopped stream's DMA is swapped for silence and the + * link keeps running until the format changes or the display goes off + * (dcpaud_release_link()). The DMA alone restarts without that loss. + */ +/* + * Read-only at runtime: a stream opened while it was lower keeps periods + * longer than a held link's synchronize allows for. + */ +static int hold_link = 1; +module_param(hold_link, int, 0444); +MODULE_PARM_DESC(hold_link, + "Hold started audio links between streams (silence): 0 never, 1 on the HDMI port (default), 2 also on USB-C"); + +/* + * The mute was measured behind the HDMI port. USB-C sinks keep one link per + * stream unless hold_link=2, until they have been qualified with this. + */ +static bool dcpaud_holds(struct dcp_audio *dcpaud) +{ + int hold = READ_ONCE(hold_link); + + return hold >= 2 || (hold == 1 && !dcp_audiosrv_typec(dcpaud->dcp_dev)); +} + +/* + * Seen once, at boot: a link started about 0.2 s after the display came on + * was not heard, and holding it kept it silent; the next connection's first + * link, started later, was heard. While links are held, the first link of a + * connection starts once the sink has been up this long, a margin rather + * than a measured bound. One link per stream needs no wait: the next stream + * starts a new link. + */ +#define DCPAUD_SINK_SETTLE_MS 1500 + +/* Queue the held link's silence; it carries the PM reference of the stream. */ +static int dcpaud_silence_start(struct dcp_audio *dcpaud) +{ + struct dma_async_tx_descriptor *desc; + + lockdep_assert_held(&dcpaud->link_lock); + if (!dcpaud->silence) + return -ENOMEM; + desc = dmaengine_prep_dma_cyclic(dcpaud->chan, dcpaud->silence_addr, + DCPAUD_SILENCE_BYTES, DCPAUD_SILENCE_PERIOD, + DMA_MEM_TO_DEV, 0); + if (!desc) + return -ENOMEM; + if (dma_submit_error(dmaengine_submit(desc))) + return -EIO; + dma_async_issue_pending(dcpaud->chan); + dcpaud->silence_running = true; + dev_dbg(dcpaud->dev, "link held, playing silence\n"); + return 0; +} + +static void dcpaud_silence_stop(struct dcp_audio *dcpaud) +{ + lockdep_assert_held(&dcpaud->link_lock); + if (!dcpaud->silence_running) + return; + dmaengine_terminate_sync(dcpaud->chan); + dcpaud->silence_running = false; + pm_runtime_mark_last_busy(dcpaud->dev); + pm_runtime_put_autosuspend(dcpaud->dev); + dev_dbg(dcpaud->dev, "held link's silence stopped\n"); +} + +static void dcpaud_link_teardown(struct dcp_audio *dcpaud); + +/* + * A held link went with its audio service (sink change, unplug): its silence + * stops and its state is dropped unsent, so that a new service's stream + * prepares and starts its own link. */ +static void dcpaud_drop_stale(struct dcp_audio *dcpaud) +{ + lockdep_assert_held(&dcpaud->link_lock); + if ((dcpaud->silence_running || dcpaud->link_prepared || dcpaud->link_started) && + !dcp_audiosrv_link_live(dcpaud->dcp_dev)) { + dev_dbg(dcpaud->dev, "held link went with its service\n"); + dcpaud_link_teardown(dcpaud); + } +} + +static void dcpaud_stale_work(struct work_struct *work) +{ + struct dcp_audio *dcpaud = container_of(work, struct dcp_audio, stale_work); + + guard(mutex)(&dcpaud->link_lock); + if (!dcpaud->dma_cleared) + dcpaud_drop_stale(dcpaud); +} + static int dcp_pcm_native_open(struct snd_pcm_substream *substream) { struct dcp_audio *dcpaud = substream->pcm->private_data; @@ -622,8 +753,15 @@ static int dcp_pcm_native_open(struct snd_pcm_substream *substream) } ret = dcp_pcm_open(substream); - if (ret && live) - dcp_audiosrv_close(dcpaud->dcp_dev); + if (ret && live) { + bool held; + + /* A held link keeps its service open. */ + scoped_guard(mutex, &dcpaud->link_lock) + held = dcpaud->silence_running; + if (!held) + dcp_audiosrv_close(dcpaud->dcp_dev); + } return ret; } @@ -632,8 +770,27 @@ static int dcp_pcm_native_close(struct snd_pcm_substream *substream) { struct dcp_audio *dcpaud = substream->pcm->private_data; int ret = dcp_pcm_close(substream); + bool held; - dcp_audiosrv_close(dcpaud->dcp_dev); + /* + * A held link keeps its service open. Closing the PCM terminated its + * channel, the silence with it: that is queued again. + */ + scoped_guard(mutex, &dcpaud->link_lock) { + dcpaud_drop_stale(dcpaud); + held = dcpaud->silence_running; + if (held) { + dcpaud->silence_running = false; + if (dcpaud_silence_start(dcpaud)) { + held = false; + dcpaud_link_teardown(dcpaud); + pm_runtime_mark_last_busy(dcpaud->dev); + pm_runtime_put_autosuspend(dcpaud->dev); + } + } + } + if (!held) + dcp_audiosrv_close(dcpaud->dcp_dev); return ret; } @@ -649,32 +806,46 @@ static void dcpaud_link_teardown(struct dcp_audio *dcpaud) int ret; lockdep_assert_held(&dcpaud->link_lock); + /* A held link's silence ends with it. */ + dcpaud_silence_stop(dcpaud); live = (dcpaud->link_prepared || dcpaud->link_started) && dcp_audiosrv_link_live(dcpaud->dcp_dev); if (dcpaud->link_started && live) { ret = dcp_audiosrv_link(dcpaud->dcp_dev, DCP_AUDIO_LINK_STOP, - &dcpaud->selected_cookie); + &dcpaud->link_cookie); if (ret && ret != -ENODEV) dev_err(dcpaud->dev, "audio link stop failed: %d\n", ret); } dcpaud->link_started = false; if (dcpaud->link_prepared && live) { ret = dcp_audiosrv_link(dcpaud->dcp_dev, DCP_AUDIO_LINK_COMPLETE, - &dcpaud->selected_cookie); + &dcpaud->link_cookie); if (ret && ret != -ENODEV) dev_err(dcpaud->dev, "audio link complete failed: %d\n", ret); } dcpaud->link_prepared = false; } -/* New parameters select a new element: the link of the old one ends first. */ +/* + * New parameters select a new element: the link of the old one ends first. + * A held link stays until a stream of another element starts (it may be + * userspace probing), and its channel stays configured. + */ static int dcp_pcm_native_hw_params(struct snd_pcm_substream *substream, struct snd_pcm_hw_params *params) { struct dcp_audio *dcpaud = substream->pcm->private_data; + bool up = !dcpaud->open_unplugged && dcpaud_connection_up(dcpaud); + int ret; - scoped_guard(mutex, &dcpaud->link_lock) + scoped_guard(mutex, &dcpaud->link_lock) { + dcpaud_drop_stale(dcpaud); + if (up && dcpaud->silence_running) { + ret = dcpaud_select_cookie(dcpaud, params); + return ret < 0 ? ret : ret ? 0 : -EINVAL; + } dcpaud_link_teardown(dcpaud); + } return dcp_pcm_hw_params(substream, params); } @@ -684,14 +855,15 @@ static int dcp_pcm_native_hw_free(struct snd_pcm_substream *substream) struct dcp_audio *dcpaud = substream->pcm->private_data; guard(mutex)(&dcpaud->link_lock); - dcpaud_link_teardown(dcpaud); + if (!dcpaud->silence_running) + dcpaud_link_teardown(dcpaud); return 0; } -static int dcp_pcm_native_prepare(struct snd_pcm_substream *substream) +/* The SIO channel is configured again before every link prepare. */ +static int dcpaud_link_prepare(struct dcp_audio *dcpaud, struct snd_pcm_substream *substream) { - struct dcp_audio *dcpaud = substream->pcm->private_data; struct dma_slave_config slave_config = { .direction = DMA_MEM_TO_DEV, /* One 32-bit DPA entry per sample word, as in hw_params. */ @@ -699,6 +871,23 @@ static int dcp_pcm_native_prepare(struct snd_pcm_substream *substream) }; int ret; + lockdep_assert_held(&dcpaud->link_lock); + ret = dmaengine_slave_config(snd_dmaengine_pcm_get_chan(substream), &slave_config); + if (ret) + return ret; + ret = dcp_audiosrv_link(dcpaud->dcp_dev, DCP_AUDIO_LINK_PREPARE, + &dcpaud->selected_cookie); + if (ret) + return ret; + dcpaud->link_prepared = true; + dcpaud->link_cookie = dcpaud->selected_cookie; + return 0; +} + +static int dcp_pcm_native_prepare(struct snd_pcm_substream *substream) +{ + struct dcp_audio *dcpaud = substream->pcm->private_data; + if (dcpaud->open_unplugged) return 0; @@ -706,44 +895,72 @@ static int dcp_pcm_native_prepare(struct snd_pcm_substream *substream) return -ENXIO; guard(mutex)(&dcpaud->link_lock); + dcpaud_drop_stale(dcpaud); if (dcpaud->link_prepared) return 0; - - /* The SIO channel is configured again before every link prepare. */ - ret = dmaengine_slave_config(snd_dmaengine_pcm_get_chan(substream), - &slave_config); - if (ret) - return ret; - - ret = dcp_audiosrv_link(dcpaud->dcp_dev, DCP_AUDIO_LINK_PREPARE, - &dcpaud->selected_cookie); - if (ret) - return ret; - - dcpaud->link_prepared = true; - return 0; + return dcpaud_link_prepare(dcpaud, substream); } static int dcp_pcm_native_start(struct snd_pcm_substream *substream, int cmd) { struct dcp_audio *dcpaud = substream->pcm->private_data; + s64 up_ms; + bool held; int ret; if (!dcpaud_connection_up(dcpaud)) return -ENXIO; + /* The first link of the connection waits for the sink to settle. */ + scoped_guard(mutex, &dcpaud->data_lock) + up_ms = ktime_ms_delta(ktime_get(), dcpaud->connected_at); + if (dcpaud_holds(dcpaud) && !READ_ONCE(dcpaud->link_started) && + up_ms >= 0 && up_ms < DCPAUD_SINK_SETTLE_MS) { + dev_dbg(dcpaud->dev, "sink up %lld ms, link waits\n", up_ms); + msleep(DCPAUD_SINK_SETTLE_MS - up_ms); + } + + guard(mutex)(&dcpaud->link_lock); + dcpaud_drop_stale(dcpaud); + /* The stream's DMA follows the held link's silence. */ + held = dcpaud->silence_running; + if (held) { + dmaengine_terminate_async(dcpaud->chan); + dcpaud->silence_running = false; + pm_runtime_mark_last_busy(dcpaud->dev); + pm_runtime_put_autosuspend(dcpaud->dev); + } + + /* + * A held link of another element ends, and the stream's is prepared; + * so is a stream's whose prepare found a link that has gone since. + */ + if (!dcpaud->link_prepared || + memcmp(&dcpaud->link_cookie, &dcpaud->selected_cookie, sizeof(dcpaud->link_cookie))) { + if (dcpaud->link_prepared) { + dmaengine_synchronize(dcpaud->chan); + dcpaud_link_teardown(dcpaud); + } + ret = dcpaud_link_prepare(dcpaud, substream); + if (ret) + return ret; + dev_dbg(dcpaud->dev, "link prepared at START\n"); + } else if (held) { + dev_dbg(dcpaud->dev, "stream follows the held link's silence\n"); + } + /* The first period descriptor is issued before the link starts. */ ret = snd_dmaengine_pcm_trigger(substream, cmd); if (ret) return ret; + WRITE_ONCE(dcpaud->stream_dma, true); - guard(mutex)(&dcpaud->link_lock); if (!dcpaud->link_prepared) return -EBADFD; if (dcpaud->link_started) return 0; ret = dcp_audiosrv_link(dcpaud->dcp_dev, DCP_AUDIO_LINK_START, - &dcpaud->selected_cookie); + &dcpaud->link_cookie); dcpaud->link_started = !ret; return ret; } @@ -765,8 +982,21 @@ static int dcp_pcm_native_trigger(struct snd_pcm_substream *substream, int cmd) return dcp_pcm_native_start(substream, cmd); case SNDRV_PCM_TRIGGER_STOP: + /* + * The DMA plays the cleared buffer on, with the stream's PM + * reference, until sync_stop swaps it for the held link's + * silence: the link must not run dry meanwhile. + */ + if (dcpaud_holds(dcpaud) && READ_ONCE(dcpaud->stream_dma) && + substream->runtime->channels == 2 && substream->runtime->dma_area) { + memset(substream->runtime->dma_area, 0, substream->runtime->dma_bytes); + WRITE_ONCE(dcpaud->dma_cleared, true); + return 0; + } + fallthrough; case SNDRV_PCM_TRIGGER_SUSPEND: - ret = snd_dmaengine_pcm_trigger(substream, cmd); + /* A START that issued no DMA leaves a held link's silence alone. */ + ret = READ_ONCE(dcpaud->stream_dma) ? snd_dmaengine_pcm_trigger(substream, cmd) : 0; pm_runtime_mark_last_busy(dcpaud->dev); __pm_runtime_put_autosuspend(dcpaud->dev); return ret; @@ -781,6 +1011,24 @@ static int dcp_pcm_native_sync_stop(struct snd_pcm_substream *substream) struct dcp_audio *dcpaud = substream->pcm->private_data; int ret; + WRITE_ONCE(dcpaud->stream_dma, false); + if (READ_ONCE(dcpaud->dma_cleared)) { + guard(mutex)(&dcpaud->link_lock); + WRITE_ONCE(dcpaud->dma_cleared, false); + dmaengine_terminate_async(dcpaud->chan); + /* Queued first, the silence follows the stream without a gap. */ + if (dcpaud->silence_running || !dcpaud->link_started || + !dcp_audiosrv_link_live(dcpaud->dcp_dev) || dcpaud_silence_start(dcpaud)) { + dmaengine_synchronize(dcpaud->chan); + dcpaud_link_teardown(dcpaud); + pm_runtime_mark_last_busy(dcpaud->dev); + pm_runtime_put_autosuspend(dcpaud->dev); + return 0; + } + dmaengine_synchronize(dcpaud->chan); + return 0; + } + /* SIO TERMINATE acknowledged and the issued periods reported back. */ ret = snd_dmaengine_pcm_sync_stop(substream); @@ -812,6 +1060,10 @@ void dcpaud_release_link(struct platform_device *pdev, const u64 *gen) /* Only that instance's link: a later instance's PCM keeps its own. */ if (gen && !dcp_audiosrv_link_on(dcpaud->dcp_dev, *gen)) return; + dcpaud_silence_stop(dcpaud); + /* A stopped stream still playing its cleared buffer: sync_stop finishes it. */ + if (dcpaud->chan && READ_ONCE(dcpaud->dma_cleared)) + dmaengine_terminate_async(dcpaud->chan); if (dcpaud->chan) dmaengine_synchronize(dcpaud->chan); dcpaud_link_teardown(dcpaud); @@ -905,8 +1157,10 @@ static void dcpaud_report_hotplug(struct dcp_audio *dcpaud, bool connected) } dcpaud->connected = connected; - if (connected) + if (connected) { dcpaud->connection_cookie++; + dcpaud->connected_at = ktime_get(); + } mutex_unlock(&dcpaud->data_lock); snd_jack_report(dcpaud->jack, connected ? SND_JACK_AVOUT : 0); @@ -1010,6 +1264,11 @@ void dcpaud_disconnect(struct platform_device *pdev) mutex_lock(&dcpaud->data_lock); dcpaud_report_hotplug(dcpaud, false); +#if IS_ENABLED(CONFIG_DRM_APPLE_DCP_T6030_AUDIO) + /* The service may go without a release, and a held link with it. */ + if (dcpaud->native_link) + schedule_work(&dcpaud->stale_work); +#endif } static int dcpaud_comp_bind(struct device *dev, struct device *main, void *data) @@ -1112,6 +1371,27 @@ static void dcpaud_comp_unbind(struct device *dev, struct device *main, /* snd_card_free_when_closed() checks for NULL */ snd_card_free_when_closed(dcpaud->card); +#if IS_ENABLED(CONFIG_DRM_APPLE_DCP_T6030_AUDIO) + if (dcpaud->native_link) { + bool held; + + cancel_work_sync(&dcpaud->stale_work); + /* A held link ends with the card; an open PCM's close finds it gone. */ + scoped_guard(mutex, &dcpaud->link_lock) { + held = dcpaud->silence_running; + if (held) + dcpaud_link_teardown(dcpaud); + if (dcpaud->silence) { + dma_free_coherent(dcpaud->chan->device->dev, DCPAUD_SILENCE_BYTES, + dcpaud->silence, dcpaud->silence_addr); + dcpaud->silence = NULL; + } + } + if (held) + dcp_audiosrv_close(dcpaud->dcp_dev); + } +#endif + if (dcpaud->dma_link) device_link_del(dcpaud->dma_link); } @@ -1143,6 +1423,7 @@ static int dcpaud_probe(struct platform_device *pdev) mutex_init(&dcpaud->data_lock); #if IS_ENABLED(CONFIG_DRM_APPLE_DCP_T6030_AUDIO) mutex_init(&dcpaud->link_lock); + INIT_WORK(&dcpaud->stale_work, dcpaud_stale_work); #endif INIT_DELAYED_WORK(&dcpaud->void_work, dcpaud_void_work); platform_set_drvdata(pdev, dcpaud); diff --git a/drivers/gpu/drm/apple/audio.h b/drivers/gpu/drm/apple/audio.h index 79bee44bcefbc..f1f125305da59 100644 --- a/drivers/gpu/drm/apple/audio.h +++ b/drivers/gpu/drm/apple/audio.h @@ -37,6 +37,8 @@ void dcpaud_release_link(struct platform_device *pdev, const u64 *gen); bool dcp_audiosrv_link_on(struct device *dev, u64 gen); /* The prepared link still belongs to the client of the published instance. */ bool dcp_audiosrv_link_live(struct device *dev); +/* The display processor drives a Type-C route rather than the HDMI port. */ +bool dcp_audiosrv_typec(struct device *dev); #endif #endif /* __AUDIO_H__ */ diff --git a/drivers/gpu/drm/apple/av.c b/drivers/gpu/drm/apple/av.c index fad5fe6c43d1a..cda3e65cbf3dd 100644 --- a/drivers/gpu/drm/apple/av.c +++ b/drivers/gpu/drm/apple/av.c @@ -411,6 +411,12 @@ bool dcp_audiosrv_link_live(struct device *dev) return asrv->link_gen == asrv->open_gen && av_audiosrv_open_published(asrv); } +bool dcp_audiosrv_typec(struct device *dev) +{ + KUNIT_STATIC_STUB_REDIRECT(dcp_audiosrv_typec, dev); + return dcp_is_typec_output(dev_get_drvdata(dev)); +} + /* * The link commands of the 14.7 external role's audio service, the native * audio driver's: prepare, start, stop and complete all take the selected From 519aa78285cb260789ef4e0fc28d10bebf354520 Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Wed, 28 Oct 2026 02:33:53 +0100 Subject: [PATCH 14/28] arm64: dts: apple: t603x-j514s-j516s: add Type-C DisplayPort audio Link the Type-C DisplayPort processor (dcpext1) to DPA2 on SIO channel 0x68, as the ADT links dcpext1 to dp-audio2, so a display on a USB-C port gets an audio device too. Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 [ryan: the DPA2 audio controller sits at the root, as this tree places DPA1 in t603x-j514s-j516s-hdmi.dtsi (neither has AP-side registers, so neither is a simple-bus child)] (cherry picked from commit 5ab5f59ee8312523c85fc479cc0c2f847aaf2803) Assisted-by: LLM Signed-off-by: Ryan Murray --- .../dts/apple/t603x-j514s-j516s-usb4.dtsi | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/arch/arm64/boot/dts/apple/t603x-j514s-j516s-usb4.dtsi b/arch/arm64/boot/dts/apple/t603x-j514s-j516s-usb4.dtsi index b6c397e17c905..8aae684af663a 100644 --- a/arch/arm64/boot/dts/apple/t603x-j514s-j516s-usb4.dtsi +++ b/arch/arm64/boot/dts/apple/t603x-j514s-j516s-usb4.dtsi @@ -70,3 +70,42 @@ }; }; +/* + * DPA2: the Type-C DisplayPort processor's (dcpext1) audio, SIO channel 0x68, + * as the ADT links dcpext1 to dp-audio2. + */ +&dcpext1 { + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + dcpext1_audio: endpoint { + remote-endpoint = <&dpaudio2_dcpext1>; + }; + }; + }; +}; + +/* DPA2, like DPA1, has no AP-side registers: not a child of the SoC bus. */ +/ { + dpaudio2: audio-controller-2 { + compatible = "apple,t6030-dpaudio", "apple,dpaudio"; + dmas = <&sio 0x68>; + dma-names = "tx"; + power-domains = <&ps_dpa2>; + + ports { + #address-cells = <1>; + #size-cells = <0>; + + port@0 { + reg = <0>; + dpaudio2_dcpext1: endpoint { + remote-endpoint = <&dcpext1_audio>; + }; + }; + }; + }; +}; From 9251c670aaf0b8fed4de9ecc5d70a7645d5c262f Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Wed, 28 Oct 2026 06:47:17 +0100 Subject: [PATCH 15/28] drm/apple: hold the HDMI audio link at one rate and sample size PipeWire changes the device rate to follow its streams: 44.1 kHz music, 48 kHz video. A held link of another rate or sample size was replaced at START by one with the same channel layout, which the J514S sink mutes like a same-format restart. After 48 kHz then 44.1 kHz streams, every stream was silent until a replug. macOS runs the link at one nominal rate and format and converts streams to it. While links are held, the PCM now offers one rate (48 kHz, else 44.1 or 32 kHz) and one sample size (24 bits, else 16), taken from one element that offers them together in stereo, and userspace resamples. The pair covers every channel layout: userspace sets the format before the channel count, so a pair applied only to stereo let it rule stereo out. Channel layouts stay free: a layout change is heard. Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 (cherry picked from commit 0ec6112931eceaaec119d04283ed838f70fa5678) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/audio.c | 74 +++++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/drivers/gpu/drm/apple/audio.c b/drivers/gpu/drm/apple/audio.c index 472e653f46d41..6b00a854a1519 100644 --- a/drivers/gpu/drm/apple/audio.c +++ b/drivers/gpu/drm/apple/audio.c @@ -669,6 +669,75 @@ static bool dcpaud_holds(struct dcp_audio *dcpaud) return hold >= 2 || (hold == 1 && !dcp_audiosrv_typec(dcpaud->dcp_dev)); } +/* + * A change of sample rate or sample size with an unchanged channel layout + * replaces the link with a new one, which this sink mutes just as it does a + * same-format restart. PipeWire changes the rate to follow its streams + * (44.1 kHz music, 48 kHz video). macOS runs the link at one nominal rate + * and format and converts the streams to it; so does this driver while it + * holds links: the PCM offers one rate (48 kHz, else 44.1 or 32 kHz) and one + * sample size (24 bits, else 16), both of one element that takes them in + * stereo, and userspace resamples. Channel layouts stay free: a layout + * change is heard. + * + * The pair covers every layout, not only the stereo that links are held in: + * userspace commonly sets the sample format before the channel count, and a + * pair applied only once a stream is stereo let it set a format that then + * ruled stereo out (PipeWire built no HDMI sink). + */ +static int dcpaud_pin_rate_format(struct dcp_audio *dcpaud, + struct snd_pcm_runtime *runtime) +{ + static const unsigned int rates[] = { 48000, 44100, 32000 }; + static const snd_pcm_format_t formats[] = { + SNDRV_PCM_FORMAT_S24_LE, SNDRV_PCM_FORMAT_S16_LE, + }; + struct dcp_sound_format_mask sieve = { + .formats = SNDRV_PCM_FMTBIT_S16_LE | SNDRV_PCM_FMTBIT_S24_LE, + .rates = GENMASK(snd_pcm_known_rates.count - 1, 0), + .nchans = BIT(2), + }; + struct dcp_sound_format_mask hits = { 0, 0, 0 }; + struct dcp_parse_ctx elements = { + .dcp = dev_get_drvdata(dcpaud->dcp_dev), + .blob = dcpaud->elements + 4, + .len = dcpaud_elements_size(dcpaud) - 4, + .pos = 0, + .v14_7_audio = dcpaud->native_link, + }; + unsigned int rate = 0, rate_bit = 0, i, j; + int ret; + + parse_sound_constraints(&elements, &sieve, &hits); + for (i = 0; i < ARRAY_SIZE(rates) && !rate; i++) + for (j = 0; j < snd_pcm_known_rates.count; j++) + if (snd_pcm_known_rates.list[j] == rates[i] && (hits.rates & BIT(j))) { + rate = rates[i]; + rate_bit = j; + } + if (!rate) + return 0; /* no stereo LPCM element: the sink decides */ + + /* The sample size of an element that takes that rate in stereo */ + sieve.rates = BIT(rate_bit); + elements.pos = 0; + parse_sound_constraints(&elements, &sieve, &hits); + for (i = 0; i < ARRAY_SIZE(formats); i++) + if (hits.formats & pcm_format_to_bits(formats[i])) + break; + if (i == ARRAY_SIZE(formats)) + return 0; + + dev_dbg(dcpaud->dev, "held links run at %u Hz, format %d\n", rate, + (__force int)formats[i]); + ret = snd_pcm_hw_constraint_single(runtime, SNDRV_PCM_HW_PARAM_RATE, rate); + if (ret < 0) + return ret; + ret = snd_pcm_hw_constraint_mask64(runtime, SNDRV_PCM_HW_PARAM_FORMAT, + pcm_format_to_bits(formats[i])); + return ret < 0 ? ret : 0; +} + /* * Seen once, at boot: a link started about 0.2 s after the display came on * was not heard, and holding it kept it silent; the next connection's first @@ -753,6 +822,11 @@ static int dcp_pcm_native_open(struct snd_pcm_substream *substream) } ret = dcp_pcm_open(substream); + if (!ret && live && !dcpaud->open_unplugged && dcpaud_holds(dcpaud)) { + ret = dcpaud_pin_rate_format(dcpaud, substream->runtime); + if (ret) + dcp_pcm_close(substream); + } if (ret && live) { bool held; From bc8ef9e9050f23609f5941f972f9c017249cde84 Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Wed, 28 Oct 2026 08:05:18 +0100 Subject: [PATCH 16/28] dmaengine: apple-sio: hand over to an idle transfer without a gap A DP audio sink behind the J514S HDMI port mutes for good when its samples stop for 20 ms or more, until the DP link trains again. Swapping one cyclic transfer for another stops the channel with TERMINATE and starts the next one after its acknowledgment: usually short enough, sometimes not. Let a client set an idle transfer (struct apple_sio_idle, passed as peripheral_config in a slave config call of its own). The channel issues it while no other transfer runs. Terminating a transfer then hands over to it after the issued descriptors, with no TERMINATE, and a transfer issued later follows its issued descriptors the same way. From its first idle transfer until it is quiet again, a channel accounts for its issued descriptors in a ring, seeded with what it has in flight: reports complete them in issue order, so each is charged to the transfer that issued it, and one ISSUE is outstanding at a time. dmaengine_synchronize() waits for an ended transfer's descriptors. If they do not complete within 500 ms, it stops the channel with TERMINATE and waits for their reports again, and resets the ring if they never come. Removing the idle transfer does the same, and fails if the channel cannot be confirmed stopped. A channel that never had an idle transfer keeps the old accounting unchanged: the DP audio of M1 and M2 Macs sets none. Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 (cherry picked from commit f4dd5a24825605d3ee8a791d7621a5ca5bfbaa74) Assisted-by: LLM Signed-off-by: Ryan Murray --- MAINTAINERS | 1 + drivers/dma/apple-sio.c | 569 ++++++++++++++++++++++++++++++++-- include/linux/dma/apple-sio.h | 44 +++ 3 files changed, 580 insertions(+), 34 deletions(-) create mode 100644 include/linux/dma/apple-sio.h diff --git a/MAINTAINERS b/MAINTAINERS index 2e0aab08c5d6f..cc21f5f3ba263 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -2695,6 +2695,7 @@ F: drivers/dma/apple-sio-probe-test.dtso F: drivers/dma/apple-sio-t6030-test.dtso F: drivers/dma/apple-sio-test.c F: drivers/dma/apple-sio.c +F: include/linux/dma/apple-sio.h F: sound/soc/apple/* F: sound/soc/codecs/cs42l83-i2c.c F: sound/soc/codecs/cs42l84.* diff --git a/drivers/dma/apple-sio.c b/drivers/dma/apple-sio.c index 0b20b5b2b74bc..5ea2209f92e7d 100644 --- a/drivers/dma/apple-sio.c +++ b/drivers/dma/apple-sio.c @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -21,6 +22,7 @@ #include #include #include +#include #include "dmaengine.h" #include "virt-dma.h" @@ -103,6 +105,38 @@ struct sio_chan { struct sio_shmem_chan_config cfg; struct sio_tx *current_tx; + + /* + * Until a client sets an idle transfer, the channel keeps the old + * accounting, charging ACKs and REPORTs to the current transfer; its + * ISSUEs not yet acknowledged are only counted. + */ + unsigned int pending; + + /* + * The ring accounting, from the first idle transfer until the channel + * is quiet again (sio_ring_leave_locked()): the client's idle transfer + * (struct apple_sio_idle), issued while no other transfer is; + * transfers ended while it is set, until their issued descriptors + * completed; and the owners of the issued descriptors in issue order, + * which is the order of their reports. The last one may still wait for + * its ISSUE to be acknowledged. + */ + bool ring; + struct sio_tx *idle; + struct list_head retired; + wait_queue_head_t drained; + struct sio_tx *issued[SIO_MAX_NINFLIGHT]; + unsigned int issued_head; + unsigned int nissued; + bool issuing; + /* Nothing is issued between a TERMINATE and the ring's reset. */ + bool stopping; + /* + * Ended transfers with nothing in flight, freed once no completion + * callback can still be running (sio_free_dead()). + */ + struct list_head dead; }; #define SIO_NTAGS 16 @@ -146,6 +180,7 @@ struct sio_tx { struct completion done; bool terminated; + bool retired; size_t period_len; int nperiods; int ninflight; @@ -436,23 +471,22 @@ static void sio_tx_free(struct virt_dma_desc *vd) kfree(siotx); } -static struct dma_async_tx_descriptor *sio_prep_dma_cyclic( - struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len, - size_t period_len, enum dma_transfer_direction direction, - unsigned long flags) +static struct sio_tx *sio_alloc_tx(struct sio_chan *siochan, dma_addr_t buf_addr, + size_t buf_len, size_t period_len, gfp_t gfp) { - struct sio_chan *siochan = to_sio_chan(chan); struct sio_tx *siotx = NULL; - int i, nperiods = buf_len / period_len; + int i, nperiods; - if (direction != sio_chan_direction(siochan->no)) + if (!period_len || buf_len < period_len) return NULL; + nperiods = buf_len / period_len; - siotx = kzalloc(struct_size(siotx, siodesc, nperiods), GFP_NOWAIT); + siotx = kzalloc(struct_size(siotx, siodesc, nperiods), gfp); if (!siotx) return NULL; init_completion(&siotx->done); + siotx->vd.tx.chan = &siochan->vc.chan; siotx->period_len = period_len; siotx->nperiods = nperiods; @@ -461,7 +495,6 @@ static struct dma_async_tx_descriptor *sio_prep_dma_cyclic( siotx->siodesc[i] = d = sio_alloc_desc(siochan->host); if (!d) { - siotx->vd.tx.chan = &siochan->vc.chan; sio_tx_free(&siotx->vd); return NULL; } @@ -478,6 +511,24 @@ static struct dma_async_tx_descriptor *sio_prep_dma_cyclic( } dma_wmb(); + return siotx; +} + +static struct dma_async_tx_descriptor *sio_prep_dma_cyclic( + struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len, + size_t period_len, enum dma_transfer_direction direction, + unsigned long flags) +{ + struct sio_chan *siochan = to_sio_chan(chan); + struct sio_tx *siotx; + + if (direction != sio_chan_direction(siochan->no)) + return NULL; + + siotx = sio_alloc_tx(siochan, buf_addr, buf_len, period_len, GFP_NOWAIT); + if (!siotx) + return NULL; + return vchan_tx_prep(&siochan->vc, &siotx->vd, flags); } @@ -522,32 +573,172 @@ static enum dma_status sio_tx_status(struct dma_chan *chan, dma_cookie_t cookie, static bool sio_fill_in_locked(struct sio_chan *siochan); -static void sio_handle_issue_ack(struct sio_chan *siochan, void *cookie, bool ok) +static struct sio_tx **sio_issued_slot(struct sio_chan *siochan, unsigned int i) { - dma_cookie_t tx_cookie = (unsigned long) cookie; - unsigned long flags; - struct sio_tx *tx; + return &siochan->issued[(siochan->issued_head + i) % SIO_MAX_NINFLIGHT]; +} + +/* Neither in flight nor waiting for its ISSUE to be acknowledged */ +static bool sio_tx_quiet_locked(struct sio_chan *siochan, struct sio_tx *tx) +{ + unsigned int i; + + if (tx->ninflight) + return false; + for (i = 0; i < siochan->nissued; i++) + if (*sio_issued_slot(siochan, i) == tx) + return false; + return true; +} + +/* A transfer that is freed no longer owns issued descriptors */ +static void sio_scrub_locked(struct sio_chan *siochan, struct sio_tx *tx) +{ + unsigned int i; + + for (i = 0; i < siochan->nissued; i++) + if (*sio_issued_slot(siochan, i) == tx) + *sio_issued_slot(siochan, i) = NULL; +} + +/* Descriptors are issued from the current transfer, else from the idle one. */ +static struct sio_tx *sio_source_locked(struct sio_chan *siochan) +{ + struct sio_tx *tx = siochan->current_tx; + if (tx) + return tx->terminated ? NULL : tx; + if (siochan->idle && !siochan->idle->terminated) + return siochan->idle; + return NULL; +} + +/* + * The first idle transfer: the ring accounting starts from what the old one + * has in flight, all of it the current transfer's. It cannot place two ISSUEs + * still waiting for their acks, nor an ended transfer's descriptors. + */ +static int sio_ring_enter_locked(struct sio_chan *siochan) +{ + struct sio_tx *tx = siochan->current_tx; + unsigned int i, n = tx ? tx->ninflight : 0; + + if (siochan->pending > 1 || (siochan->pending && !tx) || (tx && tx->terminated) || + n + siochan->pending > SIO_MAX_NINFLIGHT) + return -EBUSY; + siochan->issued_head = 0; + siochan->nissued = n + siochan->pending; + for (i = 0; i < siochan->nissued; i++) + siochan->issued[i] = tx; + siochan->issuing = siochan->pending; + siochan->pending = 0; + siochan->ring = true; + return 0; +} + +/* + * The resync point: once the channel is quiet, with no idle, current or + * retired transfer and nothing issued, the old accounting resumes. + */ +static void sio_ring_leave_locked(struct sio_chan *siochan) +{ + if (!siochan->ring || siochan->idle || siochan->current_tx || + !list_empty(&siochan->retired) || siochan->nissued || siochan->issuing) + return; + siochan->ring = false; + siochan->issued_head = 0; +} + +/* + * An ended transfer with no descriptor left in flight: a retired one goes to + * be freed, the waiter of another one is woken. + */ +static void sio_settle_locked(struct sio_chan *siochan, struct sio_tx *tx) +{ + if (!tx || !tx->terminated || !sio_tx_quiet_locked(siochan, tx)) + return; + if (tx->retired) { + list_move_tail(&tx->vd.node, &siochan->dead); + wake_up(&siochan->drained); + return; + } + /* sio_terminate_work() or sio_clear_idle() */ + complete(&tx->done); +} + +/* + * The old accounting, for channels that never had an idle transfer: an ACK + * is the current transfer's, if its cookie still is. + */ +static void sio_old_issue_ack_locked(struct sio_chan *siochan, dma_cookie_t tx_cookie, bool ok) +{ + struct sio_tx *tx = siochan->current_tx; + + if (siochan->pending) + siochan->pending--; if (!ok) { dev_err(siochan->host->dev, "nacked issue on chan %d\n", siochan->no); return; } + if (!tx || tx_cookie != tx->vd.tx.cookie || tx->terminated) + return; - spin_lock_irqsave(&siochan->vc.lock, flags); - if (!siochan->current_tx || tx_cookie != siochan->current_tx->vd.tx.cookie || - siochan->current_tx->terminated) - goto out; - - tx = siochan->current_tx; tx->next = (tx->next + 1) % tx->nperiods; tx->ninflight++; sio_fill_in_locked(siochan); +} + +/* The ring accounting: an ACK is the last issued descriptor's. */ +static void sio_ring_issue_ack_locked(struct sio_chan *siochan, bool ok) +{ + struct sio_tx *tx; -out: + siochan->issuing = false; + if (!siochan->nissued) { + dev_err_ratelimited(siochan->host->dev, "chan %d: ack without an issued descriptor\n", + siochan->no); + return; + } + + tx = *sio_issued_slot(siochan, siochan->nissued - 1); + if (!ok) { + dev_err(siochan->host->dev, "nacked issue on chan %d\n", siochan->no); + siochan->nissued--; + if (!siochan->nissued) + wake_up(&siochan->drained); + /* + * During a handover this may have been the ended transfer's + * last ISSUE, and no report is left to start the next one. A + * transfer whose own ISSUE was refused is not issued again. + */ + if (sio_source_locked(siochan) != tx) + sio_fill_in_locked(siochan); + sio_settle_locked(siochan, tx); + return; + } + + /* An ended transfer's descriptor is in flight all the same. */ + if (tx) { + tx->next = (tx->next + 1) % tx->nperiods; + tx->ninflight++; + } + sio_fill_in_locked(siochan); +} + +static void sio_handle_issue_ack(struct sio_chan *siochan, void *cookie, bool ok) +{ + unsigned long flags; + + spin_lock_irqsave(&siochan->vc.lock, flags); + if (siochan->ring) + sio_ring_issue_ack_locked(siochan, ok); + else + sio_old_issue_ack_locked(siochan, (unsigned long)cookie, ok); spin_unlock_irqrestore(&siochan->vc.lock, flags); } -static bool sio_fill_in_locked(struct sio_chan *siochan) +/* The old accounting: the current transfer's next descriptor */ +static bool sio_fill_in_old_locked(struct sio_chan *siochan) { struct sio_data *sio = siochan->host; struct sio_tx *tx = siochan->current_tx; @@ -561,13 +752,50 @@ static bool sio_fill_in_locked(struct sio_chan *siochan) ret = sio_send_siomsg_atomic(sio, FIELD_PREP(SIOMSG_EP, siochan->no) | FIELD_PREP(SIOMSG_TYPE, MSG_ISSUE) | FIELD_PREP(SIOMSG_DATA, sio_coproc_desc_slot(sio, d)), - sio_handle_issue_ack, (void *) (uintptr_t) tx->vd.tx.cookie); + sio_handle_issue_ack, (void *)(uintptr_t)tx->vd.tx.cookie); if (ret < 0) dev_err_ratelimited(sio->dev, "can't issue on chan %d ninflight %d: %d\n", siochan->no, tx->ninflight, ret); + else + siochan->pending++; + return true; +} + +/* The ring accounting: one ISSUE at a time, each after the previous one's ack */ +static bool sio_fill_in_ring_locked(struct sio_chan *siochan) +{ + struct sio_data *sio = siochan->host; + struct sio_tx *tx = sio_source_locked(siochan); + struct sio_coproc_desc *d; + int ret; + + if (!tx || siochan->issuing || siochan->stopping || + siochan->nissued >= SIO_MAX_NINFLIGHT) + return false; + + d = tx->siodesc[tx->next]; + ret = sio_send_siomsg_atomic(sio, FIELD_PREP(SIOMSG_EP, siochan->no) | + FIELD_PREP(SIOMSG_TYPE, MSG_ISSUE) | + FIELD_PREP(SIOMSG_DATA, sio_coproc_desc_slot(sio, d)), + sio_handle_issue_ack, NULL); + if (ret < 0) { + dev_err_ratelimited(sio->dev, "can't issue on chan %d ninflight %d: %d\n", + siochan->no, siochan->nissued, ret); + return false; + } + /* The ack takes the channel lock, held here. */ + *sio_issued_slot(siochan, siochan->nissued++) = tx; + siochan->issuing = true; return true; } +static bool sio_fill_in_locked(struct sio_chan *siochan) +{ + if (siochan->ring) + return sio_fill_in_ring_locked(siochan); + return siochan->current_tx && sio_fill_in_old_locked(siochan); +} + static void sio_update_current_tx_locked(struct sio_chan *siochan) { struct virt_dma_desc *vd = vchan_next_desc(&siochan->vc); @@ -593,14 +821,37 @@ static void sio_issue_pending(struct dma_chan *chan) static int sio_terminate_all(struct dma_chan *chan) { struct sio_chan *siochan = to_sio_chan(chan); + struct sio_tx *tx; unsigned long flags; LIST_HEAD(to_free); spin_lock_irqsave(&siochan->vc.lock, flags); - if (siochan->current_tx && !siochan->current_tx->terminated) { - dma_cookie_complete(&siochan->current_tx->vd.tx); - siochan->current_tx->terminated = true; - schedule_work(&siochan->terminate_wq); + tx = siochan->current_tx; + if (tx && !tx->terminated) { + dma_cookie_complete(&tx->vd.tx); + tx->terminated = true; + if (siochan->ring && siochan->idle && !siochan->idle->terminated) { + /* + * No TERMINATE: the idle transfer follows the issued + * descriptors, and the peripheral runs on without a gap. + * sio_synchronize() waits for those to complete. + */ + if (siochan->vc.cyclic == &tx->vd) + siochan->vc.cyclic = NULL; + siochan->current_tx = NULL; + /* Its callback may be running: sio_free_dead() frees it. */ + if (sio_tx_quiet_locked(siochan, tx)) { + list_add_tail(&tx->vd.node, &siochan->dead); + } else { + tx->retired = true; + list_add_tail(&tx->vd.node, &siochan->retired); + } + sio_fill_in_locked(siochan); + } else { + if (siochan->ring && sio_tx_quiet_locked(siochan, tx)) + complete(&tx->done); + schedule_work(&siochan->terminate_wq); + } } vchan_get_all_descriptors(&siochan->vc, &to_free); spin_unlock_irqrestore(&siochan->vc.lock, flags); @@ -639,23 +890,223 @@ static void sio_terminate_work(struct work_struct *wq) spin_lock_irqsave(&siochan->vc.lock, flags); WARN_ON(siochan->current_tx != tx); siochan->current_tx = NULL; + sio_scrub_locked(siochan, tx); sio_update_current_tx_locked(siochan); + sio_ring_leave_locked(siochan); spin_unlock_irqrestore(&siochan->vc.lock, flags); sio_tx_free(&tx->vd); } +static bool sio_drained(struct sio_chan *siochan) +{ + unsigned long flags; + bool drained; + + spin_lock_irqsave(&siochan->vc.lock, flags); + drained = list_empty(&siochan->retired); + spin_unlock_irqrestore(&siochan->vc.lock, flags); + + return drained; +} + +static bool sio_ring_empty(struct sio_chan *siochan) +{ + unsigned long flags; + bool empty; + + spin_lock_irqsave(&siochan->vc.lock, flags); + empty = !siochan->nissued && !siochan->issuing; + spin_unlock_irqrestore(&siochan->vc.lock, flags); + + return empty; +} + +/* + * Issued descriptors that did not complete in time: stop the channel and wait + * for their reports, so that their buffers are no longer read. Nothing is + * issued from the TERMINATE until the channel is known stopped. A channel + * that acknowledged TERMINATE and still reports nothing is resynced: its ring + * is emptied, as nothing it holds is played any more. A channel that refused + * TERMINATE or did not answer cannot be known to have stopped: -EIO, and its + * transfers stay as they are. Then the current or idle transfer is issued + * again. + */ +static int sio_stop_quiet(struct sio_chan *siochan) +{ + struct device *dev = siochan->host->dev; + unsigned long flags; + struct sio_tx *tx; + unsigned int i; + int ret; + + dev_err(dev, "chan %d: issued descriptors did not complete, stopping it\n", siochan->no); + spin_lock_irqsave(&siochan->vc.lock, flags); + siochan->stopping = true; + spin_unlock_irqrestore(&siochan->vc.lock, flags); + + /* 1 on ACK, 0 on NACK, negative without an answer */ + ret = sio_call(siochan->host, FIELD_PREP(SIOMSG_EP, siochan->no) | + FIELD_PREP(SIOMSG_TYPE, MSG_TERMINATE)); + if (ret == 1) + wait_event_timeout(siochan->drained, sio_ring_empty(siochan), + msecs_to_jiffies(500)); + else if (!ret) + dev_err(dev, "chan %d: TERMINATE refused, its buffers may still be read\n", + siochan->no); + else + dev_err(dev, "chan %d: not stopped (%d), its buffers may still be read\n", + siochan->no, ret); + + spin_lock_irqsave(&siochan->vc.lock, flags); + if (ret == 1 && (siochan->nissued || siochan->issuing)) { + dev_err(dev, "chan %d: stopped with %u descriptors unreported, ring reset\n", + siochan->no, siochan->nissued); + for (i = 0; i < siochan->nissued; i++) { + tx = *sio_issued_slot(siochan, i); + if (tx) + tx->ninflight = 0; + } + siochan->nissued = 0; + siochan->issuing = false; + siochan->issued_head = 0; + } + if (ret == 1) { + list_splice_tail_init(&siochan->retired, &siochan->dead); + if (siochan->idle && siochan->idle->terminated) + complete(&siochan->idle->done); + } + siochan->stopping = false; + sio_fill_in_locked(siochan); + spin_unlock_irqrestore(&siochan->vc.lock, flags); + + return ret == 1 ? 0 : -EIO; +} + +/* Frees the ended transfers once no completion callback can still use them. */ +static void sio_free_dead(struct sio_chan *siochan) +{ + struct sio_tx *tx, *n; + unsigned long flags; + LIST_HEAD(dead); + + spin_lock_irqsave(&siochan->vc.lock, flags); + list_splice_init(&siochan->dead, &dead); + spin_unlock_irqrestore(&siochan->vc.lock, flags); + if (list_empty(&dead)) + return; + + /* vchan_complete() runs a callback with its descriptor unlocked. */ + tasklet_kill(&siochan->vc.task); + list_for_each_entry_safe(tx, n, &dead, vd.node) { + list_del(&tx->vd.node); + sio_tx_free(&tx->vd); + } +} + static void sio_synchronize(struct dma_chan *chan) { struct sio_chan *siochan = to_sio_chan(chan); flush_work(&siochan->terminate_wq); + if (READ_ONCE(siochan->ring)) { + /* The caller frees the buffer on return; nothing can stop that. */ + if (!wait_event_timeout(siochan->drained, sio_drained(siochan), + msecs_to_jiffies(500)) && + sio_stop_quiet(siochan)) + dev_WARN_ONCE(siochan->host->dev, 1, + "chan %d: synchronized without being stopped\n", + siochan->no); + /* No callback of an ended transfer runs after this. */ + tasklet_kill(&siochan->vc.task); + } + sio_free_dead(siochan); +} + +/* + * Returns once the idle transfer's descriptors completed. If the channel + * cannot be confirmed stopped, the idle transfer stays, its buffer in use. + */ +static int sio_clear_idle(struct sio_chan *siochan) +{ + struct sio_tx *idle; + unsigned long flags; + bool quiet, stop; + int ret; + + spin_lock_irqsave(&siochan->vc.lock, flags); + idle = siochan->idle; + if (!idle) { + spin_unlock_irqrestore(&siochan->vc.lock, flags); + return 0; + } + idle->terminated = true; + quiet = sio_tx_quiet_locked(siochan, idle); + /* Nothing follows the idle transfer: the channel stops. */ + stop = !quiet && !siochan->current_tx; + spin_unlock_irqrestore(&siochan->vc.lock, flags); + + if (stop) { + ret = sio_call(siochan->host, FIELD_PREP(SIOMSG_EP, siochan->no) | + FIELD_PREP(SIOMSG_TYPE, MSG_TERMINATE)); + if (ret < 0) + dev_err(siochan->host->dev, "terminate call on chan %d failed: %d\n", + siochan->no, ret); + } + if (!quiet && !wait_for_completion_timeout(&idle->done, msecs_to_jiffies(500))) { + ret = sio_stop_quiet(siochan); + if (ret) + return ret; + } + + spin_lock_irqsave(&siochan->vc.lock, flags); + siochan->idle = NULL; + sio_scrub_locked(siochan, idle); + sio_ring_leave_locked(siochan); + spin_unlock_irqrestore(&siochan->vc.lock, flags); + + sio_tx_free(&idle->vd); + sio_free_dead(siochan); + return 0; +} + +/* See struct apple_sio_idle */ +static int sio_set_idle(struct sio_chan *siochan, const struct apple_sio_idle *config) +{ + struct sio_tx *idle; + unsigned long flags; + int ret; + + if (!config->len) + return sio_clear_idle(siochan); + if (!config->period_len || config->len < config->period_len) + return -EINVAL; + + idle = sio_alloc_tx(siochan, config->addr, config->len, config->period_len, GFP_KERNEL); + if (!idle) + return -ENOMEM; + + spin_lock_irqsave(&siochan->vc.lock, flags); + ret = siochan->idle ? -EBUSY : 0; + if (!ret && !siochan->ring) + ret = sio_ring_enter_locked(siochan); + if (ret) { + spin_unlock_irqrestore(&siochan->vc.lock, flags); + sio_tx_free(&idle->vd); + return ret; + } + siochan->idle = idle; + sio_fill_in_locked(siochan); + spin_unlock_irqrestore(&siochan->vc.lock, flags); + + return 0; } static void sio_free_chan_resources(struct dma_chan *chan) { sio_terminate_all(chan); sio_synchronize(chan); + sio_clear_idle(to_sio_chan(chan)); vchan_free_chan_resources(&to_sio_chan(chan)->vc); } @@ -678,20 +1129,61 @@ static void sio_rtk_crashed(void *cookie, const void *crashlog, size_t crashlog_ dev_err(sio->dev, "SIO down (crashed)"); } -static void sio_process_report(struct sio_chan *siochan) +/* The old accounting: a report is the current transfer's. */ +static void sio_old_report_locked(struct sio_chan *siochan) { - unsigned long flags; + struct sio_tx *tx = siochan->current_tx; - spin_lock_irqsave(&siochan->vc.lock, flags); - if (siochan->current_tx) { - struct sio_tx *tx = siochan->current_tx; + if (!tx) + return; + if (tx->ninflight) + tx->ninflight--; + vchan_cyclic_callback(&tx->vd); + if (!sio_fill_in_locked(siochan) && !tx->ninflight) + complete(&tx->done); +} + +/* The ring accounting: the oldest issued descriptor completed. */ +static void sio_ring_report_locked(struct sio_chan *siochan) +{ + struct sio_tx *tx; + /* An ISSUE not yet acknowledged has no completed descriptor. */ + if (siochan->nissued <= (siochan->issuing ? 1 : 0)) { + dev_warn_ratelimited(siochan->host->dev, "chan %d: report without a descriptor\n", + siochan->no); + return; + } + + tx = *sio_issued_slot(siochan, 0); + siochan->issued_head = (siochan->issued_head + 1) % SIO_MAX_NINFLIGHT; + siochan->nissued--; + if (tx) { if (tx->ninflight) tx->ninflight--; - vchan_cyclic_callback(&tx->vd); - if (!sio_fill_in_locked(siochan) && !tx->ninflight) - complete(&tx->done); + else + dev_err_ratelimited(siochan->host->dev, + "chan %d: report for a descriptor not in flight\n", + siochan->no); + if (tx == siochan->current_tx && !tx->terminated) + vchan_cyclic_callback(&tx->vd); } + sio_fill_in_locked(siochan); + sio_settle_locked(siochan, tx); + if (!siochan->nissued) + wake_up(&siochan->drained); + sio_ring_leave_locked(siochan); +} + +static void sio_process_report(struct sio_chan *siochan) +{ + unsigned long flags; + + spin_lock_irqsave(&siochan->vc.lock, flags); + if (siochan->ring) + sio_ring_report_locked(siochan); + else + sio_old_report_locked(siochan); spin_unlock_irqrestore(&siochan->vc.lock, flags); } @@ -828,6 +1320,12 @@ static int sio_device_config(struct dma_chan *chan, struct sio_shmem_chan_config cfg; int ret; + if (config->peripheral_config) { + if (config->peripheral_size != sizeof(struct apple_sio_idle)) + return -EINVAL; + return sio_set_idle(siochan, config->peripheral_config); + } + switch (is_tx ? config->dst_addr_width : config->src_addr_width) { case DMA_SLAVE_BUSWIDTH_1_BYTE: cfg.datashape = 0; @@ -1061,6 +1559,9 @@ static int sio_probe(struct platform_device *pdev) siochan->no = i; siochan->vc.desc_free = sio_tx_free; INIT_WORK(&siochan->terminate_wq, sio_terminate_work); + INIT_LIST_HEAD(&siochan->retired); + INIT_LIST_HEAD(&siochan->dead); + init_waitqueue_head(&siochan->drained); vchan_init(&siochan->vc, dma); } diff --git a/include/linux/dma/apple-sio.h b/include/linux/dma/apple-sio.h new file mode 100644 index 0000000000000..8665b511659b6 --- /dev/null +++ b/include/linux/dma/apple-sio.h @@ -0,0 +1,44 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef _LINUX_DMA_APPLE_SIO_H +#define _LINUX_DMA_APPLE_SIO_H + +#include + +/** + * struct apple_sio_idle - idle transfer of an Apple SIO channel + * @addr: DMA address of the buffer + * @len: buffer length; 0 removes the channel's idle transfer + * @period_len: length of each of its descriptors, not zero and at most @len + * + * Passed as &dma_slave_config.peripheral_config in a dmaengine_slave_config() + * call of its own, which sets or removes the idle transfer and nothing else: + * the channel's other settings take a separate call. + * + * While a channel has an idle transfer, it plays that buffer cyclically + * whenever no other transfer runs, and goes from one to the other in + * descriptor order: terminating a transfer hands over to the idle one instead + * of stopping the channel, and a transfer issued later follows the idle one's + * issued descriptors. The peripheral sees no gap in between. + * + * dmaengine_synchronize() returns once the descriptors of a terminated + * transfer have completed. If they do not in time, it stops the channel and + * waits for them first; the idle transfer then starts again, after a gap. + * Only an acknowledged TERMINATE counts as stopped. If the channel cannot be + * confirmed stopped, dmaengine_synchronize() warns, as it cannot fail. + * Removing the idle transfer stops the channel if nothing else runs, and + * returns 0 once its descriptors completed. If the channel cannot be + * confirmed stopped, it returns an error and the buffer is still in use. + * + * Setting an idle transfer fails with -EBUSY while the channel has one, or + * while it cannot take over the channel's issued descriptors. Setting and + * removing it, terminating and synchronizing must not run concurrently on a + * channel: its client serializes them. A channel that never had an idle + * transfer works as before. + */ +struct apple_sio_idle { + dma_addr_t addr; + size_t len; + size_t period_len; +}; + +#endif From d2997336a2303c2ffec7f5ce4ebdfbae70c07eea Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Wed, 28 Oct 2026 08:05:18 +0100 Subject: [PATCH 17/28] drm/apple: keep the held HDMI audio link's samples flowing The held link's silence replaced a stopped stream's DMA and was replaced by the next stream's with a SIO TERMINATE in between, and closing the PCM terminated it once more. A gap of 20 ms in the samples mutes the J514S sink until the DP link trains again, as a link restart does: with a gap forced to 20 ms, every later stream was silent, and Chromium resuming after a 30 s pause was silent in 2 of 7 runs without one. Make the silence the SIO channel's idle transfer while the link is held. A stream's DMA follows it and hands back to it in descriptor order, and the PCM's terminate at close leaves it running. Its periods shrink to 4 KiB, so a stream starts at most about 85 ms after its START. A stream that ends into the silence leaves up to four periods issued, which dmaengine_synchronize() waits for, 500 ms at most: on a port that holds links, stream periods are kept to 50 ms. A silence whose channel could not be stopped is not freed. Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 (cherry picked from commit c0b3d14f91bb82c09ce2f878b238495c409355ab) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/audio-link-test.c | 117 +++++++++++++++--- drivers/gpu/drm/apple/audio.c | 150 +++++++++++++++++------- 2 files changed, 205 insertions(+), 62 deletions(-) diff --git a/drivers/gpu/drm/apple/audio-link-test.c b/drivers/gpu/drm/apple/audio-link-test.c index ef897f94d7f1c..40f928396297b 100644 --- a/drivers/gpu/drm/apple/audio-link-test.c +++ b/drivers/gpu/drm/apple/audio-link-test.c @@ -13,6 +13,8 @@ struct dcpaud_link_test { bool link_on; bool link_live; bool typec; + /* The error removing the idle transfer returns */ + int idle_remove_err; u64 asked; unsigned int completes; unsigned int stops; @@ -38,12 +40,32 @@ static bool dcpaud_link_test_live(struct device *dev) return dcpaud_link_current->link_live; } +static bool dcpaud_link_test_typec(struct device *dev) +{ + return dcpaud_link_current->typec; +} + static int dcpaud_link_test_terminate(struct dma_chan *chan) { dcpaud_link_record('X'); return 0; } +/* G the channel configured; the silence as its idle transfer: I set, i removed */ +static int dcpaud_link_test_config(struct dma_chan *chan, struct dma_slave_config *config) +{ + const struct apple_sio_idle *idle = config->peripheral_config; + + if (!idle) { + dcpaud_link_record('G'); + return 0; + } + if (config->peripheral_size != sizeof(*idle)) + return -EINVAL; + dcpaud_link_record(idle->len ? 'I' : 'i'); + return idle->len ? 0 : dcpaud_link_current->idle_remove_err; +} + static dma_cookie_t dcpaud_link_test_submit(struct dma_async_tx_descriptor *tx) { return 1; @@ -61,18 +83,6 @@ dcpaud_link_test_prep(struct dma_chan *chan, dma_addr_t buf, size_t len, size_t return &t->desc; } -/* G the channel configured */ -static int dcpaud_link_test_config(struct dma_chan *chan, struct dma_slave_config *config) -{ - dcpaud_link_record('G'); - return 0; -} - -static bool dcpaud_link_test_typec(struct device *dev) -{ - return dcpaud_link_current->typec; -} - static void dcpaud_link_test_issue(struct dma_chan *chan) { } @@ -250,10 +260,11 @@ static void dcpaud_stop_holds_link(struct kunit *test) struct snd_pcm_substream *sub = dcpaud_link_test_open(test, audio); dcpaud_link_test_held_stop(test, audio); - /* The stream's DMA ends, the silence follows it; no link call */ + /* The silence set first, the stream's DMA ends into it; no link call */ KUNIT_ASSERT_EQ(test, dcp_pcm_native_sync_stop(sub), 0); - KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "XQD"); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "IXD"); KUNIT_EXPECT_TRUE(test, audio->silence_running); + KUNIT_EXPECT_TRUE(test, audio->idle_set); KUNIT_EXPECT_TRUE(test, audio->link_started); KUNIT_ASSERT_EQ(test, dcp_pcm_native_hw_free(sub), 0); @@ -262,13 +273,43 @@ static void dcpaud_stop_holds_link(struct kunit *test) /* The display goes off: the silence ends, then the link */ dcpaud_release_link(pdev, NULL); - KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "XQDXDDSC"); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "IXDiDSC"); KUNIT_EXPECT_FALSE(test, audio->silence_running); + KUNIT_EXPECT_FALSE(test, audio->idle_set); KUNIT_EXPECT_FALSE(test, audio->link_prepared); /* The stream's reference went with the silence */ KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 0); } +static void dcpaud_stuck_silence_keeps_pm(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = dcpaud_link_test_open(test, audio); + + dcpaud_link_test_held_stop(test, audio); + KUNIT_ASSERT_EQ(test, dcp_pcm_native_sync_stop(sub), 0); + KUNIT_ASSERT_EQ(test, dcp_pcm_native_hw_free(sub), 0); + + /* The channel cannot be confirmed stopped: it may still read the silence */ + dcpaud_link_current->idle_remove_err = -EIO; + dcpaud_release_link(pdev, NULL); + KUNIT_EXPECT_FALSE(test, audio->silence_running); + KUNIT_EXPECT_FALSE(test, audio->link_prepared); + KUNIT_EXPECT_TRUE(test, audio->idle_set); + KUNIT_EXPECT_TRUE(test, audio->idle_pm); + /* The stream's reference stays with the silence */ + KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 1); + + /* Removed later, it gives the reference back */ + dcpaud_link_current->idle_remove_err = 0; + scoped_guard(mutex, &audio->link_lock) + KUNIT_EXPECT_EQ(test, dcpaud_idle(audio, false), 0); + KUNIT_EXPECT_FALSE(test, audio->idle_set); + KUNIT_EXPECT_FALSE(test, audio->idle_pm); + KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 0); +} + static void dcpaud_held_stop_without_service(struct kunit *test) { struct platform_device *pdev; @@ -297,6 +338,7 @@ static struct snd_pcm_substream *dcpaud_link_test_held(struct kunit *test, struc audio->link_prepared = true; audio->link_started = true; audio->silence_running = true; + audio->idle_set = true; /* The held link's PM reference, then the stream's from its START */ pm_runtime_get_noresume(audio->dev); pm_runtime_get_noresume(audio->dev); @@ -312,10 +354,11 @@ static void dcpaud_start_on_held_link(struct kunit *test) struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); struct snd_pcm_substream *sub = dcpaud_link_test_held(test, audio); - /* Its element: the stream's DMA replaces the silence, no link call */ + /* Its element: the stream's DMA follows the silence, no link call */ KUNIT_ASSERT_EQ(test, dcp_pcm_native_start(sub, SNDRV_PCM_TRIGGER_START), 0); - KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "XQ"); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "Q"); KUNIT_EXPECT_FALSE(test, audio->silence_running); + KUNIT_EXPECT_TRUE(test, audio->idle_set); KUNIT_EXPECT_TRUE(test, audio->link_started); pm_runtime_put_noidle(audio->dev); } @@ -330,7 +373,8 @@ static void dcpaud_start_replaces_held_link(struct kunit *test) memset(&audio->link_cookie, 1, sizeof(audio->link_cookie)); memset(&audio->selected_cookie, 2, sizeof(audio->selected_cookie)); KUNIT_ASSERT_EQ(test, dcp_pcm_native_start(sub, SNDRV_PCM_TRIGGER_START), 0); - KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "XDSCGPQT"); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "iSCGPQT"); + KUNIT_EXPECT_FALSE(test, audio->idle_set); KUNIT_EXPECT_EQ(test, memcmp(&audio->link_cookie, &audio->selected_cookie, sizeof(audio->link_cookie)), 0); KUNIT_EXPECT_TRUE(test, audio->link_started); @@ -346,7 +390,7 @@ static void dcpaud_start_after_held_link_went(struct kunit *test) /* The held link's service went (unplug): the stream gets a link of its own */ dcpaud_link_current->link_live = false; KUNIT_ASSERT_EQ(test, dcp_pcm_native_start(sub, SNDRV_PCM_TRIGGER_START), 0); - KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "XDGPQT"); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "iGPQT"); KUNIT_EXPECT_FALSE(test, audio->silence_running); KUNIT_EXPECT_TRUE(test, audio->link_started); pm_runtime_put_noidle(audio->dev); @@ -374,6 +418,39 @@ static void dcpaud_failed_start_keeps_held_link(struct kunit *test) pm_runtime_put_noidle(audio->dev); } +static void dcpaud_close_keeps_held_silence(struct kunit *test) +{ + struct platform_device *pdev; + struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); + struct snd_pcm_substream *sub = kunit_kzalloc(test, sizeof(*sub), GFP_KERNEL); + struct snd_pcm_runtime *runtime = kunit_kzalloc(test, sizeof(*runtime), GFP_KERNEL); + struct snd_pcm *pcm = kunit_kzalloc(test, sizeof(*pcm), GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, sub); + KUNIT_ASSERT_NOT_NULL(test, runtime); + KUNIT_ASSERT_NOT_NULL(test, pcm); + pcm->private_data = audio; + pcm->nonatomic = true; + sub->pcm = pcm; + sub->runtime = runtime; + KUNIT_ASSERT_EQ(test, snd_dmaengine_pcm_open(sub, &dcpaud_link_current->channel), 0); + audio->chan = &dcpaud_link_current->channel; + audio->link_prepared = true; + audio->link_started = true; + audio->silence_running = true; + audio->idle_set = true; + pm_runtime_get_noresume(audio->dev); + + /* The PCM's terminate leaves the idle silence: nothing queued, no link call */ + KUNIT_ASSERT_EQ(test, dcp_pcm_native_close(sub), 0); + KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "XD"); + KUNIT_EXPECT_TRUE(test, audio->silence_running); + KUNIT_EXPECT_TRUE(test, audio->idle_set); + KUNIT_EXPECT_TRUE(test, audio->link_started); + KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 1); + pm_runtime_put_noidle(audio->dev); +} + static void dcpaud_link_test_hold_default(void *data) { WRITE_ONCE(hold_link, 1); @@ -415,11 +492,13 @@ static struct kunit_case dcpaud_link_cases[] = { KUNIT_CASE(dcpaud_disconnect_sync_completes_live_owner), KUNIT_CASE(dcpaud_gone_service_drops_link), KUNIT_CASE(dcpaud_stop_holds_link), + KUNIT_CASE(dcpaud_stuck_silence_keeps_pm), KUNIT_CASE(dcpaud_held_stop_without_service), KUNIT_CASE(dcpaud_start_on_held_link), KUNIT_CASE(dcpaud_start_replaces_held_link), KUNIT_CASE(dcpaud_start_after_held_link_went), KUNIT_CASE(dcpaud_failed_start_keeps_held_link), + KUNIT_CASE(dcpaud_close_keeps_held_silence), KUNIT_CASE(dcpaud_typec_stop_ends_stream), {} }; diff --git a/drivers/gpu/drm/apple/audio.c b/drivers/gpu/drm/apple/audio.c index 6b00a854a1519..35fadd62320cb 100644 --- a/drivers/gpu/drm/apple/audio.c +++ b/drivers/gpu/drm/apple/audio.c @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -46,11 +47,20 @@ #define DCPAUD_PERIODS_MAX 16 /* - * The silence a held link plays between streams. SIO keeps up to four - * periods of a transfer in flight, so it gets more than that. + * The silence a held link plays between streams, as the SIO channel's idle + * transfer: SIO keeps up to four periods in flight, and a stream's first + * period follows those (about 85 ms). Its four descriptor slots stay taken + * while the link is held, next to a stream's sixteen at most. */ -#define DCPAUD_SILENCE_PERIOD SZ_16K -#define DCPAUD_SILENCE_BYTES (8 * DCPAUD_SILENCE_PERIOD) +#define DCPAUD_SILENCE_PERIOD SZ_4K +#define DCPAUD_SILENCE_BYTES (4 * DCPAUD_SILENCE_PERIOD) + +/* + * A stream that ends into the held link's silence leaves up to four periods + * issued, and dmaengine_synchronize() waits 500 ms for them before it stops + * the channel. Streams on a port that holds links keep to 50 ms periods. + */ +#define DCPAUD_HELD_PERIOD_MAX_US 50000 struct dcp_audio { struct device *dev; @@ -104,6 +114,10 @@ struct dcp_audio { void *silence; dma_addr_t silence_addr; bool silence_running; + /* The silence is the channel's idle transfer, see dcpaud_idle() */ + bool idle_set; + /* It holds a PM reference: its removal failed, see dcpaud_silence_stop() */ + bool idle_pm; #endif }; @@ -633,7 +647,7 @@ struct snd_pcm_ops dcp_playback_ops = { * STOP: the DMA terminated, or for a held link (below) left playing * the cleared buffer; * sync_stop: the DMA drained, then the link stopped and completed, or the - * held link's silence queued in place of the DMA; + * held link's silence taking over from the DMA; * hw_free: a link prepared but never started, completed. * * Every link method carries its element's link data. A link of an earlier @@ -645,9 +659,14 @@ struct snd_pcm_ops dcp_playback_ops = { * audio of a link that stops and starts again with an unchanged format, * until the DP link trains again: only a different channel layout brings * it back. So a stereo link, once started, is held between streams, as - * macOS holds it: the stopped stream's DMA is swapped for silence and the - * link keeps running until the format changes or the display goes off - * (dcpaud_release_link()). The DMA alone restarts without that loss. + * macOS holds it: silence follows the stopped stream's DMA and the link + * keeps running until the format changes or the display goes off + * (dcpaud_release_link()). + * + * The link must not run dry either: a gap of 20 ms in its samples mutes the + * sink the same way. The silence is the SIO channel's idle transfer, which + * the channel issues whenever no stream's DMA does, in descriptor order: + * a stream's DMA takes over from it and hands back to it without a gap. */ /* * Read-only at runtime: a stream opened while it was lower keeps periods @@ -748,34 +767,72 @@ static int dcpaud_pin_rate_format(struct dcp_audio *dcpaud, */ #define DCPAUD_SINK_SETTLE_MS 1500 -/* Queue the held link's silence; it carries the PM reference of the stream. */ +/* + * Sets or removes the silence as the channel's idle transfer. Removed, it + * stops the channel unless a stream's DMA runs, once its periods played. + */ +static int dcpaud_idle(struct dcp_audio *dcpaud, bool on) +{ + struct apple_sio_idle idle = { + .addr = dcpaud->silence_addr, + .len = on ? DCPAUD_SILENCE_BYTES : 0, + .period_len = DCPAUD_SILENCE_PERIOD, + }; + struct dma_slave_config config = { + .direction = DMA_MEM_TO_DEV, + .peripheral_config = &idle, + .peripheral_size = sizeof(idle), + }; + int ret; + + ret = dmaengine_slave_config(dcpaud->chan, &config); + /* Not removed: the channel may still read the silence. */ + if (ret) + return ret; + dcpaud->idle_set = on; + if (!on && dcpaud->idle_pm) { + dcpaud->idle_pm = false; + pm_runtime_mark_last_busy(dcpaud->dev); + pm_runtime_put_autosuspend(dcpaud->dev); + } + return 0; +} + +/* + * The held link's silence carries the PM reference of the stream. It is set + * before the stream's DMA ends, which it then follows without a gap. + */ static int dcpaud_silence_start(struct dcp_audio *dcpaud) { - struct dma_async_tx_descriptor *desc; + int ret; lockdep_assert_held(&dcpaud->link_lock); if (!dcpaud->silence) return -ENOMEM; - desc = dmaengine_prep_dma_cyclic(dcpaud->chan, dcpaud->silence_addr, - DCPAUD_SILENCE_BYTES, DCPAUD_SILENCE_PERIOD, - DMA_MEM_TO_DEV, 0); - if (!desc) - return -ENOMEM; - if (dma_submit_error(dmaengine_submit(desc))) - return -EIO; - dma_async_issue_pending(dcpaud->chan); + if (!dcpaud->idle_set) { + ret = dcpaud_idle(dcpaud, true); + if (ret) + return ret; + } dcpaud->silence_running = true; dev_dbg(dcpaud->dev, "link held, playing silence\n"); return 0; } +/* The link ends: so does its silence, which a stream may have taken over from. */ static void dcpaud_silence_stop(struct dcp_audio *dcpaud) { lockdep_assert_held(&dcpaud->link_lock); + if (dcpaud->idle_set) + dcpaud_idle(dcpaud, false); if (!dcpaud->silence_running) return; - dmaengine_terminate_sync(dcpaud->chan); dcpaud->silence_running = false; + /* A channel that may still read the silence keeps the device up. */ + if (dcpaud->idle_set && !dcpaud->idle_pm) { + dcpaud->idle_pm = true; + return; + } pm_runtime_mark_last_busy(dcpaud->dev); pm_runtime_put_autosuspend(dcpaud->dev); dev_dbg(dcpaud->dev, "held link's silence stopped\n"); @@ -824,6 +881,13 @@ static int dcp_pcm_native_open(struct snd_pcm_substream *substream) ret = dcp_pcm_open(substream); if (!ret && live && !dcpaud->open_unplugged && dcpaud_holds(dcpaud)) { ret = dcpaud_pin_rate_format(dcpaud, substream->runtime); + if (!ret) + ret = snd_pcm_hw_constraint_minmax(substream->runtime, + SNDRV_PCM_HW_PARAM_PERIOD_TIME, 0, + DCPAUD_HELD_PERIOD_MAX_US); + /* Positive: the constraint narrowed the period time. */ + if (ret > 0) + ret = 0; if (ret) dcp_pcm_close(substream); } @@ -847,21 +911,12 @@ static int dcp_pcm_native_close(struct snd_pcm_substream *substream) bool held; /* - * A held link keeps its service open. Closing the PCM terminated its - * channel, the silence with it: that is queued again. + * A held link keeps its service open. Closing the PCM terminated the + * channel's transfer, which leaves the idle one, the silence, running. */ scoped_guard(mutex, &dcpaud->link_lock) { dcpaud_drop_stale(dcpaud); held = dcpaud->silence_running; - if (held) { - dcpaud->silence_running = false; - if (dcpaud_silence_start(dcpaud)) { - held = false; - dcpaud_link_teardown(dcpaud); - pm_runtime_mark_last_busy(dcpaud->dev); - pm_runtime_put_autosuspend(dcpaud->dev); - } - } } if (!held) dcp_audiosrv_close(dcpaud->dcp_dev); @@ -996,25 +1051,26 @@ static int dcp_pcm_native_start(struct snd_pcm_substream *substream, int cmd) guard(mutex)(&dcpaud->link_lock); dcpaud_drop_stale(dcpaud); - /* The stream's DMA follows the held link's silence. */ + /* + * The stream's DMA follows the held link's silence, which stays the + * channel's idle transfer, and takes over the PM reference. + */ held = dcpaud->silence_running; if (held) { - dmaengine_terminate_async(dcpaud->chan); dcpaud->silence_running = false; pm_runtime_mark_last_busy(dcpaud->dev); pm_runtime_put_autosuspend(dcpaud->dev); } /* - * A held link of another element ends, and the stream's is prepared; - * so is a stream's whose prepare found a link that has gone since. + * A held link of another element ends, its silence with it, and the + * stream's is prepared; so is a stream's whose prepare found a link that + * has gone since. */ if (!dcpaud->link_prepared || memcmp(&dcpaud->link_cookie, &dcpaud->selected_cookie, sizeof(dcpaud->link_cookie))) { - if (dcpaud->link_prepared) { - dmaengine_synchronize(dcpaud->chan); + if (dcpaud->link_prepared) dcpaud_link_teardown(dcpaud); - } ret = dcpaud_link_prepare(dcpaud, substream); if (ret) return ret; @@ -1058,7 +1114,7 @@ static int dcp_pcm_native_trigger(struct snd_pcm_substream *substream, int cmd) case SNDRV_PCM_TRIGGER_STOP: /* * The DMA plays the cleared buffer on, with the stream's PM - * reference, until sync_stop swaps it for the held link's + * reference, until sync_stop hands over to the held link's * silence: the link must not run dry meanwhile. */ if (dcpaud_holds(dcpaud) && READ_ONCE(dcpaud->stream_dma) && @@ -1089,16 +1145,21 @@ static int dcp_pcm_native_sync_stop(struct snd_pcm_substream *substream) if (READ_ONCE(dcpaud->dma_cleared)) { guard(mutex)(&dcpaud->link_lock); WRITE_ONCE(dcpaud->dma_cleared, false); - dmaengine_terminate_async(dcpaud->chan); - /* Queued first, the silence follows the stream without a gap. */ + /* + * The silence set first, the channel hands over to it in + * descriptor order when the stream's DMA ends; synchronize + * returns once the stream's issued periods have played. + */ if (dcpaud->silence_running || !dcpaud->link_started || !dcp_audiosrv_link_live(dcpaud->dcp_dev) || dcpaud_silence_start(dcpaud)) { + dmaengine_terminate_async(dcpaud->chan); dmaengine_synchronize(dcpaud->chan); dcpaud_link_teardown(dcpaud); pm_runtime_mark_last_busy(dcpaud->dev); pm_runtime_put_autosuspend(dcpaud->dev); return 0; } + dmaengine_terminate_async(dcpaud->chan); dmaengine_synchronize(dcpaud->chan); return 0; } @@ -1455,11 +1516,14 @@ static void dcpaud_comp_unbind(struct device *dev, struct device *main, held = dcpaud->silence_running; if (held) dcpaud_link_teardown(dcpaud); - if (dcpaud->silence) { + else if (dcpaud->idle_set) + dcpaud_idle(dcpaud, false); + if (dcpaud->silence && dcpaud->idle_set) + dev_err(dcpaud->dev, "silence kept: its DMA channel did not stop\n"); + else if (dcpaud->silence) dma_free_coherent(dcpaud->chan->device->dev, DCPAUD_SILENCE_BYTES, dcpaud->silence, dcpaud->silence_addr); - dcpaud->silence = NULL; - } + dcpaud->silence = NULL; } if (held) dcp_audiosrv_close(dcpaud->dcp_dev); From af498ab24df40ce5f15cdf2a298a86f4f7df13ee Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Wed, 28 Oct 2026 19:12:34 +0100 Subject: [PATCH 18/28] pmdomain: apple: pmp-report: withdraw external display requests in sleep 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 Assisted-by: Claude:claude-opus-5-5 [ryan: the gate hunk goes to drivers/soc/apple/display-gate.c (the renamed T6030 gate, still marking only apple,t6030-dcpext processors); the PMP graph test keeps this tree DRAM metadata cases and IRQ-safe entries beside the new sleep cases] (cherry picked from commit b28cb3a4bb171450a6bfbb84db21bfa0335dac7b) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/pmdomain/apple/pmp-report.c | 79 ++++++++++++++++++++++++++++ drivers/soc/apple/display-gate.c | 3 ++ tools/asahi/t6030-pmp-graph-tests.c | 57 +++++++++++++++++++- tools/asahi/t6030-pmp-graph-tests.py | 17 +++++- 4 files changed, 154 insertions(+), 2 deletions(-) diff --git a/drivers/pmdomain/apple/pmp-report.c b/drivers/pmdomain/apple/pmp-report.c index f09b38aa310ca..77865b5af2687 100644 --- a/drivers/pmdomain/apple/pmp-report.c +++ b/drivers/pmdomain/apple/pmp-report.c @@ -1526,6 +1526,9 @@ struct apple_pmp_report_entry { u32 id; /* The PMP does not acknowledge this request (a started PMP only). */ bool no_ack; + /* Withdrawn for system sleep (apple,release-for-sleep), and now so. */ + bool sleep_release; + bool released; bool removed; }; @@ -1589,6 +1592,80 @@ static int apple_pmp_report_entry_set_state(struct generic_pm_domain *genpd, boo return ret; } +/* + * System sleep. An external display processor's request keeps the PMP's + * external display state up while nothing is shown, about 0.25 W in + * suspend-to-idle on J514S. By the noirq phase every display processor has + * suspended, its displays off and its links released or idle: withdraw such a + * request if it is acknowledged, with one replacement write as macOS releases + * its display requests, and assert it again before the processors resume. + */ +static int apple_pmp_report_entry_sleep(struct apple_pmp_report_entry *ent, bool release) +{ + struct apple_pmp_report *rep; + int ret = -ENODEV; + + rcu_read_lock(); + list_for_each_entry_rcu(rep, &apple_pmp_reports, list) { + if (rep->dev->of_node != ent->report_node) + continue; + if (release && (!(readq(rep->base + rep->offsets->status) & PMP_REPORT_READY) || + !(readq(rep->base + rep->offsets->actual) & BIT_ULL(ent->id)))) + ret = -EALREADY; + else + ret = apple_pmp_report_set_state(rep, ent->id, !release, ent->no_ack); + break; + } + rcu_read_unlock(); + return ret; +} + +static int apple_pmp_report_entry_suspend_noirq(struct device *dev) +{ + struct apple_pmp_report_entry *ent = dev_get_drvdata(dev); + int ret; + + if (!ent || !ent->sleep_release || READ_ONCE(ent->removed)) + return 0; + ret = apple_pmp_report_entry_sleep(ent, true); + if (ret == -EALREADY) + return 0; + /* A withdrawal that was not acknowledged is asserted again all the same. */ + if (ret) + dev_warn(dev, "request %u not withdrawn for sleep: %d\n", ent->id, ret); + ent->released = true; + return 0; +} + +static int apple_pmp_report_entry_resume_noirq(struct device *dev) +{ + struct apple_pmp_report_entry *ent = dev_get_drvdata(dev); + int ret, tries = 0; + + if (!ent || !ent->released) + return 0; + ent->released = false; + /* + * Without the request the external display stays dark until reboot: + * write it again, three times at most, while the PMP does not + * acknowledge it in time. + */ + do { + ret = apple_pmp_report_entry_sleep(ent, false); + } while (ret == -ETIMEDOUT && ++tries < 3); + if (ret) + dev_err(dev, "request %u not asserted after sleep: %d\n", ent->id, ret); + else if (tries) + dev_warn(dev, "request %u asserted after sleep on write %d\n", ent->id, + tries + 1); + return ret; +} + +static const struct dev_pm_ops apple_pmp_report_entry_pm_ops = { + NOIRQ_SYSTEM_SLEEP_PM_OPS(apple_pmp_report_entry_suspend_noirq, + apple_pmp_report_entry_resume_noirq) +}; + static int apple_pmp_report_entry_power_on(struct generic_pm_domain *genpd) { return apple_pmp_report_entry_set_state(genpd, true); @@ -1661,6 +1738,7 @@ static int apple_pmp_report_entry_probe(struct platform_device *pdev) } ent->no_ack = of_property_read_bool(node, "apple,no-ack"); } + ent->sleep_release = of_property_read_bool(node, "apple,release-for-sleep"); ret = of_property_read_string(node, "label", &name); if (ret < 0) { @@ -1749,6 +1827,7 @@ static struct platform_driver apple_pmp_report_entry_driver = { .driver = { .name = "apple-pmp-report-entry", .of_match_table = apple_pmp_report_entry_of_match, + .pm = pm_sleep_ptr(&apple_pmp_report_entry_pm_ops), /* Power-domain providers are not intended for manual unbinding. */ .suppress_bind_attrs = true, }, diff --git a/drivers/soc/apple/display-gate.c b/drivers/soc/apple/display-gate.c index 2beb8ef39fd1d..3ebd66315852e 100644 --- a/drivers/soc/apple/display-gate.c +++ b/drivers/soc/apple/display-gate.c @@ -1487,6 +1487,9 @@ static int __init gate_ext_apply(struct gate_ext *ext, bool native) /* The driver must wait for this request's acknowledgement before RUN. */ if (!ret) ret = gate_set_u32(cs, ext->dcp, "apple,pmp-report", ext->entry->phandle); + /* No external display is attached in system sleep: the PMP may drop it. */ + if (!ret) + ret = gate_add_bool(cs, ext->entry, "apple,release-for-sleep"); if (!ret) ret = of_changeset_update_prop_string(cs, ext->entry, "status", "okay"); if (!ret) diff --git a/tools/asahi/t6030-pmp-graph-tests.c b/tools/asahi/t6030-pmp-graph-tests.c index 3d2493e57fb63..637359d7e6933 100644 --- a/tools/asahi/t6030-pmp-graph-tests.c +++ b/tools/asahi/t6030-pmp-graph-tests.c @@ -50,7 +50,7 @@ static void host_log(const char *format, ...) #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) struct device_node { const char *name, *compatible; - bool available, always_on, no_ack, deferred, irq_safe, uuid, soc_record; + bool available, always_on, no_ack, deferred, irq_safe, uuid, soc_record, release_sleep; u32 reg; int refs; struct device_node *parent, *child, *sibling, *pmp; @@ -196,6 +196,7 @@ struct apple_pmp_report_entry { struct device_node *node, *report_node; u32 id; bool no_ack, removed; + bool sleep_release, released; }; #define genpd_to_apple_pmp_report_entry(p) \ container_of(p, struct apple_pmp_report_entry, genpd) @@ -394,11 +395,18 @@ static u64 readq(const void *a) { return *(const u64 *)a; } +/* The sleep cases: the PMP acknowledges the requests it is given, ack_late writes late. */ +static bool ack_follows; +static unsigned int ack_late; static void writeq(u64 v, void *a) { assert(a == (char *)regs + 0x108c0); *(u64 *)a = v; regs[0x1180 / 8] = v; + if (ack_follows && ack_late) + ack_late--; + else if (ack_follows) + regs[0x11c0 / 8] = v; } #define readq_poll_timeout(address, value, condition, delay, timeout) \ ({ \ @@ -463,6 +471,8 @@ static bool of_property_read_bool(const struct device_node *n, const char *p) return n->irq_safe; if (!strcmp(p, "apple,soc-device-record")) return n->soc_record; + if (!strcmp(p, "apple,release-for-sleep")) + return n->release_sleep; return false; } static bool of_property_present(const struct device_node *n, const char *p) @@ -832,6 +842,8 @@ static struct apple_pmp_report *setup(void) put32(devices + i * 0x7c + 8, 2); } ack_polls = 0; + ack_follows = false; + ack_late = 0; check_error = enable_error = 0; hw_binds = hw_ready = true; hw_ack = BIT_ULL(7); @@ -1030,6 +1042,49 @@ int main(void) cases++; cleanup(); #endif + +#ifdef HAVE_SLEEP_RELEASE + /* System sleep withdraws an acknowledged request marked for it, then asserts it again. */ + setup(); + entries[1].release_sleep = true; + assert(apple_pmp_report_probe(&report_dev) == 0); + assert(apple_pmp_report_wait_ready(&entries[0], 30000) == 0); + struct apple_pmp_report_entry *disp = entry_devs[0].dev.data; + struct apple_pmp_report_entry *ext = entry_devs[1].dev.data; + + assert(ext->sleep_release && !disp->sleep_release); + ack_follows = true; + regs[0x11c0 / 8] = regs[0x1180 / 8]; + u64 target = regs[0x1180 / 8]; + + assert(target & BIT_ULL(8)); + assert(apple_pmp_report_entry_suspend_noirq(&entry_devs[0].dev) == 0 && + !disp->released && regs[0x1180 / 8] == target); + assert(apple_pmp_report_entry_suspend_noirq(&entry_devs[1].dev) == 0); + assert(ext->released && regs[0x1180 / 8] == (target & ~BIT_ULL(8))); + assert(apple_pmp_report_entry_resume_noirq(&entry_devs[1].dev) == 0); + assert(!ext->released && regs[0x1180 / 8] == target); + cases++; + /* A request the PMP has not acknowledged is left as it is. */ + regs[0x11c0 / 8] &= ~BIT_ULL(8); + assert(apple_pmp_report_entry_suspend_noirq(&entry_devs[1].dev) == 0); + assert(!ext->released && regs[0x1180 / 8] == target); + assert(apple_pmp_report_entry_resume_noirq(&entry_devs[1].dev) == 0 && + regs[0x1180 / 8] == target); + cases++; + /* A re-assert the PMP acknowledges late is written again, three times at most. */ + regs[0x11c0 / 8] = target; + assert(apple_pmp_report_entry_suspend_noirq(&entry_devs[1].dev) == 0 && ext->released); + ack_late = 2; + assert(apple_pmp_report_entry_resume_noirq(&entry_devs[1].dev) == 0 && + !ack_late && regs[0x11c0 / 8] == target); + assert(apple_pmp_report_entry_suspend_noirq(&entry_devs[1].dev) == 0 && ext->released); + ack_late = 5; + assert(apple_pmp_report_entry_resume_noirq(&entry_devs[1].dev) == -ETIMEDOUT && + ack_late == 2 && regs[0x1180 / 8] == target); + cases++; + cleanup(); +#endif printf("PASS: %u actual overlay/probe/entry/DCP7/managed-start/supplier graph cases\n", cases); return 0; diff --git a/tools/asahi/t6030-pmp-graph-tests.py b/tools/asahi/t6030-pmp-graph-tests.py index bbe4dd99ea6c0..b4b417b4d4f01 100644 --- a/tools/asahi/t6030-pmp-graph-tests.py +++ b/tools/asahi/t6030-pmp-graph-tests.py @@ -50,6 +50,13 @@ def main(): names.insert(0, 'static bool apple_pmp_report_running(') if 'static int apple_pmp_report_wait_t6030_bound(' in source: names.insert(names.index('static struct device_node *apple_pmp_report_t6030_entry('), 'static int apple_pmp_report_wait_t6030_bound(') + # System sleep (apple,release-for-sleep), where the source has it. + sleep = 'static int apple_pmp_report_entry_sleep(' in source + if sleep: + at = names.index('static int apple_pmp_report_entry_power_on(') + names[at:at] = ['static int apple_pmp_report_entry_sleep(', + 'static int apple_pmp_report_entry_suspend_noirq(', + 'static int apple_pmp_report_entry_resume_noirq('] offsets = re.search(r'static const struct apple_pmp_report_offsets apple_pmp_offsets_t6030 = \{.*?\n\};', source, re.S)[0] functions = '\n'.join(extract.function(source, n) for n in names) validation = re.sub(r'^#include .*\n', '', (ROOT / 'drivers/pmdomain/apple/pmp-report-validation.h').read_text(), flags=re.M) @@ -66,7 +73,8 @@ def main(): dram_attrs = ('#pragma GCC diagnostic push\n' '#pragma GCC diagnostic ignored "-Wunused-parameter"\n' + dram_attrs + '#pragma GCC diagnostic pop\n') - code = offsets + '\n' + validation + '\n' + dram_attrs + '\n' + functions + code = (('#define HAVE_SLEEP_RELEASE 1\n' if sleep else '') + offsets + '\n' + validation + + '\n' + dram_attrs + '\n' + functions) # The actual overlay must still describe a halted supplier and disabled DMA/mailbox. assert overlay.count('status = \"disabled\";') >= 3 (args.output / 'production.h').write_text(code) @@ -115,6 +123,13 @@ def main(): ('irq-safe-without-property', 'of_property_read_bool(node, "apple,irq-safe")', 'true'), ('skip-supplier-wait', 'apple_pmp_report_wait_ready(entry, msecs_to_jiffies(timeout_ms))', '((void)timeout_ms, 0)'), ] + if sleep: + controls += [ + ('sleep-releases-unacked', '!(readq(rep->base + rep->offsets->actual) & BIT_ULL(ent->id))', 'false'), + ('sleep-not-reasserted', 'ret = apple_pmp_report_entry_sleep(ent, false);', 'ret = 0;'), + ('sleep-ignores-mark', '!ent->sleep_release ||', ''), + ('sleep-not-rewritten', '++tries < 3', '++tries < 1'), + ] for name, old, new in controls: assert code.count(old) == 1, (name, code.count(old)) (args.output / 'production.h').write_text(code.replace(old, new)) From 88d7cf95f30e85118e9e9cbcc5819657efe21d7a Mon Sep 17 00:00:00 2001 From: Jeppe Bylov Date: Mon, 26 Oct 2026 21:07:35 +0100 Subject: [PATCH 19/28] drm/apple: declare a soft dependency on the display crossbar The DCPs take their HDMI ("dp-xbar") and Type-C ("typecN-dpinM") route mux controls from mux-apple-display-crossbar. Nothing tells userspace about that dependency, so mkinitcpio puts appledrm in the initramfs but not the crossbar. The crossbar then loads only from the root filesystem. With an encrypted root the passphrase prompt can outlast the deferred probe timeout. On an M3 Pro (J514S) both external DCPs gave up their routes 12 s into boot, and the crossbar bound at 44 s, after unlock: apple-dcp 2d2c00000.dcp: deferred probe timeout, ignoring dependency apple-dcp 2d2c00000.dcp: HDMI output unusable (dp-xbar: -110); USB-C displays are not affected apple-dcp 2d2c00000.dcp: Type-C display routes unusable: -110; USB-C ports go on without this processor apple-dcp 2d2c00000.dcp: external display processor unusable: its Type-C display routes did not register apple-dcp 2d6c00000.dcp: external display processor unusable: its Type-C display routes did not register [ 44.078721] apple-display-crossbar 70304c000.mux: dpphy: T602X route -2, clocks disabled No external display worked until reboot. Adding the crossbar (together with the sn201202x PD driver) to the initramfs fixed it on the next boot. The routes only take mux controls from the crossbar, so the crossbar is the dependency to declare here. Declare MODULE_SOFTDEP("pre: mux-apple-display-crossbar"). mkinitcpio adds soft dependencies to the image, and modprobe loads the crossbar first. Soft dependencies are optional, so a kernel with the crossbar built in or disabled is unaffected. Signed-off-by: Jeppe Bylov Co-Authored-By: Claude Opus 5.5 (cherry picked from commit 12c1012b573953b289cd2f1d4d6fcaeb41b30377) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/apple_drv.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index d71c5c0370f2e..ea95084e936ba 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -1559,3 +1559,10 @@ module_exit(appledrm_unregister); MODULE_AUTHOR("Asahi Linux contributors"); MODULE_DESCRIPTION(DRIVER_DESC); MODULE_LICENSE("Dual MIT/GPL"); +/* + * The DCPs take their HDMI and Type-C route mux controls from the display + * crossbar. Without this, initramfs generators leave it out, it loads only + * from the root filesystem, and a DCP probe that waits past the deferred + * probe timeout (an encrypted root's passphrase prompt) gives up its routes. + */ +MODULE_SOFTDEP("pre: mux-apple-display-crossbar"); From 5e5fc3e36a686068a695710ce9e2f3776228155a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20Carre=C3=B1o?= Date: Sat, 24 Oct 2026 09:17:29 +0200 Subject: [PATCH 20/28] drm/apple: v14.7: admit panel brightness on the J516C MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The J516C runs the J516S panel, and its own 14.7 DCP image takes the same level block in the swap (present byte +0x354, binary64 nits +0x35c, at most 500 nits): the e272b67 path presented brightness that way on this machine. Set panel_nits on its board record and admit apple,j516c. Generated-by: LLM Signed-off-by: Samuel CarreƱo (cherry picked from commit a91ccf0abdeeed7d3bd696b4c01d861a9756a0a2) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/iomfb_v14_7.c | 11 +++++++++-- drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c | 12 ++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 681c50972dc3a..612d160d1ad80 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -162,6 +162,12 @@ static const struct dcp_v14_board dcp_v14_board_j516c = { .promotion = true, .log_bringup = true, .swap_timestamps = true, + /* + * The J516S panel and this image's swap carry the level block the + * T6030 record uses (present byte +0x354, binary64 nits +0x35c, 500 nits + * at most), as the e272b67 path on this J516C presented it. + */ + .panel_nits = true, }; /* @@ -291,7 +297,7 @@ struct apple_dcp_v14 { u64 swaps; u64 swap_ns_max; - /* Immutable probe admission: only the J514S/J516S internal panel. */ + /* Immutable probe admission: only the J514S/J516S/J516C internal panel. */ bool panel_nits; /* Copy of the last accepted surface; active_fb pins its DMA under lock. */ u8 shown_surface[DCP_V14_SURFACE_SIZE]; @@ -1689,7 +1695,8 @@ static bool dcp_v14_panel_nits_admit(const struct dcp_v14_board *board, dcp_v14_compat_fn machine_is, const void *ctx) { return board && board->panel_nits && - (machine_is(ctx, "apple,j514s") || machine_is(ctx, "apple,j516s")); + (machine_is(ctx, "apple,j514s") || machine_is(ctx, "apple,j516s") || + machine_is(ctx, "apple,j516c")); } const char *iomfb_v14_7_board_name(const struct dcp_v14_board *board) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c index d2e56c427f368..061af270a7f4a 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c @@ -8,7 +8,7 @@ * a refused level never becomes the actual one). * * The objects are built here, with no DCP: nothing in a case may reach an - * RPC. A J613, J615 or J516C panel never initialises the backlight policy; + * RPC. A J613, J615 or J514C panel never initialises the backlight policy; * its state is set to answer "yes" so that a case fails if anything on * those boards looks at it. */ @@ -122,7 +122,7 @@ static bool v14_bl_ctx_machine_is(const void *ctx, const char *compat) return !strcmp(((const char *const *)ctx)[1], compat); } -/* Only the T6030 record (J514S, J516S) carries levels. */ +/* Only the T6030 record (J514S, J516S) and the J516C record carry levels. */ static void v14_bl_boards(struct kunit *test) { static const char *const t6030_j516s[] = { "apple,t6030-dcp", "apple,j516s" }; @@ -156,7 +156,8 @@ static void v14_bl_boards(struct kunit *test) KUNIT_EXPECT_FALSE(test, board->panel_nits); board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, j516c, &known); KUNIT_ASSERT_NOT_NULL(test, board); - KUNIT_EXPECT_FALSE(test, board->panel_nits); + KUNIT_EXPECT_TRUE(test, board->panel_nits); + KUNIT_EXPECT_TRUE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, j516c)); } static void v14_bl_gate_transitions(struct kunit *test) @@ -342,14 +343,13 @@ static bool v14_bl_queued(struct apple_dcp_v14 *v14) } /* - * The production paths on J613, J615 and J516C: no policy is consulted, no + * The production paths on J613, J615 and J514C: no policy is consulted, no * level is encoded, the work does nothing, sleep leaves the gate alone. */ static void v14_bl_other_boards_untouched(struct kunit *test) { const struct dcp_v14_board *boards[] = { - &dcp_v14_board_j613, &dcp_v14_board_j615, &dcp_v14_board_j516c, - &dcp_v14_board_j514c, + &dcp_v14_board_j613, &dcp_v14_board_j615, &dcp_v14_board_j514c, }; struct dcp_backlight_present present; unsigned int i; From dac8e09a31195db1eb0e5f42f523cd2fd309580e Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+chriskearney@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:00:56 -0700 Subject: [PATCH 21/28] selftests: apple-dcp: follow the one-argument cd321x_display_taken() On a tree where an occupied display pipeline is reported as -ENOSPC on every SoC, cd321x_display_taken() takes only the return code, and -EBUSY means a transient mux or PHY failure. The probe controls still call the two-argument form and no longer build there, and the disconnect scenario injects -EBUSY as an occupied display, which such a tree retries instead. When the tree has the one-argument helper, check its contract in the probe controls (-ENOSPC is taken; -EBUSY, -EAGAIN, -ETIMEDOUT, -EIO and 0 are not). In the disconnect scenario, let the mode stub classify its error through the real helper, inject -ENOSPC as the occupied display, and add a transient -EBUSY case that must retry before the USB role is switched. A tree with the two-argument helper runs the old checks unchanged. The probe control also zero-initializes its cd321x_pm_state: cd321x_pm_init() does not set setup_pending, which cd321x_pm_ready() reads. Signed-off-by: Chris Kearney <303316+chriskearney@users.noreply.github.com> Generated-by: LLM [ryan: kept the desktop 26.6 connect stub of this tree beside the new block; on this tree cd321x_display_taken() still takes two arguments, so only the zero-initialised cd321x_pm_state takes effect] (cherry picked from commit dbb6bcce98cb5b0c38428d71933a7f3aea8a2b54) Assisted-by: LLM Signed-off-by: Ryan Murray --- .../drivers/gpu/apple-dcp/lifecycle.py | 19 +++++++++++++++++++ .../selftests/drivers/gpu/apple-dcp/pm.py | 13 +++++++++++++ 2 files changed, 32 insertions(+) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index 79a0adf4a7b32..3b195023f0404 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -483,6 +483,25 @@ def function(file, name): prefix += ('#define DCP_FIRMWARE_V_26_6_DESKTOP 100\n' 'static int __attribute__((unused)) dcp_desktop_dptx_connect(struct apple_dcp *d, u32 port)\n' '{ (void)d; (void)port; assert(!"desktop 26.6 connect in an H17P scenario"); return -EINVAL; }\n') + # Let the real helper classify an occupied display on newer trees, where + # -EBUSY means a transient mux/PHY failure rather than a taken pipeline. + if re.search(r'cd321x_display_taken\(int ret\)', pm): + assert shim.count('mode_error == -EBUSY') == 1 + assert tests.count('mode_error = -EBUSY;') == 2 + shim = shim.replace('mode_error == -EBUSY', 'cd321x_display_taken(mode_error)') + tests = tests.replace('mode_error = -EBUSY;', 'mode_error = -ENOSPC;') + busy = ' CHECK(sink.connected && !notices && !port.display_route_active);\n' + assert tests.count(busy) == 1 + tests = tests.replace(busy, busy + r''' + CHECK(roles == 1 && port.pm.phase == CD321X_PM_RUNNING); + /* A transient busy mux must retry before switching the USB role. */ + init(); port.display_route_active = false; role = USB_ROLE_NONE; + port.update_status.status = TPS_STATUS_PLUG_PRESENT; + port.update_status.data_status = TPS_DATA_STATUS_DP_CONNECTION | CD321X_DATA_STATUS_HPD_LEVEL; + mode_error = -EBUSY; cd321x_update_work(&port.update_work.work); + CHECK(!roles && port.pm.phase == CD321X_PM_REVALIDATE); + CHECK(sink.connected && !notices && !port.display_route_active); +''') # How long a held mux has been retried is measured on the driver's clock. if 'jiffies_to_msecs(jiffies)' in funcs: shim += ('\nstatic unsigned long jiffies;\n' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py index faae722b9d8e1..37537310d333e 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/pm.py @@ -642,6 +642,19 @@ def function(text, name): } ''' + # Newer trees report an occupied pipeline as -ENOSPC on every SoC. + # Keep the old two-argument contract covered when testing an older tree. + if re.search(r'cd321x_display_taken\(int ret\)', pm): + old = ' assert(cd321x_display_taken(-ENOSPC,true)&&!cd321x_display_taken(-EBUSY,true)&&!cd321x_display_taken(0,true)&&cd321x_display_taken(-EBUSY,false)&&!cd321x_display_taken(-ENOSPC,false)&&!cd321x_display_taken(0,false));' + assert controls.count(old) == 1 + controls = controls.replace(old, r''' + /* An occupied display permits USB progress; transient/other errors retry. */ + assert(cd321x_display_taken(-ENOSPC)); + assert(!cd321x_display_taken(-EBUSY)&&!cd321x_display_taken(-EAGAIN)); + assert(!cd321x_display_taken(-ETIMEDOUT)&&!cd321x_display_taken(-EIO)); + assert(!cd321x_display_taken(0)); +''') + # A mux that holds a change up is retried outside the cable-state budget. if 'cd321x_pm_mux_held(' in pm: lost = ' puts("PASS actual lost events reopen a stale port; unacknowledged events keep their budget with capped backoff");\n' From 38759961f62e5e8cc8010f9ed034a67f1e0dc477 Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Fri, 30 Oct 2026 15:02:51 +0100 Subject: [PATCH 22/28] drm/apple: set an external mode again once the display is on An external mode is set with the display powered down, and the display is powered up after it. For a mode that needs DSC (3840x2160 at 100 and 120 Hz on the J514S HDMI and Type-C ports) the firmware refuses that mode set ("Modeset done, but pipe not enabled") and resets DSC, and the power-on only configures DSC: the timings start in a mode set the firmware makes on its own, after a hotplug notification it sends itself. A swap that reaches it before then is dropped ("swallowed swap ID 1 as timings are not enabled") and never completed, so A407 times out after 5 s and the display stays dark until a reboot. A fresh attach lost that race 3 times out of 3. Without DSC, the power-on starts the timings itself. Set the mode once more after the power-on, with the pipe enabled. The firmware then starts the timings before that call returns, before the first swap; its own mode set finds them running. KUnit cases check the calls of an attach and of a mode change. Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 (cherry picked from commit 7ef3bc138e59fef9d74964b61513a5be31c35aea) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/iomfb_v14_7.c | 12 ++ .../apple/iomfb_v14_7_external_modeset_test.c | 165 ++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 drivers/gpu/drm/apple/iomfb_v14_7_external_modeset_test.c diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 612d160d1ad80..17b10cf272214 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -3265,6 +3265,17 @@ static int dcp_v14_external_modeset(struct apple_dcp *dcp, struct drm_crtc_state ret = dcp_v14_set_mode(v14, mode.color_mode_id, mode.timing_mode_id); if (!ret) ret = dcp_v14_set_power(v14, true); + /* + * The mode was set with the pipe off. A mode that needs DSC is then + * reset again ("pipe not enabled"), and the power-on only configures + * DSC: the firmware starts the timings in a mode set of its own, after + * the hotplug notification it sends itself, and drops a swap that + * reaches it before then without completing it, so A407 times out. + * Setting the mode once more with the pipe on starts the timings + * before the first swap. + */ + if (!ret) + ret = dcp_v14_set_mode(v14, mode.color_mode_id, mode.timing_mode_id); if (!ret) { v14->panel_width = mode.mode.hdisplay; v14->panel_height = mode.mode.vdisplay; @@ -3377,6 +3388,7 @@ void iomfb_v14_7_poweroff(struct apple_dcp *dcp) #if IS_ENABLED(CONFIG_DRM_APPLE_DISPLAY_KUNIT_TEST) #include "iomfb_v14_7_external_audio_test.c" +#include "iomfb_v14_7_external_modeset_test.c" #include "iomfb_v14_7_backlight_test.c" #include "iomfb_v14_7_backlight_lifetime_test.c" #endif diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_external_modeset_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_external_modeset_test.c new file mode 100644 index 0000000000000..170aa59fb4234 --- /dev/null +++ b/drivers/gpu/drm/apple/iomfb_v14_7_external_modeset_test.c @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* + * The DCP calls of an external mode set, included at the end of + * iomfb_v14_7.c so that the production dcp_v14_external_modeset() runs. + * Every call is answered as accepted; the audio service is stubbed. + */ +#include +#include +#include + +#define V14_EXT_MODESET_MAX_CALLS 8 + +struct v14_ext_modeset { + struct apple_dcp *dcp; + struct apple_dcp_v14 *v14; + struct dcp_display_mode mode; + u32 tags[V14_EXT_MODESET_MAX_CALLS]; + bool power[V14_EXT_MODESET_MAX_CALLS]; + unsigned int calls; +}; + +static struct v14_ext_modeset *v14_ext_modeset_current; + +static int v14_ext_modeset_call(struct apple_dcp_v14 *v14, u32 tag, const void *in, u32 in_size, + void *out, u32 out_size, u32 completion, dcp_v14_admit_fn admit, + void *admit_cookie) +{ + struct v14_ext_modeset *s = v14_ext_modeset_current; + + if (s->calls < V14_EXT_MODESET_MAX_CALLS) { + s->tags[s->calls] = tag; + s->power[s->calls] = tag == A(472) && in_size >= 8 && get_unaligned_le64(in); + } + s->calls++; + if (out) + memset(out, 0, out_size); + return 0; +} + +static void v14_ext_modeset_audio(struct apple_dcp *dcp) +{ +} + +static void v14_ext_modeset_retry(struct work_struct *work) +{ +} + +static void v14_ext_modeset_stop(void *data) +{ + struct apple_dcp *dcp = data; + + cancel_delayed_work_sync(&dcp->external_retry_wq); +} + +/* A started, described native external session with one mode, its display @powered. */ +static struct v14_ext_modeset *v14_ext_modeset_setup(struct kunit *test, bool powered) +{ + struct v14_ext_modeset *s = kunit_kzalloc(test, sizeof(*s), GFP_KERNEL); + struct device *dev; + + KUNIT_ASSERT_NOT_NULL(test, s); + dev = kunit_device_register(test, "dcp-v14-ext-modeset"); + KUNIT_ASSERT_NOT_ERR_OR_NULL(test, dev); + s->dcp = devm_kzalloc(dev, sizeof(*s->dcp), GFP_KERNEL); + s->v14 = devm_kzalloc(dev, sizeof(*s->v14), GFP_KERNEL); + KUNIT_ASSERT_NOT_NULL(test, s->dcp); + KUNIT_ASSERT_NOT_NULL(test, s->v14); + s->dcp->dev = dev; + s->dcp->fw_compat = DCP_FIRMWARE_V_14_7; + s->dcp->external = true; + s->dcp->external_native = true; + /* Never dereferenced: the audio service is stubbed. */ + s->dcp->avep = (struct apple_dcp_afkep *)s; + mutex_init(&s->dcp->modes_lock); + mutex_init(&s->dcp->hpd_mutex); + spin_lock_init(&s->dcp->metadata.lock); + spin_lock_init(&s->dcp->mode_state.lock); + INIT_DELAYED_WORK(&s->dcp->external_retry_wq, v14_ext_modeset_retry); + /* 3840x2160 at 100 Hz, timing 167 and color 219 as a J514S TV reports it. */ + s->mode.mode = (struct drm_display_mode){ + DRM_MODE("3840x2160", DRM_MODE_TYPE_DRIVER, 1188000, 3840, 4896, 4984, 5280, 0, + 2160, 2168, 2178, 2250, 0, DRM_MODE_FLAG_PHSYNC | DRM_MODE_FLAG_PVSYNC) + }; + s->mode.timing_mode_id = 167; + s->mode.color_mode_id = 219; + s->dcp->modes = &s->mode; + s->dcp->nr_modes = 1; + s->dcp->modes_admitted = true; + s->dcp->v14 = s->v14; + s->v14->dev = dev; + s->v14->dcp = s->dcp; + s->v14->external = true; + s->v14->started = true; + s->v14->opened = true; + s->v14->powered = powered; + s->v14->described = DCP_V14_DESC_ALL; + mutex_init(&s->v14->lock); + init_waitqueue_head(&s->v14->described_wait); + KUNIT_ASSERT_EQ(test, kunit_add_action_or_reset(test, v14_ext_modeset_stop, s->dcp), 0); + v14_ext_modeset_current = s; + kunit_activate_static_stub(test, dcp_v14_call_guarded, v14_ext_modeset_call); + kunit_activate_static_stub(test, dcp_v14_audio_off, v14_ext_modeset_audio); + kunit_activate_static_stub(test, dcp_v14_audio_on, v14_ext_modeset_audio); + return s; +} + +static int v14_ext_modeset_run(struct kunit *test, struct v14_ext_modeset *s) +{ + struct drm_crtc_state *state = kunit_kzalloc(test, sizeof(*state), GFP_KERNEL); + + KUNIT_ASSERT_NOT_NULL(test, state); + drm_mode_copy(&state->mode, &s->mode.mode); + return dcp_v14_external_modeset(s->dcp, state); +} + +/* + * A display that was off, as on attach: the mode is set, the display powered + * on, and the mode set once more with the pipe on, before any swap. A DSC + * mode set with the pipe off only takes effect then. + */ +static void v14_ext_modeset_sets_mode_again_after_power_on(struct kunit *test) +{ + struct v14_ext_modeset *s = v14_ext_modeset_setup(test, false); + + KUNIT_EXPECT_EQ(test, v14_ext_modeset_run(test, s), 0); + KUNIT_ASSERT_EQ(test, s->calls, 3U); + KUNIT_EXPECT_EQ(test, s->tags[0], A(411)); + KUNIT_EXPECT_EQ(test, s->tags[1], A(472)); + KUNIT_EXPECT_TRUE(test, s->power[1]); + KUNIT_EXPECT_EQ(test, s->tags[2], A(411)); + KUNIT_EXPECT_TRUE(test, s->v14->powered); + KUNIT_EXPECT_TRUE(test, s->v14->mode_live); + KUNIT_EXPECT_EQ(test, s->v14->mode_timing, 167U); + KUNIT_EXPECT_EQ(test, s->v14->mode_color, 219U); + KUNIT_EXPECT_FALSE(test, mutex_is_locked(&s->v14->lock)); +} + +/* A mode change on a display that is on: power down, set, power up, set again. */ +static void v14_ext_modeset_mode_change(struct kunit *test) +{ + struct v14_ext_modeset *s = v14_ext_modeset_setup(test, true); + + KUNIT_EXPECT_EQ(test, v14_ext_modeset_run(test, s), 0); + KUNIT_ASSERT_EQ(test, s->calls, 4U); + KUNIT_EXPECT_EQ(test, s->tags[0], A(472)); + KUNIT_EXPECT_FALSE(test, s->power[0]); + KUNIT_EXPECT_EQ(test, s->tags[1], A(411)); + KUNIT_EXPECT_EQ(test, s->tags[2], A(472)); + KUNIT_EXPECT_TRUE(test, s->power[2]); + KUNIT_EXPECT_EQ(test, s->tags[3], A(411)); + KUNIT_EXPECT_TRUE(test, s->v14->powered); +} + +static struct kunit_case v14_ext_modeset_cases[] = { + KUNIT_CASE(v14_ext_modeset_sets_mode_again_after_power_on), + KUNIT_CASE(v14_ext_modeset_mode_change), + {} +}; + +static struct kunit_suite v14_ext_modeset_suite = { + .name = "dcp_v14_external_modeset", + .test_cases = v14_ext_modeset_cases, +}; + +kunit_test_suite(v14_ext_modeset_suite); From 640abc32a8bc4a8eade57ed854cfdecabb796b1b Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Fri, 30 Oct 2026 20:32:06 +0100 Subject: [PATCH 23/28] drm/apple: hold the audio link on the Type-C ports too A stereo audio link is held between streams so that the sink does not mute it on a restart with the same format. The hold_link parameter limited that to the HDMI port by default (hold_link=1) until the Type-C ports had been tested with it; hold_link=2 extended it to them and 0 turned it off. On the J514S, a TV on a Type-C port behaves as it does on the HDMI port: without the hold, every stream starts a new link with an unchanged format, the TV mutes it, and USB-C audio stays silent after the first stream. With hold_link=2 it played across Chromium stop/start, pw-play runs and a PipeWire restart, and held links survived 90 s idle and sleep. Hold the link on every port and drop the parameter. Nothing needs to turn the hold off, and a parameter that only makes audio go silent is not worth keeping. Streams of more than two channels still get a link of their own, as before. Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 (cherry picked from commit 653176dcb94ed7253763428f1f8522cb4685e496) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/audio-link-test.c | 44 ------------------------- drivers/gpu/drm/apple/audio.c | 42 +++++++---------------- drivers/gpu/drm/apple/audio.h | 2 -- drivers/gpu/drm/apple/av.c | 6 ---- 4 files changed, 11 insertions(+), 83 deletions(-) diff --git a/drivers/gpu/drm/apple/audio-link-test.c b/drivers/gpu/drm/apple/audio-link-test.c index 40f928396297b..0ddd42dce072c 100644 --- a/drivers/gpu/drm/apple/audio-link-test.c +++ b/drivers/gpu/drm/apple/audio-link-test.c @@ -12,7 +12,6 @@ struct dcpaud_link_test { bool link_on; bool link_live; - bool typec; /* The error removing the idle transfer returns */ int idle_remove_err; u64 asked; @@ -40,11 +39,6 @@ static bool dcpaud_link_test_live(struct device *dev) return dcpaud_link_current->link_live; } -static bool dcpaud_link_test_typec(struct device *dev) -{ - return dcpaud_link_current->typec; -} - static int dcpaud_link_test_terminate(struct dma_chan *chan) { dcpaud_link_record('X'); @@ -147,7 +141,6 @@ static struct dcp_audio *dcpaud_link_setup(struct kunit *test, struct platform_d kunit_activate_static_stub(test, dcp_audiosrv_link_on, dcpaud_link_test_on); kunit_activate_static_stub(test, dcp_audiosrv_link_live, dcpaud_link_test_live); kunit_activate_static_stub(test, dcp_audiosrv_link, dcpaud_link_test_call); - kunit_activate_static_stub(test, dcp_audiosrv_typec, dcpaud_link_test_typec); return dcpaud; } @@ -451,42 +444,6 @@ static void dcpaud_close_keeps_held_silence(struct kunit *test) pm_runtime_put_noidle(audio->dev); } -static void dcpaud_link_test_hold_default(void *data) -{ - WRITE_ONCE(hold_link, 1); -} - -static void dcpaud_typec_stop_ends_stream(struct kunit *test) -{ - struct platform_device *pdev; - struct dcp_audio *audio = dcpaud_link_setup(test, &pdev); - struct snd_pcm_substream *sub = dcpaud_link_test_open(test, audio); - - KUNIT_ASSERT_EQ(test, - kunit_add_action_or_reset(test, dcpaud_link_test_hold_default, NULL), 0); - sub->runtime->channels = 2; - sub->runtime->dma_area = kunit_kzalloc(test, 64, GFP_KERNEL); - KUNIT_ASSERT_NOT_NULL(test, sub->runtime->dma_area); - sub->runtime->dma_bytes = 64; - dcpaud_link_current->typec = true; - - /* USB-C by default: STOP ends the stream's DMA, no link is held */ - audio->stream_dma = true; - pm_runtime_get_noresume(audio->dev); - KUNIT_ASSERT_EQ(test, dcp_pcm_native_trigger(sub, SNDRV_PCM_TRIGGER_STOP), 0); - KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "X"); - KUNIT_EXPECT_FALSE(test, audio->dma_cleared); - KUNIT_EXPECT_EQ(test, atomic_read(&audio->dev->power.usage_count), 0); - - /* hold_link=2: the DMA plays the cleared buffer on for the held link */ - WRITE_ONCE(hold_link, 2); - pm_runtime_get_noresume(audio->dev); - KUNIT_ASSERT_EQ(test, dcp_pcm_native_trigger(sub, SNDRV_PCM_TRIGGER_STOP), 0); - KUNIT_EXPECT_STREQ(test, dcpaud_link_current->sequence, "X"); - KUNIT_EXPECT_TRUE(test, audio->dma_cleared); - pm_runtime_put_noidle(audio->dev); -} - static struct kunit_case dcpaud_link_cases[] = { KUNIT_CASE(dcpaud_link_release_instance), KUNIT_CASE(dcpaud_disconnect_sync_completes_live_owner), @@ -499,7 +456,6 @@ static struct kunit_case dcpaud_link_cases[] = { KUNIT_CASE(dcpaud_start_after_held_link_went), KUNIT_CASE(dcpaud_failed_start_keeps_held_link), KUNIT_CASE(dcpaud_close_keeps_held_silence), - KUNIT_CASE(dcpaud_typec_stop_ends_stream), {} }; diff --git a/drivers/gpu/drm/apple/audio.c b/drivers/gpu/drm/apple/audio.c index 35fadd62320cb..44c228deb2a89 100644 --- a/drivers/gpu/drm/apple/audio.c +++ b/drivers/gpu/drm/apple/audio.c @@ -655,12 +655,12 @@ struct snd_pcm_ops dcp_playback_ops = { * service itself is open while the PCM is, or while its link is held. */ /* - * The sink behind the J514S HDMI port (PS190 converter and TV) mutes the - * audio of a link that stops and starts again with an unchanged format, - * until the DP link trains again: only a different channel layout brings - * it back. So a stereo link, once started, is held between streams, as - * macOS holds it: silence follows the stopped stream's DMA and the link - * keeps running until the format changes or the display goes off + * The sinks behind the J514S HDMI port (PS190 converter and TV) and its + * Type-C ports mute the audio of a link that stops and starts again with an + * unchanged format, until the DP link trains again: only a different channel + * layout brings it back. So a stereo link, once started, is held between + * streams, as macOS holds it: silence follows the stopped stream's DMA and + * the link keeps running until the format changes or the display goes off * (dcpaud_release_link()). * * The link must not run dry either: a gap of 20 ms in its samples mutes the @@ -668,26 +668,6 @@ struct snd_pcm_ops dcp_playback_ops = { * the channel issues whenever no stream's DMA does, in descriptor order: * a stream's DMA takes over from it and hands back to it without a gap. */ -/* - * Read-only at runtime: a stream opened while it was lower keeps periods - * longer than a held link's synchronize allows for. - */ -static int hold_link = 1; -module_param(hold_link, int, 0444); -MODULE_PARM_DESC(hold_link, - "Hold started audio links between streams (silence): 0 never, 1 on the HDMI port (default), 2 also on USB-C"); - -/* - * The mute was measured behind the HDMI port. USB-C sinks keep one link per - * stream unless hold_link=2, until they have been qualified with this. - */ -static bool dcpaud_holds(struct dcp_audio *dcpaud) -{ - int hold = READ_ONCE(hold_link); - - return hold >= 2 || (hold == 1 && !dcp_audiosrv_typec(dcpaud->dcp_dev)); -} - /* * A change of sample rate or sample size with an unchanged channel layout * replaces the link with a new one, which this sink mutes just as it does a @@ -879,7 +859,7 @@ static int dcp_pcm_native_open(struct snd_pcm_substream *substream) } ret = dcp_pcm_open(substream); - if (!ret && live && !dcpaud->open_unplugged && dcpaud_holds(dcpaud)) { + if (!ret && live && !dcpaud->open_unplugged) { ret = dcpaud_pin_rate_format(dcpaud, substream->runtime); if (!ret) ret = snd_pcm_hw_constraint_minmax(substream->runtime, @@ -1043,8 +1023,8 @@ static int dcp_pcm_native_start(struct snd_pcm_substream *substream, int cmd) /* The first link of the connection waits for the sink to settle. */ scoped_guard(mutex, &dcpaud->data_lock) up_ms = ktime_ms_delta(ktime_get(), dcpaud->connected_at); - if (dcpaud_holds(dcpaud) && !READ_ONCE(dcpaud->link_started) && - up_ms >= 0 && up_ms < DCPAUD_SINK_SETTLE_MS) { + if (!READ_ONCE(dcpaud->link_started) && up_ms >= 0 && + up_ms < DCPAUD_SINK_SETTLE_MS) { dev_dbg(dcpaud->dev, "sink up %lld ms, link waits\n", up_ms); msleep(DCPAUD_SINK_SETTLE_MS - up_ms); } @@ -1117,8 +1097,8 @@ static int dcp_pcm_native_trigger(struct snd_pcm_substream *substream, int cmd) * reference, until sync_stop hands over to the held link's * silence: the link must not run dry meanwhile. */ - if (dcpaud_holds(dcpaud) && READ_ONCE(dcpaud->stream_dma) && - substream->runtime->channels == 2 && substream->runtime->dma_area) { + if (READ_ONCE(dcpaud->stream_dma) && substream->runtime->channels == 2 && + substream->runtime->dma_area) { memset(substream->runtime->dma_area, 0, substream->runtime->dma_bytes); WRITE_ONCE(dcpaud->dma_cleared, true); return 0; diff --git a/drivers/gpu/drm/apple/audio.h b/drivers/gpu/drm/apple/audio.h index f1f125305da59..79bee44bcefbc 100644 --- a/drivers/gpu/drm/apple/audio.h +++ b/drivers/gpu/drm/apple/audio.h @@ -37,8 +37,6 @@ void dcpaud_release_link(struct platform_device *pdev, const u64 *gen); bool dcp_audiosrv_link_on(struct device *dev, u64 gen); /* The prepared link still belongs to the client of the published instance. */ bool dcp_audiosrv_link_live(struct device *dev); -/* The display processor drives a Type-C route rather than the HDMI port. */ -bool dcp_audiosrv_typec(struct device *dev); #endif #endif /* __AUDIO_H__ */ diff --git a/drivers/gpu/drm/apple/av.c b/drivers/gpu/drm/apple/av.c index cda3e65cbf3dd..fad5fe6c43d1a 100644 --- a/drivers/gpu/drm/apple/av.c +++ b/drivers/gpu/drm/apple/av.c @@ -411,12 +411,6 @@ bool dcp_audiosrv_link_live(struct device *dev) return asrv->link_gen == asrv->open_gen && av_audiosrv_open_published(asrv); } -bool dcp_audiosrv_typec(struct device *dev) -{ - KUNIT_STATIC_STUB_REDIRECT(dcp_audiosrv_typec, dev); - return dcp_is_typec_output(dev_get_drvdata(dev)); -} - /* * The link commands of the 14.7 external role's audio service, the native * audio driver's: prepare, start, stop and complete all take the selected From fffd99a819211f0c08fda0a3227f022ed3fd5c1e Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 1 Nov 2026 11:11:21 +1100 Subject: [PATCH 24/28] drm/apple: look for a processor's audio device before its firmware avep_init() checks the DCP firmware before it looks in the device tree for the audio device linked from port@0. With hdmi_audio set, every processor that starts through the IOMFB template path gets there, including the H17P ones of the J700, whose firmware has no audio service here and whose device tree links no audio device: each logs "Audio not supported for firmware" as an error and dcp_start() then warns that the AV endpoint failed. Processors of the other firmware without an audio device (the internal panels) allocate the audio service state for nothing and log "No audio support". Look for the audio device first and return quietly without one, leaving the processor exactly as it is with hdmi_audio off: no audio service state, no AV endpoint. Processors that link an audio device go on as before. This matters once DP/HDMI audio is on by default, which the next commit does. Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/av.c | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/drivers/gpu/drm/apple/av.c b/drivers/gpu/drm/apple/av.c index fad5fe6c43d1a..5e11c704a86ef 100644 --- a/drivers/gpu/drm/apple/av.c +++ b/drivers/gpu/drm/apple/av.c @@ -1041,9 +1041,28 @@ int avep_init(struct apple_dcp *dcp) #endif } + /* + * Only a processor whose port@0 links an audio device has DP/HDMI + * audio to start. Look before the firmware: a processor without one + * (an internal panel's, or one whose firmware has no audio service + * here, such as H17P) stays as it is without hdmi_audio, quietly. + */ + endpoint = of_graph_get_endpoint_by_regs(dev->of_node, 0, 0); + if (endpoint) { + audio_node = of_graph_get_remote_port_parent(endpoint); + of_node_put(endpoint); + } + if (!audio_node || !of_device_is_available(audio_node)) { + of_node_put(audio_node); + dev_dbg(dev, "No audio support\n"); + return 0; + } + audiosrv_data = devm_kzalloc(dcp->dev, sizeof(*audiosrv_data), GFP_KERNEL); - if (!audiosrv_data) + if (!audiosrv_data) { + of_node_put(audio_node); return -ENOMEM; + } init_rwsem(&audiosrv_data->srv_rwsem); mutex_init(&audiosrv_data->plug_lock); @@ -1068,6 +1087,7 @@ int avep_init(struct apple_dcp *dcp) break; } #endif + of_node_put(audio_node); dev_err(dcp->dev, "Audio not supported for firmware\n"); return -ENODEV; } @@ -1080,17 +1100,6 @@ int avep_init(struct apple_dcp *dcp) audiosrv_data->works_live = true; dcp->audiosrv = audiosrv_data; - endpoint = of_graph_get_endpoint_by_regs(dev->of_node, 0, 0); - if (endpoint) { - audio_node = of_graph_get_remote_port_parent(endpoint); - of_node_put(endpoint); - } - if (!audio_node || !of_device_is_available(audio_node)) { - of_node_put(audio_node); - dev_info(dev, "No audio support\n"); - return av_audiosrv_register_cleanup(dcp, audiosrv_data, NULL); - } - audio_pdev = of_find_device_by_node(audio_node); of_node_put(audio_node); if (!audio_pdev) { From 94ea41017a3fd3b8ffa80a79be055044e8cb7c98 Mon Sep 17 00:00:00 2001 From: Eryk Wieliczko Date: Fri, 30 Oct 2026 23:23:15 +0100 Subject: [PATCH 25/28] drm/apple: turn DP/HDMI audio on by default DP/HDMI audio through the display coprocessor has been off unless appledrm.hdmi_audio=1 was set, so a distribution that wants it has to ship that option in a modprobe.d file. Make it the default and keep the parameter as the way to turn it off: appledrm.hdmi_audio=0 on the kernel command line, or "options appledrm hdmi_audio=0" in modprobe.d before the driver loads. The default applies to every machine the driver supports, M1 and M2 included. Signed-off-by: Eryk Wieliczko Assisted-by: Claude:claude-opus-5-5 [ryan: the dcpext_probe and t6030_show_notch parameters of the conflicting context are not in this tree; the previous commit keeps processors without an audio device (the J700 H17P ones) quiet once this is on] (cherry picked from commit c9e5b2575e38e10b9daa0061c154a22336fcc9ef) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/av.h | 2 +- drivers/gpu/drm/apple/dcp.c | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/apple/av.h b/drivers/gpu/drm/apple/av.h index 1d710b0f706f3..6d25d83d78d79 100644 --- a/drivers/gpu/drm/apple/av.h +++ b/drivers/gpu/drm/apple/av.h @@ -6,7 +6,7 @@ //int avep_audiosrv_startlink(struct apple_dcp *dcp, struct dcp_sound_cookie *cookie); //int avep_audiosrv_stoplink(struct apple_dcp *dcp); -/* dcp.c: the opt-in to DP/HDMI audio (appledrm.hdmi_audio). */ +/* dcp.c: DP/HDMI audio, on unless appledrm.hdmi_audio=0. */ extern bool hdmi_audio; #if IS_ENABLED(CONFIG_DRM_APPLE_AUDIO) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 2685a136710fc..de7f347b17a99 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -203,9 +203,9 @@ static bool show_notch; module_param(show_notch, bool, 0644); MODULE_PARM_DESC(show_notch, "Use the full display height and shows the notch"); -bool hdmi_audio; +bool hdmi_audio = true; module_param(hdmi_audio, bool, 0644); -MODULE_PARM_DESC(hdmi_audio, "Enable unstable HDMI audio support"); +MODULE_PARM_DESC(hdmi_audio, "Enable DP/HDMI audio support (default: on)"); static bool unstable_edid = true; module_param(unstable_edid, bool, 0644); @@ -3696,7 +3696,7 @@ int dcp_start(struct platform_device *pdev) /* * A native T6030 external processor: its DP/HDMI audio endpoint (EP 0x29) - * on the driver's opt-in (hdmi_audio), as the other DCPs start it, for a + * unless hdmi_audio is off, as the other DCPs start it, for a * processor whose port@0 links an audio device; once per session, and after * the first display's EDID was read: with the endpoint's interfaces * published, an EDID copy (dcpavserv_copy_edid()) has the firmware rebuild From 2984ba957d9ef4f05aaccefeb661cefc321f0b7f Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Sat, 31 Oct 2026 10:07:52 -0400 Subject: [PATCH 26/28] drm/apple: enable the J613 panel brightness path The J613 board record does not admit panel nits, so the working 14.7 display session never registers a backlight device. Admit the J613 to the existing nits path and keep the firmware UUID and display handoff checks. J615 and J514C remain excluded. Run the existing swap-level KUnit case for J613 as well as T6030. Expose the existing completion and refusal counters in debugfs so a requested level can be distinguished from one the firmware accepted. On a 13-inch M3 MacBook Air with 23J220 DCP firmware, the driver registered apple-panel-bl and completed requests for 100, 250, 400, 1, 500 and 140 nits with no refusals or display failures. The user confirmed visible brightness changes in person. Assisted-by: OpenAI Codex (gpt-6.1-sol) Signed-off-by: DjDeveloperr (cherry picked from commit eec64583605c7b13c852488c9d7ff0c6a8cbb58c) Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/gpu/drm/apple/iomfb_v14_7.c | 6 +++- .../drm/apple/iomfb_v14_7_backlight_test.c | 29 ++++++++++++++----- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 17b10cf272214..6940145afcc47 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -120,6 +120,7 @@ static const struct dcp_v14_board dcp_v14_board_j613 = { .panel_width = 2560, .panel_height = 1664, .promotion = false, + .panel_nits = true, .ctm_set = A(421), .ctm_get = A(420), }; @@ -1696,7 +1697,7 @@ static bool dcp_v14_panel_nits_admit(const struct dcp_v14_board *board, { return board && board->panel_nits && (machine_is(ctx, "apple,j514s") || machine_is(ctx, "apple,j516s") || - machine_is(ctx, "apple,j516c")); + machine_is(ctx, "apple,j516c") || machine_is(ctx, "apple,j613")); } const char *iomfb_v14_7_board_name(const struct dcp_v14_board *board) @@ -2521,6 +2522,9 @@ static int dcp_v14_status_show(struct seq_file *m, void *unused) v14->stride, v14->clock_rate); seq_printf(m, "backlight factor %#x updates %llu\n", v14->backlight_factor, v14->backlight_factor_updates); + seq_printf(m, "backlight nits_enabled %d sent %u presents %llu brightness_only %llu refused %llu\n", + dcp_v14_board_nits(v14), v14->nits_sent, v14->nits_presents, + v14->brightness_presents, v14->nits_refused); seq_printf(m, "ctm valid %d disabled %d calls %llu setter %#x getter %#x\n", v14->ctm_valid, v14->ctm_disabled, v14->ctm_calls, v14->ctm_status, v14->ctm_get_status); diff --git a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c index 061af270a7f4a..e2704e5f1ba5f 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7_backlight_test.c @@ -8,7 +8,7 @@ * a refused level never becomes the actual one). * * The objects are built here, with no DCP: nothing in a case may reach an - * RPC. A J613, J615 or J514C panel never initialises the backlight policy; + * RPC. A J615 or J514C panel never initialises the backlight policy; * its state is set to answer "yes" so that a case fails if anything on * those boards looks at it. */ @@ -122,7 +122,7 @@ static bool v14_bl_ctx_machine_is(const void *ctx, const char *compat) return !strcmp(((const char *const *)ctx)[1], compat); } -/* Only the T6030 record (J514S, J516S) and the J516C record carry levels. */ +/* Only admitted T6030, J516C and J613 internal panels carry levels. */ static void v14_bl_boards(struct kunit *test) { static const char *const t6030_j516s[] = { "apple,t6030-dcp", "apple,j516s" }; @@ -150,7 +150,9 @@ static void v14_bl_boards(struct kunit *test) other_t6030)); board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, j613, &known); KUNIT_ASSERT_NOT_NULL(test, board); - KUNIT_EXPECT_FALSE(test, board->panel_nits); + KUNIT_EXPECT_TRUE(test, board->panel_nits); + KUNIT_EXPECT_TRUE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, j613)); + KUNIT_EXPECT_FALSE(test, dcp_v14_panel_nits_admit(board, v14_bl_ctx_machine_is, j615)); board = dcp_v14_board_select(v14_bl_ctx_dcp_is, v14_bl_ctx_machine_is, j615, &known); KUNIT_ASSERT_NOT_NULL(test, board); KUNIT_EXPECT_FALSE(test, board->panel_nits); @@ -343,13 +345,13 @@ static bool v14_bl_queued(struct apple_dcp_v14 *v14) } /* - * The production paths on J613, J615 and J514C: no policy is consulted, no + * The production paths on J615 and J514C: no policy is consulted, no * level is encoded, the work does nothing, sleep leaves the gate alone. */ static void v14_bl_other_boards_untouched(struct kunit *test) { const struct dcp_v14_board *boards[] = { - &dcp_v14_board_j613, &dcp_v14_board_j615, &dcp_v14_board_j514c, + &dcp_v14_board_j615, &dcp_v14_board_j514c, }; struct dcp_backlight_present present; unsigned int i; @@ -404,13 +406,13 @@ static void v14_bl_external_untouched(struct kunit *test) } /* - * The T6030 panel: no level until one is requested; the requested level is + * An admitted panel: no level until one is requested; the requested level is * encoded; a refusal keeps the actual level without a retry; an accepted one * is the actual level, and only then does blanking follow DPMS. */ -static void v14_bl_t6030_swap_levels(struct kunit *test) +static void v14_bl_swap_levels(struct kunit *test, const struct dcp_v14_board *board) { - struct v14_bl_fixture f = v14_bl_fixture(test, &dcp_v14_board_t6030, true); + struct v14_bl_fixture f = v14_bl_fixture(test, board, true); struct dcp_backlight_present present; u8 *swap = kunit_kzalloc(test, DCP_V14_SWAP_SIZE, GFP_KERNEL); @@ -462,6 +464,16 @@ static void v14_bl_t6030_swap_levels(struct kunit *test) mutex_unlock(&f.v14->lock); } +static void v14_bl_t6030_swap_levels(struct kunit *test) +{ + v14_bl_swap_levels(test, &dcp_v14_board_t6030); +} + +static void v14_bl_j613_swap_levels(struct kunit *test) +{ + v14_bl_swap_levels(test, &dcp_v14_board_j613); +} + /* * The gate, through the production queue path: open, a kick queues the work; * suspended, nothing queues until resume; closed, nothing ever does. The @@ -550,6 +562,7 @@ static struct kunit_case v14_backlight_cases[] = { KUNIT_CASE(v14_bl_other_boards_untouched), KUNIT_CASE(v14_bl_external_untouched), KUNIT_CASE(v14_bl_t6030_swap_levels), + KUNIT_CASE(v14_bl_j613_swap_levels), KUNIT_CASE(v14_bl_t6030_gate), KUNIT_CASE(v14_bl_t6030_work), {} From e6c35fdde648d1dbe4999aa0e19fb44087fb5a94 Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 1 Nov 2026 11:19:38 +1100 Subject: [PATCH 27/28] selftests: apple-dcp: stub the 14.7 panel nits policy in the backlight fixtures The panel-takeover-registration and m4-dpms-retirement host tests build dcp_get_brightness(), dcp_backlight_register() and dcp_backlight_active() from dcp_backlight.c. Since "drm/apple: port the scoped T6030 14.7 panel backlight" these call dcp_backlight_presents_nits() and iomfb_v14_7_panel_nits() and pick dcp_backlight_ops_v14 for the 14.7 panel, so both stopped compiling. Build the real dcp_backlight_presents_nits() into both units and answer iomfb_v14_7_panel_nits() with false: neither the desktop 26.6 panel nor the T8132 one is a 14.7 panel. The checks themselves are unchanged. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py | 8 ++++++++ tools/testing/selftests/drivers/gpu/apple-dcp/m4.py | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py index b7f4f7dd633dc..8a23ffbeb83ff 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/desktop.py @@ -206,6 +206,14 @@ def brightness_takeover(root): 'static void dcp_backlight_sync_hint(', 'int dcp_backlight_register(', 'static int dcp_set_brightness(', 'static bool dcp_backlight_controls_device(')) + # The 14.7 panel's nits policy and class operations (an M3 internal + # panel) never run on the desktop panel. + if 'static bool dcp_backlight_presents_nits(' in source: + functions = ('static bool iomfb_v14_7_panel_nits(struct apple_dcp *d) ' + '{(void)d;return false;}\n' + 'static const int dcp_backlight_ops_v14;\n' + + block(source, 'static bool dcp_backlight_presents_nits(') + + '\n' + functions) return prefix + r''' #include #include diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py b/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py index cb3bac542a88c..f11078c87b087 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/m4.py @@ -482,6 +482,12 @@ def blank(root): functions = '\n'.join(block(backlight, name) for name in ( 'bool dcp_backlight_active(', 'int dcp_backlight_dpms(', 'int dcp_backlight_prepare(', 'bool dcp_backlight_complete(')) + # The 14.7 panel's nits policy (an M3 internal panel) never runs here. + if 'static bool dcp_backlight_presents_nits(' in backlight: + functions = ('static bool iomfb_v14_7_panel_nits(struct apple_dcp *dcp) ' + '{ (void)dcp; return false; }\n' + + block(backlight, 'static bool dcp_backlight_presents_nits(') + + '\n' + functions) return production(root) + r''' #define min(x,y) ((x)<(y)?(x):(y)) static u64 div_u64(u64 x,u32 y) { return x/y; } From 797fee6e64ee99fb791a5b0265c288b49b62f03e Mon Sep 17 00:00:00 2001 From: Ryan Murray Date: Sun, 1 Nov 2026 11:19:38 +1100 Subject: [PATCH 28/28] selftests: apple-dcp: fit the panel-dimensions case to this tree The panel-dimensions case from "drm/apple: restore panel dimensions after mode probes" was written against a publication fixture whose connector already carried display_info's physical size and whose get_modes() had no T8132 branch. Here neither holds, so the case did not compile. Give the fixture's display_info its width_mm and height_mm, and add the case to the fixtures that select the older external backends, which define the T8132 hooks away as publication-hpd does. The case's checks are unchanged and pass. Assisted-by: LLM Signed-off-by: Ryan Murray --- tools/testing/selftests/drivers/gpu/apple-dcp/rate.py | 5 +++++ tools/testing/selftests/drivers/gpu/apple-dcp/run.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index 8598e9ad7a7e1..78e241892ca14 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -1111,6 +1111,11 @@ def panel_dimensions(root): unit, block = n['unit'], n['block'] unit = unit.replace('bool modes_lock,modes_admitted,', 'bool external_physically_absent,modes_lock,modes_admitted,', 1) + # The connector's physical size, which DRM keeps in display_info. + old = 'struct { struct { unsigned min_vfreq,max_vfreq; } monitor_range; } display_info;' + assert unit.count(old) == 1 + unit = unit.replace(old, 'struct { struct { unsigned min_vfreq,max_vfreq; } monitor_range; ' + 'int width_mm,height_mm; } display_info;') dcp = (root / 'drivers/gpu/drm/apple/dcp.c').read_text() unit = unit.replace(block(unit, 'static bool __attribute__((unused)) dcp_has_panel('), block(dcp, 'bool dcp_has_panel(')) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py index 9fb5815566a63..9ec7ee1ac56e8 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/run.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/run.py @@ -143,7 +143,7 @@ def main(): elif isinstance(unit, tuple): unit, cases = unit if unit is not None and name in ('provider-admission', 'ordered-rx', - 'publication-hpd', 'stale-crtc-poweroff', 'readiness'): + 'publication-hpd', 'panel-dimensions', 'stale-crtc-poweroff', 'readiness'): # These fixtures select older external-display backends. The M4 # transport is covered by the separate m4 group, not mocked here. prefix = ('#include \n#include \n'