Skip to content

USB4/Thunderbolt and Type-C display support - #2

Open
DjDeveloperr wants to merge 51 commits into
usb4-basefrom
tb
Open

DjDeveloperr wants to merge 51 commits into
usb4-basefrom
tb

Conversation

@DjDeveloperr

@DjDeveloperr DjDeveloperr commented Sep 5, 2026

Copy link
Copy Markdown

USB4/Thunderbolt tunneling and Type-C display support for M1 and M2 Macs, on top of the imported prerequisites in #1.

What it adds

PCIe-C tunneling (commits 7–30). Tunneled PCIe root complexes for the T8103 and T8112 families and the non-Ultra T600x/T602x parts: DT bindings and topology, cable-powered root complexes, DART DMA aliases for tunneled devices, and a lifecycle that quiesces and restores a link as its tunnel comes and goes. nvme-pci gets ordering for non-posted queue accesses across a tunnel. USB4 stays disabled on M1/M2 Ultra, where the routing is not described.

Type-C displays (commits 33–45). Per-port hotplug state and debounce handling in cd321x, with DP alt-mode status cached and replayed; shared display routes through a common DCP fabric in drm/apple, EDID refresh on disconnect, retraining after a USB4 route change, and recovery across link interruptions and suspend.

Robustness fixes (commits 1–6, 31, 32, 46–48). The pipehandler lock is no longer fatal in USB4 mode. The M3 control register keeps its other bits when the core starts. NHIs advertising more ring IRQs than status bits are rejected rather than trusted. The CIO reset controller waits on the shared request register before reconfiguring. dwc3 keeps live ports across system sleep, and the Samsung UART driver stops touching a suspended Apple UART.

Rebase note

Rebased from asahi onto the new usb4-base (which now sits on aurora-wip). All 48 replayed with no conflicts, and 46 of them are byte-identical patches. The two that changed — drm: apple: support shared Type-C display routes and drm: apple: route Type-C displays through a shared DCP fabric — differ only in context lines, because fairydust reworked the surrounding dcp_platform_probe. Their added and removed lines are identical to before.

atcphy_configure_pipehandler_usb4() aborts the mux switch when
atcphy_pipehandler_lock() times out. That lock cannot succeed in this
path: the LOCK_PIPE_IF handshake FSM is clocked by the DWC3 PIPE clock,
and once the connector's lanes are routed to the USB4 controller that
clock is no longer sourced the way a USB3 bring-up sources it. The ACK
never asserts, so every routed-USB4 mux switch fails before
PIPEHANDLER_MUX_CTRL is ever written and the port silently stays on the
previous backend.

The handshake is advisory in this path. It reports that the PIPE
interface has quiesced, but the mux switch does not depend on that once
the lanes have already moved to the USB4 controller.

Linux already encodes that policy everywhere else, just through control
flow rather than a shared fall-through. The dummy path only warns on both
the lock and the unlock, and the sole fatal acquire is the USB3 host BIST
sequence, which genuinely needs the PIPE quiesced. Make the USB4 path
warn and continue like the dummy path, and say why in a comment so the
next refactor doesn't "fix" the missing error check back in.

Observed on a MacBook Pro 14" (M2 Pro, J414s, T6020), where the ACK poll
deadlocked the mux switch deterministically.

Signed-off-by: DjDeveloperr <hey@djdev.me>
…the core

apple_cio_start() releases the ACIO Cortex-M3 with a bare store of
APPLE_CIO_M3_CTRL_START, which clears every other bit of the RC-window
control register at 0x0c.

Apple's own enableEmbeddedCPU reads that register first, logs its current
value ("Current LSTX State") and then does a read-modify-write, so the
register is known to carry at least one other live field. On t6020 it has
been observed reading 0x3 before the kernel touches it, i.e. iBoot leaves
bit 0 set and the bare store clears it.

Nothing here needs those bits cleared, and their meaning is undocumented, so
read-modify-write instead: unknown-and-preserved beats unknown-and-cleared.

Signed-off-by: DjDeveloperr <hey@djdev.me>
apple_cio_ring_index() maps TX ring n to bit n and RX ring n to bit
n + n_rings, and apple_nhi_ring_interrupt_active() and apple_cio_ring_irq()
then use BIT(idx) against the 32-bit IRQ enable and status registers. The
index is derived from the interrupt count in the device tree, which is only
checked for being non-zero and even; the later hop_count comparison is
against that same firmware-provided number and masks off ten bits, so it
does not bound it either.

