USB4/Thunderbolt and Type-C display support - #2
DjDeveloperr wants to merge 51 commits into
Conversation
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>
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>
|
Two findings from the 1.
|
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>
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-pcigets 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
asahionto the newusb4-base(which now sits onaurora-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 routesanddrm: apple: route Type-C displays through a shared DCP fabric— differ only in context lines, because fairydust reworked the surroundingdcp_platform_probe. Their added and removed lines are identical to before.