From d34800eef43152ac775ee180fef4f1add36008f5 Mon Sep 17 00:00:00 2001 From: Marius Gehrmann Date: Tue, 13 Oct 2026 17:03:43 +0200 Subject: [PATCH 1/6] drm: apple: Never let the internal panel's Type-C route hold up the panel On J613 the internal DCP gets a Type-C display route to the left-front USB-C port in the device tree: with the lid closed and the panel off, its pipe can drive a second external display. The route stays disabled there; the display gate enables it at boot together with dcpext. When the gate does not (no boot-loader marker, a refused check, apple_t8122_display.dcpext=0, the 26.6 profile that includes the J613 device tree, or a kernel that does not know the route), the processor has a typec-routes container with no enabled route, which made route registration fail with "Type-C route container is empty" and took the panel down with it. - A typec-routes container without an enabled route means no route, for every processor but the external ones (T6030 and T8122 dcpext, whose routes are enabled together with them and must register). - On the internal panel's processor (14.7 DCPLink session), a route that fails to register is disabled in the device tree, as for a T6030 external processor, and the panel goes on without it. - On J613, a probe of the panel's processor that fails for any other reason (a deferral is not a failure) disables the gate-enabled route too, so that the left-front port does not wait for it forever. Device trees whose routes are enabled are unchanged. Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Marius Gehrmann [Chris Kearney: rebased onto siliconforest/next 6a421d4e76ff; resolved a conflict with the H17P probe path in dcp_platform_probe(); the T8140 dcpext (H17P), an external processor added since, is excluded from the empty-container exemption too, so it still fails without an enabled route; message reworded; Claude-Session trailer dropped] Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com> [TARS: ported to integration/aurora 7.2.11 (4fdd0fc5): dcp.c reorganised, so the dcp.c hunks are applied by hand; uses integration's dcp_is_v14_7_panel() instead of adding dcp_is_v14_panel()] (cherry picked from commit 93258ff9c2217dcd41fa869acfadb66a78c5b024) --- drivers/gpu/drm/apple/dcp-fabric.c | 19 +++++++++++-- drivers/gpu/drm/apple/dcp-fabric.h | 1 + drivers/gpu/drm/apple/dcp.c | 44 ++++++++++++++++++++++++++---- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 29321f8032a490..04aaaa37c511ff 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -3607,9 +3607,15 @@ static int dcp_register_typec_routes_present(void *data) * routes, so its port no longer waits for this processor. */ void dcp_typec_routes_disable(struct apple_dcp *dcp) +{ + dcp_typec_routes_disable_dev(dcp->dev); +} + +/* The same for a processor whose probe ended before it had a state. */ +void dcp_typec_routes_disable_dev(struct device *dev) { struct device_node *routes __free(device_node) = - of_get_child_by_name(dcp->dev->of_node, "typec-routes"); + of_get_child_by_name(dev->of_node, "typec-routes"); struct device_node *route; struct of_changeset *cs; int ret = 0; @@ -3630,7 +3636,7 @@ void dcp_typec_routes_disable(struct apple_dcp *dcp) if (!ret) ret = of_changeset_apply(cs); if (ret) { - dev_err(dcp->dev, "could not disable the Type-C display routes: %d\n", ret); + dev_err(dev, "could not disable the Type-C display routes: %d\n", ret); of_changeset_destroy(cs); kfree(cs); } @@ -3643,6 +3649,15 @@ int dcp_register_typec_routes(struct apple_dcp *dcp) of_get_child_by_name(dcp->dev->of_node, "typec-routes"); struct dcp_route_probe_context ctx = { .dcp = dcp, .routes = routes }; + /* + * The internal processor's route stays disabled in the device tree + * unless the display gate enables it at boot (J613, also seen by the + * 26.6 profile that includes its device tree): then it has no route. + * An external processor without an enabled route still fails. + */ + if (routes && !dcp->external && !dcp->ext_failsafe && + !dcp_is_external_h17p(dcp) && !of_get_available_child_count(routes)) + return 0; return dcp_fabric_run_probe(!!routes, dcp_register_typec_routes_present, &ctx); } diff --git a/drivers/gpu/drm/apple/dcp-fabric.h b/drivers/gpu/drm/apple/dcp-fabric.h index 1176ef56874f35..468b08ecfbacf0 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.h +++ b/drivers/gpu/drm/apple/dcp-fabric.h @@ -106,6 +106,7 @@ int dcp_tunnel_dpin_activate(struct apple_dcp *dcp, u32 unit, bool active); int dcp_register_typec_routes(struct apple_dcp *dcp); void dcp_typec_routes_disable(struct apple_dcp *dcp); void dcp_typec_retrain_work(struct work_struct *work); +void dcp_typec_routes_disable_dev(struct device *dev); void dcp_fabric_init(struct apple_dcp *dcp); void dcp_fabric_hdmi_resume(struct apple_dcp *dcp); void dcp_fabric_hdmi_reinit(struct apple_dcp *dcp, const char *why); diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 8cb040d16d552c..a28087a92eabf5 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -4256,19 +4256,24 @@ static int dcp_fixed_output_error(struct apple_dcp *dcp, int err, const char *wh * A T6030 external processor whose Type-C routes did not register: disable * them in the device tree so that the ports stop waiting for them, and add * the processor to the DRM device as a pipe that never starts, so that the - * panel is not held up either. + * panel is not held up either. The same for the internal panel's processor + * (J613): its route is an extra, the panel goes on without it. */ static int dcp_native_routes_error(struct apple_dcp *dcp, int err) { - if (!dcp->external_native) + if (!dcp->external_native && !dcp_is_v14_7_panel(dcp)) return err; if (err == -EPROBE_DEFER) { err = driver_deferred_probe_check_state(dcp->dev); if (err == -EPROBE_DEFER) return err; } - dev_err(dcp->dev, "Type-C display routes unusable: %d; USB-C ports go on without this processor\n", - err); + if (dcp->external_native) + dev_err(dcp->dev, "Type-C display routes unusable: %d; USB-C ports go on without this processor\n", + err); + else + dev_err(dcp->dev, "Type-C display route unusable: %d; the panel goes on without it\n", + err); dcp_typec_routes_disable(dcp); return 0; } @@ -4710,6 +4715,29 @@ static int dcp_ext_route_suppliers(struct device *dev) return 0; } +/* + * J613: the display gate enabled the internal processor's Type-C route, and + * the left-front port waits for it to register. If the probe fails (any + * error, not only the route's own), disable the route, so that the port goes + * on without it, as for a route that does not register. A deferral is not a + * failure: the processor may still bind, and its route register, later. The + * port is probed again with the next driver that binds. + */ +static void dcp_panel_routes_give_up(struct device *dev, int err) +{ + struct device_node *routes __free(device_node) = + of_get_child_by_name(dev->of_node, "typec-routes"); + + if (!of_device_is_compatible(dev->of_node, "apple,t8122-dcp") || + !routes || !of_get_available_child_count(routes)) + return; + if (err == -EPROBE_DEFER) + return; + dev_err(dev, "probe failed (%d): Type-C display route disabled, the left-front port goes on without it\n", + err); + dcp_typec_routes_disable_dev(dev); +} + /* * The T8122 dcpext is a component of the panel's DRM device, so it must * always register one, or the panel stays on the boot framebuffer. Keep @@ -4723,8 +4751,12 @@ static int dcp_platform_probe(struct platform_device *pdev) void *group; int ret; - if (!of_device_is_compatible(dev->of_node, "apple,t8122-dcpext")) - return dcp_platform_probe_pipe(pdev); + if (!of_device_is_compatible(dev->of_node, "apple,t8122-dcpext")) { + ret = dcp_platform_probe_pipe(pdev); + if (ret) + dcp_panel_routes_give_up(dev, ret); + return ret; + } ret = dcp_ext_route_suppliers(dev); if (ret) From 6933dcc17db5f0b6ea3607b28da1e5cfdfd8fc80 Mon Sep 17 00:00:00 2001 From: Marius Gehrmann Date: Tue, 13 Oct 2026 17:04:57 +0200 Subject: [PATCH 2/6] arm64: dts: apple: t8122-j613: Give the internal DCP a route to the front port With the lid closed and the panel off, the internal DCP's pipe can drive a second external display next to dcpext. In the J613 ADT the internal DCP is display-crossbar ufp0, port 0: crossbar state 0 (dispext0,0), here on ATC1's crossbar, with DPTX target 1 as dcpext's route to the same port. Give the internal DCP that Type-C display route to the left-front port (typec1): ATC1's DP PHY and display crossbar as typec0, crossbar state 0, DPTX target 1, and a second endpoint on the front connector's port@3, one per pipeline as on the M1 Pro/Max laptops. The route stays disabled, like dcpext and its routes; the display gate enables it in the same changeset, behind the boot loader's marker. The display driver uses it only while the lid is closed and the panel and its CRTC are off. Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Marius Gehrmann [Chris Kearney: rebased onto siliconforest/next 6a421d4e76ff; message and device tree comment reworded; Claude-Session trailer dropped] Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com> (cherry picked from commit 4c643f72980a1b6a621e07b87c8493e64e824353) --- arch/arm64/boot/dts/apple/t8122-j613.dts | 49 +++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/apple/t8122-j613.dts b/arch/arm64/boot/dts/apple/t8122-j613.dts index d895e44dcb9d3b..2fe88f461a6d62 100644 --- a/arch/arm64/boot/dts/apple/t8122-j613.dts +++ b/arch/arm64/boot/dts/apple/t8122-j613.dts @@ -651,14 +651,61 @@ }; }; +/* + * With the lid closed the internal DCP's pipe can drive a second external + * display on the left-front port. In the J613 ADT the + * internal DCP is display-crossbar ufp0, port 0, which is crossbar state 0 + * (dispext0,0) on ATC1's crossbar, DPTX target 1 like dcpext's route@1. The + * pipe's first DPTX core drives the panel otherwise, so the display driver + * only offers this route while the lid is closed and the panel is off, and + * only to a display that dcpext cannot take. + * + * The route stays disabled here, like dcpext's: the display gate enables it + * together with dcpext. The front connector has one endpoint per pipeline + * that can drive it, as on the M1 Pro/Max laptops. + */ +&dcp { + phys = <&atcphy1 PHY_TYPE_DP>; + phy-names = "typec0"; + mux-controls = <&atcphy1_xbar 0>; + mux-control-names = "typec0"; + apple,typec-mux-indices = <0>; + apple,typec-dptx-phys = <1>; + + typec-routes { + #address-cells = <1>; + #size-cells = <0>; + + route@0 { + reg = <0>; + mode-switch; + status = "disabled"; + + port { + dcp_typec1: endpoint { + remote-endpoint = <&typec1_dcp>; + }; + }; + }; + }; +}; + &typec1 { ports { port@3 { reg = <3>; + #address-cells = <1>; + #size-cells = <0>; - typec1_dcpext: endpoint { + typec1_dcpext: endpoint@0 { + reg = <0>; remote-endpoint = <&dcpext_typec1>; }; + + typec1_dcp: endpoint@1 { + reg = <1>; + remote-endpoint = <&dcp_typec1>; + }; }; }; }; From 0319f56a31f010e9f48f15c7c78827ef38137875 Mon Sep 17 00:00:00 2001 From: Marius Gehrmann Date: Wed, 14 Oct 2026 11:35:45 +0200 Subject: [PATCH 3/6] soc: apple: display gate: Enable the J613 internal DCP's route with dcpext The J613 device tree gives the internal DCP a disabled Type-C route to the left-front port, the second pipeline for a display there with the lid closed. It leads to a port that dcpext drives too, and the display driver only uses it while dcpext runs alongside, so it is usable exactly when dcpext is. Enable the internal DCP's routes in the changeset that enables dcpext, its routes, DARTs, mailbox and crossbars, with one more log line: enabled the Type-C routes of /soc/dcp@28ec00000 with it, for a second external display with the lid closed As for the rest of that changeset, the routes must be described and still disabled, and every display crossbar the internal DCP switches must be one of dcpext's, which the changeset enables; otherwise dcpext is refused and nothing is enabled. Without dcpext (no marker, other firmware, refused checks, apple_t8122_display.dcpext=0) the route stays disabled and the internal DCP has no route. Device trees without such a route are unchanged. Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Marius Gehrmann [Chris Kearney: rebased onto siliconforest/next 6a421d4e76ff; Claude-Session trailer dropped] Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com> (cherry picked from commit 1991adfb18b529bcc9fdf4b3243f7e111bcc9511) --- drivers/soc/apple/t6030-display-gate.c | 69 ++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/drivers/soc/apple/t6030-display-gate.c b/drivers/soc/apple/t6030-display-gate.c index 07ba8b9f60f571..1f2c2413b71cf5 100644 --- a/drivers/soc/apple/t6030-display-gate.c +++ b/drivers/soc/apple/t6030-display-gate.c @@ -1849,6 +1849,12 @@ static void __init gate_describe(void) * disabled. Without the marker, with other firmware, or if anything does not * check out, all of them stay disabled and the ATC power states are left * alone. apple_t8122_display.dcpext=0 keeps them disabled too. + * + * The same changeset enables the Type-C routes the device tree gives the + * internal DCP (J613: to the left-front port, for a second external display + * with the lid closed). They lead to ports that dcpext drives too, and only + * the display driver's lid handling makes them usable, so they are enabled + * exactly when dcpext is, and must be disabled until then. */ static bool gate_t8122_dcpext_requested __initdata = true; @@ -1906,12 +1912,49 @@ static bool __init gate_node_in(struct device_node *const *nodes, int n, return false; } -static void __init gate_t8122_dcpext_one(struct device_node *np) +/* + * The internal DCP's Type-C routes, which the gate enables together with + * dcpext: they must be disabled, and every crossbar the internal DCP switches + * must be one of dcpext's (@xbar), which the same changeset enables. Returns + * why not, or NULL. + */ +static const char *__init gate_t8122_dcp_routes_bad(struct device_node *dcp, + struct device_node *routes, + struct device_node *const *xbar, int nxbar) +{ + struct device_node *route; + struct of_phandle_args args; + bool listed; + int i, n; + + for_each_child_of_node(routes, route) { + if (!gate_disabled(route)) { + of_node_put(route); + return "the internal display's Type-C route is not disabled"; + } + } + n = of_count_phandle_with_args(dcp, "mux-controls", "#mux-control-cells"); + if (n < 1) + return "the internal display's Type-C route has no display crossbar"; + for (i = 0; i < n; i++) { + if (of_parse_phandle_with_args(dcp, "mux-controls", "#mux-control-cells", + i, &args)) + return "the internal display's display crossbar is not described"; + listed = gate_node_in(xbar, nxbar, args.np); + of_node_put(args.np); + if (!listed) + return "the internal display's display crossbar is not one of the dcpext's"; + } + return NULL; +} + +static void __init gate_t8122_dcpext_one(struct device_node *np, struct device_node *dcp) { struct device_node *dart = NULL, *mbox = NULL, *piodma = NULL; struct device_node *disp_dart = NULL, *ps = NULL, *entry = NULL, *link; struct device_node *xbar[GATE_T8122_XBARS] = {}; struct device_node *common[GATE_T8122_XBARS] = {}; + struct device_node *dcp_routes = NULL; struct of_phandle_args args; struct of_changeset *cs; const char *label; @@ -1993,6 +2036,15 @@ static void __init gate_t8122_dcpext_one(struct device_node *np) goto put; } + dcp_routes = of_get_child_by_name(dcp, "typec-routes"); + if (dcp_routes) { + const char *why = gate_t8122_dcp_routes_bad(dcp, dcp_routes, xbar, nxbar); + + if (why) { + gate_ext_refuse(np, why); + goto put; + } + } cs = gate_cs_alloc(); if (!cs) { gate_ext_refuse(np, "out of memory"); @@ -2028,6 +2080,8 @@ static void __init gate_t8122_dcpext_one(struct device_node *np) ret = of_changeset_update_prop_string(cs, np, "status", "okay"); if (!ret) ret = gate_ext_routes(cs, np, "okay"); + if (!ret && dcp_routes) + ret = gate_ext_routes(cs, dcp, "okay"); if (!ret) ret = gate_display_add_dart(cs, disp_dart); if (!ret) @@ -2040,7 +2094,11 @@ static void __init gate_t8122_dcpext_one(struct device_node *np) } gate_info("enabled %pOF with its Type-C routes, DARTs, mailbox, %d display crossbar(s) with ATC common power kept on, display DART %pOF in the display subsystem, CPU power floor and PMP DISPEXT request; started by the display driver\n", np, nxbar, disp_dart); + if (dcp_routes) + gate_info("enabled the Type-C routes of %pOF with it, for a second external display with the lid closed\n", + dcp); put: + of_node_put(dcp_routes); for (i = 0; i < GATE_T8122_XBARS; i++) { of_node_put(common[i]); of_node_put(xbar[i]); @@ -2053,12 +2111,13 @@ static void __init gate_t8122_dcpext_one(struct device_node *np) of_node_put(dart); } -static void __init gate_t8122_dcpext(bool panel) +/* @dcp: the internal DCP, if the internal display was handed over. */ +static void __init gate_t8122_dcpext(struct device_node *dcp) { struct device_node *np; for_each_compatible_node(np, NULL, "apple,t8122-dcpext") { - if (!panel) + if (!dcp) gate_ext_refuse(np, "the internal display was not handed over"); else if (!gate_t8122_dcpext_requested) gate_ext_refuse(np, "disabled on the command line"); @@ -2072,7 +2131,7 @@ static void __init gate_t8122_dcpext(bool panel) else if (IS_ENABLED(CONFIG_VIDEO) && video_firmware_drivers_only()) gate_ext_refuse(np, "nomodeset is on the command line"); else - gate_t8122_dcpext_one(np); + gate_t8122_dcpext_one(np, dcp); } } @@ -2141,7 +2200,7 @@ static void __init gate_run(void) if (gate_soc->dcpext) gate_dcpext(panel ? NULL : "the internal display was not handed over"); if (gate_soc == &gate_t8122) - gate_t8122_dcpext(panel); + gate_t8122_dcpext(panel ? np[GATE_DCP] : NULL); gate_reuse_put(); of_node_put(aic); for (i = 0; i < PMP_PS_NR; i++) From 154bc3d3164b2b16db78ccc8af39f612db84b0c3 Mon Sep 17 00:00:00 2001 From: Marius Gehrmann Date: Wed, 14 Oct 2026 11:39:59 +0200 Subject: [PATCH 4/6] drm: apple: Follow the J613 lid: tell the panel's DCP, power the panel off On J613 the internal panel's pipe can also drive a display on the left-front USB-C port, but only once its panel is off: the pipe's first DPTX core drives the panel otherwise. Once told the lid is closed, the firmware takes the pipe for a second external display at every panel power-on and swallows each panel frame; a swallowed frame never completes, the swap times out and the DCP is gone until reboot. For the internal pipe that has a Type-C route (dcp->clamshell), tell the panel's IOMFB the lid state, power the panel off while the lid is closed, and treat a closed lid as a panel that is not there: - Follow the lid switch (SW_LID, the SMC's on Apple laptops) with an input handler, registered once the panel has started. Only a switch with a parent device counts, not one made from user space (uinput). Lid changes are applied from a freezable work item: opening the lid wakes the system, and the change must wait until the DRM device has restored its state. - Tell IOMFB the state with A475 (one u32: 1 open, 2 closed): the real state from the start, then every change. IOMFB hands it to its DPTX power controller at each panel power-on, which refuses anything else. - Closing the lid powers the panel off at once (A472(0)), its CRTC on or not, after a black frame while IOMFB still has the lid open. Presents are serialized, so that black frame and a commit's swap never mix up which framebuffer is on screen. From A475(2) on no frame goes to the panel; one that raced with the lid is dropped under the session lock, not sent. - While the lid is closed the panel's connector reads disconnected, from boot on if the lid is closed then, so compositors switch its CRTC off and never light it. A CRTC switched on anyway leaves the panel off and gets no frames (one warning). - When the lid opens IOMFB is told first (A475(1)), then a panel whose CRTC is on is powered on again (A472(1), colour matrix resent), and its connector reads connected again. - Answer what the firmware sends around those power changes: D2, D591 and D574 as on an external processor, D593 (four bytes once after power-on, meaning not known) with nothing. Other machines, and J613 without the Type-C route, are unchanged. The next patch lets the free pipe drive the display on its route. Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Marius Gehrmann [Chris Kearney: rebased onto siliconforest/next 6a421d4e76ff; resolved a conflict with the H17P power-on path in dcp_poweron(); message and comments reworded; Claude-Session trailer dropped] Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com> [TARS: ported to integration/aurora 7.2.11 (4fdd0fc5): presents already run under v14->lock there (dcp_v14_present_locked()), so present_lock is gone and the lid's black frame is sent under v14->lock together with A475; a frame not sent with the lid closed is handed back as *retired; poweron/poweroff keep integration's backlight DPMS; dcp.c hunks applied by hand; the clamshell unbind releases the cable (integration disables the internal processor's Type-C work without releasing it)] (cherry picked from commit cce117a173359520c5c965d81e8c583c0aab5eb4) --- drivers/gpu/drm/apple/Kconfig | 1 + drivers/gpu/drm/apple/dcp-internal.h | 13 ++ drivers/gpu/drm/apple/dcp.c | 200 ++++++++++++++++++++++++ drivers/gpu/drm/apple/iomfb_v14_7.c | 220 ++++++++++++++++++++++++++- drivers/gpu/drm/apple/iomfb_v14_7.h | 9 ++ 5 files changed, 438 insertions(+), 5 deletions(-) diff --git a/drivers/gpu/drm/apple/Kconfig b/drivers/gpu/drm/apple/Kconfig index 0675e5ac1b2e3c..f4bcb3539d6fb3 100644 --- a/drivers/gpu/drm/apple/Kconfig +++ b/drivers/gpu/drm/apple/Kconfig @@ -15,6 +15,7 @@ config DRM_APPLE select DRM_GEM_DMA_HELPER select VIDEOMODE_HELPERS depends on BACKLIGHT_CLASS_DEVICE + depends on INPUT select MULTIPLEXER select DRM_APPLE_FABRIC_CORE help diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 3eac22a57aa069..6b096b54d7924c 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -315,6 +315,19 @@ struct apple_dcp { /* DCP_FIRMWARE_V_14_7 state; outlives this device once RTKit runs. */ struct apple_dcp_v14 *v14; + /* + * J613: the internal panel's pipe also has a Type-C route. The driver + * tells its firmware the lid state, and powers the panel off while the + * lid is closed (see dcp_lid_*()). While the lid is + * closed the panel's connector is reported disconnected + * (clamshell_panel_away; clamshell_panel_work). + */ + bool clamshell; + struct work_struct clamshell_start_work; + bool clamshell_panel_away; + /* The pipe's one CRTC is on (dcp_poweron() to dcp_poweroff()). */ + bool clamshell_crtc_on; + struct delayed_work clamshell_panel_work; struct apple_dcp_v26 *v26; /* Coprocessor control register */ diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index a28087a92eabf5..f0b759d70076e7 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -2420,6 +2421,184 @@ unsigned long* dcp_get_iomfb_surfaces(struct platform_device *pdev) return dcp->iomfb_surfaces; } +/* + * The lid switch, for the internal pipe that also has a Type-C route + * (dcp->clamshell, J613; one per machine). The driver tells the + * panel's firmware the lid state and powers the panel off while the lid is + * closed (iomfb_v14_7_set_lid()). + * + * A closed lid is a panel that is not there: its connector reads + * disconnected until the lid opens (clamshell_panel_away), from boot on if + * the lid is closed then, so that no compositor lights a panel nobody sees. + * That is also what keeps the firmware alive: once told the lid is closed it + * takes the pipe for a second external display at power-on and swallows + * every panel frame, which then never completes. As the lid opens, IOMFB is + * told first, then the panel's connector comes back. + */ +static DEFINE_MUTEX(dcp_lid_lock); +static struct apple_dcp *dcp_lid_dcp; /* dcp_lid_lock; the handler is registered */ +static atomic_t dcp_lid_closed = ATOMIC_INIT(-1); /* -1: no lid switch yet */ + +/* The panel's connector reads disconnected until the lid opens. */ +static void dcp_clamshell_panel_away(struct apple_dcp *dcp) +{ + struct apple_connector *panel = dcp->fixed_connector; + + cancel_delayed_work(&dcp->clamshell_panel_work); + if (!panel || !READ_ONCE(panel->connected)) + return; + WRITE_ONCE(dcp->clamshell_panel_away, true); + WRITE_ONCE(panel->connected, false); + dcp_queue_hotplug(panel); + dev_info(dcp->dev, "lid closed: %s disconnected until the lid opens\n", + panel->base.name); +} + +static void dcp_lid_work_fn(struct work_struct *work) +{ + int closed = atomic_read(&dcp_lid_closed); + struct apple_dcp *dcp; + + guard(mutex)(&dcp_lid_lock); + dcp = dcp_lid_dcp; + if (!dcp || closed < 0) + return; + iomfb_v14_7_set_lid(dcp, closed); + if (closed) + dcp_clamshell_panel_away(dcp); + else if (READ_ONCE(dcp->clamshell_panel_away)) + mod_delayed_work(system_freezable_wq, &dcp->clamshell_panel_work, 0); +} + +/* + * Freezable: a lid change seen while the system resumes (opening the lid + * wakes it) is applied once the DRM device has restored its state. Applied + * before, it hands the internal pipe back to the panel under the restore + * commit, which then fails for every CRTC. + */ +static DECLARE_WORK(dcp_lid_work, dcp_lid_work_fn); + +static void dcp_clamshell_panel_work(struct work_struct *work) +{ + struct apple_dcp *dcp = container_of(to_delayed_work(work), struct apple_dcp, + clamshell_panel_work); + struct apple_connector *panel = dcp->fixed_connector; + + /* Only once IOMFB heard the lid open. */ + if (!panel || !READ_ONCE(dcp->clamshell_panel_away) || !iomfb_v14_7_lid_open(dcp)) + return; + WRITE_ONCE(dcp->clamshell_panel_away, false); + WRITE_ONCE(panel->connected, true); + dcp_queue_hotplug(panel); + dev_info(dcp->dev, "lid open: %s connected again\n", panel->base.name); +} + +static void dcp_lid_event(struct input_handle *handle, unsigned int type, + unsigned int code, int value) +{ + if (type != EV_SW || code != SW_LID) + return; + atomic_set(&dcp_lid_closed, !!value); + queue_work(system_freezable_wq, &dcp_lid_work); +} + +static int dcp_lid_connect(struct input_handler *handler, struct input_dev *dev, + const struct input_device_id *id) +{ + struct input_handle *handle; + int ret; + + /* A switch made from user space (uinput) has no parent: not the lid. */ + if (!dev->dev.parent) + return -ENODEV; + handle = kzalloc_obj(*handle); + if (!handle) + return -ENOMEM; + handle->dev = dev; + handle->handler = handler; + handle->name = "appledrm-lid"; + ret = input_register_handle(handle); + if (ret) + goto free; + ret = input_open_device(handle); + if (ret) + goto unregister; + atomic_set(&dcp_lid_closed, test_bit(SW_LID, dev->sw)); + pr_info("appledrm: lid switch %s, lid %s\n", dev->name, + test_bit(SW_LID, dev->sw) ? "closed" : "open"); + queue_work(system_freezable_wq, &dcp_lid_work); + return 0; + +unregister: + input_unregister_handle(handle); +free: + kfree(handle); + return ret; +} + +static void dcp_lid_disconnect(struct input_handle *handle) +{ + input_close_device(handle); + input_unregister_handle(handle); + kfree(handle); +} + +static const struct input_device_id dcp_lid_ids[] = { + { + .flags = INPUT_DEVICE_ID_MATCH_EVBIT | INPUT_DEVICE_ID_MATCH_SWBIT, + .evbit = { BIT_MASK(EV_SW) }, + .swbit = { [BIT_WORD(SW_LID)] = BIT_MASK(SW_LID) }, + }, + {} +}; + +static struct input_handler dcp_lid_handler = { + .event = dcp_lid_event, + .connect = dcp_lid_connect, + .disconnect = dcp_lid_disconnect, + .name = "appledrm-lid", + .id_table = dcp_lid_ids, +}; + +static void dcp_lid_start(struct apple_dcp *dcp) +{ + int ret; + + scoped_guard(mutex, &dcp_lid_lock) { + if (dcp_lid_dcp) + return; + dcp_lid_dcp = dcp; + } + ret = input_register_handler(&dcp_lid_handler); + if (!ret) + return; + dev_err(dcp->dev, "lid switch not followed: %d\n", ret); + scoped_guard(mutex, &dcp_lid_lock) + dcp_lid_dcp = NULL; +} + +static void dcp_lid_stop(struct apple_dcp *dcp) +{ + scoped_guard(mutex, &dcp_lid_lock) { + if (dcp_lid_dcp != dcp) + return; + dcp_lid_dcp = NULL; + } + input_unregister_handler(&dcp_lid_handler); + cancel_work_sync(&dcp_lid_work); + atomic_set(&dcp_lid_closed, -1); +} + +/* After the panel started, which the DRM device does not wait for. */ +static void dcp_clamshell_start(struct work_struct *work) +{ + struct apple_dcp *dcp = container_of(work, struct apple_dcp, clamshell_start_work); + + if (iomfb_v14_7_clamshell_start(dcp)) + return; + dcp_lid_start(dcp); +} + int dcp_start(struct platform_device *pdev) { struct apple_dcp *dcp = platform_get_drvdata(pdev); @@ -2445,6 +2624,8 @@ int dcp_start(struct platform_device *pdev) ret = iomfb_v14_7_start(dcp); if (ret) return ret; + if (dcp->clamshell) + schedule_work(&dcp->clamshell_start_work); return 0; } @@ -2761,6 +2942,8 @@ void dcp_poweron(struct platform_device *pdev) return; } + if (dcp->clamshell) + WRITE_ONCE(dcp->clamshell_crtc_on, true); if (dcp_uses_soft_dpms(dcp)) { if (dcp->fw_compat == DCP_FIRMWARE_H17P && dcp->hw.iomfb_method_profile != DCP_IOMFB_METHODS_H17G && @@ -2918,6 +3101,12 @@ void dcp_poweroff(struct platform_device *pdev) if (dcp->avep) av_service_disconnect(dcp); + /* The panel's CRTC is off; with the lid open it can come back. */ + if (dcp->clamshell) { + WRITE_ONCE(dcp->clamshell_crtc_on, false); + if (READ_ONCE(dcp->clamshell_panel_away)) + mod_delayed_work(system_freezable_wq, &dcp->clamshell_panel_work, 0); + } /* * The display this CRTC was powered on for has been unplugged since: @@ -4083,6 +4272,11 @@ VISIBLE_IF_KUNIT void dcp_comp_unbind(struct device *dev, struct device *main, v disable_delayed_work_sync(&dcp->external_retry_wq); disable_delayed_work_sync(&dcp->swap_watchdog_wq); disable_work_sync(&dcp->vblank_wq); + } else if (dcp->clamshell) { + cancel_work_sync(&dcp->clamshell_start_work); + dcp_lid_stop(dcp); + cancel_delayed_work_sync(&dcp->clamshell_panel_work); + dcp_disable_typec_work(dcp, true); } else if (!dcp->external) { dcp_disable_typec_work(dcp, false); } @@ -4609,6 +4803,12 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) return ret; routes_failed = true; } + /* J613: the panel's pipe with a Type-C route the display gate enabled. */ + dcp->clamshell = dcp_is_v14_7_panel(dcp) && dcp->nr_typec_routes; + if (dcp->clamshell) { + INIT_WORK(&dcp->clamshell_start_work, dcp_clamshell_start); + INIT_DELAYED_WORK(&dcp->clamshell_panel_work, dcp_clamshell_panel_work); + } /* * The manual T6030 external path is not part of the panel's DRM diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 7a6c96dfa1f74f..63e16f392ae096 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -308,6 +308,16 @@ struct apple_dcp_v14 { struct work_struct stopped_work; bool failed; bool started; + /* + * J613 internal pipe with a Type-C route (iomfb_v14_7_clamshell_start()): + * the lid state IOMFB was told (A475: 1 open, 2 closed, 0 not yet), the + * panel's CRTC is off (panel_blank), and the panel was powered off + * because the lid is closed (panel_off, A472(0)). + */ + bool clamshell; + u32 lid; + bool panel_blank; + bool panel_off; /* * An external processor: no panel. Its modes come from the @@ -1172,6 +1182,18 @@ static int dcp_v14_callback(void *cookie, u32 tag, const void *input, u32 in_siz } #define SHAPE(i, o) (in_size == (i) && out_size == (o)) + /* + * J613: powering the panel off and on with the lid closed sends what an + * external pipe's power changes send (see dcp_v14_external_callback()), + * and D593 with four bytes once after power-on, whose meaning is not + * known. Nothing to do for any of them. + */ + if (v14->clamshell && + ((tag == D(2) && SHAPE(0, 0)) || (tag == D(591) && SHAPE(20, 0)) || + (tag == D(593) && SHAPE(4, 0)))) + return 0; + if (v14->clamshell && tag == D(574) && SHAPE(4, 4)) + return in[0] > 1 ? -EINVAL : 0; /* * Backlight correction notification: one signed 32-bit factor, no * reply payload. The firmware applies it when the colour matrix @@ -2800,6 +2822,17 @@ static int dcp_v14_ctm_locked(struct apple_dcp_v14 *v14, static bool dcp_v14_external_described(struct apple_dcp_v14 *v14); +/* + * J613: the panel takes no swaps while the lid is closed. Once IOMFB was + * told so (A475(2)) it treats the pipe as one for a second external display + * and swallows panel swaps, which then never complete and stop the DCP; a + * panel powered off with the lid closed shows nothing anyway. + */ +static bool dcp_v14_panel_dark(struct apple_dcp_v14 *v14) +{ + return !v14->external && (READ_ONCE(v14->panel_off) || READ_ONCE(v14->lid) == 2); +} + struct dcp_v14_swap_admission { struct apple_dcp_v14 *v14; u64 generation; @@ -2888,6 +2921,15 @@ static int dcp_v14_swap_locked(struct apple_dcp_v14 *v14, const u8 *surface, u64 if (!swap) return -ENOMEM; + /* + * The lid closed since the caller looked (iomfb_v14_7_set_lid()): the + * firmware takes no swap for the panel now, and one it swallows never + * completes. Nothing is sent; the caller drops the frame. + */ + if (dcp_v14_panel_dark(v14)) { + kfree(swap); + return -EAGAIN; + } admission.generation = v14->description_generation; /* * A colour matrix the firmware refuses costs night light, not the @@ -2985,6 +3027,11 @@ static bool dcp_v14_present_locked(struct apple_dcp_v14 *v14, struct drm_framebu dev_warn_ratelimited(v14->dev, "display refused a swap: %d\n", ret); return false; } + /* Not sent, the lid is closed: the panel keeps what it showed. */ + if (ret == -EAGAIN && !v14->external && !READ_ONCE(v14->failed)) { + *retired = fb; + return false; + } if (ret) { /* Either framebuffer may still be scanned out: keep both. */ dev_err(v14->dev, "native DCP flip failed %d; buffers pinned until reboot\n", ret); @@ -3164,8 +3211,8 @@ static int dcp_v14_set_mode(struct apple_dcp_v14 *v14, u32 color, u32 timing) return ret ?: (le32_to_cpu(status) ? -EIO : 0); } -/* A472: the display's power state; its status sits at byte 4 of the reply. */ -static int dcp_v14_set_power(struct apple_dcp_v14 *v14, bool on) +/* A472: the power state; its status sits at byte 4 of the reply. */ +static int dcp_v14_power_call(struct apple_dcp_v14 *v14, bool on) { u8 in[12] = {}, out[8] = {}; int ret; @@ -3174,6 +3221,14 @@ static int dcp_v14_set_power(struct apple_dcp_v14 *v14, bool on) ret = dcp_v14_call(v14, A(472), in, sizeof(in), out, sizeof(out), 0); if (!ret && get_unaligned_le32(out + 4)) ret = -EIO; + return ret; +} + +/* The attached display's power state (A472). */ +static int dcp_v14_set_power(struct apple_dcp_v14 *v14, bool on) +{ + int ret = dcp_v14_power_call(v14, on); + /* A refused power-down leaves nothing on: the display is gone already. */ if (!ret || (!on && !READ_ONCE(v14->failed))) v14->powered = on; @@ -3407,6 +3462,17 @@ void iomfb_v14_7_flush(struct apple_dcp *dcp, struct drm_crtc *crtc, if (!cs) cs = crtc->state; fb = p && p->visible ? p->fb : NULL; + /* + * No swaps to the panel while the lid is closed (dcp_v14_panel_dark()). + * The panel is powered off, so nothing reads the framebuffers earlier + * commits displaced: release them here as a completed swap would, or + * they pile up for as long as the compositor keeps the CRTC on. + */ + if (!dcp->external && dcp->v14 && dcp_v14_panel_dark(dcp->v14)) { + dcp_release_all_retained_framebuffers(dcp); + dcp_drm_crtc_vblank(dcp->crtc); + return; + } if (fb) dcp_v14_plane_scanout_size(p, dcp->external, &width, &height); /* @@ -3436,11 +3502,145 @@ void iomfb_v14_7_flush(struct apple_dcp *dcp, struct drm_crtc *crtc, } } +/* + * J613, with the lid closed: power the panel off (A472(0)), whether or + * not its CRTC is still on. The panel's connector + * reads disconnected while the lid is closed (see dcp_lid_work_fn()), and a + * compositor switches the CRTC off in its own time; until then its frames + * are dropped (dcp_v14_panel_dark()). Called with the lock held. + */ +static void dcp_v14_panel_off_locked(struct apple_dcp_v14 *v14) +{ + int ret; + + if (v14->lid != 2 || v14->panel_off || v14->failed) + return; + ret = dcp_v14_power_call(v14, false); + if (!ret) + v14->panel_off = true; + dev_info(v14->dev, "lid closed: panel off (A472(0)): %d\n", ret); +} + +/* + * J613: power the panel on again (A472(1)), its colour matrix with it, once + * IOMFB knows the lid is open: with the lid closed IOMFB takes the pipe for + * a second external display at power-on and the panel gets no frame. Called + * with the lock held. + */ +static void dcp_v14_panel_on_locked(struct apple_dcp_v14 *v14) +{ + int ret; + + if (v14->lid == 2 || !v14->panel_off || v14->failed) + return; + ret = dcp_v14_power_call(v14, true); + if (!ret) { + v14->panel_off = false; + v14->ctm_valid = false; + } + if (ret) + dev_warn(v14->dev, "panel on (A472(1)) refused: %d; it stays dark until its CRTC is switched on again\n", + ret); + else + dev_info(v14->dev, "panel on (A472(1)): 0\n"); +} + +/* + * J613: the internal pipe has a Type-C route, which only a powered-off panel + * leaves free; from now on the lid state counts (iomfb_v14_7_set_lid()). + */ +int iomfb_v14_7_clamshell_start(struct apple_dcp *dcp) +{ + struct apple_dcp_v14 *v14 = dcp->v14; + + if (!v14 || !v14->started || READ_ONCE(v14->failed)) + return -ENODEV; + mutex_lock(&v14->lock); + v14->clamshell = true; + mutex_unlock(&v14->lock); + return 0; +} + +bool iomfb_v14_7_lid_open(struct apple_dcp *dcp) +{ + return dcp->v14 && READ_ONCE(dcp->v14->lid) == 1; +} + +/* + * Tells the panel's IOMFB the lid state (A475, one u32: 1 + * open, 2 closed; no reply). IOMFB keeps the value and hands it to its DPTX + * power controller at every panel power-on, which refuses any other value, + * so the real state is sent from the start. + * + * With the lid closed the panel is powered off, its CRTC on or not, after a + * black frame while IOMFB still has the lid open; from A475(2) on no frame + * goes to the panel (dcp_v14_panel_dark()). With the lid open a panel whose + * CRTC is on is powered on again, after A475(1). + */ +void iomfb_v14_7_set_lid(struct apple_dcp *dcp, bool closed) +{ + struct apple_dcp_v14 *v14 = dcp->v14; + __le32 state = cpu_to_le32(closed ? 2 : 1); + struct drm_framebuffer *retired = NULL; + int ret; + + if (!v14 || !READ_ONCE(v14->clamshell)) + return; + mutex_lock(&v14->lock); + if (v14->failed) + goto out; + /* The black frame and the lid state under one lock: no swap between. */ + if (closed && v14->lid != 2 && !v14->panel_off && !v14->panel_blank) + dcp_v14_present_locked(v14, NULL, 0, 0, 0, NULL, &retired); + if (v14->lid != le32_to_cpu(state)) { + ret = dcp_v14_call(v14, A(475), &state, sizeof(state), NULL, 0, 0); + dev_info(dcp->dev, "lid %s: display firmware told (A475(%u)): %d\n", + closed ? "closed" : "open", le32_to_cpu(state), ret); + if (ret) + goto out; + v14->lid = le32_to_cpu(state); + } + if (closed) { + dcp_v14_panel_off_locked(v14); + /* + * A panel CRTC that was never switched on (the lid was closed + * at boot, and the panel's connector has read disconnected + * since) is as blank as one switched off. + */ + if (!READ_ONCE(dcp->clamshell_crtc_on)) + v14->panel_blank = true; + } else if (!v14->panel_blank) { + dcp_v14_panel_on_locked(v14); + } +out: + mutex_unlock(&v14->lock); + if (retired) + drm_framebuffer_put(retired); +} + void iomfb_v14_7_poweron(struct apple_dcp *dcp) { + struct apple_dcp_v14 *v14 = dcp->v14; + /* The next present restores a level only after Linux has set one. */ if (dcp->v14 && dcp_v14_bl_dpms_applies(dcp->v14, dcp)) dcp_backlight_dpms(dcp, true); + + /* The panel stays on; the next swap shows the plane again. */ + if (dcp->external || !v14 || !v14->started) + return; + mutex_lock(&v14->lock); + v14->panel_blank = false; + /* + * Powered off with the lid closed: on again once the lid is open. A + * CRTC switched on while it is closed (the panel's connector reads + * disconnected then) leaves the panel off and gets no frames; the lid + * opening powers the panel on (iomfb_v14_7_set_lid()). + */ + if (v14->panel_off && v14->lid == 2) + dev_warn_once(dcp->dev, "panel CRTC on with the lid closed: the panel stays off until the lid opens\n"); + dcp_v14_panel_on_locked(v14); + mutex_unlock(&v14->lock); } void iomfb_v14_7_poweroff(struct apple_dcp *dcp) @@ -3461,9 +3661,19 @@ void iomfb_v14_7_poweroff(struct apple_dcp *dcp) dcp_mode_invalidate(&dcp->mode_state); return; } - /* Blank to black; the panel and the DCP stay powered. */ - if (v14 && v14->started) - dcp_v14_blank(v14, dcp); + /* + * Blank to black; the panel and the DCP stay powered, unless the lid + * is closed (J613): then the panel is powered off. + */ + if (v14 && v14->started) { + if (!dcp_v14_panel_dark(v14)) + dcp_v14_blank(v14, dcp); + mutex_lock(&v14->lock); + v14->panel_blank = true; + if (v14->clamshell) + dcp_v14_panel_off_locked(v14); + mutex_unlock(&v14->lock); + } } #if IS_ENABLED(CONFIG_DRM_APPLE_DISPLAY_KUNIT_TEST) diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.h b/drivers/gpu/drm/apple/iomfb_v14_7.h index 7eb6ada2b1c26c..3defdc0e15caf1 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.h +++ b/drivers/gpu/drm/apple/iomfb_v14_7.h @@ -79,6 +79,15 @@ void iomfb_v14_7_unbind(struct apple_dcp *dcp); void iomfb_v14_7_remove(struct apple_dcp *dcp); /* DCPLink, start signal, first client open and the panel mode. */ int iomfb_v14_7_start(struct apple_dcp *dcp); +/* + * J613 internal pipe with a Type-C route (dcp->clamshell): _clamshell_start() + * after the panel started; _set_lid() tells IOMFB the lid state and powers + * the panel off with the lid closed, on again with it open if its CRTC is on. + */ +int iomfb_v14_7_clamshell_start(struct apple_dcp *dcp); +void iomfb_v14_7_set_lid(struct apple_dcp *dcp, bool closed); +/* IOMFB was told the lid is open (A475(1)). */ +bool iomfb_v14_7_lid_open(struct apple_dcp *dcp); int iomfb_v14_7_atomic_check(struct apple_dcp *dcp, struct drm_crtc *crtc, struct drm_atomic_commit *state); From 1c93ff983c97bdde6f9c3038d0211687a4954e30 Mon Sep 17 00:00:00 2001 From: Marius Gehrmann Date: Thu, 15 Oct 2026 22:38:30 +0200 Subject: [PATCH 5/6] drm: apple: Let the J613 internal pipe drive a Type-C display, lid closed With the lid closed the panel is off, and the internal DCP's pipe, whose first DPTX core is free then, can drive a second external display on the left-front USB-C port. When the lid opens, the pipe goes back to the panel. Do this on J613, where the internal pipe has a Type-C route to that port (dcp->clamshell). Fabric. The internal pipe counts as busy unless the lid is closed and the panel and its CRTC are off (dcp->clamshell_free). While it is free it ranks as a plain pipeline, before dcpext: with the panel's connector disconnected, aquamarine (Hyprland) gives a display coming up on the left-front port the lowest free CRTC it can use, the internal pipe's, and the route follows that pairing (routed to dcpext instead, every mode failed the atomic check, and the panel found its only CRTC taken on lid open). When the pipe becomes free it takes a display left waiting, as any pipeline that is let go of (dcp_typec_pipeline_changed()). When the lid opens, that display lets go of the pipe first (powered off, HPD dropped, route taken down), then IOMFB is told; the display goes to dcpext if dcpext is free, or waits again with the usual log line. Lid open, order. The panel's connector comes back once the compositor switched the pipe's CRTC off, or two seconds later, and at once if the CRTC was off already. A left-front display that leaves the pipe for a free dcpext reads disconnected until then (at most 3 s) and is reported 200 ms after the panel, so that the compositor gives the panel its CRTC back and the display dcpext's. Session. Once the panel has started, the DPTX endpoint is started for the route, and the DPAV controller endpoint with it, as for the T6030 external processors. The DPTX RemotePort unit 0 (core 0, crossbar dispext0,0) is DPTX port 0; the panel's own DPTX port is never bound. The link is connected as on the T8122 dcpext (dcp_ext_dptx_607()), the same firmware image. Call 25, which the firmware sends to each RemotePort when the lid state changes, is acknowledged quietly. While the route is active (v14->typec) the session handles the display description as on an external processor; the mode is set with A472(1) before A411 (with A411 alone the mode is set, the pipe not enabled), its CRTC off powers it off (A472(0)), swaps are sized to its mode (a larger framebuffer is shown from its top-left, as on an external processor) without notch rows or the panel's colour matrix, and a refused swap does not end the session. When the route is taken down, the panel's one mode is the pipe's catalog again. Other machines are unchanged, and so is J613 without the route. Link: https://github.com/AsahiLinux/linux/pull/607 Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Marius Gehrmann [Chris Kearney: rebased onto siliconforest/next 6a421d4e76ff; resolved a conflict with DPTX_APCALL_GET_VIRTUAL_DEVICE_MODE in dptxep.h; call 25 still gets -EOPNOTSUPP on the H17P external processor, as any call it does not handle; message and comments reworded; Claude-Session trailer dropped] Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com> [TARS: ported to integration/aurora 7.2.11 (4fdd0fc5): the panel's own port is skipped in dptxport_prepare(), where 7.2 binds DPTX services; call 25 as before; dcp_ext_dptx_607() keeps integration's fw_14_7_endpoints check; detect keeps dcp_connector_connected(); swaps keep lockdep and backlight present state; the lid's black frame (under v14->lock) skips a Type-C display; poweron does not restore the panel backlight while the pipe drives a Type-C display; fabric and dcp.c hunks applied by hand] (cherry picked from commit cdd32c061dec) --- drivers/gpu/drm/apple/apple_drv.c | 6 +- drivers/gpu/drm/apple/dcp-fabric.c | 69 ++++++- drivers/gpu/drm/apple/dcp-fabric.h | 1 + drivers/gpu/drm/apple/dcp-internal.h | 26 ++- drivers/gpu/drm/apple/dcp.c | 139 +++++++++++++- drivers/gpu/drm/apple/dcp.h | 1 + drivers/gpu/drm/apple/dptxep.c | 9 +- drivers/gpu/drm/apple/dptxep.h | 2 + drivers/gpu/drm/apple/iomfb.c | 7 +- drivers/gpu/drm/apple/iomfb_v14_7.c | 261 ++++++++++++++++++++++++--- drivers/gpu/drm/apple/iomfb_v14_7.h | 13 ++ 11 files changed, 487 insertions(+), 47 deletions(-) diff --git a/drivers/gpu/drm/apple/apple_drv.c b/drivers/gpu/drm/apple/apple_drv.c index 9028388b33915b..6e955d6892e3a4 100644 --- a/drivers/gpu/drm/apple/apple_drv.c +++ b/drivers/gpu/drm/apple/apple_drv.c @@ -222,8 +222,10 @@ apple_connector_detect(struct drm_connector *connector, bool force) return connector_status_connected; if (connector->force == DRM_FORCE_OFF) return connector_status_disconnected; - return dcp_connector_connected(apple_connector) ? - connector_status_connected : connector_status_disconnected; + if (!dcp_connector_connected(apple_connector) || + dcp_clamshell_hold_connector(apple_connector)) + return connector_status_disconnected; + return connector_status_connected; } static void apple_connector_oob_hotplug(struct drm_connector *connector, diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 04aaaa37c511ff..f1db9689547131 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -142,6 +142,12 @@ 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; + /* + * The J613 internal pipe drives its panel, with its first DPTX core, + * unless the lid is closed and the panel off (dcp->clamshell_free). + */ + if (dcp->clamshell) + return !READ_ONCE(dcp->clamshell_free); /* * 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 @@ -413,6 +419,8 @@ static int dcp_typec_route_activate(struct apple_dcp_typec_route *route, route->selected = true; dev_info(dcp->dev, "allocated Type-C DPTX PHY %u\n", route->dptx_phy); + if (dcp->clamshell) + dcp_clamshell_attached(dcp, route->port->connector_np); return 0; } @@ -514,6 +522,8 @@ static int dcp_typec_route_deactivate(struct apple_dcp_typec_route *route) dcp->typec_connector = NULL; dcp->connector = dcp->fixed_connector; WRITE_ONCE(dcp->ext_backlight, false); + if (dcp->clamshell && ended_attachment) + iomfb_v14_7_typec_detach(dcp); if (dcp->fixed_connector_type != DRM_MODE_CONNECTOR_USB) { dcp->connector_type = dcp->fixed_connector_type; @@ -888,7 +898,16 @@ static void dcp_fabric_snapshot_port(struct apple_dcp_typec_port *port, pipeline->bound = !!dcp->crtc; pipeline->crtc_index = dcp->crtc ? drm_crtc_index(&dcp->crtc->base) : 0; - pipeline->has_fixed = !!dcp->fixed_phy; + /* + * The J613 internal pipe is a hybrid too, its panel being its + * fixed output, but not while the lid is closed and the panel + * off: the panel's connector reads disconnected then, and + * aquamarine gives a display on the left-front port the lowest + * free CRTC it can use, the internal pipe's (index 0, below + * dcpext's). Ranked as a plain pipeline, the route follows. + */ + pipeline->has_fixed = !!dcp->fixed_phy || + (dcp->clamshell && !READ_ONCE(dcp->clamshell_free)); pipeline->fixed_busy = dcp_typec_route_busy(route); pipeline->owned = !!dcp->active_typec_route || dcp_typec_safe_held(dcp); @@ -1275,6 +1294,54 @@ void dcp_typec_reorder(void) dcp_typec_rebalance_locked(NULL, 0); } +static int dcp_typec_port_busy(struct apple_dcp_typec_port *port, + struct apple_dcp *dcp); + +/* + * The J613 internal pipe became free for a Type-C display (lid closed, panel + * off) or is its panel's again (the lid opens). Free: it takes a port left + * waiting for a pipeline, as any pipeline that is let go of, ranked before + * dcpext while it is free (see dcp_fabric_snapshot_port()). Its panel's + * again: the display it drives lets go of it (powered off first), and + * goes to another free pipeline or waits again. Returns whether + * a display let go. + */ +bool dcp_typec_pipeline_changed(struct apple_dcp *dcp) +{ + bool gave_back = false; + unsigned int i; + + guard(mutex)(&dcp_typec_fabric_lock); + + if (!dcp_typec_route_fixed_output_busy(&dcp->typec_routes[0])) { + dcp_typec_pipeline_freed(); + return false; + } + for (i = 0; i < dcp->nr_typec_routes; i++) { + struct apple_dcp_typec_route *route = &dcp->typec_routes[i]; + struct apple_dcp_typec_port *port = route->port; + + if (port->owner != route || route->tunnel) + continue; + iomfb_v14_7_typec_power_off(dcp); + if (port->hpd || dcp->typec_cable_connected || + (dcp->typec_connector && dcp->typec_connector->connected)) + dcp_dptx_disconnect_oob(to_platform_device(dcp->dev), 0); + port->hpd = false; + dcp_typec_route_deactivate(route); + port->owner = NULL; + port->applied_valid = false; + gave_back = true; + /* dcpext may be free by now. */ + dcp_typec_pipeline_freed(); + if (!port->owner && port->dp_wanted && port->dp_hpd) { + port->busy_noted = false; + dcp_typec_port_busy(port, dcp); + } + } + return gave_back; +} + static int dcp_typec_route_prepare(struct typec_mux_dev *mux, struct typec_mux_state *state) { diff --git a/drivers/gpu/drm/apple/dcp-fabric.h b/drivers/gpu/drm/apple/dcp-fabric.h index 468b08ecfbacf0..32e0d70e2911da 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.h +++ b/drivers/gpu/drm/apple/dcp-fabric.h @@ -73,6 +73,7 @@ void dcp_typec_port_set_connector(unsigned int idx, bool secondary, bool dcp_typec_dual_stream(void); bool dcp_typec_ordered_dual_stream(void); void dcp_typec_reorder(void); +bool dcp_typec_pipeline_changed(struct apple_dcp *dcp); bool dcp_typec_follows_crtc(struct apple_dcp *dcp); bool dcp_typec_follow_holder_off(struct drm_atomic_commit *state, struct drm_crtc *crtc, struct apple_connector *connector, struct apple_dcp *back); diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 6b096b54d7924c..dcce86d5dd7e79 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -317,13 +317,19 @@ struct apple_dcp { struct apple_dcp_v14 *v14; /* * J613: the internal panel's pipe also has a Type-C route. The driver - * tells its firmware the lid state, and powers the panel off while the - * lid is closed (see dcp_lid_*()). While the lid is - * closed the panel's connector is reported disconnected - * (clamshell_panel_away; clamshell_panel_work). + * tells its firmware the lid state, and powers the panel off with its + * CRTC while the lid is closed (see dcp_lid_*()). */ bool clamshell; struct work_struct clamshell_start_work; + /* + * The lid is closed and the panel and its CRTC off: the route may + * drive a Type-C display (see dcp_typec_pipeline_changed()). While the + * lid is closed, and until a display that had the pipe let go of it, + * the panel's connector is reported disconnected (clamshell_panel_away; + * clamshell_panel_work). + */ + bool clamshell_free; bool clamshell_panel_away; /* The pipe's one CRTC is on (dcp_poweron() to dcp_poweroff()). */ bool clamshell_crtc_on; @@ -767,20 +773,24 @@ static inline bool dcp_is_v14_7_panel(const struct apple_dcp *dcp) /* Native external pipe: start its audio endpoint, once, after the first EDID. */ void dcp_external_audio_start(struct apple_dcp *dcp); +/* J613 internal pipe: its route took a Type-C display; the lid changed. */ +void dcp_clamshell_attached(struct apple_dcp *dcp, struct device_node *connector_np); +void dcp_clamshell_changed(struct apple_dcp *dcp); /* * The T8122 dcpext connects its DPTX port as AsahiLinux/linux#607 does, * the kernel this firmware was first driven with: no validation of the * target, a connect without the supports-HPD attribute, GET_SUPPORTS_HPD * answered with 0, HPD asserted only after the link-configuration wait, - * and tiled display hints acknowledged with 0. Every other DCP keeps the - * Type-C model of dcp_dptx_connect_session(). + * and tiled display hints acknowledged with 0. So does the J613 internal + * pipe on its Type-C route, whose firmware is the same image. Every other + * DCP keeps the Type-C model of dcp_dptx_connect_session(). */ static inline bool dcp_ext_dptx_607(const struct apple_dcp *dcp) { /* Not the generic M1/M2 DCPs this tree also runs on the template. */ - return dcp->fw_compat == DCP_FIRMWARE_V_14_7_TEMPLATE && - dcp->hw.fw_14_7_endpoints; + return (dcp->fw_compat == DCP_FIRMWARE_V_14_7_TEMPLATE && + dcp->hw.fw_14_7_endpoints) || dcp->clamshell; } #define DCP_AUDIO_MAX_CHANS 15 diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index f0b759d70076e7..59bbd33d81a5e9 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -2432,13 +2432,39 @@ unsigned long* dcp_get_iomfb_surfaces(struct platform_device *pdev) * the lid is closed then, so that no compositor lights a panel nobody sees. * That is also what keeps the firmware alive: once told the lid is closed it * takes the pipe for a second external display at power-on and swallows - * every panel frame, which then never completes. As the lid opens, IOMFB is - * told first, then the panel's connector comes back. + * every panel frame, which then never completes. + * + * With the lid closed and the panel and its CRTC off the pipe is free for + * the display on its Type-C route (dcp->clamshell_free), and ranked before + * dcpext then: the compositor gives that display the pipe's one CRTC, the + * lowest free one (see dcp_typec_pipeline_changed()). When the lid + * opens, that display lets go of the pipe first, then IOMFB is told; the + * panel's connector comes back once the compositor switched the CRTC off, + * or two seconds later, and at once if no display had the pipe. A display + * that goes to dcpext meanwhile reads disconnected until then + * (DCP_CLAMSHELL_HOLD_MS), so that the panel has its CRTC back first. */ static DEFINE_MUTEX(dcp_lid_lock); static struct apple_dcp *dcp_lid_dcp; /* dcp_lid_lock; the handler is registered */ static atomic_t dcp_lid_closed = ATOMIC_INIT(-1); /* -1: no lid switch yet */ +#define DCP_CLAMSHELL_PANEL_MS 2000 + +/* + * As the lid opens, a display on the left-front port that leaves the + * internal pipe for a free dcpext reads disconnected until the panel's + * connector is connected again, for at most this long: aquamarine pairs a + * connector that comes up with the lowest free CRTC it can use, and until + * the panel's connector is back that is the internal pipe's, which the + * panel needs (its only one) and the display is not routed to. + */ +#define DCP_CLAMSHELL_HOLD_MS 3000 + +static unsigned long dcp_clamshell_hold_until; /* jiffies */ +static struct apple_connector *dcp_clamshell_held; +static void dcp_clamshell_hold_work_fn(struct work_struct *work); +static DECLARE_DELAYED_WORK(dcp_clamshell_hold_work, dcp_clamshell_hold_work_fn); + /* The panel's connector reads disconnected until the lid opens. */ static void dcp_clamshell_panel_away(struct apple_dcp *dcp) { @@ -2457,17 +2483,40 @@ static void dcp_clamshell_panel_away(struct apple_dcp *dcp) static void dcp_lid_work_fn(struct work_struct *work) { int closed = atomic_read(&dcp_lid_closed); + bool gave_back = false, free; struct apple_dcp *dcp; guard(mutex)(&dcp_lid_lock); dcp = dcp_lid_dcp; if (!dcp || closed < 0) return; + if (!closed && READ_ONCE(dcp->clamshell_panel_away)) { + WRITE_ONCE(dcp_clamshell_hold_until, + jiffies + msecs_to_jiffies(DCP_CLAMSHELL_HOLD_MS)); + mod_delayed_work(system_freezable_wq, &dcp_clamshell_hold_work, + msecs_to_jiffies(DCP_CLAMSHELL_HOLD_MS)); + } + if (!closed && READ_ONCE(dcp->clamshell_free)) { + WRITE_ONCE(dcp->clamshell_free, false); + gave_back = dcp_typec_pipeline_changed(dcp); + } iomfb_v14_7_set_lid(dcp, closed); if (closed) dcp_clamshell_panel_away(dcp); - else if (READ_ONCE(dcp->clamshell_panel_away)) - mod_delayed_work(system_freezable_wq, &dcp->clamshell_panel_work, 0); + free = closed && iomfb_v14_7_clamshell_panel_off(dcp); + if (free != READ_ONCE(dcp->clamshell_free)) { + WRITE_ONCE(dcp->clamshell_free, free); + gave_back |= dcp_typec_pipeline_changed(dcp); + } + /* + * A display that let go of the pipe still has the CRTC until the + * compositor switches it off (dcp_poweroff() brings the panel back + * then); if it has done so already, the panel comes back at once. + */ + if (!closed && READ_ONCE(dcp->clamshell_panel_away)) + mod_delayed_work(system_freezable_wq, &dcp->clamshell_panel_work, + gave_back && READ_ONCE(dcp->clamshell_crtc_on) ? + msecs_to_jiffies(DCP_CLAMSHELL_PANEL_MS) : 0); } /* @@ -2478,19 +2527,83 @@ static void dcp_lid_work_fn(struct work_struct *work) */ static DECLARE_WORK(dcp_lid_work, dcp_lid_work_fn); +/* The panel was powered off or on: the pipe may be free, or not. */ +void dcp_clamshell_changed(struct apple_dcp *dcp) +{ + queue_work(system_freezable_wq, &dcp_lid_work); +} + +/* + * The route took a display, with the lid closed and the panel off (from the + * fabric, under its lock): the pipe drives that display now. + */ +void dcp_clamshell_attached(struct apple_dcp *dcp, struct device_node *connector_np) +{ + const char *label = NULL; + + iomfb_v14_7_typec_attach(dcp); + of_property_read_string(connector_np, "label", &label); + dev_info(dcp->dev, "lid closed, panel off: the internal pipe drives the display on %s\n", + label ?: "a USB-C port"); + /* already disconnected with the lid closed; makes sure */ + dcp_clamshell_panel_away(dcp); +} + static void dcp_clamshell_panel_work(struct work_struct *work) { struct apple_dcp *dcp = container_of(to_delayed_work(work), struct apple_dcp, clamshell_panel_work); struct apple_connector *panel = dcp->fixed_connector; - /* Only once IOMFB heard the lid open. */ - if (!panel || !READ_ONCE(dcp->clamshell_panel_away) || !iomfb_v14_7_lid_open(dcp)) + /* Only once the display let go and IOMFB heard the lid open. */ + if (!panel || !READ_ONCE(dcp->clamshell_panel_away) || + READ_ONCE(dcp->active_typec_route) || !iomfb_v14_7_lid_open(dcp)) return; WRITE_ONCE(dcp->clamshell_panel_away, false); WRITE_ONCE(panel->connected, true); dcp_queue_hotplug(panel); dev_info(dcp->dev, "lid open: %s connected again\n", panel->base.name); + /* a held display after it, so that it is paired on its own */ + if (READ_ONCE(dcp_clamshell_held)) + mod_delayed_work(system_freezable_wq, &dcp_clamshell_hold_work, + msecs_to_jiffies(200)); +} + +/* + * Should @connector, a Type-C connector that has just come up, still read + * disconnected (see DCP_CLAMSHELL_HOLD_MS)? Only one the compositor has + * not seen connected and that could be paired with the internal pipe's CRTC. + */ +bool dcp_clamshell_hold_connector(struct apple_connector *connector) +{ + struct apple_dcp *dcp = READ_ONCE(dcp_lid_dcp); + + if (!dcp || !dcp->crtc || connector == dcp->fixed_connector || + connector->base.status == connector_status_connected || + !(connector->candidate_crtcs & BIT(drm_crtc_index(&dcp->crtc->base))) || + atomic_read(&dcp_lid_closed) != 0 || + !READ_ONCE(dcp->clamshell_panel_away) || + !time_before(jiffies, READ_ONCE(dcp_clamshell_hold_until))) + return false; + if (xchg(&dcp_clamshell_held, connector) != connector) + dev_info(dcp->dev, "lid open: %s reads disconnected until %s is connected again\n", + connector->base.name, + dcp->fixed_connector ? dcp->fixed_connector->base.name : "the panel"); + return true; +} + +/* The panel's connector is back, or the hold ran out: report the display. */ +static void dcp_clamshell_hold_work_fn(struct work_struct *work) +{ + struct apple_connector *held = xchg(&dcp_clamshell_held, NULL); + struct apple_dcp *dcp = READ_ONCE(dcp_lid_dcp); + + if (!held) + return; + if (dcp && READ_ONCE(dcp->clamshell_panel_away)) + dev_info(dcp->dev, "lid open: %s connected after %u ms without the panel\n", + held->base.name, DCP_CLAMSHELL_HOLD_MS); + dcp_queue_hotplug(held); } static void dcp_lid_event(struct input_handle *handle, unsigned int type, @@ -2586,6 +2699,8 @@ static void dcp_lid_stop(struct apple_dcp *dcp) } input_unregister_handler(&dcp_lid_handler); cancel_work_sync(&dcp_lid_work); + cancel_delayed_work_sync(&dcp_clamshell_hold_work); + WRITE_ONCE(dcp_clamshell_held, NULL); atomic_set(&dcp_lid_closed, -1); } @@ -2596,6 +2711,7 @@ static void dcp_clamshell_start(struct work_struct *work) if (iomfb_v14_7_clamshell_start(dcp)) return; + /* The bind already enabled the Type-C work (dcp_enable_typec_work()). */ dcp_lid_start(dcp); } @@ -3101,7 +3217,11 @@ void dcp_poweroff(struct platform_device *pdev) if (dcp->avep) av_service_disconnect(dcp); - /* The panel's CRTC is off; with the lid open it can come back. */ + /* + * The CRTC a Type-C display let go of: its panel can come back now, + * or, while the lid work is still taking that display's route down, + * once it has (see dcp_lid_work_fn()). + */ if (dcp->clamshell) { WRITE_ONCE(dcp->clamshell_crtc_on, false); if (READ_ONCE(dcp->clamshell_panel_away)) @@ -4275,6 +4395,11 @@ VISIBLE_IF_KUNIT void dcp_comp_unbind(struct device *dev, struct device *main, v } else if (dcp->clamshell) { cancel_work_sync(&dcp->clamshell_start_work); dcp_lid_stop(dcp); + /* A display on the route lets go of the pipe going away. */ + if (READ_ONCE(dcp->clamshell_free)) { + WRITE_ONCE(dcp->clamshell_free, false); + dcp_typec_pipeline_changed(dcp); + } cancel_delayed_work_sync(&dcp->clamshell_panel_work); dcp_disable_typec_work(dcp, true); } else if (!dcp->external) { diff --git a/drivers/gpu/drm/apple/dcp.h b/drivers/gpu/drm/apple/dcp.h index baff04ee201e72..38f50f8b34fa9c 100644 --- a/drivers/gpu/drm/apple/dcp.h +++ b/drivers/gpu/drm/apple/dcp.h @@ -64,6 +64,7 @@ int dcp_get_connector_type(struct platform_device *pdev); bool dcp_fw_compat_is_12_x(struct platform_device *pdev); bool dcp_fw_compat_is_14_7(struct platform_device *pdev); bool dcp_ext_unusable(struct platform_device *pdev); +bool dcp_clamshell_hold_connector(struct apple_connector *connector); unsigned long* dcp_get_iomfb_surfaces(struct platform_device *pdev); int dcp_start(struct platform_device *pdev); int dcp_wait_ready(struct platform_device *pdev, u64 timeout); diff --git a/drivers/gpu/drm/apple/dptxep.c b/drivers/gpu/drm/apple/dptxep.c index 741f40bc8c9ed5..07e230123ba03a 100644 --- a/drivers/gpu/drm/apple/dptxep.c +++ b/drivers/gpu/drm/apple/dptxep.c @@ -889,13 +889,15 @@ static int dptxport_call(struct apple_epic_service *service, u32 idx, if (reply_size >= 4) memset(reply, 0, 4); return 0; + case DPTX_APCALL_SET_ACTIVE: default: if (service->ep->dcp->hw.iomfb_method_profile == DCP_IOMFB_METHODS_H17P_EXTERNAL) return -EOPNOTSUPP; /* just try to ACK and hope for the best... */ - dev_info(service->ep->dcp->dev, "DPTXPort: acking unhandled call %u\n", - idx); + if (idx != DPTX_APCALL_SET_ACTIVE) + dev_info(service->ep->dcp->dev, + "DPTXPort: acking unhandled call %u\n", idx); fallthrough; case DPTX_APCALL_GET_DOWN_SPREAD: case DPTX_APCALL_SET_DOWN_SPREAD: @@ -912,6 +914,9 @@ static int dptxport_prepare(struct apple_epic_service *service, const char *name struct apple_dcp *dcp = service->ep->dcp; int port; + /* The J613 internal pipe's own panel port is never bound. */ + if (dcp->clamshell && name && !strcmp(name, "dcp-lpdptx-port-epic")) + return -ENODEV; /* * T6030 14.7 also announces the low-power port service, and may leave * its class empty. diff --git a/drivers/gpu/drm/apple/dptxep.h b/drivers/gpu/drm/apple/dptxep.h index 211e72d9855ea8..ee7497f75ae3da 100644 --- a/drivers/gpu/drm/apple/dptxep.h +++ b/drivers/gpu/drm/apple/dptxep.h @@ -30,6 +30,8 @@ enum dptx_apcall { DPTX_APCALL_DEVICE_NOT_RESPONDING = 22, DPTX_APCALL_DEVICE_BUSY_TIMEOUT = 23, DPTX_APCALL_DEVICE_NOT_STARTED = 24, + /* The port's controller became active or not (J613 lid close/open). */ + DPTX_APCALL_SET_ACTIVE = 25, DPTX_APCALL_GET_VIRTUAL_DEVICE_MODE = 26, }; diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index 6e80eca48ad9ac..f76f5af025cd2c 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -1576,6 +1576,7 @@ int dcp_modes_replace(struct apple_dcp *dcp, struct dcp_parse_ctx *ctx, unsigned long flags; bool provisional; u32 entries = 0; + bool panel; int ret = 0; /* A stale transfer is refused before it is parsed, as it is published. */ @@ -1595,9 +1596,11 @@ int dcp_modes_replace(struct apple_dcp *dcp, struct dcp_parse_ctx *ctx, if (ret) return ret; } + /* Not the panel's notch and rules while the J613 panel's pipe drives a Type-C display. */ + panel = dcp->fixed_connector_type == DRM_MODE_CONNECTOR_eDP && + dcp->connector_type == DRM_MODE_CONNECTOR_eDP; modes = enumerate_modes(ctx, &nr_modes, dcp->width_mm, dcp->height_mm, - dcp->notch_height, - dcp->fixed_connector_type == DRM_MODE_CONNECTOR_eDP, + panel ? dcp->notch_height : 0, panel, h17p_external); if (IS_ERR(modes)) { ret = PTR_ERR(modes); diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.c b/drivers/gpu/drm/apple/iomfb_v14_7.c index 63e16f392ae096..61e5b562349b92 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.c +++ b/drivers/gpu/drm/apple/iomfb_v14_7.c @@ -318,6 +318,17 @@ struct apple_dcp_v14 { u32 lid; bool panel_blank; bool panel_off; + /* + * The pipe drives the display on its Type-C route (typec): its + * description, mode and power state are kept as on an external + * processor (described, mode_*, powered), and swaps are sized to its + * mode (typec_width x typec_height), without the notch rows or the + * panel's colour matrix. panel_mode is the panel's one mode, for its + * connector again once the display let go. + */ + bool typec; + u32 typec_width, typec_height; + struct dcp_display_mode panel_mode; /* * An external processor: no panel. Its modes come from the @@ -1164,6 +1175,17 @@ static int dcp_v14_external_callback(struct apple_dcp_v14 *v14, u32 tag, const u return 0; } +/* + * The callbacks that describe a display: on the J613 internal pipe they + * describe the display on its Type-C route while it drives one, and are + * handled as on an external processor then. + */ +static bool dcp_v14_description_tag(u32 tag) +{ + return tag == D(107) || tag == D(127) || tag == D(128) || tag == D(129) || + tag == D(413) || tag == D(552) || tag == D(561) || tag == D(567); +} + /* Runs on the thread that owns the RPC stream; the lock is held. */ static int dcp_v14_callback(void *cookie, u32 tag, const void *input, u32 in_size, void *output, u32 out_size) @@ -1175,7 +1197,7 @@ static int dcp_v14_callback(void *cookie, u32 tag, const void *input, u32 in_siz u32 count, offset, service, key_offset, id; int ret; - if (v14->external) { + if (v14->external || (v14->typec && dcp_v14_description_tag(tag))) { ret = dcp_v14_external_callback(v14, tag, in, in_size, out, out_size); if (ret != -ENOENT) return ret; @@ -2686,6 +2708,7 @@ static int dcp_v14_mode(struct apple_dcp *dcp, struct apple_dcp_v14 *v14) kfree(modes); if (!selected) return -ENOMEM; + v14->panel_mode = *selected; mutex_lock(&dcp->modes_lock); kfree(dcp->modes); dcp->modes = selected; @@ -2776,7 +2799,7 @@ static int dcp_v14_ctm_locked(struct apple_dcp_v14 *v14, __le32 status = 0; int i, ret; - if (!v14->board || !v14->board->ctm_set || !state || v14->ctm_disabled || + if (!v14->board || !v14->board->ctm_set || !state || v14->ctm_disabled || v14->typec || (v14->ctm_valid && !state->color_mgmt_changed && !state->mode_changed && !state->active_changed)) return 0; @@ -2830,7 +2853,8 @@ static bool dcp_v14_external_described(struct apple_dcp_v14 *v14); */ static bool dcp_v14_panel_dark(struct apple_dcp_v14 *v14) { - return !v14->external && (READ_ONCE(v14->panel_off) || READ_ONCE(v14->lid) == 2); + return !v14->external && !READ_ONCE(v14->typec) && + (READ_ONCE(v14->panel_off) || READ_ONCE(v14->lid) == 2); } struct dcp_v14_swap_admission { @@ -2845,7 +2869,7 @@ static bool dcp_v14_swap_admit(void *cookie) struct apple_dcp *dcp = READ_ONCE(v14->dcp); struct apple_connector *connector; - if (!v14->external) + if (!v14->external && !v14->typec) return true; if (!dcp || READ_ONCE(v14->failed) || !READ_ONCE(v14->powered) || !READ_ONCE(v14->mode_live) || @@ -2909,13 +2933,16 @@ static int dcp_v14_swap_locked(struct apple_dcp_v14 *v14, const u8 *surface, u64 __le32 start[4] = {}, started[2] = {}, result[3]; struct dcp_backlight_present present; bool nits; + bool typec = READ_ONCE(v14->typec); + u32 max_width = typec ? v14->typec_width : v14->panel_width; + u32 max_height = typec ? v14->typec_height : v14->panel_height; u8 *swap; u32 id; int ret; lockdep_assert_held(&v14->lock); - if (surface && (!iova || !width || width > v14->panel_width || !height || - dst_y > v14->panel_height || height > v14->panel_height - dst_y)) + if (surface && (!iova || !width || width > max_width || !height || + dst_y > max_height || height > max_height - dst_y)) return -EINVAL; swap = kmalloc(DCP_V14_SWAP_SIZE, GFP_KERNEL); if (!swap) @@ -2962,7 +2989,7 @@ static int dcp_v14_swap_locked(struct apple_dcp_v14 *v14, const u8 *surface, u64 * refuses it, which ends nothing. Link failures stop the session in * dcp_v14_call() either way. */ - if (ret && !v14->external) + if (ret && !v14->external && !v14->typec) v14->failed = true; kfree(swap); @@ -3020,7 +3047,7 @@ static bool dcp_v14_present_locked(struct apple_dcp_v14 *v14, struct drm_framebu ret = dcp_v14_swap_locked(v14, fb ? surface : NULL, iova, fb ? width : 0, fb ? height : 0, dst_y, ctm_state); elapsed = ktime_get_ns() - start; - if (ret && v14->external && !READ_ONCE(v14->failed)) { + if (ret && (v14->external || READ_ONCE(v14->typec)) && !READ_ONCE(v14->failed)) { /* Refused before it was taken: the old framebuffer stays on screen. */ *retired = fb; WRITE_ONCE(v14->mode_swaps, 0); @@ -3162,6 +3189,7 @@ int iomfb_v14_7_atomic_check(struct apple_dcp *dcp, struct drm_crtc *crtc, struct drm_gem_dma_object *obj; struct drm_framebuffer *fb; u32 width, height; + bool ext; if (dcp->external) { /* An external pipe can always be switched off, started or not. */ @@ -3193,7 +3221,12 @@ int iomfb_v14_7_atomic_check(struct apple_dcp *dcp, struct drm_crtc *crtc, if (fb->modifier != DRM_FORMAT_MOD_LINEAR || fb->offsets[0] || fb->pitches[0] < width * 4 || (fb->pitches[0] & 63)) return -EINVAL; - if (!dcp_v14_plane_fits_mode(p, width, height, dcp->external)) + /* + * An external pipe, or the J613 internal pipe driving a Type-C + * display, may show the top-left of a larger framebuffer. + */ + ext = dcp->external || READ_ONCE(v14->typec); + if (!dcp_v14_plane_fits_mode(p, width, height, ext)) return -EINVAL; obj = drm_fb_dma_get_gem_obj(fb, 0); if (!obj || !obj->dma_addr || (u64)fb->pitches[0] * height > obj->base.size) @@ -3437,13 +3470,77 @@ static int dcp_v14_external_modeset(struct apple_dcp *dcp, struct drm_crtc_state return 0; } +/* + * J613: the display on the internal pipe's Type-C route. As on an external + * processor, but this firmware wants the pipe powered before the mode is set + * (with A411 alone the mode is set, the pipe not enabled): power off for + * a mode change, power on (A472(1)), then the mode (A411). + */ +static int dcp_v14_typec_modeset(struct apple_dcp *dcp, struct drm_crtc_state *crtc_state) +{ + struct apple_dcp_v14 *v14 = dcp->v14; + struct dcp_display_mode mode = {}; + int ret; + + ret = dcp_v14_external_settle(dcp, v14); + if (ret) + goto out; + if (!v14->typec || !lookup_mode(dcp, &crtc_state->mode, &mode)) { + ret = v14->typec ? -EINVAL : -ENODEV; + mutex_unlock(&v14->lock); + goto out; + } + if (dcp_v14_external_mode_live(v14, &mode)) { + mutex_unlock(&v14->lock); + dcp_mode_set_valid(&dcp->mode_state, true); + return 0; + } + if (v14->powered && dcp_v14_set_power(v14, false)) + dev_warn(dcp->dev, "Type-C display did not power off before the mode change\n"); + ret = v14->failed ? -EIO : dcp_v14_set_power(v14, true); + if (!ret) + ret = dcp_v14_set_mode(v14, mode.color_mode_id, mode.timing_mode_id); + if (!ret) { + v14->typec_width = mode.mode.hdisplay; + v14->typec_height = mode.mode.vdisplay; + v14->mode_timing = mode.timing_mode_id; + v14->mode_color = mode.color_mode_id; + v14->mode_swaps = 0; + v14->mode_live = true; + } + mutex_unlock(&v14->lock); +out: + dev_info(dcp->dev, "internal pipe: Type-C display mode " DRM_MODE_FMT + " (timing %u, color %u): %d\n", DRM_MODE_ARG(&crtc_state->mode), + mode.timing_mode_id, mode.color_mode_id, ret); + if (!ret) + dcp_mode_set_valid(&dcp->mode_state, true); + return ret; +} + +static void dcp_v14_panel_on_locked(struct apple_dcp_v14 *v14); + int iomfb_v14_7_modeset(struct apple_dcp *dcp, struct drm_crtc_state *crtc_state) { if (dcp->external) return dcp_v14_external_modeset(dcp, crtc_state); + if (dcp->v14 && READ_ONCE(dcp->v14->typec)) + return dcp_v14_typec_modeset(dcp, crtc_state); /* The firmware keeps the timing it booted with, which is the only mode. */ if (!lookup_mode(dcp, &crtc_state->mode, NULL)) return -EINVAL; + /* + * J613: a CRTC the display on the Type-C route let go of can be given + * to the panel without being switched off and on: power the panel on + * here too, once the lid is open. + */ + if (dcp->v14 && READ_ONCE(dcp->v14->clamshell) && READ_ONCE(dcp->v14->panel_off)) { + mutex_lock(&dcp->v14->lock); + dcp->v14->panel_blank = false; + dcp_v14_panel_on_locked(dcp->v14); + mutex_unlock(&dcp->v14->lock); + dcp_clamshell_changed(dcp); + } scoped_guard(spinlock_irqsave, &dcp->mode_state.lock) dcp->mode_state.valid = true; return 0; @@ -3455,6 +3552,8 @@ void iomfb_v14_7_flush(struct apple_dcp *dcp, struct drm_crtc *crtc, struct drm_plane_state *p = drm_atomic_get_new_plane_state(state, crtc->primary); struct drm_crtc_state *cs = drm_atomic_get_new_crtc_state(state, crtc); struct drm_framebuffer *fb; + bool typec = dcp->v14 && READ_ONCE(dcp->v14->typec); + bool ext = dcp->external || typec; u32 width = 0, height = 0; if (!p) @@ -3468,28 +3567,31 @@ void iomfb_v14_7_flush(struct apple_dcp *dcp, struct drm_crtc *crtc, * commits displaced: release them here as a completed swap would, or * they pile up for as long as the compositor keeps the CRTC on. */ - if (!dcp->external && dcp->v14 && dcp_v14_panel_dark(dcp->v14)) { + if (!dcp->external && !typec && dcp->v14 && dcp_v14_panel_dark(dcp->v14)) { dcp_release_all_retained_framebuffers(dcp); dcp_drm_crtc_vblank(dcp->crtc); return; } if (fb) - dcp_v14_plane_scanout_size(p, dcp->external, &width, &height); + dcp_v14_plane_scanout_size(p, ext, &width, &height); /* - * No swaps while an external display is off, unset, unplugged, or - * being described again. + * No swaps while an external display, or the one on the J613 internal + * pipe's Type-C route, is off, unset, unplugged, or being described + * again. */ - if (dcp->external && (!dcp->v14 || !READ_ONCE(dcp->v14->powered) || - !dcp_v14_external_described(dcp->v14) || - !READ_ONCE(dcp->mode_state.valid) || !dcp->connector || - !READ_ONCE(dcp->connector->connected))) { + if ((dcp->external || typec) && + (!dcp->v14 || !READ_ONCE(dcp->v14->powered) || + !dcp_v14_external_described(dcp->v14) || + !READ_ONCE(dcp->mode_state.valid) || !dcp->connector || + !READ_ONCE(dcp->connector->connected))) { dcp_drm_crtc_vblank(dcp->crtc); return; } if (!dcp->external) dcp_v14_configure_backlight(dcp); dcp->swap_start = ktime_get(); - if (dcp->v14 && dcp_v14_present(dcp->v14, fb, width, height, dcp->notch_height, cs)) { + if (dcp->v14 && dcp_v14_present(dcp->v14, fb, width, height, + typec ? 0 : dcp->notch_height, cs)) { dcp_drm_crtc_page_flip(dcp, ktime_get()); /* * The swap completed: the framebuffers this commit and earlier @@ -3513,7 +3615,7 @@ static void dcp_v14_panel_off_locked(struct apple_dcp_v14 *v14) { int ret; - if (v14->lid != 2 || v14->panel_off || v14->failed) + if (v14->lid != 2 || v14->typec || v14->panel_off || v14->failed) return; ret = dcp_v14_power_call(v14, false); if (!ret) @@ -3531,7 +3633,7 @@ static void dcp_v14_panel_on_locked(struct apple_dcp_v14 *v14) { int ret; - if (v14->lid == 2 || !v14->panel_off || v14->failed) + if (v14->lid == 2 || v14->typec || !v14->panel_off || v14->failed) return; ret = dcp_v14_power_call(v14, true); if (!ret) { @@ -3548,24 +3650,120 @@ static void dcp_v14_panel_on_locked(struct apple_dcp_v14 *v14) /* * J613: the internal pipe has a Type-C route, which only a powered-off panel * leaves free; from now on the lid state counts (iomfb_v14_7_set_lid()). + * The route needs the DPTX endpoint; the DPAV controller endpoint is started + * too, as for the T6030 external processors (in the tests the firmware only + * sent reports there, no call to answer). */ int iomfb_v14_7_clamshell_start(struct apple_dcp *dcp) { struct apple_dcp_v14 *v14 = dcp->v14; + int ret; if (!v14 || !v14->started || READ_ONCE(v14->failed)) return -ENODEV; + if (!apple_rtkit_has_endpoint(v14->rtk, DPTX_ENDPOINT)) { + dev_warn(dcp->dev, "no DPTX endpoint: the Type-C route stays unused\n"); + return -ENODEV; + } + if (apple_rtkit_has_endpoint(v14->rtk, DPAV_CTRL_ENDPOINT)) + dpav_ctrl_init(dcp); + ret = dptxep_init(dcp); + if (ret) { + dev_err(dcp->dev, "DPTX endpoint did not start (%d): the Type-C route stays unused\n", + ret); + return ret; + } mutex_lock(&v14->lock); v14->clamshell = true; mutex_unlock(&v14->lock); + dev_info(dcp->dev, "Type-C route ready: with the lid closed and the panel off, the internal pipe can drive a display on it\n"); return 0; } +/* + * The pipe is free for a Type-C display: the lid is closed, the panel off, + * and its CRTC off as well, so that the display that takes the pipe does + * not share the CRTC with a panel the compositor still drives. + */ +bool iomfb_v14_7_clamshell_panel_off(struct apple_dcp *dcp) +{ + struct apple_dcp_v14 *v14 = dcp->v14; + + return v14 && READ_ONCE(v14->clamshell) && READ_ONCE(v14->lid) == 2 && + READ_ONCE(v14->panel_off) && READ_ONCE(v14->panel_blank) && + !READ_ONCE(v14->failed); +} + bool iomfb_v14_7_lid_open(struct apple_dcp *dcp) { return dcp->v14 && READ_ONCE(dcp->v14->lid) == 1; } +void iomfb_v14_7_typec_attach(struct apple_dcp *dcp) +{ + struct apple_dcp_v14 *v14 = dcp->v14; + + if (!v14) + return; + mutex_lock(&v14->lock); + v14->typec = true; + v14->described = 0; + v14->description_generation++; + v14->mode_live = false; + mutex_unlock(&v14->lock); +} + +/* Called with the lock held. */ +static void dcp_v14_typec_power_off_locked(struct apple_dcp_v14 *v14) +{ + int ret; + + if (!v14->typec || !v14->powered || v14->failed) + return; + ret = dcp_v14_set_power(v14, false); + dev_info(v14->dev, "Type-C display on the internal pipe off (A472(0)): %d\n", ret); +} + +void iomfb_v14_7_typec_power_off(struct apple_dcp *dcp) +{ + struct apple_dcp_v14 *v14 = dcp->v14; + + if (!v14) + return; + mutex_lock(&v14->lock); + dcp_v14_typec_power_off_locked(v14); + mutex_unlock(&v14->lock); +} + +void iomfb_v14_7_typec_detach(struct apple_dcp *dcp) +{ + struct apple_dcp_v14 *v14 = dcp->v14; + struct dcp_display_mode *panel; + + if (!v14) + return; + panel = kmemdup(&v14->panel_mode, sizeof(*panel), GFP_KERNEL); + mutex_lock(&v14->lock); + dcp_v14_typec_power_off_locked(v14); + v14->typec = false; + v14->powered = false; + v14->mode_live = false; + v14->described = 0; + v14->description_generation++; + mutex_unlock(&v14->lock); + dcp_mode_invalidate(&dcp->mode_state); + /* The panel's one mode, for its connector again. */ + guard(mutex)(&dcp->modes_lock); + dcp->modes_generation++; + dcp->modes_provisional = false; + dcp->modes_admitted = !!panel; + if (panel) { + kfree(dcp->modes); + dcp->modes = panel; + dcp->nr_modes = 1; + } +} + /* * Tells the panel's IOMFB the lid state (A475, one u32: 1 * open, 2 closed; no reply). IOMFB keeps the value and hands it to its DPTX @@ -3590,7 +3788,7 @@ void iomfb_v14_7_set_lid(struct apple_dcp *dcp, bool closed) if (v14->failed) goto out; /* The black frame and the lid state under one lock: no swap between. */ - if (closed && v14->lid != 2 && !v14->panel_off && !v14->panel_blank) + if (closed && v14->lid != 2 && !v14->panel_off && !v14->panel_blank && !v14->typec) dcp_v14_present_locked(v14, NULL, 0, 0, 0, NULL, &retired); if (v14->lid != le32_to_cpu(state)) { ret = dcp_v14_call(v14, A(475), &state, sizeof(state), NULL, 0, 0); @@ -3605,7 +3803,7 @@ void iomfb_v14_7_set_lid(struct apple_dcp *dcp, bool closed) /* * A panel CRTC that was never switched on (the lid was closed * at boot, and the panel's connector has read disconnected - * since) is as blank as one switched off. + * since) is as blank as one switched off: the pipe is free. */ if (!READ_ONCE(dcp->clamshell_crtc_on)) v14->panel_blank = true; @@ -3623,11 +3821,14 @@ void iomfb_v14_7_poweron(struct apple_dcp *dcp) struct apple_dcp_v14 *v14 = dcp->v14; /* The next present restores a level only after Linux has set one. */ - if (dcp->v14 && dcp_v14_bl_dpms_applies(dcp->v14, dcp)) + if (dcp->v14 && !READ_ONCE(dcp->v14->typec) && dcp_v14_bl_dpms_applies(dcp->v14, dcp)) dcp_backlight_dpms(dcp, true); - /* The panel stays on; the next swap shows the plane again. */ - if (dcp->external || !v14 || !v14->started) + /* + * The panel stays on; the next swap shows the plane again. A display + * on the J613 internal pipe's Type-C route is powered by its mode set. + */ + if (dcp->external || !v14 || !v14->started || READ_ONCE(v14->typec)) return; mutex_lock(&v14->lock); v14->panel_blank = false; @@ -3641,6 +3842,8 @@ void iomfb_v14_7_poweron(struct apple_dcp *dcp) dev_warn_once(dcp->dev, "panel CRTC on with the lid closed: the panel stays off until the lid opens\n"); dcp_v14_panel_on_locked(v14); mutex_unlock(&v14->lock); + if (v14->clamshell) + dcp_clamshell_changed(dcp); } void iomfb_v14_7_poweroff(struct apple_dcp *dcp) @@ -3661,6 +3864,12 @@ void iomfb_v14_7_poweroff(struct apple_dcp *dcp) dcp_mode_invalidate(&dcp->mode_state); return; } + /* So is the display on the J613 internal pipe's Type-C route. */ + if (v14 && READ_ONCE(v14->typec)) { + iomfb_v14_7_typec_power_off(dcp); + dcp_mode_invalidate(&dcp->mode_state); + return; + } /* * Blank to black; the panel and the DCP stay powered, unless the lid * is closed (J613): then the panel is powered off. @@ -3673,6 +3882,8 @@ void iomfb_v14_7_poweroff(struct apple_dcp *dcp) if (v14->clamshell) dcp_v14_panel_off_locked(v14); mutex_unlock(&v14->lock); + if (v14->clamshell) + dcp_clamshell_changed(dcp); } } diff --git a/drivers/gpu/drm/apple/iomfb_v14_7.h b/drivers/gpu/drm/apple/iomfb_v14_7.h index 3defdc0e15caf1..f01b8702c264f6 100644 --- a/drivers/gpu/drm/apple/iomfb_v14_7.h +++ b/drivers/gpu/drm/apple/iomfb_v14_7.h @@ -86,8 +86,21 @@ int iomfb_v14_7_start(struct apple_dcp *dcp); */ int iomfb_v14_7_clamshell_start(struct apple_dcp *dcp); void iomfb_v14_7_set_lid(struct apple_dcp *dcp, bool closed); +/* + * The lid is closed and IOMFB was told so (A475(2)), and the panel and its + * CRTC are off: the pipe is free for a Type-C display. + */ +bool iomfb_v14_7_clamshell_panel_off(struct apple_dcp *dcp); /* IOMFB was told the lid is open (A475(1)). */ bool iomfb_v14_7_lid_open(struct apple_dcp *dcp); +/* + * The pipe drives the display on its Type-C route from _typec_attach() to + * _typec_detach(), which gives the catalog back to the panel; + * _typec_power_off() powers that display off (A472(0)). + */ +void iomfb_v14_7_typec_attach(struct apple_dcp *dcp); +void iomfb_v14_7_typec_power_off(struct apple_dcp *dcp); +void iomfb_v14_7_typec_detach(struct apple_dcp *dcp); int iomfb_v14_7_atomic_check(struct apple_dcp *dcp, struct drm_crtc *crtc, struct drm_atomic_commit *state); From 268800455b3b5d87f79d4dc6f478094e0eef7beb Mon Sep 17 00:00:00 2001 From: Marius Gehrmann Date: Wed, 14 Oct 2026 11:40:53 +0200 Subject: [PATCH 6/6] drm: apple: Move the J613 left-front display to the internal pipe when needed On J613 with the lid closed, dcpext can drive either USB-C port and the internal pipe only the left-front one. A display on the left-front port can hold dcpext while a display on the left-back port, which only dcpext can drive, waits, although the internal pipe is free. Move the left-front display to the internal pipe when that is needed, and only then: as any move, an unplug and replug (its link goes down first, which also tells the old CRTC's late power-off to leave the next display alone), then the other display gets the dcpext it let go of. If the internal pipe cannot take the moved display after all, that display waits and the other one is driven. Three cases need it: - A display arrives on the left-back port with the lid closed while dcpext drives the left-front one (dcp_typec_clamshell_make_room()). - The lid closes while a left-back display waits for dcpext, which drives the left-front one (dcp_typec_clamshell_serve_waiting()). - Boot with the lid closed, before a compositor has paired the connectors: a left-front display that took dcpext first moves to the internal pipe, where aquamarine will pair it, and a waiting display then takes dcpext (dcp_typec_clamshell_settle()). Whether a compositor runs is looked up from any pipeline's CRTC: the dual-stream helper finds no DRM device on J613. Otherwise a lid close moves nothing, so the left-front display stays lit. (Moving it at every lid close frees dcpext early too, but blanks it for about four seconds each time, for nothing when no second display comes.) A display that takes a pipeline it waited for says so ("the display on takes over this pipeline"). Tested on J613 (MacBook Air 13" M3, display firmware 14.7) with a 1080p display on the left-front port and a 3440x1440 one on the left-back port, Hyprland, and a user service that suspends a closed laptop only after ten seconds without an external display: lid closed and opened five times with both displays, both lit each time with the lid closed and the panel back 33-173 ms after opening; a left-back display waiting with the lid open gets dcpext on lid close; a display arriving with the lid closed; boot with the lid closed (panel never powered); suspend with the lid closed and wake by opening it. Assisted-by: Claude:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 Signed-off-by: Marius Gehrmann [Chris Kearney: rebased onto siliconforest/next 6a421d4e76ff; resolved conflicts with the H17P SAFE-phase mux callbacks and the H17P -ENOSPC check in dcp_typec_route_set() (dcp-fabric.c); message and comments reworded; Claude-Session trailer dropped] Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com> [TARS: ported to integration/aurora 7.2.11 (4fdd0fc5): integration reports a port without a pipeline as -ENOSPC on every DCP, so the H17P-only -ENOSPC before dcp_typec_port_busy() is gone; the new helpers sit after integration's dcp_typec_port_busy()] (cherry picked from commit d39a9813652a) --- drivers/gpu/drm/apple/dcp-fabric.c | 193 ++++++++++++++++++++++++++++- 1 file changed, 189 insertions(+), 4 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index f1db9689547131..ddca7c54510b5c 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -584,6 +584,19 @@ static struct drm_device *dcp_typec_drm(void) return drm; } +/* The DRM device the Type-C pipelines belong to, on any machine. */ +static struct drm_device *dcp_typec_any_drm(void) +{ + struct apple_dcp_typec_port *port; + struct apple_dcp_typec_route *route; + + list_for_each_entry(port, &dcp_typec_ports, link) + list_for_each_entry(route, &port->routes, port_link) + if (route->dcp->crtc) + return route->dcp->crtc->base.dev; + return NULL; +} + /* * Has a compositor (or boot splash) taken the display? It paired its * connectors with CRTCs when it started and keeps that pairing, also @@ -1043,6 +1056,14 @@ static void dcp_typec_route_waiting(void) if (!route || dcp_typec_route_activate(route, route->xbar)) continue; port->owner = route; + if (port->busy_noted) { + const char *label = NULL; + + of_property_read_string(port->connector_np, "label", &label); + dev_info(route->dcp->dev, "the display on %s takes over this pipeline\n", + label ?: "a USB-C port"); + } + port->busy_noted = false; dcp_typec_port_attach(port); } @@ -1296,15 +1317,22 @@ void dcp_typec_reorder(void) static int dcp_typec_port_busy(struct apple_dcp_typec_port *port, struct apple_dcp *dcp); +static void dcp_typec_clamshell_settle(void); +static void dcp_typec_clamshell_serve_waiting(void); /* * The J613 internal pipe became free for a Type-C display (lid closed, panel * off) or is its panel's again (the lid opens). Free: it takes a port left * waiting for a pipeline, as any pipeline that is let go of, ranked before - * dcpext while it is free (see dcp_fabric_snapshot_port()). Its panel's - * again: the display it drives lets go of it (powered off first), and - * goes to another free pipeline or waits again. Returns whether - * a display let go. + * dcpext while it is free (see dcp_fabric_snapshot_port()); and before a + * compositor owns the displays (boot with the lid closed), a display that + * took dcpext on the left-front port moves to it, where the compositor will + * pair that display (dcp_typec_clamshell_settle()); once a compositor runs, + * that display moves only if another one waits for dcpext + * (dcp_typec_clamshell_serve_waiting()). Its panel's again: the + * display it drives lets go of it (powered off first), and + * goes to another free pipeline or waits again. Returns whether a display + * let go. */ bool dcp_typec_pipeline_changed(struct apple_dcp *dcp) { @@ -1315,6 +1343,8 @@ bool dcp_typec_pipeline_changed(struct apple_dcp *dcp) if (!dcp_typec_route_fixed_output_busy(&dcp->typec_routes[0])) { dcp_typec_pipeline_freed(); + dcp_typec_clamshell_settle(); + dcp_typec_clamshell_serve_waiting(); return false; } for (i = 0; i < dcp->nr_typec_routes; i++) { @@ -1445,6 +1475,159 @@ static int dcp_typec_port_busy(struct apple_dcp_typec_port *port, return -ENOSPC; } +/* + * Move the display on @port from @from (dcpext) to @to, the free J613 + * internal pipe: an unplug and replug, as any move. Returns whether @from + * was let go of. + */ +static bool dcp_typec_clamshell_move(struct apple_dcp_typec_port *port, + struct apple_dcp_typec_route *from, + struct apple_dcp_typec_route *to) +{ + struct apple_dcp *dcp = from->dcp; + int ret; + + lockdep_assert_held(&dcp_typec_fabric_lock); + if (port->hpd || dcp->typec_cable_connected || + (dcp->typec_connector && dcp->typec_connector->connected)) { + ret = dcp_dptx_disconnect_oob(to_platform_device(dcp->dev), 0); + if (ret) + return false; + } + port->hpd = false; + ret = dcp_typec_route_deactivate(from); + if (ret) + return false; + port->owner = NULL; + port->applied_valid = false; + port->preferred_route = to; + /* @from is free now, whether the internal pipe takes over or not */ + if (dcp_typec_route_activate(to, to->xbar)) { + port->busy_noted = false; + dcp_typec_port_busy(port, to->dcp); + return true; + } + port->owner = to; + dcp_typec_port_attach(port); + return true; +} + +/* + * J613 with the lid closed: a display arriving on @port can only have + * dcpext, which drives the display on the left-front port, and the internal + * pipe, which only reaches that port, is free. Move the left-front + * display to the internal pipe (an unplug and replug, as any move), so that + * dcpext is free for @port. Only then: nothing moves otherwise. Returns + * whether a pipeline @port can have was let go of. + */ +static bool dcp_typec_clamshell_make_room(struct apple_dcp_typec_port *port) +{ + struct apple_dcp_typec_port *other; + struct apple_dcp_typec_route *route, *from, *to; + struct apple_dcp *dcp; + bool reachable; + + lockdep_assert_held(&dcp_typec_fabric_lock); + if (!port->dp_hpd) + return false; + list_for_each_entry(other, &dcp_typec_ports, link) { + from = other->owner; + if (other == port || !from || from->tunnel || other->secondary_owner || + from->dcp->clamshell) + continue; + /* @port could have the pipeline @other has... */ + reachable = false; + list_for_each_entry(route, &port->routes, port_link) + reachable |= route->dcp == from->dcp && !route->tunnel; + if (!reachable) + continue; + /* ...and @other could have the free internal pipe instead. */ + to = NULL; + list_for_each_entry(route, &other->routes, port_link) + if (route->dcp->clamshell && !route->tunnel && + dcp_typec_route_available(route)) + to = route; + if (!to) + continue; + + dcp = from->dcp; + dev_info(dcp->dev, "lid closed: the display on %pOF moves to the internal pipe, so that this pipeline can drive the one on %pOF\n", + other->connector_np, port->connector_np); + return dcp_typec_clamshell_move(other, from, to); + } + return false; +} + +/* + * Before a compositor owns the displays (boot with the lid closed): a + * display on the left-front port that took dcpext before the internal pipe + * was free moves to the internal pipe, the CRTC aquamarine will pair it + * with (the lowest free one it can use while the panel's connector reads + * disconnected). Once a compositor runs it keeps the CRTC it gave the + * display, so nothing moves then. + */ +static void dcp_typec_clamshell_settle(void) +{ + struct apple_dcp_typec_port *port; + struct apple_dcp_typec_route *route, *from, *to; + struct drm_device *drm; + bool moved = false; + + lockdep_assert_held(&dcp_typec_fabric_lock); + /* + * Not dcp_typec_drm(): that one only answers on dual-stream machines + * (every port with a second connector), so on J613 it never found the + * device and this moved displays under a running compositor. + */ + drm = dcp_typec_any_drm(); + if (drm && dcp_typec_frozen(drm)) + return; + list_for_each_entry(port, &dcp_typec_ports, link) { + from = port->owner; + if (!from || from->tunnel || port->secondary_owner || + from->dcp->clamshell) + continue; + to = NULL; + list_for_each_entry(route, &port->routes, port_link) + if (route->dcp->clamshell && !route->tunnel && + dcp_typec_route_available(route)) + to = route; + if (!to) + continue; + dev_info(from->dcp->dev, "lid closed, before a compositor started: the display on %pOF moves to the internal pipe, where the compositor will pair it\n", + port->connector_np); + moved |= dcp_typec_clamshell_move(port, from, to); + } + /* dcpext is free now: a display waiting for it takes it */ + if (moved) + dcp_typec_route_waiting(); +} + +/* + * The internal pipe became free with the lid closed while a display waits + * for dcpext, which drives the display on the left-front port (the lid was + * opened, both were plugged in, the second one waited): move the left-front + * display to the internal pipe and give dcpext to the one waiting, as when + * that display arrives with the lid already closed + * (dcp_typec_clamshell_make_room()). Nothing moves when nothing waits: the + * compositor keeps the display where it is. + */ +static void dcp_typec_clamshell_serve_waiting(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) { + if (port->owner || port->secondary_owner || !port->dp_wanted || + !port->dp_hpd || dcp_typec_free_route(port)) + continue; + moved |= dcp_typec_clamshell_make_room(port); + } + if (moved) + dcp_typec_route_waiting(); +} + static int dcp_typec_route_set(struct typec_mux_dev *mux, struct typec_mux_state *state) { @@ -1586,6 +1769,8 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, if (!port->owner) { best = dcp_typec_free_route(port); + if (!best && dcp_typec_clamshell_make_room(port)) + best = dcp_typec_free_route(port); /* Every pipeline is driving another port's display. */ if (!best) return hpd ? dcp_typec_port_busy(port, route->dcp) : -ENOSPC;