Skip to content

Commit

Permalink
thunderbolt: Handle DisplayPort tunnel activation asynchronously
Browse files Browse the repository at this point in the history
Sometimes setting up a DisplayPort tunnel may take quite long time. The
reason is that the graphics driver (DPRX) is expected to issue read of
certain monitor capabilities over the AUX channel and the "suggested"
timeout from VESA is 5 seconds. If there is no graphics driver loaded
this does not happen and currently we timeout and tear the tunnel down.
The reason for this is that at least Intel discrete USB4 controllers do
not send plug/unplug events about whether the DisplayPort cable from the
GPU to the controller is connected or not, so in order to "release" the
DisplayPort OUT adapter (the one that has monitor connected) we must
tear the tunnel down after this timeout has been elapsed.

In typical cases there is always graphics driver loaded, and also all
the cables are connected but for instance in Intel graphics CI they only
load the graphics driver after the system is fully booted up. This
makes the driver to tear down the DisplayPort tunnel. To help this case
we allow passing bigger or indefinite timeout through a new module
parameter (dprx_timeout). To keep the driver bit more responsive during
that time we change the way DisplayPort tunnels get activated. We first
do the normal tunnel setup and then run the polling of DPRX capabilities
read completion in a separate worker. This also makes the driver to
accept bandwidth requests to already established DisplayPort tunnels
more responsive.

If the tunnel still fails to establish we will tear it down and remove
the DisplayPort IN adapter from the dp_resource list to avoid using it
again (unless we get hotplug to that adapter).

Signed-off-by: Mika Westerberg <mika.westerberg@linux.intel.com>
  • Loading branch information
Mika Westerberg committed Jan 3, 2025
1 parent a70cd9c commit d6d458d
Show file tree
Hide file tree
Showing 4 changed files with 444 additions and 208 deletions.
174 changes: 127 additions & 47 deletions drivers/thunderbolt/tb.c
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@
#define TB_TIMEOUT 100 /* ms */
#define TB_RELEASE_BW_TIMEOUT 10000 /* ms */

/*
* How many time bandwidth allocation request from graphics driver is
* retried if the DP tunnel is still activating.
*/
#define TB_BW_ALLOC_RETRIES 3

/*
* Minimum bandwidth (in Mb/s) that is needed in the single transmitter/receiver
* direction. This is 40G - 10% guard band bandwidth.
Expand Down Expand Up @@ -69,15 +75,20 @@ static inline struct tb *tcm_to_tb(struct tb_cm *tcm)
}

struct tb_hotplug_event {
struct work_struct work;
struct delayed_work work;
struct tb *tb;
u64 route;
u8 port;
bool unplug;
int retry;
};

static void tb_scan_port(struct tb_port *port);
static void tb_handle_hotplug(struct work_struct *work);
static void tb_dp_resource_unavailable(struct tb *tb, struct tb_port *port,
const char *reason);
static void tb_queue_dp_bandwidth_request(struct tb *tb, u64 route, u8 port,
int retry, unsigned long delay);

static void tb_queue_hotplug(struct tb *tb, u64 route, u8 port, bool unplug)
{
Expand All @@ -91,8 +102,8 @@ static void tb_queue_hotplug(struct tb *tb, u64 route, u8 port, bool unplug)
ev->route = route;
ev->port = port;
ev->unplug = unplug;
INIT_WORK(&ev->work, tb_handle_hotplug);
queue_work(tb->wq, &ev->work);
INIT_DELAYED_WORK(&ev->work, tb_handle_hotplug);
queue_delayed_work(tb->wq, &ev->work, 0);
}

/* enumeration & hot plug handling */
Expand Down Expand Up @@ -962,7 +973,7 @@ static int tb_tunnel_usb3(struct tb *tb, struct tb_switch *sw)
return 0;

err_free:
tb_tunnel_free(tunnel);
tb_tunnel_put(tunnel);
err_reclaim:
if (tb_route(parent))
tb_reclaim_usb3_bandwidth(tb, down, up);
Expand Down Expand Up @@ -1726,7 +1737,7 @@ static void tb_deactivate_and_free_tunnel(struct tb_tunnel *tunnel)
break;
}

tb_tunnel_free(tunnel);
tb_tunnel_put(tunnel);
}

