From 8f30da9409163f97fbd1ef922f03c08e9d0d48c8 Mon Sep 17 00:00:00 2001 From: a-ramses Date: Fri, 28 Aug 2026 19:44:56 +0200 Subject: [PATCH 01/12] drm: apple: iomfb: fix DCP 14.7 swap request layout DCP 14.7 expects its 0x234-byte extension before the pointer-null flags. Linux placed it after the flags, so DCP read the flags at the old offsets and rejected surface-clear requests. Move the extension before the pointer-null flags. Signed-off-by: a-ramses --- drivers/gpu/drm/apple/iomfb_template.h | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_template.h b/drivers/gpu/drm/apple/iomfb_template.h index f3f93c17a050f9..8aea39d6664c9a 100644 --- a/drivers/gpu/drm/apple/iomfb_template.h +++ b/drivers/gpu/drm/apple/iomfb_template.h @@ -112,6 +112,9 @@ struct DCP_FW_NAME(dcp_swap_submit_req) { u32 clear; // or maybe switch to default fb? #if DCP_FW_VER >= DCP_FW_VERSION(13, 2, 0) u32 unkU32Ptr; +#endif +#if DCP_FW_VER >= DCP_FW_VERSION(14, 7, 0) + u8 padding_14_7[0x234]; #endif u8 swap_null; u8 surf_null[SWAP_SURFACES]; @@ -124,12 +127,6 @@ struct DCP_FW_NAME(dcp_swap_submit_req) { u8 unkU32out_null; #endif u8 padding[1]; -#if DCP_FW_VER >= DCP_FW_VERSION(14, 7, 0) - u8 padding_14_7[0x1e9]; - u8 unk_14_7_zero[0x46]; - u32 unk_14_7_u32; - u8 unk_bool; -#endif } __packed; struct DCP_FW_NAME(dcp_swap_submit_resp) { From c67faa7a73c7dc6035c6dadb2199a51bda1aa83b Mon Sep 17 00:00:00 2001 From: a-ramses Date: Fri, 28 Aug 2026 19:45:04 +0200 Subject: [PATCH 02/12] Revert "HACK: drm: apple: do not destroy stale framebuffer references" This reverts commit 47508c049d0964c15ff23f431995d63365fa7f91. The DCP 14.7 request layout now matches the firmware, so surface-clear requests work and stale framebuffer references can be retired again. Signed-off-by: a-ramses --- drivers/gpu/drm/apple/iomfb_template.c | 6 ------ 1 file changed, 6 deletions(-) diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 845e7e1d9a5cbd..a41db2949f48f8 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -739,8 +739,6 @@ static void dcp_swap_cleared(struct apple_dcp *dcp, void *data, void *cookie) return; } - /* TODO: Figure out how to clear surfaces for 14.x */ -#if DCP_FW_VER < DCP_FW_VERSION(14, 7, 0) while (!list_empty(&dcp->swapped_out_fbs)) { struct dcp_fb_reference *entry; entry = list_first_entry(&dcp->swapped_out_fbs, @@ -752,7 +750,6 @@ static void dcp_swap_cleared(struct apple_dcp *dcp, void *data, void *cookie) list_del(&entry->head); kfree(entry); } -#endif } static void dcp_swap_clear_started(struct apple_dcp *dcp, void *data, @@ -1153,8 +1150,6 @@ static void dcp_swapped(struct apple_dcp *dcp, void *data, void *cookie) } dcp->swap_start = ktime_get(); - /* TODO: Figure out how to clear surfaces on 14.x */ -#if DCP_FW_VER < DCP_FW_VERSION(14, 7, 0) while (!list_empty(&dcp->swapped_out_fbs)) { struct dcp_fb_reference *entry; entry = list_first_entry(&dcp->swapped_out_fbs, @@ -1166,7 +1161,6 @@ static void dcp_swapped(struct apple_dcp *dcp, void *data, void *cookie) list_del(&entry->head); kfree(entry); } -#endif } static void dcp_swap_started(struct apple_dcp *dcp, void *data, void *cookie) From b41fda7d4b6253ed50ae3c0e746235b920a25e89 Mon Sep 17 00:00:00 2001 From: Matthew Altman Date: Sun, 30 Aug 2026 18:03:57 -0500 Subject: [PATCH 03/12] thunderbolt: apple: trace ACIO shutdown milestones --- drivers/thunderbolt/apple.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/drivers/thunderbolt/apple.c b/drivers/thunderbolt/apple.c index cdefe608d4352b..af9bf8a5e2983a 100644 --- a/drivers/thunderbolt/apple.c +++ b/drivers/thunderbolt/apple.c @@ -575,6 +575,8 @@ static void apple_cio_stop(struct apple_cio *acio) int ret, i; lockdep_assert_held(&acio->lock); + dev_info(acio->dev, "stopping ACIO: current cable info 0x%x, target 0x%x\n", + acio->current_cable_info, acio->target_cable_info); /* * First, shutdown the blocks inside the ACIO complex, like the NHI and the IOMMU. @@ -582,6 +584,7 @@ static void apple_cio_stop(struct apple_cio *acio) * the MMIO space of these so make sure nothing tries to do just that. */ of_platform_depopulate(acio->dev); + dev_info(acio->dev, "ACIO child devices stopped\n"); /* Try to shut down and power off the co-processor gracefully */ ret = apple_rtkit_poweroff(acio->rtk); @@ -589,6 +592,7 @@ static void apple_cio_stop(struct apple_cio *acio) dev_warn(acio->dev, "Failed to shutdown M3 RTKit, continuing ACIO shutdown anyway\n"); apple_rtkit_free(acio->rtk); + dev_info(acio->dev, "ACIO RTKit stopped\n"); /* Finally, remove the links to the PD domains to power everything off */ for (i = 0; i < acio->pd_list->num_pds; i++) { @@ -598,6 +602,7 @@ static void apple_cio_stop(struct apple_cio *acio) } acio->current_cable_info = 0; + dev_info(acio->dev, "ACIO stopped\n"); } static int apple_cio_start(struct apple_cio *acio) From ea2844db592b72200e6020a5b192a30c8863def6 Mon Sep 17 00:00:00 2001 From: Matthew Altman Date: Sun, 30 Aug 2026 18:06:15 -0500 Subject: [PATCH 04/12] thunderbolt: apple: trace NHI domain removal --- drivers/thunderbolt/apple.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/thunderbolt/apple.c b/drivers/thunderbolt/apple.c index af9bf8a5e2983a..98e4fcec4b1ac3 100644 --- a/drivers/thunderbolt/apple.c +++ b/drivers/thunderbolt/apple.c @@ -549,8 +549,10 @@ static void apple_nhi_remove(struct platform_device *pdev) { struct apple_nhi *anhi = platform_get_drvdata(pdev); + dev_info(anhi->dev, "removing Thunderbolt domain\n"); tb_domain_remove(anhi->tb); wait_for_completion(&anhi->nhi.domain_released); + dev_info(anhi->dev, "Thunderbolt domain released\n"); } static const struct of_device_id apple_nhi_match[] = { From 052ab23b27a3782cc2f027fc271dae0579103e30 Mon Sep 17 00:00:00 2001 From: Matthew Altman Date: Sun, 30 Aug 2026 18:41:20 -0500 Subject: [PATCH 05/12] thunderbolt: add platform DP source lifecycle hooks Add optional platform callbacks around DP tunnel activation. Track completed phases so rollback only undoes work that succeeded. Platforms without callbacks keep the existing path. This experimental plumbing has no display-routing side effects. It is compile-tested only and has not been hardware-tested. Signed-off-by: Matthew Altman --- drivers/thunderbolt/nhi.h | 23 +++++++++++++ drivers/thunderbolt/tb.c | 4 ++- drivers/thunderbolt/tunnel.c | 66 +++++++++++++++++++++++++++++------- drivers/thunderbolt/tunnel.h | 4 +++ 4 files changed, 84 insertions(+), 13 deletions(-) diff --git a/drivers/thunderbolt/nhi.h b/drivers/thunderbolt/nhi.h index 50fcf330eb6f32..b97b9a5ce8d7e7 100644 --- a/drivers/thunderbolt/nhi.h +++ b/drivers/thunderbolt/nhi.h @@ -11,6 +11,8 @@ #include +struct tb_port; + enum nhi_fw_mode { NHI_FW_SAFE_MODE, NHI_FW_AUTH_MODE, @@ -79,6 +81,19 @@ struct tb_nhi_ring_layout { * the standard USB4 NHI registers are used. * @is_present: Whether the device is currently present on the parent bus * @init_interrupts: NHI specific interrupt initialization hook + * @dp_tunnel_prepare: Optional platform DP source reservation hook. Called + * after DP capabilities are exchanged but before tunnel paths + * are activated. + * @dp_tunnel_enable: Optional platform DP source enable hook. Called after + * both DP adapters are enabled and before DPRX polling starts. + * @dp_tunnel_disable: Optional platform DP source disable hook. Called after + * DPRX polling stops and before the adapters are disabled. + * @dp_tunnel_unprepare: Optional platform DP source release hook. Called + * after the tunnel paths are deactivated. + * + * The DP tunnel hooks run in sleepable context with the Thunderbolt domain + * lock held. They must not wait on work that needs that lock. Enable failures + * are followed by disable/unprepare only for phases that completed. */ struct tb_nhi_ops { int (*init)(struct tb_nhi *nhi); @@ -96,6 +111,14 @@ struct tb_nhi_ops { void (*ring_configure)(struct tb_ring *ring, u32 flags, u32 e2e_flags); bool (*is_present)(struct tb_nhi *nhi); int (*init_interrupts)(struct tb_nhi *nhi); + int (*dp_tunnel_prepare)(struct tb_nhi *nhi, struct tb_port *in, + struct tb_port *out); + int (*dp_tunnel_enable)(struct tb_nhi *nhi, struct tb_port *in, + struct tb_port *out); + void (*dp_tunnel_disable)(struct tb_nhi *nhi, struct tb_port *in, + struct tb_port *out); + void (*dp_tunnel_unprepare)(struct tb_nhi *nhi, struct tb_port *in, + struct tb_port *out); }; /* diff --git a/drivers/thunderbolt/tb.c b/drivers/thunderbolt/tb.c index ddc9141170c111..7d488080018bbf 100644 --- a/drivers/thunderbolt/tb.c +++ b/drivers/thunderbolt/tb.c @@ -2966,7 +2966,9 @@ static void tb_stop(struct tb *tb) */ if (tb_tunnel_is_dma(tunnel)) tb_tunnel_deactivate(tunnel); - else if (tb_tunnel_is_dp(tunnel) && !tb_tunnel_is_active(tunnel)) + else if (tb_tunnel_is_dp(tunnel) && + (!tb_tunnel_is_active(tunnel) || tunnel->dp_source_prepared || + tunnel->dp_source_enabled)) tb_tunnel_deactivate(tunnel); tb_tunnel_put(tunnel); } diff --git a/drivers/thunderbolt/tunnel.c b/drivers/thunderbolt/tunnel.c index e4c2001b745123..8cd78381aa0d4b 100644 --- a/drivers/thunderbolt/tunnel.c +++ b/drivers/thunderbolt/tunnel.c @@ -1019,37 +1019,60 @@ static int tb_dp_pre_activate(struct tb_tunnel *tunnel) struct tb_port *in = tunnel->src_port; struct tb_switch *sw = in->sw; struct tb *tb = in->sw->tb; + const struct tb_nhi_ops *ops = tb->nhi->ops; + bool bw_mode_enabled = false; int ret; ret = tb_dp_xchg_caps(tunnel); if (ret) return ret; - if (!tb_switch_is_usb4(sw)) - return 0; + if (tb_switch_is_usb4(sw) && + usb4_dp_port_bandwidth_mode_supported(in)) { + tb_tunnel_dbg(tunnel, "bandwidth allocation mode supported\n"); - if (!usb4_dp_port_bandwidth_mode_supported(in)) - return 0; + ret = usb4_dp_port_set_cm_id(in, tb->index); + if (ret) + return ret; - tb_tunnel_dbg(tunnel, "bandwidth allocation mode supported\n"); + ret = tb_dp_bandwidth_alloc_mode_enable(tunnel); + if (ret) + return ret; + bw_mode_enabled = true; + } - ret = usb4_dp_port_set_cm_id(in, tb->index); - if (ret) - return ret; + if (ops->dp_tunnel_prepare && !tunnel->dp_source_prepared) { + ret = ops->dp_tunnel_prepare(tb->nhi, tunnel->src_port, + tunnel->dst_port); + if (ret) { + if (bw_mode_enabled) + usb4_dp_port_set_cm_bandwidth_mode_supported(in, false); + return ret; + } + tunnel->dp_source_prepared = true; + } - return tb_dp_bandwidth_alloc_mode_enable(tunnel); + return 0; } static void tb_dp_post_deactivate(struct tb_tunnel *tunnel) { struct tb_port *in = tunnel->src_port; + struct tb *tb = tunnel->tb; + const struct tb_nhi_ops *ops = tb->nhi->ops; - if (!usb4_dp_port_bandwidth_mode_supported(in)) - return; - if (usb4_dp_port_bandwidth_mode_enabled(in)) { + if (usb4_dp_port_bandwidth_mode_supported(in) && + usb4_dp_port_bandwidth_mode_enabled(in)) { usb4_dp_port_set_cm_bandwidth_mode_supported(in, false); tb_tunnel_dbg(tunnel, "bandwidth allocation mode disabled\n"); } + + if (tunnel->dp_source_prepared) { + if (ops->dp_tunnel_unprepare) + ops->dp_tunnel_unprepare(tb->nhi, tunnel->src_port, + tunnel->dst_port); + tunnel->dp_source_prepared = false; + } } static ktime_t dprx_timeout_to_ktime(int timeout_msec) @@ -1171,6 +1194,15 @@ static int tb_dp_activate(struct tb_tunnel *tunnel, bool active) paths[TB_DP_AUX_PATH_OUT]->hops[last].next_hop_index); } else { tb_dp_dprx_stop(tunnel); + if (tunnel->dp_source_enabled) { + const struct tb_nhi_ops *ops = tunnel->tb->nhi->ops; + + if (ops->dp_tunnel_disable) + ops->dp_tunnel_disable(tunnel->tb->nhi, + tunnel->src_port, + tunnel->dst_port); + tunnel->dp_source_enabled = false; + } tb_dp_port_hpd_clear(tunnel->src_port); tb_dp_port_set_hops(tunnel->src_port, 0, 0, 0); if (tb_port_is_dpout(tunnel->dst_port)) @@ -1187,6 +1219,16 @@ static int tb_dp_activate(struct tb_tunnel *tunnel, bool active) return ret; } + if (active && !tunnel->dp_source_enabled && + tunnel->tb->nhi->ops->dp_tunnel_enable) { + ret = tunnel->tb->nhi->ops->dp_tunnel_enable(tunnel->tb->nhi, + tunnel->src_port, + tunnel->dst_port); + if (ret) + return ret; + tunnel->dp_source_enabled = true; + } + return active ? tb_dp_dprx_start(tunnel) : 0; } diff --git a/drivers/thunderbolt/tunnel.h b/drivers/thunderbolt/tunnel.h index 28f49f7e9f5633..b1e5f83c50e655 100644 --- a/drivers/thunderbolt/tunnel.h +++ b/drivers/thunderbolt/tunnel.h @@ -66,6 +66,8 @@ enum tb_tunnel_state { * @dprx_canceled: Was DPRX capabilities read poll canceled * @dprx_timeout: If set DPRX capabilities read poll work will timeout after this passes * @dprx_work: Worker that is scheduled to poll completion of DPRX capabilities read + * @dp_source_prepared: Platform DP source prepare hook completed + * @dp_source_enabled: Platform DP source enable hook completed * @callback: Callback called when DP tunnel is fully activated * @paths: All paths required by the tunnel */ @@ -103,6 +105,8 @@ struct tb_tunnel { bool dprx_canceled; ktime_t dprx_timeout; struct delayed_work dprx_work; + bool dp_source_prepared; + bool dp_source_enabled; void (*callback)(struct tb_tunnel *tunnel); struct tb_path *paths[] __counted_by(npaths); From 5b29ed3b64e5149034cd887ac2eb61a51f268f74 Mon Sep 17 00:00:00 2001 From: Matthew Altman Date: Sun, 30 Aug 2026 18:41:24 -0500 Subject: [PATCH 06/12] thunderbolt: apple: trace DP source lifecycle Log the selected host DP-IN and remote DP-OUT at each phase. The callbacks do not touch DCP, ATC PHY, crossbar, or DT state. T602x DPIN routing remains unproven. This is compile-tested only and does not enable hardware video. Signed-off-by: Matthew Altman --- drivers/thunderbolt/apple.c | 49 +++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/drivers/thunderbolt/apple.c b/drivers/thunderbolt/apple.c index 98e4fcec4b1ac3..2d896d169fdb2b 100644 --- a/drivers/thunderbolt/apple.c +++ b/drivers/thunderbolt/apple.c @@ -381,12 +381,61 @@ static void apple_nhi_ring_configure(struct tb_ring *ring, u32 flags, u32 e2e_fl writel(flags | e2e_flags, options); } +/* + * Experimental lifecycle tracing only. These callbacks deliberately do not + * touch DCP, ATC PHY, display-crossbar, or device-tree state. T602x DPIN + * register programming is not understood well enough to enable a route. + */ +static int apple_nhi_dp_tunnel_prepare(struct tb_nhi *nhi, struct tb_port *in, + struct tb_port *out) +{ + struct apple_nhi *anhi = nhi_to_anhi(nhi); + + dev_info(anhi->dev, + "experimental DP source prepare %llx:%u -> %llx:%u (trace only)\n", + tb_route(in->sw), in->port, tb_route(out->sw), out->port); + return 0; +} + +static int apple_nhi_dp_tunnel_enable(struct tb_nhi *nhi, struct tb_port *in, + struct tb_port *out) +{ + struct apple_nhi *anhi = nhi_to_anhi(nhi); + + dev_info(anhi->dev, + "experimental DP source enable %llx:%u -> %llx:%u (no video side effects)\n", + tb_route(in->sw), in->port, tb_route(out->sw), out->port); + return 0; +} + +static void apple_nhi_dp_tunnel_disable(struct tb_nhi *nhi, struct tb_port *in, + struct tb_port *out) +{ + struct apple_nhi *anhi = nhi_to_anhi(nhi); + + dev_info(anhi->dev, "experimental DP source disable %llx:%u -> %llx:%u\n", + tb_route(in->sw), in->port, tb_route(out->sw), out->port); +} + +static void apple_nhi_dp_tunnel_unprepare(struct tb_nhi *nhi, struct tb_port *in, + struct tb_port *out) +{ + struct apple_nhi *anhi = nhi_to_anhi(nhi); + + dev_info(anhi->dev, "experimental DP source unprepare %llx:%u -> %llx:%u\n", + tb_route(in->sw), in->port, tb_route(out->sw), out->port); +} + static const struct tb_nhi_ops apple_nhi_ops = { .request_ring_irq = apple_nhi_request_irq, .release_ring_irq = apple_nhi_release_irq, .ring_interrupt_active = apple_nhi_ring_interrupt_active, .ring_interrupt_mask = apple_nhi_ring_interrupt_mask, .ring_configure = apple_nhi_ring_configure, + .dp_tunnel_prepare = apple_nhi_dp_tunnel_prepare, + .dp_tunnel_enable = apple_nhi_dp_tunnel_enable, + .dp_tunnel_disable = apple_nhi_dp_tunnel_disable, + .dp_tunnel_unprepare = apple_nhi_dp_tunnel_unprepare, }; static const struct tb_nhi_ring_layout apple_nhi_ring_layout = { From cc81ffa4074e21303c0e0c430bba6f6b939ec7cf Mon Sep 17 00:00:00 2001 From: Matthew Altman Date: Sun, 30 Aug 2026 19:42:58 -0500 Subject: [PATCH 07/12] thunderbolt: apple: queue DP source state asynchronously Move the experimental Apple DP source lifecycle behind an ACIO-owned ordered workqueue. Keep the callbacks limited to scalar reservation state while the worker only traces converged desired state. Use session and token cookies to reject stale teardown, quiesce and flush the manager around ACIO shutdown, and add pure reducer KUnit coverage. This remains an offline trace-only prototype with no DCP, PHY, mux, MMIO, or DT side effects. Signed-off-by: Matthew Altman --- drivers/thunderbolt/Makefile | 1 + drivers/thunderbolt/apple-dp-source-test.c | 189 ++++++++++++++++++++ drivers/thunderbolt/apple-dp-source.h | 143 +++++++++++++++ drivers/thunderbolt/apple.c | 197 +++++++++++++++++++-- drivers/thunderbolt/nhi.h | 23 ++- drivers/thunderbolt/tunnel.c | 12 +- drivers/thunderbolt/tunnel.h | 2 + 7 files changed, 542 insertions(+), 25 deletions(-) create mode 100644 drivers/thunderbolt/apple-dp-source-test.c create mode 100644 drivers/thunderbolt/apple-dp-source.h diff --git a/drivers/thunderbolt/Makefile b/drivers/thunderbolt/Makefile index edcfe58bc8dea7..3c78fbe07b9d16 100644 --- a/drivers/thunderbolt/Makefile +++ b/drivers/thunderbolt/Makefile @@ -9,6 +9,7 @@ thunderbolt-${CONFIG_ACPI} += acpi.o thunderbolt-$(CONFIG_DEBUG_FS) += debugfs.o thunderbolt-$(CONFIG_USB4_CONFIGFS) += configfs.o thunderbolt-${CONFIG_USB4_KUNIT_TEST} += test.o +thunderbolt-${CONFIG_USB4_KUNIT_TEST} += apple-dp-source-test.o CFLAGS_test.o += $(DISABLE_STRUCTLEAK_PLUGIN) thunderbolt_dma_test-${CONFIG_USB4_DMA_TEST} += dma_test.o diff --git a/drivers/thunderbolt/apple-dp-source-test.c b/drivers/thunderbolt/apple-dp-source-test.c new file mode 100644 index 00000000000000..6a75c547d8d0e4 --- /dev/null +++ b/drivers/thunderbolt/apple-dp-source-test.c @@ -0,0 +1,189 @@ +// SPDX-License-Identifier: GPL-2.0 + +#include + +#include "apple-dp-source.h" + +static const struct apple_dp_source_endpoint endpoint_a = { + .in_route = 0, + .out_route = 1, + .in_port = 5, + .out_port = 14, +}; + +static const struct apple_dp_source_endpoint endpoint_b = { + .in_route = 0, + .out_route = 2, + .in_port = 6, + .out_port = 15, +}; + +static void apple_dp_source_test_lifecycle(struct kunit *test) +{ + struct apple_dp_source_cookie cookie; + struct apple_dp_source_state state; + + apple_dp_source_state_init(&state); + KUNIT_EXPECT_EQ(test, apple_dp_source_prepare(&state, &endpoint_a, &cookie), + -ESHUTDOWN); + KUNIT_ASSERT_EQ(test, apple_dp_source_begin_session(&state), 0); + KUNIT_ASSERT_EQ(test, apple_dp_source_prepare(&state, &endpoint_a, &cookie), 0); + KUNIT_EXPECT_EQ(test, state.phase, APPLE_DP_SOURCE_PREPARED); + KUNIT_ASSERT_EQ(test, apple_dp_source_enable(&state, &cookie), 0); + KUNIT_EXPECT_EQ(test, state.phase, APPLE_DP_SOURCE_ENABLED); + KUNIT_EXPECT_EQ(test, apple_dp_source_enable(&state, &cookie), 0); + KUNIT_EXPECT_EQ(test, state.generation, 2ULL); + KUNIT_EXPECT_TRUE(test, apple_dp_source_disable(&state, &cookie)); + KUNIT_EXPECT_EQ(test, state.phase, APPLE_DP_SOURCE_PREPARED); + KUNIT_EXPECT_TRUE(test, apple_dp_source_unprepare(&state, &cookie)); + KUNIT_EXPECT_EQ(test, state.phase, APPLE_DP_SOURCE_IDLE); + KUNIT_EXPECT_EQ(test, state.generation, 4ULL); +} + +static void apple_dp_source_test_conflict_is_unchanged(struct kunit *test) +{ + struct apple_dp_source_cookie cookie_a, cookie_b; + struct apple_dp_source_state state; + u64 generation; + + apple_dp_source_state_init(&state); + KUNIT_ASSERT_EQ(test, apple_dp_source_begin_session(&state), 0); + KUNIT_ASSERT_EQ(test, apple_dp_source_prepare(&state, &endpoint_a, &cookie_a), 0); + generation = state.generation; + KUNIT_EXPECT_EQ(test, apple_dp_source_prepare(&state, &endpoint_b, &cookie_b), + -EBUSY); + KUNIT_EXPECT_EQ(test, apple_dp_source_prepare(&state, &endpoint_a, &cookie_b), + -EBUSY); + KUNIT_EXPECT_EQ(test, state.generation, generation); + KUNIT_EXPECT_TRUE(test, apple_dp_source_cookie_equal(&state.cookie, &cookie_a)); +} + +static void apple_dp_source_test_stale_release_is_ignored(struct kunit *test) +{ + struct apple_dp_source_cookie old_cookie, new_cookie; + struct apple_dp_source_state state; + + apple_dp_source_state_init(&state); + KUNIT_ASSERT_EQ(test, apple_dp_source_begin_session(&state), 0); + KUNIT_ASSERT_EQ(test, + apple_dp_source_prepare(&state, &endpoint_a, &old_cookie), 0); + KUNIT_ASSERT_TRUE(test, apple_dp_source_unprepare(&state, &old_cookie)); + KUNIT_ASSERT_EQ(test, + apple_dp_source_prepare(&state, &endpoint_a, &new_cookie), 0); + KUNIT_ASSERT_EQ(test, apple_dp_source_enable(&state, &new_cookie), 0); + KUNIT_EXPECT_FALSE(test, apple_dp_source_disable(&state, &old_cookie)); + KUNIT_EXPECT_FALSE(test, apple_dp_source_unprepare(&state, &old_cookie)); + KUNIT_EXPECT_EQ(test, state.phase, APPLE_DP_SOURCE_ENABLED); + KUNIT_EXPECT_TRUE(test, apple_dp_source_cookie_equal(&state.cookie, &new_cookie)); +} + +static void apple_dp_source_test_coalesces_to_idle(struct kunit *test) +{ + struct apple_dp_source_cookie cookie; + struct apple_dp_source_state state; + + apple_dp_source_state_init(&state); + KUNIT_ASSERT_EQ(test, apple_dp_source_begin_session(&state), 0); + KUNIT_EXPECT_EQ(test, apple_dp_source_begin_session(&state), -EBUSY); + KUNIT_ASSERT_EQ(test, apple_dp_source_prepare(&state, &endpoint_a, &cookie), 0); + KUNIT_ASSERT_TRUE(test, apple_dp_source_unprepare(&state, &cookie)); + KUNIT_EXPECT_EQ(test, state.phase, APPLE_DP_SOURCE_IDLE); + KUNIT_EXPECT_EQ(test, state.generation, 2ULL); +} + +static void apple_dp_source_test_invalid_phase_and_direct_release(struct kunit *test) +{ + struct apple_dp_source_cookie cookie = {}; + struct apple_dp_source_state state; + + apple_dp_source_state_init(&state); + KUNIT_ASSERT_EQ(test, apple_dp_source_begin_session(&state), 0); + KUNIT_EXPECT_EQ(test, apple_dp_source_enable(&state, &cookie), -EINVAL); + KUNIT_ASSERT_EQ(test, apple_dp_source_prepare(&state, &endpoint_a, &cookie), 0); + KUNIT_ASSERT_EQ(test, apple_dp_source_enable(&state, &cookie), 0); + KUNIT_EXPECT_TRUE(test, apple_dp_source_unprepare(&state, &cookie)); + KUNIT_EXPECT_EQ(test, state.phase, APPLE_DP_SOURCE_IDLE); +} + +static void apple_dp_source_test_quiesce_invalidates_cookie(struct kunit *test) +{ + struct apple_dp_source_cookie cookie, ignored; + struct apple_dp_source_state state; + + apple_dp_source_state_init(&state); + KUNIT_ASSERT_EQ(test, apple_dp_source_begin_session(&state), 0); + KUNIT_ASSERT_EQ(test, apple_dp_source_prepare(&state, &endpoint_a, &cookie), 0); + KUNIT_EXPECT_TRUE(test, apple_dp_source_quiesce(&state)); + KUNIT_EXPECT_EQ(test, state.phase, APPLE_DP_SOURCE_IDLE); + KUNIT_EXPECT_EQ(test, state.endpoint.in_port, 0); + KUNIT_EXPECT_EQ(test, state.endpoint.out_port, 0); + KUNIT_EXPECT_EQ(test, apple_dp_source_enable(&state, &cookie), -ESTALE); + KUNIT_EXPECT_EQ(test, apple_dp_source_prepare(&state, &endpoint_a, &ignored), + -ESHUTDOWN); +} + +static void apple_dp_source_test_session_epoch(struct kunit *test) +{ + struct apple_dp_source_cookie old_cookie, new_cookie; + struct apple_dp_source_state state; + + apple_dp_source_state_init(&state); + KUNIT_ASSERT_EQ(test, apple_dp_source_begin_session(&state), 0); + KUNIT_ASSERT_EQ(test, + apple_dp_source_prepare(&state, &endpoint_a, &old_cookie), 0); + apple_dp_source_quiesce(&state); + KUNIT_ASSERT_EQ(test, apple_dp_source_begin_session(&state), 0); + KUNIT_ASSERT_EQ(test, + apple_dp_source_prepare(&state, &endpoint_a, &new_cookie), 0); + KUNIT_EXPECT_NE(test, old_cookie.session, new_cookie.session); + KUNIT_EXPECT_FALSE(test, apple_dp_source_unprepare(&state, &old_cookie)); +} + +static void apple_dp_source_test_generation_wrap(struct kunit *test) +{ + struct apple_dp_source_cookie cookie; + struct apple_dp_source_state state; + + apple_dp_source_state_init(&state); + KUNIT_ASSERT_EQ(test, apple_dp_source_begin_session(&state), 0); + state.generation = ~0ULL; + KUNIT_ASSERT_EQ(test, apple_dp_source_prepare(&state, &endpoint_a, &cookie), 0); + KUNIT_EXPECT_EQ(test, state.generation, 0ULL); +} + +static void apple_dp_source_test_identity_wrap(struct kunit *test) +{ + struct apple_dp_source_cookie cookie; + struct apple_dp_source_state state; + + apple_dp_source_state_init(&state); + state.session = ~0ULL; + state.next_token = ~0ULL; + KUNIT_ASSERT_EQ(test, apple_dp_source_begin_session(&state), 0); + KUNIT_EXPECT_EQ(test, state.session, 1ULL); + KUNIT_ASSERT_EQ(test, apple_dp_source_prepare(&state, &endpoint_a, &cookie), 0); + KUNIT_EXPECT_EQ(test, cookie.token, ~0ULL); + KUNIT_ASSERT_TRUE(test, apple_dp_source_unprepare(&state, &cookie)); + KUNIT_ASSERT_EQ(test, apple_dp_source_prepare(&state, &endpoint_a, &cookie), 0); + KUNIT_EXPECT_EQ(test, cookie.token, 1ULL); +} + +static struct kunit_case apple_dp_source_test_cases[] = { + KUNIT_CASE(apple_dp_source_test_lifecycle), + KUNIT_CASE(apple_dp_source_test_conflict_is_unchanged), + KUNIT_CASE(apple_dp_source_test_stale_release_is_ignored), + KUNIT_CASE(apple_dp_source_test_coalesces_to_idle), + KUNIT_CASE(apple_dp_source_test_invalid_phase_and_direct_release), + KUNIT_CASE(apple_dp_source_test_quiesce_invalidates_cookie), + KUNIT_CASE(apple_dp_source_test_session_epoch), + KUNIT_CASE(apple_dp_source_test_generation_wrap), + KUNIT_CASE(apple_dp_source_test_identity_wrap), + {} +}; + +static struct kunit_suite apple_dp_source_test_suite = { + .name = "apple-dp-source", + .test_cases = apple_dp_source_test_cases, +}; + +kunit_test_suite(apple_dp_source_test_suite); diff --git a/drivers/thunderbolt/apple-dp-source.h b/drivers/thunderbolt/apple-dp-source.h new file mode 100644 index 00000000000000..63f68486bc93ab --- /dev/null +++ b/drivers/thunderbolt/apple-dp-source.h @@ -0,0 +1,143 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef APPLE_DP_SOURCE_H_ +#define APPLE_DP_SOURCE_H_ + +#include +#include + +enum apple_dp_source_phase { + APPLE_DP_SOURCE_IDLE, + APPLE_DP_SOURCE_PREPARED, + APPLE_DP_SOURCE_ENABLED, +}; + +struct apple_dp_source_endpoint { + u64 in_route; + u64 out_route; + u8 in_port; + u8 out_port; +}; + +struct apple_dp_source_cookie { + u64 session; + u64 token; +}; + +struct apple_dp_source_state { + struct apple_dp_source_endpoint endpoint; + struct apple_dp_source_cookie cookie; + enum apple_dp_source_phase phase; + u64 generation; + u64 session; + u64 next_token; + bool quiescing; +}; + +static inline bool +apple_dp_source_cookie_equal(const struct apple_dp_source_cookie *a, + const struct apple_dp_source_cookie *b) +{ + return a->session == b->session && a->token == b->token; +} + +static inline void apple_dp_source_state_init(struct apple_dp_source_state *state) +{ + *state = (struct apple_dp_source_state) { + .next_token = 1, + .quiescing = true, + }; +} + +static inline int apple_dp_source_begin_session(struct apple_dp_source_state *state) +{ + if (!state->quiescing || state->phase != APPLE_DP_SOURCE_IDLE) + return -EBUSY; + + state->session++; + if (!state->session) + state->session++; + state->quiescing = false; + return 0; +} + +static inline int +apple_dp_source_prepare(struct apple_dp_source_state *state, + const struct apple_dp_source_endpoint *endpoint, + struct apple_dp_source_cookie *cookie) +{ + if (state->quiescing) + return -ESHUTDOWN; + if (state->phase != APPLE_DP_SOURCE_IDLE) + return -EBUSY; + + state->endpoint = *endpoint; + state->cookie.session = state->session; + state->cookie.token = state->next_token++; + if (!state->next_token) + state->next_token++; + state->phase = APPLE_DP_SOURCE_PREPARED; + state->generation++; + *cookie = state->cookie; + + return 0; +} + +static inline int +apple_dp_source_enable(struct apple_dp_source_state *state, + const struct apple_dp_source_cookie *cookie) +{ + if (!apple_dp_source_cookie_equal(&state->cookie, cookie)) + return -ESTALE; + if (state->phase == APPLE_DP_SOURCE_ENABLED) + return 0; + if (state->phase != APPLE_DP_SOURCE_PREPARED) + return -EINVAL; + + state->phase = APPLE_DP_SOURCE_ENABLED; + state->generation++; + return 0; +} + +static inline bool +apple_dp_source_disable(struct apple_dp_source_state *state, + const struct apple_dp_source_cookie *cookie) +{ + if (!apple_dp_source_cookie_equal(&state->cookie, cookie) || + state->phase != APPLE_DP_SOURCE_ENABLED) + return false; + + state->phase = APPLE_DP_SOURCE_PREPARED; + state->generation++; + return true; +} + +static inline bool +apple_dp_source_unprepare(struct apple_dp_source_state *state, + const struct apple_dp_source_cookie *cookie) +{ + if (!apple_dp_source_cookie_equal(&state->cookie, cookie) || + state->phase == APPLE_DP_SOURCE_IDLE) + return false; + + state->phase = APPLE_DP_SOURCE_IDLE; + state->endpoint = (struct apple_dp_source_endpoint) {}; + state->cookie = (struct apple_dp_source_cookie) {}; + state->generation++; + return true; +} + +static inline bool apple_dp_source_quiesce(struct apple_dp_source_state *state) +{ + bool changed = state->phase != APPLE_DP_SOURCE_IDLE; + + state->quiescing = true; + state->phase = APPLE_DP_SOURCE_IDLE; + state->endpoint = (struct apple_dp_source_endpoint) {}; + state->cookie = (struct apple_dp_source_cookie) {}; + if (changed) + state->generation++; + + return changed; +} + +#endif diff --git a/drivers/thunderbolt/apple.c b/drivers/thunderbolt/apple.c index 2d896d169fdb2b..8b7ad358605d57 100644 --- a/drivers/thunderbolt/apple.c +++ b/drivers/thunderbolt/apple.c @@ -70,7 +70,9 @@ #include #include #include +#include +#include "apple-dp-source.h" #include "nhi.h" #include "tb.h" @@ -132,6 +134,14 @@ struct apple_cio { int nhi_boot_status; struct typec_thunderbolt_switch_dev *tbt_switch; + + struct { + spinlock_t lock; /* protects desired and applied */ + struct apple_dp_source_state desired; + struct apple_dp_source_state applied; + struct work_struct work; + struct workqueue_struct *wq; + } dp_source; }; struct apple_nhi { @@ -156,6 +166,89 @@ struct apple_nhi { #define nhi_to_anhi(nhi_) container_of((nhi_), struct apple_nhi, nhi) +static const char *apple_dp_source_phase_name(enum apple_dp_source_phase phase) +{ + switch (phase) { + case APPLE_DP_SOURCE_IDLE: + return "idle"; + case APPLE_DP_SOURCE_PREPARED: + return "prepared"; + case APPLE_DP_SOURCE_ENABLED: + return "enabled"; + } + + return "unknown"; +} + +static void apple_dp_source_work(struct work_struct *work) +{ + struct apple_cio *acio = container_of(work, struct apple_cio, dp_source.work); + struct apple_dp_source_state desired; + struct apple_dp_source_state applied; + unsigned long flags; + bool stable; + + /* + * This worker must never acquire acio->lock or a Thunderbolt lock. It is + * flushed while ACIO teardown holds acio->lock, after the NHI is gone. + */ + do { + spin_lock_irqsave(&acio->dp_source.lock, flags); + desired = acio->dp_source.desired; + applied = acio->dp_source.applied; + spin_unlock_irqrestore(&acio->dp_source.lock, flags); + + dev_info(acio->dev, + "experimental DP source apply %s -> %s generation %llu, session %llu, token %llu, %llx:%u -> %llx:%u (trace only)\n", + apple_dp_source_phase_name(applied.phase), + apple_dp_source_phase_name(desired.phase), desired.generation, + desired.cookie.session, desired.cookie.token, + desired.endpoint.in_route, desired.endpoint.in_port, + desired.endpoint.out_route, desired.endpoint.out_port); + + spin_lock_irqsave(&acio->dp_source.lock, flags); + acio->dp_source.applied = desired; + stable = desired.generation == acio->dp_source.desired.generation; + spin_unlock_irqrestore(&acio->dp_source.lock, flags); + } while (!stable); +} + +static void apple_dp_source_queue(struct apple_cio *acio) +{ + queue_work(acio->dp_source.wq, &acio->dp_source.work); +} + +static int apple_dp_source_start(struct apple_cio *acio) +{ + unsigned long flags; + int ret; + + /* The first accepted prepare publishes the new session to the worker. */ + spin_lock_irqsave(&acio->dp_source.lock, flags); + ret = apple_dp_source_begin_session(&acio->dp_source.desired); + spin_unlock_irqrestore(&acio->dp_source.lock, flags); + return ret; +} + +static void apple_dp_source_stop(struct apple_cio *acio) +{ + unsigned long flags; + bool changed; + + spin_lock_irqsave(&acio->dp_source.lock, flags); + changed = apple_dp_source_quiesce(&acio->dp_source.desired); + spin_unlock_irqrestore(&acio->dp_source.lock, flags); + if (changed) + apple_dp_source_queue(acio); +} + +static void apple_dp_source_destroy(void *data) +{ + struct apple_cio *acio = data; + + destroy_workqueue(acio->dp_source.wq); +} + static int apple_cio_rtkit_shmem_setup(void *cookie, struct apple_rtkit_shmem *bfr) { struct apple_cio *acio = cookie; @@ -387,43 +480,96 @@ static void apple_nhi_ring_configure(struct tb_ring *ring, u32 flags, u32 e2e_fl * register programming is not understood well enough to enable a route. */ static int apple_nhi_dp_tunnel_prepare(struct tb_nhi *nhi, struct tb_port *in, - struct tb_port *out) + struct tb_port *out, + struct tb_dp_source_cookie *cookie) { struct apple_nhi *anhi = nhi_to_anhi(nhi); + struct apple_cio *acio = anhi->acio; + struct apple_dp_source_endpoint endpoint = { + .in_route = tb_route(in->sw), + .out_route = tb_route(out->sw), + .in_port = in->port, + .out_port = out->port, + }; + struct apple_dp_source_cookie apple_cookie; + unsigned long flags; + int ret; + spin_lock_irqsave(&acio->dp_source.lock, flags); + ret = apple_dp_source_prepare(&acio->dp_source.desired, &endpoint, + &apple_cookie); + spin_unlock_irqrestore(&acio->dp_source.lock, flags); + if (ret) + return ret; + + cookie->session = apple_cookie.session; + cookie->token = apple_cookie.token; dev_info(anhi->dev, - "experimental DP source prepare %llx:%u -> %llx:%u (trace only)\n", - tb_route(in->sw), in->port, tb_route(out->sw), out->port); + "experimental DP source reserved session %llu token %llu (trace only)\n", + cookie->session, cookie->token); + apple_dp_source_queue(acio); return 0; } static int apple_nhi_dp_tunnel_enable(struct tb_nhi *nhi, struct tb_port *in, - struct tb_port *out) + struct tb_port *out, + const struct tb_dp_source_cookie *cookie) { struct apple_nhi *anhi = nhi_to_anhi(nhi); + struct apple_dp_source_cookie apple_cookie = { + .session = cookie->session, + .token = cookie->token, + }; + unsigned long flags; + int ret; - dev_info(anhi->dev, - "experimental DP source enable %llx:%u -> %llx:%u (no video side effects)\n", - tb_route(in->sw), in->port, tb_route(out->sw), out->port); - return 0; + spin_lock_irqsave(&anhi->acio->dp_source.lock, flags); + ret = apple_dp_source_enable(&anhi->acio->dp_source.desired, + &apple_cookie); + spin_unlock_irqrestore(&anhi->acio->dp_source.lock, flags); + if (!ret) + apple_dp_source_queue(anhi->acio); + return ret; } static void apple_nhi_dp_tunnel_disable(struct tb_nhi *nhi, struct tb_port *in, - struct tb_port *out) + struct tb_port *out, + const struct tb_dp_source_cookie *cookie) { struct apple_nhi *anhi = nhi_to_anhi(nhi); - - dev_info(anhi->dev, "experimental DP source disable %llx:%u -> %llx:%u\n", - tb_route(in->sw), in->port, tb_route(out->sw), out->port); + struct apple_dp_source_cookie apple_cookie = { + .session = cookie->session, + .token = cookie->token, + }; + unsigned long flags; + bool changed; + + spin_lock_irqsave(&anhi->acio->dp_source.lock, flags); + changed = apple_dp_source_disable(&anhi->acio->dp_source.desired, + &apple_cookie); + spin_unlock_irqrestore(&anhi->acio->dp_source.lock, flags); + if (changed) + apple_dp_source_queue(anhi->acio); } static void apple_nhi_dp_tunnel_unprepare(struct tb_nhi *nhi, struct tb_port *in, - struct tb_port *out) + struct tb_port *out, + const struct tb_dp_source_cookie *cookie) { struct apple_nhi *anhi = nhi_to_anhi(nhi); - - dev_info(anhi->dev, "experimental DP source unprepare %llx:%u -> %llx:%u\n", - tb_route(in->sw), in->port, tb_route(out->sw), out->port); + struct apple_dp_source_cookie apple_cookie = { + .session = cookie->session, + .token = cookie->token, + }; + unsigned long flags; + bool changed; + + spin_lock_irqsave(&anhi->acio->dp_source.lock, flags); + changed = apple_dp_source_unprepare(&anhi->acio->dp_source.desired, + &apple_cookie); + spin_unlock_irqrestore(&anhi->acio->dp_source.lock, flags); + if (changed) + apple_dp_source_queue(anhi->acio); } static const struct tb_nhi_ops apple_nhi_ops = { @@ -628,6 +774,7 @@ static void apple_cio_stop(struct apple_cio *acio) lockdep_assert_held(&acio->lock); dev_info(acio->dev, "stopping ACIO: current cable info 0x%x, target 0x%x\n", acio->current_cable_info, acio->target_cable_info); + apple_dp_source_stop(acio); /* * First, shutdown the blocks inside the ACIO complex, like the NHI and the IOMMU. @@ -635,6 +782,7 @@ static void apple_cio_stop(struct apple_cio *acio) * the MMIO space of these so make sure nothing tries to do just that. */ of_platform_depopulate(acio->dev); + flush_workqueue(acio->dp_source.wq); dev_info(acio->dev, "ACIO child devices stopped\n"); /* Try to shut down and power off the co-processor gracefully */ @@ -708,6 +856,9 @@ static int apple_cio_start(struct apple_cio *acio) } apple_tunable_apply(acio->rc_base, acio->rc_tunable); + ret = apple_dp_source_start(acio); + if (ret) + goto err_depopulate; /* * Bring up devices which are part of ACIO and are now accessible by the main SoC @@ -735,7 +886,9 @@ static int apple_cio_start(struct apple_cio *acio) return 0; err_depopulate: + apple_dp_source_stop(acio); of_platform_depopulate(acio->dev); + flush_workqueue(acio->dp_source.wq); err_shutdown_rtkit: /* Ignore errors here since we're about to cut power to the entire block anyway */ apple_rtkit_poweroff(acio->rtk); @@ -843,6 +996,16 @@ static int apple_cio_probe(struct platform_device *pdev) init_completion(&acio->nhi_boot_completion); acio->dev = &pdev->dev; acio->np = dev->of_node; + spin_lock_init(&acio->dp_source.lock); + apple_dp_source_state_init(&acio->dp_source.desired); + apple_dp_source_state_init(&acio->dp_source.applied); + INIT_WORK(&acio->dp_source.work, apple_dp_source_work); + acio->dp_source.wq = alloc_ordered_workqueue("apple_cio_dp", 0); + if (!acio->dp_source.wq) + return -ENOMEM; + ret = devm_add_action_or_reset(dev, apple_dp_source_destroy, acio); + if (ret) + return ret; acio->sram_res = platform_get_resource_byname(pdev, IORESOURCE_MEM, "sram"); if (!acio->sram_res) @@ -911,6 +1074,8 @@ static void apple_cio_remove(struct platform_device *pdev) guard(mutex)(&acio->lock); if (acio->current_cable_info) apple_cio_stop(acio); + else + flush_workqueue(acio->dp_source.wq); } static const struct of_device_id apple_acio_match[] = { diff --git a/drivers/thunderbolt/nhi.h b/drivers/thunderbolt/nhi.h index b97b9a5ce8d7e7..0e71c24fc09330 100644 --- a/drivers/thunderbolt/nhi.h +++ b/drivers/thunderbolt/nhi.h @@ -13,6 +13,11 @@ struct tb_port; +struct tb_dp_source_cookie { + u64 session; + u64 token; +}; + enum nhi_fw_mode { NHI_FW_SAFE_MODE, NHI_FW_AUTH_MODE, @@ -93,7 +98,11 @@ struct tb_nhi_ring_layout { * * The DP tunnel hooks run in sleepable context with the Thunderbolt domain * lock held. They must not wait on work that needs that lock. Enable failures - * are followed by disable/unprepare only for phases that completed. + * are followed by disable/unprepare only for phases that completed. Prepare + * fills a provider cookie that identifies the reservation; all later phases + * receive the same cookie so stale teardown cannot release a newer source. + * Providers that implement enable, disable, or unprepare must also implement + * prepare. */ struct tb_nhi_ops { int (*init)(struct tb_nhi *nhi); @@ -112,13 +121,17 @@ struct tb_nhi_ops { bool (*is_present)(struct tb_nhi *nhi); int (*init_interrupts)(struct tb_nhi *nhi); int (*dp_tunnel_prepare)(struct tb_nhi *nhi, struct tb_port *in, - struct tb_port *out); + struct tb_port *out, + struct tb_dp_source_cookie *cookie); int (*dp_tunnel_enable)(struct tb_nhi *nhi, struct tb_port *in, - struct tb_port *out); + struct tb_port *out, + const struct tb_dp_source_cookie *cookie); void (*dp_tunnel_disable)(struct tb_nhi *nhi, struct tb_port *in, - struct tb_port *out); + struct tb_port *out, + const struct tb_dp_source_cookie *cookie); void (*dp_tunnel_unprepare)(struct tb_nhi *nhi, struct tb_port *in, - struct tb_port *out); + struct tb_port *out, + const struct tb_dp_source_cookie *cookie); }; /* diff --git a/drivers/thunderbolt/tunnel.c b/drivers/thunderbolt/tunnel.c index 8cd78381aa0d4b..6c5b61763da91f 100644 --- a/drivers/thunderbolt/tunnel.c +++ b/drivers/thunderbolt/tunnel.c @@ -1043,7 +1043,8 @@ static int tb_dp_pre_activate(struct tb_tunnel *tunnel) if (ops->dp_tunnel_prepare && !tunnel->dp_source_prepared) { ret = ops->dp_tunnel_prepare(tb->nhi, tunnel->src_port, - tunnel->dst_port); + tunnel->dst_port, + &tunnel->dp_source_cookie); if (ret) { if (bw_mode_enabled) usb4_dp_port_set_cm_bandwidth_mode_supported(in, false); @@ -1070,7 +1071,8 @@ static void tb_dp_post_deactivate(struct tb_tunnel *tunnel) if (tunnel->dp_source_prepared) { if (ops->dp_tunnel_unprepare) ops->dp_tunnel_unprepare(tb->nhi, tunnel->src_port, - tunnel->dst_port); + tunnel->dst_port, + &tunnel->dp_source_cookie); tunnel->dp_source_prepared = false; } } @@ -1200,7 +1202,8 @@ static int tb_dp_activate(struct tb_tunnel *tunnel, bool active) if (ops->dp_tunnel_disable) ops->dp_tunnel_disable(tunnel->tb->nhi, tunnel->src_port, - tunnel->dst_port); + tunnel->dst_port, + &tunnel->dp_source_cookie); tunnel->dp_source_enabled = false; } tb_dp_port_hpd_clear(tunnel->src_port); @@ -1223,7 +1226,8 @@ static int tb_dp_activate(struct tb_tunnel *tunnel, bool active) tunnel->tb->nhi->ops->dp_tunnel_enable) { ret = tunnel->tb->nhi->ops->dp_tunnel_enable(tunnel->tb->nhi, tunnel->src_port, - tunnel->dst_port); + tunnel->dst_port, + &tunnel->dp_source_cookie); if (ret) return ret; tunnel->dp_source_enabled = true; diff --git a/drivers/thunderbolt/tunnel.h b/drivers/thunderbolt/tunnel.h index b1e5f83c50e655..4a26bb009bdc12 100644 --- a/drivers/thunderbolt/tunnel.h +++ b/drivers/thunderbolt/tunnel.h @@ -68,6 +68,7 @@ enum tb_tunnel_state { * @dprx_work: Worker that is scheduled to poll completion of DPRX capabilities read * @dp_source_prepared: Platform DP source prepare hook completed * @dp_source_enabled: Platform DP source enable hook completed + * @dp_source_cookie: Provider reservation identity for DP source callbacks * @callback: Callback called when DP tunnel is fully activated * @paths: All paths required by the tunnel */ @@ -107,6 +108,7 @@ struct tb_tunnel { struct delayed_work dprx_work; bool dp_source_prepared; bool dp_source_enabled; + struct tb_dp_source_cookie dp_source_cookie; void (*callback)(struct tb_tunnel *tunnel); struct tb_path *paths[] __counted_by(npaths); From 7dc02de0183f1a5592a06665e53f65e481bb285c Mon Sep 17 00:00:00 2001 From: Matthew Altman Date: Sun, 30 Aug 2026 20:26:48 -0500 Subject: [PATCH 08/12] drm/apple: add DPTX transport seam Move the existing physical PHY operations behind per-port transport ops and install them before publishing each DPTX service. Preserve the native APCALL behavior while separating physical lane, rate, activation, and deactivation handling. Add an unreachable logical USB4 model, remote-target helpers, and KUnit coverage. The logical model only records scalar link state and contains no PHY, Type-C, mux, MMIO, DT, or Thunderbolt operations. No production path selects it, and no DCP connect or timeout behavior changes. Reject out-of-range remote target fields instead of silently masking them. This remains an offline compile-tested seam, not a video-producing route. Signed-off-by: Matthew Altman --- drivers/gpu/drm/apple/Kconfig | 11 ++ drivers/gpu/drm/apple/Makefile | 4 +- drivers/gpu/drm/apple/dptx-transport-phy.c | 117 +++++++++++++ drivers/gpu/drm/apple/dptx-transport-test.c | 184 ++++++++++++++++++++ drivers/gpu/drm/apple/dptx-transport.c | 165 ++++++++++++++++++ drivers/gpu/drm/apple/dptx-transport.h | 74 ++++++++ drivers/gpu/drm/apple/dptxep.c | 153 +++++++--------- drivers/gpu/drm/apple/dptxep.h | 15 +- 8 files changed, 623 insertions(+), 100 deletions(-) create mode 100644 drivers/gpu/drm/apple/dptx-transport-phy.c create mode 100644 drivers/gpu/drm/apple/dptx-transport-test.c create mode 100644 drivers/gpu/drm/apple/dptx-transport.c create mode 100644 drivers/gpu/drm/apple/dptx-transport.h diff --git a/drivers/gpu/drm/apple/Kconfig b/drivers/gpu/drm/apple/Kconfig index ecf7afd76cedaf..c6cbc4c4baa99e 100644 --- a/drivers/gpu/drm/apple/Kconfig +++ b/drivers/gpu/drm/apple/Kconfig @@ -28,3 +28,14 @@ config DRM_APPLE_DEBUG bool "Enable additional driver debugging" depends on DRM_APPLE depends on EXPERT # only for developers + +config DRM_APPLE_KUNIT_TEST + bool "KUnit tests for Apple DRM" if !KUNIT_ALL_TESTS + depends on DRM_APPLE && KUNIT=y + default KUNIT_ALL_TESTS + help + This builds unit tests for Apple DPTX transport-independent logic, + including remote-target encoding and the side-effect-free logical + USB4 transport model. These tests are intended for kernel developers. + + If in doubt, say "N". diff --git a/drivers/gpu/drm/apple/Makefile b/drivers/gpu/drm/apple/Makefile index b0a7dbfbcaca83..486cf4b303b091 100644 --- a/drivers/gpu/drm/apple/Makefile +++ b/drivers/gpu/drm/apple/Makefile @@ -3,7 +3,9 @@ CFLAGS_trace.o = -I$(src) appledrm-y := apple_drv.o -appledrm-y += afk.o dcp.o dcp_backlight.o dptxep.o iomfb.o parser.o systemep.o +appledrm-y += afk.o dcp.o dcp_backlight.o dptxep.o dptx-transport.o +appledrm-y += dptx-transport-phy.o iomfb.o parser.o systemep.o +appledrm-$(CONFIG_DRM_APPLE_KUNIT_TEST) += dptx-transport-test.o appledrm-$(CONFIG_DRM_APPLE_AUDIO) += audio.o appledrm-$(CONFIG_DRM_APPLE_AUDIO) += av.o appledrm-y += connector.o diff --git a/drivers/gpu/drm/apple/dptx-transport-phy.c b/drivers/gpu/drm/apple/dptx-transport-phy.c new file mode 100644 index 00000000000000..0c1740d9c4dfc9 --- /dev/null +++ b/drivers/gpu/drm/apple/dptx-transport-phy.c @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* Copyright 2026 Matthew Altman */ + +#include + +#include "dcp-internal.h" +#include "dptx-transport.h" + +static int apple_dptx_physical_get_max_lane_count(struct dptx_port *dptx, + struct apple_dcp *dcp, + u32 *lane_count) +{ + union phy_configure_opts phy_ops; + int ret; + + ret = phy_validate(dptx->atcphy, PHY_MODE_DP, 0, &phy_ops); + if (ret < 0) { + dev_err(dcp->dev, "phy_validate failed: %d\n", ret); + return ret; + } + + if (phy_ops.dp.lanes < 2) { + /* phy_validate can report zero until the ATC PHY enters DP mode. */ + dev_dbg(dcp->dev, "get_max_lane_count: phy lanes: %d\n", + phy_ops.dp.lanes); + *lane_count = 4; + } else { + *lane_count = phy_ops.dp.lanes; + } + dptx->lane_count = *lane_count; + + return 0; +} + +static int apple_dptx_physical_set_active_lane_count(struct dptx_port *dptx, + struct apple_dcp *dcp, + u32 lane_count) +{ + dptx->phy_ops.dp.lanes = lane_count; + /* Standalone DPTX PHYs require explicit lane programming. */ + dptx->phy_ops.dp.set_lanes = dcp->dptx_phy > 3; + if (!dptx->phy_ops.dp.set_lanes) + return 0; + + if (dptx->atcphy) { + int ret = phy_configure(dptx->atcphy, &dptx->phy_ops); + + if (ret) + return ret; + } + dptx->phy_ops.dp.set_lanes = 0; + dptx->lane_count = lane_count; + + return 0; +} + +static int apple_dptx_physical_get_max_link_rate(struct dptx_port *dptx, + struct apple_dcp *dcp, + u32 *link_rate) +{ + *link_rate = LINK_RATE_HBR3; + return 0; +} + +static int apple_dptx_physical_set_link_rate(struct dptx_port *dptx, + struct apple_dcp *dcp, + u32 link_rate) +{ + u32 phy_link_rate; + int ret; + + ret = apple_dptx_rate_to_mbps(link_rate, &phy_link_rate); + if (ret) { + dev_err(dcp->dev, + "DPTXPort: Unsupported link rate 0x%x requested\n", + link_rate); + return ret; + } + + dptx->phy_ops.dp.link_rate = phy_link_rate; + dptx->phy_ops.dp.set_rate = 1; + if (dptx->atcphy) { + ret = phy_configure(dptx->atcphy, &dptx->phy_ops); + if (ret) + return ret; + } + dptx->link_rate = link_rate; + dptx->pending_link_rate = link_rate; + + return 0; +} + +static int apple_dptx_physical_activate(struct dptx_port *dptx, + struct apple_dcp *dcp) +{ + /* Standalone DPTX PHYs use the mode argument to select the DCP input. */ + if (!dcp->typec_mux) + phy_set_mode_ext(dptx->atcphy, PHY_MODE_DP, dcp->index); + + return 0; +} + +static int apple_dptx_physical_deactivate(struct dptx_port *dptx, + struct apple_dcp *dcp) +{ + phy_set_mode_ext(dptx->atcphy, PHY_MODE_INVALID, 0); + return 0; +} + +const struct apple_dptx_transport_ops apple_dptx_physical_ops = { + .get_max_lane_count = apple_dptx_physical_get_max_lane_count, + .set_active_lane_count = apple_dptx_physical_set_active_lane_count, + .get_max_link_rate = apple_dptx_physical_get_max_link_rate, + .set_link_rate = apple_dptx_physical_set_link_rate, + .activate = apple_dptx_physical_activate, + .deactivate = apple_dptx_physical_deactivate, +}; diff --git a/drivers/gpu/drm/apple/dptx-transport-test.c b/drivers/gpu/drm/apple/dptx-transport-test.c new file mode 100644 index 00000000000000..ffaa78326d6c42 --- /dev/null +++ b/drivers/gpu/drm/apple/dptx-transport-test.c @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +#include + +#include "dptxep.h" + +static void apple_dptx_target_round_trip_test(struct kunit *test) +{ + const struct apple_dptx_target fixtures[] = { + { .core = 0, .atc = 0, .die = 0 }, + { .core = 0, .atc = 1, .die = 0 }, + /* Public traces observed this tuple; it is not J414c policy. */ + { .core = 0, .atc = 2, .die = 0 }, + { .core = 15, .atc = 15, .die = 15 }, + }; + struct apple_dptx_target decoded; + u32 encoded; + int i, ret; + + for (i = 0; i < ARRAY_SIZE(fixtures); i++) { + ret = apple_dptx_target_encode(&fixtures[i], &encoded); + KUNIT_ASSERT_EQ(test, ret, 0); + ret = apple_dptx_target_decode(encoded, &decoded); + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, decoded.core, fixtures[i].core); + KUNIT_EXPECT_EQ(test, decoded.atc, fixtures[i].atc); + KUNIT_EXPECT_EQ(test, decoded.die, fixtures[i].die); + } +} + +static void apple_dptx_target_rejects_invalid_test(struct kunit *test) +{ + struct apple_dptx_target target = { .core = 16 }; + u32 invalid = BIT(14) | DCPDPTX_REMOTE_PORT_CONNECTED; + u32 encoded; + int ret; + + ret = apple_dptx_target_encode(&target, &encoded); + KUNIT_EXPECT_EQ(test, ret, -ERANGE); + ret = apple_dptx_target_decode(invalid, &target); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + KUNIT_EXPECT_EQ(test, apple_dptx_target_decode(0, &target), -EINVAL); +} + +static void apple_dptx_rate_mapping_test(struct kunit *test) +{ + static const struct { + u32 rate; + u32 mbps; + } fixtures[] = { + { 0, 0 }, + { LINK_RATE_RBR, 1620 }, + { LINK_RATE_HBR, 2700 }, + { LINK_RATE_HBR2, 5400 }, + { LINK_RATE_HBR3, 8100 }, + }; + u32 mbps; + int i, ret; + + for (i = 0; i < ARRAY_SIZE(fixtures); i++) { + ret = apple_dptx_rate_to_mbps(fixtures[i].rate, &mbps); + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, mbps, fixtures[i].mbps); + } + KUNIT_EXPECT_EQ(test, apple_dptx_rate_to_mbps(0xff, &mbps), -EINVAL); +} + +static void apple_dptx_usb4_transport_test(struct kunit *test) +{ + const struct apple_dptx_target target = { .core = 0, .atc = 2 }; + const struct apple_dptx_link_caps caps = { + .max_lanes = 4, + .max_link_rate = LINK_RATE_HBR3, + }; + struct dptx_port dptx = {}; + u32 value; + int ret; + + ret = apple_dptx_transport_init_usb4(&dptx, &target, &caps); + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, dptx.transport.kind, APPLE_DPTX_TRANSPORT_USB4); + ret = dptx.transport.ops->get_max_lane_count(&dptx, NULL, &value); + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, value, 4U); + ret = dptx.transport.ops->get_max_link_rate(&dptx, NULL, &value); + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, value, (u32)LINK_RATE_HBR3); + + ret = dptx.transport.ops->set_active_lane_count(&dptx, NULL, 2); + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, dptx.lane_count, 2U); + ret = dptx.transport.ops->set_active_lane_count(&dptx, NULL, 3); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + KUNIT_EXPECT_EQ(test, dptx.lane_count, 2U); + ret = dptx.transport.ops->set_active_lane_count(&dptx, NULL, 0); + KUNIT_EXPECT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, dptx.lane_count, 0U); + + ret = dptx.transport.ops->set_link_rate(&dptx, NULL, LINK_RATE_HBR2); + KUNIT_ASSERT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, dptx.link_rate, (u32)LINK_RATE_HBR2); + ret = dptx.transport.ops->set_link_rate(&dptx, NULL, 0xff); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + KUNIT_EXPECT_EQ(test, dptx.link_rate, (u32)LINK_RATE_HBR2); + ret = dptx.transport.ops->set_link_rate(&dptx, NULL, 0); + KUNIT_EXPECT_EQ(test, ret, 0); + KUNIT_EXPECT_EQ(test, dptx.link_rate, 0U); + + KUNIT_ASSERT_EQ(test, dptx.transport.ops->activate(&dptx, NULL), 0); + KUNIT_EXPECT_TRUE(test, dptx.transport.active); + KUNIT_ASSERT_EQ(test, dptx.transport.ops->deactivate(&dptx, NULL), 0); + KUNIT_EXPECT_FALSE(test, dptx.transport.active); +} + +static void apple_dptx_usb4_caps_validation_test(struct kunit *test) +{ + const struct apple_dptx_target target = {}; + struct apple_dptx_link_caps caps = { + .max_lanes = 3, + .max_link_rate = LINK_RATE_HBR3, + }; + struct dptx_port dptx = {}; + int ret; + + apple_dptx_transport_init_physical(&dptx); + ret = apple_dptx_transport_init_usb4(&dptx, &target, &caps); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + KUNIT_EXPECT_PTR_EQ(test, dptx.transport.ops, + &apple_dptx_physical_ops); + KUNIT_EXPECT_EQ(test, dptx.transport.kind, + APPLE_DPTX_TRANSPORT_PHYSICAL); + caps.max_lanes = 4; + caps.max_link_rate = 0xff; + ret = apple_dptx_transport_init_usb4(&dptx, &target, &caps); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); +} + +static void apple_dptx_usb4_over_caps_test(struct kunit *test) +{ + const struct apple_dptx_target target = {}; + const struct apple_dptx_link_caps caps = { + .max_lanes = 2, + .max_link_rate = LINK_RATE_HBR, + }; + struct dptx_port dptx = {}; + int ret; + + ret = apple_dptx_transport_init_usb4(&dptx, &target, &caps); + KUNIT_ASSERT_EQ(test, ret, 0); + ret = dptx.transport.ops->set_active_lane_count(&dptx, NULL, 4); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + KUNIT_EXPECT_EQ(test, dptx.lane_count, 0U); + ret = dptx.transport.ops->set_link_rate(&dptx, NULL, LINK_RATE_HBR3); + KUNIT_EXPECT_EQ(test, ret, -EINVAL); + KUNIT_EXPECT_EQ(test, dptx.link_rate, 0U); +} + +static void apple_dptx_physical_is_default_test(struct kunit *test) +{ + struct dptx_port dptx = {}; + + apple_dptx_transport_init_physical(&dptx); + KUNIT_EXPECT_PTR_EQ(test, dptx.transport.ops, &apple_dptx_physical_ops); + KUNIT_EXPECT_EQ(test, dptx.transport.kind, + APPLE_DPTX_TRANSPORT_PHYSICAL); +} + +static struct kunit_case apple_dptx_transport_test_cases[] = { + KUNIT_CASE(apple_dptx_target_round_trip_test), + KUNIT_CASE(apple_dptx_target_rejects_invalid_test), + KUNIT_CASE(apple_dptx_rate_mapping_test), + KUNIT_CASE(apple_dptx_usb4_transport_test), + KUNIT_CASE(apple_dptx_usb4_caps_validation_test), + KUNIT_CASE(apple_dptx_usb4_over_caps_test), + KUNIT_CASE(apple_dptx_physical_is_default_test), + {} +}; + +static struct kunit_suite apple_dptx_transport_test_suite = { + .name = "apple-dptx-transport", + .test_cases = apple_dptx_transport_test_cases, +}; + +kunit_test_suite(apple_dptx_transport_test_suite); diff --git a/drivers/gpu/drm/apple/dptx-transport.c b/drivers/gpu/drm/apple/dptx-transport.c new file mode 100644 index 00000000000000..b5d3c1baa8c746 --- /dev/null +++ b/drivers/gpu/drm/apple/dptx-transport.c @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* Copyright 2026 Matthew Altman */ + +#include + +#include "dcp-internal.h" +#include "dptx-transport.h" + +static bool apple_dptx_lane_count_valid(u32 lane_count) +{ + return lane_count == 1 || lane_count == 2 || lane_count == 4; +} + +int apple_dptx_target_encode(const struct apple_dptx_target *target, u32 *value) +{ + if (target->core > FIELD_MAX(DCPDPTX_REMOTE_PORT_CORE) || + target->atc > FIELD_MAX(DCPDPTX_REMOTE_PORT_ATC) || + target->die > FIELD_MAX(DCPDPTX_REMOTE_PORT_DIE)) + return -ERANGE; + + *value = FIELD_PREP(DCPDPTX_REMOTE_PORT_CORE, target->core) | + FIELD_PREP(DCPDPTX_REMOTE_PORT_ATC, target->atc) | + FIELD_PREP(DCPDPTX_REMOTE_PORT_DIE, target->die) | + DCPDPTX_REMOTE_PORT_CONNECTED; + + return 0; +} + +int apple_dptx_target_decode(u32 value, struct apple_dptx_target *target) +{ + u32 valid = DCPDPTX_REMOTE_PORT_CORE | DCPDPTX_REMOTE_PORT_ATC | + DCPDPTX_REMOTE_PORT_DIE | DCPDPTX_REMOTE_PORT_CONNECTED; + + if ((value & ~valid) || !(value & DCPDPTX_REMOTE_PORT_CONNECTED)) + return -EINVAL; + + target->core = FIELD_GET(DCPDPTX_REMOTE_PORT_CORE, value); + target->atc = FIELD_GET(DCPDPTX_REMOTE_PORT_ATC, value); + target->die = FIELD_GET(DCPDPTX_REMOTE_PORT_DIE, value); + + return 0; +} + +int apple_dptx_rate_to_mbps(u32 link_rate, u32 *mbps) +{ + switch (link_rate) { + case LINK_RATE_RBR: + *mbps = 1620; + break; + case LINK_RATE_HBR: + *mbps = 2700; + break; + case LINK_RATE_HBR2: + *mbps = 5400; + break; + case LINK_RATE_HBR3: + *mbps = 8100; + break; + case 0: + *mbps = 0; + break; + default: + return -EINVAL; + } + + return 0; +} + +static int apple_dptx_usb4_get_max_lane_count(struct dptx_port *dptx, + struct apple_dcp *dcp, + u32 *lane_count) +{ + *lane_count = dptx->transport.caps.max_lanes; + return 0; +} + +static int apple_dptx_usb4_set_active_lane_count(struct dptx_port *dptx, + struct apple_dcp *dcp, + u32 lane_count) +{ + if (lane_count && (!apple_dptx_lane_count_valid(lane_count) || + lane_count > dptx->transport.caps.max_lanes)) + return -EINVAL; + + dptx->lane_count = lane_count; + return 0; +} + +static int apple_dptx_usb4_get_max_link_rate(struct dptx_port *dptx, + struct apple_dcp *dcp, + u32 *link_rate) +{ + *link_rate = dptx->transport.caps.max_link_rate; + return 0; +} + +static int apple_dptx_usb4_set_link_rate(struct dptx_port *dptx, + struct apple_dcp *dcp, u32 link_rate) +{ + u32 ignored; + + if (apple_dptx_rate_to_mbps(link_rate, &ignored) || + link_rate > dptx->transport.caps.max_link_rate) + return -EINVAL; + + dptx->link_rate = link_rate; + dptx->pending_link_rate = link_rate; + return 0; +} + +static int apple_dptx_usb4_activate(struct dptx_port *dptx, + struct apple_dcp *dcp) +{ + dptx->transport.active = true; + return 0; +} + +static int apple_dptx_usb4_deactivate(struct dptx_port *dptx, + struct apple_dcp *dcp) +{ + dptx->transport.active = false; + return 0; +} + +/* + * This backend is intentionally unreachable from production code. It only + * models DCP firmware's logical link requests without touching a physical PHY. + */ +const struct apple_dptx_transport_ops apple_dptx_usb4_ops = { + .get_max_lane_count = apple_dptx_usb4_get_max_lane_count, + .set_active_lane_count = apple_dptx_usb4_set_active_lane_count, + .get_max_link_rate = apple_dptx_usb4_get_max_link_rate, + .set_link_rate = apple_dptx_usb4_set_link_rate, + .activate = apple_dptx_usb4_activate, + .deactivate = apple_dptx_usb4_deactivate, +}; + +void apple_dptx_transport_init_physical(struct dptx_port *dptx) +{ + dptx->transport = (struct apple_dptx_transport){ + .ops = &apple_dptx_physical_ops, + .kind = APPLE_DPTX_TRANSPORT_PHYSICAL, + }; +} + +int apple_dptx_transport_init_usb4(struct dptx_port *dptx, + const struct apple_dptx_target *target, + const struct apple_dptx_link_caps *caps) +{ + u32 ignored; + + if (!apple_dptx_lane_count_valid(caps->max_lanes) || + apple_dptx_rate_to_mbps(caps->max_link_rate, &ignored) || + apple_dptx_target_encode(target, &ignored)) + return -EINVAL; + + dptx->transport = (struct apple_dptx_transport){ + .ops = &apple_dptx_usb4_ops, + .target = *target, + .caps = *caps, + .kind = APPLE_DPTX_TRANSPORT_USB4, + }; + + return 0; +} diff --git a/drivers/gpu/drm/apple/dptx-transport.h b/drivers/gpu/drm/apple/dptx-transport.h new file mode 100644 index 00000000000000..39159228154467 --- /dev/null +++ b/drivers/gpu/drm/apple/dptx-transport.h @@ -0,0 +1,74 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef __APPLE_DCP_DPTX_TRANSPORT_H__ +#define __APPLE_DCP_DPTX_TRANSPORT_H__ + +#include +#include + +#define DCPDPTX_REMOTE_PORT_CORE GENMASK(3, 0) +#define DCPDPTX_REMOTE_PORT_ATC GENMASK(7, 4) +#define DCPDPTX_REMOTE_PORT_DIE GENMASK(11, 8) +#define DCPDPTX_REMOTE_PORT_CONNECTED BIT(15) + +enum dptx_link_rate { + LINK_RATE_RBR = 0x06, + LINK_RATE_HBR = 0x0a, + LINK_RATE_HBR2 = 0x14, + LINK_RATE_HBR3 = 0x1e, +}; + +enum apple_dptx_transport_kind { + APPLE_DPTX_TRANSPORT_PHYSICAL, + APPLE_DPTX_TRANSPORT_USB4, +}; + +struct apple_dptx_target { + u8 core; + u8 atc; + u8 die; +}; + +struct apple_dptx_link_caps { + u32 max_lanes; + u32 max_link_rate; +}; + +struct apple_dptx_transport_ops; + +struct apple_dptx_transport { + const struct apple_dptx_transport_ops *ops; + struct apple_dptx_target target; + struct apple_dptx_link_caps caps; + enum apple_dptx_transport_kind kind; + bool active; +}; + +struct apple_dcp; +struct dptx_port; + +struct apple_dptx_transport_ops { + int (*get_max_lane_count)(struct dptx_port *dptx, struct apple_dcp *dcp, + u32 *lane_count); + int (*set_active_lane_count)(struct dptx_port *dptx, + struct apple_dcp *dcp, u32 lane_count); + int (*get_max_link_rate)(struct dptx_port *dptx, struct apple_dcp *dcp, + u32 *link_rate); + int (*set_link_rate)(struct dptx_port *dptx, struct apple_dcp *dcp, + u32 link_rate); + int (*activate)(struct dptx_port *dptx, struct apple_dcp *dcp); + int (*deactivate)(struct dptx_port *dptx, struct apple_dcp *dcp); +}; + +extern const struct apple_dptx_transport_ops apple_dptx_physical_ops; +extern const struct apple_dptx_transport_ops apple_dptx_usb4_ops; + +int apple_dptx_target_encode(const struct apple_dptx_target *target, + u32 *value); +int apple_dptx_target_decode(u32 value, struct apple_dptx_target *target); +int apple_dptx_rate_to_mbps(u32 link_rate, u32 *mbps); +void apple_dptx_transport_init_physical(struct dptx_port *dptx); +int apple_dptx_transport_init_usb4(struct dptx_port *dptx, + const struct apple_dptx_target *target, + const struct apple_dptx_link_caps *caps); + +#endif diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 1ea00602d476d8..4d041d54d9c366 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -73,14 +73,19 @@ int dptxport_validate_connection(struct apple_epic_service *service, u8 core, u8 atc, u8 die) { struct dptx_port *dptx = service->cookie; + struct apple_dptx_target remote = { + .core = core, + .atc = atc, + .die = die, + }; struct dcpdptx_connection_cmd cmd, resp; int ret; - u32 target = FIELD_PREP(DCPDPTX_REMOTE_PORT_CORE, core) | - FIELD_PREP(DCPDPTX_REMOTE_PORT_ATC, atc) | - FIELD_PREP(DCPDPTX_REMOTE_PORT_DIE, die) | - DCPDPTX_REMOTE_PORT_CONNECTED; + u32 target; trace_dptxport_validate_connection(dptx, core, atc, die); + ret = apple_dptx_target_encode(&remote, &target); + if (ret) + return ret; cmd.target = cpu_to_le32(target); cmd.unk = cpu_to_le32(0x100); @@ -101,15 +106,20 @@ int dptxport_connect(struct apple_epic_service *service, u8 core, u8 atc, u8 die) { struct dptx_port *dptx = service->cookie; + struct apple_dptx_target remote = { + .core = core, + .atc = atc, + .die = die, + }; struct dcpdptx_connection_cmd cmd, resp; u32 unk_field = 0x0; // seen as 0x100 under some conditions int ret; - u32 target = FIELD_PREP(DCPDPTX_REMOTE_PORT_CORE, core) | - FIELD_PREP(DCPDPTX_REMOTE_PORT_ATC, atc) | - FIELD_PREP(DCPDPTX_REMOTE_PORT_DIE, die) | - DCPDPTX_REMOTE_PORT_CONNECTED; + u32 target; trace_dptxport_connect(dptx, core, atc, die); + ret = apple_dptx_target_encode(&remote, &target); + if (ret) + return ret; cmd.target = cpu_to_le32(target); cmd.unk = cpu_to_le32(unk_field); @@ -234,13 +244,22 @@ dptxport_call_set_drive_settings(struct apple_epic_service *service, static int dptxport_call_get_max_link_rate(struct apple_epic_service *service, void *reply_, size_t reply_size) { + struct dptx_port *dptx = service->cookie; struct dptxport_apcall_link_rate *reply = reply_; + u32 link_rate; + int ret; if (reply_size < sizeof(*reply)) return -EINVAL; + ret = dptx->transport.ops->get_max_link_rate(dptx, service->ep->dcp, + &link_rate); reply->retcode = cpu_to_le32(0); - reply->link_rate = cpu_to_le32(LINK_RATE_HBR3); + if (ret) { + reply->retcode = cpu_to_le32(1); + link_rate = 0; + } + reply->link_rate = cpu_to_le32(link_rate); return 0; } @@ -250,31 +269,20 @@ static int dptxport_call_get_max_lane_count(struct apple_epic_service *service, { struct dptxport_apcall_lane_count *reply = reply_; struct dptx_port *dptx = service->cookie; - struct apple_dcp *dcp = service->ep->dcp; - union phy_configure_opts phy_ops; + u32 lane_count; int ret; if (reply_size < sizeof(*reply)) return -EINVAL; - ret = phy_validate(dptx->atcphy, PHY_MODE_DP, 0, &phy_ops); + ret = dptx->transport.ops->get_max_lane_count(dptx, service->ep->dcp, + &lane_count); if (ret < 0) { - dev_err(dcp->dev, "phy_validate failed: %d\n", ret); reply->retcode = cpu_to_le32(1); reply->lane_count = cpu_to_le64(0); } else { - if (phy_ops.dp.lanes < 2) { - // phy_validate might return 0 lanes if atc phy is not - // yet switched to DP mode - dev_dbg(dcp->dev, "get_max_lane_count: phy lanes: %d\n", - phy_ops.dp.lanes); - // default to 4 lanes - dptx->lane_count = 4; - } else { - dptx->lane_count = phy_ops.dp.lanes; - } reply->retcode = cpu_to_le32(0); - reply->lane_count = cpu_to_le64(dptx->lane_count); + reply->lane_count = cpu_to_le64(lane_count); } return 0; @@ -285,7 +293,6 @@ static int dptxport_call_set_active_lane_count(struct apple_epic_service *servic void *reply_, size_t reply_size) { struct dptx_port *dptx = service->cookie; - struct apple_dcp *dcp = service->ep->dcp; const struct dptxport_apcall_set_active_lane_count *request = data; struct dptxport_apcall_set_active_lane_count *reply = reply_; int ret = 0; @@ -299,34 +306,35 @@ static int dptxport_call_set_active_lane_count(struct apple_epic_service *servic u64 lane_count = le64_to_cpu(request->lane_count); if (dptx->lane_count < lane_count) - dev_err(dcp->dev, "set_active_lane_count: unexpected lane " + dev_err(service->ep->dcp->dev, + "set_active_lane_count: unexpected lane " "count:%llu phy: %d\n", lane_count, dptx->lane_count); switch (lane_count) { case 0 ... 2: case 4: - dptx->phy_ops.dp.lanes = lane_count; - // Use dptx phy index > 3 as indication for dptx-phy or - // lpdptx-phy and configure the number of lanes for those - dptx->phy_ops.dp.set_lanes = (dcp->dptx_phy > 3); break; default: - dev_err(dcp->dev, "set_active_lane_count: invalid lane count:%llu\n", lane_count); + dev_err(service->ep->dcp->dev, + "set_active_lane_count: invalid lane count:%llu\n", + lane_count); retcode = 1; lane_count = 0; break; } - if (dptx->phy_ops.dp.set_lanes) { - if (dptx->atcphy) { - ret = phy_configure(dptx->atcphy, &dptx->phy_ops); - if (ret) - return ret; - } - dptx->phy_ops.dp.set_lanes = 0; - dptx->lane_count = lane_count; + if (!retcode && dptx->transport.kind == APPLE_DPTX_TRANSPORT_USB4 && + lane_count > dptx->transport.caps.max_lanes) { + retcode = 1; + lane_count = 0; } + if (!retcode) + ret = dptx->transport.ops->set_active_lane_count(dptx, + service->ep->dcp, lane_count); + if (ret) + return ret; + reply->retcode = cpu_to_le32(retcode); reply->lane_count = cpu_to_le64(lane_count); @@ -379,8 +387,7 @@ static int dptxport_call_set_link_rate(struct apple_epic_service *service, struct dptx_port *dptx = service->cookie; const struct dptxport_apcall_link_rate *request = data; struct dptxport_apcall_link_rate *reply = reply_; - u32 link_rate, phy_link_rate; - bool phy_set_rate = false; + u32 link_rate, ignored; int ret; if (reply_size < sizeof(*reply)) @@ -391,52 +398,22 @@ static int dptxport_call_set_link_rate(struct apple_epic_service *service, link_rate = le32_to_cpu(request->link_rate); trace_dptxport_call_set_link_rate(dptx, link_rate); - switch (link_rate) { - case LINK_RATE_RBR: - phy_link_rate = 1620; - phy_set_rate = true; - break; - case LINK_RATE_HBR: - phy_link_rate = 2700; - phy_set_rate = true; - break; - case LINK_RATE_HBR2: - phy_link_rate = 5400; - phy_set_rate = true; - break; - case LINK_RATE_HBR3: - phy_link_rate = 8100; - phy_set_rate = true; - break; - case 0: - phy_link_rate = 0; - phy_set_rate = true; - break; - default: + if (apple_dptx_rate_to_mbps(link_rate, &ignored) || + (dptx->transport.kind == APPLE_DPTX_TRANSPORT_USB4 && + link_rate > dptx->transport.caps.max_link_rate)) { dev_err(service->ep->dcp->dev, "DPTXPort: Unsupported link rate 0x%x requested\n", link_rate); link_rate = 0; - phy_set_rate = false; - break; + goto out_reply; } - if (phy_set_rate) { - dptx->phy_ops.dp.link_rate = phy_link_rate; - dptx->phy_ops.dp.set_rate = 1; - - if (dptx->atcphy) { - ret = phy_configure(dptx->atcphy, &dptx->phy_ops); - if (ret) - return ret; - } - - //if (dptx->phy_ops.dp.set_rate) - dptx->link_rate = dptx->pending_link_rate = link_rate; - - } + ret = dptx->transport.ops->set_link_rate(dptx, service->ep->dcp, + link_rate); + if (ret) + return ret; - //dptx->pending_link_rate = link_rate; +out_reply: reply->retcode = cpu_to_le32(0); reply->link_rate = cpu_to_le32(link_rate); @@ -476,12 +453,11 @@ dptxport_call_activate(struct apple_epic_service *service, void *reply, size_t reply_size) { struct dptx_port *dptx = service->cookie; - const struct apple_dcp *dcp = service->ep->dcp; + int ret; - // TODO: hack, use phy_set_mode to select the correct DCP(EXT) input - // for standalone phy (i.e. not atc phy). - if (!dcp->typec_mux) - phy_set_mode_ext(dptx->atcphy, PHY_MODE_DP, dcp->index); + ret = dptx->transport.ops->activate(dptx, service->ep->dcp); + if (ret) + return ret; memcpy(reply, data, min(reply_size, data_size)); if (reply_size >= 4) @@ -496,9 +472,11 @@ dptxport_call_deactivate(struct apple_epic_service *service, void *reply, size_t reply_size) { struct dptx_port *dptx = service->cookie; + int ret; - /* deactivate phy */ - phy_set_mode_ext(dptx->atcphy, PHY_MODE_INVALID, 0); + ret = dptx->transport.ops->deactivate(dptx, service->ep->dcp); + if (ret) + return ret; memcpy(reply, data, min(reply_size, data_size)); if (reply_size >= 4) @@ -588,6 +566,7 @@ static void dptxport_init(struct apple_epic_service *service, const char *name, } service->ep->dcp->dptxport[unit].unit = unit; service->ep->dcp->dptxport[unit].service = service; + apple_dptx_transport_init_physical(&service->ep->dcp->dptxport[unit]); service->ep->dcp->dptxport[unit].enabled = true; service->cookie = (void *)&service->ep->dcp->dptxport[unit]; complete(&service->ep->dcp->dptxport[unit].enable_completion); diff --git a/drivers/gpu/drm/apple/dptxep.h b/drivers/gpu/drm/apple/dptxep.h index 0bf2534054fd7b..6ea3a2289d3d71 100644 --- a/drivers/gpu/drm/apple/dptxep.h +++ b/drivers/gpu/drm/apple/dptxep.h @@ -4,6 +4,8 @@ #include #include +#include "dptx-transport.h" + enum dptx_apcall { DPTX_APCALL_ACTIVATE = 0, DPTX_APCALL_DEACTIVATE = 1, @@ -32,18 +34,6 @@ enum dptx_apcall { DPTX_APCALL_DEVICE_NOT_STARTED = 24, }; -#define DCPDPTX_REMOTE_PORT_CORE GENMASK(3, 0) -#define DCPDPTX_REMOTE_PORT_ATC GENMASK(7, 4) -#define DCPDPTX_REMOTE_PORT_DIE GENMASK(11, 8) -#define DCPDPTX_REMOTE_PORT_CONNECTED BIT(15) - -enum dptx_link_rate { - LINK_RATE_RBR = 0x06, - LINK_RATE_HBR = 0x0a, - LINK_RATE_HBR2 = 0x14, - LINK_RATE_HBR3 = 0x1e, -}; - struct apple_epic_service; struct dptx_port { @@ -58,6 +48,7 @@ struct dptx_port { u32 lane_count; u32 link_rate, pending_link_rate; u32 drive_settings[2]; + struct apple_dptx_transport transport; }; int dptxport_validate_connection(struct apple_epic_service *service, u8 core, From db0c7626c698d81d41b432221c07d7e980a524c9 Mon Sep 17 00:00:00 2001 From: Matthew Altman Date: Sun, 30 Aug 2026 21:00:15 -0500 Subject: [PATCH 09/12] drm/apple: harden DPTX connection lifecycle Model DPTX connection attempts explicitly so disconnect and shutdown can cancel an in-flight link-configuration wait without allowing the waiter to assert HPD or connect AV afterward. Correctly treat a zero wait_for_completion_timeout return as a timeout and propagate EPIC request and HPD errors. Serialize completion reuse with attempt publication, distinguish remote display ownership from a fully connected output, drain connect callers before AFK shutdown, and quarantine every unsuccessful attempt until DCP reinitialization. The quarantine is intentionally conservative because firmware APCALLs carry no attempt token and a late APCALL must not satisfy a newer attempt. Add KUnit coverage for success, timeout, cancellation, stale tickets, anomalous completion, generation wrap, and forced failure. This is compile-tested experimental infrastructure: KUnit runtime, native HDMI timeout behavior, and dock video remain hardware-untested. --- drivers/gpu/drm/apple/Makefile | 1 + drivers/gpu/drm/apple/dcp.c | 270 +++++++++++++++++++--- drivers/gpu/drm/apple/dptx-attempt-test.c | 174 ++++++++++++++ drivers/gpu/drm/apple/dptx-attempt.h | 129 +++++++++++ drivers/gpu/drm/apple/dptxep.c | 30 ++- drivers/gpu/drm/apple/dptxep.h | 8 +- 6 files changed, 576 insertions(+), 36 deletions(-) create mode 100644 drivers/gpu/drm/apple/dptx-attempt-test.c create mode 100644 drivers/gpu/drm/apple/dptx-attempt.h diff --git a/drivers/gpu/drm/apple/Makefile b/drivers/gpu/drm/apple/Makefile index 486cf4b303b091..da7668b202b130 100644 --- a/drivers/gpu/drm/apple/Makefile +++ b/drivers/gpu/drm/apple/Makefile @@ -6,6 +6,7 @@ appledrm-y := apple_drv.o appledrm-y += afk.o dcp.o dcp_backlight.o dptxep.o dptx-transport.o appledrm-y += dptx-transport-phy.o iomfb.o parser.o systemep.o appledrm-$(CONFIG_DRM_APPLE_KUNIT_TEST) += dptx-transport-test.o +appledrm-$(CONFIG_DRM_APPLE_KUNIT_TEST) += dptx-attempt-test.o appledrm-$(CONFIG_DRM_APPLE_AUDIO) += audio.o appledrm-$(CONFIG_DRM_APPLE_AUDIO) += av.o appledrm-y += connector.o diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 03262b07293319..84e15646fb8023 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -357,10 +357,35 @@ int dcp_get_connector_type(struct platform_device *pdev) #define DPTX_CONNECT_TIMEOUT msecs_to_jiffies(2000) +static void +dcp_dptx_attempt_quarantine(struct dptx_port *dptx, + const struct apple_dptx_attempt_ticket *ticket) +{ + unsigned long flags; + + spin_lock_irqsave(&dptx->attempt_lock, flags); + apple_dptx_attempt_fail(&dptx->attempt, ticket); + spin_unlock_irqrestore(&dptx->attempt_lock, flags); +} + +static void dcp_dptx_attempt_fail(struct dptx_port *dptx) +{ + dcp_dptx_attempt_quarantine(dptx, NULL); +} + static int dcp_dptx_connect(struct apple_dcp *dcp, u32 port) { + struct apple_dptx_attempt_ticket ticket; + struct dptx_port *dptx; + unsigned long flags; + unsigned long remaining; + int release_ret; int ret = 0; + if (port >= ARRAY_SIZE(dcp->dptxport)) + return -EINVAL; + dptx = &dcp->dptxport[port]; + if (!dcp->phy) { dev_warn(dcp->dev, "dcp_dptx_connect: missing phy\n"); return -ENODEV; @@ -368,41 +393,116 @@ static int dcp_dptx_connect(struct apple_dcp *dcp, u32 port) dev_info(dcp->dev, "%s(port=%d)\n", __func__, port); mutex_lock(&dcp->hpd_mutex); - if (!dcp->dptxport[port].enabled) { + if (!dptx->enabled) { dev_warn(dcp->dev, "dcp_dptx_connect: dptx service for port %d not enabled\n", port); ret = -ENODEV; goto out_unlock; } + if (dptx->shutting_down) { + ret = -ESHUTDOWN; + goto out_unlock; + } - if (dcp->dptxport[port].connected) + spin_lock_irqsave(&dptx->attempt_lock, flags); + if (dptx->attempt.phase == APPLE_DPTX_ATTEMPT_CONNECTED) { + spin_unlock_irqrestore(&dptx->attempt_lock, flags); + goto out_unlock; + } + if (dptx->attempt.phase == APPLE_DPTX_ATTEMPT_FAILED) { + spin_unlock_irqrestore(&dptx->attempt_lock, flags); + dev_warn_ratelimited(dcp->dev, + "%s: port %u is quarantined until DPTX restart\n", + __func__, port); + ret = -EIO; + goto out_unlock; + } + /* Publish CONNECTING only after its completion has been cleared. */ + ret = apple_dptx_attempt_begin(&dptx->attempt, &ticket); + if (!ret) + reinit_completion(&dptx->linkcfg_completion); + spin_unlock_irqrestore(&dptx->attempt_lock, flags); + if (ret) goto out_unlock; - reinit_completion(&dcp->dptxport[port].linkcfg_completion); - dcp->dptxport[port].atcphy = dcp->phy; - dptxport_connect(dcp->dptxport[port].service, 0, dcp->dptx_phy, dcp->dptx_die); - dptxport_request_display(dcp->dptxport[port].service); - dcp->dptxport[port].connected = true; + /* Shutdown cannot interleave here because hpd_mutex is still held. */ + reinit_completion(&dptx->connect_idle); + dptx->connect_inflight = true; + dptx->cleanup_owned = false; + + dptx->atcphy = dcp->phy; + ret = dptxport_connect(dptx->service, 0, dcp->dptx_phy, + dcp->dptx_die); + if (ret) { + /* The command may have reached firmware even if its reply failed. */ + goto out_abort; + } + /* Once issued, a lost response leaves remote ownership ambiguous. */ + dptx->remote_requested = true; + ret = dptxport_request_display(dptx->service); + if (ret) { + /* There is no protocol token with which to prove remote ownership. */ + goto out_release; + } mutex_unlock(&dcp->hpd_mutex); - ret = wait_for_completion_timeout(&dcp->dptxport[port].linkcfg_completion, - DPTX_CONNECT_TIMEOUT); - if (ret < 0) + remaining = wait_for_completion_timeout(&dptx->linkcfg_completion, + DPTX_CONNECT_TIMEOUT); + mutex_lock(&dcp->hpd_mutex); + spin_lock_irqsave(&dptx->attempt_lock, flags); + ret = apple_dptx_attempt_finish_wait(&dptx->attempt, &ticket, + remaining != 0); + spin_unlock_irqrestore(&dptx->attempt_lock, flags); + if (ret) { dev_warn(dcp->dev, "dcp_dptx_connect: port %d link complete failed:%d\n", port, ret); - else + goto out_release; + } else { dev_dbg(dcp->dev, "dcp_dptx_connect: waited %d ms for link\n", - jiffies_to_msecs(DPTX_CONNECT_TIMEOUT - ret)); + jiffies_to_msecs(DPTX_CONNECT_TIMEOUT - remaining)); + } usleep_range(5, 10); - if (dcp->connector_type == DRM_MODE_CONNECTOR_DisplayPort) - dptxport_set_hpd(dcp->dptxport[port].service, true); + if (dcp->connector_type == DRM_MODE_CONNECTOR_DisplayPort) { + ret = dptxport_set_hpd(dptx->service, true); + if (ret) { + release_ret = dptxport_set_hpd(dptx->service, false); + if (release_ret) { + dev_warn(dcp->dev, + "%s: port %d HPD rollback failed:%d\n", + __func__, port, release_ret); + } + goto out_release; + } + } if (dcp->avep) av_service_connect(dcp); - - return 0; - + dptx->connected = true; + + goto out_done; + +out_release: + if (dptx->remote_requested && !dptx->cleanup_owned) { + release_ret = dptxport_release_display(dptx->service); + if (release_ret) { + dev_warn(dcp->dev, + "%s: port %d release failed:%d\n", __func__, + port, release_ret); + if (!ret) + ret = release_ret; + } else { + dptx->remote_requested = false; + } + } + dptx->connected = false; +out_abort: + /* Failed attempts are never reusable: firmware APCALLs have no ticket. */ + dcp_dptx_attempt_quarantine(dptx, &ticket); +out_done: + dptx->connect_inflight = false; + dptx->cleanup_owned = false; + complete_all(&dptx->connect_idle); out_unlock: mutex_unlock(&dcp->hpd_mutex); return ret; @@ -416,18 +516,127 @@ static void disconnected_hpd_event(struct apple_connector *con) } } -static int dcp_dptx_disconnect(struct apple_dcp *dcp, u32 port) +static int dcp_dptx_disconnect(struct apple_dcp *dcp, u32 port, bool oob) { + struct dptx_port *dptx; + unsigned long flags; + bool wake = false; + bool admitted = false; + bool active; + int ret = 0; + int tmp; + + if (port >= ARRAY_SIZE(dcp->dptxport)) + return -EINVAL; + dptx = &dcp->dptxport[port]; + dev_info(dcp->dev, "%s(port=%d)\n", __func__, port); mutex_lock(&dcp->hpd_mutex); - if (dcp->dptxport[port].enabled && dcp->dptxport[port].connected) { - dptxport_release_display(dcp->dptxport[port].service); - dcp->dptxport[port].connected = false; + if (dptx->shutting_down) { + ret = -ESHUTDOWN; + goto out_unlock; + } + admitted = true; + if (oob && dcp->avep) + av_service_disconnect(dcp); + if (!dptx->enabled) + goto out_unlock; + + active = dptx->connected || dptx->remote_requested || + dptx->connect_inflight; + if (dptx->connect_inflight) + dptx->cleanup_owned = true; + spin_lock_irqsave(&dptx->attempt_lock, flags); + wake = apple_dptx_attempt_cancel(&dptx->attempt); + spin_unlock_irqrestore(&dptx->attempt_lock, flags); + if (wake) + complete_all(&dptx->linkcfg_completion); + + if (oob && active) { + tmp = dptxport_set_hpd(dptx->service, false); + if (tmp) { + dcp_dptx_attempt_fail(dptx); + if (!ret) + ret = tmp; + } + } + + if (dptx->connected) + dptx->connected = false; + + if (dptx->remote_requested) { + tmp = dptxport_release_display(dptx->service); + if (tmp) { + dcp_dptx_attempt_fail(dptx); + if (!ret) + ret = tmp; + } else { + dptx->remote_requested = false; + } } + +out_unlock: mutex_unlock(&dcp->hpd_mutex); + if (oob && admitted) + disconnected_hpd_event(dcp->connector); - return 0; + return ret; +} + +static void dcp_dptx_shutdown(struct apple_dcp *dcp) +{ + struct dptx_port *dptx; + unsigned long flags; + unsigned int port; + bool wake; + int ret; + + /* Stop admission and wake every waiter while the DPTX endpoint is alive. */ + mutex_lock(&dcp->hpd_mutex); + for (port = 0; port < ARRAY_SIZE(dcp->dptxport); port++) { + dptx = &dcp->dptxport[port]; + dptx->shutting_down = true; + if (dptx->connect_inflight) + dptx->cleanup_owned = true; + spin_lock_irqsave(&dptx->attempt_lock, flags); + wake = apple_dptx_attempt_cancel(&dptx->attempt); + spin_unlock_irqrestore(&dptx->attempt_lock, flags); + if (wake) + complete_all(&dptx->linkcfg_completion); + } + mutex_unlock(&dcp->hpd_mutex); + + /* Connect cleanup can use both DPTX and AV services, so drain it first. */ + for (port = 0; port < ARRAY_SIZE(dcp->dptxport); port++) + wait_for_completion(&dcp->dptxport[port].connect_idle); + + mutex_lock(&dcp->hpd_mutex); + for (port = 0; port < ARRAY_SIZE(dcp->dptxport); port++) { + dptx = &dcp->dptxport[port]; + if (!dptx->enabled) + continue; + + ret = dptxport_set_hpd(dptx->service, false); + if (ret) + dev_warn(dcp->dev, + "%s: port %u HPD low failed:%d\n", __func__, + port, ret); + + if (dptx->remote_requested) { + ret = dptxport_release_display(dptx->service); + if (ret) { + dev_warn(dcp->dev, + "%s: port %u release failed:%d\n", + __func__, port, ret); + dcp_dptx_attempt_fail(dptx); + } else { + dptx->remote_requested = false; + } + } + dptx->connected = false; + } + mutex_unlock(&dcp->hpd_mutex); } int dcp_dptx_connect_oob(struct platform_device *pdev, u32 port) @@ -440,15 +649,7 @@ int dcp_dptx_disconnect_oob(struct platform_device *pdev, u32 port) { struct apple_dcp *dcp = platform_get_drvdata(pdev); - disconnected_hpd_event(dcp->connector); - - if (dcp->avep) - av_service_disconnect(dcp); - - if (dcp->dptxport[port].enabled) - dptxport_set_hpd(dcp->dptxport[port].service, false); - - return dcp_dptx_disconnect(dcp, port); + return dcp_dptx_disconnect(dcp, port, true); } static irqreturn_t dcp_dp2hdmi_hpd(int irq, void *data) @@ -700,7 +901,7 @@ void dcp_poweroff(struct platform_device *pdev) bool connected = gpiod_get_value_cansleep(dcp->hdmi_hpd); if (!connected) { disconnected_hpd_event(dcp->connector); - dcp_dptx_disconnect(dcp, 0); + dcp_dptx_disconnect(dcp, 0, false); } } } @@ -1115,6 +1316,9 @@ static void dcp_comp_unbind(struct device *dev, struct device *main, void *data) if (dcp->hdmi_hpd_irq) disable_irq(dcp->hdmi_hpd_irq); + if (dcp->dptxep) + dcp_dptx_shutdown(dcp); + typec_mux_put(dcp->typec_mux); if (dcp->avep) { @@ -1326,7 +1530,7 @@ static int dcp_platform_suspend(struct device *dev) if (dcp->hdmi_hpd_irq) { disable_irq(dcp->hdmi_hpd_irq); disconnected_hpd_event(dcp->connector); - dcp_dptx_disconnect(dcp, 0); + dcp_dptx_disconnect(dcp, 0, false); } /* * Set the device as a wakeup device, which forces its power diff --git a/drivers/gpu/drm/apple/dptx-attempt-test.c b/drivers/gpu/drm/apple/dptx-attempt-test.c new file mode 100644 index 00000000000000..78e23c28b8b80d --- /dev/null +++ b/drivers/gpu/drm/apple/dptx-attempt-test.c @@ -0,0 +1,174 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +#include + +#include "dptx-attempt.h" + +static void apple_dptx_attempt_success_test(struct kunit *test) +{ + struct apple_dptx_attempt_ticket ticket; + struct apple_dptx_attempt attempt; + + apple_dptx_attempt_init(&attempt); + KUNIT_ASSERT_EQ(test, apple_dptx_attempt_begin(&attempt, &ticket), 0); + KUNIT_EXPECT_TRUE(test, apple_dptx_attempt_link_configured(&attempt)); + KUNIT_EXPECT_EQ(test, + apple_dptx_attempt_finish_wait(&attempt, &ticket, true), 0); + KUNIT_EXPECT_EQ(test, attempt.phase, APPLE_DPTX_ATTEMPT_CONNECTED); + KUNIT_EXPECT_FALSE(test, apple_dptx_attempt_cancel(&attempt)); + KUNIT_EXPECT_EQ(test, attempt.phase, APPLE_DPTX_ATTEMPT_IDLE); +} + +static void apple_dptx_attempt_timeout_test(struct kunit *test) +{ + struct apple_dptx_attempt_ticket ticket; + struct apple_dptx_attempt attempt; + + apple_dptx_attempt_init(&attempt); + KUNIT_ASSERT_EQ(test, apple_dptx_attempt_begin(&attempt, &ticket), 0); + KUNIT_EXPECT_EQ(test, + apple_dptx_attempt_finish_wait(&attempt, &ticket, false), + -ETIMEDOUT); + KUNIT_EXPECT_EQ(test, attempt.phase, APPLE_DPTX_ATTEMPT_CANCELING); + KUNIT_EXPECT_TRUE(test, apple_dptx_attempt_reset(&attempt, &ticket)); + KUNIT_EXPECT_EQ(test, attempt.phase, APPLE_DPTX_ATTEMPT_IDLE); +} + +static void apple_dptx_attempt_cancel_wakes_waiter_test(struct kunit *test) +{ + struct apple_dptx_attempt_ticket ticket; + struct apple_dptx_attempt attempt; + + apple_dptx_attempt_init(&attempt); + KUNIT_ASSERT_EQ(test, apple_dptx_attempt_begin(&attempt, &ticket), 0); + KUNIT_EXPECT_TRUE(test, apple_dptx_attempt_cancel(&attempt)); + KUNIT_EXPECT_EQ(test, + apple_dptx_attempt_finish_wait(&attempt, &ticket, true), + -ECANCELED); + KUNIT_EXPECT_TRUE(test, apple_dptx_attempt_reset(&attempt, &ticket)); +} + +static void apple_dptx_attempt_late_completion_test(struct kunit *test) +{ + struct apple_dptx_attempt_ticket ticket; + struct apple_dptx_attempt attempt; + + apple_dptx_attempt_init(&attempt); + KUNIT_ASSERT_EQ(test, apple_dptx_attempt_begin(&attempt, &ticket), 0); + KUNIT_ASSERT_TRUE(test, apple_dptx_attempt_cancel(&attempt)); + KUNIT_EXPECT_FALSE(test, apple_dptx_attempt_link_configured(&attempt)); + KUNIT_EXPECT_EQ(test, + apple_dptx_attempt_finish_wait(&attempt, &ticket, true), + -ECANCELED); +} + +static void apple_dptx_attempt_duplicate_and_busy_test(struct kunit *test) +{ + struct apple_dptx_attempt_ticket ticket, ignored; + struct apple_dptx_attempt attempt; + + apple_dptx_attempt_init(&attempt); + KUNIT_ASSERT_EQ(test, apple_dptx_attempt_begin(&attempt, &ticket), 0); + KUNIT_EXPECT_EQ(test, apple_dptx_attempt_begin(&attempt, &ignored), -EBUSY); + KUNIT_ASSERT_TRUE(test, apple_dptx_attempt_link_configured(&attempt)); + KUNIT_EXPECT_FALSE(test, apple_dptx_attempt_link_configured(&attempt)); + KUNIT_ASSERT_EQ(test, + apple_dptx_attempt_finish_wait(&attempt, &ticket, true), 0); + KUNIT_EXPECT_FALSE(test, apple_dptx_attempt_reset(&attempt, &ticket)); +} + +static void apple_dptx_attempt_stale_ticket_test(struct kunit *test) +{ + struct apple_dptx_attempt_ticket old_ticket, new_ticket; + struct apple_dptx_attempt attempt; + + apple_dptx_attempt_init(&attempt); + KUNIT_ASSERT_EQ(test, apple_dptx_attempt_begin(&attempt, &old_ticket), 0); + KUNIT_ASSERT_TRUE(test, apple_dptx_attempt_cancel(&attempt)); + KUNIT_ASSERT_TRUE(test, apple_dptx_attempt_reset(&attempt, &old_ticket)); + KUNIT_ASSERT_EQ(test, apple_dptx_attempt_begin(&attempt, &new_ticket), 0); + KUNIT_EXPECT_EQ(test, + apple_dptx_attempt_finish_wait(&attempt, &old_ticket, true), + -ESTALE); + KUNIT_EXPECT_FALSE(test, apple_dptx_attempt_reset(&attempt, &old_ticket)); + KUNIT_EXPECT_NE(test, old_ticket.generation, new_ticket.generation); +} + +static void apple_dptx_attempt_generation_wrap_test(struct kunit *test) +{ + struct apple_dptx_attempt_ticket ticket; + struct apple_dptx_attempt attempt; + + apple_dptx_attempt_init(&attempt); + attempt.generation = ~0ULL; + KUNIT_ASSERT_EQ(test, apple_dptx_attempt_begin(&attempt, &ticket), 0); + KUNIT_EXPECT_EQ(test, ticket.generation, 1ULL); +} + +static void apple_dptx_attempt_completion_without_link_test(struct kunit *test) +{ + struct apple_dptx_attempt_ticket ticket; + struct apple_dptx_attempt attempt; + + apple_dptx_attempt_init(&attempt); + KUNIT_ASSERT_EQ(test, apple_dptx_attempt_begin(&attempt, &ticket), 0); + KUNIT_EXPECT_EQ(test, + apple_dptx_attempt_finish_wait(&attempt, &ticket, true), + -EIO); + KUNIT_EXPECT_EQ(test, attempt.phase, APPLE_DPTX_ATTEMPT_CANCELING); +} + +static void apple_dptx_attempt_failure_quarantines_test(struct kunit *test) +{ + struct apple_dptx_attempt_ticket ticket, ignored; + struct apple_dptx_attempt attempt; + + apple_dptx_attempt_init(&attempt); + KUNIT_ASSERT_EQ(test, apple_dptx_attempt_begin(&attempt, &ticket), 0); + KUNIT_ASSERT_TRUE(test, apple_dptx_attempt_fail(&attempt, &ticket)); + KUNIT_EXPECT_EQ(test, attempt.phase, APPLE_DPTX_ATTEMPT_FAILED); + KUNIT_EXPECT_FALSE(test, apple_dptx_attempt_link_configured(&attempt)); + KUNIT_EXPECT_FALSE(test, apple_dptx_attempt_cancel(&attempt)); + KUNIT_EXPECT_FALSE(test, apple_dptx_attempt_reset(&attempt, &ticket)); + KUNIT_EXPECT_EQ(test, apple_dptx_attempt_begin(&attempt, &ignored), -EBUSY); + KUNIT_EXPECT_EQ(test, + apple_dptx_attempt_finish_wait(&attempt, &ticket, true), + -EIO); + KUNIT_EXPECT_EQ(test, attempt.phase, APPLE_DPTX_ATTEMPT_FAILED); +} + +static void apple_dptx_attempt_forced_failure_test(struct kunit *test) +{ + struct apple_dptx_attempt_ticket ticket, ignored; + struct apple_dptx_attempt attempt; + + apple_dptx_attempt_init(&attempt); + KUNIT_ASSERT_EQ(test, apple_dptx_attempt_begin(&attempt, &ticket), 0); + KUNIT_ASSERT_TRUE(test, apple_dptx_attempt_link_configured(&attempt)); + KUNIT_ASSERT_EQ(test, + apple_dptx_attempt_finish_wait(&attempt, &ticket, true), 0); + KUNIT_ASSERT_TRUE(test, apple_dptx_attempt_fail(&attempt, NULL)); + KUNIT_EXPECT_EQ(test, attempt.phase, APPLE_DPTX_ATTEMPT_FAILED); + KUNIT_EXPECT_EQ(test, apple_dptx_attempt_begin(&attempt, &ignored), -EBUSY); +} + +static struct kunit_case apple_dptx_attempt_test_cases[] = { + KUNIT_CASE(apple_dptx_attempt_success_test), + KUNIT_CASE(apple_dptx_attempt_timeout_test), + KUNIT_CASE(apple_dptx_attempt_cancel_wakes_waiter_test), + KUNIT_CASE(apple_dptx_attempt_late_completion_test), + KUNIT_CASE(apple_dptx_attempt_duplicate_and_busy_test), + KUNIT_CASE(apple_dptx_attempt_stale_ticket_test), + KUNIT_CASE(apple_dptx_attempt_generation_wrap_test), + KUNIT_CASE(apple_dptx_attempt_completion_without_link_test), + KUNIT_CASE(apple_dptx_attempt_failure_quarantines_test), + KUNIT_CASE(apple_dptx_attempt_forced_failure_test), + {} +}; + +static struct kunit_suite apple_dptx_attempt_test_suite = { + .name = "apple-dptx-attempt", + .test_cases = apple_dptx_attempt_test_cases, +}; + +kunit_test_suite(apple_dptx_attempt_test_suite); diff --git a/drivers/gpu/drm/apple/dptx-attempt.h b/drivers/gpu/drm/apple/dptx-attempt.h new file mode 100644 index 00000000000000..ed1f7b2af3dc1a --- /dev/null +++ b/drivers/gpu/drm/apple/dptx-attempt.h @@ -0,0 +1,129 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef __APPLE_DCP_DPTX_ATTEMPT_H__ +#define __APPLE_DCP_DPTX_ATTEMPT_H__ + +#include +#include + +enum apple_dptx_attempt_phase { + APPLE_DPTX_ATTEMPT_IDLE, + APPLE_DPTX_ATTEMPT_CONNECTING, + APPLE_DPTX_ATTEMPT_LINK_CONFIGURED, + APPLE_DPTX_ATTEMPT_CONNECTED, + APPLE_DPTX_ATTEMPT_CANCELING, + APPLE_DPTX_ATTEMPT_FAILED, +}; + +struct apple_dptx_attempt_ticket { + u64 generation; +}; + +struct apple_dptx_attempt { + enum apple_dptx_attempt_phase phase; + u64 generation; +}; + +static inline void apple_dptx_attempt_init(struct apple_dptx_attempt *attempt) +{ + *attempt = (struct apple_dptx_attempt) {}; +} + +static inline int +apple_dptx_attempt_begin(struct apple_dptx_attempt *attempt, + struct apple_dptx_attempt_ticket *ticket) +{ + if (attempt->phase != APPLE_DPTX_ATTEMPT_IDLE) + return -EBUSY; + + attempt->generation++; + if (!attempt->generation) + attempt->generation++; + attempt->phase = APPLE_DPTX_ATTEMPT_CONNECTING; + ticket->generation = attempt->generation; + + return 0; +} + +static inline bool +apple_dptx_attempt_link_configured(struct apple_dptx_attempt *attempt) +{ + /* + * Firmware supplies no attempt token. Callers must quarantine every + * unsuccessful attempt until the DPTX endpoint restarts, so an old APCALL + * cannot complete a newer attempt. + */ + if (attempt->phase != APPLE_DPTX_ATTEMPT_CONNECTING) + return false; + + attempt->phase = APPLE_DPTX_ATTEMPT_LINK_CONFIGURED; + return true; +} + +static inline bool apple_dptx_attempt_cancel(struct apple_dptx_attempt *attempt) +{ + switch (attempt->phase) { + case APPLE_DPTX_ATTEMPT_CONNECTING: + case APPLE_DPTX_ATTEMPT_LINK_CONFIGURED: + attempt->phase = APPLE_DPTX_ATTEMPT_CANCELING; + return true; + case APPLE_DPTX_ATTEMPT_CONNECTED: + attempt->phase = APPLE_DPTX_ATTEMPT_IDLE; + return false; + case APPLE_DPTX_ATTEMPT_IDLE: + case APPLE_DPTX_ATTEMPT_CANCELING: + case APPLE_DPTX_ATTEMPT_FAILED: + return false; + } + + return false; +} + +static inline int +apple_dptx_attempt_finish_wait(struct apple_dptx_attempt *attempt, + const struct apple_dptx_attempt_ticket *ticket, + bool completed) +{ + if (ticket->generation != attempt->generation) + return -ESTALE; + if (attempt->phase == APPLE_DPTX_ATTEMPT_FAILED) + return -EIO; + if (attempt->phase == APPLE_DPTX_ATTEMPT_CANCELING) + return -ECANCELED; + if (!completed) { + attempt->phase = APPLE_DPTX_ATTEMPT_CANCELING; + return -ETIMEDOUT; + } + if (attempt->phase != APPLE_DPTX_ATTEMPT_LINK_CONFIGURED) { + attempt->phase = APPLE_DPTX_ATTEMPT_CANCELING; + return -EIO; + } + + attempt->phase = APPLE_DPTX_ATTEMPT_CONNECTED; + return 0; +} + +static inline bool +apple_dptx_attempt_reset(struct apple_dptx_attempt *attempt, + const struct apple_dptx_attempt_ticket *ticket) +{ + if (ticket->generation != attempt->generation || + attempt->phase == APPLE_DPTX_ATTEMPT_CONNECTED || + attempt->phase == APPLE_DPTX_ATTEMPT_FAILED) + return false; + + attempt->phase = APPLE_DPTX_ATTEMPT_IDLE; + return true; +} + +static inline bool +apple_dptx_attempt_fail(struct apple_dptx_attempt *attempt, + const struct apple_dptx_attempt_ticket *ticket) +{ + if (ticket && ticket->generation != attempt->generation) + return false; + + attempt->phase = APPLE_DPTX_ATTEMPT_FAILED; + return true; +} + +#endif diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 4d041d54d9c366..0876702506c0c2 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -338,8 +338,16 @@ static int dptxport_call_set_active_lane_count(struct apple_epic_service *servic reply->retcode = cpu_to_le32(retcode); reply->lane_count = cpu_to_le64(lane_count); - if (lane_count > 0) - complete(&dptx->linkcfg_completion); + if (lane_count > 0) { + unsigned long flags; + bool wake; + + spin_lock_irqsave(&dptx->attempt_lock, flags); + wake = apple_dptx_attempt_link_configured(&dptx->attempt); + spin_unlock_irqrestore(&dptx->attempt_lock, flags); + if (wake) + complete(&dptx->linkcfg_completion); + } return ret; } @@ -596,6 +604,24 @@ int dptxep_init(struct apple_dcp *dcp) init_completion(&dcp->dptxport[1].enable_completion); init_completion(&dcp->dptxport[0].linkcfg_completion); init_completion(&dcp->dptxport[1].linkcfg_completion); + init_completion(&dcp->dptxport[0].connect_idle); + init_completion(&dcp->dptxport[1].connect_idle); + complete_all(&dcp->dptxport[0].connect_idle); + complete_all(&dcp->dptxport[1].connect_idle); + spin_lock_init(&dcp->dptxport[0].attempt_lock); + spin_lock_init(&dcp->dptxport[1].attempt_lock); + apple_dptx_attempt_init(&dcp->dptxport[0].attempt); + apple_dptx_attempt_init(&dcp->dptxport[1].attempt); + dcp->dptxport[0].connect_inflight = false; + dcp->dptxport[1].connect_inflight = false; + dcp->dptxport[0].cleanup_owned = false; + dcp->dptxport[1].cleanup_owned = false; + dcp->dptxport[0].shutting_down = false; + dcp->dptxport[1].shutting_down = false; + dcp->dptxport[0].connected = false; + dcp->dptxport[1].connected = false; + dcp->dptxport[0].remote_requested = false; + dcp->dptxport[1].remote_requested = false; dcp->dptxep = afk_init(dcp, DPTX_ENDPOINT, dptxep_ops); if (IS_ERR(dcp->dptxep)) diff --git a/drivers/gpu/drm/apple/dptxep.h b/drivers/gpu/drm/apple/dptxep.h index 6ea3a2289d3d71..9bc3490d4f4122 100644 --- a/drivers/gpu/drm/apple/dptxep.h +++ b/drivers/gpu/drm/apple/dptxep.h @@ -3,7 +3,9 @@ #include #include +#include +#include "dptx-attempt.h" #include "dptx-transport.h" enum dptx_apcall { @@ -37,9 +39,11 @@ enum dptx_apcall { struct apple_epic_service; struct dptx_port { - bool enabled, connected; + bool enabled, connected, remote_requested; + bool connect_inflight, cleanup_owned, shutting_down; struct completion enable_completion; struct completion linkcfg_completion; + struct completion connect_idle; u32 unit; struct apple_epic_service *service; union phy_configure_opts phy_ops; @@ -49,6 +53,8 @@ struct dptx_port { u32 link_rate, pending_link_rate; u32 drive_settings[2]; struct apple_dptx_transport transport; + spinlock_t attempt_lock; /* protects attempt */ + struct apple_dptx_attempt attempt; }; int dptxport_validate_connection(struct apple_epic_service *service, u8 core, From 8822dbe9bd1b6a8072ba5a87f9db5af313d0397e Mon Sep 17 00:00:00 2001 From: Matthew Altman Date: Sun, 30 Aug 2026 21:23:47 -0500 Subject: [PATCH 10/12] mux: apple: trace display crossbar routing --- drivers/mux/Makefile | 1 + drivers/mux/apple-display-crossbar.c | 32 +++++++++-- drivers/mux/apple_dpxbar_trace.h | 80 ++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 drivers/mux/apple_dpxbar_trace.h diff --git a/drivers/mux/Makefile b/drivers/mux/Makefile index 7b5b3325068010..c3d550d1cc74b6 100644 --- a/drivers/mux/Makefile +++ b/drivers/mux/Makefile @@ -9,6 +9,7 @@ mux-adgs1408-objs := adgs1408.o mux-gpio-objs := gpio.o mux-mmio-objs := mmio.o mux-apple-display-crossbar-objs := apple-display-crossbar.o +CFLAGS_apple-display-crossbar.o := -I$(src) obj-$(CONFIG_MULTIPLEXER) += mux-core.o obj-$(CONFIG_MUX_ADG792A) += mux-adg792a.o diff --git a/drivers/mux/apple-display-crossbar.c b/drivers/mux/apple-display-crossbar.c index 9b17371d92c3ba..80fad3e1edda4f 100644 --- a/drivers/mux/apple-display-crossbar.c +++ b/drivers/mux/apple-display-crossbar.c @@ -18,6 +18,9 @@ #include #include +#define CREATE_TRACE_POINTS +#include "apple_dpxbar_trace.h" + /* * T602x register interface is cleary different so most of the names below are * probably wrong. @@ -102,10 +105,13 @@ struct apple_dpxbar { static inline void dpxbar_mask32(struct apple_dpxbar *xbar, u32 reg, u32 mask, u32 set) { - u32 value = readl(xbar->regs + reg); + u32 old = readl(xbar->regs + reg); + u32 value = old; + value &= ~mask; value |= set; writel(value, xbar->regs + reg); + trace_apple_dpxbar_rmw(xbar, reg, old, mask, set, value); } static inline void dpxbar_set32(struct apple_dpxbar *xbar, u32 reg, u32 set) @@ -126,6 +132,7 @@ static int apple_dpxbar_set_t602x(struct mux_control *mux, int state) unsigned int mux_state; unsigned int dispext_bit; unsigned int dispext_bit_en; + int selected_before; bool enable; int ret = 0; @@ -143,10 +150,13 @@ static int apple_dpxbar_set_t602x(struct mux_control *mux, int state) mux_state = state; enable = true; } else { + trace_apple_dpxbar_route(dev_name(dpxbar->dev), index, state, + -2, -2, -EINVAL); return -EINVAL; } spin_lock_irqsave(&dpxbar->lock, flags); + selected_before = dpxbar->selected_dispext[index]; /* ensure the selected dispext isn't already used in this crossbar */ if (enable) { @@ -155,6 +165,10 @@ static int apple_dpxbar_set_t602x(struct mux_control *mux, int state) continue; if (dpxbar->selected_dispext[i] == state) { spin_unlock_irqrestore(&dpxbar->lock, flags); + trace_apple_dpxbar_route(dev_name(dpxbar->dev), + index, state, + selected_before, + selected_before, -EBUSY); return -EBUSY; } } @@ -207,8 +221,9 @@ static int apple_dpxbar_set_t602x(struct mux_control *mux, int state) dpxbar->selected_dispext[index] = state; } - spin_unlock_irqrestore(&dpxbar->lock, flags); + trace_apple_dpxbar_route(dev_name(dpxbar->dev), index, state, + selected_before, enable ? state : -1, 0); if (enable) dev_info(dpxbar->dev, "Switched %s to dispext%u,%u\n", @@ -230,6 +245,7 @@ static int apple_dpxbar_set(struct mux_control *mux, int state) unsigned int dispext_bit; unsigned int dispext_bit_en; unsigned int atc_bit; + int selected_before; bool enable; int ret = 0; u32 mux_mask, mux_set; @@ -248,6 +264,8 @@ static int apple_dpxbar_set(struct mux_control *mux, int state) mux_state = state; enable = true; } else { + trace_apple_dpxbar_route(dev_name(dpxbar->dev), index, state, + -2, -2, -EINVAL); return -EINVAL; } @@ -277,10 +295,13 @@ static int apple_dpxbar_set(struct mux_control *mux, int state) atc_bit = ATC_DPIN1; break; default: + trace_apple_dpxbar_route(dev_name(dpxbar->dev), index, state, + -2, -2, -EINVAL); return -EINVAL; } spin_lock_irqsave(&dpxbar->lock, flags); + selected_before = dpxbar->selected_dispext[index]; /* ensure the selected dispext isn't already used in this crossbar */ if (enable) { @@ -289,6 +310,10 @@ static int apple_dpxbar_set(struct mux_control *mux, int state) continue; if (dpxbar->selected_dispext[i] == state) { spin_unlock_irqrestore(&dpxbar->lock, flags); + trace_apple_dpxbar_route(dev_name(dpxbar->dev), + index, state, + selected_before, + selected_before, -EBUSY); return -EBUSY; } } @@ -342,8 +367,9 @@ static int apple_dpxbar_set(struct mux_control *mux, int state) dpxbar->selected_dispext[index] = state; } - spin_unlock_irqrestore(&dpxbar->lock, flags); + trace_apple_dpxbar_route(dev_name(dpxbar->dev), index, state, + selected_before, enable ? state : -1, 0); if (enable) dev_info(dpxbar->dev, "Switched %s to dispext%u,%u\n", diff --git a/drivers/mux/apple_dpxbar_trace.h b/drivers/mux/apple_dpxbar_trace.h new file mode 100644 index 00000000000000..2807f76e70aca3 --- /dev/null +++ b/drivers/mux/apple_dpxbar_trace.h @@ -0,0 +1,80 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ + +#undef TRACE_SYSTEM +#define TRACE_SYSTEM apple_dpxbar + +#if !defined(_TRACE_APPLE_DPXBAR_H) || defined(TRACE_HEADER_MULTI_READ) +#define _TRACE_APPLE_DPXBAR_H + +#include + +TRACE_EVENT(apple_dpxbar_route, + TP_PROTO(const char *devname, unsigned int control, int state, + int selected_before, int selected_after, int ret), + TP_ARGS(devname, control, state, selected_before, selected_after, ret), + + TP_STRUCT__entry(__string(devname, devname) + __field(unsigned int, control) + __field(int, state) + __field(int, dispext) + __field(int, core) + __field(int, selected_before) + __field(int, selected_after) + __field(int, ret) + ), + + TP_fast_assign(__assign_str(devname); + __entry->control = control; + __entry->state = state; + __entry->dispext = state >= 0 ? state >> 1 : -1; + __entry->core = state >= 0 ? state & 1 : -1; + __entry->selected_before = selected_before; + __entry->selected_after = selected_after; + __entry->ret = ret; + ), + + TP_printk("%s: control=%s(%u) state=%d dispext=%d core=%d selected=%d->%d ret=%d", + __get_str(devname), __print_symbolic(__entry->control, + { 0, "dpphy" }, + { 1, "dpin0" }, + { 2, "dpin1" }), + __entry->control, __entry->state, __entry->dispext, + __entry->core, __entry->selected_before, + __entry->selected_after, __entry->ret) +); + +TRACE_EVENT(apple_dpxbar_rmw, + TP_PROTO(const void *xbar, u32 reg, u32 old, u32 mask, u32 set, + u32 value), + TP_ARGS(xbar, reg, old, mask, set, value), + + TP_STRUCT__entry(__field(const void *, xbar) + __field(u32, reg) + __field(u32, old) + __field(u32, mask) + __field(u32, set) + __field(u32, value) + ), + + TP_fast_assign(__entry->xbar = xbar; + __entry->reg = reg; + __entry->old = old; + __entry->mask = mask; + __entry->set = set; + __entry->value = value; + ), + + TP_printk("xbar=%p reg=0x%03x old=0x%08x mask=0x%08x set=0x%08x new=0x%08x", + __entry->xbar, __entry->reg, __entry->old, + __entry->mask, __entry->set, __entry->value) +); + +#endif /* _TRACE_APPLE_DPXBAR_H */ + +#undef TRACE_INCLUDE_PATH +#define TRACE_INCLUDE_PATH . + +#undef TRACE_INCLUDE_FILE +#define TRACE_INCLUDE_FILE apple_dpxbar_trace + +#include From d6f77bd41bdfa35a6858bb5c5b8f80dacf24f3f0 Mon Sep 17 00:00:00 2001 From: Matthew Altman Date: Sun, 30 Aug 2026 21:42:44 -0500 Subject: [PATCH 11/12] drm/apple: model external display source ownership Add a pure, KUnit-only reducer for explicit dcpext engine leases and DP-IN sink ownership. Keep the model unreferenced by production code until J414c engine/core and T602x DPIN routing are proven. A future live integration must compose DPTX attempt failure with an engine-level quarantine or retained lease. --- drivers/gpu/drm/apple/Kconfig | 5 +- drivers/gpu/drm/apple/Makefile | 1 + .../gpu/drm/apple/dcp-display-source-test.c | 286 ++++++++++++++++++ drivers/gpu/drm/apple/dcp-display-source.h | 247 +++++++++++++++ 4 files changed, 537 insertions(+), 2 deletions(-) create mode 100644 drivers/gpu/drm/apple/dcp-display-source-test.c create mode 100644 drivers/gpu/drm/apple/dcp-display-source.h diff --git a/drivers/gpu/drm/apple/Kconfig b/drivers/gpu/drm/apple/Kconfig index c6cbc4c4baa99e..e0144b0fd9eda3 100644 --- a/drivers/gpu/drm/apple/Kconfig +++ b/drivers/gpu/drm/apple/Kconfig @@ -35,7 +35,8 @@ config DRM_APPLE_KUNIT_TEST default KUNIT_ALL_TESTS help This builds unit tests for Apple DPTX transport-independent logic, - including remote-target encoding and the side-effect-free logical - USB4 transport model. These tests are intended for kernel developers. + including remote-target encoding, the side-effect-free logical USB4 + transport model, and the unconnected external-display ownership + reducer. These tests are intended for kernel developers. If in doubt, say "N". diff --git a/drivers/gpu/drm/apple/Makefile b/drivers/gpu/drm/apple/Makefile index da7668b202b130..ab3fe390712565 100644 --- a/drivers/gpu/drm/apple/Makefile +++ b/drivers/gpu/drm/apple/Makefile @@ -7,6 +7,7 @@ appledrm-y += afk.o dcp.o dcp_backlight.o dptxep.o dptx-transport.o appledrm-y += dptx-transport-phy.o iomfb.o parser.o systemep.o appledrm-$(CONFIG_DRM_APPLE_KUNIT_TEST) += dptx-transport-test.o appledrm-$(CONFIG_DRM_APPLE_KUNIT_TEST) += dptx-attempt-test.o +appledrm-$(CONFIG_DRM_APPLE_KUNIT_TEST) += dcp-display-source-test.o appledrm-$(CONFIG_DRM_APPLE_AUDIO) += audio.o appledrm-$(CONFIG_DRM_APPLE_AUDIO) += av.o appledrm-y += connector.o diff --git a/drivers/gpu/drm/apple/dcp-display-source-test.c b/drivers/gpu/drm/apple/dcp-display-source-test.c new file mode 100644 index 00000000000000..afd0aa228b876f --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-display-source-test.c @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT + +#include + +#include "dcp-display-source.h" + +#define TEST_ENGINES 2 + +struct apple_dcp_display_test_ctx { + struct apple_dcp_display_lease leases[TEST_ENGINES]; + struct apple_dcp_display_pool pool; +}; + +static const struct apple_dcp_display_route route_atc2_dpin0 = { + .die = 0, + .atc = 2, + .dpin = 0, + .core = 0, +}; + +static int apple_dcp_display_test_init(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx; + + ctx = kunit_kzalloc(test, sizeof(*ctx), GFP_KERNEL); + if (!ctx) + return -ENOMEM; + apple_dcp_display_pool_init(&ctx->pool, ctx->leases, TEST_ENGINES); + test->priv = ctx; + + return apple_dcp_display_begin_session(&ctx->pool); +} + +static void apple_dcp_display_lifecycle_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_cookie cookie; + + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &cookie), 0); + KUNIT_ASSERT_EQ(test, apple_dcp_display_enable(&ctx->pool, 0, &cookie), 0); + KUNIT_EXPECT_EQ(test, apple_dcp_display_enable(&ctx->pool, 0, &cookie), 0); + KUNIT_EXPECT_EQ(test, apple_dcp_display_release(&ctx->pool, 0, &cookie), + -EBUSY); + KUNIT_ASSERT_EQ(test, apple_dcp_display_disable(&ctx->pool, 0, &cookie), 0); + KUNIT_EXPECT_EQ(test, apple_dcp_display_disable(&ctx->pool, 0, &cookie), 0); + KUNIT_ASSERT_EQ(test, apple_dcp_display_release(&ctx->pool, 0, &cookie), 0); + KUNIT_EXPECT_EQ(test, ctx->leases[0].phase, APPLE_DCP_DISPLAY_IDLE); +} + +static void apple_dcp_display_engine_conflict_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_cookie first, untouched = { .token = 42 }; + + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &first), 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &untouched), -EBUSY); + KUNIT_EXPECT_EQ(test, untouched.token, 42ULL); + KUNIT_EXPECT_TRUE(test, + apple_dcp_display_cookie_equal(&ctx->leases[0].cookie, &first)); +} + +static void apple_dcp_display_sink_conflict_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_route same_sink = route_atc2_dpin0; + struct apple_dcp_display_cookie first, second; + + same_sink.core = 1; + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &first), 0); + KUNIT_ASSERT_EQ(test, apple_dcp_display_enable(&ctx->pool, 0, &first), 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 1, &same_sink, &second), + -EBUSY); +} + +static void apple_dcp_display_independent_dpin_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_route dpin1 = route_atc2_dpin0; + struct apple_dcp_display_cookie first, second; + + dpin1.dpin = 1; + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &first), 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 1, &dpin1, &second), 0); +} + +static void apple_dcp_display_independent_atc_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_route other_atc = route_atc2_dpin0; + struct apple_dcp_display_cookie first, second; + + other_atc.atc = 1; + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &first), 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 1, &other_atc, &second), 0); +} + +static void apple_dcp_display_independent_die_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_route other_die = route_atc2_dpin0; + struct apple_dcp_display_cookie first, second; + + other_die.die = 1; + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &first), 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 1, &other_die, &second), 0); +} + +static void apple_dcp_display_stale_cookie_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_cookie first, second; + + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &first), 0); + KUNIT_ASSERT_EQ(test, apple_dcp_display_release(&ctx->pool, 0, &first), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &second), 0); + KUNIT_EXPECT_EQ(test, apple_dcp_display_enable(&ctx->pool, 0, &first), + -ESTALE); + KUNIT_EXPECT_EQ(test, apple_dcp_display_disable(&ctx->pool, 0, &first), + -ESTALE); + KUNIT_EXPECT_EQ(test, apple_dcp_display_release(&ctx->pool, 0, &first), + -ESTALE); + KUNIT_EXPECT_EQ(test, apple_dcp_display_enable(&ctx->pool, 1, &second), + -ESTALE); + KUNIT_EXPECT_TRUE(test, + apple_dcp_display_cookie_equal(&ctx->leases[0].cookie, &second)); +} + +static void apple_dcp_display_session_stale_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_cookie first, second; + + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &first), 0); + KUNIT_ASSERT_EQ(test, apple_dcp_display_release(&ctx->pool, 0, &first), 0); + KUNIT_ASSERT_TRUE(test, apple_dcp_display_quiesce(&ctx->pool)); + KUNIT_ASSERT_EQ(test, apple_dcp_display_begin_session(&ctx->pool), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &second), 0); + KUNIT_EXPECT_EQ(test, first.token, second.token); + KUNIT_EXPECT_NE(test, first.session, second.session); + KUNIT_EXPECT_EQ(test, apple_dcp_display_release(&ctx->pool, 0, &first), + -ESTALE); +} + +static void apple_dcp_display_quiesce_drain_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_cookie cookie, ignored; + + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &cookie), 0); + KUNIT_EXPECT_TRUE(test, apple_dcp_display_quiesce(&ctx->pool)); + KUNIT_EXPECT_FALSE(test, apple_dcp_display_quiesce(&ctx->pool)); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 1, &route_atc2_dpin0, + &ignored), -ESHUTDOWN); + KUNIT_EXPECT_EQ(test, apple_dcp_display_enable(&ctx->pool, 0, &cookie), + -ESHUTDOWN); + KUNIT_EXPECT_EQ(test, apple_dcp_display_begin_session(&ctx->pool), -EBUSY); + KUNIT_ASSERT_EQ(test, apple_dcp_display_disable(&ctx->pool, 0, &cookie), 0); + KUNIT_ASSERT_EQ(test, apple_dcp_display_release(&ctx->pool, 0, &cookie), 0); + KUNIT_EXPECT_EQ(test, apple_dcp_display_begin_session(&ctx->pool), 0); +} + +static void apple_dcp_display_overflow_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_cookie first, second; + + KUNIT_ASSERT_TRUE(test, apple_dcp_display_quiesce(&ctx->pool)); + ctx->pool.session = ~0ULL; + KUNIT_EXPECT_EQ(test, apple_dcp_display_begin_session(&ctx->pool), + -EOVERFLOW); + ctx->pool.session = 1; + KUNIT_ASSERT_EQ(test, apple_dcp_display_begin_session(&ctx->pool), 0); + ctx->pool.next_token = ~0ULL; + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &first), 0); + KUNIT_EXPECT_EQ(test, first.token, ~0ULL); + KUNIT_ASSERT_EQ(test, apple_dcp_display_release(&ctx->pool, 0, &first), 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &second), -EOVERFLOW); +} + +static void apple_dcp_display_route_validation_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_route invalid = route_atc2_dpin0; + struct apple_dcp_display_cookie cookie; + struct apple_dptx_target target; + u32 encoded; + + invalid.dpin = 2; + target = (struct apple_dptx_target) { .core = 1 }; + KUNIT_EXPECT_EQ(test, + apple_dcp_display_route_to_target(&invalid, &target), -ERANGE); + KUNIT_EXPECT_EQ(test, target.core, 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &invalid, &cookie), + -ERANGE); + invalid = route_atc2_dpin0; + invalid.core = 16; + target = (struct apple_dptx_target) { .core = 1 }; + KUNIT_EXPECT_EQ(test, + apple_dcp_display_route_to_target(&invalid, &target), -ERANGE); + KUNIT_EXPECT_EQ(test, target.core, 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &invalid, &cookie), + -ERANGE); + target = (struct apple_dptx_target) { .core = 1 }; + KUNIT_EXPECT_EQ(test, apple_dcp_display_route_to_target(NULL, &target), + -EINVAL); + KUNIT_EXPECT_EQ(test, target.core, 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, TEST_ENGINES, + &route_atc2_dpin0, &cookie), + -EINVAL); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_enable(&ctx->pool, TEST_ENGINES, &cookie), + -EINVAL); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_disable(&ctx->pool, TEST_ENGINES, &cookie), + -EINVAL); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_release(&ctx->pool, TEST_ENGINES, &cookie), + -EINVAL); + + /* Public traces observed this tuple; it is not J414c route policy. */ + KUNIT_ASSERT_EQ(test, + apple_dcp_display_route_to_target(&route_atc2_dpin0, &target), 0); + KUNIT_ASSERT_EQ(test, apple_dptx_target_encode(&target, &encoded), 0); + KUNIT_EXPECT_EQ(test, target.die, route_atc2_dpin0.die); + KUNIT_EXPECT_EQ(test, target.atc, route_atc2_dpin0.atc); + KUNIT_EXPECT_EQ(test, target.core, route_atc2_dpin0.core); +} + +static struct kunit_case apple_dcp_display_test_cases[] = { + KUNIT_CASE(apple_dcp_display_lifecycle_test), + KUNIT_CASE(apple_dcp_display_engine_conflict_test), + KUNIT_CASE(apple_dcp_display_sink_conflict_test), + KUNIT_CASE(apple_dcp_display_independent_dpin_test), + KUNIT_CASE(apple_dcp_display_independent_atc_test), + KUNIT_CASE(apple_dcp_display_independent_die_test), + KUNIT_CASE(apple_dcp_display_stale_cookie_test), + KUNIT_CASE(apple_dcp_display_session_stale_test), + KUNIT_CASE(apple_dcp_display_quiesce_drain_test), + KUNIT_CASE(apple_dcp_display_overflow_test), + KUNIT_CASE(apple_dcp_display_route_validation_test), + {} +}; + +static struct kunit_suite apple_dcp_display_test_suite = { + .name = "apple-dcp-display-source", + .init = apple_dcp_display_test_init, + .test_cases = apple_dcp_display_test_cases, +}; + +kunit_test_suite(apple_dcp_display_test_suite); diff --git a/drivers/gpu/drm/apple/dcp-display-source.h b/drivers/gpu/drm/apple/dcp-display-source.h new file mode 100644 index 00000000000000..047932a4d422e5 --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-display-source.h @@ -0,0 +1,247 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef __APPLE_DCP_DISPLAY_SOURCE_H__ +#define __APPLE_DCP_DISPLAY_SOURCE_H__ + +#include +#include + +#include "dptx-transport.h" + +enum apple_dcp_display_phase { + APPLE_DCP_DISPLAY_IDLE, + APPLE_DCP_DISPLAY_PREPARED, + APPLE_DCP_DISPLAY_ENABLED, +}; + +struct apple_dcp_display_route { + u8 die; + u8 atc; + u8 dpin; + u8 core; +}; + +struct apple_dcp_display_cookie { + u64 session; + u64 token; +}; + +struct apple_dcp_display_lease { + struct apple_dcp_display_route route; + struct apple_dcp_display_cookie cookie; + enum apple_dcp_display_phase phase; +}; + +struct apple_dcp_display_pool { + struct apple_dcp_display_lease *leases; + unsigned int n_engines; + u64 session; + u64 next_token; + bool quiescing; +}; + +/* All pool operations require serialization by the eventual owner. */ + +static inline bool +apple_dcp_display_cookie_equal(const struct apple_dcp_display_cookie *a, + const struct apple_dcp_display_cookie *b) +{ + return a->session == b->session && a->token == b->token; +} + +static inline bool +apple_dcp_display_sink_equal(const struct apple_dcp_display_route *a, + const struct apple_dcp_display_route *b) +{ + /* A physical DP-IN mux control can select only one source/core. */ + return a->die == b->die && a->atc == b->atc && a->dpin == b->dpin; +} + +static inline int +apple_dcp_display_route_to_target(const struct apple_dcp_display_route *route, + struct apple_dptx_target *target) +{ + u32 ignored; + int ret; + + if (!target) + return -EINVAL; + *target = (struct apple_dptx_target) {}; + if (!route) + return -EINVAL; + if (route->dpin > 1) + return -ERANGE; + + *target = (struct apple_dptx_target) { + .core = route->core, + .atc = route->atc, + .die = route->die, + }; + ret = apple_dptx_target_encode(target, &ignored); + if (ret) + *target = (struct apple_dptx_target) {}; + + return ret; +} + +static inline void +apple_dcp_display_pool_init(struct apple_dcp_display_pool *pool, + struct apple_dcp_display_lease *leases, + unsigned int n_engines) +{ + unsigned int i; + + /* The owner supplies non-NULL storage for exactly n_engines leases. */ + *pool = (struct apple_dcp_display_pool) { + .leases = leases, + .n_engines = n_engines, + .next_token = 1, + .quiescing = true, + }; + for (i = 0; i < n_engines; i++) + leases[i] = (struct apple_dcp_display_lease) {}; +} + +static inline int +apple_dcp_display_begin_session(struct apple_dcp_display_pool *pool) +{ + unsigned int i; + + if (!pool || !pool->leases || !pool->n_engines) + return -EINVAL; + if (!pool->quiescing) + return -EBUSY; + for (i = 0; i < pool->n_engines; i++) { + if (pool->leases[i].phase != APPLE_DCP_DISPLAY_IDLE) + return -EBUSY; + } + if (pool->session == ~0ULL) + return -EOVERFLOW; + + pool->session++; + pool->next_token = 1; + pool->quiescing = false; + + return 0; +} + +static inline bool +apple_dcp_display_quiesce(struct apple_dcp_display_pool *pool) +{ + bool changed; + + if (!pool) + return false; + + changed = !pool->quiescing; + pool->quiescing = true; + return changed; +} + +static inline int +apple_dcp_display_prepare(struct apple_dcp_display_pool *pool, + unsigned int engine, + const struct apple_dcp_display_route *route, + struct apple_dcp_display_cookie *cookie) +{ + struct apple_dcp_display_lease *lease; + struct apple_dptx_target target; + unsigned int i; + int ret; + + if (!pool || !pool->leases || engine >= pool->n_engines || !cookie) + return -EINVAL; + if (pool->quiescing) + return -ESHUTDOWN; + + ret = apple_dcp_display_route_to_target(route, &target); + if (ret) + return ret; + + lease = &pool->leases[engine]; + if (lease->phase != APPLE_DCP_DISPLAY_IDLE) + return -EBUSY; + + for (i = 0; i < pool->n_engines; i++) { + if (i != engine && + pool->leases[i].phase != APPLE_DCP_DISPLAY_IDLE && + apple_dcp_display_sink_equal(&pool->leases[i].route, route)) + return -EBUSY; + } + if (!pool->next_token) + return -EOVERFLOW; + + lease->route = *route; + lease->cookie.session = pool->session; + lease->cookie.token = pool->next_token++; + lease->phase = APPLE_DCP_DISPLAY_PREPARED; + *cookie = lease->cookie; + + return 0; +} + +static inline int +apple_dcp_display_enable(struct apple_dcp_display_pool *pool, + unsigned int engine, + const struct apple_dcp_display_cookie *cookie) +{ + struct apple_dcp_display_lease *lease; + + if (!pool || !pool->leases || engine >= pool->n_engines || !cookie) + return -EINVAL; + lease = &pool->leases[engine]; + if (!apple_dcp_display_cookie_equal(&lease->cookie, cookie)) + return -ESTALE; + if (lease->phase == APPLE_DCP_DISPLAY_ENABLED) + return 0; + if (pool->quiescing) + return -ESHUTDOWN; + if (lease->phase != APPLE_DCP_DISPLAY_PREPARED) + return -EINVAL; + + lease->phase = APPLE_DCP_DISPLAY_ENABLED; + return 0; +} + +static inline int +apple_dcp_display_disable(struct apple_dcp_display_pool *pool, + unsigned int engine, + const struct apple_dcp_display_cookie *cookie) +{ + struct apple_dcp_display_lease *lease; + + if (!pool || !pool->leases || engine >= pool->n_engines || !cookie) + return -EINVAL; + lease = &pool->leases[engine]; + if (!apple_dcp_display_cookie_equal(&lease->cookie, cookie)) + return -ESTALE; + if (lease->phase == APPLE_DCP_DISPLAY_PREPARED) + return 0; + if (lease->phase != APPLE_DCP_DISPLAY_ENABLED) + return -EINVAL; + + lease->phase = APPLE_DCP_DISPLAY_PREPARED; + return 0; +} + +static inline int +apple_dcp_display_release(struct apple_dcp_display_pool *pool, + unsigned int engine, + const struct apple_dcp_display_cookie *cookie) +{ + struct apple_dcp_display_lease *lease; + + if (!pool || !pool->leases || engine >= pool->n_engines || !cookie) + return -EINVAL; + lease = &pool->leases[engine]; + if (!apple_dcp_display_cookie_equal(&lease->cookie, cookie)) + return -ESTALE; + if (lease->phase == APPLE_DCP_DISPLAY_ENABLED) + return -EBUSY; + if (lease->phase != APPLE_DCP_DISPLAY_PREPARED) + return -EINVAL; + + *lease = (struct apple_dcp_display_lease) {}; + return 0; +} + +#endif /* __APPLE_DCP_DISPLAY_SOURCE_H__ */ From 5c1b036f12165f993b33bd37aab0c82f579c6079 Mon Sep 17 00:00:00 2001 From: Matthew Altman Date: Sun, 30 Aug 2026 22:13:06 -0500 Subject: [PATCH 12/12] drm/apple: harden display source allocation recovery Model provider-selected ordered engine allocation without exposing engine policy to Thunderbolt. Add terminal sink-preserving quarantine and an owner-ticketed DPTX endpoint restart barrier that rejects active leases and stale completions.\n\nThis remains KUnit-only and has no production call sites, DT changes, MMIO changes, or hardware validation. --- .../gpu/drm/apple/dcp-display-source-test.c | 473 +++++++++++++++++- drivers/gpu/drm/apple/dcp-display-source.h | 213 +++++++- 2 files changed, 674 insertions(+), 12 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-display-source-test.c b/drivers/gpu/drm/apple/dcp-display-source-test.c index afd0aa228b876f..15d0290c14da16 100644 --- a/drivers/gpu/drm/apple/dcp-display-source-test.c +++ b/drivers/gpu/drm/apple/dcp-display-source-test.c @@ -18,6 +18,12 @@ static const struct apple_dcp_display_route route_atc2_dpin0 = { .core = 0, }; +static const struct apple_dcp_display_sink sink_atc2_dpin0 = { + .die = 0, + .atc = 2, + .dpin = 0, +}; + static int apple_dcp_display_test_init(struct kunit *test) { struct apple_dcp_display_test_ctx *ctx; @@ -210,6 +216,446 @@ static void apple_dcp_display_overflow_test(struct kunit *test) &second), -EOVERFLOW); } +static void apple_dcp_display_acquire_order_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + const struct apple_dcp_display_candidate candidates[] = { + { .engine = 1, .core = 3 }, + { .engine = 0, .core = 2 }, + }; + struct apple_dcp_display_allocation allocation; + + KUNIT_ASSERT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &sink_atc2_dpin0, + candidates, ARRAY_SIZE(candidates), + &allocation), 0); + KUNIT_EXPECT_EQ(test, allocation.engine, 1U); + KUNIT_EXPECT_EQ(test, allocation.route.die, sink_atc2_dpin0.die); + KUNIT_EXPECT_EQ(test, allocation.route.atc, sink_atc2_dpin0.atc); + KUNIT_EXPECT_EQ(test, allocation.route.dpin, sink_atc2_dpin0.dpin); + KUNIT_EXPECT_EQ(test, allocation.route.core, 3); + KUNIT_EXPECT_TRUE(test, + apple_dcp_display_cookie_equal(&allocation.cookie, + &ctx->leases[1].cookie)); + KUNIT_EXPECT_EQ(test, ctx->leases[1].phase, + APPLE_DCP_DISPLAY_PREPARED); +} + +static void apple_dcp_display_acquire_fallback_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + const struct apple_dcp_display_candidate candidates[] = { + { .engine = 0, .core = 0 }, + { .engine = 1, .core = 1 }, + }; + struct apple_dcp_display_route other_sink = route_atc2_dpin0; + struct apple_dcp_display_allocation allocation; + struct apple_dcp_display_cookie busy; + + other_sink.atc = 1; + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &other_sink, &busy), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &sink_atc2_dpin0, + candidates, ARRAY_SIZE(candidates), + &allocation), 0); + KUNIT_EXPECT_EQ(test, allocation.engine, 1U); + KUNIT_EXPECT_EQ(test, allocation.route.core, 1); +} + +static void apple_dcp_display_acquire_atomic_validation_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + const struct apple_dcp_display_candidate candidates[] = { + { .engine = 0, .core = 0 }, + { .engine = 1, .core = 16 }, + }; + struct apple_dcp_display_allocation allocation = { + .engine = 7, + .route = { .die = 7, .atc = 7, .dpin = 7, .core = 7 }, + .cookie = { .session = 7, .token = 7 }, + }; + u64 next_token = ctx->pool.next_token; + + KUNIT_EXPECT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &sink_atc2_dpin0, + candidates, ARRAY_SIZE(candidates), + &allocation), -ERANGE); + KUNIT_EXPECT_EQ(test, allocation.engine, 0U); + KUNIT_EXPECT_EQ(test, allocation.route.core, 0); + KUNIT_EXPECT_EQ(test, allocation.cookie.session, 0ULL); + KUNIT_EXPECT_EQ(test, allocation.cookie.token, 0ULL); + KUNIT_EXPECT_EQ(test, ctx->pool.next_token, next_token); + KUNIT_EXPECT_EQ(test, ctx->leases[0].phase, APPLE_DCP_DISPLAY_IDLE); + KUNIT_EXPECT_EQ(test, ctx->leases[1].phase, APPLE_DCP_DISPLAY_IDLE); +} + +static void apple_dcp_display_acquire_input_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + const struct apple_dcp_display_candidate duplicate[] = { + { .engine = 0, .core = 4 }, + { .engine = 0, .core = 5 }, + }; + const struct apple_dcp_display_candidate invalid_engine = { + .engine = TEST_ENGINES, + }; + struct apple_dcp_display_sink opaque_dpin = sink_atc2_dpin0; + struct apple_dcp_display_allocation allocation; + + KUNIT_EXPECT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &sink_atc2_dpin0, + duplicate, 0, &allocation), -EINVAL); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &sink_atc2_dpin0, + NULL, 1, &allocation), -EINVAL); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, NULL, duplicate, + ARRAY_SIZE(duplicate), &allocation), + -EINVAL); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &sink_atc2_dpin0, + &invalid_engine, 1, &allocation), + -EINVAL); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &sink_atc2_dpin0, + duplicate, ARRAY_SIZE(duplicate), NULL), + -EINVAL); + + KUNIT_ASSERT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &sink_atc2_dpin0, + duplicate, ARRAY_SIZE(duplicate), + &allocation), 0); + KUNIT_EXPECT_EQ(test, allocation.engine, 0U); + KUNIT_EXPECT_EQ(test, allocation.route.core, 4); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_release(&ctx->pool, allocation.engine, + &allocation.cookie), 0); + + /* DP-IN is opaque identity here; the topology provider owns its range. */ + opaque_dpin.dpin = 42; + KUNIT_ASSERT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &opaque_dpin, duplicate, + ARRAY_SIZE(duplicate), &allocation), 0); + KUNIT_EXPECT_EQ(test, allocation.route.dpin, 42); +} + +static void apple_dcp_display_acquire_busy_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + const struct apple_dcp_display_candidate candidates[] = { + { .engine = 0, .core = 0 }, + { .engine = 1, .core = 1 }, + }; + struct apple_dcp_display_route first_route = route_atc2_dpin0; + struct apple_dcp_display_route second_route = route_atc2_dpin0; + struct apple_dcp_display_allocation allocation = { .engine = 7 }; + struct apple_dcp_display_cookie first, second; + + first_route.atc = 0; + second_route.atc = 1; + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &first_route, &first), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 1, &second_route, &second), 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &sink_atc2_dpin0, + candidates, ARRAY_SIZE(candidates), + &allocation), -EBUSY); + KUNIT_EXPECT_EQ(test, allocation.engine, 0U); +} + +static void apple_dcp_display_acquire_quarantine_fallback_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + const struct apple_dcp_display_candidate candidates[] = { + { .engine = 0, .core = 0 }, + { .engine = 1, .core = 1 }, + }; + struct apple_dcp_display_route poisoned_sink = route_atc2_dpin0; + struct apple_dcp_display_sink independent_sink = sink_atc2_dpin0; + struct apple_dcp_display_allocation allocation; + struct apple_dcp_display_cookie cookie; + + poisoned_sink.atc = 0; + independent_sink.atc = 1; + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &poisoned_sink, + &cookie), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_quarantine(&ctx->pool, 0, &cookie), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &independent_sink, + candidates, ARRAY_SIZE(candidates), + &allocation), 0); + KUNIT_EXPECT_EQ(test, allocation.engine, 1U); + KUNIT_EXPECT_EQ(test, allocation.route.core, 1); +} + +static void apple_dcp_display_acquire_shutdown_overflow_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + const struct apple_dcp_display_candidate candidate = { + .engine = 0, + .core = 0, + }; + struct apple_dcp_display_allocation allocation = { .engine = 7 }; + + KUNIT_ASSERT_TRUE(test, apple_dcp_display_quiesce(&ctx->pool)); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &sink_atc2_dpin0, + &candidate, 1, &allocation), + -ESHUTDOWN); + KUNIT_EXPECT_EQ(test, allocation.engine, 0U); + KUNIT_ASSERT_EQ(test, apple_dcp_display_begin_session(&ctx->pool), 0); + ctx->pool.next_token = 0; + allocation.engine = 7; + KUNIT_EXPECT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &sink_atc2_dpin0, + &candidate, 1, &allocation), + -EOVERFLOW); + KUNIT_EXPECT_EQ(test, allocation.engine, 0U); +} + +static void apple_dcp_display_quarantine_lifecycle_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_cookie cookie, stale; + struct apple_dcp_display_lease before; + + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &cookie), 0); + before = ctx->leases[0]; + stale = cookie; + stale.token++; + KUNIT_EXPECT_EQ(test, + apple_dcp_display_quarantine(&ctx->pool, 0, &stale), -ESTALE); + KUNIT_EXPECT_EQ(test, ctx->leases[0].phase, + APPLE_DCP_DISPLAY_PREPARED); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_quarantine(&ctx->pool, 0, &cookie), 0); + KUNIT_EXPECT_EQ(test, ctx->leases[0].route.atc, before.route.atc); + KUNIT_EXPECT_TRUE(test, + apple_dcp_display_cookie_equal(&ctx->leases[0].cookie, + &before.cookie)); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_quarantine(&ctx->pool, 0, &cookie), 0); + KUNIT_EXPECT_EQ(test, apple_dcp_display_enable(&ctx->pool, 0, &cookie), + -EIO); + KUNIT_EXPECT_EQ(test, apple_dcp_display_disable(&ctx->pool, 0, &cookie), + -EIO); + KUNIT_EXPECT_EQ(test, apple_dcp_display_release(&ctx->pool, 0, &cookie), + -EIO); + KUNIT_EXPECT_EQ(test, apple_dcp_display_enable(&ctx->pool, 0, &stale), + -ESTALE); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_quarantine(&ctx->pool, 1, &cookie), -ESTALE); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_quarantine(&ctx->pool, 1, NULL), -EINVAL); +} + +static void apple_dcp_display_quarantine_enabled_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_cookie cookie; + + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &cookie), 0); + KUNIT_ASSERT_EQ(test, apple_dcp_display_enable(&ctx->pool, 0, &cookie), 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_quarantine(&ctx->pool, 0, &cookie), 0); + KUNIT_EXPECT_EQ(test, ctx->leases[0].phase, + APPLE_DCP_DISPLAY_QUARANTINED); +} + +static void apple_dcp_display_quarantine_conflict_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + const struct apple_dcp_display_candidate candidate = { + .engine = 1, + .core = 1, + }; + struct apple_dcp_display_allocation allocation; + struct apple_dcp_display_cookie cookie; + struct apple_dcp_display_route other_sink = route_atc2_dpin0; + struct apple_dcp_display_cookie ignored; + + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &cookie), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_quarantine(&ctx->pool, 0, &cookie), 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &sink_atc2_dpin0, + &candidate, 1, &allocation), -EIO); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 1, &route_atc2_dpin0, + &ignored), -EIO); + other_sink.atc = 1; + KUNIT_EXPECT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &other_sink, &ignored), + -EIO); +} + +static void apple_dcp_display_all_quarantined_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + const struct apple_dcp_display_candidate candidates[] = { + { .engine = 0, .core = 0 }, + { .engine = 1, .core = 1 }, + }; + struct apple_dcp_display_route first_route = route_atc2_dpin0; + struct apple_dcp_display_route second_route = route_atc2_dpin0; + struct apple_dcp_display_sink third_sink = sink_atc2_dpin0; + struct apple_dcp_display_allocation allocation; + struct apple_dcp_display_cookie first, second; + + first_route.atc = 0; + second_route.atc = 1; + third_sink.atc = 3; + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &first_route, &first), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 1, &second_route, &second), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_quarantine(&ctx->pool, 0, &first), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_quarantine(&ctx->pool, 1, &second), 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &third_sink, candidates, + ARRAY_SIZE(candidates), &allocation), -EIO); + KUNIT_ASSERT_TRUE(test, apple_dcp_display_quiesce(&ctx->pool)); + KUNIT_EXPECT_EQ(test, apple_dcp_display_begin_session(&ctx->pool), -EIO); +} + +static void apple_dcp_display_mixed_quarantine_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + const struct apple_dcp_display_candidate candidates[] = { + { .engine = 0, .core = 0 }, + { .engine = 1, .core = 1 }, + }; + struct apple_dcp_display_route first_route = route_atc2_dpin0; + struct apple_dcp_display_route second_route = route_atc2_dpin0; + struct apple_dcp_display_sink third_sink = sink_atc2_dpin0; + struct apple_dcp_display_allocation allocation; + struct apple_dcp_display_cookie first, second; + + first_route.atc = 0; + second_route.atc = 1; + third_sink.atc = 3; + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &first_route, &first), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 1, &second_route, &second), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_quarantine(&ctx->pool, 0, &first), 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_acquire(&ctx->pool, &third_sink, candidates, + ARRAY_SIZE(candidates), &allocation), + -EBUSY); + KUNIT_ASSERT_TRUE(test, apple_dcp_display_quiesce(&ctx->pool)); + /* A terminal quarantine takes precedence over a drainable lease. */ + KUNIT_EXPECT_EQ(test, apple_dcp_display_begin_session(&ctx->pool), -EIO); +} + +static void apple_dcp_display_restart_epoch_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_cookie old, fresh; + struct apple_dcp_display_restart_ticket ticket = {}; + u64 session; + + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, &old), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_quarantine(&ctx->pool, 0, &old), 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_endpoint_restarted(&ctx->pool, &ticket), + -EBUSY); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_quiesce_for_restart(&ctx->pool, &ticket), 0); + session = ctx->pool.session; + KUNIT_ASSERT_EQ(test, + apple_dcp_display_endpoint_restarted(&ctx->pool, &ticket), 0); + KUNIT_EXPECT_EQ(test, ctx->pool.session, session); + KUNIT_EXPECT_TRUE(test, ctx->pool.quiescing); + KUNIT_EXPECT_EQ(test, ctx->leases[0].phase, APPLE_DCP_DISPLAY_IDLE); + KUNIT_ASSERT_EQ(test, apple_dcp_display_begin_session(&ctx->pool), 0); + KUNIT_EXPECT_EQ(test, ctx->pool.session, session + 1); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &fresh), 0); + KUNIT_EXPECT_EQ(test, old.token, fresh.token); + KUNIT_EXPECT_NE(test, old.session, fresh.session); + KUNIT_EXPECT_EQ(test, apple_dcp_display_release(&ctx->pool, 0, &old), + -ESTALE); +} + +static void apple_dcp_display_restart_exhaustion_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_restart_ticket ticket; + + ctx->pool.session = ~0ULL; + KUNIT_ASSERT_EQ(test, + apple_dcp_display_quiesce_for_restart(&ctx->pool, &ticket), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_endpoint_restarted(&ctx->pool, &ticket), 0); + KUNIT_EXPECT_EQ(test, apple_dcp_display_begin_session(&ctx->pool), + -EOVERFLOW); +} + +static void apple_dcp_display_restart_active_rejected_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_route other_route = route_atc2_dpin0; + struct apple_dcp_display_restart_ticket ticket; + struct apple_dcp_display_cookie prepared, enabled; + + other_route.atc = 1; + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 0, &route_atc2_dpin0, + &prepared), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_prepare(&ctx->pool, 1, &other_route, &enabled), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_enable(&ctx->pool, 1, &enabled), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_quiesce_for_restart(&ctx->pool, &ticket), 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_endpoint_restarted(&ctx->pool, &ticket), + -EBUSY); + KUNIT_EXPECT_EQ(test, ctx->leases[0].phase, + APPLE_DCP_DISPLAY_PREPARED); + KUNIT_EXPECT_EQ(test, ctx->leases[1].phase, + APPLE_DCP_DISPLAY_ENABLED); + KUNIT_EXPECT_TRUE(test, ctx->pool.restart_armed); +} + +static void apple_dcp_display_restart_ticket_stale_test(struct kunit *test) +{ + struct apple_dcp_display_test_ctx *ctx = test->priv; + struct apple_dcp_display_restart_ticket old, fresh; + + KUNIT_ASSERT_EQ(test, + apple_dcp_display_quiesce_for_restart(&ctx->pool, &old), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_endpoint_restarted(&ctx->pool, &old), 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_endpoint_restarted(&ctx->pool, &old), -ESTALE); + KUNIT_ASSERT_EQ(test, apple_dcp_display_begin_session(&ctx->pool), 0); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_quiesce_for_restart(&ctx->pool, &fresh), 0); + KUNIT_EXPECT_EQ(test, + apple_dcp_display_endpoint_restarted(&ctx->pool, &old), -ESTALE); + KUNIT_EXPECT_TRUE(test, ctx->pool.restart_armed); + KUNIT_ASSERT_EQ(test, + apple_dcp_display_endpoint_restarted(&ctx->pool, &fresh), 0); +} + static void apple_dcp_display_route_validation_test(struct kunit *test) { struct apple_dcp_display_test_ctx *ctx = test->priv; @@ -218,7 +664,7 @@ static void apple_dcp_display_route_validation_test(struct kunit *test) struct apple_dptx_target target; u32 encoded; - invalid.dpin = 2; + invalid.core = 16; target = (struct apple_dptx_target) { .core = 1 }; KUNIT_EXPECT_EQ(test, apple_dcp_display_route_to_target(&invalid, &target), -ERANGE); @@ -227,14 +673,11 @@ static void apple_dcp_display_route_validation_test(struct kunit *test) apple_dcp_display_prepare(&ctx->pool, 0, &invalid, &cookie), -ERANGE); invalid = route_atc2_dpin0; - invalid.core = 16; + invalid.dpin = 42; target = (struct apple_dptx_target) { .core = 1 }; KUNIT_EXPECT_EQ(test, - apple_dcp_display_route_to_target(&invalid, &target), -ERANGE); - KUNIT_EXPECT_EQ(test, target.core, 0); - KUNIT_EXPECT_EQ(test, - apple_dcp_display_prepare(&ctx->pool, 0, &invalid, &cookie), - -ERANGE); + apple_dcp_display_route_to_target(&invalid, &target), 0); + KUNIT_EXPECT_EQ(test, target.core, invalid.core); target = (struct apple_dptx_target) { .core = 1 }; KUNIT_EXPECT_EQ(test, apple_dcp_display_route_to_target(NULL, &target), -EINVAL); @@ -273,6 +716,22 @@ static struct kunit_case apple_dcp_display_test_cases[] = { KUNIT_CASE(apple_dcp_display_session_stale_test), KUNIT_CASE(apple_dcp_display_quiesce_drain_test), KUNIT_CASE(apple_dcp_display_overflow_test), + KUNIT_CASE(apple_dcp_display_acquire_order_test), + KUNIT_CASE(apple_dcp_display_acquire_fallback_test), + KUNIT_CASE(apple_dcp_display_acquire_atomic_validation_test), + KUNIT_CASE(apple_dcp_display_acquire_input_test), + KUNIT_CASE(apple_dcp_display_acquire_busy_test), + KUNIT_CASE(apple_dcp_display_acquire_quarantine_fallback_test), + KUNIT_CASE(apple_dcp_display_acquire_shutdown_overflow_test), + KUNIT_CASE(apple_dcp_display_quarantine_lifecycle_test), + KUNIT_CASE(apple_dcp_display_quarantine_enabled_test), + KUNIT_CASE(apple_dcp_display_quarantine_conflict_test), + KUNIT_CASE(apple_dcp_display_all_quarantined_test), + KUNIT_CASE(apple_dcp_display_mixed_quarantine_test), + KUNIT_CASE(apple_dcp_display_restart_epoch_test), + KUNIT_CASE(apple_dcp_display_restart_exhaustion_test), + KUNIT_CASE(apple_dcp_display_restart_active_rejected_test), + KUNIT_CASE(apple_dcp_display_restart_ticket_stale_test), KUNIT_CASE(apple_dcp_display_route_validation_test), {} }; diff --git a/drivers/gpu/drm/apple/dcp-display-source.h b/drivers/gpu/drm/apple/dcp-display-source.h index 047932a4d422e5..6bfbdade73d4fb 100644 --- a/drivers/gpu/drm/apple/dcp-display-source.h +++ b/drivers/gpu/drm/apple/dcp-display-source.h @@ -11,6 +11,13 @@ enum apple_dcp_display_phase { APPLE_DCP_DISPLAY_IDLE, APPLE_DCP_DISPLAY_PREPARED, APPLE_DCP_DISPLAY_ENABLED, + APPLE_DCP_DISPLAY_QUARANTINED, +}; + +struct apple_dcp_display_sink { + u8 die; + u8 atc; + u8 dpin; }; struct apple_dcp_display_route { @@ -25,6 +32,22 @@ struct apple_dcp_display_cookie { u64 token; }; +struct apple_dcp_display_candidate { + unsigned int engine; + u8 core; +}; + +struct apple_dcp_display_allocation { + unsigned int engine; + struct apple_dcp_display_route route; + struct apple_dcp_display_cookie cookie; +}; + +struct apple_dcp_display_restart_ticket { + u64 session; + u64 generation; +}; + struct apple_dcp_display_lease { struct apple_dcp_display_route route; struct apple_dcp_display_cookie cookie; @@ -36,7 +59,9 @@ struct apple_dcp_display_pool { unsigned int n_engines; u64 session; u64 next_token; + u64 restart_generation; bool quiescing; + bool restart_armed; }; /* All pool operations require serialization by the eventual owner. */ @@ -68,8 +93,6 @@ apple_dcp_display_route_to_target(const struct apple_dcp_display_route *route, *target = (struct apple_dptx_target) {}; if (!route) return -EINVAL; - if (route->dpin > 1) - return -ERANGE; *target = (struct apple_dptx_target) { .core = route->core, @@ -90,7 +113,11 @@ apple_dcp_display_pool_init(struct apple_dcp_display_pool *pool, { unsigned int i; - /* The owner supplies non-NULL storage for exactly n_engines leases. */ + /* + * Construction/object-lifetime reset only. Runtime recovery must use + * endpoint_restarted() so stale cookies cannot alias a new session. + * The owner supplies non-NULL storage for exactly n_engines leases. + */ *pool = (struct apple_dcp_display_pool) { .leases = leases, .n_engines = n_engines, @@ -104,16 +131,23 @@ apple_dcp_display_pool_init(struct apple_dcp_display_pool *pool, static inline int apple_dcp_display_begin_session(struct apple_dcp_display_pool *pool) { + bool busy = false; unsigned int i; if (!pool || !pool->leases || !pool->n_engines) return -EINVAL; if (!pool->quiescing) return -EBUSY; + if (pool->restart_armed) + return -EBUSY; for (i = 0; i < pool->n_engines; i++) { + if (pool->leases[i].phase == APPLE_DCP_DISPLAY_QUARANTINED) + return -EIO; if (pool->leases[i].phase != APPLE_DCP_DISPLAY_IDLE) - return -EBUSY; + busy = true; } + if (busy) + return -EBUSY; if (pool->session == ~0ULL) return -EOVERFLOW; @@ -137,6 +171,62 @@ apple_dcp_display_quiesce(struct apple_dcp_display_pool *pool) return changed; } +static inline int +apple_dcp_display_quiesce_for_restart( + struct apple_dcp_display_pool *pool, + struct apple_dcp_display_restart_ticket *ticket) +{ + if (!ticket) + return -EINVAL; + *ticket = (struct apple_dcp_display_restart_ticket) {}; + if (!pool || !pool->leases || !pool->n_engines) + return -EINVAL; + if (pool->restart_generation == ~0ULL) + return -EOVERFLOW; + + pool->quiescing = true; + pool->restart_generation++; + pool->restart_armed = true; + *ticket = (struct apple_dcp_display_restart_ticket) { + .session = pool->session, + .generation = pool->restart_generation, + }; + + return 0; +} + +static inline int +apple_dcp_display_endpoint_restarted( + struct apple_dcp_display_pool *pool, + const struct apple_dcp_display_restart_ticket *ticket) +{ + unsigned int i; + + if (!pool || !pool->leases || !pool->n_engines || !ticket) + return -EINVAL; + if (!pool->quiescing) + return -EBUSY; + if (!pool->restart_armed || ticket->session != pool->session || + ticket->generation != pool->restart_generation) + return -ESTALE; + for (i = 0; i < pool->n_engines; i++) { + if (pool->leases[i].phase == APPLE_DCP_DISPLAY_PREPARED || + pool->leases[i].phase == APPLE_DCP_DISPLAY_ENABLED) + return -EBUSY; + } + + /* + * The Apple display owner may attest this only after the DPTX endpoint + * has restarted and all callbacks and work using old allocations drained. + * Preserve session so begin_session() advances the epoch before reuse. + */ + for (i = 0; i < pool->n_engines; i++) + pool->leases[i] = (struct apple_dcp_display_lease) {}; + pool->restart_armed = false; + + return 0; +} + static inline int apple_dcp_display_prepare(struct apple_dcp_display_pool *pool, unsigned int engine, @@ -158,14 +248,20 @@ apple_dcp_display_prepare(struct apple_dcp_display_pool *pool, return ret; lease = &pool->leases[engine]; + if (lease->phase == APPLE_DCP_DISPLAY_QUARANTINED) + return -EIO; if (lease->phase != APPLE_DCP_DISPLAY_IDLE) return -EBUSY; for (i = 0; i < pool->n_engines; i++) { if (i != engine && pool->leases[i].phase != APPLE_DCP_DISPLAY_IDLE && - apple_dcp_display_sink_equal(&pool->leases[i].route, route)) + apple_dcp_display_sink_equal(&pool->leases[i].route, route)) { + if (pool->leases[i].phase == + APPLE_DCP_DISPLAY_QUARANTINED) + return -EIO; return -EBUSY; + } } if (!pool->next_token) return -EOVERFLOW; @@ -179,6 +275,107 @@ apple_dcp_display_prepare(struct apple_dcp_display_pool *pool, return 0; } +/* + * Apple-provider-internal selection. The provider derives candidates from + * topology; a Thunderbolt requester must not choose an engine or DPTX core. + */ +static inline int +apple_dcp_display_acquire(struct apple_dcp_display_pool *pool, + const struct apple_dcp_display_sink *sink, + const struct apple_dcp_display_candidate *candidates, + unsigned int n_candidates, + struct apple_dcp_display_allocation *allocation) +{ + struct apple_dcp_display_route route; + struct apple_dcp_display_cookie cookie; + struct apple_dptx_target target; + bool all_quarantined = true; + unsigned int i; + int ret; + + if (!allocation) + return -EINVAL; + *allocation = (struct apple_dcp_display_allocation) {}; + if (!pool || !pool->leases || !pool->n_engines || !sink || + !candidates || !n_candidates) + return -EINVAL; + + /* Reject malformed topology atomically, including later candidates. */ + for (i = 0; i < n_candidates; i++) { + if (candidates[i].engine >= pool->n_engines) + return -EINVAL; + route = (struct apple_dcp_display_route) { + .die = sink->die, + .atc = sink->atc, + .dpin = sink->dpin, + .core = candidates[i].core, + }; + ret = apple_dcp_display_route_to_target(&route, &target); + if (ret) + return ret; + } + + if (pool->quiescing) + return -ESHUTDOWN; + + route = (struct apple_dcp_display_route) { + .die = sink->die, + .atc = sink->atc, + .dpin = sink->dpin, + }; + for (i = 0; i < pool->n_engines; i++) { + if (pool->leases[i].phase == APPLE_DCP_DISPLAY_IDLE || + !apple_dcp_display_sink_equal(&pool->leases[i].route, &route)) + continue; + return pool->leases[i].phase == APPLE_DCP_DISPLAY_QUARANTINED ? + -EIO : -EBUSY; + } + + for (i = 0; i < n_candidates; i++) { + if (pool->leases[candidates[i].engine].phase == + APPLE_DCP_DISPLAY_IDLE) { + route.core = candidates[i].core; + ret = apple_dcp_display_prepare(pool, candidates[i].engine, + &route, &cookie); + if (ret) + return ret; + *allocation = (struct apple_dcp_display_allocation) { + .engine = candidates[i].engine, + .route = route, + .cookie = cookie, + }; + return 0; + } + if (pool->leases[candidates[i].engine].phase != + APPLE_DCP_DISPLAY_QUARANTINED) + all_quarantined = false; + } + + return all_quarantined ? -EIO : -EBUSY; +} + +static inline int +apple_dcp_display_quarantine(struct apple_dcp_display_pool *pool, + unsigned int engine, + const struct apple_dcp_display_cookie *cookie) +{ + struct apple_dcp_display_lease *lease; + + if (!pool || !pool->leases || engine >= pool->n_engines || !cookie) + return -EINVAL; + lease = &pool->leases[engine]; + if (!apple_dcp_display_cookie_equal(&lease->cookie, cookie)) + return -ESTALE; + if (lease->phase == APPLE_DCP_DISPLAY_QUARANTINED) + return 0; + if (lease->phase != APPLE_DCP_DISPLAY_PREPARED && + lease->phase != APPLE_DCP_DISPLAY_ENABLED) + return -EINVAL; + + lease->phase = APPLE_DCP_DISPLAY_QUARANTINED; + return 0; +} + static inline int apple_dcp_display_enable(struct apple_dcp_display_pool *pool, unsigned int engine, @@ -191,6 +388,8 @@ apple_dcp_display_enable(struct apple_dcp_display_pool *pool, lease = &pool->leases[engine]; if (!apple_dcp_display_cookie_equal(&lease->cookie, cookie)) return -ESTALE; + if (lease->phase == APPLE_DCP_DISPLAY_QUARANTINED) + return -EIO; if (lease->phase == APPLE_DCP_DISPLAY_ENABLED) return 0; if (pool->quiescing) @@ -214,6 +413,8 @@ apple_dcp_display_disable(struct apple_dcp_display_pool *pool, lease = &pool->leases[engine]; if (!apple_dcp_display_cookie_equal(&lease->cookie, cookie)) return -ESTALE; + if (lease->phase == APPLE_DCP_DISPLAY_QUARANTINED) + return -EIO; if (lease->phase == APPLE_DCP_DISPLAY_PREPARED) return 0; if (lease->phase != APPLE_DCP_DISPLAY_ENABLED) @@ -235,6 +436,8 @@ apple_dcp_display_release(struct apple_dcp_display_pool *pool, lease = &pool->leases[engine]; if (!apple_dcp_display_cookie_equal(&lease->cookie, cookie)) return -ESTALE; + if (lease->phase == APPLE_DCP_DISPLAY_QUARANTINED) + return -EIO; if (lease->phase == APPLE_DCP_DISPLAY_ENABLED) return -EBUSY; if (lease->phase != APPLE_DCP_DISPLAY_PREPARED)