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/audio.c b/drivers/gpu/drm/apple/audio.c index cfbb09863fa4b..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; @@ -53,6 +60,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; @@ -73,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, }; /* @@ -82,7 +94,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 | @@ -99,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) @@ -324,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; @@ -365,12 +377,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 +420,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 +501,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 +513,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 +546,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 +566,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 +649,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 +862,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); 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) 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..4d706327793c0 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; @@ -524,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 651c5eb428eea..7b01eaf6d59a7 100644 --- a/drivers/gpu/drm/apple/dcp.c +++ b/drivers/gpu/drm/apple/dcp.c @@ -1129,6 +1129,111 @@ 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 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) +{ + 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); @@ -1652,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 @@ -1660,6 +1771,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); @@ -1671,6 +1784,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); @@ -2158,6 +2272,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 +2284,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 +2730,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.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); diff --git a/drivers/gpu/drm/apple/iomfb_template.c b/drivers/gpu/drm/apple/iomfb_template.c index b11451c53eece..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 @@ -1121,6 +1123,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); @@ -1138,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/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); } 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) diff --git a/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py b/tools/testing/selftests/drivers/gpu/apple-dcp/rate.py index 32585f1755845..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''' @@ -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:]