/*
Expand Down Expand Up @@ -1863,12 +1874,76 @@ static struct tb_port *tb_find_dp_out(struct tb *tb, struct tb_port *in)
return NULL;
}

static void tb_dp_tunnel_active(struct tb_tunnel *tunnel, void *data)
{
struct tb_port *in = tunnel->src_port;
struct tb_port *out = tunnel->dst_port;
struct tb *tb = data;

mutex_lock(&tb->lock);
if (tb_tunnel_is_active(tunnel)) {
int consumed_up, consumed_down, ret;

tb_tunnel_dbg(tunnel, "DPRX capabilities read completed\n");

/* If fail reading tunnel's consumed bandwidth, tear it down */
ret = tb_tunnel_consumed_bandwidth(tunnel, &consumed_up,
&consumed_down);
if (ret) {
tb_tunnel_warn(tunnel,
"failed to read consumed bandwidth, tearing down\n");
tb_deactivate_and_free_tunnel(tunnel);
} else {
tb_reclaim_usb3_bandwidth(tb, in, out);
/*
* Transition the links to asymmetric if the
* consumption exceeds the threshold.
*/
tb_configure_asym(tb, in, out, consumed_up,
consumed_down);
/*
* Update the domain with the new bandwidth
* estimation.
*/
tb_recalc_estimated_bandwidth(tb);
/*
* In case of DP tunnel exists, change host
* router's 1st children TMU mode to HiFi for
* CL0s to work.
*/
tb_increase_tmu_accuracy(tunnel);
}
} else {
struct tb_port *in = tunnel->src_port;

/*
* This tunnel failed to establish. This means DPRX
* negotiation most likely did not complete which
* happens either because there is no graphics driver
* loaded or not all DP cables where connected to the
* discrete router.
*
* In both cases we remove the DP IN adapter from the
* available resources as it is not usable. This will
* also tear down the tunnel and try to re-use the
* released DP OUT.
*
* It will be added back only if there is hotplug for
* the DP IN again.
*/
tb_tunnel_warn(tunnel, "not active, tearing down\n");
tb_dp_resource_unavailable(tb, in, "DPRX negotiation failed");
}
mutex_unlock(&tb->lock);

tb_domain_put(tb);
}

static void tb_tunnel_one_dp(struct tb *tb, struct tb_port *in,
struct tb_port *out)
{
int available_up, available_down, ret, link_nr;
struct tb_cm *tcm = tb_priv(tb);
int consumed_up, consumed_down;
struct tb_tunnel *tunnel;

/*
Expand Down Expand Up @@ -1920,47 +1995,29 @@ static void tb_tunnel_one_dp(struct tb *tb, struct tb_port *in,
available_up, available_down);

tunnel = tb_tunnel_alloc_dp(tb, in, out, link_nr, available_up,
available_down);
available_down, tb_dp_tunnel_active,
tb_domain_get(tb));
if (!tunnel) {
tb_port_dbg(out, "could not allocate DP tunnel\n");
goto err_reclaim_usb;
}

if (tb_tunnel_activate(tunnel)) {
list_add_tail(&tunnel->list, &tcm->tunnel_list);

ret = tb_tunnel_activate(tunnel);
if (ret && ret != -EINPROGRESS) {
tb_port_info(out, "DP tunnel activation failed, aborting\n");
list_del(&tunnel->list);
goto err_free;
}

/* If fail reading tunnel's consumed bandwidth, tear it down */
ret = tb_tunnel_consumed_bandwidth(tunnel, &consumed_up, &consumed_down);
if (ret)
goto err_deactivate;

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.
*/
tb_configure_asym(tb, in, out, consumed_up, consumed_down);

/* Update the domain with the new bandwidth estimation */
tb_recalc_estimated_bandwidth(tb);

/*
* In case of DP tunnel exists, change host router's 1st children
* TMU mode to HiFi for CL0s to work.
*/
tb_increase_tmu_accuracy(tunnel);
return;

err_deactivate:
tb_tunnel_deactivate(tunnel);
err_free:
tb_tunnel_free(tunnel);
tb_tunnel_put(tunnel);
err_reclaim_usb:
tb_reclaim_usb3_bandwidth(tb, in, out);
tb_domain_put(tb);
err_detach_group:
tb_detach_bandwidth_group(in);
err_dealloc_dp:
Expand Down Expand Up @@ -2180,7 +2237,7 @@ static int tb_disconnect_pci(struct tb *tb, struct tb_switch *sw)

tb_tunnel_deactivate(tunnel);
list_del(&tunnel->list);
tb_tunnel_free(tunnel);
tb_tunnel_put(tunnel);
return 0;
}

Expand Down Expand Up @@ -2210,7 +2267,7 @@ static int tb_tunnel_pci(struct tb *tb, struct tb_switch *sw)
if (tb_tunnel_activate(tunnel)) {
tb_port_info(up,
"PCIe tunnel activation failed, aborting\n");
tb_tunnel_free(tunnel);
tb_tunnel_put(tunnel);
return -EIO;
}

