ALSA: hda - hdmi: Allow HDA patches to customize more operations

Upcoming AMD multichannel support requires many customized operations
(channel mapping, ELD, HBR) but can otherwise share most of its code
with the generic patch.

Add a local struct hdmi_ops containing customizable HDMI-specific
callbacks and move the current code to those callbacks. Functionality is
unaltered.

Signed-off-by: Anssi Hannula <anssi.hannula@iki.fi>
Signed-off-by: Takashi Iwai <tiwai@suse.de>
diff --git a/sound/pci/hda/patch_hdmi.c b/sound/pci/hda/patch_hdmi.c
index 0f9b103..acc00fe 100644
--- a/sound/pci/hda/patch_hdmi.c
+++ b/sound/pci/hda/patch_hdmi.c
@@ -82,6 +82,39 @@
 #endif
 };
 
+struct cea_channel_speaker_allocation;
+
+/* operations used by generic code that can be overridden by patches */
+struct hdmi_ops {
+	int (*pin_get_eld)(struct hda_codec *codec, hda_nid_t pin_nid,
+			   unsigned char *buf, int *eld_size);
+
+	/* get and set channel assigned to each HDMI ASP (audio sample packet) slot */
+	int (*pin_get_slot_channel)(struct hda_codec *codec, hda_nid_t pin_nid,
+				    int asp_slot);
+	int (*pin_set_slot_channel)(struct hda_codec *codec, hda_nid_t pin_nid,
+				    int asp_slot, int channel);
+
+	void (*pin_setup_infoframe)(struct hda_codec *codec, hda_nid_t pin_nid,
+				    int ca, int active_channels, int conn_type);
+
+	/* enable/disable HBR (HD passthrough) */
+	int (*pin_hbr_setup)(struct hda_codec *codec, hda_nid_t pin_nid, bool hbr);
+
+	int (*setup_stream)(struct hda_codec *codec, hda_nid_t cvt_nid,
+			    hda_nid_t pin_nid, u32 stream_tag, int format);
+
+	/* Helpers for producing the channel map TLVs. These can be overridden
+	 * for devices that have non-standard mapping requirements. */
+	int (*chmap_cea_alloc_validate_get_type)(struct cea_channel_speaker_allocation *cap,
+						 int channels);
+	void (*cea_alloc_to_tlv_chmap)(struct cea_channel_speaker_allocation *cap,
+				       unsigned int *chmap, int channels);
+
+	/* check that the user-given chmap is supported */
+	int (*chmap_validate)(int ca, int channels, unsigned char *chmap);
+};
+
 struct hdmi_spec {
 	int num_cvts;
 	struct snd_array cvts; /* struct hdmi_spec_per_cvt */
@@ -93,6 +126,7 @@
 	unsigned int channels_max; /* max over all cvts */
 
 	struct hdmi_eld temp_eld;
+	struct hdmi_ops ops;
 	/*
 	 * Non-generic ATI/NVIDIA specific
 	 */
@@ -648,24 +682,24 @@
 				       hda_nid_t pin_nid)
 {
 #ifdef CONFIG_SND_DEBUG_VERBOSE
+	struct hdmi_spec *spec = codec->spec;
 	int i;
-	int slot;
+	int channel;
 
 	for (i = 0; i < 8; i++) {
-		slot = snd_hda_codec_read(codec, pin_nid, 0,
-						AC_VERB_GET_HDMI_CHAN_SLOT, i);
+		channel = spec->ops.pin_get_slot_channel(codec, pin_nid, i);
 		printk(KERN_DEBUG "HDMI: ASP channel %d => slot %d\n",
-						slot >> 4, slot & 0xf);
+						channel, i);
 	}
 #endif
 }
 
