Skip to content

thunderbolt: apple: HBR3 for a lone external monitor on the M4 (T8132), HBR2 when a second one comes - #1008

Merged
RyanTheTide merged 2 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/tb-hbr3
Nov 6, 2026
Merged

RyanTheTide merged 2 commits into
aurora-silicon:integration/aurorafrom
erikwb:j713/tb-hbr3

Conversation

@erikwb

@erikwb erikwb commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Follows #955. There, every DP tunnel from a T8132 host router was capped at HBR2 (TB_HOST_DP_HBR2_MAX), because both DP IN streams of the ATC PHY must run at one link rate and two 4K60 streams fit at HBR2 x4. A single monitor doesn't need the cap: alone, a tunnel can run at HBR3 x4 (25920 Mb/s instead of 17280), which carries uncompressed modes HBR2 x4 can't, those between 17.28 and 25.92 Gb/s. (3840x2160 at 120 Hz with DSC fits HBR2 x4 and doesn't need it.) This PR (#955's listed follow-up) lets a lone tunnel use HBR3 and re-links when the number of tunnels changes.

  • One tunnel: up to HBR3, two: HBR2. The flag becomes TB_HOST_DP_SHARED_RATE. The connection manager decides the cap where DP IN and DP OUT adapters are paired, under the domain lock, and gives it to the tunnel before activation:
    • HBR3 when one tunnel will be up on the host router, HBR2 when more will;
    • a tunnel alone that is short of bandwidth takes the combination with the most bandwidth (HBR2 x4 rather than HBR3 x2, which can't carry 4K60);
    • next to another tunnel, or held at HBR2, the rate is kept and lanes go first, as before;
    • in the bandwidth left for other tunnels, a host DP tunnel counts at its cap at most. Once its DPRX is done, what its DP IN adapter reports isn't bounded by the cap, so a display held at HBR2 between adapters that take HBR3 could count as HBR3 x4 and leave a second one only HBR2 x2.
  • A second monitor arrives. When another tunnel is wanted (a second DP OUT becomes available) while one runs above HBR2, that one is torn down and set up again between the same adapters at HBR2 first; then the new one takes its rate. The first display sees an unplug and a plug: it blanks for a few seconds and the compositor brings it back. If it can't be set up again, pairing runs again, and at the next event.
  • The second monitor leaves. After a hot unplug of a DP OUT or a router that took a host DP tunnel, the tunnel left alone below the rate its adapters take is set up again at up to HBR3 once a 2.5 s debounce is over, unless another tunnel is up or can be paired by then. Until that re-link happens, the tunnel keeps its HBR2 cap and counts as HBR2.
  • A monitor arrives during the debounce. The pending re-link up is dropped first. The tunnel waiting for it is held at HBR2, its estimated bandwidth is worked out again and the debounce is stopped, all before the new tunnel is given any bandwidth. The new monitor gets HBR2 x4 next to it, and the first one isn't re-linked at all.
  • Loop guards.
    • The re-link up happens once per unplug: the re-linked tunnel isn't marked again.
    • DPRX failures, failed activations, a PHY refusing a rate and teardown for system sleep never lead to it.
    • If a second tunnel takes the rate back down within a minute of a re-link up, the next debounce doubles, up to 40 s.
    • A tunnel re-linked at HBR3 that can't be set up, or doesn't come up, is set up at HBR2 again, once, keeping its adapters for pairing.
  • System sleep. Nothing is re-linked during a sleep transition: from PREPARE (a PM notifier ahead of all others) until the transition is over, POST callbacks and the rollback of a refused PREPARE included. A work item takes the system sleep lock after POST and only then reopens; PREPARE drops a reopen still pending from an earlier transition. A re-link in progress holds the transition back until it's done. A pairing that needs a re-link waits, and is replayed after the sleep and at resume, thaw and runtime resume. Docked s2idle with the tunnels kept re-links nothing. All re-links run on the domain's ordered workqueue, behind any DPRX or teardown work for the same tunnel.

The PHY needs no change: one stream starting at HBR3 runs alone on DP IN 0 (PCLK1) and on DP IN 1 (PCLK1 prerequisite plus PCLK2), and the one-rate check still refuses HBR2 next to HBR3. Neither does the DRM side: the external outputs' modes are filtered by the trained link (HBR3 x4: 25.92 Gb/s at 8b/10b), and the link rate change and the route release each send a hotplug, so the modes are probed again after a re-link.

KUnit (thunderbolt suite): tb_test_dp_shared_rate_single (HBR3 alone, HBR2 for two, reduction alone vs shared), _second (a second tunnel triggers the re-link down; lanes first with two; a tunnel counts at its cap), _relink_up (debounce, once per unplug, dropped when another comes, back-off), _recover (HBR2 recovery after a failed re-link up), _sleep (no re-link during a transition; the gate stays closed through a refused PREPARE's rollback and through a stale reopen) and _retry (bounded retry when the domain doesn't runtime resume). Four scenario tests follow the tunnels on a dock's link (34650 Mb/s for DP): _lone (one monitor at HBR3 x4), _two (a second one arrives: both HBR2 x4), _leave (the second leaves; held and counted at HBR2 through the debounce, then HBR3 x4) and _flap (quick back-and-forth: no re-link within the debounce, a new debounce at each unplug, the doubled debounce after a quick re-link down). Three tests run the connection manager itself on a domain with no hardware, the adapter registers read through a KUnit static stub on tb_cfg_read(), the DP IN adapter reporting HBR3 x4 once its DPRX is done: _counted (the bandwidth accounting, tb_consumed_dp_bandwidth(), counts a tunnel capped at HBR2 at HBR2 x4, from its caps and from its allocated bandwidth), _debounce (a tunnel marked by tb_dp_host_tunnel_gone(); a second display arrives and pairing, tb_dp_host_rate_limit(), drops the re-link up, holds the tunnel at HBR2 and stops the debounce timer, after which the new one gets HBR2 x4; in both modes) and _debounce_deferred (the same with pairing deferred for system sleep: the re-link up is dropped, the re-link work stays armed). tb_cm_init() is split out of tb_probe() so that the tests set the connection manager up as it is set up for a real domain.

Testing:

  • KUnit, tools/testing/kunit/kunit.py run --arch=arm64 with CONFIG_USB4_KUNIT_TEST, CONFIG_USB4_APPLE_DPIN_KUNIT_TEST and the Apple ATC PHY and DCP fabric suites: 269 tests, 267 passed, 2 skipped (unrelated: one needs CONFIG_UCLAMP_TASK, one is covered elsewhere). The thunderbolt suite passes 69/69, including the 13 tb_test_dp_shared_rate_* cases, with no warnings in the kernel log. The first commit on its own: 261 tests, 259 passed, the same 2 skipped.

  • The tests fail when the fix is taken out, one part at a time:

    • without the call that drops the re-link up in tb_dp_host_rate_limit(): _debounce and _debounce_deferred;
    • with the caps counted uncapped in tb_dp_consumed_bandwidth(): _counted, _debounce and _debounce_deferred;
    • with the allocated bandwidth counted uncapped: _counted and _debounce.
  • tools/asahi/tb-dp-repair-tests.py: 16 cases pass, on both commits.

  • scripts/checkpatch.pl --strict: clean on both commits.

  • arm64 build of the J713 config: no new warnings in drivers/thunderbolt.

  • J713 with a CalDigit TS5 dock and two 3840x2160 monitors:

    • One monitor: the tunnel runs at HBR3 x4.
    • A second one arrives: the first is re-linked at HBR2 and both come up at 4K (one at 120 Hz with DSC, one at 60 Hz).
    • The second one leaves: the first is re-linked at HBR3 once the debounce is over; the debounce backs off to 5 s and 10 s after quick re-links down.
    • A monitor replugged during the debounce: the re-link up is dropped, the other tunnel stays at HBR2, and the new one is given 17500 Mb/s and comes up at 4K, with either monitor replugged.
    • Docked s2idle with two monitors re-links nothing; the dock works on either Type-C port.
  • Open: once, a monitor replugged during the debounce had every bandwidth request of its new tunnel refused ("not enough bandwidth available"), and came up at 1920x1080 at 60 Hz; 3840x2160 at 60 Hz could be set right after. Two more replugs during the debounce, one with each monitor and with debug logging, went as described above (the new tunnel offered 18400 Mb/s and given 17500), so the cause isn't known yet.

This was developed and tested by Claude (Anthropic), working with me on the J713.

TB_HOST_DP_HBR2_MAX caps every DP tunnel from the host router's DP IN
adapters at HBR2, because both DP IN streams of the PHY must run at one
link rate. A single display does not need that cap: alone on the host
router a DP tunnel can run at HBR3 x4 (25920 Mb/s), which carries modes
that HBR2 x4 (17280 Mb/s) cannot.

Rename the flag to TB_HOST_DP_SHARED_RATE and decide the cap where DP IN
and DP OUT adapters are paired, under the domain lock:

- when one DP tunnel will be up on the host router, it may offer up to
  HBR3; when more will, each new one offers HBR2 at most;
- when another DP tunnel is wanted (a second DP OUT adapter became
  available) while one is up above HBR2, that one is torn down and set up
  again between the same adapters at HBR2 first; the new one then takes
  its rate. Its display sees an unplug and a plug. If it cannot be set up
  again, pairing runs once more, and at the next event;
- a tunnel alone that has to be reduced takes the combination with the
  most bandwidth (HBR2 x4 rather than HBR3 x2). Held at HBR2, or next to
  another tunnel, it keeps the rate and gives up lanes as before.

A host DP tunnel is also counted at its cap at most in the bandwidth
left for the others. Once its DPRX is done, the bandwidth it takes is
worked out from what its DP IN adapter reports, which the cap does not
bound: held at HBR2 between adapters that take HBR3, it could be counted
at HBR3 x4, and a second display next to it would then get HBR2 x2
rather than x4.

Nothing is set up again during a system sleep transition: from its
PREPARE event (a PM notifier ahead of all others) until it is over, POST
callbacks and the rollback of a refused PREPARE included (the gate is
reopened by a work item that takes the system sleep lock after the POST
event); a re-link admitted before holds the transition back until it is
done. The re-link, and the pairing that needs it, then wait:
a work item on the freezable workqueue hands them to the domain's ordered
workqueue once the transition is over, behind any DPRX or teardown work
queued before. The request also stays pending for the resume and thaw
replays, and a failed runtime resume of the domain is retried a few
times.

The KUnit tests reach the connection manager's DP bandwidth accounting
on a domain set up without hardware, reading adapter registers through
a static stub on tb_cfg_read(); tb_cm_init() is split out of
tb_probe() for that. The DP repair host test models the connection
manager's private data: add the new work item there, and take the work
initializers from tb_cm_init().

Signed-off-by: Erik Bourget <erik@ebourget.net>
Under TB_HOST_DP_SHARED_RATE two DP tunnels from the host router run at
HBR2. When one of them goes because its display or its router was
unplugged, the one left stays at HBR2 although it could now run at HBR3.

Set it up again at up to HBR3, between the same adapters, once a
debounce of 2.5 s is over, from the re-link work on the domain's ordered
workqueue (so that DPRX or teardown work queued before it has released
its paths):

- only after a hot unplug of a DP OUT adapter or a router that took DP
  tunnels from the host router, and only for the one left alone below the
  rate both its adapters take. DPRX failures, failed activations and
  teardown for system sleep never lead to it;
- once per unplug: the tunnel set up again is not marked;
- dropped when another DP tunnel is up or can be paired by then, so an
  unplug and replug, or a display that drops and asserts HPD again while
  it retrains, causes no re-link. Until it is set up again the tunnel
  keeps its HBR2 cap and is counted at HBR2. When another one is wanted
  during the debounce, the tunnel is held at HBR2, its estimated
  bandwidth is worked out again and the debounce is stopped, all before
  the new one is given any bandwidth, so the new one gets HBR2 x4 next
  to it;
- when a second DP tunnel takes the rate back down within a minute of
  such a re-link, the next debounce is doubled, up to 40 s;
- not during a system sleep transition: tunnels that go then mark
  nothing, and a re-link that is due waits for the transition to end; a
  marked tunnel is looked at again at resume and runtime resume;
- if the tunnel cannot be set up at the higher rate, it is set up at
  HBR2 again at once; if it is set up but does not come up (DPRX), it is
  set up at HBR2 again once its DPRX work is over, with its adapters kept
  for pairing and no new re-link up. If neither works, pairing runs again,
  at HBR2, and at the next event.

The KUnit tests run the marking and the pairing on such a domain too.
The DP repair host test models the connection manager: add the new
fields there, with no tunnel re-linked at a higher rate.

Signed-off-by: Erik Bourget <erik@ebourget.net>
@RyanTheTide

Copy link
Copy Markdown
Contributor

Merge train (MTR, 06/11): merging. An independent adversarial review found it gated:

  • TB_HOST_DP_SHARED_RATE is only in the M4 DP-IN policy (apple_cio_t8132_hw), and the KUnit tests assert m1/m2/m3 lack it.
  • max_rate is only non-zero under that policy.
  • The re-link work holds a domain reference per queued run. tb_stop/tb_deinit cancel it and the work re-checks stopping under tb->lock.

Two small changes reach every Thunderbolt host: a PM notifier (lock only, no state change) and a KUnit-only tb_cfg_read hook. The thunderbolt KUnit suite (69 cases) and the host tb-dp-repair-tests.py pass. We have no J713, so the hardware proof is your one/two-monitor run.

Gates: test merge 2d3579ef = integration/aurora 11eccbe + #1005 #994 #930 #658 #992 #1006 #1007 #1008 #531 (#992 has since landed on its own); base 11eccbe.

  • KUnit: drm/apple 112/113 (1 skip); Rust (asahi, incl. asahi_g17_validation_pages) 200/201 (1 skip); arm64 apple-isp-*, apple-pmc, apple-pmgr-pwrstate and thunderbolt 137/138 (1 skip).
  • W=1 on drm/apple, media/apple/isp, pmdomain/apple, thunderbolt and nvmem/apple-spmi-nvmem: 0 new warnings.
  • dt_binding_check (apple,dcp, apple,dart, apple,t8132-isp, apple,spmi-nvmem, apple,pmc): 0 new.
  • Host tests run-host-tests.py 185/185; apple-dcp selftests 86/86.

Hardware (7.3.0-aurora-mtr1106a, one-shot boots, fleet recipe):

  • J616s (M4 Pro) vs fleet17: 0 kernel errors, 0 AVC, 0 failed units; eDP + DP-2, Wi-Fi, Vulkan G16S. Camera NV12 1080p and 720p, and P010 720p, streamed 30 frames each, with no ISP errors. vkcube OK; glmark2 6617/6738/6683 vs fleet17 6646/6555/6664.
  • Neo (A18 Pro), two boots vs fleet16: 0 kernel errors, 0 AVC, os_uuid pin present; eDP, Wi-Fi, Vulkan G17P. Camera NV12 1080p and 720p, no ISP errors. vkcube OK; glmark2 2718/2729/2726/2725 vs fleet16 2043–2108; 0 GPU faults.
  • Both Macs were restored to their default kernels afterwards.

@RyanTheTide
RyanTheTide merged commit 3b5b3a3 into aurora-silicon:integration/aurora Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants