From c20cc217203e693c73e27fa9245ae88d1b6ff3a2 Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Tue, 6 Oct 2026 21:56:12 +0800 Subject: [PATCH 1/9] soundwire: stream: add support for ignoring system suspend Some streams, such as those used for wake-on-voice, must remain active during system suspend. Add an ignore_suspend flag to sdw_stream_runtime and export sdw_bus_ignore_suspend() so bus drivers can check whether any stream on the bus requires it to remain active. Signed-off-by: Bard Liao --- drivers/soundwire/stream.c | 19 +++++++++++++++++++ include/linux/soundwire/sdw.h | 3 +++ 2 files changed, 22 insertions(+) diff --git a/drivers/soundwire/stream.c b/drivers/soundwire/stream.c index c158bf91e020f9..76e5c0369ca04f 100644 --- a/drivers/soundwire/stream.c +++ b/drivers/soundwire/stream.c @@ -1688,6 +1688,25 @@ int sdw_enable_stream(struct sdw_stream_runtime *stream) } EXPORT_SYMBOL(sdw_enable_stream); +bool sdw_bus_ignore_suspend(struct sdw_bus *bus) +{ + struct sdw_master_runtime *m_rt; + bool ret = false; + + mutex_lock(&bus->bus_lock); + list_for_each_entry(m_rt, &bus->m_rt_list, bus_node) { + if (m_rt->stream->state == SDW_STREAM_ENABLED && + m_rt->stream->ignore_suspend) { + ret = true; + break; + } + } + mutex_unlock(&bus->bus_lock); + + return ret; +} +EXPORT_SYMBOL(sdw_bus_ignore_suspend); + static int _sdw_disable_stream(struct sdw_stream_runtime *stream) { struct sdw_master_runtime *m_rt; diff --git a/include/linux/soundwire/sdw.h b/include/linux/soundwire/sdw.h index 326db122bbe6ea..ac9755d11daa2d 100644 --- a/include/linux/soundwire/sdw.h +++ b/include/linux/soundwire/sdw.h @@ -978,6 +978,7 @@ struct sdw_stream_params { * @master_list: List of Master runtime(s) in this stream. * master_list can contain only one m_rt per Master instance * for a stream + * @ignore_suspend: stream keeps running during system suspend */ struct sdw_stream_runtime { const char *name; @@ -986,6 +987,7 @@ struct sdw_stream_runtime { enum sdw_stream_type type; int m_rt_count; struct list_head master_list; + bool ignore_suspend; }; /** @@ -1086,6 +1088,7 @@ int sdw_stream_remove_master(struct sdw_bus *bus, int sdw_startup_stream(void *sdw_substream); int sdw_prepare_stream(struct sdw_stream_runtime *stream); int sdw_enable_stream(struct sdw_stream_runtime *stream); +bool sdw_bus_ignore_suspend(struct sdw_bus *bus); int sdw_disable_stream(struct sdw_stream_runtime *stream); int sdw_deprepare_stream(struct sdw_stream_runtime *stream); void sdw_shutdown_stream(void *sdw_substream); From 16f5615b109c296d387dfaca6a39085332362b4d Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Tue, 6 Oct 2026 22:06:50 +0800 Subject: [PATCH 2/9] soundwire: Intel: preserve buses for ignore-suspend streams Honor the SoundWire bus ignore-suspend state in the Intel drivers. Preserve the master across system suspend when a stream requires the bus to remain active. Signed-off-by: Bard Liao --- drivers/soundwire/intel.c | 3 +++ drivers/soundwire/intel_ace2x.c | 3 +++ drivers/soundwire/intel_auxdevice.c | 16 ++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/drivers/soundwire/intel.c b/drivers/soundwire/intel.c index af65214836b4d0..2064e64c2c5455 100644 --- a/drivers/soundwire/intel.c +++ b/drivers/soundwire/intel.c @@ -953,6 +953,9 @@ static int intel_component_dais_suspend(struct snd_soc_component *component) struct sdw_cdns *cdns = snd_soc_dai_get_drvdata(dai); struct sdw_cdns_dai_runtime *dai_runtime; + if (sdw_bus_ignore_suspend(&cdns->bus)) + continue; + dai_runtime = cdns->dai_runtime_array[dai->id]; if (dai_runtime) diff --git a/drivers/soundwire/intel_ace2x.c b/drivers/soundwire/intel_ace2x.c index 642b33ab552606..717531d737f586 100644 --- a/drivers/soundwire/intel_ace2x.c +++ b/drivers/soundwire/intel_ace2x.c @@ -952,6 +952,9 @@ static int intel_component_dais_suspend(struct snd_soc_component *component) struct sdw_cdns *cdns = snd_soc_dai_get_drvdata(dai); struct sdw_cdns_dai_runtime *dai_runtime; + if (sdw_bus_ignore_suspend(&cdns->bus)) + continue; + dai_runtime = cdns->dai_runtime_array[dai->id]; if (dai_runtime) diff --git a/drivers/soundwire/intel_auxdevice.c b/drivers/soundwire/intel_auxdevice.c index 5983fbc5725a26..5c1bb08ac9dba9 100644 --- a/drivers/soundwire/intel_auxdevice.c +++ b/drivers/soundwire/intel_auxdevice.c @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -650,6 +651,14 @@ static int __maybe_unused intel_suspend(struct device *dev) return 0; } + /* + * ignore_suspend is set by the PCM trigger for S0ix and may remain set + * if a later sleep transition does not issue another PCM trigger. + */ + if (pm_suspend_target_state == PM_SUSPEND_TO_IDLE && + sdw_bus_ignore_suspend(bus)) + return 0; + /* Prevent runtime PM from racing with the code below. */ pm_runtime_disable(dev); @@ -737,6 +746,13 @@ static int __maybe_unused intel_resume(struct device *dev) return 0; } + if (pm_suspend_target_state == PM_SUSPEND_TO_IDLE && + sdw_bus_ignore_suspend(bus)) { + dev_dbg(dev, "SoundWire master %d remained active during system suspend\n", + bus->link_id); + return 0; + } + ret = sdw_intel_link_power_up(sdw); if (ret) { dev_err(dev, "%s failed: %d\n", __func__, ret); From 35da46843f483dd00b3465145c62b6a14bbcb86a Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Tue, 6 Oct 2026 21:49:39 +0800 Subject: [PATCH 3/9] ASoC: soc_sdw_utils: preserve streams that ignore suspend Do not disable a SoundWire stream on suspend when it is marked to remain active. If the stream is already enabled on resume, no further action is needed. Signed-off-by: Bard Liao --- sound/soc/sdw_utils/soc_sdw_utils.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sound/soc/sdw_utils/soc_sdw_utils.c b/sound/soc/sdw_utils/soc_sdw_utils.c index 7a19de75f32b53..2072e3e215708c 100644 --- a/sound/soc/sdw_utils/soc_sdw_utils.c +++ b/sound/soc/sdw_utils/soc_sdw_utils.c @@ -1902,6 +1902,10 @@ int asoc_sdw_trigger(struct snd_pcm_substream *substream, int cmd) switch (cmd) { case SNDRV_PCM_TRIGGER_RESUME: + if (sdw_stream->state == SDW_STREAM_ENABLED) { + ret = 0; + break; + } /* * The peripherals lose their port configuration when the * controller is power-gated during system suspend, and an @@ -1923,10 +1927,12 @@ int asoc_sdw_trigger(struct snd_pcm_substream *substream, int cmd) break; case SNDRV_PCM_TRIGGER_PAUSE_PUSH: - case SNDRV_PCM_TRIGGER_SUSPEND: case SNDRV_PCM_TRIGGER_STOP: ret = sdw_disable_stream(sdw_stream); break; + case SNDRV_PCM_TRIGGER_SUSPEND: + ret = sdw_stream->ignore_suspend ? 0 : sdw_disable_stream(sdw_stream); + break; default: ret = -EINVAL; break; From 982728e80b023c0629644a644958a3b55b49c109 Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Tue, 27 Oct 2026 22:59:08 +0800 Subject: [PATCH 4/9] ASoC: dapm: Export snd_soc_dapm_mark_endpoints_dirty The follow up commit will use snd_soc_dapm_mark_endpoints_dirty() to invalidate endpoint paths after changing widget ignore_suspend flags. Signed-off-by: Bard Liao --- sound/soc/soc-dapm.c | 1 + 1 file changed, 1 insertion(+) diff --git a/sound/soc/soc-dapm.c b/sound/soc/soc-dapm.c index 74be5e20949337..436643567f6704 100644 --- a/sound/soc/soc-dapm.c +++ b/sound/soc/soc-dapm.c @@ -355,6 +355,7 @@ void snd_soc_dapm_mark_endpoints_dirty(struct snd_soc_card *card) snd_soc_dapm_mutex_unlock(card); } +EXPORT_SYMBOL_GPL(snd_soc_dapm_mark_endpoints_dirty); /* create a new dapm widget */ static inline struct snd_soc_dapm_widget *dapm_cnew_widget( From 808c2826a9fedc36ce9ac767c058427cdf07c3f3 Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Wed, 21 Oct 2026 13:54:50 +0800 Subject: [PATCH 5/9] ASoC: SOF: manage WoV DAPM retention across suspend Keep connected DAPM endpoint widgets active while SOF retains a wake-on-voice PCM across suspend. Register the card's endpoint widgets for the PCM direction when opening a PCM, along with the DAI widgets of that direction since DAPM only marks them as endpoints once their streams start. Track an ignore_suspend reference count for each registered widget. Determine the connected endpoints of the PCM direction when retaining a stream at SUSPEND, rather than caching them during prepare. Record the references acquired by each stream so shared widgets remain marked until their final WoV user releases them, even if routes change. Restore the original ignore_suspend value on the final release and refresh DAPM endpoint path caches after changing ignore_suspend. Treat widget retention as best-effort: log failures but still mark the stream suspend_ignored so FE/BE suspend bookkeeping and the retained stream resume path remain consistent. A DROP on a suspended PCM does not issue STOP, so suspend_ignored can remain set if userspace prepares the PCM instead of resuming it. Clear suspend_ignored and release DAPM retention from hw_free and close, which may also run without START or RESUME. Clear them in prepare as a final fallback when a suspended PCM is prepared directly while suspend_ignored is still set. Signed-off-by: Bard Liao Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sound/soc/sof/core.c | 1 + sound/soc/sof/pcm.c | 266 +++++++++++++++++++++++++++++++++++++- sound/soc/sof/sof-audio.h | 2 + sound/soc/sof/sof-priv.h | 10 ++ sound/soc/sof/topology.c | 1 + 5 files changed, 276 insertions(+), 4 deletions(-) diff --git a/sound/soc/sof/core.c b/sound/soc/sof/core.c index 1f38fe03e49395..37fca4c37d59bc 100644 --- a/sound/soc/sof/core.c +++ b/sound/soc/sof/core.c @@ -690,6 +690,7 @@ int snd_sof_device_probe(struct device *dev, struct snd_sof_pdata *plat_data) INIT_LIST_HEAD(&sdev->dai_list); INIT_LIST_HEAD(&sdev->dai_link_list); INIT_LIST_HEAD(&sdev->route_list); + INIT_LIST_HEAD(&sdev->ignore_suspend_widget_list); INIT_LIST_HEAD(&sdev->ipc_client_list); INIT_LIST_HEAD(&sdev->ipc_rx_handler_list); INIT_LIST_HEAD(&sdev->fw_state_handler_list); diff --git a/sound/soc/sof/pcm.c b/sound/soc/sof/pcm.c index 0161c0ae8bbdb4..08901397539ea9 100644 --- a/sound/soc/sof/pcm.c +++ b/sound/soc/sof/pcm.c @@ -66,6 +66,224 @@ void snd_sof_pcm_period_elapsed(struct snd_pcm_substream *substream) } EXPORT_SYMBOL(snd_sof_pcm_period_elapsed); +static void sof_pcm_free_ignore_suspend_widgets(struct list_head *widgets) +{ + struct snd_sof_ignore_suspend_widget *isw, *tmp; + + list_for_each_entry_safe(isw, tmp, widgets, list) { + list_del(&isw->list); + kfree(isw); + } +} + +static int sof_pcm_add_ignore_suspend_widget(struct list_head *widgets, + struct snd_soc_dapm_widget *dapm_widget) +{ + struct snd_sof_ignore_suspend_widget *isw; + + list_for_each_entry(isw, widgets, list) { + if (isw->widget == dapm_widget) + return 0; + } + + isw = kzalloc_obj(*isw); + if (!isw) + return -ENOMEM; + + isw->widget = dapm_widget; + list_add_tail(&isw->list, widgets); + + return 0; +} + +static struct snd_sof_ignore_suspend_widget * +sof_pcm_find_ignore_suspend_widget(struct snd_sof_dev *sdev, struct snd_soc_dapm_widget *widget) +{ + struct snd_sof_ignore_suspend_widget *isw; + + list_for_each_entry(isw, &sdev->ignore_suspend_widget_list, list) { + if (isw->widget == widget) + return isw; + } + + return NULL; +} + +static unsigned int sof_pcm_dir_to_ep(int dir) +{ + return SND_SOC_DAPM_DIR_TO_EP(dir == SNDRV_PCM_STREAM_PLAYBACK ? + SND_SOC_DAPM_DIR_OUT : SND_SOC_DAPM_DIR_IN); +} + +static int sof_pcm_init_ignore_suspend_widgets(struct snd_sof_dev *sdev, + struct snd_soc_card *card, int dir) +{ + enum snd_soc_dapm_type dai_id = dir == SNDRV_PCM_STREAM_PLAYBACK ? + snd_soc_dapm_dai_in : snd_soc_dapm_dai_out; + unsigned int ep = sof_pcm_dir_to_ep(dir); + struct snd_soc_dapm_widget *widget; + LIST_HEAD(widgets); + int ret = 0; + + snd_soc_dapm_mutex_lock(card); + for_each_card_widgets(card, widget) { + /* DAI widgets become endpoints when their streams start. */ + if (!(widget->is_ep & ep) && widget->id != dai_id) + continue; + + if (sof_pcm_find_ignore_suspend_widget(sdev, widget)) + continue; + + ret = sof_pcm_add_ignore_suspend_widget(&widgets, widget); + if (ret) + goto unlock; + } + list_splice_tail_init(&widgets, &sdev->ignore_suspend_widget_list); +unlock: + snd_soc_dapm_mutex_unlock(card); + sof_pcm_free_ignore_suspend_widgets(&widgets); + + return ret; +} + +static int sof_pcm_collect_ignore_suspend_widgets(struct snd_sof_dev *sdev, + struct snd_soc_pcm_runtime *rtd, + struct snd_sof_pcm *spcm, int dir, + struct list_head *widgets) +{ + enum snd_soc_dapm_type dai_id = dir == SNDRV_PCM_STREAM_PLAYBACK ? + snd_soc_dapm_dai_in : snd_soc_dapm_dai_out; + unsigned int ep = sof_pcm_dir_to_ep(dir); + struct snd_soc_dai *dai; + int j; + + for_each_rtd_cpu_dais(rtd, j, dai) { + struct snd_soc_dapm_widget_list *connected_widgets; + struct snd_soc_dapm_widget *widget; + int connected, i, ret; + + connected = snd_soc_dapm_dai_get_connected_widgets(dai, dir, + &connected_widgets, + NULL); + if (connected < 0) { + dev_dbg(sdev->dev, + "failed to enumerate D0i3-compatible PCM %s widgets: %d\n", + spcm->pcm.caps[dir].name, connected); + return connected; + } + + widget = snd_soc_dai_get_widget(dai, dir); + if (widget && widget->is_ep) { + ret = sof_pcm_add_ignore_suspend_widget(widgets, widget); + if (ret) { + snd_soc_dapm_dai_free_widgets(&connected_widgets); + return ret; + } + } + + for_each_dapm_widgets(connected_widgets, i, widget) { + if (!(widget->is_ep & ep) && widget->id != dai_id) + continue; + + ret = sof_pcm_add_ignore_suspend_widget(widgets, widget); + if (ret) { + snd_soc_dapm_dai_free_widgets(&connected_widgets); + return ret; + } + } + + snd_soc_dapm_dai_free_widgets(&connected_widgets); + } + + return 0; +} + +static void sof_pcm_put_widget_ignore_suspend(struct snd_sof_ignore_suspend_widget *isw) +{ + if (WARN_ON_ONCE(!isw->refcount)) + return; + + if (--isw->refcount) + return; + + isw->widget->ignore_suspend = isw->initial_ignore_suspend; +} + +static int sof_pcm_set_widget_ignore_suspend(struct snd_sof_dev *sdev, + struct snd_soc_pcm_runtime *rtd, + struct snd_sof_pcm *spcm, int dir, + bool ignore_suspend) +{ + struct list_head *retained_widgets = &spcm->stream[dir].ignore_suspend_widgets; + struct snd_sof_ignore_suspend_widget *isw; + LIST_HEAD(widgets); + int ret = 0; + + if (!ignore_suspend && list_empty(retained_widgets)) + return 0; + + if (ignore_suspend) { + ret = sof_pcm_collect_ignore_suspend_widgets(sdev, rtd, spcm, dir, &widgets); + if (ret) + goto out; + } + + snd_soc_dapm_mutex_lock(rtd->card); + list_for_each_entry(isw, &widgets, list) { + struct snd_sof_ignore_suspend_widget *owned; + + owned = sof_pcm_find_ignore_suspend_widget(sdev, isw->widget); + if (WARN_ON_ONCE(!owned)) { + ret = -EINVAL; + goto unlock; + } + if (owned->refcount == UINT_MAX) { + ret = -EOVERFLOW; + goto unlock; + } + } + + list_for_each_entry(isw, &widgets, list) { + struct snd_sof_ignore_suspend_widget *owned; + + owned = sof_pcm_find_ignore_suspend_widget(sdev, isw->widget); + if (!owned->refcount) + owned->initial_ignore_suspend = owned->widget->ignore_suspend; + owned->refcount++; + owned->widget->ignore_suspend = 1; + dev_dbg(sdev->dev, "WoV PCM %s ignore suspend for widget %s\n", + spcm->pcm.caps[dir].name, isw->widget->name); + } + if (ignore_suspend) { + list_splice_tail_init(&widgets, retained_widgets); + } else { + list_for_each_entry(isw, retained_widgets, list) { + struct snd_sof_ignore_suspend_widget *owned; + + owned = sof_pcm_find_ignore_suspend_widget(sdev, isw->widget); + if (WARN_ON_ONCE(!owned)) + continue; + + sof_pcm_put_widget_ignore_suspend(owned); + } + } +unlock: + snd_soc_dapm_mutex_unlock(rtd->card); + if (ret) + goto out; + + if (!ignore_suspend) + sof_pcm_free_ignore_suspend_widgets(retained_widgets); + + snd_soc_dapm_mark_endpoints_dirty(rtd->card); +out: + sof_pcm_free_ignore_suspend_widgets(&widgets); + if (ret) + spcm_err(spcm, dir, "failed to retain WoV widgets: %d\n", ret); + + return ret; +} + int sof_pcm_setup_connected_widgets(struct snd_sof_dev *sdev, struct snd_soc_pcm_runtime *rtd, struct snd_sof_pcm *spcm, struct snd_pcm_hw_params *params, @@ -303,6 +521,10 @@ static int sof_pcm_hw_free(struct snd_soc_component *component, spcm_dbg(spcm, substream->stream, "Entry: hw_free\n"); + if (spcm->stream[substream->stream].suspend_ignored) { + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, substream->stream, false); + spcm->stream[substream->stream].suspend_ignored = false; + } ret = sof_pcm_stream_free(sdev, substream, spcm, substream->stream, true); /* unprepare and free the list of DAPM widgets */ @@ -336,6 +558,16 @@ static int sof_pcm_prepare(struct snd_soc_component *component, spcm_dbg(spcm, substream->stream, "Entry: prepare\n"); + /* + * A DROP on a suspended PCM does not issue STOP. Clear the frontend's + * retained state before a later prepare/start sequence can consume it. + * The backend will check for another retained WoV FE before skipping START. + */ + if (spcm->stream[dir].suspend_ignored) { + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, dir, false); + spcm->stream[dir].suspend_ignored = false; + } + if (spcm->prepared[substream->stream]) { if (!spcm->pending_stop[substream->stream]) return 0; @@ -421,6 +653,8 @@ static int sof_pcm_trigger(struct snd_soc_component *component, * not supported, no need to re-start streams that * remained enabled in D0ix. */ + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, + substream->stream, false); spcm->stream[substream->stream].suspend_ignored = false; return 0; } @@ -430,6 +664,8 @@ static int sof_pcm_trigger(struct snd_soc_component *component, break; case SNDRV_PCM_TRIGGER_RESUME: if (spcm->stream[substream->stream].suspend_ignored) { + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, + substream->stream, false); spcm->stream[substream->stream].suspend_ignored = false; return 0; } @@ -448,6 +684,12 @@ static int sof_pcm_trigger(struct snd_soc_component *component, substream->stream == SNDRV_PCM_STREAM_CAPTURE && spcm->stream[substream->stream].d0i3_compatible && spcm->stream[substream->stream].dsp_max_burst_size_in_ms <= 1) { + if (spcm->stream[substream->stream].suspend_ignored) + return 0; + + /* Widget retention is best-effort; keep FE/BE suspend state balanced. */ + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, + substream->stream, true); spcm->stream[substream->stream].suspend_ignored = true; return 0; } @@ -566,6 +808,14 @@ static int sof_pcm_open(struct snd_soc_component *component, spcm_dbg(spcm, substream->stream, "Entry: open\n"); + ret = sof_pcm_init_ignore_suspend_widgets(sdev, rtd->card, + substream->stream); + if (ret) { + spcm_err(spcm, substream->stream, + "failed to initialize WoV endpoint widgets: %d\n", ret); + return ret; + } + caps = &spcm->pcm.caps[substream->stream]; /* set runtime config */ @@ -614,7 +864,7 @@ static int sof_pcm_close(struct snd_soc_component *component, struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(component); struct snd_sof_pcm *spcm; - int err; + int ret; /* nothing to do for BE */ if (rtd->dai_link->no_pcm) @@ -626,10 +876,14 @@ static int sof_pcm_close(struct snd_soc_component *component, spcm_dbg(spcm, substream->stream, "Entry: close\n"); - err = snd_sof_pcm_platform_close(sdev, substream); - if (err < 0) { + if (spcm->stream[substream->stream].suspend_ignored) { + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, substream->stream, false); + spcm->stream[substream->stream].suspend_ignored = false; + } + ret = snd_sof_pcm_platform_close(sdev, substream); + if (ret < 0) { spcm_err(spcm, substream->stream, - "platform pcm close failed %d\n", err); + "platform pcm close failed %d\n", ret); /* * keep going, no point in preventing the close * from happening @@ -808,6 +1062,10 @@ static int sof_pcm_probe(struct snd_soc_component *component) static void sof_pcm_remove(struct snd_soc_component *component) { + struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(component); + + sof_pcm_free_ignore_suspend_widgets(&sdev->ignore_suspend_widget_list); + /* remove topology */ snd_soc_tplg_component_remove(component); } diff --git a/sound/soc/sof/sof-audio.h b/sound/soc/sof/sof-audio.h index 10b41ab18b7990..c691d322eceee4 100644 --- a/sound/soc/sof/sof-audio.h +++ b/sound/soc/sof/sof-audio.h @@ -346,6 +346,8 @@ struct snd_sof_pcm_stream { * active or not while suspending the stream */ bool suspend_ignored; + /* Endpoint references acquired when retaining this stream across suspend. */ + struct list_head ignore_suspend_widgets; struct snd_sof_pcm_stream_pipeline_list pipeline_list; /* used by IPC implementation and core does not touch it */ diff --git a/sound/soc/sof/sof-priv.h b/sound/soc/sof/sof-priv.h index 83e4d83cc27cbb..4b17b58a2b9b8f 100644 --- a/sound/soc/sof/sof-priv.h +++ b/sound/soc/sof/sof-priv.h @@ -407,6 +407,14 @@ struct snd_sof_dfsentry { }; }; +/* DAPM endpoint or DAI widget tracked for WoV ignore_suspend management */ +struct snd_sof_ignore_suspend_widget { + struct snd_soc_dapm_widget *widget; + unsigned int refcount; + bool initial_ignore_suspend; + struct list_head list; +}; + /* Debug mapping for any DSP memory or registers that can used for debug */ struct snd_sof_debugfs_map { const char *name; @@ -653,6 +661,8 @@ struct snd_sof_dev { struct list_head dai_list; struct list_head dai_link_list; struct list_head route_list; + /* All card DAPM endpoints/DAI widgets and their WoV ignore_suspend reference counts */ + struct list_head ignore_suspend_widget_list; struct snd_soc_component *component; u32 enabled_cores_mask; /* keep track of enabled cores */ bool led_present; diff --git a/sound/soc/sof/topology.c b/sound/soc/sof/topology.c index 750916b57def74..f91798b21f586f 100644 --- a/sound/soc/sof/topology.c +++ b/sound/soc/sof/topology.c @@ -1773,6 +1773,7 @@ static int sof_dai_load(struct snd_soc_component *scomp, int index, spcm->scomp = scomp; for_each_pcm_streams(stream) { + INIT_LIST_HEAD(&spcm->stream[stream].ignore_suspend_widgets); spcm->stream[stream].comp_id = COMP_ID_UNASSIGNED; if (pcm->compress) snd_sof_compr_init_elapsed_work(&spcm->stream[stream].period_elapsed_work); From 6899c0a21b324cd91a2b4fb3e890588079a87fde Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Tue, 6 Oct 2026 22:10:20 +0800 Subject: [PATCH 6/9] ASoC: SOF: Intel: keep SoundWire streams active for D0i3-compatible PCMs Keep a SoundWire stream active during system suspend when it is used by a WoV stream. WoV streams can be identified by: They are capture streams and. They have the d0i3_compatible flag set and They don't use Deep Buffer. The decision is made in the BE SUSPEND trigger. The BE DAI trigger runs before the SoundWire link trigger, so the ignore_suspend flag of the SoundWire stream is updated before the link trigger decides whether to disable the stream. When a BE is shared by WoV and regular FEs, the BE only receives SUSPEND once the last FE is suspended, and all connected FEs are checked at that point regardless of their open or suspend order. A WoV PCM retained in a previous S0ix suspend stays in the SUSPENDED state and will not receive another SUSPEND trigger. If the system then enters a deeper suspend state, stop its FE and BE pipelines and release its DAPM retention at PM prepare time. Assisted-by: LLM Signed-off-by: Bard Liao Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sound/soc/sof/intel/hda-dai.c | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/sound/soc/sof/intel/hda-dai.c b/sound/soc/sof/intel/hda-dai.c index 7a8875a885ab34..4a6553c7bd82b6 100644 --- a/sound/soc/sof/intel/hda-dai.c +++ b/sound/soc/sof/intel/hda-dai.c @@ -308,6 +308,27 @@ static bool hda_dai_has_wov_fe(struct snd_sof_dev *sdev, struct snd_pcm_substrea return false; } +static void hda_dai_set_sdw_ignore_suspend(struct snd_soc_dai *dai, int dir, + bool ignore_suspend) +{ + struct snd_soc_dapm_widget *w = snd_soc_dai_get_widget(dai, dir); + struct sdw_stream_runtime *sdw_stream; + struct snd_sof_widget *swidget; + struct snd_sof_dai *sof_dai; + + if (!w || !w->dobj.private) + return; + + swidget = w->dobj.private; + sof_dai = swidget->private; + if (!sof_dai || sof_dai->type != SOF_DAI_INTEL_ALH) + return; + + sdw_stream = snd_soc_dai_get_stream(dai, dir); + if (!IS_ERR_OR_NULL(sdw_stream)) + sdw_stream->ignore_suspend = ignore_suspend; +} + /* * In contrast to IPC3, the dai trigger in IPC4 mixes pipeline state changes * (over IPC channel) and DMA state change (direct host register changes). @@ -691,6 +712,16 @@ EXPORT_SYMBOL_NS(sdw_hda_dai_hw_free, "SND_SOC_SOF_INTEL_HDA_COMMON"); int sdw_hda_dai_trigger(struct snd_pcm_substream *substream, int cmd, struct snd_soc_dai *cpu_dai) { + struct snd_sof_dev *sdev = dai_to_sdev(substream, cpu_dai); + bool retain = false; + + if (cmd == SNDRV_PCM_TRIGGER_SUSPEND) + retain = sdev->system_suspend_target == SOF_SUSPEND_S0IX && + hda_dai_has_wov_fe(sdev, substream); + + if (cmd == SNDRV_PCM_TRIGGER_SUSPEND || cmd == SNDRV_PCM_TRIGGER_STOP) + hda_dai_set_sdw_ignore_suspend(cpu_dai, substream->stream, retain); + return hda_dai_trigger(substream, cmd, cpu_dai); } EXPORT_SYMBOL_NS(sdw_hda_dai_trigger, "SND_SOC_SOF_INTEL_HDA_COMMON"); From a63c745bf324ca944695c559f367fa4a302290dc Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Fri, 16 Oct 2026 16:10:41 +0800 Subject: [PATCH 7/9] ASoC: soc_sdw_utils: Handle enabled SoundWire streams Multiple FE streams can share a SoundWire BE, so prepare may arrive after another FE has enabled the shared stream. Skip sdw_prepare_stream() when the stream is already enabled. When hw_free is called for an enabled stream, disable it before depreparing. Propagate disable errors rather than attempting to deprepare an enabled stream. Signed-off-by: Bard Liao --- sound/soc/sdw_utils/soc_sdw_utils.c | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/sound/soc/sdw_utils/soc_sdw_utils.c b/sound/soc/sdw_utils/soc_sdw_utils.c index 2072e3e215708c..556284f48a8913 100644 --- a/sound/soc/sdw_utils/soc_sdw_utils.c +++ b/sound/soc/sdw_utils/soc_sdw_utils.c @@ -1880,6 +1880,9 @@ int asoc_sdw_prepare(struct snd_pcm_substream *substream) return PTR_ERR(sdw_stream); } + if (sdw_stream->state == SDW_STREAM_ENABLED) + return 0; + return sdw_prepare_stream(sdw_stream); } EXPORT_SYMBOL_NS(asoc_sdw_prepare, "SND_SOC_SDW_UTILS"); @@ -2011,6 +2014,16 @@ int asoc_sdw_hw_free(struct snd_pcm_substream *substream) return PTR_ERR(sdw_stream); } + if (sdw_stream->state == SDW_STREAM_ENABLED) { + int ret; + + ret = sdw_disable_stream(sdw_stream); + if (ret) { + dev_err(rtd->dev, "failed to disable SoundWire stream: %d\n", ret); + return ret; + } + } + return sdw_deprepare_stream(sdw_stream); } EXPORT_SYMBOL_NS(asoc_sdw_hw_free, "SND_SOC_SDW_UTILS"); From d71abbdbccd8850f1d65cf578927376fd22c6dc6 Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Tue, 20 Oct 2026 08:05:19 +0800 Subject: [PATCH 8/9] ASoC: SOF: Intel: Handle retained HDA BE trigger recovery A SoundWire capture BE shared by regular and WoV FEs can remain running across S0ix. Its HDA suspend trigger sets suspend_ignored and skips the pipeline and link DMA teardown. Recovery must consume that flag without starting the retained hardware again. The first BE trigger after system resume is not necessarily RESUME. A regular capture FE can recover through prepare followed by START. Also, when another FE has already prepared the shared BE, DPCM translates the first RESUME into START rather than resuming an unprepared stream. Only handling RESUME in the retained-stream shortcut lets START fall through to the normal HDA trigger callbacks. This unnecessarily pauses and restarts the retained IPC4 pipeline, starts the link DMA again and increments the BE pipeline's started_count even though its existing running reference was preserved during suspend. It also leaves suspend_ignored set after recovery. Consume suspend_ignored on either START or RESUME and return without retriggering the retained hardware. DPCM still accounts for the recovering FE in be_start, while the HDA pipeline keeps its existing running reference until the last BE user stops. Ordinary START triggers are unchanged when suspend_ignored is false. With IPC4 capture links the FE is triggered before the BE. If a regular FE has prepared the shared BE before a retained WoV FE resumes, the WoV FE trigger consumes its suspend_ignored flag before DPCM sends START to the BE, and its DPCM state is only updated after the trigger completes. Treat a WoV FE which is being triggered while its PCM is still SUSPENDED as resuming so the retained BE is not restarted. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Bard Liao --- sound/soc/sof/intel/hda-dai-ops.c | 4 ++++ sound/soc/sof/intel/hda-dai.c | 40 ++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/sound/soc/sof/intel/hda-dai-ops.c b/sound/soc/sof/intel/hda-dai-ops.c index f0be42048db33c..c002c234c45797 100644 --- a/sound/soc/sof/intel/hda-dai-ops.c +++ b/sound/soc/sof/intel/hda-dai-ops.c @@ -580,6 +580,10 @@ static int hda_ipc3_post_trigger(struct snd_sof_dev *sdev, struct snd_soc_dai *c { struct snd_soc_dapm_widget *w = snd_soc_dai_get_widget(cpu_dai, substream->stream); + if (sdev->fw_state != SOF_FW_BOOT_COMPLETE && + (cmd == SNDRV_PCM_TRIGGER_STOP || cmd == SNDRV_PCM_TRIGGER_SUSPEND)) + return 0; + switch (cmd) { case SNDRV_PCM_TRIGGER_SUSPEND: case SNDRV_PCM_TRIGGER_STOP: diff --git a/sound/soc/sof/intel/hda-dai.c b/sound/soc/sof/intel/hda-dai.c index 4a6553c7bd82b6..f9f9d8c6212591 100644 --- a/sound/soc/sof/intel/hda-dai.c +++ b/sound/soc/sof/intel/hda-dai.c @@ -282,6 +282,25 @@ static int __maybe_unused hda_dai_hw_params(struct snd_pcm_substream *substream, return hda_dai_hw_params_data(substream, params, dai, &data, flags); } +/* + * A retained WoV FE that is being resumed has already consumed its + * suspend_ignored flag when the FE is triggered before the BE, but its + * DPCM state is not updated until the trigger completes. Its PCM is still + * SUSPENDED during the trigger. + */ +static bool hda_dai_fe_is_resuming(struct snd_soc_pcm_runtime *fe) +{ + struct snd_pcm_substream *fe_substream; + + if (fe->dpcm[SNDRV_PCM_STREAM_CAPTURE].runtime_update != SND_SOC_DPCM_UPDATE_FE) + return false; + + fe_substream = snd_soc_dpcm_get_substream(fe, SNDRV_PCM_STREAM_CAPTURE); + + return fe_substream && fe_substream->runtime && + fe_substream->runtime->state == SNDRV_PCM_STATE_SUSPENDED; +} + static bool hda_dai_has_wov_fe(struct snd_sof_dev *sdev, struct snd_pcm_substream *substream) { struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); @@ -292,16 +311,17 @@ static bool hda_dai_has_wov_fe(struct snd_sof_dev *sdev, struct snd_pcm_substrea return false; /* - * Only retain the BE for a connected WoV capture FE that is running - * or has already ignored its suspend trigger. An idle WoV FE may - * still be connected to the BE. + * Only retain the BE for a connected WoV capture FE that is running, + * has already ignored its suspend trigger or is being resumed. An idle + * WoV FE may still be connected to the BE. */ for_each_dpcm_fe(rtd, SNDRV_PCM_STREAM_CAPTURE, dpcm) { spcm = snd_sof_find_spcm_dai(sdev->component, dpcm->fe); if (spcm && spcm->stream[SNDRV_PCM_STREAM_CAPTURE].d0i3_compatible && spcm->stream[SNDRV_PCM_STREAM_CAPTURE].dsp_max_burst_size_in_ms <= 1 && (dpcm->fe->dpcm[SNDRV_PCM_STREAM_CAPTURE].state == SND_SOC_DPCM_STATE_START || - spcm->stream[SNDRV_PCM_STREAM_CAPTURE].suspend_ignored)) + spcm->stream[SNDRV_PCM_STREAM_CAPTURE].suspend_ignored || + hda_dai_fe_is_resuming(dpcm->fe))) return true; } @@ -340,6 +360,7 @@ static int __maybe_unused hda_dai_trigger(struct snd_pcm_substream *substream, i struct hdac_ext_stream *hext_stream; struct sof_intel_hda_stream *hda_stream; struct snd_sof_dev *sdev; + bool skip_fw_ipc; int ret; if (!ops) { @@ -351,13 +372,20 @@ static int __maybe_unused hda_dai_trigger(struct snd_pcm_substream *substream, i dai->name, substream->stream); sdev = dai_to_sdev(substream, dai); + /* + * Firmware IPC is unavailable after a crash, but STOP/SUSPEND must + * still clean up the host-side DMA resources below. + */ + skip_fw_ipc = sdev->fw_state != SOF_FW_BOOT_COMPLETE && + (cmd == SNDRV_PCM_TRIGGER_STOP || cmd == SNDRV_PCM_TRIGGER_SUSPEND); hext_stream = ops->get_hext_stream(sdev, dai, substream); if (!hext_stream) return -EINVAL; hda_stream = hstream_to_sof_hda_stream(hext_stream); - if (cmd == SNDRV_PCM_TRIGGER_RESUME && hda_stream->suspend_ignored) { + if ((cmd == SNDRV_PCM_TRIGGER_START || cmd == SNDRV_PCM_TRIGGER_RESUME) && + hda_stream->suspend_ignored) { hda_stream->suspend_ignored = false; return 0; } @@ -374,7 +402,7 @@ static int __maybe_unused hda_dai_trigger(struct snd_pcm_substream *substream, i return 0; } - if (ops->pre_trigger) { + if (ops->pre_trigger && !skip_fw_ipc) { ret = ops->pre_trigger(sdev, dai, substream, cmd); if (ret < 0) return ret; From 1ce8ce5fb17ce6fe744dfef480f08a36c5ed7b47 Mon Sep 17 00:00:00 2001 From: Bard Liao Date: Mon, 2 Nov 2026 13:11:12 +0800 Subject: [PATCH 9/9] ASoC: SOF: Invalidate WoV retention before deeper system sleep WoV paths retained across S0ix may still belong to a SUSPENDED PCM after wakeup, until userspace recovers the stream. If the system enters S3 or S4 first, ALSA does not send another SUSPEND trigger. Cleanup tied only to PCM triggers therefore leaves retention state valid in software even though the deeper sleep powers down and resets the DSP and SoundWire hardware. A controller suspend-target check cannot fix the stale stream state. SoundWire ENABLED and HDA suspend_ignored/link_prepared shortcuts can skip port, LinkDMA and channel mapping initialization on restart. DAPM references can also keep widgets exempt from suspend after retention is no longer appropriate. Closing such a PCM without STOP introduces a second lifecycle problem: its IPC4 started/paused contributions can remain on shared pipelines. Resetting counts only when the scheduler is finally freed is insufficient while another PCM still holds it, and stale counts can suppress later START or PAUSE transitions. Track contributions per PCM, including transitions requiring no firmware IPC, and remove only its references during hw_free. Stop any remaining active contribution before RESET and clear counts when the scheduler is finally freed. Derive pipeline count changes from each PCM's old and new state, sharing the same accounting logic between trigger handling and hw_free. Revalidate retention during PM prepare using the SOF suspend target and firmware state, before codec and controller suspend. Revoke FE widget retention, disable retained SoundWire streams and invalidate retained HDA link preparation on the system D3 path. Record whether the bus was actually retained so resume does not reevaluate a changed firmware state. Prepare WoV widget references during the system PM prepare phase, before any PCM suspend trigger can select the retained FE and BE paths. ALSA suspends PCMs unconditionally even if their suspend trigger fails, so reporting an allocation error from that trigger cannot prevent a broken WoV route. Abort PM prepare if widget collection fails, releasing only unused references acquired by this attempt. Suspend triggers consume the prebuilt references without allocation; complete releases references not taken over by a retained stream and leaves existing retention intact. If cleanup fails or sleep is aborted after retention was revoked, report the error rather than reconstructing the old stream. Userspace must close and reopen the PCM instead of relying on the retained path. Continue local accounting cleanup after STOP errors, and balance runtime PM disabling when stopping the bus fails. No rollback snapshots or automatic stream restoration are used; normal S0ix WoV retention remains unchanged. Signed-off-by: Bard Liao --- drivers/soundwire/intel.c | 14 ++- drivers/soundwire/intel.h | 1 + drivers/soundwire/intel_ace2x.c | 14 ++- drivers/soundwire/intel_auxdevice.c | 55 ++++++++-- include/linux/soundwire/sdw_intel.h | 1 + sound/soc/sof/intel/hda-dsp.c | 16 +++ sound/soc/sof/intel/hda.c | 10 ++ sound/soc/sof/ipc4-pcm.c | 152 ++++++++++++++++------------ sound/soc/sof/ipc4-topology.c | 2 + sound/soc/sof/pcm.c | 95 ++++++++++++++++- sound/soc/sof/pm.c | 28 ++++- sound/soc/sof/sof-audio.h | 4 + sound/soc/sof/sof-priv.h | 2 + 13 files changed, 312 insertions(+), 82 deletions(-) diff --git a/drivers/soundwire/intel.c b/drivers/soundwire/intel.c index 2064e64c2c5455..a159ca8d869a56 100644 --- a/drivers/soundwire/intel.c +++ b/drivers/soundwire/intel.c @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -952,14 +953,19 @@ static int intel_component_dais_suspend(struct snd_soc_component *component) for_each_component_dais(component, dai) { struct sdw_cdns *cdns = snd_soc_dai_get_drvdata(dai); struct sdw_cdns_dai_runtime *dai_runtime; + struct sdw_stream_runtime *stream; - if (sdw_bus_ignore_suspend(&cdns->bus)) + dai_runtime = cdns->dai_runtime_array[dai->id]; + if (!dai_runtime) continue; - dai_runtime = cdns->dai_runtime_array[dai->id]; + stream = dai_runtime->stream; + if (pm_suspend_target_state == PM_SUSPEND_TO_IDLE && + stream && stream->ignore_suspend && + stream->state == SDW_STREAM_ENABLED) + continue; - if (dai_runtime) - dai_runtime->suspended = true; + dai_runtime->suspended = true; } return 0; diff --git a/drivers/soundwire/intel.h b/drivers/soundwire/intel.h index 7a2e7e73ad632a..87f1203e74a3b7 100644 --- a/drivers/soundwire/intel.h +++ b/drivers/soundwire/intel.h @@ -78,6 +78,7 @@ struct sdw_intel { struct sdw_intel_link_res *link_res; bool startup_done; struct sdw_intel_bpt bpt_ctx; + bool suspend_bus_retained; #ifdef CONFIG_DEBUG_FS struct dentry *debugfs; #endif diff --git a/drivers/soundwire/intel_ace2x.c b/drivers/soundwire/intel_ace2x.c index 717531d737f586..39ffe722812aba 100644 --- a/drivers/soundwire/intel_ace2x.c +++ b/drivers/soundwire/intel_ace2x.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -951,14 +952,19 @@ static int intel_component_dais_suspend(struct snd_soc_component *component) for_each_component_dais(component, dai) { struct sdw_cdns *cdns = snd_soc_dai_get_drvdata(dai); struct sdw_cdns_dai_runtime *dai_runtime; + struct sdw_stream_runtime *stream; - if (sdw_bus_ignore_suspend(&cdns->bus)) + dai_runtime = cdns->dai_runtime_array[dai->id]; + if (!dai_runtime) continue; - dai_runtime = cdns->dai_runtime_array[dai->id]; + stream = dai_runtime->stream; + if (pm_suspend_target_state == PM_SUSPEND_TO_IDLE && + stream && stream->ignore_suspend && + stream->state == SDW_STREAM_ENABLED) + continue; - if (dai_runtime) - dai_runtime->suspended = true; + dai_runtime->suspended = true; } return 0; diff --git a/drivers/soundwire/intel_auxdevice.c b/drivers/soundwire/intel_auxdevice.c index 5c1bb08ac9dba9..48ba160f0009f8 100644 --- a/drivers/soundwire/intel_auxdevice.c +++ b/drivers/soundwire/intel_auxdevice.c @@ -577,13 +577,23 @@ int intel_resume_child_device(struct device *dev, void *data) return 0; } +static bool intel_ignore_suspend(struct sdw_intel *sdw) +{ + const struct sdw_intel_link_res *res = sdw->link_res; + + if (res->ops && res->ops->ignore_suspend) + return res->ops->ignore_suspend(res->dev); + + return pm_suspend_target_state == PM_SUSPEND_TO_IDLE; +} + static int __maybe_unused intel_pm_prepare(struct device *dev) { struct sdw_cdns *cdns = dev_get_drvdata(dev); struct sdw_intel *sdw = cdns_to_intel(cdns); struct sdw_bus *bus = &cdns->bus; u32 clock_stop_quirks; - int ret; + int i, ret, stream_error = 0; if (bus->prop.hw_disabled || !sdw->startup_done) { dev_dbg(dev, "SoundWire master %d is disabled or not-started, ignoring\n", @@ -592,6 +602,7 @@ static int __maybe_unused intel_pm_prepare(struct device *dev) } clock_stop_quirks = sdw->link_res->clock_stop_quirks; + sdw->suspend_bus_retained = false; if (pm_runtime_suspended(dev) && pm_runtime_suspended(dev->parent) && @@ -634,7 +645,38 @@ static int __maybe_unused intel_pm_prepare(struct device *dev) dev_err(dev, "%s: intel_resume_child_device failed: %d\n", __func__, ret); } - return 0; + /* + * Revoke retention from an earlier S0ix before any controller or codec + * suspends. An already-suspended PCM will not issue another trigger. + */ + if (!intel_ignore_suspend(sdw)) { + for (i = 0; i < cdns->pcm.num_pdi; i++) { + struct sdw_cdns_dai_runtime *dai_runtime = cdns->dai_runtime_array[i]; + struct sdw_stream_runtime *stream; + + if (!dai_runtime || !dai_runtime->stream) + continue; + + stream = dai_runtime->stream; + if (!stream->ignore_suspend) + continue; + + stream->ignore_suspend = false; + dai_runtime->suspended = true; + + if (stream->state == SDW_STREAM_ENABLED) { + ret = sdw_disable_stream(stream); + if (ret) { + dev_err(dev, "failed to disable retained stream %s: %d; close and reopen PCM\n", + stream->name, ret); + if (!stream_error) + stream_error = ret; + } + } + } + } + + return stream_error; } static int __maybe_unused intel_suspend(struct device *dev) @@ -655,9 +697,10 @@ static int __maybe_unused intel_suspend(struct device *dev) * ignore_suspend is set by the PCM trigger for S0ix and may remain set * if a later sleep transition does not issue another PCM trigger. */ - if (pm_suspend_target_state == PM_SUSPEND_TO_IDLE && - sdw_bus_ignore_suspend(bus)) + if (intel_ignore_suspend(sdw) && sdw_bus_ignore_suspend(bus)) { + sdw->suspend_bus_retained = true; return 0; + } /* Prevent runtime PM from racing with the code below. */ pm_runtime_disable(dev); @@ -688,6 +731,7 @@ static int __maybe_unused intel_suspend(struct device *dev) ret = sdw_intel_stop_bus(sdw, true, false); if (ret < 0) { dev_err(dev, "%s: cannot stop bus: %d\n", __func__, ret); + pm_runtime_enable(dev); return ret; } @@ -746,8 +790,7 @@ static int __maybe_unused intel_resume(struct device *dev) return 0; } - if (pm_suspend_target_state == PM_SUSPEND_TO_IDLE && - sdw_bus_ignore_suspend(bus)) { + if (sdw->suspend_bus_retained) { dev_dbg(dev, "SoundWire master %d remained active during system suspend\n", bus->link_id); return 0; diff --git a/include/linux/soundwire/sdw_intel.h b/include/linux/soundwire/sdw_intel.h index 9710f2dc04e298..697cc0357cffca 100644 --- a/include/linux/soundwire/sdw_intel.h +++ b/include/linux/soundwire/sdw_intel.h @@ -226,6 +226,7 @@ struct sdw_intel_ops { int (*free_stream)(struct device *dev, struct sdw_intel_stream_free_data *free_data); int (*trigger)(struct snd_pcm_substream *substream, int cmd, struct snd_soc_dai *dai); + bool (*ignore_suspend)(struct device *dev); }; /** diff --git a/sound/soc/sof/intel/hda-dsp.c b/sound/soc/sof/intel/hda-dsp.c index ec49aeaa9bd7af..9dcc4c55554c3b 100644 --- a/sound/soc/sof/intel/hda-dsp.c +++ b/sound/soc/sof/intel/hda-dsp.c @@ -852,6 +852,22 @@ static int hda_suspend(struct snd_sof_dev *sdev, bool runtime_suspend) hda_dsp_ctrl_ppcap_enable(sdev, false); hda_dsp_ctrl_ppcap_int_enable(sdev, false); skip_dsp: + if (!runtime_suspend) { + struct hdac_stream *s; + + /* Do not carry WoV retention state across a system D3 transition. */ + list_for_each_entry(s, &bus->stream_list, list) { + struct sof_intel_hda_stream *hda_stream; + struct hdac_ext_stream *hext_stream; + + hext_stream = stream_to_hdac_ext_stream(s); + hda_stream = hstream_to_sof_hda_stream(hext_stream); + if (sdev->system_suspend_target != SOF_SUSPEND_S0IX && + hda_stream->suspend_ignored) + hext_stream->link_prepared = 0; + hda_stream->suspend_ignored = false; + } + } /* disable hda bus irq and streams */ hda_dsp_ctrl_stop_chip(sdev); diff --git a/sound/soc/sof/intel/hda.c b/sound/soc/sof/intel/hda.c index e1815bfe358b5d..787d975d4f1853 100644 --- a/sound/soc/sof/intel/hda.c +++ b/sound/soc/sof/intel/hda.c @@ -100,9 +100,18 @@ static int sdw_params_free(struct device *dev, struct sdw_intel_stream_free_data return 0; } +static bool sdw_ignore_suspend(struct device *dev) +{ + struct snd_sof_dev *sdev = dev_get_drvdata(dev); + + return sdev->system_suspend_target == SOF_SUSPEND_S0IX && + sdev->fw_state == SOF_FW_BOOT_COMPLETE; +} + struct sdw_intel_ops sdw_callback = { .params_stream = sdw_params_stream, .free_stream = sdw_params_free, + .ignore_suspend = sdw_ignore_suspend, }; static int sdw_ace2x_params_stream(struct device *dev, @@ -132,6 +141,7 @@ static struct sdw_intel_ops sdw_ace2x_callback = { .params_stream = sdw_ace2x_params_stream, .free_stream = sdw_ace2x_free_stream, .trigger = sdw_ace2x_trigger, + .ignore_suspend = sdw_ignore_suspend, }; static int hda_sdw_acpi_scan(struct snd_sof_dev *sdev) diff --git a/sound/soc/sof/ipc4-pcm.c b/sound/soc/sof/ipc4-pcm.c index cbf948d8479148..47cb9252af775a 100644 --- a/sound/soc/sof/ipc4-pcm.c +++ b/sound/soc/sof/ipc4-pcm.c @@ -19,11 +19,15 @@ * struct sof_ipc4_pcm_stream_priv - IPC4 specific private data * @time_info: pointer to time info struct if it is supported, otherwise NULL * @chain_dma_allocated: indicates the ChainDMA allocation state + * @started: this PCM holds a started reference on its FE-triggered pipelines + * @paused: this PCM holds a paused reference on its FE-triggered pipelines */ struct sof_ipc4_pcm_stream_priv { struct sof_ipc4_timestamp_info *time_info; bool chain_dma_allocated; + bool started; + bool paused; }; /* @@ -189,66 +193,71 @@ sof_ipc4_add_pipeline_to_trigger_list(struct snd_sof_dev *sdev, int state, } } -static void -sof_ipc4_update_pipeline_state(struct snd_sof_dev *sdev, int state, int cmd, - struct snd_sof_pipeline *spipe, - struct ipc4_pipeline_set_state_data *trigger_list) +static void sof_ipc4_set_stream_state(struct snd_sof_pcm_stream *stream, + bool started, bool paused) { - struct snd_sof_widget *pipe_widget = spipe->pipe_widget; - struct sof_ipc4_pipeline *pipeline = pipe_widget->private; + struct sof_ipc4_pcm_stream_priv *stream_priv = stream->private; + struct snd_sof_pcm_stream_pipeline_list *pipeline_list = &stream->pipeline_list; + int started_delta = (int)started - stream_priv->started; + int paused_delta = (int)paused - stream_priv->paused; int i; - if (pipeline->skip_during_fe_trigger && state != SOF_IPC4_PIPE_RESET) - return; + for (i = 0; i < pipeline_list->count; i++) { + struct snd_sof_pipeline *spipe = pipeline_list->pipelines[i]; + struct sof_ipc4_pipeline *pipeline = spipe->pipe_widget->private; - /* set state for pipeline if it was just triggered */ - for (i = 0; i < trigger_list->count; i++) { - if (trigger_list->pipeline_instance_ids[i] == pipe_widget->instance_id) { - pipeline->state = state; - break; - } + if (pipeline->skip_during_fe_trigger || pipeline->use_chain_dma) + continue; + + spipe->started_count += started_delta; + spipe->paused_count += paused_delta; } - switch (state) { - case SOF_IPC4_PIPE_PAUSED: - switch (cmd) { - case SNDRV_PCM_TRIGGER_PAUSE_PUSH: - /* - * increment paused_count if the PAUSED is the final state during - * the PAUSE trigger - */ - spipe->paused_count++; - break; - case SNDRV_PCM_TRIGGER_STOP: - case SNDRV_PCM_TRIGGER_SUSPEND: - /* - * decrement started_count if PAUSED is the final state during the - * STOP trigger - */ - spipe->started_count--; - break; - default: - break; + stream_priv->started = started; + stream_priv->paused = paused; +} + +static void sof_ipc4_update_stream_state(int state, int cmd, + struct snd_sof_pcm_stream *stream, + struct ipc4_pipeline_set_state_data *trigger_list) +{ + struct sof_ipc4_pcm_stream_priv *stream_priv = stream->private; + struct snd_sof_pcm_stream_pipeline_list *pipeline_list = &stream->pipeline_list; + bool started = stream_priv->started; + bool paused = stream_priv->paused; + int i, trigger_index; + + for (i = 0; i < pipeline_list->count; i++) { + struct snd_sof_widget *pipe_widget = pipeline_list->pipelines[i]->pipe_widget; + struct sof_ipc4_pipeline *pipeline = pipe_widget->private; + + if (pipeline->skip_during_fe_trigger && state != SOF_IPC4_PIPE_RESET) + continue; + + for (trigger_index = 0; trigger_index < trigger_list->count; trigger_index++) { + if (trigger_list->pipeline_instance_ids[trigger_index] == + pipe_widget->instance_id) { + pipeline->state = state; + break; + } } - break; - case SOF_IPC4_PIPE_RUNNING: - switch (cmd) { - case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: - /* decrement paused_count for RELEASE */ - spipe->paused_count--; - break; - case SNDRV_PCM_TRIGGER_START: - case SNDRV_PCM_TRIGGER_RESUME: - /* increment started_count for START/RESUME */ - spipe->started_count++; - break; - default: - break; + } + + if (state == SOF_IPC4_PIPE_RUNNING) { + if (cmd == SNDRV_PCM_TRIGGER_START || cmd == SNDRV_PCM_TRIGGER_RESUME) + started = true; + if (cmd == SNDRV_PCM_TRIGGER_PAUSE_RELEASE) + paused = false; + } else if (state == SOF_IPC4_PIPE_PAUSED) { + if (cmd == SNDRV_PCM_TRIGGER_PAUSE_PUSH) + paused = started; + if (cmd == SNDRV_PCM_TRIGGER_STOP || cmd == SNDRV_PCM_TRIGGER_SUSPEND) { + started = false; + paused = false; } - break; - default: - break; } + + sof_ipc4_set_stream_state(stream, started, paused); } /* @@ -401,6 +410,7 @@ static int sof_ipc4_trigger_pipelines(struct snd_soc_component *component, struct snd_sof_dev *sdev = snd_soc_component_get_drvdata(component); struct snd_sof_pcm_stream_pipeline_list *pipeline_list; struct sof_ipc4_fw_data *ipc4_data = sdev->private; + struct sof_ipc4_pcm_stream_priv *stream_priv = spcm->stream[dir].private; struct ipc4_pipeline_set_state_data *trigger_list; struct snd_sof_widget *pipe_widget; struct sof_ipc4_pipeline *pipeline; @@ -414,6 +424,9 @@ static int sof_ipc4_trigger_pipelines(struct snd_soc_component *component, pipeline_list = &spcm->stream[dir].pipeline_list; guard(mutex)(&ipc4_data->pipeline_state_mutex); + if (state == SOF_IPC4_PIPE_RESET) + sof_ipc4_set_stream_state(&spcm->stream[dir], false, false); + /* nothing to trigger if the list is empty */ if (!pipeline_list->pipelines || !pipeline_list->count) return 0; @@ -487,6 +500,10 @@ static int sof_ipc4_trigger_pipelines(struct snd_soc_component *component, } else for (i = 0; i < pipeline_list->count; i++) { + if (state == SOF_IPC4_PIPE_PAUSED && + (!stream_priv->started || stream_priv->paused)) + continue; + spipe = pipeline_list->pipelines[i]; sof_ipc4_add_pipeline_to_trigger_list(sdev, state, spipe, trigger_list, pipe_priority); @@ -495,7 +512,9 @@ static int sof_ipc4_trigger_pipelines(struct snd_soc_component *component, /* return if all pipelines are in the requested state already */ if (!trigger_list->count) { ret = 0; - goto free; + if (state == SOF_IPC4_PIPE_PAUSED) + goto update_paused_state; + goto update_state; } /* no need to pause before reset, EOS or before pause release */ @@ -526,11 +545,9 @@ static int sof_ipc4_trigger_pipelines(struct snd_soc_component *component, } /* update PAUSED state for all pipelines just triggered */ - for (i = 0; i < pipeline_list->count ; i++) { - spipe = pipeline_list->pipelines[i]; - sof_ipc4_update_pipeline_state(sdev, SOF_IPC4_PIPE_PAUSED, cmd, spipe, - trigger_list); - } +update_paused_state: + sof_ipc4_update_stream_state(SOF_IPC4_PIPE_PAUSED, cmd, + &spcm->stream[dir], trigger_list); /* return if this is the final state */ if (state == SOF_IPC4_PIPE_PAUSED) { @@ -569,10 +586,8 @@ static int sof_ipc4_trigger_pipelines(struct snd_soc_component *component, } /* update RUNNING/RESET state for all pipelines that were just triggered */ - for (i = 0; i < pipeline_list->count; i++) { - spipe = pipeline_list->pipelines[i]; - sof_ipc4_update_pipeline_state(sdev, state, cmd, spipe, trigger_list); - } +update_state: + sof_ipc4_update_stream_state(state, cmd, &spcm->stream[dir], trigger_list); free: kfree(trigger_list); @@ -615,8 +630,21 @@ static int sof_ipc4_pcm_hw_free(struct snd_soc_component *component, struct snd_pcm_substream *substream, struct snd_sof_pcm *spcm, int dir) { + struct sof_ipc4_pcm_stream_priv *stream_priv = spcm->stream[dir].private; + int ret = 0, reset_ret; + + if (stream_priv->started) { + ret = sof_ipc4_trigger_pipelines(component, substream, SOF_IPC4_PIPE_PAUSED, + SNDRV_PCM_TRIGGER_STOP, spcm, dir); + if (ret) + spcm_err(spcm, dir, "failed to stop PCM during hw_free: %d\n", ret); + } + /* command is not relevant with RESET, so just pass 0 */ - return sof_ipc4_trigger_pipelines(component, substream, SOF_IPC4_PIPE_RESET, 0, spcm, dir); + reset_ret = sof_ipc4_trigger_pipelines(component, substream, + SOF_IPC4_PIPE_RESET, 0, spcm, dir); + + return ret ? ret : reset_ret; } static int ipc4_ssp_dai_config_pcm_params_match(struct snd_sof_dev *sdev, diff --git a/sound/soc/sof/ipc4-topology.c b/sound/soc/sof/ipc4-topology.c index 8b13c97b348044..76be638cfe555e 100644 --- a/sound/soc/sof/ipc4-topology.c +++ b/sound/soc/sof/ipc4-topology.c @@ -3737,6 +3737,8 @@ static int sof_ipc4_widget_free(struct snd_sof_dev *sdev, struct snd_sof_widget swidget->heap_bytes = 0; swidget->stack_bytes = 0; pipeline->state = SOF_IPC4_PIPE_UNINITIALIZED; + swidget->spipe->started_count = 0; + swidget->spipe->paused_count = 0; ida_free(&pipeline_ida, swidget->instance_id); swidget->instance_id = -EINVAL; } else { diff --git a/sound/soc/sof/pcm.c b/sound/soc/sof/pcm.c index 08901397539ea9..b801002d0bb4fd 100644 --- a/sound/soc/sof/pcm.c +++ b/sound/soc/sof/pcm.c @@ -284,6 +284,94 @@ static int sof_pcm_set_widget_ignore_suspend(struct snd_sof_dev *sdev, return ret; } +void snd_sof_pcm_complete_suspend(struct snd_sof_dev *sdev) +{ + struct snd_sof_pcm *spcm; + int dir; + + list_for_each_entry(spcm, &sdev->pcm_list, list) { + for_each_pcm_streams(dir) { + struct snd_pcm_substream *substream = spcm->stream[dir].substream; + struct snd_soc_pcm_runtime *rtd; + + if (!spcm->stream[dir].suspend_widgets_prepared) + continue; + + if (!spcm->stream[dir].suspend_ignored && !WARN_ON_ONCE(!substream)) { + rtd = snd_soc_substream_to_rtd(substream); + sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, dir, false); + } + spcm->stream[dir].suspend_widgets_prepared = false; + } + } +} + +int snd_sof_pcm_prepare_suspend(struct snd_sof_dev *sdev) +{ + struct snd_sof_pcm *spcm; + int dir = SNDRV_PCM_STREAM_CAPTURE; + int ret; + + list_for_each_entry(spcm, &sdev->pcm_list, list) { + struct snd_pcm_substream *substream = spcm->stream[dir].substream; + struct snd_soc_pcm_runtime *rtd; + + if (spcm->stream[dir].suspend_ignored || + !spcm->stream[dir].d0i3_compatible || + spcm->stream[dir].dsp_max_burst_size_in_ms > 1 || + !substream || !substream->runtime || !snd_pcm_running(substream)) + continue; + + rtd = snd_soc_substream_to_rtd(substream); + ret = sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, dir, true); + if (ret) { + spcm_err(spcm, dir, "failed to prepare WoV retention: %d; aborting suspend\n", + ret); + snd_sof_pcm_complete_suspend(sdev); + return ret; + } + spcm->stream[dir].suspend_widgets_prepared = true; + } + + return 0; +} + +int snd_sof_pcm_clear_suspend_ignored(struct snd_sof_dev *sdev) +{ + struct snd_sof_pcm *spcm; + int dir, ret = 0; + + list_for_each_entry(spcm, &sdev->pcm_list, list) { + for_each_pcm_streams(dir) { + struct snd_pcm_substream *substream; + struct snd_soc_pcm_runtime *rtd; + int stream_ret; + + if (!spcm->stream[dir].suspend_ignored) + continue; + + sdev->system_suspend_retention_revoked = true; + spcm->stream[dir].suspend_ignored = false; + substream = spcm->stream[dir].substream; + if (WARN_ON_ONCE(!substream)) { + stream_ret = -EINVAL; + } else { + rtd = snd_soc_substream_to_rtd(substream); + stream_ret = sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, + dir, false); + } + if (stream_ret) { + spcm_err(spcm, dir, "failed to clear WoV retention: %d; close and reopen PCM\n", + stream_ret); + if (!ret) + ret = stream_ret; + } + } + } + + return ret; +} + int sof_pcm_setup_connected_widgets(struct snd_sof_dev *sdev, struct snd_soc_pcm_runtime *rtd, struct snd_sof_pcm *spcm, struct snd_pcm_hw_params *params, @@ -687,9 +775,10 @@ static int sof_pcm_trigger(struct snd_soc_component *component, if (spcm->stream[substream->stream].suspend_ignored) return 0; - /* Widget retention is best-effort; keep FE/BE suspend state balanced. */ - sof_pcm_set_widget_ignore_suspend(sdev, rtd, spcm, - substream->stream, true); + if (!spcm->stream[substream->stream].suspend_widgets_prepared) { + spcm_err(spcm, substream->stream, "WoV widgets were not prepared for suspend\n"); + return -EINVAL; + } spcm->stream[substream->stream].suspend_ignored = true; return 0; } diff --git a/sound/soc/sof/pm.c b/sound/soc/sof/pm.c index b5d239b12e59d2..2d210b26a5a94e 100644 --- a/sound/soc/sof/pm.c +++ b/sound/soc/sof/pm.c @@ -370,6 +370,10 @@ EXPORT_SYMBOL(snd_sof_resume); int snd_sof_suspend(struct device *dev) { + struct snd_sof_dev *sdev = dev_get_drvdata(dev); + + sdev->system_suspend_started = true; + return sof_suspend(dev, false); } EXPORT_SYMBOL(snd_sof_suspend); @@ -378,6 +382,10 @@ int snd_sof_prepare(struct device *dev) { struct snd_sof_dev *sdev = dev_get_drvdata(dev); const struct sof_dev_desc *desc = sdev->pdata->desc; + int ret; + + sdev->system_suspend_started = false; + sdev->system_suspend_retention_revoked = false; /* will suspend to S3 by default */ sdev->system_suspend_target = SOF_SUSPEND_S3; @@ -388,10 +396,10 @@ int snd_sof_prepare(struct device *dev) */ if (sdev->fw_state == SOF_FW_CRASHED || sdev->fw_state == SOF_FW_BOOT_FAILED) - return 0; + return snd_sof_pcm_clear_suspend_ignored(sdev); if (!desc->use_acpi_target_states) - return 0; + return snd_sof_pcm_clear_suspend_ignored(sdev); #if defined(CONFIG_ACPI) switch (acpi_target_system_state()) { @@ -414,7 +422,14 @@ int snd_sof_prepare(struct device *dev) } #endif - return 0; + if (sdev->system_suspend_target != SOF_SUSPEND_S0IX) + return snd_sof_pcm_clear_suspend_ignored(sdev); + + ret = snd_sof_pcm_prepare_suspend(sdev); + if (ret) + sdev->system_suspend_target = SOF_SUSPEND_NONE; + + return ret; } EXPORT_SYMBOL(snd_sof_prepare); @@ -422,6 +437,13 @@ void snd_sof_complete(struct device *dev) { struct snd_sof_dev *sdev = dev_get_drvdata(dev); + snd_sof_pcm_complete_suspend(sdev); + + if (sdev->system_suspend_retention_revoked && !sdev->system_suspend_started) + dev_err(sdev->dev, "system suspend aborted after revoking WoV retention; close and reopen PCM\n"); + + sdev->system_suspend_started = false; + sdev->system_suspend_retention_revoked = false; sdev->system_suspend_target = SOF_SUSPEND_NONE; } EXPORT_SYMBOL(snd_sof_complete); diff --git a/sound/soc/sof/sof-audio.h b/sound/soc/sof/sof-audio.h index c691d322eceee4..02f966eb7d0abf 100644 --- a/sound/soc/sof/sof-audio.h +++ b/sound/soc/sof/sof-audio.h @@ -346,6 +346,7 @@ struct snd_sof_pcm_stream { * active or not while suspending the stream */ bool suspend_ignored; + bool suspend_widgets_prepared; /* Endpoint references acquired when retaining this stream across suspend. */ struct list_head ignore_suspend_widgets; struct snd_sof_pcm_stream_pipeline_list pipeline_list; @@ -642,6 +643,9 @@ struct snd_sof_pcm *snd_sof_find_spcm_comp(struct snd_soc_component *scomp, int *direction); void snd_sof_pcm_period_elapsed(struct snd_pcm_substream *substream); void snd_sof_pcm_init_elapsed_work(struct work_struct *work); +int snd_sof_pcm_clear_suspend_ignored(struct snd_sof_dev *sdev); +int snd_sof_pcm_prepare_suspend(struct snd_sof_dev *sdev); +void snd_sof_pcm_complete_suspend(struct snd_sof_dev *sdev); int sof_pcm_setup_connected_widgets(struct snd_sof_dev *sdev, struct snd_soc_pcm_runtime *rtd, struct snd_sof_pcm *spcm, struct snd_pcm_hw_params *params, struct snd_sof_platform_stream_params *platform_params, diff --git a/sound/soc/sof/sof-priv.h b/sound/soc/sof/sof-priv.h index 4b17b58a2b9b8f..f21d4fd9e2944c 100644 --- a/sound/soc/sof/sof-priv.h +++ b/sound/soc/sof/sof-priv.h @@ -608,6 +608,8 @@ struct snd_sof_dev { /* Intended power target of system suspend */ enum sof_system_suspend_state system_suspend_target; + bool system_suspend_started; + bool system_suspend_retention_revoked; /* DSP firmware boot */ wait_queue_head_t boot_wait;