-
 static void hdmi_std_setup_channel_mapping(struct hda_codec *codec,
 				       hda_nid_t pin_nid,
 				       bool non_pcm,
 				       int ca)
 {
+	struct hdmi_spec *spec = codec->spec;
 	struct cea_channel_speaker_allocation *ch_alloc;
 	int i;
 	int err;
@@ -698,9 +732,10 @@
 	}
 
 	for (i = 0; i < 8; i++) {
-		err = snd_hda_codec_write(codec, pin_nid, 0,
-					  AC_VERB_SET_HDMI_CHAN_SLOT,
-					  non_pcm ? non_pcm_mapping[i] : hdmi_channel_mapping[ca][i]);
+		int slotsetup = non_pcm ? non_pcm_mapping[i] : hdmi_channel_mapping[ca][i];
+		int hdmi_slot = slotsetup & 0x0f;
+		int channel = (slotsetup & 0xf0) >> 4;
+		err = spec->ops.pin_set_slot_channel(codec, pin_nid, hdmi_slot, channel);
 		if (err) {
 			snd_printdd(KERN_NOTICE
 				    "HDMI: channel mapping failed\n");
@@ -810,6 +845,7 @@
 					     int chs, unsigned char *map,
 					     int ca)
 {
+	struct hdmi_spec *spec = codec->spec;
 	int ordered_ca = get_channel_allocation_order(ca);
 	int alsa_pos, hdmi_slot;
 	int assignments[8] = {[0 ... 7] = 0xf};
@@ -825,11 +861,10 @@
 	}
 
 	for (hdmi_slot = 0; hdmi_slot < 8; hdmi_slot++) {
-		int val, err;
+		int err;
 
-		val = (assignments[hdmi_slot] << 4) | hdmi_slot;
-		err = snd_hda_codec_write(codec, pin_nid, 0,
-					  AC_VERB_SET_HDMI_CHAN_SLOT, val);
+		err = spec->ops.pin_set_slot_channel(codec, pin_nid, hdmi_slot,
+						     assignments[hdmi_slot]);
 		if (err)
 			return -EINVAL;
 	}
@@ -865,6 +900,22 @@
 	hdmi_debug_channel_mapping(codec, pin_nid);
 }
 
+static int hdmi_pin_set_slot_channel(struct hda_codec *codec, hda_nid_t pin_nid,
+				     int asp_slot, int channel)
+{
+	return snd_hda_codec_write(codec, pin_nid, 0,
+				   AC_VERB_SET_HDMI_CHAN_SLOT,
+				   (channel << 4) | asp_slot);
+}
+
+static int hdmi_pin_get_slot_channel(struct hda_codec *codec, hda_nid_t pin_nid,
+				     int asp_slot)
+{
+	return (snd_hda_codec_read(codec, pin_nid, 0,
+				   AC_VERB_GET_HDMI_CHAN_SLOT,
+				   asp_slot) & 0xf0) >> 4;
+}
+
 /*
  * Audio InfoFrame routines
  */
@@ -986,16 +1037,64 @@
 	return true;
 }
 
+static void hdmi_pin_setup_infoframe(struct hda_codec *codec,
+				     hda_nid_t pin_nid,
+				     int ca, int active_channels,
+				     int conn_type)
+{
+	union audio_infoframe ai;
+
+	if (conn_type == 0) { /* HDMI */
+		struct hdmi_audio_infoframe *hdmi_ai = &ai.hdmi;
+
+		hdmi_ai->type		= 0x84;
+		hdmi_ai->ver		= 0x01;
+		hdmi_ai->len		= 0x0a;
+		hdmi_ai->CC02_CT47	= active_channels - 1;
+		hdmi_ai->CA		= ca;
+		hdmi_checksum_audio_infoframe(hdmi_ai);
+	} else if (conn_type == 1) { /* DisplayPort */
+		struct dp_audio_infoframe *dp_ai = &ai.dp;
+
+		dp_ai->type		= 0x84;
+		dp_ai->len		= 0x1b;
+		dp_ai->ver		= 0x11 << 2;
+		dp_ai->CC02_CT47	= active_channels - 1;
+		dp_ai->CA		= ca;
+	} else {
+		snd_printd("HDMI: unknown connection type at pin %d\n",
+			    pin_nid);
+		return;
+	}
+
+	/*
+	 * sizeof(ai) is used instead of sizeof(*hdmi_ai) or
+	 * sizeof(*dp_ai) to avoid partial match/update problems when
+	 * the user switches between HDMI/DP monitors.
+	 */
+	if (!hdmi_infoframe_uptodate(codec, pin_nid, ai.bytes,
+					sizeof(ai))) {
+		snd_printdd("hdmi_pin_setup_infoframe: "
+			    "pin=%d channels=%d ca=0x%02x\n",
+			    pin_nid,
+			    active_channels, ca);
+		hdmi_stop_infoframe_trans(codec, pin_nid);
+		hdmi_fill_audio_infoframe(codec, pin_nid,
+					    ai.bytes, sizeof(ai));
+		hdmi_start_infoframe_trans(codec, pin_nid);
+	}
+}
+
 static void hdmi_setup_audio_infoframe(struct hda_codec *codec,
 				       struct hdmi_spec_per_pin *per_pin,
 				       bool non_pcm)
 {
+	struct hdmi_spec *spec = codec->spec;
 	hda_nid_t pin_nid = per_pin->pin_nid;
 	int channels = per_pin->channels;
 	int active_channels;
 	struct hdmi_eld *eld;
 	int ca, ordered_ca;
-	union audio_infoframe ai;
 
 	if (!channels)
 		return;
@@ -1021,30 +1120,6 @@
 
 	hdmi_set_channel_count(codec, per_pin->cvt_nid, active_channels);
 
-	memset(&ai, 0, sizeof(ai));
-	if (eld->info.conn_type == 0) { /* HDMI */
-		struct hdmi_audio_infoframe *hdmi_ai = &ai.hdmi;
-
-		hdmi_ai->type		= 0x84;
-		hdmi_ai->ver		= 0x01;
-		hdmi_ai->len		= 0x0a;
-		hdmi_ai->CC02_CT47	= active_channels - 1;
-		hdmi_ai->CA		= ca;
-		hdmi_checksum_audio_infoframe(hdmi_ai);
-	} else if (eld->info.conn_type == 1) { /* DisplayPort */
-		struct dp_audio_infoframe *dp_ai = &ai.dp;
-
-		dp_ai->type		= 0x84;
-		dp_ai->len		= 0x1b;
-		dp_ai->ver		= 0x11 << 2;
-		dp_ai->CC02_CT47	= active_channels - 1;
-		dp_ai->CA		= ca;
-	} else {
-		snd_printd("HDMI: unknown connection type at pin %d\n",
-			    pin_nid);
-		return;
-	}
-
 	/*
 	 * always configure channel mapping, it may have been changed by the
 	 * user in the meantime
@@ -1053,27 +1128,12 @@
 				   channels, per_pin->chmap,
 				   per_pin->chmap_set);
 
-	/*
-	 * sizeof(ai) is used instead of sizeof(*hdmi_ai) or
-	 * sizeof(*dp_ai) to avoid partial match/update problems when
-	 * the user switches between HDMI/DP monitors.
-	 */
-	if (!hdmi_infoframe_uptodate(codec, pin_nid, ai.bytes,
-					sizeof(ai))) {
-		snd_printdd("hdmi_setup_audio_infoframe: "
-			    "pin=%d channels=%d ca=0x%02x\n",
-			    pin_nid,
-			    active_channels, ca);
-		hdmi_stop_infoframe_trans(codec, pin_nid);
-		hdmi_fill_audio_infoframe(codec, pin_nid,
-					    ai.bytes, sizeof(ai));
-		hdmi_start_infoframe_trans(codec, pin_nid);
-	}
+	spec->ops.pin_setup_infoframe(codec, pin_nid, ca, active_channels,
+				      eld->info.conn_type);
 
 	per_pin->non_pcm = non_pcm;
 }
 
-
 /*
  * Unsolicited events
  */
@@ -1176,26 +1236,22 @@
 #define is_hbr_format(format) \
 	((format & AC_FMT_TYPE_NON_PCM) && (format & AC_FMT_CHAN_MASK) == 7)
 
