diff --git a/Documentation/devicetree/bindings/display/apple,dcp.yaml b/Documentation/devicetree/bindings/display/apple,dcp.yaml index d79ad457f7703..38be82bc7d1bd 100644 --- a/Documentation/devicetree/bindings/display/apple,dcp.yaml +++ b/Documentation/devicetree/bindings/display/apple,dcp.yaml @@ -33,6 +33,7 @@ properties: - apple,t8112-dcpext - apple,t6000-dcpext - apple,t6020-dcpext + - apple,t6041-dcpext - const: apple,dcpext - items: - const: apple,t8140-dcpext @@ -213,6 +214,23 @@ properties: $ref: /schemas/types.yaml#/definitions/uint32 description: Display coprocessor index on multi-DCP systems. + apple,typec-hybrid-first: + type: boolean + description: + A hybrid pipeline (a fixed output plus Type-C routes) is planned for a + Type-C port before the other pipelines, while its fixed output has no + sink, instead of being kept back for that output. Set where this + pipeline is the only one that can be the primary of a display wider than + one pipe. + + apple,dcp-endpoint: + $ref: /schemas/types.yaml#/definitions/uint32 + description: + The display endpoint this coprocessor is to its firmware (its display + index), by which two external pipes name each other when a display + wider than one pipe is driven by two of them. 0 is the internal + display. + apple,dptx-phy: $ref: /schemas/types.yaml#/definitions/uint32 description: DisplayPort transmitter PHY selector. diff --git a/Documentation/devicetree/bindings/power/apple,t8140-pmp-report.yaml b/Documentation/devicetree/bindings/power/apple,t8140-pmp-report.yaml index e8eac9e5411e3..e79ea331865b5 100644 --- a/Documentation/devicetree/bindings/power/apple,t8140-pmp-report.yaml +++ b/Documentation/devicetree/bindings/power/apple,t8140-pmp-report.yaml @@ -93,6 +93,13 @@ patternProperties: holds this entry's device ID, instead of by reg. Only valid on a report whose PMP the host starts. + apple,irq-safe: + type: boolean + description: + The entry is the parent of IRQ-safe PMGR power states and is + switched with interrupts disabled. Setting the state takes a + spinlock and polls the acknowledgment atomically. + required: - compatible - reg diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-hdmi.dtsi b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-hdmi.dtsi new file mode 100644 index 0000000000000..b414a0bddee7d --- /dev/null +++ b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-hdmi.dtsi @@ -0,0 +1,308 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * The fixed HDMI path of t6040-j616s-extdisp.dtsi (dcpext0 -> atc3 crossbar + * -> atcphy3 -> DP-to-HDMI converter) with every node left DISABLED. The + * nodes must not be enabled at boot in the device tree: powering the DISPEXT + * chain without the PMP acknowledgement hangs the machine before root. The + * enable gate (drivers/soc/apple/t6041-display-gate.c) switches them on in + * order once the PMP is bound. + * + * dispext0's DART is not in the display node's iommus: the gate appends it + * when it enables the chain, so the internal display's DART domain stays as + * it is without the external displays. + */ + +/* + * DISPEXT0 may only be powered once the PMP has acknowledged it in its + * device-state table (SOC-DEV-PS-REQ/ACK, PTD entries 480/488 on T6041): the + * soc-device record of DISPEXT0 has ID 18 (ADT iop-pmp-nub soc-device, ack + * flag set), so its request bit is 17. Without it, enabling the DISPEXT0 PMGR + * chain hung the machine before root. + * Same contract as the T6030 PMP device-state provider of + * aurora-silicon/linux#358, through the existing pmp-report entries here. + */ +&pmp_report { + pmp_dispext0: report@11 { + compatible = "apple,t6041-pmp-v2-report-entry", + "apple,t6000-pmp-v2-report-entry"; + reg = <0x11>; + label = "pmp-dispext0"; + #power-domain-cells = <0>; + /* parent of the IRQ-safe PMGR chain */ + apple,irq-safe; + status = "disabled"; + }; +}; + +&ps_dispext0_sys { + power-domains = <&pmp_dispext0>; + status = "disabled"; +}; + +&ps_dispext0_fe { + status = "disabled"; +}; + +&ps_dispext0_cpu { + /* + * NEVER enable: the ADT marks DISPEXT0_CPU no_ps (no PMGR power-state + * register, like the internal DISP_CPU). Its probe reads offset 0x350, + * and that read resets the SoC. DCPEXT0's own power gate (ADT power-gates 431) is the + * virtual DISPEXT0-CPU-V; the last real register of the chain is FE. + */ + /delete-property/ apple,always-on; + status = "disabled"; +}; + +&ps_atc3_common { + status = "disabled"; +}; + +&ps_atc3_common_dp { + status = "disabled"; +}; + +&ps_atc3_usb_aon { + status = "disabled"; +}; + +&ps_atc3_phymxwrap { + status = "disabled"; +}; + +&pmgr_mini { + /* ADT ATC3_USB: +0x148, parents ATC3_COMMON and ATC3_USB_AON; the crossbar's domain as on T6030 */ + ps_atc3_usb: power-controller@148 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x148 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_usb"; + power-domains = <&ps_atc3_common>, <&ps_atc3_usb_aon>; + status = "disabled"; + }; +}; + +&pinctrl_aop { + status = "disabled"; +}; + +&dcpext0_dart { + /* in the DISPEXT0 block: powered (after the PMP request) before probing */ + power-domains = <&ps_dispext0_fe>; + status = "disabled"; +}; + +&dispext0_dart { + /* in the DISPEXT0 block: powered (after the PMP request) before probing */ + power-domains = <&ps_dispext0_fe>; + status = "disabled"; +}; + +&dcpext0_mbox { + power-domains = <&ps_dispext0_fe>; + status = "disabled"; +}; + +&dcpext0 { + power-domains = <&ps_dispext0_fe>; + /* + * The IOMFB surface layout of the internal DCP; + * without apple,iomfb-surfaces apple-dcp's probe fails with -EINVAL. + */ + apple,iomfb-surfaces = <1 1 0 0>; + status = "disabled"; +}; + +&atcphy3_mbox { + status = "disabled"; +}; + +&atcphy3_xbar { + power-domains = <&ps_atc3_usb>; + status = "disabled"; +}; + +/* + * atcphy3 apply_tunables tables (aciophy_phy_init mode 3), from this machine's ADT /arm-io/atc-phy3 + * (privateartifacts/omarchy-m4-unit/adt.bin): AXI2AF converted entry by entry; every other table is + * byte-identical to atc-phy0 in the ADT, so the atcphy0 values of t6040-j616s-usb3.dtsi are reused + * (AUS40CMN_SHM differs: 0x50). Only the first six (axi2af..lane1) are applied in DP mode. + */ +&atcphy3 { + /* ATC3 ADT tunable_ATC0AXI2AF (port-specific), entries */ + apple,tunable-axi2af = + <0x0 0x800000ff 0x80000025>, + <0x4 0x80ff00ff 0x80250003>, + <0x8 0x80ff00ff 0x80250003>, + <0xc 0x80ff00ff 0x80250005>, + <0x10 0x80ff00ff 0x80250003>, + <0x14 0x80ff00ff 0x80250003>, + <0x18 0x80ff00ff 0x80250003>, + <0x1c 0x80ff00ff 0x80250003>, + <0x20 0x800000ff 0x8000001e>, + <0x24 0x800000ff 0x8000004a>, + <0x28 0x80ff00ff 0x804a0006>, + <0x2c 0x80ff00ff 0x804a0006>, + <0x30 0x80ff00ff 0x804a0006>, + <0x34 0x80ff00ff 0x804a0006>, + <0x38 0x80ff00ff 0x804a0006>, + <0x3c 0x80ff00ff 0x804a0006>, + <0x40 0x800000ff 0x8000001e>, + <0x44 0x800000ff 0x8000001e>, + <0x48 0x800000ff 0x8000001e>, + <0x100 0x000000ff 0x000000ff>, + <0x108 0xffff0006 0x00100000>, + <0x304 0x3f3f0000 0x04040000>, + <0x308 0x000fffff 0x00001681>, + <0x30c 0x000fffff 0x00001680>, + <0x310 0x70000fff 0x000001e0>, + <0x314 0xf0000fff 0x800001e0>, + <0x318 0x70000fff 0x000001e0>, + <0x31c 0xf0000fff 0x800001e0>, + <0x320 0x70000fff 0x000005a0>, + <0x324 0xf0000fff 0x800005a0>, + <0x328 0x70000fff 0x000005a0>, + <0x32c 0xf0000fff 0x800005a0>, + <0x330 0x70000fff 0x000005a0>, + <0x334 0xf0000fff 0x800005a0>, + <0x338 0x70000fff 0x000005a0>, + <0x33c 0x70000fff 0x000005a0>, + <0x340 0x70000fff 0x000005a0>, + <0x344 0x70000fff 0x000005a0>, + <0x348 0x70000fff 0x000005a0>, + <0x34c 0x70000fff 0x000005a0>, + <0x350 0x70000fff 0x000005a0>, + <0x358 0xffffffff 0x0000ffff>, + <0x35c 0xffffffff 0x0000ffff>, + <0x408 0xffff0020 0x00210020>, + <0x40c 0x000fffff 0x00001681>, + <0x410 0x000fffff 0x00001680>, + <0x414 0x70000fff 0x000001e0>, + <0x418 0x70000fff 0x000001e0>, + <0x41c 0x70000fff 0x000001e0>, + <0x420 0x70000fff 0x000001e0>, + <0x424 0x70000fff 0x000005a0>, + <0x428 0x70000fff 0x000005a0>, + <0x42c 0x70000fff 0x000005a0>, + <0x430 0x70000fff 0x000005a0>, + <0x434 0x70000fff 0x000005a0>, + <0x438 0x70000fff 0x000005a0>, + <0x43c 0x70000fff 0x000005a0>, + <0x440 0x70000fff 0x000005a0>, + <0x444 0x70000fff 0x000005a0>, + <0x448 0x70000fff 0x000005a0>, + <0x44c 0x70000fff 0x000005a0>, + <0x450 0x70000fff 0x000005a0>, + <0x454 0x70000fff 0x000005a0>, + <0x948 0x07ff07ff 0x00280000>, + <0xa48 0x07ff07ff 0x00400040>, + <0xb48 0x07ff07ff 0x00b8010e>, + <0xc48 0x07ff07ff 0x00400020>, + <0xd48 0x07ff07ff 0x008a008a>, + <0x1000 0xffffffff 0x7fffffff>, + <0x4008 0x00000001 0x00000001>, + <0x400c 0x00000001 0x00000001>, + <0x4014 0x00000001 0x00000001>, + <0x4018 0x00000001 0x00000000>, + <0x401c 0x00000001 0x00000001>; + /* the remaining tables equal atc-phy0 in the ADT, except AUS40CMN_SHM (0x50) */ + apple,tunable-fabric = + <0x4 0x003f003f 0x00200020>, + <0xc 0x000000f1 0x000000a1>, + <0x10 0x0000801f 0x00000004>, + <0x18 0x003f003f 0x00200020>, + <0x20 0x000000f1 0x000000a1>, + <0x24 0x0000801f 0x00000004>, + <0x2c 0x00ff00ff 0x00960096>, + <0x34 0x000000f1 0x00000071>, + <0x38 0x0000801f 0x00000004>, + <0x40 0x0000003f 0x00000020>, + <0x48 0x0000801f 0x00000004>, + <0x50 0x007f007f 0x00450045>, + <0x58 0x0000801f 0x00000004>, + <0x60 0x0000003f 0x00000020>, + <0x68 0x0000801f 0x00000004>, + <0x70 0x00000001 0x00000001>, + <0x74 0x0000801f 0x00000004>, + <0x7c 0x007f007f 0x00400040>, + <0x80 0x000000f1 0x00000011>, + <0x84 0x00000001 0x00000001>, + <0x88 0x00000001 0x00000001>, + <0x8c 0x0000801f 0x00000004>, + <0x94 0x001f001f 0x00100010>, + <0x98 0x000000f1 0x00000011>, + <0x9c 0x0000801f 0x00000004>, + <0xa4 0x000000ff 0x000000e0>, + <0xa8 0x0000801f 0x00000004>, + <0xb0 0x0000003f 0x00000020>, + <0xb8 0x0000801f 0x00000004>, + <0xc0 0x0000003f 0x00000020>, + <0xc8 0x0000801f 0x00000004>, + <0xd0 0x001f001f 0x00100010>, + <0xd8 0x00000001 0x00000001>, + <0xdc 0x00000001 0x00000001>, + <0xe0 0x0000801f 0x00000004>, + <0xe8 0x0000000f 0x00000008>, + <0xec 0x0000801f 0x00000004>, + <0xf4 0xffffffff 0x01010101>, + <0xf8 0x0000801f 0x00000004>, + <0x100 0x00000001 0x00000001>, + <0x104 0x00000001 0x00000001>, + <0x108 0x0000801f 0x00000004>, + <0x110 0x0000801f 0x00000004>, + <0x118 0x0000801f 0x00000004>, + <0x120 0x00000001 0x00000001>, + <0x124 0x00000001 0x00000001>, + <0x128 0x0000801f 0x00000004>, + <0x130 0xffffffff 0x01010101>, + <0x134 0x0000801f 0x00000004>, + <0x13c 0x00000001 0x00000001>, + <0x140 0x00000001 0x00000001>, + <0x144 0x0000801f 0x00000004>, + <0x14c 0x00000001 0x00000001>, + <0x150 0x00000001 0x00000001>, + <0x154 0x0000801f 0x00000004>, + <0x15c 0x0000801f 0x00000004>, + <0x164 0xffffffff 0x01010101>, + <0x168 0x0000ffff 0x00000101>, + <0x16c 0x0000801f 0x00000004>, + <0x174 0x00000001 0x00000001>, + <0x178 0x00000001 0x00000001>, + <0x17c 0x0000801f 0x00000004>, + <0x184 0x00000001 0x00000001>, + <0x188 0x00000001 0x00000001>, + <0x18c 0x0000801f 0x00000004>; + apple,tunable-common-cfg = + <0x8 0x00000001 0x00000001>; + apple,tunable-axi2af-lioa = + <0x28 0x0000fe2f 0x00000000>, + <0x2c 0x00000007 0x00000000>, + <0x30 0x0000007f 0x00000000>, + <0x34 0x000000ff 0x00000000>, + <0x58 0x0000010b 0x00000000>; + apple,tunable-lane0 = + <0xd094 0x00080000 0x00080000>, + <0xd098 0x00080000 0x00080000>, + <0xd0a4 0x00800000 0x00800000>, + <0xd0a8 0x00200000 0x00200000>; + apple,tunable-lane1 = + <0x1d094 0x00080000 0x00080000>, + <0x1d098 0x00080000 0x00080000>, + <0x1d0a4 0x00800000 0x00800000>, + <0x1d0a8 0x00200000 0x00200000>; + apple,tunable-aus40cmn-shm = + <0xa04 0x0000007c 0x00000050>; + apple,tunable-auspll = + <0x3224 0x00000c00 0x00000800>, + <0x3230 0x0000001f 0x0000000f>, + <0x3240 0x07d8007f 0x04000059>, + <0x32b0 0x0b800000 0x01800000>, + <0x32e8 0x00060001 0x00040001>, + <0x3338 0x0f800000 0x07800000>; + apple,tunable-cio4pll = + <0x3a30 0x0000001f 0x0000000f>, + <0x3a4c 0x0000fe7f 0x0000501e>, + <0x3ab0 0x00700000 0x00300000>, + <0x3ae8 0x00060000 0x00020000>; +}; diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-mem-loader.dtsi b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-mem-loader.dtsi new file mode 100644 index 0000000000000..08d4768494634 --- /dev/null +++ b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-mem-loader.dtsi @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * The firmware memory of the external DCPs, for a boot loader that fills it + * (m4-loader m4_dcpext_fixup()). + * + * The loader reads /arm-io/dcpextN/iop-dcpextN-nub segment-ranges of the + * booting Mac (__TEXT, __DATA and the heap; __OS_LOG is left out as for the + * internal DCP), gives each placeholder below its reg, iommu-addresses and + * name, enables it, names the regions in the node's memory-region-names and + * sets apple,firmware-version/compat. A placeholder it does not fill is + * deleted together with the node's memory-region. A boot loader without this + * fix-up leaves them disabled, and the enable gate then keeps the external + * displays off. + * + * Nothing here enables dcpext0/1 or their DART/mailbox. + */ + +/ { + reserved-memory { + dcpext0_text: dcpext0-text { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + + dcpext0_data: dcpext0-data { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + + dcpext0_heap: dcpext0-heap { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + + dcpext1_text: dcpext1-text { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + + dcpext1_data: dcpext1-data { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + + dcpext1_heap: dcpext1-heap { + compatible = "apple,asc-mem"; + reg = <0x0 0x0 0x0 0x4000>; + no-map; + status = "disabled"; + }; + }; +}; + +&dcpext0 { + memory-region = <&dcpext0_text>, <&dcpext0_data>, <&dcpext0_heap>; + memory-region-names = "text", "data", "heap"; +}; + +&dcpext1 { + memory-region = <&dcpext1_text>, <&dcpext1_data>, <&dcpext1_heap>; + memory-region-names = "text", "data", "heap"; +}; diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-usbc.dtsi b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-usbc.dtsi new file mode 100644 index 0000000000000..3d375fd93a98e --- /dev/null +++ b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp-usbc.dtsi @@ -0,0 +1,268 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * USB-C DisplayPort: dcpext1 over any of the three Type-C ports. As for + * HDMI, the DISPEXT1 chain and dcpext1 stay DISABLED in the DTB and are + * enabled by the enable gate (drivers/soc/apple/t6041-display-gate.c). The + * routes go through the display fabric (typec-routes, end of this file): + * tipd's mode switch reaches dcpext1, which claims the port. Boot-time + * changes: mode-switch on atcphy0..2 (the PHY registers a Type-C mode + * switch; tipd drives it) and the port@3 route endpoints of the connectors. + */ + +/* PMP device-state acknowledgement for DISPEXT1 (soc-device 19, request bit 18), as DISPEXT0. */ +&pmp_report { + pmp_dispext1: report@12 { + compatible = "apple,t6041-pmp-v2-report-entry", + "apple,t6000-pmp-v2-report-entry"; + reg = <0x12>; + label = "pmp-dispext1"; + #power-domain-cells = <0>; + apple,irq-safe; + status = "disabled"; + }; +}; + +/* + * DISPEXT1_SYS sits on FAB7_SOC (ADT pmgr device id2 134, pmgr_aux 0x488, parent + * FAB6_SOC, off at boot), which t6040-pmgr.dtsi does not describe. m4.62 enabled + * DISPEXT1_SYS without it and the first DCPEXT1 register access (its DART) reset + * the SoC. DISPEXT0_SYS's parent FAB2_SOC is on anyway through ATC0..2_COMMON. + */ +&pmgr_aux { + ps_fab7_soc: power-controller@488 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x488 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "fab7_soc"; + power-domains = <&ps_fab6_soc>; + status = "disabled"; + }; +}; + +&ps_dispext1_sys { + power-domains = <&ps_fab7_soc>, <&pmp_dispext1>; + status = "disabled"; +}; + +&ps_dispext1_fe { + status = "disabled"; +}; + +&ps_dispext1_cpu { + /* NEVER enable: ADT no_ps, as DISPEXT0_CPU (its register read resets the SoC). */ + /delete-property/ apple,always-on; + status = "disabled"; +}; + +/* The per-ATC crossbars of the Type-C ports: the ATC USB domain, as atcphy3's. */ +&atcphy0_xbar { + power-domains = <&ps_atc0_usb>; + status = "disabled"; +}; + +&atcphy1_xbar { + power-domains = <&ps_atc1_usb>; + status = "disabled"; +}; + +&atcphy2_xbar { + power-domains = <&ps_atc2_usb>; + status = "disabled"; +}; + +/* tipd switches the PHY into DP alt mode through the connector graph. */ +&atcphy0 { + mode-switch; +}; + +&atcphy1 { + mode-switch; +}; + +&atcphy2 { + mode-switch; +}; + +/* + * Type-C display routes through the display fabric, as T6030 and J700: + * dcpext1 offers one route per port, each a mode switch the port controller + * drives over the connector graph (port@3). The gate enables the routes + * before devices exist, so the port controllers wait for dcpext1's mode + * switches, and probes the controllers again once dcpext1 is bound. A route + * without a registered mode switch would make the port wait for one forever. + */ +&typec0 { + ports { + port@3 { + #address-cells = <1>; + #size-cells = <0>; + reg = <3>; + + typec0_dcpext1: endpoint@0 { + reg = <0>; + remote-endpoint = <&dcpext1_typec0>; + }; + + typec0_dcpext0: endpoint@1 { + reg = <1>; + remote-endpoint = <&dcpext0_typec0>; + }; + }; + }; +}; + +&typec1 { + ports { + port@3 { + #address-cells = <1>; + #size-cells = <0>; + reg = <3>; + + typec1_dcpext1: endpoint@0 { + reg = <0>; + remote-endpoint = <&dcpext1_typec1>; + }; + + typec1_dcpext0: endpoint@1 { + reg = <1>; + remote-endpoint = <&dcpext0_typec1>; + }; + }; + }; +}; + +&typec2 { + ports { + port@3 { + #address-cells = <1>; + #size-cells = <0>; + reg = <3>; + + typec2_dcpext1: endpoint@0 { + reg = <0>; + remote-endpoint = <&dcpext1_typec2>; + }; + + typec2_dcpext0: endpoint@1 { + reg = <1>; + remote-endpoint = <&dcpext0_typec2>; + }; + }; + }; +}; + +&dcpext1 { + apple,connector-type = "USB-C"; + /* as dcpext0: without it apple-dcp's probe fails with -EINVAL (m4.63) */ + apple,iomfb-surfaces = <1 1 0 0>; + phys = <&atcphy0 PHY_TYPE_DP>, <&atcphy1 PHY_TYPE_DP>, <&atcphy2 PHY_TYPE_DP>; + phy-names = "typec0", "typec1", "typec2"; + mux-controls = <&atcphy0_xbar 0>, <&atcphy1_xbar 0>, <&atcphy2_xbar 0>; + mux-control-names = "typec0", "typec1", "typec2"; + /* ADT: atcN-dpphy dp-switch-dfp-endpoint N; dcpext1 ufp 1 port 0 = crossbar state 2 */ + apple,typec-dptx-phys = <0 1 2>; + apple,typec-mux-indices = <2 2 2>; + + typec-routes { + #address-cells = <1>; + #size-cells = <0>; + + route@0 { + reg = <0>; + mode-switch; + status = "disabled"; + + port { + dcpext1_typec0: endpoint { + remote-endpoint = <&typec0_dcpext1>; + }; + }; + }; + + route@1 { + reg = <1>; + mode-switch; + status = "disabled"; + + port { + dcpext1_typec1: endpoint { + remote-endpoint = <&typec1_dcpext1>; + }; + }; + }; + + route@2 { + reg = <2>; + mode-switch; + status = "disabled"; + + port { + dcpext1_typec2: endpoint { + remote-endpoint = <&typec2_dcpext1>; + }; + }; + }; + }; +}; + +/* + * dcpext0 also offers a route to every Type-C port, next to its fixed HDMI + * path (a hybrid pipeline, as the T6030 J514s). It is the only pipe that + * can be the primary of a two-pipe display (firmware endpoint 1; the pipes + * merge upwards only, m4.138), so a 7680 px sink on USB-C needs it: the + * fabric takes it for a Type-C port with appledrm.typec_hybrid_first. + * ADT: atcN-dpphy dp-switch-dfp-endpoint 0 = dispext0 = crossbar state 0, + * as its HDMI route on atcphy3 (mux-index 0). + */ +&dcpext0 { + phys = <&atcphy3 PHY_TYPE_DP>, + <&atcphy0 PHY_TYPE_DP>, <&atcphy1 PHY_TYPE_DP>, <&atcphy2 PHY_TYPE_DP>; + phy-names = "dp-phy", "typec0", "typec1", "typec2"; + mux-controls = <&atcphy3_xbar 0>, + <&atcphy0_xbar 0>, <&atcphy1_xbar 0>, <&atcphy2_xbar 0>; + mux-control-names = "dp-xbar", "typec0", "typec1", "typec2"; + apple,typec-dptx-phys = <0 1 2>; + apple,typec-mux-indices = <0 0 0>; + + typec-routes { + #address-cells = <1>; + #size-cells = <0>; + + route@0 { + reg = <0>; + mode-switch; + status = "disabled"; + + port { + dcpext0_typec0: endpoint { + remote-endpoint = <&typec0_dcpext0>; + }; + }; + }; + + route@1 { + reg = <1>; + mode-switch; + status = "disabled"; + + port { + dcpext0_typec1: endpoint { + remote-endpoint = <&typec1_dcpext0>; + }; + }; + }; + + route@2 { + reg = <2>; + mode-switch; + status = "disabled"; + + port { + dcpext0_typec2: endpoint { + remote-endpoint = <&typec2_dcpext0>; + }; + }; + }; + }; +}; diff --git a/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi new file mode 100644 index 0000000000000..b6486f293da9d --- /dev/null +++ b/arch/arm64/boot/dts/apple/t6040-j616s-extdisp.dtsi @@ -0,0 +1,350 @@ +// SPDX-License-Identifier: GPL-2.0+ OR MIT +/* + * External display resources of the J616s. Everything here is disabled in + * the device tree: the enable gate (drivers/soc/apple/t6041-display-gate.c) + * switches the chain on once the boot loader has provided the firmware + * memory of the external display processors, so without that the booted + * hardware state is the same as without this file. + * + * Values come from this unit's ADT (25G83); CPU address = ADT address + + * 0x2_0000_0000. Path of the picture: + * HDMI: dcpext0 -> atc3 display crossbar -> atcphy3 (fixed DP PHY, no + * USB controller) -> on-board DP-to-HDMI converter + * USB-C: dcpext1 -> atcN display crossbar -> atcphyN (DP alt mode) + * + * Needs the DP mode of the T6040 Type-C PHY driver (phy-apple-t6040-usb2) + * with its probe path for a PHY without a USB controller (atcphy3), the + * atc-dpxbar,t604x display crossbar driver and the dcpext firmware memory + * (dcp-map style memory-region with iommu-addresses, t6040-j616s-extdisp- + * mem-loader.dtsi). + */ + +/ { + pinctrl_aop: pinctrl@510824000 { + compatible = "apple,t6041-pinctrl", "apple,t8103-pinctrl"; + reg = <0x5 0x10824000 0x0 0x4000>; + + gpio-controller; + #gpio-cells = <2>; + gpio-ranges = <&pinctrl_aop 0 0 85>; + apple,npins = <85>; + + interrupt-controller; + #interrupt-cells = <2>; + interrupt-parent = <&aic>; + interrupts = , + , + , + , + , + , + ; + status = "disabled"; + }; + + dispext0_dart: iommu@3a2300000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0xa2300000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext0_dart: iommu@3a2340000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x3 0xa2340000 0x0 0x20000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext0: dcp@3a2e00000 { + compatible = "apple,t6041-dcpext", "apple,dcpext"; + reg = <0x3 0xa2e00000 0x0 0x4000>, + <0x3 0xa0000000 0x0 0x4000000>, + <0x3 0xa2240000 0x0 0x10000>, + <0x3 0xa23a8000 0x0 0x4000>, + <0x3 0xa2800000 0x0 0x800000>; + reg-names = "coproc", "disp-0", "disp-1", "disp-2", "disp-3"; + mboxes = <&dcpext0_mbox>; + mbox-names = "mbox"; + iommus = <&dcpext0_dart 23>; + /* guessed: the internal DCP uses 0x20000, J700 DISPEXT0 is +0x8000 */ + apple,bw-scratch = <&pmgr_dcp 0 4 0x28000>; + /* firmware Disp_Index, the dual-pipe endpoint ID */ + apple,dcp-endpoint = <1>; + /* the only pipe that can be the primary of a two-pipe display */ + apple,typec-hybrid-first; + status = "disabled"; + + dispext0_piodma: piodma { + iommus = <&dispext0_dart 16>; + }; + }; + + dcpext0_mbox: mailbox@3a2e08000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0xa2e08000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + status = "disabled"; + }; + + dispext1_dart: iommu@602300000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x6 0x02300000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext1_dart: iommu@602340000 { + compatible = "apple,t6041-dart", "apple,t8110-dart"; + reg = <0x6 0x02340000 0x0 0xc000>; + #iommu-cells = <1>; + interrupt-parent = <&aic>; + interrupts = ; + status = "disabled"; + }; + + dcpext1: dcp@602e00000 { + compatible = "apple,t6041-dcpext", "apple,dcpext"; + reg = <0x6 0x02e00000 0x0 0x4000>, + <0x6 0x00000000 0x0 0x4000000>, + <0x6 0x02240000 0x0 0x10000>, + <0x6 0x023a8000 0x0 0x4000>, + <0x6 0x02800000 0x0 0x800000>; + reg-names = "coproc", "disp-0", "disp-1", "disp-2", "disp-3"; + mboxes = <&dcpext1_mbox>; + mbox-names = "mbox"; + iommus = <&dcpext1_dart 23>; + /* guessed, as dcpext0 */ + apple,bw-scratch = <&pmgr_dcp 0 4 0x30000>; + apple,dcp-endpoint = <2>; + status = "disabled"; + + dispext1_piodma: piodma { + iommus = <&dispext1_dart 16>; + }; + }; + + dcpext1_mbox: mailbox@602e08000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x6 0x02e08000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + status = "disabled"; + }; + + /* ATC3: the HDMI converter's fixed DP PHY; no USB controller, no pipehandler. */ + atcphy3: phy@39aa90000 { + compatible = "apple,t6040-atcphy"; + reg = <0x3 0x9aa90000 0x0 0x4000>, + <0x3 0x9a800000 0x0 0x4000>, + <0x3 0x9b000000 0x0 0x30000>, + <0x3 0x9b200000 0x0 0x4000>, + <0x3 0x9b140000 0x0 0x4000>, + <0x3 0x9b0c0000 0x0 0x10000>, + <0x3 0x98000000 0x0 0x8000>, + <0x3 0x98020000 0x0 0x4000>, + <0x3 0x9b244000 0x0 0x4000>, + <0x3 0x9b250000 0x0 0x4000>; + reg-names = "bank0", "bank1", "core", "cfg", "cpu", "cpu-data", + "axi2af", "axi2af-lioa", "fabric", "common-cfg"; + #phy-cells = <1>; + power-domains = <&ps_atc3_common_dp>; + mboxes = <&atcphy3_mbox>; + mbox-names = "phy-cpu"; + nonposted-mmio; + status = "disabled"; + }; + + atcphy3_mbox: mailbox@39b148000 { + compatible = "apple,t6041-asc-mailbox", "apple,asc-mailbox-v4"; + reg = <0x3 0x9b148000 0x0 0x4000>; + interrupt-parent = <&aic>; + interrupts = , + , + , + ; + interrupt-names = "send-empty", "send-not-empty", + "recv-empty", "recv-not-empty"; + #mbox-cells = <0>; + power-domains = <&ps_atc3_phymxwrap>; + status = "disabled"; + }; + + /* Display crossbars (ADT atcN-dpxbar, atc-dpxbar,t604x): no driver yet. */ + atcphy0_xbar: mux@38324c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x8324c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; + + atcphy1_xbar: mux@38b24c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x8b24c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; + + atcphy2_xbar: mux@39324c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x9324c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; + + atcphy3_xbar: mux@39b24c000 { + compatible = "apple,t6041-display-crossbar"; + reg = <0x3 0x9b24c000 0x0 0x4000>; + #mux-control-cells = <1>; + status = "disabled"; + }; +}; + +/* + * PMGR domains from the ADT device table (+16 offset, +19 block; block 0 = + * pmgr_aux, block 3 = pmgr_mini; checked against the ATC0 nodes). Disabled + * so the pwrstate driver does not touch them yet: DISPEXT0/1 run the + * firmware iBoot loaded, and an always-on domain that happens to be off + * would be powered on by the driver. + */ +&pmgr_aux { + ps_dispext0_sys: power-controller@340 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x340 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext0_sys"; + status = "disabled"; + }; + + ps_dispext0_fe: power-controller@348 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x348 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext0_fe"; + power-domains = <&ps_dispext0_sys>; + status = "disabled"; + }; + + ps_dispext0_cpu: power-controller@350 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x350 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext0_cpu"; + power-domains = <&ps_dispext0_fe>; + apple,always-on; + status = "disabled"; + }; + + ps_atc3_common: power-controller@408 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x408 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_common"; + apple,always-on; + status = "disabled"; + }; + + ps_dispext1_sys: power-controller@490 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x490 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext1_sys"; + status = "disabled"; + }; + + ps_dispext1_fe: power-controller@498 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x498 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext1_fe"; + power-domains = <&ps_dispext1_sys>; + status = "disabled"; + }; + + ps_dispext1_cpu: power-controller@4a0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x4a0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "dispext1_cpu"; + power-domains = <&ps_dispext1_fe>; + apple,always-on; + status = "disabled"; + }; +}; + +&pmgr_mini { + ps_atc3_common_dp: power-controller@b8 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xb8 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_common_dp"; + power-domains = <&ps_atc3_phymxwrap>; + status = "disabled"; + }; + + ps_atc3_usb_aon: power-controller@e0 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0xe0 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_usb_aon"; + apple,always-on; + status = "disabled"; + }; + + ps_atc3_phymxwrap: power-controller@168 { + compatible = "apple,t6041-pmgr-pwrstate", "apple,t8103-pmgr-pwrstate"; + reg = <0x168 4>; + #power-domain-cells = <0>; + #reset-cells = <0>; + label = "atc3_phymxwrap"; + power-domains = <&ps_atc3_common>, <&ps_atc3_usb_aon>; + status = "disabled"; + }; +}; + +/* + * HDMI port (disabled with dcpext0): converter power SMC gP0f/gP1a (as on + * the M3 Pro J514s), HPD on AOP GPIO 0x24. The converter's reset (gP10), + * force-dpfu (AOP GPIO 0x2a) and its I2C side channel (i2c8 0x08) are left + * undescribed, as on the J514s. + */ +&dcpext0 { + apple,connector-type = "HDMI-A"; + hdmi-hpd-gpios = <&pinctrl_aop 36 GPIO_ACTIVE_HIGH>; + hdmi-pwren-gpios = <&smc_gpio 15 GPIO_ACTIVE_HIGH>; + dp2hdmi-pwren-gpios = <&smc_gpio 26 GPIO_ACTIVE_HIGH>; + phys = <&atcphy3 PHY_TYPE_DP>; + phy-names = "dp-phy"; + mux-controls = <&atcphy3_xbar 0>; + mux-control-names = "dp-xbar"; + mux-index = <0>; + apple,dptx-phy = <3>; +}; diff --git a/arch/arm64/boot/dts/apple/t6040-j616s.dts b/arch/arm64/boot/dts/apple/t6040-j616s.dts index a40a3fa1e1dc0..bccb167340159 100644 --- a/arch/arm64/boot/dts/apple/t6040-j616s.dts +++ b/arch/arm64/boot/dts/apple/t6040-j616s.dts @@ -1086,3 +1086,9 @@ }; #include "t6040-j616s-usb3.dtsi" + +/* External display chain, all disabled: the gate in drivers/soc/apple/t6041-display-gate.c enables it. */ +#include "t6040-j616s-extdisp.dtsi" +#include "t6040-j616s-extdisp-mem-loader.dtsi" +#include "t6040-j616s-extdisp-hdmi.dtsi" +#include "t6040-j616s-extdisp-usbc.dtsi" diff --git a/drivers/gpu/drm/apple/dcp-fabric-core.c b/drivers/gpu/drm/apple/dcp-fabric-core.c index 8c952dc024560..f703ea061fb50 100644 --- a/drivers/gpu/drm/apple/dcp-fabric-core.c +++ b/drivers/gpu/drm/apple/dcp-fabric-core.c @@ -136,8 +136,8 @@ unsigned int dcp_fabric_score(const struct dcp_fabric_pipeline *pipeline, * route would only follow it there. */ return pipeline->crtc_index + - (!policy->dual_stream && !policy->follow && pipeline->has_fixed ? - 100 : 0); + (!policy->dual_stream && !policy->follow && !policy->hybrid_first && + pipeline->has_fixed ? 100 : 0); } EXPORT_SYMBOL_GPL(dcp_fabric_score); diff --git a/drivers/gpu/drm/apple/dcp-fabric-core.h b/drivers/gpu/drm/apple/dcp-fabric-core.h index 1b8c59ac15a54..dc85dbc40c15b 100644 --- a/drivers/gpu/drm/apple/dcp-fabric-core.h +++ b/drivers/gpu/drm/apple/dcp-fabric-core.h @@ -64,6 +64,8 @@ struct dcp_fabric_policy { bool dual_stream; /* routes follow the CRTC a modeset pairs them with */ bool follow; + /* a hybrid pipeline is not kept back for a fixed display */ + bool hybrid_first; }; enum dcp_fabric_wiring { diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index b5e2eafb8c090..b3bb5982524cc 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,53 @@ MODULE_PARM_DESC(tiled_split, static bool typec_follow_crtc = true; module_param(typec_follow_crtc, bool, 0444); + +/* + * The hybrid pipeline (dcpext0: fixed HDMI PHY + Type-C routes) normally + * comes last for a Type-C port, kept for an HDMI display. On the J616s it + * is also the only pipe that can be the primary of a two-pipe display + * (firmware endpoint 1; the pipes merge upwards only): with this set a + * Type-C port takes it first, so a 7680 px sink on USB-C can pair with + * dcpext1 as its peer. -1 follows the device tree; a write applies at + * once, handing the hybrid between an HDMI display and a Type-C display + * wider than one pipe (one monitor on two inputs: the desktop's choice of + * input, the monitor cannot tell which one it shows). + */ +static int typec_hybrid_first = -1; + +static void dcp_fabric_hybrid_policy_changed(void); + +static int dcp_typec_hybrid_first_set(const char *val, const struct kernel_param *kp) +{ + int ret = param_set_int(val, kp); + + if (!ret) + dcp_fabric_hybrid_policy_changed(); + return ret; +} + +static const struct kernel_param_ops dcp_typec_hybrid_first_ops = { + .set = dcp_typec_hybrid_first_set, + .get = param_get_int, +}; +module_param_cb(typec_hybrid_first, &dcp_typec_hybrid_first_ops, &typec_hybrid_first, 0644); +MODULE_PARM_DESC(typec_hybrid_first, + "Hybrid (HDMI) pipeline: -1 from the device tree (default), 0 HDMI display first, 1 Type-C display first (dual-pipe primary)"); + +/* Set by a pipeline whose device tree asks for it (apple,typec-hybrid-first). */ +static bool typec_hybrid_first_dt; + +void dcp_typec_hybrid_first_dt(void) +{ + WRITE_ONCE(typec_hybrid_first_dt, true); +} + +static bool dcp_typec_hybrid_first(void) +{ + int policy = READ_ONCE(typec_hybrid_first); + + return policy < 0 ? READ_ONCE(typec_hybrid_first_dt) : policy > 0; +} MODULE_PARM_DESC(typec_follow_crtc, "Let direct DP-alt routes follow their selected CRTC (default: on)"); @@ -78,6 +126,12 @@ struct apple_dcp_typec_port { struct apple_dcp_typec_route *secondary_target; /* left out of the plan: its planned pipeline is held by a tunnel */ bool plan_dark; + /* + * HDMI display first (typec_hybrid_first): the display on this port + * has given both pipes to an HDMI display wider than one pipe and + * waits, routed nowhere, until that one fits one pipe or leaves. + */ + bool typec_yielded; }; /* @@ -92,6 +146,12 @@ struct apple_dcp_typec_port { static DEFINE_MUTEX(dcp_typec_fabric_lock); static LIST_HEAD(dcp_typec_ports); +/* hybrid-first hand-over (dcp_hybrid_work()), under dcp_typec_fabric_lock: */ +/* when the hybrid last changed hands, 0 if never */ +static unsigned long dcp_hybrid_moved; +/* failed takes of the hybrid since the last success or Type-C DP exit */ +static unsigned int dcp_hybrid_fails; +#define DCP_HYBRID_MAX_FAILS 2 /* Current DTs wire the same stream capacity on every physical port. */ static atomic_t dcp_dual_stream_routes = ATOMIC_INIT(0); @@ -102,9 +162,6 @@ static bool dcp_hdmi_borrowed(void *ctx); bool dcp_is_typec_output(struct apple_dcp *dcp) { - /* Desktop 26.x keeps its own Type-C route, outside this fabric. */ - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) - return false; return dcp->active_typec_route || dcp->fixed_connector_type == DRM_MODE_CONNECTOR_USB; } @@ -146,6 +203,9 @@ static bool dcp_typec_route_fixed_output_busy(struct apple_dcp_typec_route *rout if (dcp->fixed_connector_type == DRM_MODE_CONNECTOR_USB) return false; + /* Its HDMI display gave the pipeline to a Type-C display. */ + if (READ_ONCE(dcp->hdmi_yielded)) + return false; /* * The 14.7 firmware drives the internal panel on the IOMFB path. * The DPTX service is a separate output, so the live panel does not @@ -195,7 +255,8 @@ static bool dcp_typec_route_available(struct apple_dcp_typec_route *route) .fixed_busy = dcp_typec_route_busy(route), }; - struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream() }; + struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream(), + .hybrid_first = dcp_typec_hybrid_first() }; pipeline.presence = dcp_hdmi_presence(route->dcp); return dcp_fabric_available(&pipeline, &policy); @@ -306,6 +367,68 @@ static int dcp_dpxbar_tunnel_select_source(struct mux_control *mux, int state) return ret; } +/* + * Desktop 26.x firmware does not power the DP PHY of a Type-C route itself: + * the route does, once its crossbar points at the pipeline, and gives it + * back before the crossbar is released. + */ +static int dcp_typec_route_phy_on(struct apple_dcp_typec_route *route) +{ + int ret; + + if (route->dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || + route->phy_powered) + return 0; + ret = phy_init(route->phy); + if (!ret) { + ret = phy_power_on(route->phy); + if (ret) + phy_exit(route->phy); + } + if (ret) { + dev_warn(route->dcp->dev, "Type-C DP PHY init/power: %d\n", ret); + return ret; + } + route->phy_powered = true; + msleep(25); + return 0; +} + +/* + * The PHY is powered when the display connects, not when the route is + * claimed: the port controller's mode switches run in connector order, and + * the PHY must have seen DP alt mode and the plug orientation first. + */ +int dcp_typec_phy_on(struct apple_dcp *dcp) +{ + struct apple_dcp_typec_route *route; + + /* + * Ordered against dcp_typec_route_deactivate(), which powers the PHY + * off and releases the crossbar under tb_lock: a connect racing the + * release either powers the PHY first and has it taken down again, or + * finds the route released and powers nothing. A desktop connect does + * not take the fabric lock, so tb_lock is the common one. + */ + guard(mutex)(&dcp->tb_lock); + route = READ_ONCE(dcp->active_typec_route); + if (!route) + return 0; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + route->deactivate_xbar_done) + return -ENODEV; + return dcp_typec_route_phy_on(route); +} + +static void dcp_typec_route_phy_off(struct apple_dcp_typec_route *route) +{ + if (!route->phy_powered) + return; + route->phy_powered = false; + phy_power_off(route->phy); + phy_exit(route->phy); +} + static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, struct mux_control *xbar) { @@ -327,12 +450,26 @@ static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, return ret; if (dcp->fixed_route_selected) { - ret = mux_control_deselect(dcp->xbar); + ret = mux_control_deselect(dcp->fixed_xbar); dcp->fixed_route_selected = false; if (ret) return ret; } + /* + * A pipeline reserved as the peer of a display that has not paired + * (an HDMI display on its monitor's unselected input, or a pair whose + * cable went) takes this display: drop the reservation, or this + * display could neither pair nor ask for the hybrid. A running pair + * keeps its peer (dcp_peer_release_26_6() refuses). + */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + struct apple_dcp *owner = READ_ONCE(dcp->peer_owner); + + if (owner) + dcp_peer_release_26_6(owner); + } + /* * Thunderbolt DP IN: the crossbar connection may only be brought up * once DCP has configured the link and the tunnel pixel clock runs, @@ -347,19 +484,28 @@ static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, ret = dcp_dpxbar_tunnel_select_source(xbar, route->mux_index); if (ret == -EOPNOTSUPP) ret = 0; - } else { - ret = xbar == route->xbar ? - mux_control_select(xbar, route->mux_index) : 0; + } else if (xbar == route->xbar) { + /* + * The control is only ever held by a route of this port, which + * lets it go under the fabric lock held here: waiting for it + * would wait for good, with the port controller's update work + * and its interrupt stuck behind us. + */ + ret = mux_control_try_select(xbar, route->mux_index); + if (ret == -EBUSY) + dev_err(dcp->dev, "Type-C crossbar control still held, route not taken\n"); } if (ret) { - if (dcp->xbar) { + if (dcp->fixed_xbar) { int restore_ret; - restore_ret = mux_control_select(dcp->xbar, + restore_ret = mux_control_select(dcp->fixed_xbar, dcp->fixed_mux_index); - if (!restore_ret) + if (!restore_ret) { dcp->fixed_route_selected = true; - else + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + WRITE_ONCE(dcp->xbar, dcp->fixed_xbar); + } else dev_err(dcp->dev, "failed to restore fixed display route: %d\n", restore_ret); @@ -370,6 +516,12 @@ static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, dcp_modes_begin_attachment(dcp); apple_connector_edid_set_live(dcp->connector, false); dcp->phy = route->phy; + /* + * Desktop 26.x compact DPTX takes the crossbar link down and up around + * a link configuration (dptxport_compact_xbar_link()) through dcp->xbar. + */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + WRITE_ONCE(dcp->xbar, route->xbar); dcp->dptx_phy = route->dptx_phy; dcp->connector_type = DRM_MODE_CONNECTOR_USB; WRITE_ONCE(dcp->ext_backlight, false); @@ -429,6 +581,12 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) bool ended_attachment; int ret = 0; + WRITE_ONCE(dcp->peer_starved, false); + WRITE_ONCE(dcp->typec_narrow, false); + /* A peer reserved for this route's display is not needed any more. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + dcp_peer_release_26_6(dcp); + guard(mutex)(&dcp->external_lock); if (dcp_is_external_h17p(dcp) && (dcp->external_error || (dcp->external_power_attempted && !dcp->external_stopped))) @@ -444,6 +602,7 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) if (route->deactivate_error) return route->deactivate_error; if (!route->deactivate_xbar_done) { + dcp_typec_route_phy_off(route); if (!route->tunnel || route->xbar_up) ret = mux_control_deselect(route->active_xbar ?: route->xbar); else @@ -530,12 +689,19 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) * following DPTX call times out. Park on a Type-C PHY instead, * for the same reason the USB-C-only case does below. */ - if (dcp->hdmi_hpd && gpiod_get_value_cansleep(dcp->hdmi_hpd)) { + /* + * A yielded hybrid stays with the Type-C display through its + * sink's HPD blinks; the HDMI display waits. + */ + if (dcp->hdmi_hpd && !READ_ONCE(dcp->hdmi_yielded) && + gpiod_get_value_cansleep(dcp->hdmi_hpd)) { dcp->phy = dcp->fixed_phy; dcp->dptx_phy = dcp->fixed_dptx_phy; - if (dcp->xbar && !dcp->fixed_route_selected) { - ret = mux_control_select(dcp->xbar, + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + WRITE_ONCE(dcp->xbar, dcp->fixed_xbar); + if (dcp->fixed_xbar && !dcp->fixed_route_selected) { + ret = mux_control_select(dcp->fixed_xbar, dcp->fixed_mux_index); if (ret) return ret; @@ -544,10 +710,20 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) } else { dcp->phy = dcp->typec_routes[0].phy; dcp->dptx_phy = dcp->typec_routes[0].dptx_phy; + /* The released route's control may go to another pipeline. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + WRITE_ONCE(dcp->xbar, NULL); } } else { - /* Keep DPTX endpoint discovery working before a cable is attached. */ - dcp->phy = dcp->typec_routes[0].phy; + /* + * Keep DPTX endpoint discovery working before a cable is + * attached. Desktop 26.x powers the PHY with the route, so an + * idle pipeline must not point at one. + */ + dcp->phy = dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP ? + NULL : dcp->typec_routes[0].phy; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + WRITE_ONCE(dcp->xbar, NULL); dcp->dptx_phy = dcp->typec_routes[0].dptx_phy; dcp->connector_type = DRM_MODE_CONNECTOR_USB; } @@ -623,6 +799,7 @@ static bool dcp_typec_keep_order(void) static void dcp_typec_pipeline_freed(void); static int dcp_fixed_output_select(struct apple_dcp *dcp); +static void dcp_typec_port_attach(struct apple_dcp_typec_port *port); static bool dcp_hdmi_settle_enabled(struct apple_dcp *dcp) { @@ -636,7 +813,8 @@ static enum dcp_fabric_presence_state dcp_hdmi_presence(struct apple_dcp *dcp) enum dcp_fabric_presence_state state; unsigned long flags; - if (!dcp_hdmi_settle_enabled(dcp)) + /* A yielded hybrid is free for Type-C, whatever HDMI shows. */ + if (!dcp_hdmi_settle_enabled(dcp) || READ_ONCE(dcp->hdmi_yielded)) return DCP_FABRIC_ABSENT; spin_lock_irqsave(&dcp->hdmi_presence_lock, flags); state = dcp->hdmi_presence.state; @@ -653,6 +831,8 @@ static u64 dcp_hdmi_edge(struct apple_dcp *dcp) if (!dcp_hdmi_settle_enabled(dcp)) return 0; spin_lock_irqsave(&dcp->hdmi_presence_lock, flags); + if (dcp->hdmi_presence.state != DCP_FABRIC_SETTLING) + dcp->hdmi_presence_prev = dcp->hdmi_presence.state; generation = dcp_fabric_presence_edge(&dcp->hdmi_presence, jiffies, msecs_to_jiffies(DCP_HDMI_HOLD_MS)); spin_unlock_irqrestore(&dcp->hdmi_presence_lock, flags); @@ -689,14 +869,149 @@ static bool dcp_hdmi_sample(struct apple_dcp *dcp, u64 generation, int level) accepted = dcp_fabric_presence_sample(&dcp->hdmi_presence, generation, level > 0, jiffies, msecs_to_jiffies(DCP_HDMI_HOLD_MS)); + /* + * The debounce guards against a glitch after an unplug. A sample that + * finds no sink where there was none before (start-up, and the sample + * after system sleep) has nothing to debounce: the pipeline is free + * now, not ten seconds later, when a Type-C port has already been + * planned or moved onto another pipeline. + */ + if (accepted && level == 0 && dcp->hdmi_presence_prev == DCP_FABRIC_ABSENT) { + dcp->hdmi_presence.state = DCP_FABRIC_ABSENT; + dcp->hdmi_presence.deadline = 0; + } spin_unlock_irqrestore(&dcp->hdmi_presence_lock, flags); dcp_hdmi_schedule(dcp); return accepted; } +/* + * A Type-C display wider than one pipe sits on a Type-C-only pipeline, + * which has no peer above it, because the hybrid was taken when it came + * (by an HDMI display, gone since). Move it onto the free hybrid, as a + * replug does, so that it gets both pipes. + */ +static bool dcp_typec_move_starved_locked(void) +{ + struct apple_dcp_typec_port *port; + bool moved = false; + + lockdep_assert_held(&dcp_typec_fabric_lock); + + list_for_each_entry(port, &dcp_typec_ports, link) { + struct apple_dcp_typec_route *owner = port->owner, *to = NULL; + struct apple_dcp_typec_route *route; + struct apple_dcp *dcp; + + if (!owner || owner->tunnel || !READ_ONCE(owner->dcp->peer_starved)) + continue; + list_for_each_entry(route, &port->routes, port_link) { + if (route != owner && route->dcp->fixed_phy && + dcp_typec_route_available(route)) { + to = route; + break; + } + } + if (!to) + continue; + + dcp = owner->dcp; + dev_info(dcp->dev, "display wider than one pipe moves to the free %s\n", + dev_name(to->dcp->dev)); + if (dcp_dptx_disconnect_oob(to_platform_device(dcp->dev), 0)) + continue; + port->preferred_route = to; + port->hpd = false; + if (dcp_typec_route_deactivate(owner)) + continue; + port->owner = NULL; + if (dcp_typec_route_activate(to, to->xbar)) { + dev_warn(dcp->dev, "hybrid not taken, the display stays on its pipeline\n"); + if (dcp_typec_route_activate(owner, owner->xbar)) + continue; + to = owner; + } + port->owner = to; + dcp_typec_port_attach(port); + moved |= to != owner; + } + return moved; +} + +/* + * The Type-C display on the hybrid fits one pipe again and an HDMI display + * waits: move the Type-C display onto a free Type-C-only pipeline, as a + * replug does, and connect the HDMI display. The reclaim of the CRTC + * following machines does not apply on desktop 26.x. + */ +static bool dcp_typec_move_off_hybrid_locked(struct apple_dcp_typec_port *port, + struct apple_dcp *hyb) +{ + struct apple_dcp_typec_route *owner = port->owner, *route, *to = NULL; + + lockdep_assert_held(&dcp_typec_fabric_lock); + if (!owner || owner->dcp != hyb || owner->tunnel || port->safe_pending) + return false; + list_for_each_entry(route, &port->routes, port_link) { + if (route != owner && !route->dcp->fixed_phy && route->dcp->crtc && + dcp_typec_route_available(route)) { + to = route; + break; + } + } + if (!to) + return false; + + dev_info(hyb->dev, "Type-C display moves to %s, HDMI display connects\n", + dev_name(to->dcp->dev)); + if (dcp_dptx_disconnect_oob(to_platform_device(hyb->dev), 0)) + return false; + port->preferred_route = to; + port->hpd = false; + if (dcp_typec_route_deactivate(owner)) + return false; + port->owner = NULL; + if (dcp_typec_route_activate(to, to->xbar)) { + dev_warn(hyb->dev, "Type-C display not moved, it stays on the hybrid\n"); + if (dcp_typec_route_activate(owner, owner->xbar)) + return false; + to = owner; + } + port->owner = to; + dcp_typec_port_attach(port); + if (to != owner && hyb->active && gpiod_get_value_cansleep(hyb->hdmi_hpd) > 0) + dcp_dptx_connect(hyb, 0); + return to != owner; +} + static void dcp_hdmi_capacity_available(void *ctx) { dcp_typec_pipeline_freed(); + dcp_typec_move_starved_locked(); +} + +/* + * Desktop 26.x: the HDMI display is gone (its converter's HPD fell and + * stayed low); its firmware reports no unplug. Start the presence hold from + * here, as the edge handler of other machines does, also where presence was + * never tracked: when the hold expires, the desktop has let the HDMI CRTC go + * and the hybrid is free for a Type-C display again. + */ +void dcp_fabric_hdmi_unplugged(struct apple_dcp *dcp) +{ + unsigned long flags; + u64 generation; + + guard(mutex)(&dcp_typec_fabric_lock); + generation = dcp_hdmi_edge(dcp); + if (!generation) + return; + spin_lock_irqsave(&dcp->hdmi_presence_lock, flags); + dcp->hdmi_presence_prev = DCP_FABRIC_PRESENT; + spin_unlock_irqrestore(&dcp->hdmi_presence_lock, flags); + dcp_hdmi_sample(dcp, generation, gpiod_get_value_cansleep(dcp->hdmi_hpd)); + /* A Type-C display that gave its pipes to this one takes them back. */ + dcp_fabric_hybrid_kick(dcp, 0); } static void dcp_hdmi_settle_work(struct work_struct *work) @@ -763,14 +1078,19 @@ static void dcp_hdmi_recheck_work(struct work_struct *work) mod_delayed_work(system_freezable_wq, &dcp->hdmi_recheck_wq, 0); } +static void dcp_hybrid_work(struct work_struct *work); + void dcp_fabric_init(struct apple_dcp *dcp) { spin_lock_init(&dcp->hdmi_presence_lock); dcp->hdmi_presence.state = DCP_FABRIC_ABSENT; + dcp->hdmi_presence_prev = DCP_FABRIC_ABSENT; INIT_DELAYED_WORK(&dcp->hdmi_settle_wq, dcp_hdmi_settle_work); INIT_DELAYED_WORK(&dcp->hdmi_recheck_wq, dcp_hdmi_recheck_work); + INIT_DELAYED_WORK(&dcp->hybrid_wq, dcp_hybrid_work); disable_delayed_work(&dcp->hdmi_settle_wq); disable_delayed_work(&dcp->hdmi_recheck_wq); + disable_delayed_work(&dcp->hybrid_wq); } /* Caller enables the HPD IRQ first; a later edge invalidates this sample. */ @@ -831,10 +1151,10 @@ static void dcp_fixed_hdmi_reinit_locked(struct apple_dcp *dcp, const char *why) if (ret) dev_warn(dcp->dev, "HDMI: PHY not set up again: %d\n", ret); } - if (dcp->xbar && dcp->fixed_route_selected && !dcp->active_typec_route) { + if (dcp->fixed_xbar && dcp->fixed_route_selected && !dcp->active_typec_route) { scoped_guard(mutex, &dcp->tb_lock) dcp->direct_xbar_up = NULL; - ret = mux_control_deselect(dcp->xbar); + ret = mux_control_deselect(dcp->fixed_xbar); dcp->fixed_route_selected = false; if (!ret) ret = dcp_fixed_output_select(dcp); @@ -876,7 +1196,7 @@ static void dcp_fabric_snapshot_port(struct apple_dcp_typec_port *port, port->secondary_owner ? &port->secondary_owner->core : NULL; port->core.preferred = port->preferred_route ? &port->preferred_route->core : NULL; - port->core.wanted = port->dp_wanted; + port->core.wanted = port->dp_wanted && !port->typec_yielded; port->core.hpd = port->dp_hpd; port->core.candidate_crtcs[0] = port->connector ? port->connector->candidate_crtcs : 0; @@ -995,6 +1315,7 @@ dcp_typec_free_route(struct apple_dcp_typec_port *port) struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream(), .follow = dcp_typec_port_follows(port), + .hybrid_first = dcp_typec_hybrid_first(), }; dcp_fabric_snapshot_port(port, false); @@ -1017,7 +1338,7 @@ static void dcp_typec_route_waiting(void) list_for_each_entry(port, &dcp_typec_ports, link) { struct dcp_fabric_port state = { .owner[0] = port->owner ? &port->owner->core : NULL, - .wanted = port->dp_wanted, + .wanted = port->dp_wanted && !port->typec_yielded, .hpd = port->dp_hpd, }; @@ -1431,6 +1752,10 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, port->applied_conf = dp_conf; /* Failed route acquisition must remain retryable on the next update. */ port->applied_valid = false; + + /* did the pairing pass count this port's direct stream so far? */ + was_counted = port->dp_wanted && port->dp_hpd; + /* Keep a direct route until DCP acknowledges release of its display. */ if (!hpd && port->owner && !port->owner->tunnel) { struct apple_dcp *dcp = port->owner->dcp; @@ -1442,15 +1767,50 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, if (ret && !dcp_is_external_h17p(dcp)) return ret; } - } + /* + * Desktop 26.x has one Type-C pipeline and powers the PHY with + * the route. A sink that went away while the port stays in DP + * alt mode (a dock whose monitor was unplugged) must give back + * the pipeline, its crossbar and the PHY, or a display on + * another port is refused as busy. The route is taken again + * when HPD returns. + */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + port->preferred_route = port->owner; + port->hpd = false; + /* + * Not waiting for a pipeline either until HPD returns: + * the pipeline just freed must not route this port again. + */ + port->dp_hpd = false; + ret = dcp_typec_route_deactivate(port->owner); + if (ret) + return ret; + port->owner = NULL; - /* did the pairing pass count this port's direct stream so far? */ - was_counted = port->dp_wanted && port->dp_hpd; + /* The hybrid went back to a live HDMI display. */ + if (dcp->hdmi_hpd && dcp->active && !READ_ONCE(dcp->hdmi_yielded) && + gpiod_get_value_cansleep(dcp->hdmi_hpd)) + dcp_dptx_connect(dcp, 0); + dcp_typec_pipeline_freed(); + } + } if (!is_dp) { + struct apple_dcp_typec_route *r; + port->dp_wanted = false; port->dp_hpd = false; port->busy_noted = false; + /* The Type-C display is gone: a hybrid it took goes back to HDMI. */ + dcp_hybrid_fails = 0; + port->typec_yielded = false; + list_for_each_entry(r, &port->routes, port_link) { + if (!READ_ONCE(r->dcp->hdmi_yielded)) + continue; + dev_info(r->dcp->dev, "Type-C display gone: the hybrid goes back to HDMI\n"); + WRITE_ONCE(r->dcp->hdmi_yielded, false); + } /* a Thunderbolt/USB4 DP tunnel is torn down by its own path */ if (port->owner && port->owner->tunnel) { @@ -1521,6 +1881,23 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, } } + /* + * Desktop 26.x has one Type-C pipeline: only a port with a sink (HPD) + * takes it. A dock or cable in DP alt mode without a display behind it + * would otherwise hold it against the port that has one. + */ + if (!port->owner && !hpd && + route->dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) { + port->applied_valid = true; + return 0; + } + + /* It gave its pipes to an HDMI display, see dcp_hybrid_work(). */ + if (!port->owner && port->typec_yielded) { + port->applied_valid = true; + return 0; + } + if (!port->owner) { best = dcp_typec_free_route(port); /* Every pipeline is driving another port's display. */ @@ -1774,7 +2151,7 @@ int dcp_direct_crossbar_link(struct apple_dcp *dcp, bool up) if (route && !route->tunnel && route->selected) xbar = route->active_xbar ?: route->xbar; else if (!route && dcp->fixed_route_selected) - xbar = dcp->xbar; + xbar = dcp->fixed_xbar; /* A deselected output went idle, and its clocks with it. */ if (dcp->direct_xbar_up && dcp->direct_xbar_up != xbar) dcp->direct_xbar_up = NULL; @@ -1943,6 +2320,7 @@ static int dcp_tb_candidate(void *data) struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream(), .follow = dcp_typec_port_follows(port), + .hybrid_first = dcp_typec_hybrid_first(), }; struct dcp_fabric_route *chosen; bool connector_present = dpin ? !!port->secondary_connector : !!port->connector; @@ -2674,7 +3052,8 @@ static int dcp_typec_follow_decide(struct apple_dcp *dcp, struct drm_crtc *crtc, struct apple_connector *connector, struct dcp_typec_follow *follow) { - struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream() }; + struct dcp_fabric_policy policy = { .dual_stream = dcp_typec_ordered_dual_stream(), + .hybrid_first = dcp_typec_hybrid_first() }; struct apple_dcp_typec_route *from, *to, *holder, *back = NULL; struct apple_dcp_typec_port *port, *other = NULL; bool secondary, off = false; @@ -3666,10 +4045,13 @@ static void dcp_fixed_restore_phy(void *ctx) static int dcp_fixed_select_mux(void *ctx) { struct apple_dcp *dcp = ctx; - int ret = mux_control_select(dcp->xbar, dcp->fixed_mux_index); + int ret = mux_control_select(dcp->fixed_xbar, dcp->fixed_mux_index); - if (!ret) + if (!ret) { dcp->fixed_route_selected = true; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + WRITE_ONCE(dcp->xbar, dcp->fixed_xbar); + } return ret; } @@ -3682,7 +4064,7 @@ static int dcp_fixed_output_select(struct apple_dcp *dcp) { lockdep_assert_held(&dcp_typec_fabric_lock); return dcp_fabric_run_fixed(!!dcp->fixed_phy, !!dcp->active_typec_route, - dcp->xbar && !dcp->fixed_route_selected, + dcp->fixed_xbar && !dcp->fixed_route_selected, &dcp_fixed_ops, dcp); } @@ -3998,6 +4380,176 @@ static const struct dcp_fabric_hdmi_ops dcp_hdmi_ops = { .connect_fixed = dcp_hdmi_connect_fixed, }; +#define DCP_HYBRID_SETTLE_MS 3000 +#define DCP_HYBRID_COOLDOWN_MS 10000 + + +void dcp_fabric_hybrid_kick(struct apple_dcp *dcp, unsigned int delay_ms) +{ + if (dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP) + return; + mod_delayed_work(system_freezable_wq, &dcp->hybrid_wq, msecs_to_jiffies(delay_ms)); +} + +/* typec_hybrid_first was written: apply it now, past any cooldown. */ +static void dcp_fabric_hybrid_policy_changed(void) +{ + struct apple_dcp_typec_port *port; + struct apple_dcp_typec_route *route; + + guard(mutex)(&dcp_typec_fabric_lock); + dcp_hybrid_moved = 0; + dcp_hybrid_fails = 0; + list_for_each_entry(port, &dcp_typec_ports, link) + list_for_each_entry(route, &port->routes, port_link) + if (route->dcp->fixed_phy && route->dcp->hdmi_hpd) + dcp_fabric_hybrid_kick(route->dcp, 0); +} + +static struct apple_dcp *dcp_typec_port_hybrid(struct apple_dcp_typec_port *port) +{ + struct apple_dcp_typec_route *route; + + list_for_each_entry(route, &port->routes, port_link) + if (route->dcp->fixed_phy && route->dcp->hdmi_hpd) + return route->dcp; + return NULL; +} + +/* + * Who drives the hybrid of a desktop 26.x machine when an HDMI display and + * a Type-C display wider than one pipe both want it (typically one monitor + * on two inputs; it cannot tell which input it shows, so typec_hybrid_first + * says, and the desktop may switch it): + * - Type-C first: the Type-C display takes the hybrid and pairs, the HDMI + * display waits; it gets the hybrid back while the Type-C display fits + * one pipe (picture by picture) or is gone. + * - HDMI first: an HDMI display keeps the hybrid; a Type-C display moves + * off it, and takes it only while no HDMI display is there. + * A hand-over is an unplug and replug, so at most one per cooldown. + */ +static void dcp_hybrid_work(struct work_struct *work) +{ + struct apple_dcp *self = container_of(to_delayed_work(work), struct apple_dcp, + hybrid_wq); + unsigned long cooldown = msecs_to_jiffies(DCP_HYBRID_COOLDOWN_MS); + bool typec_first = dcp_typec_hybrid_first(); + struct apple_dcp_typec_port *port; + bool unyielded = false; + + guard(mutex)(&dcp_typec_fabric_lock); + if (dcp_hybrid_moved && time_before(jiffies, dcp_hybrid_moved + cooldown)) { + mod_delayed_work(system_freezable_wq, &self->hybrid_wq, + dcp_hybrid_moved + cooldown - jiffies); + return; + } + + /* A display that gave its pipes to HDMI takes one again when it may. */ + list_for_each_entry(port, &dcp_typec_ports, link) { + struct apple_dcp *hyb = dcp_typec_port_hybrid(port); + + if (!port->typec_yielded) + continue; + if (!typec_first && hyb && gpiod_get_value_cansleep(hyb->hdmi_hpd) > 0 && + hyb->connector && READ_ONCE(hyb->connector->connected) && + !READ_ONCE(hyb->typec_narrow)) + continue; + dev_info(self->dev, "the display on %pOF takes a pipe again\n", + port->connector_np); + port->typec_yielded = false; + unyielded = true; + } + if (unyielded) { + dcp_typec_route_waiting(); + dcp_hybrid_moved = jiffies ?: 1; + return; + } + + list_for_each_entry(port, &dcp_typec_ports, link) { + struct apple_dcp_typec_route *owner = port->owner, *route, *hybrid = NULL; + struct apple_dcp *hyb; + bool hdmi; + + if (!owner || owner->tunnel) + continue; + list_for_each_entry(route, &port->routes, port_link) { + if (route->dcp->fixed_phy && route->dcp->hdmi_hpd) { + hybrid = route; + break; + } + } + if (!hybrid) + continue; + hyb = hybrid->dcp; + hdmi = gpiod_get_value_cansleep(hyb->hdmi_hpd) > 0; + if (!typec_first) + WRITE_ONCE(hyb->hdmi_yielded, false); + + if (owner == hybrid) { + if (!hdmi || (typec_first && !READ_ONCE(hyb->typec_narrow))) + continue; + dev_info(hyb->dev, "%s: the hybrid goes back to the HDMI display\n", + typec_first ? "Type-C display fits one pipe" : "HDMI display first"); + WRITE_ONCE(hyb->hdmi_yielded, false); + if (dcp_typec_move_off_hybrid_locked(port, hyb)) + dcp_hybrid_moved = jiffies ?: 1; + return; + } + + /* + * HDMI first, and the HDMI display is wider than one pipe: the + * Type-C display on the other pipe gives it up and waits, and + * the HDMI display asks for both pipes again. + */ + if (!typec_first && hdmi && READ_ONCE(hyb->peer_starved) && + hyb->connector && READ_ONCE(hyb->connector->connected)) { + struct apple_dcp *dcp = owner->dcp; + + dev_info(hyb->dev, "the display on %pOF waits: the HDMI display takes both pipes\n", + port->connector_np); + if (dcp_dptx_disconnect_oob(to_platform_device(dcp->dev), 0)) + continue; + port->hpd = false; + if (dcp_typec_route_deactivate(owner)) + continue; + port->owner = NULL; + port->typec_yielded = true; + WRITE_ONCE(hyb->peer_starved, false); + if (!dcp_dptx_disconnect(hyb, 0)) + dcp_dptx_connect(hyb, 0); + dcp_hybrid_moved = jiffies ?: 1; + return; + } + + if (!READ_ONCE(owner->dcp->peer_starved) || hyb->active_typec_route || + dcp_typec_safe_held(hyb) || dcp_hybrid_fails >= DCP_HYBRID_MAX_FAILS || + (!typec_first && hdmi)) + continue; + if (typec_first && !READ_ONCE(hyb->hdmi_yielded)) { + WRITE_ONCE(hyb->hdmi_yielded, true); + dev_info(hyb->dev, "hybrid yielded to the Type-C display on %pOF\n", + port->connector_np); + /* Also a link the firmware keeps without a described display. */ + if ((hyb->connector && READ_ONCE(hyb->connector->connected)) || + hyb->dptxport[0].connected) { + dev_info(hyb->dev, "HDMI display waits: the hybrid goes to a Type-C display wider than one pipe\n"); + dcp_hdmi_yield_link(hyb); + /* The desktop turns the HDMI CRTC off first. */ + mod_delayed_work(system_freezable_wq, &self->hybrid_wq, + msecs_to_jiffies(DCP_HYBRID_SETTLE_MS)); + return; + } + } + if (dcp_typec_move_starved_locked()) + dcp_hybrid_fails = 0; + else if (++dcp_hybrid_fails >= DCP_HYBRID_MAX_FAILS) + dev_warn(hyb->dev, "hybrid not taken %u times, waiting for the next plug\n", + dcp_hybrid_fails); + dcp_hybrid_moved = jiffies ?: 1; + return; + } +} + static void dcp_hdmi_update_locked(struct apple_dcp *dcp) { unsigned long flags; diff --git a/drivers/gpu/drm/apple/dcp-fabric.h b/drivers/gpu/drm/apple/dcp-fabric.h index 869a2c3716cfe..57bffb02d715f 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.h +++ b/drivers/gpu/drm/apple/dcp-fabric.h @@ -39,6 +39,8 @@ struct apple_dcp_typec_route { u32 dptx_phy; u32 mux_index; bool selected; + /* desktop 26.x: the DP PHY is initialised and powered for this route */ + bool phy_powered; /* crossbar output actually selected: xbar (dpphy) or a Thunderbolt dpin */ unsigned int tunnel_dpin; u64 tunnel_generation; @@ -51,6 +53,7 @@ struct apple_dcp_typec_route { int deactivate_error; }; +int dcp_typec_phy_on(struct apple_dcp *dcp); bool dcp_is_typec_output(struct apple_dcp *dcp); bool dcp_is_usb4_output(struct apple_dcp *dcp); bool dcp_uses_t6020_tunnel_flow(struct apple_dcp *dcp); diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 5950b5883d28f..4ffa59bcf9b68 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -18,6 +18,7 @@ #include #include "dcp-fabric.h" +#include "dcp-peer-v26_6.h" #include "dcp-fabric-session.h" #include "dcp-relink.h" #include "dptxep.h" @@ -270,17 +271,6 @@ struct dcp_desktop_state { * display (unplug); the next swap needs a power-on and a mode set. */ bool ext_pipe_off; - /* - * Dynamic Type-C DP route (T6041 dcpext1): the DP PHYs and crossbars - * of every Type-C port, the one in use (or -1), the DPTX target PHY - * of each and the crossbar state selecting this processor. - */ - unsigned int ndyn; - int dyn_active; - struct phy *dyn_phy[3]; - struct mux_control *dyn_xbar[3]; - u32 dyn_dptx_phy[3]; - u32 dyn_mux_index; }; /* TODO: move IOMFB members to its own struct */ @@ -298,6 +288,36 @@ struct apple_dcp { bool vblank_works_disabled; struct dcp_fabric_pipeline fabric; struct device *dev; + /* Peer DCP for dual-pipe (desktop 26.6): the primary reserves an idle + * peer pipe; the peer records its owner. Protected by dcp_peer_lock. + */ + struct apple_dcp *peer_reserved; + struct apple_dcp *peer_owner; + /* + * This pipeline's Type-C display is wider than one pipe and found no + * peer above it: it needs the hybrid, which was taken when it came. + */ + bool peer_starved; + /* + * Desktop 26.x hybrid with apple,typec-hybrid-first: its HDMI display + * has given the pipeline to a Type-C display wider than one pipe and + * waits until that one fits one pipe again or leaves. + */ + bool hdmi_yielded; + /* The display on this pipeline runs a mode within one pipe. */ + bool typec_narrow; + struct delayed_work hybrid_wq; + struct dcp_peer_tiling peer_tiling; + bool peer_unbinding; + /* Firmware display endpoint (its Disp_Index), apple,dcp-endpoint; + * separate from index, which also selects the doorbell and PHY mode. */ + u32 peer_endpoint; + /* Paired DRM scanout on the primary; under dcp_peer_lock. */ + bool peer_active, peer_failed; + /* The DRM mode the pair was set to (timing, color mode IDs, minRR). */ + u32 peer_mode_timing, peer_mode_color, peer_min_vrr; + /* On the peer: its first completion since the pair was set was logged. */ + bool peer_complete_logged; struct platform_device *piodma; bool piodma_created; struct iommu_domain *iommu_dom; @@ -582,7 +602,13 @@ struct apple_dcp { /* these fields are output port specific */ struct phy *phy; struct phy *fixed_phy; + /* + * The crossbar control a link configuration takes up and down: the + * fixed output's, or on desktop 26.x the active Type-C route's. + */ struct mux_control *xbar; + /* The fixed (HDMI) output's own crossbar control, selected for it. */ + struct mux_control *fixed_xbar; struct typec_mux *typec_mux; struct apple_dcp_typec_route typec_routes[DCP_MAX_TYPEC_ROUTES]; struct apple_dcp_typec_route *active_typec_route; @@ -707,6 +733,17 @@ struct apple_dcp { /* Hardirq, resume and expiry share one generation under this lock. */ spinlock_t hdmi_presence_lock; struct dcp_fabric_presence hdmi_presence; + /* The state before the current series of HPD edges began. */ + enum dcp_fabric_presence_state hdmi_presence_prev; + /* + * A desktop 26.x Type-C display that came back from system sleep with a + * partial mode list (the sink was not ready for its EDID yet) is + * connected again after a retrain found no mode. + */ + struct delayed_work link_recovery_wq; + unsigned int link_recovery_tries; + unsigned int link_recovery_snap; + atomic_t modesets_done; unsigned long hdmi_edge_jiffies; bool hdmi_edge_seen; struct delayed_work hdmi_settle_wq; diff --git a/drivers/gpu/drm/apple/dcp-peer-v26_6.h b/drivers/gpu/drm/apple/dcp-peer-v26_6.h new file mode 100644 index 0000000000000..3b88d5e040303 --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-peer-v26_6.h @@ -0,0 +1,112 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +#ifndef APPLE_DCP_PEER_V26_6_H +#define APPLE_DCP_PEER_V26_6_H + +#include +#include +#include +#include + +struct dcp_peer_tiling { + u32 phase, modeset_state, x, y, width, height, display_id; + bool member, vftg, primary, ready, mode_flag, display_id_valid; + bool modeset_complete; +}; + +static inline bool dcp_peer_tiling_get( + const struct dcp_peer_tiling *own, + const struct dcp_peer_tiling *related, u32 event, u32 param, u32 *value) +{ + const struct dcp_peer_tiling *target = own; + + if (event == 2 && own->member) { + if (!related) + return false; + target = related; + } + switch (param) { + case 1: *value = target->member; return true; + case 2: + if (event == 1 || target->member) + *value = target->phase; + return true; + case 3: return true; + case 4: + if (event != 1 && !target->member) + return true; + if (!target->display_id_valid) + return false; + *value = target->display_id; + return true; + case 5: case 12: *value = target->x; return true; + case 6: + if (event == 1 || target->member) + *value = target->ready; + return true; + case 8: *value = own->vftg; return true; + case 9: *value = own->primary; return true; + case 10: + if (event == 1 || target->member) + *value = target->modeset_state; + return true; + case 11: *value = 0; return true; + case 13: *value = target->y; return true; + case 14: *value = target->mode_flag; return true; + default: return false; + } +} + +static inline bool dcp_peer_tiling_set(struct dcp_peer_tiling *own, + struct dcp_peer_tiling *related, u32 event, u32 param, u32 value) +{ + struct dcp_peer_tiling *target = event == 2 ? related : own; + + if (!target) + return false; + switch (param) { + case 2: target->phase = value; return true; + case 6: target->ready = !!value; return true; + case 10: target->modeset_state = value; return true; + default: return false; + } +} + +/* + * Display allocation hints — 160 bytes, layout shared with fw27. + * The AP fills primary/peer endpoint IDs and extra-pipe count for a sink + * wider than one pipe's maximum. + */ +struct dcp_display_hints { + u8 hints[0x6c]; + u8 padding[4]; + /* The allocation the AP fills in, or clears when it allocates no peer. */ + struct_group_attr(allocation, __packed, + __le32 primary_id, peer_id; + u8 extra_pipes, flags; + u8 allocation_reserved[22]; + ); + __le32 status; + u8 tail[12]; +} __packed; + +static_assert(sizeof(struct dcp_display_hints) == 160); +static_assert(offsetof(struct dcp_display_hints, primary_id) == 0x70); +static_assert(offsetof(struct dcp_display_hints, status) == 0x90); +static_assert(sizeof_field(struct dcp_display_hints, allocation) == 32); + +/* The widest mode one desktop 26.6 DISPEXT pipe drives (firmware: "max width: 6720"). */ +#define DCP_DESKTOP_PIPE_MAX_WIDTH 6720 + +/* + * Peer validity by firmware endpoint (apple,dcp-endpoint, the firmware's + * Disp_Index). Endpoint 0 is the internal display and never a peer; the + * external pipes merge upwards only (the T8132 rule inherited by T6050): + * dcpext0 (1) is the primary, dcpext1 (2) its peer. The other direction + * pairs, but the peer's half of the picture is noise. + */ +static inline bool dcp_peer_index_allowed_26_6(u32 primary, u32 peer) +{ + return primary && peer > primary; +} + +#endif diff --git a/drivers/gpu/drm/apple/dcp-peer-v26_6.inc b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc new file mode 100644 index 0000000000000..29c4b6e697ebc --- /dev/null +++ b/drivers/gpu/drm/apple/dcp-peer-v26_6.inc @@ -0,0 +1,1116 @@ +/* SPDX-License-Identifier: GPL-2.0-only OR MIT */ +/* + * Desktop 26.6 dual-pipe peer DCP — allocation, tiling rendezvous and + * paired modeset for a sink wider than one pipe's maximum (G95NC 7680x2160). + * + * The 26.6 firmware uses the same A360-A373 display-pipeline methods as + * firmware 27; its set_power_state is A473. The sequence follows the + * T6050 firmware-27 peer (aurora-silicon/linux#485), with the method and + * callback numbers of the desktop 26.6 firmware: D115 reads the tiling + * state, D116 writes it, and the peer's swaps go as a direct + * swap_start/swap_submit chain (nothing advances the IOMFB transaction + * queue on this firmware). The two DISPEXT pipes merge upwards only: + * the primary is firmware endpoint 1 (dcpext0), the peer endpoint 2. + */ + +static DEFINE_MUTEX(dcp_peer_lock); + +/* ---- tiling state (D114/D115 callbacks) ---- */ + +bool dcp_peer_tiling_state_26_6(struct apple_dcp *dcp, bool write, + u32 event, u32 param, u32 *value) +{ + struct apple_dcp *peer; + + guard(mutex)(&dcp_peer_lock); + if (dcp->peer_unbinding || dcp->crashed) + return false; + peer = dcp->peer_reserved ?: dcp->peer_owner; + if (!peer || peer->peer_unbinding || peer->crashed) + return false; + if (!dcp->peer_tiling.vftg || !peer->peer_tiling.vftg) + return false; + if (write) + return dcp_peer_tiling_set(&dcp->peer_tiling, + &peer->peer_tiling, event, param, *value); + return dcp_peer_tiling_get(&dcp->peer_tiling, &peer->peer_tiling, + event, param, value); +} + +/* ---- synchronous RPC helper ---- */ + +struct dcp_peer_wait { + struct kref refs; + struct completion done; + size_t size; + u8 reply[128]; +}; + +static void dcp_peer_wait_free(struct kref *ref) +{ + kfree(container_of(ref, struct dcp_peer_wait, refs)); +} + +static void dcp_peer_wait_done(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct dcp_peer_wait *wait = cookie; + + if (wait->size && out) + memcpy(wait->reply, out, min(wait->size, sizeof(wait->reply))); + complete(&wait->done); + kref_put(&wait->refs, dcp_peer_wait_free); +} + +static int dcp_peer_call(struct apple_dcp *dcp, const char *tag, + void *in, size_t in_size, void *out, size_t out_size) +{ + struct dcp_method_entry method = { .name = "peer_call" }; + struct dcp_peer_wait *wait; + int ret = 0; + + if (out_size > sizeof(wait->reply)) + return -EINVAL; + wait = kzalloc(sizeof(*wait), GFP_KERNEL); + if (!wait) + return -ENOMEM; + memcpy(method.tag, tag, 4); + wait->size = out_size; + init_completion(&wait->done); + kref_init(&wait->refs); + kref_get(&wait->refs); + dcp_push(dcp, false, &method, in_size, out_size, in, + dcp_peer_wait_done, wait); + if (!wait_for_completion_timeout(&wait->done, + msecs_to_jiffies(!memcmp(tag, "A413", 4) ? 8500 : 2000))) + ret = -ETIMEDOUT; + else if (out_size && out) + memcpy(out, wait->reply, out_size); + kref_put(&wait->refs, dcp_peer_wait_free); + return ret; +} + +static int dcp_peer_call_oob(struct apple_dcp *dcp, const char *tag, + void *in, size_t in_size, void *out, size_t out_size) +{ + struct dcp_method_entry method = { .name = "peer_call_oob" }; + struct dcp_peer_wait *wait; + int ret = 0; + + if (out_size > sizeof(wait->reply)) + return -EINVAL; + wait = kzalloc(sizeof(*wait), GFP_KERNEL); + if (!wait) + return -ENOMEM; + memcpy(method.tag, tag, 4); + wait->size = out_size; + init_completion(&wait->done); + kref_init(&wait->refs); + kref_get(&wait->refs); + dcp_push(dcp, true, &method, in_size, out_size, in, + dcp_peer_wait_done, wait); + if (!wait_for_completion_timeout(&wait->done, msecs_to_jiffies(2000))) + ret = -ETIMEDOUT; + else if (out_size && out) + memcpy(out, wait->reply, out_size); + kref_put(&wait->refs, dcp_peer_wait_free); + return ret; +} + +/* ---- peer release / forget ---- */ + +/* + * The primary's display is gone (cable removed, port moved, system sleep): + * give the reserved peer back. A merged pair is stopped first; a pair that + * still runs is kept. The next allocation reserves a peer again. Without + * this the peer stays marked as an owner's peer after its port was taken by + * another display, and swap_complete would discard that display's own swaps. + */ +/* Drop @owner's reservation of @peer unless their pair runs; peer lock held. */ +static void dcp_peer_drop_locked(struct apple_dcp *owner, struct apple_dcp *peer) +{ + lockdep_assert_held(&dcp_peer_lock); + if (owner->peer_reserved != peer || owner->peer_tiling.vftg || + owner->peer_tiling.member || peer->peer_tiling.vftg || + peer->peer_tiling.member) + return; + memset(&owner->peer_tiling, 0, sizeof(owner->peer_tiling)); + memset(&peer->peer_tiling, 0, sizeof(peer->peer_tiling)); + peer->peer_owner = NULL; + owner->peer_reserved = NULL; + owner->peer_active = false; + dev_info(owner->dev, "peer %s no longer reserved: it drives a display of its own\n", + dev_name(peer->dev)); +} + +void dcp_peer_release_26_6(struct apple_dcp *dcp) +{ + guard(mutex)(&dcp_peer_lock); + if (dcp->peer_reserved) { + struct apple_dcp *peer = dcp->peer_reserved; + + if (dcp->peer_tiling.vftg || dcp->peer_tiling.member || + peer->peer_tiling.vftg || peer->peer_tiling.member) + return; + memset(&dcp->peer_tiling, 0, sizeof(dcp->peer_tiling)); + memset(&peer->peer_tiling, 0, sizeof(peer->peer_tiling)); + peer->peer_owner = NULL; + dcp->peer_reserved = NULL; + dcp->peer_active = false; + } +} + +void dcp_peer_bound_26_6(struct apple_dcp *dcp) +{ + guard(mutex)(&dcp_peer_lock); + dcp->peer_unbinding = false; +} + +static void dcp_peer_forget(struct apple_dcp *dcp) +{ + guard(mutex)(&dcp_peer_lock); + dcp->peer_unbinding = true; + if (dcp->peer_reserved) { + dcp->peer_reserved->peer_owner = NULL; + memset(&dcp->peer_reserved->peer_tiling, 0, + sizeof(dcp->peer_reserved->peer_tiling)); + dcp->peer_reserved = NULL; + memset(&dcp->peer_tiling, 0, sizeof(dcp->peer_tiling)); + } + if (dcp->peer_owner) { + memset(&dcp->peer_owner->peer_tiling, 0, + sizeof(dcp->peer_owner->peer_tiling)); + memset(&dcp->peer_tiling, 0, sizeof(dcp->peer_tiling)); + dcp->peer_owner->peer_reserved = NULL; + dcp->peer_owner = NULL; + } +} + +/* ---- display IDs from D575 hotplug ---- */ + +void dcp_peer_display_ids(struct apple_dcp *dcp, u32 own, u32 related) +{ + struct apple_dcp *peer; + + guard(mutex)(&dcp_peer_lock); + peer = dcp->peer_reserved ?: dcp->peer_owner; + if (!peer || own == related || dcp->peer_unbinding || peer->peer_unbinding) + return; + dcp->peer_tiling.display_id = own; + dcp->peer_tiling.display_id_valid = true; + peer->peer_tiling.display_id = related; + peer->peer_tiling.display_id_valid = true; +} + +/* ---- peer allocation (DPTX call 21 handler) ---- */ + +/* + * Unlike T6050, where the peer is a pipe without outputs, the only T6041 + * candidate (dcpext0) has Type-C routes and HDMI. It qualifies while idle: + * no active route and nothing connected. Its fixed HDMI path brings the + * DPTX link up at start without a sink (dcp_desktop_dp2hdmi_hpd()); that + * link alone does not make it busy. Returns a reason mask, 0 = eligible. + */ +static bool dcp_peer_hdmi_sinkless(struct apple_dcp *p) +{ + return p->fixed_phy && p->hdmi_hpd && + !(p->connector && READ_ONCE(p->connector->connected)); +} + +static u32 dcp_peer_reject_26_6(struct apple_dcp *dcp, struct apple_dcp *p) +{ + u32 why = 0; + + if (!p->active || p->crashed || p->peer_unbinding) + why |= BIT(0); + if (p->peer_owner || p->peer_reserved) + why |= BIT(1); + if (p->active_typec_route) + why |= BIT(2); + if ((p->connector && p->connector->connected) || p->dptxport[1].connected) + why |= BIT(3); + if (p->dptxport[0].connected && !dcp_peer_hdmi_sinkless(p)) + why |= BIT(8); + if (!dcp_peer_index_allowed_26_6(dcp->peer_endpoint, p->peer_endpoint)) + why |= BIT(4); + if (p->dptx_die != dcp->dptx_die) + why |= BIT(5); + if (!p->crtc || p->crtc->base.dev != dcp->crtc->base.dev) + why |= BIT(6); + if (p->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP) + why |= BIT(7); + return why; +} + +int dcp_peer_hints_26_6(struct apple_dcp *dcp, struct dptx_port *port, + const void *data, size_t size, void *reply, size_t reply_size) +{ + struct dcp_display_hints *r = reply; + struct apple_dcp *peer = NULL; + struct drm_crtc *crtc; + u32 width; + + if (size != sizeof(*r) || reply_size != sizeof(*r)) + return -EINVAL; + memcpy(reply, data, sizeof(*r)); + r->status = 0; + if (!of_device_is_compatible(dcp->dev->of_node, "apple,t6041-dcpext") || + port->unit || !dcp->crtc) + return 0; + + width = get_unaligned_le32(r->hints + 0x38); + if (!(r->hints[0] & 1) || !width || width <= DCP_DESKTOP_PIPE_MAX_WIDTH) { + memset(&r->allocation, 0, sizeof(r->allocation)); + return 0; + } + + guard(mutex)(&dcp_peer_lock); + if (dcp->peer_unbinding || dcp->crashed) + return -ENODEV; + /* + * A reservation made before a pipe got a display of its own (a Type-C + * display moved there, or an HDMI display that came) is stale: the + * pipe is nobody's peer any more. A running pair keeps it. + */ + if (dcp->peer_owner && READ_ONCE(dcp->active_typec_route)) + dcp_peer_drop_locked(dcp->peer_owner, dcp); + if (dcp->peer_reserved && + (dcp_peer_reject_26_6(dcp, dcp->peer_reserved) & ~BIT(1))) + dcp_peer_drop_locked(dcp, dcp->peer_reserved); + if (dcp->peer_owner) { + memset(&r->allocation, 0, sizeof(r->allocation)); + return 0; + } + peer = dcp->peer_reserved; + if (!peer) { + drm_for_each_crtc(crtc, dcp->crtc->base.dev) { + struct apple_dcp *p = platform_get_drvdata(to_apple_crtc(crtc)->dcp); + u32 why; + + if (!p || p == dcp || + !of_device_is_compatible(p->dev->of_node, "apple,t6041-dcpext")) + continue; + why = dcp_peer_reject_26_6(dcp, p); + dev_dbg(dcp->dev, "peer candidate %s endpoint %u: reject %#x\n", + dev_name(p->dev), p->peer_endpoint, why); + if (why) + continue; + if (!peer || p->peer_endpoint < peer->peer_endpoint) + peer = p; + } + if (!peer) { + dev_info(dcp->dev, "no idle peer pipe for a %u px display\n", width); + WRITE_ONCE(dcp->peer_starved, true); + dcp_fabric_hybrid_kick(dcp, 0); + memset(&r->allocation, 0, sizeof(r->allocation)); + return 0; + } + WRITE_ONCE(dcp->peer_starved, false); + dcp->peer_reserved = peer; + peer->peer_owner = dcp; + dev_info(dcp->dev, "reserved peer %s endpoint %u for %ux%u\n", + dev_name(peer->dev), peer->peer_endpoint, width, + get_unaligned_le32(r->hints + 0x3c)); + } + + r->primary_id = cpu_to_le32(dcp->peer_endpoint); + r->peer_id = cpu_to_le32(peer->peer_endpoint); + /* One extra pipe, full dual pipe: the limited-dual-pipe flag stays clear. */ + r->extra_pipes = 1; + r->flags &= ~3; + return 0; +} + +/* ---- paired power (A473 set_power_state) ---- */ + +/* As dcp_power_state_req(): the state, no transition flags, a result. */ +static void dcp_peer_power_req(u8 *req, bool on) +{ + memset(req, 0, 16); + put_unaligned_le64(on, req); + memset(req + 13, 0xaa, 3); +} + +static int dcp_peer_power(struct apple_dcp *dcp, struct apple_dcp *peer, bool on) +{ + struct dcp_peer_wait *wait; + struct dcp_method_entry method = { .name = "peer_power", + .tag = {'A', '4', '7', '3'} }; + u8 power[16], response[8]; + int ret; + + wait = kzalloc(sizeof(*wait), GFP_KERNEL); + if (!wait) + return -ENOMEM; + kref_init(&wait->refs); + init_completion(&wait->done); + wait->size = sizeof(response); + dcp_peer_power_req(power, on); + kref_get(&wait->refs); + dcp_push(dcp, false, &method, sizeof(power), sizeof(response), power, + dcp_peer_wait_done, wait); + ret = dcp_peer_call(peer, "A473", power, sizeof(power), + response, sizeof(response)); + if (!ret && get_unaligned_le32(response + 4)) + ret = -EIO; + if (!wait_for_completion_timeout(&wait->done, msecs_to_jiffies(2000))) + ret = ret ?: -ETIMEDOUT; + else if (!ret && get_unaligned_le32(wait->reply + 4)) + ret = -EIO; + kref_put(&wait->refs, dcp_peer_wait_free); + dev_dbg(dcp->dev, "peer power %u result=%d\n", on, ret); + return ret; +} + +/* ---- quiesce: tear down the pair ---- */ + +static int dcp_peer_quiesce(struct apple_dcp *dcp, struct apple_dcp *peer) +{ + __le32 manage = 0; + int ret; + + { + guard(mutex)(&dcp_peer_lock); + dcp->peer_tiling.modeset_complete = false; + peer->peer_tiling.modeset_complete = false; + } + ret = dcp_peer_power(dcp, peer, false); + if (ret) + return ret; + msleep(100); + ret = dcp_peer_call(dcp, "A362", &manage, sizeof(manage), NULL, 0); + if (ret) + return ret; + ret = dcp_peer_call(peer, "A362", &manage, sizeof(manage), NULL, 0); + if (ret) + return ret; + { + guard(mutex)(&dcp_peer_lock); + dcp->peer_tiling.member = peer->peer_tiling.member = false; + dcp->peer_tiling.vftg = peer->peer_tiling.vftg = false; + dcp->peer_tiling.modeset_complete = false; + peer->peer_tiling.modeset_complete = false; + dcp->peer_tiling.phase = peer->peer_tiling.phase = 0; + dcp->peer_tiling.modeset_state = peer->peer_tiling.modeset_state = 0; + dcp->peer_tiling.ready = peer->peer_tiling.ready = false; + } + return 0; +} + +/* + * Back to one pipe: quiesce the pair, then power the primary again. A DRM + * commit that only changes the mode does not power the CRTC on again. + */ +static int dcp_peer_unpair(struct apple_dcp *dcp, struct apple_dcp *peer) +{ + u8 power[16], response[8]; + int ret; + + dcp_peer_power_req(power, true); + ret = dcp_peer_quiesce(dcp, peer); + if (!ret) + ret = dcp_peer_call(dcp, "A473", power, sizeof(power), + response, sizeof(response)); + if (!ret && get_unaligned_le32(response + 4)) + ret = -EIO; + dev_info(dcp->dev, "peer unpair result=%d\n", ret); + return ret; +} + +/* + * The primary is being powered off or put to sleep (DPMS, lock screen): + * stop the pair first. On m4.135 the lock screen's DPMS-off ran the + * single-pipe power-off against a merged pair: set_power_state(0) timed + * out, the DPTX HPD deassert failed and the firmware looped until reboot. + * Returns 1 after a paired stop, 0 for an unpaired pipe, or an error. + */ +int dcp_peer_stop_26_6(struct apple_dcp *dcp) +{ + struct apple_dcp *peer; + int ret; + + scoped_guard(mutex, &dcp_peer_lock) { + peer = dcp->peer_reserved; + if (!peer || peer->peer_owner != dcp || + (!dcp->peer_tiling.vftg && !dcp->peer_tiling.member && + !dcp->peer_active)) + return 0; + dcp->peer_active = false; + } + ret = dcp_peer_quiesce(dcp, peer); + dev_info(dcp->dev, "peer stop before power-off: %d\n", ret); + return ret ? ret : 1; +} + +/* ---- DSC copy from primary to peer ---- */ + +static int dcp_peer_copy_dsc(struct apple_dcp *primary, struct apple_dcp *peer) +{ + u8 caps_req[36] = {}, caps[32], bpp_req[4] = {}, bpp[8]; + u8 hints_req[12] = {}, hints[12]; + u32 required; + int ret; + + ret = dcp_peer_call_oob(primary, "A369", NULL, 0, &required, 4); + dev_dbg(primary->dev, "peer DSC required: ret=%d value=%#x\n", ret, required); + if (ret || !(required & 1)) + return ret; + ret = dcp_peer_call_oob(primary, "A367", caps_req, 36, caps, 32); + if (ret) + return ret; + memcpy(caps_req, caps, 32); + ret = dcp_peer_call(peer, "A368", caps_req, 36, caps, 32); + if (ret) + return ret; + ret = dcp_peer_call_oob(primary, "A370", bpp_req, 4, bpp, 8); + if (ret || get_unaligned_le32(bpp + 2)) + return ret ?: -EIO; + memcpy(bpp_req, bpp, 2); + ret = dcp_peer_call(peer, "A371", bpp_req, 4, bpp, 8); + if (ret || get_unaligned_le32(bpp + 2)) + return ret ?: -EIO; + ret = dcp_peer_call_oob(primary, "A372", hints_req, 12, hints, 12); + if (ret || get_unaligned_le32(hints + 8)) + return ret ?: -EIO; + memcpy(hints_req, hints, 8); + ret = dcp_peer_call(peer, "A373", hints_req, 12, hints, 12); + if (ret || get_unaligned_le32(hints + 8)) + return ret ?: -EIO; + dev_dbg(primary->dev, "peer DSC copied: bpp=%u hints=%*ph\n", + get_unaligned_le16(bpp), 8, hints); + return 0; +} + +/* ---- split timing for the half-width derived mode ---- */ + +static void dcp_peer_split_timing(u8 *timing) +{ + static const u8 half_fields[] = { 4, 8, 12, 16, 20, 28, 68 }; + + for (unsigned int i = 0; i < sizeof(half_fields); i++) { + u8 *field = timing + half_fields[i]; + + put_unaligned_le32(get_unaligned_le32(field) >> 1, field); + } + put_unaligned_le32((s32)get_unaligned_le32(timing + 24) / 2, + timing + 24); +} + +/* ---- A364/A365: get video data and insert merge modes ---- */ + +struct dcp_peer_video_data { + u8 timing[0x50]; + u8 color[0x20]; +} __packed; + +struct dcp_peer_get_video_req { + struct dcp_peer_video_data video; + __le32 timing_id, color_id; + u8 requires_merge, timing_null, color_null, requires_merge_null; +} __packed; + +struct dcp_peer_get_video_resp { + struct dcp_peer_video_data video; + u8 requires_merge, success, padding[2]; +} __packed; + +struct dcp_peer_insert_video_req { + struct dcp_peer_video_data video; + __le32 timing_id, color_id; + u8 timing_null, color_null, padding[2]; +} __packed; + +struct dcp_peer_insert_video_resp { + struct dcp_peer_video_data video; + u8 success, padding[3]; +} __packed; + +static_assert(sizeof(struct dcp_peer_get_video_req) == 0x7c); +static_assert(sizeof(struct dcp_peer_get_video_resp) == 0x74); +static_assert(sizeof(struct dcp_peer_insert_video_req) == 0x7c); +static_assert(sizeof(struct dcp_peer_insert_video_resp) == 0x74); + +/* ---- paired modeset ---- */ + +static int dcp_peer_prepare_vftg(struct apple_dcp *dcp, struct apple_dcp *peer, + const struct dcp_peer_insert_video_req *video) +{ + guard(mutex)(&dcp_peer_lock); + if (dcp->peer_reserved != peer || peer->peer_owner != dcp) + return -ENODEV; + dcp->peer_tiling.vftg = peer->peer_tiling.vftg = true; + dcp->peer_tiling.modeset_complete = false; + peer->peer_tiling.modeset_complete = false; + dcp->peer_tiling.primary = true; + peer->peer_tiling.primary = false; + dcp->peer_tiling.width = peer->peer_tiling.width = + get_unaligned_le32(video->video.timing + 8); + dcp->peer_tiling.height = peer->peer_tiling.height = + get_unaligned_le32(video->video.timing + 0x28); + dcp->peer_tiling.x = dcp->peer_tiling.y = peer->peer_tiling.y = 0; + peer->peer_tiling.x = dcp->peer_tiling.width; + return 0; +} + +/* + * A360/A361: both engines must hand out the same swap ID for a paired + * frame. The primary consumed one more ID than the peer during the paired + * mode set on m4.134 (peer 74, primary 75), so this runs again afterwards. + */ +static int dcp_peer_sync_swap_ids(struct apple_dcp *dcp, struct apple_dcp *peer, + const char *when) +{ + u32 primary_id, peer_id, status; + int ret; + + ret = dcp_peer_call(dcp, "A360", NULL, 0, &primary_id, 4); + if (ret) + return ret; + ret = dcp_peer_call(peer, "A360", NULL, 0, &peer_id, 4); + if (ret) + return ret; + dev_dbg(dcp->dev, "peer master swap IDs %s: primary %u peer %u\n", + when, primary_id, peer_id); + if (primary_id != peer_id) { + status = max(primary_id, peer_id); + ret = dcp_peer_call(primary_id < peer_id ? dcp : peer, + "A361", &status, 4, NULL, 0); + } + return ret; +} + +/* + * Adaptive sync (IOMFBParameter 14, minimum rate in 16.16 Hz, 0 = off) goes + * to both pipes before the paired mode set, as the single-pipe mode set + * sends it before the mode is selected. A pipe keeps the parameter across + * a replug: a peer that last ran a VRR mode alone would otherwise merge + * with VRR on while the primary runs without it, which crashed its firmware. + */ +static int dcp_peer_adaptive_sync(struct apple_dcp *pipe, u32 min_vrr) +{ + struct dcp_set_parameter_dcp param = { + .param = IOMFBPARAM_ADAPTIVE_SYNC, + .value = { min_vrr }, + .count = 3, + }; + u32 status = 0; + int ret; + + ret = dcp_peer_call(pipe, "A442", ¶m, sizeof(param), &status, 4); + dev_dbg(pipe->dev, "peer adaptive sync minRR %#x: ret=%d status=%#x\n", + min_vrr, ret, status); + return ret; +} + +static int dcp_peer_modeset(struct apple_dcp *dcp, struct apple_dcp *peer, + const struct dcp_peer_insert_video_req *video, + bool firmware_requires_merge, u32 min_vrr) +{ + struct dcp_peer_wait *wait; + struct dcp_method_entry method = { .name = "peer_modeset", + .tag = {'A', '4', '1', '3'} }; + /* A413 takes the color mode first, as dcp_set_digital_out_mode_req. */ + __le32 mode[2] = {video->color_id, video->timing_id}; + u32 phase, status; + unsigned long deadline; + int ret; + + { + guard(mutex)(&dcp_peer_lock); + if (dcp->peer_reserved != peer || peer->peer_owner != dcp || + !dcp->peer_tiling.display_id_valid || + !peer->peer_tiling.display_id_valid) + return -ENODEV; + dcp->peer_tiling.member = peer->peer_tiling.member = true; + dcp->peer_tiling.mode_flag = peer->peer_tiling.mode_flag = + !firmware_requires_merge; + } + ret = dcp_peer_power(dcp, peer, true); + if (ret) + return ret; + msleep(100); + + ret = dcp_peer_adaptive_sync(dcp, min_vrr); + if (!ret) + ret = dcp_peer_adaptive_sync(peer, min_vrr); + if (ret) + return ret; + + ret = dcp_peer_sync_swap_ids(dcp, peer, "before A413"); + if (ret) + return ret; + + /* Set phase to 1 on both engines */ + phase = 1; + dcp_peer_tiling_state_26_6(dcp, true, 1, 10, &phase); + dcp_peer_tiling_state_26_6(peer, true, 1, 10, &phase); + + /* A413 on primary (async), then wait for phase >= 2 */ + wait = kzalloc(sizeof(*wait), GFP_KERNEL); + if (!wait) + return -ENOMEM; + init_completion(&wait->done); + kref_init(&wait->refs); + kref_get(&wait->refs); + wait->size = 4; + dcp_push(dcp, false, &method, sizeof(mode), 4, mode, + dcp_peer_wait_done, wait); + + deadline = jiffies + msecs_to_jiffies(8500); + for (;;) { + phase = 0; + if (!dcp_peer_tiling_state_26_6(dcp, false, 1, 10, &phase)) { + ret = -ENODEV; + goto put; + } + if (phase >= 2) + break; + /* + * The 26.6 desktop primary merges the timing and completes its + * A413 without waiting for the peer (no phase2 rendezvous seen + * on m4.132): go on to the peer once it succeeded. + */ + if (completion_done(&wait->done)) { + status = get_unaligned_le32(wait->reply); + dev_dbg(dcp->dev, + "peer primary A413 done before phase2: phase=%u status=%#x\n", + phase, status); + if (status) { + ret = -EIO; + goto put; + } + break; + } + if (time_after_eq(jiffies, deadline)) { + ret = -ETIMEDOUT; + dev_err(dcp->dev, + "peer primary timed out before phase2: phase=%u\n", phase); + goto put; + } + msleep(2); + } + dev_dbg(dcp->dev, "peer primary reached modeset phase%u\n", phase); + + /* Copy DSC parameters from primary to peer */ + ret = dcp_peer_copy_dsc(dcp, peer); + if (ret) + goto put; + + /* A413 on peer */ + ret = dcp_peer_call(peer, "A413", mode, sizeof(mode), &status, 4); + if (ret || status) { + dev_err(dcp->dev, "peer A413 failed: ret=%d status=%#x\n", ret, status); + ret = ret ?: -EIO; + goto put; + } + + /* Wait for primary's A413 to complete */ + if (!wait_for_completion_timeout(&wait->done, msecs_to_jiffies(8500))) + ret = -ETIMEDOUT; + else if (get_unaligned_le32(wait->reply)) + ret = -EIO; + if (!ret) + ret = dcp_peer_sync_swap_ids(dcp, peer, "after the paired modeset"); +put: + kref_put(&wait->refs, dcp_peer_wait_free); + if (!ret) { + guard(mutex)(&dcp_peer_lock); + dcp->peer_tiling.modeset_complete = true; + peer->peer_tiling.modeset_complete = true; + } + dev_dbg(dcp->dev, "peer paired modeset result=%d\n", ret); + return ret; +} + +/* ---- install video: A364+A365+A366+modeset ---- */ + +static int dcp_peer_install_video(struct apple_dcp *dcp, struct apple_dcp *peer, + u32 timing, u32 color, u32 min_vrr) +{ + struct dcp_peer_get_video_req req = {}; + struct dcp_peer_get_video_resp resp; + struct dcp_peer_insert_video_req insert = {}; + struct dcp_peer_insert_video_resp inserted; + __le32 enabled = cpu_to_le32(1); + int ret; + + req.timing_id = cpu_to_le32(timing); + req.color_id = cpu_to_le32(color); + ret = dcp_peer_call(dcp, "A364", &req, sizeof(req), &resp, sizeof(resp)); + if (ret) + return ret; + dev_dbg(dcp->dev, "peer video timing=%u color=%u success=%u merge=%u\n", + timing, color, resp.success, resp.requires_merge); + if (!(resp.success & 1)) + return -ENOENT; + if (!(resp.requires_merge & 1)) { + if (dcp->peer_tiling.vftg || peer->peer_tiling.vftg) { + ret = dcp_peer_unpair(dcp, peer); + if (ret) + return ret; + } + return -EOPNOTSUPP; + } + + /* An HDMI sink plugged later connects the link again (HPD irq). */ + if (peer->dptxport[0].connected && dcp_peer_hdmi_sinkless(peer)) { + scoped_guard(mutex, &peer->hpd_mutex) + if (dptx_service_available(&peer->dptxport[0])) + dptxport_set_hpd(peer->dptxport[0].service, false); + ret = dcp_dptx_disconnect(peer, 0); + dev_dbg(dcp->dev, "peer HDMI link released: %d\n", ret); + if (ret) + return ret; + } + + ret = dcp_peer_quiesce(dcp, peer); + if (ret) + return ret; + + insert.video = resp.video; + dcp_peer_split_timing(insert.video.timing); + insert.timing_id = cpu_to_le32(timing + 10000); + insert.color_id = cpu_to_le32(color + 10000); + + ret = dcp_peer_prepare_vftg(dcp, peer, &insert); + if (ret) + return ret; + + /* A366: setup_peer_dcp on the peer */ + ret = dcp_peer_call(peer, "A366", &enabled, sizeof(enabled), NULL, 0); + if (ret) + return ret; + + /* A365: insertMergeModes on primary */ + ret = dcp_peer_call(dcp, "A365", &insert, sizeof(insert), + &inserted, sizeof(inserted)); + if (!ret && !(inserted.success & 1)) + ret = -EIO; + if (ret) + return ret; + + /* A365: insertMergeModes on peer with primary's output */ + insert.video = inserted.video; + ret = dcp_peer_call(peer, "A365", &insert, sizeof(insert), + &inserted, sizeof(inserted)); + if (!ret && !(inserted.success & 1)) + ret = -EIO; + if (ret) + return ret; + + dev_dbg(dcp->dev, "peer derived mode timing=%u color=%u width=%u height=%u\n", + timing + 10000, color + 10000, + get_unaligned_le32(insert.video.timing + 8), + get_unaligned_le32(insert.video.timing + 0x28)); + + /* Paired modeset: A473+A360/A361+A413 on both */ + return dcp_peer_modeset(dcp, peer, &insert, resp.requires_merge & 1, + min_vrr); +} + +/* ---- swaps on the peer pipe ---- */ + +static_assert(sizeof(struct dcp_swap_submit_req_v26_6_desktop) == DCP_V26_SWAP_SIZE); + +static const struct dcp_method_entry dcp_peer_swap_start_method = { + .name = "peer_swap_start", .tag = { 'A', '4', '0', '6' }, +}; + +static const struct dcp_method_entry dcp_peer_swap_submit_method = { + .name = "peer_swap_submit", .tag = { 'A', '4', '0', '7' }, +}; + +static const struct dcp_method_entry dcp_peer_set_master_method = { + .name = "peer_set_master_swap_id", .tag = { 'A', '3', '6', '1' }, +}; + +/* + * One swap on a pipe: swap_start, then swap_submit of @wire. The desktop + * 26.6 firmware does not use the IOMFB transaction queue (nothing advances + * it there: m4.134 timed every peer swap out after 10 s), so the two calls + * are chained directly. The firmware's swap ID must equal @swap_id unless + * that is U32_MAX; on a mismatch the peer's master swap ID is set to it + * once and the swap is started again. @owner is the primary of a paired + * DRM frame. + */ +struct dcp_peer_swap { + struct apple_dcp *owner; + u32 swap_id; + __le32 master; + bool retried; + struct dcp_swap_submit_req_v26_6_desktop wire; +}; + +static void dcp_peer_frame_failed(struct apple_dcp *owner) +{ + guard(mutex)(&dcp_peer_lock); + owner->peer_failed = true; +} + +static void dcp_peer_swap_finish(struct apple_dcp *dcp, struct dcp_peer_swap *ps, + int result, u32 swap_id) +{ + if (result) { + dev_err_ratelimited(dcp->dev, "peer swap %u failed: %d\n", swap_id, result); + if (ps->owner) + dcp_peer_frame_failed(ps->owner); + } + kfree(ps); +} + +static void dcp_peer_swap_submitted(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct dcp_peer_swap *ps = cookie; + struct dcp_swap_submit_resp_v26_6_desktop *resp = out; + + dcp_peer_swap_finish(dcp, ps, resp && !resp->ret ? 0 : -EIO, + ps->wire.swap.swap_id); +} + +static void dcp_peer_swap_begin(struct apple_dcp *dcp, struct dcp_peer_swap *ps); + +static void dcp_peer_swap_resynced(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct dcp_peer_swap *ps = cookie; + + dcp_peer_swap_begin(dcp, ps); +} + +static void dcp_peer_swap_started(struct apple_dcp *dcp, void *out, void *cookie) +{ + struct dcp_peer_swap *ps = cookie; + struct dcp_swap_start_resp_v26_6_desktop *resp = out; + + if (!resp || resp->ret) { + dcp_peer_swap_finish(dcp, ps, -EIO, resp ? resp->swap_id : 0); + return; + } + if (ps->swap_id != U32_MAX && resp->swap_id != ps->swap_id) { + if (!ps->retried && !READ_ONCE(dcp->crashed)) { + dev_info_ratelimited(dcp->dev, + "peer swap ID %u, primary %u: setting the master ID\n", + resp->swap_id, ps->swap_id); + ps->retried = true; + ps->master = cpu_to_le32(ps->swap_id); + dcp_push(dcp, false, &dcp_peer_set_master_method, + sizeof(ps->master), 0, &ps->master, + dcp_peer_swap_resynced, ps); + return; + } + dev_err(dcp->dev, "peer swap ID %u, primary %u\n", + resp->swap_id, ps->swap_id); + dcp_peer_swap_finish(dcp, ps, -EPROTO, resp->swap_id); + return; + } + ps->wire.swap.swap_id = resp->swap_id; + dcp_push(dcp, false, &dcp_peer_swap_submit_method, sizeof(ps->wire), + sizeof(struct dcp_swap_submit_resp_v26_6_desktop), &ps->wire, + dcp_peer_swap_submitted, ps); +} + +static void dcp_peer_swap_begin(struct apple_dcp *dcp, struct dcp_peer_swap *ps) +{ + struct dcp_swap_start_req_v26_6_desktop req = { + .client = dcp->desktop->client, + .padding = { 0xaa, 0xaa, 0xaa }, + }; + + dcp_push(dcp, false, &dcp_peer_swap_start_method, sizeof(req), + sizeof(struct dcp_swap_start_resp_v26_6_desktop), &req, + dcp_peer_swap_started, ps); +} + +/* Takes over @ps unless it returns an error. */ +static int dcp_peer_queue_swap(struct apple_dcp *dcp, struct dcp_peer_swap *ps) +{ + if (READ_ONCE(dcp->crashed) || !dcp->active || !dcp->desktop) { + kfree(ps); + return -EIO; + } + dcp_peer_swap_begin(dcp, ps); + return 0; +} + +/* + * The part of a layer inside the tile [@tile_x, @tile_x + @tile_w), in the + * tile's coordinates. The shared source boundary is rounded once, so the + * halves neither lose nor repeat a source column. + */ +static bool dcp_peer_clip(const struct dcp_rect *src, const struct dcp_rect *dst, + u32 tile_x, u32 tile_w, + struct dcp_rect *tile_src, struct dcp_rect *tile_dst) +{ + u64 left = max_t(u64, dst->x, tile_x); + u64 right = min_t(u64, (u64)dst->x + dst->w, (u64)tile_x + tile_w); + u64 first, last; + + if (!src->w || !src->h || !dst->w || !dst->h || left >= right) + return false; + first = div_u64((left - dst->x) * src->w + dst->w / 2, dst->w); + last = div_u64((right - dst->x) * src->w + dst->w / 2, dst->w); + if (first >= last || (u64)src->x + last > U32_MAX) + return false; + *tile_src = *src; + tile_src->x += first; + tile_src->w = last - first; + *tile_dst = *dst; + tile_dst->x = left - tile_x; + tile_dst->w = right - left; + return true; +} + +/* Both pipes scan the same buffers: the display DARTs share one domain. */ +static void dcp_peer_split_swap(struct dcp_swap_submit_req_v26_6_desktop *left, + struct dcp_swap_submit_req_v26_6_desktop *right, + u32 tile_w) +{ + *right = *left; + for (unsigned int i = 0; i < SWAP_SURFACES; i++) { + struct dcp_swap_submit_req_v26_6_desktop *halves[] = { left, right }; + struct dcp_rect src = left->swap.src_rect[i]; + struct dcp_rect dst = left->swap.dst_rect[i]; + bool present = !left->surf_null[i]; + + for (unsigned int side = 0; side < 2; side++) { + struct dcp_swap_submit_req_v26_6_desktop *req = halves[side]; + struct dcp_rect tile_src, tile_dst; + + if (present && dcp_peer_clip(&src, &dst, side * tile_w, tile_w, + &tile_src, &tile_dst)) { + req->swap.src_rect[i] = tile_src; + req->swap.dst_rect[i] = tile_dst; + } else { + req->surf_null[i] = true; + req->surf_iova[i] = 0; + memset(&req->swap.src_rect[i], 0, sizeof(struct dcp_rect)); + memset(&req->swap.dst_rect[i], 0, sizeof(struct dcp_rect)); + } + } + } +} + +/* + * Paired DRM frame: the primary's swap_start returned @swap_id for @req. + * @req keeps the left tile; the right tile goes to the peer as its own swap. + */ +void dcp_peer_swap_submit_26_6(struct apple_dcp *dcp, + struct dcp_swap_submit_req_v26_6_desktop *req, + u32 swap_id) +{ + struct dcp_peer_swap *ps; + struct apple_dcp *peer; + u32 tile_w; + + if (!READ_ONCE(dcp->peer_active)) + return; + scoped_guard(mutex, &dcp_peer_lock) { + peer = dcp->peer_reserved; + if (!dcp->peer_active || dcp->peer_failed || !peer || + peer->peer_owner != dcp || peer->crashed || peer->peer_unbinding || + !dcp->peer_tiling.modeset_complete || + !peer->peer_tiling.modeset_complete || !dcp->peer_tiling.width) + return; + tile_w = dcp->peer_tiling.width; + } + ps = kzalloc(sizeof(*ps), GFP_KERNEL); + if (!ps) { + dcp_peer_frame_failed(dcp); + return; + } + ps->owner = dcp; + ps->swap_id = swap_id; + dcp_peer_split_swap(req, &ps->wire, tile_w); + if (dcp_peer_queue_swap(peer, ps)) { + dev_err_ratelimited(dcp->dev, "peer swap %u not queued\n", swap_id); + dcp_peer_frame_failed(dcp); + } +} + +/* + * A swap completed on a reserved peer. Its own CRTC is idle; the frame is + * the primary's, which reports it from its own completion. + */ +bool dcp_peer_swap_complete_26_6(struct apple_dcp *dcp, u32 swap_id) +{ + /* Only a peer without a display of its own: its swaps are the pair's. */ + if (!READ_ONCE(dcp->peer_owner) || READ_ONCE(dcp->active_typec_route)) + return false; + WRITE_ONCE(dcp->last_swap_id, swap_id); + WRITE_ONCE(dcp->have_swap_complete, true); + if (!dcp->peer_complete_logged) { + dcp->peer_complete_logged = true; + dev_info(dcp->dev, "peer pipe completed swap %u\n", swap_id); + } + return true; +} + +/* + * DRM mode set on a primary with a reserved peer. A mode wider than one + * pipe is set on both pipes through the merge path; -EOPNOTSUPP leaves the + * mode to the single-pipe path, also after a failed paired mode set. + */ +int dcp_peer_drm_modeset_26_6(struct apple_dcp *dcp, u32 hdisplay, + u32 timing, u32 color, u32 min_vrr) +{ + struct apple_dcp *peer; + bool restart = false; + int ret; + + scoped_guard(mutex, &dcp_peer_lock) { + peer = dcp->peer_reserved; + if (!peer || peer->peer_owner != dcp) + return -EOPNOTSUPP; + dcp->peer_active = false; + } + if (hdisplay <= DCP_DESKTOP_PIPE_MAX_WIDTH || timing > U32_MAX - 10000 || + color > U32_MAX - 10000) { + if (dcp->peer_tiling.vftg || peer->peer_tiling.vftg) + dcp_peer_unpair(dcp, peer); + return -EOPNOTSUPP; + } + /* + * DRM sets the same mode again right after the pair came up. The + * merged pipe no longer resolves the original timing in A364, so a + * second install would fail and tear the pair down: keep it. The pair + * runs with the mode's adaptive-sync rate, so VRR switched on or off + * keeps it too; only a changed rate for the same timing (a new mode + * list) stops the pair and installs it again. + */ + scoped_guard(mutex, &dcp_peer_lock) { + if (dcp->peer_tiling.modeset_complete && + peer->peer_tiling.modeset_complete && !dcp->peer_failed && + dcp->peer_mode_timing == timing && dcp->peer_mode_color == color) { + if (dcp->peer_min_vrr == min_vrr) { + dcp->peer_active = true; + dev_info(dcp->dev, + "paired DRM modeset %u px timing %u color %u: kept\n", + hdisplay, timing, color); + return 0; + } + restart = true; + } + } + if (restart) { + ret = dcp_peer_quiesce(dcp, peer); + dev_info(dcp->dev, "paired DRM modeset: adaptive sync %#x -> %#x, pair stopped: %d\n", + dcp->peer_min_vrr, min_vrr, ret); + if (ret) + return -EOPNOTSUPP; + } + ret = dcp_peer_install_video(dcp, peer, timing, color, min_vrr); + dev_info(dcp->dev, "paired DRM modeset %u px timing %u color %u adaptive sync %#x: %d\n", + hdisplay, timing, color, min_vrr, ret); + if (ret) { + if (dcp->peer_tiling.vftg || peer->peer_tiling.vftg) + dcp_peer_unpair(dcp, peer); + return -EOPNOTSUPP; + } + scoped_guard(mutex, &dcp_peer_lock) { + dcp->peer_active = true; + dcp->peer_failed = false; + dcp->peer_mode_timing = timing; + dcp->peer_mode_color = color; + dcp->peer_min_vrr = min_vrr; + peer->peer_complete_logged = false; + } + return 0; +} diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 8a2e2c5bcbf10..28274b11f43f3 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -60,6 +60,7 @@ #include "dcp-external-bw.h" #include "dptx-service.h" #include "iomfb.h" +#include "iomfb_internal.h" #include "iomfb_m4.h" #include "ibootep.h" #include "parser.h" @@ -230,6 +231,9 @@ bool dcp_t6041_ext_enabled(void) return t6041_dcpext; } +#include "iomfb_v26_6_swap.h" +#include "dcp-peer-v26_6.inc" + /* * The backlight maximum of a desktop 26.x panel whose node has no * apple,max-brightness: the J616s panel's user-accessible SDR maximum (ADT @@ -1203,6 +1207,15 @@ int dcp_crtc_atomic_check(struct drm_crtc *crtc, struct drm_atomic_commit *commi crtc_state->vrr_enabled) return -EINVAL; + /* The desktop 26.6 firmware applies adaptive sync with a mode set. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && crtc_state->enable) { + struct drm_crtc_state *old = + drm_atomic_get_old_crtc_state(commit, crtc); + + if (old && old->vrr_enabled != crtc_state->vrr_enabled) + crtc_state->mode_changed = true; + } + /* DRM core only caps the blob size; the gamma update copies a full table. */ if (crtc_state->color_mgmt_changed && crtc_state->gamma_lut && drm_color_lut_size(crtc_state->gamma_lut) != dcp_gamma_lut_size(pdev)) @@ -1342,6 +1355,7 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable { /* T8122 dcpext: HPD only after the link wait, see dcp_ext_dptx_607() */ bool late_hpd = dcp_ext_dptx_607(dcp); + bool compact; unsigned long timeout; u64 generation; u8 dfp_port; @@ -1367,6 +1381,8 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable dcp->connector_type, dcp->dptxport[port].connected); mutex_lock(&dcp->hpd_mutex); + compact = dptx_service_available(&dcp->dptxport[port]) && + READ_ONCE(dcp->dptxport[port].service->ep->compact); if (!dcp_connect_session_valid_locked(dcp, session, cable, recovery)) { ret = -ESTALE; goto out_unlock; @@ -1395,9 +1411,14 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable reinit_completion(&dcp->dptxport[port].linkcfg_completion); WRITE_ONCE(dcp->dptxport[port].tile_hint, false); dcp->dptxport[port].atcphy = dcp->phy; + /* A desktop 26.x route powers its PHY when the display connects. */ + ret = dcp_typec_phy_on(dcp); + if (ret) + goto out_unlock; /* a tiled display's second half comes in on the port's dpin1 */ dfp_port = port && READ_ONCE(dcp->split.active) ? 2 : dcp->dptx_dfp_port; - if (!dcp_ext_dptx_607(dcp)) { + /* Compact DPTX (desktop 26.x) has no connection validation call. */ + if (!dcp_ext_dptx_607(dcp) && !compact) { ret = dptxport_validate_connection(dcp->dptxport[port].service, dfp_port, dcp->dptx_phy, dcp->dptx_die); @@ -1472,7 +1493,12 @@ static int dcp_dptx_connect_raw(struct apple_dcp *dcp, u32 port, bool *retryable /* A revoked wait must not disconnect or publish readiness for a new session. */ if (!dcp_connect_session_valid(dcp, session, cable, recovery)) return -ESTALE; - if (!ret && late_hpd) { + if (!ret && compact) { + /* Without a sink the link is configured once one arrives. */ + dev_dbg(dcp->dev, + "dcp_dptx_connect: no link configuration on port %u yet\n", port); + ret = 0; + } else if (!ret && late_hpd) { /* Without HPD this firmware does not train: assert it now. */ dev_dbg(dcp->dev, "dcp_dptx_connect: no link configuration on port %u yet, asserting HPD\n", @@ -1893,119 +1919,30 @@ static int dcp_dptx_connect_tile(struct apple_dcp *dcp) /* * Desktop 26.x external processors (T6041 dcpext0 HDMI, dcpext1 USB-C). - * They keep the classic external model of the driver this was ported from: - * no Type-C display fabric, the DPTX link set up on HPD (HDMI) or on the - * DP alt mode hotplug of the port (USB-C), and the Type-C port chosen - * dynamically from the PHY that entered DP alt mode. + * The DPTX link is set up on HPD (HDMI) or when the Type-C display fabric + * routes a port's DP alt mode to the processor (USB-C). */ -/* The caller holds hpd_mutex, which also orders dcp->phy against a connect. */ -static int dcp_desktop_dyn_route_select_locked(struct apple_dcp *dcp) -{ - struct dcp_desktop_state *d = dcp->desktop; - typeof(&apple_t6040_phy_dp_alt) dp_alt; - unsigned int i; - int ret; - - lockdep_assert_held(&dcp->hpd_mutex); - if (!d->ndyn || d->dyn_active >= 0) - return 0; - /* Looked up at runtime so appledrm does not require the T6040 PHY driver. */ -#if IS_ENABLED(CONFIG_PHY_APPLE_T6040_USB2) - dp_alt = symbol_get(apple_t6040_phy_dp_alt); -#else - dp_alt = NULL; -#endif - if (!dp_alt) { - dev_dbg(dcp->dev, "T6040 PHY driver not loaded\n"); - return -ENODEV; - } - for (i = 0; i < d->ndyn; i++) - if (dp_alt(d->dyn_phy[i])) - break; -#if IS_ENABLED(CONFIG_PHY_APPLE_T6040_USB2) - symbol_put(apple_t6040_phy_dp_alt); -#endif - if (i == d->ndyn) { - dev_dbg(dcp->dev, "no Type-C port in DP alt mode\n"); - return -ENODEV; - } - ret = mux_control_select(d->dyn_xbar[i], d->dyn_mux_index); - if (ret) { - dev_warn(dcp->dev, "Type-C port %u: crossbar select: %d\n", i, ret); - return ret; - } - ret = phy_init(d->dyn_phy[i]); - if (!ret) { - ret = phy_power_on(d->dyn_phy[i]); - if (ret) - phy_exit(d->dyn_phy[i]); - } - if (ret) { - dev_warn(dcp->dev, "Type-C port %u: DP PHY init/power: %d\n", i, ret); - mux_control_deselect(d->dyn_xbar[i]); - return ret; - } - msleep(25); - dcp->phy = d->dyn_phy[i]; - dcp->xbar = d->dyn_xbar[i]; - dcp->dptx_phy = d->dyn_dptx_phy[i]; - d->dyn_active = i; - dev_dbg(dcp->dev, "Type-C port %u selected (DPTX target PHY %u, crossbar state %u)\n", - i, dcp->dptx_phy, d->dyn_mux_index); - return 0; -} - -static void dcp_desktop_dyn_route_release_locked(struct apple_dcp *dcp) -{ - struct dcp_desktop_state *d = dcp->desktop; - int i = d->dyn_active; - - lockdep_assert_held(&dcp->hpd_mutex); - if (!d->ndyn || i < 0) - return; - d->dyn_active = -1; - dcp->phy = NULL; - dcp->xbar = NULL; - phy_power_off(d->dyn_phy[i]); - phy_exit(d->dyn_phy[i]); - mux_control_deselect(d->dyn_xbar[i]); - dev_dbg(dcp->dev, "Type-C port %u released\n", i); -} - -/* Unbind: a selected route holds its crossbar and its powered PHY. */ -static void dcp_desktop_dyn_route_release(struct apple_dcp *dcp) -{ - if (!dcp->desktop) - return; - guard(mutex)(&dcp->hpd_mutex); - dcp_desktop_dyn_route_release_locked(dcp); -} - static int dcp_desktop_dptx_connect(struct apple_dcp *dcp, u32 port) { struct dptx_port *dptx = &dcp->dptxport[port]; unsigned long left; - bool selected; int ret = 0; mutex_lock(&dcp->hpd_mutex); - /* A route selected here is released again if the connect fails. */ - selected = !dcp->phy && dcp->desktop->ndyn; - if (selected) { - ret = dcp_desktop_dyn_route_select_locked(dcp); - if (ret) - goto out_unlock; - } if (!dcp->phy) { dev_warn(dcp->dev, "dcp_dptx_connect: missing phy\n"); ret = -ENODEV; goto out_unlock; } + /* A fabric route powers its PHY here (a no-op for the other routes). */ + ret = dcp_typec_phy_on(dcp); + if (ret) + goto out_unlock; if (!dptx_service_available(dptx)) { dev_warn(dcp->dev, "dcp_dptx_connect: dptx service for port %u not enabled\n", port); ret = -ENODEV; - goto out_release; + goto out_unlock; } if (dptx->connected) goto out_unlock; @@ -2015,13 +1952,13 @@ static int dcp_desktop_dptx_connect(struct apple_dcp *dcp, u32 port) ret = dptxport_connect(dptx->service, 0, dcp->dptx_phy, dcp->dptx_die, true); if (ret) { dev_warn(dcp->dev, "DPTX port %u: connect failed: %d\n", port, ret); - goto out_release; + goto out_unlock; } ret = dptxport_request_display(dptx->service); if (ret) { dev_warn(dcp->dev, "DPTX port %u: display request failed: %d\n", port, ret); - goto out_release; + goto out_unlock; } dptx->connected = true; @@ -2050,9 +1987,6 @@ static int dcp_desktop_dptx_connect(struct apple_dcp *dcp, u32 port) av_service_connect(dcp); return 0; -out_release: - if (selected) - dcp_desktop_dyn_route_release_locked(dcp); out_unlock: mutex_unlock(&dcp->hpd_mutex); return ret; @@ -2070,32 +2004,64 @@ static int dcp_desktop_dptx_disconnect_oob(struct apple_dcp *dcp, u32 port) dptxport_set_hpd(dcp->dptxport[port].service, false); } ret = dcp_dptx_disconnect(dcp, port); - scoped_guard(mutex, &dcp->hpd_mutex) - dcp_desktop_dyn_route_release_locked(dcp); if (!dcp->main_display) dcp->desktop->ext_pipe_off = true; return ret; } /* - * HDMI HPD of a desktop 26.x external processor. A disconnect is left to - * the firmware, which detects it itself. A cold plug after boot finds the - * port connected without a sink: the firmware never saw this HPD, so pulse - * the AP-asserted HPD to make it export the modes now. + * HDMI HPD of a desktop 26.x external processor. A cold plug after boot + * finds the port connected without a sink: the firmware never saw this HPD, + * so pulse the AP-asserted HPD to make it export the modes now. An unplug + * is reported by nothing else: the DP-to-HDMI converter keeps its DP link + * up without a sink and the firmware sees no change. */ +/* The HDMI display gives its pipeline up: report it gone, release its link. */ +void dcp_hdmi_yield_link(struct apple_dcp *dcp) +{ + int tries = 5, ret; + + disconnected_hpd_event(dcp->connector); + /* A link the firmware keeps retrying is released between attempts. */ + while ((ret = dcp_dptx_disconnect(dcp, 0)) && --tries) + msleep(600); + if (ret) + dev_warn(dcp->dev, "HDMI link not released: %d\n", ret); +} + irqreturn_t dcp_desktop_dp2hdmi_hpd(struct apple_dcp *dcp) { struct dptx_port *dptx = &dcp->dptxport[0]; - bool connected = gpiod_get_value_cansleep(dcp->hdmi_hpd); + bool connected; int ret; - if (connected) { - msleep(500); - connected = gpiod_get_value_cansleep(dcp->hdmi_hpd); - } + msleep(500); + connected = gpiod_get_value_cansleep(dcp->hdmi_hpd); dev_dbg(dcp->dev, "DP2HDMI HPD irq, connected:%d after 500 ms\n", connected); - if (!connected) + if (!connected) { + /* + * A yield outlasts HDMI blinks (a monitor reconfiguring its + * inputs drops HPD on all of them): it ends with the Type-C + * display, not with HPD here. + */ + if (!READ_ONCE(dcp->active_typec_route) && dcp->connector && + READ_ONCE(dcp->connector->connected)) { + dev_info(dcp->dev, "HDMI display unplugged\n"); + disconnected_hpd_event(dcp->connector); + dcp_dptx_disconnect(dcp, 0); + dcp_fabric_hdmi_unplugged(dcp); + } + return IRQ_HANDLED; + } + /* + * The pipeline drives a Type-C display, or is about to: the HDMI + * display waits for it, and gets it back once that one fits one pipe. + */ + if (READ_ONCE(dcp->active_typec_route) || READ_ONCE(dcp->hdmi_yielded)) { + dev_info(dcp->dev, "HDMI display waits: its pipeline drives a Type-C display\n"); + dcp_fabric_hybrid_kick(dcp, 1000); return IRQ_HANDLED; + } mutex_lock(&dcp->hpd_mutex); if (dptx_service_available(dptx) && dptx->connected && @@ -2119,7 +2085,8 @@ int dcp_dptx_connect(struct apple_dcp *dcp, u32 port) struct dcp_fabric_session session = {}; int ret; - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + /* The fixed HDMI output of a desktop 26.x processor has no Type-C session. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !dcp_is_typec_output(dcp)) return dcp_desktop_dptx_connect(dcp, port); if (!dcp_is_external_h17p(dcp)) { @@ -2395,6 +2362,43 @@ static void dcp_placeholder_edid_work(struct work_struct *work) mutex_unlock(&dcp->hpd_mutex); } +#define DCP_LINK_RECOVERY_TRIES 4 +#define DCP_LINK_RECOVERY_DELAY_MS 2000 + +/* + * A retrain after a hotplug found no mode for the active CRTC: the sink's + * EDID was incomplete (it had just woken from sleep and listed only its base + * modes). Nothing re-reads it until the next HPD event, which can be a + * minute away, so a Type-C display stays at a fallback mode. Connect the + * link again, as a replug does, unless a mode set completed in between. + */ +void dcp_link_recovery_schedule(struct apple_dcp *dcp) +{ + if (dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || dcp->main_display || + !dcp_is_typec_output(dcp) || !READ_ONCE(dcp->typec_cable_connected) || + dcp->link_recovery_tries >= DCP_LINK_RECOVERY_TRIES) + return; + dcp->link_recovery_snap = atomic_read(&dcp->modesets_done); + mod_delayed_work(system_freezable_wq, &dcp->link_recovery_wq, + msecs_to_jiffies(DCP_LINK_RECOVERY_DELAY_MS)); +} + +static void dcp_link_recovery_work(struct work_struct *work) +{ + struct apple_dcp *dcp = container_of(to_delayed_work(work), + struct apple_dcp, link_recovery_wq); + struct platform_device *pdev = to_platform_device(dcp->dev); + + if (!READ_ONCE(dcp->typec_cable_connected) || + atomic_read(&dcp->modesets_done) != dcp->link_recovery_snap) + return; + dev_info(dcp->dev, "link recovery %u: connecting again after a retrain that found no mode\n", + ++dcp->link_recovery_tries); + dcp_dptx_disconnect_oob(pdev, 0); + msleep(500); + dcp_dptx_connect_oob(pdev, 0); +} + static void dcp_typec_reconnect_work(struct work_struct *work) { struct apple_dcp *dcp = @@ -2698,6 +2702,8 @@ int dcp_dptx_disconnect(struct apple_dcp *dcp, u32 port) if (!ret) WRITE_ONCE(dcp->dptxport[port].tile_hint, false); mutex_unlock(&dcp->hpd_mutex); + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !port) + dcp_peer_release_26_6(dcp); return ret; } @@ -2706,7 +2712,7 @@ int dcp_dptx_connect_oob(struct platform_device *pdev, u32 port) { struct apple_dcp *dcp = platform_get_drvdata(pdev); - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !dcp_is_typec_output(dcp)) return dcp_desktop_dptx_connect(dcp, port); if (dcp_is_external_h17p(dcp)) { @@ -2749,8 +2755,9 @@ int dcp_dptx_connect_oob(struct platform_device *pdev, u32 port) int dcp_dptx_disconnect_oob(struct platform_device *pdev, u32 port) { struct apple_dcp *dcp = platform_get_drvdata(pdev); + int ret; - if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP) + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !dcp_is_typec_output(dcp)) return dcp_desktop_dptx_disconnect_oob(dcp, port); if (dcp_is_external_h17p(dcp)) @@ -2778,13 +2785,16 @@ int dcp_dptx_disconnect_oob(struct platform_device *pdev, u32 port) av_service_disconnect(dcp); if (dptx_service_available(&dcp->dptxport[port])) { - int ret = dptxport_set_hpd(dcp->dptxport[port].service, false); - + ret = dptxport_set_hpd(dcp->dptxport[port].service, false); if (ret) return ret; } - return dcp_dptx_disconnect(dcp, port); + ret = dcp_dptx_disconnect(dcp, port); + /* The firmware shuts a desktop 26.x external pipe down with its display. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && !dcp->main_display) + dcp->desktop->ext_pipe_off = true; + return ret; } /* @@ -3142,7 +3152,8 @@ static int dcp_start_desktop_external(struct apple_dcp *dcp, u32 map) if (ret) return ret; - if (!dcp->phy && !d->ndyn) { + /* An idle Type-C pipeline has no PHY until a route is claimed. */ + if (!dcp->phy && !dcp->nr_typec_routes) { dev_warn(dcp->dev, "no DP PHY and no Type-C route: no display link\n"); return 0; } @@ -3170,7 +3181,12 @@ static int dcp_start_desktop_external(struct apple_dcp *dcp, u32 map) if (ret) return ret; - if (!dptx_ret && dptx_service_available(&dcp->dptxport[0])) { + /* + * A Type-C route powers the PHY and selects the crossbar when it is + * claimed; until then there is no link to bring up. + */ + if (!dptx_ret && dptx_service_available(&dcp->dptxport[0]) && + (dcp->fixed_phy || dcp->active_typec_route)) { ret = dcp_dptx_connect(dcp, 0); if (ret) dev_info(dcp->dev, "no DPTX link at startup: %d\n", ret); @@ -3479,9 +3495,9 @@ static int dcp_enable_dp2hdmi_hpd(struct apple_dcp *dcp) bool connected = gpiod_get_value_cansleep(dcp->hdmi_hpd); dev_info(dcp->dev, "%s: DP2HDMI HPD connected:%d\n", __func__, connected); - if (connected) + if (connected && !READ_ONCE(dcp->hdmi_yielded)) dcp_dptx_connect(dcp, 0); - else + else if (!connected) _dcp_poweroff(dcp); } @@ -3686,7 +3702,8 @@ void dcp_poweron(struct platform_device *pdev) bool connected = gpiod_get_value_cansleep(dcp->hdmi_hpd); dev_info(dcp->dev, "%s: DP2HDMI HPD connected:%d\n", __func__, connected); - if (connected) + /* A yielded hybrid is about to take a Type-C display. */ + if (connected && !READ_ONCE(dcp->hdmi_yielded)) dcp_dptx_connect(dcp, 0); } @@ -4435,7 +4452,7 @@ static void dcp_release_fixed_route(void *data) struct apple_dcp *dcp = data; if (dcp->fixed_route_selected) { - mux_control_deselect(dcp->xbar); + mux_control_deselect(dcp->fixed_xbar); dcp->fixed_route_selected = false; } typec_mux_put(dcp->typec_mux); @@ -4466,6 +4483,8 @@ VISIBLE_IF_KUNIT void dcp_disable_typec_work(struct apple_dcp *dcp, bool release cancel_delayed_work_sync(&dcp->external_stop_retry_wq); disable_delayed_work_sync(&dcp->hdmi_settle_wq); disable_delayed_work_sync(&dcp->hdmi_recheck_wq); + disable_delayed_work_sync(&dcp->link_recovery_wq); + disable_delayed_work_sync(&dcp->hybrid_wq); dcp_hdmi_disable(dcp); } @@ -4479,6 +4498,8 @@ VISIBLE_IF_KUNIT void dcp_enable_typec_work(struct apple_dcp *dcp) enable_delayed_work(&dcp->announce_wq); enable_delayed_work(&dcp->hdmi_settle_wq); enable_delayed_work(&dcp->hdmi_recheck_wq); + enable_delayed_work(&dcp->link_recovery_wq); + enable_delayed_work(&dcp->hybrid_wq); dcp_hdmi_enable(dcp); scoped_guard(mutex, &dcp->hpd_mutex) dcp->typec_work_enabled = true; @@ -4886,6 +4907,7 @@ static int dcp_comp_bind(struct device *dev, struct device *main, void *data) if (READ_ONCE(dcp->ext_dead)) return 0; + dcp_peer_bound_26_6(dcp); ret = dcp_comp_bind_pipe(dev, main, data); if (ret && dcp->ext_failsafe) { dcp_ext_give_up(dcp, ret, "bind"); @@ -4930,6 +4952,7 @@ VISIBLE_IF_KUNIT void dcp_comp_unbind(struct device *dev, struct device *main, v } disable_work_sync(&dcp->dimensions_wq); + dcp_peer_forget(dcp); if (dcp->fw_compat == DCP_FIRMWARE_V_26_6) { iomfb_v26_6_unbind(dcp); return; @@ -5045,8 +5068,6 @@ VISIBLE_IF_KUNIT void dcp_comp_unbind(struct device *dev, struct device *main, v dcp->systemep = NULL; } - dcp_desktop_dyn_route_release(dcp); - /* * The auxiliary endpoints a desktop 26.x start brought up, still * routed while their shutdown is acknowledged. @@ -5108,8 +5129,9 @@ static int dcp_fixed_output_error(struct apple_dcp *dcp, int err, const char *wh dev_err(dcp->dev, "HDMI output unusable (%s: %d); USB-C displays are not affected\n", what, err); if (dcp->fixed_route_selected) - mux_control_deselect(dcp->xbar); + mux_control_deselect(dcp->fixed_xbar); dcp->fixed_route_selected = false; + dcp->fixed_xbar = NULL; dcp->xbar = NULL; if (!IS_ERR_OR_NULL(dcp->typec_mux)) typec_mux_put(dcp->typec_mux); @@ -5157,62 +5179,31 @@ static void dcp_desktop_phy_off(void *data) } /* - * The outputs of a desktop 26.x external processor (T6041 dcpext): the - * fixed HDMI route's DP PHY is powered here, as the boot loader's working - * sequence does; a dynamic Type-C route lists the DP PHY and crossbar of - * every port (dp-phyN, dp-xbarN, apple,dptx-phys, apple,dptx-mux-index), - * and the one in DP alt mode is claimed at connect. + * The fixed HDMI output of a desktop 26.x external processor (T6041 dcpext0): + * its DP PHY is powered here, as the boot loader's working sequence does. A + * Type-C output (dcpext1) has no fixed PHY; the display fabric powers the + * PHY of the route a port claims (see dcp_typec_phy_on()). */ static int dcp_desktop_probe_routes(struct apple_dcp *dcp) { - struct dcp_desktop_state *d = dcp->desktop; struct device *dev = dcp->dev; - unsigned int i; - int nd, ret; + int ret; - d->dyn_active = -1; - if (!dcp_desktop_external(dcp)) + if (!dcp_desktop_external(dcp) || !dcp->phy) return 0; - if (dcp->phy) { - ret = phy_init(dcp->phy); - if (ret) - return dev_err_probe(dev, ret, "Failed to initialize dp-phy\n"); - ret = phy_power_on(dcp->phy); - if (ret) { - phy_exit(dcp->phy); - return dev_err_probe(dev, ret, "Failed to power on dp-phy\n"); - } - ret = devm_add_action_or_reset(dev, dcp_desktop_phy_off, dcp->phy); - if (ret) - return ret; - msleep(25); + ret = phy_init(dcp->phy); + if (ret) + return dev_err_probe(dev, ret, "Failed to initialize dp-phy\n"); + ret = phy_power_on(dcp->phy); + if (ret) { + phy_exit(dcp->phy); + return dev_err_probe(dev, ret, "Failed to power on dp-phy\n"); } - - nd = of_property_count_u32_elems(dev->of_node, "apple,dptx-phys"); - if (nd <= 0) - return 0; - if (nd > ARRAY_SIZE(d->dyn_phy)) - return dev_err_probe(dev, -EINVAL, "too many Type-C routes: %d\n", nd); - - for (i = 0; i < nd; i++) { - char name[16]; - - snprintf(name, sizeof(name), "dp-phy%u", i); - d->dyn_phy[i] = devm_phy_get(dev, name); - if (IS_ERR(d->dyn_phy[i])) - return dev_err_probe(dev, PTR_ERR(d->dyn_phy[i]), "%s\n", name); - snprintf(name, sizeof(name), "dp-xbar%u", i); - d->dyn_xbar[i] = devm_mux_control_get(dev, name); - if (IS_ERR(d->dyn_xbar[i])) - return dev_err_probe(dev, PTR_ERR(d->dyn_xbar[i]), "%s\n", name); - of_property_read_u32_index(dev->of_node, "apple,dptx-phys", i, - &d->dyn_dptx_phy[i]); - } - of_property_read_u32(dev->of_node, "apple,dptx-mux-index", &d->dyn_mux_index); - d->ndyn = nd; - dev_info(dev, "%u Type-C DP routes, crossbar state %u\n", d->ndyn, - d->dyn_mux_index); + ret = devm_add_action_or_reset(dev, dcp_desktop_phy_off, dcp->phy); + if (ret) + return ret; + msleep(25); return 0; } @@ -5332,12 +5323,16 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) dcp->fixed_connector_type = dcp_connector_type_from_dt(dev->of_node); dcp->connector_type = dcp->fixed_connector_type; of_property_read_u32(dev->of_node, "apple,dcp-index", &dcp->index); + of_property_read_u32(dev->of_node, "apple,dcp-endpoint", &dcp->peer_endpoint); + if (of_property_read_bool(dev->of_node, "apple,typec-hybrid-first")) + dcp_typec_hybrid_first_dt(); of_property_read_u32(dev->of_node, "apple,dptx-phy", &dcp->dptx_phy); of_property_read_u32(dev->of_node, "apple,dptx-die", &dcp->dptx_die); dcp->fixed_dptx_phy = dcp->dptx_phy; init_completion(&dcp->typec_iomfb_hpd_ready); INIT_DELAYED_WORK(&dcp->typec_reconnect_wq, dcp_typec_reconnect_work); + INIT_DELAYED_WORK(&dcp->link_recovery_wq, dcp_link_recovery_work); INIT_DELAYED_WORK(&dcp->placeholder_edid_wq, dcp_placeholder_edid_work); INIT_DELAYED_WORK(&dcp->typec_relink_wq, dcp_typec_relink_work); @@ -5355,6 +5350,7 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) disable_delayed_work(&dcp->placeholder_edid_wq); disable_delayed_work(&dcp->typec_relink_wq); disable_delayed_work(&dcp->announce_wq); + disable_delayed_work(&dcp->link_recovery_wq); dcp->announce.stopped = true; /* * A native external pipe's bind enables these; firmware callbacks may @@ -5547,7 +5543,8 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) ret = devm_add_action_or_reset(dev, dcp_release_fixed_route, dcp); if (ret) return ret; - ret = mux_control_select(dcp->xbar, mux_index); + dcp->fixed_xbar = dcp->xbar; + ret = mux_control_select(dcp->fixed_xbar, mux_index); if (ret) dev_warn(dev, "mux_control_select failed: %d\n", ret); else @@ -6084,6 +6081,7 @@ static int dcp_platform_resume(struct device *dev) if (READ_ONCE(dcp->ext_dead)) return 0; + dcp->link_recovery_tries = 0; dcp_release_after_sleep(dcp); dcp_enable_typec_work(dcp); /* The HDMI output's PHY and crossbar may have lost their setup in sleep. */ diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index 30b728820e2c7..9ad01a427f6a2 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -143,4 +143,29 @@ unsigned int apple_j613_25g83_clock_hz(void); /* A J613, or a J615 whose boot loader carries the experimental opt-in. */ bool apple_t8122_25g83_board(void); +void dcp_typec_hybrid_first_dt(void); +void dcp_fabric_hdmi_unplugged(struct apple_dcp *dcp); +void dcp_fabric_hybrid_kick(struct apple_dcp *dcp, unsigned int delay_ms); +void dcp_hdmi_yield_link(struct apple_dcp *dcp); +void dcp_link_recovery_schedule(struct apple_dcp *dcp); + +/* Desktop 26.6 dual-pipe peer (dcp-peer-v26_6.inc, included from dcp.c) */ +struct dptx_port; +int dcp_peer_hints_26_6(struct apple_dcp *dcp, struct dptx_port *port, + const void *data, size_t size, + void *reply, size_t reply_size); +bool dcp_peer_tiling_state_26_6(struct apple_dcp *dcp, bool write, + u32 event, u32 param, u32 *value); +void dcp_peer_display_ids(struct apple_dcp *dcp, u32 own, u32 related); +struct dcp_swap_submit_req_v26_6_desktop; +int dcp_peer_drm_modeset_26_6(struct apple_dcp *dcp, u32 hdisplay, + u32 timing, u32 color, u32 min_vrr); +void dcp_peer_swap_submit_26_6(struct apple_dcp *dcp, + struct dcp_swap_submit_req_v26_6_desktop *req, + u32 swap_id); +bool dcp_peer_swap_complete_26_6(struct apple_dcp *dcp, u32 swap_id); +int dcp_peer_stop_26_6(struct apple_dcp *dcp); +void dcp_peer_release_26_6(struct apple_dcp *dcp); +void dcp_peer_bound_26_6(struct apple_dcp *dcp); + #endif diff --git a/drivers/gpu/drm/apple/dcp_external_announce_test.c b/drivers/gpu/drm/apple/dcp_external_announce_test.c index bd0af843698f9..fcb9a7125b51b 100644 --- a/drivers/gpu/drm/apple/dcp_external_announce_test.c +++ b/drivers/gpu/drm/apple/dcp_external_announce_test.c @@ -111,6 +111,8 @@ static struct announce_test *announce_test_init(struct kunit *test) INIT_DELAYED_WORK(&dcp->typec_relink_wq, announce_test_noop); INIT_DELAYED_WORK(&dcp->hdmi_settle_wq, announce_test_noop); INIT_DELAYED_WORK(&dcp->hdmi_recheck_wq, announce_test_noop); + INIT_DELAYED_WORK(&dcp->link_recovery_wq, announce_test_noop); + INIT_DELAYED_WORK(&dcp->hybrid_wq, announce_test_noop); INIT_DELAYED_WORK(&dcp->bl_fallback_wq, announce_test_noop); INIT_DELAYED_WORK(&dcp->backlight.fade_work, announce_test_noop); INIT_WORK(&dcp->external_stop_work, announce_test_noop); diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index a7620457f1f46..66196c8c5cc61 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -490,11 +490,12 @@ static int dptxport_call_get_link_rate(struct apple_epic_service *service, static int dptxport_compact_xbar_link(struct apple_epic_service *service, bool up) { struct apple_dcp *dcp = service->ep->dcp; + struct mux_control *xbar = READ_ONCE(dcp->xbar); int ret; - if (!READ_ONCE(service->ep->compact) || !dcp->xbar) + if (!READ_ONCE(service->ep->compact) || !xbar) return 0; - ret = dcp_dpxbar_link(dcp->xbar, up); + ret = dcp_dpxbar_link(xbar, up); dev_dbg(dcp->dev, "DPTXPort: crossbar link %s: %d\n", up ? "up" : "down", ret); return ret; } @@ -693,6 +694,27 @@ static int dptxport_call_set_tiled_display_hint(struct apple_epic_service *servi return 0; } +/* + * Desktop 26.6 display allocation: the 160-byte SET_TILED_DISPLAY_HINTS + * record carries the sink's size and pixel rates and takes the main and + * peer endpoint IDs back (dcp_peer_hints_26_6()). A sink wider than one + * pipe gets the idle pipe above this one as its peer. + */ +static bool dptxport_dual_pipe_alloc(struct apple_epic_service *service, + const void *data, size_t data_size, + void *reply, size_t reply_size) +{ + struct apple_dcp *dcp = service->ep->dcp; + struct dptx_port *dptx = service->cookie; + + if (data_size != 160 || reply_size < 160 || + dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP) + return false; + + return dcp_peer_hints_26_6(dcp, dptx, data, data_size, + reply, reply_size) == 0; +} + static int dptxport_call_activate(struct apple_epic_service *service, const void *data, size_t data_size, @@ -878,6 +900,9 @@ static int dptxport_call(struct apple_epic_service *service, u32 idx, case DPTX_APCALL_SET_TILED_DISPLAY_HINTS: { int ret; + if (dptxport_dual_pipe_alloc(service, data, data_size, reply, + reply_size)) + return 0; memcpy(reply, data, min(reply_size, data_size)); ret = dptxport_call_set_tiled_display_hint(service, data, data_size, reply, diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index c0bef660c25ec..7daa5ac848a5c 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -90,17 +90,6 @@ static inline u64 dcpep_ack(enum dcp_context_id id) return dcpep_msg(id, 0, 0) | IOMFB_MSG_ACK; } -/* - * Adaptive sync through IOMFBParameter 14 black-screens the J616s panel on - * the desktop 26.6 firmware: the firmware accepts the VRR mode, then no swap - * completes. Do not offer it there. - */ -static bool dcp_mode_vrr_supported(struct apple_dcp *dcp, - const struct dcp_display_mode *mode) -{ - return dcp->fw_compat != DCP_FIRMWARE_V_26_6_DESKTOP || !mode->vrr; -} - /* * A channel is busy if we have sent a message that has yet to be * acked. The driver must not sent a message to a busy channel. @@ -1232,10 +1221,12 @@ void dcp_hotplug(struct work_struct *work) * callback has completed. */ ret = dcp_retrain_active_crtc(connector); - if (ret) + if (ret) { dev_warn(dcp->dev, "failed to retrain active CRTC after hotplug: %d\n", ret); + dcp_link_recovery_schedule(dcp); + } } dcp_test_point(dcp, DCP_TEST_HOTPLUG_EVENT); @@ -1284,11 +1275,27 @@ static void dcpep_handle_cb(struct apple_dcp *dcp, enum dcp_context_id context, return; } + /* The header is read below: check the record lies in the shared buffer first. */ + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && + (length < sizeof(*hdr) || offset >= 0x8000 || length > 0x8000 - offset)) { + dev_err(dev, "invalid IOMFB callback envelope\n"); + dcp->crashed = true; + return; + } + tag = dcp_parse_tag(hdr->tag); handled = tag >= 0 && tag < IOMFB_MAX_CB && dcp->cb_handlers && dcp->cb_handlers[tag]; + if (dcp->fw_compat == DCP_FIRMWARE_V_26_6_DESKTOP && handled && + ((u64)sizeof(*hdr) + hdr->in_len + hdr->out_len > length || + !iomfb_check_callback_v26_6_desktop(tag, hdr->in_len, hdr->out_len))) { + dev_err(dev, "short IOMFB callback %d (%u, %u, length %u)\n", + tag, hdr->in_len, hdr->out_len, length); + dcp->crashed = true; + return; + } if (dcp->fw_compat == DCP_FIRMWARE_H17P && (tag < 0 || tag >= IOMFB_MAX_CB || (u64)sizeof(*hdr) + hdr->in_len + hdr->out_len != length || @@ -1947,8 +1954,6 @@ int dcp_get_modes(struct drm_connector *connector) continue; published_180hz |= modes[i].automatic_rgb8_quirk; published++; - if (!dcp_mode_vrr_supported(dcp, &modes[i])) - continue; if (modes[i].vrr) { u16 lo = modes[i].min_vrr >> 16; u16 hi = modes[i].max_vrr >> 16; diff --git a/drivers/gpu/drm/apple/iomfb.h b/drivers/gpu/drm/apple/iomfb.h index 7e6e16f1dddf7..7c60a81207ad7 100644 --- a/drivers/gpu/drm/apple/iomfb.h +++ b/drivers/gpu/drm/apple/iomfb.h @@ -212,6 +212,8 @@ bool iomfb_validate_callback_h17p(int tag, u32 in_len, u32 out_len); struct apple_dcp; bool iomfb_check_callback_h17p(struct apple_dcp *dcp, int tag, u32 in_len, u32 out_len); +/* Desktop 26.6: does the record hold what the tag's handler reads and writes? */ +bool iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len); typedef bool (*iomfb_cb_handler)(struct apple_dcp *, int, void *, void *); diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 246498a8e843c..bdb78c999eab2 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -425,6 +425,10 @@ static void dcpep_cb_swap_complete(struct apple_dcp *dcp, ktime_t now = ktime_get(); bool retire = true; +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + if (dcp_peer_swap_complete_26_6(dcp, resp->swap_id)) + return; +#endif #if DCP_FW_H17(DCP_FW_VER) if (dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G && !dcp_present_complete_h17p(&dcp->present_state_h17p, resp->swap_id)) { @@ -461,6 +465,7 @@ static void dcpep_cb_swap_complete(struct apple_dcp *dcp, #endif trace_iomfb_swap_complete(dcp, resp->swap_id); dcp->last_swap_id = resp->swap_id; + dcp->have_swap_complete = true; dcp_swap_watchdog_complete(dcp); if (retire) { dcp_release_retained_framebuffers(dcp, resp->swap_id); @@ -2141,6 +2146,15 @@ void DCP_FW_NAME(iomfb_poweroff)(struct apple_dcp *dcp) return; #endif +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + /* A merged pair stops as a pair; its clear swap would not complete. */ + ret = dcp_peer_stop_26_6(dcp); + if (ret < 0) + dev_err(dcp->dev, "paired power-off failed: %d\n", ret); + if (ret > 0) + goto peer_stopped; +#endif + cookie = kzalloc(sizeof(*cookie), GFP_KERNEL); if (!cookie) return; @@ -2177,6 +2191,9 @@ void DCP_FW_NAME(iomfb_poweroff)(struct apple_dcp *dcp) dev_dbg(dcp->dev, "%s: clear swap submitted: %u\n", __func__, swap_id); +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +peer_stopped: +#endif poff_cookie = kzalloc(sizeof(*poff_cookie), GFP_KERNEL); if (!poff_cookie) return; @@ -2269,6 +2286,12 @@ void DCP_FW_NAME(iomfb_sleep)(struct apple_dcp *dcp) return; #endif +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + ret = dcp_peer_stop_26_6(dcp); + if (ret < 0) + dev_err(dcp->dev, "paired sleep failed: %d\n", ret); +#endif + cookie = kzalloc(sizeof(*cookie), GFP_KERNEL); if (!cookie) return; @@ -2385,6 +2408,24 @@ dcpep_cb_hotplug_v26_6(struct apple_dcp *dcp, struct dcp_hotplug_req_v26_6 *req) { struct dcp_hotplug_resp_v26_6 resp = { 0 }; u64 connected = req->connected; + const u8 *tiled = req->opaque; + bool tiled_valid = !(req->opaque[sizeof(resp.opaque)] & 1); + bool paired = READ_ONCE(dcp->peer_reserved) || READ_ONCE(dcp->peer_owner); + + /* + * With a peer reserved, the record names this pipe's and the related + * pipe's display at 0x3b and 0x47, as on firmware 27; it is echoed + * back as that firmware's client does. + */ + if (paired) { + dev_dbg(dcp->dev, "peer HPD connected=%llu null=%u rec=%*ph\n", + connected, req->opaque[sizeof(resp.opaque)], + (int)sizeof(resp.opaque), tiled); + if (tiled_valid && connected && + !(get_unaligned_le32(tiled + 0xc) & BIT(3))) + dcp_peer_display_ids(dcp, get_unaligned_le32(tiled + 0x3b), + get_unaligned_le32(tiled + 0x47)); + } /* A display that went away is bootstrapped again on its next mode. */ if (!dcp->main_display && @@ -2397,7 +2438,9 @@ dcpep_cb_hotplug_v26_6(struct apple_dcp *dcp, struct dcp_hotplug_req_v26_6 *req) dcpep_cb_hotplug(dcp, &connected); - if (connected) + if (paired && tiled_valid) + memcpy(resp.opaque, tiled, sizeof(resp.opaque)); + else if (connected) memset(&resp.opaque[0x47], 0xff, sizeof(u32)); return resp; @@ -2455,12 +2498,42 @@ static struct dcpep_get_tiling_state_resp dcpep_cb_get_tiling_state(struct apple_dcp *dcp, struct dcpep_get_tiling_state_req *req) { +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + /* D115 reads the tiling state of a paired pipe; D116 writes it. */ + u32 value = req->value; + bool handled = dcp_peer_tiling_state_26_6(dcp, false, req->event, + req->param, &value); + + dev_dbg(dcp->dev, "peer tiling get event %u param %u value %u handled %d\n", + req->event, req->param, value, handled); + if (handled) + return (struct dcpep_get_tiling_state_resp){ + .value = value, + .ret = 1, + }; +#endif return (struct dcpep_get_tiling_state_resp){ .value = 0, .ret = 1, }; } +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +/* D116 set_tiling_state: the D115 record in, a boolean (padded to 4) out. */ +static u32 dcpep_cb_set_tiling_state_v26_6(struct apple_dcp *dcp, + struct dcpep_get_tiling_state_req *req) +{ + u32 value = req->value; + bool handled = !req->value_null && + dcp_peer_tiling_state_26_6(dcp, true, req->event, + req->param, &value); + + dev_dbg(dcp->dev, "peer tiling set event %u param %u value %u null %u handled %d\n", + req->event, req->param, req->value, req->value_null, handled); + return handled; +} +#endif + static u8 dcpep_cb_create_backlight_service(struct apple_dcp *dcp) { #if DCP_FW_H17(DCP_FW_VER) @@ -2548,6 +2621,10 @@ TRAMPOLINE_IN(trampoline_pr_publish, iomfb_cb_pr_publish, struct iomfb_property); TRAMPOLINE_INOUT(trampoline_get_tiling_state, dcpep_cb_get_tiling_state, struct dcpep_get_tiling_state_req, struct dcpep_get_tiling_state_resp); +#if DCP_FW_DESKTOP_26(DCP_FW_VER) +TRAMPOLINE_INOUT(trampoline_set_tiling_state_v26_6, dcpep_cb_set_tiling_state_v26_6, + struct dcpep_get_tiling_state_req, u32); +#endif TRAMPOLINE_OUT(trampoline_create_backlight_service, dcpep_cb_create_backlight_service, u8); /* @@ -2603,6 +2680,9 @@ static void dcp_swap_started(struct apple_dcp *dcp, void *data, void *cookie) trace_iomfb_swap_submit(dcp, resp->swap_id); dcp_h17p_prepare_swap(dcp); +#if DCP_FW_DESKTOP_26(DCP_FW_VER) + dcp_peer_swap_submit_26_6(dcp, &DCP_FW_SWAP(dcp), resp->swap_id); +#endif dcp_swap_submit(dcp, false, &DCP_FW_SWAP(dcp), dcp_swapped, NULL); } @@ -2927,11 +3007,41 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, .timing_mode_id = mode->timing_mode_id }; + /* VRR modes need swap timestamps, see iomfb_flush(). */ + dcp->use_timestamps = mode->vrr; + + /* A display within one pipe may let the other input have a pipe again. */ + { + bool narrow = crtc_state->mode.hdisplay <= DCP_DESKTOP_PIPE_MAX_WIDTH; + + WRITE_ONCE(dcp->typec_narrow, narrow); + if (narrow) + dcp_fabric_hybrid_kick(dcp, 1000); + } + /* - * Suspected VRR modes need swap timestamps, see iomfb_flush(). - * Adaptive sync itself stays off, see dcp_mode_vrr_supported(). + * A mode wider than one pipe goes to both pipes of a reserved pair, + * with adaptive sync set the same on both. The pair takes the mode's + * VRR range from its first install, whatever the CRTC asks: switching + * VRR on later would stop and install the pair again, and an install + * that fails (the sink dropped its link meanwhile) leaves the display + * dark. Swaps keep their timestamps, so the compositor still paces it. */ - dcp->use_timestamps = mode->vrr; + if (!dcp->main_display) { + dev_dbg(dcp->dev, "modeset %ux%u vrr %u min %#x enabled %u\n", + crtc_state->mode.hdisplay, crtc_state->mode.vdisplay, + mode->vrr, mode->min_vrr, crtc_state->vrr_enabled); + ret = dcp_peer_drm_modeset_26_6(dcp, crtc_state->mode.hdisplay, + mode->timing_mode_id, + mode->color_mode_id, + mode->vrr ? mode->min_vrr : 0); + if (ret != -EOPNOTSUPP) { + dcp->desktop->mode_live = !ret; + if (!ret) + atomic_inc(&dcp->modesets_done); + return ret; + } + } /* * The firmware changes a fixed panel's mode through the swap geometry, @@ -2971,7 +3081,23 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, modeset->bootstrap.timing_mode_id); } - dcp_modeset_select(dcp, NULL, cookie); + /* + * Adaptive sync (IOMFBParameter 14, minimum rate in 16.16 Hz, 0 = off) + * goes before the mode is selected; the firmware then reports the mode + * set with VRR on. + */ + if (mode->vrr) { + struct dcp_set_parameter_dcp param = { + .param = IOMFBPARAM_ADAPTIVE_SYNC, + .value = { crtc_state->vrr_enabled ? mode->min_vrr : 0 }, + .count = 3, + }; + + dcp_set_parameter_dcp(dcp, false, ¶m, dcp_modeset_select, + cookie); + } else { + dcp_modeset_select(dcp, NULL, cookie); + } /* * The DCP firmware has an internal timeout of ~8 seconds for @@ -3004,6 +3130,8 @@ int DCP_FW_NAME(iomfb_modeset)(struct apple_dcp *dcp, kref_put(&cookie->refcount, release_wait_cookie); dcp->desktop->mode_live = !ret; + if (!ret) + atomic_inc(&dcp->modesets_done); return ret; } #else diff --git a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c index 05417d2c2b2cb..b5e71329eb9ce 100644 --- a/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c +++ b/drivers/gpu/drm/apple/iomfb_v26_6_desktop.c @@ -153,6 +153,7 @@ static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { [114] = trampoline_passthrough_dict_v26_6, /* set_tiling_state: the value followed by success */ [115] = trampoline_get_tiling_state, + [116] = trampoline_set_tiling_state_v26_6, [121] = dcpep_cb_boot_1, /* start_hardware_boot */ [122] = trampoline_false, /* is_dark_boot / is_waking_from_hibernate */ [123] = trampoline_false, @@ -200,6 +201,98 @@ static const iomfb_cb_handler cb_handlers[IOMFB_MAX_CB] = { [599] = trampoline_nop, }; +/* + * What each handler above reads from the request and writes to the reply. + * The firmware may send longer records; a shorter one is refused. + */ +struct dcp_cb_min_v26_6_desktop { + u32 in_len; + u32 out_len; + bool valid; +}; + +#define CB_MIN(in, out) { .in_len = (in), .out_len = (out), .valid = true } + +static const struct dcp_cb_min_v26_6_desktop cb_min[IOMFB_MAX_CB] = { + [0] = CB_MIN(0, sizeof(u8)), + [1] = CB_MIN(0, sizeof(u8)), + [2] = CB_MIN(0, 0), + [3] = CB_MIN(0, sizeof(struct dcp_rt_bandwidth)), + [6] = CB_MIN(sizeof(struct dcp_set_frame_sync_props_req_v26_6_desktop), + sizeof(struct dcp_set_frame_sync_props_resp_v26_6_desktop)), + [100] = CB_MIN(0, 0), + [101] = CB_MIN(0, sizeof(u32)), + [102] = CB_MIN(0, 0), + [104] = CB_MIN(0, 0), + [107] = CB_MIN(0, 0), + [108] = CB_MIN(0, sizeof(u8)), + [109] = CB_MIN(0, sizeof(u8)), + [110] = CB_MIN(0, sizeof(u8)), + [111] = CB_MIN(0, sizeof(u8)), + [112] = CB_MIN(0, sizeof(u8)), + [113] = CB_MIN(0, sizeof(u8)), + [114] = CB_MIN(sizeof(struct dcp_dict_passthrough_req_v26_6), + sizeof(struct dcp_dict_passthrough_resp_v26_6)), + [115] = CB_MIN(sizeof(struct dcpep_get_tiling_state_req), + sizeof(struct dcpep_get_tiling_state_resp)), + [116] = CB_MIN(sizeof(struct dcpep_get_tiling_state_req), sizeof(u32)), + [121] = CB_MIN(0, 0), + [122] = CB_MIN(0, sizeof(u8)), + [123] = CB_MIN(0, sizeof(u8)), + [125] = CB_MIN(sizeof(struct dcp_read_edt_data_req), + sizeof(dcp_read_edt_data_resp_t)), + [127] = CB_MIN(sizeof(u32), sizeof(u8)), + [128] = CB_MIN(sizeof(struct dcp_set_dcpav_prop_chunk_req), sizeof(u8)), + [129] = CB_MIN(sizeof(struct dcp_set_dcpav_prop_end_req), sizeof(u8)), + [134] = CB_MIN(0, sizeof(u32)), + [201] = CB_MIN(sizeof(struct dcp_map_buf_req), + sizeof(struct DCP_FW_NAME(dcp_map_buf_resp))), + [206] = CB_MIN(0, sizeof(u8)), + [207] = CB_MIN(0, sizeof(u8)), + [208] = CB_MIN(0, 0), + [209] = CB_MIN(0, sizeof(u64)), + [300] = CB_MIN(sizeof(struct iomfb_property), 0), + [400] = CB_MIN(0, 0), + [401] = CB_MIN(sizeof(struct dcp_get_uint_prop_req), + sizeof(struct dcp_get_uint_prop_resp)), + [406] = CB_MIN(0, 0), + [408] = CB_MIN(0, sizeof(u64)), + [411] = CB_MIN(sizeof(struct DCP_FW_NAME(dcp_map_reg_req)), + sizeof(struct DCP_FW_NAME(dcp_map_reg_resp))), + [413] = CB_MIN(0, sizeof(u8)), + [414] = CB_MIN(sizeof(struct iomfb_sr_set_property_int_req), sizeof(u8)), + [415] = CB_MIN(0, sizeof(u8)), + [451] = CB_MIN(sizeof(struct dcp_allocate_buffer_req), + sizeof(struct dcp_allocate_buffer_resp)), + [552] = CB_MIN(0, sizeof(u8)), + [561] = CB_MIN(0, sizeof(u8)), + [563] = CB_MIN(0, sizeof(u8)), + [565] = CB_MIN(0, sizeof(u8)), + [567] = CB_MIN(0, sizeof(u8)), + [572] = CB_MIN(0, sizeof(u32)), + [575] = CB_MIN(sizeof(struct dcp_hotplug_req_v26_6), + sizeof(struct dcp_hotplug_resp_v26_6)), + [576] = CB_MIN(0, 0), + [581] = CB_MIN(0, 0), + [582] = CB_MIN(0, sizeof(u8)), + [590] = CB_MIN(sizeof(struct DCP_FW_NAME(dc_swap_complete_resp)), 0), + [592] = CB_MIN(sizeof(dcp_swap_intent_t), 0), + [593] = CB_MIN(sizeof(u32), 0), + [594] = CB_MIN(sizeof(u8), 0), + [595] = CB_MIN(0, 0), + [598] = CB_MIN(0, 0), + [599] = CB_MIN(0, 0), +}; + +#undef CB_MIN + +bool iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len) +{ + if (tag < 0 || tag >= IOMFB_MAX_CB || !cb_min[tag].valid) + return false; + return in_len >= cb_min[tag].in_len && out_len >= cb_min[tag].out_len; +} + void DCP_FW_NAME(iomfb_start)(struct apple_dcp *dcp) { dcp->cb_handlers = cb_handlers; diff --git a/drivers/gpu/drm/asahi/fwid.rs b/drivers/gpu/drm/asahi/fwid.rs index bb1fd3c36f623..be81eac5dfb74 100644 --- a/drivers/gpu/drm/asahi/fwid.rs +++ b/drivers/gpu/drm/asahi/fwid.rs @@ -213,7 +213,7 @@ mod tests { } #[test] - fn t6040_accepts_both_loaded_images() { + fn t6040_accepts_all_loaded_images() { let crate::g17::fw::variant::Firmware::Identified(builds) = crate::hw::t6040::CONFIG.firmware else { @@ -231,6 +231,11 @@ mod tests { 0x31, 0xc3, 0xb2, 0x8d, 0x05, 0x09, 0xa1, 0x7e, 0xb7, 0x61, 0x08, 0xb7, 0xc9, 0xa5, 0x2a, 0xcc, 0xd9, 0xac, ], + [ + 0xd7, 0x25, 0x26, 0x4d, 0x48, 0x31, 0xb9, 0x5a, 0x5b, 0x8a, 0x37, 0x5a, 0xbf, 0x2f, + 0xb5, 0xcd, 0xfb, 0xd6, 0xd7, 0xcb, 0xc7, 0xf1, 0xba, 0xa2, 0xcc, 0x8b, 0x37, 0xee, + 0x5d, 0x6e, 0x5e, 0x98, + ], ]; for (i, digest) in digests.into_iter().enumerate() { let found = measured(build.images, 0x138000, [0x59839, 0x5991c], digest).unwrap(); diff --git a/drivers/gpu/drm/asahi/hw/t6040.rs b/drivers/gpu/drm/asahi/hw/t6040.rs index 0007fdf132949..c6ea928630e56 100644 --- a/drivers/gpu/drm/asahi/hw/t6040.rs +++ b/drivers/gpu/drm/asahi/hw/t6040.rs @@ -85,6 +85,17 @@ pub(crate) const CONFIG: Config = Config { 0xc9, 0xa5, 0x2a, 0xcc, 0xd9, 0xac, ], }, + crate::fwid::FirmwareBuild { + name: "Apple GFX 25G83 for T6040 (normalized loaded image, second alternate J616s fill)", + text_size: 0x60000, + data_size: 0x138000, + entropy: [0x59839, 0x5991c], + text_sha256: [ + 0xd7, 0x25, 0x26, 0x4d, 0x48, 0x31, 0xb9, 0x5a, 0x5b, 0x8a, 0x37, 0x5a, 0xbf, + 0x2f, 0xb5, 0xcd, 0xfb, 0xd6, 0xd7, 0xcb, 0xc7, 0xf1, 0xba, 0xa2, 0xcc, 0x8b, + 0x37, 0xee, 0x5d, 0x6e, 0x5e, 0x98, + ], + }, ], variant: &fw::variant::V25G83, }]), diff --git a/drivers/phy/apple/t6040-usb2.c b/drivers/phy/apple/t6040-usb2.c index 868d4304b2395..20fb5e9da5cb0 100644 --- a/drivers/phy/apple/t6040-usb2.c +++ b/drivers/phy/apple/t6040-usb2.c @@ -237,6 +237,9 @@ struct t6040_usb2_phy { struct completion app_reply; enum typec_orientation orientation; bool usb3_up; /* tuned by the coprocessor this power cycle */ + bool usb3_on; /* dwc3 holds the USB3 phy powered on */ + bool usb3_pipe; /* dwc3 asked for the PIPE switch this power cycle */ + bool usb3_lent; /* DP mode took the lanes of this Type-C port */ bool pipe_switched; /* the controller's PIPE runs on this PHY */ /* DisplayPort (PHY mode 3); dp_ok is false when the DT lacks a DP route */ @@ -882,15 +885,19 @@ static void t6040_usb3_pipe_park(struct t6040_usb2_phy *tphy) * its PIPE on the dummy PHY. A failure here leaves the port at USB2, so it is * logged and not returned: the USB2 PHY and the core must still come up. */ -static int t6040_usb3_power_on(struct phy *phy) +static void t6040_usb3_up_locked(struct t6040_usb2_phy *tphy) { - struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); bool swap; int ret; - guard(mutex)(&tphy->lock); + lockdep_assert_held(&tphy->lock); if (!tphy->usb3_ok || tphy->usb3_up) - return 0; + return; + /* DP mode owns all four lanes; USB3 comes back in t6040_usb3_reclaim_locked(). */ + if (tphy->dp_up) { + dev_dbg(tphy->dev, "USB3: lanes held by DP, USB2 only for now\n"); + return; + } switch (tphy->orientation) { case TYPEC_ORIENTATION_NORMAL: @@ -901,7 +908,7 @@ static int t6040_usb3_power_on(struct phy *phy) break; default: dev_info(tphy->dev, "USB3: no plug orientation, USB2 only\n"); - return 0; + return; } /* PIPE_HANDLER_DEFAULT: the dummy PIPE clock the controller keeps using at USB2 */ @@ -918,7 +925,7 @@ static int t6040_usb3_power_on(struct phy *phy) tphy->usb3_up = true; dev_dbg(tphy->dev, "USB3 PHY up, lane %d\n", swap); - return 0; + return; usb2_only: /* power_off skips a PHY that never came up: undo here, so the next attach starts clean */ @@ -926,6 +933,15 @@ static int t6040_usb3_power_on(struct phy *phy) sleep: t6040_usb3_sleep(tphy); dev_warn(tphy->dev, "USB3 PHY bring-up failed (%d), USB2 only\n", ret); +} + +static int t6040_usb3_power_on(struct phy *phy) +{ + struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); + + guard(mutex)(&tphy->lock); + tphy->usb3_on = true; + t6040_usb3_up_locked(tphy); return 0; } @@ -955,6 +971,8 @@ static int t6040_usb3_power_off(struct phy *phy) struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); guard(mutex)(&tphy->lock); + tphy->usb3_on = false; + tphy->usb3_pipe = false; t6040_usb3_down_locked(tphy); return 0; } @@ -972,6 +990,7 @@ static int t6040_usb3_set_mode(struct phy *phy, enum phy_mode mode, int submode) return -EOPNOTSUPP; guard(mutex)(&tphy->lock); + tphy->usb3_pipe = true; if (!tphy->usb3_up || tphy->pipe_switched) return 0; t6040_usb3_pipe_switch(tphy); @@ -1353,6 +1372,31 @@ static void t6040_dp_drive(struct t6040_usb2_phy *tphy, unsigned int lane, u32 p t6040_usb2_mask32(base + (tx ? 0x100 : 0xa8), 0x7ffff, eq); } +/* + * Give the lanes back to USB3 once the DP session is over and the port has + * left DP alt mode while dwc3 still holds the USB3 phy on: the USB3 bring-up + * programs the PHY afresh, as it does after DP on the next attach, and the + * PIPE returns to the PHY if the controller had asked for it. Called when + * the DP phy powers off and when the mux leaves DP, as either can come last. + * tipd drops the USB role before it leaves DP alt mode, so on an unplug or a + * DP mode change dwc3 powers the USB3 phy off first and this does nothing. + */ +static void t6040_usb3_reclaim_locked(struct t6040_usb2_phy *tphy) +{ + lockdep_assert_held(&tphy->lock); + if (!tphy->usb3_lent || tphy->dp_up || tphy->dp_alt) + return; + tphy->usb3_lent = false; + if (!tphy->usb3_on || tphy->usb3_up) + return; + t6040_usb3_up_locked(tphy); + if (tphy->usb3_up && tphy->usb3_pipe && !tphy->pipe_switched) { + t6040_usb3_pipe_switch(tphy); + tphy->pipe_switched = true; + } + dev_info(tphy->dev, "USB3 %s after DP\n", tphy->usb3_up ? "back" : "not restored"); +} + /* ---- DP phy_ops ---- */ static int t6040_dp_power_on(struct phy *phy) @@ -1368,8 +1412,10 @@ static int t6040_dp_power_on(struct phy *phy) * lanes to DP: aciophy_phy_shutdown of the USB3 side first, then the * same mode-3 init as the fixed DP PHY. */ - if (!t6040_dp_only(tphy)) + if (!t6040_dp_only(tphy)) { t6040_usb3_down_locked(tphy); + tphy->usb3_lent = true; + } ret = t6040_dp_phy_init(tphy, tphy->dp_flip); if (ret) { dev_err(tphy->dev, "DP PHY init failed: %d\n", ret); @@ -1389,6 +1435,7 @@ static int t6040_dp_power_off(struct phy *phy) guard(mutex)(&tphy->lock); tphy->dp_up = false; + t6040_usb3_reclaim_locked(tphy); return 0; } @@ -1506,18 +1553,10 @@ static int t6040_dp_mux_set(struct typec_mux_dev *mux, struct typec_mux_state *s dp ? "DP" : "non-DP", state->mode, alt, tphy->dp_flip ? "reversed" : "normal"); if (alt && tphy->dp_up) t6040_dp_xbar_config(tphy, tphy->dp_flip); + t6040_usb3_reclaim_locked(tphy); return 0; } -/* For the DCP's Type-C route: is this PHY's port in DP alt mode (C/E)? */ -bool apple_t6040_phy_dp_alt(struct phy *phy) -{ - struct t6040_usb2_phy *tphy = phy_get_drvdata(phy); - - return tphy && tphy->dp_alt; -} -EXPORT_SYMBOL_GPL(apple_t6040_phy_dp_alt); - static void t6040_dp_mux_unregister(void *mux) { typec_mux_unregister(mux); diff --git a/drivers/pmdomain/apple/pmp-report.c b/drivers/pmdomain/apple/pmp-report.c index 823669e27ad74..aaa38c06daaaf 100644 --- a/drivers/pmdomain/apple/pmp-report.c +++ b/drivers/pmdomain/apple/pmp-report.c @@ -1473,6 +1473,13 @@ static int apple_pmp_report_entry_probe(struct platform_device *pdev) ent->genpd.power_on = apple_pmp_report_entry_power_on; ent->genpd.power_off = apple_pmp_report_entry_power_off; + /* + * An entry that is the parent of PMGR power states (which are IRQ safe) + * must be IRQ safe too. Setting the state takes a spinlock and polls + * the acknowledgment atomically, so it may be called with IRQs off. + */ + if (of_property_read_bool(node, "apple,irq-safe")) + ent->genpd.flags |= GENPD_FLAG_IRQ_SAFE; ret = pm_genpd_init(&ent->genpd, NULL, true); if (ret) { diff --git a/drivers/soc/apple/Kconfig b/drivers/soc/apple/Kconfig index 9033a75140946..cbd8a0f0d430a 100644 --- a/drivers/soc/apple/Kconfig +++ b/drivers/soc/apple/Kconfig @@ -269,8 +269,10 @@ config APPLE_T6041_DISPLAY_GATE boot loader has handed the running display coprocessor over. This option enables those nodes before devices are created, only when the boot loader has published a complete T6041 handoff for the - 26.6.2 display firmware. Without it, or with nomodeset, it does - nothing. + 26.6.2 display firmware. It also powers up the J616s external + display chain (HDMI, Type-C DisplayPort) once the boot loader has + given the external display processors their firmware memory. + Without these, or with nomodeset, it does nothing. Say 'y' if you want the native display on M4 Pro MacBook Pros. diff --git a/drivers/soc/apple/t6041-display-gate.c b/drivers/soc/apple/t6041-display-gate.c index aa088ddb957de..cccbec79e095f 100644 --- a/drivers/soc/apple/t6041-display-gate.c +++ b/drivers/soc/apple/t6041-display-gate.c @@ -156,3 +156,460 @@ static int __init apple_t6041_display_gate(void) /* Runs before of_platform_default_populate_init() at arch_initcall_sync. */ arch_initcall(apple_t6041_display_gate); + +/* + * External displays (HDMI behind dcpext0, Type-C behind dcpext1). The + * device tree describes the whole chain disabled; it is enabled here once the + * boot loader has given each external display processor its firmware memory + * (the memory-region nodes of the dcpext are available, or they stay disabled + * placeholders). Before any device exists only the Type-C routes are enabled + * (the port controllers then wait for dcpext1's mode switches) and the + * display subsystem is held back. The power-up waits for the PMP firmware, + * whose acknowledgement the DISPEXT domains need: HDMI chain, Type-C chain, + * then both processors and the display subsystem together, so the display + * master is created once with every DART in its iommus. + */ +#include +#include +#include +#include +#include +#include +#include + +/* + * Two stages, each with its own display DART (a DART still disabled in the + * display subsystem's iommus would make of_iommu drop its whole fwspec): the + * HDMI chain, then the Type-C chain. Run at the same time the two processors + * crashed (dcpext0: axi_rd_err), as they never did one after the other. + * Parents first; the genpd links in the tree do the real ordering. + */ +struct gate_stage { + const char *dart; /* display DART of this stage */ + const char *const *nodes; + unsigned int nr; +}; + +static const char *const gate_hdmi_nodes[] = { + /* ATC3 power, PMP DISPEXT0 request, DISPEXT0, AOP pins, PHY, crossbar, DARTs, mailbox, dcpext0 */ + "/power-management@502800000/power-controller@408", + "/power-management@508300000/power-controller@e0", + "/power-management@508300000/power-controller@168", + "/power-management@508300000/power-controller@b8", + "/power-management@508300000/power-controller@148", + "/pmp-report@5003c0000/report@11", + "/power-management@502800000/power-controller@340", + "/power-management@502800000/power-controller@348", + "/pinctrl@510824000", + "/mailbox@39b148000", + "/phy@39aa90000", + "/mux@39b24c000", + "/iommu@3a2340000", + "/iommu@3a2300000", + "/mailbox@3a2e08000", + "/dcp@3a2e00000/typec-routes/route@0", + "/dcp@3a2e00000/typec-routes/route@1", + "/dcp@3a2e00000/typec-routes/route@2", + "/dcp@3a2e00000", +}; + +static const char *const gate_usbc_nodes[] = { + /* PMP DISPEXT1 request, FAB7_SOC, DISPEXT1, crossbars, DARTs, mailbox, routes, dcpext1 */ + "/pmp-report@5003c0000/report@12", + "/power-management@502800000/power-controller@488", + "/power-management@502800000/power-controller@490", + "/power-management@502800000/power-controller@498", + "/mux@38324c000", + "/mux@38b24c000", + "/mux@39324c000", + "/iommu@602340000", + "/iommu@602300000", + "/mailbox@602e08000", + "/dcp@602e00000/typec-routes/route@0", + "/dcp@602e00000/typec-routes/route@1", + "/dcp@602e00000/typec-routes/route@2", + "/dcp@602e00000", +}; + +static const struct gate_stage gate_stages[] = { + { "/iommu@3a2300000", gate_hdmi_nodes, ARRAY_SIZE(gate_hdmi_nodes) }, + { "/iommu@602300000", gate_usbc_nodes, ARRAY_SIZE(gate_usbc_nodes) }, +}; + +/* A display processor (a direct child of the root) and a Type-C display route. */ +static bool gate_is_dcp(const char *path) +{ + return !strncmp(path, "/dcp@", 5) && !strchr(path + 5, '/'); +} + +static bool gate_is_route(const char *path) +{ + return strstr(path, "/typec-routes/"); +} + +static int gate_display_add_dart(struct of_changeset *cs, struct device_node *display, + struct device_node *dart) +{ + struct property *old, *prop; + __be32 *value; + + old = of_find_property(display, "iommus", NULL); + if (!old || !dart->phandle || old->length % (2 * sizeof(*value))) + return -EINVAL; + prop = kzalloc(sizeof(*prop), GFP_KERNEL); + value = kmalloc(old->length + 2 * sizeof(*value), GFP_KERNEL); + if (!prop || !value) { + kfree(prop); + kfree(value); + return -ENOMEM; + } + memcpy(value, old->value, old->length); + value[old->length / sizeof(*value)] = cpu_to_be32(dart->phandle); + value[old->length / sizeof(*value) + 1] = cpu_to_be32(0); + prop->name = kstrdup("iommus", GFP_KERNEL); + prop->length = old->length + 2 * sizeof(*value); + prop->value = value; + if (!prop->name) { + kfree(value); + kfree(prop); + return -ENOMEM; + } + return of_changeset_update_property(cs, display, prop); +} + +/* + * The processor's firmware memory must be described: every memory-region of + * the dcpext node (the stage's last node) must be an available reserved-memory + * node. A loader that fills the regions from the ADT (m4-loader + * m4_dcpext_fixup) leaves disabled placeholders when it refuses, and an older + * loader leaves them all disabled; enabling the processor then would only + * make its driver fail. /chosen m4,dcpext is that loader's record (0 filled, + * 2 static memory), printed when the check fails. + */ +static bool gate_firmware_memory(const char *dcp) +{ + struct device_node *np, *region; + int i, n; + u32 record[2]; + + np = of_find_node_by_path(dcp); + if (!np) + return false; + n = of_count_phandle_with_args(np, "memory-region", NULL); + for (i = 0; i < n; i++) { + region = of_parse_phandle(np, "memory-region", i); + if (region && of_device_is_available(region) && + of_device_is_compatible(region, "apple,asc-mem")) { + of_node_put(region); + continue; + } + of_node_put(region); + n = -ENODEV; + break; + } + of_node_put(np); + if (n > 0) + return true; + np = of_find_node_by_path("/chosen"); + if (np && !of_property_read_u32_array(np, "m4,dcpext", record, 2)) + pr_err("%s: no firmware memory (loader record <%u %u>), nothing enabled\n", dcp, + record[0], record[1]); + else + pr_err("%s: no firmware memory, nothing enabled\n", dcp); + of_node_put(np); + return false; +} + +/* Every node of @st is there and still disabled, and its processor has firmware memory. */ +static bool gate_stage_ready(const struct gate_stage *st) +{ + struct device_node *np; + unsigned int i; + + if (!gate_firmware_memory(st->nodes[st->nr - 1])) + return false; + for (i = 0; i < st->nr; i++) { + np = of_find_node_by_path(st->nodes[i]); + if (!np) { + pr_err("%s: no such node, nothing enabled\n", st->nodes[i]); + return false; + } + if (of_device_is_available(np)) { + pr_err("%s: already enabled, nothing changed\n", st->nodes[i]); + of_node_put(np); + return false; + } + of_node_put(np); + } + return true; +} + +/* Enable @st's nodes and put its display DART into the display subsystem's iommus. */ +static int gate_stage_apply(const struct gate_stage *st, struct device_node *display) +{ + struct device_node *np; + struct of_changeset *cs; + unsigned int i; + int ret; + + cs = kzalloc(sizeof(*cs), GFP_KERNEL); + if (!cs) + return -ENOMEM; + of_changeset_init(cs); + + np = of_find_node_by_path(st->dart); + ret = np ? gate_display_add_dart(cs, display, np) : -ENODEV; + of_node_put(np); + for (i = 0; !ret && i < st->nr; i++) { + if (gate_is_dcp(st->nodes[i]) || gate_is_route(st->nodes[i])) + continue; + np = of_find_node_by_path(st->nodes[i]); + ret = np ? of_changeset_update_prop_string(cs, np, "status", "okay") : -ENODEV; + of_node_put(np); + } + if (!ret) + ret = of_changeset_apply(cs); + if (ret) { + of_changeset_destroy(cs); + kfree(cs); + } + /* Applied, it stays: the live tree refers to it. */ + return ret; +} + +#define GATE_PMP_WAIT_MS 20000 + +/* Set the status of the processors (externals) or the routes, and the display subsystem, in one changeset. */ +static int gate_set(struct device_node *display, bool dcps, const char *dcp_status, bool routes, + const char *route_status, const char *display_status) +{ + struct device_node *np; + struct of_changeset *cs; + unsigned int i, j; + int ret = 0; + + cs = kzalloc(sizeof(*cs), GFP_KERNEL); + if (!cs) + return -ENOMEM; + of_changeset_init(cs); + for (i = 0; !ret && i < ARRAY_SIZE(gate_stages); i++) + for (j = 0; !ret && j < gate_stages[i].nr; j++) { + const char *path = gate_stages[i].nodes[j]; + + if (!(dcps && gate_is_dcp(path)) && !(routes && gate_is_route(path))) + continue; + np = of_find_node_by_path(path); + ret = np ? of_changeset_update_prop_string(cs, np, "status", + gate_is_dcp(path) ? dcp_status : + route_status) : -ENODEV; + of_node_put(np); + } + if (!ret && display_status) + ret = of_changeset_update_prop_string(cs, display, "status", display_status); + if (!ret) + ret = of_changeset_apply(cs); + if (ret) { + of_changeset_destroy(cs); + kfree(cs); + } + return ret; +} + +static struct device_node *gate_display; +static struct device_node *gate_pmp; +static atomic_t gate_started = ATOMIC_INIT(0); + +/* Internal panel only: the routes go again, so the port controllers probe without mode switches. */ +static void gate_fallback(const char *why) +{ + int ret = gate_set(gate_display, false, NULL, true, "disabled", "okay"); + + pr_err("%s: external displays stay off, internal panel only (%d)\n", why, ret); +} + +/* + * The Type-C core treats a mode switch below a disabled node as absent, so the + * port controllers, which probe before dcpext1 is enabled, only took the USB + * muxes. Once dcpext1 is bound (its route mode switches are registered), let + * each controller wired to a route probe again, so that it takes them too. + */ +static void gate_reprobe_port_controllers(void) +{ + struct device *done[ARRAY_SIZE(gate_usbc_nodes)] = {}; + struct device_node *dcp, *np, *ep, *connector, *ctrl; + struct device *dev = NULL; + unsigned int i, n = 0, tries; + + dcp = of_find_node_by_path("/dcp@602e00000"); + for (tries = 0; dcp && tries < 150; tries++) { + dev = dcp->fwnode.dev; + if (dev && READ_ONCE(dev->driver)) + break; + msleep(200); + } + of_node_put(dcp); + if (!dev || !READ_ONCE(dev->driver)) { + pr_err("dcpext1 not bound, port controllers left as they are\n"); + return; + } + for (i = 0; i < ARRAY_SIZE(gate_usbc_nodes); i++) { + unsigned int k; + + if (!gate_is_route(gate_usbc_nodes[i])) + continue; + np = of_find_node_by_path(gate_usbc_nodes[i]); + ep = np ? of_graph_get_next_endpoint(np, NULL) : NULL; + connector = ep ? of_graph_get_remote_port_parent(ep) : NULL; + ctrl = connector ? of_get_parent(connector) : NULL; + dev = ctrl ? get_device(ctrl->fwnode.dev) : NULL; + of_node_put(ctrl); + of_node_put(connector); + of_node_put(ep); + of_node_put(np); + if (!dev) + continue; + for (k = 0; k < n && done[k] != dev; k++) + ; + if (k < n || !READ_ONCE(dev->driver)) { + put_device(dev); + continue; + } + done[n++] = dev; + if (device_reprobe(dev)) + dev_err(dev, "probe again failed\n"); + else + dev_info(dev, "probed again for the display routes\n"); + } + while (n) + put_device(done[--n]); +} + +/* + * A processor that finds its crossbar or DP PHY without a driver is given up + * for good (the deferred probe timeout is 0 here), and those drivers are + * modules that load only once the stage has created their devices. Wait until + * every such device of the enabled infrastructure has its driver. Returns the + * first path still without one, or NULL. + */ +static const char *gate_unbound_infra(void) +{ + unsigned int i, j; + + for (i = 0; i < ARRAY_SIZE(gate_stages); i++) + for (j = 0; j < gate_stages[i].nr; j++) { + const char *path = gate_stages[i].nodes[j]; + struct device_node *np; + struct device *dev; + + /* Power domains are the genpd core's to wait for; the modules are the point here. */ + if (gate_is_dcp(path) || gate_is_route(path) || + !strncmp(path, "/power-management@", 18)) + continue; + np = of_find_node_by_path(path); + dev = np ? READ_ONCE(np->fwnode.dev) : NULL; + of_node_put(np); + if (dev && !READ_ONCE(dev->driver)) + return path; + } + return NULL; +} + +#define GATE_DRIVER_WAIT_MS 10000 + +static void gate_wait_for_drivers(void) +{ + const char *path = NULL; + unsigned int tries; + + for (tries = 0; tries < GATE_DRIVER_WAIT_MS / 100; tries++) { + path = gate_unbound_infra(); + if (!path) + return; + msleep(100); + } + pr_warn("%s has no driver after " __stringify(GATE_DRIVER_WAIT_MS) " ms, enabling the processors anyway\n", + path); +} + +static void gate_power_up(struct work_struct *work) +{ + unsigned int i; + int ret = 0; + + for (i = 0; !ret && i < ARRAY_SIZE(gate_stages); i++) { + ret = gate_stage_apply(&gate_stages[i], gate_display); + if (!ret) + pr_info("stage %u enabled\n", i); + } + if (!ret) + gate_wait_for_drivers(); + /* Both processors and the display subsystem together: the master binds once, with all DCPs. */ + if (!ret) + ret = gate_set(gate_display, true, "okay", false, NULL, "okay"); + if (ret) { + gate_fallback("could not enable the external displays"); + return; + } + pr_info("processors and display subsystem enabled\n"); + gate_reprobe_port_controllers(); +} +static DECLARE_WORK(gate_power_up_work, gate_power_up); + +static void gate_timeout(struct work_struct *work) +{ + if (!atomic_xchg(&gate_started, 1)) + gate_fallback("no PMP within " __stringify(GATE_PMP_WAIT_MS) " ms"); +} +static DECLARE_DELAYED_WORK(gate_timeout_work, gate_timeout); + +/* The PMP's driver is bound: DISPEXT can be acknowledged now. */ +static int gate_pmp_bound(struct notifier_block *nb, unsigned long action, void *data) +{ + struct device *dev = data; + + if (action != BUS_NOTIFY_BOUND_DRIVER || dev->of_node != gate_pmp || atomic_xchg(&gate_started, 1)) + return NOTIFY_DONE; + cancel_delayed_work(&gate_timeout_work); + queue_work(system_long_wq, &gate_power_up_work); + return NOTIFY_OK; +} +static struct notifier_block gate_pmp_nb = { .notifier_call = gate_pmp_bound }; + +static int __init apple_t6041_extdisp_gate(void) +{ + unsigned int i; + int ret; + + if (!of_machine_is_compatible("apple,j616s")) + return 0; + if (IS_ENABLED(CONFIG_VIDEO) && video_firmware_drivers_only()) + return 0; + + /* Everything must be there and disabled, with firmware memory, before anything is changed. */ + for (i = 0; i < ARRAY_SIZE(gate_stages); i++) + if (!gate_stage_ready(&gate_stages[i])) + return 0; + gate_display = of_find_node_by_path("/display-subsystem"); + gate_pmp = of_find_node_by_path("/pmp@500700000"); + if (!gate_display || !gate_pmp || !of_device_is_available(gate_display) || + !of_device_is_available(gate_pmp)) { + pr_err("no display subsystem or PMP, nothing enabled\n"); + return 0; + } + + /* No device exists yet: routes on, display subsystem held back until the power-up. */ + ret = gate_set(gate_display, false, NULL, true, "okay", "disabled"); + if (ret) { + pr_err("could not prepare the tree: %d, nothing enabled\n", ret); + return 0; + } + ret = bus_register_notifier(&platform_bus_type, &gate_pmp_nb); + if (ret) { + gate_fallback("no bus notifier"); + return 0; + } + schedule_delayed_work(&gate_timeout_work, msecs_to_jiffies(GATE_PMP_WAIT_MS)); + pr_info("routes enabled, waiting for the PMP\n"); + return 0; +} +arch_initcall(apple_t6041_extdisp_gate); diff --git a/include/linux/soc/apple/dp-tunnel.h b/include/linux/soc/apple/dp-tunnel.h index 868de62db1156..540d48fe8e0dc 100644 --- a/include/linux/soc/apple/dp-tunnel.h +++ b/include/linux/soc/apple/dp-tunnel.h @@ -61,21 +61,6 @@ int apple_atc_dp_tunnel_rate_source(struct phy *phy, unsigned int source, */ int apple_atc_dp_tunnel_open(struct phy *phy); -/* - * T6040 Type-C PHY: true while the Type-C controller holds the PHY's port in - * DisplayPort alt mode with pin assignment C or E (DP on all four lanes), so - * appledrm can route a DCP that lists several Type-C DP PHYs to the one that - * has a DisplayPort sink plugged. - */ -#if IS_ENABLED(CONFIG_PHY_APPLE_T6040_USB2) -bool apple_t6040_phy_dp_alt(struct phy *phy); -#else -static inline bool apple_t6040_phy_dp_alt(struct phy *phy) -{ - return false; -} -#endif - /* * Display crossbar: take the connection of an already selected output down or * bring it back up (clock gates and enables) around a link reconfiguration. diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/accounting.py b/tools/testing/selftests/drivers/gpu/apple-dcp/accounting.py index ce2791cc3a379..9c74ec637be24 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/accounting.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/accounting.py @@ -103,7 +103,7 @@ def resources(ROOT): puts("PASS successful DCP finalizer is not repeated after a later provider failure");return 0; } if(argv[1][0]=='h')hw_failures=1; - if(argv[1][0]=='f') {display.fixed_connector_type=2;display.hdmi_hpd=(void *)1;gpio_level=1;display.xbar=&fixed_xbar;fixed_restore_failures=1;} + if(argv[1][0]=='f') {display.fixed_connector_type=2;display.hdmi_hpd=(void *)1;gpio_level=1;display.xbar=display.fixed_xbar=&fixed_xbar;fixed_restore_failures=1;} assert(typec_mux_set(&mux,&safe)==-EIO&&xbar.lock==1&&port.owner==&route&&port.safe_pending&&!port.safe_finish_ready); bool selected_after_error=route.selected; int ret=typec_mux_set(&mux,&safe); diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py index 21a962fb10cb3..ede39666c8413 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/lifecycle.py @@ -490,6 +490,8 @@ def bounce(root): m = re.search(r'^static int dcp_dptx_connect_raw\(', dcp, re.M) assert m, 'dcp_dptx_connect_raw' raw = block(dcp, m.start()) + # Compact DPTX is the desktop 26.x wire; this scenario's endpoint is not. + raw = raw.replace('READ_ONCE(dcp->dptxport[port].service->ep->compact)', 'false') def swap(old, new): nonlocal unit @@ -510,6 +512,8 @@ def swap(old, new): swap(old_raw, 'static int dcp_dptx_connect_raw(struct apple_dcp *, u32, bool *' + (', const struct dcp_fabric_session *, bool, bool' if sessions else '') + ');') swap('static int dptxport_set_hpd(void *p, bool on)\n{ (void)p; assert(!on); return hpd_error; }', + '/* The route powers a desktop 26.x PHY at connect: nothing to do for this scenario. */\n' + 'static int __attribute__((unused)) dcp_typec_phy_on(struct apple_dcp *d) { (void)d; return 0; }\n' 'static bool fw_hpd, fw_display, link_trains;\n' 'static int hpd_drops, connects, release_error_once, assert_error;\n' '/* A request that times out may still be carried out; a refused one is not. */\n' @@ -1668,7 +1672,8 @@ def vrr(root): struct apple_connector { bool connected; }; /* The native T6030 14.7 backend is never selected in these scenarios. */ enum dcp_firmware_version { DCP_FIRMWARE_UNKNOWN, DCP_FIRMWARE_V_12_3, DCP_FIRMWARE_V_13_5, - DCP_FIRMWARE_V_14_7, DCP_FIRMWARE_V_14_7_TEMPLATE, DCP_FIRMWARE_V_26_6 }; + DCP_FIRMWARE_V_14_7, DCP_FIRMWARE_V_14_7_TEMPLATE, DCP_FIRMWARE_V_26_6, + DCP_FIRMWARE_V_26_6_DESKTOP }; struct apple_dcp { bool crashed, external, external_native, quiescing; unsigned int nr_typec_routes; @@ -1681,14 +1686,19 @@ def vrr(root): struct platform_device { struct apple_dcp *data; }; struct drm_crtc { struct platform_device *dcp; void *primary; }; struct drm_display_mode { int unused; }; -struct drm_crtc_state { bool enable, vrr_enabled, modeset, color_mgmt_changed; struct drm_display_mode mode; void *ctm, *degamma_lut, *gamma_lut; unsigned plane_mask; }; +struct drm_crtc_state { bool enable, vrr_enabled, modeset, mode_changed, color_mgmt_changed; struct drm_display_mode mode; void *ctm, *degamma_lut, *gamma_lut; unsigned plane_mask; }; /* mainline 7.2 renamed struct drm_atomic_state to struct drm_atomic_commit */ -struct drm_atomic_commit { struct drm_crtc_state crtc; }; +struct drm_atomic_commit { struct drm_crtc_state crtc, old_crtc; bool no_old_crtc; }; #define to_apple_crtc(x) (x) #define platform_get_drvdata(x) ((x)->data) static struct drm_crtc_state *drm_atomic_get_new_crtc_state( struct drm_atomic_commit *s, struct drm_crtc *c) { (void)c; return &s->crtc; } +/* Only the desktop 26.6 firmware compares against the old CRTC state. */ +static unsigned int old_state_lookups; +static struct drm_crtc_state * __attribute__((unused)) drm_atomic_get_old_crtc_state( + struct drm_atomic_commit *s, struct drm_crtc *c) +{ (void)c; old_state_lookups++; return s->no_old_crtc ? NULL : &s->old_crtc; } static bool drm_atomic_crtc_needs_modeset(struct drm_crtc_state *s) { return s->modeset; } static int __attribute__((unused)) iomfb_v14_7_atomic_check(struct apple_dcp *d, @@ -1746,14 +1756,45 @@ def vrr(root): } cases++; } - dcp.crashed = true; + /* No other firmware turns a VRR change into a mode set. */ + assert(!old_state_lookups && !state.crtc.mode_changed); +DESKTOP_VRR dcp.crashed = true; state.crtc.vrr_enabled = false; assert(dcp_crtc_atomic_check(&crtc, &state) == -EINVAL); printf("PASS: %u production atomic VRR admission cases and crashed-device rejection\n", cases); return 0; } """ - return prefix + check + suffix + desktop = '' + if 'DCP_FIRMWARE_V_26_6_DESKTOP' in check: + # Desktop 26.6 applies adaptive sync with a mode set: a VRR_ENABLED + # change of an enabled CRTC must become one, nothing else may. + desktop = r""" dcp.fw_compat = DCP_FIRMWARE_V_26_6_DESKTOP; + dcp.hw.iomfb_method_profile = 0; + dcp.mode_state.valid = true; + dcp.connector = &connector; + connector.connected = true; + state.crtc.modeset = false; + for (unsigned int enable = 0; enable < 2; enable++) + for (unsigned int old_vrr = 0; old_vrr < 3; old_vrr++) + for (unsigned int vrr = 0; vrr < 2; vrr++) { + state.crtc.enable = enable; + state.crtc.vrr_enabled = vrr; + state.crtc.mode_changed = false; + state.no_old_crtc = old_vrr == 2; + state.old_crtc.vrr_enabled = old_vrr == 1; + assert(dcp_crtc_atomic_check(&crtc, &state) == 0); + bool expected = enable && old_vrr != 2 && (old_vrr == 1) != vrr; + if (state.crtc.mode_changed != expected) { + fprintf(stderr, "desktop enable=%u old_vrr=%u vrr=%u: mode_changed=%d expected %d\n", + enable, old_vrr, vrr, state.crtc.mode_changed, expected); + return 1; + } + cases++; + } + dcp.fw_compat = DCP_FIRMWARE_UNKNOWN; +""" + return prefix + check + suffix.replace('DESKTOP_VRR', desktop, 1) def power_reply(root): @@ -1842,6 +1883,7 @@ def dispatcher(root): typedef uint32_t u32; typedef uint64_t u64; #define DCP_FIRMWARE_H17P 1 +#define DCP_FIRMWARE_V_26_6_DESKTOP 100 #define DCP_IOMFB_METHODS_H17P_EXTERNAL 2 #define DCP_IOMFB_METHODS_H17G 3 #define DCP_MAX_CALL_DEPTH 8 @@ -1871,6 +1913,9 @@ def dispatcher(root): static struct dcp_channel *dcp_get_channel(struct apple_dcp *d, enum dcp_context_id c) { (void)c; return &d->channel; } static int dcp_parse_tag(char *p) { (void)p; return case_tag; } +/* A desktop 26.6 callback is not under test here: no scenario selects that firmware. */ +static bool __attribute__((unused)) iomfb_check_callback_v26_6_desktop(int tag, u32 in_len, u32 out_len) +{ (void)tag; (void)in_len; (void)out_len; assert(!"desktop 26.6 callback in an H17P scenario"); return false; } static bool of_machine_is_compatible(const char *n) { (void)n; return machine; } static bool of_device_is_compatible(struct device_node *n, const char *s) { (void)s; return n->external; } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py b/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py index 48d7c3078092c..30eb0a183d6b7 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/quiesce.py @@ -80,6 +80,10 @@ def make_unit(source, parent=False, reference=None): static struct drm_device *dcp_typec_drm(void) { return NULL; } static void dcp_typec_plan(struct drm_device *d,struct apple_dcp_typec_port *p,unsigned n) { (void)d;(void)p;(void)n; } static bool dcp_typec_tunnel_held(struct apple_dcp *d) { (void)d;return false; } +/* The desktop 26.6 (T6041) Type-C path is not under test: nothing here is on that firmware. */ +#define DCP_FIRMWARE_V_26_6_DESKTOP 100 +static void __attribute__((unused)) dcp_typec_route_phy_off(struct apple_dcp_typec_route *r) +{ (void)r; /* never powered: the real one returns at once */ } static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *r) { deactivate_calls++;r->selected=false;return 0; } static void dcp_typec_port_attach(struct apple_dcp_typec_port *p) { (void)p; } static int gpiod_get_value_cansleep(void *p) { (void)p;return 0; } diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index 7bd087c364c52..72b281ab2d710 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -677,7 +677,7 @@ def publication(root): #define DCP_FIRMWARE_V_14_7 (DCP_FIRMWARE_H17P + 1) #define DCP_FIRMWARE_V_14_7_TEMPLATE (DCP_FIRMWARE_H17P + 2) #endif -/* Nor the desktop 26.6 firmware, the one that withholds VRR modes. */ +/* Nor the desktop 26.6 firmware. */ #define DCP_FIRMWARE_V_26_6_DESKTOP (DCP_FIRMWARE_H17P + 3) '''+firmware_lineage(root)+internal_inline(root,'dcp_fw_is_14_7')+r'''/* Off T6030 machines the direct crossbar output is not clocked separately. */ static int __attribute__((unused)) dcp_direct_crossbar_link(struct apple_dcp *d,bool up) { (void)d;(void)up;return 0; } @@ -700,7 +700,9 @@ def publication(root): + ''.join(apple_function(root, name) for name in ('dcp_queue_hotplug', 'dcp_modes_for_connector', 'apple_connector_invalidate_edid', 'apple_connector_edid_set_live')) + mode_helpers) - publication = block(iomfb,'static void dcp_clear_connector_edid(')+'\n'+block(iomfb,'static void dcp_consume_edid_invalidation(')+mode_helpers+block(iomfb,'static bool dcp_mode_vrr_supported(')+'\n'+block(iomfb,'int dcp_get_modes(')+block(iomfb,re.search(r'^[^\n;(]*\blookup_mode\(',iomfb,re.M).start()) + # The desktop 26.6 VRR filter is gone once that firmware takes adaptive sync. + vrr_filter = block(iomfb,'static bool dcp_mode_vrr_supported(')+'\n' if 'static bool dcp_mode_vrr_supported(' in iomfb else '' + publication = block(iomfb,'static void dcp_clear_connector_edid(')+'\n'+block(iomfb,'static void dcp_consume_edid_invalidation(')+mode_helpers+vrr_filter+block(iomfb,'int dcp_get_modes(')+block(iomfb,re.search(r'^[^\n;(]*\blookup_mode\(',iomfb,re.M).start()) # Drive malformed replacements through the same chunk/end ingress as firmware. # The dictionary cache is a boundary substitute and takes ownership of the blob. receipt = r''' diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py b/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py index 5dea64a19054e..6c40adf5d3a85 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py @@ -40,13 +40,21 @@ def check(name, unit, cases=((),), negative=False, flags=()): bool external_power_attempted,dptx_tunnel,tb_clock_ok,fixed_route_selected; int tb_lock; unsigned typec_generation,dptx_dfp_port,dptx_phy,fixed_dptx_phy,fixed_mux_index; void *tb_dpin_set_active,*tb_dpin_ctx,*phy,*fixed_phy; - struct mux_control *xbar; + struct mux_control *xbar,*fixed_xbar; + /* desktop 26.x hybrid hand-over and dual-pipe state; fw_compat is never desktop here */ + bool peer_starved,typec_narrow,hdmi_yielded; struct apple_dcp_typec_route *active_typec_route,*typec_routes; struct connector *connector,*fixed_connector;int connector_type,fixed_connector_type; struct mux_control *direct_xbar_up; bool external_native, ext_dead; int fw_compat; unsigned long long tb_generation; int modes_lock; unsigned long long modes_generation; bool modes_admitted,modes_provisional; struct { int iomfb_method_profile; } hw; ''',1) + # The hybrid hand-over walks a port's routes; the only route is this port's. + actual=actual.replace('struct apple_dcp_typec_port { int link;', 'struct apple_dcp_typec_port { int link,routes; bool typec_yielded;',1) + actual=actual.replace('#define list_for_each_entry(p,h,m) for((void)(h),(p)=sole_port();(p);(p)=NULL)', + '#define list_for_each_entry(p,h,m) for((void)(h),(p)=_Generic((p),struct apple_dcp_typec_route *:sole_route(),default:sole_port());(p);(p)=NULL)',1) + actual=actual.replace('static struct apple_dcp_typec_port *sole_port(void) { return &port; }', + 'static struct apple_dcp_typec_port *sole_port(void) { return &port; }\nstatic struct apple_dcp_typec_route * __attribute__((unused)) sole_route(void) { return &route; }',1) actual=actual.replace('bool applied_valid,safe_pending,dp_wanted,dp_hpd,hpd;', 'struct typec_altmode *applied_alt; unsigned long applied_mode; unsigned applied_status,applied_conf; bool applied_valid,safe_pending,dp_wanted,dp_hpd,hpd;',1) actual=actual.replace('void *xbar; bool deactivate_xbar_done; int deactivate_error; bool selected,tunnel;', 'struct mux_control *xbar,*active_xbar; void *phy; unsigned dptx_phy,tunnel_dpin,mux_index;unsigned long long tunnel_generation;bool deactivate_xbar_done; int deactivate_error;bool xbar_up,selected,tunnel;',1) extra=r''' @@ -70,6 +78,10 @@ def check(name, unit, cases=((),), negative=False, flags=()): static int mux_control_deselect(struct mux_control *m) { (void)m;event('C');xbar_calls++;return xbar_error; } static int mux_control_select(struct mux_control *m,unsigned n) { (void)m;(void)n;return 0; } static void dpavservep_detach(struct apple_dcp *d) { (void)d; } + /* Desktop 26.6 dual-pipe peers: no scenario here is on that firmware. */ + static void __attribute__((unused)) dcp_peer_release_26_6(struct apple_dcp *d) { (void)d;assert(!"desktop 26.6 peer release"); } + /* failed takes of the hybrid, reset when a Type-C display leaves DP */ + static unsigned int __attribute__((unused)) dcp_hybrid_fails; static void dcp_firmware_metadata_connection(struct apple_dcp *d,unsigned n,bool c) { (void)d;(void)n;(void)c; } static bool dcp_typec_keep_order(void) { return false; } static struct apple_dcp_typec_route * __attribute__((unused)) dcp_typec_lowest_free(struct apple_dcp_typec_port *p) { (void)p;return &route; } @@ -115,6 +127,11 @@ def check(name, unit, cases=((),), negative=False, flags=()): '''+available # Route-follow: the fixed output, or a failed T6030 external processor, keeps the route busy. available+=apple_function(ROOT,'dcp_typec_route_busy') + # Hybrid-first policy: the real helper over the module parameter's and the + # device tree's defaults (-1: from the DT; no apple,typec-hybrid-first here). + hybrid_first=apple_function(ROOT,'dcp_typec_hybrid_first') + if hybrid_first: + available='static int typec_hybrid_first=-1;\n static bool typec_hybrid_first_dt;\n '+hybrid_first+available if available: actual=actual.replace('struct apple_dcp { int external_lock,external_error;','struct apple_dcp { int external_lock,external_error;\n struct dcp_fabric_presence hdmi_presence; int hdmi_presence_lock; unsigned nr_typec_routes;',1) available='''#define atomic_read(p) (*(p))