From f416c93f59e52f4a442e049f439c2cd65b236ea5 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Wed, 26 Aug 2026 18:37:03 +0100 Subject: [PATCH 01/49] ASoC: codecs: lpass-va-macro: fix Denali DMIC divider Denali uses a 4.8 MHz direct-DMIC rate with the VA path clocked at 19.2 MHz. Validate the sample rate against that clock so the driver selects DIV4, matching the native Windows programming, instead of DIV2. Keep the existing calculation on every other machine. This is based on geoca's Windows-parity analysis in patch 0072. Link: https://github.com/geocausa/SP11X1e-audio/blob/a1d51ecc7416a905acdad50d31600fff7f28ac1c/patches/0072-ASoC-lpass-va-macro-SP11-match-Windows-DMIC-divider.patch Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/58e36b1acb042b128a450a42cbab3da462368c15 Co-authored-by: geoca <272055834+geocausa@users.noreply.github.com> Signed-off-by: Leon Silcott --- sound/soc/codecs/lpass-va-macro.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sound/soc/codecs/lpass-va-macro.c b/sound/soc/codecs/lpass-va-macro.c index d043e79dc7f393..f22256557cc1bf 100644 --- a/sound/soc/codecs/lpass-va-macro.c +++ b/sound/soc/codecs/lpass-va-macro.c @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -1454,6 +1455,10 @@ static int va_macro_validate_dmic_sample_rate(u32 dmic_sample_rate, u32 div_factor; u32 mclk_rate = VA_MACRO_MCLK_FREQ; + /* Denali programs its divider from the 19.2 MHz VA MCLK vote. */ + if (of_machine_is_compatible("microsoft,denali")) + mclk_rate = 2 * VA_MACRO_MCLK_FREQ; + if (!dmic_sample_rate || mclk_rate % dmic_sample_rate != 0) goto undefined_rate; From 25b8d91c622789e6256aa8862ad6ad41933d2581 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Wed, 26 Aug 2026 18:37:03 +0100 Subject: [PATCH 02/49] ASoC: codecs: lpass: share Denali VA DMIC clocks The Surface Pro 11 microphone array feeds the TX macro while the VA macro owns its direct-DMIC pad clocks. Without a cross-macro clock request, the TX capture path cannot reproduce the working firmware sequence. Add an LPASS-internal DMIC clock broker, register the Denali VA macro as its provider, and request DMIC1 then DMIC0 from the TX DEC event group. Validate the exact two-channel Denali route before changing the clocks. Both the provider and consumer paths are gated by the microsoft,denali root compatible. Other machines retain the existing VA and TX behavior; the common broker remains unused on those systems. This implementation follows geoca's Windows-parity work in patch 0078. Link: https://github.com/geocausa/SP11X1e-audio/blob/a1d51ecc7416a905acdad50d31600fff7f28ac1c/patches/0078-ASoC-lpass-SP11-share-VA-DMIC-clock-with-TX-capture.patch Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/58e36b1acb042b128a450a42cbab3da462368c15 Co-authored-by: geoca <272055834+geocausa@users.noreply.github.com> Signed-off-by: Leon Silcott --- sound/soc/codecs/lpass-macro-common.c | 52 +++++++++++++++++++ sound/soc/codecs/lpass-macro-common.h | 8 +++ sound/soc/codecs/lpass-tx-macro.c | 75 +++++++++++++++++++++++++++ sound/soc/codecs/lpass-va-macro.c | 26 ++++++++++ 4 files changed, 161 insertions(+) diff --git a/sound/soc/codecs/lpass-macro-common.c b/sound/soc/codecs/lpass-macro-common.c index 6e3b8d0897dd62..f42887caf64e87 100644 --- a/sound/soc/codecs/lpass-macro-common.c +++ b/sound/soc/codecs/lpass-macro-common.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-only // Copyright (c) 2022, The Linux Foundation. All rights reserved. +#include #include #include #include @@ -14,6 +15,10 @@ static DEFINE_MUTEX(lpass_codec_mutex); static enum lpass_codec_version lpass_codec_version; +static DEFINE_MUTEX(lpass_dmic_clk_mutex); +static void *lpass_dmic_clk_priv; +static const struct lpass_macro_dmic_clk_ops *lpass_dmic_clk_ops; + struct lpass_macro *lpass_macro_pds_init(struct device *dev) { struct lpass_macro *l_pds; @@ -89,5 +94,52 @@ enum lpass_codec_version lpass_macro_get_codec_version(void) } EXPORT_SYMBOL_GPL(lpass_macro_get_codec_version); +int lpass_macro_register_dmic_clk_provider(void *priv, const struct lpass_macro_dmic_clk_ops *ops) +{ + int ret = 0; + + if (!priv || !ops || !ops->request) + return -EINVAL; + + mutex_lock(&lpass_dmic_clk_mutex); + if (lpass_dmic_clk_priv && lpass_dmic_clk_priv != priv) { + ret = -EBUSY; + } else { + lpass_dmic_clk_priv = priv; + lpass_dmic_clk_ops = ops; + } + mutex_unlock(&lpass_dmic_clk_mutex); + + return ret; +} +EXPORT_SYMBOL_GPL(lpass_macro_register_dmic_clk_provider); + +void lpass_macro_unregister_dmic_clk_provider(void *priv) +{ + mutex_lock(&lpass_dmic_clk_mutex); + if (lpass_dmic_clk_priv == priv) { + lpass_dmic_clk_ops = NULL; + lpass_dmic_clk_priv = NULL; + } + mutex_unlock(&lpass_dmic_clk_mutex); +} +EXPORT_SYMBOL_GPL(lpass_macro_unregister_dmic_clk_provider); + +int lpass_macro_dmic_clk_request(unsigned int dmic, bool enable) +{ + int ret; + + mutex_lock(&lpass_dmic_clk_mutex); + if (!lpass_dmic_clk_ops) + ret = -ENODEV; + else + ret = lpass_dmic_clk_ops->request(lpass_dmic_clk_priv, dmic, + enable); + mutex_unlock(&lpass_dmic_clk_mutex); + + return ret; +} +EXPORT_SYMBOL_GPL(lpass_macro_dmic_clk_request); + MODULE_DESCRIPTION("Common macro driver"); MODULE_LICENSE("GPL"); diff --git a/sound/soc/codecs/lpass-macro-common.h b/sound/soc/codecs/lpass-macro-common.h index 10ad682019fa7b..e13b1e541da63b 100644 --- a/sound/soc/codecs/lpass-macro-common.h +++ b/sound/soc/codecs/lpass-macro-common.h @@ -42,6 +42,14 @@ void lpass_macro_pds_exit(struct lpass_macro *pds); void lpass_macro_set_codec_version(enum lpass_codec_version version); enum lpass_codec_version lpass_macro_get_codec_version(void); +struct lpass_macro_dmic_clk_ops { + int (*request)(void *priv, unsigned int dmic, bool enable); +}; + +int lpass_macro_register_dmic_clk_provider(void *priv, const struct lpass_macro_dmic_clk_ops *ops); +void lpass_macro_unregister_dmic_clk_provider(void *priv); +int lpass_macro_dmic_clk_request(unsigned int dmic, bool enable); + static inline void lpass_macro_pds_exit_action(void *pds) { lpass_macro_pds_exit(pds); diff --git a/sound/soc/codecs/lpass-tx-macro.c b/sound/soc/codecs/lpass-tx-macro.c index 0cbf50647ff57c..fb961bbb9db805 100644 --- a/sound/soc/codecs/lpass-tx-macro.c +++ b/sound/soc/codecs/lpass-tx-macro.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -49,6 +50,7 @@ #define CDC_TX_INP_MUX_ADC_MUXn_CFG0(n) (0x0100 + 0x8 * n) #define CDC_TX_MACRO_SWR_MIC_MUX_SEL_MASK GENMASK(3, 0) #define CDC_TX_MACRO_DMIC_MUX_SEL_MASK GENMASK(7, 4) +#define CDC_TX_MACRO_DEC_MUX_SEL_MASK GENMASK(1, 0) #define CDC_TX_INP_MUX_ADC_MUX0_CFG0 (0x0100) #define CDC_TX_INP_MUX_ADC_MUXn_CFG1(n) (0x0104 + 0x8 * n) #define CDC_TX_INP_MUX_ADC_MUX0_CFG1 (0x0104) @@ -285,6 +287,7 @@ struct tx_macro { int dec_mode[NUM_DECIMATORS]; struct lpass_macro *pds; bool bcs_clk_en; + bool denali_va_dmic_active; }; #define to_tx_macro(_hw) container_of(_hw, struct tx_macro, hw) @@ -906,6 +909,69 @@ static int tx_macro_tx_mixer_put(struct snd_kcontrol *kcontrol, return 1; } +static int tx_macro_denali_va_dmic_request(struct snd_soc_component *component, + bool enable) +{ + struct tx_macro *tx = snd_soc_component_get_drvdata(component); + unsigned long expected = BIT(TX_MACRO_DEC0) | BIT(TX_MACRO_DEC1); + unsigned int dec0_src, dec1_src, dec0_dmic, dec1_dmic; + int ret, ret2; + + if (!of_machine_is_compatible("microsoft,denali")) + return 0; + + if (enable == tx->denali_va_dmic_active) + return 0; + + if (!enable) { + ret = lpass_macro_dmic_clk_request(1, false); + ret2 = lpass_macro_dmic_clk_request(0, false); + if (!ret && ret2) + ret = ret2; + if (!ret) + tx->denali_va_dmic_active = false; + + return ret; + } + + if (tx->active_ch_mask[TX_MACRO_AIF1_CAP] != expected || + tx->active_ch_mask[TX_MACRO_AIF2_CAP] || + tx->active_ch_mask[TX_MACRO_AIF3_CAP]) + return -EINVAL; + + dec0_src = snd_soc_component_read(component, + CDC_TX_INP_MUX_ADC_MUXn_CFG1(TX_MACRO_DEC0)); + dec0_src &= CDC_TX_MACRO_DEC_MUX_SEL_MASK; + dec1_src = snd_soc_component_read(component, + CDC_TX_INP_MUX_ADC_MUXn_CFG1(TX_MACRO_DEC1)); + dec1_src &= CDC_TX_MACRO_DEC_MUX_SEL_MASK; + dec0_dmic = snd_soc_component_read(component, + CDC_TX_INP_MUX_ADC_MUXn_CFG0(TX_MACRO_DEC0)); + dec0_dmic = FIELD_GET(CDC_TX_MACRO_DMIC_MUX_SEL_MASK, dec0_dmic); + dec1_dmic = snd_soc_component_read(component, + CDC_TX_INP_MUX_ADC_MUXn_CFG0(TX_MACRO_DEC1)); + dec1_dmic = FIELD_GET(CDC_TX_MACRO_DMIC_MUX_SEL_MASK, dec1_dmic); + + /* Windows EP16 uses DEC0 <- DMIC1 and DEC1 <- DMIC0. */ + if (dec0_src != MSM_DMIC || dec1_src != MSM_DMIC || + dec0_dmic != 2 || dec1_dmic != 1) + return -EINVAL; + + ret = lpass_macro_dmic_clk_request(1, true); + if (ret) + return ret; + + ret = lpass_macro_dmic_clk_request(0, true); + if (ret) { + lpass_macro_dmic_clk_request(1, false); + return ret; + } + + tx->denali_va_dmic_active = true; + + return 0; +} + static int tx_macro_enable_dec(struct snd_soc_dapm_widget *w, struct snd_kcontrol *kcontrol, int event) { @@ -915,6 +981,7 @@ static int tx_macro_enable_dec(struct snd_soc_dapm_widget *w, u8 hpf_cut_off_freq; int hpf_delay = TX_MACRO_DMIC_HPF_DELAY_MS; int unmute_delay = TX_MACRO_DMIC_UNMUTE_DELAY_MS; + int ret; u16 adc_mux_reg, adc_reg, adc_n, dmic; u16 dmic_clk_reg; struct tx_macro *tx = snd_soc_component_get_drvdata(component); @@ -949,6 +1016,10 @@ static int tx_macro_enable_dec(struct snd_soc_dapm_widget *w, CDC_TXn_PGA_MUTE_MASK, 0x1); break; case SND_SOC_DAPM_POST_PMU: + ret = tx_macro_denali_va_dmic_request(component, true); + if (ret) + return ret; + snd_soc_component_write_field(component, tx_vol_ctl_reg, CDC_TXn_CLK_EN_MASK, 0x1); if (!is_amic_enabled(component, tx, decimator)) { @@ -1047,6 +1118,10 @@ static int tx_macro_enable_dec(struct snd_soc_dapm_widget *w, cancel_delayed_work_sync(&tx->tx_mute_dwork[decimator].dwork); break; case SND_SOC_DAPM_POST_PMD: + ret = tx_macro_denali_va_dmic_request(component, false); + if (ret) + return ret; + snd_soc_component_write_field(component, tx_vol_ctl_reg, CDC_TXn_CLK_EN_MASK, 0x0); snd_soc_component_write_field(component, dec_cfg_reg, diff --git a/sound/soc/codecs/lpass-va-macro.c b/sound/soc/codecs/lpass-va-macro.c index f22256557cc1bf..34fe8d9f742bc9 100644 --- a/sound/soc/codecs/lpass-va-macro.c +++ b/sound/soc/codecs/lpass-va-macro.c @@ -725,6 +725,16 @@ static int va_dmic_clk_enable(struct snd_soc_component *component, return 0; } +static int va_macro_shared_dmic_clk_request(void *priv, unsigned int dmic, + bool enable) +{ + return va_dmic_clk_enable(priv, dmic, enable); +} + +static const struct lpass_macro_dmic_clk_ops va_macro_dmic_clk_ops = { + .request = va_macro_shared_dmic_clk_request, +}; + static int va_macro_enable_dmic(struct snd_soc_dapm_widget *w, struct snd_kcontrol *kcontrol, int event) { @@ -1332,15 +1342,31 @@ static const struct snd_kcontrol_new va_macro_snd_controls[] = { static int va_macro_component_probe(struct snd_soc_component *component) { struct va_macro *va = snd_soc_component_get_drvdata(component); + int ret; snd_soc_component_init_regmap(component, va->regmap); + if (!of_machine_is_compatible("microsoft,denali")) + return 0; + + ret = lpass_macro_register_dmic_clk_provider(component, &va_macro_dmic_clk_ops); + if (ret) + return dev_err_probe(component->dev, ret, + "failed to register shared DMIC clock provider\n"); + return 0; } +static void va_macro_component_remove(struct snd_soc_component *component) +{ + if (of_machine_is_compatible("microsoft,denali")) + lpass_macro_unregister_dmic_clk_provider(component); +} + static const struct snd_soc_component_driver va_macro_component_drv = { .name = "VA MACRO", .probe = va_macro_component_probe, + .remove = va_macro_component_remove, .controls = va_macro_snd_controls, .num_controls = ARRAY_SIZE(va_macro_snd_controls), .dapm_widgets = va_macro_dapm_widgets, From 516a32e82fcb6ac980016ce4e0a1be555afcca44 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Wed, 26 Aug 2026 07:46:58 +0100 Subject: [PATCH 03/49] arm64: dts: qcom: x1-denali: add TX DMIC capture The Surface Pro 11 exposes its microphone array as two 48 kHz PCM channels. The existing VA backend covers the voice path but does not provide the regular TX-macro host capture path. Add a TX macro link through AudioReach TX_CODEC_DMA_TX_3 and route DMIC0 and DMIC1 through the microphone regulator. Keep the existing VA capture link available for low-power use and hardware comparison. Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/ab34e9455e2107fb7d90be1cb0453710ab526182 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/58e36b1acb042b128a450a42cbab3da462368c15 Signed-off-by: Leon Silcott --- .../boot/dts/qcom/x1-microsoft-denali.dtsi | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi b/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi index b12117d0ff1cbd..1c68ddc24f5b36 100644 --- a/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi +++ b/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi @@ -327,7 +327,9 @@ audio-routing = "SpkrLeft IN", "WSA WSA_SPK1 OUT", "SpkrRight IN", "WSA WSA_SPK2 OUT", "VA DMIC0", "vdd-micb", - "VA DMIC1", "vdd-micb"; + "VA DMIC1", "vdd-micb", + "TX DMIC0", "vdd-micb", + "TX DMIC1", "vdd-micb"; wsa-dai-link { link-name = "WSA Playback"; @@ -361,6 +363,22 @@ sound-dai = <&q6apm>; }; }; + + tx-dai-link { + link-name = "TX Capture"; + + codec { + sound-dai = <&lpass_txmacro 0>; + }; + + cpu { + sound-dai = <&q6apmbedai TX_CODEC_DMA_TX_3>; + }; + + platform { + sound-dai = <&q6apm>; + }; + }; }; wcn7850-pmu { From 90bd9c4206586a2667293cdd446023c8ba0677de 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/49] 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 c4106741be673666abcccb50c2e2ccd475075f31 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/49] 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 8ffd6b06eedc2d3ca4326ed95eb3297e16dc7215 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/49] 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 f43539b23e6dd68dbabe8da4461a3426da7319e0 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/49] 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 4d104a8542f83628f61c238d64cf0edacf39aa97 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 08/49] 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 2f818748a492f6d8506e97255c5735f640ea48c8 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 09/49] 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 4b45973528e79d8bd282c4bbfff7e42e5aadd1d3 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 10/49] 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 c60a9a47d9c314af3a6a4ff0e47748e495375221 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sun, 23 Aug 2026 12:33:00 +0100 Subject: [PATCH 11/49] 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 12b3dc5fc2296e180b6de1569032ff3adbe220a7 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Tue, 1 Sep 2026 03:28:01 +0100 Subject: [PATCH 12/49] 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 4566257d33e87e362870d24b31da677df7809474 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Tue, 1 Sep 2026 01:15:49 +0100 Subject: [PATCH 13/49] 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 16df7af40c1e0575c73832d2c3dd64ad02df943d 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 14/49] 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 fa9f83adbace5c271b55b5c558f89d8d8ddc01b6 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 15/49] 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 210a40ca67149aa3da7a2db7acfde5c54862fb81 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 16/49] 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 47a243483ffb62bc730b04c7b416e9cafbc26d3f Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Tue, 1 Sep 2026 00:28:36 +0100 Subject: [PATCH 17/49] 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 5322b950962aa699d50abd5e2059d52c0f97aa32 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Mon, 31 Aug 2026 23:57:33 +0100 Subject: [PATCH 18/49] 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 1157df73c3d6bbc2edbafe2cfa990e3c11011a44 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 19/49] 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 e01cfb34422a37ab1b57bf1002481628df44795a 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 20/49] 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 98a1bf19a2a58338e428c3bf4a1205db8ec4d3d1 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 21/49] 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 1c68ddc24f5b36..969922521141c8 100644 --- a/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi +++ b/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi @@ -322,7 +322,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", @@ -335,7 +336,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>; }; @@ -348,6 +349,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"; @@ -1160,11 +1206,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 */ @@ -1172,11 +1221,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 a2726fe91fa127efe863b3168b73416fe1624435 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Mon, 31 Aug 2026 23:58:02 +0100 Subject: [PATCH 22/49] 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 969922521141c8..d472a4eb6e82c6 100644 --- a/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi +++ b/arch/arm64/boot/dts/qcom/x1-microsoft-denali.dtsi @@ -1211,6 +1211,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>; @@ -1226,6 +1227,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 5df38f6399a77f89563cfcb8c8273a0c7c93af2a Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Wed, 2 Sep 2026 10:57:59 +0100 Subject: [PATCH 23/49] 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); From 23531665bd43e905df288ece2c69de346c24a1e8 Mon Sep 17 00:00:00 2001 From: Justin White Date: Mon, 24 Aug 2026 17:39:14 -0500 Subject: [PATCH 24/49] platform/surface: registry: omit SP11 SAM battery node Qualcomm battmgr is the authoritative battery provider on the Surface Pro 11. Do not instantiate the primary Surface Aggregator Module battery device in the Denali-specific software-node group, avoiding a duplicate battery provider without adding a machine quirk to the shared driver. Keep the separate SAM AC-adapter node registered. This extracts and narrows the Denali battery fix from the original combined touchscreen and battery commit. Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/3fc7c5249f4aa9ef70d1f63e82201c8a21b33827 Signed-off-by: Justin White Signed-off-by: Leon Silcott --- drivers/platform/surface/surface_aggregator_registry.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/platform/surface/surface_aggregator_registry.c b/drivers/platform/surface/surface_aggregator_registry.c index d37a7e4c6b02a6..83e2a8eeeca33d 100644 --- a/drivers/platform/surface/surface_aggregator_registry.c +++ b/drivers/platform/surface/surface_aggregator_registry.c @@ -408,8 +408,8 @@ static const struct software_node *ssam_node_group_sp9_5g[] = { static const struct software_node *ssam_node_group_sp11[] = { &ssam_node_root, &ssam_node_hub_kip, + /* Primary battery data is provided by Qualcomm battmgr. */ &ssam_node_bat_ac, - &ssam_node_bat_main, &ssam_node_tmp_sensors, &ssam_node_hid_kip_keyboard, &ssam_node_hid_kip_penstash, From 64104e910a31550df84470f81ef8a2d222b5f93a Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Tue, 1 Sep 2026 06:05:14 +0100 Subject: [PATCH 25/49] platform/surface: aggregator_registry: omit SP11 SAM AC node The X1E Qualcomm battery manager registers qcom-battmgr-ac as the authoritative mains supply. Retaining the Surface Aggregator ADP1 node creates a second mains interface for the same Denali power source. Follow the upstream Surface Laptop 7 precedent and omit the SAM AC node from the Denali-only registry group. Keep this correction separate from the Justin White battery-node extraction so its original authorship and sign-off remain intact. Link: https://github.com/torvalds/linux/commit/0488073a6c84571dd3cffe581a4a73a5fceb099d Signed-off-by: Leon Silcott --- drivers/platform/surface/surface_aggregator_registry.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/drivers/platform/surface/surface_aggregator_registry.c b/drivers/platform/surface/surface_aggregator_registry.c index 83e2a8eeeca33d..c10c2cb9199990 100644 --- a/drivers/platform/surface/surface_aggregator_registry.c +++ b/drivers/platform/surface/surface_aggregator_registry.c @@ -408,8 +408,6 @@ static const struct software_node *ssam_node_group_sp9_5g[] = { static const struct software_node *ssam_node_group_sp11[] = { &ssam_node_root, &ssam_node_hub_kip, - /* Primary battery data is provided by Qualcomm battmgr. */ - &ssam_node_bat_ac, &ssam_node_tmp_sensors, &ssam_node_hid_kip_keyboard, &ssam_node_hid_kip_penstash, From fa6dc7a9992dfeaea70dd00dee7f7e234e0f06ba Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sun, 23 Aug 2026 05:04:25 +0100 Subject: [PATCH 26/49] ACPI: platform_profile: keep class interface without ACPI Device-tree systems can use platform-profile providers even when ACPI is disabled, but the framework currently declines to register its class in that case. Register the class unconditionally and create the legacy ACPI attribute group only after ACPI has initialized successfully. Record that group registration and use it to guard legacy notifications and teardown. This keeps the class path independent of ACPI while also avoiding a stale acpi_kobj after an ACPI initialization failure. This exposes the native /sys/class/platform-profile/ interface without creating a synthetic /sys/firmware/acpi hierarchy. Existing ACPI systems retain the legacy aggregate attributes unchanged. This is a contained rework of the original SP11 non-ACPI enablement. Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/106ad0d2cd9e0fea88c7281de245424d3aff8910 Signed-off-by: Leon Silcott --- drivers/acpi/platform_profile.c | 42 +++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/drivers/acpi/platform_profile.c b/drivers/acpi/platform_profile.c index 79d2e92e5c28d7..1408a162b60f0f 100644 --- a/drivers/acpi/platform_profile.c +++ b/drivers/acpi/platform_profile.c @@ -43,6 +43,13 @@ static const char * const profile_names[] = { static_assert(ARRAY_SIZE(profile_names) == PLATFORM_PROFILE_LAST); static DEFINE_IDA(platform_profile_ida); +static bool platform_profile_legacy_registered; + +static void platform_profile_legacy_notify(void) +{ + if (platform_profile_legacy_registered) + sysfs_notify(acpi_kobj, NULL, "platform_profile"); +} /** * _commmon_choices_show - Show the available profile choices @@ -216,7 +223,7 @@ static ssize_t profile_store(struct device *dev, return ret; } - sysfs_notify(acpi_kobj, NULL, "platform_profile"); + platform_profile_legacy_notify(); return count; } @@ -436,7 +443,7 @@ static ssize_t platform_profile_store(struct kobject *kobj, return ret; } - sysfs_notify(acpi_kobj, NULL, "platform_profile"); + platform_profile_legacy_notify(); return count; } @@ -473,6 +480,14 @@ static const struct attribute_group platform_profile_group = { .is_visible = profile_class_is_visible, }; +static int platform_profile_legacy_update_group(void) +{ + if (!platform_profile_legacy_registered) + return 0; + + return sysfs_update_group(acpi_kobj, &platform_profile_group); +} + /** * platform_profile_notify - Notify class device and legacy sysfs interface * @dev: The class device @@ -482,7 +497,7 @@ void platform_profile_notify(struct device *dev) scoped_cond_guard(mutex_intr, return, &profile_lock) { _notify_class_profile(dev, NULL); } - sysfs_notify(acpi_kobj, NULL, "platform_profile"); + platform_profile_legacy_notify(); } EXPORT_SYMBOL_GPL(platform_profile_notify); @@ -532,7 +547,7 @@ int platform_profile_cycle(void) return err; } - sysfs_notify(acpi_kobj, NULL, "platform_profile"); + platform_profile_legacy_notify(); return 0; } @@ -605,9 +620,9 @@ struct device *platform_profile_register(struct device *dev, const char *name, goto cleanup_ida; } - sysfs_notify(acpi_kobj, NULL, "platform_profile"); + platform_profile_legacy_notify(); - err = sysfs_update_group(acpi_kobj, &platform_profile_group); + err = platform_profile_legacy_update_group(); if (err) goto cleanup_cur; @@ -641,8 +656,8 @@ void platform_profile_remove(struct device *dev) ida_free(&platform_profile_ida, pprof->minor); device_unregister(&pprof->dev); - sysfs_notify(acpi_kobj, NULL, "platform_profile"); - sysfs_update_group(acpi_kobj, &platform_profile_group); + platform_profile_legacy_notify(); + platform_profile_legacy_update_group(); } EXPORT_SYMBOL_GPL(platform_profile_remove); @@ -690,23 +705,26 @@ static int __init platform_profile_init(void) { int err; - if (acpi_disabled) - return -EOPNOTSUPP; - err = class_register(&platform_profile_class); if (err) return err; + if (acpi_disabled || !acpi_kobj) + return 0; + err = sysfs_create_group(acpi_kobj, &platform_profile_group); if (err) class_unregister(&platform_profile_class); + else + platform_profile_legacy_registered = true; return err; } static void __exit platform_profile_exit(void) { - sysfs_remove_group(acpi_kobj, &platform_profile_group); + if (platform_profile_legacy_registered) + sysfs_remove_group(acpi_kobj, &platform_profile_group); class_unregister(&platform_profile_class); } module_init(platform_profile_init); From 9b60a3d5098b5e2ceed04c526da31ce07a178003 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Mon, 10 Aug 2026 08:58:45 +0100 Subject: [PATCH 27/49] platform/surface: add profile and fan nodes for SP11 The Surface Pro 11 has active cooling and supports SAM performance profiles, but its registry group does not instantiate the existing platform-profile and fan-speed devices. Add the existing fan-capable profile node and fan-speed node only to the Denali software-node group. Other Surface device groups are unchanged. Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/5c513ff791fd968fc5965d9be780674f1014a5a8 Signed-off-by: Leon Silcott --- drivers/platform/surface/surface_aggregator_registry.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/platform/surface/surface_aggregator_registry.c b/drivers/platform/surface/surface_aggregator_registry.c index c10c2cb9199990..2ee6071506e311 100644 --- a/drivers/platform/surface/surface_aggregator_registry.c +++ b/drivers/platform/surface/surface_aggregator_registry.c @@ -408,6 +408,8 @@ static const struct software_node *ssam_node_group_sp9_5g[] = { static const struct software_node *ssam_node_group_sp11[] = { &ssam_node_root, &ssam_node_hub_kip, + &ssam_node_tmp_perf_profile_with_fan, + &ssam_node_fan_speed, &ssam_node_tmp_sensors, &ssam_node_hid_kip_keyboard, &ssam_node_hid_kip_penstash, From 1600ae1970aec445dad8a51bd73912518c86bfed Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sat, 22 Aug 2026 09:59:22 +0100 Subject: [PATCH 28/49] platform/surface: gate low-power CPU cap to SP11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Surface Pro 11 needs a lower CPU-frequency ceiling in its low-power profile to match the validated cool-running baseline. Describe the 2.515 GHz ceiling and default low-power selection on an SP11-only software node. Set up frequency-QoS only when that per-device property is present. Devices without it retain the legacy TMP-then-fan write order, including writes that repeat the current profile. Track cpufreq policies through CREATE and REMOVE notifications plus a per-device CPU-hotplug state, so a policy that is fully offline at probe is capped when a CPU returns. Hold policy references while attaching requests, clear stored raw pointers synchronously on REMOVE, and stop hotplug callbacks before tearing down requests and the notifier. When entering low power, apply the cap before fan and TMP changes; when leaving, restore fan and TMP before dropping the cap. If a SAM write fails, read TMP back and reconcile fan and cap conservatively. An unavailable cpufreq subsystem degrades to profile-only operation. Omit the staging implementation’s no-op power-supply notifier. This is a contained rework of the original SP11 low-power implementation. Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/30972bdca0879e8f33adcfd620b8f9dd09755ac7 Signed-off-by: Leon Silcott --- .../surface/surface_aggregator_registry.c | 15 +- .../surface/surface_platform_profile.c | 730 +++++++++++++++++- 2 files changed, 718 insertions(+), 27 deletions(-) diff --git a/drivers/platform/surface/surface_aggregator_registry.c b/drivers/platform/surface/surface_aggregator_registry.c index 2ee6071506e311..fa20d00a29f84c 100644 --- a/drivers/platform/surface/surface_aggregator_registry.c +++ b/drivers/platform/surface/surface_aggregator_registry.c @@ -89,6 +89,19 @@ static const struct software_node ssam_node_tmp_perf_profile_with_fan = { .properties = ssam_node_tmp_perf_profile_has_fan, }; +static const struct property_entry ssam_node_tmp_perf_profile_sp11_props[] = { + PROPERTY_ENTRY_BOOL("has_fan"), + PROPERTY_ENTRY_BOOL("default-low-power"), + PROPERTY_ENTRY_U32("low-power-max-frequency-khz", 2515000), + { } +}; + +static const struct software_node ssam_node_tmp_perf_profile_sp11 = { + .name = "ssam:01:03:01:00:01", + .parent = &ssam_node_root, + .properties = ssam_node_tmp_perf_profile_sp11_props, +}; + /* Thermal sensors. */ static const struct software_node ssam_node_tmp_sensors = { .name = "ssam:01:03:01:00:02", @@ -408,7 +421,7 @@ static const struct software_node *ssam_node_group_sp9_5g[] = { static const struct software_node *ssam_node_group_sp11[] = { &ssam_node_root, &ssam_node_hub_kip, - &ssam_node_tmp_perf_profile_with_fan, + &ssam_node_tmp_perf_profile_sp11, &ssam_node_fan_speed, &ssam_node_tmp_sensors, &ssam_node_hid_kip_keyboard, diff --git a/drivers/platform/surface/surface_platform_profile.c b/drivers/platform/surface/surface_platform_profile.c index 0e479e35e66e16..aa5eba8f95d7fd 100644 --- a/drivers/platform/surface/surface_platform_profile.c +++ b/drivers/platform/surface/surface_platform_profile.c @@ -6,11 +6,17 @@ * Copyright (C) 2021-2022 Maximilian Luz */ -#include +#include +#include +#include #include #include +#include #include +#include +#include #include +#include #include @@ -41,6 +47,26 @@ struct ssam_tmp_profile_info { struct ssam_platform_profile_device { struct ssam_device *sdev; struct device *ppdev; + struct mutex profile_lock; /* Protects TMP, fan, and frequency transitions. */ + bool has_freq_cap; +#if IS_ENABLED(CONFIG_CPU_FREQ) + struct cpufreq_policy **cpufreq_policies; + struct freq_qos_request *freq_qos_reqs; + unsigned int *freq_cap_khz; + unsigned int low_power_max_freq_khz; + struct mutex freq_qos_lock; /* Protects frequency QoS state. */ + struct hlist_node cpuhp_node; + enum cpuhp_state cpuhp_state; + struct notifier_block cpufreq_nb; + int freq_qos_setup_status; + bool accept_policies; + bool cpuhp_instance_registered; + bool cpuhp_replaying; + bool cpuhp_state_registered; + bool cpufreq_notifier_registered; + bool freq_capped; + bool freq_qos_enabled; +#endif bool has_fan; }; @@ -88,6 +114,121 @@ static int ssam_fan_profile_set(struct ssam_device *sdev, enum ssam_fan_profile return ssam_retry(__ssam_fan_profile_set, sdev->ctrl, &profile); } +#if IS_ENABLED(CONFIG_CPU_FREQ) +static unsigned int ssam_find_cap_freq(const struct cpufreq_policy *policy, + unsigned int ceiling_khz) +{ + struct cpufreq_frequency_table *entry; + unsigned int cap_freq = 0; + + if (!policy->freq_table) + return ceiling_khz; + + cpufreq_for_each_valid_entry(entry, policy->freq_table) { + if (entry->frequency <= ceiling_khz && + entry->frequency > cap_freq) + cap_freq = entry->frequency; + } + + return cap_freq ?: ceiling_khz; +} + +static unsigned int +ssam_platform_profile_cap_freq(struct ssam_platform_profile_device *tpd, + struct cpufreq_policy *policy) +{ + unsigned int max_freq; + + down_read(&policy->rwsem); + max_freq = ssam_find_cap_freq(policy, tpd->low_power_max_freq_khz); + up_read(&policy->rwsem); + + return max_freq; +} + +static int +ssam_platform_profile_apply_freq_cap(struct ssam_platform_profile_device *tpd, + enum platform_profile_option profile) +{ + struct cpufreq_policy *policy; + bool capped; + int i, status; + + if (!tpd->freq_qos_enabled) + return 0; + + switch (profile) { + case PLATFORM_PROFILE_LOW_POWER: + capped = true; + break; + case PLATFORM_PROFILE_BALANCED: + case PLATFORM_PROFILE_BALANCED_PERFORMANCE: + case PLATFORM_PROFILE_PERFORMANCE: + capped = false; + break; + default: + return -EOPNOTSUPP; + } + + mutex_lock(&tpd->freq_qos_lock); + + if (tpd->freq_capped == capped) { + mutex_unlock(&tpd->freq_qos_lock); + return 0; + } + + for (i = 0; i < num_possible_cpus(); i++) { + unsigned int max_freq; + + policy = tpd->cpufreq_policies[i]; + if (!policy) + continue; + + max_freq = capped ? tpd->freq_cap_khz[i] : + FREQ_QOS_MAX_DEFAULT_VALUE; + status = freq_qos_update_request(&tpd->freq_qos_reqs[i], + max_freq); + if (status < 0) + goto rollback; + } + + tpd->freq_capped = capped; + mutex_unlock(&tpd->freq_qos_lock); + return 0; + +rollback: + dev_err(&tpd->sdev->dev, + "failed to update CPU frequency cap for policy %u: %d\n", + policy->cpu, status); + + while (--i >= 0) { + unsigned int max_freq; + + policy = tpd->cpufreq_policies[i]; + if (!policy) + continue; + + max_freq = tpd->freq_capped ? tpd->freq_cap_khz[i] : + FREQ_QOS_MAX_DEFAULT_VALUE; + if (freq_qos_update_request(&tpd->freq_qos_reqs[i], + max_freq) < 0) + dev_warn(&tpd->sdev->dev, + "failed to roll back CPU frequency cap for policy %u\n", + policy->cpu); + } + + mutex_unlock(&tpd->freq_qos_lock); + return status; +} +#else +static int +ssam_platform_profile_apply_freq_cap(struct ssam_platform_profile_device *tpd, + enum platform_profile_option profile) +{ + return 0; +} +#endif + static int convert_ssam_tmp_to_profile(struct ssam_device *sdev, enum ssam_tmp_profile p) { switch (p) { @@ -109,7 +250,6 @@ static int convert_ssam_tmp_to_profile(struct ssam_device *sdev, enum ssam_tmp_p } } - static int convert_profile_to_ssam_tmp(struct ssam_device *sdev, enum platform_profile_option p) { switch (p) { @@ -154,15 +294,13 @@ static int convert_profile_to_ssam_fan(struct ssam_device *sdev, enum platform_p } } -static int ssam_platform_profile_get(struct device *dev, - enum platform_profile_option *profile) +static int +ssam_platform_profile_get_internal(struct ssam_platform_profile_device *tpd, + enum platform_profile_option *profile) { - struct ssam_platform_profile_device *tpd; enum ssam_tmp_profile tp; int status; - tpd = dev_get_drvdata(dev); - status = ssam_tmp_profile_get(tpd->sdev, &tp); if (status) return status; @@ -175,30 +313,166 @@ static int ssam_platform_profile_get(struct device *dev, return 0; } -static int ssam_platform_profile_set(struct device *dev, - enum platform_profile_option profile) +static int ssam_platform_profile_get(struct device *dev, + enum platform_profile_option *profile) { - struct ssam_platform_profile_device *tpd; - int tp; + struct ssam_platform_profile_device *tpd = dev_get_drvdata(dev); + int status; + + mutex_lock(&tpd->profile_lock); + status = ssam_platform_profile_get_internal(tpd, profile); + mutex_unlock(&tpd->profile_lock); + + return status; +} + +static void +ssam_platform_profile_reconcile(struct ssam_platform_profile_device *tpd, + enum platform_profile_option old_profile, + enum platform_profile_option target_profile, + int transition_status) +{ + enum platform_profile_option current_profile; + bool fan_uncertain = false; + int fan_profile; + int status; + + status = ssam_platform_profile_get_internal(tpd, ¤t_profile); + if (status) { + dev_err(&tpd->sdev->dev, + "profile transition failed (%d) and TMP readback failed: %d\n", + transition_status, status); + + if (old_profile == PLATFORM_PROFILE_LOW_POWER || + target_profile == PLATFORM_PROFILE_LOW_POWER) { + status = ssam_platform_profile_apply_freq_cap(tpd, + PLATFORM_PROFILE_LOW_POWER); + if (status) + dev_err(&tpd->sdev->dev, + "failed to retain the conservative CPU cap: %d\n", + status); + } + return; + } + + if (tpd->has_fan) { + fan_profile = convert_profile_to_ssam_fan(tpd->sdev, current_profile); + if (fan_profile >= 0) { + status = ssam_fan_profile_set(tpd->sdev, fan_profile); + if (status) { + dev_warn(&tpd->sdev->dev, + "failed to reconcile fan profile: %d\n", + status); + fan_uncertain = true; + } + } + } + + if (fan_uncertain && + (old_profile == PLATFORM_PROFILE_LOW_POWER || + target_profile == PLATFORM_PROFILE_LOW_POWER)) + current_profile = PLATFORM_PROFILE_LOW_POWER; + + status = ssam_platform_profile_apply_freq_cap(tpd, current_profile); + if (status) + dev_err(&tpd->sdev->dev, + "failed to reconcile the CPU frequency cap: %d\n", + status); +} + +static int +ssam_platform_profile_set_legacy(struct ssam_platform_profile_device *tpd, + enum platform_profile_option profile) +{ + int fan_profile; + int tmp_profile; + int status; + + tmp_profile = convert_profile_to_ssam_tmp(tpd->sdev, profile); + if (tmp_profile < 0) + return tmp_profile; + + status = ssam_tmp_profile_set(tpd->sdev, tmp_profile); + if (status) + return status; + + if (!tpd->has_fan) + return 0; + + fan_profile = convert_profile_to_ssam_fan(tpd->sdev, profile); + if (fan_profile < 0) + return fan_profile; + + return ssam_fan_profile_set(tpd->sdev, fan_profile); +} + +static int +ssam_platform_profile_set_internal(struct ssam_platform_profile_device *tpd, + enum platform_profile_option profile) +{ + enum platform_profile_option old_profile; + int fan_profile; + int tmp_profile; + int status; + + if (!tpd->has_freq_cap) + return ssam_platform_profile_set_legacy(tpd, profile); - tpd = dev_get_drvdata(dev); + status = ssam_platform_profile_get_internal(tpd, &old_profile); + if (status) + return status; - tp = convert_profile_to_ssam_tmp(tpd->sdev, profile); - if (tp < 0) - return tp; + tmp_profile = convert_profile_to_ssam_tmp(tpd->sdev, profile); + if (tmp_profile < 0) + return tmp_profile; - tp = ssam_tmp_profile_set(tpd->sdev, tp); - if (tp < 0) - return tp; + fan_profile = convert_profile_to_ssam_fan(tpd->sdev, profile); + if (fan_profile < 0) + return fan_profile; + + /* Enter low power only after every tracked CPU policy is capped. */ + if (profile == PLATFORM_PROFILE_LOW_POWER) { + status = ssam_platform_profile_apply_freq_cap(tpd, profile); + if (status) + return status; + } if (tpd->has_fan) { - tp = convert_profile_to_ssam_fan(tpd->sdev, profile); - if (tp < 0) - return tp; - tp = ssam_fan_profile_set(tpd->sdev, tp); + status = ssam_fan_profile_set(tpd->sdev, fan_profile); + if (status) + goto reconcile; + } + + /* TMP is authoritative and is therefore the firmware commit point. */ + status = ssam_tmp_profile_set(tpd->sdev, tmp_profile); + if (status) + goto reconcile; + + /* Keep the cap until TMP has committed a non-low-power profile. */ + if (profile != PLATFORM_PROFILE_LOW_POWER) { + status = ssam_platform_profile_apply_freq_cap(tpd, profile); + if (status) + goto reconcile; } - return tp; + return 0; + +reconcile: + ssam_platform_profile_reconcile(tpd, old_profile, profile, status); + return status; +} + +static int ssam_platform_profile_set(struct device *dev, + enum platform_profile_option profile) +{ + struct ssam_platform_profile_device *tpd = dev_get_drvdata(dev); + int status; + + mutex_lock(&tpd->profile_lock); + status = ssam_platform_profile_set_internal(tpd, profile); + mutex_unlock(&tpd->profile_lock); + + return status; } static int ssam_platform_profile_probe(void *drvdata, unsigned long *choices) @@ -217,9 +491,374 @@ static const struct platform_profile_ops ssam_platform_profile_ops = { .profile_set = ssam_platform_profile_set, }; +#if IS_ENABLED(CONFIG_CPU_FREQ) +static int +ssam_platform_profile_add_policy_locked(struct ssam_platform_profile_device *tpd, + struct cpufreq_policy *policy, + unsigned int cap_freq) +{ + unsigned int max_freq; + int free_slot = -1; + int i; + int status; + + lockdep_assert_held(&tpd->freq_qos_lock); + + if (!tpd->accept_policies) + return -ESHUTDOWN; + + for (i = 0; i < num_possible_cpus(); i++) { + if (tpd->cpufreq_policies[i] == policy) + return 0; + + if (!tpd->cpufreq_policies[i] && free_slot < 0) + free_slot = i; + } + + if (free_slot < 0) + return -ENOSPC; + + max_freq = tpd->freq_capped ? cap_freq : + FREQ_QOS_MAX_DEFAULT_VALUE; + status = freq_qos_add_request(&policy->constraints, + &tpd->freq_qos_reqs[free_slot], + FREQ_QOS_MAX, max_freq); + if (status < 0) + return status; + + /* CPUFREQ_REMOVE_POLICY synchronously clears this raw pointer. */ + tpd->cpufreq_policies[free_slot] = policy; + tpd->freq_cap_khz[free_slot] = cap_freq; + + return 0; +} + +static void +ssam_platform_profile_remove_policy_locked(struct ssam_platform_profile_device *tpd, + struct cpufreq_policy *policy) +{ + int i; + + lockdep_assert_held(&tpd->freq_qos_lock); + + for (i = 0; i < num_possible_cpus(); i++) { + if (tpd->cpufreq_policies[i] != policy) + continue; + + if (freq_qos_request_active(&tpd->freq_qos_reqs[i])) + freq_qos_remove_request(&tpd->freq_qos_reqs[i]); + tpd->cpufreq_policies[i] = NULL; + tpd->freq_cap_khz[i] = 0; + break; + } +} + +static void +ssam_platform_profile_remove_all_policies_locked(struct ssam_platform_profile_device *tpd) +{ + int i; + + lockdep_assert_held(&tpd->freq_qos_lock); + + for (i = 0; i < num_possible_cpus(); i++) { + if (!tpd->cpufreq_policies[i]) + continue; + + if (freq_qos_request_active(&tpd->freq_qos_reqs[i])) + freq_qos_remove_request(&tpd->freq_qos_reqs[i]); + tpd->cpufreq_policies[i] = NULL; + tpd->freq_cap_khz[i] = 0; + } + + tpd->freq_capped = false; +} + +static int +ssam_platform_profile_add_active_cpu(struct ssam_platform_profile_device *tpd, + unsigned int cpu) +{ + struct cpufreq_policy *validation; + struct cpufreq_policy *policy; + unsigned int cap_freq; + int status; + + policy = cpufreq_cpu_get(cpu); + if (!policy) + return -ENODEV; + + cap_freq = ssam_platform_profile_cap_freq(tpd, policy); + + mutex_lock(&tpd->freq_qos_lock); + validation = cpufreq_cpu_get(cpu); + if (validation != policy) { + status = -ENODEV; + } else { + status = ssam_platform_profile_add_policy_locked(tpd, policy, + cap_freq); + } + mutex_unlock(&tpd->freq_qos_lock); + + if (validation) + cpufreq_cpu_put(validation); + cpufreq_cpu_put(policy); + + return status; +} + +static int +ssam_platform_profile_add_hotplug_cpu(struct ssam_platform_profile_device *tpd, + unsigned int cpu) +{ + struct cpufreq_policy *policy; + unsigned int cap_freq; + int status; + + /* CPU hotplug serialization keeps this inactive policy alive. */ + policy = cpufreq_cpu_policy(cpu); + if (!policy) + return -ENODEV; + + cap_freq = ssam_platform_profile_cap_freq(tpd, policy); + + mutex_lock(&tpd->freq_qos_lock); + status = ssam_platform_profile_add_policy_locked(tpd, policy, + cap_freq); + mutex_unlock(&tpd->freq_qos_lock); + + return status; +} + +static int +ssam_platform_profile_cpuhp_online(unsigned int cpu, + struct hlist_node *node) +{ + struct ssam_platform_profile_device *tpd = + hlist_entry(node, struct ssam_platform_profile_device, cpuhp_node); + int status; + + status = ssam_platform_profile_add_active_cpu(tpd, cpu); + if (status == -ENODEV && !READ_ONCE(tpd->cpuhp_replaying)) + status = ssam_platform_profile_add_hotplug_cpu(tpd, cpu); + + if (status == -ENODEV || status == -ESHUTDOWN) + return 0; + + if (status) { + dev_err(&tpd->sdev->dev, + "failed to add CPU frequency QoS request for CPU%u: %d\n", + cpu, status); + if (READ_ONCE(tpd->cpuhp_replaying) && + !tpd->freq_qos_setup_status) + tpd->freq_qos_setup_status = status; + } + + /* A CPUHP startup callback must not leave a partially replayed setup. */ + return 0; +} + +static int +ssam_platform_profile_cpufreq_event(struct notifier_block *nb, + unsigned long event, void *data) +{ + struct ssam_platform_profile_device *tpd = + container_of(nb, struct ssam_platform_profile_device, cpufreq_nb); + struct cpufreq_policy *policy = data; + unsigned int cap_freq; + unsigned int cpu; + int status; + + switch (event) { + case CPUFREQ_CREATE_POLICY: + /* The cpufreq core holds policy->rwsem for write here. */ + cap_freq = ssam_find_cap_freq(policy, + tpd->low_power_max_freq_khz); + cpu = policy->cpu; + mutex_lock(&tpd->freq_qos_lock); + status = ssam_platform_profile_add_policy_locked(tpd, policy, + cap_freq); + mutex_unlock(&tpd->freq_qos_lock); + if (status < 0 && status != -ESHUTDOWN) + dev_err(&tpd->sdev->dev, + "failed to add CPU frequency QoS request for policy %u: %d\n", + cpu, status); + break; + + case CPUFREQ_REMOVE_POLICY: + mutex_lock(&tpd->freq_qos_lock); + ssam_platform_profile_remove_policy_locked(tpd, policy); + mutex_unlock(&tpd->freq_qos_lock); + break; + } + + return NOTIFY_OK; +} + +static void ssam_platform_profile_remove_qos(void *data) +{ + struct ssam_platform_profile_device *tpd = data; + + mutex_lock(&tpd->freq_qos_lock); + tpd->accept_policies = false; + mutex_unlock(&tpd->freq_qos_lock); + + if (tpd->cpuhp_instance_registered) { + cpuhp_state_remove_instance_nocalls(tpd->cpuhp_state, + &tpd->cpuhp_node); + tpd->cpuhp_instance_registered = false; + } + if (tpd->cpuhp_state_registered) { + cpuhp_remove_multi_state(tpd->cpuhp_state); + tpd->cpuhp_state_registered = false; + } + + /* Keep REMOVE notifications live until no raw policy pointer remains. */ + mutex_lock(&tpd->freq_qos_lock); + ssam_platform_profile_remove_all_policies_locked(tpd); + tpd->freq_qos_enabled = false; + mutex_unlock(&tpd->freq_qos_lock); + + if (tpd->cpufreq_notifier_registered) { + cpufreq_unregister_notifier(&tpd->cpufreq_nb, + CPUFREQ_POLICY_NOTIFIER); + tpd->cpufreq_notifier_registered = false; + } +} + +static int +ssam_platform_profile_add_freq_qos(struct ssam_platform_profile_device *tpd, + unsigned int freq_cap) +{ + struct device *dev = &tpd->sdev->dev; + size_t policy_count = num_possible_cpus(); + unsigned int cpu; + int status; + + tpd->cpufreq_policies = devm_kcalloc(dev, policy_count, + sizeof(*tpd->cpufreq_policies), + GFP_KERNEL); + if (!tpd->cpufreq_policies) + return -ENOMEM; + + tpd->freq_qos_reqs = devm_kcalloc(dev, policy_count, + sizeof(*tpd->freq_qos_reqs), + GFP_KERNEL); + if (!tpd->freq_qos_reqs) + return -ENOMEM; + + tpd->freq_cap_khz = devm_kcalloc(dev, policy_count, + sizeof(*tpd->freq_cap_khz), + GFP_KERNEL); + if (!tpd->freq_cap_khz) + return -ENOMEM; + + mutex_init(&tpd->freq_qos_lock); + tpd->low_power_max_freq_khz = freq_cap; + tpd->cpufreq_nb.notifier_call = ssam_platform_profile_cpufreq_event; + + status = cpufreq_register_notifier(&tpd->cpufreq_nb, + CPUFREQ_POLICY_NOTIFIER); + if (status) { + dev_warn(dev, + "CPU frequency control unavailable; low-power profile will not cap CPUs: %d\n", + status); + return 0; + } + tpd->cpufreq_notifier_registered = true; + + mutex_lock(&tpd->freq_qos_lock); + tpd->accept_policies = true; + mutex_unlock(&tpd->freq_qos_lock); + + status = cpuhp_setup_state_multi(CPUHP_AP_ONLINE_DYN, + "surface/platform-profile:online", + ssam_platform_profile_cpuhp_online, + NULL); + if (status < 0) + goto disable_cap; + tpd->cpuhp_state = status; + tpd->cpuhp_state_registered = true; + + WRITE_ONCE(tpd->cpuhp_replaying, true); + status = cpuhp_state_add_instance(tpd->cpuhp_state, &tpd->cpuhp_node); + WRITE_ONCE(tpd->cpuhp_replaying, false); + if (status) + goto disable_cap; + tpd->cpuhp_instance_registered = true; + + /* + * Close the interval between add_instance() dropping its CPU read lock + * and cpuhp_replaying becoming false. A CPU that crossed our callback + * in that interval either appears in this scan or is caught on its next + * online transition. + */ + cpus_read_lock(); + for_each_online_cpu(cpu) { + status = ssam_platform_profile_add_active_cpu(tpd, cpu); + if (status == -ENODEV || status == -ESHUTDOWN) + continue; + if (status) { + dev_err(dev, + "failed to add CPU frequency QoS request for CPU%u: %d\n", + cpu, status); + if (!tpd->freq_qos_setup_status) + tpd->freq_qos_setup_status = status; + } + } + cpus_read_unlock(); + + if (tpd->freq_qos_setup_status) { + status = tpd->freq_qos_setup_status; + goto disable_cap; + } + + mutex_lock(&tpd->freq_qos_lock); + tpd->freq_qos_enabled = true; + mutex_unlock(&tpd->freq_qos_lock); + + return 0; + +disable_cap: + dev_warn(dev, + "CPU frequency cap setup failed; platform profiles remain available: %d\n", + status); + ssam_platform_profile_remove_qos(tpd); + return 0; +} +#else +static bool +ssam_platform_profile_freq_qos_enabled(struct ssam_platform_profile_device *tpd) +{ + return false; +} + +static int +ssam_platform_profile_add_freq_qos(struct ssam_platform_profile_device *tpd, + unsigned int freq_cap) +{ + dev_warn(&tpd->sdev->dev, + "CPU frequency control is disabled; low-power profile will not cap CPUs\n"); + return 0; +} + +static void ssam_platform_profile_remove_qos(void *data) +{ +} +#endif + +#if IS_ENABLED(CONFIG_CPU_FREQ) +static bool +ssam_platform_profile_freq_qos_enabled(struct ssam_platform_profile_device *tpd) +{ + return tpd->freq_qos_enabled; +} +#endif + static int surface_platform_profile_probe(struct ssam_device *sdev) { + const char *freq_property = "low-power-max-frequency-khz"; struct ssam_platform_profile_device *tpd; + unsigned int freq_cap; + int status; tpd = devm_kzalloc(&sdev->dev, sizeof(*tpd), GFP_KERNEL); if (!tpd) @@ -227,13 +866,52 @@ static int surface_platform_profile_probe(struct ssam_device *sdev) tpd->sdev = sdev; ssam_device_set_drvdata(sdev, tpd); + mutex_init(&tpd->profile_lock); tpd->has_fan = device_property_read_bool(&sdev->dev, "has_fan"); - tpd->ppdev = devm_platform_profile_register(&sdev->dev, "Surface Platform Profile", - tpd, &ssam_platform_profile_ops); + if (device_property_present(&sdev->dev, freq_property)) { + status = device_property_read_u32(&sdev->dev, freq_property, &freq_cap); + if (status) + return status; + if (!freq_cap) + return -EINVAL; + + tpd->has_freq_cap = true; + status = ssam_platform_profile_add_freq_qos(tpd, freq_cap); + if (status) + return status; + + if (ssam_platform_profile_freq_qos_enabled(tpd)) { + status = devm_add_action_or_reset(&sdev->dev, + ssam_platform_profile_remove_qos, + tpd); + if (status) + return status; + } + } - return PTR_ERR_OR_ZERO(tpd->ppdev); + tpd->ppdev = devm_platform_profile_register(&sdev->dev, + "Surface Platform Profile", + tpd, + &ssam_platform_profile_ops); + if (IS_ERR(tpd->ppdev)) { + status = PTR_ERR(tpd->ppdev); + return status; + } + + if (device_property_read_bool(&sdev->dev, "default-low-power")) { + mutex_lock(&tpd->profile_lock); + status = ssam_platform_profile_set_internal(tpd, + PLATFORM_PROFILE_LOW_POWER); + mutex_unlock(&tpd->profile_lock); + if (status) + dev_warn(&sdev->dev, + "failed to select the default low-power profile: %d\n", + status); + } + + return 0; } static const struct ssam_device_id ssam_platform_profile_match[] = { From 80ecee88b749d7324e1274c0a1cf6dfd752fddfa Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Wed, 2 Sep 2026 19:58:30 +0100 Subject: [PATCH 29/49] UBUNTU: Ubuntu-qcom-x1e-7.2.2-jg-0sp11beta1 Signed-off-by: Leon Silcott --- debian.qcom-x1e/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian.qcom-x1e/changelog b/debian.qcom-x1e/changelog index 81920b5e93ef5f..46183be849324a 100644 --- a/debian.qcom-x1e/changelog +++ b/debian.qcom-x1e/changelog @@ -1,3 +1,9 @@ +linux-qcom-x1e (7.2.2-jg-0sp11beta1) resolute; urgency=medium + + * Assemble the guarded SP11 topic stack for qualification. + + -- Leon Silcott Wed, 02 Sep 2026 19:30:00 +0100 + linux-qcom-x1e (7.2.2-jg-0) resolute; urgency=medium * arm64: dts: qcom: x1-lenovo-ideacentre-mini-01q8x10: discriminate compatible strings From ad96ca177f3960ca191c92e357abd0a0600a6c0a Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 27 Aug 2026 16:00:31 +0100 Subject: [PATCH 30/49] media: qcom: camss: disable unconfigured CSID680 RDI crop/drop The CSID680 RDI path enables horizontal and vertical crop/drop without programming the corresponding configuration. Leave these optional operations disabled while retaining the timestamp, packing and interrupt setup. Do not add drop-pattern writes or sensor-specific crop geometry. Adapt the enable-bit correction from Jeroen Karsies' SP11 camera bring-up snapshot b08f76f40b8d7b715bd4da6aef484f86142cc147. Retain the existing upstream source attribution and limit this change to receiver setup. This remains experimental groundwork: stream counters, capture layout and repeated starts still need validation on the target hardware. Link: https://github.com/karsies-wq/sp11-imx681-linux/blob/b08f76f40b8d7b715bd4da6aef484f86142cc147/camss/camss-csid-680.c Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/b910f68a07fe817e48db17a9ead68402c31aed78 Signed-off-by: Leon Silcott --- drivers/media/platform/qcom/camss/camss-csid-680.c | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/drivers/media/platform/qcom/camss/camss-csid-680.c b/drivers/media/platform/qcom/camss/camss-csid-680.c index b510e85b3d7c6d..9d1884cce3c2f6 100644 --- a/drivers/media/platform/qcom/camss/camss-csid-680.c +++ b/drivers/media/platform/qcom/camss/camss-csid-680.c @@ -282,13 +282,10 @@ static void __csid_configure_rdi_stream(struct csid_device *csid, u8 enable, u8 val |= dt_id << RDI_CFG0_DT_ID; writel(val, csid->base + CSID_RDI_CFG0(port)); + /* No crop or drop configuration is programmed for the RDI path. */ val = RDI_CFG1_TIMESTAMP_STB_FRAME; val |= RDI_CFG1_BYTE_CNTR_EN; val |= RDI_CFG1_TIMESTAMP_EN; - val |= RDI_CFG1_DROP_H_EN; - val |= RDI_CFG1_DROP_V_EN; - val |= RDI_CFG1_CROP_H_EN; - val |= RDI_CFG1_CROP_V_EN; val |= RDI_CFG1_PACKING_MIPI; writel(val, csid->base + CSID_RDI_CFG1(port)); From a825cb4003f73935392811443359cd0c21b65f62 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 27 Aug 2026 16:00:32 +0100 Subject: [PATCH 31/49] media: qcom: camss: select MIPI RAW mode for VFE680 RDI Select the MIPI_RAW write-client mode used in Jeroen Karsies' SP11 camera bring-up snapshot b08f76f40b8d7b715bd4da6aef484f86142cc147. Name the mode bit and set it together with the existing write-client enable bit. Keep stride, image configuration, packing, frame increment, interrupts and stream lifecycle unchanged. This does not establish alignment padding for unaligned packed RAW widths and does not enable any sensor. Retain the existing upstream source attribution. This register-mode adaptation remains experimental until VFE680 stream status and capture layout are validated on the target hardware. Link: https://github.com/karsies-wq/sp11-imx681-linux/blob/b08f76f40b8d7b715bd4da6aef484f86142cc147/camss/camss-vfe-680.c Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/651e711719df2d1cdc2159251c37aa11e1878017 Signed-off-by: Leon Silcott --- drivers/media/platform/qcom/camss/camss-vfe-680.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/drivers/media/platform/qcom/camss/camss-vfe-680.c b/drivers/media/platform/qcom/camss/camss-vfe-680.c index 99036e7c1e763a..0d228bc3032bae 100644 --- a/drivers/media/platform/qcom/camss/camss-vfe-680.c +++ b/drivers/media/platform/qcom/camss/camss-vfe-680.c @@ -38,6 +38,7 @@ #define VFE_BUS_WRITE_CLIENT_CFG(vfe, c) ((vfe_is_lite(vfe) ? 0x400 : 0xe00) + (c) * 0x100) #define VFE_BUS_WRITE_CLIENT_CFG_EN BIT(0) +#define VFE_BUS_WRITE_CLIENT_CFG_MODE_MIPI_RAW BIT(16) #define VFE_BUS_IMAGE_ADDR(vfe, c) ((vfe_is_lite(vfe) ? 0x404 : 0xe04) + (c) * 0x100) #define VFE_BUS_FRAME_INCR(vfe, c) ((vfe_is_lite(vfe) ? 0x408 : 0xe08) + (c) * 0x100) #define VFE_BUS_IMAGE_CFG0(vfe, c) ((vfe_is_lite(vfe) ? 0x40c : 0xe0c) + (c) * 0x100) @@ -186,7 +187,8 @@ static void vfe_wm_start(struct vfe_device *vfe, u8 rdi, struct vfe_line *line) vfe_disable_irq(vfe); /* Enable WM */ - writel(VFE_BUS_WRITE_CLIENT_CFG_EN, + writel(VFE_BUS_WRITE_CLIENT_CFG_EN | + VFE_BUS_WRITE_CLIENT_CFG_MODE_MIPI_RAW, vfe->base + VFE_BUS_WRITE_CLIENT_CFG(vfe, wm)); dev_dbg(vfe->camss->dev, "RDI%d WM:%d width %d height %d stride %d\n", From 4df59a24510b1f50ab7e036901f58eb65102dce3 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Thu, 27 Aug 2026 19:35:16 +0100 Subject: [PATCH 32/49] media: qcom: camss: restore IMX681 receiver crop Restore the CSID-specific part of the qualified camera path: detect the one-trio C-PHY RAW10 3844x2640 sink format, decode it for the receiver crop, program a 3840-pixel horizontal crop, and expose 3840 pixels on the CSID source pad. Keep the final source branch's exact 3840-or-3844 IMX681 signature helper so the standalone 3840x2640 driver can use the same guarded diagnostics. C-PHY routing itself is already carried by the preceding beta camera topic, so it is intentionally not duplicated here. This extracts the remaining receiver-crop and standalone-signature behavior from the two linked hardware-qualified source commits while preserving their author. Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/73960c772d24f1ed4957ce3323541ce4cb3403b4 Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/6621d73e732c5dc24cdb6c28240e900bcb32c192 Signed-off-by: Leon Silcott --- .../platform/qcom/camss/camss-csid-680.c | 15 +++++++-- .../media/platform/qcom/camss/camss-csid.c | 33 ++++++++++++++++--- .../media/platform/qcom/camss/camss-csid.h | 5 +++ 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/drivers/media/platform/qcom/camss/camss-csid-680.c b/drivers/media/platform/qcom/camss/camss-csid-680.c index 9d1884cce3c2f6..11e8236458b174 100644 --- a/drivers/media/platform/qcom/camss/camss-csid-680.c +++ b/drivers/media/platform/qcom/camss/camss-csid-680.c @@ -161,6 +161,7 @@ #define CSID_RDI_FRM_DROP_PERIOD(rdi) (0x544 + 0x100 * (rdi)) #define CSID_RDI_IRQ_SUBSAMPLE_PATTERN(rdi) (0x548 + 0x100 * (rdi)) #define CSID_RDI_IRQ_SUBSAMPLE_PERIOD(rdi) (0x54c + 0x100 * (rdi)) +#define CSID_RDI_RPP_HCROP(rdi) (0x550 + 0x100 * (rdi)) #define CSID_RDI_PIX_DROP_PATTERN(rdi) (0x558 + 0x100 * (rdi)) #define CSID_RDI_PIX_DROP_PERIOD(rdi) (0x55c + 0x100 * (rdi)) #define CSID_RDI_LINE_DROP_PATTERN(rdi) (0x560 + 0x100 * (rdi)) @@ -248,9 +249,11 @@ static void __csid_configure_top(struct csid_device *csid) static void __csid_configure_rdi_stream(struct csid_device *csid, u8 enable, u8 port, u8 vc) { struct v4l2_mbus_framefmt *input_format = &csid->fmt[MSM_CSID_PAD_FIRST_SRC + port]; + struct v4l2_mbus_framefmt *sink_format = &csid->fmt[MSM_CSID_PAD_SINK]; const struct csid_format_info *format = csid_get_fmt_entry(csid->res->formats->formats, csid->res->formats->nformats, input_format->code); + bool crop = csid_is_cphy_raw10_3844(csid, sink_format); u8 lane_cnt = csid->phy.lane_cnt; u8 dt_id; u32 val; @@ -275,8 +278,11 @@ static void __csid_configure_rdi_stream(struct csid_device *csid, u8 enable, u8 */ dt_id = port & 0x03; - /* note: for non-RDI path, this should be format->decode_format */ - val |= DECODE_FORMAT_PAYLOAD_ONLY << RDI_CFG0_DECODE_FORMAT; + /* Decoding is required for the explicitly selected receiver crop. */ + if (crop) + val |= format->decode_format << RDI_CFG0_DECODE_FORMAT; + else + val |= DECODE_FORMAT_PAYLOAD_ONLY << RDI_CFG0_DECODE_FORMAT; val |= format->data_type << RDI_CFG0_DATA_TYPE; val |= vc << RDI_CFG0_VIRTUAL_CHANNEL; val |= dt_id << RDI_CFG0_DT_ID; @@ -287,6 +293,11 @@ static void __csid_configure_rdi_stream(struct csid_device *csid, u8 enable, u8 val |= RDI_CFG1_BYTE_CNTR_EN; val |= RDI_CFG1_TIMESTAMP_EN; val |= RDI_CFG1_PACKING_MIPI; + if (crop) { + /* C-PHY RAW10 3844 is the IMX681 route: crop to 4800-byte rows. */ + writel(3839 << 16, csid->base + CSID_RDI_RPP_HCROP(port)); + val |= RDI_CFG1_CROP_H_EN; + } writel(val, csid->base + CSID_RDI_CFG1(port)); diff --git a/drivers/media/platform/qcom/camss/camss-csid.c b/drivers/media/platform/qcom/camss/camss-csid.c index 86d0e24069a9aa..4f75a5808d6e82 100644 --- a/drivers/media/platform/qcom/camss/camss-csid.c +++ b/drivers/media/platform/qcom/camss/camss-csid.c @@ -532,6 +532,27 @@ const struct csid_format_info *csid_get_fmt_entry(const struct csid_format_info return &formats[0]; } +bool csid_is_cphy_raw10_3844(struct csid_device *csid, + const struct v4l2_mbus_framefmt *format) +{ + return format->width == 3844 && + csid_is_sp11_imx681_format(csid, format); +} + +bool csid_is_sp11_imx681_format(struct csid_device *csid, + const struct v4l2_mbus_framefmt *format) +{ + const struct csid_format_info *fmt; + + fmt = csid_get_fmt_entry(csid->res->formats->formats, + csid->res->formats->nformats, format->code); + + /* These explicit transport/geometry signatures are unique to the IMX681. */ + return csid->phy.bus_type == V4L2_MBUS_CSI2_CPHY && + (format->width == 3840 || format->width == 3844) && + format->height == 2640 && fmt->data_type == MIPI_CSI2_DT_RAW10; +} + /* * csid_set_clock_rates - Calculate and set clock rates on CSID module * @csiphy: CSID device @@ -849,13 +870,17 @@ static void csid_try_format(struct csid_device *csid, case MSM_CSID_PAD_SRC: if (csid->testgen.nmodes == CSID_PAYLOAD_MODE_DISABLED || csid->testgen_mode->cur.val == 0) { - /* Test generator is disabled, */ - /* keep pad formats in sync */ + const struct v4l2_mbus_framefmt *sink_format; u32 code = fmt->code; - *fmt = *__csid_get_format(csid, sd_state, - MSM_CSID_PAD_SINK, which); + /* Test generator is disabled, */ + /* keep pad formats in sync */ + sink_format = __csid_get_format(csid, sd_state, + MSM_CSID_PAD_SINK, which); + *fmt = *sink_format; fmt->code = csid->res->hw_ops->src_pad_code(csid, fmt->code, 0, code); + if (csid_is_cphy_raw10_3844(csid, sink_format)) + fmt->width = 3840; } else { /* Test generator is enabled, set format on source */ /* pad to allow test generator usage */ diff --git a/drivers/media/platform/qcom/camss/camss-csid.h b/drivers/media/platform/qcom/camss/camss-csid.h index b98973d7edd524..e993cdaacce609 100644 --- a/drivers/media/platform/qcom/camss/camss-csid.h +++ b/drivers/media/platform/qcom/camss/camss-csid.h @@ -197,6 +197,11 @@ const struct csid_format_info *csid_get_fmt_entry(const struct csid_format_info unsigned int nformats, u32 code); +bool csid_is_cphy_raw10_3844(struct csid_device *csid, + const struct v4l2_mbus_framefmt *format); +bool csid_is_sp11_imx681_format(struct csid_device *csid, + const struct v4l2_mbus_framefmt *format); + int msm_csid_subdev_init(struct camss *camss, struct csid_device *csid, const struct camss_subdev_resources *res, u8 id); From 9db37e100d82da48054b8775817d5456491245bf Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Fri, 28 Aug 2026 08:50:30 +0100 Subject: [PATCH 33/49] media: qcom: camss: program CSID680 RDI keep-all state Program period 1 and pattern 0 for the frame, pixel, and line drop engines. This preserves every RDI sample explicitly instead of relying on reset values, while the independently configured IMX681 horizontal crop remains enabled only for its exact C-PHY RAW10 geometry. Extract the CSID portion of the hardware-qualified source commit and preserve its author. Sensor, PHY, device-tree, and packaging changes from that original multi-area commit are already represented by the preceding beta camera topic. Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/1592ec3774189f107d9267ffd65a71e841bccf1e Signed-off-by: Leon Silcott --- drivers/media/platform/qcom/camss/camss-csid-680.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/drivers/media/platform/qcom/camss/camss-csid-680.c b/drivers/media/platform/qcom/camss/camss-csid-680.c index 11e8236458b174..5a270cde4ff823 100644 --- a/drivers/media/platform/qcom/camss/camss-csid-680.c +++ b/drivers/media/platform/qcom/camss/camss-csid-680.c @@ -261,9 +261,6 @@ static void __csid_configure_rdi_stream(struct csid_device *csid, u8 enable, u8 if (!lane_cnt) lane_cnt = 4; - val = 0; - writel(val, csid->base + CSID_RDI_FRM_DROP_PERIOD(port)); - /* * DT_ID is a two bit bitfield that is concatenated with * the four least significant bits of the five bit VC @@ -277,6 +274,7 @@ static void __csid_configure_rdi_stream(struct csid_device *csid, u8 enable, u8 * CID : VC 3:0 << 2 | DT_ID 1:0 */ dt_id = port & 0x03; + val = 0; /* Decoding is required for the explicitly selected receiver crop. */ if (crop) @@ -301,6 +299,14 @@ static void __csid_configure_rdi_stream(struct csid_device *csid, u8 enable, u8 writel(val, csid->base + CSID_RDI_CFG1(port)); + /* Program an explicit keep-all state for every drop engine. */ + writel(1, csid->base + CSID_RDI_FRM_DROP_PERIOD(port)); + writel(0, csid->base + CSID_RDI_FRM_DROP_PATTERN(port)); + writel(1, csid->base + CSID_RDI_PIX_DROP_PERIOD(port)); + writel(0, csid->base + CSID_RDI_PIX_DROP_PATTERN(port)); + writel(1, csid->base + CSID_RDI_LINE_DROP_PERIOD(port)); + writel(0, csid->base + CSID_RDI_LINE_DROP_PATTERN(port)); + val = 0; writel(val, csid->base + CSID_RDI_IRQ_SUBSAMPLE_PERIOD(port)); From 992be226739cae2aaced81c085251637b24a56cc Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Fri, 28 Aug 2026 14:35:01 +0100 Subject: [PATCH 34/49] media: qcom: camss: unwind partial stream starts Track the last subdevice that accepted stream start. If a later subdevice fails, stop only the subdevices that were successfully started before tearing down the media pipeline. Reuse the same helper for normal stream stop and keep teardown going after nonfatal stop errors. This preserves the CAMSS portion of the hardware-qualified source commit. Its sensor, CCS, and packaging changes are not copied because the beta already uses the validated standalone IMX681 driver and focused packaging. Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/0097c12b0fec69b2d1aef031d4cd63fd78fd7a48 Signed-off-by: Leon Silcott --- .../media/platform/qcom/camss/camss-video.c | 61 +++++++++++-------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/drivers/media/platform/qcom/camss/camss-video.c b/drivers/media/platform/qcom/camss/camss-video.c index 0852eb6f13154c..7ff03ebb5450db 100644 --- a/drivers/media/platform/qcom/camss/camss-video.c +++ b/drivers/media/platform/qcom/camss/camss-video.c @@ -249,11 +249,43 @@ static int video_prepare_streaming(struct vb2_queue *q) return ret; } +static void video_stop_subdevices(struct camss_video *video, + struct media_entity *last_started) +{ + struct video_device *vdev = &video->vdev; + struct media_entity *entity = &vdev->entity; + struct media_pad *pad; + struct v4l2_subdev *subdev; + int ret; + + while (1) { + pad = &entity->pads[0]; + if (!(pad->flags & MEDIA_PAD_FL_SINK)) + break; + + pad = media_pad_remote_pad_first(pad); + if (!pad || !is_media_entity_v4l2_subdev(pad->entity)) + break; + + entity = pad->entity; + subdev = media_entity_to_v4l2_subdev(entity); + + ret = v4l2_subdev_call(subdev, video, s_stream, 0); + if (ret < 0 && ret != -ENOIOCTLCMD) + dev_err(video->camss->dev, + "Video pipeline stop failed: %d\n", ret); + + if (last_started && entity == last_started) + break; + } +} + static int video_start_streaming(struct vb2_queue *q, unsigned int count) { struct camss_video *video = vb2_get_drv_priv(q); struct video_device *vdev = &video->vdev; struct media_entity *entity; + struct media_entity *last_started = NULL; struct media_pad *pad; struct v4l2_subdev *subdev; int ret; @@ -284,11 +316,15 @@ static int video_start_streaming(struct vb2_queue *q, unsigned int count) ret = v4l2_subdev_call(subdev, video, s_stream, 1); if (ret < 0 && ret != -ENOIOCTLCMD) goto error; + if (ret >= 0) + last_started = entity; } return 0; error: + if (last_started) + video_stop_subdevices(video, last_started); video_device_pipeline_stop(vdev); flush_buffers: @@ -301,31 +337,8 @@ static void video_stop_streaming(struct vb2_queue *q) { struct camss_video *video = vb2_get_drv_priv(q); struct video_device *vdev = &video->vdev; - struct media_entity *entity; - struct media_pad *pad; - struct v4l2_subdev *subdev; - int ret; - - entity = &vdev->entity; - while (1) { - pad = &entity->pads[0]; - if (!(pad->flags & MEDIA_PAD_FL_SINK)) - break; - pad = media_pad_remote_pad_first(pad); - if (!pad || !is_media_entity_v4l2_subdev(pad->entity)) - break; - - entity = pad->entity; - subdev = media_entity_to_v4l2_subdev(entity); - - ret = v4l2_subdev_call(subdev, video, s_stream, 0); - - if (ret) { - dev_err(video->camss->dev, "Video pipeline stop failed: %d\n", ret); - return; - } - } + video_stop_subdevices(video, NULL); video_device_pipeline_stop(vdev); From 9e2e6c46423ba3e3f519c6db1874aaa6d2a8c9c6 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Fri, 28 Aug 2026 22:43:19 +0100 Subject: [PATCH 35/49] media: qcom: camss: restore IMX681 stop diagnostics Sample read-only CSID680 receiver, packet, error, IRQ, crop, and linked- RDI state while its clocks are still running. Sample matching VFE680 write-master and IRQ state before disabling the writer. Keep the source branch's narrow activation signature: X1E80100, one-trio C-PHY RAW10, 3840x2640 or its 3844-pixel pre-crop form, and the exact packed VFE output geometry. Other SoCs, D-PHY routes, formats, and VFE Lite remain unchanged. This extracts the active CAMSS diagnostics from the hardware-qualified source commit and preserves its author. The old CCS sensor diagnostics are not copied: the qualified final tree replaced that unbound path with the standalone IMX681 driver. Link: https://github.com/ooaklee/linux_ms_dev_kit-sp11/commit/4d190bc96139bdf7ddb3cabd0551a86826600aae Signed-off-by: Leon Silcott --- .../platform/qcom/camss/camss-csid-680.c | 40 ++++++++++++++++++- .../media/platform/qcom/camss/camss-csid.c | 4 ++ .../media/platform/qcom/camss/camss-csid.h | 6 +++ .../media/platform/qcom/camss/camss-vfe-680.c | 39 ++++++++++++++++++ 4 files changed, 88 insertions(+), 1 deletion(-) diff --git a/drivers/media/platform/qcom/camss/camss-csid-680.c b/drivers/media/platform/qcom/camss/camss-csid-680.c index 5a270cde4ff823..0db656aed46c4d 100644 --- a/drivers/media/platform/qcom/camss/camss-csid-680.c +++ b/drivers/media/platform/qcom/camss/camss-csid-680.c @@ -286,7 +286,6 @@ static void __csid_configure_rdi_stream(struct csid_device *csid, u8 enable, u8 val |= dt_id << RDI_CFG0_DT_ID; writel(val, csid->base + CSID_RDI_CFG0(port)); - /* No crop or drop configuration is programmed for the RDI path. */ val = RDI_CFG1_TIMESTAMP_STB_FRAME; val |= RDI_CFG1_BYTE_CNTR_EN; val |= RDI_CFG1_TIMESTAMP_EN; @@ -324,6 +323,44 @@ static void __csid_configure_rdi_stream(struct csid_device *csid, u8 enable, u8 writel(val, csid->base + CSID_RDI_CFG0(port)); } +static void csid_log_status(struct csid_device *csid) +{ + struct v4l2_mbus_framefmt *sink_format = + &csid->fmt[MSM_CSID_PAD_SINK]; + int i; + + if (csid->camss->res->version != CAMSS_X1E80100 || + !csid_is_sp11_imx681_format(csid, sink_format)) + return; + + dev_info(csid->camss->dev, + "CSID%u IMX681 stop: RX_CFG0=%#010x RX_CFG1=%#010x TOTAL_PKTS=%u ECC=%u CRC=%u\n", + csid->id, readl(csid->base + CSID_CSI2_RX_CFG0), + readl(csid->base + CSID_CSI2_RX_CFG1), + readl(csid->base + CSID_CSI2_RX_TOTAL_PKTS_RCVD), + readl(csid->base + CSID_CSI2_RX_STATS_ECC), + readl(csid->base + CSID_CSI2_RX_CRC_ERRORS)); + dev_info(csid->camss->dev, + "CSID%u IMX681 IRQ: TOP=%#010x BUF_DONE=%#010x RX=%#010x\n", + csid->id, readl(csid->base + CSID_TOP_IRQ_STATUS), + readl(csid->base + CSID_BUF_DONE_IRQ_STATUS), + readl(csid->base + CSID_CSI2_RX_IRQ_STATUS)); + + for (i = 0; i < MSM_CSID_MAX_SRC_STREAMS; i++) { + if (!(csid->phy.en_vc & BIT(i))) + continue; + + dev_info(csid->camss->dev, + "CSID%u IMX681 linked RDI%d: IRQ=%#010x CFG0=%#010x CTRL=%#010x CFG1=%#010x HCROP=%#010x\n", + csid->id, i, + readl(csid->base + CSID_CSI2_RDIN_IRQ_STATUS(i)), + readl(csid->base + CSID_RDI_CFG0(i)), + readl(csid->base + CSID_RDI_CTRL(i)), + readl(csid->base + CSID_RDI_CFG1(i)), + readl(csid->base + CSID_RDI_RPP_HCROP(i))); + } +} + static void csid_configure_stream(struct csid_device *csid, u8 enable) { int i; @@ -453,6 +490,7 @@ static void csid_subdev_init(struct csid_device *csid) {} const struct csid_hw_ops csid_ops_680 = { .configure_testgen_pattern = NULL, .configure_stream = csid_configure_stream, + .log_status = csid_log_status, .hw_version = csid_hw_version, .isr = csid_isr, .reset = csid_reset, diff --git a/drivers/media/platform/qcom/camss/camss-csid.c b/drivers/media/platform/qcom/camss/camss-csid.c index 4f75a5808d6e82..f91f51d2eb75b2 100644 --- a/drivers/media/platform/qcom/camss/camss-csid.c +++ b/drivers/media/platform/qcom/camss/camss-csid.c @@ -802,6 +802,10 @@ static int csid_set_stream(struct v4l2_subdev *sd, int enable) return -ENOLINK; } + /* Sample status before pipeline PM turns the CSID clocks off. */ + if (!enable && csid->res->hw_ops->log_status) + csid->res->hw_ops->log_status(csid); + if (csid->phy.need_vc_update) { csid->res->hw_ops->configure_stream(csid, enable); csid->phy.need_vc_update = false; diff --git a/drivers/media/platform/qcom/camss/camss-csid.h b/drivers/media/platform/qcom/camss/camss-csid.h index e993cdaacce609..7a91ff319ae966 100644 --- a/drivers/media/platform/qcom/camss/camss-csid.h +++ b/drivers/media/platform/qcom/camss/camss-csid.h @@ -82,6 +82,12 @@ struct csid_hw_ops { */ void (*configure_stream)(struct csid_device *csid, u8 enable); + /* + * log_status - Log a read-only hardware status snapshot while powered + * @csid: CSID device + */ + void (*log_status)(struct csid_device *csid); + /* * configure_testgen_pattern - Validates and configures output pattern mode * of test pattern generator diff --git a/drivers/media/platform/qcom/camss/camss-vfe-680.c b/drivers/media/platform/qcom/camss/camss-vfe-680.c index 0d228bc3032bae..db1c0e0d296c78 100644 --- a/drivers/media/platform/qcom/camss/camss-vfe-680.c +++ b/drivers/media/platform/qcom/camss/camss-vfe-680.c @@ -195,10 +195,49 @@ static void vfe_wm_start(struct vfe_device *vfe, u8 rdi, struct vfe_line *line) rdi, wm, pix->width, pix->height, stride); } +static bool vfe_is_imx681_raw(struct vfe_device *vfe, u8 rdi) +{ + struct v4l2_pix_format_mplane *pix; + + if (vfe->camss->res->version != CAMSS_X1E80100 || + vfe_is_lite(vfe) || rdi >= vfe->res->line_num) + return false; + + pix = &vfe->line[rdi].video_out.active_fmt.fmt.pix_mp; + + return pix->pixelformat == V4L2_PIX_FMT_SRGGB10P && + pix->width == 3840 && pix->height == 2640 && + pix->plane_fmt[0].bytesperline == 4800; +} + static void vfe_wm_stop(struct vfe_device *vfe, u8 rdi) { u8 wm = RDI_WM(rdi); + if (vfe_is_imx681_raw(vfe, rdi)) { + dev_info(vfe->camss->dev, + "VFE%u IMX681 RDI%u WM%u stop: CFG=%#010x ADDR=%#010x INCR=%#010x IMG=%#010x/%#010x/%#010x PACKER=%#010x\n", + vfe->id, rdi, wm, + readl(vfe->base + VFE_BUS_WRITE_CLIENT_CFG(vfe, wm)), + readl(vfe->base + VFE_BUS_IMAGE_ADDR(vfe, wm)), + readl(vfe->base + VFE_BUS_FRAME_INCR(vfe, wm)), + readl(vfe->base + VFE_BUS_IMAGE_CFG0(vfe, wm)), + readl(vfe->base + VFE_BUS_IMAGE_CFG1(vfe, wm)), + readl(vfe->base + VFE_BUS_IMAGE_CFG2(vfe, wm)), + readl(vfe->base + VFE_BUS_PACKER_CFG(vfe, wm))); + dev_info(vfe->camss->dev, + "VFE%u IMX681 IRQ: TOP=%#010x/%#010x BUS=%#010x/%#010x VIOL=%#010x OVERFLOW=%#010x IMAGE_VIOL=%#010x\n", + vfe->id, + readl(vfe->base + VFE_TOP_IRQn_STATUS(vfe, 0)), + readl(vfe->base + VFE_TOP_IRQn_STATUS(vfe, 1)), + readl(vfe->base + VFE_BUS_IRQn_STATUS(vfe, 0)), + readl(vfe->base + VFE_BUS_IRQn_STATUS(vfe, 1)), + readl(vfe->base + VFE_BUS_WR_VIOLATION_STATUS(vfe)), + readl(vfe->base + VFE_BUS_WR_OVERFLOW_STATUS(vfe)), + readl(vfe->base + + VFE_BUS_WR_IMAGE_VIOLATION_STATUS(vfe))); + } + writel(0, vfe->base + VFE_BUS_WRITE_CLIENT_CFG(vfe, wm)); } From 396fdb0dcf905516b90f568c95a5757270905179 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Wed, 2 Sep 2026 21:23:56 +0100 Subject: [PATCH 36/49] UBUNTU: Ubuntu-qcom-x1e-7.2.2-jg-0sp11beta2 Supersede the withdrawn sp11beta1 qualification carrier after restoring the active camera behavior identified by the final-tree parity audit. Signed-off-by: Leon Silcott --- debian.qcom-x1e/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/debian.qcom-x1e/changelog b/debian.qcom-x1e/changelog index 46183be849324a..27fe761a2535e9 100644 --- a/debian.qcom-x1e/changelog +++ b/debian.qcom-x1e/changelog @@ -1,3 +1,9 @@ +linux-qcom-x1e (7.2.2-jg-0sp11beta2) resolute; urgency=medium + + * Supersede sp11beta1 with the restored camera parity series. + + -- Leon Silcott Wed, 02 Sep 2026 21:25:00 +0100 + linux-qcom-x1e (7.2.2-jg-0sp11beta1) resolute; urgency=medium * Assemble the guarded SP11 topic stack for qualification. From 68ed7f4d811afa6da2b62966437ced2901dc47af Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Wed, 2 Sep 2026 21:28:33 +0100 Subject: [PATCH 37/49] media: qcom: camss: contain SP11 receiver changes The qualified integration source applied its CSID680 keep-all state and VFE680 MIPI RAW write mode to every user of those shared blocks. Preserve the observed behavior for the X1E80100 C-PHY RAW10 IMX681 route while restoring the existing programming for all other formats and SoCs. Use the already exact transport and geometry signature for containment. For that route, keep crop/drop disabled unless the 3844-to-3840 horizontal crop is selected, explicitly program all drop engines to keep every sample, and select the MIPI RAW VFE write-client mode. Other routes retain their prior RDI flags, reset-value drop state, and VFE mode. This is an intentional guardrail relative to the qualified source, not a claim of cross-device equivalence. The SP11 path and a non-SP11 VFE680 RDI path both require hardware qualification before integration. Signed-off-by: Leon Silcott --- .../platform/qcom/camss/camss-csid-680.c | 27 ++++++++++++++----- .../media/platform/qcom/camss/camss-csid.c | 3 ++- .../media/platform/qcom/camss/camss-vfe-680.c | 11 +++++--- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/drivers/media/platform/qcom/camss/camss-csid-680.c b/drivers/media/platform/qcom/camss/camss-csid-680.c index 0db656aed46c4d..31e500f22677c1 100644 --- a/drivers/media/platform/qcom/camss/camss-csid-680.c +++ b/drivers/media/platform/qcom/camss/camss-csid-680.c @@ -253,6 +253,7 @@ static void __csid_configure_rdi_stream(struct csid_device *csid, u8 enable, u8 const struct csid_format_info *format = csid_get_fmt_entry(csid->res->formats->formats, csid->res->formats->nformats, input_format->code); + bool sp11_imx681 = csid_is_sp11_imx681_format(csid, sink_format); bool crop = csid_is_cphy_raw10_3844(csid, sink_format); u8 lane_cnt = csid->phy.lane_cnt; u8 dt_id; @@ -261,6 +262,10 @@ static void __csid_configure_rdi_stream(struct csid_device *csid, u8 enable, u8 if (!lane_cnt) lane_cnt = 4; + /* Preserve the existing reset-value path outside the qualified route. */ + if (!sp11_imx681) + writel(0, csid->base + CSID_RDI_FRM_DROP_PERIOD(port)); + /* * DT_ID is a two bit bitfield that is concatenated with * the four least significant bits of the five bit VC @@ -289,6 +294,12 @@ static void __csid_configure_rdi_stream(struct csid_device *csid, u8 enable, u8 val = RDI_CFG1_TIMESTAMP_STB_FRAME; val |= RDI_CFG1_BYTE_CNTR_EN; val |= RDI_CFG1_TIMESTAMP_EN; + if (!sp11_imx681) { + val |= RDI_CFG1_DROP_H_EN; + val |= RDI_CFG1_DROP_V_EN; + val |= RDI_CFG1_CROP_H_EN; + val |= RDI_CFG1_CROP_V_EN; + } val |= RDI_CFG1_PACKING_MIPI; if (crop) { /* C-PHY RAW10 3844 is the IMX681 route: crop to 4800-byte rows. */ @@ -298,13 +309,15 @@ static void __csid_configure_rdi_stream(struct csid_device *csid, u8 enable, u8 writel(val, csid->base + CSID_RDI_CFG1(port)); - /* Program an explicit keep-all state for every drop engine. */ - writel(1, csid->base + CSID_RDI_FRM_DROP_PERIOD(port)); - writel(0, csid->base + CSID_RDI_FRM_DROP_PATTERN(port)); - writel(1, csid->base + CSID_RDI_PIX_DROP_PERIOD(port)); - writel(0, csid->base + CSID_RDI_PIX_DROP_PATTERN(port)); - writel(1, csid->base + CSID_RDI_LINE_DROP_PERIOD(port)); - writel(0, csid->base + CSID_RDI_LINE_DROP_PATTERN(port)); + if (sp11_imx681) { + /* Program an explicit keep-all state for every drop engine. */ + writel(1, csid->base + CSID_RDI_FRM_DROP_PERIOD(port)); + writel(0, csid->base + CSID_RDI_FRM_DROP_PATTERN(port)); + writel(1, csid->base + CSID_RDI_PIX_DROP_PERIOD(port)); + writel(0, csid->base + CSID_RDI_PIX_DROP_PATTERN(port)); + writel(1, csid->base + CSID_RDI_LINE_DROP_PERIOD(port)); + writel(0, csid->base + CSID_RDI_LINE_DROP_PATTERN(port)); + } val = 0; writel(val, csid->base + CSID_RDI_IRQ_SUBSAMPLE_PERIOD(port)); diff --git a/drivers/media/platform/qcom/camss/camss-csid.c b/drivers/media/platform/qcom/camss/camss-csid.c index f91f51d2eb75b2..a615ac1541288f 100644 --- a/drivers/media/platform/qcom/camss/camss-csid.c +++ b/drivers/media/platform/qcom/camss/camss-csid.c @@ -548,7 +548,8 @@ bool csid_is_sp11_imx681_format(struct csid_device *csid, csid->res->formats->nformats, format->code); /* These explicit transport/geometry signatures are unique to the IMX681. */ - return csid->phy.bus_type == V4L2_MBUS_CSI2_CPHY && + return csid->camss->res->version == CAMSS_X1E80100 && + csid->phy.bus_type == V4L2_MBUS_CSI2_CPHY && (format->width == 3840 || format->width == 3844) && format->height == 2640 && fmt->data_type == MIPI_CSI2_DT_RAW10; } diff --git a/drivers/media/platform/qcom/camss/camss-vfe-680.c b/drivers/media/platform/qcom/camss/camss-vfe-680.c index db1c0e0d296c78..54600ecab9885c 100644 --- a/drivers/media/platform/qcom/camss/camss-vfe-680.c +++ b/drivers/media/platform/qcom/camss/camss-vfe-680.c @@ -153,12 +153,15 @@ static void vfe_wm_update(struct vfe_device *vfe, u8 rdi, u32 addr, writel(addr, vfe->base + VFE_BUS_IMAGE_ADDR(vfe, wm)); } +static bool vfe_is_imx681_raw(struct vfe_device *vfe, u8 rdi); + static void vfe_wm_start(struct vfe_device *vfe, u8 rdi, struct vfe_line *line) { struct v4l2_pix_format_mplane *pix = &line->video_out.active_fmt.fmt.pix_mp; u32 stride = pix->plane_fmt[0].bytesperline; u32 cfg; + u32 wm_cfg = VFE_BUS_WRITE_CLIENT_CFG_EN; u8 wm; cfg = VFE_BUS_IMAGE_CFG0_DATA(pix->height, stride); @@ -186,9 +189,11 @@ static void vfe_wm_start(struct vfe_device *vfe, u8 rdi, struct vfe_line *line) /* We don't process IRQs for VFE in RDI mode at the moment */ vfe_disable_irq(vfe); - /* Enable WM */ - writel(VFE_BUS_WRITE_CLIENT_CFG_EN | - VFE_BUS_WRITE_CLIENT_CFG_MODE_MIPI_RAW, + if (vfe_is_imx681_raw(vfe, rdi)) + wm_cfg |= VFE_BUS_WRITE_CLIENT_CFG_MODE_MIPI_RAW; + + /* Enable WM, selecting MIPI RAW only for the qualified IMX681 route. */ + writel(wm_cfg, vfe->base + VFE_BUS_WRITE_CLIENT_CFG(vfe, wm)); dev_dbg(vfe->camss->dev, "RDI%d WM:%d width %d height %d stride %d\n", From 6d4eeb281d2ecf718696330c838cb9fb30620a43 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sun, 23 Aug 2026 12:33:00 +0100 Subject: [PATCH 38/49] ASoC: q6apm: restore SP11 protected pull geometry The hardware-qualified Denali graph uses a 48 kHz, 16-bit, stereo pull ring with two 1920-byte periods. Restore those ALSA constraints and map only the page which contains the 3840-byte ring. Keep the existing generic push/pull allocation for every other card and topology. Fail closed if the protected Denali runtime reaches prepare with geometry other than the qualified values. Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/q6apm-dai.c | 74 ++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 9 deletions(-) diff --git a/sound/soc/qcom/qdsp6/q6apm-dai.c b/sound/soc/qcom/qdsp6/q6apm-dai.c index 82b2cd249040f3..b973aa67994a4e 100644 --- a/sound/soc/qcom/qdsp6/q6apm-dai.c +++ b/sound/soc/qcom/qdsp6/q6apm-dai.c @@ -32,6 +32,8 @@ #define CAPTURE_MIN_PERIOD_SIZE 6144 #define BUFFER_BYTES_MAX (PLAYBACK_MAX_NUM_PERIODS * PLAYBACK_MAX_PERIOD_SIZE) #define BUFFER_BYTES_MIN (PLAYBACK_MIN_NUM_PERIODS * PLAYBACK_MIN_PERIOD_SIZE) +#define SP11_PULL_PERIOD_BYTES 1920 +#define SP11_PULL_BUFFER_BYTES 3840 #define COMPR_PLAYBACK_MAX_FRAGMENT_SIZE (128 * 1024) #define COMPR_PLAYBACK_MAX_NUM_FRAGMENTS (16 * 4) #define COMPR_PLAYBACK_MIN_FRAGMENT_SIZE (8 * 1024) @@ -72,6 +74,7 @@ struct q6apm_dai_rtd { phys_addr_t phys; phys_addr_t pos_phys; unsigned int pcm_size; + size_t mapped_pcm_bytes; unsigned int push_pull_size; unsigned int pcm_count; unsigned int periods; @@ -117,6 +120,14 @@ static bool q6apm_denali_protection_runtime(struct snd_pcm_substream *substream, q6apm_graph_has_protection(graph); } +static bool q6apm_denali_pull_runtime(struct snd_pcm_substream *substream, + struct q6apm_graph *graph) +{ + return substream->stream == SNDRV_PCM_STREAM_PLAYBACK && + q6apm_denali_protection_runtime(substream, graph) && + q6apm_is_graph_in_push_pull_mode(graph); +} + static bool q6apm_denali_protected_graph(struct snd_soc_component *component, struct snd_soc_pcm_runtime *rtd, unsigned int graph_id) @@ -184,6 +195,30 @@ static const struct snd_pcm_hardware q6apm_dai_hardware_playback = { .fifo_size = 0, }; +static const struct snd_pcm_hardware q6apm_dai_hardware_sp11_pull = { + .info = (SNDRV_PCM_INFO_MMAP | + SNDRV_PCM_INFO_BLOCK_TRANSFER | + SNDRV_PCM_INFO_MMAP_VALID | + SNDRV_PCM_INFO_INTERLEAVED | + SNDRV_PCM_INFO_PAUSE | + SNDRV_PCM_INFO_RESUME | + SNDRV_PCM_INFO_NO_REWINDS | + SNDRV_PCM_INFO_SYNC_APPLPTR | + SNDRV_PCM_INFO_BATCH), + .formats = SNDRV_PCM_FMTBIT_S16_LE, + .rates = SNDRV_PCM_RATE_48000, + .rate_min = 48000, + .rate_max = 48000, + .channels_min = 2, + .channels_max = 2, + .buffer_bytes_max = SP11_PULL_BUFFER_BYTES, + .period_bytes_min = SP11_PULL_PERIOD_BYTES, + .period_bytes_max = SP11_PULL_PERIOD_BYTES, + .periods_min = 2, + .periods_max = 2, + .fifo_size = 0, +}; + static bool q6apm_dai_callback_get(struct q6apm_dai_rtd *prtd) { unsigned long flags; @@ -305,6 +340,7 @@ static int q6apm_dai_prepare(struct snd_soc_component *component, struct device *dev = component->dev; struct q6apm_dai_data *pdata; bool protected; + bool protected_pull; int ret; pdata = snd_soc_component_get_drvdata(component); @@ -318,6 +354,16 @@ static int q6apm_dai_prepare(struct snd_soc_component *component, if (prtd->state == Q6APM_STREAM_QUARANTINED) return -EIO; protected = q6apm_denali_protection_runtime(substream, prtd->graph); + protected_pull = q6apm_denali_pull_runtime(substream, prtd->graph); + if (protected_pull && + (runtime->rate != 48000 || runtime->channels != 2 || + prtd->bits_per_sample != 16 || + prtd->pcm_size != SP11_PULL_BUFFER_BYTES || + snd_pcm_lib_period_bytes(substream) != SP11_PULL_PERIOD_BYTES || + prtd->periods != 2)) { + dev_err(dev, "Denali pull PCM geometry is invalid\n"); + return -EINVAL; + } cfg.direction = substream->stream; cfg.sample_rate = runtime->rate; @@ -496,6 +542,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; + bool protected_pull; int close_ret, graph_id, ret; graph_id = cpu_dai->driver->id; @@ -523,9 +570,11 @@ static int q6apm_dai_open(struct snd_soc_component *component, ret = PTR_ERR(prtd->graph); goto err; } + protected_pull = q6apm_denali_pull_runtime(substream, prtd->graph); if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) - runtime->hw = q6apm_dai_hardware_playback; + runtime->hw = protected_pull ? q6apm_dai_hardware_sp11_pull : + q6apm_dai_hardware_playback; else if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) runtime->hw = q6apm_dai_hardware_capture; @@ -536,7 +585,7 @@ static int q6apm_dai_open(struct snd_soc_component *component, goto err; } - if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) { + if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK && !protected_pull) { ret = snd_pcm_hw_constraint_minmax(runtime, SNDRV_PCM_HW_PARAM_BUFFER_BYTES, BUFFER_BYTES_MIN, BUFFER_BYTES_MAX); if (ret < 0) { @@ -559,7 +608,8 @@ static int q6apm_dai_open(struct snd_soc_component *component, } runtime->private_data = prtd; - runtime->dma_bytes = BUFFER_BYTES_MAX; + prtd->mapped_pcm_bytes = protected_pull ? PAGE_SIZE : BUFFER_BYTES_MAX; + runtime->dma_bytes = prtd->mapped_pcm_bytes; if (pdata->sid < 0) prtd->phys = substream->dma_buffer.addr; else @@ -568,8 +618,9 @@ static int q6apm_dai_open(struct snd_soc_component *component, if (q6apm_is_graph_in_push_pull_mode(prtd->graph)) { void *pos_buffer; - prtd->pos_phys = prtd->phys + BUFFER_BYTES_MAX; - pos_buffer = (void *)(substream->dma_buffer.area + BUFFER_BYTES_MAX); + prtd->pos_phys = prtd->phys + prtd->mapped_pcm_bytes; + pos_buffer = (void *)(substream->dma_buffer.area + + prtd->mapped_pcm_bytes); prtd->pos_buffer = (struct sh_mem_pull_push_mode_position_buffer *)(pos_buffer); } @@ -744,6 +795,7 @@ static int q6apm_dai_memory_map(struct snd_soc_component *component, { struct q6apm_dai_data *pdata; struct device *dev = component->dev; + size_t map_size; phys_addr_t phys; int ret; @@ -758,7 +810,8 @@ static int q6apm_dai_memory_map(struct snd_soc_component *component, else phys = substream->dma_buffer.addr | (pdata->sid << 32); - ret = q6apm_map_memory_fixed_region(dev, graph_id, phys, BUFFER_BYTES_MAX); + map_size = protected && is_push_pull ? PAGE_SIZE : BUFFER_BYTES_MAX; + ret = q6apm_map_memory_fixed_region(dev, graph_id, phys, map_size); if (ret < 0) { *retain_buffer = protected && (ret == -ETIMEDOUT || ret == -ESHUTDOWN); @@ -768,9 +821,10 @@ static int q6apm_dai_memory_map(struct snd_soc_component *component, if (is_push_pull) { if (pdata->sid < 0) - phys = substream->dma_buffer.addr + BUFFER_BYTES_MAX; + phys = substream->dma_buffer.addr + map_size; else - phys = (substream->dma_buffer.addr + BUFFER_BYTES_MAX) | (pdata->sid << 32); + phys = (substream->dma_buffer.addr + map_size) | + (pdata->sid << 32); ret = q6apm_map_pos_buffer(dev, graph_id, phys, POS_BUFFER_BYTES); if (ret < 0) { @@ -818,7 +872,7 @@ static int q6apm_dai_pcm_new(struct snd_soc_component *component, struct snd_soc * address arithmetic can overflow when the buffer is placed near the * end of the addressable range. */ - int size = BUFFER_BYTES_MAX + PAGE_SIZE; + int size; int graph_id, ret; bool is_push_pull; bool protected; @@ -842,6 +896,8 @@ static int q6apm_dai_pcm_new(struct snd_soc_component *component, struct snd_soc is_push_pull = q6apm_is_graph_in_push_pull_mode_from_id(component->dev, graph_id, substream->stream); + size = protected && is_push_pull ? PAGE_SIZE : BUFFER_BYTES_MAX; + size += PAGE_SIZE; if (is_push_pull) size += POS_BUFFER_BYTES; From b59a82c3761922f4aa71c31ade19cb80864ccc1a Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sun, 23 Aug 2026 12:33:00 +0100 Subject: [PATCH 39/49] ASoC: q6apm: restore protected pull pause lifecycle The qualified SP11 playback path keeps its integrated pull graph running across ALSA STOP and reprepare, and uses the topology soft-pause module for pause and resume. Restore the exact zero-length parameters for instance 0x466b, register both completion events, and retain the measured 20 ms ramp, 25 ms downstream delay, and 5 ms completion margin. Limit the persistent lifecycle to Denali playback with both a protected profile and a pull endpoint. Keep the existing command correlation, uncertain-state quarantine, and final close handling around it. Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/audioreach.h | 2 + sound/soc/qcom/qdsp6/q6apm-dai.c | 115 +++++++++++++++++++++++- sound/soc/qcom/qdsp6/q6apm-lpass-dais.c | 6 +- sound/soc/qcom/qdsp6/q6apm.c | 108 +++++++++++++++++++++- sound/soc/qcom/qdsp6/q6apm.h | 4 + 5 files changed, 230 insertions(+), 5 deletions(-) diff --git a/sound/soc/qcom/qdsp6/audioreach.h b/sound/soc/qcom/qdsp6/audioreach.h index 1c641d17102954..5dab94880ecbf6 100644 --- a/sound/soc/qcom/qdsp6/audioreach.h +++ b/sound/soc/qcom/qdsp6/audioreach.h @@ -73,6 +73,8 @@ struct audioreach_graph; #define APM_CMD_CLOSE_ALL 0x01001013 #define APM_CMD_REGISTER_SHARED_CFG 0x0100100A #define EVENT_ID_SH_MEM_PULL_PUSH_MODE_WATERMARK 0x0800101C +#define EVENT_ID_SOFT_PAUSE_PAUSE_COMPLETE 0x0800103F +#define EVENT_ID_SOFT_PAUSE_RESUME_COMPLETE 0x08001043 /** * struct event_cfg_sh_mem_pull_push_mode_watermark_t - Watermark config diff --git a/sound/soc/qcom/qdsp6/q6apm-dai.c b/sound/soc/qcom/qdsp6/q6apm-dai.c index b973aa67994a4e..fdf1263b8bcda9 100644 --- a/sound/soc/qcom/qdsp6/q6apm-dai.c +++ b/sound/soc/qcom/qdsp6/q6apm-dai.c @@ -3,7 +3,9 @@ #include #include +#include #include +#include #include #include #include @@ -34,6 +36,12 @@ #define BUFFER_BYTES_MIN (PLAYBACK_MIN_NUM_PERIODS * PLAYBACK_MIN_PERIOD_SIZE) #define SP11_PULL_PERIOD_BYTES 1920 #define SP11_PULL_BUFFER_BYTES 3840 +#define SP11_SOFT_PAUSE_RAMPDOWN_MS 20 +#define SP11_SOFT_PAUSE_DOWNSTREAM_MS 25 +#define SP11_SOFT_PAUSE_MARGIN_MS 5 +#define SP11_SOFT_PAUSE_TIMEOUT_MS (SP11_SOFT_PAUSE_RAMPDOWN_MS + \ + SP11_SOFT_PAUSE_DOWNSTREAM_MS + \ + SP11_SOFT_PAUSE_MARGIN_MS) #define COMPR_PLAYBACK_MAX_FRAGMENT_SIZE (128 * 1024) #define COMPR_PLAYBACK_MAX_NUM_FRAGMENTS (16 * 4) #define COMPR_PLAYBACK_MIN_FRAGMENT_SIZE (8 * 1024) @@ -93,6 +101,10 @@ struct q6apm_dai_rtd { wait_queue_head_t callback_wait; bool callbacks_quarantined; bool notify_on_drain; + bool soft_paused; + bool pull_started; + struct completion soft_pause_done; + struct completion soft_resume_done; }; struct q6apm_dai_data { @@ -252,7 +264,14 @@ static void event_handler(uint32_t opcode, uint32_t token, void *payload, void * switch (opcode) { case APM_CLIENT_EVENT_WATERMARK_EVENT: - snd_pcm_period_elapsed(substream); + if (prtd->state == Q6APM_STREAM_RUNNING) + snd_pcm_period_elapsed(substream); + break; + case APM_CLIENT_EVENT_SOFT_PAUSE_COMPLETE: + complete(&prtd->soft_pause_done); + break; + case APM_CLIENT_EVENT_SOFT_RESUME_COMPLETE: + complete(&prtd->soft_resume_done); break; case APM_CLIENT_EVENT_CMD_EOS_DONE: prtd->state = Q6APM_STREAM_STOPPED; @@ -364,6 +383,11 @@ static int q6apm_dai_prepare(struct snd_soc_component *component, dev_err(dev, "Denali pull PCM geometry is invalid\n"); return -EINVAL; } + if (protected_pull && prtd->pull_started) { + prtd->state = Q6APM_STREAM_RUNNING; + dev_dbg(dev, "reusing persistent Denali pull graph\n"); + return 0; + } cfg.direction = substream->stream; cfg.sample_rate = runtime->rate; @@ -403,6 +427,15 @@ static int q6apm_dai_prepare(struct snd_soc_component *component, dev_err(dev, "WaterMark event config failed rc = %d\n", ret); return ret; } + if (protected_pull) { + ret = q6apm_register_sp11_soft_pause_events(prtd->graph); + if (ret < 0) { + dev_err(dev, + "soft-pause event registration failed: %d\n", + ret); + return ret; + } + } prtd->push_pull_size = prtd->pcm_size; } } else { @@ -460,6 +493,8 @@ static int q6apm_dai_prepare(struct snd_soc_component *component, /* Now that graph as been prepared and started update the internal state accordingly */ prtd->state = Q6APM_STREAM_RUNNING; + if (protected_pull) + prtd->pull_started = true; return 0; } @@ -488,24 +523,81 @@ static int q6apm_dai_ack(struct snd_soc_component *component, struct snd_pcm_sub return ret; } +static int q6apm_dai_sp11_soft_pause(struct snd_soc_component *component, + struct q6apm_dai_rtd *prtd, + bool pause) +{ + struct completion *done = pause ? &prtd->soft_pause_done : + &prtd->soft_resume_done; + unsigned long timeout; + int ret; + + if (!q6apm_graph_has_protection(prtd->graph) || + !q6apm_is_graph_in_push_pull_mode(prtd->graph)) + return -EOPNOTSUPP; + + /* Suppress watermark callbacks while trigger() holds the PCM lock. */ + if (pause) + prtd->state = Q6APM_STREAM_STOPPED; + + reinit_completion(done); + ret = q6apm_graph_sp11_soft_pause(prtd->graph, pause); + if (ret) { + if (pause) + prtd->state = Q6APM_STREAM_RUNNING; + return ret; + } + + prtd->soft_paused = pause; + timeout = wait_for_completion_timeout(done, msecs_to_jiffies(SP11_SOFT_PAUSE_TIMEOUT_MS)); + if (!timeout) { + dev_err(component->dev, "Denali soft-%s completion timed out\n", + pause ? "pause" : "resume"); + return -ETIMEDOUT; + } + + return 0; +} + static int q6apm_dai_trigger(struct snd_soc_component *component, struct snd_pcm_substream *substream, int cmd) { struct snd_pcm_runtime *runtime = substream->runtime; struct q6apm_dai_rtd *prtd = runtime->private_data; bool protected; + bool protected_pull; int ret = 0; protected = q6apm_denali_protection_runtime(substream, prtd->graph); + protected_pull = q6apm_denali_pull_runtime(substream, prtd->graph); if (prtd->state == Q6APM_STREAM_QUARANTINED) return -EIO; switch (cmd) { case SNDRV_PCM_TRIGGER_START: + break; case SNDRV_PCM_TRIGGER_RESUME: case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: + if (protected_pull && prtd->soft_paused) { + ret = q6apm_dai_sp11_soft_pause(component, prtd, false); + if (!ret) { + prtd->soft_paused = false; + prtd->state = Q6APM_STREAM_RUNNING; + } + } break; case SNDRV_PCM_TRIGGER_STOP: - /* TODO support be handled via SoftPause Module */ + if (protected_pull) { + if (prtd->soft_paused) { + ret = q6apm_dai_sp11_soft_pause(component, prtd, false); + if (ret) + break; + prtd->soft_paused = false; + } + prtd->state = Q6APM_STREAM_STOPPED; + prtd->queue_ptr = 0; + prtd->last_pos_index = 0; + break; + } if (protected && (prtd->state == Q6APM_STREAM_RUNNING || prtd->state == Q6APM_STREAM_UNCERTAIN)) { @@ -518,7 +610,13 @@ static int q6apm_dai_trigger(struct snd_soc_component *component, prtd->last_pos_index = 0; break; case SNDRV_PCM_TRIGGER_SUSPEND: + break; case SNDRV_PCM_TRIGGER_PAUSE_PUSH: + if (protected_pull && !prtd->soft_paused) { + ret = q6apm_dai_sp11_soft_pause(component, prtd, true); + if (!ret) + prtd->state = Q6APM_STREAM_STOPPED; + } break; default: ret = -EINVAL; @@ -561,6 +659,8 @@ static int q6apm_dai_open(struct snd_soc_component *component, spin_lock_init(&prtd->callback_lock); atomic_set(&prtd->callback_users, 0); init_waitqueue_head(&prtd->callback_wait); + init_completion(&prtd->soft_pause_done); + init_completion(&prtd->soft_resume_done); prtd->substream = substream; prtd->graph = q6apm_graph_open(dev, event_handler, prtd, graph_id, substream->stream, @@ -680,6 +780,9 @@ static int q6apm_dai_hw_free(struct snd_soc_component *component, protected = q6apm_denali_protection_runtime(substream, prtd->graph); if (prtd->state == Q6APM_STREAM_QUARANTINED) return -EIO; + if (q6apm_denali_pull_runtime(substream, prtd->graph) && + prtd->state != Q6APM_STREAM_UNCERTAIN) + return 0; if (prtd->state != Q6APM_STREAM_UNCERTAIN && (!protected || prtd->state != Q6APM_STREAM_RUNNING)) return 0; @@ -700,19 +803,25 @@ static int q6apm_dai_close(struct snd_soc_component *component, struct snd_pcm_runtime *runtime = substream->runtime; struct q6apm_dai_rtd *prtd = runtime->private_data; bool protected; + bool protected_pull; int close_ret; int ret = 0; protected = q6apm_denali_protection_runtime(substream, prtd->graph); + protected_pull = q6apm_denali_pull_runtime(substream, prtd->graph); if (prtd->state == Q6APM_STREAM_QUARANTINED) { ret = -EIO; + } else if (protected_pull && prtd->pull_started) { + ret = q6apm_graph_stop(prtd->graph); + if (!ret) + prtd->pull_started = false; } else if (prtd->state) { /* only stop graph that is started */ if (!protected || prtd->state == Q6APM_STREAM_RUNNING || prtd->state == Q6APM_STREAM_UNCERTAIN) { ret = q6apm_graph_stop(prtd->graph); } - if (!ret) + if (!ret && !q6apm_is_graph_in_push_pull_mode(prtd->graph)) q6apm_free_fragments(prtd->graph, substream->stream); } if (ret && q6apm_graph_execution_uncertain(prtd->graph)) diff --git a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c index 45701ac1a7f7e5..8599ebc6eb0040 100644 --- a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c +++ b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c @@ -218,7 +218,8 @@ static int q6apm_lpass_dai_trigger(struct snd_pcm_substream *substream, int cmd, break; case SNDRV_PCM_TRIGGER_STOP: if (dai_data->is_port_started[dai->id] && - q6apm_graph_has_protection(dai_data->graph[dai->id])) { + q6apm_graph_has_protection(dai_data->graph[dai->id]) && + !q6apm_is_graph_in_push_pull_mode(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", @@ -246,6 +247,9 @@ static int q6apm_lpass_dai_prepare(struct snd_pcm_substream *substream, struct s return graph_id; if (dai_data->is_port_started[dai->id]) { + if (q6apm_graph_has_protection(dai_data->graph[dai->id]) && + q6apm_is_graph_in_push_pull_mode(dai_data->graph[dai->id])) + return 0; rc = q6apm_graph_stop(dai_data->graph[dai->id]); if (rc < 0) { dev_err(dai->dev, "failed to stop APM port (%d)\n", rc); diff --git a/sound/soc/qcom/qdsp6/q6apm.c b/sound/soc/qcom/qdsp6/q6apm.c index 81fe13a8d25593..f72ead47655c83 100644 --- a/sound/soc/qcom/qdsp6/q6apm.c +++ b/sound/soc/qcom/qdsp6/q6apm.c @@ -26,6 +26,12 @@ #define APM_MMAP_TOKEN_OOB_SEQ_MASK GENMASK(31, 18) #define Q6APM_DSP_SID_MASK GENMASK(3, 0) +/* Windows DEFAULT soft-pause module in the qualified SP11 graph. */ +#define SP11_MODULE_ID_SOFT_PAUSE 0x07001019 +#define SP11_SOFT_PAUSE_IID 0x0000466b +#define SP11_PARAM_SOFT_PAUSE_PAUSE 0x0800102e +#define SP11_PARAM_SOFT_PAUSE_RESUME 0x0800102f + struct apm_graph_mgmt_cmd { struct apm_module_param_data param_data; uint32_t num_sub_graphs; @@ -1434,11 +1440,28 @@ static int graph_callback(const struct gpr_resp_pkt *data, void *priv, int op) switch (hdr->opcode) { case APM_EVENT_MODULE_TO_CLIENT: + if (data->payload_size < sizeof(*event)) + break; event = data->payload; switch (event->event_id) { case EVENT_ID_SH_MEM_PULL_PUSH_MODE_WATERMARK: client_event = APM_CLIENT_EVENT_WATERMARK_EVENT; - graph->cb(client_event, hdr->token, data->payload, graph->priv); + if (graph->cb) + graph->cb(client_event, hdr->token, data->payload, + graph->priv); + break; + case EVENT_ID_SOFT_PAUSE_PAUSE_COMPLETE: + case EVENT_ID_SOFT_PAUSE_RESUME_COMPLETE: + if (hdr->src_port != SP11_SOFT_PAUSE_IID || + !graph->protection_runtime || + !graph->is_push_pull_mode || !graph->cb) + break; + client_event = event->event_id == + EVENT_ID_SOFT_PAUSE_PAUSE_COMPLETE ? + APM_CLIENT_EVENT_SOFT_PAUSE_COMPLETE : + APM_CLIENT_EVENT_SOFT_RESUME_COMPLETE; + graph->cb(client_event, hdr->token, data->payload, + graph->priv); break; } @@ -1537,6 +1560,56 @@ int q6apm_register_watermark_event(struct q6apm_graph *graph, int water_mark_lev } EXPORT_SYMBOL_GPL(q6apm_register_watermark_event); +static int q6apm_register_module_event(struct q6apm_graph *graph, u32 iid, + u32 event_id) +{ + struct apm_module_register_events *event; + size_t payload_size = ALIGN(sizeof(*event), 8); + void *p; + + struct gpr_pkt *pkt __free(kfree) = + audioreach_alloc_cmd_pkt(payload_size, + APM_CMD_REGISTER_MODULE_EVENTS, 0, + graph->port->id, iid); + if (IS_ERR(pkt)) + return PTR_ERR(pkt); + + p = (u8 *)pkt + GPR_HDR_SIZE + APM_CMD_HDR_SIZE; + memset(p, 0, payload_size); + event = p; + event->module_instance_id = iid; + event->event_id = event_id; + event->is_register = 1; + + return audioreach_graph_send_cmd_sync(graph, pkt, 0); +} + +int q6apm_register_sp11_soft_pause_events(struct q6apm_graph *graph) +{ + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + struct audioreach_module *module; + int ret; + + if (!active) + return -ESHUTDOWN; + if (!graph->protection_runtime || !graph->is_push_pull_mode) + return -EOPNOTSUPP; + + module = q6apm_find_module_by_mid(graph, SP11_MODULE_ID_SOFT_PAUSE); + if (!module || module->instance_id != SP11_SOFT_PAUSE_IID) + return -ENODEV; + + ret = q6apm_register_module_event(graph, module->instance_id, + EVENT_ID_SOFT_PAUSE_PAUSE_COMPLETE); + if (ret) + return ret; + + return q6apm_register_module_event(graph, module->instance_id, + EVENT_ID_SOFT_PAUSE_RESUME_COMPLETE); +} +EXPORT_SYMBOL_GPL(q6apm_register_sp11_soft_pause_events); + int q6apm_push_pull_config(struct q6apm_graph *graph, phys_addr_t bphys, phys_addr_t pphys, uint32_t size) { @@ -2017,6 +2090,39 @@ int q6apm_graph_flush(struct q6apm_graph *graph) } EXPORT_SYMBOL_GPL(q6apm_graph_flush); +int q6apm_graph_sp11_soft_pause(struct q6apm_graph *graph, bool pause) +{ + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + struct apm_module_param_data *param; + struct audioreach_module *module; + size_t payload_size = ALIGN(sizeof(*param), 8); + + if (!active) + return -ESHUTDOWN; + if (!graph->protection_runtime || !graph->is_push_pull_mode) + return -EOPNOTSUPP; + + module = q6apm_find_module_by_mid(graph, SP11_MODULE_ID_SOFT_PAUSE); + if (!module || module->instance_id != SP11_SOFT_PAUSE_IID) + return -EOPNOTSUPP; + + 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); + + param = (void *)pkt + GPR_HDR_SIZE + APM_CMD_HDR_SIZE; + memset(param, 0, payload_size); + param->module_instance_id = module->instance_id; + param->param_id = pause ? SP11_PARAM_SOFT_PAUSE_PAUSE : + SP11_PARAM_SOFT_PAUSE_RESUME; + + return audioreach_graph_send_cmd_sync(graph, pkt, 0); +} +EXPORT_SYMBOL_GPL(q6apm_graph_sp11_soft_pause); + static int q6apm_audio_probe(struct snd_soc_component *component) { return audioreach_tplg_init(component); diff --git a/sound/soc/qcom/qdsp6/q6apm.h b/sound/soc/qcom/qdsp6/q6apm.h index 5ebec2938af39b..f28f9ab9d0b036 100644 --- a/sound/soc/qcom/qdsp6/q6apm.h +++ b/sound/soc/qcom/qdsp6/q6apm.h @@ -43,6 +43,8 @@ #define APM_CLIENT_EVENT_DATA_WRITE_DONE 0x1009 #define APM_CLIENT_EVENT_DATA_READ_DONE 0x100a #define APM_CLIENT_EVENT_WATERMARK_EVENT 0x100b +#define APM_CLIENT_EVENT_SOFT_PAUSE_COMPLETE 0x100c +#define APM_CLIENT_EVENT_SOFT_RESUME_COMPLETE 0x100d #define APM_WRITE_TOKEN_MASK GENMASK(15, 0) #define APM_WRITE_TOKEN_LEN_MASK GENMASK(31, 16) #define APM_WRITE_TOKEN_LEN_SHIFT 16 @@ -190,6 +192,7 @@ 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); +int q6apm_graph_sp11_soft_pause(struct q6apm_graph *graph, bool pause); 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); @@ -256,4 +259,5 @@ int q6apm_push_pull_config(struct q6apm_graph *graph, phys_addr_t bphys, phys_addr_t pphys, uint32_t size); int q6apm_register_watermark_event(struct q6apm_graph *graph, int watermark_bytes, int num_levels); +int q6apm_register_sp11_soft_pause_events(struct q6apm_graph *graph); #endif /* __APM_GRAPH_ */ From 70e3340a63b9c778fba4049db487e1beec3a2b1a Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sun, 23 Aug 2026 12:36:07 +0100 Subject: [PATCH 40/49] ASoC: qcom: x1e80100: restore Denali PA gain range The hardware-qualified Denali protected profile uses the full WSA PA gain range and selects its operating point through UCM. Do not apply the provisional 0 dB PA cap to that profile. Keep the -3 dB digital-volume limit everywhere, and retain the upstream PA cap for every X1E80100 machine without the Denali speaker-feedback guard. Signed-off-by: Leon Silcott --- sound/soc/qcom/x1e80100.c | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/sound/soc/qcom/x1e80100.c b/sound/soc/qcom/x1e80100.c index 74b662296996e2..413d2ff94c2d89 100644 --- a/sound/soc/qcom/x1e80100.c +++ b/sound/soc/qcom/x1e80100.c @@ -45,21 +45,25 @@ static int x1e80100_snd_init(struct snd_soc_pcm_runtime *rtd) switch (cpu_dai->id) { case WSA_CODEC_DMA_RX_0: case WSA_CODEC_DMA_RX_1: - /* - * Set limit of -3 dB on Digital Volume and 0 dB on PA Volume - * to reduce the risk of speaker damage until we have active - * speaker protection in place. - */ + /* Keep the -3 dB digital limit on every X1E80100 card. */ snd_soc_limit_volume(card, "WSA WSA_RX0 Digital Volume", 81); snd_soc_limit_volume(card, "WSA WSA_RX1 Digital Volume", 81); snd_soc_limit_volume(card, "WSA2 WSA_RX0 Digital Volume", 81); snd_soc_limit_volume(card, "WSA2 WSA_RX1 Digital Volume", 81); - snd_soc_limit_volume(card, "SpkrLeft PA Volume", 6); - snd_soc_limit_volume(card, "SpkrRight PA Volume", 6); - snd_soc_limit_volume(card, "WooferLeft PA Volume", 6); - snd_soc_limit_volume(card, "TweeterLeft PA Volume", 6); - snd_soc_limit_volume(card, "WooferRight PA Volume", 6); - snd_soc_limit_volume(card, "TweeterRight PA Volume", 6); + + /* + * Denali's hardware-qualified protected profile uses the full PA + * range and selects its operating point in UCM. Keep the upstream + * 0 dB damage-prevention cap on every unprotected machine. + */ + if (!data->cfg->protected_speaker_feedback) { + snd_soc_limit_volume(card, "SpkrLeft PA Volume", 6); + snd_soc_limit_volume(card, "SpkrRight PA Volume", 6); + snd_soc_limit_volume(card, "WooferLeft PA Volume", 6); + snd_soc_limit_volume(card, "TweeterLeft PA Volume", 6); + 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: From 266b20111927c0c4f25d6f0e28d084b28882ddd9 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sun, 23 Aug 2026 12:33:00 +0100 Subject: [PATCH 41/49] ASoC: q6apm: scope protected pull setup to graph client The qualified integrated SP11 graph sends its pull-ring parameter through the graph client port. Use that route when protection is active so command correlation and ordering remain graph-scoped. Preserve the existing APM service route for generic unprotected push/pull users. Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/audioreach.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/sound/soc/qcom/qdsp6/audioreach.c b/sound/soc/qcom/qdsp6/audioreach.c index e609d115d9931e..58a8228c1b201d 100644 --- a/sound/soc/qcom/qdsp6/audioreach.c +++ b/sound/soc/qcom/qdsp6/audioreach.c @@ -2163,10 +2163,15 @@ int audioreach_setup_push_pull(struct q6apm_graph *graph, phys_addr_t bphys, struct apm_module_param_data *param_data; int payload_size; struct gpr_pkt *pkt __free(kfree) = NULL; + bool protected = q6apm_graph_has_protection(graph); void *p; payload_size = sizeof(*cfg) + APM_MODULE_PARAM_DATA_SIZE; - pkt = audioreach_alloc_apm_cmd_pkt(payload_size, APM_CMD_SET_CFG, 0); + if (protected) + pkt = audioreach_alloc_cmd_pkt(payload_size, APM_CMD_SET_CFG, 0, + graph->port->id, graph->shm_iid); + else + pkt = audioreach_alloc_apm_cmd_pkt(payload_size, APM_CMD_SET_CFG, 0); if (IS_ERR(pkt)) return PTR_ERR(pkt); @@ -2189,6 +2194,9 @@ int audioreach_setup_push_pull(struct q6apm_graph *graph, phys_addr_t bphys, cfg->shared_pos_buf_addr_msw = upper_32_bits(pphys); cfg->pos_buf_mem_map_handle = pos_buf_mem_map_handle; + if (protected) + return audioreach_graph_send_cmd_sync(graph, pkt, 0); + return q6apm_send_cmd_sync(graph->apm, pkt, 0); } EXPORT_SYMBOL_GPL(audioreach_setup_push_pull); From 9f7e7d965351a44921c2e5dd5cf209de46388733 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sun, 23 Aug 2026 12:33:00 +0100 Subject: [PATCH 42/49] ASoC: q6apm: restore SP11 pull position ownership Restore the qualified protected graph's own uncached position page and use its DSP address for the fixed pull ring. Read the live counter with DMA barriers and reject zero, torn, and out-of-range indices before updating the ALSA pointer. Identify this lifecycle only when the protected topology contains pull instance 0x4660 and soft-pause instance 0x466b. Preserve the generic ALSA-owned position buffer for every other push/pull graph, and retain uncertain mappings under the existing fail-closed teardown policy. Use the captured SP11 subgraph order for graph-client run-state commands. Treat a lost soft-pause completion as uncertain so the next teardown or prepare performs a confirmed graph stop instead of claiming that playback resumed. Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/audioreach.c | 2 +- sound/soc/qcom/qdsp6/q6apm-dai.c | 52 +++- sound/soc/qcom/qdsp6/q6apm-lpass-dais.c | 5 +- sound/soc/qcom/qdsp6/q6apm.c | 337 ++++++++++++++++++++++-- sound/soc/qcom/qdsp6/q6apm.h | 13 + 5 files changed, 372 insertions(+), 37 deletions(-) diff --git a/sound/soc/qcom/qdsp6/audioreach.c b/sound/soc/qcom/qdsp6/audioreach.c index 58a8228c1b201d..a4df88ad7ea394 100644 --- a/sound/soc/qcom/qdsp6/audioreach.c +++ b/sound/soc/qcom/qdsp6/audioreach.c @@ -2163,7 +2163,7 @@ int audioreach_setup_push_pull(struct q6apm_graph *graph, phys_addr_t bphys, struct apm_module_param_data *param_data; int payload_size; struct gpr_pkt *pkt __free(kfree) = NULL; - bool protected = q6apm_graph_has_protection(graph); + bool protected = q6apm_graph_is_sp11_pull(graph); void *p; payload_size = sizeof(*cfg) + APM_MODULE_PARAM_DATA_SIZE; diff --git a/sound/soc/qcom/qdsp6/q6apm-dai.c b/sound/soc/qcom/qdsp6/q6apm-dai.c index fdf1263b8bcda9..898c1f79495637 100644 --- a/sound/soc/qcom/qdsp6/q6apm-dai.c +++ b/sound/soc/qcom/qdsp6/q6apm-dai.c @@ -137,7 +137,7 @@ static bool q6apm_denali_pull_runtime(struct snd_pcm_substream *substream, { return substream->stream == SNDRV_PCM_STREAM_PLAYBACK && q6apm_denali_protection_runtime(substream, graph) && - q6apm_is_graph_in_push_pull_mode(graph); + q6apm_graph_is_sp11_pull(graph); } static bool q6apm_denali_protected_graph(struct snd_soc_component *component, @@ -148,6 +148,14 @@ static bool q6apm_denali_protected_graph(struct snd_soc_component *component, q6apm_graph_id_has_protection(component->dev, graph_id); } +static bool q6apm_denali_sp11_pull_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_is_sp11_pull(component->dev, graph_id); +} + static void q6apm_latch_protected_graph(struct snd_soc_component *component, unsigned int graph_id) { @@ -383,7 +391,8 @@ static int q6apm_dai_prepare(struct snd_soc_component *component, dev_err(dev, "Denali pull PCM geometry is invalid\n"); return -EINVAL; } - if (protected_pull && prtd->pull_started) { + if (protected_pull && prtd->pull_started && + prtd->state != Q6APM_STREAM_UNCERTAIN) { prtd->state = Q6APM_STREAM_RUNNING; dev_dbg(dev, "reusing persistent Denali pull graph\n"); return 0; @@ -405,6 +414,11 @@ static int q6apm_dai_prepare(struct snd_soc_component *component, return ret; } prtd->state = Q6APM_STREAM_STOPPED; + if (protected_pull) { + prtd->pull_started = false; + prtd->push_pull_size = 0; + prtd->soft_paused = false; + } } q6apm_free_fragments(prtd->graph, substream->stream); } @@ -532,8 +546,7 @@ static int q6apm_dai_sp11_soft_pause(struct snd_soc_component *component, unsigned long timeout; int ret; - if (!q6apm_graph_has_protection(prtd->graph) || - !q6apm_is_graph_in_push_pull_mode(prtd->graph)) + if (!q6apm_graph_is_sp11_pull(prtd->graph)) return -EOPNOTSUPP; /* Suppress watermark callbacks while trigger() holds the PCM lock. */ @@ -551,6 +564,7 @@ static int q6apm_dai_sp11_soft_pause(struct snd_soc_component *component, prtd->soft_paused = pause; timeout = wait_for_completion_timeout(done, msecs_to_jiffies(SP11_SOFT_PAUSE_TIMEOUT_MS)); if (!timeout) { + prtd->state = Q6APM_STREAM_UNCERTAIN; dev_err(component->dev, "Denali soft-%s completion timed out\n", pause ? "pause" : "resume"); return -ETIMEDOUT; @@ -715,7 +729,7 @@ static int q6apm_dai_open(struct snd_soc_component *component, else prtd->phys = substream->dma_buffer.addr | (pdata->sid << 32); - if (q6apm_is_graph_in_push_pull_mode(prtd->graph)) { + if (q6apm_is_graph_in_push_pull_mode(prtd->graph) && !protected_pull) { void *pos_buffer; prtd->pos_phys = prtd->phys + prtd->mapped_pcm_bytes; @@ -794,6 +808,10 @@ static int q6apm_dai_hw_free(struct snd_soc_component *component, } prtd->state = Q6APM_STREAM_STOPPED; + if (q6apm_denali_pull_runtime(substream, prtd->graph)) { + prtd->pull_started = false; + prtd->push_pull_size = 0; + } return 0; } @@ -813,8 +831,10 @@ static int q6apm_dai_close(struct snd_soc_component *component, ret = -EIO; } else if (protected_pull && prtd->pull_started) { ret = q6apm_graph_stop(prtd->graph); - if (!ret) + if (!ret) { prtd->pull_started = false; + prtd->push_pull_size = 0; + } } else if (prtd->state) { /* only stop graph that is started */ if (!protected || prtd->state == Q6APM_STREAM_RUNNING || @@ -847,6 +867,9 @@ static snd_pcm_uframes_t q6apm_dai_pointer(struct snd_soc_component *component, struct q6apm_dai_rtd *prtd = runtime->private_data; snd_pcm_uframes_t ptr; + if (q6apm_denali_pull_runtime(substream, prtd->graph)) + return q6apm_get_pull_hw_pointer(prtd->graph, runtime); + if (q6apm_is_graph_in_push_pull_mode(prtd->graph)) { int retries = 10; uint32_t index, fc1, fc2; @@ -900,7 +923,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, - bool protected, bool *retain_buffer) + bool sp11_pull, bool protected, + bool *retain_buffer) { struct q6apm_dai_data *pdata; struct device *dev = component->dev; @@ -919,7 +943,7 @@ static int q6apm_dai_memory_map(struct snd_soc_component *component, else phys = substream->dma_buffer.addr | (pdata->sid << 32); - map_size = protected && is_push_pull ? PAGE_SIZE : BUFFER_BYTES_MAX; + map_size = sp11_pull ? PAGE_SIZE : BUFFER_BYTES_MAX; ret = q6apm_map_memory_fixed_region(dev, graph_id, phys, map_size); if (ret < 0) { *retain_buffer = protected && @@ -928,7 +952,7 @@ static int q6apm_dai_memory_map(struct snd_soc_component *component, return ret; } - if (is_push_pull) { + if (is_push_pull && !sp11_pull) { if (pdata->sid < 0) phys = substream->dma_buffer.addr + map_size; else @@ -985,6 +1009,7 @@ static int q6apm_dai_pcm_new(struct snd_soc_component *component, struct snd_soc int graph_id, ret; bool is_push_pull; bool protected; + bool sp11_pull; bool retain_buffer = false; struct snd_pcm_substream *substream = NULL; @@ -1005,9 +1030,11 @@ static int q6apm_dai_pcm_new(struct snd_soc_component *component, struct snd_soc is_push_pull = q6apm_is_graph_in_push_pull_mode_from_id(component->dev, graph_id, substream->stream); - size = protected && is_push_pull ? PAGE_SIZE : BUFFER_BYTES_MAX; + sp11_pull = q6apm_denali_sp11_pull_graph(component, rtd, graph_id) && + substream->stream == SNDRV_PCM_STREAM_PLAYBACK; + size = sp11_pull ? PAGE_SIZE : BUFFER_BYTES_MAX; size += PAGE_SIZE; - if (is_push_pull) + if (is_push_pull && !sp11_pull) size += POS_BUFFER_BYTES; ret = snd_pcm_set_fixed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV, component->dev, size); @@ -1016,7 +1043,8 @@ static int q6apm_dai_pcm_new(struct snd_soc_component *component, struct snd_soc ret = q6apm_dai_memory_map(component, substream, graph_id, is_push_pull, - protected, &retain_buffer); + sp11_pull, protected, + &retain_buffer); if (ret) { if (retain_buffer) q6apm_dai_retain_dma_buffer(component, diff --git a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c index 8599ebc6eb0040..e7c70159f5fff8 100644 --- a/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c +++ b/sound/soc/qcom/qdsp6/q6apm-lpass-dais.c @@ -219,7 +219,7 @@ static int q6apm_lpass_dai_trigger(struct snd_pcm_substream *substream, int cmd, case SNDRV_PCM_TRIGGER_STOP: if (dai_data->is_port_started[dai->id] && q6apm_graph_has_protection(dai_data->graph[dai->id]) && - !q6apm_is_graph_in_push_pull_mode(dai_data->graph[dai->id])) { + !q6apm_graph_is_sp11_pull(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", @@ -247,8 +247,7 @@ static int q6apm_lpass_dai_prepare(struct snd_pcm_substream *substream, struct s return graph_id; if (dai_data->is_port_started[dai->id]) { - if (q6apm_graph_has_protection(dai_data->graph[dai->id]) && - q6apm_is_graph_in_push_pull_mode(dai_data->graph[dai->id])) + if (q6apm_graph_is_sp11_pull(dai_data->graph[dai->id])) return 0; rc = q6apm_graph_stop(dai_data->graph[dai->id]); if (rc < 0) { diff --git a/sound/soc/qcom/qdsp6/q6apm.c b/sound/soc/qcom/qdsp6/q6apm.c index f72ead47655c83..3fa92ac4fd6b24 100644 --- a/sound/soc/qcom/qdsp6/q6apm.c +++ b/sound/soc/qcom/qdsp6/q6apm.c @@ -22,12 +22,15 @@ /* Graph Management */ #define APM_MMAP_TOKEN_MAP_TYPE_OOB BIT(17) +#define APM_MMAP_TOKEN_MAP_TYPE_GRAPH_POS BIT(30) #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) +#define Q6APM_POSITION_MAP_UNCACHED BIT(1) /* Windows DEFAULT soft-pause module in the qualified SP11 graph. */ #define SP11_MODULE_ID_SOFT_PAUSE 0x07001019 +#define SP11_PULL_IID 0x00004660 #define SP11_SOFT_PAUSE_IID 0x0000466b #define SP11_PARAM_SOFT_PAUSE_PAUSE 0x0800102e #define SP11_PARAM_SOFT_PAUSE_RESUME 0x0800102f @@ -46,6 +49,9 @@ static atomic_t q6apm_mmap_token_seq = ATOMIC_INIT(0); static int audioreach_graph_mgmt_cmd(struct audioreach_graph *graph, u32 opcode); +static struct audioreach_module * +__q6apm_find_module_by_mid(struct q6apm *apm, + struct audioreach_graph_info *info, u32 mid); bool q6apm_graph_user_get(struct q6apm_graph *graph) { @@ -212,6 +218,133 @@ static u32 q6apm_next_mmap_token(u32 graph_id, u32 map_type) (sequence & APM_MMAP_TOKEN_OOB_SEQ_MASK); } +static int q6apm_map_position_buffer(struct audioreach_graph *graph) +{ + 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; + u32 token; + int ret; + + if (!graph->dma_dev) + return -ENODEV; + + graph->position_token = (graph->id & APM_MMAP_TOKEN_GID_MASK) | + APM_MMAP_TOKEN_MAP_TYPE_POS_BUF | + APM_MMAP_TOKEN_MAP_TYPE_GRAPH_POS; + token = graph->position_token; + graph->position_size = PAGE_SIZE; + graph->position_virt = + dma_alloc_coherent(graph->dma_dev, graph->position_size, + &graph->position_dma, GFP_KERNEL); + if (!graph->position_virt) + return -ENOMEM; + memset(graph->position_virt, 0, graph->position_size); + graph->position_dsp_addr = q6apm_dsp_addr(graph->dma_dev, graph->position_dma); + + pkt = audioreach_alloc_apm_cmd_pkt(payload_size, opcode, 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 = Q6APM_POSITION_MAP_UNCACHED; + region = payload + sizeof(*cmd); + region->shm_addr_lsw = lower_32_bits(graph->position_dsp_addr); + region->shm_addr_msw = upper_32_bits(graph->position_dsp_addr); + region->mem_size_bytes = graph->position_size; + + graph->position_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->position_mem_map_handle; + map_uncertain = graph->position_map_uncertain; + spin_unlock(&graph->apm->graph_lock); + if (mapped) + return 0; + if (map_uncertain) { + if (!ret) + ret = -EIO; + return ret; + } + if (!ret) + ret = -EIO; + +free_buffer: + dma_free_coherent(graph->dma_dev, graph->position_size, + graph->position_virt, graph->position_dma); + graph->position_virt = NULL; + graph->position_dma = 0; + graph->position_dsp_addr = 0; + graph->position_size = 0; + return ret; +} + +static int q6apm_unmap_position_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; + u32 token = graph->position_token; + bool map_uncertain; + int ret = 0; + + mutex_lock(&graph->position_lock); + spin_lock(&graph->apm->graph_lock); + map_uncertain = graph->position_map_uncertain; + mem_map_handle = graph->position_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) = + audioreach_alloc_apm_cmd_pkt(sizeof(*cmd), opcode, token); + + if (IS_ERR(pkt)) { + ret = PTR_ERR(pkt); + goto unlock; + } + 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->position_mem_map_handle; + spin_unlock(&graph->apm->graph_lock); + if (mem_map_handle) { + ret = -EIO; + goto unlock; + } + } + + if (graph->position_virt) { + dma_free_coherent(graph->dma_dev, graph->position_size, + graph->position_virt, graph->position_dma); + graph->position_virt = NULL; + graph->position_dma = 0; + graph->position_dsp_addr = 0; + graph->position_size = 0; + } + +unlock: + mutex_unlock(&graph->position_lock); + return ret; +} + static int q6apm_map_oob_buffer(struct audioreach_graph *graph, size_t data_size) { struct apm_shared_map_region_payload *region; @@ -422,6 +555,22 @@ int q6apm_send_graph_oob_config(struct q6apm_graph *graph, return __q6apm_send_oob_config(graph->ar_graph, graph, data, size); } +static bool q6apm_info_is_sp11_pull(struct q6apm *apm, + struct audioreach_graph_info *info) +{ + struct audioreach_module *pause; + struct audioreach_module *pull; + + if (audioreach_graph_protection_profile(info) <= 0) + return false; + + pull = __q6apm_find_module_by_mid(apm, info, MODULE_ID_SH_MEM_PULL_MODE); + pause = __q6apm_find_module_by_mid(apm, info, SP11_MODULE_ID_SOFT_PAUSE); + + return pull && pull->instance_id == SP11_PULL_IID && pause && + pause->instance_id == SP11_SOFT_PAUSE_IID; +} + static struct audioreach_graph * q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id, bool enable_protection) @@ -431,8 +580,9 @@ q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id, struct device *dma_dev; bool graph_faulted; bool mode_mismatch; + bool sp11_pull; size_t oob_size; - int id; + int id, position_ret; mutex_lock(&apm->lock); spin_lock(&apm->graph_lock); @@ -462,6 +612,7 @@ q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id, if (!info) return ERR_PTR(-ENODEV); + sp11_pull = enable_protection && q6apm_info_is_sp11_pull(apm, info); graph = kzalloc_obj(*graph); if (!graph) { @@ -475,6 +626,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->position_lock); mutex_init(&graph->protection_lock); graph->graph = audioreach_alloc_graph_pkt(apm, info); @@ -501,6 +653,18 @@ q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id, return ERR_PTR(id); } mutex_unlock(&apm->lock); + if (sp11_pull) { + id = q6apm_map_position_buffer(graph); + if (id) { + if (graph->position_map_uncertain) { + dev_err(apm->dev, + "retaining graph %u after uncertain position map\n", + graph_id); + return ERR_PTR(id); + } + goto remove_graph; + } + } id = q6apm_send_cmd_sync(apm, graph->graph, 0); if (id == -ETIMEDOUT) { @@ -509,8 +673,16 @@ q6apm_get_audioreach_graph(struct q6apm *apm, u32 graph_id, graph_id); return ERR_PTR(id); } - if (id) + if (id) { + position_ret = q6apm_unmap_position_buffer(graph); + if (position_ret) { + dev_err(apm->dev, + "retaining graph %u after position unmap failure (%d)\n", + graph_id, position_ret); + return ERR_PTR(id); + } goto remove_graph; + } id = audioreach_graph_protection_oob_size(info, &oob_size); if (id < 0) { @@ -597,12 +769,17 @@ static int audioreach_graph_mgmt_cmd(struct audioreach_graph *graph, uint32_t op static int audioreach_graph_client_mgmt_cmd(struct q6apm_graph *graph, u32 opcode) { + static const u32 sp11_sub_graph_order[] = { + 0xb0000001, 0xb000007f, 0xb000007e, + }; 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; + bool sp11_pull = q6apm_graph_is_sp11_pull(graph); + int num_sub_graphs = sp11_pull ? ARRAY_SIZE(sp11_sub_graph_order) : + info->num_sub_graphs; int payload_size; int i = 0; u32 dest = APM_MODULE_INSTANCE_ID; @@ -619,9 +796,27 @@ static int audioreach_graph_client_mgmt_cmd(struct q6apm_graph *graph, 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; + if (sp11_pull) { + if (info->num_sub_graphs != ARRAY_SIZE(sp11_sub_graph_order)) + return -EINVAL; + for (i = 0; i < ARRAY_SIZE(sp11_sub_graph_order); i++) { + bool found = false; + + list_for_each_entry(sg, &info->sg_list, node) { + if (sg->sub_graph_id != sp11_sub_graph_order[i]) + continue; + mgmt_cmd->sub_graph_id_list[i] = sg->sub_graph_id; + found = true; + break; + } + if (!found) + return -ENODEV; + } + } else { + /* FullIO emits the generic graph's required run-state order. */ + 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); } @@ -632,6 +827,7 @@ static void q6apm_put_audioreach_graph(struct kref *ref) struct q6apm *apm; int close_ret; int unmap_ret = 0; + int position_unmap_ret = 0; graph = container_of(ref, struct audioreach_graph, refcount); apm = graph->apm; @@ -655,6 +851,7 @@ static void q6apm_put_audioreach_graph(struct kref *ref) graph->oob_transfer_uncertain = false; mutex_unlock(&graph->oob_lock); unmap_ret = q6apm_unmap_oob_buffer(graph); + position_unmap_ret = q6apm_unmap_position_buffer(graph); mutex_lock(&apm->lock); spin_lock(&apm->graph_lock); @@ -663,10 +860,10 @@ static void q6apm_put_audioreach_graph(struct kref *ref) mutex_unlock(&apm->lock); kfree(graph->graph); - if (graph->oob_virt) + if (graph->oob_virt || graph->position_virt) dev_err(apm->dev, - "retaining protected OOB DMA buffer after teardown failure (close %d, unmap %d)\n", - close_ret, unmap_ret); + "retaining protected DMA after teardown failure (close %d, OOB unmap %d, position unmap %d)\n", + close_ret, unmap_ret, position_unmap_ret); else put_device(graph->dma_dev); kfree(graph); @@ -1453,8 +1650,7 @@ static int graph_callback(const struct gpr_resp_pkt *data, void *priv, int op) case EVENT_ID_SOFT_PAUSE_PAUSE_COMPLETE: case EVENT_ID_SOFT_PAUSE_RESUME_COMPLETE: if (hdr->src_port != SP11_SOFT_PAUSE_IID || - !graph->protection_runtime || - !graph->is_push_pull_mode || !graph->cb) + !q6apm_graph_is_sp11_pull(graph) || !graph->cb) break; client_event = event->event_id == EVENT_ID_SOFT_PAUSE_PAUSE_COMPLETE ? @@ -1593,7 +1789,7 @@ int q6apm_register_sp11_soft_pause_events(struct q6apm_graph *graph) if (!active) return -ESHUTDOWN; - if (!graph->protection_runtime || !graph->is_push_pull_mode) + if (!q6apm_graph_is_sp11_pull(graph)) return -EOPNOTSUPP; module = q6apm_find_module_by_mid(graph, SP11_MODULE_ID_SOFT_PAUSE); @@ -1616,13 +1812,25 @@ int q6apm_push_pull_config(struct q6apm_graph *graph, phys_addr_t bphys, struct q6apm_graph *active __free(q6apm_graph_user) = q6apm_graph_user_get(graph) ? graph : NULL; struct audioreach_graph_info *info; + phys_addr_t position_addr = pphys; + u32 position_handle; if (!active) return -ESHUTDOWN; info = graph->info; + position_handle = info->pos_buf_mem_map_handle; + if (q6apm_graph_is_sp11_pull(graph)) { + if (size != 3840 || !graph->ar_graph || + !graph->ar_graph->position_virt || + !graph->ar_graph->position_mem_map_handle) + return -EINVAL; + position_addr = graph->ar_graph->position_dsp_addr; + position_handle = graph->ar_graph->position_mem_map_handle; + } - return audioreach_setup_push_pull(graph, bphys, pphys, info->mem_map_handle, - info->pos_buf_mem_map_handle, size); + return audioreach_setup_push_pull(graph, bphys, position_addr, + info->mem_map_handle, + position_handle, size); } EXPORT_SYMBOL_GPL(q6apm_push_pull_config); @@ -1657,6 +1865,71 @@ bool q6apm_is_graph_in_push_pull_mode(struct q6apm_graph *graph) } EXPORT_SYMBOL_GPL(q6apm_is_graph_in_push_pull_mode); +bool q6apm_graph_is_sp11_pull(struct q6apm_graph *graph) +{ + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + + if (!active || !graph->protection_runtime || + !graph->is_push_pull_mode) + return false; + + return q6apm_info_is_sp11_pull(graph->apm, graph->info); +} +EXPORT_SYMBOL_GPL(q6apm_graph_is_sp11_pull); + +bool q6apm_graph_id_is_sp11_pull(struct device *dev, unsigned int graph_id) +{ + struct q6apm *apm = dev_get_drvdata(dev->parent); + struct audioreach_graph_info *info; + bool sp11_pull; + + if (!apm) + return false; + guard(mutex)(&apm->client_lock); + if (apm->removing) + return false; + mutex_lock(&apm->lock); + info = idr_find(&apm->graph_info_idr, graph_id); + sp11_pull = info && q6apm_info_is_sp11_pull(apm, info); + mutex_unlock(&apm->lock); + + return sp11_pull; +} +EXPORT_SYMBOL_GPL(q6apm_graph_id_is_sp11_pull); + +snd_pcm_uframes_t q6apm_get_pull_hw_pointer(struct q6apm_graph *graph, + struct snd_pcm_runtime *runtime) +{ + struct q6apm_graph *active __free(q6apm_graph_user) = + q6apm_graph_user_get(graph) ? graph : NULL; + struct sh_mem_pull_push_mode_position_buffer *position; + u32 before, after, index; + int retries = 4; + + if (!active || !q6apm_graph_is_sp11_pull(graph) || + !graph->ar_graph || !graph->ar_graph->position_virt) + return 0; + + position = graph->ar_graph->position_virt; + do { + before = READ_ONCE(position->frame_counter); + dma_rmb(); + index = READ_ONCE(position->index); + dma_rmb(); + after = READ_ONCE(position->frame_counter); + } while ((!before || before != after) && --retries); + + if (!before || before != after || + index >= frames_to_bytes(runtime, runtime->buffer_size)) + index = atomic_read(&graph->rx_data.hw_ptr); + else + atomic_set(&graph->rx_data.hw_ptr, index); + + return bytes_to_frames(runtime, index); +} +EXPORT_SYMBOL_GPL(q6apm_get_pull_hw_pointer); + static int q6apm_graph_get_module_iid(struct q6apm_graph *graph, uint32_t mid) { struct audioreach_module *module; @@ -2100,7 +2373,7 @@ int q6apm_graph_sp11_soft_pause(struct q6apm_graph *graph, bool pause) if (!active) return -ESHUTDOWN; - if (!graph->protection_runtime || !graph->is_push_pull_mode) + if (!q6apm_graph_is_sp11_pull(graph)) return -EOPNOTSUPP; module = q6apm_find_module_by_mid(graph, SP11_MODULE_ID_SOFT_PAUSE); @@ -2255,9 +2528,9 @@ 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); - if (ar_graph->oob_virt) { + if (ar_graph->oob_virt || ar_graph->position_virt) { dev_err(apm->dev, - "retaining graph %u OOB DMA until reboot after service removal\n", + "retaining graph %u protected DMA until reboot after service removal\n", ar_graph->id); /* Keep the DMA descriptor and its device reference pinned. */ } else { @@ -2343,7 +2616,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_oob, is_pos_buf; + int graph_id, is_graph_pos, is_oob, is_pos_buf; result = data->payload; @@ -2425,6 +2698,8 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) is_oob = hdr->token & APM_MMAP_TOKEN_MAP_TYPE_OOB; is_pos_buf = hdr->token & APM_MMAP_TOKEN_MAP_TYPE_POS_BUF; + is_graph_pos = !is_oob && + (hdr->token & APM_MMAP_TOKEN_MAP_TYPE_GRAPH_POS); spin_lock(&apm->result_lock); expected = __q6apm_cmd_response_expected(apm, hdr, result->opcode, @@ -2437,6 +2712,14 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) else graph = NULL; spin_unlock(&apm->graph_lock); + } else if (is_graph_pos && expected) { + spin_lock(&apm->graph_lock); + graph = idr_find(&apm->graph_idr, graph_id); + if (graph && graph->position_token == hdr->token) + graph->position_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) @@ -2452,7 +2735,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 ((!is_oob && !info) || (is_oob && !graph)) + if ((is_oob || is_graph_pos) ? !graph : !info) dev_err(dev, "no mapping for 0x%08x response token 0x%08x\n", result->opcode, hdr->token); @@ -2467,6 +2750,8 @@ 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; + is_graph_pos = !is_oob && + (hdr->token & APM_MMAP_TOKEN_MAP_TYPE_GRAPH_POS); spin_lock(&apm->result_lock); expected = __q6apm_cmd_response_expected(apm, hdr, hdr->opcode, @@ -2481,10 +2766,20 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) graph = NULL; } spin_unlock(&apm->graph_lock); + } else if (is_graph_pos && expected) { + spin_lock(&apm->graph_lock); + graph = idr_find(&apm->graph_idr, graph_id); + if (graph && graph->position_token == hdr->token) { + graph->position_mem_map_handle = rsp->mem_map_handle; + graph->position_map_uncertain = false; + } else { + graph = NULL; + } + spin_unlock(&apm->graph_lock); } else if (expected) { info = idr_find(&apm->graph_info_idr, graph_id); } - if (!is_oob && info) { + if (!is_oob && !is_graph_pos && info) { if (is_pos_buf) info->pos_buf_mem_map_handle = rsp->mem_map_handle; else @@ -2499,7 +2794,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 (expected && ((!is_oob && !info) || (is_oob && !graph))) { + if (expected && ((is_oob || is_graph_pos) ? !graph : !info)) { dev_err(dev, "no graph for 0x%08x response token 0x%08x\n", hdr->opcode, hdr->token); diff --git a/sound/soc/qcom/qdsp6/q6apm.h b/sound/soc/qcom/qdsp6/q6apm.h index f28f9ab9d0b036..0f89bf59b568e8 100644 --- a/sound/soc/qcom/qdsp6/q6apm.h +++ b/sound/soc/qcom/qdsp6/q6apm.h @@ -133,6 +133,15 @@ struct audioreach_graph { bool dma_quarantined; bool oob_map_uncertain; bool oob_transfer_uncertain; + void *position_virt; + dma_addr_t position_dma; + phys_addr_t position_dsp_addr; + size_t position_size; + u32 position_mem_map_handle; + u32 position_token; + bool position_map_uncertain; + /* Serialize teardown of the DSP-owned position mapping. */ + struct mutex position_lock; /* Cached Graph data */ void *graph; struct kref refcount; @@ -255,6 +264,10 @@ int q6apm_set_real_module_id(struct device *dev, struct q6apm_graph *graph, uint int q6apm_get_hw_pointer(struct q6apm_graph *graph, int dir); bool q6apm_is_graph_in_push_pull_mode(struct q6apm_graph *graph); bool q6apm_is_graph_in_push_pull_mode_from_id(struct device *dev, unsigned int graph_id, int dir); +bool q6apm_graph_is_sp11_pull(struct q6apm_graph *graph); +bool q6apm_graph_id_is_sp11_pull(struct device *dev, unsigned int graph_id); +snd_pcm_uframes_t q6apm_get_pull_hw_pointer(struct q6apm_graph *graph, + struct snd_pcm_runtime *runtime); int q6apm_push_pull_config(struct q6apm_graph *graph, phys_addr_t bphys, phys_addr_t pphys, uint32_t size); From ca7606e3fcc7f12351792f31d614b81f1abd71e1 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sun, 23 Aug 2026 12:33:00 +0100 Subject: [PATCH 43/49] ASoC: q6apm: preserve SP11 pull run-state order The hardware-qualified FullIO pull transaction proceeds from its protected configuration directly to the ordered graph-client START. Do not insert the generic APM GRAPH_PREPARE command for the exact SP11 topology. Advance the existing fail-closed prepared state locally so start still requires successful protection configuration or confirmed bypass. All other protected and generic graphs retain the existing PREPARE command. Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/q6apm.c | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/sound/soc/qcom/qdsp6/q6apm.c b/sound/soc/qcom/qdsp6/q6apm.c index 3fa92ac4fd6b24..3d46093b728e91 100644 --- a/sound/soc/qcom/qdsp6/q6apm.c +++ b/sound/soc/qcom/qdsp6/q6apm.c @@ -2144,11 +2144,22 @@ int q6apm_graph_prepare(struct q6apm_graph *graph) q6apm_graph_user_get(graph) ? graph : NULL; struct audioreach_graph *ar_graph; bool protected = q6apm_graph_has_protection(graph); - int ret; + int ret = 0; if (!active) return -ESHUTDOWN; ar_graph = graph->ar_graph; + if (q6apm_graph_is_sp11_pull(graph)) { + /* The qualified FullIO pull sequence advances directly to START. */ + mutex_lock(&ar_graph->protection_lock); + if (ar_graph->protection_faulted) + ret = -EIO; + else + ar_graph->prepared = true; + ar_graph->prepare_uncertain = false; + mutex_unlock(&ar_graph->protection_lock); + return ret; + } ret = audioreach_graph_mgmt_cmd(ar_graph, APM_CMD_GRAPH_PREPARE); if (protected) { mutex_lock(&ar_graph->protection_lock); From c775c70adbaa020947449a0acb172845db9c6663 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sun, 23 Aug 2026 12:33:00 +0100 Subject: [PATCH 44/49] ASoC: q6apm: preserve SP11 pull media order The qualified integrated transaction configures the pull endpoint media format before walking the protected PCM converter and MFC chain. Restore that order for the exact SP11 topology. Keep the existing PCM-then-shared-memory order for every ordinary and non-SP11 graph. Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/q6apm-dai.c | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/sound/soc/qcom/qdsp6/q6apm-dai.c b/sound/soc/qcom/qdsp6/q6apm-dai.c index 898c1f79495637..00e7b23751a14b 100644 --- a/sound/soc/qcom/qdsp6/q6apm-dai.c +++ b/sound/soc/qcom/qdsp6/q6apm-dai.c @@ -462,6 +462,14 @@ static int q6apm_dai_prepare(struct snd_soc_component *component, } + if (protected_pull) { + ret = q6apm_graph_media_format_shmem(prtd->graph, &cfg); + if (ret < 0) { + dev_err(dev, "Failed to set pull media format %d\n", ret); + return ret; + } + } + ret = q6apm_graph_media_format_pcm(prtd->graph, &cfg); if (ret < 0) { dev_err(dev, "%s: CMD Format block failed\n", __func__); @@ -469,7 +477,8 @@ static int q6apm_dai_prepare(struct snd_soc_component *component, } /* rate and channels are sent to audio driver */ - ret = q6apm_graph_media_format_shmem(prtd->graph, &cfg); + ret = protected_pull ? 0 : + q6apm_graph_media_format_shmem(prtd->graph, &cfg); if (ret < 0) { dev_err(dev, "Failed to set media format %d\n", ret); return ret; From 38b51ffed6992a618cc3a5c00bd203fcf6f637ca Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sun, 23 Aug 2026 12:33:00 +0100 Subject: [PATCH 45/49] ASoC: q6apm: match SP11 control routing Match the qualified position-map token and send the soft-pause parameter over the APM service port used by the captured SP11 transaction. The exact protected-topology predicate still prevents either path from serving another graph. Keep pull-ring setup and event registration on the graph client, matching their qualified routes and preserving graph-scoped command correlation. Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/q6apm.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sound/soc/qcom/qdsp6/q6apm.c b/sound/soc/qcom/qdsp6/q6apm.c index 3d46093b728e91..80f48572b6d8cc 100644 --- a/sound/soc/qcom/qdsp6/q6apm.c +++ b/sound/soc/qcom/qdsp6/q6apm.c @@ -235,7 +235,6 @@ static int q6apm_map_position_buffer(struct audioreach_graph *graph) return -ENODEV; graph->position_token = (graph->id & APM_MMAP_TOKEN_GID_MASK) | - APM_MMAP_TOKEN_MAP_TYPE_POS_BUF | APM_MMAP_TOKEN_MAP_TYPE_GRAPH_POS; token = graph->position_token; graph->position_size = PAGE_SIZE; @@ -2392,8 +2391,7 @@ int q6apm_graph_sp11_soft_pause(struct q6apm_graph *graph, bool pause) return -EOPNOTSUPP; struct gpr_pkt *pkt __free(kfree) = - audioreach_alloc_cmd_pkt(payload_size, APM_CMD_SET_CFG, 0, - graph->port->id, module->instance_id); + audioreach_alloc_apm_cmd_pkt(payload_size, APM_CMD_SET_CFG, 0); if (IS_ERR(pkt)) return PTR_ERR(pkt); @@ -2403,7 +2401,7 @@ int q6apm_graph_sp11_soft_pause(struct q6apm_graph *graph, bool pause) param->param_id = pause ? SP11_PARAM_SOFT_PAUSE_PAUSE : SP11_PARAM_SOFT_PAUSE_RESUME; - return audioreach_graph_send_cmd_sync(graph, pkt, 0); + return q6apm_send_cmd_sync(graph->apm, pkt, 0); } EXPORT_SYMBOL_GPL(q6apm_graph_sp11_soft_pause); From b2be828a7cebdc5a8ff9a5e290e6b277f7336201 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Wed, 2 Sep 2026 22:12:37 +0100 Subject: [PATCH 46/49] ASoC: q6apm: resolve protected position map errors Clear the retained-map uncertainty only when the DSP definitively rejects the matching graph-owned position mapping. This lets the synchronous failure path release coherent DMA without treating a confirmed NACK like an unknown DSP outcome. Reject a second position allocation on the same graph as a defensive backstop against overwriting retained mapping state. Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/q6apm.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/sound/soc/qcom/qdsp6/q6apm.c b/sound/soc/qcom/qdsp6/q6apm.c index 80f48572b6d8cc..205183ad24d9a6 100644 --- a/sound/soc/qcom/qdsp6/q6apm.c +++ b/sound/soc/qcom/qdsp6/q6apm.c @@ -233,6 +233,8 @@ static int q6apm_map_position_buffer(struct audioreach_graph *graph) if (!graph->dma_dev) return -ENODEV; + if (graph->position_virt) + return -EBUSY; graph->position_token = (graph->id & APM_MMAP_TOKEN_GID_MASK) | APM_MMAP_TOKEN_MAP_TYPE_GRAPH_POS; @@ -2652,13 +2654,18 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) expected = __q6apm_cmd_response_expected(apm, hdr, result->opcode, result->status, true); - if (expected && result->status && - (hdr->token & APM_MMAP_TOKEN_MAP_TYPE_OOB)) { + if (expected && result->status) { graph_id = hdr->token & APM_MMAP_TOKEN_GID_MASK; + is_oob = hdr->token & APM_MMAP_TOKEN_MAP_TYPE_OOB; + is_graph_pos = !is_oob && + (hdr->token & APM_MMAP_TOKEN_MAP_TYPE_GRAPH_POS); spin_lock(&apm->graph_lock); graph = idr_find(&apm->graph_idr, graph_id); - if (graph && graph->oob_token == hdr->token) + if (graph && is_oob && graph->oob_token == hdr->token) graph->oob_map_uncertain = false; + else if (graph && is_graph_pos && + graph->position_token == hdr->token) + graph->position_map_uncertain = false; spin_unlock(&apm->graph_lock); } if (expected) { From 88d419ac7adb3f6ef9d380151818fe04112203e3 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Wed, 2 Sep 2026 22:12:38 +0100 Subject: [PATCH 47/49] ASoC: q6apm: resume protected pull before prepare reuse A persistent Denali pull graph may reach prepare while it is still DSP soft-paused. Resume it and require the matching completion before reporting the reused graph as running. Keep the stream uncertain on a missing completion so a later prepare cannot silently reuse a potentially paused graph. Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/q6apm-dai.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/sound/soc/qcom/qdsp6/q6apm-dai.c b/sound/soc/qcom/qdsp6/q6apm-dai.c index 00e7b23751a14b..1a90a8382e7709 100644 --- a/sound/soc/qcom/qdsp6/q6apm-dai.c +++ b/sound/soc/qcom/qdsp6/q6apm-dai.c @@ -112,6 +112,10 @@ struct q6apm_dai_data { DECLARE_BITMAP(protected_graphs, APM_PORT_MAX); }; +static int q6apm_dai_sp11_soft_pause(struct snd_soc_component *component, + struct q6apm_dai_rtd *prtd, + bool pause); + static bool q6apm_denali_rtd(const struct snd_soc_pcm_runtime *rtd) { return rtd && rtd->card && @@ -393,6 +397,12 @@ static int q6apm_dai_prepare(struct snd_soc_component *component, } if (protected_pull && prtd->pull_started && prtd->state != Q6APM_STREAM_UNCERTAIN) { + if (prtd->soft_paused) { + ret = q6apm_dai_sp11_soft_pause(component, prtd, false); + if (ret) + return ret; + prtd->soft_paused = false; + } prtd->state = Q6APM_STREAM_RUNNING; dev_dbg(dev, "reusing persistent Denali pull graph\n"); return 0; From 7353bf6721d33348c702c4e460785ec02d1158c3 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Wed, 2 Sep 2026 22:17:34 +0100 Subject: [PATCH 48/49] ASoC: q6apm: reserve the protected position token The graph-owned position mapping uses the qualified fixed BIT(30) token. Reserve that sequence value from generated mapping tokens and classify graph-position responses by the complete non-graph-id token class. This prevents a generic fixed-region mapping from becoming indistinguishable after the sequence counter reaches the reserved value while preserving the qualified DSP transaction. Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/q6apm.c | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/sound/soc/qcom/qdsp6/q6apm.c b/sound/soc/qcom/qdsp6/q6apm.c index 205183ad24d9a6..8b998bdaa5568c 100644 --- a/sound/soc/qcom/qdsp6/q6apm.c +++ b/sound/soc/qcom/qdsp6/q6apm.c @@ -53,6 +53,12 @@ static struct audioreach_module * __q6apm_find_module_by_mid(struct q6apm *apm, struct audioreach_graph_info *info, u32 mid); +static bool q6apm_is_graph_position_token(u32 token) +{ + return (token & ~APM_MMAP_TOKEN_GID_MASK) == + APM_MMAP_TOKEN_MAP_TYPE_GRAPH_POS; +} + bool q6apm_graph_user_get(struct q6apm_graph *graph) { unsigned long flags; @@ -211,11 +217,14 @@ 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_OOB_SEQ_SHIFT; + do { + sequence = (u32)atomic_inc_return(&q6apm_mmap_token_seq) << + APM_MMAP_TOKEN_OOB_SEQ_SHIFT; + sequence &= APM_MMAP_TOKEN_OOB_SEQ_MASK; + } while (sequence == APM_MMAP_TOKEN_MAP_TYPE_GRAPH_POS); return (graph_id & APM_MMAP_TOKEN_GID_MASK) | map_type | - (sequence & APM_MMAP_TOKEN_OOB_SEQ_MASK); + sequence; } static int q6apm_map_position_buffer(struct audioreach_graph *graph) @@ -2657,8 +2666,7 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) if (expected && result->status) { graph_id = hdr->token & APM_MMAP_TOKEN_GID_MASK; is_oob = hdr->token & APM_MMAP_TOKEN_MAP_TYPE_OOB; - is_graph_pos = !is_oob && - (hdr->token & APM_MMAP_TOKEN_MAP_TYPE_GRAPH_POS); + is_graph_pos = q6apm_is_graph_position_token(hdr->token); spin_lock(&apm->graph_lock); graph = idr_find(&apm->graph_idr, graph_id); if (graph && is_oob && graph->oob_token == hdr->token) @@ -2714,8 +2722,7 @@ static int apm_callback(const struct gpr_resp_pkt *data, void *priv, int op) is_oob = hdr->token & APM_MMAP_TOKEN_MAP_TYPE_OOB; is_pos_buf = hdr->token & APM_MMAP_TOKEN_MAP_TYPE_POS_BUF; - is_graph_pos = !is_oob && - (hdr->token & APM_MMAP_TOKEN_MAP_TYPE_GRAPH_POS); + is_graph_pos = q6apm_is_graph_position_token(hdr->token); spin_lock(&apm->result_lock); expected = __q6apm_cmd_response_expected(apm, hdr, result->opcode, @@ -2766,8 +2773,7 @@ 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; - is_graph_pos = !is_oob && - (hdr->token & APM_MMAP_TOKEN_MAP_TYPE_GRAPH_POS); + is_graph_pos = q6apm_is_graph_position_token(hdr->token); spin_lock(&apm->result_lock); expected = __q6apm_cmd_response_expected(apm, hdr, hdr->opcode, From 08249986a3f4efd7e0826c29e830601bfde928ed Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Wed, 2 Sep 2026 22:19:57 +0100 Subject: [PATCH 49/49] ASoC: q6apm: hard-stop uncertain protected pull Do not let a later STOP overwrite the fail-closed state left by a missing soft-pause completion. Confirm a hard graph stop first, then clear persistent pull state so the next prepare performs the complete setup. Also clear the soft-pause marker after hw_free confirms the same recovery, preventing stale pause state from surviving into a rebuilt graph. Signed-off-by: Leon Silcott --- sound/soc/qcom/qdsp6/q6apm-dai.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/sound/soc/qcom/qdsp6/q6apm-dai.c b/sound/soc/qcom/qdsp6/q6apm-dai.c index 1a90a8382e7709..58776e221fd754 100644 --- a/sound/soc/qcom/qdsp6/q6apm-dai.c +++ b/sound/soc/qcom/qdsp6/q6apm-dai.c @@ -620,7 +620,14 @@ static int q6apm_dai_trigger(struct snd_soc_component *component, break; case SNDRV_PCM_TRIGGER_STOP: if (protected_pull) { - if (prtd->soft_paused) { + if (prtd->state == Q6APM_STREAM_UNCERTAIN) { + ret = q6apm_graph_stop(prtd->graph); + if (ret) + break; + prtd->pull_started = false; + prtd->push_pull_size = 0; + prtd->soft_paused = false; + } else if (prtd->soft_paused) { ret = q6apm_dai_sp11_soft_pause(component, prtd, false); if (ret) break; @@ -830,6 +837,7 @@ static int q6apm_dai_hw_free(struct snd_soc_component *component, if (q6apm_denali_pull_runtime(substream, prtd->graph)) { prtd->pull_started = false; prtd->push_pull_size = 0; + prtd->soft_paused = false; } return 0; }