-static int hdmi_setup_stream(struct hda_codec *codec, hda_nid_t cvt_nid,
-			      hda_nid_t pin_nid, u32 stream_tag, int format)
+static int hdmi_pin_hbr_setup(struct hda_codec *codec, hda_nid_t pin_nid,
+			      bool hbr)
 {
-	int pinctl;
-	int new_pinctl = 0;
-
-	if (is_haswell(codec))
-		haswell_verify_D0(codec, cvt_nid, pin_nid);
+	int pinctl, new_pinctl;
 
 	if (snd_hda_query_pin_caps(codec, pin_nid) & AC_PINCAP_HBR) {
 		pinctl = snd_hda_codec_read(codec, pin_nid, 0,
 					    AC_VERB_GET_PIN_WIDGET_CONTROL, 0);
 
 		new_pinctl = pinctl & ~AC_PINCTL_EPT;
-		if (is_hbr_format(format))
+		if (hbr)
 			new_pinctl |= AC_PINCTL_EPT_HBR;
 		else
 			new_pinctl |= AC_PINCTL_EPT_NATIVE;
 
-		snd_printdd("hdmi_setup_stream: "
+		snd_printdd("hdmi_pin_hbr_setup: "
 			    "NID=0x%x, %spinctl=0x%x\n",
 			    pin_nid,
 			    pinctl == new_pinctl ? "" : "new-",
@@ -1205,11 +1261,26 @@
 			snd_hda_codec_write(codec, pin_nid, 0,
 					    AC_VERB_SET_PIN_WIDGET_CONTROL,
 					    new_pinctl);
-
-	}
-	if (is_hbr_format(format) && !new_pinctl) {
-		snd_printdd("hdmi_setup_stream: HBR is not supported\n");
+	} else if (hbr)
 		return -EINVAL;
+
+	return 0;
+}
+
+static int hdmi_setup_stream(struct hda_codec *codec, hda_nid_t cvt_nid,
+			      hda_nid_t pin_nid, u32 stream_tag, int format)
+{
+	struct hdmi_spec *spec = codec->spec;
+	int err;
+
+	if (is_haswell(codec))
+		haswell_verify_D0(codec, cvt_nid, pin_nid);
+
+	err = spec->ops.pin_hbr_setup(codec, pin_nid, is_hbr_format(format));
+
+	if (err) {
+		snd_printdd("hdmi_setup_stream: HBR is not supported\n");
+		return err;
 	}
 
 	snd_hda_codec_setup_stream(codec, cvt_nid, stream_tag, 0, format);
@@ -1424,7 +1495,7 @@
 		codec->addr, pin_nid, pin_eld->monitor_present, eld->eld_valid);
 
 	if (eld->eld_valid) {
-		if (snd_hdmi_get_eld(codec, pin_nid, eld->eld_buffer,
+		if (spec->ops.pin_get_eld(codec, pin_nid, eld->eld_buffer,
 						     &eld->eld_size) < 0)
 			eld->eld_valid = false;
 		else {
@@ -1654,7 +1725,7 @@
 	hdmi_setup_audio_infoframe(codec, per_pin, non_pcm);
 	mutex_unlock(&per_pin->lock);
 
-	return hdmi_setup_stream(codec, cvt_nid, pin_nid, stream_tag, format);
+	return spec->ops.setup_stream(codec, cvt_nid, pin_nid, stream_tag, format);
 }
 
 static int generic_hdmi_playback_pcm_cleanup(struct hda_pcm_stream *hinfo,
@@ -1726,6 +1797,34 @@
 	return 0;
 }
 
+static int hdmi_chmap_cea_alloc_validate_get_type(struct cea_channel_speaker_allocation *cap,
+						  int channels)
+{
+	/* If the speaker allocation matches the channel count, it is OK.*/
+	if (cap->channels != channels)
+		return -1;
+
+	/* all channels are remappable freely */
+	return SNDRV_CTL_TLVT_CHMAP_VAR;
+}
+
+static void hdmi_cea_alloc_to_tlv_chmap(struct cea_channel_speaker_allocation *cap,
+					unsigned int *chmap, int channels)
+{
+	int count = 0;
+	int c;
+
+	for (c = 7; c >= 0; c--) {
+		int spk = cap->speakers[c];
+		if (!spk)
+			continue;
+
+		chmap[count++] = spk_to_chmap(spk);
+	}
+
+	WARN_ON(count != channels);
+}
+
 static int hdmi_chmap_ctl_tlv(struct snd_kcontrol *kcontrol, int op_flag,
 			      unsigned int size, unsigned int __user *tlv)
 {
@@ -1742,16 +1841,19 @@
 	size -= 8;
 	dst = tlv + 2;
 	for (chs = 2; chs <= spec->channels_max; chs++) {
-		int i, c;
+		int i;
 		struct cea_channel_speaker_allocation *cap;
 		cap = channel_allocations;
 		for (i = 0; i < ARRAY_SIZE(channel_allocations); i++, cap++) {
 			int chs_bytes = chs * 4;
-			if (cap->channels != chs)
+			int type = spec->ops.chmap_cea_alloc_validate_get_type(cap, chs);
+			unsigned int tlv_chmap[8];
+
+			if (type < 0)
 				continue;
 			if (size < 8)
 				return -ENOMEM;
-			if (put_user(SNDRV_CTL_TLVT_CHMAP_VAR, dst) ||
+			if (put_user(type, dst) ||
 			    put_user(chs_bytes, dst + 1))
 				return -EFAULT;
 			dst += 2;
@@ -1761,14 +1863,10 @@
 				return -ENOMEM;
 			size -= chs_bytes;
 			count += chs_bytes;
-			for (c = 7; c >= 0; c--) {
-				int spk = cap->speakers[c];
-				if (!spk)
-					continue;
-				if (put_user(spk_to_chmap(spk), dst))
-					return -EFAULT;
-				dst++;
-			}
+			spec->ops.cea_alloc_to_tlv_chmap(cap, tlv_chmap, chs);
+			if (copy_to_user(dst, tlv_chmap, chs_bytes))
+				return -EFAULT;
+			dst += chs;
 		}
 	}
 	if (put_user(count, tlv + 1))
@@ -1802,7 +1900,7 @@
 	unsigned int ctl_idx;
 	struct snd_pcm_substream *substream;
 	unsigned char chmap[8];
-	int i, ca, prepared = 0;
+	int i, err, ca, prepared = 0;
 
 	ctl_idx = snd_ctl_get_ioffidx(kcontrol, &ucontrol->id);
 	substream = snd_pcm_chmap_substream(info, ctl_idx);
@@ -1826,6 +1924,11 @@
 	ca = hdmi_manual_channel_allocation(ARRAY_SIZE(chmap), chmap);
 	if (ca < 0)
 		return -EINVAL;
+	if (spec->ops.chmap_validate) {
+		err = spec->ops.chmap_validate(ca, ARRAY_SIZE(chmap), chmap);
+		if (err)
+			return err;
+	}
 	mutex_lock(&per_pin->lock);
 	per_pin->chmap_set = true;
 	memcpy(per_pin->chmap, chmap, sizeof(chmap));
@@ -2032,6 +2135,17 @@
 #endif
 };
 
+static const struct hdmi_ops generic_standard_hdmi_ops = {
+	.pin_get_eld				= snd_hdmi_get_eld,
+	.pin_get_slot_channel			= hdmi_pin_get_slot_channel,
+	.pin_set_slot_channel			= hdmi_pin_set_slot_channel,
+	.pin_setup_infoframe			= hdmi_pin_setup_infoframe,
+	.pin_hbr_setup				= hdmi_pin_hbr_setup,
+	.setup_stream				= hdmi_setup_stream,
+	.chmap_cea_alloc_validate_get_type	= hdmi_chmap_cea_alloc_validate_get_type,
+	.cea_alloc_to_tlv_chmap			= hdmi_cea_alloc_to_tlv_chmap,
+};
+
 
 static void intel_haswell_fixup_connect_list(struct hda_codec *codec,
 					     hda_nid_t nid)
@@ -2114,6 +2228,7 @@
 	if (spec == NULL)
 		return -ENOMEM;
 
+	spec->ops = generic_standard_hdmi_ops;
 	codec->spec = spec;
 	hdmi_array_init(spec, 4);