Merge tag 'usb-6.7-rc1' of git://git.kernel.org/pub/scm/linux/kernel/git/gregkh/usb
[sfrench/cifs-2.6.git] / drivers / thunderbolt / tb.c
index 27bd6ca6f99e4101b8b4d8c9eab8c58cfd034726..5acdeb766860da51f912677a37c99c6b0166d922 100644 (file)
 #include "tb_regs.h"
 #include "tunnel.h"
 
-#define TB_TIMEOUT     100     /* ms */
-#define MAX_GROUPS     7       /* max Group_ID is 7 */
+#define TB_TIMEOUT             100     /* ms */
+
+/*
+ * Minimum bandwidth (in Mb/s) that is needed in the single transmitter/receiver
+ * direction. This is 40G - 10% guard band bandwidth.
+ */
+#define TB_ASYM_MIN            (40000 * 90 / 100)
+
+/*
+ * Threshold bandwidth (in Mb/s) that is used to switch the links to
+ * asymmetric and back. This is selected as 45G which means when the
+ * request is higher than this, we switch the link to asymmetric, and
+ * when it is less than this we switch it back. The 45G is selected so
+ * that we still have 27G (of the total 72G) for bulk PCIe traffic when
+ * switching back to symmetric.
+ */
+#define TB_ASYM_THRESHOLD      45000
+
+#define MAX_GROUPS             7       /* max Group_ID is 7 */
+
+static unsigned int asym_threshold = TB_ASYM_THRESHOLD;
+module_param_named(asym_threshold, asym_threshold, uint, 0444);
+MODULE_PARM_DESC(asym_threshold,
+               "threshold (Mb/s) when to Gen 4 switch link symmetry. 0 disables. (default: "
+               __MODULE_STRING(TB_ASYM_THRESHOLD) ")");
 
 /**
  * struct tb_cm - Simple Thunderbolt connection manager
@@ -190,7 +213,7 @@ static void tb_add_dp_resources(struct tb_switch *sw)
                if (!tb_switch_query_dp_resource(sw, port))
                        continue;
 
-               list_add_tail(&port->list, &tcm->dp_resources);
+               list_add(&port->list, &tcm->dp_resources);
                tb_port_dbg(port, "DP IN resource available\n");
        }
 }
@@ -255,13 +278,13 @@ static int tb_enable_clx(struct tb_switch *sw)
         * this in the future to cover the whole topology if it turns
         * out to be beneficial.
         */
-       while (sw && sw->config.depth > 1)
+       while (sw && tb_switch_depth(sw) > 1)
                sw = tb_switch_parent(sw);
 
        if (!sw)
                return 0;
 
-       if (sw->config.depth != 1)
+       if (tb_switch_depth(sw) != 1)
                return 0;
 
        /*
@@ -285,14 +308,32 @@ static int tb_enable_clx(struct tb_switch *sw)
        return ret == -EOPNOTSUPP ? 0 : ret;
 }
 
-/* Disables CL states up to the host router */
-static void tb_disable_clx(struct tb_switch *sw)
+/**
+ * tb_disable_clx() - Disable CL states up to host router
+ * @sw: Router to start
+ *
+ * Disables CL states from @sw up to the host router. Returns true if
+ * any CL state were disabled. This can be used to figure out whether
+ * the link was setup by us or the boot firmware so we don't
+ * accidentally enable them if they were not enabled during discovery.
+ */
+static bool tb_disable_clx(struct tb_switch *sw)
 {
+       bool disabled = false;
+
        do {
-               if (tb_switch_clx_disable(sw) < 0)
+               int ret;
+
+               ret = tb_switch_clx_disable(sw);
+               if (ret > 0)
+                       disabled = true;
+               else if (ret < 0)
                        tb_sw_warn(sw, "failed to disable CL states\n");
+
                sw = tb_switch_parent(sw);
        } while (sw);
+
+       return disabled;
 }
 
 static int tb_increase_switch_tmu_accuracy(struct device *dev, void *data)
@@ -553,7 +594,7 @@ static struct tb_tunnel *tb_find_first_usb3_tunnel(struct tb *tb,
        struct tb_switch *sw;
 
        /* Pick the router that is deepest in the topology */
-       if (dst_port->sw->config.depth > src_port->sw->config.depth)
+       if (tb_port_path_direction_downstream(src_port, dst_port))
                sw = dst_port->sw;
        else
                sw = src_port->sw;
@@ -572,133 +613,294 @@ static struct tb_tunnel *tb_find_first_usb3_tunnel(struct tb *tb,
        return tb_find_tunnel(tb, TB_TUNNEL_USB3, usb3_down, NULL);
 }
 
-static int tb_available_bandwidth(struct tb *tb, struct tb_port *src_port,
-       struct tb_port *dst_port, int *available_up, int *available_down)
-{
-       int usb3_consumed_up, usb3_consumed_down, ret;
-       struct tb_cm *tcm = tb_priv(tb);
+/**
+ * tb_consumed_usb3_pcie_bandwidth() - Consumed USB3/PCIe bandwidth over a single link
+ * @tb: Domain structure
+ * @src_port: Source protocol adapter
+ * @dst_port: Destination protocol adapter
+ * @port: USB4 port the consumed bandwidth is calculated
+ * @consumed_up: Consumed upsream bandwidth (Mb/s)
+ * @consumed_down: Consumed downstream bandwidth (Mb/s)
+ *
+ * Calculates consumed USB3 and PCIe bandwidth at @port between path
+ * from @src_port to @dst_port. Does not take tunnel starting from
+ * @src_port and ending from @src_port into account.
+ */
+static int tb_consumed_usb3_pcie_bandwidth(struct tb *tb,
+                                          struct tb_port *src_port,
+                                          struct tb_port *dst_port,
+                                          struct tb_port *port,
+                                          int *consumed_up,
+                                          int *consumed_down)
+{
+       int pci_consumed_up, pci_consumed_down;
        struct tb_tunnel *tunnel;
-       struct tb_port *port;
 
-       tb_dbg(tb, "calculating available bandwidth between %llx:%u <-> %llx:%u\n",
-              tb_route(src_port->sw), src_port->port, tb_route(dst_port->sw),
-              dst_port->port);
+       *consumed_up = *consumed_down = 0;
 
        tunnel = tb_find_first_usb3_tunnel(tb, src_port, dst_port);
        if (tunnel && tunnel->src_port != src_port &&
            tunnel->dst_port != dst_port) {
-               ret = tb_tunnel_consumed_bandwidth(tunnel, &usb3_consumed_up,
-                                                  &usb3_consumed_down);
+               int ret;
+
+               ret = tb_tunnel_consumed_bandwidth(tunnel, consumed_up,
+                                                  consumed_down);
                if (ret)
                        return ret;
-       } else {
-               usb3_consumed_up = 0;
-               usb3_consumed_down = 0;
        }
 
-       /* Maximum possible bandwidth asymmetric Gen 4 link is 120 Gb/s */
-       *available_up = *available_down = 120000;
+       /*
+        * If there is anything reserved for PCIe bulk traffic take it
+        * into account here too.
+        */
+       if (tb_tunnel_reserved_pci(port, &pci_consumed_up, &pci_consumed_down)) {
+               *consumed_up += pci_consumed_up;
+               *consumed_down += pci_consumed_down;
+       }
 
-       /* Find the minimum available bandwidth over all links */
-       tb_for_each_port_on_path(src_port, dst_port, port) {
-               int link_speed, link_width, up_bw, down_bw;
+       return 0;
+}
 
-               if (!tb_port_is_null(port))
+/**
+ * tb_consumed_dp_bandwidth() - Consumed DP bandwidth over a single link
+ * @tb: Domain structure
+ * @src_port: Source protocol adapter
+ * @dst_port: Destination protocol adapter
+ * @port: USB4 port the consumed bandwidth is calculated
+ * @consumed_up: Consumed upsream bandwidth (Mb/s)
+ * @consumed_down: Consumed downstream bandwidth (Mb/s)
+ *
+ * Calculates consumed DP bandwidth at @port between path from @src_port
+ * to @dst_port. Does not take tunnel starting from @src_port and ending
+ * from @src_port into account.
+ */
+static int tb_consumed_dp_bandwidth(struct tb *tb,
+                                   struct tb_port *src_port,
+                                   struct tb_port *dst_port,
+                                   struct tb_port *port,
+                                   int *consumed_up,
+                                   int *consumed_down)
+{
+       struct tb_cm *tcm = tb_priv(tb);
+       struct tb_tunnel *tunnel;
+       int ret;
+
+       *consumed_up = *consumed_down = 0;
+
+       /*
+        * Find all DP tunnels that cross the port and reduce
+        * their consumed bandwidth from the available.
+        */
+       list_for_each_entry(tunnel, &tcm->tunnel_list, list) {
+               int dp_consumed_up, dp_consumed_down;
+
+               if (tb_tunnel_is_invalid(tunnel))
                        continue;
 
-               if (tb_is_upstream_port(port)) {
-                       link_speed = port->sw->link_speed;
+               if (!tb_tunnel_is_dp(tunnel))
+                       continue;
+
+               if (!tb_tunnel_port_on_path(tunnel, port))
+                       continue;
+
+               /*
+                * Ignore the DP tunnel between src_port and dst_port
+                * because it is the same tunnel and we may be
+                * re-calculating estimated bandwidth.
+                */
+               if (tunnel->src_port == src_port &&
+                   tunnel->dst_port == dst_port)
+                       continue;
+
+               ret = tb_tunnel_consumed_bandwidth(tunnel, &dp_consumed_up,
+                                                  &dp_consumed_down);
+               if (ret)
+                       return ret;
+
+               *consumed_up += dp_consumed_up;
+               *consumed_down += dp_consumed_down;
+       }
+
+       return 0;
+}
+
+static bool tb_asym_supported(struct tb_port *src_port, struct tb_port *dst_port,
+                             struct tb_port *port)
+{
+       bool downstream = tb_port_path_direction_downstream(src_port, dst_port);
+       enum tb_link_width width;
+
+       if (tb_is_upstream_port(port))
+               width = downstream ? TB_LINK_WIDTH_ASYM_RX : TB_LINK_WIDTH_ASYM_TX;
+       else
+               width = downstream ? TB_LINK_WIDTH_ASYM_TX : TB_LINK_WIDTH_ASYM_RX;
+
+       return tb_port_width_supported(port, width);
+}
+
+/**
+ * tb_maximum_bandwidth() - Maximum bandwidth over a single link
+ * @tb: Domain structure
+ * @src_port: Source protocol adapter
+ * @dst_port: Destination protocol adapter
+ * @port: USB4 port the total bandwidth is calculated
+ * @max_up: Maximum upstream bandwidth (Mb/s)
+ * @max_down: Maximum downstream bandwidth (Mb/s)
+ * @include_asym: Include bandwidth if the link is switched from
+ *               symmetric to asymmetric
+ *
+ * Returns maximum possible bandwidth in @max_up and @max_down over a
+ * single link at @port. If @include_asym is set then includes the
+ * additional banwdith if the links are transitioned into asymmetric to
+ * direction from @src_port to @dst_port.
+ */
+static int tb_maximum_bandwidth(struct tb *tb, struct tb_port *src_port,
+                               struct tb_port *dst_port, struct tb_port *port,
+                               int *max_up, int *max_down, bool include_asym)
+{
+       bool downstream = tb_port_path_direction_downstream(src_port, dst_port);
+       int link_speed, link_width, up_bw, down_bw;
+
+       /*
+        * Can include asymmetric, only if it is actually supported by
+        * the lane adapter.
+        */
+       if (!tb_asym_supported(src_port, dst_port, port))
+               include_asym = false;
+
+       if (tb_is_upstream_port(port)) {
+               link_speed = port->sw->link_speed;
+               /*
+                * sw->link_width is from upstream perspective so we use
+                * the opposite for downstream of the host router.
+                */
+               if (port->sw->link_width == TB_LINK_WIDTH_ASYM_TX) {
+                       up_bw = link_speed * 3 * 1000;
+                       down_bw = link_speed * 1 * 1000;
+               } else if (port->sw->link_width == TB_LINK_WIDTH_ASYM_RX) {
+                       up_bw = link_speed * 1 * 1000;
+                       down_bw = link_speed * 3 * 1000;
+               } else if (include_asym) {
                        /*
-                        * sw->link_width is from upstream perspective
-                        * so we use the opposite for downstream of the
-                        * host router.
+                        * The link is symmetric at the moment but we
+                        * can switch it to asymmetric as needed. Report
+                        * this bandwidth as available (even though it
+                        * is not yet enabled).
                         */
-                       if (port->sw->link_width == TB_LINK_WIDTH_ASYM_TX) {
-                               up_bw = link_speed * 3 * 1000;
-                               down_bw = link_speed * 1 * 1000;
-                       } else if (port->sw->link_width == TB_LINK_WIDTH_ASYM_RX) {
+                       if (downstream) {
                                up_bw = link_speed * 1 * 1000;
                                down_bw = link_speed * 3 * 1000;
                        } else {
-                               up_bw = link_speed * port->sw->link_width * 1000;
-                               down_bw = up_bw;
+                               up_bw = link_speed * 3 * 1000;
+                               down_bw = link_speed * 1 * 1000;
                        }
                } else {
-                       link_speed = tb_port_get_link_speed(port);
-                       if (link_speed < 0)
-                               return link_speed;
-
-                       link_width = tb_port_get_link_width(port);
-                       if (link_width < 0)
-                               return link_width;
-
-                       if (link_width == TB_LINK_WIDTH_ASYM_TX) {
+                       up_bw = link_speed * port->sw->link_width * 1000;
+                       down_bw = up_bw;
+               }
+       } else {
+               link_speed = tb_port_get_link_speed(port);
+               if (link_speed < 0)
+                       return link_speed;
+
+               link_width = tb_port_get_link_width(port);
+               if (link_width < 0)
+                       return link_width;
+
+               if (link_width == TB_LINK_WIDTH_ASYM_TX) {
+                       up_bw = link_speed * 1 * 1000;
+                       down_bw = link_speed * 3 * 1000;
+               } else if (link_width == TB_LINK_WIDTH_ASYM_RX) {
+                       up_bw = link_speed * 3 * 1000;
+                       down_bw = link_speed * 1 * 1000;
+               } else if (include_asym) {
+                       /*
+                        * The link is symmetric at the moment but we
+                        * can switch it to asymmetric as needed. Report
+                        * this bandwidth as available (even though it
+                        * is not yet enabled).
+                        */
+                       if (downstream) {
                                up_bw = link_speed * 1 * 1000;
                                down_bw = link_speed * 3 * 1000;
-                       } else if (link_width == TB_LINK_WIDTH_ASYM_RX) {
+                       } else {
                                up_bw = link_speed * 3 * 1000;
                                down_bw = link_speed * 1 * 1000;
-                       } else {
-                               up_bw = link_speed * link_width * 1000;
-                               down_bw = up_bw;
                        }
+               } else {
+                       up_bw = link_speed * link_width * 1000;
+                       down_bw = up_bw;
                }
+       }
 
-               /* Leave 10% guard band */
-               up_bw -= up_bw / 10;
-               down_bw -= down_bw / 10;
-
-               tb_port_dbg(port, "link total bandwidth %d/%d Mb/s\n", up_bw,
-                           down_bw);
+       /* Leave 10% guard band */
+       *max_up = up_bw - up_bw / 10;
+       *max_down = down_bw - down_bw / 10;
 
-               /*
-                * Find all DP tunnels that cross the port and reduce
-                * their consumed bandwidth from the available.
-                */
-               list_for_each_entry(tunnel, &tcm->tunnel_list, list) {
-                       int dp_consumed_up, dp_consumed_down;
+       tb_port_dbg(port, "link maximum bandwidth %d/%d Mb/s\n", *max_up, *max_down);
+       return 0;
+}
 
-                       if (tb_tunnel_is_invalid(tunnel))
-                               continue;
+/**
+ * tb_available_bandwidth() - Available bandwidth for tunneling
+ * @tb: Domain structure
+ * @src_port: Source protocol adapter
+ * @dst_port: Destination protocol adapter
+ * @available_up: Available bandwidth upstream (Mb/s)
+ * @available_down: Available bandwidth downstream (Mb/s)
+ * @include_asym: Include bandwidth if the link is switched from
+ *               symmetric to asymmetric
+ *
+ * Calculates maximum available bandwidth for protocol tunneling between
+ * @src_port and @dst_port at the moment. This is minimum of maximum
+ * link bandwidth across all links reduced by currently consumed
+ * bandwidth on that link.
+ *
+ * If @include_asym is true then includes also bandwidth that can be
+ * added when the links are transitioned into asymmetric (but does not
+ * transition the links).
+ */
+static int tb_available_bandwidth(struct tb *tb, struct tb_port *src_port,
+                                struct tb_port *dst_port, int *available_up,
+                                int *available_down, bool include_asym)
+{
+       struct tb_port *port;
+       int ret;
 
-                       if (!tb_tunnel_is_dp(tunnel))
-                               continue;
+       /* Maximum possible bandwidth asymmetric Gen 4 link is 120 Gb/s */
+       *available_up = *available_down = 120000;
 
-                       if (!tb_tunnel_port_on_path(tunnel, port))
-                               continue;
+       /* Find the minimum available bandwidth over all links */
+       tb_for_each_port_on_path(src_port, dst_port, port) {
+               int max_up, max_down, consumed_up, consumed_down;
 
-                       /*
-                        * Ignore the DP tunnel between src_port and
-                        * dst_port because it is the same tunnel and we
-                        * may be re-calculating estimated bandwidth.
-                        */
-                       if (tunnel->src_port == src_port &&
-                           tunnel->dst_port == dst_port)
-                               continue;
+               if (!tb_port_is_null(port))
+                       continue;
 
-                       ret = tb_tunnel_consumed_bandwidth(tunnel,
-                                                          &dp_consumed_up,
-                                                          &dp_consumed_down);
-                       if (ret)
-                               return ret;
+               ret = tb_maximum_bandwidth(tb, src_port, dst_port, port,
+                                          &max_up, &max_down, include_asym);
+               if (ret)
+                       return ret;
 
-                       up_bw -= dp_consumed_up;
-                       down_bw -= dp_consumed_down;
-               }
+               ret = tb_consumed_usb3_pcie_bandwidth(tb, src_port, dst_port,
+                                                     port, &consumed_up,
+                                                     &consumed_down);
+               if (ret)
+                       return ret;
+               max_up -= consumed_up;
+               max_down -= consumed_down;
 
-               /*
-                * If USB3 is tunneled from the host router down to the
-                * branch leading to port we need to take USB3 consumed
-                * bandwidth into account regardless whether it actually
-                * crosses the port.
-                */
-               up_bw -= usb3_consumed_up;
-               down_bw -= usb3_consumed_down;
+               ret = tb_consumed_dp_bandwidth(tb, src_port, dst_port, port,
+                                              &consumed_up, &consumed_down);
+               if (ret)
+                       return ret;
+               max_up -= consumed_up;
+               max_down -= consumed_down;
 
-               if (up_bw < *available_up)
-                       *available_up = up_bw;
-               if (down_bw < *available_down)
-                       *available_down = down_bw;
+               if (max_up < *available_up)
+                       *available_up = max_up;
+               if (max_down < *available_down)
+                       *available_down = max_down;
        }
 
        if (*available_up < 0)
@@ -729,21 +931,21 @@ static void tb_reclaim_usb3_bandwidth(struct tb *tb, struct tb_port *src_port,
        if (!tunnel)
                return;
 
-       tb_dbg(tb, "reclaiming unused bandwidth for USB3\n");
+       tb_tunnel_dbg(tunnel, "reclaiming unused bandwidth\n");
 
        /*
         * Calculate available bandwidth for the first hop USB3 tunnel.
         * That determines the whole USB3 bandwidth for this branch.
         */
        ret = tb_available_bandwidth(tb, tunnel->src_port, tunnel->dst_port,
-                                    &available_up, &available_down);
+                                    &available_up, &available_down, false);
        if (ret) {
-               tb_warn(tb, "failed to calculate available bandwidth\n");
+               tb_tunnel_warn(tunnel, "failed to calculate available bandwidth\n");
                return;
        }
 
-       tb_dbg(tb, "available bandwidth for USB3 %d/%d Mb/s\n",
-              available_up, available_down);
+       tb_tunnel_dbg(tunnel, "available bandwidth %d/%d Mb/s\n", available_up,
+                     available_down);
 
        tb_tunnel_reclaim_available_bandwidth(tunnel, &available_up, &available_down);
 }
@@ -794,8 +996,8 @@ static int tb_tunnel_usb3(struct tb *tb, struct tb_switch *sw)
                        return ret;
        }
 
-       ret = tb_available_bandwidth(tb, down, up, &available_up,
-                                    &available_down);
+       ret = tb_available_bandwidth(tb, down, up, &available_up, &available_down,
+                                    false);
        if (ret)
                goto err_reclaim;
 
@@ -856,6 +1058,225 @@ static int tb_create_usb3_tunnels(struct tb_switch *sw)
        return 0;
 }
 
+/**
+ * tb_configure_asym() - Transition links to asymmetric if needed
+ * @tb: Domain structure
+ * @src_port: Source adapter to start the transition
+ * @dst_port: Destination adapter
+ * @requested_up: Additional bandwidth (Mb/s) required upstream
+ * @requested_down: Additional bandwidth (Mb/s) required downstream
+ *
+ * Transition links between @src_port and @dst_port into asymmetric, with
+ * three lanes in the direction from @src_port towards @dst_port and one lane
+ * in the opposite direction, if the bandwidth requirements
+ * (requested + currently consumed) on that link exceed @asym_threshold.
+ *
+ * Must be called with available >= requested over all links.
+ */
+static int tb_configure_asym(struct tb *tb, struct tb_port *src_port,
+                            struct tb_port *dst_port, int requested_up,
+                            int requested_down)
+{
+       struct tb_switch *sw;
+       bool clx, downstream;
+       struct tb_port *up;
+       int ret = 0;
+
+       if (!asym_threshold)
+               return 0;
+
+       /* Disable CL states before doing any transitions */
+       downstream = tb_port_path_direction_downstream(src_port, dst_port);
+       /* Pick up router deepest in the hierarchy */
+       if (downstream)
+               sw = dst_port->sw;
+       else
+               sw = src_port->sw;
+
+       clx = tb_disable_clx(sw);
+
+       tb_for_each_upstream_port_on_path(src_port, dst_port, up) {
+               int consumed_up, consumed_down;
+               enum tb_link_width width;
+
+               ret = tb_consumed_dp_bandwidth(tb, src_port, dst_port, up,
+                                              &consumed_up, &consumed_down);
+               if (ret)
+                       break;
+
+               if (downstream) {
+                       /*
+                        * Downstream so make sure upstream is within the 36G
+                        * (40G - guard band 10%), and the requested is above
+                        * what the threshold is.
+                        */
+                       if (consumed_up + requested_up >= TB_ASYM_MIN) {
+                               ret = -ENOBUFS;
+                               break;
+                       }
+                       /* Does consumed + requested exceed the threshold */
+                       if (consumed_down + requested_down < asym_threshold)
+                               continue;
+
+                       width = TB_LINK_WIDTH_ASYM_RX;
+               } else {
+                       /* Upstream, the opposite of above */
+                       if (consumed_down + requested_down >= TB_ASYM_MIN) {
+                               ret = -ENOBUFS;
+                               break;
+                       }
+                       if (consumed_up + requested_up < asym_threshold)
+                               continue;
+
+                       width = TB_LINK_WIDTH_ASYM_TX;
+               }
+
+               if (up->sw->link_width == width)
+                       continue;
+
+               if (!tb_port_width_supported(up, width))
+                       continue;
+
+               tb_sw_dbg(up->sw, "configuring asymmetric link\n");
+
+               /*
+                * Here requested + consumed > threshold so we need to
+                * transtion the link into asymmetric now.
+                */
+               ret = tb_switch_set_link_width(up->sw, width);
+               if (ret) {
+                       tb_sw_warn(up->sw, "failed to set link width\n");
+                       break;
+               }
+       }
+
+       /* Re-enable CL states if they were previosly enabled */
+       if (clx)
+               tb_enable_clx(sw);
+
+       return ret;
+}
+
+/**
+ * tb_configure_sym() - Transition links to symmetric if possible
+ * @tb: Domain structure
+ * @src_port: Source adapter to start the transition
+ * @dst_port: Destination adapter
+ * @requested_up: New lower bandwidth request upstream (Mb/s)
+ * @requested_down: New lower bandwidth request downstream (Mb/s)
+ *
+ * Goes over each link from @src_port to @dst_port and tries to
+ * transition the link to symmetric if the currently consumed bandwidth
+ * allows.
+ */
+static int tb_configure_sym(struct tb *tb, struct tb_port *src_port,
+                           struct tb_port *dst_port, int requested_up,
+                           int requested_down)
+{
+       struct tb_switch *sw;
+       bool clx, downstream;
+       struct tb_port *up;
+       int ret = 0;
+
+       if (!asym_threshold)
+               return 0;
+
+       /* Disable CL states before doing any transitions */
+       downstream = tb_port_path_direction_downstream(src_port, dst_port);
+       /* Pick up router deepest in the hierarchy */
+       if (downstream)
+               sw = dst_port->sw;
+       else
+               sw = src_port->sw;
+
+       clx = tb_disable_clx(sw);
+
+       tb_for_each_upstream_port_on_path(src_port, dst_port, up) {
+               int consumed_up, consumed_down;
+
+               /* Already symmetric */
+               if (up->sw->link_width <= TB_LINK_WIDTH_DUAL)
+                       continue;
+               /* Unplugged, no need to switch */
+               if (up->sw->is_unplugged)
+                       continue;
+
+               ret = tb_consumed_dp_bandwidth(tb, src_port, dst_port, up,
+                                              &consumed_up, &consumed_down);
+               if (ret)
+                       break;
+
+               if (downstream) {
+                       /*
+                        * Downstream so we want the consumed_down < threshold.
+                        * Upstream traffic should be less than 36G (40G
+                        * guard band 10%) as the link was configured asymmetric
+                        * already.
+                        */
+                       if (consumed_down + requested_down >= asym_threshold)
+                               continue;
+               } else {
+                       if (consumed_up + requested_up >= asym_threshold)
+                               continue;
+               }
+
+               if (up->sw->link_width == TB_LINK_WIDTH_DUAL)
+                       continue;
+
+               tb_sw_dbg(up->sw, "configuring symmetric link\n");
+
+               ret = tb_switch_set_link_width(up->sw, TB_LINK_WIDTH_DUAL);
+               if (ret) {
+                       tb_sw_warn(up->sw, "failed to set link width\n");
+                       break;
+               }
+       }
+
+       /* Re-enable CL states if they were previosly enabled */
+       if (clx)
+               tb_enable_clx(sw);
+
+       return ret;
+}
+
+static void tb_configure_link(struct tb_port *down, struct tb_port *up,
+                             struct tb_switch *sw)
+{
+       struct tb *tb = sw->tb;
+
+       /* Link the routers using both links if available */
+       down->remote = up;
+       up->remote = down;
+       if (down->dual_link_port && up->dual_link_port) {
+               down->dual_link_port->remote = up->dual_link_port;
+               up->dual_link_port->remote = down->dual_link_port;
+       }
+
+       /*
+        * Enable lane bonding if the link is currently two single lane
+        * links.
+        */
+       if (sw->link_width < TB_LINK_WIDTH_DUAL)
+               tb_switch_set_link_width(sw, TB_LINK_WIDTH_DUAL);
+
+       /*
+        * Device router that comes up as symmetric link is
+        * connected deeper in the hierarchy, we transition the links
+        * above into symmetric if bandwidth allows.
+        */
+       if (tb_switch_depth(sw) > 1 &&
+           tb_port_get_link_generation(up) >= 4 &&
+           up->sw->link_width == TB_LINK_WIDTH_DUAL) {
+               struct tb_port *host_port;
+
+               host_port = tb_port_at(tb_route(sw), tb->root_switch);
+               tb_configure_sym(tb, host_port, up, 0, 0);
+       }
+
+       /* Set the link configured */
+       tb_switch_configure_link(sw);
+}
+
 static void tb_scan_port(struct tb_port *port);
 
 /*
@@ -964,19 +1385,9 @@ static void tb_scan_port(struct tb_port *port)
                goto out_rpm_put;
        }
 
-       /* Link the switches using both links if available */
        upstream_port = tb_upstream_port(sw);
-       port->remote = upstream_port;
-       upstream_port->remote = port;
-       if (port->dual_link_port && upstream_port->dual_link_port) {
-               port->dual_link_port->remote = upstream_port->dual_link_port;
-               upstream_port->dual_link_port->remote = port->dual_link_port;
-       }
+       tb_configure_link(port, upstream_port, sw);
 
-       /* Enable lane bonding if supported */
-       tb_switch_lane_bonding_enable(sw);
-       /* Set the link configured */
-       tb_switch_configure_link(sw);
        /*
         * CL0s and CL1 are enabled and supported together.
         * Silently ignore CLx enabling in case CLx is not supported.
@@ -1040,6 +1451,11 @@ static void tb_deactivate_and_free_tunnel(struct tb_tunnel *tunnel)
                 * deallocated properly.
                 */
                tb_switch_dealloc_dp_resource(src_port->sw, src_port);
+               /*
+                * If bandwidth on a link is < asym_threshold
+                * transition the link to symmetric.
+                */
+               tb_configure_sym(tb, src_port, dst_port, 0, 0);
                /* Now we can allow the domain to runtime suspend again */
                pm_runtime_mark_last_busy(&dst_port->sw->dev);
                pm_runtime_put_autosuspend(&dst_port->sw->dev);
@@ -1092,7 +1508,8 @@ static void tb_free_unplugged_children(struct tb_switch *sw)
                        tb_retimer_remove_all(port);
                        tb_remove_dp_resources(port->remote->sw);
                        tb_switch_unconfigure_link(port->remote->sw);
-                       tb_switch_lane_bonding_disable(port->remote->sw);
+                       tb_switch_set_link_width(port->remote->sw,
+                                                TB_LINK_WIDTH_SINGLE);
                        tb_switch_remove(port->remote->sw);
                        port->remote = NULL;
                        if (port->dual_link_port)
@@ -1188,7 +1605,7 @@ tb_recalc_estimated_bandwidth_for_group(struct tb_bandwidth_group *group)
                        ret = tb_release_unused_usb3_bandwidth(tb,
                                first_tunnel->src_port, first_tunnel->dst_port);
                        if (ret) {
-                               tb_port_warn(in,
+                               tb_tunnel_warn(tunnel,
                                        "failed to release unused bandwidth\n");
                                break;
                        }
@@ -1196,9 +1613,9 @@ tb_recalc_estimated_bandwidth_for_group(struct tb_bandwidth_group *group)
 
                out = tunnel->dst_port;
                ret = tb_available_bandwidth(tb, in, out, &estimated_up,
-                                            &estimated_down);
+                                            &estimated_down, true);
                if (ret) {
-                       tb_port_warn(in,
+                       tb_tunnel_warn(tunnel,
                                "failed to re-calculate estimated bandwidth\n");
                        break;
                }
@@ -1209,16 +1626,18 @@ tb_recalc_estimated_bandwidth_for_group(struct tb_bandwidth_group *group)
                 *  - available bandwidth along the path
                 *  - bandwidth allocated for USB 3.x but not used.
                 */
-               tb_port_dbg(in, "re-calculated estimated bandwidth %u/%u Mb/s\n",
-                           estimated_up, estimated_down);
+               tb_tunnel_dbg(tunnel,
+                             "re-calculated estimated bandwidth %u/%u Mb/s\n",
+                             estimated_up, estimated_down);
 
-               if (in->sw->config.depth < out->sw->config.depth)
+               if (tb_port_path_direction_downstream(in, out))
                        estimated_bw = estimated_down;
                else
                        estimated_bw = estimated_up;
 
                if (usb4_dp_port_set_estimated_bandwidth(in, estimated_bw))
-                       tb_port_warn(in, "failed to update estimated bandwidth\n");
+                       tb_tunnel_warn(tunnel,
+                                      "failed to update estimated bandwidth\n");
        }
 
        if (first_tunnel)
@@ -1282,18 +1701,14 @@ static struct tb_port *tb_find_dp_out(struct tb *tb, struct tb_port *in)
        return NULL;
 }
 
-static void tb_tunnel_dp(struct tb *tb)
+static bool tb_tunnel_one_dp(struct tb *tb)
 {
        int available_up, available_down, ret, link_nr;
        struct tb_cm *tcm = tb_priv(tb);
        struct tb_port *port, *in, *out;
+       int consumed_up, consumed_down;
        struct tb_tunnel *tunnel;
 
-       if (!tb_acpi_may_tunnel_dp()) {
-               tb_dbg(tb, "DP tunneling disabled, not creating tunnel\n");
-               return;
-       }
-
        /*
         * Find pair of inactive DP IN and DP OUT adapters and then
         * establish a DP tunnel between them.
@@ -1311,22 +1726,21 @@ static void tb_tunnel_dp(struct tb *tb)
                        continue;
                }
 
-               tb_port_dbg(port, "DP IN available\n");
+               in = port;
+               tb_port_dbg(in, "DP IN available\n");
 
                out = tb_find_dp_out(tb, port);
-               if (out) {
-                       in = port;
+               if (out)
                        break;
-               }
        }
 
        if (!in) {
                tb_dbg(tb, "no suitable DP IN adapter available, not tunneling\n");
-               return;
+               return false;
        }
        if (!out) {
                tb_dbg(tb, "no suitable DP OUT adapter available, not tunneling\n");
-               return;
+               return false;
        }
 
        /*
@@ -1369,7 +1783,8 @@ static void tb_tunnel_dp(struct tb *tb)
                goto err_detach_group;
        }
 
-       ret = tb_available_bandwidth(tb, in, out, &available_up, &available_down);
+       ret = tb_available_bandwidth(tb, in, out, &available_up, &available_down,
+                                    true);
        if (ret)
                goto err_reclaim_usb;
 
@@ -1391,6 +1806,13 @@ static void tb_tunnel_dp(struct tb *tb)
        list_add_tail(&tunnel->list, &tcm->tunnel_list);
        tb_reclaim_usb3_bandwidth(tb, in, out);
 
+       /*
+        * Transition the links to asymmetric if the consumption exceeds
+        * the threshold.
+        */
+       if (!tb_tunnel_consumed_bandwidth(tunnel, &consumed_up, &consumed_down))
+               tb_configure_asym(tb, in, out, consumed_up, consumed_down);
+
        /* Update the domain with the new bandwidth estimation */
        tb_recalc_estimated_bandwidth(tb);
 
@@ -1399,7 +1821,7 @@ static void tb_tunnel_dp(struct tb *tb)
         * TMU mode to HiFi for CL0s to work.
         */
        tb_increase_tmu_accuracy(tunnel);
-       return;
+       return true;
 
 err_free:
        tb_tunnel_free(tunnel);
@@ -1414,6 +1836,19 @@ err_rpm_put:
        pm_runtime_put_autosuspend(&out->sw->dev);
        pm_runtime_mark_last_busy(&in->sw->dev);
        pm_runtime_put_autosuspend(&in->sw->dev);
+
+       return false;
+}
+
+static void tb_tunnel_dp(struct tb *tb)
+{
+       if (!tb_acpi_may_tunnel_dp()) {
+               tb_dbg(tb, "DP tunneling disabled, not creating tunnel\n");
+               return;
+       }
+
+       while (tb_tunnel_one_dp(tb))
+               ;
 }
 
 static void tb_dp_resource_unavailable(struct tb *tb, struct tb_port *port)
@@ -1701,7 +2136,8 @@ static void tb_handle_hotplug(struct work_struct *work)
                        tb_remove_dp_resources(port->remote->sw);
                        tb_switch_tmu_disable(port->remote->sw);
                        tb_switch_unconfigure_link(port->remote->sw);
-                       tb_switch_lane_bonding_disable(port->remote->sw);
+                       tb_switch_set_link_width(port->remote->sw,
+                                                TB_LINK_WIDTH_SINGLE);
                        tb_switch_remove(port->remote->sw);
                        port->remote = NULL;
                        if (port->dual_link_port)
@@ -1781,8 +2217,8 @@ static int tb_alloc_dp_bandwidth(struct tb_tunnel *tunnel, int *requested_up,
        in = tunnel->src_port;
        out = tunnel->dst_port;
 
-       tb_port_dbg(in, "bandwidth allocated currently %d/%d Mb/s\n",
-                   allocated_up, allocated_down);
+       tb_tunnel_dbg(tunnel, "bandwidth allocated currently %d/%d Mb/s\n",
+                     allocated_up, allocated_down);
 
        /*
         * If we get rounded up request from graphics side, say HBR2 x 4
@@ -1823,19 +2259,25 @@ static int tb_alloc_dp_bandwidth(struct tb_tunnel *tunnel, int *requested_up,
        else if (requested_down_corrected < 0)
                requested_down_corrected = 0;
 
-       tb_port_dbg(in, "corrected bandwidth request %d/%d Mb/s\n",
-                   requested_up_corrected, requested_down_corrected);
+       tb_tunnel_dbg(tunnel, "corrected bandwidth request %d/%d Mb/s\n",
+                     requested_up_corrected, requested_down_corrected);
 
        if ((*requested_up >= 0 && requested_up_corrected > max_up_rounded) ||
            (*requested_down >= 0 && requested_down_corrected > max_down_rounded)) {
-               tb_port_dbg(in, "bandwidth request too high (%d/%d Mb/s > %d/%d Mb/s)\n",
-                           requested_up_corrected, requested_down_corrected,
-                           max_up_rounded, max_down_rounded);
+               tb_tunnel_dbg(tunnel,
+                             "bandwidth request too high (%d/%d Mb/s > %d/%d Mb/s)\n",
+                             requested_up_corrected, requested_down_corrected,
+                             max_up_rounded, max_down_rounded);
                return -ENOBUFS;
        }
 
        if ((*requested_up >= 0 && requested_up_corrected <= allocated_up) ||
            (*requested_down >= 0 && requested_down_corrected <= allocated_down)) {
+               /*
+                * If bandwidth on a link is < asym_threshold transition
+                * the link to symmetric.
+                */
+               tb_configure_sym(tb, in, out, *requested_up, *requested_down);
                /*
                 * If requested bandwidth is less or equal than what is
                 * currently allocated to that tunnel we simply change
@@ -1861,17 +2303,33 @@ static int tb_alloc_dp_bandwidth(struct tb_tunnel *tunnel, int *requested_up,
         * are also in the same group but we use the same function here
         * that we use with the normal bandwidth allocation).
         */
-       ret = tb_available_bandwidth(tb, in, out, &available_up, &available_down);
+       ret = tb_available_bandwidth(tb, in, out, &available_up, &available_down,
+                                    true);
        if (ret)
                goto reclaim;
 
-       tb_port_dbg(in, "bandwidth available for allocation %d/%d Mb/s\n",
-                   available_up, available_down);
+       tb_tunnel_dbg(tunnel, "bandwidth available for allocation %d/%d Mb/s\n",
+                     available_up, available_down);
 
        if ((*requested_up >= 0 && available_up >= requested_up_corrected) ||
            (*requested_down >= 0 && available_down >= requested_down_corrected)) {
+               /*
+                * If bandwidth on a link is >= asym_threshold
+                * transition the link to asymmetric.
+                */
+               ret = tb_configure_asym(tb, in, out, *requested_up,
+                                       *requested_down);
+               if (ret) {
+                       tb_configure_sym(tb, in, out, 0, 0);
+                       return ret;
+               }
+
                ret = tb_tunnel_alloc_bandwidth(tunnel, requested_up,
                                                requested_down);
+               if (ret) {
+                       tb_tunnel_warn(tunnel, "failed to allocate bandwidth\n");
+                       tb_configure_sym(tb, in, out, 0, 0);
+               }
        } else {
                ret = -ENOBUFS;
        }
@@ -1937,7 +2395,7 @@ static void tb_handle_dp_bandwidth_request(struct work_struct *work)
 
        out = tunnel->dst_port;
 
-       if (in->sw->config.depth < out->sw->config.depth) {
+       if (tb_port_path_direction_downstream(in, out)) {
                requested_up = -1;
                requested_down = requested_bw;
        } else {
@@ -1948,12 +2406,15 @@ static void tb_handle_dp_bandwidth_request(struct work_struct *work)
        ret = tb_alloc_dp_bandwidth(tunnel, &requested_up, &requested_down);
        if (ret) {
                if (ret == -ENOBUFS)
-                       tb_port_warn(in, "not enough bandwidth available\n");
+                       tb_tunnel_warn(tunnel,
+                                      "not enough bandwidth available\n");
                else
-                       tb_port_warn(in, "failed to change bandwidth allocation\n");
+                       tb_tunnel_warn(tunnel,
+                                      "failed to change bandwidth allocation\n");
        } else {
-               tb_port_dbg(in, "bandwidth allocation changed to %d/%d Mb/s\n",
-                           requested_up, requested_down);
+               tb_tunnel_dbg(tunnel,
+                             "bandwidth allocation changed to %d/%d Mb/s\n",
+                             requested_up, requested_down);
 
                /* Update other clients about the allocation change */
                tb_recalc_estimated_bandwidth(tb);
@@ -2181,7 +2642,8 @@ static void tb_restore_children(struct tb_switch *sw)
                        continue;
 
                if (port->remote) {
-                       tb_switch_lane_bonding_enable(port->remote->sw);
+                       tb_switch_set_link_width(port->remote->sw,
+                                                port->remote->sw->link_width);
                        tb_switch_configure_link(port->remote->sw);
 
                        tb_restore_children(port->remote->sw);