From bbba96a83de96c79d422221e0a1563126cf42272 Mon Sep 17 00:00:00 2001 From: geoca <272055834+geocausa@users.noreply.github.com> Date: Sun, 23 Aug 2026 08:07:44 +0100 Subject: [PATCH 01/20] dt-bindings: sound: describe Denali protected audio Describe the optional WSA8845 VISENSE and CPS feedback paths, including their DAI selectors and slave-only SoundWire transport properties. Add a board-specific Denali sound-card compatible with the generic X1E80100 fallback. [Leon: Split the source binding changes into a reviewable commit, rebased them onto the current schemas, added the Denali sound-card compatible with its generic fallback, and kept the new properties optional.] Link: https://github.com/geocausa/SP11X1e-audio/commit/31466d62b38c8138d476b2fd3606a76272fb3d34 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/4bcfa10f65f2ea99ca66a42b5e523c351b45ced9 Signed-off-by: Leon Silcott --- .../bindings/sound/qcom,sm8250.yaml | 3 ++ .../bindings/sound/qcom,wsa8840.yaml | 28 +++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Documentation/devicetree/bindings/sound/qcom,sm8250.yaml b/Documentation/devicetree/bindings/sound/qcom,sm8250.yaml index 2c59501c33c511..6875c1515512bd 100644 --- a/Documentation/devicetree/bindings/sound/qcom,sm8250.yaml +++ b/Documentation/devicetree/bindings/sound/qcom,sm8250.yaml @@ -29,6 +29,9 @@ properties: - qcom,sm8650-sndcard - qcom,sm8750-sndcard - const: qcom,sm8450-sndcard + - items: + - const: microsoft,denali-sndcard + - const: qcom,x1e80100-sndcard - enum: - dell,xps13-9345-sndcard - fairphone,fp4-sndcard diff --git a/Documentation/devicetree/bindings/sound/qcom,wsa8840.yaml b/Documentation/devicetree/bindings/sound/qcom,wsa8840.yaml index 866c5e780fb0aa..0f1efbd568e6f3 100644 --- a/Documentation/devicetree/bindings/sound/qcom,wsa8840.yaml +++ b/Documentation/devicetree/bindings/sound/qcom,wsa8840.yaml @@ -40,8 +40,32 @@ properties: minItems: 6 maxItems: 6 + qcom,enable-cps: + type: boolean + description: Enable the dedicated CPS SoundWire source DAI on data port 6. + + qcom,cps-offset1: + $ref: /schemas/types.yaml#/definitions/uint32 + maximum: 255 + description: | + Slave-only SoundWire DP6 OffsetCtrl1 override for CPS. This does not + change the physical master-port schedule selected by qcom,port-mapping. + + qcom,visense-channel-mask: + $ref: /schemas/types.yaml#/definitions/uint32 + minimum: 1 + maximum: 255 + description: | + Native WSA data-port-5 channel-enable mask for the VISENSE protection + sidechain. If absent, the codec default is used. + '#sound-dai-cells': - const: 0 + enum: [0, 1] + description: | + Zero selects the legacy single speaker-playback DAI. With one cell, + DAI 0 is speaker playback, DAI 1 is the render-coupled VISENSE + protection-feedback sidechain, and DAI 2 is the dedicated CPS feedback + sidechain when qcom,enable-cps is present. vdd-1p8-supply: true vdd-io-supply: true @@ -75,7 +99,7 @@ examples: pinctrl-names = "default"; pinctrl-0 = <&spkr_2_sd_n_active>; powerdown-gpios = <&lpass_tlmm 18 GPIO_ACTIVE_LOW>; - #sound-dai-cells = <0>; + #sound-dai-cells = <1>; sound-name-prefix = "SpkrRight"; vdd-1p8-supply = <&vreg_l15b_1p8>; vdd-io-supply = <&vreg_l3g_1p2>; From 840000da3ad5eac5bb14dc4b5e1fd9705f400d94 Mon Sep 17 00:00:00 2001 From: geoca <272055834+geocausa@users.noreply.github.com> Date: Sun, 23 Aug 2026 08:07:44 +0100 Subject: [PATCH 02/20] soundwire: add optional slave transport metadata Let a slave describe optional banked registers implemented by a SIMPLE data port, and let a stream provide slave-only transport overrides. The new fields default to zero so existing SoundWire devices are unchanged. [Leon: Split the zero-default slave transport metadata from the source overlay and rebased it onto the current SoundWire types.] Link: https://github.com/geocausa/SP11X1e-audio/commit/31466d62b38c8138d476b2fd3606a76272fb3d34 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/4bcfa10f65f2ea99ca66a42b5e523c351b45ced9 Signed-off-by: Leon Silcott --- include/linux/soundwire/sdw.h | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/include/linux/soundwire/sdw.h b/include/linux/soundwire/sdw.h index 79dd44922fbcae..b3c74c876b83fa 100644 --- a/include/linux/soundwire/sdw.h +++ b/include/linux/soundwire/sdw.h @@ -305,7 +305,14 @@ struct sdw_dp0_prop { * @read_only_wordlength: Read Only wordlength field in DPN_BlockCtrl1 register * @simple_ch_prep_sm: If the port supports simplified channel prepare state * machine + * @simple_transport_registers: Optional banked transport registers implemented + * by a SIMPLE port in addition to the mandatory SIMPLE register set. */ +#define SDW_DPN_SIMPLE_TRANSPORT_BLOCKCTRL3 BIT(0) +#define SDW_DPN_SIMPLE_TRANSPORT_SAMPLECTRL2 BIT(1) +#define SDW_DPN_SIMPLE_TRANSPORT_HCTRL BIT(2) +#define SDW_DPN_SIMPLE_TRANSPORT_OFFSETCTRL2 BIT(3) + struct sdw_dpn_prop { u32 num; u32 max_word; @@ -330,6 +337,7 @@ struct sdw_dpn_prop { bool block_pack_mode; bool read_only_wordlength; bool simple_ch_prep_sm; + u32 simple_transport_registers; }; /** @@ -890,10 +898,18 @@ void sdw_show_ping_status(struct sdw_bus *bus, bool sync_delay); * * @num: Port number * @ch_mask: channels mask for port + * @transport_params_override_mask: Bitmask selecting slave transport fields + * which override values computed by the bus master. Master port parameters are + * never affected by these per-slave overrides. + * @transport_params_override: Values for the selected transport fields. */ +#define SDW_PORT_CONFIG_OVERRIDE_OFFSET1 BIT(0) + struct sdw_port_config { unsigned int num; unsigned int ch_mask; + unsigned int transport_params_override_mask; + struct sdw_transport_params transport_params_override; }; /** From 9b61451addcce2351d66710db7eaffc49c74df04 Mon Sep 17 00:00:00 2001 From: geoca <272055834+geocausa@users.noreply.github.com> Date: Sun, 23 Aug 2026 08:07:44 +0100 Subject: [PATCH 03/20] ASoC: qcom: x1e80100: prepare Denali feedback backends Give the Denali sound-card compatible a private configuration and use it to constrain the WSA VI and CPS rate, format, and channel-map setup. Other X1E80100 sound cards keep their existing backend behavior and speaker volume limits. Do not carry the source branch's global AudioReach readiness variables. Readiness must instead be represented by the relevant card and graph instances in the runtime part of the series. [Leon: Replaced the source global readiness and volume changes with a Denali-private card configuration while retaining non-Denali behavior.] Link: https://github.com/geocausa/SP11X1e-audio/commit/31466d62b38c8138d476b2fd3606a76272fb3d34 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/4f9ac01837b4242623d46b08b7662c6d9c5d52e2 Signed-off-by: Leon Silcott --- sound/soc/qcom/x1e80100.c | 54 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/sound/soc/qcom/x1e80100.c b/sound/soc/qcom/x1e80100.c index c92fe652f544ff..fab698f886dd75 100644 --- a/sound/soc/qcom/x1e80100.c +++ b/sound/soc/qcom/x1e80100.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -19,6 +20,7 @@ struct x1e80100_snd_cfg { const char *driver_name; const unsigned int *channels_map; int channels_num; + bool protected_speaker_feedback; }; struct x1e80100_snd_data { @@ -57,6 +59,11 @@ static int x1e80100_snd_init(struct snd_soc_pcm_runtime *rtd) snd_soc_limit_volume(card, "WooferRight PA Volume", 6); snd_soc_limit_volume(card, "TweeterRight PA Volume", 6); break; + case WSA_CODEC_DMA_TX_0: + case WSA_CODEC_DMA_TX_1: + if (data->cfg->protected_speaker_feedback) + return 0; + break; case DISPLAY_PORT_RX_0: dp_pcm_id = 0; dp_jack = &data->dp_jack[dp_pcm_id]; @@ -78,11 +85,14 @@ static int x1e80100_snd_init(struct snd_soc_pcm_runtime *rtd) static int x1e80100_be_hw_params_fixup(struct snd_soc_pcm_runtime *rtd, struct snd_pcm_hw_params *params) { + struct x1e80100_snd_data *data = snd_soc_card_get_drvdata(rtd->card); struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(rtd, 0); struct snd_interval *rate = hw_param_interval(params, SNDRV_PCM_HW_PARAM_RATE); struct snd_interval *channels = hw_param_interval(params, SNDRV_PCM_HW_PARAM_CHANNELS); + struct snd_mask *format = hw_param_mask(params, + SNDRV_PCM_HW_PARAM_FORMAT); rate->min = rate->max = 48000; switch (cpu_dai->id) { @@ -92,6 +102,26 @@ static int x1e80100_be_hw_params_fixup(struct snd_soc_pcm_runtime *rtd, case TX_CODEC_DMA_TX_3: channels->min = 1; break; + case WSA_CODEC_DMA_TX_0: + if (!data->cfg->protected_speaker_feedback) + break; + rate->min = 8000; + rate->max = 8000; + channels->min = 2; + channels->max = 2; + snd_mask_none(format); + snd_mask_set_format(format, SNDRV_PCM_FORMAT_S32_LE); + break; + case WSA_CODEC_DMA_TX_1: + if (!data->cfg->protected_speaker_feedback) + break; + rate->min = 24000; + rate->max = 24000; + channels->min = 2; + channels->max = 2; + snd_mask_none(format); + snd_mask_set_format(format, SNDRV_PCM_FORMAT_S32_LE); + break; default: break; } @@ -142,6 +172,7 @@ static int x1e80100_snd_prepare(struct snd_pcm_substream *substream) struct x1e80100_snd_data *data = snd_soc_card_get_drvdata(rtd->card); unsigned int channels = substream->runtime->channels; unsigned int rx_slot[4]; + unsigned int tx_slot[4]; int ret; switch (cpu_dai->id) { @@ -156,11 +187,26 @@ static int x1e80100_snd_prepare(struct snd_pcm_substream *substream) if (ret) return ret; break; + case WSA_CODEC_DMA_TX_0: + case WSA_CODEC_DMA_TX_1: + if (!data->cfg->protected_speaker_feedback) + break; + + ret = x1e80100_snd_hw_map_channels(data, tx_slot, channels); + if (ret) + return ret; + + ret = snd_soc_dai_set_channel_map(cpu_dai, channels, tx_slot, + 0, NULL); + if (ret) + return ret; + break; default: break; } - return qcom_snd_sdw_prepare(substream, &data->stream_prepared[cpu_dai->id]); + return qcom_snd_sdw_prepare(substream, + &data->stream_prepared[cpu_dai->id]); } static int x1e80100_snd_hw_free(struct snd_pcm_substream *substream) @@ -229,6 +275,11 @@ static const struct x1e80100_snd_cfg x1e80100_cfg = { .driver_name = "x1e80100", }; +static const struct x1e80100_snd_cfg denali_cfg = { + .driver_name = "x1e80100", + .protected_speaker_feedback = true, +}; + static const struct x1e80100_snd_cfg glymur_cfg = { .driver_name = "glymur", }; @@ -247,6 +298,7 @@ static const struct x1e80100_snd_cfg dell_xps13_9345_cfg = { }; static const struct of_device_id snd_x1e80100_dt_match[] = { + { .compatible = "microsoft,denali-sndcard", .data = &denali_cfg, }, { .compatible = "qcom,x1e80100-sndcard", .data = &x1e80100_cfg, }, { .compatible = "dell,xps13-9345-sndcard", .data = &dell_xps13_9345_cfg, }, { .compatible = "qcom,glymur-sndcard", .data = &glymur_cfg, }, From e30fc75df56b3b13e0a83e8734bd48bee2d19a40 Mon Sep 17 00:00:00 2001 From: geoca <272055834+geocausa@users.noreply.github.com> Date: Sun, 23 Aug 2026 08:07:44 +0100 Subject: [PATCH 04/20] soundwire: support opt-in SIMPLE transport extensions Let a slave describe optional banked registers implemented by a SIMPLE data port, and let a stream provide slave-only transport overrides. The new fields default to zero, so existing SoundWire devices keep the current programming path. This is the generic core portion required by the Denali WSA8845 feedback ports; controller and codec use is added separately. [Leon: Split the generic SIMPLE transport/register override support from its Qualcomm controller and codec consumers and retained the current stream API.] Link: https://github.com/geocausa/SP11X1e-audio/commit/31466d62b38c8138d476b2fd3606a76272fb3d34 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/5cf86f7144e3449e7b2dcb79d8e28dc888da90d8 Signed-off-by: Leon Silcott --- drivers/soundwire/bus.h | 4 ++ drivers/soundwire/stream.c | 80 +++++++++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/drivers/soundwire/bus.h b/drivers/soundwire/bus.h index 8115c64dd48e2f..bc3f862878a4b2 100644 --- a/drivers/soundwire/bus.h +++ b/drivers/soundwire/bus.h @@ -117,6 +117,8 @@ int sdw_find_col_index(int col); * @ch_mask: Channel mask * @transport_params: Transport parameters * @port_params: Port parameters + * @transport_params_override_mask: Bitmask of per-slave transport overrides + * @transport_params_override: Per-slave transport override values * @port_node: List node for Master or Slave port_list * @lane: Which lane is used * @@ -128,6 +130,8 @@ struct sdw_port_runtime { int ch_mask; struct sdw_transport_params transport_params; struct sdw_port_params port_params; + unsigned int transport_params_override_mask; + struct sdw_transport_params transport_params_override; struct list_head port_node; unsigned int lane; }; diff --git a/drivers/soundwire/stream.c b/drivers/soundwire/stream.c index 5d20e95a1e23eb..b00d3354f40c40 100644 --- a/drivers/soundwire/stream.c +++ b/drivers/soundwire/stream.c @@ -62,6 +62,65 @@ int sdw_find_row_index(int row) } EXPORT_SYMBOL(sdw_find_row_index); +static int sdw_program_simple_ext(struct sdw_bus *bus, struct sdw_slave *slave, + struct sdw_transport_params *t_params, + u32 registers) +{ + u32 blockctrl3, samplectrl2, hctrl, offsetctrl2; + u16 wbuf; + int ret; + + if (bus->params.next_bank) { + blockctrl3 = SDW_DPN_BLOCKCTRL3_B1(t_params->port_num); + samplectrl2 = SDW_DPN_SAMPLECTRL2_B1(t_params->port_num); + hctrl = SDW_DPN_HCTRL_B1(t_params->port_num); + offsetctrl2 = SDW_DPN_OFFSETCTRL2_B1(t_params->port_num); + } else { + blockctrl3 = SDW_DPN_BLOCKCTRL3_B0(t_params->port_num); + samplectrl2 = SDW_DPN_SAMPLECTRL2_B0(t_params->port_num); + hctrl = SDW_DPN_HCTRL_B0(t_params->port_num); + offsetctrl2 = SDW_DPN_OFFSETCTRL2_B0(t_params->port_num); + } + + if (registers & SDW_DPN_SIMPLE_TRANSPORT_BLOCKCTRL3) { + ret = sdw_write_no_pm(slave, blockctrl3, t_params->blk_pkg_mode); + if (ret < 0) + return ret; + } + + if (registers & SDW_DPN_SIMPLE_TRANSPORT_SAMPLECTRL2) { + wbuf = FIELD_GET(SDW_DPN_SAMPLECTRL_HIGH, + t_params->sample_interval - 1); + ret = sdw_write_no_pm(slave, samplectrl2, wbuf); + if (ret < 0) + return ret; + } + + if (registers & SDW_DPN_SIMPLE_TRANSPORT_HCTRL) { + wbuf = FIELD_PREP(SDW_DPN_HCTRL_HSTART, t_params->hstart); + wbuf |= FIELD_PREP(SDW_DPN_HCTRL_HSTOP, t_params->hstop); + ret = sdw_write_no_pm(slave, hctrl, wbuf); + if (ret < 0) + return ret; + } + + if (registers & SDW_DPN_SIMPLE_TRANSPORT_OFFSETCTRL2) { + ret = sdw_write_no_pm(slave, offsetctrl2, t_params->offset2); + if (ret < 0) + return ret; + } + + return 0; +} + +static void sdw_apply_slave_transport_overrides(struct sdw_port_runtime *p_rt) +{ + if (p_rt->transport_params_override_mask & + SDW_PORT_CONFIG_OVERRIDE_OFFSET1) + p_rt->transport_params.offset1 = + p_rt->transport_params_override.offset1; +} + static int _sdw_program_slave_port_params(struct sdw_bus *bus, struct sdw_slave *slave, struct sdw_transport_params *t_params, @@ -137,6 +196,7 @@ static int sdw_program_slave_port_params(struct sdw_bus *bus, struct sdw_slave_prop *slave_prop = &s_rt->slave->prop; u32 addr1, addr2, addr3, addr4, addr5, addr6; enum sdw_dpn_type port_type; + u32 simple_transport_registers = 0; bool read_only_wordlength; int ret; u8 wbuf; @@ -144,6 +204,12 @@ static int sdw_program_slave_port_params(struct sdw_bus *bus, if (s_rt->slave->is_mockup_device) return 0; + /* Bus compute_params() establishes the common master schedule. Apply + * explicitly requested slave-only deltas immediately before the slave is + * programmed so master transport state remains shared. + */ + sdw_apply_slave_transport_overrides(p_rt); + if (t_params->port_num) { struct sdw_dpn_prop *dpn_prop; @@ -154,6 +220,7 @@ static int sdw_program_slave_port_params(struct sdw_bus *bus, read_only_wordlength = dpn_prop->read_only_wordlength; port_type = dpn_prop->type; + simple_transport_registers = dpn_prop->simple_transport_registers; } else { read_only_wordlength = false; port_type = SDW_DPN_FULL; @@ -239,7 +306,14 @@ static int sdw_program_slave_port_params(struct sdw_bus *bus, } } - if (port_type != SDW_DPN_SIMPLE) { + if (port_type == SDW_DPN_SIMPLE && simple_transport_registers) { + ret = sdw_program_simple_ext(bus, s_rt->slave, t_params, + simple_transport_registers); + if (ret < 0) + dev_err(&s_rt->slave->dev, + "Extended SIMPLE transport reg write failed for port: %d\n", + t_params->port_num); + } else if (port_type != SDW_DPN_SIMPLE) { ret = _sdw_program_slave_port_params(bus, s_rt->slave, t_params, port_type); if (ret < 0) @@ -983,6 +1057,10 @@ static int sdw_port_config(struct sdw_port_runtime *p_rt, { p_rt->ch_mask = port_config[port_index].ch_mask; p_rt->num = port_config[port_index].num; + p_rt->transport_params_override_mask = + port_config[port_index].transport_params_override_mask; + p_rt->transport_params_override = + port_config[port_index].transport_params_override; /* * TODO: Check port capabilities for requested configuration From e0bde7747ca3e811da8b2f24c4b617b08d8cb0f4 Mon Sep 17 00:00:00 2001 From: geoca <272055834+geocausa@users.noreply.github.com> Date: Sun, 23 Aug 2026 08:07:44 +0100 Subject: [PATCH 05/20] soundwire: qcom: expose Denali WSA feedback DAIs Expose companion playback streams for the two Denali WSA input DAIs so DPCM can start VI and CPS with speaker render while SoundWire keeps their physical direction toward the master. Merge the two amplifier channels only for Denali's shared CPS master port. Gate both changes on the Denali machine compatible, the WSA controller, and DAI IDs 9 and 10. Other Qualcomm SoundWire controllers retain their existing DAI capabilities, rates, formats, direction, and port allocation. The source branch's global diagnostic module parameters are intentionally not carried. [Leon: Scoped the feedback direction, shared-port merge, and companion DAIs to Denali WSA controller IDs 9 and 10. Omitted the source diagnostic module parameters.] Link: https://github.com/geocausa/SP11X1e-audio/commit/31466d62b38c8138d476b2fd3606a76272fb3d34 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/5cf86f7144e3449e7b2dcb79d8e28dc888da90d8 Signed-off-by: Leon Silcott --- drivers/soundwire/qcom.c | 63 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/drivers/soundwire/qcom.c b/drivers/soundwire/qcom.c index 3d8f5a81eff195..826affd615c0f0 100644 --- a/drivers/soundwire/qcom.c +++ b/drivers/soundwire/qcom.c @@ -1217,10 +1217,37 @@ static void qcom_swrm_stream_free_ports(struct qcom_swrm_ctrl *ctrl, mutex_unlock(&ctrl->port_lock); } +#define SWRM_DENALI_VI_DAI 9 +#define SWRM_DENALI_CPS_DAI 10 + +static bool qcom_swrm_is_denali_feedback_dai(struct qcom_swrm_ctrl *ctrl, int dai_id) +{ + return of_machine_is_compatible("microsoft,denali") && + ctrl->bus.controller_id == MASTER_ID_WSA && + (dai_id == SWRM_DENALI_VI_DAI || + dai_id == SWRM_DENALI_CPS_DAI); +} + +static bool qcom_swrm_merge_port(struct sdw_port_config *pconfig, int nports, + int port, struct sdw_port_runtime *p_rt) +{ + int i; + + for (i = 0; i < nports; i++) { + if (pconfig[i].num != port) + continue; + + pconfig[i].ch_mask |= p_rt->ch_mask; + return true; + } + + return false; +} + static int qcom_swrm_stream_alloc_ports(struct qcom_swrm_ctrl *ctrl, struct sdw_stream_runtime *stream, - struct snd_pcm_hw_params *params, - int direction) + struct snd_pcm_hw_params *params, + int direction, int dai_id) { struct sdw_stream_config sconfig; struct sdw_master_runtime *m_rt; @@ -1235,7 +1262,15 @@ static int qcom_swrm_stream_alloc_ports(struct qcom_swrm_ctrl *ctrl, if (!pconfig) return -ENOMEM; - if (direction == SNDRV_PCM_STREAM_CAPTURE) + /* + * Direction is a property of the controller data port. A speaker VI + * sidechain is intentionally exposed as a companion playback BE so + * DPCM starts it with render, while the physical DIN port still carries + * data from the SoundWire slaves into the master. + */ + if (qcom_swrm_is_denali_feedback_dai(ctrl, dai_id)) + sconfig.direction = SDW_DATA_DIR_TX; + else if (direction == SNDRV_PCM_STREAM_CAPTURE) sconfig.direction = SDW_DATA_DIR_TX; else sconfig.direction = SDW_DATA_DIR_RX; @@ -1275,6 +1310,12 @@ static int qcom_swrm_stream_alloc_ports(struct qcom_swrm_ctrl *ctrl, dev_err(ctrl->dev, "All ports busy\n"); return -EBUSY; } + + /* Both Denali amplifiers use one master CPS port. */ + if (qcom_swrm_is_denali_feedback_dai(ctrl, dai_id) && + qcom_swrm_merge_port(pconfig, nports, pn, p_rt)) + continue; + set_bit(pn, port_mask); pconfig[nports].num = pn; pconfig[nports].ch_mask = p_rt->ch_mask; @@ -1298,7 +1339,7 @@ static int qcom_swrm_hw_params(struct snd_pcm_substream *substream, int ret; ret = qcom_swrm_stream_alloc_ports(ctrl, sruntime, params, - substream->stream); + substream->stream, dai->id); if (ret) qcom_swrm_stream_free_ports(ctrl, sruntime); @@ -1404,6 +1445,20 @@ static int qcom_swrm_register_dais(struct qcom_swrm_ctrl *ctrl) stream->rates = SNDRV_PCM_RATE_48000; stream->formats = SNDRV_PCM_FMTBIT_S16_LE; + if (qcom_swrm_is_denali_feedback_dai(ctrl, i)) { + dais[i].playback = (struct snd_soc_pcm_stream) { + .channels_min = 1, + .channels_max = 1, + .rates = i == SWRM_DENALI_VI_DAI ? + SNDRV_PCM_RATE_8000 : SNDRV_PCM_RATE_24000, + .formats = SNDRV_PCM_FMTBIT_S32_LE, + }; + dais[i].playback.stream_name = + devm_kasprintf(dev, GFP_KERNEL, "SoundWire Feedback%d", i); + if (!dais[i].playback.stream_name) + return -ENOMEM; + } + dais[i].ops = &qcom_swrm_pdm_dai_ops; dais[i].id = i; } From 6a1d152564fc04f875d8e216f6b619c9502c7b68 Mon Sep 17 00:00:00 2001 From: geoca <272055834+geocausa@users.noreply.github.com> Date: Sun, 23 Aug 2026 08:07:44 +0100 Subject: [PATCH 06/20] ASoC: wsa884x: add opt-in VI and CPS feedback DAIs Register separate VI and CPS feedback DAIs when a WSA884x instance has the qcom,enable-cps property. Split its SoundWire sink and source ports, apply the DT-selected VISENSE mask and CPS Offset1, and keep playback and feedback stream state separate. Devices without the property still register only the existing speaker DAI and retain the existing port description and stream setup. This commit does not include the Denali PA register profile or lifecycle changes. [Leon: Split the WSA884x feedback DAI/port subset, made it an explicit per-node opt-in with private stream state, and omitted the broad PA lifecycle changes.] Link: https://github.com/geocausa/SP11X1e-audio/commit/31466d62b38c8138d476b2fd3606a76272fb3d34 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/e72bffcb10ef197105f0dc8f0b173d99f472114b Signed-off-by: Leon Silcott --- sound/soc/codecs/wsa884x.c | 170 ++++++++++++++++++++++++++++++++++--- 1 file changed, 158 insertions(+), 12 deletions(-) diff --git a/sound/soc/codecs/wsa884x.c b/sound/soc/codecs/wsa884x.c index 6c6b497657d0c8..5a3fa70e9a2a1a 100644 --- a/sound/soc/codecs/wsa884x.c +++ b/sound/soc/codecs/wsa884x.c @@ -731,8 +731,11 @@ struct wsa884x_priv { struct regulator_bulk_data supplies[WSA884X_SUPPLIES_NUM]; struct sdw_slave *slave; struct sdw_stream_config sconfig; - struct sdw_stream_runtime *sruntime; + struct sdw_stream_runtime *sruntime[3]; struct sdw_port_config port_config[WSA884X_MAX_SWR_PORTS]; + u8 cps_offset1; + u8 visense_channel_mask; + bool protected_feedback; struct gpio_desc *sd_n; struct reset_control *sd_reset; bool port_prepared[WSA884X_MAX_SWR_PORTS]; @@ -872,6 +875,29 @@ static struct sdw_dpn_prop wsa884x_sink_dpn_prop[WSA884X_MAX_SWR_PORTS] = { } }; +static struct sdw_dpn_prop wsa884x_source_dpn_prop[] = { + { + .num = WSA884X_PORT_VISENSE + 1, + .type = SDW_DPN_SIMPLE, + .min_ch = 1, + .max_ch = 1, + .simple_ch_prep_sm = true, + .read_only_wordlength = true, + }, + { + .num = WSA884X_PORT_CPS + 1, + .type = SDW_DPN_SIMPLE, + .min_ch = 1, + .max_ch = 2, + .simple_ch_prep_sm = true, + .read_only_wordlength = false, + .simple_transport_registers = + SDW_DPN_SIMPLE_TRANSPORT_BLOCKCTRL3 | + SDW_DPN_SIMPLE_TRANSPORT_SAMPLECTRL2 | + SDW_DPN_SIMPLE_TRANSPORT_HCTRL, + }, +}; + static const struct sdw_port_config wsa884x_pconfig[WSA884X_MAX_SWR_PORTS] = { [WSA884X_PORT_DAC] = { .num = WSA884X_PORT_DAC + 1, @@ -1773,22 +1799,74 @@ static int wsa884x_hw_params(struct snd_pcm_substream *substream, struct snd_soc_dai *dai) { struct wsa884x_priv *wsa884x = dev_get_drvdata(dai->dev); + struct sdw_port_config port_config[WSA884X_MAX_SWR_PORTS]; + struct sdw_stream_config sconfig = { + .ch_count = 1, + .bps = 1, + .type = SDW_STREAM_PDM, + }; + int active_ports = 0; int i; - wsa884x->active_ports = 0; + if (!wsa884x->protected_feedback) { + wsa884x->active_ports = 0; + for (i = 0; i < WSA884X_MAX_SWR_PORTS; i++) { + if (!wsa884x->port_enable[i]) + continue; + + wsa884x->port_config[wsa884x->active_ports] = + wsa884x_pconfig[i]; + wsa884x->active_ports++; + } + + wsa884x->sconfig.frame_rate = params_rate(params); + + return sdw_stream_add_slave(wsa884x->slave, &wsa884x->sconfig, + wsa884x->port_config, + wsa884x->active_ports, + wsa884x->sruntime[0]); + } + for (i = 0; i < WSA884X_MAX_SWR_PORTS; i++) { if (!wsa884x->port_enable[i]) continue; - wsa884x->port_config[wsa884x->active_ports] = wsa884x_pconfig[i]; - wsa884x->active_ports++; + if (dai->id == 0) { + if (i != WSA884X_PORT_DAC && + i != WSA884X_PORT_COMP && + i != WSA884X_PORT_BOOST) + continue; + } else if (dai->id == 1) { + if (i != WSA884X_PORT_VISENSE) + continue; + } else if (dai->id == 2) { + if (i != WSA884X_PORT_CPS) + continue; + } else { + continue; + } + + port_config[active_ports] = wsa884x_pconfig[i]; + if (i == WSA884X_PORT_VISENSE) + port_config[active_ports].ch_mask = + wsa884x->visense_channel_mask; + if (i == WSA884X_PORT_CPS) { + port_config[active_ports].transport_params_override_mask = + SDW_PORT_CONFIG_OVERRIDE_OFFSET1; + port_config[active_ports].transport_params_override.offset1 = + wsa884x->cps_offset1; + } + active_ports++; } - wsa884x->sconfig.frame_rate = params_rate(params); + if (!active_ports) + return -ENODEV; + + sconfig.frame_rate = params_rate(params); + sconfig.direction = dai->id == 0 ? SDW_DATA_DIR_RX : SDW_DATA_DIR_TX; - return sdw_stream_add_slave(wsa884x->slave, &wsa884x->sconfig, - wsa884x->port_config, wsa884x->active_ports, - wsa884x->sruntime); + return sdw_stream_add_slave(wsa884x->slave, &sconfig, port_config, + active_ports, wsa884x->sruntime[dai->id]); } static int wsa884x_hw_free(struct snd_pcm_substream *substream, @@ -1796,7 +1874,7 @@ static int wsa884x_hw_free(struct snd_pcm_substream *substream, { struct wsa884x_priv *wsa884x = dev_get_drvdata(dai->dev); - sdw_stream_remove_slave(wsa884x->slave, wsa884x->sruntime); + sdw_stream_remove_slave(wsa884x->slave, wsa884x->sruntime[dai->id]); return 0; } @@ -1805,6 +1883,9 @@ static int wsa884x_mute_stream(struct snd_soc_dai *dai, int mute, int stream) { struct snd_soc_component *component = dai->component; + if (dai->id != 0) + return 0; + if (mute) { snd_soc_component_write_field(component, WSA884X_DRE_CTL_1, WSA884X_DRE_CTL_1_CSR_GAIN_EN_MASK, @@ -1830,7 +1911,7 @@ static int wsa884x_set_stream(struct snd_soc_dai *dai, { struct wsa884x_priv *wsa884x = dev_get_drvdata(dai->dev); - wsa884x->sruntime = stream; + wsa884x->sruntime[dai->id] = stream; return 0; } @@ -1846,6 +1927,7 @@ static const struct snd_soc_dai_ops wsa884x_dai_ops = { static struct snd_soc_dai_driver wsa884x_dais[] = { { .name = "SPKR", + .id = 0, .playback = { .stream_name = "SPKR Playback", .rates = WSA884X_RATES | WSA884X_FRAC_RATES, @@ -1857,6 +1939,34 @@ static struct snd_soc_dai_driver wsa884x_dais[] = { }, .ops = &wsa884x_dai_ops, }, + { + .name = "SPKR_VI", + .id = 1, + .playback = { + .stream_name = "SPKR VI Protection", + .rates = SNDRV_PCM_RATE_8000, + .formats = SNDRV_PCM_FMTBIT_S32_LE, + .rate_min = 8000, + .rate_max = 8000, + .channels_min = 1, + .channels_max = 1, + }, + .ops = &wsa884x_dai_ops, + }, + { + .name = "SPKR_CPS", + .id = 2, + .playback = { + .stream_name = "SPKR CPS Protection", + .rates = SNDRV_PCM_RATE_24000, + .formats = SNDRV_PCM_FMTBIT_S32_LE, + .rate_min = 24000, + .rate_max = 24000, + .channels_min = 1, + .channels_max = 1, + }, + .ops = &wsa884x_dai_ops, + }, }; static int wsa884x_get_temp(struct wsa884x_priv *wsa884x, long *temp) @@ -2046,6 +2156,8 @@ static int wsa884x_probe(struct sdw_slave *pdev, struct device *dev = &pdev->dev; struct wsa884x_priv *wsa884x; unsigned int i; + u32 cps_offset1; + u32 visense_channel_mask; int ret; wsa884x = devm_kzalloc(dev, sizeof(*wsa884x), GFP_KERNEL); @@ -2083,6 +2195,29 @@ static int wsa884x_probe(struct sdw_slave *pdev, wsa884x->sconfig.bps = 1; wsa884x->sconfig.direction = SDW_DATA_DIR_RX; wsa884x->sconfig.type = SDW_STREAM_PDM; + wsa884x->visense_channel_mask = + wsa884x_pconfig[WSA884X_PORT_VISENSE].ch_mask; + wsa884x->protected_feedback = + of_property_read_bool(dev->of_node, "qcom,enable-cps"); + if (wsa884x->protected_feedback) { + ret = of_property_read_u32(dev->of_node, + "qcom,visense-channel-mask", + &visense_channel_mask); + if (!ret) { + if (!visense_channel_mask || visense_channel_mask > U8_MAX) + return dev_err_probe(dev, -EINVAL, + "invalid qcom,visense-channel-mask\n"); + wsa884x->visense_channel_mask = visense_channel_mask; + } + + ret = of_property_read_u32(dev->of_node, "qcom,cps-offset1", + &cps_offset1); + if (ret || cps_offset1 > U8_MAX) + return dev_err_probe(dev, ret ?: -ERANGE, + "invalid qcom,cps-offset1\n"); + wsa884x->cps_offset1 = cps_offset1; + wsa884x->port_enable[WSA884X_PORT_CPS] = true; + } /* * Port map index starts with 0, however the data port for this codec @@ -2092,7 +2227,17 @@ static int wsa884x_probe(struct sdw_slave *pdev, WSA884X_MAX_SWR_PORTS)) dev_dbg(dev, "Static Port mapping not specified\n"); - pdev->prop.sink_ports = GENMASK(WSA884X_MAX_SWR_PORTS - 1, 0); + if (wsa884x->protected_feedback) { + pdev->prop.sink_ports = BIT(WSA884X_PORT_DAC + 1) | + BIT(WSA884X_PORT_COMP + 1) | + BIT(WSA884X_PORT_BOOST + 1) | + BIT(WSA884X_PORT_PBR + 1); + pdev->prop.source_ports = BIT(WSA884X_PORT_VISENSE + 1) | + BIT(WSA884X_PORT_CPS + 1); + pdev->prop.src_dpn_prop = wsa884x_source_dpn_prop; + } else { + pdev->prop.sink_ports = GENMASK(WSA884X_MAX_SWR_PORTS - 1, 0); + } pdev->prop.simple_clk_stop_capable = true; pdev->prop.sink_dpn_prop = wsa884x_sink_dpn_prop; pdev->prop.scp_int1_mask = SDW_SCP_INT1_BUS_CLASH | SDW_SCP_INT1_PARITY; @@ -2131,7 +2276,8 @@ static int wsa884x_probe(struct sdw_slave *pdev, return devm_snd_soc_register_component(dev, &wsa884x_component_drv, wsa884x_dais, - ARRAY_SIZE(wsa884x_dais)); + wsa884x->protected_feedback ? + ARRAY_SIZE(wsa884x_dais) : 1); } static int wsa884x_runtime_suspend(struct device *dev) From 80469198474addfb8e8a7f9237c11b17476c0c2e Mon Sep 17 00:00:00 2001 From: geoca <272055834+geocausa@users.noreply.github.com> Date: Sun, 23 Aug 2026 08:07:44 +0100 Subject: [PATCH 07/20] ASoC: lpass-wsa-macro: add Denali feedback endpoints On Denali, add playback-side VI and CPS endpoints so the protected render graph can start both feedback paths atomically. Add the associated DAPM widgets and keep the existing VI capture endpoint intact. Build the extended DAI table from a private copy only on Denali. Other machines continue to register the original DAI table and DAPM graph, so the shared WSA macro driver does not redefine their TX interfaces. [Leon: Split the WSA macro feedback endpoints, built them from a Denali-private DAI/DAPM copy, and preserved the existing VI capture path.] Link: https://github.com/geocausa/SP11X1e-audio/commit/31466d62b38c8138d476b2fd3606a76272fb3d34 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/e72bffcb10ef197105f0dc8f0b173d99f472114b Signed-off-by: Leon Silcott --- sound/soc/codecs/lpass-wsa-macro.c | 86 ++++++++++++++++++++++++++++-- 1 file changed, 83 insertions(+), 3 deletions(-) diff --git a/sound/soc/codecs/lpass-wsa-macro.c b/sound/soc/codecs/lpass-wsa-macro.c index cb457f3edf7d59..8d0ce721bcf747 100644 --- a/sound/soc/codecs/lpass-wsa-macro.c +++ b/sound/soc/codecs/lpass-wsa-macro.c @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -374,6 +375,7 @@ enum { WSA_MACRO_AIF_MIX1_PB, WSA_MACRO_AIF_VI, WSA_MACRO_AIF_ECHO, + WSA_MACRO_AIF_CPS, WSA_MACRO_MAX_DAIS, }; @@ -414,6 +416,7 @@ struct wsa_macro { u32 pcm_rate_vi; int is_softclip_on[WSA_MACRO_SOFTCLIP_MAX]; int softclip_clk_users[WSA_MACRO_SOFTCLIP_MAX]; + bool protected_feedback; struct regmap *regmap; struct clk *mclk; struct clk *npl; @@ -1278,6 +1281,15 @@ static int wsa_macro_hw_params(struct snd_pcm_substream *substream, struct wsa_macro *wsa = snd_soc_component_get_drvdata(component); int ret; + if (wsa->protected_feedback && dai->id == WSA_MACRO_AIF_VI && + substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + wsa->pcm_rate_vi = params_rate(params); + return 0; + } + + if (wsa->protected_feedback && dai->id == WSA_MACRO_AIF_CPS) + return 0; + switch (substream->stream) { case SNDRV_PCM_STREAM_PLAYBACK: ret = wsa_macro_set_interpolator_rate(dai, params_rate(params)); @@ -1312,6 +1324,10 @@ static int wsa_macro_get_channel_map(const struct snd_soc_dai *dai, *tx_slot = wsa->active_ch_mask[dai->id]; *tx_num = wsa->active_ch_cnt[dai->id]; break; + case WSA_MACRO_AIF_CPS: + *tx_num = 2; + *tx_slot = GENMASK(1, 0); + break; case WSA_MACRO_AIF1_PB: case WSA_MACRO_AIF_MIX1_PB: for_each_set_bit(temp, &wsa->active_ch_mask[dai->id], @@ -2388,6 +2404,25 @@ static const struct snd_soc_dapm_widget wsa_macro_dapm_widgets[] = { SND_SOC_DAPM_PRE_PMU | SND_SOC_DAPM_POST_PMD), }; +static const struct snd_soc_dapm_widget wsa_macro_denali_widgets[] = { + SND_SOC_DAPM_AIF_IN_E("WSA AIF_VI Protection", + "WSA_AIF_VI Protection", 0, + SND_SOC_NOPM, WSA_MACRO_AIF_VI, 0, + wsa_macro_enable_vi_feedback, + SND_SOC_DAPM_POST_PMU | SND_SOC_DAPM_POST_PMD), + SND_SOC_DAPM_AIF_IN("WSA AIF_CPS Protection", + "WSA_AIF_CPS Protection", 0, + SND_SOC_NOPM, WSA_MACRO_AIF_CPS, 0), + SND_SOC_DAPM_INPUT("CPSINPUT_WSA"), +}; + +static const struct snd_soc_dapm_route wsa_macro_denali_routes[] = { + { "WSA AIF_VI Protection", NULL, "WSA_AIF_VI Mixer" }, + { "WSA AIF_VI Protection", NULL, "WSA_MCLK" }, + { "WSA AIF_CPS Protection", NULL, "CPSINPUT_WSA" }, + { "WSA AIF_CPS Protection", NULL, "WSA_MCLK" }, +}; + static const struct snd_soc_dapm_widget wsa_macro_dapm_widgets_v2_1[] = { SND_SOC_DAPM_MUX("WSA_RX0 INP0", SND_SOC_NOPM, 0, 0, &rx0_prim_inp0_mux_v2_1), SND_SOC_DAPM_MUX("WSA_RX0 INP1", SND_SOC_NOPM, 0, 0, &rx0_prim_inp1_mux_v2_1), @@ -2562,6 +2597,7 @@ static int wsa_macro_component_probe(struct snd_soc_component *comp) struct wsa_macro *wsa = snd_soc_component_get_drvdata(comp); const struct snd_soc_dapm_widget *widgets; unsigned int num_widgets; + int ret; snd_soc_component_init_regmap(comp, wsa->regmap); @@ -2599,7 +2635,17 @@ static int wsa_macro_component_probe(struct snd_soc_component *comp) return -EINVAL; } - return snd_soc_dapm_new_controls(dapm, widgets, num_widgets); + ret = snd_soc_dapm_new_controls(dapm, widgets, num_widgets); + if (ret || !wsa->protected_feedback) + return ret; + + ret = snd_soc_dapm_new_controls(dapm, wsa_macro_denali_widgets, + ARRAY_SIZE(wsa_macro_denali_widgets)); + if (ret) + return ret; + + return snd_soc_dapm_add_routes(dapm, wsa_macro_denali_routes, + ARRAY_SIZE(wsa_macro_denali_routes)); } static int swclk_gate_enable(struct clk_hw *hw) @@ -2679,9 +2725,11 @@ static const struct snd_soc_component_driver wsa_macro_component_drv = { static int wsa_macro_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; + struct snd_soc_dai_driver *dais = wsa_macro_dai; struct wsa_macro *wsa; kernel_ulong_t flags; void __iomem *base; + int num_dais = ARRAY_SIZE(wsa_macro_dai); int ret, def_count; flags = (kernel_ulong_t)device_get_match_data(dev); @@ -2771,6 +2819,39 @@ static int wsa_macro_probe(struct platform_device *pdev) dev_set_drvdata(dev, wsa); wsa->dev = dev; + wsa->protected_feedback = + of_machine_is_compatible("microsoft,denali"); + if (wsa->protected_feedback) { + dais = devm_kcalloc(dev, num_dais + 1, sizeof(*dais), GFP_KERNEL); + if (!dais) + return -ENOMEM; + + memcpy(dais, wsa_macro_dai, sizeof(wsa_macro_dai)); + dais[WSA_MACRO_AIF_VI].playback = (struct snd_soc_pcm_stream) { + .stream_name = "WSA_AIF_VI Protection", + .rates = SNDRV_PCM_RATE_8000, + .formats = SNDRV_PCM_FMTBIT_S32_LE, + .rate_max = 8000, + .rate_min = 8000, + .channels_min = 1, + .channels_max = 4, + }; + dais[num_dais] = (struct snd_soc_dai_driver) { + .name = "wsa_macro_cps", + .id = WSA_MACRO_AIF_CPS, + .playback = { + .stream_name = "WSA_AIF_CPS Protection", + .rates = SNDRV_PCM_RATE_24000, + .formats = SNDRV_PCM_FMTBIT_S32_LE, + .rate_max = 24000, + .rate_min = 24000, + .channels_min = 2, + .channels_max = 2, + }, + .ops = &wsa_macro_dai_ops, + }; + num_dais++; + } /* set MCLK and NPL rates */ clk_set_rate(wsa->mclk, WSA_MACRO_MCLK_FREQ); @@ -2807,8 +2888,7 @@ static int wsa_macro_probe(struct platform_device *pdev) CDC_WSA_SWR_RST_EN_MASK, CDC_WSA_SWR_RST_DISABLE); ret = devm_snd_soc_register_component(dev, &wsa_macro_component_drv, - wsa_macro_dai, - ARRAY_SIZE(wsa_macro_dai)); + dais, num_dais); if (ret) goto err_rpm_put; From 0d5a0086aae66bc2b45ed4e14eeae2879e1babd0 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sun, 23 Aug 2026 12:33:00 +0100 Subject: [PATCH 08/20] ASoC: q6dsp: expose Denali WSA protection streams Add playback capabilities for the WSA TX0 and TX1 backend DAIs used by Denali's VI and CPS protection links. Build the modified table as a device-managed copy only for the Denali AudioReach backend provider. Other machines and the legacy Q6AFE provider keep the existing capture- only definitions. Propagate allocation failure from the AudioReach DAI probe. Link: https://github.com/geocausa/SP11X1e-audio/commit/31466d62b38c8138d476b2fd3606a76272fb3d34 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/22bdec204438f5e1e19286cb7f1688f196c51967 Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/q6apm-lpass-dais.c | 2 ++ sound/soc/qcom/qdsp6/q6dsp-lpass-ports.c | 42 +++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c index 006b283484d9e9..f40ed81717fd79 100644 --- a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c +++ b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c @@ -316,6 +316,8 @@ static int q6apm_lpass_dai_dev_probe(struct platform_device *pdev) cfg.q6dma_ops = &q6dma_ops; cfg.q6hdmi_ops = &q6hdmi_ops; dais = q6dsp_audio_ports_set_config(dev, &cfg, &num_dais); + if (IS_ERR(dais)) + return PTR_ERR(dais); return devm_snd_soc_register_component(dev, &q6apm_lpass_dai_component, dais, num_dais); } diff --git a/sound/soc/qcom/qdsp6/q6dsp-lpass-ports.c b/sound/soc/qcom/qdsp6/q6dsp-lpass-ports.c index e5cd82f77b5520..6cdd6586ff1f8c 100644 --- a/sound/soc/qcom/qdsp6/q6dsp-lpass-ports.c +++ b/sound/soc/qcom/qdsp6/q6dsp-lpass-ports.c @@ -1,6 +1,8 @@ // SPDX-License-Identifier: GPL-2.0 // Copyright (c) 2020, Linaro Limited +#include +#include #include #include #include @@ -694,6 +696,7 @@ struct snd_soc_dai_driver *q6dsp_audio_ports_set_config(struct device *dev, struct q6dsp_audio_port_dai_driver_config *cfg, int *num_dais) { + struct snd_soc_dai_driver *dais = q6dsp_audio_fe_dais; int i; for (i = 0; i < ARRAY_SIZE(q6dsp_audio_fe_dais); i++) { @@ -729,6 +732,43 @@ struct snd_soc_dai_driver *q6dsp_audio_ports_set_config(struct device *dev, } *num_dais = ARRAY_SIZE(q6dsp_audio_fe_dais); - return q6dsp_audio_fe_dais; + if (!of_machine_is_compatible("microsoft,denali") || + !of_device_is_compatible(dev->of_node, "qcom,q6apm-lpass-dais")) + return dais; + + dais = devm_kmemdup(dev, q6dsp_audio_fe_dais, sizeof(q6dsp_audio_fe_dais), GFP_KERNEL); + if (!dais) + return ERR_PTR(-ENOMEM); + + for (i = 0; i < *num_dais; i++) { + switch (dais[i].id) { + case WSA_CODEC_DMA_TX_0: + dais[i].playback = (struct snd_soc_pcm_stream) { + .stream_name = "WSA_CODEC_DMA_TX_0 Protection", + .rates = SNDRV_PCM_RATE_8000, + .formats = SNDRV_PCM_FMTBIT_S32_LE, + .channels_min = 2, + .channels_max = 2, + .rate_min = 8000, + .rate_max = 8000, + }; + break; + case WSA_CODEC_DMA_TX_1: + dais[i].playback = (struct snd_soc_pcm_stream) { + .stream_name = "WSA_CODEC_DMA_TX_1 Protection", + .rates = SNDRV_PCM_RATE_24000, + .formats = SNDRV_PCM_FMTBIT_S32_LE, + .channels_min = 2, + .channels_max = 2, + .rate_min = 24000, + .rate_max = 24000, + }; + break; + default: + break; + } + } + + return dais; } EXPORT_SYMBOL_GPL(q6dsp_audio_ports_set_config); From af2f1d6697a03cfb911a4f3b7f4bff6afc247cb0 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Tue, 1 Sep 2026 03:28:01 +0100 Subject: [PATCH 09/20] soc: qcom: apr: drain callbacks before freeing services APR receive processing can hold a service pointer after dropping svcs_lock, while dynamic port removal used to remove and immediately free that service. A concurrent callback could therefore use freed memory. Give every service a registrar reference and take callback references while holding svcs_lock. Remove the service from the IDR first, then wait for admitted callbacks to drain before freeing it. Apply the same unwind discipline to static service registration failures. gpr_free_port() may sleep and must run in process context outside the port's own callback. This avoids self-deadlock while preserving a simple removal contract. Signed-off-by: Leon Silcott --- drivers/soc/qcom/apr.c | 61 ++++++++++++++++++++++++++++-------- include/linux/soc/qcom/apr.h | 4 +++ 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/drivers/soc/qcom/apr.c b/drivers/soc/qcom/apr.c index ea7f83916d8d8d..f348be37cc2c80 100644 --- a/drivers/soc/qcom/apr.c +++ b/drivers/soc/qcom/apr.c @@ -74,15 +74,35 @@ int apr_send_pkt(struct apr_device *adev, struct apr_pkt *pkt) } EXPORT_SYMBOL_GPL(apr_send_pkt); -void gpr_free_port(gpr_port_t *port) +static void pkt_router_svc_put(struct pkt_router_svc *svc) +{ + if (refcount_dec_and_test(&svc->callback_refs)) + complete(&svc->callbacks_drained); +} + +static void pkt_router_svc_remove(struct pkt_router_svc *svc) { - struct packet_router *gpr = port->pr; + struct packet_router *pr = svc->pr; unsigned long flags; - spin_lock_irqsave(&gpr->svcs_lock, flags); - idr_remove(&gpr->svcs_idr, port->id); - spin_unlock_irqrestore(&gpr->svcs_lock, flags); + spin_lock_irqsave(&pr->svcs_lock, flags); + idr_remove(&pr->svcs_idr, svc->id); + spin_unlock_irqrestore(&pr->svcs_lock, flags); + + if (!refcount_dec_and_test(&svc->callback_refs)) + wait_for_completion(&svc->callbacks_drained); +} +/** + * gpr_free_port() - unregister a dynamic GPR port after callbacks drain + * @port: dynamic port returned by gpr_alloc_port() + * + * Context: process context, and never from @port's own callback. + */ +void gpr_free_port(gpr_port_t *port) +{ + might_sleep(); + pkt_router_svc_remove(port); kfree(port); } EXPORT_SYMBOL_GPL(gpr_free_port); @@ -105,6 +125,8 @@ gpr_port_t *gpr_alloc_port(struct apr_device *gdev, struct device *dev, svc->priv = priv; svc->dev = dev; spin_lock_init(&svc->lock); + refcount_set(&svc->callback_refs, 1); + init_completion(&svc->callbacks_drained); spin_lock(&pr->svcs_lock); id = idr_alloc_cyclic(&pr->svcs_idr, svc, GPR_DYNAMIC_PORT_START, @@ -191,7 +213,7 @@ static int apr_do_rx_callback(struct packet_router *apr, struct apr_rx_buf *abuf { uint16_t hdr_size, msg_type, ver, svc_id; struct pkt_router_svc *svc; - struct apr_device *adev; + struct apr_device *adev = NULL; struct apr_driver *adrv = NULL; struct apr_resp_pkt resp; struct apr_hdr *hdr; @@ -232,6 +254,8 @@ static int apr_do_rx_callback(struct packet_router *apr, struct apr_rx_buf *abuf svc_id = hdr->dest_svc; spin_lock_irqsave(&apr->svcs_lock, flags); svc = idr_find(&apr->svcs_idr, svc_id); + if (svc && !refcount_inc_not_zero(&svc->callback_refs)) + svc = NULL; if (svc && svc->dev->driver) { adev = svc_to_apr_device(svc); adrv = to_apr_driver(adev->dev.driver); @@ -239,6 +263,8 @@ static int apr_do_rx_callback(struct packet_router *apr, struct apr_rx_buf *abuf spin_unlock_irqrestore(&apr->svcs_lock, flags); if (!adrv || !adev) { + if (svc) + pkt_router_svc_put(svc); dev_err(apr->dev, "APR: service is not registered (%d)\n", svc_id); return -EINVAL; @@ -255,6 +281,7 @@ static int apr_do_rx_callback(struct packet_router *apr, struct apr_rx_buf *abuf resp.payload = buf + hdr_size; adrv->callback(adev, &resp); + pkt_router_svc_put(svc); return 0; } @@ -298,6 +325,8 @@ static int gpr_do_rx_callback(struct packet_router *gpr, struct apr_rx_buf *abuf spin_lock_irqsave(&gpr->svcs_lock, flags); svc = idr_find(&gpr->svcs_idr, hdr->dest_port); + if (svc && !refcount_inc_not_zero(&svc->callback_refs)) + svc = NULL; spin_unlock_irqrestore(&gpr->svcs_lock, flags); if (!svc) { @@ -308,6 +337,7 @@ static int gpr_do_rx_callback(struct packet_router *gpr, struct apr_rx_buf *abuf if (svc->callback) svc->callback(&resp, svc->priv, 0); + pkt_router_svc_put(svc); return 0; } @@ -378,13 +408,10 @@ static void apr_device_remove(struct device *dev) { struct apr_device *adev = to_apr_device(dev); struct apr_driver *adrv = to_apr_driver(dev->driver); - struct packet_router *apr = dev_get_drvdata(adev->dev.parent); + pkt_router_svc_remove(&adev->svc); if (adrv->remove) adrv->remove(adev); - spin_lock(&apr->svcs_lock); - idr_remove(&apr->svcs_idr, adev->svc.id); - spin_unlock(&apr->svcs_lock); } static int apr_uevent(const struct device *dev, struct kobj_uevent_env *env) @@ -428,6 +455,8 @@ static int apr_add_device(struct device *dev, struct device_node *np, svc->priv = adev; svc->dev = dev; spin_lock_init(&svc->lock); + refcount_set(&svc->callback_refs, 1); + init_completion(&svc->callbacks_drained); adev->domain_id = domain_id; @@ -458,7 +487,7 @@ static int apr_add_device(struct device *dev, struct device_node *np, spin_unlock(&apr->svcs_lock); if (ret < 0) { dev_err(dev, "idr_alloc failed: %d\n", ret); - goto out; + goto free_adev; } /* Protection domain is optional, it does not exist on older platforms */ @@ -466,7 +495,7 @@ static int apr_add_device(struct device *dev, struct device_node *np, 1, &adev->service_path); if (ret < 0 && ret != -EINVAL) { dev_err(dev, "Failed to read second value of qcom,protection-domain\n"); - goto out; + goto remove_svc; } dev_info(dev, "Adding APR/GPR dev: %s\n", dev_name(&adev->dev)); @@ -474,10 +503,16 @@ static int apr_add_device(struct device *dev, struct device_node *np, ret = device_register(&adev->dev); if (ret) { dev_err(dev, "device_register failed: %d\n", ret); + pkt_router_svc_remove(svc); put_device(&adev->dev); } -out: + return ret; + +remove_svc: + pkt_router_svc_remove(svc); +free_adev: + kfree(adev); return ret; } diff --git a/include/linux/soc/qcom/apr.h b/include/linux/soc/qcom/apr.h index 909e84f84e0cfa..15c18100494294 100644 --- a/include/linux/soc/qcom/apr.h +++ b/include/linux/soc/qcom/apr.h @@ -3,6 +3,8 @@ #ifndef __QCOM_APR_H_ #define __QCOM_APR_H_ +#include +#include #include #include #include @@ -129,6 +131,8 @@ struct pkt_router_svc { gpr_port_cb callback; struct packet_router *pr; spinlock_t lock; + refcount_t callback_refs; + struct completion callbacks_drained; int id; void *priv; }; From 9ef4471620728bde97ee16c0ef3ed5d6d0bd654c Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Tue, 1 Sep 2026 01:15:49 +0100 Subject: [PATCH 10/20] ASoC: q6apm: correlate commands and memory mappings Global AudioReach commands reused token zero and shared one result slot. Late responses after a timeout could complete a later command or mutate the wrong mapping handle. Dynamic graph clients could also disappear while callbacks or command users still held them. Assign unique tokens to synchronous global and per-client commands while preserving the graph and position-buffer token fields. Claim replies only when token and opcode match the active command, and update mapping handles inside the same critical section. Publish graph clients under a lifecycle lock, reject new users once teardown begins, abort pending waits, and drain admitted users and callbacks before releasing ports. This also corrects the position-buffer unmap token. Link: https://github.com/geocausa/SP11X1e-audio/commit/33d5145179c7b75913c85c31906a9f2fa14ab695 Link: https://github.com/geocausa/SP11X1e-audio/commit/39499f0b2bbf2d3e7144f82b20b95c01190f3f1c Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/22bdec204438f5e1e19286cb7f1688f196c51967 Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/audioreach.c | 112 +++- sound/soc/qcom/qdsp6/audioreach.h | 2 +- sound/soc/qcom/qdsp6/q6apm-lpass-dais.c | 33 +- sound/soc/qcom/qdsp6/q6apm.c | 724 +++++++++++++++++++++--- sound/soc/qcom/qdsp6/q6apm.h | 39 +- 5 files changed, 809 insertions(+), 101 deletions(-) diff --git a/sound/soc/qcom/qdsp6/audioreach.c b/sound/soc/qcom/qdsp6/audioreach.c index e6e9eb2e85aa1f..d212937ce0398d 100644 --- a/sound/soc/qcom/qdsp6/audioreach.c +++ b/sound/soc/qcom/qdsp6/audioreach.c @@ -12,6 +12,10 @@ #include "q6apm.h" #include "audioreach.h" +DEFINE_FREE(q6apm_graph_user, struct q6apm_graph *, q6apm_graph_user_put(_T)) + +#define AUDIOREACH_DSP_EUNSUPPORTED 3 + /* SubGraph Config */ struct apm_sub_graph_data { struct apm_sub_graph_cfg sub_graph_cfg; @@ -610,7 +614,8 @@ int audioreach_send_cmd_sync(struct device *dev, gpr_device_t *gdev, rc = -ETIMEDOUT; } else if (result->status > 0) { dev_err(dev, "DSP returned error[%x] %x\n", hdr->opcode, result->status); - rc = -EINVAL; + rc = result->status == AUDIOREACH_DSP_EUNSUPPORTED ? + -EOPNOTSUPP : -EINVAL; } else { /* DSP successfully finished the command */ rc = 0; @@ -622,12 +627,83 @@ int audioreach_send_cmd_sync(struct device *dev, gpr_device_t *gdev, } EXPORT_SYMBOL_GPL(audioreach_send_cmd_sync); -int audioreach_graph_send_cmd_sync(struct q6apm_graph *graph, const struct gpr_pkt *pkt, +int audioreach_graph_send_cmd_sync(struct q6apm_graph *graph, struct gpr_pkt *pkt, uint32_t rsp_opcode) { + struct gpr_hdr *hdr = &pkt->hdr; + u32 result_status; + bool aborted; + int ret; + + if (!q6apm_graph_user_get(graph)) + return -ESHUTDOWN; + + mutex_lock(&graph->cmd_lock); + if (!++graph->cmd_token) + graph->cmd_token++; + hdr->token = graph->cmd_token; + + spin_lock(&graph->lifecycle_lock); + if (graph->dying || graph->detached) { + spin_unlock(&graph->lifecycle_lock); + ret = -ESHUTDOWN; + goto unlock; + } + spin_lock(&graph->result_lock); + graph->result.opcode = 0; + graph->result.status = 0; + graph->result_token = U32_MAX; + graph->pending_opcode = hdr->opcode; + graph->pending_rsp_opcode = rsp_opcode; + graph->pending_token = hdr->token; + graph->cmd_pending = true; + spin_unlock(&graph->result_lock); + spin_unlock(&graph->lifecycle_lock); + + ret = gpr_send_port_pkt(graph->port, pkt); + if (ret >= 0) + ret = wait_event_timeout(graph->cmd_wait, + !READ_ONCE(graph->cmd_pending) || + (((READ_ONCE(graph->result.opcode) == + hdr->opcode) || + (rsp_opcode && + READ_ONCE(graph->result.opcode) == + rsp_opcode)) && + READ_ONCE(graph->result_token) == + hdr->token), 5 * HZ); + + spin_lock(&graph->result_lock); + aborted = !graph->cmd_pending || READ_ONCE(graph->dying); + if (!ret && graph->result_token == hdr->token && + (graph->result.opcode == hdr->opcode || + (rsp_opcode && graph->result.opcode == rsp_opcode))) + ret = 1; + graph->cmd_pending = false; + result_status = graph->result.status; + spin_unlock(&graph->result_lock); + + if (aborted) { + ret = -ESHUTDOWN; + goto unlock; + } + if (ret < 0) + goto unlock; + if (!ret) { + dev_err(graph->dev, "CMD timeout for [%x] opcode\n", hdr->opcode); + ret = -ETIMEDOUT; + } else if (result_status > 0) { + dev_err(graph->dev, "DSP returned error[%x] %x\n", + hdr->opcode, result_status); + ret = result_status == AUDIOREACH_DSP_EUNSUPPORTED ? + -EOPNOTSUPP : -EINVAL; + } else { + ret = 0; + } - return audioreach_send_cmd_sync(graph->dev, NULL, &graph->result, &graph->lock, - graph->port, &graph->cmd_wait, pkt, rsp_opcode); +unlock: + mutex_unlock(&graph->cmd_lock); + q6apm_graph_user_put(graph); + return ret; } EXPORT_SYMBOL_GPL(audioreach_graph_send_cmd_sync); @@ -952,15 +1028,21 @@ static int audioreach_set_compr_media_format(struct media_format *media_fmt_hdr, int audioreach_compr_set_param(struct q6apm_graph *graph, const struct audioreach_module_config *mcfg) { + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; struct media_format *header; int rc; void *p; - int iid = graph->shm_iid; + int iid; int payload_size = sizeof(struct apm_sh_module_media_fmt_cmd); + struct gpr_pkt *pkt __free(kfree) = NULL; - struct gpr_pkt *pkt __free(kfree) = audioreach_alloc_cmd_pkt(payload_size, - DATA_CMD_WR_SH_MEM_EP_MEDIA_FORMAT, - 0, graph->port->id, iid); + if (!active) + return -ESHUTDOWN; + iid = graph->shm_iid; + pkt = audioreach_alloc_cmd_pkt(payload_size, + DATA_CMD_WR_SH_MEM_EP_MEDIA_FORMAT, + 0, graph->port->id, iid); if (IS_ERR(pkt)) return -ENOMEM; @@ -1478,10 +1560,18 @@ EXPORT_SYMBOL_GPL(audioreach_setup_push_pull); int audioreach_shared_memory_send_eos(struct q6apm_graph *graph) { + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; struct data_cmd_wr_sh_mem_ep_eos *eos; - int iid = graph->shm_iid; - struct gpr_pkt *pkt __free(kfree) = audioreach_alloc_cmd_pkt(sizeof(*eos), - DATA_CMD_WR_SH_MEM_EP_EOS, 0, graph->port->id, iid); + struct gpr_pkt *pkt __free(kfree) = NULL; + int iid; + + if (!active) + return -ESHUTDOWN; + iid = graph->shm_iid; + pkt = audioreach_alloc_cmd_pkt(sizeof(*eos), + DATA_CMD_WR_SH_MEM_EP_EOS, 0, + graph->port->id, iid); if (IS_ERR(pkt)) return PTR_ERR(pkt); diff --git a/sound/soc/qcom/qdsp6/audioreach.h b/sound/soc/qcom/qdsp6/audioreach.h index 62a2fd79bbcb93..0b5eda7e2598d8 100644 --- a/sound/soc/qcom/qdsp6/audioreach.h +++ b/sound/soc/qcom/qdsp6/audioreach.h @@ -933,7 +933,7 @@ void audioreach_graph_free_buf(struct q6apm_graph *graph); int audioreach_send_cmd_sync(struct device *dev, gpr_device_t *gdev, struct gpr_ibasic_rsp_result_t *result, struct mutex *cmd_lock, gpr_port_t *port, wait_queue_head_t *cmd_wait, const struct gpr_pkt *pkt, uint32_t rsp_opcode); -int audioreach_graph_send_cmd_sync(struct q6apm_graph *graph, const struct gpr_pkt *pkt, +int audioreach_graph_send_cmd_sync(struct q6apm_graph *graph, struct gpr_pkt *pkt, uint32_t rsp_opcode); int audioreach_set_media_format(struct q6apm_graph *graph, const struct audioreach_module *module, diff --git a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c index f40ed81717fd79..4aa62d8f4c1e76 100644 --- a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c +++ b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c @@ -148,13 +148,26 @@ static void q6apm_lpass_dai_shutdown(struct snd_pcm_substream *substream, struct if (dai_data->is_port_started[dai->id]) { rc = q6apm_graph_stop(dai_data->graph[dai->id]); - dai_data->is_port_started[dai->id] = false; - if (rc < 0) + if (rc < 0) { + if (rc == -ESHUTDOWN && + !q6apm_graph_close(dai_data->graph[dai->id])) { + dai_data->graph[dai->id] = NULL; + dai_data->is_port_started[dai->id] = false; + return; + } dev_err(dai->dev, "failed to stop APM port (%d)\n", rc); + return; + } + dai_data->is_port_started[dai->id] = false; } if (dai_data->graph[dai->id]) { - q6apm_graph_close(dai_data->graph[dai->id]); + rc = q6apm_graph_close(dai_data->graph[dai->id]); + if (rc) { + dev_err(dai->dev, + "retaining uncertain APM port (%d)\n", rc); + return; + } dai_data->graph[dai->id] = NULL; } } @@ -193,9 +206,12 @@ static int q6apm_lpass_dai_prepare(struct snd_pcm_substream *substream, struct s int rc; if (dai_data->is_port_started[dai->id]) { - q6apm_graph_stop(dai_data->graph[dai->id]); + rc = q6apm_graph_stop(dai_data->graph[dai->id]); + if (rc < 0) { + dev_err(dai->dev, "failed to stop APM port (%d)\n", rc); + return rc; + } dai_data->is_port_started[dai->id] = false; - } /** @@ -227,8 +243,11 @@ static int q6apm_lpass_dai_prepare(struct snd_pcm_substream *substream, struct s return 0; err: if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { - q6apm_graph_close(dai_data->graph[dai->id]); - dai_data->graph[dai->id] = NULL; + int close_ret; + + close_ret = q6apm_graph_close(dai_data->graph[dai->id]); + if (!close_ret) + dai_data->graph[dai->id] = NULL; } return rc; } diff --git a/sound/soc/qcom/qdsp6/q6apm.c b/sound/soc/qcom/qdsp6/q6apm.c index 641d6d24322990..a6cdd1eec0c342 100644 --- a/sound/soc/qcom/qdsp6/q6apm.c +++ b/sound/soc/qcom/qdsp6/q6apm.c @@ -17,8 +17,12 @@ #include #include "audioreach.h" #include "q6apm.h" +#include "q6dsp-errno.h" /* Graph Management */ +#define APM_MMAP_TOKEN_SEQ_SHIFT 17 +#define APM_MMAP_TOKEN_SEQ_MASK GENMASK(31, 17) + struct apm_graph_mgmt_cmd { struct apm_module_param_data param_data; uint32_t num_sub_graphs; @@ -28,32 +32,180 @@ struct apm_graph_mgmt_cmd { #define APM_GRAPH_MGMT_PSIZE(p, n) ALIGN(struct_size(p, sub_graph_id_list, n), 8) static struct q6apm *g_apm; +static DEFINE_MUTEX(g_apm_lock); +static atomic_t q6apm_mmap_token_seq = ATOMIC_INIT(0); + +bool q6apm_graph_user_get(struct q6apm_graph *graph) +{ + unsigned long flags; + bool acquired = false; + + if (!graph) + return false; + + spin_lock_irqsave(&graph->lifecycle_lock, flags); + if (!graph->dying && !graph->detached) { + graph->active_users++; + acquired = true; + } + spin_unlock_irqrestore(&graph->lifecycle_lock, flags); + + return acquired; +} + +void q6apm_graph_user_put(struct q6apm_graph *graph) +{ + unsigned long flags; + bool wake = false; + + if (!graph) + return; + + spin_lock_irqsave(&graph->lifecycle_lock, flags); + if (WARN_ON(!graph->active_users)) { + spin_unlock_irqrestore(&graph->lifecycle_lock, flags); + return; + } + wake = --graph->active_users == 0; + spin_unlock_irqrestore(&graph->lifecycle_lock, flags); + + if (wake) + wake_up(&graph->users_wait); +} + +DEFINE_FREE(q6apm_graph_user, struct q6apm_graph *, q6apm_graph_user_put(_T)) + +static void q6apm_graph_abort_cmd(struct q6apm_graph *graph) +{ + spin_lock(&graph->result_lock); + graph->cmd_pending = false; + spin_unlock(&graph->result_lock); + wake_up(&graph->cmd_wait); +} -int q6apm_send_cmd_sync(struct q6apm *apm, const struct gpr_pkt *pkt, +static void q6apm_graph_mark_dying(struct q6apm_graph *graph) +{ + unsigned long flags; + + spin_lock_irqsave(&graph->lifecycle_lock, flags); + graph->dying = true; + spin_unlock_irqrestore(&graph->lifecycle_lock, flags); + q6apm_graph_abort_cmd(graph); +} + +static void q6apm_graph_wait_users(struct q6apm_graph *graph) +{ + wait_event(graph->users_wait, !READ_ONCE(graph->active_users)); +} + +int q6apm_send_cmd_sync(struct q6apm *apm, struct gpr_pkt *pkt, uint32_t rsp_opcode) { + struct gpr_hdr *hdr = &pkt->hdr; gpr_device_t *gdev = apm->gdev; + u32 result_status; + bool aborted; + int ret; + + mutex_lock(&apm->lock); + if (READ_ONCE(apm->removing)) { + ret = -ESHUTDOWN; + goto unlock; + } + if (!hdr->token) { + do { + hdr->token = ++apm->cmd_token; + } while (!hdr->token); + } + spin_lock(&apm->result_lock); + apm->result.opcode = 0; + apm->result.status = 0; + apm->result_token = U32_MAX; + apm->pending_opcode = hdr->opcode; + apm->pending_rsp_opcode = rsp_opcode; + apm->pending_token = hdr->token; + apm->cmd_pending = true; + spin_unlock(&apm->result_lock); + + ret = gpr_send_pkt(gdev, pkt); + if (ret >= 0) + ret = wait_event_timeout(apm->wait, + !READ_ONCE(apm->cmd_pending) || + (((READ_ONCE(apm->result.opcode) == + hdr->opcode) || + (rsp_opcode && + READ_ONCE(apm->result.opcode) == + rsp_opcode)) && + READ_ONCE(apm->result_token) == + hdr->token), 5 * HZ); + + spin_lock(&apm->result_lock); + aborted = !apm->cmd_pending || apm->removing; + if (!ret && apm->result_token == hdr->token && + (apm->result.opcode == hdr->opcode || + (rsp_opcode && apm->result.opcode == rsp_opcode))) + ret = 1; + apm->cmd_pending = false; + result_status = apm->result.status; + spin_unlock(&apm->result_lock); + + if (aborted) { + ret = -ESHUTDOWN; + goto unlock; + } + if (ret < 0) + goto unlock; + if (!ret) { + dev_err(&gdev->dev, "CMD timeout for [%x] opcode\n", + hdr->opcode); + ret = -ETIMEDOUT; + } else if (result_status > 0) { + dev_err(&gdev->dev, "DSP returned error[%x] %x\n", + hdr->opcode, result_status); + ret = result_status == ADSP_EUNSUPPORTED ? + -EOPNOTSUPP : -EINVAL; + } else { + ret = 0; + } + +unlock: + mutex_unlock(&apm->lock); + return ret; +} + +static u32 q6apm_next_mmap_token(u32 graph_id, u32 map_type) +{ + u32 sequence; + + sequence = (u32)atomic_inc_return(&q6apm_mmap_token_seq) << + APM_MMAP_TOKEN_SEQ_SHIFT; - return audioreach_send_cmd_sync(&gdev->dev, gdev, &apm->result, &apm->lock, - NULL, &apm->wait, pkt, rsp_opcode); + return (graph_id & APM_MMAP_TOKEN_GID_MASK) | map_type | + (sequence & APM_MMAP_TOKEN_SEQ_MASK); } -static struct audioreach_graph *q6apm_get_audioreach_graph(struct q6apm *apm, uint32_t graph_id) +static struct audioreach_graph * +q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id) { struct audioreach_graph_info *info; struct audioreach_graph *graph; int id; mutex_lock(&apm->lock); + spin_lock(&apm->graph_lock); graph = idr_find(&apm->graph_idr, graph_id); + if (graph && (graph->initializing || + !kref_get_unless_zero(&graph->refcount))) { + spin_unlock(&apm->graph_lock); + mutex_unlock(&apm->lock); + return ERR_PTR(-EBUSY); + } + spin_unlock(&apm->graph_lock); + info = idr_find(&apm->graph_info_idr, graph_id); mutex_unlock(&apm->lock); - if (graph) { - kref_get(&graph->refcount); + if (graph) return graph; - } - - info = idr_find(&apm->graph_info_idr, graph_id); if (!info) return ERR_PTR(-ENODEV); @@ -65,6 +217,7 @@ static struct audioreach_graph *q6apm_get_audioreach_graph(struct q6apm *apm, ui graph->apm = apm; graph->info = info; graph->id = graph_id; + graph->initializing = true; graph->graph = audioreach_alloc_graph_pkt(apm, info); if (IS_ERR(graph->graph)) { @@ -74,8 +227,12 @@ static struct audioreach_graph *q6apm_get_audioreach_graph(struct q6apm *apm, ui return ERR_CAST(err); } + kref_init(&graph->refcount); mutex_lock(&apm->lock); - id = idr_alloc(&apm->graph_idr, graph, graph_id, graph_id + 1, GFP_KERNEL); + spin_lock(&apm->graph_lock); + id = idr_alloc(&apm->graph_idr, graph, graph_id, graph_id + 1, + GFP_ATOMIC); + spin_unlock(&apm->graph_lock); if (id < 0) { dev_err(apm->dev, "Unable to allocate graph id (%d)\n", graph_id); kfree(graph->graph); @@ -85,11 +242,31 @@ static struct audioreach_graph *q6apm_get_audioreach_graph(struct q6apm *apm, ui } mutex_unlock(&apm->lock); - kref_init(&graph->refcount); + id = q6apm_send_cmd_sync(apm, graph->graph, 0); + if (id == -ETIMEDOUT) { + dev_err(apm->dev, + "retaining graph %u after uncertain GRAPH_OPEN\n", + graph_id); + return ERR_PTR(id); + } + if (id) + goto remove_graph; - q6apm_send_cmd_sync(apm, graph->graph, 0); + spin_lock(&apm->graph_lock); + graph->initializing = false; + spin_unlock(&apm->graph_lock); return graph; + +remove_graph: + mutex_lock(&apm->lock); + spin_lock(&apm->graph_lock); + idr_remove(&apm->graph_idr, graph->id); + spin_unlock(&apm->graph_lock); + mutex_unlock(&apm->lock); + kfree(graph->graph); + kfree(graph); + return ERR_PTR(id); } static int audioreach_graph_mgmt_cmd(struct audioreach_graph *graph, uint32_t opcode) @@ -132,7 +309,9 @@ static void q6apm_put_audioreach_graph(struct kref *ref) audioreach_graph_mgmt_cmd(graph, APM_CMD_GRAPH_CLOSE); mutex_lock(&apm->lock); + spin_lock(&apm->graph_lock); graph = idr_remove(&apm->graph_idr, graph->id); + spin_unlock(&apm->graph_lock); mutex_unlock(&apm->lock); kfree(graph->graph); @@ -154,10 +333,14 @@ static int q6apm_get_apm_state(struct q6apm *apm) bool q6apm_is_adsp_ready(void) { + bool ready = false; + + mutex_lock(&g_apm_lock); if (g_apm) - return q6apm_get_apm_state(g_apm); + ready = q6apm_get_apm_state(g_apm); + mutex_unlock(&g_apm_lock); - return false; + return ready; } EXPORT_SYMBOL_GPL(q6apm_is_adsp_ready); @@ -184,16 +367,28 @@ static struct audioreach_module *__q6apm_find_module_by_mid(struct q6apm *apm, int q6apm_graph_media_format_shmem(struct q6apm_graph *graph, struct audioreach_module_config *cfg) { + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; struct audioreach_module *module; + if (!active) + return -ESHUTDOWN; if (cfg->direction == SNDRV_PCM_STREAM_CAPTURE) { - module = q6apm_find_module_by_mid(graph, MODULE_ID_SH_MEM_PUSH_MODE); + module = __q6apm_find_module_by_mid(graph->apm, + graph->info, + MODULE_ID_SH_MEM_PUSH_MODE); if (!module) - module = q6apm_find_module_by_mid(graph, MODULE_ID_RD_SHARED_MEM_EP); + module = __q6apm_find_module_by_mid(graph->apm, + graph->info, + MODULE_ID_RD_SHARED_MEM_EP); } else { - module = q6apm_find_module_by_mid(graph, MODULE_ID_SH_MEM_PULL_MODE); + module = __q6apm_find_module_by_mid(graph->apm, + graph->info, + MODULE_ID_SH_MEM_PULL_MODE); if (!module) - module = q6apm_find_module_by_mid(graph, MODULE_ID_WR_SHARED_MEM_EP); + module = __q6apm_find_module_by_mid(graph->apm, + graph->info, + MODULE_ID_WR_SHARED_MEM_EP); } if (!module) { @@ -216,9 +411,15 @@ static int __q6apm_map_memory_fixed_region(struct device *dev, unsigned int grap uint32_t buf_sz; void *p; uint32_t pos_mask = is_pos_buf ? APM_MMAP_TOKEN_MAP_TYPE_POS_BUF : 0; + u32 token = q6apm_next_mmap_token(graph_id, pos_mask); struct gpr_pkt *pkt __free(kfree) = audioreach_alloc_apm_cmd_pkt(payload_size, - APM_CMD_SHARED_MEM_MAP_REGIONS, (graph_id | pos_mask)); + APM_CMD_SHARED_MEM_MAP_REGIONS, token); + if (!apm) + return -ENODEV; + guard(mutex)(&apm->client_lock); + if (apm->removing) + return -ESHUTDOWN; if (IS_ERR(pkt)) return PTR_ERR(pkt); @@ -271,10 +472,14 @@ EXPORT_SYMBOL_GPL(q6apm_map_memory_fixed_region); int q6apm_alloc_fragments(struct q6apm_graph *graph, unsigned int dir, phys_addr_t phys, size_t period_sz, unsigned int periods) { + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; struct audioreach_graph_data *data; struct audio_buffer *buf; int cnt; + if (!active) + return -ESHUTDOWN; if (dir == SNDRV_PCM_STREAM_PLAYBACK) data = &graph->rx_data; else @@ -326,8 +531,15 @@ static int __q6apm_unmap_memory_fixed_region(struct device *dev, unsigned int gr struct q6apm *apm = dev_get_drvdata(dev->parent); struct audioreach_graph_info *info; uint32_t mem_map_handle; + u32 map_type = is_pos_buf ? APM_MMAP_TOKEN_MAP_TYPE_POS_BUF : 0; + u32 token = q6apm_next_mmap_token(graph_id, map_type); struct gpr_pkt *pkt __free(kfree) = audioreach_alloc_apm_cmd_pkt(sizeof(*cmd), - APM_CMD_SHARED_MEM_UNMAP_REGIONS, graph_id); + APM_CMD_SHARED_MEM_UNMAP_REGIONS, token); + if (!apm) + return -ENODEV; + guard(mutex)(&apm->client_lock); + if (apm->removing) + return -ESHUTDOWN; if (IS_ERR(pkt)) return PTR_ERR(pkt); @@ -366,6 +578,11 @@ EXPORT_SYMBOL_GPL(q6apm_unmap_pos_buffer); int q6apm_free_fragments(struct q6apm_graph *graph, unsigned int dir) { + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + + if (!active) + return -ESHUTDOWN; audioreach_graph_free_buf(graph); return 0; @@ -374,9 +591,14 @@ EXPORT_SYMBOL_GPL(q6apm_free_fragments); int q6apm_remove_initial_silence(struct device *dev, struct q6apm_graph *graph, uint32_t samples) { + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; struct audioreach_module *module; - module = q6apm_find_module_by_mid(graph, MODULE_ID_PLACEHOLDER_DECODER); + if (!active) + return -ESHUTDOWN; + module = __q6apm_find_module_by_mid(graph->apm, graph->info, + MODULE_ID_PLACEHOLDER_DECODER); if (!module) return -ENODEV; @@ -386,9 +608,14 @@ EXPORT_SYMBOL_GPL(q6apm_remove_initial_silence); int q6apm_remove_trailing_silence(struct device *dev, struct q6apm_graph *graph, uint32_t samples) { + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; struct audioreach_module *module; - module = q6apm_find_module_by_mid(graph, MODULE_ID_PLACEHOLDER_DECODER); + if (!active) + return -ESHUTDOWN; + module = __q6apm_find_module_by_mid(graph->apm, graph->info, + MODULE_ID_PLACEHOLDER_DECODER); if (!module) return -ENODEV; @@ -398,9 +625,14 @@ EXPORT_SYMBOL_GPL(q6apm_remove_trailing_silence); int q6apm_enable_compress_module(struct device *dev, struct q6apm_graph *graph, bool en) { + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; struct audioreach_module *module; - module = q6apm_find_module_by_mid(graph, MODULE_ID_PLACEHOLDER_DECODER); + if (!active) + return -ESHUTDOWN; + module = __q6apm_find_module_by_mid(graph->apm, graph->info, + MODULE_ID_PLACEHOLDER_DECODER); if (!module) return -ENODEV; @@ -411,10 +643,15 @@ EXPORT_SYMBOL_GPL(q6apm_enable_compress_module); int q6apm_set_real_module_id(struct device *dev, struct q6apm_graph *graph, uint32_t codec_id) { + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; struct audioreach_module *module; uint32_t module_id; - module = q6apm_find_module_by_mid(graph, MODULE_ID_PLACEHOLDER_DECODER); + if (!active) + return -ESHUTDOWN; + module = __q6apm_find_module_by_mid(graph->apm, graph->info, + MODULE_ID_PLACEHOLDER_DECODER); if (!module) return -ENODEV; @@ -442,12 +679,18 @@ EXPORT_SYMBOL_GPL(q6apm_set_real_module_id); int q6apm_graph_media_format_pcm(struct q6apm_graph *graph, struct audioreach_module_config *cfg) { - struct audioreach_graph_info *info = graph->info; + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + struct audioreach_graph_info *info; struct audioreach_sub_graph *sgs; struct audioreach_container *container; struct audioreach_module *module; int ret; + if (!active) + return -ESHUTDOWN; + info = graph->info; + list_for_each_entry(sgs, &info->sg_list, node) { list_for_each_entry(container, &sgs->container_list, node) { list_for_each_entry(module, &container->modules_list, node) { @@ -472,11 +715,18 @@ EXPORT_SYMBOL_GPL(q6apm_graph_media_format_pcm); int q6apm_write_async(struct q6apm_graph *graph, uint32_t len, uint32_t msw_ts, uint32_t lsw_ts, uint32_t wflags) { + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; struct apm_data_cmd_wr_sh_mem_ep_data_buffer_v2 *write_buffer; struct audio_buffer *ab; + int ret; - struct gpr_pkt *pkt __free(kfree) = audioreach_alloc_pkt(sizeof(*write_buffer), - DATA_CMD_WR_SH_MEM_EP_DATA_BUFFER_V2, + struct gpr_pkt *pkt __free(kfree) = NULL; + + if (!active) + return -ESHUTDOWN; + pkt = audioreach_alloc_pkt(sizeof(*write_buffer), + DATA_CMD_WR_SH_MEM_EP_DATA_BUFFER_V2, graph->rx_data.dsp_buf | (len << APM_WRITE_TOKEN_LEN_SHIFT), graph->port->id, graph->shm_iid); if (IS_ERR(pkt)) @@ -502,18 +752,26 @@ int q6apm_write_async(struct q6apm_graph *graph, uint32_t len, uint32_t msw_ts, mutex_unlock(&graph->lock); - return gpr_send_port_pkt(graph->port, pkt); + ret = gpr_send_port_pkt(graph->port, pkt); + return ret; } EXPORT_SYMBOL_GPL(q6apm_write_async); int q6apm_read(struct q6apm_graph *graph) { + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; struct data_cmd_rd_sh_mem_ep_data_buffer_v2 *read_buffer; struct audioreach_graph_data *port; struct audio_buffer *ab; + int ret; - struct gpr_pkt *pkt __free(kfree) = audioreach_alloc_pkt(sizeof(*read_buffer), - DATA_CMD_RD_SH_MEM_EP_DATA_BUFFER_V2, + struct gpr_pkt *pkt __free(kfree) = NULL; + + if (!active) + return -ESHUTDOWN; + pkt = audioreach_alloc_pkt(sizeof(*read_buffer), + DATA_CMD_RD_SH_MEM_EP_DATA_BUFFER_V2, graph->tx_data.dsp_buf, graph->port->id, graph->shm_iid); if (IS_ERR(pkt)) return PTR_ERR(pkt); @@ -536,14 +794,19 @@ int q6apm_read(struct q6apm_graph *graph) mutex_unlock(&graph->lock); - return gpr_send_port_pkt(graph->port, pkt); + ret = gpr_send_port_pkt(graph->port, pkt); + return ret; } EXPORT_SYMBOL_GPL(q6apm_read); int q6apm_get_hw_pointer(struct q6apm_graph *graph, int dir) { + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; struct audioreach_graph_data *data; + if (!active) + return 0; if (dir == SNDRV_PCM_STREAM_PLAYBACK) data = &graph->rx_data; else @@ -553,6 +816,32 @@ int q6apm_get_hw_pointer(struct q6apm_graph *graph, int dir) } EXPORT_SYMBOL_GPL(q6apm_get_hw_pointer); +static bool q6apm_graph_try_complete_cmd(struct q6apm_graph *graph, + const struct gpr_hdr *hdr, u32 opcode, + u32 status, bool basic_response) +{ + bool expected; + + spin_lock(&graph->result_lock); + expected = graph->cmd_pending && graph->pending_token == hdr->token && + (graph->pending_opcode == opcode || + graph->pending_rsp_opcode == opcode); + if (expected && basic_response && !status && + graph->pending_rsp_opcode && graph->pending_rsp_opcode != opcode) + expected = false; + if (expected) { + graph->result.status = status; + graph->result_token = hdr->token; + graph->result.opcode = opcode; + } + spin_unlock(&graph->result_lock); + + if (expected) + wake_up(&graph->cmd_wait); + + return expected; +} + static int graph_callback(const struct gpr_resp_pkt *data, void *priv, int op) { struct data_cmd_rsp_rd_sh_mem_ep_data_buffer_done_v2 *rd_done; @@ -561,11 +850,16 @@ static int graph_callback(const struct gpr_resp_pkt *data, void *priv, int op) const struct gpr_ibasic_rsp_result_t *result; struct q6apm_graph *graph = priv; const struct gpr_hdr *hdr = &data->hdr; - struct device *dev = graph->dev; + struct device *dev; uint32_t client_event; + bool expected; phys_addr_t phys; int token; + if (!q6apm_graph_user_get(graph)) + return 0; + + dev = graph->dev; result = data->payload; switch (hdr->opcode) { @@ -597,8 +891,6 @@ static int graph_callback(const struct gpr_resp_pkt *data, void *priv, int op) atomic_set(&graph->rx_data.hw_ptr, token + 1); if (lower_32_bits(phys) == done->buf_addr_lsw && upper_32_bits(phys) == done->buf_addr_msw) { - graph->result.opcode = hdr->opcode; - graph->result.status = done->status; if (graph->cb) graph->cb(client_event, hdr->token, data->payload, graph->priv); } else { @@ -624,8 +916,6 @@ static int graph_callback(const struct gpr_resp_pkt *data, void *priv, int op) if (upper_32_bits(phys) == rd_done->buf_addr_msw && lower_32_bits(phys) == rd_done->buf_addr_lsw) { - graph->result.opcode = hdr->opcode; - graph->result.status = rd_done->status; if (graph->cb) graph->cb(client_event, hdr->token, data->payload, graph->priv); } else { @@ -644,12 +934,15 @@ static int graph_callback(const struct gpr_resp_pkt *data, void *priv, int op) case DATA_CMD_WR_SH_MEM_EP_MEDIA_FORMAT: case APM_CMD_REGISTER_MODULE_EVENTS: case APM_CMD_SET_CFG: - graph->result.opcode = result->opcode; - graph->result.status = result->status; - if (result->status) + case APM_CMD_GRAPH_START: + case APM_CMD_GRAPH_STOP: + case APM_CMD_GRAPH_FLUSH: + expected = q6apm_graph_try_complete_cmd(graph, hdr, + result->opcode, + result->status, true); + if (expected && result->status) dev_err(dev, "Error (%d) Processing 0x%08x cmd\n", result->status, result->opcode); - wake_up(&graph->cmd_wait); break; default: break; @@ -658,12 +951,18 @@ static int graph_callback(const struct gpr_resp_pkt *data, void *priv, int op) default: break; } + q6apm_graph_user_put(graph); return 0; } int q6apm_register_watermark_event(struct q6apm_graph *graph, int water_mark_level_bytes, int num_levels) { + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + + if (!active) + return -ESHUTDOWN; return audioreach_shmem_register_event(graph, water_mark_level_bytes, num_levels); } EXPORT_SYMBOL_GPL(q6apm_register_watermark_event); @@ -671,7 +970,13 @@ EXPORT_SYMBOL_GPL(q6apm_register_watermark_event); int q6apm_push_pull_config(struct q6apm_graph *graph, phys_addr_t bphys, phys_addr_t pphys, uint32_t size) { - struct audioreach_graph_info *info = graph->info; + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + struct audioreach_graph_info *info; + + if (!active) + return -ESHUTDOWN; + info = graph->info; return audioreach_setup_push_pull(graph, bphys, pphys, info->mem_map_handle, info->pos_buf_mem_map_handle, size); @@ -684,6 +989,11 @@ bool q6apm_is_graph_in_push_pull_mode_from_id(struct device *dev, unsigned int g struct q6apm *apm = dev_get_drvdata(dev->parent); struct audioreach_module *module; + if (!apm) + return false; + guard(mutex)(&apm->client_lock); + if (apm->removing) + return false; info = idr_find(&apm->graph_info_idr, graph_id); if (!info) return false; @@ -700,7 +1010,7 @@ EXPORT_SYMBOL_GPL(q6apm_is_graph_in_push_pull_mode_from_id); bool q6apm_is_graph_in_push_pull_mode(struct q6apm_graph *graph) { - return graph->info->is_push_pull_mode; + return graph && READ_ONCE(graph->is_push_pull_mode); } EXPORT_SYMBOL_GPL(q6apm_is_graph_in_push_pull_mode); @@ -723,10 +1033,20 @@ struct q6apm_graph *q6apm_graph_open(struct device *dev, q6apm_cb cb, struct q6apm_graph *graph; int ret, iid = 0; + if (!apm) + return ERR_PTR(-ENODEV); + + mutex_lock(&apm->client_lock); + if (apm->removing) { + ret = -ESHUTDOWN; + goto unlock_clients; + } + ar_graph = q6apm_get_audioreach_graph(apm, graph_id); if (IS_ERR(ar_graph)) { dev_err(dev, "No graph found with id %d\n", graph_id); - return ERR_CAST(ar_graph); + ret = PTR_ERR(ar_graph); + goto unlock_clients; } graph = kzalloc_obj(*graph); @@ -741,7 +1061,10 @@ struct q6apm_graph *q6apm_graph_open(struct device *dev, q6apm_cb cb, graph->info = ar_graph->info; graph->ar_graph = ar_graph; graph->id = ar_graph->id; - graph->dev = dev; + graph->dev = get_device(dev); + spin_lock_init(&graph->lifecycle_lock); + init_waitqueue_head(&graph->users_wait); + init_completion(&graph->detached_done); if (dir == SNDRV_PCM_STREAM_PLAYBACK) { iid = q6apm_graph_get_module_iid(graph, MODULE_ID_SH_MEM_PULL_MODE); @@ -757,11 +1080,14 @@ struct q6apm_graph *q6apm_graph_open(struct device *dev, q6apm_cb cb, else graph->info->is_push_pull_mode = true; } + graph->is_push_pull_mode = graph->info->is_push_pull_mode; if (iid > 0) graph->shm_iid = iid; mutex_init(&graph->lock); + mutex_init(&graph->cmd_lock); + spin_lock_init(&graph->result_lock); init_waitqueue_head(&graph->cmd_wait); graph->port = gpr_alloc_port(apm->gdev, dev, graph_callback, graph); @@ -769,24 +1095,70 @@ struct q6apm_graph *q6apm_graph_open(struct device *dev, q6apm_cb cb, ret = PTR_ERR(graph->port); goto free_graph; } + list_add_tail(&graph->node, &apm->graph_client_list); + mutex_unlock(&apm->client_lock); return graph; free_graph: + put_device(graph->dev); kfree(graph); put_ar_graph: kref_put(&ar_graph->refcount, q6apm_put_audioreach_graph); + mutex_unlock(&apm->client_lock); + return ERR_PTR(ret); + +unlock_clients: + mutex_unlock(&apm->client_lock); return ERR_PTR(ret); } EXPORT_SYMBOL_GPL(q6apm_graph_open); int q6apm_graph_close(struct q6apm_graph *graph) { - struct audioreach_graph *ar_graph = graph->ar_graph; + struct audioreach_graph *ar_graph; + struct q6apm *apm; + unsigned long flags; + bool detached; + bool dying; + +retry: + spin_lock_irqsave(&graph->lifecycle_lock, flags); + detached = graph->detached; + dying = graph->dying; + apm = graph->apm; + spin_unlock_irqrestore(&graph->lifecycle_lock, flags); + if (detached) { + put_device(graph->dev); + kfree(graph); + return 0; + } + if (dying) { + wait_for_completion(&graph->detached_done); + goto retry; + } + if (!apm) + return -ESHUTDOWN; + + mutex_lock(&apm->client_lock); + spin_lock_irqsave(&graph->lifecycle_lock, flags); + if (graph->dying || graph->detached) { + spin_unlock_irqrestore(&graph->lifecycle_lock, flags); + mutex_unlock(&apm->client_lock); + goto retry; + } + spin_unlock_irqrestore(&graph->lifecycle_lock, flags); + ar_graph = graph->ar_graph; + q6apm_graph_mark_dying(graph); + q6apm_graph_wait_users(graph); + list_del_init(&graph->node); + gpr_free_port(graph->port); + graph->port = NULL; graph->ar_graph = NULL; kref_put(&ar_graph->refcount, q6apm_put_audioreach_graph); - gpr_free_port(graph->port); + mutex_unlock(&apm->client_lock); + put_device(graph->dev); kfree(graph); return 0; @@ -795,19 +1167,30 @@ EXPORT_SYMBOL_GPL(q6apm_graph_close); int q6apm_graph_prepare(struct q6apm_graph *graph) { + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + + if (!active) + return -ESHUTDOWN; return audioreach_graph_mgmt_cmd(graph->ar_graph, APM_CMD_GRAPH_PREPARE); } EXPORT_SYMBOL_GPL(q6apm_graph_prepare); int q6apm_graph_start(struct q6apm_graph *graph) { - struct audioreach_graph *ar_graph = graph->ar_graph; + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + struct audioreach_graph *ar_graph; int ret = 0; + if (!active) + return -ESHUTDOWN; + ar_graph = graph->ar_graph; if (ar_graph->start_count == 0) ret = audioreach_graph_mgmt_cmd(ar_graph, APM_CMD_GRAPH_START); - ar_graph->start_count++; + if (!ret) + ar_graph->start_count++; return ret; } @@ -815,17 +1198,34 @@ EXPORT_SYMBOL_GPL(q6apm_graph_start); int q6apm_graph_stop(struct q6apm_graph *graph) { - struct audioreach_graph *ar_graph = graph->ar_graph; + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + struct audioreach_graph *ar_graph; + int ret; + if (!active) + return -ESHUTDOWN; + ar_graph = graph->ar_graph; + if (ar_graph->start_count <= 0) + return 0; if (--ar_graph->start_count > 0) return 0; - return audioreach_graph_mgmt_cmd(ar_graph, APM_CMD_GRAPH_STOP); + ret = audioreach_graph_mgmt_cmd(ar_graph, APM_CMD_GRAPH_STOP); + if (ret) + ar_graph->start_count++; + + return ret; } EXPORT_SYMBOL_GPL(q6apm_graph_stop); int q6apm_graph_flush(struct q6apm_graph *graph) { + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + + if (!active) + return -ESHUTDOWN; return audioreach_graph_mgmt_cmd(graph->ar_graph, APM_CMD_GRAPH_FLUSH); } EXPORT_SYMBOL_GPL(q6apm_graph_flush); @@ -863,11 +1263,15 @@ static int apm_probe(gpr_device_t *gdev) dev_set_drvdata(dev, apm); mutex_init(&apm->lock); + mutex_init(&apm->client_lock); + spin_lock_init(&apm->result_lock); + spin_lock_init(&apm->graph_lock); apm->dev = dev; apm->gdev = gdev; init_waitqueue_head(&apm->wait); INIT_LIST_HEAD(&apm->widget_list); + INIT_LIST_HEAD(&apm->graph_client_list); idr_init(&apm->graph_idr); idr_init(&apm->graph_info_idr); idr_init(&apm->sub_graphs_idr); @@ -875,7 +1279,9 @@ static int apm_probe(gpr_device_t *gdev) idr_init(&apm->modules_idr); + mutex_lock(&g_apm_lock); g_apm = apm; + mutex_unlock(&g_apm_lock); q6apm_get_apm_state(apm); @@ -892,45 +1298,166 @@ static int apm_probe(gpr_device_t *gdev) return ret; } +static void q6apm_abort_global_cmd(struct q6apm *apm) +{ + spin_lock(&apm->result_lock); + apm->cmd_pending = false; + spin_unlock(&apm->result_lock); + wake_up(&apm->wait); +} + +static void q6apm_detach_graph_clients(struct q6apm *apm) +{ + struct q6apm_graph *graph, *next; + LIST_HEAD(detaching); + + mutex_lock(&apm->client_lock); + apm->removing = true; + list_for_each_entry(graph, &apm->graph_client_list, node) + q6apm_graph_mark_dying(graph); + list_splice_init(&apm->graph_client_list, &detaching); + mutex_unlock(&apm->client_lock); + q6apm_abort_global_cmd(apm); + /* Drain the command thread awakened by the abort before teardown. */ + mutex_lock(&apm->lock); + mutex_unlock(&apm->lock); + + list_for_each_entry_safe(graph, next, &detaching, node) { + unsigned long flags; + + list_del_init(&graph->node); + q6apm_graph_wait_users(graph); + gpr_free_port(graph->port); + + spin_lock_irqsave(&graph->lifecycle_lock, flags); + graph->port = NULL; + graph->ar_graph = NULL; + graph->info = NULL; + graph->apm = NULL; + graph->detached = true; + spin_unlock_irqrestore(&graph->lifecycle_lock, flags); + complete_all(&graph->detached_done); + /* The ALSA owner releases the detached client on its close path. */ + } +} + +static void q6apm_remove_graph_state(struct q6apm *apm) +{ + struct audioreach_graph *ar_graph; + int id = 0; + + guard(mutex)(&apm->client_lock); + while ((ar_graph = idr_get_next(&apm->graph_idr, &id))) { + idr_remove(&apm->graph_idr, id); + kfree(ar_graph->graph); + kfree(ar_graph); + id++; + } +} + static void apm_remove(gpr_device_t *gdev) { + struct q6apm *apm = dev_get_drvdata(&gdev->dev); + + mutex_lock(&g_apm_lock); + if (g_apm == apm) + g_apm = NULL; + mutex_unlock(&g_apm_lock); + q6apm_detach_graph_clients(apm); of_platform_depopulate(&gdev->dev); snd_soc_unregister_component(&gdev->dev); + q6apm_remove_graph_state(apm); } struct audioreach_module *q6apm_find_module_by_mid(struct q6apm_graph *graph, uint32_t mid) { - struct audioreach_graph_info *info = graph->info; - struct q6apm *apm = graph->apm; + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + + if (!active) + return NULL; + + return __q6apm_find_module_by_mid(graph->apm, graph->info, mid); +} + +static bool __q6apm_cmd_response_expected(struct q6apm *apm, + const struct gpr_hdr *hdr, + u32 opcode, u32 status, + bool basic_response) +{ + bool expected; + + expected = apm->cmd_pending && apm->pending_token == hdr->token && + (apm->pending_opcode == opcode || + apm->pending_rsp_opcode == opcode); + if (expected && basic_response && !status && + apm->pending_rsp_opcode && apm->pending_rsp_opcode != opcode) + expected = false; + return expected; +} + +static bool q6apm_try_complete_cmd(struct q6apm *apm, + const struct gpr_hdr *hdr, u32 opcode, + u32 status, bool basic_response) +{ + bool expected; + + spin_lock(&apm->result_lock); + expected = __q6apm_cmd_response_expected(apm, hdr, opcode, status, + basic_response); + if (expected) { + apm->result.status = status; + apm->result_token = hdr->token; + apm->result.opcode = opcode; + } + spin_unlock(&apm->result_lock); - return __q6apm_find_module_by_mid(apm, info, mid); + if (expected) + wake_up(&apm->wait); + return expected; } static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) { gpr_device_t *gdev = priv; - struct audioreach_graph_info *info; + struct audioreach_graph_info *info = NULL; struct q6apm *apm = dev_get_drvdata(&gdev->dev); struct apm_cmd_rsp_shared_mem_map_regions *rsp; struct device *dev = &gdev->dev; struct gpr_ibasic_rsp_result_t *result; const struct gpr_hdr *hdr = &data->hdr; + bool expected; int graph_id, is_pos_buf; result = data->payload; switch (hdr->opcode) { case APM_CMD_RSP_GET_SPF_STATE: - apm->result.opcode = hdr->opcode; - apm->result.status = 0; - /* First word of result it state */ - apm->state = result->opcode; - wake_up(&apm->wait); + /* First word of result is the state. */ + spin_lock(&apm->result_lock); + expected = __q6apm_cmd_response_expected(apm, hdr, hdr->opcode, + 0, false); + if (expected) { + apm->state = result->opcode; + apm->result.status = 0; + apm->result_token = hdr->token; + apm->result.opcode = hdr->opcode; + } + spin_unlock(&apm->result_lock); + if (expected) + wake_up(&apm->wait); break; case GPR_BASIC_RSP_RESULT: switch (result->opcode) { case APM_CMD_SHARED_MEM_MAP_REGIONS: + expected = q6apm_try_complete_cmd(apm, hdr, + result->opcode, + result->status, true); + if (expected && result->status) + dev_err(dev, "Error (%d) Processing 0x%08x cmd\n", + result->status, result->opcode); + break; case APM_CMD_GRAPH_START: case APM_CMD_GRAPH_OPEN: case APM_CMD_GRAPH_PREPARE: @@ -938,50 +1465,85 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) case APM_CMD_GRAPH_FLUSH: case APM_CMD_GRAPH_STOP: case APM_CMD_SET_CFG: - apm->result.opcode = result->opcode; - apm->result.status = result->status; - if (result->status) + expected = q6apm_try_complete_cmd(apm, hdr, + result->opcode, + result->status, true); + if (expected && result->status) dev_err(dev, "Error (%d) Processing 0x%08x cmd\n", result->status, result->opcode); - wake_up(&apm->wait); break; case APM_CMD_SHARED_MEM_UNMAP_REGIONS: - apm->result.opcode = hdr->opcode; - apm->result.status = 0; - rsp = data->payload; - - info = idr_find(&apm->graph_info_idr, hdr->token); - if (info) - info->mem_map_handle = 0; - else - dev_err(dev, "Error (%d) Processing 0x%08x cmd\n", result->status, - result->opcode); + if (result->status) { + expected = q6apm_try_complete_cmd(apm, hdr, + result->opcode, + result->status, + true); + if (expected) + dev_err(dev, + "Error (%d) Processing 0x%08x cmd\n", + result->status, result->opcode); + break; + } - wake_up(&apm->wait); + graph_id = hdr->token & APM_MMAP_TOKEN_GID_MASK; + is_pos_buf = + hdr->token & APM_MMAP_TOKEN_MAP_TYPE_POS_BUF; + spin_lock(&apm->result_lock); + expected = __q6apm_cmd_response_expected(apm, hdr, + result->opcode, + 0, true); + if (expected) { + info = idr_find(&apm->graph_info_idr, graph_id); + if (info && is_pos_buf) + info->pos_buf_mem_map_handle = 0; + else if (info) + info->mem_map_handle = 0; + apm->result.status = 0; + apm->result_token = hdr->token; + apm->result.opcode = result->opcode; + } + spin_unlock(&apm->result_lock); + if (expected) { + wake_up(&apm->wait); + if (!info) + dev_err(dev, + "no mapping for 0x%08x response token 0x%08x\n", + result->opcode, hdr->token); + } break; default: break; } break; case APM_CMD_RSP_SHARED_MEM_MAP_REGIONS: - apm->result.opcode = hdr->opcode; - apm->result.status = 0; rsp = data->payload; graph_id = hdr->token & APM_MMAP_TOKEN_GID_MASK; is_pos_buf = hdr->token & APM_MMAP_TOKEN_MAP_TYPE_POS_BUF; - info = idr_find(&apm->graph_info_idr, graph_id); + spin_lock(&apm->result_lock); + expected = __q6apm_cmd_response_expected(apm, hdr, hdr->opcode, + 0, false); + if (expected) + info = idr_find(&apm->graph_info_idr, graph_id); if (info) { if (is_pos_buf) info->pos_buf_mem_map_handle = rsp->mem_map_handle; else info->mem_map_handle = rsp->mem_map_handle; - } else { - dev_err(dev, "Error (%d) Processing 0x%08x cmd\n", result->status, - result->opcode); } - wake_up(&apm->wait); + if (expected) { + apm->result.status = 0; + apm->result_token = hdr->token; + apm->result.opcode = hdr->opcode; + } + spin_unlock(&apm->result_lock); + if (expected) + wake_up(&apm->wait); + if (expected && !info) + dev_err(dev, + "no graph for 0x%08x response token 0x%08x\n", + hdr->opcode, hdr->token); break; default: break; diff --git a/sound/soc/qcom/qdsp6/q6apm.h b/sound/soc/qcom/qdsp6/q6apm.h index 5cb51ca491dc4f..f586acd563e1d7 100644 --- a/sound/soc/qcom/qdsp6/q6apm.h +++ b/sound/soc/qcom/qdsp6/q6apm.h @@ -3,6 +3,7 @@ #define __Q6APM_H__ #include #include +#include #include #include #include @@ -57,12 +58,26 @@ struct q6apm { /* For Graph OPEN/START/STOP/CLOSE operations */ wait_queue_head_t wait; struct gpr_ibasic_rsp_result_t result; + u32 result_token; + u32 pending_opcode; + u32 pending_rsp_opcode; + u32 pending_token; + u32 cmd_token; + bool cmd_pending; struct mutex cmd_lock; struct mutex lock; + /* Serialize graph-client publication against service removal. */ + struct mutex client_lock; + /* Protect command matching from asynchronous GPR responses. */ + spinlock_t result_lock; + /* Protect graph lifetime during asynchronous GPR responses. */ + spinlock_t graph_lock; uint32_t state; struct list_head widget_list; + struct list_head graph_client_list; + bool removing; struct idr graph_idr; struct idr graph_info_idr; struct idr sub_graphs_idr; @@ -87,6 +102,7 @@ struct audioreach_graph { uint32_t id; int state; int start_count; + bool initializing; /* Cached Graph data */ void *graph; struct kref refcount; @@ -102,12 +118,31 @@ struct q6apm_graph { uint32_t shm_iid; struct device *dev; struct q6apm *apm; + struct list_head node; + bool is_push_pull_mode; + bool dying; + bool detached; + unsigned int active_users; + /* Protect admission and detach state for graph API users. */ + spinlock_t lifecycle_lock; + wait_queue_head_t users_wait; + struct completion detached_done; gpr_port_t *port; struct audioreach_graph_data rx_data; struct audioreach_graph_data tx_data; struct gpr_ibasic_rsp_result_t result; + u32 result_token; + u32 pending_opcode; + u32 pending_rsp_opcode; + u32 pending_token; + u32 cmd_token; + bool cmd_pending; wait_queue_head_t cmd_wait; + /* Serialize synchronous commands without blocking data callbacks. */ + struct mutex cmd_lock; struct mutex lock; + /* Protect command matching from asynchronous client responses. */ + spinlock_t result_lock; struct audioreach_graph *ar_graph; struct audioreach_graph_info *info; }; @@ -115,6 +150,8 @@ struct q6apm_graph { /* Graph Operations */ struct q6apm_graph *q6apm_graph_open(struct device *dev, q6apm_cb cb, void *priv, int graph_id, int dir); +bool q6apm_graph_user_get(struct q6apm_graph *graph); +void q6apm_graph_user_put(struct q6apm_graph *graph); int q6apm_graph_close(struct q6apm_graph *graph); int q6apm_graph_prepare(struct q6apm_graph *graph); int q6apm_graph_start(struct q6apm_graph *graph); @@ -147,7 +184,7 @@ int q6apm_alloc_fragments(struct q6apm_graph *graph, int q6apm_free_fragments(struct q6apm_graph *graph, unsigned int dir); int q6apm_unmap_memory_fixed_region(struct device *dev, unsigned int graph_id); /* Helpers */ -int q6apm_send_cmd_sync(struct q6apm *apm, const struct gpr_pkt *pkt, +int q6apm_send_cmd_sync(struct q6apm *apm, struct gpr_pkt *pkt, uint32_t rsp_opcode); /* Callback for graph specific */ From 3e1320986c6021270e1a1f35d48f3d7df5543944 Mon Sep 17 00:00:00 2001 From: geoca <272055834+geocausa@users.noreply.github.com> Date: Sun, 23 Aug 2026 08:07:44 +0100 Subject: [PATCH 11/20] ASoC: q6dsp: parse FullIO topology extensions FullIO v19c topologies add raw module stage classes and data, graph control links, extended container placement, and integrated-backend metadata. The 7.2 parser did not retain or emit those fields. Parse the extended tokens and build matching graph-open payloads without hard-coded diagnostic or volume instance IDs. Use one checked iterator for vendor arrays and known raw records, reject truncated, reordered, duplicate, oversized, or overflowing input before packet allocation, and preserve the historical no-op for unsupported legacy module extensions. [Leon: ported the format to 7.2, preserved legacy-topology no-op behavior, and hardened raw-block bounds, ordering, duplicates, ownership, and aggregate allocation arithmetic.] Link: https://github.com/geocausa/SP11X1e-audio/commit/31466d62b38c8138d476b2fd3606a76272fb3d34 Link: https://github.com/geocausa/SP11X1e-audio/commit/7af8f21e9966f6adb40c102653b6acb5d81742 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/4bcfa10f65f2ea99ca66a42b5e523c351b45ced9 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/22bdec204438f5e1e19286cb7f1688f196c51967 Signed-off-by: Leon Silcott --- include/uapi/sound/snd_ar_tokens.h | 30 ++ sound/soc/qcom/qdsp6/audioreach.c | 218 +++++++++++--- sound/soc/qcom/qdsp6/audioreach.h | 18 ++ sound/soc/qcom/qdsp6/topology.c | 444 +++++++++++++++++++++-------- 4 files changed, 560 insertions(+), 150 deletions(-) diff --git a/include/uapi/sound/snd_ar_tokens.h b/include/uapi/sound/snd_ar_tokens.h index 6b8102eaa121f1..748733c77c4f9c 100644 --- a/include/uapi/sound/snd_ar_tokens.h +++ b/include/uapi/sound/snd_ar_tokens.h @@ -96,6 +96,10 @@ enum ar_event_types { * APM_PROC_DOMAIN_ID_SDSP = 4, * APM_PROC_DOMAIN_ID_CDSP = 5 * + * %AR_TKN_U32_CONTAINER_PARENT_ID: Parent container instance ID. + * + * %AR_TKN_U32_CONTAINER_HEAP_ID: Container heap ID. + * * %AR_TKN_U32_MODULE_ID: Module ID * * %AR_TKN_U32_MODULE_INSTANCE_ID: Module Instance ID. @@ -112,6 +116,14 @@ enum ar_event_types { * * %AR_TKN_U32_MODULE_DST_IN_PORT_ID: Destination module input port ID * + * %AR_TKN_U32_MODULE_SPEAKER_PROTECTION_BYPASS: + * Leave speaker-protection modules + * in their default disabled state. + * + * %AR_TKN_U32_MODULE_INTEGRATED_BACKEND_ID: + * Backend DAI ID represented inside + * the same graph as the frontend. + * * %AR_TKN_U32_MODULE_HW_IF_IDX: Interface index types for I2S/LPAIF * * %AR_TKN_U32_MODULE_HW_IF_TYPE: Interface type @@ -186,6 +198,8 @@ enum ar_event_types { #define AR_TKN_U32_CONTAINER_STACK_SIZE 102 #define AR_TKN_U32_CONTAINER_GRAPH_POS 103 #define AR_TKN_U32_CONTAINER_PROC_DOMAIN 104 +#define AR_TKN_U32_CONTAINER_PARENT_ID 105 +#define AR_TKN_U32_CONTAINER_HEAP_ID 106 /* Module Tokens */ #define AR_TKN_U32_MODULE_ID 200 @@ -239,8 +253,24 @@ enum ar_event_types { #define AR_TKN_U32_MODULE_LOG_CODE 259 #define AR_TKN_U32_MODULE_LOG_TAP_POINT_ID 260 #define AR_TKN_U32_MODULE_LOG_MODE 261 +#define AR_TKN_U32_MODULE_SPEAKER_PROTECTION_BYPASS 262 +#define AR_TKN_U32_MODULE_INTEGRATED_BACKEND_ID 263 #define SND_SOC_AR_TPLG_MODULE_CFG_TYPE 0x01001006 +#define SND_SOC_AR_TPLG_CTRL_LINK_CFG_TYPE 0x08001061 + +/* Ordered private-data classes emitted by the protected FullIO topology. */ +#define SND_SOC_AR_TPLG_GRAPH_CAL_CFG_TYPE 0x53503101 +#define SND_SOC_AR_TPLG_RENDER_EP_CFG_TYPE 0x53503102 +#define SND_SOC_AR_TPLG_SP_TAG_CFG_TYPE 0x53503103 +#define SND_SOC_AR_TPLG_SPVI_TAG_CFG_TYPE 0x53503104 +#define SND_SOC_AR_TPLG_VI_EP_CFG_TYPE 0x53503105 +#define SND_SOC_AR_TPLG_PROTECTION_DYNAMIC_CFG_TYPE 0x53503106 +#define SND_SOC_AR_TPLG_VOLUME_GAIN_CFG_TYPE 0x53503107 +#define SND_SOC_AR_TPLG_VOLUME_FILTER_CFG_TYPE 0x53503108 +#define SND_SOC_AR_TPLG_VOLUME_MUTE_CFG_TYPE 0x53503109 +#define SND_SOC_AR_TPLG_CHANNEL_MIXER_CFG_TYPE 0x5350310a + struct audioreach_module_priv_data { __le32 size; /* size in bytes of the array, including all elements */ __le32 type; /* SND_SOC_AR_TPLG_MODULE_CFG_TYPE */ diff --git a/sound/soc/qcom/qdsp6/audioreach.c b/sound/soc/qcom/qdsp6/audioreach.c index d212937ce0398d..d59d60b4cbdc7f 100644 --- a/sound/soc/qcom/qdsp6/audioreach.c +++ b/sound/soc/qcom/qdsp6/audioreach.c @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -59,14 +60,20 @@ struct apm_container_obj { struct apm_cont_prop_id_domain domain; } __packed; +struct apm_container_extended_obj { + struct apm_container_obj common; + struct apm_prop_data parent_data; + struct apm_cont_prop_id_parent_container parent; + struct apm_prop_data heap_data; + struct apm_cont_prop_id_headp_id heap; +} __packed; + struct apm_container_params { struct apm_module_param_data param_data; uint32_t num_containers; - struct apm_container_obj cont_obj[]; + u8 cont_obj[]; } __packed; -#define APM_CONTAINER_PSIZE(p, n) ALIGN(struct_size(p, cont_obj, n), 8) - /* Module List config */ struct apm_mod_list_obj { /* Modules list cfg */ @@ -113,6 +120,15 @@ struct apm_mod_conn_list_params { #define APM_MOD_CONN_PSIZE(p, n) ALIGN(struct_size(p, conn_obj, n), 8) +struct apm_mod_ctrl_link_list_params { + struct apm_module_param_data param_data; + u32 num_ctrl_links; + u8 ctrl_link_cfg[]; +} __packed; + +#define APM_MOD_CTRL_LINK_PSIZE(p, n) \ + ALIGN(struct_size(p, ctrl_link_cfg, n), 8) + struct apm_graph_open_params { struct apm_cmd_header *cmd_header; struct apm_sub_graph_params *sg_data; @@ -120,6 +136,7 @@ struct apm_graph_open_params { struct apm_module_list_params *mod_list_data; struct apm_prop_list_params *mod_prop_data; struct apm_mod_conn_list_params *mod_conn_list_data; + struct apm_mod_ctrl_link_list_params *mod_ctrl_link_data; } __packed; struct apm_pcm_module_media_fmt_cmd { @@ -296,6 +313,32 @@ void *audioreach_alloc_apm_cmd_pkt(int pkt_size, uint32_t opcode, uint32_t token } EXPORT_SYMBOL_GPL(audioreach_alloc_apm_cmd_pkt); +static int audioreach_size_add(size_t *total, size_t value) +{ + return check_add_overflow(*total, value, total) ? -EOVERFLOW : 0; +} + +static int audioreach_flex_size(size_t base, size_t count, size_t element, + size_t *size) +{ + size_t elements; + + if (check_mul_overflow(count, element, &elements) || + check_add_overflow(base, elements, size)) + return -EOVERFLOW; + + return 0; +} + +static int audioreach_align_size(size_t *size) +{ + if (*size > SIZE_MAX - 7) + return -EOVERFLOW; + + *size = ALIGN(*size, 8); + return 0; +} + void audioreach_set_default_channel_mapping(u8 *ch_map, int num_channels) { if (num_channels == 1) { @@ -312,13 +355,21 @@ void audioreach_set_default_channel_mapping(u8 *ch_map, int num_channels) } EXPORT_SYMBOL_GPL(audioreach_set_default_channel_mapping); -static void apm_populate_container_config(struct apm_container_obj *cfg, +static size_t apm_container_obj_size(const struct audioreach_container *cont) +{ + return cont->has_extended_properties ? + sizeof(struct apm_container_extended_obj) : + sizeof(struct apm_container_obj); +} + +static void apm_populate_container_config(void *data, const struct audioreach_container *cont) { + struct apm_container_obj *cfg = data; /* Container Config */ cfg->container_cfg.container_id = cont->container_id; - cfg->container_cfg.num_prop = 4; + cfg->container_cfg.num_prop = cont->has_extended_properties ? 6 : 4; /* Capability list */ cfg->cap_data.prop_id = APM_CONTAINER_PROP_ID_CAPABILITY_LIST; @@ -340,6 +391,19 @@ static void apm_populate_container_config(struct apm_container_obj *cfg, cfg->domain_data.prop_id = APM_CONTAINER_PROP_ID_PROC_DOMAIN; cfg->domain_data.prop_size = sizeof(struct apm_cont_prop_id_domain); cfg->domain.proc_domain = cont->proc_domain; + + if (cont->has_extended_properties) { + struct apm_container_extended_obj *ext = data; + + ext->parent_data.prop_id = + APM_CONTAINER_PROP_ID_PARENT_CONTAINER_ID; + ext->parent_data.prop_size = sizeof(ext->parent); + ext->parent.parent_container_id = cont->parent_container_id; + + ext->heap_data.prop_id = APM_CONTAINER_PROP_ID_HEAP_ID; + ext->heap_data.prop_size = sizeof(ext->heap); + ext->heap.heap_id = cont->heap_id; + } } static void apm_populate_sub_graph_config(struct apm_sub_graph_data *cfg, @@ -402,23 +466,26 @@ static void audioreach_populate_graph(struct q6apm *apm, { struct apm_mod_conn_list_params *mc_data = open->mod_conn_list_data; struct apm_module_list_params *ml_data = open->mod_list_data; + struct apm_mod_ctrl_link_list_params *cl_data = open->mod_ctrl_link_data; + u8 *ctrl_link_cfg = cl_data ? cl_data->ctrl_link_cfg : NULL; struct apm_prop_list_params *mp_data = open->mod_prop_data; struct apm_container_params *c_data = open->cont_data; struct apm_sub_graph_params *sg_data = open->sg_data; - int ncontainer = 0, nmodule = 0, nconn = 0; + int nmodule = 0, nconn = 0; struct apm_mod_prop_obj *module_prop_obj; struct audioreach_container *container; struct apm_module_conn_obj *conn_obj; struct audioreach_module *module; struct audioreach_sub_graph *sg; - struct apm_container_obj *cobj; + u8 *cobj = c_data->cont_obj; struct apm_mod_list_obj *mlobj; int i = 0; mlobj = &ml_data->mod_list_obj[0]; - if (info->dst_mod_inst_id && info->src_mod_inst_id) { + if (!info->internal_vmixer_connection && + info->dst_mod_inst_id && info->src_mod_inst_id) { conn_obj = &mc_data->conn_obj[nconn]; conn_obj->src_mod_inst_id = info->src_mod_inst_id; conn_obj->src_mod_op_port_id = info->src_mod_op_port_id; @@ -433,8 +500,6 @@ static void audioreach_populate_graph(struct q6apm *apm, apm_populate_sub_graph_config(sg_cfg, sg); list_for_each_entry(container, &sg->container_list, node) { - cobj = &c_data->cont_obj[ncontainer]; - apm_populate_container_config(cobj, container); apm_populate_module_list_obj(mlobj, container, sg->sub_graph_id); @@ -444,6 +509,16 @@ static void audioreach_populate_graph(struct q6apm *apm, module_prop_obj = &mp_data->mod_prop_obj[nmodule++]; apm_populate_module_prop_obj(module_prop_obj, module); + if (module->ctrl_link_data) { + u32 size = le32_to_cpu(module->ctrl_link_data->size); + + memcpy(ctrl_link_cfg, + (u8 *)module->ctrl_link_data->data + + sizeof(u32), + size - sizeof(u32)); + ctrl_link_cfg += size - sizeof(u32); + } + if (!module->max_op_port) continue; @@ -463,8 +538,7 @@ static void audioreach_populate_graph(struct q6apm *apm, } mlobj = (void *) mlobj + APM_MOD_LIST_OBJ_PSIZE(mlobj, container->num_modules); - - ncontainer++; + cobj += apm_container_obj_size(container); } } } @@ -472,58 +546,115 @@ static void audioreach_populate_graph(struct q6apm *apm, void *audioreach_alloc_graph_pkt(struct q6apm *apm, const struct audioreach_graph_info *info) { - int payload_size, sg_sz, cont_sz, ml_sz, mp_sz, mc_sz; + size_t payload_size = 0, sg_sz, cont_sz, ml_sz = 0; + size_t mp_sz, mc_sz, cl_sz = 0, object_size; struct apm_module_param_data *param_data; - struct apm_container_params *cont_params; struct audioreach_container *container; struct apm_sub_graph_params *sg_params; struct apm_mod_conn_list_params *mcon; struct apm_graph_open_params params; struct apm_prop_list_params *mprop; + struct apm_mod_ctrl_link_list_params *mctrl; struct audioreach_module *module; struct audioreach_sub_graph *sgs; struct apm_mod_list_obj *mlobj; const struct list_head *sg_list; - int num_connections = 0; - int num_containers = 0; - int num_sub_graphs = 0; - int num_modules = 0; - int num_modules_list; + size_t num_connections = 0; + size_t num_containers = 0; + size_t num_sub_graphs = 0; + size_t num_ctrl_links = 0; + size_t ctrl_link_cfg_size = 0; + size_t num_modules = 0; + size_t num_modules_list; struct gpr_pkt *pkt; void *p; + int ret; sg_list = &info->sg_list; - ml_sz = 0; + cont_sz = sizeof(struct apm_container_params); /* add FE-BE connections */ - if (info->dst_mod_inst_id && info->src_mod_inst_id) - num_connections++; + if (!info->internal_vmixer_connection && + info->dst_mod_inst_id && info->src_mod_inst_id) + num_connections = 1; list_for_each_entry(sgs, sg_list, node) { - num_sub_graphs++; + if (check_add_overflow(num_sub_graphs, 1UL, &num_sub_graphs)) + return ERR_PTR(-EOVERFLOW); list_for_each_entry(container, &sgs->container_list, node) { - num_containers++; - num_modules += container->num_modules; - ml_sz = ml_sz + sizeof(struct apm_module_list_params) + - APM_MOD_LIST_OBJ_PSIZE(mlobj, container->num_modules); + if (check_add_overflow(num_containers, 1UL, + &num_containers) || + check_add_overflow(num_modules, + (size_t)container->num_modules, + &num_modules) || + audioreach_size_add(&cont_sz, + apm_container_obj_size(container))) + return ERR_PTR(-EOVERFLOW); + + ret = audioreach_flex_size(sizeof(*mlobj), + container->num_modules, + sizeof(mlobj->mod_cfg[0]), + &object_size); + if (ret || + audioreach_size_add(&ml_sz, + sizeof(struct apm_module_list_params)) || + audioreach_size_add(&ml_sz, object_size)) + return ERR_PTR(-EOVERFLOW); list_for_each_entry(module, &container->modules_list, node) { - num_connections += module->num_connections; + if (check_add_overflow(num_connections, + (size_t)module->num_connections, + &num_connections)) + return ERR_PTR(-EOVERFLOW); + if (module->ctrl_link_data) { + u32 size = le32_to_cpu(module->ctrl_link_data->size); + u32 links; + + links = get_unaligned_le32(module->ctrl_link_data->data); + if (check_add_overflow(num_ctrl_links, + (size_t)links, + &num_ctrl_links) || + audioreach_size_add(&ctrl_link_cfg_size, + size - sizeof(u32))) + return ERR_PTR(-EOVERFLOW); + } } } } - num_modules_list = num_containers; - sg_sz = APM_SUB_GRAPH_PSIZE(sg_params, num_sub_graphs); - cont_sz = APM_CONTAINER_PSIZE(cont_params, num_containers); - - ml_sz = ALIGN(ml_sz, 8); + if (num_sub_graphs > U32_MAX || num_containers > U32_MAX || + num_modules > U32_MAX || num_connections > U32_MAX || + num_ctrl_links > U32_MAX) + return ERR_PTR(-EOVERFLOW); - mp_sz = APM_MOD_PROP_PSIZE(mprop, num_modules); - mc_sz = APM_MOD_CONN_PSIZE(mcon, num_connections); - - payload_size = sg_sz + cont_sz + ml_sz + mp_sz + mc_sz; - pkt = audioreach_alloc_apm_cmd_pkt(payload_size, APM_CMD_GRAPH_OPEN, 0); + num_modules_list = num_containers; + ret = audioreach_flex_size(sizeof(*sg_params), num_sub_graphs, + sizeof(sg_params->sg_cfg[0]), &sg_sz); + ret = ret ?: audioreach_flex_size(sizeof(*mprop), num_modules, + sizeof(mprop->mod_prop_obj[0]), &mp_sz); + ret = ret ?: audioreach_flex_size(sizeof(*mcon), num_connections, + sizeof(mcon->conn_obj[0]), &mc_sz); + if (num_ctrl_links) + ret = ret ?: audioreach_flex_size(sizeof(*mctrl), + ctrl_link_cfg_size, 1, &cl_sz); + ret = ret ?: audioreach_align_size(&sg_sz); + ret = ret ?: audioreach_align_size(&cont_sz); + ret = ret ?: audioreach_align_size(&ml_sz); + ret = ret ?: audioreach_align_size(&mp_sz); + ret = ret ?: audioreach_align_size(&mc_sz); + if (num_ctrl_links) + ret = ret ?: audioreach_align_size(&cl_sz); + if (ret || audioreach_size_add(&payload_size, sg_sz) || + audioreach_size_add(&payload_size, cont_sz) || + audioreach_size_add(&payload_size, ml_sz) || + audioreach_size_add(&payload_size, mp_sz) || + audioreach_size_add(&payload_size, mc_sz) || + audioreach_size_add(&payload_size, cl_sz) || + payload_size > INT_MAX - GPR_HDR_SIZE - APM_CMD_HDR_SIZE) + return ERR_PTR(-EOVERFLOW); + + pkt = audioreach_alloc_apm_cmd_pkt((int)payload_size, + APM_CMD_GRAPH_OPEN, 0); if (IS_ERR(pkt)) return pkt; @@ -574,6 +705,17 @@ void *audioreach_alloc_graph_pkt(struct q6apm *apm, params.mod_conn_list_data->num_connections = num_connections; p += mc_sz; + if (num_ctrl_links) { + params.mod_ctrl_link_data = p; + param_data = ¶ms.mod_ctrl_link_data->param_data; + param_data->module_instance_id = APM_MODULE_INSTANCE_ID; + param_data->param_id = APM_PARAM_ID_MODULE_CTRL_LINK_CFG; + param_data->param_size = sizeof(u32) + ctrl_link_cfg_size; + params.mod_ctrl_link_data->num_ctrl_links = num_ctrl_links; + } else { + params.mod_ctrl_link_data = NULL; + } + audioreach_populate_graph(apm, info, ¶ms, sg_list, num_sub_graphs); return pkt; diff --git a/sound/soc/qcom/qdsp6/audioreach.h b/sound/soc/qcom/qdsp6/audioreach.h index 0b5eda7e2598d8..6a6aaa54538d47 100644 --- a/sound/soc/qcom/qdsp6/audioreach.h +++ b/sound/soc/qcom/qdsp6/audioreach.h @@ -157,6 +157,7 @@ struct apm_module_prop_cfg { } __packed; #define APM_PARAM_ID_MODULE_CONN 0x08001004 +#define APM_PARAM_ID_MODULE_CTRL_LINK_CFG 0x08001061 struct apm_param_id_module_conn { uint32_t num_connections; @@ -821,6 +822,7 @@ struct audioreach_graph_info { uint32_t src_mod_op_port_id; uint32_t dst_mod_inst_id; uint32_t dst_mod_ip_port_id; + bool internal_vmixer_connection; }; struct audioreach_sub_graph { @@ -841,6 +843,9 @@ struct audioreach_container { uint32_t graph_pos; uint32_t stack_size; uint32_t proc_domain; + u32 parent_container_id; + u32 heap_id; + bool has_extended_properties; struct list_head node; uint32_t num_modules; @@ -887,12 +892,25 @@ struct audioreach_module { uint32_t log_code; uint32_t log_tap_point_id; uint32_t log_mode; + bool speaker_protection_bypass; + u32 integrated_backend_id; /* bookkeeping */ struct list_head node; struct audioreach_container *container; struct snd_soc_dapm_widget *widget; struct audioreach_module_priv_data *data; + struct audioreach_module_priv_data *ctrl_link_data; + struct audioreach_module_priv_data *graph_cal_data; + struct audioreach_module_priv_data *render_ep_data; + struct audioreach_module_priv_data *sp_tag_data; + struct audioreach_module_priv_data *spvi_tag_data; + struct audioreach_module_priv_data *vi_ep_data; + struct audioreach_module_priv_data *protection_dynamic_data; + struct audioreach_module_priv_data *volume_gain_data; + struct audioreach_module_priv_data *volume_filter_data; + struct audioreach_module_priv_data *volume_mute_data; + struct audioreach_module_priv_data *channel_mixer_data; }; struct audioreach_module_config { diff --git a/sound/soc/qcom/qdsp6/topology.c b/sound/soc/qcom/qdsp6/topology.c index 1f69fba6de26d1..067145e83aa30a 100644 --- a/sound/soc/qcom/qdsp6/topology.c +++ b/sound/soc/qcom/qdsp6/topology.c @@ -2,6 +2,7 @@ // Copyright (c) 2020, Linaro Limited #include +#include #include #include #include @@ -138,6 +139,8 @@ static struct audioreach_container *audioreach_tplg_alloc_container(struct q6apm if (!cont) return ERR_PTR(-ENOMEM); + cont->parent_container_id = U32_MAX; + cont->heap_id = APM_CONT_HEAP_DEFAULT; INIT_LIST_HEAD(&cont->modules_list); mutex_lock(&apm->lock); @@ -206,131 +209,256 @@ static struct audioreach_module *audioreach_tplg_alloc_module(struct q6apm *apm, return mod; } -static const struct snd_soc_tplg_vendor_array * -audioreach_get_sg_array(const struct snd_soc_tplg_private *private) +static bool audioreach_is_raw_private_type(u32 type) { - const struct snd_soc_tplg_vendor_array *sg_array = NULL; - bool found = false; - int sz; - - for (sz = 0; !found && (sz < le32_to_cpu(private->size)); ) { - const struct snd_soc_tplg_vendor_value_elem *sg_elem; - int tkn_count = 0; - - sg_array = (struct snd_soc_tplg_vendor_array *)((u8 *)private->array + sz); - sg_elem = sg_array->value; - sz = sz + le32_to_cpu(sg_array->size); - while (!found && tkn_count <= (le32_to_cpu(sg_array->num_elems) - 1)) { - switch (le32_to_cpu(sg_elem->token)) { - case AR_TKN_U32_SUB_GRAPH_INSTANCE_ID: - found = true; - break; - default: - break; - } - tkn_count++; - sg_elem++; + return type == SND_SOC_AR_TPLG_MODULE_CFG_TYPE || + type == SND_SOC_AR_TPLG_CTRL_LINK_CFG_TYPE || + type == SND_SOC_AR_TPLG_GRAPH_CAL_CFG_TYPE || + type == SND_SOC_AR_TPLG_RENDER_EP_CFG_TYPE || + type == SND_SOC_AR_TPLG_SP_TAG_CFG_TYPE || + type == SND_SOC_AR_TPLG_SPVI_TAG_CFG_TYPE || + type == SND_SOC_AR_TPLG_VI_EP_CFG_TYPE || + type == SND_SOC_AR_TPLG_PROTECTION_DYNAMIC_CFG_TYPE || + type == SND_SOC_AR_TPLG_VOLUME_GAIN_CFG_TYPE || + type == SND_SOC_AR_TPLG_VOLUME_FILTER_CFG_TYPE || + type == SND_SOC_AR_TPLG_VOLUME_MUTE_CFG_TYPE || + type == SND_SOC_AR_TPLG_CHANNEL_MIXER_CFG_TYPE; +} + +static int +audioreach_next_private_block(const struct snd_soc_tplg_private *private, + size_t *offset, + const struct snd_soc_tplg_vendor_array **array, + bool *raw) +{ + const struct audioreach_module_priv_data *header; + size_t private_size = le32_to_cpu(private->size); + size_t block_size, elements_size, remaining; + u32 array_size, num_elems, type; + + if (*offset == private_size) + return 0; + if (*offset > private_size) + return -EINVAL; + + remaining = private_size - *offset; + if (remaining < 2 * sizeof(__le32)) + return -EINVAL; + header = (const void *)((const u8 *)private->array + *offset); + array_size = le32_to_cpu(header->size); + type = le32_to_cpu(header->type); + *raw = audioreach_is_raw_private_type(type); + *array = (const void *)header; + + if (*raw) { + if (remaining < sizeof(*header) || + check_add_overflow(sizeof(*header), (size_t)array_size, + &block_size) || + block_size > remaining) + return -EINVAL; + } else { + if (remaining < sizeof(**array) || + array_size < sizeof(**array) || array_size > remaining) + return -EINVAL; + block_size = array_size; + if (type == SND_SOC_TPLG_TUPLE_TYPE_WORD) { + num_elems = le32_to_cpu((*array)->num_elems); + if (check_mul_overflow((size_t)num_elems, + sizeof((*array)->value[0]), + &elements_size) || + elements_size > block_size - sizeof(**array)) + return -EINVAL; } } - if (found) - return sg_array; - - return NULL; + *offset += block_size; + return 1; } static const struct snd_soc_tplg_vendor_array * -audioreach_get_cont_array(const struct snd_soc_tplg_private *private) +audioreach_get_token_array(const struct snd_soc_tplg_private *private, u32 token) { - const struct snd_soc_tplg_vendor_array *cont_array = NULL; - bool found = false; - int sz; - - for (sz = 0; !found && (sz < le32_to_cpu(private->size)); ) { - const struct snd_soc_tplg_vendor_value_elem *cont_elem; - int tkn_count = 0; - - cont_array = (struct snd_soc_tplg_vendor_array *)((u8 *)private->array + sz); - cont_elem = cont_array->value; - sz = sz + le32_to_cpu(cont_array->size); - while (!found && tkn_count <= (le32_to_cpu(cont_array->num_elems) - 1)) { - switch (le32_to_cpu(cont_elem->token)) { - case AR_TKN_U32_CONTAINER_INSTANCE_ID: - found = true; - break; - default: - break; - } - tkn_count++; - cont_elem++; + const struct snd_soc_tplg_vendor_array *array; + const struct snd_soc_tplg_vendor_array *found = NULL; + size_t offset = 0; + bool raw; + int ret; + + while ((ret = audioreach_next_private_block(private, &offset, + &array, &raw)) > 0) { + u32 count, i; + + if (raw || le32_to_cpu(array->type) != + SND_SOC_TPLG_TUPLE_TYPE_WORD) + continue; + count = le32_to_cpu(array->num_elems); + for (i = 0; i < count; i++) { + if (le32_to_cpu(array->value[i].token) != token) + continue; + if (found) + return ERR_PTR(-EEXIST); + found = array; } } - if (found) - return cont_array; + return ret < 0 ? ERR_PTR(ret) : found; +} - return NULL; +static const struct snd_soc_tplg_vendor_array * +audioreach_get_sg_array(const struct snd_soc_tplg_private *private) +{ + return audioreach_get_token_array(private, + AR_TKN_U32_SUB_GRAPH_INSTANCE_ID); +} + +static const struct snd_soc_tplg_vendor_array * +audioreach_get_cont_array(const struct snd_soc_tplg_private *private) +{ + return audioreach_get_token_array(private, + AR_TKN_U32_CONTAINER_INSTANCE_ID); } static const struct snd_soc_tplg_vendor_array * audioreach_get_module_array(const struct snd_soc_tplg_private *private) { - const struct snd_soc_tplg_vendor_array *mod_array = NULL; - bool found = false; - int sz = 0; - - for (sz = 0; !found && (sz < le32_to_cpu(private->size)); ) { - const struct snd_soc_tplg_vendor_value_elem *mod_elem; - int tkn_count = 0; - - mod_array = (struct snd_soc_tplg_vendor_array *)((u8 *)private->array + sz); - mod_elem = mod_array->value; - sz = sz + le32_to_cpu(mod_array->size); - while (!found && tkn_count <= (le32_to_cpu(mod_array->num_elems) - 1)) { - switch (le32_to_cpu(mod_elem->token)) { - case AR_TKN_U32_MODULE_INSTANCE_ID: - found = true; - break; - default: - break; - } - tkn_count++; - mod_elem++; + return audioreach_get_token_array(private, + AR_TKN_U32_MODULE_INSTANCE_ID); +} + +static struct audioreach_module_priv_data * +audioreach_get_module_priv_data(const struct snd_soc_tplg_private *private, + u32 requested_type) +{ + const struct snd_soc_tplg_vendor_array *array; + struct audioreach_module_priv_data *pdata = NULL; + size_t offset = 0; + bool raw; + int ret; + + while ((ret = audioreach_next_private_block(private, &offset, + &array, &raw)) > 0) { + size_t block_size, size; + + if (!raw || le32_to_cpu(array->type) != requested_type) + continue; + if (pdata) { + ret = -EEXIST; + goto error; } + + size = le32_to_cpu(array->size); + if (check_add_overflow(sizeof(*pdata), size, &block_size)) + return ERR_PTR(-EINVAL); + pdata = kmemdup(array, block_size, GFP_KERNEL); + if (!pdata) + return ERR_PTR(-ENOMEM); } - if (found) - return mod_array; + if (ret < 0) + goto error; - return NULL; + return pdata; + +error: + kfree(pdata); + return ERR_PTR(ret); } -static struct audioreach_module_priv_data * -audioreach_get_module_priv_data(const struct snd_soc_tplg_private *private) +static int +audioreach_validate_ctrl_links(const struct audioreach_module_priv_data *pdata) { - int sz; + size_t size = le32_to_cpu(pdata->size); + const u8 *cursor = (const u8 *)pdata->data; + const u8 *end = cursor + size; + u32 links, link, property; - for (sz = 0; sz < le32_to_cpu(private->size); ) { - const struct snd_soc_tplg_vendor_array *mod_array; + if (size < sizeof(u32)) + return -EINVAL; - mod_array = (struct snd_soc_tplg_vendor_array *)((u8 *)private->array + sz); - if (le32_to_cpu(mod_array->type) == SND_SOC_AR_TPLG_MODULE_CFG_TYPE) { - struct audioreach_module_priv_data *pdata; + links = get_unaligned_le32(cursor); + if (!links) + return -EINVAL; + cursor += sizeof(u32); + for (link = 0; link < links; link++) { + u32 properties; + + if (end - cursor < 5 * sizeof(u32)) + return -EINVAL; + properties = get_unaligned_le32(cursor + 4 * sizeof(u32)); + cursor += 5 * sizeof(u32); + + for (property = 0; property < properties; property++) { + u32 property_size; + + if (end - cursor < 2 * sizeof(u32)) + return -EINVAL; + property_size = get_unaligned_le32(cursor + sizeof(u32)); + cursor += 2 * sizeof(u32); + if (property_size > end - cursor) + return -EINVAL; + cursor += property_size; + } + } - pdata = kzalloc_flex(*pdata, data, - le32_to_cpu(mod_array->size)); - if (!pdata) - return ERR_PTR(-ENOMEM); + return cursor == end ? 0 : -EINVAL; +} - memcpy(pdata, ((u8 *)private->data + sz), struct_size(pdata, data, - le32_to_cpu(mod_array->size))); - return pdata; - } +static void audioreach_free_extended_data(struct audioreach_module *mod) +{ + kfree(mod->ctrl_link_data); + kfree(mod->graph_cal_data); + kfree(mod->render_ep_data); + kfree(mod->sp_tag_data); + kfree(mod->spvi_tag_data); + kfree(mod->vi_ep_data); + kfree(mod->protection_dynamic_data); + kfree(mod->volume_gain_data); + kfree(mod->volume_filter_data); + kfree(mod->volume_mute_data); + kfree(mod->channel_mixer_data); +} - sz = sz + le32_to_cpu(mod_array->size); +static int audioreach_load_extended_data(struct audioreach_module *mod, + const struct snd_soc_tplg_private *private) +{ + static const u32 types[] = { + SND_SOC_AR_TPLG_GRAPH_CAL_CFG_TYPE, + SND_SOC_AR_TPLG_RENDER_EP_CFG_TYPE, + SND_SOC_AR_TPLG_SP_TAG_CFG_TYPE, + SND_SOC_AR_TPLG_SPVI_TAG_CFG_TYPE, + SND_SOC_AR_TPLG_VI_EP_CFG_TYPE, + SND_SOC_AR_TPLG_PROTECTION_DYNAMIC_CFG_TYPE, + SND_SOC_AR_TPLG_VOLUME_GAIN_CFG_TYPE, + SND_SOC_AR_TPLG_VOLUME_FILTER_CFG_TYPE, + SND_SOC_AR_TPLG_VOLUME_MUTE_CFG_TYPE, + SND_SOC_AR_TPLG_CHANNEL_MIXER_CFG_TYPE, + }; + struct audioreach_module_priv_data **destinations[] = { + &mod->graph_cal_data, + &mod->render_ep_data, + &mod->sp_tag_data, + &mod->spvi_tag_data, + &mod->vi_ep_data, + &mod->protection_dynamic_data, + &mod->volume_gain_data, + &mod->volume_filter_data, + &mod->volume_mute_data, + &mod->channel_mixer_data, + }; + int i; + + for (i = 0; i < ARRAY_SIZE(types); i++) { + *destinations[i] = + audioreach_get_module_priv_data(private, types[i]); + if (IS_ERR(*destinations[i])) { + int ret = PTR_ERR(*destinations[i]); + + *destinations[i] = NULL; + return ret; + } } - return NULL; + return 0; } static struct audioreach_sub_graph *audioreach_parse_sg_tokens(struct q6apm *apm, @@ -340,13 +468,17 @@ static struct audioreach_sub_graph *audioreach_parse_sg_tokens(struct q6apm *apm const struct snd_soc_tplg_vendor_array *sg_array; struct audioreach_graph_info *info = NULL; int graph_id, sub_graph_id, tkn_count = 0; - struct audioreach_sub_graph *sg; + struct audioreach_sub_graph *sg = NULL; bool found; sg_array = audioreach_get_sg_array(private); + if (IS_ERR(sg_array)) + return ERR_CAST(sg_array); + if (!sg_array) + return ERR_PTR(-EINVAL); sg_elem = sg_array->value; - while (tkn_count <= (le32_to_cpu(sg_array->num_elems) - 1)) { + while (tkn_count < le32_to_cpu(sg_array->num_elems)) { switch (le32_to_cpu(sg_elem->token)) { case AR_TKN_U32_SUB_GRAPH_INSTANCE_ID: sub_graph_id = le32_to_cpu(sg_elem->value); @@ -366,12 +498,18 @@ static struct audioreach_sub_graph *audioreach_parse_sg_tokens(struct q6apm *apm return ERR_CAST(info); break; case AR_TKN_U32_SUB_GRAPH_PERF_MODE: + if (!sg) + return ERR_PTR(-EINVAL); sg->perf_mode = le32_to_cpu(sg_elem->value); break; case AR_TKN_U32_SUB_GRAPH_DIRECTION: + if (!sg) + return ERR_PTR(-EINVAL); sg->direction = le32_to_cpu(sg_elem->value); break; case AR_TKN_U32_SUB_GRAPH_SCENARIO_ID: + if (!sg) + return ERR_PTR(-EINVAL); sg->scenario_id = le32_to_cpu(sg_elem->value); break; default: @@ -382,6 +520,8 @@ static struct audioreach_sub_graph *audioreach_parse_sg_tokens(struct q6apm *apm tkn_count++; sg_elem++; } + if (!sg) + return ERR_PTR(-EINVAL); /* Sub graph is associated with predefined graph */ if (info) @@ -396,14 +536,18 @@ static struct audioreach_container *audioreach_parse_cont_tokens(struct q6apm *a { const struct snd_soc_tplg_vendor_value_elem *cont_elem; const struct snd_soc_tplg_vendor_array *cont_array; - struct audioreach_container *cont; + struct audioreach_container *cont = NULL; int container_id, tkn_count = 0; bool found = false; cont_array = audioreach_get_cont_array(private); + if (IS_ERR(cont_array)) + return ERR_CAST(cont_array); + if (!cont_array) + return ERR_PTR(-EINVAL); cont_elem = cont_array->value; - while (tkn_count <= (le32_to_cpu(cont_array->num_elems) - 1)) { + while (tkn_count < le32_to_cpu(cont_array->num_elems)) { switch (le32_to_cpu(cont_elem->token)) { case AR_TKN_U32_CONTAINER_INSTANCE_ID: container_id = le32_to_cpu(cont_elem->value); @@ -412,17 +556,37 @@ static struct audioreach_container *audioreach_parse_cont_tokens(struct q6apm *a return cont; break; case AR_TKN_U32_CONTAINER_CAPABILITY_ID: + if (!cont) + return ERR_PTR(-EINVAL); cont->capability_id = le32_to_cpu(cont_elem->value); break; case AR_TKN_U32_CONTAINER_STACK_SIZE: + if (!cont) + return ERR_PTR(-EINVAL); cont->stack_size = le32_to_cpu(cont_elem->value); break; case AR_TKN_U32_CONTAINER_GRAPH_POS: + if (!cont) + return ERR_PTR(-EINVAL); cont->graph_pos = le32_to_cpu(cont_elem->value); break; case AR_TKN_U32_CONTAINER_PROC_DOMAIN: + if (!cont) + return ERR_PTR(-EINVAL); cont->proc_domain = le32_to_cpu(cont_elem->value); break; + case AR_TKN_U32_CONTAINER_PARENT_ID: + if (!cont) + return ERR_PTR(-EINVAL); + cont->parent_container_id = le32_to_cpu(cont_elem->value); + cont->has_extended_properties = true; + break; + case AR_TKN_U32_CONTAINER_HEAP_ID: + if (!cont) + return ERR_PTR(-EINVAL); + cont->heap_id = le32_to_cpu(cont_elem->value); + cont->has_extended_properties = true; + break; default: dev_err(apm->dev, "Not a valid token %d for graph\n", cont_elem->token); break; @@ -432,7 +596,7 @@ static struct audioreach_container *audioreach_parse_cont_tokens(struct q6apm *a cont_elem++; } - return cont; + return cont ?: ERR_PTR(-EINVAL); } static struct audioreach_module *audioreach_parse_common_tokens(struct q6apm *apm, @@ -445,6 +609,8 @@ static struct audioreach_module *audioreach_parse_common_tokens(struct q6apm *ap uint32_t dst_mod_inst_id[AR_MAX_MOD_LINKS] = { 0, }; uint32_t dst_mod_ip_port_id[AR_MAX_MOD_LINKS] = { 0, }; uint32_t src_mod_inst_id = 0; + u32 integrated_backend_id = 0; + bool speaker_protection_bypass = false; int module_id = 0, instance_id = 0, tkn_count = 0; const struct snd_soc_tplg_vendor_value_elem *mod_elem; @@ -455,9 +621,13 @@ static struct audioreach_module *audioreach_parse_common_tokens(struct q6apm *ap int max_tokens; mod_array = audioreach_get_module_array(private); + if (IS_ERR(mod_array)) + return ERR_CAST(mod_array); + if (!mod_array) + return ERR_PTR(-EINVAL); mod_elem = mod_array->value; max_tokens = le32_to_cpu(mod_array->num_elems); - while (tkn_count <= (max_tokens - 1)) { + while (tkn_count < max_tokens) { token = le32_to_cpu(mod_elem->token); switch (token) { /* common module info */ @@ -479,9 +649,13 @@ static struct audioreach_module *audioreach_parse_common_tokens(struct q6apm *ap break; case AR_TKN_U32_MODULE_MAX_IP_PORTS: max_ip_port = le32_to_cpu(mod_elem->value); + if (max_ip_port > AR_MAX_MOD_LINKS) + return ERR_PTR(-EINVAL); break; case AR_TKN_U32_MODULE_MAX_OP_PORTS: max_op_port = le32_to_cpu(mod_elem->value); + if (max_op_port > AR_MAX_MOD_LINKS) + return ERR_PTR(-EINVAL); break; case AR_TKN_U32_MODULE_SRC_INSTANCE_ID: src_mod_inst_id = le32_to_cpu(mod_elem->value); @@ -558,6 +732,12 @@ static struct audioreach_module *audioreach_parse_common_tokens(struct q6apm *ap case AR_TKN_U32_MODULE_DST_IN_PORT_ID7: dst_mod_ip_port_id[7] = le32_to_cpu(mod_elem->value); break; + case AR_TKN_U32_MODULE_SPEAKER_PROTECTION_BYPASS: + speaker_protection_bypass = !!le32_to_cpu(mod_elem->value); + break; + case AR_TKN_U32_MODULE_INTEGRATED_BACKEND_ID: + integrated_backend_id = le32_to_cpu(mod_elem->value); + break; default: break; @@ -573,6 +753,8 @@ static struct audioreach_module *audioreach_parse_common_tokens(struct q6apm *ap mod->max_ip_port = max_ip_port; mod->max_op_port = max_op_port; mod->src_mod_inst_id = src_mod_inst_id; + mod->speaker_protection_bypass = speaker_protection_bypass; + mod->integrated_backend_id = integrated_backend_id; for (pn = 0; pn < mod->max_op_port; pn++) { if (src_mod_op_port_id[pn] && dst_mod_inst_id[pn] && dst_mod_ip_port_id[pn]) { @@ -585,7 +767,7 @@ static struct audioreach_module *audioreach_parse_common_tokens(struct q6apm *ap } } - return mod; + return mod ?: ERR_PTR(-EINVAL); } static int audioreach_widget_load_module_common(struct snd_soc_component *component, @@ -597,6 +779,7 @@ static int audioreach_widget_load_module_common(struct snd_soc_component *compon struct audioreach_sub_graph *sg; struct audioreach_module *mod; struct snd_soc_dobj *dobj; + const u32 ctrl_type = SND_SOC_AR_TPLG_CTRL_LINK_CFG_TYPE; sg = audioreach_parse_sg_tokens(apm, &tplg_w->priv); if (IS_ERR(sg)) @@ -610,12 +793,34 @@ static int audioreach_widget_load_module_common(struct snd_soc_component *compon if (IS_ERR_OR_NULL(mod)) return mod ? PTR_ERR(mod) : -ENODEV; - mod->data = audioreach_get_module_priv_data(&tplg_w->priv); - dobj = &w->dobj; dobj->private = mod; - return 0; + mod->data = audioreach_get_module_priv_data(&tplg_w->priv, + SND_SOC_AR_TPLG_MODULE_CFG_TYPE); + if (IS_ERR(mod->data)) { + int ret = PTR_ERR(mod->data); + + mod->data = NULL; + return ret; + } + + mod->ctrl_link_data = + audioreach_get_module_priv_data(&tplg_w->priv, ctrl_type); + if (IS_ERR(mod->ctrl_link_data)) { + int ret = PTR_ERR(mod->ctrl_link_data); + + mod->ctrl_link_data = NULL; + return ret; + } + if (mod->ctrl_link_data) { + int ret = audioreach_validate_ctrl_links(mod->ctrl_link_data); + + if (ret) + return ret; + } + + return audioreach_load_extended_data(mod, &tplg_w->priv); } static int audioreach_widget_load_enc_dec_cnv(struct snd_soc_component *component, @@ -636,9 +841,13 @@ static int audioreach_widget_load_enc_dec_cnv(struct snd_soc_component *componen dobj = &w->dobj; mod = dobj->private; mod_array = audioreach_get_module_array(&tplg_w->priv); + if (IS_ERR(mod_array)) + return PTR_ERR(mod_array); + if (!mod_array) + return -EINVAL; mod_elem = mod_array->value; - while (tkn_count <= (le32_to_cpu(mod_array->num_elems) - 1)) { + while (tkn_count < le32_to_cpu(mod_array->num_elems)) { switch (le32_to_cpu(mod_elem->token)) { case AR_TKN_U32_MODULE_FMT_INTERLEAVE: mod->interleave_type = le32_to_cpu(mod_elem->value); @@ -793,8 +1002,17 @@ static int audioreach_widget_load_buffer(struct snd_soc_component *component, mod = dobj->private; mod_array = audioreach_get_module_array(&tplg_w->priv); + if (IS_ERR(mod_array)) + return PTR_ERR(mod_array); + if (!mod_array) + return -EINVAL; switch (mod->module_id) { + case MODULE_ID_WR_SHARED_MEM_EP: + case MODULE_ID_RD_SHARED_MEM_EP: + case MODULE_ID_SH_MEM_PULL_MODE: + case MODULE_ID_SH_MEM_PUSH_MODE: + break; case MODULE_ID_CODEC_DMA_SINK: case MODULE_ID_CODEC_DMA_SOURCE: audioreach_widget_dma_module_load(mod, mod_array); @@ -810,7 +1028,8 @@ static int audioreach_widget_load_buffer(struct snd_soc_component *component, audioreach_widget_dp_module_load(mod, mod_array); break; default: - return -EINVAL; + /* Common tokens are sufficient for modules without a special loader. */ + break; } return 0; @@ -919,16 +1138,14 @@ static int audioreach_widget_ready(struct snd_soc_component *component, switch (w->id) { case snd_soc_dapm_aif_in: case snd_soc_dapm_aif_out: - audioreach_widget_load_buffer(component, index, w, tplg_w); - break; + return audioreach_widget_load_buffer(component, index, w, tplg_w); case snd_soc_dapm_decoder: case snd_soc_dapm_encoder: case snd_soc_dapm_src: - audioreach_widget_load_enc_dec_cnv(component, index, w, tplg_w); - break; + return audioreach_widget_load_enc_dec_cnv(component, index, w, + tplg_w); case snd_soc_dapm_buffer: - audioreach_widget_load_buffer(component, index, w, tplg_w); - break; + return audioreach_widget_load_buffer(component, index, w, tplg_w); case snd_soc_dapm_mixer: return audioreach_widget_load_mixer(component, index, w, tplg_w); case snd_soc_dapm_pga: @@ -972,6 +1189,7 @@ static int audioreach_widget_unload(struct snd_soc_component *scomp, list_del(&mod->node); kfree(mod->data); + audioreach_free_extended_data(mod); kfree(mod); /* Graph Info has N sub-graphs, sub-graph has N containers, Container has N Modules */ if (list_empty(&cont->modules_list)) { /* if no modules in the container then remove it */ @@ -1115,12 +1333,14 @@ static void audioreach_connect_sub_graphs(struct q6apm *apm, info->src_mod_op_port_id = 1; info->dst_mod_inst_id = m2->module_instance_id; info->dst_mod_ip_port_id = 2; + info->internal_vmixer_connection = m1->graph_id == m2->graph_id; } else { info->src_mod_inst_id = 0; info->src_mod_op_port_id = 0; info->dst_mod_inst_id = 0; info->dst_mod_ip_port_id = 0; + info->internal_vmixer_connection = false; } } From 8997725d8f72471e264d34dcae15146bca98a0c7 Mon Sep 17 00:00:00 2001 From: geoca <272055834+geocausa@users.noreply.github.com> Date: Sun, 23 Aug 2026 08:07:44 +0100 Subject: [PATCH 12/20] ASoC: q6apm: add graph-scoped OOB configuration FullIO protection calibration can exceed the in-band SET_CFG limit, and its storage belongs to the DSP graph rather than global card state. Size each graph payload from topology, allocate coherent memory through the APM DMA device, translate the address for the DSP SID, and map it with a graph-scoped token. Serialize buffer writes and correlate MAP, SET_CFG, and UNMAP replies to the exact command before changing mapping state. Close the DSP graph before unmapping its OOB buffer. If a reply leaves ownership ambiguous, retain the coherent memory and device reference until reboot or a future proven reset hook rather than allowing the DMA range to be reused. [Leon: made OOB ownership graph-scoped, keyed reply mutation to the current command, and retained ambiguous mappings instead of reusing DMA.] Link: https://github.com/geocausa/SP11X1e-audio/commit/9de3dc9b5b25982b35ae50ee3ae708e6a8fe20ca Link: https://github.com/geocausa/SP11X1e-audio/commit/af28565091a9299a0fc066f201324a2711760334 Link: https://github.com/geocausa/SP11X1e-audio/commit/1ca382057d302aed20525822d02d9a229b7f3ad9 Link: https://github.com/geocausa/SP11X1e-audio/commit/31466d62b38c8138d476b2fd3606a76272fb3d34 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/22bdec204438f5e1e19286cb7f1688f196c51967 Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/audioreach.c | 93 ++++++++ sound/soc/qcom/qdsp6/audioreach.h | 5 + sound/soc/qcom/qdsp6/q6apm-dai.c | 41 +++- sound/soc/qcom/qdsp6/q6apm.c | 367 ++++++++++++++++++++++++++++-- sound/soc/qcom/qdsp6/q6apm.h | 16 ++ 5 files changed, 499 insertions(+), 23 deletions(-) diff --git a/sound/soc/qcom/qdsp6/audioreach.c b/sound/soc/qcom/qdsp6/audioreach.c index d59d60b4cbdc7f..f17a6a7a86bd1b 100644 --- a/sound/soc/qcom/qdsp6/audioreach.c +++ b/sound/soc/qcom/qdsp6/audioreach.c @@ -722,6 +722,99 @@ void *audioreach_alloc_graph_pkt(struct q6apm *apm, } EXPORT_SYMBOL_GPL(audioreach_alloc_graph_pkt); +static struct audioreach_module_priv_data * +audioreach_module_data(const struct audioreach_module *module, u32 type) +{ + switch (type) { + case SND_SOC_AR_TPLG_GRAPH_CAL_CFG_TYPE: + return module->graph_cal_data; + case SND_SOC_AR_TPLG_RENDER_EP_CFG_TYPE: + return module->render_ep_data; + case SND_SOC_AR_TPLG_SP_TAG_CFG_TYPE: + return module->sp_tag_data; + case SND_SOC_AR_TPLG_SPVI_TAG_CFG_TYPE: + return module->spvi_tag_data; + case SND_SOC_AR_TPLG_VI_EP_CFG_TYPE: + return module->vi_ep_data; + case SND_SOC_AR_TPLG_PROTECTION_DYNAMIC_CFG_TYPE: + return module->protection_dynamic_data; + case SND_SOC_AR_TPLG_VOLUME_GAIN_CFG_TYPE: + return module->volume_gain_data; + case SND_SOC_AR_TPLG_VOLUME_FILTER_CFG_TYPE: + return module->volume_filter_data; + case SND_SOC_AR_TPLG_VOLUME_MUTE_CFG_TYPE: + return module->volume_mute_data; + case SND_SOC_AR_TPLG_CHANNEL_MIXER_CFG_TYPE: + return module->channel_mixer_data; + default: + return NULL; + } +} + +const struct audioreach_module_priv_data * +audioreach_graph_find_data(const struct audioreach_graph_info *info, u32 type) +{ + const struct audioreach_module_priv_data *found = NULL; + struct audioreach_container *container; + struct audioreach_sub_graph *sg; + struct audioreach_module *module; + + list_for_each_entry(sg, &info->sg_list, node) { + list_for_each_entry(container, &sg->container_list, node) { + list_for_each_entry(module, &container->modules_list, node) { + struct audioreach_module_priv_data *candidate; + + candidate = audioreach_module_data(module, type); + if (!candidate) + continue; + if (found) + return ERR_PTR(-EEXIST); + found = candidate; + } + } + } + + return found; +} + +int audioreach_graph_protection_oob_size(const struct audioreach_graph_info *info, + size_t *size) +{ + static const u32 oob_types[] = { + SND_SOC_AR_TPLG_GRAPH_CAL_CFG_TYPE, + SND_SOC_AR_TPLG_RENDER_EP_CFG_TYPE, + SND_SOC_AR_TPLG_SP_TAG_CFG_TYPE, + SND_SOC_AR_TPLG_SPVI_TAG_CFG_TYPE, + SND_SOC_AR_TPLG_VI_EP_CFG_TYPE, + SND_SOC_AR_TPLG_VOLUME_FILTER_CFG_TYPE, + SND_SOC_AR_TPLG_CHANNEL_MIXER_CFG_TYPE, + }; + const struct audioreach_module_priv_data *data; + size_t max_size = 0; + int i; + + data = audioreach_graph_find_data(info, + SND_SOC_AR_TPLG_GRAPH_CAL_CFG_TYPE); + if (IS_ERR(data)) + return PTR_ERR(data); + if (!data) { + *size = 0; + return 0; + } + + for (i = 0; i < ARRAY_SIZE(oob_types); i++) { + data = audioreach_graph_find_data(info, oob_types[i]); + if (IS_ERR(data)) + return PTR_ERR(data); + if (data) + max_size = max(max_size, + (size_t)le32_to_cpu(data->size)); + } + + *size = max_size; + return 0; +} + int audioreach_send_cmd_sync(struct device *dev, gpr_device_t *gdev, struct gpr_ibasic_rsp_result_t *result, struct mutex *cmd_lock, gpr_port_t *port, wait_queue_head_t *cmd_wait, diff --git a/sound/soc/qcom/qdsp6/audioreach.h b/sound/soc/qcom/qdsp6/audioreach.h index 6a6aaa54538d47..04f0f61130e728 100644 --- a/sound/soc/qcom/qdsp6/audioreach.h +++ b/sound/soc/qcom/qdsp6/audioreach.h @@ -8,6 +8,7 @@ #include struct q6apm; struct q6apm_graph; +struct audioreach_graph; /* Module IDs */ #define MODULE_ID_WR_SHARED_MEM_EP 0x07001000 @@ -943,6 +944,10 @@ void *audioreach_alloc_pkt(int payload_size, uint32_t opcode, uint32_t dest_port); void *audioreach_alloc_graph_pkt(struct q6apm *apm, const struct audioreach_graph_info *info); +const struct audioreach_module_priv_data * +audioreach_graph_find_data(const struct audioreach_graph_info *info, u32 type); +int audioreach_graph_protection_oob_size(const struct audioreach_graph_info *info, + size_t *size); /* Topology specific */ int audioreach_tplg_init(struct snd_soc_component *component); diff --git a/sound/soc/qcom/qdsp6/q6apm-dai.c b/sound/soc/qcom/qdsp6/q6apm-dai.c index bf1f872a09f45a..3052831ba313f0 100644 --- a/sound/soc/qcom/qdsp6/q6apm-dai.c +++ b/sound/soc/qcom/qdsp6/q6apm-dai.c @@ -1017,27 +1017,62 @@ static const struct snd_soc_component_driver q6apm_fe_dai_component = { .remove_order = SND_SOC_COMP_ORDER_EARLY, }; +static void q6apm_dai_release_dma_dev(void *data) +{ + struct device *dev = data; + struct q6apm *apm = dev_get_drvdata(dev->parent); + + if (apm) { + mutex_lock(&apm->lock); + if (apm->dma_dev == dev) + apm->dma_dev = NULL; + mutex_unlock(&apm->lock); + } + put_device(dev); +} + static int q6apm_dai_probe(struct platform_device *pdev) { struct device *dev = &pdev->dev; struct device_node *node = dev->of_node; struct q6apm_dai_data *pdata; + struct q6apm *apm; struct of_phandle_args args; int rc; + apm = dev_get_drvdata(dev->parent); + if (!apm) + return -EPROBE_DEFER; + pdata = devm_kzalloc(dev, sizeof(*pdata), GFP_KERNEL); if (!pdata) return -ENOMEM; rc = of_parse_phandle_with_fixed_args(node, "iommus", 1, 0, &args); - if (rc < 0) + if (rc < 0) { pdata->sid = -1; - else + } else { pdata->sid = args.args[0] & SID_MASK_DEFAULT; + of_node_put(args.np); + } dev_set_drvdata(dev, pdata); + rc = 0; + mutex_lock(&apm->lock); + if (apm->dma_dev) + rc = -EBUSY; + else + apm->dma_dev = get_device(dev); + mutex_unlock(&apm->lock); + if (rc) + return rc; + + rc = devm_add_action_or_reset(dev, q6apm_dai_release_dma_dev, dev); + if (rc) + return rc; - return devm_snd_soc_register_component(dev, &q6apm_fe_dai_component, NULL, 0); + return devm_snd_soc_register_component(dev, &q6apm_fe_dai_component, + NULL, 0); } #ifdef CONFIG_OF diff --git a/sound/soc/qcom/qdsp6/q6apm.c b/sound/soc/qcom/qdsp6/q6apm.c index a6cdd1eec0c342..516880c1f1da64 100644 --- a/sound/soc/qcom/qdsp6/q6apm.c +++ b/sound/soc/qcom/qdsp6/q6apm.c @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -20,8 +21,10 @@ #include "q6dsp-errno.h" /* Graph Management */ -#define APM_MMAP_TOKEN_SEQ_SHIFT 17 -#define APM_MMAP_TOKEN_SEQ_MASK GENMASK(31, 17) +#define APM_MMAP_TOKEN_MAP_TYPE_OOB BIT(17) +#define APM_MMAP_TOKEN_OOB_SEQ_SHIFT 18 +#define APM_MMAP_TOKEN_OOB_SEQ_MASK GENMASK(31, 18) +#define Q6APM_DSP_SID_MASK GENMASK(3, 0) struct apm_graph_mgmt_cmd { struct apm_module_param_data param_data; @@ -35,6 +38,9 @@ static struct q6apm *g_apm; static DEFINE_MUTEX(g_apm_lock); static atomic_t q6apm_mmap_token_seq = ATOMIC_INIT(0); +static int audioreach_graph_mgmt_cmd(struct audioreach_graph *graph, + u32 opcode); + bool q6apm_graph_user_get(struct q6apm_graph *graph) { unsigned long flags; @@ -173,15 +179,240 @@ int q6apm_send_cmd_sync(struct q6apm *apm, struct gpr_pkt *pkt, return ret; } +static phys_addr_t q6apm_dsp_addr(struct device *dev, dma_addr_t dma_addr) +{ + struct of_phandle_args args; + phys_addr_t dsp_addr = dma_addr; + + if (!dev->of_node || + of_parse_phandle_with_fixed_args(dev->of_node, "iommus", 1, 0, + &args)) + return dsp_addr; + + dsp_addr |= (phys_addr_t)(args.args[0] & Q6APM_DSP_SID_MASK) << 32; + of_node_put(args.np); + + return dsp_addr; +} + static u32 q6apm_next_mmap_token(u32 graph_id, u32 map_type) { u32 sequence; sequence = (u32)atomic_inc_return(&q6apm_mmap_token_seq) << - APM_MMAP_TOKEN_SEQ_SHIFT; + APM_MMAP_TOKEN_OOB_SEQ_SHIFT; return (graph_id & APM_MMAP_TOKEN_GID_MASK) | map_type | - (sequence & APM_MMAP_TOKEN_SEQ_MASK); + (sequence & APM_MMAP_TOKEN_OOB_SEQ_MASK); +} + +static int q6apm_map_oob_buffer(struct audioreach_graph *graph, size_t data_size) +{ + struct apm_shared_map_region_payload *region; + struct apm_cmd_shared_mem_map_regions *cmd; + struct gpr_pkt *pkt __free(kfree) = NULL; + u32 opcode = APM_CMD_SHARED_MEM_MAP_REGIONS; + int payload_size = sizeof(*cmd) + sizeof(*region); + bool map_uncertain; + bool mapped; + void *payload; + int ret; + + if (!graph->dma_dev) + return -ENODEV; + if (!data_size || data_size > U32_MAX - (PAGE_SIZE - 1)) + return -E2BIG; + graph->oob_token = q6apm_next_mmap_token(graph->id, + APM_MMAP_TOKEN_MAP_TYPE_OOB); + + graph->oob_size = PAGE_ALIGN(data_size); + graph->oob_virt = dma_alloc_coherent(graph->dma_dev, graph->oob_size, + &graph->oob_dma, GFP_KERNEL); + if (!graph->oob_virt) + return -ENOMEM; + graph->oob_dsp_addr = q6apm_dsp_addr(graph->dma_dev, graph->oob_dma); + + pkt = audioreach_alloc_apm_cmd_pkt(payload_size, opcode, graph->oob_token); + if (IS_ERR(pkt)) { + ret = PTR_ERR(pkt); + goto free_buffer; + } + + payload = (u8 *)pkt + GPR_HDR_SIZE; + cmd = payload; + cmd->mem_pool_id = APM_MEMORY_MAP_SHMEM8_4K_POOL; + cmd->num_regions = 1; + cmd->property_flag = 0; + region = payload + sizeof(*cmd); + region->shm_addr_lsw = lower_32_bits(graph->oob_dsp_addr); + region->shm_addr_msw = upper_32_bits(graph->oob_dsp_addr); + region->mem_size_bytes = graph->oob_size; + + graph->oob_map_uncertain = true; + ret = q6apm_send_cmd_sync(graph->apm, pkt, + APM_CMD_RSP_SHARED_MEM_MAP_REGIONS); + spin_lock(&graph->apm->graph_lock); + mapped = graph->oob_mem_map_handle; + map_uncertain = graph->oob_map_uncertain; + spin_unlock(&graph->apm->graph_lock); + if (mapped) + return 0; + if (!map_uncertain) { + if (!ret) + ret = -EIO; + goto free_buffer; + } + if (!ret) + ret = -EIO; + + /* + * An unanswered command does not prove that the DSP failed to map the + * buffer. Keep it allocated until reboot or a proven reset hook. + */ + return ret; + +free_buffer: + dma_free_coherent(graph->dma_dev, graph->oob_size, graph->oob_virt, + graph->oob_dma); + graph->oob_virt = NULL; + graph->oob_dma = 0; + graph->oob_dsp_addr = 0; + graph->oob_size = 0; + return ret; +} + +static int q6apm_unmap_oob_buffer(struct audioreach_graph *graph) +{ + struct apm_cmd_shared_mem_unmap_regions *cmd; + u32 opcode = APM_CMD_SHARED_MEM_UNMAP_REGIONS; + u32 mem_map_handle; + bool map_uncertain; + int ret = 0; + + mutex_lock(&graph->oob_lock); + if (graph->oob_transfer_uncertain) { + ret = -EAGAIN; + goto unlock; + } + + spin_lock(&graph->apm->graph_lock); + map_uncertain = graph->oob_map_uncertain; + if (map_uncertain) + mem_map_handle = 0; + else + mem_map_handle = graph->oob_mem_map_handle; + spin_unlock(&graph->apm->graph_lock); + if (map_uncertain) { + ret = -EAGAIN; + goto unlock; + } + + if (mem_map_handle) { + struct gpr_pkt *pkt __free(kfree) = NULL; + + pkt = audioreach_alloc_apm_cmd_pkt(sizeof(*cmd), opcode, graph->oob_token); + + if (IS_ERR(pkt)) { + ret = PTR_ERR(pkt); + } else { + cmd = (void *)pkt + GPR_HDR_SIZE; + cmd->mem_map_handle = mem_map_handle; + ret = q6apm_send_cmd_sync(graph->apm, pkt, opcode); + } + if (ret) + goto unlock; + spin_lock(&graph->apm->graph_lock); + mem_map_handle = graph->oob_mem_map_handle; + spin_unlock(&graph->apm->graph_lock); + if (mem_map_handle) { + ret = -EIO; + goto unlock; + } + } + + if (graph->oob_virt) { + dma_free_coherent(graph->dma_dev, graph->oob_size, + graph->oob_virt, graph->oob_dma); + graph->oob_virt = NULL; + graph->oob_dma = 0; + graph->oob_dsp_addr = 0; + graph->oob_size = 0; + } + +unlock: + mutex_unlock(&graph->oob_lock); + return ret; +} + +static int __q6apm_send_oob_config(struct audioreach_graph *graph, + struct q6apm_graph *client, + const void *data, size_t size) +{ + struct apm_cmd_header *cmd; + struct gpr_pkt *pkt; + int ret; + + if (!data || !size || size > graph->oob_size || + !graph->oob_virt || !graph->oob_mem_map_handle) + return -EINVAL; + + mutex_lock(&graph->oob_lock); + if (graph->oob_transfer_uncertain) { + ret = -EIO; + goto unlock; + } + memset(graph->oob_virt, 0, graph->oob_size); + memcpy(graph->oob_virt, data, size); + dma_wmb(); + + if (client) + pkt = audioreach_alloc_cmd_pkt(0, APM_CMD_SET_CFG, 0, + client->port->id, + APM_MODULE_INSTANCE_ID); + else + pkt = audioreach_alloc_apm_cmd_pkt(0, APM_CMD_SET_CFG, 0); + if (IS_ERR(pkt)) { + ret = PTR_ERR(pkt); + goto unlock; + } + + cmd = (void *)pkt + GPR_HDR_SIZE; + cmd->payload_address_lsw = lower_32_bits(graph->oob_dsp_addr); + cmd->payload_address_msw = upper_32_bits(graph->oob_dsp_addr); + cmd->mem_map_handle = graph->oob_mem_map_handle; + cmd->payload_size = size; + + if (client) + ret = audioreach_graph_send_cmd_sync(client, pkt, 0); + else + ret = q6apm_send_cmd_sync(graph->apm, pkt, 0); + kfree(pkt); + if (ret == -ETIMEDOUT || ret == -ESHUTDOWN) + graph->oob_transfer_uncertain = true; + +unlock: + mutex_unlock(&graph->oob_lock); + return ret; +} + +int q6apm_send_oob_config(struct audioreach_graph *graph, + const void *data, size_t size) +{ + return __q6apm_send_oob_config(graph, NULL, data, size); +} + +int q6apm_send_graph_oob_config(struct q6apm_graph *graph, + const void *data, size_t size) +{ + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + + if (!active) + return -ESHUTDOWN; + if (!graph->ar_graph) + return -ENODEV; + + return __q6apm_send_oob_config(graph->ar_graph, graph, data, size); } static struct audioreach_graph * @@ -189,6 +420,8 @@ q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id) { struct audioreach_graph_info *info; struct audioreach_graph *graph; + struct device *dma_dev; + size_t oob_size; int id; mutex_lock(&apm->lock); @@ -202,6 +435,7 @@ q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id) } spin_unlock(&apm->graph_lock); info = idr_find(&apm->graph_info_idr, graph_id); + dma_dev = !graph && info ? get_device(apm->dma_dev) : NULL; mutex_unlock(&apm->lock); if (graph) @@ -211,18 +445,23 @@ q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id) return ERR_PTR(-ENODEV); graph = kzalloc_obj(*graph); - if (!graph) + if (!graph) { + put_device(dma_dev); return ERR_PTR(-ENOMEM); + } graph->apm = apm; graph->info = info; graph->id = graph_id; + graph->dma_dev = dma_dev; graph->initializing = true; + mutex_init(&graph->oob_lock); graph->graph = audioreach_alloc_graph_pkt(apm, info); if (IS_ERR(graph->graph)) { void *err = graph->graph; + put_device(graph->dma_dev); kfree(graph); return ERR_CAST(err); } @@ -236,6 +475,7 @@ q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id) if (id < 0) { dev_err(apm->dev, "Unable to allocate graph id (%d)\n", graph_id); kfree(graph->graph); + put_device(graph->dma_dev); kfree(graph); mutex_unlock(&apm->lock); return ERR_PTR(id); @@ -252,6 +492,21 @@ q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id) if (id) goto remove_graph; + id = audioreach_graph_protection_oob_size(info, &oob_size); + if (id) { + dev_warn(apm->dev, + "graph OOB topology data is incomplete (%d)\n", id); + goto graph_ready; + } + if (oob_size) { + id = q6apm_map_oob_buffer(graph, oob_size); + if (id) + dev_warn(apm->dev, + "graph OOB map failed (%d); retaining uncertain ownership when required\n", + id); + } + +graph_ready: spin_lock(&apm->graph_lock); graph->initializing = false; spin_unlock(&apm->graph_lock); @@ -265,6 +520,7 @@ q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id) spin_unlock(&apm->graph_lock); mutex_unlock(&apm->lock); kfree(graph->graph); + put_device(graph->dma_dev); kfree(graph); return ERR_PTR(id); } @@ -302,11 +558,28 @@ static void q6apm_put_audioreach_graph(struct kref *ref) { struct audioreach_graph *graph; struct q6apm *apm; + int close_ret; + int unmap_ret = 0; graph = container_of(ref, struct audioreach_graph, refcount); apm = graph->apm; - audioreach_graph_mgmt_cmd(graph, APM_CMD_GRAPH_CLOSE); + /* Stop every DSP user before releasing the graph's OOB mapping. */ + close_ret = audioreach_graph_mgmt_cmd(graph, APM_CMD_GRAPH_CLOSE); + if (!close_ret) + unmap_ret = q6apm_unmap_oob_buffer(graph); + + if (graph->oob_virt) { + /* Keep the ID and DMA descriptor pinned until ownership is known. */ + spin_lock(&apm->graph_lock); + graph->initializing = true; + kref_init(&graph->refcount); + spin_unlock(&apm->graph_lock); + dev_err(apm->dev, + "retaining graph %u OOB DMA until reboot after teardown failure (close %d, unmap %d)\n", + graph->id, close_ret, unmap_ret); + return; + } mutex_lock(&apm->lock); spin_lock(&apm->graph_lock); @@ -315,6 +588,7 @@ static void q6apm_put_audioreach_graph(struct kref *ref) mutex_unlock(&apm->lock); kfree(graph->graph); + put_device(graph->dma_dev); kfree(graph); } @@ -1349,8 +1623,16 @@ static void q6apm_remove_graph_state(struct q6apm *apm) guard(mutex)(&apm->client_lock); while ((ar_graph = idr_get_next(&apm->graph_idr, &id))) { idr_remove(&apm->graph_idr, id); - kfree(ar_graph->graph); - kfree(ar_graph); + if (ar_graph->oob_virt) { + dev_err(apm->dev, + "retaining graph %u OOB DMA until reboot after service removal\n", + ar_graph->id); + /* Keep the DMA descriptor and its device reference pinned. */ + } else { + kfree(ar_graph->graph); + put_device(ar_graph->dma_dev); + kfree(ar_graph); + } id++; } } @@ -1421,6 +1703,7 @@ static bool q6apm_try_complete_cmd(struct q6apm *apm, static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) { gpr_device_t *gdev = priv; + struct audioreach_graph *graph = NULL; struct audioreach_graph_info *info = NULL; struct q6apm *apm = dev_get_drvdata(&gdev->dev); struct apm_cmd_rsp_shared_mem_map_regions *rsp; @@ -1428,7 +1711,7 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) struct gpr_ibasic_rsp_result_t *result; const struct gpr_hdr *hdr = &data->hdr; bool expected; - int graph_id, is_pos_buf; + int graph_id, is_oob, is_pos_buf; result = data->payload; @@ -1451,12 +1734,32 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) case GPR_BASIC_RSP_RESULT: switch (result->opcode) { case APM_CMD_SHARED_MEM_MAP_REGIONS: - expected = q6apm_try_complete_cmd(apm, hdr, - result->opcode, - result->status, true); - if (expected && result->status) - dev_err(dev, "Error (%d) Processing 0x%08x cmd\n", - result->status, result->opcode); + spin_lock(&apm->result_lock); + expected = __q6apm_cmd_response_expected(apm, hdr, + result->opcode, + result->status, true); + if (expected && result->status && + (hdr->token & APM_MMAP_TOKEN_MAP_TYPE_OOB)) { + graph_id = hdr->token & APM_MMAP_TOKEN_GID_MASK; + spin_lock(&apm->graph_lock); + graph = idr_find(&apm->graph_idr, graph_id); + if (graph && graph->oob_token == hdr->token) + graph->oob_map_uncertain = false; + spin_unlock(&apm->graph_lock); + } + if (expected) { + apm->result.status = result->status; + apm->result_token = hdr->token; + apm->result.opcode = result->opcode; + } + spin_unlock(&apm->result_lock); + if (expected) { + wake_up(&apm->wait); + if (result->status) + dev_err(dev, + "Error (%d) Processing 0x%08x cmd\n", + result->status, result->opcode); + } break; case APM_CMD_GRAPH_START: case APM_CMD_GRAPH_OPEN: @@ -1486,18 +1789,29 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) } graph_id = hdr->token & APM_MMAP_TOKEN_GID_MASK; + is_oob = hdr->token & APM_MMAP_TOKEN_MAP_TYPE_OOB; is_pos_buf = hdr->token & APM_MMAP_TOKEN_MAP_TYPE_POS_BUF; spin_lock(&apm->result_lock); expected = __q6apm_cmd_response_expected(apm, hdr, result->opcode, 0, true); - if (expected) { + if (is_oob && expected) { + spin_lock(&apm->graph_lock); + graph = idr_find(&apm->graph_idr, graph_id); + if (graph && graph->oob_token == hdr->token) + graph->oob_mem_map_handle = 0; + else + graph = NULL; + spin_unlock(&apm->graph_lock); + } else if (expected) { info = idr_find(&apm->graph_info_idr, graph_id); if (info && is_pos_buf) info->pos_buf_mem_map_handle = 0; else if (info) info->mem_map_handle = 0; + } + if (expected) { apm->result.status = 0; apm->result_token = hdr->token; apm->result.opcode = result->opcode; @@ -1505,7 +1819,7 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) spin_unlock(&apm->result_lock); if (expected) { wake_up(&apm->wait); - if (!info) + if ((!is_oob && !info) || (is_oob && !graph)) dev_err(dev, "no mapping for 0x%08x response token 0x%08x\n", result->opcode, hdr->token); @@ -1518,14 +1832,26 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) case APM_CMD_RSP_SHARED_MEM_MAP_REGIONS: rsp = data->payload; graph_id = hdr->token & APM_MMAP_TOKEN_GID_MASK; + is_oob = hdr->token & APM_MMAP_TOKEN_MAP_TYPE_OOB; is_pos_buf = hdr->token & APM_MMAP_TOKEN_MAP_TYPE_POS_BUF; spin_lock(&apm->result_lock); expected = __q6apm_cmd_response_expected(apm, hdr, hdr->opcode, 0, false); - if (expected) + if (is_oob && expected) { + spin_lock(&apm->graph_lock); + graph = idr_find(&apm->graph_idr, graph_id); + if (graph && graph->oob_token == hdr->token) { + graph->oob_mem_map_handle = rsp->mem_map_handle; + graph->oob_map_uncertain = false; + } else { + graph = NULL; + } + spin_unlock(&apm->graph_lock); + } else if (expected) { info = idr_find(&apm->graph_info_idr, graph_id); - if (info) { + } + if (!is_oob && info) { if (is_pos_buf) info->pos_buf_mem_map_handle = rsp->mem_map_handle; else @@ -1540,10 +1866,11 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) spin_unlock(&apm->result_lock); if (expected) wake_up(&apm->wait); - if (expected && !info) + if (expected && ((!is_oob && !info) || (is_oob && !graph))) { dev_err(dev, "no graph for 0x%08x response token 0x%08x\n", hdr->opcode, hdr->token); + } break; default: break; diff --git a/sound/soc/qcom/qdsp6/q6apm.h b/sound/soc/qcom/qdsp6/q6apm.h index f586acd563e1d7..e3ed71cc35809c 100644 --- a/sound/soc/qcom/qdsp6/q6apm.h +++ b/sound/soc/qcom/qdsp6/q6apm.h @@ -53,6 +53,7 @@ struct q6apm { struct device *dev; + struct device *dma_dev; gpr_port_t *port; gpr_device_t *gdev; /* For Graph OPEN/START/STOP/CLOSE operations */ @@ -103,6 +104,17 @@ struct audioreach_graph { int state; int start_count; bool initializing; + struct device *dma_dev; + void *oob_virt; + dma_addr_t oob_dma; + phys_addr_t oob_dsp_addr; + size_t oob_size; + u32 oob_mem_map_handle; + u32 oob_token; + /* Serialize use of the graph's shared OOB buffer. */ + struct mutex oob_lock; + bool oob_map_uncertain; + bool oob_transfer_uncertain; /* Cached Graph data */ void *graph; struct kref refcount; @@ -186,6 +198,10 @@ int q6apm_unmap_memory_fixed_region(struct device *dev, unsigned int graph_id); /* Helpers */ int q6apm_send_cmd_sync(struct q6apm *apm, struct gpr_pkt *pkt, uint32_t rsp_opcode); +int q6apm_send_oob_config(struct audioreach_graph *graph, + const void *data, size_t size); +int q6apm_send_graph_oob_config(struct q6apm_graph *graph, + const void *data, size_t size); /* Callback for graph specific */ struct audioreach_module *q6apm_find_module_by_mid(struct q6apm_graph *graph, From d6670b73f922945e8b82f65f90218493037dfdd6 Mon Sep 17 00:00:00 2001 From: geoca <272055834+geocausa@users.noreply.github.com> Date: Sun, 23 Aug 2026 08:07:44 +0100 Subject: [PATCH 13/20] ASoC: q6apm: run FullIO protection per graph FullIO v19c describes protected-speaker calibration and runtime stages in topology. Execute graph calibration and ordered protection or bypass SET_CFG payloads on the owning graph instead of using global card state or hard-coded module instance IDs. Keep configuration, bypass proof, backend readiness, start references, and faults per graph. Enable this path only for Denali PCM clients and a topology-declared protected graph; reject partial profiles and mixed ordinary or protected clients. Start only after protected configuration or both SP and SPVI bypass commands are confirmed. Correlate every state transition and make final close consume-on-success. If execution, close, OOB, or mapping ownership is unconfirmed, block reuse and retain the complete client, callback context, and DMA ownership until reboot or a future proven reset hook. [Leon: limited the runtime to Denali plus a topology-declared protected graph, made malformed or unconfirmed setup fail closed, serialized backend/readiness and start references, enforced immutable shared runtime modes, and quarantined callbacks, clients, and DMA after unconfirmed teardown.] Link: https://github.com/geocausa/SP11X1e-audio/commit/db2af54947d04d8ec5d54a1dc248b3ff09e5c320 Link: https://github.com/geocausa/SP11X1e-audio/commit/1ca382057d302aed20525822d02d9a229b7f3ad9 Link: https://github.com/geocausa/SP11X1e-audio/commit/33d5145179c7b75913c85c31906a9f2fa14ab695 Link: https://github.com/geocausa/SP11X1e-audio/commit/31466d62b38c8138d476b2fd3606a76272fb3d34 Link: https://github.com/geocausa/SP11X1e-audio/commit/7af8f21e9966f6adb40c102653b6acb5d81742 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/22bdec204438f5e1e19286cb7f1688f196c51967 Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/audioreach.c | 422 ++++++++++++++++++++- sound/soc/qcom/qdsp6/audioreach.h | 5 + sound/soc/qcom/qdsp6/q6apm-dai.c | 353 ++++++++++++++++-- sound/soc/qcom/qdsp6/q6apm-lpass-dais.c | 24 +- sound/soc/qcom/qdsp6/q6apm.c | 467 ++++++++++++++++++++++-- sound/soc/qcom/qdsp6/q6apm.h | 29 +- 6 files changed, 1221 insertions(+), 79 deletions(-) diff --git a/sound/soc/qcom/qdsp6/audioreach.c b/sound/soc/qcom/qdsp6/audioreach.c index f17a6a7a86bd1b..e609d115d9931e 100644 --- a/sound/soc/qcom/qdsp6/audioreach.c +++ b/sound/soc/qcom/qdsp6/audioreach.c @@ -777,6 +777,144 @@ audioreach_graph_find_data(const struct audioreach_graph_info *info, u32 type) return found; } +const struct audioreach_module * +audioreach_graph_find_module(const struct audioreach_graph_info *info, u32 mid) +{ + const struct audioreach_module *found = NULL; + struct audioreach_container *container; + struct audioreach_sub_graph *sg; + struct audioreach_module *module; + + list_for_each_entry(sg, &info->sg_list, node) { + list_for_each_entry(container, &sg->container_list, node) { + list_for_each_entry(module, &container->modules_list, node) { + if (module->module_id != mid) + continue; + if (found) + return ERR_PTR(-EEXIST); + found = module; + } + } + } + + return found; +} + +static bool +audioreach_graph_has_iid(const struct audioreach_graph_info *info, u32 iid) +{ + struct audioreach_container *container; + struct audioreach_sub_graph *sg; + struct audioreach_module *module; + + list_for_each_entry(sg, &info->sg_list, node) + list_for_each_entry(container, &sg->container_list, node) + list_for_each_entry(module, &container->modules_list, node) + if (module->instance_id == iid) + return true; + + return false; +} + +static int audioreach_validate_stage(const struct audioreach_graph_info *info, + const struct audioreach_module_priv_data *stage) +{ + const u8 *cursor = (const u8 *)stage->data; + size_t remaining = le32_to_cpu(stage->size); + + if (!remaining || !IS_ALIGNED(remaining, 8)) + return -EINVAL; + + while (remaining) { + size_t frame_size; + u32 param_size; + + if (remaining < sizeof(struct apm_module_param_data)) + return -EINVAL; + + param_size = get_unaligned_le32(cursor + 2 * sizeof(u32)); + if (check_add_overflow(sizeof(struct apm_module_param_data), + (size_t)param_size, &frame_size)) + return -EOVERFLOW; + frame_size = ALIGN(frame_size, 8); + if (frame_size > remaining) + return -EINVAL; + if (!audioreach_graph_has_iid(info, + get_unaligned_le32(cursor))) + return -ENOENT; + + cursor += frame_size; + remaining -= frame_size; + } + + return 0; +} + +int audioreach_graph_protection_profile(const struct audioreach_graph_info *info) +{ + static const u32 stage_types[] = { + SND_SOC_AR_TPLG_GRAPH_CAL_CFG_TYPE, + SND_SOC_AR_TPLG_RENDER_EP_CFG_TYPE, + SND_SOC_AR_TPLG_SP_TAG_CFG_TYPE, + SND_SOC_AR_TPLG_SPVI_TAG_CFG_TYPE, + SND_SOC_AR_TPLG_VI_EP_CFG_TYPE, + SND_SOC_AR_TPLG_PROTECTION_DYNAMIC_CFG_TYPE, + SND_SOC_AR_TPLG_VOLUME_GAIN_CFG_TYPE, + SND_SOC_AR_TPLG_VOLUME_FILTER_CFG_TYPE, + SND_SOC_AR_TPLG_VOLUME_MUTE_CFG_TYPE, + SND_SOC_AR_TPLG_CHANNEL_MIXER_CFG_TYPE, + }; + struct audioreach_container *container; + const struct audioreach_module_priv_data *data; + const struct audioreach_module *sp, *spvi; + struct audioreach_sub_graph *sg; + struct audioreach_module *module; + bool declared = false; + int i, ret; + + for (i = 0; i < ARRAY_SIZE(stage_types); i++) { + data = audioreach_graph_find_data(info, stage_types[i]); + if (IS_ERR(data)) + return PTR_ERR(data); + if (data) + declared = true; + } + list_for_each_entry(sg, &info->sg_list, node) + list_for_each_entry(container, &sg->container_list, node) + list_for_each_entry(module, &container->modules_list, node) + if (module->speaker_protection_bypass || + module->integrated_backend_id) + declared = true; + if (!declared) + return 0; + + for (i = 0; i < ARRAY_SIZE(stage_types); i++) { + data = audioreach_graph_find_data(info, stage_types[i]); + if (IS_ERR(data)) + return PTR_ERR(data); + if (!data || !le32_to_cpu(data->size) || + !IS_ALIGNED(le32_to_cpu(data->size), 8)) + return -EINVAL; + /* The graph-calibration aggregate is opaque and may carry sentinels. */ + if (i) { + ret = audioreach_validate_stage(info, data); + if (ret) + return ret; + } + } + + sp = audioreach_graph_find_module(info, MODULE_ID_SPEAKER_PROTECTION); + spvi = audioreach_graph_find_module(info, + MODULE_ID_SPEAKER_PROTECTION_VI); + if (IS_ERR(sp) || IS_ERR(spvi)) + return -EEXIST; + if (!sp || !spvi || !sp->speaker_protection_bypass || + !spvi->speaker_protection_bypass) + return -EINVAL; + + return 1; +} + int audioreach_graph_protection_oob_size(const struct audioreach_graph_info *info, size_t *size) { @@ -791,15 +929,12 @@ int audioreach_graph_protection_oob_size(const struct audioreach_graph_info *inf }; const struct audioreach_module_priv_data *data; size_t max_size = 0; - int i; + int i, ret; - data = audioreach_graph_find_data(info, - SND_SOC_AR_TPLG_GRAPH_CAL_CFG_TYPE); - if (IS_ERR(data)) - return PTR_ERR(data); - if (!data) { + ret = audioreach_graph_protection_profile(info); + if (ret <= 0) { *size = 0; - return 0; + return ret; } for (i = 0; i < ARRAY_SIZE(oob_types); i++) { @@ -815,6 +950,260 @@ int audioreach_graph_protection_oob_size(const struct audioreach_graph_info *inf return 0; } +int audioreach_send_protected_graph_calibration(struct audioreach_graph *graph) +{ + const struct audioreach_module_priv_data *graph_cal; + int ret; + + ret = audioreach_graph_protection_profile(graph->info); + if (ret <= 0) + return ret < 0 ? ret : 0; + + graph_cal = audioreach_graph_find_data(graph->info, + SND_SOC_AR_TPLG_GRAPH_CAL_CFG_TYPE); + ret = q6apm_send_oob_config(graph, graph_cal->data, + le32_to_cpu(graph_cal->size)); + if (ret == -EOPNOTSUPP) { + /* Qualcomm calibration aggregates may contain query-only records. */ + dev_warn(graph->apm->dev, + "graph calibration returned AR_EUNSUPPORTED; continuing\n"); + ret = 0; + } + + return ret; +} + +static size_t audioreach_frame_size(const u8 *frame) +{ + return ALIGN(sizeof(struct apm_module_param_data) + + get_unaligned_le32(frame + 2 * sizeof(u32)), 8); +} + +static int audioreach_send_inband_frame(struct q6apm_graph *graph, + const u8 *frame, size_t frame_size) +{ + struct gpr_pkt *pkt __free(kfree) = NULL; + void *payload; + + pkt = audioreach_alloc_cmd_pkt(frame_size, APM_CMD_SET_CFG, 0, graph->port->id, + get_unaligned_le32(frame)); + if (IS_ERR(pkt)) + return PTR_ERR(pkt); + + payload = (u8 *)pkt + GPR_HDR_SIZE + APM_CMD_HDR_SIZE; + memcpy(payload, frame, frame_size); + + return audioreach_graph_send_cmd_sync(graph, pkt, 0); +} + +static int audioreach_send_inband_stage(struct q6apm_graph *graph, + const struct audioreach_module_priv_data *stage, + size_t offset) +{ + const u8 *cursor = (const u8 *)stage->data + offset; + size_t remaining = le32_to_cpu(stage->size) - offset; + int ret; + + while (remaining) { + size_t frame_size = audioreach_frame_size(cursor); + + ret = audioreach_send_inband_frame(graph, cursor, frame_size); + if (ret) + return ret; + cursor += frame_size; + remaining -= frame_size; + } + + return 0; +} + +static int audioreach_graph_module_enable(struct q6apm_graph *graph, + const struct audioreach_module *module, + bool enable) +{ + struct gpr_pkt *pkt __free(kfree) = NULL; + struct apm_module_param_data *param; + u32 *value; + + pkt = audioreach_alloc_cmd_pkt(APM_MODULE_PARAM_DATA_SIZE + sizeof(*value), + APM_CMD_SET_CFG, 0, graph->port->id, + module->instance_id); + if (IS_ERR(pkt)) + return PTR_ERR(pkt); + + param = (void *)pkt + GPR_HDR_SIZE + APM_CMD_HDR_SIZE; + param->module_instance_id = module->instance_id; + param->param_id = PARAM_ID_MODULE_ENABLE; + param->param_size = sizeof(*value); + value = (void *)param + APM_MODULE_PARAM_DATA_SIZE; + *value = enable; + + return audioreach_graph_send_cmd_sync(graph, pkt, 0); +} + +static int audioreach_protection_enable(struct q6apm_graph *graph, + bool enable) +{ + const struct audioreach_module *first, *second; + const struct audioreach_module *sp, *spvi; + int ret; + + sp = audioreach_graph_find_module(graph->info, + MODULE_ID_SPEAKER_PROTECTION); + spvi = audioreach_graph_find_module(graph->info, + MODULE_ID_SPEAKER_PROTECTION_VI); + if (IS_ERR_OR_NULL(sp) || IS_ERR_OR_NULL(spvi)) + return -ENODEV; + + /* Bring feedback up first; tear render down first. */ + first = enable ? spvi : sp; + second = enable ? sp : spvi; + ret = audioreach_graph_module_enable(graph, first, enable); + if (ret) + return ret; + + return audioreach_graph_module_enable(graph, second, enable); +} + +static int audioreach_send_oob_stage(struct q6apm_graph *graph, u32 type) +{ + const struct audioreach_module_priv_data *stage; + + stage = audioreach_graph_find_data(graph->info, type); + if (IS_ERR_OR_NULL(stage)) + return stage ? PTR_ERR(stage) : -ENODATA; + + return q6apm_send_graph_oob_config(graph, stage->data, + le32_to_cpu(stage->size)); +} + +int audioreach_configure_protection(struct q6apm_graph *graph) +{ + const struct audioreach_module_priv_data *dynamic, *gain, *mute; + struct audioreach_graph *ar_graph = graph->ar_graph; + size_t first_size; + int bypass_ret; + int ret = 0; + + mutex_lock(&ar_graph->protection_lock); + if (ar_graph->protection_faulted) { + ret = -EIO; + goto unlock; + } + if (ar_graph->protection_configured || + ar_graph->protection_bypass_confirmed) + goto unlock; + if (!ar_graph->protection_available || + READ_ONCE(ar_graph->oob_transfer_uncertain)) { + ret = audioreach_protection_enable(graph, false); + if (ret) { + ar_graph->protection_faulted = true; + dev_err(graph->dev, + "protected graph calibration is unavailable; bypass failed (%d)\n", + ret); + } else { + ar_graph->protection_bypass_confirmed = true; + dev_warn(graph->dev, + "protected graph calibration is unavailable; using bypass\n"); + } + goto unlock; + } + + if (!ar_graph->protection_vi_ready || + !ar_graph->protection_cps_ready) { + ret = audioreach_protection_enable(graph, false); + if (ret) { + ar_graph->protection_faulted = true; + dev_err(graph->dev, + "protected speaker feedback is incomplete; bypass failed (%d)\n", + ret); + } else { + ar_graph->protection_bypass_confirmed = true; + dev_warn(graph->dev, + "protected speaker feedback is incomplete; using bypass\n"); + } + goto unlock; + } + + dynamic = audioreach_graph_find_data(graph->info, + SND_SOC_AR_TPLG_PROTECTION_DYNAMIC_CFG_TYPE); + gain = audioreach_graph_find_data(graph->info, + SND_SOC_AR_TPLG_VOLUME_GAIN_CFG_TYPE); + mute = audioreach_graph_find_data(graph->info, + SND_SOC_AR_TPLG_VOLUME_MUTE_CFG_TYPE); + if (IS_ERR_OR_NULL(dynamic) || IS_ERR_OR_NULL(gain) || + IS_ERR_OR_NULL(mute)) { + ret = -ENODATA; + goto bypass; + } + + first_size = audioreach_frame_size((const u8 *)dynamic->data); + ret = audioreach_send_inband_frame(graph, + (const u8 *)dynamic->data, + first_size); + if (ret) + goto bypass; + ret = audioreach_send_oob_stage(graph, + SND_SOC_AR_TPLG_SP_TAG_CFG_TYPE); + if (ret) + goto bypass; + ret = audioreach_send_inband_stage(graph, dynamic, first_size); + if (ret) + goto bypass; + ret = audioreach_send_oob_stage(graph, + SND_SOC_AR_TPLG_SPVI_TAG_CFG_TYPE); + if (ret) + goto bypass; + ret = audioreach_send_oob_stage(graph, + SND_SOC_AR_TPLG_RENDER_EP_CFG_TYPE); + if (ret) + goto bypass; + ret = audioreach_send_oob_stage(graph, + SND_SOC_AR_TPLG_VI_EP_CFG_TYPE); + if (ret) + goto bypass; + ret = audioreach_protection_enable(graph, true); + if (ret) + goto bypass; + ret = audioreach_send_inband_stage(graph, gain, 0); + if (ret) + goto bypass; + ret = audioreach_send_oob_stage(graph, + SND_SOC_AR_TPLG_VOLUME_FILTER_CFG_TYPE); + if (ret) + goto bypass; + ret = audioreach_send_inband_stage(graph, mute, 0); + if (ret) + goto bypass; + ret = audioreach_send_oob_stage(graph, + SND_SOC_AR_TPLG_CHANNEL_MIXER_CFG_TYPE); + if (ret) + goto bypass; + + ar_graph->protection_configured = true; + ar_graph->protection_bypass_confirmed = false; + goto unlock; + +bypass: + bypass_ret = audioreach_protection_enable(graph, false); + if (!bypass_ret) { + ar_graph->protection_bypass_confirmed = true; + dev_err(graph->dev, + "protected speaker setup failed (%d); using bypass\n", ret); + ret = 0; + } else { + ar_graph->protection_faulted = true; + dev_err(graph->dev, + "protected speaker setup failed (%d), bypass failed (%d)\n", + ret, bypass_ret); + ret = bypass_ret; + } + +unlock: + mutex_unlock(&ar_graph->protection_lock); + return ret; +} + int audioreach_send_cmd_sync(struct device *dev, gpr_device_t *gdev, struct gpr_ibasic_rsp_result_t *result, struct mutex *cmd_lock, gpr_port_t *port, wait_queue_head_t *cmd_wait, @@ -1154,7 +1543,9 @@ static int audioreach_mfc_set_media_format(struct q6apm_graph *graph, int i; void *p; - struct gpr_pkt *pkt __free(kfree) = audioreach_alloc_apm_cmd_pkt(payload_size, APM_CMD_SET_CFG, 0); + struct gpr_pkt *pkt __free(kfree) = + audioreach_alloc_cmd_pkt(payload_size, APM_CMD_SET_CFG, 0, graph->port->id, + module->instance_id); if (IS_ERR(pkt)) return PTR_ERR(pkt); @@ -1174,7 +1565,7 @@ static int audioreach_mfc_set_media_format(struct q6apm_graph *graph, for (i = 0; i < num_channels; i++) media_format->channel_mapping[i] = cfg->channel_map[i]; - return q6apm_send_cmd_sync(graph->apm, pkt, 0); + return audioreach_graph_send_cmd_sync(graph, pkt, 0); } static int audioreach_set_compr_media_format(struct media_format *media_fmt_hdr, @@ -1406,7 +1797,8 @@ static int audioreach_pcm_set_media_format(struct q6apm_graph *graph, payload_size = APM_PCM_MODULE_FMT_CMD_PSIZE(num_channels); struct gpr_pkt *pkt __free(kfree) = - audioreach_alloc_apm_cmd_pkt(payload_size, APM_CMD_SET_CFG, 0); + audioreach_alloc_cmd_pkt(payload_size, APM_CMD_SET_CFG, 0, graph->port->id, + module->instance_id); if (IS_ERR(pkt)) return PTR_ERR(pkt); @@ -1432,7 +1824,7 @@ static int audioreach_pcm_set_media_format(struct q6apm_graph *graph, media_cfg->bits_per_sample = mcfg->bit_width; memcpy(media_cfg->channel_mapping, mcfg->channel_map, mcfg->num_channels); - return q6apm_send_cmd_sync(graph->apm, pkt, 0); + return audioreach_graph_send_cmd_sync(graph, pkt, 0); } int audioreach_shmem_register_event(struct q6apm_graph *graph, int bytes, int num_levels) @@ -1717,6 +2109,10 @@ int audioreach_set_media_format(struct q6apm_graph *graph, rc = audioreach_gapless_set_media_format(graph, module, cfg); break; case MODULE_ID_SPEAKER_PROTECTION: + if (module->speaker_protection_bypass) { + rc = 0; + break; + } rc = audioreach_speaker_protection(graph, module, PARAM_ID_SP_OP_MODE_NORMAL); if (!rc) @@ -1724,6 +2120,10 @@ int audioreach_set_media_format(struct q6apm_graph *graph, break; case MODULE_ID_SPEAKER_PROTECTION_VI: + if (module->speaker_protection_bypass) { + rc = 0; + break; + } rc = audioreach_speaker_protection_vi(graph, module, cfg); if (!rc) rc = audioreach_module_enable(graph, module, true); diff --git a/sound/soc/qcom/qdsp6/audioreach.h b/sound/soc/qcom/qdsp6/audioreach.h index 04f0f61130e728..1c641d17102954 100644 --- a/sound/soc/qcom/qdsp6/audioreach.h +++ b/sound/soc/qcom/qdsp6/audioreach.h @@ -946,8 +946,13 @@ void *audioreach_alloc_graph_pkt(struct q6apm *apm, const struct audioreach_graph_info *info); const struct audioreach_module_priv_data * audioreach_graph_find_data(const struct audioreach_graph_info *info, u32 type); +const struct audioreach_module * +audioreach_graph_find_module(const struct audioreach_graph_info *info, u32 mid); +int audioreach_graph_protection_profile(const struct audioreach_graph_info *info); int audioreach_graph_protection_oob_size(const struct audioreach_graph_info *info, size_t *size); +int audioreach_send_protected_graph_calibration(struct audioreach_graph *graph); +int audioreach_configure_protection(struct q6apm_graph *graph); /* Topology specific */ int audioreach_tplg_init(struct snd_soc_component *component); diff --git a/sound/soc/qcom/qdsp6/q6apm-dai.c b/sound/soc/qcom/qdsp6/q6apm-dai.c index 3052831ba313f0..82b2cd249040f3 100644 --- a/sound/soc/qcom/qdsp6/q6apm-dai.c +++ b/sound/soc/qcom/qdsp6/q6apm-dai.c @@ -2,11 +2,13 @@ // Copyright (c) 2021, Linaro Limited #include +#include #include #include #include #include #include +#include #include #include #include @@ -55,6 +57,8 @@ enum stream_state { Q6APM_STREAM_IDLE = 0, Q6APM_STREAM_STOPPED, Q6APM_STREAM_RUNNING, + Q6APM_STREAM_UNCERTAIN, + Q6APM_STREAM_QUARANTINED, }; struct q6apm_dai_rtd { @@ -80,13 +84,66 @@ struct q6apm_dai_rtd { enum stream_state state; struct q6apm_graph *graph; spinlock_t lock; + /* Serialize callback admission against terminal DMA quarantine. */ + spinlock_t callback_lock; + atomic_t callback_users; + wait_queue_head_t callback_wait; + bool callbacks_quarantined; bool notify_on_drain; }; struct q6apm_dai_data { long long sid; + DECLARE_BITMAP(protected_graphs, APM_PORT_MAX); }; +static bool q6apm_denali_rtd(const struct snd_soc_pcm_runtime *rtd) +{ + return rtd && rtd->card && + rtd->card->dev->of_node && + of_device_is_compatible(rtd->card->dev->of_node, + "microsoft,denali-sndcard"); +} + +static bool q6apm_denali_card(struct snd_pcm_substream *substream) +{ + return q6apm_denali_rtd(snd_soc_substream_to_rtd(substream)); +} + +static bool q6apm_denali_protection_runtime(struct snd_pcm_substream *substream, + struct q6apm_graph *graph) +{ + return q6apm_denali_card(substream) && + q6apm_graph_has_protection(graph); +} + +static bool q6apm_denali_protected_graph(struct snd_soc_component *component, + struct snd_soc_pcm_runtime *rtd, + unsigned int graph_id) +{ + return q6apm_denali_rtd(rtd) && graph_id < APM_PORT_MAX && + q6apm_graph_id_has_protection(component->dev, graph_id); +} + +static void q6apm_latch_protected_graph(struct snd_soc_component *component, + unsigned int graph_id) +{ + struct q6apm_dai_data *pdata = snd_soc_component_get_drvdata(component); + + if (pdata && graph_id < APM_PORT_MAX) + set_bit(graph_id, pdata->protected_graphs); +} + +static bool q6apm_protected_graph_latched(struct snd_soc_component *component, + struct snd_soc_pcm_runtime *rtd, + unsigned int graph_id) +{ + struct q6apm_dai_data *pdata = snd_soc_component_get_drvdata(component); + + return pdata && q6apm_denali_rtd(rtd) && graph_id < APM_PORT_MAX && + test_bit(graph_id, pdata->protected_graphs); +} + static const struct snd_pcm_hardware q6apm_dai_hardware_capture = { .info = (SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_BLOCK_TRANSFER | SNDRV_PCM_INFO_MMAP_VALID | SNDRV_PCM_INFO_INTERLEAVED | @@ -127,10 +184,36 @@ static const struct snd_pcm_hardware q6apm_dai_hardware_playback = { .fifo_size = 0, }; +static bool q6apm_dai_callback_get(struct q6apm_dai_rtd *prtd) +{ + unsigned long flags; + bool acquired = false; + + spin_lock_irqsave(&prtd->callback_lock, flags); + if (!prtd->callbacks_quarantined) { + atomic_inc(&prtd->callback_users); + acquired = true; + } + spin_unlock_irqrestore(&prtd->callback_lock, flags); + + return acquired; +} + +static void q6apm_dai_callback_put(struct q6apm_dai_rtd *prtd) +{ + if (atomic_dec_and_test(&prtd->callback_users)) + wake_up(&prtd->callback_wait); +} + static void event_handler(uint32_t opcode, uint32_t token, void *payload, void *priv) { struct q6apm_dai_rtd *prtd = priv; - struct snd_pcm_substream *substream = prtd->substream; + struct snd_pcm_substream *substream; + + if (!q6apm_dai_callback_get(prtd)) + return; + + substream = prtd->substream; switch (opcode) { case APM_CLIENT_EVENT_WATERMARK_EVENT: @@ -152,6 +235,8 @@ static void event_handler(uint32_t opcode, uint32_t token, void *payload, void * default: break; } + + q6apm_dai_callback_put(prtd); } static void event_handler_compr(uint32_t opcode, uint32_t token, @@ -219,6 +304,7 @@ static int q6apm_dai_prepare(struct snd_soc_component *component, struct audioreach_module_config cfg; struct device *dev = component->dev; struct q6apm_dai_data *pdata; + bool protected; int ret; pdata = snd_soc_component_get_drvdata(component); @@ -229,6 +315,9 @@ static int q6apm_dai_prepare(struct snd_soc_component *component, dev_err(dev, "%s: private data null or audio client freed\n", __func__); return -EINVAL; } + if (prtd->state == Q6APM_STREAM_QUARANTINED) + return -EIO; + protected = q6apm_denali_protection_runtime(substream, prtd->graph); cfg.direction = substream->stream; cfg.sample_rate = runtime->rate; @@ -238,7 +327,15 @@ static int q6apm_dai_prepare(struct snd_soc_component *component, audioreach_set_default_channel_mapping(cfg.channel_map, runtime->channels); if (prtd->state) { /* clear the previous setup if any */ - q6apm_graph_stop(prtd->graph); + if (!protected || prtd->state == Q6APM_STREAM_RUNNING || + prtd->state == Q6APM_STREAM_UNCERTAIN) { + ret = q6apm_graph_stop(prtd->graph); + if (ret) { + dev_err(dev, "Failed to stop Graph %d\n", ret); + return ret; + } + prtd->state = Q6APM_STREAM_STOPPED; + } q6apm_free_fragments(prtd->graph, substream->stream); } @@ -285,6 +382,14 @@ static int q6apm_dai_prepare(struct snd_soc_component *component, return ret; } + if (protected) { + ret = q6apm_graph_configure_protection(prtd->graph); + if (ret < 0) { + dev_err(dev, "Failed to configure protected graph %d\n", ret); + return ret; + } + } + ret = q6apm_graph_prepare(prtd->graph); if (ret) { dev_err(dev, "Failed to prepare Graph %d\n", ret); @@ -293,6 +398,8 @@ static int q6apm_dai_prepare(struct snd_soc_component *component, ret = q6apm_graph_start(prtd->graph); if (ret) { + if (q6apm_graph_execution_uncertain(prtd->graph)) + prtd->state = Q6APM_STREAM_UNCERTAIN; dev_err(dev, "Failed to Start Graph %d\n", ret); return ret; } @@ -340,8 +447,12 @@ static int q6apm_dai_trigger(struct snd_soc_component *component, { struct snd_pcm_runtime *runtime = substream->runtime; struct q6apm_dai_rtd *prtd = runtime->private_data; + bool protected; int ret = 0; + protected = q6apm_denali_protection_runtime(substream, prtd->graph); + if (prtd->state == Q6APM_STREAM_QUARANTINED) + return -EIO; switch (cmd) { case SNDRV_PCM_TRIGGER_START: case SNDRV_PCM_TRIGGER_RESUME: @@ -349,6 +460,13 @@ static int q6apm_dai_trigger(struct snd_soc_component *component, break; case SNDRV_PCM_TRIGGER_STOP: /* TODO support be handled via SoftPause Module */ + if (protected && + (prtd->state == Q6APM_STREAM_RUNNING || + prtd->state == Q6APM_STREAM_UNCERTAIN)) { + ret = q6apm_graph_stop(prtd->graph); + if (ret) + break; + } prtd->state = Q6APM_STREAM_STOPPED; prtd->queue_ptr = 0; prtd->last_pos_index = 0; @@ -364,6 +482,11 @@ static int q6apm_dai_trigger(struct snd_soc_component *component, return ret; } +static void q6apm_dai_quarantine_buffer(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct q6apm_dai_rtd *prtd, + int reason); + static int q6apm_dai_open(struct snd_soc_component *component, struct snd_pcm_substream *substream) { @@ -373,7 +496,7 @@ static int q6apm_dai_open(struct snd_soc_component *component, struct device *dev = component->dev; struct q6apm_dai_data *pdata; struct q6apm_dai_rtd *prtd; - int graph_id, ret; + int close_ret, graph_id, ret; graph_id = cpu_dai->driver->id; @@ -388,8 +511,13 @@ static int q6apm_dai_open(struct snd_soc_component *component, return -ENOMEM; spin_lock_init(&prtd->lock); + spin_lock_init(&prtd->callback_lock); + atomic_set(&prtd->callback_users, 0); + init_waitqueue_head(&prtd->callback_wait); prtd->substream = substream; - prtd->graph = q6apm_graph_open(dev, event_handler, prtd, graph_id, substream->stream); + prtd->graph = q6apm_graph_open(dev, event_handler, prtd, + graph_id, substream->stream, + q6apm_denali_card(substream)); if (IS_ERR(prtd->graph)) { dev_err(dev, "%s: Could not allocate memory\n", __func__); ret = PTR_ERR(prtd->graph); @@ -447,29 +575,109 @@ static int q6apm_dai_open(struct snd_soc_component *component, return 0; err: + if (!IS_ERR_OR_NULL(prtd->graph)) { + close_ret = q6apm_graph_close(prtd->graph); + if (close_ret) { + q6apm_dai_quarantine_buffer(component, substream, prtd, + close_ret); + return ret; + } + } kfree(prtd); return ret; } +static void q6apm_dai_quarantine_buffer(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + struct q6apm_dai_rtd *prtd, int reason) +{ + unsigned long flags; + + if (prtd->state == Q6APM_STREAM_QUARANTINED) + return; + + spin_lock_irqsave(&prtd->callback_lock, flags); + prtd->callbacks_quarantined = true; + spin_unlock_irqrestore(&prtd->callback_lock, flags); + wait_event(prtd->callback_wait, + !atomic_read(&prtd->callback_users)); + + q6apm_graph_quarantine_dma(prtd->graph); + snd_pcm_set_runtime_buffer(substream, NULL); + /* DSP ownership is uncertain, so ALSA must never reclaim mapped pages. */ + if (substream->dma_buffer.area && substream->dma_buffer.dev.dev) + get_device(substream->dma_buffer.dev.dev); + substream->dma_buffer.area = NULL; + substream->dma_buffer.addr = 0; + substream->dma_buffer.bytes = 0; + prtd->state = Q6APM_STREAM_QUARANTINED; + dev_err(component->dev, + "retaining protected client and DMA buffer after unconfirmed stop (%d)\n", + reason); +} + +static int q6apm_dai_hw_free(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct q6apm_dai_rtd *prtd = substream->runtime->private_data; + bool protected; + int ret; + + if (!prtd) + return 0; + protected = q6apm_denali_protection_runtime(substream, prtd->graph); + if (prtd->state == Q6APM_STREAM_QUARANTINED) + return -EIO; + if (prtd->state != Q6APM_STREAM_UNCERTAIN && + (!protected || prtd->state != Q6APM_STREAM_RUNNING)) + return 0; + + ret = q6apm_graph_stop(prtd->graph); + if (ret) { + q6apm_dai_quarantine_buffer(component, substream, prtd, ret); + return ret; + } + + prtd->state = Q6APM_STREAM_STOPPED; + return 0; +} + static int q6apm_dai_close(struct snd_soc_component *component, struct snd_pcm_substream *substream) { struct snd_pcm_runtime *runtime = substream->runtime; struct q6apm_dai_rtd *prtd = runtime->private_data; + bool protected; + int close_ret; + int ret = 0; - if (prtd->state) { + protected = q6apm_denali_protection_runtime(substream, prtd->graph); + if (prtd->state == Q6APM_STREAM_QUARANTINED) { + ret = -EIO; + } else if (prtd->state) { /* only stop graph that is started */ - q6apm_graph_stop(prtd->graph); - q6apm_free_fragments(prtd->graph, substream->stream); + if (!protected || prtd->state == Q6APM_STREAM_RUNNING || + prtd->state == Q6APM_STREAM_UNCERTAIN) { + ret = q6apm_graph_stop(prtd->graph); + } + if (!ret) + q6apm_free_fragments(prtd->graph, substream->stream); + } + if (ret && q6apm_graph_execution_uncertain(prtd->graph)) + q6apm_dai_quarantine_buffer(component, substream, prtd, ret); + + close_ret = q6apm_graph_close(prtd->graph); + if (close_ret && prtd->state != Q6APM_STREAM_QUARANTINED) + q6apm_dai_quarantine_buffer(component, substream, prtd, + close_ret); + if (prtd->state != Q6APM_STREAM_QUARANTINED) { + prtd->graph = NULL; + kfree(prtd); } - - q6apm_graph_close(prtd->graph); - prtd->graph = NULL; - kfree(prtd); runtime->private_data = NULL; - return 0; + return ret ?: close_ret; } static snd_pcm_uframes_t q6apm_dai_pointer(struct snd_soc_component *component, @@ -531,7 +739,8 @@ static int q6apm_dai_hw_params(struct snd_soc_component *component, static int q6apm_dai_memory_map(struct snd_soc_component *component, struct snd_pcm_substream *substream, - int graph_id, bool is_push_pull) + int graph_id, bool is_push_pull, + bool protected, bool *retain_buffer) { struct q6apm_dai_data *pdata; struct device *dev = component->dev; @@ -550,8 +759,12 @@ static int q6apm_dai_memory_map(struct snd_soc_component *component, phys = substream->dma_buffer.addr | (pdata->sid << 32); ret = q6apm_map_memory_fixed_region(dev, graph_id, phys, BUFFER_BYTES_MAX); - if (ret < 0) - dev_err(dev, "Audio Start: Buffer Allocation failed rc = %d\n", ret); + if (ret < 0) { + *retain_buffer = protected && + (ret == -ETIMEDOUT || ret == -ESHUTDOWN); + dev_err(dev, "Audio Start: Buffer Allocation failed rc = %d\n", ret); + return ret; + } if (is_push_pull) { if (pdata->sid < 0) @@ -560,8 +773,15 @@ static int q6apm_dai_memory_map(struct snd_soc_component *component, phys = (substream->dma_buffer.addr + BUFFER_BYTES_MAX) | (pdata->sid << 32); ret = q6apm_map_pos_buffer(dev, graph_id, phys, POS_BUFFER_BYTES); - if (ret < 0) - dev_err(dev, "Audio Start: Buffer Allocation failed rc = %d\n", ret); + if (ret < 0) { + int unmap_ret; + + dev_err(dev, "Audio Start: Buffer Allocation failed rc = %d\n", + ret); + unmap_ret = q6apm_unmap_memory_fixed_region(dev, graph_id); + *retain_buffer = protected && + (ret == -ETIMEDOUT || unmap_ret); + } } else { } @@ -569,6 +789,26 @@ static int q6apm_dai_memory_map(struct snd_soc_component *component, return ret; } +static void q6apm_dai_retain_dma_buffer(struct snd_soc_component *component, + struct snd_pcm_substream *substream, + int reason) +{ + if (!substream->dma_buffer.area) + return; + + if (substream->runtime) + snd_pcm_set_runtime_buffer(substream, NULL); + /* Retain until reboot or a future, proven DSP-reset reclamation hook. */ + if (substream->dma_buffer.dev.dev) + get_device(substream->dma_buffer.dev.dev); + substream->dma_buffer.area = NULL; + substream->dma_buffer.addr = 0; + substream->dma_buffer.bytes = 0; + dev_err(component->dev, + "retaining DMA buffer after uncertain DSP mapping (%d)\n", + reason); +} + static int q6apm_dai_pcm_new(struct snd_soc_component *component, struct snd_soc_pcm_runtime *rtd) { struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(rtd, 0); @@ -581,6 +821,8 @@ static int q6apm_dai_pcm_new(struct snd_soc_component *component, struct snd_soc int size = BUFFER_BYTES_MAX + PAGE_SIZE; int graph_id, ret; bool is_push_pull; + bool protected; + bool retain_buffer = false; struct snd_pcm_substream *substream = NULL; graph_id = cpu_dai->driver->id; @@ -593,6 +835,10 @@ static int q6apm_dai_pcm_new(struct snd_soc_component *component, struct snd_soc if (substream) { + protected = q6apm_denali_protected_graph(component, rtd, + graph_id); + if (protected) + q6apm_latch_protected_graph(component, graph_id); is_push_pull = q6apm_is_graph_in_push_pull_mode_from_id(component->dev, graph_id, substream->stream); @@ -603,34 +849,64 @@ static int q6apm_dai_pcm_new(struct snd_soc_component *component, struct snd_soc if (ret) return ret; - ret = q6apm_dai_memory_map(component, substream, graph_id, is_push_pull); - if (ret) + ret = q6apm_dai_memory_map(component, substream, + graph_id, is_push_pull, + protected, &retain_buffer); + if (ret) { + if (retain_buffer) + q6apm_dai_retain_dma_buffer(component, + substream, ret); return ret; + } } return 0; } -static void q6apm_dai_memory_unmap(struct snd_soc_component *component, - struct snd_pcm_substream *substream) +static int q6apm_dai_memory_unmap(struct snd_soc_component *component, + struct snd_pcm_substream *substream) { struct snd_soc_pcm_runtime *soc_prtd; struct snd_soc_dai *cpu_dai; int graph_id; + int ret; soc_prtd = snd_soc_substream_to_rtd(substream); if (!soc_prtd) - return; + return 0; cpu_dai = snd_soc_rtd_to_cpu(soc_prtd, 0); if (!cpu_dai) - return; + return 0; graph_id = cpu_dai->driver->id; - q6apm_unmap_memory_fixed_region(component->dev, graph_id); + if (q6apm_graph_dma_quarantined(component->dev, graph_id)) + return -EBUSY; + + ret = q6apm_unmap_memory_fixed_region(component->dev, graph_id); + if (ret) + return ret; if (q6apm_is_graph_in_push_pull_mode_from_id(component->dev, graph_id, substream->stream)) - q6apm_unmap_pos_buffer(component->dev, graph_id); + return q6apm_unmap_pos_buffer(component->dev, graph_id); + + return 0; +} + +static bool q6apm_dai_protected_substream(struct snd_soc_component *component, + struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); + struct snd_soc_dai *cpu_dai; + + if (!rtd) + return false; + cpu_dai = snd_soc_rtd_to_cpu(rtd, 0); + if (!cpu_dai) + return false; + + return q6apm_protected_graph_latched(component, rtd, + cpu_dai->driver->id); } static void q6apm_dai_pcm_free(struct snd_soc_component *component, struct snd_pcm *pcm) @@ -638,12 +914,20 @@ static void q6apm_dai_pcm_free(struct snd_soc_component *component, struct snd_p struct snd_pcm_substream *substream; substream = pcm->streams[SNDRV_PCM_STREAM_CAPTURE].substream; - if (substream) - q6apm_dai_memory_unmap(component, substream); + if (substream) { + int ret = q6apm_dai_memory_unmap(component, substream); + + if (ret && q6apm_dai_protected_substream(component, substream)) + q6apm_dai_retain_dma_buffer(component, substream, ret); + } substream = pcm->streams[SNDRV_PCM_STREAM_PLAYBACK].substream; - if (substream) - q6apm_dai_memory_unmap(component, substream); + if (substream) { + int ret = q6apm_dai_memory_unmap(component, substream); + + if (ret && q6apm_dai_protected_substream(component, substream)) + q6apm_dai_retain_dma_buffer(component, substream, ret); + } } static int q6apm_dai_compr_open(struct snd_soc_component *component, @@ -669,7 +953,7 @@ static int q6apm_dai_compr_open(struct snd_soc_component *component, prtd->cstream = stream; prtd->graph = q6apm_graph_open(dev, event_handler_compr, prtd, graph_id, - SNDRV_PCM_STREAM_PLAYBACK); + SNDRV_PCM_STREAM_PLAYBACK, false); if (IS_ERR(prtd->graph)) { ret = PTR_ERR(prtd->graph); kfree(prtd); @@ -680,8 +964,14 @@ static int q6apm_dai_compr_open(struct snd_soc_component *component, runtime->dma_bytes = BUFFER_BYTES_MAX; size = COMPR_PLAYBACK_MAX_FRAGMENT_SIZE * COMPR_PLAYBACK_MAX_NUM_FRAGMENTS; ret = snd_dma_alloc_pages(SNDRV_DMA_TYPE_DEV, dev, size, &prtd->dma_buffer); - if (ret) + if (ret) { + if (!q6apm_graph_close(prtd->graph)) { + prtd->graph = NULL; + runtime->private_data = NULL; + kfree(prtd); + } return ret; + } if (pdata->sid < 0) prtd->phys = prtd->dma_buffer.addr; @@ -1009,6 +1299,7 @@ static const struct snd_soc_component_driver q6apm_fe_dai_component = { .pcm_new = q6apm_dai_pcm_new, .pcm_free = q6apm_dai_pcm_free, .hw_params = q6apm_dai_hw_params, + .hw_free = q6apm_dai_hw_free, .pointer = q6apm_dai_pointer, .trigger = q6apm_dai_trigger, .ack = q6apm_dai_ack, diff --git a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c index 4aa62d8f4c1e76..ece084f94f0113 100644 --- a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c +++ b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c @@ -184,10 +184,24 @@ static int q6apm_lpass_dai_trigger(struct snd_pcm_substream *substream, int cmd, case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: if (!dai_data->is_port_started[dai->id]) { ret = q6apm_graph_start(dai_data->graph[dai->id]); - if (ret < 0) + if (ret < 0) { + if (q6apm_graph_execution_uncertain(dai_data->graph[dai->id])) + dai_data->is_port_started[dai->id] = true; dev_err(dai->dev, "Failed to start APM port %d\n", dai->id); - else + } else { dai_data->is_port_started[dai->id] = true; + } + } + break; + case SNDRV_PCM_TRIGGER_STOP: + if (dai_data->is_port_started[dai->id] && + q6apm_graph_has_protection(dai_data->graph[dai->id])) { + ret = q6apm_graph_stop(dai_data->graph[dai->id]); + if (ret < 0) + dev_err(dai->dev, "Failed to stop APM port %d\n", + dai->id); + else + dai_data->is_port_started[dai->id] = false; } break; default: @@ -219,7 +233,8 @@ static int q6apm_lpass_dai_prepare(struct snd_pcm_substream *substream, struct s * graph, so sequence for playback and capture will be different */ if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK && dai_data->graph[dai->id] == NULL) { - graph = q6apm_graph_open(dai->dev, NULL, dai->dev, graph_id, substream->stream); + graph = q6apm_graph_open(dai->dev, NULL, dai->dev, graph_id, + substream->stream, false); if (IS_ERR(graph)) { dev_err(dai->dev, "Failed to open graph (%d)\n", graph_id); rc = PTR_ERR(graph); @@ -259,7 +274,8 @@ static int q6apm_lpass_dai_startup(struct snd_pcm_substream *substream, struct s int graph_id = dai->id; if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) { - graph = q6apm_graph_open(dai->dev, NULL, dai->dev, graph_id, substream->stream); + graph = q6apm_graph_open(dai->dev, NULL, dai->dev, graph_id, + substream->stream, false); if (IS_ERR(graph)) { dev_err(dai->dev, "Failed to open graph (%d)\n", graph_id); return PTR_ERR(graph); diff --git a/sound/soc/qcom/qdsp6/q6apm.c b/sound/soc/qcom/qdsp6/q6apm.c index 516880c1f1da64..d4ce6063c55a56 100644 --- a/sound/soc/qcom/qdsp6/q6apm.c +++ b/sound/soc/qcom/qdsp6/q6apm.c @@ -266,8 +266,9 @@ static int q6apm_map_oob_buffer(struct audioreach_graph *graph, size_t data_size ret = -EIO; /* - * An unanswered command does not prove that the DSP failed to map the - * buffer. Keep it allocated until reboot or a proven reset hook. + * A timeout does not prove that the DSP failed to map the buffer. Keep + * it allocated; teardown retains it unless a correlated response makes + * the mapping state certain. */ return ret; @@ -387,7 +388,7 @@ static int __q6apm_send_oob_config(struct audioreach_graph *graph, else ret = q6apm_send_cmd_sync(graph->apm, pkt, 0); kfree(pkt); - if (ret == -ETIMEDOUT || ret == -ESHUTDOWN) + if (ret == -ETIMEDOUT) graph->oob_transfer_uncertain = true; unlock: @@ -416,22 +417,34 @@ int q6apm_send_graph_oob_config(struct q6apm_graph *graph, } static struct audioreach_graph * -q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id) +q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id, + bool enable_protection) { struct audioreach_graph_info *info; struct audioreach_graph *graph; struct device *dma_dev; + bool graph_faulted; + bool mode_mismatch; size_t oob_size; int id; mutex_lock(&apm->lock); spin_lock(&apm->graph_lock); graph = idr_find(&apm->graph_idr, graph_id); + graph_faulted = graph && + (READ_ONCE(graph->protection_faulted) || + (enable_protection && READ_ONCE(graph->protection_malformed))); + mode_mismatch = graph && !graph->initializing && + ((enable_protection && graph->protection_profile) != + graph->protection_runtime); if (graph && (graph->initializing || + graph_faulted || + mode_mismatch || !kref_get_unless_zero(&graph->refcount))) { spin_unlock(&apm->graph_lock); mutex_unlock(&apm->lock); - return ERR_PTR(-EBUSY); + return ERR_PTR(mode_mismatch ? -EBUSY : + graph_faulted ? -EIO : -EBUSY); } spin_unlock(&apm->graph_lock); info = idr_find(&apm->graph_info_idr, graph_id); @@ -456,6 +469,7 @@ q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id) graph->dma_dev = dma_dev; graph->initializing = true; mutex_init(&graph->oob_lock); + mutex_init(&graph->protection_lock); graph->graph = audioreach_alloc_graph_pkt(apm, info); if (IS_ERR(graph->graph)) { @@ -493,24 +507,44 @@ q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id) goto remove_graph; id = audioreach_graph_protection_oob_size(info, &oob_size); - if (id) { - dev_warn(apm->dev, - "graph OOB topology data is incomplete (%d)\n", id); + if (id < 0) { + graph->protection_profile = true; + graph->protection_malformed = true; + if (enable_protection) { + graph->protection_runtime = true; + graph->protection_faulted = true; + dev_warn(apm->dev, + "protected topology is incomplete (%d); refusing runtime\n", + id); + } goto graph_ready; } if (oob_size) { + graph->protection_profile = true; + if (!enable_protection) + goto graph_ready; + graph->protection_runtime = true; id = q6apm_map_oob_buffer(graph, oob_size); - if (id) + if (id) { + dev_warn(apm->dev, + "protected OOB map failed (%d); using bypass\n", id); + goto graph_ready; + } + id = audioreach_send_protected_graph_calibration(graph); + if (id) { dev_warn(apm->dev, - "graph OOB map failed (%d); retaining uncertain ownership when required\n", + "protected graph calibration failed (%d); using bypass\n", id); + q6apm_unmap_oob_buffer(graph); + goto graph_ready; + } + graph->protection_available = true; } graph_ready: spin_lock(&apm->graph_lock); graph->initializing = false; spin_unlock(&apm->graph_lock); - return graph; remove_graph: @@ -554,6 +588,38 @@ static int audioreach_graph_mgmt_cmd(struct audioreach_graph *graph, uint32_t op return q6apm_send_cmd_sync(apm, pkt, 0); } +static int audioreach_graph_client_mgmt_cmd(struct q6apm_graph *graph, + u32 opcode) +{ + struct audioreach_graph_info *info = graph->info; + int num_sub_graphs = info->num_sub_graphs; + struct apm_module_param_data *param_data; + struct apm_graph_mgmt_cmd *mgmt_cmd; + struct gpr_pkt *pkt __free(kfree) = NULL; + struct audioreach_sub_graph *sg; + int payload_size; + int i = 0; + u32 dest = APM_MODULE_INSTANCE_ID; + + payload_size = APM_GRAPH_MGMT_PSIZE(mgmt_cmd, num_sub_graphs); + pkt = audioreach_alloc_cmd_pkt(payload_size, opcode, 0, graph->port->id, dest); + if (IS_ERR(pkt)) + return PTR_ERR(pkt); + + mgmt_cmd = (void *)pkt + GPR_HDR_SIZE + APM_CMD_HDR_SIZE; + mgmt_cmd->num_sub_graphs = num_sub_graphs; + param_data = &mgmt_cmd->param_data; + param_data->module_instance_id = APM_MODULE_INSTANCE_ID; + param_data->param_id = APM_PARAM_ID_SUB_GRAPH_LIST; + param_data->param_size = payload_size - APM_MODULE_PARAM_DATA_SIZE; + + /* FullIO emits the graph's required run-state order in this list. */ + list_for_each_entry(sg, &info->sg_list, node) + mgmt_cmd->sub_graph_id_list[i++] = sg->sub_graph_id; + + return audioreach_graph_send_cmd_sync(graph, pkt, 0); +} + static void q6apm_put_audioreach_graph(struct kref *ref) { struct audioreach_graph *graph; @@ -564,23 +630,26 @@ static void q6apm_put_audioreach_graph(struct kref *ref) graph = container_of(ref, struct audioreach_graph, refcount); apm = graph->apm; - /* Stop every DSP user before releasing the graph's OOB mapping. */ - close_ret = audioreach_graph_mgmt_cmd(graph, APM_CMD_GRAPH_CLOSE); - if (!close_ret) - unmap_ret = q6apm_unmap_oob_buffer(graph); - - if (graph->oob_virt) { - /* Keep the ID and DMA descriptor pinned until ownership is known. */ + /* Stop all DSP users before releasing their OOB mapping. */ + close_ret = graph->close_confirmed ? 0 : + audioreach_graph_mgmt_cmd(graph, APM_CMD_GRAPH_CLOSE); + if (close_ret && graph->protection_runtime) { + /* Keep the ID reserved until reboot or a proven reset hook. */ spin_lock(&apm->graph_lock); graph->initializing = true; kref_init(&graph->refcount); spin_unlock(&apm->graph_lock); dev_err(apm->dev, - "retaining graph %u OOB DMA until reboot after teardown failure (close %d, unmap %d)\n", - graph->id, close_ret, unmap_ret); + "retaining graph %u after uncertain GRAPH_CLOSE (%d)\n", + graph->id, close_ret); return; } + mutex_lock(&graph->oob_lock); + graph->oob_transfer_uncertain = false; + mutex_unlock(&graph->oob_lock); + unmap_ret = q6apm_unmap_oob_buffer(graph); + mutex_lock(&apm->lock); spin_lock(&apm->graph_lock); graph = idr_remove(&apm->graph_idr, graph->id); @@ -588,7 +657,12 @@ static void q6apm_put_audioreach_graph(struct kref *ref) mutex_unlock(&apm->lock); kfree(graph->graph); - put_device(graph->dma_dev); + if (graph->oob_virt) + dev_err(apm->dev, + "retaining protected OOB DMA buffer after teardown failure (close %d, unmap %d)\n", + close_ret, unmap_ret); + else + put_device(graph->dma_dev); kfree(graph); } @@ -951,6 +1025,102 @@ int q6apm_set_real_module_id(struct device *dev, struct q6apm_graph *graph, } EXPORT_SYMBOL_GPL(q6apm_set_real_module_id); +bool q6apm_graph_has_protection(const struct q6apm_graph *graph) +{ + return graph && READ_ONCE(graph->protection_runtime); +} +EXPORT_SYMBOL_GPL(q6apm_graph_has_protection); + +int q6apm_graph_configure_protection(struct q6apm_graph *graph) +{ + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + + if (!active) + return -ESHUTDOWN; + if (!q6apm_graph_has_protection(graph)) + return 0; + + return audioreach_configure_protection(graph); +} +EXPORT_SYMBOL_GPL(q6apm_graph_configure_protection); + +static const struct audioreach_module * +q6apm_find_module_by_iid(const struct audioreach_graph_info *info, u32 iid) +{ + struct audioreach_container *container; + struct audioreach_sub_graph *sg; + struct audioreach_module *module; + + list_for_each_entry(sg, &info->sg_list, node) + list_for_each_entry(container, &sg->container_list, node) + list_for_each_entry(module, &container->modules_list, node) + if (module->instance_id == iid) + return module; + + return NULL; +} + +static unsigned int +q6apm_graph_module_count(const struct audioreach_graph_info *info) +{ + struct audioreach_container *container; + struct audioreach_sub_graph *sg; + unsigned int count = 0; + + list_for_each_entry(sg, &info->sg_list, node) + list_for_each_entry(container, &sg->container_list, node) + count += container->num_modules; + + return count; +} + +static int q6apm_protected_media_format_pcm(struct q6apm_graph *graph, + struct audioreach_module_config *cfg) +{ + const struct audioreach_module *source; + const struct audioreach_module *module; + unsigned int limit, step; + bool pcm_configured = false; + int ret; + + if (cfg->direction != SNDRV_PCM_STREAM_PLAYBACK || + cfg->sample_rate != 48000 || cfg->bit_width != 16 || + cfg->num_channels != 2) + return -EINVAL; + + source = audioreach_graph_find_module(graph->info, + MODULE_ID_SH_MEM_PULL_MODE); + if (IS_ERR_OR_NULL(source)) + return source ? PTR_ERR(source) : -ENODEV; + + module = source; + limit = q6apm_graph_module_count(graph->info); + for (step = 0; step < limit; step++) { + if (module->module_id == MODULE_ID_PCM_CNV) { + if (pcm_configured) + return -EEXIST; + ret = audioreach_set_media_format(graph, module, cfg); + if (ret) + return ret; + pcm_configured = true; + } else if (module->module_id == MODULE_ID_MFC) { + if (!pcm_configured) + return -EINVAL; + return audioreach_set_media_format(graph, module, cfg); + } + + if (module->num_connections != 1) + return -EINVAL; + module = q6apm_find_module_by_iid(graph->info, + module->dst_mod_inst_id[0]); + if (!module) + return -ENODEV; + } + + return -ELOOP; +} + int q6apm_graph_media_format_pcm(struct q6apm_graph *graph, struct audioreach_module_config *cfg) { struct q6apm_graph *active __free(q6apm_graph_user) = @@ -964,6 +1134,8 @@ int q6apm_graph_media_format_pcm(struct q6apm_graph *graph, struct audioreach_mo if (!active) return -ESHUTDOWN; info = graph->info; + if (q6apm_graph_has_protection(graph)) + return q6apm_protected_media_format_pcm(graph, cfg); list_for_each_entry(sgs, &info->sg_list, node) { list_for_each_entry(container, &sgs->container_list, node) { @@ -1300,7 +1472,8 @@ static int q6apm_graph_get_module_iid(struct q6apm_graph *graph, uint32_t mid) } struct q6apm_graph *q6apm_graph_open(struct device *dev, q6apm_cb cb, - void *priv, int graph_id, int dir) + void *priv, int graph_id, int dir, + bool enable_protection) { struct q6apm *apm = dev_get_drvdata(dev->parent); struct audioreach_graph *ar_graph; @@ -1316,12 +1489,17 @@ struct q6apm_graph *q6apm_graph_open(struct device *dev, q6apm_cb cb, goto unlock_clients; } - ar_graph = q6apm_get_audioreach_graph(apm, graph_id); + ar_graph = q6apm_get_audioreach_graph(apm, graph_id, + enable_protection); if (IS_ERR(ar_graph)) { dev_err(dev, "No graph found with id %d\n", graph_id); ret = PTR_ERR(ar_graph); goto unlock_clients; } + if (enable_protection && ar_graph->protection_malformed) { + ret = -EINVAL; + goto put_ar_graph; + } graph = kzalloc_obj(*graph); if (!graph) { @@ -1336,6 +1514,8 @@ struct q6apm_graph *q6apm_graph_open(struct device *dev, q6apm_cb cb, graph->ar_graph = ar_graph; graph->id = ar_graph->id; graph->dev = get_device(dev); + graph->protection_profile = ar_graph->protection_profile; + graph->protection_runtime = ar_graph->protection_runtime; spin_lock_init(&graph->lifecycle_lock); init_waitqueue_head(&graph->users_wait); init_completion(&graph->detached_done); @@ -1393,6 +1573,11 @@ int q6apm_graph_close(struct q6apm_graph *graph) struct audioreach_graph *ar_graph; struct q6apm *apm; unsigned long flags; + int active_refs = 0; + int close_ret; + bool retain_graph = false; + bool final_protected; + bool retain_dma; bool detached; bool dying; @@ -1400,9 +1585,12 @@ int q6apm_graph_close(struct q6apm_graph *graph) spin_lock_irqsave(&graph->lifecycle_lock, flags); detached = graph->detached; dying = graph->dying; + retain_dma = graph->retain_dma_on_detach; apm = graph->apm; spin_unlock_irqrestore(&graph->lifecycle_lock, flags); if (detached) { + if (retain_dma) + return -EBUSY; put_device(graph->dev); kfree(graph); return 0; @@ -1424,8 +1612,49 @@ int q6apm_graph_close(struct q6apm_graph *graph) spin_unlock_irqrestore(&graph->lifecycle_lock, flags); ar_graph = graph->ar_graph; + if (graph->protection_runtime) { + mutex_lock(&ar_graph->protection_lock); + active_refs = ar_graph->start_count; + retain_graph = ar_graph->execution_uncertain || + (ar_graph->protection_faulted && active_refs > 0); + mutex_unlock(&ar_graph->protection_lock); + } + if (retain_graph) { + graph->retained = true; + mutex_unlock(&apm->client_lock); + dev_err(graph->dev, + "retaining uncertain DSP client with %d active references\n", + active_refs); + return -EBUSY; + } + q6apm_graph_mark_dying(graph); q6apm_graph_wait_users(graph); + final_protected = graph->protection_runtime && + kref_read(&ar_graph->refcount) == 1; + if (final_protected) { + close_ret = audioreach_graph_mgmt_cmd(ar_graph, + APM_CMD_GRAPH_CLOSE); + if (close_ret) { + mutex_lock(&ar_graph->protection_lock); + ar_graph->protection_faulted = true; + ar_graph->execution_uncertain = true; + ar_graph->dma_quarantined = true; + mutex_unlock(&ar_graph->protection_lock); + spin_lock_irqsave(&graph->lifecycle_lock, flags); + graph->dying = false; + spin_unlock_irqrestore(&graph->lifecycle_lock, flags); + graph->retained = true; + mutex_unlock(&apm->client_lock); + dev_err(graph->dev, + "retaining protected client after GRAPH_CLOSE failure (%d)\n", + close_ret); + return close_ret; + } + mutex_lock(&ar_graph->protection_lock); + ar_graph->close_confirmed = true; + mutex_unlock(&ar_graph->protection_lock); + } list_del_init(&graph->node); gpr_free_port(graph->port); graph->port = NULL; @@ -1443,10 +1672,22 @@ int q6apm_graph_prepare(struct q6apm_graph *graph) { struct q6apm_graph *active __free(q6apm_graph_user) = q6apm_graph_user_get(graph) ? graph : NULL; + struct audioreach_graph *ar_graph; + bool protected = q6apm_graph_has_protection(graph); + int ret; if (!active) return -ESHUTDOWN; - return audioreach_graph_mgmt_cmd(graph->ar_graph, APM_CMD_GRAPH_PREPARE); + ar_graph = graph->ar_graph; + ret = audioreach_graph_mgmt_cmd(ar_graph, APM_CMD_GRAPH_PREPARE); + if (protected) { + mutex_lock(&ar_graph->protection_lock); + ar_graph->prepared = !ret; + ar_graph->prepare_uncertain = ret == -ETIMEDOUT; + mutex_unlock(&ar_graph->protection_lock); + } + + return ret; } EXPORT_SYMBOL_GPL(q6apm_graph_prepare); @@ -1455,16 +1696,49 @@ int q6apm_graph_start(struct q6apm_graph *graph) struct q6apm_graph *active __free(q6apm_graph_user) = q6apm_graph_user_get(graph) ? graph : NULL; struct audioreach_graph *ar_graph; + bool protected = q6apm_graph_has_protection(graph); int ret = 0; if (!active) return -ESHUTDOWN; ar_graph = graph->ar_graph; - if (ar_graph->start_count == 0) - ret = audioreach_graph_mgmt_cmd(ar_graph, APM_CMD_GRAPH_START); + if (protected) + mutex_lock(&ar_graph->protection_lock); + if (protected && ar_graph->protection_faulted) { + ret = -EIO; + goto unlock; + } + if (protected && !ar_graph->protection_configured && + !ar_graph->protection_bypass_confirmed) { + ret = -EAGAIN; + goto unlock; + } + if (protected && (!ar_graph->prepared || + ar_graph->prepare_uncertain)) { + ret = -EAGAIN; + goto unlock; + } + if (ar_graph->start_count == 0) { + if (protected) + ret = audioreach_graph_client_mgmt_cmd(graph, APM_CMD_GRAPH_START); + else + ret = audioreach_graph_mgmt_cmd(ar_graph, + APM_CMD_GRAPH_START); + } if (!ret) ar_graph->start_count++; + else if (protected) { + ar_graph->protection_faulted = true; + if (ret == -ETIMEDOUT) { + ar_graph->start_count = 1; + ar_graph->execution_uncertain = true; + } + } + +unlock: + if (protected) + mutex_unlock(&ar_graph->protection_lock); return ret; } @@ -1475,24 +1749,135 @@ int q6apm_graph_stop(struct q6apm_graph *graph) struct q6apm_graph *active __free(q6apm_graph_user) = q6apm_graph_user_get(graph) ? graph : NULL; struct audioreach_graph *ar_graph; + bool protected = q6apm_graph_has_protection(graph); int ret; if (!active) return -ESHUTDOWN; ar_graph = graph->ar_graph; - if (ar_graph->start_count <= 0) - return 0; - if (--ar_graph->start_count > 0) - return 0; + if (protected) + mutex_lock(&ar_graph->protection_lock); + if (ar_graph->start_count <= 0) { + ret = 0; + goto unlock; + } + if (--ar_graph->start_count > 0) { + ret = 0; + goto unlock; + } - ret = audioreach_graph_mgmt_cmd(ar_graph, APM_CMD_GRAPH_STOP); + if (protected) + ret = audioreach_graph_client_mgmt_cmd(graph, APM_CMD_GRAPH_STOP); + else + ret = audioreach_graph_mgmt_cmd(ar_graph, APM_CMD_GRAPH_STOP); if (ret) ar_graph->start_count++; + if (ret && protected) { + ar_graph->protection_faulted = true; + ar_graph->execution_uncertain = true; + } else if (protected) { + ar_graph->execution_uncertain = false; + ar_graph->protection_configured = false; + ar_graph->protection_bypass_confirmed = false; + ar_graph->prepared = false; + ar_graph->prepare_uncertain = false; + } +unlock: + if (protected) + mutex_unlock(&ar_graph->protection_lock); return ret; } EXPORT_SYMBOL_GPL(q6apm_graph_stop); +bool q6apm_graph_execution_uncertain(struct q6apm_graph *graph) +{ + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + struct audioreach_graph *ar_graph; + bool uncertain; + + if (!active) + return false; + if (!q6apm_graph_has_protection(graph)) + return false; + ar_graph = graph->ar_graph; + + mutex_lock(&ar_graph->protection_lock); + uncertain = ar_graph->execution_uncertain; + mutex_unlock(&ar_graph->protection_lock); + + return uncertain; +} +EXPORT_SYMBOL_GPL(q6apm_graph_execution_uncertain); + +void q6apm_graph_quarantine_dma(struct q6apm_graph *graph) +{ + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + struct audioreach_graph *ar_graph; + + if (!active) + return; + if (!q6apm_graph_has_protection(graph)) + return; + ar_graph = graph->ar_graph; + + mutex_lock(&ar_graph->protection_lock); + ar_graph->dma_quarantined = true; + mutex_unlock(&ar_graph->protection_lock); +} +EXPORT_SYMBOL_GPL(q6apm_graph_quarantine_dma); + +bool q6apm_graph_dma_quarantined(struct device *dev, unsigned int graph_id) +{ + struct q6apm *apm = dev_get_drvdata(dev->parent); + struct audioreach_graph *graph; + bool quarantined = false; + + if (!apm) + return false; + + guard(mutex)(&apm->client_lock); + mutex_lock(&apm->lock); + spin_lock(&apm->graph_lock); + graph = idr_find(&apm->graph_idr, graph_id); + if (graph && !kref_get_unless_zero(&graph->refcount)) + graph = NULL; + spin_unlock(&apm->graph_lock); + mutex_unlock(&apm->lock); + if (!graph) + return false; + + mutex_lock(&graph->protection_lock); + quarantined = graph->dma_quarantined; + mutex_unlock(&graph->protection_lock); + kref_put(&graph->refcount, q6apm_put_audioreach_graph); + + return quarantined; +} +EXPORT_SYMBOL_GPL(q6apm_graph_dma_quarantined); + +bool q6apm_graph_id_has_protection(struct device *dev, unsigned int graph_id) +{ + struct q6apm *apm = dev_get_drvdata(dev->parent); + struct audioreach_graph_info *info; + int profile = 0; + + if (!apm) + return false; + guard(mutex)(&apm->client_lock); + mutex_lock(&apm->lock); + info = idr_find(&apm->graph_info_idr, graph_id); + if (info) + profile = audioreach_graph_protection_profile(info); + mutex_unlock(&apm->lock); + + /* A malformed partial declaration is protected and must fail closed. */ + return profile != 0; +} +EXPORT_SYMBOL_GPL(q6apm_graph_id_has_protection); + int q6apm_graph_flush(struct q6apm_graph *graph) { struct q6apm_graph *active __free(q6apm_graph_user) = @@ -1500,6 +1885,10 @@ int q6apm_graph_flush(struct q6apm_graph *graph) if (!active) return -ESHUTDOWN; + if (q6apm_graph_has_protection(graph)) + return audioreach_graph_client_mgmt_cmd(graph, + APM_CMD_GRAPH_FLUSH); + return audioreach_graph_mgmt_cmd(graph->ar_graph, APM_CMD_GRAPH_FLUSH); } EXPORT_SYMBOL_GPL(q6apm_graph_flush); @@ -1597,17 +1986,30 @@ static void q6apm_detach_graph_clients(struct q6apm *apm) mutex_unlock(&apm->lock); list_for_each_entry_safe(graph, next, &detaching, node) { + struct audioreach_graph *ar_graph = graph->ar_graph; unsigned long flags; + bool retain_dma = false; list_del_init(&graph->node); q6apm_graph_wait_users(graph); gpr_free_port(graph->port); + if (ar_graph && graph->protection_runtime) { + mutex_lock(&ar_graph->protection_lock); + retain_dma = ar_graph->start_count > 0 || + ar_graph->execution_uncertain || + ar_graph->dma_quarantined; + if (retain_dma) + ar_graph->dma_quarantined = true; + mutex_unlock(&ar_graph->protection_lock); + } + spin_lock_irqsave(&graph->lifecycle_lock, flags); graph->port = NULL; graph->ar_graph = NULL; graph->info = NULL; graph->apm = NULL; + graph->retain_dma_on_detach = retain_dma; graph->detached = true; spin_unlock_irqrestore(&graph->lifecycle_lock, flags); complete_all(&graph->detached_done); @@ -1781,10 +2183,11 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) result->opcode, result->status, true); - if (expected) + if (expected) { dev_err(dev, "Error (%d) Processing 0x%08x cmd\n", result->status, result->opcode); + } break; } diff --git a/sound/soc/qcom/qdsp6/q6apm.h b/sound/soc/qcom/qdsp6/q6apm.h index e3ed71cc35809c..a991cb78439252 100644 --- a/sound/soc/qcom/qdsp6/q6apm.h +++ b/sound/soc/qcom/qdsp6/q6apm.h @@ -113,6 +113,22 @@ struct audioreach_graph { u32 oob_token; /* Serialize use of the graph's shared OOB buffer. */ struct mutex oob_lock; + /* Serialize topology-derived protection state for this graph only. */ + struct mutex protection_lock; + bool protection_profile; + bool protection_malformed; + bool protection_runtime; + bool protection_available; + bool protection_configured; + bool protection_bypass_confirmed; + bool protection_vi_ready; + bool protection_cps_ready; + bool protection_faulted; + bool prepared; + bool prepare_uncertain; + bool close_confirmed; + bool execution_uncertain; + bool dma_quarantined; bool oob_map_uncertain; bool oob_transfer_uncertain; /* Cached Graph data */ @@ -131,9 +147,13 @@ struct q6apm_graph { struct device *dev; struct q6apm *apm; struct list_head node; + bool retained; + bool protection_profile; + bool protection_runtime; bool is_push_pull_mode; bool dying; bool detached; + bool retain_dma_on_detach; unsigned int active_users; /* Protect admission and detach state for graph API users. */ spinlock_t lifecycle_lock; @@ -161,7 +181,8 @@ struct q6apm_graph { /* Graph Operations */ struct q6apm_graph *q6apm_graph_open(struct device *dev, q6apm_cb cb, - void *priv, int graph_id, int dir); + void *priv, int graph_id, int dir, + bool enable_protection); bool q6apm_graph_user_get(struct q6apm_graph *graph); void q6apm_graph_user_put(struct q6apm_graph *graph); int q6apm_graph_close(struct q6apm_graph *graph); @@ -169,6 +190,10 @@ int q6apm_graph_prepare(struct q6apm_graph *graph); int q6apm_graph_start(struct q6apm_graph *graph); int q6apm_graph_stop(struct q6apm_graph *graph); int q6apm_graph_flush(struct q6apm_graph *graph); +bool q6apm_graph_execution_uncertain(struct q6apm_graph *graph); +void q6apm_graph_quarantine_dma(struct q6apm_graph *graph); +bool q6apm_graph_dma_quarantined(struct device *dev, unsigned int graph_id); +bool q6apm_graph_id_has_protection(struct device *dev, unsigned int graph_id); /* Media Format */ int q6apm_graph_media_format_pcm(struct q6apm_graph *graph, @@ -202,6 +227,8 @@ int q6apm_send_oob_config(struct audioreach_graph *graph, const void *data, size_t size); int q6apm_send_graph_oob_config(struct q6apm_graph *graph, const void *data, size_t size); +bool q6apm_graph_has_protection(const struct q6apm_graph *graph); +int q6apm_graph_configure_protection(struct q6apm_graph *graph); /* Callback for graph specific */ struct audioreach_module *q6apm_find_module_by_mid(struct q6apm_graph *graph, From 5965691b708c69d3f8dffd5a8b98ed1ff2c0fe46 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Tue, 1 Sep 2026 00:28:36 +0100 Subject: [PATCH 14/20] ASoC: qcom: wire Denali protection backends Denali speaker protection needs both VI feedback and CPS telemetry before the protected path can be enabled. Map those backend DAIs to the graph that owns their topology-declared integrated backend, and publish readiness as the SoundWire links prepare or tear down. Keep the mapping and lifecycle limited to microsoft,denali-sndcard. Require a valid, prepared SoundWire runtime before publishing readiness, reject readiness transitions while the graph is started, and drain the matching FE and BE start references symmetrically. Refuse SoundWire teardown until the graph is known stopped. Other machines and ordinary graphs keep their existing DAI behavior. Link: https://github.com/geocausa/SP11X1e-audio/commit/31466d62b38c8138d476b2fd3606a76272fb3d34 Link: https://github.com/geocausa/SP11X1e-audio/commit/7af8f21e9966f6adb40c102653b6acb5d81742 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/22bdec204438f5e1e19286cb7f1688f196c51967 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/4f9ac01837b4242623d46b08b7662c6d9c5d52e2 Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/q6apm-lpass-dais.c | 47 +++++++-- sound/soc/qcom/qdsp6/q6apm.c | 124 ++++++++++++++++++++++++ sound/soc/qcom/qdsp6/q6apm.h | 10 ++ sound/soc/qcom/x1e80100.c | 113 ++++++++++++++++++++- 4 files changed, 284 insertions(+), 10 deletions(-) diff --git a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c index ece084f94f0113..45701ac1a7f7e5 100644 --- a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c +++ b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -24,6 +25,28 @@ struct q6apm_lpass_dai_data { struct audioreach_module_config module_config[APM_PORT_MAX]; }; +static bool q6apm_lpass_protection_runtime(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); + + return rtd && rtd->card && rtd->card->dev->of_node && + of_device_is_compatible(rtd->card->dev->of_node, + "microsoft,denali-sndcard"); +} + +static int q6apm_lpass_graph_id(struct snd_pcm_substream *substream, + struct snd_soc_dai *dai) +{ + struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); + + if (!rtd || !rtd->card || !rtd->card->dev->of_node || + !of_device_is_compatible(rtd->card->dev->of_node, + "microsoft,denali-sndcard")) + return dai->id; + + return q6apm_graph_id_for_backend(dai->dev, dai->id); +} + static int q6dma_set_channel_map(struct snd_soc_dai *dai, unsigned int tx_num, const unsigned int *tx_ch_mask, @@ -216,9 +239,12 @@ static int q6apm_lpass_dai_prepare(struct snd_pcm_substream *substream, struct s struct q6apm_lpass_dai_data *dai_data = dev_get_drvdata(dai->dev); struct audioreach_module_config *cfg = &dai_data->module_config[dai->id]; struct q6apm_graph *graph; - int graph_id = dai->id; + int graph_id = q6apm_lpass_graph_id(substream, dai); int rc; + if (graph_id < 0) + return graph_id; + if (dai_data->is_port_started[dai->id]) { rc = q6apm_graph_stop(dai_data->graph[dai->id]); if (rc < 0) { @@ -234,14 +260,17 @@ static int q6apm_lpass_dai_prepare(struct snd_pcm_substream *substream, struct s */ if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK && dai_data->graph[dai->id] == NULL) { graph = q6apm_graph_open(dai->dev, NULL, dai->dev, graph_id, - substream->stream, false); + substream->stream, + q6apm_lpass_protection_runtime(substream)); if (IS_ERR(graph)) { dev_err(dai->dev, "Failed to open graph (%d)\n", graph_id); rc = PTR_ERR(graph); return rc; } - dai_data->graph[graph_id] = graph; + dai_data->graph[dai->id] = graph; } + if (q6apm_graph_has_protection(dai_data->graph[dai->id])) + return 0; cfg->direction = substream->stream; rc = q6apm_graph_media_format_pcm(dai_data->graph[dai->id], cfg); @@ -271,16 +300,20 @@ static int q6apm_lpass_dai_startup(struct snd_pcm_substream *substream, struct s { struct q6apm_lpass_dai_data *dai_data = dev_get_drvdata(dai->dev); struct q6apm_graph *graph; - int graph_id = dai->id; + int graph_id = q6apm_lpass_graph_id(substream, dai); + + if (graph_id < 0) + return graph_id; - if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) { + if (substream->stream == SNDRV_PCM_STREAM_CAPTURE || graph_id != dai->id) { graph = q6apm_graph_open(dai->dev, NULL, dai->dev, graph_id, - substream->stream, false); + substream->stream, + q6apm_lpass_protection_runtime(substream)); if (IS_ERR(graph)) { dev_err(dai->dev, "Failed to open graph (%d)\n", graph_id); return PTR_ERR(graph); } - dai_data->graph[graph_id] = graph; + dai_data->graph[dai->id] = graph; } return 0; diff --git a/sound/soc/qcom/qdsp6/q6apm.c b/sound/soc/qcom/qdsp6/q6apm.c index d4ce6063c55a56..81fe13a8d25593 100644 --- a/sound/soc/qcom/qdsp6/q6apm.c +++ b/sound/soc/qcom/qdsp6/q6apm.c @@ -1045,6 +1045,130 @@ int q6apm_graph_configure_protection(struct q6apm_graph *graph) } EXPORT_SYMBOL_GPL(q6apm_graph_configure_protection); +static int __q6apm_graph_id_for_backend(struct q6apm *apm, int backend_id) +{ + struct audioreach_graph_info *info; + struct audioreach_container *container; + struct audioreach_sub_graph *sg; + struct audioreach_module *module; + int graph_id = -ENOENT; + int profile; + int id; + + mutex_lock(&apm->lock); + idr_for_each_entry(&apm->graph_info_idr, info, id) { + profile = audioreach_graph_protection_profile(info); + if (profile <= 0) + continue; + list_for_each_entry(sg, &info->sg_list, node) { + list_for_each_entry(container, &sg->container_list, node) { + list_for_each_entry(module, + &container->modules_list, node) { + if (!module->integrated_backend_id || + module->integrated_backend_id != backend_id) + continue; + if (graph_id >= 0 && graph_id != info->id) { + graph_id = -EEXIST; + goto unlock; + } + graph_id = info->id; + } + } + } + } + +unlock: + mutex_unlock(&apm->lock); + return graph_id == -ENOENT ? backend_id : graph_id; +} + +int q6apm_graph_id_for_backend(struct device *dev, int backend_id) +{ + struct q6apm *apm = dev_get_drvdata(dev->parent); + + if (!apm) + return -ENODEV; + guard(mutex)(&apm->client_lock); + if (apm->removing) + return -ESHUTDOWN; + + return __q6apm_graph_id_for_backend(apm, backend_id); +} +EXPORT_SYMBOL_GPL(q6apm_graph_id_for_backend); + +int q6apm_set_protection_backend_ready(struct device *dev, int backend_id, + enum q6apm_protection_backend backend, + bool ready) +{ + struct q6apm *apm = dev_get_drvdata(dev->parent); + struct audioreach_graph *graph; + bool *backend_ready; + int graph_id; + int ret = 0; + + if (!apm) + return -ENODEV; + guard(mutex)(&apm->client_lock); + if (apm->removing) + return -ESHUTDOWN; + + graph_id = __q6apm_graph_id_for_backend(apm, backend_id); + if (graph_id < 0) + return graph_id; + + mutex_lock(&apm->lock); + spin_lock(&apm->graph_lock); + graph = idr_find(&apm->graph_idr, graph_id); + if (graph && (graph->initializing || + !kref_get_unless_zero(&graph->refcount))) + graph = NULL; + spin_unlock(&apm->graph_lock); + mutex_unlock(&apm->lock); + if (!graph) + return -ENODEV; + if (!graph->protection_profile) { + kref_put(&graph->refcount, q6apm_put_audioreach_graph); + return -ENODEV; + } + + mutex_lock(&graph->protection_lock); + switch (backend) { + case Q6APM_PROTECTION_BACKEND_VI: + backend_ready = &graph->protection_vi_ready; + break; + case Q6APM_PROTECTION_BACKEND_CPS: + backend_ready = &graph->protection_cps_ready; + break; + default: + ret = -EINVAL; + goto unlock; + } + if (*backend_ready == ready) + goto unlock; + if (graph->start_count > 0) { + ret = -EBUSY; + goto unlock; + } + if (ready && graph->protection_faulted) { + ret = -EIO; + goto unlock; + } + /* + * Keep ready=false available after a fault once execution has stopped so + * backend teardown can proceed. + */ + *backend_ready = ready; + graph->protection_configured = false; + graph->protection_bypass_confirmed = false; + +unlock: + mutex_unlock(&graph->protection_lock); + kref_put(&graph->refcount, q6apm_put_audioreach_graph); + + return ret; +} +EXPORT_SYMBOL_GPL(q6apm_set_protection_backend_ready); + static const struct audioreach_module * q6apm_find_module_by_iid(const struct audioreach_graph_info *info, u32 iid) { diff --git a/sound/soc/qcom/qdsp6/q6apm.h b/sound/soc/qcom/qdsp6/q6apm.h index a991cb78439252..5ebec2938af39b 100644 --- a/sound/soc/qcom/qdsp6/q6apm.h +++ b/sound/soc/qcom/qdsp6/q6apm.h @@ -227,9 +227,19 @@ int q6apm_send_oob_config(struct audioreach_graph *graph, const void *data, size_t size); int q6apm_send_graph_oob_config(struct q6apm_graph *graph, const void *data, size_t size); +int q6apm_graph_id_for_backend(struct device *dev, int backend_id); bool q6apm_graph_has_protection(const struct q6apm_graph *graph); int q6apm_graph_configure_protection(struct q6apm_graph *graph); +enum q6apm_protection_backend { + Q6APM_PROTECTION_BACKEND_VI, + Q6APM_PROTECTION_BACKEND_CPS, +}; + +int q6apm_set_protection_backend_ready(struct device *dev, int backend_id, + enum q6apm_protection_backend backend, + bool ready); + /* Callback for graph specific */ struct audioreach_module *q6apm_find_module_by_mid(struct q6apm_graph *graph, uint32_t mid); diff --git a/sound/soc/qcom/x1e80100.c b/sound/soc/qcom/x1e80100.c index fab698f886dd75..74b662296996e2 100644 --- a/sound/soc/qcom/x1e80100.c +++ b/sound/soc/qcom/x1e80100.c @@ -2,6 +2,7 @@ // Copyright (c) 2023, Linaro Limited #include +#include #include #include #include @@ -13,6 +14,7 @@ #include "common.h" #include "qdsp6/q6afe.h" +#include "qdsp6/q6apm.h" #include "qdsp6/q6dsp-common.h" #include "sdw.h" @@ -165,14 +167,45 @@ static int x1e80100_snd_hw_map_channels(struct x1e80100_snd_data *data, return 0; } +static int x1e80100_protection_backend(unsigned int dai_id) +{ + switch (dai_id) { + case WSA_CODEC_DMA_TX_0: + return Q6APM_PROTECTION_BACKEND_VI; + case WSA_CODEC_DMA_TX_1: + return Q6APM_PROTECTION_BACKEND_CPS; + default: + return -EINVAL; + } +} + +static int x1e80100_set_protection_ready(struct snd_soc_pcm_runtime *rtd, + struct snd_soc_dai *cpu_dai, + int backend, bool ready) +{ + int ret; + + ret = q6apm_set_protection_backend_ready(cpu_dai->dev, cpu_dai->id, + backend, ready); + if (ret && ret != -ENODEV) + dev_warn(rtd->dev, + "failed to update protected backend readiness: %d\n", ret); + + return ret; +} + static int x1e80100_snd_prepare(struct snd_pcm_substream *substream) { struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(rtd, 0); struct x1e80100_snd_data *data = snd_soc_card_get_drvdata(rtd->card); + struct sdw_stream_runtime *sruntime; unsigned int channels = substream->runtime->channels; unsigned int rx_slot[4]; unsigned int tx_slot[4]; + bool ready; + int backend; + int protection_ret; int ret; switch (cpu_dai->id) { @@ -204,9 +237,50 @@ static int x1e80100_snd_prepare(struct snd_pcm_substream *substream) default: break; } + if (data->cfg->protected_speaker_feedback) { + backend = x1e80100_protection_backend(cpu_dai->id); + if (backend >= 0) { + sruntime = qcom_snd_sdw_get_stream(substream); + if (IS_ERR_OR_NULL(sruntime)) { + data->stream_prepared[cpu_dai->id] = false; + ret = x1e80100_set_protection_ready(rtd, cpu_dai, + backend, false); + if (ret && ret != -ENODEV) + return ret; + dev_warn(rtd->dev, + "speaker-feedback backend %d has no SoundWire runtime; using bypass\n", + cpu_dai->id); + return 0; + } + } + } + + ret = qcom_snd_sdw_prepare(substream, + &data->stream_prepared[cpu_dai->id]); + if (!data->cfg->protected_speaker_feedback) + return ret; + + backend = x1e80100_protection_backend(cpu_dai->id); + if (backend < 0) + return ret; + + ready = !ret && data->stream_prepared[cpu_dai->id]; + protection_ret = x1e80100_set_protection_ready(rtd, cpu_dai, backend, + ready); + if (!ready) { + if (protection_ret) + return ret ? ret : protection_ret; + dev_warn(rtd->dev, + "speaker-feedback backend %d unavailable; using bypass\n", + cpu_dai->id); + return 0; + } + if (protection_ret && protection_ret != -ENODEV) { + qcom_snd_sdw_hw_free(substream, &data->stream_prepared[cpu_dai->id]); + return protection_ret; + } - return qcom_snd_sdw_prepare(substream, - &data->stream_prepared[cpu_dai->id]); + return 0; } static int x1e80100_snd_hw_free(struct snd_pcm_substream *substream) @@ -214,13 +288,46 @@ static int x1e80100_snd_hw_free(struct snd_pcm_substream *substream) struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); struct x1e80100_snd_data *data = snd_soc_card_get_drvdata(rtd->card); struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(rtd, 0); + int backend; + int ret; + + if (data->cfg->protected_speaker_feedback) { + backend = x1e80100_protection_backend(cpu_dai->id); + if (backend >= 0) { + ret = x1e80100_set_protection_ready(rtd, cpu_dai, + backend, false); + if (ret && ret != -ENODEV) + return ret; + } + } return qcom_snd_sdw_hw_free(substream, &data->stream_prepared[cpu_dai->id]); } +static void x1e80100_snd_shutdown(struct snd_pcm_substream *substream) +{ + struct snd_soc_pcm_runtime *rtd = snd_soc_substream_to_rtd(substream); + struct x1e80100_snd_data *data = snd_soc_card_get_drvdata(rtd->card); + struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(rtd, 0); + int backend; + int ret; + + if (data->cfg->protected_speaker_feedback) { + backend = x1e80100_protection_backend(cpu_dai->id); + if (backend >= 0) { + ret = x1e80100_set_protection_ready(rtd, cpu_dai, + backend, false); + if (ret && ret != -ENODEV) + return; + } + } + + qcom_snd_sdw_shutdown(substream); +} + static const struct snd_soc_ops x1e80100_be_ops = { .startup = qcom_snd_sdw_startup, - .shutdown = qcom_snd_sdw_shutdown, + .shutdown = x1e80100_snd_shutdown, .hw_free = x1e80100_snd_hw_free, .prepare = x1e80100_snd_prepare, }; From 048a9b43e234831d09a4d65d4eb2277470c37600 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Mon, 31 Aug 2026 23:57:33 +0100 Subject: [PATCH 15/20] dt-bindings: sound: describe WSA884x speaker load Describe the optional nominal speaker load used to select board-specific PA gain and PBR thresholds. An absent property retains the existing 8-ohm defaults. Link: https://github.com/geocausa/SP11X1e-audio/commit/967b539690f263fdc2c1e0b6265af83be0c7e751 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/40932bba5ace44c7aa415e758b10876ac06fa923 Signed-off-by: Leon Silcott --- Documentation/devicetree/bindings/sound/qcom,wsa8840.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Documentation/devicetree/bindings/sound/qcom,wsa8840.yaml b/Documentation/devicetree/bindings/sound/qcom,wsa8840.yaml index 0f1efbd568e6f3..9744cea5ee42b8 100644 --- a/Documentation/devicetree/bindings/sound/qcom,wsa8840.yaml +++ b/Documentation/devicetree/bindings/sound/qcom,wsa8840.yaml @@ -59,6 +59,13 @@ properties: Native WSA data-port-5 channel-enable mask for the VISENSE protection sidechain. If absent, the codec default is used. + qcom,speaker-load-ohms: + enum: [4, 8] + description: | + Nominal speaker load impedance in ohms. A 4-ohm load selects the + matching PA gain and PBR thresholds when the amplifier reports a 2S + VPHX supply. If absent, the existing 8-ohm defaults are retained. + '#sound-dai-cells': enum: [0, 1] description: | From c912416c9ce1999119d9e1b7cab8252bcebbcccf Mon Sep 17 00:00:00 2001 From: geoca <272055834+geocausa@users.noreply.github.com> Date: Wed, 29 Jul 2026 16:28:47 +0100 Subject: [PATCH 16/20] ASoC: wsa884x: add opt-in 2S 4-ohm profile Read the nominal speaker load from firmware and apply the 2S 4-ohm PA gain, PBR, class-H, VCM, and UVLO values only when the amplifier also reports a 2S VPHX supply. Devices without the property retain the current defaults. [Leon: Re-lifted the profile onto the current driver, gated it by qcom,speaker-load-ohms and a fresh hardware-confirmed 2S status, and added explicit default restoration after failed re-attach reads.] Link: https://github.com/geocausa/SP11X1e-audio/commit/967b539690f263fdc2c1e0b6265af83be0c7e751 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/40932bba5ace44c7aa415e758b10876ac06fa923 Signed-off-by: Leon Silcott --- sound/soc/codecs/wsa884x.c | 144 ++++++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 1 deletion(-) diff --git a/sound/soc/codecs/wsa884x.c b/sound/soc/codecs/wsa884x.c index 5a3fa70e9a2a1a..50c32492bd16ae 100644 --- a/sound/soc/codecs/wsa884x.c +++ b/sound/soc/codecs/wsa884x.c @@ -155,6 +155,7 @@ #define WSA884X_BBM_CTL (WSA884X_ANA_SPK_TOP_BASE + 0x28) #define WSA884X_TOP_MISC1 (WSA884X_ANA_SPK_TOP_BASE + 0x29) #define WSA884X_DAC_VCM_CTRL_REG7 (WSA884X_ANA_SPK_TOP_BASE + 0x2a) +#define WSA884X_DAC_VCM_CTRL_REG7_FINAL_OVERRIDE_MASK 0x02 #define WSA884X_TOP_BIAS_REG5 (WSA884X_ANA_SPK_TOP_BASE + 0x2b) #define WSA884X_DRV_LF_MISC_CTL2 (WSA884X_ANA_SPK_TOP_BASE + 0x2c) #define WSA884X_SPK_TOP_SPARE_TUNE_2 (WSA884X_ANA_SPK_TOP_BASE + 0x2d) @@ -437,7 +438,9 @@ #define WSA884X_CLSH_CTL_0_INPUT_EN_SHIFT 1 #define WSA884X_CLSH_CTL_0_CLSH_EN_SHIFT 0 #define WSA884X_CLSH_CTL_1 (WSA884X_DIG_CTRL0_BASE + 0xd1) +#define WSA884X_CLSH_CTL_1_SLR_MAX_MASK GENMASK(7, 4) #define WSA884X_CLSH_V_HD_PA (WSA884X_DIG_CTRL0_BASE + 0xd2) +#define WSA884X_CLSH_V_HD_PA_MASK GENMASK(4, 0) #define WSA884X_CLSH_V_PA_MIN (WSA884X_DIG_CTRL0_BASE + 0xd3) #define WSA884X_CLSH_OVRD_VAL (WSA884X_DIG_CTRL0_BASE + 0xd4) #define WSA884X_CLSH_HARD_MAX (WSA884X_DIG_CTRL0_BASE + 0xd5) @@ -750,6 +753,15 @@ struct wsa884x_priv { struct mutex sp_lock; unsigned int temperature; bool pa_on; + unsigned int supply_config; + unsigned int speaker_load_ohms; +}; + +enum wsa884x_supply_config { + WSA884X_SUPPLY_EXT_ABOVE_3S, + WSA884X_SUPPLY_1S, + WSA884X_SUPPLY_2S, + WSA884X_SUPPLY_3S, }; enum { @@ -1483,11 +1495,26 @@ static const struct reg_sequence wsa884x_reg_init[] = { { WSA884X_OTP_REG_40, FIELD_PREP_CONST(WSA884X_OTP_REG_40_ISENSE_RESCAL_MASK, 0x8) }, }; +static bool wsa884x_uses_2s_4ohm_profile(struct wsa884x_priv *wsa884x) +{ + return wsa884x->speaker_load_ohms == 4 && + wsa884x->supply_config == WSA884X_SUPPLY_2S; +} + static void wsa884x_set_gain_parameters(struct wsa884x_priv *wsa884x) { struct regmap *regmap = wsa884x->regmap; unsigned int min_gain, igain, vgain, comp_offset; + if (wsa884x_uses_2s_4ohm_profile(wsa884x) && + wsa884x->dev_mode == WSA884X_SPEAKER) { + comp_offset = COMP_OFFSET0; + min_gain = G_0_DB; + igain = ISENSE_6_DB; + vgain = VSENSE_M21_DB; + goto apply; + } + /* * Downstream sets gain parameters customized per boards per use-case. * Choose here some sane values matching knowon users, like QRD8550 @@ -1510,6 +1537,7 @@ static void wsa884x_set_gain_parameters(struct wsa884x_priv *wsa884x) vgain = VSENSE_M24_DB; } +apply: regmap_update_bits(regmap, WSA884X_ISENSE2, WSA884X_ISENSE2_ISENSE_GAIN_CTL_MASK, FIELD_PREP(WSA884X_ISENSE2_ISENSE_GAIN_CTL_MASK, igain)); @@ -1535,18 +1563,121 @@ static void wsa884x_set_gain_parameters(struct wsa884x_priv *wsa884x) } } +static void wsa884x_apply_2s_4ohm_profile(struct wsa884x_priv *wsa884x) +{ + static const struct reg_sequence pbr_thresholds[] = { + { WSA884X_CLSH_VTH1, WSA884X_VTH_TO_REG(808) }, + { WSA884X_CLSH_VTH2, WSA884X_VTH_TO_REG(839) }, + { WSA884X_CLSH_VTH3, WSA884X_VTH_TO_REG(894) }, + { WSA884X_CLSH_VTH4, WSA884X_VTH_TO_REG(925) }, + { WSA884X_CLSH_VTH5, WSA884X_VTH_TO_REG(973) }, + { WSA884X_CLSH_VTH6, WSA884X_VTH_TO_REG(996) }, + { WSA884X_CLSH_VTH7, WSA884X_VTH_TO_REG(1051) }, + { WSA884X_CLSH_VTH8, WSA884X_VTH_TO_REG(1114) }, + { WSA884X_CLSH_VTH9, WSA884X_VTH_TO_REG(1184) }, + { WSA884X_CLSH_VTH10, WSA884X_VTH_TO_REG(1255) }, + { WSA884X_CLSH_VTH11, WSA884X_VTH_TO_REG(1318) }, + { WSA884X_CLSH_VTH12, WSA884X_VTH_TO_REG(1467) }, + { WSA884X_CLSH_VTH13, WSA884X_VTH_TO_REG(1616) }, + { WSA884X_CLSH_VTH14, WSA884X_VTH_TO_REG(1788) }, + { WSA884X_CLSH_VTH15, WSA884X_VTH_TO_REG(2000) }, + }; + struct regmap *regmap = wsa884x->regmap; + + if (wsa884x->speaker_load_ohms != 4) + return; + + if (!wsa884x_uses_2s_4ohm_profile(wsa884x)) { + /* + * A SoundWire detach leaves the cached profile values behind. Put + * every fixed 2S override back on the upstream defaults when a + * subsequent status read does not confirm 2S. wsa884x_reg_init + * restores the PBR thresholds before this function is called. + */ + regmap_write(regmap, WSA884X_OCP_CTL, 0xc6); + regmap_update_bits(regmap, WSA884X_CLSH_CTL_1, + WSA884X_CLSH_CTL_1_SLR_MAX_MASK, + FIELD_PREP(WSA884X_CLSH_CTL_1_SLR_MAX_MASK, 0x8)); + regmap_update_bits(regmap, WSA884X_CLSH_V_HD_PA, + WSA884X_CLSH_V_HD_PA_MASK, + FIELD_PREP(WSA884X_CLSH_V_HD_PA_MASK, 0xc)); + regmap_write(regmap, WSA884X_DAC_VCM_CTRL_REG2, 0x00); + regmap_write(regmap, WSA884X_DAC_VCM_CTRL_REG3, 0x00); + regmap_write(regmap, WSA884X_DAC_VCM_CTRL_REG4, 0x00); + regmap_write(regmap, WSA884X_DAC_VCM_CTRL_REG5, 0x00); + regmap_write(regmap, WSA884X_DAC_VCM_CTRL_REG6, 0x00); + regmap_update_bits(regmap, WSA884X_DAC_VCM_CTRL_REG7, + WSA884X_DAC_VCM_CTRL_REG7_FINAL_OVERRIDE_MASK, 0); + regmap_write(regmap, WSA884X_UVLO_PROG, 0x99); + regmap_write(regmap, WSA884X_PA_FSM_TIMER0, 0x80); + regmap_write(regmap, WSA884X_UVLO_DEGLITCH_CTL, 0x05); + regmap_write(regmap, WSA884X_UVLO_PROG1, 0x70); + regmap_update_bits(regmap, WSA884X_TOP_CTRL1, + WSA884X_TOP_CTRL1_OCP_LOWVBAT_ITH_EN_MASK, + WSA884X_TOP_CTRL1_OCP_LOWVBAT_ITH_EN_MASK); + return; + } + + regmap_write(regmap, WSA884X_OCP_CTL, 0xf6); + regmap_multi_reg_write(regmap, pbr_thresholds, + ARRAY_SIZE(pbr_thresholds)); + regmap_update_bits(regmap, WSA884X_CLSH_CTL_1, + WSA884X_CLSH_CTL_1_SLR_MAX_MASK, + FIELD_PREP(WSA884X_CLSH_CTL_1_SLR_MAX_MASK, 0x2)); + regmap_update_bits(regmap, WSA884X_CLSH_V_HD_PA, + WSA884X_CLSH_V_HD_PA_MASK, + FIELD_PREP(WSA884X_CLSH_V_HD_PA_MASK, 0x13)); + regmap_write(regmap, WSA884X_DAC_VCM_CTRL_REG2, 0x06); + regmap_write(regmap, WSA884X_DAC_VCM_CTRL_REG3, 0x14); + regmap_write(regmap, WSA884X_DAC_VCM_CTRL_REG4, 0x19); + regmap_write(regmap, WSA884X_DAC_VCM_CTRL_REG5, 0x1b); + regmap_write(regmap, WSA884X_DAC_VCM_CTRL_REG6, 0x1c); + regmap_update_bits(regmap, WSA884X_DAC_VCM_CTRL_REG7, + WSA884X_DAC_VCM_CTRL_REG7_FINAL_OVERRIDE_MASK, + WSA884X_DAC_VCM_CTRL_REG7_FINAL_OVERRIDE_MASK); + regmap_write(regmap, WSA884X_UVLO_PROG, 0x77); + regmap_write(regmap, WSA884X_PA_FSM_TIMER0, 0xc0); + regmap_write(regmap, WSA884X_UVLO_DEGLITCH_CTL, 0x1d); + regmap_write(regmap, WSA884X_UVLO_PROG1, 0x40); + regmap_update_bits(regmap, WSA884X_TOP_CTRL1, + WSA884X_TOP_CTRL1_OCP_LOWVBAT_ITH_EN_MASK, 0); +} + static void wsa884x_init(struct wsa884x_priv *wsa884x) { unsigned int wo_ctl_0; + unsigned int supply_config; unsigned int variant = 0; if (!regmap_read(wsa884x->regmap, WSA884X_OTP_REG_0, &variant)) variant = variant & WSA884X_OTP_REG_0_ID_MASK; + /* Never reuse a prior attach's supply status if this read fails. */ + wsa884x->supply_config = WSA884X_SUPPLY_1S; + if (wsa884x->speaker_load_ohms == 4) { + if (regmap_read(wsa884x->regmap, WSA884X_VPHX_SYS_EN_STATUS, + &supply_config)) { + dev_warn(wsa884x->dev, + "cannot read VPHX supply configuration; retaining defaults\n"); + } else { + wsa884x->supply_config = supply_config; + if (supply_config == WSA884X_SUPPLY_2S) + dev_dbg(wsa884x->dev, + "selected 2S 4-ohm speaker profile\n"); + else + dev_warn(wsa884x->dev, + "4-ohm profile requires 2S VPHX; retaining defaults\n"); + } + } + regmap_multi_reg_write(wsa884x->regmap, wsa884x_reg_init, ARRAY_SIZE(wsa884x_reg_init)); + wsa884x_apply_2s_4ohm_profile(wsa884x); wo_ctl_0 = 0xc; + if (wsa884x_uses_2s_4ohm_profile(wsa884x)) + wo_ctl_0 |= FIELD_PREP(WSA884X_ANA_WO_CTL_0_VPHX_SYS_EN_MASK, + WSA884X_SUPPLY_2S); wo_ctl_0 |= FIELD_PREP(WSA884X_ANA_WO_CTL_0_DAC_CM_CLAMP_EN_MASK, WSA884X_ANA_WO_CTL_0_DAC_CM_CLAMP_EN_MODE_SPEAKER); /* Assume that compander is enabled by default unless it is haptics sku */ @@ -1703,7 +1834,8 @@ static void wsa884x_spkr_post_pmu(struct snd_soc_component *component, if (wsa884x->port_enable[WSA884X_PORT_PBR]) { curr_ovrd_en = 0x0; - curr_limit = 0x15; + curr_limit = wsa884x_uses_2s_4ohm_profile(wsa884x) ? + 0x11 : 0x15; } else { curr_ovrd_en = 0x1; if (wsa884x->dev_mode == WSA884X_RECEIVER) @@ -2157,6 +2289,7 @@ static int wsa884x_probe(struct sdw_slave *pdev, struct wsa884x_priv *wsa884x; unsigned int i; u32 cps_offset1; + u32 speaker_load_ohms; u32 visense_channel_mask; int ret; @@ -2191,6 +2324,15 @@ static int wsa884x_probe(struct sdw_slave *pdev, wsa884x->slave = pdev; wsa884x->dev = dev; wsa884x->dev_mode = WSA884X_SPEAKER; + wsa884x->speaker_load_ohms = 8; + wsa884x->supply_config = WSA884X_SUPPLY_1S; + if (!of_property_read_u32(dev->of_node, "qcom,speaker-load-ohms", + &speaker_load_ohms)) { + if (speaker_load_ohms != 4 && speaker_load_ohms != 8) + return dev_err_probe(dev, -EINVAL, + "invalid qcom,speaker-load-ohms\n"); + wsa884x->speaker_load_ohms = speaker_load_ohms; + } wsa884x->sconfig.ch_count = 1; wsa884x->sconfig.bps = 1; wsa884x->sconfig.direction = SDW_DATA_DIR_RX; From e1dfe375e1ed88ce7503a2192f8927e112c8f7e3 Mon Sep 17 00:00:00 2001 From: geoca <272055834+geocausa@users.noreply.github.com> Date: Sun, 23 Aug 2026 08:07:44 +0100 Subject: [PATCH 17/20] ASoC: codecs: order WSA protection clocks after both PAs Have each opted-in WSA884x report a successful, error-free PA enable to the WSA macro. Enable both protection paths after the second confirmed PA and disable them before the first PA teardown. Require protected feedback DAIs to have a live 2S/4-ohm classification so VI/CPS readiness cannot outrun the PA profile. Roll back PA and DRE state when an enable write, PA status check, or macro lookup fails. Keep the counters, lock, and enabled state in each WSA macro instance so non-protected cards remain unchanged. [Leon: Replaced the source globals with per-macro state, scoped lookup to the source card, balanced duplicate/remove events per amp, made the helper Kconfig-safe, required a confirmed 2S/4-ohm feedback profile, and withheld the PA event after write or FSM errors.] Link: https://github.com/geocausa/SP11X1e-audio/commit/31466d62b38c8138d476b2fd3606a76272fb3d34 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/e72bffcb10ef197105f0dc8f0b173d99f472114b Signed-off-by: Leon Silcott --- sound/soc/codecs/lpass-wsa-macro.c | 67 ++++++++++++++++++++++ sound/soc/codecs/lpass-wsa-macro.h | 15 +++++ sound/soc/codecs/wsa884x.c | 90 ++++++++++++++++++++++++++---- 3 files changed, 160 insertions(+), 12 deletions(-) diff --git a/sound/soc/codecs/lpass-wsa-macro.c b/sound/soc/codecs/lpass-wsa-macro.c index 8d0ce721bcf747..4d0430a2917e53 100644 --- a/sound/soc/codecs/lpass-wsa-macro.c +++ b/sound/soc/codecs/lpass-wsa-macro.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -76,6 +77,7 @@ #define CDC_WSA_TX2_SPKR_PROT_PATH_CFG0 (0x0288) #define CDC_WSA_TX3_SPKR_PROT_PATH_CTL (0x02A4) #define CDC_WSA_TX3_SPKR_PROT_PATH_CFG0 (0x02A8) +#define WSA_MACRO_PROTECTION_PA_COUNT 2 #define CDC_WSA_INTR_CTRL_CFG (0x0340) #define CDC_WSA_INTR_CTRL_CLR_COMMIT (0x0344) #define CDC_WSA_INTR_CTRL_PIN1_MASK0 (0x0360) @@ -417,6 +419,10 @@ struct wsa_macro { int is_softclip_on[WSA_MACRO_SOFTCLIP_MAX]; int softclip_clk_users[WSA_MACRO_SOFTCLIP_MAX]; bool protected_feedback; + /* Protects protection_pa_users and protection_clocks_enabled. */ + struct mutex protection_lock; + unsigned int protection_pa_users; + bool protection_clocks_enabled; struct regmap *regmap; struct clk *mclk; struct clk *npl; @@ -427,6 +433,8 @@ struct wsa_macro { }; #define to_wsa_macro(_hw) container_of(_hw, struct wsa_macro, hw) +static const struct snd_soc_component_driver wsa_macro_component_drv; + static const struct wsa_reg_layout wsa_codec_v2_1 = { .rx_intx_1_mix_inp0_sel_mask = GENMASK(2, 0), .rx_intx_1_mix_inp1_sel_mask = GENMASK(5, 3), @@ -1510,6 +1518,63 @@ static void wsa_macro_enable_disable_vi_sense(struct snd_soc_component *componen } } +static void wsa_macro_set_protection_clocks(struct snd_soc_component *component, + bool enable) +{ + wsa_macro_enable_disable_vi_sense(component, enable, + CDC_WSA_TX0_SPKR_PROT_PATH_CTL, + CDC_WSA_TX1_SPKR_PROT_PATH_CTL, 0); + wsa_macro_enable_disable_vi_sense(component, enable, + CDC_WSA_TX2_SPKR_PROT_PATH_CTL, + CDC_WSA_TX3_SPKR_PROT_PATH_CTL, 0); +} + +bool wsa_macro_protection_pa_event(struct snd_soc_component *source, + bool enable) +{ + struct snd_soc_component *component; + struct wsa_macro *wsa; + + if (!source || !source->card) + return false; + + for_each_card_components(source->card, component) { + if (component->driver != &wsa_macro_component_drv) + continue; + + wsa = snd_soc_component_get_drvdata(component); + if (!wsa->protected_feedback) + continue; + + guard(mutex)(&wsa->protection_lock); + + if (enable) { + if (wsa->protection_pa_users < WSA_MACRO_PROTECTION_PA_COUNT) + wsa->protection_pa_users++; + + if (wsa->protection_pa_users == WSA_MACRO_PROTECTION_PA_COUNT && + !wsa->protection_clocks_enabled) { + wsa_macro_set_protection_clocks(component, true); + wsa->protection_clocks_enabled = true; + } + } else { + if (wsa->protection_pa_users == WSA_MACRO_PROTECTION_PA_COUNT && + wsa->protection_clocks_enabled) { + wsa_macro_set_protection_clocks(component, false); + wsa->protection_clocks_enabled = false; + } + + if (wsa->protection_pa_users) + wsa->protection_pa_users--; + } + + return true; + } + + return false; +} +EXPORT_SYMBOL_GPL(wsa_macro_protection_pa_event); + static void wsa_macro_enable_disable_vi_feedback(struct snd_soc_component *component, bool enable, u32 rate) { @@ -2738,6 +2803,8 @@ static int wsa_macro_probe(struct platform_device *pdev) if (!wsa) return -ENOMEM; + mutex_init(&wsa->protection_lock); + wsa->macro = devm_clk_get_optional(dev, "macro"); if (IS_ERR(wsa->macro)) return dev_err_probe(dev, PTR_ERR(wsa->macro), "unable to get macro clock\n"); diff --git a/sound/soc/codecs/lpass-wsa-macro.h b/sound/soc/codecs/lpass-wsa-macro.h index d3d62b3f6500c9..d88b92350c7d0d 100644 --- a/sound/soc/codecs/lpass-wsa-macro.h +++ b/sound/soc/codecs/lpass-wsa-macro.h @@ -3,6 +3,10 @@ #ifndef __LPASS_WSA_MACRO_H__ #define __LPASS_WSA_MACRO_H__ +#include + +struct snd_soc_component; + /* * Selects compander and smart boost settings * for a given speaker mode @@ -14,4 +18,15 @@ enum { int wsa_macro_set_spkr_mode(struct snd_soc_component *component, int mode); +#if IS_REACHABLE(CONFIG_SND_SOC_LPASS_WSA_MACRO) +bool wsa_macro_protection_pa_event(struct snd_soc_component *source, + bool enable); +#else +static inline bool +wsa_macro_protection_pa_event(struct snd_soc_component *source, bool enable) +{ + return false; +} +#endif + #endif /* __LPASS_WSA_MACRO_H__ */ diff --git a/sound/soc/codecs/wsa884x.c b/sound/soc/codecs/wsa884x.c index 50c32492bd16ae..7241c1d6765090 100644 --- a/sound/soc/codecs/wsa884x.c +++ b/sound/soc/codecs/wsa884x.c @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -27,6 +28,8 @@ #include #include +#include "lpass-wsa-macro.h" + #define WSA884X_BASE 0x3000 #define WSA884X_ANA_BG_TSADC_BASE (WSA884X_BASE + 0x0001) #define WSA884X_BG_CTRL (WSA884X_ANA_BG_TSADC_BASE + 0x00) @@ -298,6 +301,7 @@ #define WSA884X_PA_FSM_TIMER1 (WSA884X_DIG_CTRL0_BASE + 0x34) #define WSA884X_PA_FSM_STA0 (WSA884X_DIG_CTRL0_BASE + 0x35) #define WSA884X_PA_FSM_STA1 (WSA884X_DIG_CTRL0_BASE + 0x36) +#define WSA884X_PA_ERROR_MASK GENMASK(4, 0) #define WSA884X_PA_FSM_ERR_CTL (WSA884X_DIG_CTRL0_BASE + 0x37) #define WSA884X_PA_FSM_ERR_COND0 (WSA884X_DIG_CTRL0_BASE + 0x38) #define WSA884X_PA_FSM_ERR_COND1 (WSA884X_DIG_CTRL0_BASE + 0x39) @@ -755,6 +759,7 @@ struct wsa884x_priv { bool pa_on; unsigned int supply_config; unsigned int speaker_load_ohms; + bool protection_pa_reported; }; enum wsa884x_supply_config { @@ -1811,6 +1816,16 @@ static int wsa884x_codec_probe(struct snd_soc_component *comp) return 0; } +static void wsa884x_codec_remove(struct snd_soc_component *comp) +{ + struct wsa884x_priv *wsa884x = snd_soc_component_get_drvdata(comp); + + if (wsa884x->protection_pa_reported) { + wsa_macro_protection_pa_event(comp, false); + wsa884x->protection_pa_reported = false; + } +} + static void wsa884x_spkr_post_pmu(struct snd_soc_component *component, struct wsa884x_priv *wsa884x) { @@ -1918,6 +1933,7 @@ static const struct snd_soc_dapm_route wsa884x_audio_map[] = { static const struct snd_soc_component_driver wsa884x_component_drv = { .name = "WSA884x", .probe = wsa884x_codec_probe, + .remove = wsa884x_codec_remove, .controls = wsa884x_snd_controls, .num_controls = ARRAY_SIZE(wsa884x_snd_controls), .dapm_widgets = wsa884x_dapm_widgets, @@ -1940,6 +1956,9 @@ static int wsa884x_hw_params(struct snd_pcm_substream *substream, int active_ports = 0; int i; + if (dai->id && !wsa884x_uses_2s_4ohm_profile(wsa884x)) + return -ENODEV; + if (!wsa884x->protected_feedback) { wsa884x->active_ports = 0; for (i = 0; i < WSA884X_MAX_SWR_PORTS; i++) { @@ -2011,28 +2030,75 @@ static int wsa884x_hw_free(struct snd_pcm_substream *substream, return 0; } +static void wsa884x_disable_pa(struct snd_soc_component *component) +{ + snd_soc_component_write_field(component, WSA884X_DRE_CTL_1, + WSA884X_DRE_CTL_1_CSR_GAIN_EN_MASK, 0x0); + snd_soc_component_write_field(component, WSA884X_PA_FSM_EN, + WSA884X_PA_FSM_EN_GLOBAL_PA_EN_MASK, 0x0); +} + static int wsa884x_mute_stream(struct snd_soc_dai *dai, int mute, int stream) { struct snd_soc_component *component = dai->component; + struct wsa884x_priv *wsa884x = snd_soc_component_get_drvdata(component); + unsigned int pa_status = 0; + int ret; if (dai->id != 0) return 0; if (mute) { - snd_soc_component_write_field(component, WSA884X_DRE_CTL_1, - WSA884X_DRE_CTL_1_CSR_GAIN_EN_MASK, - 0x0); - snd_soc_component_write_field(component, WSA884X_PA_FSM_EN, - WSA884X_PA_FSM_EN_GLOBAL_PA_EN_MASK, - 0x0); + if (wsa884x->protection_pa_reported) { + wsa_macro_protection_pa_event(component, false); + wsa884x->protection_pa_reported = false; + } + + wsa884x_disable_pa(component); } else { - snd_soc_component_write_field(component, WSA884X_DRE_CTL_1, - WSA884X_DRE_CTL_1_CSR_GAIN_EN_MASK, - 0x1); - snd_soc_component_write_field(component, WSA884X_PA_FSM_EN, - WSA884X_PA_FSM_EN_GLOBAL_PA_EN_MASK, - 0x1); + ret = snd_soc_component_write_field(component, WSA884X_DRE_CTL_1, + WSA884X_DRE_CTL_1_CSR_GAIN_EN_MASK, + 0x1); + if (ret < 0) { + wsa884x_disable_pa(component); + return ret; + } + + ret = snd_soc_component_write_field(component, WSA884X_PA_FSM_EN, + WSA884X_PA_FSM_EN_GLOBAL_PA_EN_MASK, + 0x1); + if (ret < 0) { + wsa884x_disable_pa(component); + return ret; + } + + if (!wsa884x->protection_pa_reported && + wsa884x_uses_2s_4ohm_profile(wsa884x) && + wsa884x->protected_feedback) { + usleep_range(1000, 1100); + ret = regmap_read(wsa884x->regmap, WSA884X_PA_FSM_STA1, + &pa_status); + if (ret || (pa_status & WSA884X_PA_ERROR_MASK)) { + wsa884x_disable_pa(component); + if (ret) + dev_err(wsa884x->dev, + "cannot read protected PA status: %pe\n", + ERR_PTR(ret)); + else + dev_err(wsa884x->dev, + "protected PA start failed: status %#x\n", + pa_status); + return ret ?: -EIO; + } + + if (!wsa_macro_protection_pa_event(component, true)) { + wsa884x_disable_pa(component); + return -ENODEV; + } + + wsa884x->protection_pa_reported = true; + } } return 0; From b03caeb43bf20f66f05446f056ff4d582fab55ed Mon Sep 17 00:00:00 2001 From: geoca <272055834+geocausa@users.noreply.github.com> Date: Sun, 23 Aug 2026 08:07:44 +0100 Subject: [PATCH 18/20] arm64: dts: qcom: denali: describe VI and CPS audio paths Describe the WSA8845 VISENSE and CPS feedback links used by the protected speaker graph. Select them only from the Denali sound card and give that card a board-specific compatible ahead of the generic X1E80100 fallback. [Leon: Re-lifted only the common Denali VI/CPS graph and transport data, added the board-specific compatible, and omitted the experimental board target.] Link: https://github.com/geocausa/SP11X1e-audio/commit/31466d62b38c8138d476b2fd3606a76272fb3d34 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/6035b3e32c7f74c0dc41b4b0b1f45954c3ed9cfc Signed-off-by: Leon Silcott --- .../boot/dts/qcom/x1-microsoft-denali.dtsi | 60 +++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi b/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi index 0886eb11095d60..5b8240817bf6e2 100644 --- a/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi +++ b/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi @@ -307,7 +307,8 @@ }; sound { - compatible = "qcom,x1e80100-sndcard"; + compatible = "microsoft,denali-sndcard", + "qcom,x1e80100-sndcard"; model = "X1E80100-Microsoft-Surface-Pro-11"; audio-routing = "SpkrLeft IN", "WSA WSA_SPK1 OUT", "SpkrRight IN", "WSA WSA_SPK2 OUT", @@ -318,7 +319,7 @@ link-name = "WSA Playback"; codec { - sound-dai = <&left_spkr>, <&right_spkr>, + sound-dai = <&left_spkr 0>, <&right_spkr 0>, <&swr0 0>, <&lpass_wsamacro 0>; }; @@ -331,6 +332,51 @@ }; }; + wsa-vi-dai-link { + link-name = "WSA VI Protection"; + + /* + * One two-channel Windows-equivalent feedback transport: + * amplifier VISENSE ports 10/11 -> WSA macro VI -> + * SoundWire DIN0 -> AudioReach WSA TX0. + */ + codec { + sound-dai = <&left_spkr 1>, <&right_spkr 1>, + <&swr0 9>, <&lpass_wsamacro 2>; + }; + + cpu { + sound-dai = <&q6apmbedai WSA_CODEC_DMA_TX_0>; + }; + + platform { + sound-dai = <&q6apm>; + }; + }; + + wsa-cps-dai-link { + link-name = "WSA CPS Protection"; + + /* + * Dedicated CPS sidechain: both WSA8845 DP6 sources share + * physical SoundWire master port 13. swr0 DAI 10 is only + * the independent ASoC/SoundWire stream handle; qcom,port-mapping + * below remains authoritative for the physical master ports. + */ + codec { + sound-dai = <&left_spkr 2>, <&right_spkr 2>, + <&swr0 10>, <&lpass_wsamacro 4>; + }; + + cpu { + sound-dai = <&q6apmbedai WSA_CODEC_DMA_TX_1>; + }; + + platform { + sound-dai = <&q6apm>; + }; + }; + va-dai-link { link-name = "VA Capture"; @@ -1075,11 +1121,14 @@ compatible = "sdw20217020400"; reg = <0 0>; reset-gpios = <&lpass_tlmm 12 GPIO_ACTIVE_LOW>; - #sound-dai-cells = <0>; + #sound-dai-cells = <1>; sound-name-prefix = "SpkrLeft"; vdd-1p8-supply = <&vreg_l15b_1p8>; vdd-io-supply = <&vreg_l12b_1p2>; qcom,port-mapping = <1 2 3 7 10 13>; + qcom,visense-channel-mask = <3>; + qcom,enable-cps; + qcom,cps-offset1 = <0>; }; /* WSA8845, Right Speaker */ @@ -1087,11 +1136,14 @@ compatible = "sdw20217020400"; reg = <0 1>; reset-gpios = <&lpass_tlmm 12 GPIO_ACTIVE_LOW>; - #sound-dai-cells = <0>; + #sound-dai-cells = <1>; sound-name-prefix = "SpkrRight"; vdd-1p8-supply = <&vreg_l15b_1p8>; vdd-io-supply = <&vreg_l12b_1p2>; qcom,port-mapping = <4 5 6 7 11 13>; + qcom,visense-channel-mask = <3>; + qcom,enable-cps; + qcom,cps-offset1 = <25>; }; }; From 766f34d63900cf897ed9902005c5a5bac1b1ec5b Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Mon, 31 Aug 2026 23:58:02 +0100 Subject: [PATCH 19/20] arm64: dts: qcom: denali: select 4-ohm speaker profile Describe both integrated WSA8845 speaker loads as 4 ohms so the codec can select the matching PA profile after confirming the hardware 2S supply. Link: https://github.com/geocausa/SP11X1e-audio/commit/967b539690f263fdc2c1e0b6265af83be0c7e751 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/6035b3e32c7f74c0dc41b4b0b1f45954c3ed9cfc Signed-off-by: Leon Silcott --- arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi b/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi index 5b8240817bf6e2..8452469efae27d 100644 --- a/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi +++ b/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi @@ -1126,6 +1126,7 @@ vdd-1p8-supply = <&vreg_l15b_1p8>; vdd-io-supply = <&vreg_l12b_1p2>; qcom,port-mapping = <1 2 3 7 10 13>; + qcom,speaker-load-ohms = <4>; qcom,visense-channel-mask = <3>; qcom,enable-cps; qcom,cps-offset1 = <0>; @@ -1141,6 +1142,7 @@ vdd-1p8-supply = <&vreg_l15b_1p8>; vdd-io-supply = <&vreg_l12b_1p2>; qcom,port-mapping = <4 5 6 7 11 13>; + qcom,speaker-load-ohms = <4>; qcom,visense-channel-mask = <3>; qcom,enable-cps; qcom,cps-offset1 = <25>; From 61c0ef0695dad1e612c42bce916e4418dc7399d5 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Wed, 2 Sep 2026 10:57:59 +0100 Subject: [PATCH 20/20] ASoC: q6dsp: allow modules with more than eight input ports The SP11 FullIO topology SAL module declares ten input ports. AR_MAX_MOD_LINKS bounds the stored output connection arrays, but it does not bound this module metadata. Rejecting the input count prevents the sound card from probing with -EINVAL. Keep the output-port array bound while accepting the valid SAL declaration. Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/topology.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/sound/soc/qcom/qdsp6/topology.c b/sound/soc/qcom/qdsp6/topology.c index 067145e83aa30a..aa88a30d5c1658 100644 --- a/sound/soc/qcom/qdsp6/topology.c +++ b/sound/soc/qcom/qdsp6/topology.c @@ -649,8 +649,6 @@ static struct audioreach_module *audioreach_parse_common_tokens(struct q6apm *ap break; case AR_TKN_U32_MODULE_MAX_IP_PORTS: max_ip_port = le32_to_cpu(mod_elem->value); - if (max_ip_port > AR_MAX_MOD_LINKS) - return ERR_PTR(-EINVAL); break; case AR_TKN_U32_MODULE_MAX_OP_PORTS: max_op_port = le32_to_cpu(mod_elem->value);