mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH] thunderbolt: Optionally keep PCIe tunnels up through suspend-to-idle
@ 2026-11-11 22:57 Andrei Rusu de Castro
  2026-11-12  1:58 ` [PATCH v2] " Andrei Rusu de Castro
  2026-11-12  5:49 ` [PATCH] " Mika Westerberg
  0 siblings, 2 replies; 3+ messages in thread
From: Andrei Rusu de Castro @ 2026-11-11 22:57 UTC (permalink / raw)
  To: Mika Westerberg, Andreas Noever, Yehezkel Bernat; +Cc: linux-usb, linux-kernel

Every system sleep puts the USB4 routers to sleep, which takes all
tunnels down, and the connection manager re-creates them on resume. A
PCIe device behind a tunnel disappears from the bus in between. An
external GPU that drives a display does not survive that: on a ROG Flow
Z13 (GZ302EA) with an RTX 5090 in a Razer Core X V2 driving an external
monitor, the GPU driver found the device gone after suspend-to-idle, the
enclosure was removed and re-added under a compositor still using it,
and the desktop froze.

For suspend-to-idle the domain can keep running instead. Add a module
parameter, s2idle_keep_pcie, off by default. When it is set, the target
state is suspend-to-idle and the software connection manager has a PCIe
tunnel, leave the routers, links and control channel running and keep
the host interface in D0 by saving its PCI state in the noirq phase, so
the PCI core does not put it into D3. The devices behind the tunnel then
go through an ordinary PCI suspend and resume. Without a PCIe tunnel, in
other sleep states and with the firmware connection manager nothing
changes.

With the parameter set on the GZ302EA, suspend-to-idle with the eGPU
driving the monitor resumes with the GPU still on the bus, the same
compositor process and the dock's USB devices intact. With this
enclosure attached the system does not reach hardware sleep either
way, with the tunnels kept or torn down (amd-s2idle: 0% hardware
sleep); without it, the same system reaches 99% hardware sleep at
about 1.1 W. The cost of keeping a tunnel up on a system that
otherwise reaches hardware sleep was not measured.

An LLM assisted with finding the cause, writing this change and its
description, and running the tests above on my GZ302EA.

Signed-off-by: Andrei Rusu de Castro <arc@empyreal.works>
---
 drivers/thunderbolt/domain.c |  5 +++--
 drivers/thunderbolt/nhi.c    | 14 ++++++++++++
 drivers/thunderbolt/tb.c     | 43 ++++++++++++++++++++++++++++++++++++
 include/linux/thunderbolt.h  |  3 +++
 4 files changed, 63 insertions(+), 2 deletions(-)

diff --git a/drivers/thunderbolt/domain.c b/drivers/thunderbolt/domain.c
index 24611f05b..168a286cd 100644
--- a/drivers/thunderbolt/domain.c
+++ b/drivers/thunderbolt/domain.c
@@ -535,7 +535,7 @@ int tb_domain_suspend_noirq(struct tb *tb)
 	mutex_lock(&tb->lock);
 	if (tb->cm_ops->suspend_noirq)
 		ret = tb->cm_ops->suspend_noirq(tb);
-	if (!ret)
+	if (!ret && !tb->kept_running)
 		tb_ctl_stop(tb->ctl);
 	mutex_unlock(&tb->lock);
 
@@ -556,7 +556,8 @@ int tb_domain_resume_noirq(struct tb *tb)
 	int ret = 0;
 
 	mutex_lock(&tb->lock);
-	tb_ctl_start(tb->ctl);
+	if (!tb->kept_running)
+		tb_ctl_start(tb->ctl);
 	if (tb->cm_ops->resume_noirq)
 		ret = tb->cm_ops->resume_noirq(tb);
 	mutex_unlock(&tb->lock);
diff --git a/drivers/thunderbolt/nhi.c b/drivers/thunderbolt/nhi.c
index f54ab2189..c453153c2 100644
--- a/drivers/thunderbolt/nhi.c
+++ b/drivers/thunderbolt/nhi.c
@@ -1089,6 +1089,17 @@ static int __nhi_suspend_noirq(struct device *dev, bool wakeup)
 	if (ret)
 		return ret;
 
