From b609a9e03cbe63fb6d2c00a4c11c6a4e046887d5 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 11 Oct 2026 16:23:33 +0200 Subject: [PATCH 1/9] drm: apple: relink a Type-C sink that lost its link to a hub's USB3 A USB-C monitor with a built-in USB hub (EIZO EV2780) intermittently stays dark after a cold boot or a hot-plug on the M1 MacBook Air and the M1 Mac mini. DCP trains the link for the first modeset at about the time the monitor's hub brings its SuperSpeed link up. When the two overlap, the sink loses the link, but DCP still reports the modeset as linked: UnifiedPipeline::mode_set_gated(): 2560x1440@59 Hz link: 1 Then one of two things happens. DCP gives up on its own 2.008 s after asserting HPD: IOMFB: IOAVVideoInterface terminated VideoInterfaceIOAV::unplug_gated(): display HPD removed or it keeps driving the dead link without noticing, while the monitor pulses HPD about 2-4 s later to be detected again. In both cases the CD321x still reports DP alt mode with HPD high, and nothing re-detects the sink. With dwc3 kept off the port, 5 out of 5 boots brought the display up; with it, 3 out of 6 failed. Replay an unplug and replug of the sink when DCP drops a sink that still asserts HPD, or when the sink asks for attention (IRQ_HPD): the link is then retrained after the hub has settled, as a cable replug does. A request from the sink within 4 s after a relink is the sink's late reaction to the loss the relink already handled and is ignored; another relink would only interrupt the new link. A sink request must not be dropped as an unchanged route state either. A connection gets at most three relinks; the replay does not reset that count. Pulsing only DCP's HPD instead, with the connector kept connected, raced with DCP re-detecting the sink on its own and left the display dark on the Mac mini. H17P and native external pipes keep their own recovery. Once the relink budget is spent, a sink request falls back to dcp_dptx_recover_irq() or dcp_retrain_oob() as before. With this, 8 out of 8 cold boots of the MacBook Air and 8 out of 8 of the Mac mini (EIZO on the port that failed) brought the display up. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/dcp-fabric.c | 19 +-- drivers/gpu/drm/apple/dcp-internal.h | 8 ++ drivers/gpu/drm/apple/dcp.c | 108 ++++++++++++++++++ drivers/gpu/drm/apple/iomfb_template.c | 6 + .../selftests/drivers/gpu/apple-dcp/rate.py | 1 + .../drivers/gpu/apple-dcp/transaction.py | 1 + 6 files changed, 135 insertions(+), 8 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-fabric.c b/drivers/gpu/drm/apple/dcp-fabric.c index 49e759e6e1131..80c205b8a1f67 100644 --- a/drivers/gpu/drm/apple/dcp-fabric.c +++ b/drivers/gpu/drm/apple/dcp-fabric.c @@ -1438,7 +1438,8 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, * without a word, so a nominated route might never be called at all -- * and the port would then never be routed. */ - if (port->applied_valid && port->applied_alt == state->alt && + if (port->applied_valid && !(dp_status & DP_STATUS_IRQ_HPD) && + port->applied_alt == state->alt && port->applied_mode == state->mode && port->applied_status == dp_status && port->applied_conf == dp_conf) return 0; @@ -1571,13 +1572,15 @@ static int dcp_typec_route_set(struct typec_mux_dev *mux, dcp_retrain_oob(dcp->typec_connector); else if (dcp->typec_connector && dcp->external_native) dcp_external_sink_irq(dcp); - else if (dcp->typec_connector && - !READ_ONCE(dcp->typec_connector->connected)) { - ret = dcp_dptx_recover_irq(dcp); - if (ret) - return ret; - } else if (dcp->typec_connector) - dcp_retrain_oob(dcp->typec_connector); + else if (dcp->typec_connector && !dcp_typec_sink_relink(dcp)) { + if (!READ_ONCE(dcp->typec_connector->connected)) { + ret = dcp_dptx_recover_irq(dcp); + if (ret) + return ret; + } else { + dcp_retrain_oob(dcp->typec_connector); + } + } } port->hpd = hpd; port->applied_valid = true; diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 5392736e45312..699689a0770b8 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -61,6 +61,8 @@ void dcp_swap_watchdog_arm(struct apple_dcp *dcp); void dcp_swap_watchdog_complete(struct apple_dcp *dcp); void dcp_retry_placeholder_edid(struct apple_dcp *dcp, const struct drm_edid *drm_edid); +bool dcp_typec_relink(struct apple_dcp *dcp); +bool dcp_typec_sink_relink(struct apple_dcp *dcp); struct dcpav_service_epic; @@ -380,6 +382,12 @@ struct apple_dcp { u64 crtc_unplugs; u64 placeholder_generation; struct delayed_work placeholder_edid_wq; + /* replugs replayed for a Type-C sink that lost its link */ + u32 relink_tries; + bool relink_active; + unsigned long relink_done; + u64 relink_generation; + struct delayed_work typec_relink_wq; bool use_timestamps; bool vrr_enabled; struct dcp_set_digital_out_mode_req mode; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 651c5eb428eea..24e54d47a3b56 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -1129,6 +1129,110 @@ static void dcp_typec_reconnect_work(struct work_struct *work) dcp->typec_reconnect_tries, ret); } +#define DPTX_RELINK_RETRIES 3 +#define DPTX_RELINK_UNPLUG_MS 1000 +/* a monitor pulses HPD up to about 4 s after it lost the link */ +#define DPTX_RELINK_SETTLE_MS 4000 + +/* + * A USB-C monitor with a built-in hub can lose the DP link when the hub + * brings its SuperSpeed link up right around DCP's link training for the + * first modeset. DCP may then report the sink as unplugged two seconds + * later although it still asserts HPD, or keep driving the dead link + * without noticing while the monitor pulses HPD to be detected again. + * Neither is recovered by anything else: replay an unplug and replug of + * the sink, which retrains the link once the hub has settled, as a cable + * replug does. + * + * The retries cover one connection: the replay itself does not reset them. + */ +static bool dcp_typec_relink_start(struct apple_dcp *dcp, const char *why) +{ + u64 generation = READ_ONCE(dcp->typec_generation); + + /* H17P and native external pipes recover through their own lifecycle */ + if (dcp->fw_compat == DCP_FIRMWARE_H17P || dcp->external_native || + !dcp_is_typec_output(dcp) || dcp_is_usb4_output(dcp) || + dcp_is_external_h17p(dcp) || + !READ_ONCE(dcp->typec_cable_connected) || + READ_ONCE(dcp->typec_crtc_off) || !dcp->typec_connector) + return false; + + if (dcp->relink_generation != generation) { + dcp->relink_generation = generation; + dcp->relink_tries = 0; + WRITE_ONCE(dcp->relink_active, false); + } + /* the unplug a running relink replays makes DCP report one too */ + if (READ_ONCE(dcp->relink_active)) + return true; + if (dcp->relink_tries >= DPTX_RELINK_RETRIES) + return false; + + dcp->relink_tries++; + WRITE_ONCE(dcp->relink_active, true); + dev_info(dcp->dev, "%s, relinking the Type-C sink (%u/%u)\n", why, + dcp->relink_tries, DPTX_RELINK_RETRIES); + mod_delayed_work(system_freezable_wq, &dcp->typec_relink_wq, 0); + + return true; +} + +/* DCP reported an unplug of a Type-C sink that still asserts HPD */ +bool dcp_typec_relink(struct apple_dcp *dcp) +{ + return dcp_typec_relink_start(dcp, "DCP dropped a sink that asserts HPD"); +} + +/* + * The sink asked for attention: IRQ_HPD or a coalesced HPD pulse. Right + * after a relink that is the sink's late reaction to the loss the relink + * already handled; another relink would only interrupt the new link. + */ +bool dcp_typec_sink_relink(struct apple_dcp *dcp) +{ + unsigned long done = READ_ONCE(dcp->relink_done); + + if (done && time_before(jiffies, done + msecs_to_jiffies(DPTX_RELINK_SETTLE_MS)) && + READ_ONCE(dcp->relink_generation) == READ_ONCE(dcp->typec_generation)) { + dev_info(dcp->dev, "Type-C sink requested attention right after a relink, ignored\n"); + return true; + } + + return dcp_typec_relink_start(dcp, "Type-C sink requested attention"); +} + +static void dcp_typec_relink_work(struct work_struct *work) +{ + struct apple_dcp *dcp = + container_of(to_delayed_work(work), struct apple_dcp, + typec_relink_wq); + struct platform_device *pdev = to_platform_device(dcp->dev); + u64 generation; + + scoped_guard(mutex, &dcp->hpd_mutex) + generation = dcp->typec_generation; + if (generation != dcp->relink_generation || + !READ_ONCE(dcp->typec_cable_connected)) + goto out; + + dcp_dptx_disconnect_oob(pdev, 0); + msleep(DPTX_RELINK_UNPLUG_MS); + + /* a real unplug or replug meanwhile takes over */ + scoped_guard(mutex, &dcp->hpd_mutex) { + if (dcp->typec_generation != generation + 1) + goto out; + } + dcp_dptx_connect_oob(pdev, 0); + + scoped_guard(mutex, &dcp->hpd_mutex) + dcp->relink_generation = dcp->typec_generation; + WRITE_ONCE(dcp->relink_done, jiffies); +out: + WRITE_ONCE(dcp->relink_active, false); +} + static void disconnected_hpd_event(struct apple_connector *con) { apple_connector_edid_set_live(con, false); @@ -2158,6 +2262,7 @@ static void dcp_disable_typec_work(struct apple_dcp *dcp, bool release_cable) /* Block new enqueues as well as draining users of the AFK endpoints. */ disable_delayed_work_sync(&dcp->typec_reconnect_wq); disable_delayed_work_sync(&dcp->placeholder_edid_wq); + disable_delayed_work_sync(&dcp->typec_relink_wq); disable_delayed_work_sync(&dcp->hdmi_settle_wq); disable_delayed_work_sync(&dcp->hdmi_recheck_wq); dcp_hdmi_disable(dcp); @@ -2169,6 +2274,7 @@ static void dcp_enable_typec_work(struct apple_dcp *dcp) return dcp_enable_typec_work_h17p(dcp); enable_delayed_work(&dcp->typec_reconnect_wq); enable_delayed_work(&dcp->placeholder_edid_wq); + enable_delayed_work(&dcp->typec_relink_wq); enable_delayed_work(&dcp->hdmi_settle_wq); enable_delayed_work(&dcp->hdmi_recheck_wq); dcp_hdmi_enable(dcp); @@ -2614,9 +2720,11 @@ static int dcp_platform_probe_pipe(struct platform_device *pdev) dcp_typec_reconnect_work); INIT_DELAYED_WORK(&dcp->placeholder_edid_wq, dcp_placeholder_edid_work); + INIT_DELAYED_WORK(&dcp->typec_relink_wq, dcp_typec_relink_work); /* Balanced by enable at successful component bind. */ disable_delayed_work(&dcp->typec_reconnect_wq); disable_delayed_work(&dcp->placeholder_edid_wq); + disable_delayed_work(&dcp->typec_relink_wq); /* * A native external pipe's bind enables these; firmware callbacks may * come before it. The 14.7 panel never uses them, but suspend cancels diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index b11451c53eece..5e6a81ee6419a 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -1121,6 +1121,12 @@ static void dcpep_cb_hotplug(struct apple_dcp *dcp, u64 *connected) schedule_work(&dcp->vblank_wq); return; } + /* DCP lost a sink which is still attached, see dcp_typec_relink() */ + if (!(*connected) && dcp_typec_relink(dcp)) { + dcp_mode_invalidate(&dcp->mode_state); + schedule_work(&dcp->vblank_wq); + return; + } if (dcp_is_typec_output(dcp) && *connected && dcp->nr_modes) complete_all(&dcp->typec_iomfb_hpd_ready); diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index 32585f1755845..7145d3b13d229 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -936,6 +936,7 @@ def hotplug(root): #define dcp_mode_invalidate(s) ((s)->valid=false) #define dcp_mode_hotplug(s,c,p) __extension__ ({ (void)(s);if(p)*(p)=(c);1u; }) static void dcp_handle_hotplug_actions(struct apple_dcp *d,unsigned action) { (void)d;(void)action; } +static bool dcp_typec_relink(struct apple_dcp *d) { (void)d;return false; } ''' dcp = (root / 'drivers/gpu/drm/apple/dcp.c').read_text() template = (root / 'drivers/gpu/drm/apple/iomfb_template.c').read_text() diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py b/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py index 0ec0cc21c26c0..fe4143c84ac57 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/transaction.py @@ -72,6 +72,7 @@ def check(name, unit, cases=((),), negative=False, flags=()): static int connect_calls; static int dcp_dptx_connect_oob(void *dev,unsigned n) { (void)dev;(void)n;connect_calls++;display.external_cable=true;display.dptxport[0].connected=true;display.typec_cable_connected=true;connector.connected=true;return 0; } static void dcp_retrain_oob(struct connector *c) { (void)c; } + static bool dcp_typec_sink_relink(struct apple_dcp *d) { (void)d;return false; } ''' pos=actual.index('static int dcp_typec_route_deactivate(') actual=actual[:pos]+extra+actual[pos:] From 74474a204a38b6441e4188b74b104d16bc19ae08 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 11 Oct 2026 18:06:38 +0200 Subject: [PATCH 2/9] usb: typec: tipd: report a coalesced sink HPD pulse as IRQ_HPD A DisplayPort sink that lost the link drops HPD and raises it again to be detected anew. The EIZO EV2780 does so for about 250 ms, which falls within the 500 ms debounce of cd321x_update_work(). The worker then sees DP alt mode with HPD high, the state it already applied, and cd321x_typec_update_mode() returns without telling the mux, so the display driver never learns that the sink wants attention and the display stays dark. The out-of-band DRM hotplug event does not cover this either on the M1 MacBook Air and Mac mini: their connector nodes have no "displayport" reference, so connector_fwnode is NULL. When HPD changed within the debounce window and is high before and after, pass the state on with DP_STATUS_IRQ_HPD set, even though it otherwise matches the applied one. Signed-off-by: Thomas Glanzmann --- drivers/usb/typec/tipd/core.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/drivers/usb/typec/tipd/core.c b/drivers/usb/typec/tipd/core.c index 492544f8568c6..6c90eb3221adf 100644 --- a/drivers/usb/typec/tipd/core.c +++ b/drivers/usb/typec/tipd/core.c @@ -704,7 +704,19 @@ static int cd321x_typec_update_mode(struct tps6598x *tps, else dp_data.status &= ~DP_STATUS_HPD_STATE; + /* + * The sink dropped HPD and raised it again within the debounce + * window, as a monitor does after losing the link. The coalesced + * state looks unchanged, so the display side would never hear of + * it: report the pulse as IRQ_HPD. + */ + if ((st->data_status_changed & CD321X_DATA_STATUS_HPD_LEVEL) && + (dp_data.status & DP_STATUS_HPD_STATE) && + (cd321x->dp_status & DP_STATUS_HPD_STATE)) + dp_data.status |= DP_STATUS_IRQ_HPD; + if (cd321x->state_valid && cd321x->state.alt == cd321x->port_altmode_dp && + !(dp_data.status & DP_STATUS_IRQ_HPD) && cd321x->state.mode == mode && cd321x->dp_status == dp_data.status && cd321x->dp_conf == dp_data.conf) From 1739b38bc583dee273a7d9331fa818f297563b3b Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Mon, 12 Oct 2026 13:04:11 +0200 Subject: [PATCH 3/9] drm: apple: do not report a bad link for the HPD a Type-C wake awaits Waking a DP alt mode display from DPMS off with sway ("output * power on") on the M1 Mac mini (Fujitsu E24T-7 Pro on USB-C, which keeps HPD asserted while blanked) power cycles the display about every 750 ms until reboot: dcp_hotplug() connected:1 valid_mode:0 nr_modes:1 dcp_poweron() starting set_digital_out_mode finished dcp_poweroff() done ... With the cable attached, dcp_poweron() reconnects DPTX and waits for IOMFB to report the sink before the commit does its modeset. At that point no mode is valid yet, so dcpep_cb_hotplug() takes the HPD for a sink that asserted HPD only after a failed modeset and sends DCP_HOTPLUG_NOTIFY. dcp_hotplug() then sets the connector's link-status to BAD. wlroots treats a bad link as a disconnect and recreates the output, which disables the CRTC and enables it again. That wake does the same, so the loop never ends. Each round also leaks AFK service slots ("AFK[ep:29]: no reusable service slots") and the system becomes sluggish. Do not take the HPD that dcp_poweron() waits for as a late one: the modeset of the same commit follows. A sink that asserts HPD only after dcp_poweron() stopped waiting is still handled as before. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/dcp-internal.h | 2 ++ drivers/gpu/drm/apple/dcp.c | 3 +++ drivers/gpu/drm/apple/iomfb_template.c | 9 ++++++++- tools/testing/selftests/drivers/gpu/apple-dcp/rate.py | 2 +- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/drivers/gpu/drm/apple/dcp-internal.h b/drivers/gpu/drm/apple/dcp-internal.h index 699689a0770b8..4d706327793c0 100644 --- a/drivers/gpu/drm/apple/dcp-internal.h +++ b/drivers/gpu/drm/apple/dcp-internal.h @@ -532,6 +532,8 @@ struct apple_dcp { bool typec_follow_start; bool typec_follow_retiring; u64 typec_follow_gen; + /* dcp_poweron() reconnects DPTX; its own modeset follows the HPD */ + bool typec_poweron_hpd; /* IOMFB reports its video interface ready after DPTX link training. */ struct completion typec_iomfb_hpd_ready; struct delayed_work typec_reconnect_wq; diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 24e54d47a3b56..00f9449d3d1d8 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -1764,6 +1764,8 @@ void dcp_poweron(struct platform_device *pdev) if (READ_ONCE(dcp->typec_cable_connected)) { cancel_delayed_work(&dcp->typec_reconnect_wq); dcp->typec_reconnect_tries = 0; + /* the modeset of this commit follows, see dcpep_cb_hotplug() */ + WRITE_ONCE(dcp->typec_poweron_hpd, true); ret = dcp_dptx_connect(dcp, 0); if (ret) dcp_queue_typec_reconnect(dcp, DPTX_RECONNECT_DELAY); @@ -1775,6 +1777,7 @@ void dcp_poweron(struct platform_device *pdev) dev_warn(dcp->dev, "Type-C IOMFB hotplug not ready on wake\n"); } + WRITE_ONCE(dcp->typec_poweron_hpd, false); } } else if (dcp->hdmi_hpd) { bool connected = gpiod_get_value_cansleep(dcp->hdmi_hpd); diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index 5e6a81ee6419a..783ef63094734 100644 --- a/drivers/gpu/drm/apple/iomfb_template.c +++ b/drivers/gpu/drm/apple/iomfb_template.c @@ -1091,6 +1091,8 @@ void DCP_FW_NAME(iomfb_sleep)(struct apple_dcp *dcp) static void dcpep_cb_hotplug(struct apple_dcp *dcp, u64 *connected) { struct apple_connector *connector = dcp->connector; + /* sampled before complete_all() lets dcp_poweron() clear it */ + bool poweron_hpd = READ_ONCE(dcp->typec_poweron_hpd); unsigned int action; /* DCP issues hotplug_gated callbacks after SetPowerState() calls on @@ -1144,8 +1146,13 @@ static void dcpep_cb_hotplug(struct apple_dcp *dcp, u64 *connected) * A Type-C sink can assert HPD only after a modeset has already failed, * as a TV behind a DP-to-HDMI converter does when it wakes from standby. * The connector state does not change then; re-apply the mode anyway. + * + * Not for the HPD dcp_poweron() waits for: the modeset of that commit + * has not started yet, rather than failed. Reporting the link as bad + * then makes wlroots replace the output, which powers the CRTC off and + * on again, and the next wake does the same, forever. */ - if (*connected && dcp_is_typec_output(dcp) && + if (*connected && dcp_is_typec_output(dcp) && !poweron_hpd && !READ_ONCE(dcp->mode_state.valid) && !READ_ONCE(dcp->mode_state.changing)) { dcp->swap_watchdog_retrains = 0; diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index 7145d3b13d229..88943830f6544 100644 --- a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py +++ b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py @@ -925,7 +925,7 @@ def hotplug(root): struct { bool valid,changing; } mode_state; struct work_struct external_stop_work,vblank_wq; bool external_physically_absent,main_display,typec_crtc_off,external_cable,external_error,external_power_attempted,external_stopped; - bool external_hotplug_pending,external_hotplug_connected; + bool external_hotplug_pending,external_hotplug_connected,typec_poweron_hpd; u64 external_stop_generation,external_rx_generation,external_crtc_generation; unsigned swap_watchdog_retrains;int typec_iomfb_hpd_ready;''') stubs = r''' From cf7ddf4de16c678739540f02c62071a221cfe271 Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Mon, 12 Oct 2026 14:01:54 +0200 Subject: [PATCH 4/9] drm: apple: audio: play into the void without a sink On the M1 Mac mini with two Fujitsu E24T-7 Pro monitors, which have no audio (single-block EDID 1.3), PipeWire logs spa.alsa: hw:0p: snd_pcm_start: No such device or address about 130000 times a second, for hours, as soon as something plays to a DCP audio output: 4.1 million log lines in one boot, with journald dropping most of them, and a core kept busy. Without a sink, opening, configuring and preparing the PCM succeed, as userspace drops an output that fails its probe for good, but START fails with -ENXIO. That does not stop userspace from trying: PipeWire starts playback after its first write and ignores the error, and a stream that never starts never makes it wait, so the next wakeup is immediate and tries again. Any other error ends up in its recovery, which prepares and starts again. Play into the void instead, as HDMI PCMs on other platforms do: without a sink, or once the sink the stream was opened for is gone, START succeeds and the stream runs on the clock, without DMA and without a DCP audio link. The position follows the time since START and a work item reports the periods. The samples are dropped. A stream playing into the void is not stopped as disconnected when a sink goes away; a sink attached later is used by the next stream opened. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/audio.c | 113 ++++++++++++++++++++++++++++++---- 1 file changed, 100 insertions(+), 13 deletions(-) diff --git a/drivers/gpu/drm/apple/audio.c b/drivers/gpu/drm/apple/audio.c index cfbb09863fa4b..b2706c7ac3f82 100644 --- a/drivers/gpu/drm/apple/audio.c +++ b/drivers/gpu/drm/apple/audio.c @@ -53,6 +53,11 @@ struct dcp_audio { struct snd_pcm_chmap_elem selected_chmap; struct dcp_sound_cookie selected_cookie; + /* stream runs without a sink, see dcpaud_void_start() */ + bool void_running; + ktime_t void_start; + u64 void_periods; + struct delayed_work void_work; void *elements; void *productattrs; @@ -82,7 +87,7 @@ static const struct snd_pcm_hardware dcp_pcm_hw = { * configured, as HDMI PCMs on other platforms can. Userspace (PipeWire, * PulseAudio) probes the PCM once when the card appears and drops the * output for good if that fails, so a monitor attached later would never - * show up. Starting the stream still fails until a sink is connected. + * show up. The stream then plays into the void, see dcpaud_void_start(). */ static const struct snd_pcm_hardware dcp_pcm_hw_unplugged = { .info = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID | @@ -365,12 +370,10 @@ static int dcp_pcm_hw_params(struct snd_pcm_substream *substream, struct dma_chan *chan = snd_dmaengine_pcm_get_chan(substream); int ret; - if (dcpaud->open_unplugged) + /* no sink: the stream runs without DMA, see dcpaud_void_start() */ + if (dcpaud->open_unplugged || !dcpaud_connection_up(dcpaud)) return 0; - if (!dcpaud_connection_up(dcpaud)) - return -ENXIO; - ret = dcpaud_select_cookie(dcpaud, params); if (ret < 0) return ret; @@ -410,16 +413,79 @@ static int dcp_pcm_prepare(struct snd_pcm_substream *substream) struct dcp_audio *dcpaud = substream->pcm->private_data; /* alsa-lib prepares right after hw_params; see dcp_pcm_hw_unplugged */ - if (dcpaud->open_unplugged) + if (dcpaud->open_unplugged || !dcpaud_connection_up(dcpaud)) return 0; - if (!dcpaud_connection_up(dcpaud)) - return -ENXIO; - return dcp_audiosrv_prepare(dcpaud->dcp_dev, &dcpaud->selected_cookie); } +static u64 dcpaud_void_period_ns(struct snd_pcm_runtime *runtime) +{ + return div_u64((u64)runtime->period_size * NSEC_PER_SEC, runtime->rate); +} + +static void dcpaud_void_work(struct work_struct *work) +{ + struct dcp_audio *dcpaud = container_of(to_delayed_work(work), + struct dcp_audio, void_work); + struct snd_pcm_substream *substream = dcpaud->substream; + ktime_t next; + s64 delay; + + snd_pcm_period_elapsed(substream); + + snd_pcm_stream_lock_irq(substream); + if (dcpaud->void_running) { + /* the next period boundary, so that periods do not drift */ + next = ktime_add_ns(dcpaud->void_start, + ++dcpaud->void_periods * + dcpaud_void_period_ns(substream->runtime)); + delay = ktime_to_ns(ktime_sub(next, ktime_get())); + schedule_delayed_work(&dcpaud->void_work, + delay > 0 ? nsecs_to_jiffies(delay) : 0); + } + snd_pcm_stream_unlock_irq(substream); +} + +/* + * Without a sink, DCP has no audio link to start. Failing START does not + * stop userspace from trying: PipeWire retries it on every wakeup, and a + * stream that never starts never makes it wait, so it retries at about + * 130000 times a second for as long as something plays (a monitor + * without audio, or none attached). Play into the void instead, as HDMI + * PCMs elsewhere do: the stream runs on the clock, without DMA, and its + * samples are dropped. A sink attached later gets the next stream opened. + */ +static int dcpaud_void_start(struct snd_pcm_substream *substream) +{ + struct dcp_audio *dcpaud = substream->pcm->private_data; + u64 period_ns = dcpaud_void_period_ns(substream->runtime); + + dev_dbg(dcpaud->dev, "no sink, playing into the void\n"); + dcpaud->void_running = true; + dcpaud->void_start = ktime_get(); + dcpaud->void_periods = 0; + schedule_delayed_work(&dcpaud->void_work, nsecs_to_jiffies(period_ns)); + + return 0; +} + +static snd_pcm_uframes_t dcp_pcm_pointer(struct snd_pcm_substream *substream) +{ + struct dcp_audio *dcpaud = substream->pcm->private_data; + struct snd_pcm_runtime *runtime = substream->runtime; + u64 frames; + + if (!dcpaud->void_running) + return snd_dmaengine_pcm_pointer(substream); + + frames = mul_u64_u32_div(ktime_to_ns(ktime_sub(ktime_get(), + dcpaud->void_start)), + runtime->rate, NSEC_PER_SEC); + return do_div(frames, runtime->buffer_size); +} + static int dcp_pcm_trigger(struct snd_pcm_substream *substream, int cmd) { struct dcp_audio *dcpaud = substream->pcm->private_data; @@ -428,8 +494,8 @@ static int dcp_pcm_trigger(struct snd_pcm_substream *substream, int cmd) switch (cmd) { case SNDRV_PCM_TRIGGER_START: case SNDRV_PCM_TRIGGER_RESUME: - if (!dcpaud_connection_up(dcpaud)) - return -ENXIO; + if (dcpaud->open_unplugged || !dcpaud_connection_up(dcpaud)) + return dcpaud_void_start(substream); WARN_ON(pm_runtime_get_sync(dcpaud->dev) < 0); ret = dcp_audiosrv_startlink(dcpaud->dcp_dev, @@ -440,6 +506,11 @@ static int dcp_pcm_trigger(struct snd_pcm_substream *substream, int cmd) case SNDRV_PCM_TRIGGER_STOP: case SNDRV_PCM_TRIGGER_SUSPEND: + if (dcpaud->void_running) { + /* sync_stop waits for the work */ + dcpaud->void_running = false; + return 0; + } break; default: @@ -468,6 +539,19 @@ static int dcp_pcm_trigger(struct snd_pcm_substream *substream, int cmd) return 0; } +/* + * ALSA calls this once a stream has stopped, outside the stream lock, before + * the next hw_params, prepare or hw_free and at close. + */ +static int dcp_pcm_sync_stop(struct snd_pcm_substream *substream) +{ + struct dcp_audio *dcpaud = substream->pcm->private_data; + + cancel_delayed_work_sync(&dcpaud->void_work); + + return 0; +} + struct snd_pcm_ops dcp_playback_ops = { .open = dcp_pcm_open, .close = dcp_pcm_close, @@ -475,7 +559,8 @@ struct snd_pcm_ops dcp_playback_ops = { .hw_free = dcp_pcm_hw_free, .prepare = dcp_pcm_prepare, .trigger = dcp_pcm_trigger, - .pointer = snd_dmaengine_pcm_pointer, + .sync_stop = dcp_pcm_sync_stop, + .pointer = dcp_pcm_pointer, }; // Transitional workaround: for the chmap control TLV, advertise options @@ -557,7 +642,8 @@ static void dcpaud_report_hotplug(struct dcp_audio *dcpaud, bool connected) if (!connected) { snd_pcm_stream_lock(substream); - if (substream->runtime) + /* a stream without a sink plays on, see dcpaud_void_start() */ + if (substream->runtime && !dcpaud->void_running) snd_pcm_stop(substream, SNDRV_PCM_STATE_DISCONNECTED); snd_pcm_stream_unlock(substream); } @@ -769,6 +855,7 @@ static int dcpaud_probe(struct platform_device *pdev) dcpaud->dev = &pdev->dev; mutex_init(&dcpaud->data_lock); + INIT_DELAYED_WORK(&dcpaud->void_work, dcpaud_void_work); platform_set_drvdata(pdev, dcpaud); return component_add(&pdev->dev, &dcpaud_comp_ops); From 99415c89b3b217d6f95c70fb7cd95e8bb9d2c74a Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Wed, 14 Oct 2026 20:02:50 +0200 Subject: [PATCH 5/9] drm: apple: reset the Type-C relink budget on every DPMS wake A USB-C monitor with a built-in hub drops the DP link right after the first modeset of every DPMS wake, which dcp_typec_relink() recovers by replaying an unplug and replug. Its retry counter is only reset when the Type-C generation changes, i.e. on a real cable replug, so all wakes of one connection share DPTX_RELINK_RETRIES tries: after three wakes the fourth drop is handled as an unplug and the display stays dark until the cable is replugged. The limit exists to stop relink loops within one link bring-up. Reset it in dcp_poweron() when the CRTC comes back from DPMS off. No relink can run while the CRTC is off, dcp_typec_relink_start() refuses then. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/dcp.c | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/drivers/gpu/drm/apple/dcp.c b/drivers/gpu/drm/apple/dcp.c index 00f9449d3d1d8..7b01eaf6d59a7 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -1144,7 +1144,8 @@ static void dcp_typec_reconnect_work(struct work_struct *work) * the sink, which retrains the link once the hub has settled, as a cable * replug does. * - * The retries cover one connection: the replay itself does not reset them. + * The retries cover one connection or one DPMS wake: the replay itself + * does not reset them, dcp_poweron() does. */ static bool dcp_typec_relink_start(struct apple_dcp *dcp, const char *why) { @@ -1756,6 +1757,12 @@ void dcp_poweron(struct platform_device *pdev) READ_ONCE(dcp->typec_cable_connected); WRITE_ONCE(dcp->typec_crtc_off, false); WRITE_ONCE(dcp->typec_follow_start, false); + /* + * A DPMS wake retrains the link like a replug: give it a fresh + * relink budget. No relink runs while the CRTC was off. + */ + if (wait_for_typec_hpd) + dcp->relink_tries = 0; /* * A Type-C CRTC disable releases its DPTX session. Re-establish it From ce9084c4138f96e8e68ad15ef623d3f48549321e Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Wed, 14 Oct 2026 21:41:25 +0200 Subject: [PATCH 6/9] drm: apple: do not report a bad link for a powered-off Type-C CRTC Since commit 57f9f3623299 ("drm/apple: fence EDID publication by service lifetime and unblock RX"), the teardown of DCP's AV service schedules the connector hotplug worker to invalidate the EDID. DCP tears that service down whenever a Type-C CRTC is powered off with the display attached (DPMS off). dcp_hotplug() then finds the connector connected without a valid mode and reports the link as bad. wlroots treats a bad link as a disconnect and replaces the output on the next wake, which powers the CRTC off and on again, and that power off does the same: the display never comes back from the first DPMS off (Mac mini, EIZO EV2780 on USB-C). There is no link while the CRTC is off, and dcp_poweron() retrains one on wake. Leave the link status and the CRTC alone then; the EDID is still invalidated. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/iomfb.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/drivers/gpu/drm/apple/iomfb.c b/drivers/gpu/drm/apple/iomfb.c index e0aa8b2e8d29c..f2673d637279c 100644 --- a/drivers/gpu/drm/apple/iomfb.c +++ b/drivers/gpu/drm/apple/iomfb.c @@ -960,8 +960,16 @@ void dcp_hotplug(struct work_struct *work) * display modes from atomic_flush, so userspace needs to trigger a * flush, or the CRTC gets no signal. */ + /* + * Not for a Type-C CRTC powered off with the display attached: it has + * no link, and dcp_poweron() retrains one on wake. DCP tears its AV + * service down at power off, which lands here; a link reported bad + * then makes wlroots replace the output on wake, which powers the + * CRTC off and on again, and that wake does the same, forever. + */ if (!notify_only && connector->base.state && !READ_ONCE(dcp->mode_state.valid) && connector->connected && + !READ_ONCE(dcp->typec_crtc_off) && !(dcp_uses_t6020_tunnel_flow(dcp))) { drm_connector_set_link_status_property(&connector->base, DRM_MODE_LINK_STATUS_BAD); From ffadcdbe431ddc6c753a573d5aa291ee7d039e4a Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Sun, 11 Oct 2026 21:00:37 +0200 Subject: [PATCH 7/9] drm: apple: audio: limit the number of periods to what SIO can serve SIO has 64 descriptor slots, shared by all its channels, and its cyclic prep takes one slot per period. The PCM allowed any number of periods, so PipeWire's default of 256-frame periods in a 32768-frame buffer asked for 128 of them. sio_prep_dma_cyclic() then ran out of slots, snd_dmaengine_pcm_trigger() returned -ENOMEM, ALSA undid the START with a STOP (a stop_link round trip to DCP), and PipeWire retried about 100 times a second with no sound. Up to three DP audio outputs share one SIO on t600x and t602x, so allow at most 16 periods per stream, 48 slots for all three. Fixes: cb24343242a7 ("gpu: drm: apple: Add DCP audio driver") Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/audio.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/drivers/gpu/drm/apple/audio.c b/drivers/gpu/drm/apple/audio.c index b2706c7ac3f82..cfdfd606cf129 100644 --- a/drivers/gpu/drm/apple/audio.c +++ b/drivers/gpu/drm/apple/audio.c @@ -34,6 +34,13 @@ #define DCPAUD_ELEMENTS_MAXSIZE 16384 #define DCPAUD_PRODUCTATTRS_MAXSIZE 1024 +/* + * SIO takes one of its 64 descriptor slots per period, shared by all its + * channels, and up to three DP audio outputs use one SIO. A stream with + * more periods fails START with -ENOMEM. + */ +#define DCPAUD_PERIODS_MAX 16 + struct dcp_audio { struct device *dev; struct device *dcp_dev; @@ -78,7 +85,7 @@ static const struct snd_pcm_hardware dcp_pcm_hw = { .period_bytes_min = 4096, /* TODO */ .period_bytes_max = SIZE_MAX, .periods_min = 2, - .periods_max = UINT_MAX, + .periods_max = DCPAUD_PERIODS_MAX, }; /* @@ -104,7 +111,7 @@ static const struct snd_pcm_hardware dcp_pcm_hw_unplugged = { .period_bytes_min = 4096, .period_bytes_max = SIZE_MAX, .periods_min = 2, - .periods_max = UINT_MAX, + .periods_max = DCPAUD_PERIODS_MAX, }; static bool dcpaud_h17p(struct dcp_audio *dcpaud) @@ -329,7 +336,7 @@ static int dcp_pcm_open(struct snd_pcm_substream *substream) hw.info = SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID | SNDRV_PCM_INFO_INTERLEAVED; hw.periods_min = 2; - hw.periods_max = UINT_MAX; + hw.periods_max = DCPAUD_PERIODS_MAX; hw.period_bytes_min = 256; hw.period_bytes_max = SIZE_MAX; // TODO dma_get_max_seg_size(dma_dev); hw.buffer_bytes_max = SIZE_MAX; From 2f645406d54107b4de63ba68fe48e3ea6e55e88e Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Mon, 12 Oct 2026 12:45:13 +0200 Subject: [PATCH 8/9] media: apple: avd: allow more than 32 capture buffers per context The capture queue never sets max_num_buffers, so vb2 limits every context to VB2_MAX_FRAME (32) buffers. Nothing in the driver needs that limit: reference frames are looked up by timestamp (vb2_find_buffer()) and handed to the hardware by DMA address, never by buffer index. 32 buffers leave too little room for userspace to decode ahead. With a 16-frame DPB, four frame threads and the frames held for display, mpv's decoder queue (--vd-queue) gets only a few frames before VIDIOC_CREATE_BUFS fails with ENOBUFS, and the FFmpeg V4L2 request hwaccel then falls back to software decoding. Decoding ahead is needed because the t8103 AVD decodes 4K59.94 H.264 at only ~41 fps during a 125-156 Mbit/s burst, so playback drops frames unless the decoder has built up a lead of 60-90 frames beforehand. Add a max_capture_buffers module parameter, default 128. Buffers are still allocated on demand by userspace, so this only raises the limit. Signed-off-by: Thomas Glanzmann --- drivers/media/platform/apple/avd/avd-drv.c | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/drivers/media/platform/apple/avd/avd-drv.c b/drivers/media/platform/apple/avd/avd-drv.c index 8f7a9d99322b5..270ffb6996596 100644 --- a/drivers/media/platform/apple/avd/avd-drv.c +++ b/drivers/media/platform/apple/avd/avd-drv.c @@ -429,6 +429,17 @@ static void avd_device_run(void *priv) avd_job_finish(ctx, result); } +/* + * Capture buffers are found by timestamp and handed to the hardware by DMA + * address, so nothing limits them to VB2_MAX_FRAME. A larger pool lets + * userspace decode ahead to ride out bitrate spikes the hardware can't + * decode in real time (e.g. 150+ Mbit/s 4K60 H.264). + */ +static unsigned int max_capture_buffers = 128; +module_param(max_capture_buffers, uint, 0444); +MODULE_PARM_DESC(max_capture_buffers, + "Maximum number of capture buffers per context (default 128)"); + static int avd_queue_init(void *priv, struct vb2_queue *src_vq, struct vb2_queue *dst_vq) { @@ -466,6 +477,7 @@ static int avd_queue_init(void *priv, struct vb2_queue *src_vq, dst_vq->lock = &ctx->dev->vdev_lock; dst_vq->dev = ctx->dev->v4l2_dev.dev; dst_vq->allow_cache_hints = true; + dst_vq->max_num_buffers = max(max_capture_buffers, VB2_MAX_FRAME); return vb2_queue_init(dst_vq); } From d0153933646a4199821eec1364a162389cc76dfc Mon Sep 17 00:00:00 2001 From: Thomas Glanzmann Date: Thu, 15 Oct 2026 08:49:52 +0200 Subject: [PATCH 9/9] drm/apple: av: recycle AV endpoint service slots The AV endpoint (0x29) announces a fresh DCPAVSimpleVideoInterface and DCPAVAudioInterface on new channels for every DPTX connection, so every display replug or DPMS wake that drops HPD uses two more slots. Since "drm/apple: pin DP service owners before recycling retired AFK slots" a torn-down slot is only reclaimed if its ops opt in with .reusable, which the AV services never did. After eight connections the 16-slot table is full, every later announcement fails with AFK[ep:29]: no reusable service slots and the audio service of that port is not re-registered until reboot. Convert the AV owners and opt in to reuse: - DCPAVSimpleVideoInterface keeps no pointer to its service. - asrv->srv now holds an owner pin taken in init and dropped in teardown. A teardown of an older generation no longer clears the pointer or unplugs the jack of its successor. - av_service_connect() and av_service_disconnect() call into the service after dropping srv_rwsem, so they pin it for the duration. Add afk_service_pin() for this; unlike afk_service_get() it also succeeds after teardown, so the close is still sent. Add a KUnit case for a close pinned across teardown. Signed-off-by: Thomas Glanzmann --- drivers/gpu/drm/apple/afk-service-test.c | 18 ++++++++++ drivers/gpu/drm/apple/afk.h | 19 ++++++++++ drivers/gpu/drm/apple/av.c | 46 ++++++++++++++++++------ 3 files changed, 72 insertions(+), 11 deletions(-) diff --git a/drivers/gpu/drm/apple/afk-service-test.c b/drivers/gpu/drm/apple/afk-service-test.c index 1cf589ae7a417..75a352bec0481 100644 --- a/drivers/gpu/drm/apple/afk-service-test.c +++ b/drivers/gpu/drm/apple/afk-service-test.c @@ -101,6 +101,23 @@ static void afk_service_completed_reader_test(struct kunit *test) KUNIT_EXPECT_TRUE(test, afk_service_reinit(service, NULL, &reusable_ops, 2)); } +static void afk_service_close_pin_test(struct kunit *test) +{ + struct apple_epic_service *service = test->priv; + + KUNIT_EXPECT_PTR_EQ(test, afk_service_pin(NULL), NULL); + KUNIT_ASSERT_TRUE(test, afk_service_reinit(service, NULL, &reusable_ops, 1)); + KUNIT_ASSERT_PTR_EQ(test, afk_service_get(service), service); /* owner */ + afk_test_retire(service); + /* A close issued after teardown still pins the owner's service. */ + KUNIT_ASSERT_PTR_EQ(test, afk_service_pin(service), service); + afk_service_put(service); /* owner detaches */ + KUNIT_EXPECT_FALSE(test, afk_service_reinit(service, NULL, &reusable_ops, 2)); + KUNIT_EXPECT_EQ(test, service->channel, 1U); + afk_service_put(service); + KUNIT_EXPECT_TRUE(test, afk_service_reinit(service, NULL, &reusable_ops, 2)); +} + static void afk_service_tag_sequence_test(struct kunit *test) { struct apple_epic_service *service = test->priv; @@ -226,6 +243,7 @@ static struct kunit_case afk_service_cases[] = { KUNIT_CASE(afk_service_reader_pin_test), KUNIT_CASE(afk_service_pending_reply_test), KUNIT_CASE(afk_service_completed_reader_test), + KUNIT_CASE(afk_service_close_pin_test), KUNIT_CASE(afk_service_tag_sequence_test), KUNIT_CASE(afk_service_unconverted_owner_test), { } diff --git a/drivers/gpu/drm/apple/afk.h b/drivers/gpu/drm/apple/afk.h index 94926860ba33f..1186ec19f2f75 100644 --- a/drivers/gpu/drm/apple/afk.h +++ b/drivers/gpu/drm/apple/afk.h @@ -159,6 +159,25 @@ static inline void afk_service_put(struct apple_epic_service *service) afk_service_retry(service->ep); } +/* + * Take another pin on a service the caller already keeps pinned, e.g. an + * owner pointer read under the owner's lock. Unlike afk_service_get() this + * also succeeds after teardown, so a close can still be issued. + */ +static inline struct apple_epic_service *afk_service_pin(struct apple_epic_service *service) +{ + unsigned long flags; + + if (!service) + return NULL; + + spin_lock_irqsave(&service->lock, flags); + service->users++; + spin_unlock_irqrestore(&service->lock, flags); + + return service; +} + static inline void afk_service_disable(struct apple_epic_service *service) { unsigned long flags; diff --git a/drivers/gpu/drm/apple/av.c b/drivers/gpu/drm/apple/av.c index 563a02bb4e551..f79fbd8e32614 100644 --- a/drivers/gpu/drm/apple/av.c +++ b/drivers/gpu/drm/apple/av.c @@ -96,12 +96,21 @@ static void av_audiosrv_init(struct apple_epic_service *service, const char *nam { struct apple_dcp *dcp = service->ep->dcp; struct audiosrv_data *asrv = dcp->audiosrv; + struct apple_epic_service *old; + + /* The owner pin keeps the slot from being recycled while published. */ + service = afk_service_get(service); + if (!service) + return; mutex_lock(&asrv->plug_lock); down_write(&asrv->srv_rwsem); + old = asrv->srv; asrv->srv = service; up_write(&asrv->srv_rwsem); + if (old) + afk_service_put(old); asrv->plugged = true; mutex_unlock(&asrv->plug_lock); @@ -112,18 +121,27 @@ static void av_audiosrv_teardown(struct apple_epic_service *service) { struct apple_dcp *dcp = service->ep->dcp; struct audiosrv_data *asrv = dcp->audiosrv; + bool owned; mutex_lock(&asrv->plug_lock); down_write(&asrv->srv_rwsem); - asrv->srv = NULL; + owned = asrv->srv == service; + if (owned) + asrv->srv = NULL; up_write(&asrv->srv_rwsem); - asrv->plugged = false; - if (asrv->audio_dev) - dcpaud_disconnect(asrv->audio_dev); + /* A stale generation must not unplug its successor. */ + if (owned) { + asrv->plugged = false; + if (asrv->audio_dev) + dcpaud_disconnect(asrv->audio_dev); + } mutex_unlock(&asrv->plug_lock); + + if (owned) + afk_service_put(service); } int dcp_audiosrv_prepare(struct device *dev, struct dcp_sound_cookie *cookie) @@ -279,11 +297,13 @@ static int av_audiosrv_report(struct apple_epic_service *service, u32 idx, static const struct apple_epic_service_ops avep_ops[] = { { .name = "DCPAVSimpleVideoInterface", + .reusable = true, .init = av_interface_init, .teardown = av_interface_teardown, }, { .name = "DCPAVAudioInterface", + .reusable = true, .init = av_audiosrv_init, .report = av_audiosrv_report, .teardown = av_audiosrv_teardown, @@ -298,21 +318,21 @@ void av_service_connect(struct apple_dcp *dcp) int ret; scoped_guard(rwsem_write, &asrv->srv_rwsem) { - if (!asrv->srv) + service = afk_service_pin(asrv->srv); + if (!service) return; - service = asrv->srv; } /* open AV audio service */ dev_info(dcp->dev, "%s: starting audio service, plugged:%d\n", __func__, asrv->plugged); if (asrv->is_open) - return; + goto out; ret = afk_service_call(service, 0, asrv->cmds.open, NULL, 0, 32, NULL, 0, 32); if (ret) { dev_err(dcp->dev, "error opening audio service: %d\n", ret); - return; + goto out; } mutex_lock(&asrv->plug_lock); asrv->is_open = true; @@ -320,6 +340,8 @@ void av_service_connect(struct apple_dcp *dcp) if (asrv->audio_dev) dcpaud_connect(asrv->audio_dev, asrv->plugged); mutex_unlock(&asrv->plug_lock); +out: + afk_service_put(service); } void av_service_disconnect(struct apple_dcp *dcp) @@ -329,15 +351,15 @@ void av_service_disconnect(struct apple_dcp *dcp) int ret; scoped_guard(rwsem_write, &asrv->srv_rwsem) { - if (!asrv->srv) + service = afk_service_pin(asrv->srv); + if (!service) return; - service = asrv->srv; } /* close AV audio service */ dev_info(dcp->dev, "%s: stopping audio service\n", __func__); if (!asrv->is_open) - return; + goto out; mutex_lock(&asrv->plug_lock); @@ -354,6 +376,8 @@ void av_service_disconnect(struct apple_dcp *dcp) if (service->torndown) afk_service_disable(service); asrv->is_open = false; +out: + afk_service_put(service); } static void av_work_service_start(struct work_struct *work)