Nothing in the path stops a firmware description with more than sixteen ring
pairs from shifting a u32 by 32 or more, which is undefined behaviour and
would quietly enable and acknowledge the wrong ring. Reject it at probe
instead. Real hardware reports twelve pairs, so this only rules out
descriptions the register layout cannot express.

Signed-off-by: DjDeveloperr <hey@djdev.me>
…guring

On t600x and t602x every port on a die shares one CIO reconfigure
register, so a request issued for a different port can still be in flight
when this one runs. The driver's mutex serialises its own callers but says
nothing about the hardware state machine, and a request stored while the
block is still busy is dropped.

The block needs two waits rather than one: poll the busy bits until they
clear, store the request bit, then poll again. The same shape applies
across t600x and t602x, with only the register offset and the die split
changing.

Keep the first wait advisory and store the request either way. A timeout
there means the block was busy for 192 ms, which is worth a message, but
failing the reset would turn a slow reconfigure into a probe failure of
the whole USB4 complex.

Worth noting for later: t8103, t8112 and t8122 have no PMGR CIO
reconfigure at all, which is why the t8103 variant here drives a
dedicated register page instead.

Signed-off-by: DjDeveloperr <hey@djdev.me>
…e layouts

The t8103 variant has the same problem the t600x one did: it stores the
request without first checking whether the block is still busy from an
earlier one. Poll bit 1 of the port's control word until it clears, then
set the request bit, keeping the wait advisory for the reason given on the
t600x side.

Bit 1 had no name here because nothing read it. Call it INIT_BUSY.

On t8103 and t8112 the request can also be set with a read-modify-write
rather than a plain store. Deliberately not done here: the existing poll
waits for the register to read exactly INIT_DONE, which works on every
machine this has run on, and preserving unknown bits could stop that value
ever appearing. Noted in the code so the difference reads as a decision
rather than an oversight.

Record the known register layouts while they are in hand. The two variants
here cover four SoCs and give no hint what the next one needs, so anyone
extending this would otherwise redo the work. Three things in that table
matter:

The first argument selects a PMGR register map rather than an address.
Only map 0 is pinned down, the PMGR block itself, which is why t8103
resolves to 0x23b784000 and t6020 to the pmgr node plus 0xa02c. t8122 and
t6030 select map 0x62 and t8112 map 0x63, and those are unresolved. M3 is
therefore blocked on identifying one map index rather than on finding a
register: its offset and bit layout are already known and match t6030's.

t6050 uses two bits per port instead of a request bit and a busy bit
sixteen apart, so it needs a third variant rather than a new offset.

t8132 and t8142 index a per-port table rather than forming an offset, and
are not decoded.

Signed-off-by: DjDeveloperr <hey@djdev.me>
… TBT VDO

The reconstructed cable Discover Mode VDO takes TBT_CABLE_ACTIVE_PASSIVE
from TPS_DATA_STATUS_ACTIVE_CABLE, which is TI's bit 2. CD321x does not
use TI's layout for this register: the upper half is already known to
differ, which is why CD321X_DATA_STATUS_USB4_CONNECTION sits at bit 23
where TI documents FORCE_LSX, and why HPD_IRQ/HPD_LEVEL need their own
defines.

On CD321x the active-cable state is bit 22 of the same register, and the
cable speed field is bits [27:25], matching
TPS_DATA_STATUS_TBT_CABLE_SPEED_MASK as already used here.

This matters beyond the VDO itself. TBT_CABLE_ACTIVE_PASSIVE becomes
TB_VSE_CAP_APPLE_CABLE_INFO_ACTIVE_CABLE in the host router's cable word
and gates BIDIR_LSRX, so describing a passive cable as active programs the
router incorrectly.

Add a CD321x-specific define and use it in the cd321x path only; tps6598x
keeps TI's bit 2. Verified passive on a J414s (T6020), where DATA_STATUS
reads 0x96800011: bit 22 clear, speed field 3. An active cable is still
worth testing against.

Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
A reconnect IRQ can arrive after the update worker releases its lock but before the workqueue clears the running state.  In that window cancel_delayed_work() does not cancel anything and schedule_delayed_work() refuses to queue another pass, leaving the accumulated cable state unprocessed.\n\nUse mod_delayed_work() so a later pass is guaranteed even when the previous update is still completing.

Signed-off-by: DjDeveloperr <hey@djdev.me>
Every CD321x port on these systems references the same external DCP
connector.  A second sink can negotiate DisplayPort Alt Mode while the
DCP mux rejects its route with -EBUSY.  The port nevertheless emitted
shared-connector disconnect events, blanking the display already
carried by another port.