+	if (tb->kept_running) {
+		/*
+		 * Saving the state here keeps the PCI core from putting the
+		 * host router into D3, which would take the tunnels down;
+		 * for suspend-to-idle it then skips the bus PM of this
+		 * device on resume as well.
+		 */
+		pci_save_state(to_pci_dev(dev));
+		return 0;
+	}
+
 	if (nhi->ops->suspend_noirq) {
 		ret = nhi->ops->suspend_noirq(tb->nhi, wakeup);
 		if (ret)
@@ -1145,6 +1156,9 @@ static int nhi_resume_noirq(struct device *dev)
 	struct tb_nhi *nhi = tb->nhi;
 	int ret;
 
+	if (tb->kept_running)
+		return tb_domain_resume_noirq(tb);
+
 	/*
 	 * Check that the device is still there. It may be that the user
 	 * unplugged last device which causes the host controller to go
diff --git a/drivers/thunderbolt/tb.c b/drivers/thunderbolt/tb.c
index 418ad0a61..641502ec2 100644
--- a/drivers/thunderbolt/tb.c
+++ b/drivers/thunderbolt/tb.c
@@ -9,8 +9,10 @@
 #include <linux/slab.h>
 #include <linux/errno.h>
 #include <linux/delay.h>
+#include <linux/moduleparam.h>
 #include <linux/pm_runtime.h>
 #include <linux/stringify.h>
+#include <linux/suspend.h>
 
 #include "tb.h"
 #include "tb_regs.h"
@@ -3106,10 +3108,45 @@ static int tb_start(struct tb *tb, bool reset)
 	return 0;
 }
 
+static bool s2idle_keep_pcie;
+module_param(s2idle_keep_pcie, bool, 0644);
+MODULE_PARM_DESC(s2idle_keep_pcie,
+		 "keep a domain with PCIe tunnels running through suspend-to-idle so the devices behind them stay connected (default: false)");
+
+/*
+ * Putting the routers to sleep takes every tunnel down, and the PCIe
+ * devices behind a tunnel then disappear from the bus until it is
+ * re-created on resume. A device that cannot lose its link while its
+ * driver holds it (an external GPU driving a display) does not survive
+ * that. For suspend-to-idle the domain can instead keep running: the
+ * routers stay awake, the links and tunnels stay up and the devices
+ * behind them go through an ordinary PCI suspend and resume.
+ */
+static bool tb_keep_running_s2idle(struct tb *tb)
+{
+	struct tb_cm *tcm = tb_priv(tb);
+	struct tb_tunnel *tunnel;
+
+	if (!s2idle_keep_pcie || pm_suspend_target_state != PM_SUSPEND_TO_IDLE)
+		return false;
+
+	list_for_each_entry(tunnel, &tcm->tunnel_list, list) {
+		if (tb_tunnel_is_pci(tunnel))
+			return true;
+	}
+	return false;
+}
+
 static int tb_suspend_noirq(struct tb *tb)
 {
 	struct tb_cm *tcm = tb_priv(tb);
 
+	if (tb_keep_running_s2idle(tb)) {
+		tb_dbg(tb, "PCIe tunnels active, keeping the domain running\n");
+		tb->kept_running = true;
+		return 0;
+	}
+
 	tb_dbg(tb, "suspending...\n");
 	tb_disconnect_and_release_dp(tb);
 	tb_switch_exit_redrive(tb->root_switch);
@@ -3177,6 +3214,12 @@ static int tb_resume_noirq(struct tb *tb)
 	unsigned int usb3_delay = 0;
 	LIST_HEAD(tunnels);
 
+	if (tb->kept_running) {
+		tb->kept_running = false;
+		tb_dbg(tb, "domain kept running, tunnels intact\n");
+		return 0;
+	}
+
 	tb_dbg(tb, "resuming...\n");
 
 	/*
diff --git a/include/linux/thunderbolt.h b/include/linux/thunderbolt.h
index 57502da29..8cd7c1b75 100644
--- a/include/linux/thunderbolt.h
+++ b/include/linux/thunderbolt.h
@@ -78,6 +78,8 @@ enum tb_security_level {
  * @index: Linux assigned domain number
  * @security_level: Current security level
  * @nboot_acl: Number of boot ACLs the domain supports
+ * @kept_running: The domain was left running through the current
+ *		  suspend-to-idle, its tunnels intact
  * @privdata: Private connection manager specific data
  */
 struct tb {
@@ -91,6 +93,7 @@ struct tb {
 	int index;
 	enum tb_security_level security_level;
 	size_t nboot_acl;
+	bool kept_running;
 	unsigned long privdata[];
 };
 
-- 
2.56.0



^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-11-12  5:49 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-11 22:57 [PATCH] thunderbolt: Optionally keep PCIe tunnels up through suspend-to-idle Andrei Rusu de Castro
2026-11-12  1:58 ` [PATCH v2] " Andrei Rusu de Castro
2026-11-12  5:49 ` [PATCH] " Mika Westerberg

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®