From 736d7350a4faa4a5b5c322b4e6879a918314b2a7 Mon Sep 17 00:00:00 2001 From: Brian Toll Date: Sun, 18 Oct 2026 09:23:23 -0400 Subject: [PATCH] thunderbolt: share an Apple host link between DP tunnels that come up together The DP IN adapters of an Apple silicon host never report the trained link rate in DP_COMMON_CAP, so tb_dp_consumed_bandwidth() keeps charging a DP tunnel the full rate it was offered in tb_dp_xchg_caps(). When two DP OUT adapters of one router come up together, the two tiles of a 5K display or two monitors on a dock, the first tunnel is offered all that fits, HBR3 x4 (25.92 Gb/s of the 40 Gb/s link), and the second is left with HBR x4 (8.64 Gb/s). That is too little for a 2560x2880@60 tile (about 14 Gb/s at 10 bpc), so a tiled 5K display only runs its single-stream modes. A Studio Display XDR on a MacBook Pro 14" M1 Max (j314c) shows this: 3840x2160@120 works (one DSC stream on the first tile), 5120x2880@60 fails in set_digital_out_mode ("setmode failed", 80000104) with the second DPTX port trained at HBR (DP IN adapter 6: DP_REMOTE_CAP 0x0547a135, DP_COMMON_CAP 0x95402134). Lowering the rate the DCP firmware is offered through GET_MAX_LINK_RATE does not help on its own: the first tunnel still books HBR3 x4 in the connection manager. The second tile cannot be seen coming: the display raises HPD on its second DP OUT adapter only after the first tile's link has trained (dyndbg: "1:12: DP OUT resource available after hotplug" follows the first port's SET_LINK_RATE), and until then that adapter looks exactly like the router's third DP OUT adapter, which never gets a sink (same DP_LOCAL_CAP 0x05c7a335, no HPD, no DP_REMOTE_CAP). Counting adapters instead of HPDs therefore offers the first tile a third of the link. So, when a tunnel starts at an Apple host DP IN adapter, offer it an even share of the available bandwidth among the DP OUT adapters of the destination router that have HPD asserted and no tunnel yet, and let the first DP tunnel to a router with more than one DP OUT adapter reserve room for one more. tb_dp_reduce_bandwidth() turns the share into the best fitting rate and lane count as for any other bandwidth-limited tunnel. On the 34.65 Gb/s left for DP on a 40 Gb/s link the first tile gets 17325 Mb/s, HBR2 x4, and the second the remaining 17370 Mb/s, HBR2 x4 again. A router with a single DP OUT adapter, a wider link, or a host that reports its trained rate is not affected. The cost is that a single monitor on a dock with two DP OUT adapters is offered half the link (HBR2 x4 on 40 Gb/s, enough for 4K60 at 10 bpc) instead of HBR3 x4. The old behaviour is available with thunderbolt.apple_dpin_share_bandwidth=0. The M2 Pro/Max DP IN profile (no TB_HOST_DP_NOTIFY) is not covered by tb_port_is_apple_host_dpin() and is left as it was. Tested on j314c with a Studio Display XDR over an Apple Thunderbolt 5 cable (USB4, 2x20 Gb/s). Plugging at 3840x2160@120: tunnel 0:5<->1:11 logs "sharing the path with 2 DP OUT adapters, offering 17325 Mb/s", tunnel 0:6<->1:12 gets the remaining 17370 Mb/s, both DPTX ports report SET_LINK_RATE 0x14 with 4 lanes. 5120x2880@60 then comes up as two uncompressed tiles, both at plug time and through a compositor mode change in either direction, including after the firmware has idled the second tile's link (it sends SET_LINK_RATE 0 for it 30 s into any single-stream mode). Without the change the first tile trains HBR3 x4, the second HBR x4, and the 5K mode fails as above. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01QhYiD43tUYf8SqrkTnG5LQ Signed-off-by: Brian Toll --- drivers/thunderbolt/tunnel.c | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/drivers/thunderbolt/tunnel.c b/drivers/thunderbolt/tunnel.c index 3c99c25df6f4ef..68b73207abc650 100644 --- a/drivers/thunderbolt/tunnel.c +++ b/drivers/thunderbolt/tunnel.c @@ -914,6 +914,53 @@ static int tb_dp_reduce_bandwidth(int max_bw, u32 in_rate, u32 in_lanes, return -ENOSR; } +/* + * The DP IN adapters of an Apple silicon host never report the trained link + * rate in DP_COMMON_CAP, so tb_dp_consumed_bandwidth() keeps charging a + * tunnel the full rate it was offered. When two DP OUT adapters of one + * router come up together, the two tiles of a 5K display or two monitors on + * a dock, the first tunnel is offered all that fits (HBR3 x4, 25.92 Gb/s of + * a 40 Gb/s link) and the second is left with HBR x4, too little for a + * 2560x2880@60 tile. Offer each of them an even share instead: 2 x HBR2 x4 + * fits the 40 Gb/s link, and a host or sink that needs no sharing is not + * limited. A dock with two DP OUT adapters and one monitor gets half the + * link for that monitor (HBR2 x4 on 40 Gb/s, enough for 4K60 at 10 bpc). + */ +static bool apple_dpin_share_bandwidth = true; +module_param(apple_dpin_share_bandwidth, bool, 0444); +MODULE_PARM_DESC(apple_dpin_share_bandwidth, + "Offer a DP tunnel from an Apple host DP IN adapter an even share of the available bandwidth when other DP OUT adapters of the same router have no tunnel yet (default: true)"); + +/* + * DP OUT adapters of @out's router that share the bandwidth of the path to + * it with this tunnel, @out included: those with HPD asserted and no tunnel + * yet. The second tile of a Studio Display XDR raises HPD only after the + * first tile's link is trained, and a DP OUT adapter without a sink (the + * display's third one) never does, so the first DP tunnel to a router with + * more than one DP OUT adapter reserves room for one more instead of + * counting them. + */ +static int tb_dp_pending_sinks(struct tb_port *out) +{ + struct tb_port *port; + int outs = 0, tunneled = 0, sinks = 0; + + tb_switch_for_each_port(out->sw, port) { + if (!tb_port_is_dpout(port)) + continue; + outs++; + if (tb_port_is_enabled(port)) + tunneled++; + else if (port == out || tb_dp_port_hpd_is_active(port) > 0) + sinks++; + } + + if (!tunneled && outs > 1 && sinks < 2) + sinks = 2; + + return sinks; +} + static int tb_dp_xchg_caps(struct tb_tunnel *tunnel) { u32 out_dp_cap, out_rate, out_lanes, in_dp_cap, in_rate, in_lanes, bw; @@ -975,6 +1022,18 @@ static int tb_dp_xchg_caps(struct tb_tunnel *tunnel) else max_bw = tunnel->max_up; + if (apple_dpin_share_bandwidth && max_bw > 0 && + tb_port_is_apple_host_dpin(in)) { + int sinks = tb_dp_pending_sinks(out); + + if (sinks > 1) { + max_bw /= sinks; + tb_tunnel_info(tunnel, + "sharing the path with %d DP OUT adapters, offering %d Mb/s\n", + sinks, max_bw); + } + } + if (max_bw && bw > max_bw) { u32 new_rate, new_lanes, new_bw;