Track display-route ownership only after typec_mux_set() succeeds,
propagate mux failures into that decision, and allow only the owner to
emit disconnect events.  This makes a rejected second display
non-disruptive and keeps the software mux state retryable after an
error.

Fixes: 5a46b5c ("usb: typec: cd321x: isolate hotplug state by port")
Signed-off-by: DjDeveloperr <hey@djdev.me>
cd321x only forwarded a DisplayPort mux state when the alternate mode or
the pin assignment changed.  A partner that re-asserts HPD, or that
changes its DP status while staying in the same mode, produced no mux
notification at all, so a DisplayPort sink could sit powered and
enumerated with no display route ever being established.

Cache the DP status and configuration alongside the mux state and resend
the notification whenever either changes.  Take the HPD level from the
CD321x DATA_STATUS register, which reports it reliably, rather than from
the status_rx VDO.  Clear the cache when the port leaves DP so the next
entry always replays a fresh state.

Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
A Type-C display that briefly loses its route came back as a powered but
blank sink.  DCP discards its display mode on link loss while the DRM
CRTC stays active, so userspace keeps submitting plane-only commits that
never retrain the link, and the swap for such a commit is never
completed.

Track whether the cable is present, re-establish the DPTX session from a
freezable worker when DCP tears its synthetic HPD down with the cable
still attached, and re-apply the active CRTC state once the link-config
callback has run.  Bounce plane-only commits that arrive without a valid
mode to the vblank worker so they cannot sit in front of the recovery
commit with an unsignalled flip event.

Tell DCP whether the routed output supports HPD when connecting the DPTX
port, report the real HPD state back from set_hpd instead of assuming
success, and answer the tiled-display hint so DCP stops retrying it.

Signed-off-by: DjDeveloperr <hey@djdev.me>
Signed-off-by: DjDeveloperr <hey@djdev.me>
The out-of-band Type-C disconnect path marks the connector offline and emits a hotplug event before the DCP callback arrives.  Since that callback then observes no connector state transition, the normal worker never releases the cached EDID.  A replacement display can therefore inherit the previous monitor name even though DCP has discovered its new modes.

Release the cached EDID in the synchronous out-of-band disconnect path before notifying DRM.  The next mode probe will fetch the replacement display EDID from DCPAV.

Fixes: 85dd82b ("drm/apple: support shared Type-C display routes")
Signed-off-by: DjDeveloperr <hey@djdev.me>
Every Type-C port was bound to one fixed external DCP, so a second
DP-alt-mode connection collided with the first and was rejected with
"DP route is already in use by another Type-C port" even when another
external DCP sat idle. The hardware does not work that way: a display
crossbar sits between the DCPs and the Type-C PHYs, and any pipeline it
can reach can drive any port wired to it.

Build a display fabric from the OF graph. Candidate pipelines are grouped
by the remote USB-C connector and the first notification to arrive for a
port does the work, the rest falling through. There is deliberately no
nominated coordinator: fwnode_typec_mux_get() caps the providers one
connector may have and drops the remainder silently, so a nominated route
might never be called and the port would never be routed at all. The
candidate count comes from the device tree, so machines with one, two or
more dcpext instances work without a per-board constant.

A Type-C output is a physical port, not a pipeline, so give each port its
own DRM connector. Userspace keys its per-monitor configuration on the
connector name, so ports are enumerated in device-tree order and a port
keeps its name whichever pipeline drives it. A pipeline whose only output
is Type-C has no connector, and no encoder, of its own.

Give each port a single encoder whose possible_crtcs covers every pipeline
that can drive it, and narrow that mask to the pipeline actually routed
once the fabric picks one. Both halves are needed. Userspace takes the
CRTCs a connector can use to be what all of its encoders have in common,
so one encoder per pipeline would leave nothing in common and the port
would have no usable CRTC at all. Leaving the mask wide instead lets
userspace pair the port with a pipeline holding another monitor's mode
list, which the CRTC's mode check then rejects. The hotplug that follows a
route change makes userspace re-read the mask.

While a port has no pipeline it reports disconnected, so nothing probes an
output whose ->dcp is NULL.

A hybrid pipeline saves its fixed PHY, crossbar and DPTX target, and is
only borrowed while its fixed output is idle. On release it parks its DPTX
target on a Type-C PHY rather than the fixed one: re-targeting an idle
fixed PHY leaves DCP unable to train a link on a later Type-C target,
answering DEVICE_NOT_STARTED until every DPTX call times out.