Expand Down Expand Up @@ -2269,7 +2326,7 @@ static int tb_approve_xdomain_paths(struct tb *tb, struct tb_xdomain *xd,
return 0;

err_free:
tb_tunnel_free(tunnel);
tb_tunnel_put(tunnel);
err_clx:
tb_enable_clx(sw);
mutex_unlock(&tb->lock);
Expand Down Expand Up @@ -2332,7 +2389,7 @@ static int tb_disconnect_xdomain_paths(struct tb *tb, struct tb_xdomain *xd,
*/
static void tb_handle_hotplug(struct work_struct *work)
{
struct tb_hotplug_event *ev = container_of(work, typeof(*ev), work);
struct tb_hotplug_event *ev = container_of(work, typeof(*ev), work.work);
struct tb *tb = ev->tb;
struct tb_cm *tcm = tb_priv(tb);
struct tb_switch *sw;
Expand Down Expand Up @@ -2637,7 +2694,7 @@ static int tb_alloc_dp_bandwidth(struct tb_tunnel *tunnel, int *requested_up,

static void tb_handle_dp_bandwidth_request(struct work_struct *work)
{
struct tb_hotplug_event *ev = container_of(work, typeof(*ev), work);
struct tb_hotplug_event *ev = container_of(work, typeof(*ev), work.work);
int requested_bw, requested_up, requested_down, ret;
struct tb_tunnel *tunnel;
struct tb *tb = ev->tb;
Expand All @@ -2664,7 +2721,7 @@ static void tb_handle_dp_bandwidth_request(struct work_struct *work)
goto put_sw;
}

tb_port_dbg(in, "handling bandwidth allocation request\n");
tb_port_dbg(in, "handling bandwidth allocation request, retry %d\n", ev->retry);

tunnel = tb_find_tunnel(tb, TB_TUNNEL_DP, in, NULL);
if (!tunnel) {
Expand Down Expand Up @@ -2717,12 +2774,33 @@ 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)
if (ret == -ENOBUFS) {
tb_tunnel_warn(tunnel,
"not enough bandwidth available\n");
else
} else if (ret == -ENOTCONN) {
tb_tunnel_dbg(tunnel, "not active yet\n");
/*
* We got bandwidth allocation request but the
* tunnel is not yet active. This means that
* tb_dp_tunnel_active() is not yet called for
* this tunnel. Allow it some time and retry
* this request a couple of times.
*/
if (ev->retry < TB_BW_ALLOC_RETRIES) {
tb_tunnel_dbg(tunnel,
"retrying bandwidth allocation request\n");
tb_queue_dp_bandwidth_request(tb, ev->route,
ev->port,
ev->retry + 1,
msecs_to_jiffies(50));
} else {
tb_tunnel_dbg(tunnel,
"run out of retries, failing the request");
}
} else {
tb_tunnel_warn(tunnel,
"failed to change bandwidth allocation\n");
}
} else {
tb_tunnel_dbg(tunnel,
"bandwidth allocation changed to %d/%d Mb/s\n",
Expand All @@ -2743,7 +2821,8 @@ static void tb_handle_dp_bandwidth_request(struct work_struct *work)
kfree(ev);
}

static void tb_queue_dp_bandwidth_request(struct tb *tb, u64 route, u8 port)
static void tb_queue_dp_bandwidth_request(struct tb *tb, u64 route, u8 port,
int retry, unsigned long delay)
{
struct tb_hotplug_event *ev;

Expand All @@ -2754,8 +2833,9 @@ static void tb_queue_dp_bandwidth_request(struct tb *tb, u64 route, u8 port)
ev->tb = tb;
ev->route = route;
ev->port = port;
INIT_WORK(&ev->work, tb_handle_dp_bandwidth_request);
queue_work(tb->wq, &ev->work);
ev->retry = retry;
INIT_DELAYED_WORK(&ev->work, tb_handle_dp_bandwidth_request);
queue_delayed_work(tb->wq, &ev->work, delay);
}

static void tb_handle_notification(struct tb *tb, u64 route,
Expand All @@ -2775,7 +2855,7 @@ static void tb_handle_notification(struct tb *tb, u64 route,
if (tb_cfg_ack_notification(tb->ctl, route, error))
tb_warn(tb, "could not ack notification on %llx\n",
route);
tb_queue_dp_bandwidth_request(tb, route, error->port);
tb_queue_dp_bandwidth_request(tb, route, error->port, 0, 0);
break;

default:
Expand Down Expand Up @@ -2830,7 +2910,7 @@ static void tb_stop(struct tb *tb)
*/
if (tb_tunnel_is_dma(tunnel))
tb_tunnel_deactivate(tunnel);
tb_tunnel_free(tunnel);
tb_tunnel_put(tunnel);
}
tb_switch_remove(tb->root_switch);
tcm->hotplug_active = false; /* signal tb_handle_hotplug to quit */
Expand Down Expand Up @@ -3026,7 +3106,7 @@ static int tb_resume_noirq(struct tb *tb)
if (tb_tunnel_is_usb3(tunnel))
usb3_delay = 500;
tb_tunnel_deactivate(tunnel);
tb_tunnel_free(tunnel);
tb_tunnel_put(tunnel);
}

/* Re-create our tunnels now */
Expand Down
Loading

0 comments on commit d6d458d

Please sign in to comment.