While debugging #634 I looked at why DP only works on USB-C Left Front on the M1 Pro. As far as I can tell it's only the static wiring in the DT (ENABLE_DCPEXT_TYPEC, /* hacks */), not a hardware or firmware limit. I tested that on a MacBookPro18,3 (t6000, j314s) with fairydust ce9f2eba72c0, and I'd like to check a direction before writing real code.
How it's wired today
dcpext0 is fixed to the HDMI port (atcphy3, mux-index = <0>), and dcpext1 to Left Front (atcphy1, mux-index = <2>, apple,dptx-phy = <1>, displayport = <&dcpext1> on typec1 only).
dcp_platform_probe() selects the crossbar once and never deselects it. dcp->phy and dcp->dptx_phy come from the DT at probe time.
- There is one DRM connector per DCP, and its fwnode is the DCP node.
The pieces for doing this dynamically are mostly there already:
- Each ATC PHY has its own display crossbar, and on t6000 it accepts any dispext (
n_ufp = 9, state = dispext × 2 + sub-output). Deselecting it goes back to MUX_IDLE_DISCONNECT.
- The firmware gets the target PHY with every
dptxport_connect(service, 0, atc, die) call, not once at boot.
Experiments
-
dcpext1 statically on Left Back. A DT-only change: phys = <&atcphy0 PHY_TYPE_DP>, mux-controls = <&atcphy0_xbar 0>, mux-index = <2>, apple,dptx-phy = <0>, and displayport moved to typec0. The crossbar logged 70304c000.mux: Switched dpphy to dispext1,0. A plug on Left Back gave link, EDID (19 modes) and a 1920x1080@60 modeset, with no new firmware errors, and unplug tore down cleanly.
-
Moving dcpext1 between two ports at runtime. This was throwaway code: dcpext1 got both PHYs and crossbars in the DT, plus a debugfs knob. While nothing is connected, the knob deselects the old crossbar, mux_control_try_select()s the new one, and swaps dcp->phy and dcp->dptx_phy. It worked in one boot, both ways, with no reboot or firmware restart:
302.050019 b0304c000.mux: Switched dpphy to disconnected state
302.050342 70304c000.mux: Switched dpphy to dispext1,0
302.050568 28cc00000.dcp: DP route 0 -> 1, dptx target phy 0
323.289997 28cc00000.dcp: dcp_dptx_connect(port=0) <- Left Back, picture
344.228438 b0304c000.mux: Switched dpphy to dispext1,0
344.228723 28cc00000.dcp: DP route 1 -> 0, dptx target phy 1
349.084967 28cc00000.dcp: dcp_dptx_connect(port=0) <- Left Front, picture
The test also shows why a shared connector doesn't work: with both ports' displayport pointing at dcpext1, disconnect events from either port land on the same connector.
-
HDMI (by reading the code, not tested). The HDMI converter is on atcphy3, and its crossbar should take dispext1 just as well. dcpext0 is tied to HDMI only because the HDMI port's GPIOs, PHY and crossbar sit in the dcpext0 node.
Proposal
- One DRM connector per physical port (the three USB-C ports plus HDMI), with the
usb-c-connector node as connector->fwnode. typec/altmodes/displayport.c already falls back to the port fwnode, so OOB HPD stays one-to-one and userspace gets stable per-port outputs.
- dcpext as a pool, bound at plug time. On HPD: pick a free dcpext, select that port's crossbar, point the DCP at the port's PHY and ATC index, power the port's domain, then connect. The firmware reads EDID after connect, so binding can't wait until modeset. On unplug, release everything. If no dcpext is free, the connector says so in the log instead of going black.
- HDMI is just another member of the pool, not a permanent owner of dcpext0. On t6000 that means any two of HDMI plus the three USB-C ports, which matches Apple's two-external-display spec.
- DT bindings that describe per-port links (connector → PHY/crossbar) instead of per-DCP phandles, replacing
ENABLE_DCPEXT_TYPEC.
- Longer term, drop the tipd OOB hack in favour of the mainline DP altmode path (as on
sven/atcphy).
Open questions
- Is per-port connector + dcpext pool the direction you'd want, or do you have another design in mind?
- How should the DT binding look? Where should the port → crossbar/PHY/ATC-index information live, and what's left in the DCP nodes?
- Boot handoff: dcpext0 is "enabled by the loader". Does m1n1/iBoot leave HDMI running on dcpext0, so the pool has to adopt that state at probe?
- Power domains:
ps_atc1_common and ps_atc3_common are forced always-on for DP-alt/HDMI resume. Is controlling them per bind realistic, or is there a known reason they must stay on?
- What are
dispextN,1 and the dpin0/dpin1 crossbar controls? Are they the second dptxport unit, or USB4 tunnelling?
- For t6001/t6002: is the
die field in dptxport_connect absolute or relative to the DCP's die?
If this direction sounds right, I'm happy to implement it in pieces (per-port connectors, dcpext allocator + crossbar handling, power domains, bindings) and test it on j314s.
Experiment code (throwaway, not for merging): makhov@68538f3aab9d on branch exp/dp-runtime-route.
DT change used for experiment 2
--- a/arch/arm64/boot/dts/apple/t600x-j314-j316.dtsi
+++ b/arch/arm64/boot/dts/apple/t600x-j314-j316.dtsi
@@ -156,6 +156,8 @@
typec0: connector {
compatible = "usb-c-connector";
label = "USB-C Left Back";
+ /* EXPERIMENT: DP runtime route 1 */
+ displayport = <&dcpext1>;
power-role = "dual";
data-role = "dual";
@@ -281,10 +283,11 @@
/* hacks */
apple,dptx-phy = <1>;
- phys = <&atcphy1 PHY_TYPE_DP>;
- phy-names = "dp-phy";
- mux-controls = <&atcphy1_xbar 0>;
- mux-control-names = "dp-xbar";
+ apple,dptx-phy-alt = <0>;
+ phys = <&atcphy1 PHY_TYPE_DP>, <&atcphy0 PHY_TYPE_DP>;
+ phy-names = "dp-phy", "dp-phy-alt";
+ mux-controls = <&atcphy1_xbar 0>, <&atcphy0_xbar 0>;
+ mux-control-names = "dp-xbar", "dp-xbar-alt";
mux-index = <2>;
};
@@ -296,6 +299,14 @@
status = "okay";
};
+&atcphy0_xbar {
+ status = "okay";
+};
+
+&ps_atc0_common {
+ apple,always-on;
+};
+
&ps_atc1_common {
apple,always-on; /* Needs to stay on for DP-alt suspend/resume */
};
While debugging #634 I looked at why DP only works on USB-C Left Front on the M1 Pro. As far as I can tell it's only the static wiring in the DT (
ENABLE_DCPEXT_TYPEC,/* hacks */), not a hardware or firmware limit. I tested that on a MacBookPro18,3 (t6000, j314s) with fairydustce9f2eba72c0, and I'd like to check a direction before writing real code.How it's wired today
dcpext0is fixed to the HDMI port (atcphy3,mux-index = <0>), anddcpext1to Left Front (atcphy1,mux-index = <2>,apple,dptx-phy = <1>,displayport = <&dcpext1>ontypec1only).dcp_platform_probe()selects the crossbar once and never deselects it.dcp->phyanddcp->dptx_phycome from the DT at probe time.The pieces for doing this dynamically are mostly there already:
n_ufp = 9, state = dispext × 2 + sub-output). Deselecting it goes back toMUX_IDLE_DISCONNECT.dptxport_connect(service, 0, atc, die)call, not once at boot.Experiments
dcpext1 statically on Left Back. A DT-only change:
phys = <&atcphy0 PHY_TYPE_DP>,mux-controls = <&atcphy0_xbar 0>,mux-index = <2>,apple,dptx-phy = <0>, anddisplayportmoved totypec0. The crossbar logged70304c000.mux: Switched dpphy to dispext1,0. A plug on Left Back gave link, EDID (19 modes) and a 1920x1080@60 modeset, with no new firmware errors, and unplug tore down cleanly.Moving dcpext1 between two ports at runtime. This was throwaway code: dcpext1 got both PHYs and crossbars in the DT, plus a debugfs knob. While nothing is connected, the knob deselects the old crossbar,
mux_control_try_select()s the new one, and swapsdcp->phyanddcp->dptx_phy. It worked in one boot, both ways, with no reboot or firmware restart:The test also shows why a shared connector doesn't work: with both ports'
displayportpointing at dcpext1, disconnect events from either port land on the same connector.HDMI (by reading the code, not tested). The HDMI converter is on
atcphy3, and its crossbar should takedispext1just as well. dcpext0 is tied to HDMI only because the HDMI port's GPIOs, PHY and crossbar sit in the dcpext0 node.Proposal
usb-c-connectornode asconnector->fwnode.typec/altmodes/displayport.calready falls back to the port fwnode, so OOB HPD stays one-to-one and userspace gets stable per-port outputs.ENABLE_DCPEXT_TYPEC.sven/atcphy).Open questions
ps_atc1_commonandps_atc3_commonare forced always-on for DP-alt/HDMI resume. Is controlling them per bind realistic, or is there a known reason they must stay on?dispextN,1and thedpin0/dpin1crossbar controls? Are they the second dptxport unit, or USB4 tunnelling?diefield indptxport_connectabsolute or relative to the DCP's die?If this direction sounds right, I'm happy to implement it in pieces (per-port connectors, dcpext allocator + crossbar handling, power domains, bindings) and test it on j314s.
Experiment code (throwaway, not for merging): makhov@68538f3aab9d on branch
exp/dp-runtime-route.DT change used for experiment 2