[PATCH v3 3/3] ASoC: qcom: x1e80100: Allow userspace WSA RX chmap override
From: Abel Vesa
Date: Thu Nov 05 2026 - 11:16:09 EST
The WSA RX slot mapping is currently computed entirely from the channel
count in .prepare() and pushed unconditionally via
snd_soc_dai_set_channel_map(). Userspace has no way to express a
different channel map, so any map defined in UCM is ignored.
Add writable "WSA Playback Channel Map" and "WSA2 Playback Channel Map"
controls on the WSA RX backend PCMs, using the standard ALSA channel map
TLV. Store the userspace-selected SNDRV_CHMAP_* map per WSA backend,
translate it to Q6 PCM_CHANNEL_* slots at .prepare() time, and keep the
previous default mapping when userspace has not provided a complete map.
The controls are added from the card late_probe callback. The backend
PCMs are only created after all DAI links are initialized, and adding
controls from the stream startup callback would take controls_rwsem
under dapm_mutex, deadlocking against DAPM mixer writes.
Allow up to six WSA channels for Glymur.
Signed-off-by: Abel Vesa <abel.vesa@xxxxxxxxxxxxxxxx>
---
sound/soc/qcom/x1e80100.c | 193 +++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 189 insertions(+), 4 deletions(-)
diff --git a/sound/soc/qcom/x1e80100.c b/sound/soc/qcom/x1e80100.c
index c81df41ace88..6a91b3581c75 100644
--- a/sound/soc/qcom/x1e80100.c
+++ b/sound/soc/qcom/x1e80100.c
@@ -15,14 +15,117 @@
#include "qdsp6/q6dsp-common.h"
#include "sdw.h"
+#define X1E80100_WSA_MAX_CHANNELS 6
+#define X1E80100_WSA_PORTS 2
+
struct x1e80100_snd_data {
bool stream_prepared[AFE_PORT_MAX];
struct snd_soc_card *card;
struct snd_soc_jack jack;
struct snd_soc_jack dp_jack[8];
bool jack_setup;
+ /* Userspace-selected SNDRV_CHMAP_* positions, 0 means unset */
+ unsigned int wsa_chmap[X1E80100_WSA_PORTS][X1E80100_WSA_MAX_CHANNELS];
};
+static int x1e80100_wsa_port_index(unsigned int dai_id)
+{
+ switch (dai_id) {
+ case WSA_CODEC_DMA_RX_0:
+ return 0;
+ case WSA_CODEC_DMA_RX_1:
+ return 1;
+ default:
+ return -EINVAL;
+ }
+}
+
+static unsigned int *
+x1e80100_wsa_chmap_from_kcontrol(struct snd_kcontrol *kcontrol)
+{
+ struct snd_pcm_chmap *info = snd_kcontrol_chip(kcontrol);
+ struct snd_soc_pcm_runtime *rtd = info->pcm->private_data;
+ 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);
+ int idx = x1e80100_wsa_port_index(cpu_dai->id);
+
+ if (idx < 0)
+ return NULL;
+
+ return data->wsa_chmap[idx];
+}
+
+static int x1e80100_wsa_chmap_ctl_get(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ unsigned int *map = x1e80100_wsa_chmap_from_kcontrol(kcontrol);
+ int i;
+
+ if (!map)
+ return -EINVAL;
+
+ for (i = 0; i < X1E80100_WSA_MAX_CHANNELS; i++)
+ ucontrol->value.integer.value[i] = map[i];
+
+ return 0;
+}
+
+static int x1e80100_wsa_chmap_ctl_put(struct snd_kcontrol *kcontrol,
+ struct snd_ctl_elem_value *ucontrol)
+{
+ unsigned int *map = x1e80100_wsa_chmap_from_kcontrol(kcontrol);
+ bool changed = false;
+ int i;
+
+ if (!map)
+ return -EINVAL;
+
+ for (i = 0; i < X1E80100_WSA_MAX_CHANNELS; i++) {
+ unsigned int pos = ucontrol->value.integer.value[i];
+
+ if (pos && qcom_snd_chmap_to_q6(pos) < 0)
+ return -EINVAL;
+
+ if (map[i] != pos)
+ changed = true;
+ }
+
+ if (!changed)
+ return 0;
+
+ for (i = 0; i < X1E80100_WSA_MAX_CHANNELS; i++)
+ map[i] = ucontrol->value.integer.value[i];
+
+ return 1;
+}
+
+static int x1e80100_add_wsa_chmap_ctls(struct snd_soc_pcm_runtime *rtd,
+ const char *name)
+{
+ struct snd_pcm_chmap *info;
+ unsigned int i;
+ int ret;
+
+ ret = snd_pcm_add_named_chmap_ctls(rtd->pcm, SNDRV_PCM_STREAM_PLAYBACK,
+ name, snd_pcm_std_chmaps,
+ X1E80100_WSA_MAX_CHANNELS, 0, &info);
+ if (ret)
+ return ret;
+
+ /*
+ * The core registers a query-only control. Make it writable so that
+ * userspace (UCM) can select the WSA channel map. The card is not yet
+ * registered at this point, so the control is not visible to
+ * userspace while being modified.
+ */
+ for (i = 0; i < info->kctl->count; i++)
+ info->kctl->vd[i].access |= SNDRV_CTL_ELEM_ACCESS_WRITE;
+ info->kctl->get = x1e80100_wsa_chmap_ctl_get;
+ info->kctl->put = x1e80100_wsa_chmap_ctl_put;
+
+ return 0;
+}
+
static int x1e80100_snd_init(struct snd_soc_pcm_runtime *rtd)
{
struct x1e80100_snd_data *data = snd_soc_card_get_drvdata(rtd->card);
@@ -92,9 +195,11 @@ static int x1e80100_be_hw_params_fixup(struct snd_soc_pcm_runtime *rtd,
return 0;
}
-static int x1e80100_snd_hw_map_channels(unsigned int *ch_map, int num)
+/* Default WSA RX slot mapping when userspace has not provided a channel map */
+static int x1e80100_snd_default_wsa_slots(unsigned int *ch_map,
+ unsigned int channels)
{
- switch (num) {
+ switch (channels) {
case 1:
ch_map[0] = PCM_CHANNEL_FC;
break;
@@ -113,6 +218,14 @@ static int x1e80100_snd_hw_map_channels(unsigned int *ch_map, int num)
ch_map[2] = PCM_CHANNEL_FR;
ch_map[3] = PCM_CHANNEL_RB;
break;
+ case 6:
+ ch_map[0] = PCM_CHANNEL_FL;
+ ch_map[1] = PCM_CHANNEL_FR;
+ ch_map[2] = PCM_CHANNEL_LB;
+ ch_map[3] = PCM_CHANNEL_RB;
+ ch_map[4] = PCM_CHANNEL_FC;
+ ch_map[5] = PCM_CHANNEL_LFE;
+ break;
default:
return -EINVAL;
}
@@ -120,19 +233,56 @@ static int x1e80100_snd_hw_map_channels(unsigned int *ch_map, int num)
return 0;
}
+static int x1e80100_snd_build_wsa_slots(struct x1e80100_snd_data *data,
+ unsigned int dai_id,
+ unsigned int channels,
+ unsigned int *rx_slot)
+{
+ unsigned int i, set = 0;
+ unsigned int *user;
+ int idx;
+
+ idx = x1e80100_wsa_port_index(dai_id);
+ if (idx < 0)
+ return idx;
+
+ if (channels > X1E80100_WSA_MAX_CHANNELS)
+ return -EINVAL;
+
+ user = data->wsa_chmap[idx];
+ for (i = 0; i < channels; i++)
+ if (user[i])
+ set++;
+
+ /* Fall back to the default map unless userspace set all channels */
+ if (set != channels)
+ return x1e80100_snd_default_wsa_slots(rx_slot, channels);
+
+ for (i = 0; i < channels; i++) {
+ int q6 = qcom_snd_chmap_to_q6(user[i]);
+
+ if (q6 < 0)
+ return q6;
+ rx_slot[i] = q6;
+ }
+
+ return 0;
+}
+
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);
unsigned int channels = substream->runtime->channels;
- unsigned int rx_slot[4];
+ unsigned int rx_slot[X1E80100_WSA_MAX_CHANNELS];
int ret;
switch (cpu_dai->id) {
case WSA_CODEC_DMA_RX_0:
case WSA_CODEC_DMA_RX_1:
- ret = x1e80100_snd_hw_map_channels(rx_slot, channels);
+ ret = x1e80100_snd_build_wsa_slots(data, cpu_dai->id,
+ channels, rx_slot);
if (ret)
return ret;
@@ -164,6 +314,40 @@ static const struct snd_soc_ops x1e80100_be_ops = {
.prepare = x1e80100_snd_prepare,
};
+/*
+ * The WSA channel map controls are added here rather than in the DAI link
+ * init or startup callbacks: the backend PCMs are created only after all
+ * links are initialized, and adding controls from startup would take
+ * controls_rwsem under dapm_mutex, deadlocking against DAPM mixer writes.
+ */
+static int x1e80100_late_probe(struct snd_soc_card *card)
+{
+ struct snd_soc_pcm_runtime *rtd;
+ int ret;
+
+ for_each_card_rtds(card, rtd) {
+ struct snd_soc_dai *cpu_dai = snd_soc_rtd_to_cpu(rtd, 0);
+ const char *name;
+
+ switch (cpu_dai->id) {
+ case WSA_CODEC_DMA_RX_0:
+ name = "WSA Playback Channel Map";
+ break;
+ case WSA_CODEC_DMA_RX_1:
+ name = "WSA2 Playback Channel Map";
+ break;
+ default:
+ continue;
+ }
+
+ ret = x1e80100_add_wsa_chmap_ctls(rtd, name);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
static void x1e80100_add_be_ops(struct snd_soc_card *card)
{
struct snd_soc_dai_link *link;
@@ -203,6 +387,7 @@ static int x1e80100_platform_probe(struct platform_device *pdev)
return ret;
card->driver_name = of_device_get_match_data(dev);
+ card->late_probe = x1e80100_late_probe;
x1e80100_add_be_ops(card);
return devm_snd_soc_register_card(dev, card);
--
2.54.0