Signed-off-by: DjDeveloperr <hey@djdev.me>
A display attached over Type-C came back blank after resume and stayed
that way: the compositor reported every page flip as EINVAL, gave up on
the output and tore it down.

DCP reports the display back while the resume modeset is still in flight,
and cb_hotplug() discarded any hotplug seen during a modeset.  Nothing
re-sends it, so the connector stayed marked disconnected for good.  Latch
that state instead and replay it once the modeset completes, taking the
same path it would have taken had it arrived a moment later.

The connector is briefly disconnected either way, because resume marks
the mode valid again before the firmware reports the display back.  A
plane-only commit landing in that window used to be failed outright,
which is what the compositor saw; defer it in dcp_flush() as is already
done while a mode is invalid, and let the check pass.

Signed-off-by: DjDeveloperr <hey@djdev.me>
With the DCP display fabric in place, describe the routes the hardware
actually has instead of pinning every Type-C port to one external DCP.

On the M2 Pro/Max laptops dcpext0 drives the fixed HDMI output but can
also reach all three Type-C PHYs, so give it those PHYs, crossbar
controls and typec-routes alongside its fixed dp-phy, and point each
Type-C connector at both external DCPs.  External DCP 0 uses crossbar
state 0 and external DCP 1 state 2, matching the ADT.

Keep this in the t602x file rather than the shared t600x one: on the
M1 Pro/Max laptops dcpext0 is disabled, and a route node inherits no
status from its parent, so describing routes there would leave
typec_mux_match() deferring forever on a mux that never registers and
take Type-C DP down on those machines entirely.

Drop the stale mux-index from dcpext1, which selects its crossbar per
route, and remove the static displayport phandles wherever a
typec-routes provider now owns hotplug notification, so a connector
cannot be notified twice or through the wrong pipeline.  On the M2 Pro
mini and M2 Max Studio that phandle is guarded by APPLE_T600X_NON_ULTRA,
which only t6001.dtsi defines, so it survives into t602x and has to be
deleted explicitly.  Boards without such a provider keep their route.

Signed-off-by: DjDeveloperr <hey@djdev.me>
The DCP that receives a chunked transfer allocates the buffer and hands
it to the connector, which frees the previous one when the next arrives.
devres ties the buffer to the allocating DCP, but the free uses the
connector's current DCP, and a Type-C port can move to another pipeline
in between.

devres_destroy() then fails to find the resource on the device it is
given, so devm_kfree() warns and the buffer leaks, once per dictionary:

  WARNING: drivers/base/devres.c:1184 at devm_kfree+0x80/0x98

The lifetime is managed explicitly already. Use kzalloc() and kfree(),
and release the dictionaries when the connector is destroyed.

Signed-off-by: DjDeveloperr <hey@djdev.me>
A host controller can need to react to a PCI tunnel going away, not just
to one being established. On Apple silicon the tunneled PCIe hierarchy is
only reachable through the tunnel, so it has to be torn down before the
paths are disabled rather than left behind with nothing underneath it.

Add pci_tunnel_deactivate to tb_nhi_ops as the counterpart to the existing
pre- and post-activate callbacks, and call it at the start of tunnel
deactivation while the hierarchy can still be reached.

Signed-off-by: DjDeveloperr <hey@djdev.me>
Only an NHI shutdown quiesced the tunneled PCI hierarchy. A tunnel torn
down while the router stays up left the endpoint enumerated with its data
path gone, so every access to it blocked until the function driver's own
timeout fired. For NVMe that is around a minute of hung I/O ending in

  nvme nvme1: controller is down; CSTS=0xffffffff, PCI_STATUS=0xffff
  nvme nvme1: Disabling device after reset failure: -19

and the device never came back when the tunnel did, because nothing had
removed it for the host to enumerate again.

Quiesce PCIe-C from the new deactivate callback, and restore it when the
tunnel is activated again. Restoring restarts the ports the way resume
does and rescans the bus.

Quiescing now removes the hierarchy rather than only stopping it. Stopping
unbinds the drivers but leaves the pci_dev objects behind, so a later
rescan finds stale devices: the endpoint returns with an unbalanced
runtime-PM count and its driver fails to probe.

