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