From 2c361fb027f3f6b861ab3f02cbd5fee0f69c1eb5 Mon Sep 17 00:00:00 2001 From: nekolyanich Date: Sat, 24 Oct 2026 16:45:22 +0300 Subject: [PATCH] thunderbolt: apple: let T6020 DP IN tunnels wait for a display pipeline apple_dpin_m2, the T6020/T6021 (M2 Pro/Max) ACIO policy, leaves capacity_retry off "pending M2 hardware qualification". A DP IN tunnel that finds no free display pipeline gets -EBUSY once and that is final ("dpin0: DP tunnel setup failed: -16"). The connection manager keeps the tunnel through the DPRX timeout (TB_HOST_DP_KEEP_DPRX_TIMEOUT), but nothing asks for a pipeline again, so the display stays dark. On a MacBook Pro 16" M2 Pro (j416s) with a monitor on a Thunderbolt cable and a TV on a USB-C to HDMI adapter (DP Alt Mode), the fabric moves the TV to dcpext1 at boot and gives the tunnel dcpext0. After a system resume the TV comes back first on dcpext0 and the tunnel's request is refused. The fabric moves the TV to the other pipeline a few seconds later, too late for a request that is not retried. Set capacity_retry on apple_dpin_m2. A refused tunnel then waits as on M1: the DP IN work arms its retry and routes the tunnel once a pipeline comes free. M2 needs no dp_tunnel_awaits_display hook, which is set up for the APPLE_DPIN_CHANGED flow only: KEEP_DPRX_TIMEOUT already keeps its tunnels. As on M1, -EAGAIN waits too: an attempt that prepare cut short returns it and has to ask again after complete. A DCP service that is never published then waits until unplug, with a 120 s readiness wait per retry, where it used to fail once. Pause these retries across system sleep and at NHI removal. The PM gate (apple_dpin_pause_retries(), apple_dpin_resume_retries(), apple_dpin_end_pm_gate()) now applies wherever the policy has capacity_retry or defer_new_bringup, not defer_new_bringup only. apple_nhi_prepare() pauses the retries of a gated policy without defer_new_bringup (M2) before its resume_recovery=0 return, so at the default a waiting M2 tunnel stops asking at prepare; with resume_recovery set it pauses where M1 does. Either way its pause ends when complete starts, before tb_domain_complete(): every device has resumed by then, and the tunnel that the domain's complete brings back makes its first attempt there, since M2 does not defer it. Its retries then run from the refusal on, as before, and do not wait for the end of complete, which can take seconds. M1 and M3 at the default are unchanged: their pause stays behind resume_recovery (6ceb98a238d8). Without the gate a waiting M2 tunnel would keep calling into appledrm on the non-freezable dp_wq while the DCPs power off, and after the NHI is removed. M2 does not get defer_new_bringup: that would defer every first M2 handoff across sleep, which has not been tested, and DPRX retention for a deferred handoff goes through the hook M2 does not install. apple_dpin_end_pm_gate() now pauses both adapters before it drains them, as apple_dpin_pause_retries() does: work in flight at NHI removal could otherwise get -EBUSY and rearm the retry it had just cancelled. On M1 this is the only change: a bring-up in flight at NHI removal is deferred and then dropped instead of attaching. Base M2 (T8112) uses apple_dpin_m1, and M3 is unchanged. Update the comments above apple_dpin_work_fn() and apple_dpin_pause_retries(), which named M1-family hosts. Tests: the M2 row of apple_dpin_all_policy_fields; the t602x rearm trace, whose -EBUSY now waits and recovers; the capacity trace run for M2 as well; terminal errors end an M2 wait; apple_dpin_m2_capacity_suspend (pause, resume, a refusal after the pause has ended, resume with no pause, -EAGAIN under pause, unplug while waiting, end of the gate). In omacom/linux-aurora, commit 068fd0978db3 ("thunderbolt: apple: let T6030 DP IN tunnels wait for a display pipeline") restores the same flag for M3 after the #139 merge dropped it; M2 never had it. Testing: j416s, DCP firmware 13.5, under the m1n1 hypervisor, pm_test=platform, with a bench-only DT change that keeps the hypervisor's USB-C port out of the DCP routes. Monitor and TV: the monitor came back after 6 of 6 resumes with this change (0 of 2 without, on aurora 7.3 ac6b017b, same thunderbolt and appledrm code), routed 6.2-6.5 s after the refusal and 3.4-3.8 s ahead of the DPRX timeout; no DPRX timeout, no kernel WARNINGs. Monitor alone: back after 4 of 4 resumes, no wait. KUnit apple-dpin-lifecycle 35 of 35. Not tested: natively, a suspend while a tunnel waits (KUnit only), j414s, j414c, j416c, other firmware, a wait past the DPRX timeout, NHI removal while waiting. Assisted-by: Claude:claude-opus-5-5 checkpatch Signed-off-by: nekolyanich Assisted-by: LLM Signed-off-by: Ryan Murray --- drivers/thunderbolt/apple-dpin-state.c | 8 +- drivers/thunderbolt/apple-dpin-state.h | 1 + drivers/thunderbolt/apple-dpin-test.c | 116 +++++++++++++++++++++---- drivers/thunderbolt/apple.c | 52 ++++++++--- 4 files changed, 149 insertions(+), 28 deletions(-) diff --git a/drivers/thunderbolt/apple-dpin-state.c b/drivers/thunderbolt/apple-dpin-state.c index afaa64e3f6ba6..8a54958305ef3 100644 --- a/drivers/thunderbolt/apple-dpin-state.c +++ b/drivers/thunderbolt/apple-dpin-state.c @@ -17,7 +17,7 @@ const struct apple_dpin_policy apple_dpin_m1 = { const struct apple_dpin_policy apple_dpin_m2 = { .flow = APPLE_DPIN_PRE_POST, - /* Capacity retry remains disabled pending M2 hardware qualification. */ + .capacity_retry = true, .t602x_handshake = true, .host_policy = TB_HOST_DP_HPD_ON_ACTIVATE | TB_HOST_DP_ACTIVE_BEFORE_DPRX | @@ -152,6 +152,12 @@ bool apple_dpin_awaits_display(const struct apple_dpin_state *s, return s->alive && (s->waiting || (p->defer_new_bringup && s->deferred_first)); } +/* Retries pause across system sleep wherever a tunnel can wait or be deferred. */ +bool apple_dpin_pm_gated(const struct apple_dpin_policy *p) +{ + return p->capacity_retry || p->defer_new_bringup; +} + enum apple_dpin_event apple_dpin_request_event(const struct apple_dpin_state *s, const struct apple_dpin_tokens *t, u64 generation, bool active) diff --git a/drivers/thunderbolt/apple-dpin-state.h b/drivers/thunderbolt/apple-dpin-state.h index 38b8ec7eb439e..6ddb128d8b2f0 100644 --- a/drivers/thunderbolt/apple-dpin-state.h +++ b/drivers/thunderbolt/apple-dpin-state.h @@ -115,5 +115,6 @@ bool apple_dpin_admission_blocked(const struct apple_dpin_state *state, const struct apple_dpin_policy *policy, bool admitted); bool apple_dpin_awaits_display(const struct apple_dpin_state *state, const struct apple_dpin_policy *policy); +bool apple_dpin_pm_gated(const struct apple_dpin_policy *policy); #endif diff --git a/drivers/thunderbolt/apple-dpin-test.c b/drivers/thunderbolt/apple-dpin-test.c index dd532a2a051b5..94292ef00f8a9 100644 --- a/drivers/thunderbolt/apple-dpin-test.c +++ b/drivers/thunderbolt/apple-dpin-test.c @@ -217,6 +217,7 @@ static void apple_dpin_capacity_trace(struct kunit *test) }; run_trace(test, &apple_dpin_m1, trace, ARRAY_SIZE(trace)); + run_trace(test, &apple_dpin_m2, trace, ARRAY_SIZE(trace)); run_trace(test, &apple_dpin_m3, trace, ARRAY_SIZE(trace)); } @@ -233,8 +234,12 @@ static void apple_dpin_t602x_rearm_trace(struct kunit *test) { APPLE_DPIN_DROPPED, false, 0, 0, APPLE_DPIN_ACTIVATING }, { APPLE_DPIN_WORK, false, 0, APPLE_DPIN_ATTACH, APPLE_DPIN_ACTIVATING }, { APPLE_DPIN_RESULT, true, -EBUSY, - APPLE_DPIN_CANCEL_RETRY | APPLE_DPIN_WARN, APPLE_DPIN_FAILED }, - { APPLE_DPIN_RETRY, true, 0, 0, APPLE_DPIN_FAILED }, + APPLE_DPIN_FIRST_WAIT | APPLE_DPIN_ARM_RETRY, APPLE_DPIN_WAITING_PIPELINE }, + { APPLE_DPIN_RETRY, true, 0, APPLE_DPIN_QUEUE, APPLE_DPIN_WAITING_PIPELINE }, + { APPLE_DPIN_WORK, true, 0, APPLE_DPIN_ATTACH, APPLE_DPIN_ACTIVATING }, + { APPLE_DPIN_RESULT, true, 0, + APPLE_DPIN_RECOVERED | APPLE_DPIN_LOG_CONNECTED | APPLE_DPIN_AGAIN, + APPLE_DPIN_HANDED }, }; run_trace(test, &apple_dpin_m2, trace, ARRAY_SIZE(trace)); @@ -297,18 +302,24 @@ static void apple_dpin_unplug_during_attach(struct kunit *test) static void apple_dpin_terminal_ends_wait(struct kunit *test) { const int errors[] = { -ENODEV, -EADDRINUSE, -EOPNOTSUPP, -ESHUTDOWN, -ENOMEM }; - unsigned int i, actions; - - for (i = 0; i < ARRAY_SIZE(errors); i++) { - struct apple_dpin_state s = { .alive = true, .waiting = true }; - - actions = apple_dpin_step(&s, &apple_dpin_m1, APPLE_DPIN_RESULT, - true, errors[i]); - KUNIT_EXPECT_EQ(test, actions, APPLE_DPIN_CANCEL_RETRY | APPLE_DPIN_WARN); - KUNIT_EXPECT_FALSE(test, s.waiting); - KUNIT_EXPECT_EQ(test, s.phase, APPLE_DPIN_FAILED); - KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, &apple_dpin_m1, - APPLE_DPIN_RETRY, true, 0), 0U); + const struct apple_dpin_policy * const policies[] = { + &apple_dpin_m1, &apple_dpin_m2, &apple_dpin_m3, + }; + unsigned int i, j, actions; + + for (j = 0; j < ARRAY_SIZE(policies); j++) { + for (i = 0; i < ARRAY_SIZE(errors); i++) { + struct apple_dpin_state s = { .alive = true, .waiting = true }; + + actions = apple_dpin_step(&s, policies[j], APPLE_DPIN_RESULT, + true, errors[i]); + KUNIT_EXPECT_EQ(test, actions, + APPLE_DPIN_CANCEL_RETRY | APPLE_DPIN_WARN); + KUNIT_EXPECT_FALSE(test, s.waiting); + KUNIT_EXPECT_EQ(test, s.phase, APPLE_DPIN_FAILED); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, policies[j], + APPLE_DPIN_RETRY, true, 0), 0U); + } } } @@ -414,7 +425,7 @@ static void apple_dpin_all_policy_fields(struct kunit *test) const struct apple_dpin_policy expected[] = { {}, { APPLE_DPIN_CHANGED, true, true, false, true, TB_HOST_DP_NOTIFY }, - { APPLE_DPIN_PRE_POST, false, false, true, false, + { APPLE_DPIN_PRE_POST, true, false, true, false, TB_HOST_DP_HPD_ON_ACTIVATE | TB_HOST_DP_ACTIVE_BEFORE_DPRX | TB_HOST_DP_KEEP_DPRX_TIMEOUT | TB_HOST_DP_ADAPTER_QUIRKS | TB_HOST_DP_INITIAL_BW_GRANT }, @@ -632,6 +643,7 @@ static void apple_dpin_sleep_dprx_retention(struct kunit *test) s = (struct apple_dpin_state){ .alive = true, .handed = true, .paused = true }; KUNIT_EXPECT_FALSE(test, apple_dpin_awaits_display(&s, &apple_dpin_m1)); s.handed = false; + /* M2 never installs the hook: KEEP_DPRX_TIMEOUT holds its tunnels. */ KUNIT_EXPECT_FALSE(test, apple_dpin_awaits_display(&s, &apple_dpin_m2)); apple_dpin_step(&s, &apple_dpin_m3, APPLE_DPIN_UP, false, 0); KUNIT_EXPECT_TRUE(test, apple_dpin_awaits_display(&s, &apple_dpin_m3)); @@ -1185,7 +1197,81 @@ static void apple_dpin_m3_capacity_suspend(struct kunit *test) KUNIT_EXPECT_FALSE(test, apple_dpin_awaits_display(&s, p)); } +/* M2 pauses retries over sleep without deferring new handoffs. */ +static void apple_dpin_m2_capacity_suspend(struct kunit *test) +{ + struct apple_dpin_state s = {}; + const struct apple_dpin_policy *p = &apple_dpin_m2; + + KUNIT_EXPECT_FALSE(test, apple_dpin_pm_gated(&apple_dpin_disabled)); + KUNIT_EXPECT_TRUE(test, apple_dpin_pm_gated(&apple_dpin_m1)); + KUNIT_EXPECT_TRUE(test, apple_dpin_pm_gated(p)); + + apple_dpin_step(&s, p, APPLE_DPIN_UP, false, 0); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_WORK, false, 0), + APPLE_DPIN_ATTACH); + apple_dpin_step(&s, p, APPLE_DPIN_ADMITTED, true, 0); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_RESULT, true, -EBUSY), + APPLE_DPIN_FIRST_WAIT | APPLE_DPIN_ARM_RETRY); + apple_dpin_step(&s, p, APPLE_DPIN_PAUSE, true, 0); + KUNIT_EXPECT_FALSE(test, apple_dpin_admission_blocked(&s, p, false)); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_RETRY, true, 0), 0U); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_RESUME, true, 0), + APPLE_DPIN_ARM_RETRY); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_RETRY, true, 0), + APPLE_DPIN_QUEUE); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_WORK, true, 0), + APPLE_DPIN_ATTACH); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_RESULT, true, 0), + APPLE_DPIN_RECOVERED | APPLE_DPIN_LOG_CONNECTED | APPLE_DPIN_AGAIN); + + /* A tunnel refused after an early RESUME arms its own retry. */ + s = (struct apple_dpin_state){}; + apple_dpin_step(&s, p, APPLE_DPIN_PAUSE, false, 0); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_RESUME, false, 0), 0U); + apple_dpin_step(&s, p, APPLE_DPIN_UP, false, 0); + apple_dpin_step(&s, p, APPLE_DPIN_WORK, false, 0); + apple_dpin_step(&s, p, APPLE_DPIN_ADMITTED, true, 0); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_RESULT, true, -EBUSY), + APPLE_DPIN_FIRST_WAIT | APPLE_DPIN_ARM_RETRY); + + /* RESUME with no PAUSE before it only rearms a wait. */ + s = (struct apple_dpin_state){ .alive = true }; + apple_dpin_step(&s, p, APPLE_DPIN_RESULT, true, -EBUSY); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_RESUME, true, 0), + APPLE_DPIN_ARM_RETRY); + KUNIT_EXPECT_TRUE(test, s.waiting); + KUNIT_EXPECT_FALSE(test, s.paused); + KUNIT_EXPECT_EQ(test, s.phase, APPLE_DPIN_WAITING_PIPELINE); + + /* A readiness wait cut short by prepare waits, and asks after complete. */ + s = (struct apple_dpin_state){}; + apple_dpin_step(&s, p, APPLE_DPIN_UP, false, 0); + apple_dpin_step(&s, p, APPLE_DPIN_WORK, false, 0); + apple_dpin_step(&s, p, APPLE_DPIN_ADMITTED, true, 0); + apple_dpin_step(&s, p, APPLE_DPIN_PAUSE, true, 0); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_RESULT, true, -EAGAIN), + APPLE_DPIN_FIRST_WAIT); + KUNIT_EXPECT_EQ(test, s.phase, APPLE_DPIN_WAITING_PIPELINE); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_RESUME, true, 0), + APPLE_DPIN_ARM_RETRY); + + /* Unplug while waiting cancels the retry; NHI removal ends the gate. */ + apple_dpin_step(&s, p, APPLE_DPIN_PAUSE, true, 0); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_DOWN, true, 0), + APPLE_DPIN_QUEUE); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_WORK, true, 0), + APPLE_DPIN_CANCEL_RETRY | APPLE_DPIN_DROP); + KUNIT_EXPECT_FALSE(test, s.waiting); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_RETRY, true, 0), 0U); + apple_dpin_step(&s, p, APPLE_DPIN_DROPPED, false, 0); + apple_dpin_step(&s, p, APPLE_DPIN_END_PM_GATE, false, 0); + KUNIT_EXPECT_FALSE(test, s.paused); + KUNIT_EXPECT_EQ(test, apple_dpin_step(&s, p, APPLE_DPIN_RESUME, false, 0), 0U); +} + static struct kunit_case apple_dpin_cases[] = { + KUNIT_CASE(apple_dpin_m2_capacity_suspend), KUNIT_CASE(apple_dpin_m3_capacity_suspend), KUNIT_CASE(apple_dpin_provider_handed_lifetime), KUNIT_CASE(apple_dpin_provider_failed_retry), diff --git a/drivers/thunderbolt/apple.c b/drivers/thunderbolt/apple.c index 70a21ade49c06..0a37cbdd6f55f 100644 --- a/drivers/thunderbolt/apple.c +++ b/drivers/thunderbolt/apple.c @@ -1060,9 +1060,10 @@ static void apple_dpin_retry_fn(struct work_struct *work) } /* - * Only where both the display allocator and the connection manager can hold - * a tunnel for a pipeline (capacity-retry hosts, see - * apple_nhi_dp_tunnel_awaits_display()) does a refused tunnel wait for one. + * Only where the policy has capacity_retry does a refused tunnel wait for a + * pipeline. The connection manager keeps it meanwhile: through + * apple_nhi_dp_tunnel_awaits_display() on the APPLE_DPIN_CHANGED flow, and + * through TB_HOST_DP_KEEP_DPRX_TIMEOUT on the APPLE_DPIN_PRE_POST one. */ static void apple_dpin_work_fn(struct work_struct *work) { @@ -2606,9 +2607,20 @@ static int apple_nhi_probe(struct platform_device *pdev) static void apple_dpin_end_pm_gate(struct apple_cio *acio) { - if (!acio->dp_wq || !acio->dp->defer_new_bringup) + if (!acio->dp_wq || !apple_dpin_pm_gated(acio->dp)) return; - /* Domain removal stopped producers. No DP-IN/domain/fabric lock held. */ + /* + * Domain removal stopped producers. No DP-IN/domain/fabric lock held. + * Pause first, as apple_dpin_pause_retries() does, so that work still + * in flight cannot rearm the retry once it is cancelled. + */ + for (unsigned int i = 0; i < ARRAY_SIZE(acio->dpin); i++) { + struct apple_dpin_ctx *c = &acio->dpin[i]; + + scoped_guard(mutex, &c->lock) + apple_dpin_step(&c->state, acio->dp, APPLE_DPIN_PAUSE, + !!c->regs, 0); + } for (unsigned int i = 0; i < ARRAY_SIZE(acio->dpin); i++) { struct apple_dpin_ctx *c = &acio->dpin[i]; @@ -2644,13 +2656,14 @@ static void apple_nhi_remove(struct platform_device *pdev) * seconds. Not while devices suspend and resume: stop asking before any of * them suspends, let an attempt in flight finish (a wait for appledrm to * load gives way, see apple_dpin_connect()), and ask again once all of them - * have resumed. The tunnel keeps waiting meanwhile. Suspend-gated handoffs - * are deferred until complete, including after an aborted suspend. Established - * handed callbacks and synchronous tunnel teardown remain permitted. + * have resumed. The tunnel keeps waiting meanwhile. Where the policy has + * defer_new_bringup, new handoffs are deferred until complete, including + * after an aborted suspend. Established handed callbacks and synchronous + * tunnel teardown remain permitted. */ static void apple_dpin_pause_retries(struct apple_cio *acio) { - if (!acio->dp_wq || !acio->dp->defer_new_bringup) + if (!acio->dp_wq || !apple_dpin_pm_gated(acio->dp)) return; for (unsigned int i = 0; i < ARRAY_SIZE(acio->dpin); i++) { struct apple_dpin_ctx *c = &acio->dpin[i]; @@ -2668,7 +2681,7 @@ static void apple_dpin_pause_retries(struct apple_cio *acio) static void apple_dpin_resume_retries(struct apple_cio *acio) { - if (!acio->dp_wq || !acio->dp->defer_new_bringup) + if (!acio->dp_wq || !apple_dpin_pm_gated(acio->dp)) return; for (unsigned int i = 0; i < ARRAY_SIZE(acio->dpin); i++) { struct apple_dpin_ctx *c = &acio->dpin[i]; @@ -2692,8 +2705,15 @@ static int apple_nhi_prepare(struct device *dev) acio->resume_had_peer = false; /* Only resume recovery reads it: don't take the domain lock otherwise. */ - if (!READ_ONCE(resume_recovery)) + if (!READ_ONCE(resume_recovery)) { + /* + * Policies that defer new handoffs keep that to resume recovery; + * capacity retries alone pause across every sleep. + */ + if (!acio->dp->defer_new_bringup) + apple_dpin_pause_retries(acio); return 0; + } scoped_guard(mutex, &anhi->tb->lock) { struct tb_port *port; @@ -2821,8 +2841,16 @@ static void apple_nhi_complete(struct device *dev) struct apple_nhi *anhi = dev_get_drvdata(dev); struct apple_cio *acio = anhi->acio; + /* + * Every device has resumed. Without deferred handoffs, a tunnel that + * the domain's complete brings up makes its first attempt right away; + * let its retries run from then on too. + */ + if (!acio->dp->defer_new_bringup) + apple_dpin_resume_retries(acio); tb_domain_complete(anhi->tb); - apple_dpin_resume_retries(acio); + if (acio->dp->defer_new_bringup) + apple_dpin_resume_retries(acio); guard(mutex)(&acio->pcie_tunnel_lock); acio->pcie_pm_prepared = false;