Signed-off-by: DjDeveloperr <hey@djdev.me>
The cable information written to the switch only set the 20 Gbps bit when
the Enter_USB4 data object reported exactly Gen3. That field is three bits
wide and USB4 v2 defines Gen4 above Gen3, so a Thunderbolt 5 cable reports
4 and fails the equality test. The firmware is then told the cable cannot
carry 20 Gbps and mistrains the link:

  thunderbolt-apple-acio 701ac0000.cio: RTKit: syslog message:
    high_speed_lane.c:289: Gen2/3 link error. lane=0, error=89

Those errors repeat until the port gives up. No router is ever found, so a
dock on such a cable enumerates nothing at all, while a Gen3 cable on the
same port works. A device that does link comes up on one lane and its PCIe
tunnel fails to activate.

The host supports nothing beyond Gen3, so any cable reporting Gen3 or
faster can carry 20 Gbps. Compare accordingly.

Signed-off-by: DjDeveloperr <hey@djdev.me>
Marcelo added 2 commits September 10, 2026 09:37
cd321x_update_work() points desc.identity at st.partner_identity, where
"st" is a stack local. typec_register_partner() stores that pointer in
partner->identity rather than copying the structure, so once the work
function returns the partner is left referencing a freed stack page.

Any later read of the partner's PD identity then dereferences it. On a
MacBook Pro (16-inch, M2 Max) reading

  /sys/class/typec/port0-partner/type

as an unprivileged user is enough:

  Unable to handle kernel paging request at virtual address ffff800083a87d0c
  Internal error: Oops: 0000000096000007 [#1] SMP
  CPU: 10 UID: 1000 PID: 83017 Comm: cat
  pc : get_pd_product_type+0x54/0xa4 [typec]
  lr : type_show+0x14/0x3c [typec]

get_pd_product_type() NULL-checks partner->identity and then reads
id->id_header, which is the faulting access; the pointer is not NULL,
just dangling. With VMAP_STACK the dead stack is unmapped, hence the
vmalloc-range fault address.

The driver already keeps a persistent copy in cd321x->cur_partner_identity,
but only assigns it after registration. Fill it in first and register the
partner against that instead. The generic tps6598x path already does the
equivalent, pointing desc.identity at tps->partner_identity.

Fixes: 82432bb ("usb: typec: tipd: Handle mode transitions for CD321x")
Signed-off-by: Marcelo <marcelo@omarchy.org>
usb_mode_show() and usb_capability_show() build a space-separated list
and then overwrite the trailing space with a newline:

	sysfs_emit_at(buf, len - 1, "\n");

When the partner or port advertises none of USB2/USB3/USB4 the loop body
never runs, len stays 0, and the call is made with at == -1:

  invalid sysfs_emit_at: buf:00000000ffaad2a3 at:-1
  WARNING: fs/sysfs/file.c:785 at sysfs_emit_at+0x54/0xd4, CPU#1: cat/892

sysfs_emit_at() rejects the negative offset, so the attribute also reads
back empty rather than as a newline like every other list attribute.

Seen on a MacBook Pro (16-inch, M2 Max), where one of the Type-C partners
registered by the CD321x has no USB capability bits set, by reading

  /sys/class/typec/port1-partner/usb_mode

Return a bare newline when nothing was written.

Fixes: 2140a95 ("usb: typec: Add attribute file showing the USB Modes of the partner")
Signed-off-by: Marcelo <marcelo@omarchy.org>
@maralcbr

Copy link
Copy Markdown

Two findings from the tb branch on a MacBook Pro (16-inch, M2 Max, apple,j416c, Mac14,6/J416c), kernel built from tb as 7.1.12-ARCH-dev. Posting here since the repo has issues disabled — happy to move them wherever you prefer.


1. dcpext2/dcpext3 crash the display bind (null RTKit handle)

The T602x device tree describes only two external display pipelines, so a J416c lights at most two external monitors even though the SoC has four (dispext0dispext3). I described all four and aliased them so m1n1 enables the nodes; all five DCPs then bind, but the aggregate bind dies:

Unable to handle kernel NULL pointer dereference at virtual address 0000000000000098
Internal error: Oops: 0000000096000004 [#1] SMP
CPU: 6 UID: 0 PID: 299 Comm: kworker/u51:5 Tainted: G S  7.1.12-ARCH-dev #6
Hardware name: Apple Inc. Mac14,6/J416c, BIOS 2026.04 04/01/2026
Workqueue: events_unbound deferred_probe_work_func
pc : apple_rtkit_start_ep+0x10/0x78
lr : afk_start+0x28/0x60 [appledrm]
Call trace:
 apple_rtkit_start_ep+0x10/0x78 (P)
 systemep_init+0x58/0xb4 [appledrm]
 dcp_start+0x40/0x174 [appledrm]
 apple_drm_init_dcp+0x430/0x6b0 [appledrm]
 apple_drm_bind+0x110/0x1d0 [appledrm]
 try_to_bring_up_aggregate_device+0x240/0x328
 __component_add+0xa4/0x198
 component_add+0x14/0x20
 dcp_platform_probe+0x818/0xd20 [appledrm]
 platform_probe+0x80/0xac
 really_probe+0xc0/0x3e0
 ...
 deferred_probe_work_func+0xa8/0x104

x0 is 0, so dcp->rtk is still null when dcp_start() runs: the component aggregate binds as soon as the last component registers, while one DCP has not finished its own probe. With two external pipelines the timing apparently never exposes this; with four it reproduces on every boot.

Afterwards /sys/bus/platform/drivers/apple-dcp/ shows all five devices bound (289c00000, 315c00000, 389c00000, 48dc00000, 50dc00000), but appledrm never registers its card — only simpledrm and the GPU are left, so the machine boots with no working display output.

The device-tree side is on my fork if it's useful: maralcbr/linux wip/t602x-dcpext2-dcpext3. It adds dcpext2 (0x4_8dc00000, dart 0x4_8d30c000, IRQs 1022 / 1043–1046, bw-scratch 0x1220, dcp-index 3) and dcpext3 (0x5_0dc00000, IRQs 1058 / 1079–1082, bw-scratch 0x1228, dcp-index 4) plus the matching clk_dispext2/clk_dispext3, and deletes them again for t6020. I left the aliases out there, so the nodes stay off until the driver side can cope. I can test any ordering fix on this machine.


2. atcphy_mux_set() warns on USB-C replug (pipehandler still up)

WARNING: drivers/phy/apple/atc.c:2359 at atcphy_mux_set+0x1010/0x1260 [phy_apple_atc], CPU#10: kworker/10:1/143
Hardware name: Apple Inc. Mac14,6/J416c, BIOS 2026.04 04/01/2026
Workqueue: events cd321x_update_work [tps6598x_core]
Call trace:
 atcphy_mux_set+0x1010/0x1260 [phy_apple_atc] (P)
 typec_mux_set+0x60/0xa0 [typec]
 cd321x_update_work+0x75c/0x8a0 [tps6598x_core]
 process_one_work+0x160/0x4e0

That is the WARN_ON_ONCE(atcphy->pipehandler_up) before the mode switch, whose comment says the pipehandler being up there means a bug somewhere.

How I got there, with the other end being another Mac over a Thunderbolt cable (so the link negotiates USB4 and the ends pick USB data roles at attach):

  1. Port in device role, configfs ACM gadget bound on 702280000.usb, /sys/class/udc/702280000.usb/state reads configured.
  2. Unbind and rebind the gadget. dwc3-apple then logs wait for SETUP phase timed out and ep0 data phase end transfer failed: -110, and the UDC stays not attached.
  3. Unplug and replug the cable. The roles come back the other way round (this machine becomes the USB host) and the warning fires during that transition.

The machine stays up and Thunderbolt reconnects normally. I haven't tried it without the gadget step, so it may simply be that the gadget left the pipehandler up with nothing tearing it down before the mode switch.

DCP exposes an adaptive-sync parameter taking a minimum refresh rate, a
media target rate and a fractional rate. Program it during modesets and
report the panel's range through the standard vrr_capable connector
property, so userspace drives VRR through the atomic VRR_ENABLED CRTC
property rather than a driver-specific knob.

The swap path previously wrote a constant 120 into the three timestamp
fields to hold the internal panel at its maximum rate. Replace that with
architectural counter timestamps taken at swap submit and at present,
which is what the firmware needs in order to vary the rate at all.

An internal ProMotion panel carries no EDID or DisplayID, so DCP reports
no adaptive-sync range for it. Assume the ProMotion floor of 24 Hz up to
the rate the mode itself advertises. Key that off the connector type from
the device tree rather than the main-display flag, which is filled in by
an asynchronous response and is not reliably set by the time the timing
elements are parsed. External displays keep the range DCP reports, and a
reported range is only believed when its maximum exceeds its minimum.

Fill in display_info.monitor_range as well. Nothing else populates it for
a panel with no EDID, so without it the range is missing from debugfs and
from anything else that reads it.

Signed-off-by: DjDeveloperr <hey@djdev.me>
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