diff mbox series

[2/4] ALSA: hda/hdmi: Allow audio component for AMD/ATI HDMI

Message ID 20180723145030.25133-3-tiwai@suse.de (mailing list archive)
State New, archived
Headers show
Series drm_audio_component support for AMD/ATI HDMI codecs | expand

Commit Message

Takashi Iwai July 23, 2018, 2:50 p.m. UTC
AMD/ATI HDMI codec drivers didn't have the audio component binding
like i915, but it worked only with the traditional HD-audio
unsolicited event for the HDMI hotplug detection and the ELD read-up
thereafter.  This has been a problem in many ways: first of all, it
goes through the hardware event transition (from GPU register write,
HD-audio controller trigger, and finally to HD-audio unsolicited event
handling), which is often unreliable and may miss some opportunities.
Second, each unsol event handling and ELD read-up need the explicit
power up / down when the codec is in the runtime suspend.  Last but
not least, which is the most important, the hotplug wakeup may be
missed when the HD-audio controller is in runtime suspend.  Especially
the last point is a big problem due to the recent change relevant with
vga_switcheroo that forcibly enables the runtime PM for AMD HDMI
controllers.

These issues are solved by introducing the audio component; the
hotplug notification is done by a direct function callback, which is
more accurate and reliable, and it can be processed without the actual
hardware access, i.e. no runtime PM trigger is needed, and the
HD-audio gets the event even if it's in runtime suspend.  The same for
ELD query, as it's read directly from the cached ELD bytes stored in
the DRM driver, hence the whole hardware access can be skipped.

So here it is: this patch implements the audio component binding with
AMD/ATI DRM driver.  The biggest difference from i915 implementation
is that this binding is fully optional and it can be enabled
asynchronously on the fly.  That is, the driver will switch from the
HD-audio unsolicited event to the notify callback once when the DRM
component gets bound. Similarly, when DRM driver gets unloaded, the
HDMI event handling returns to the legacy mode, too.

Also, another difference from i915 is that AMD HDMI registers the
component in the codec driver, while i915 HDMI codec assumes the
component binding was already done.  Hence AMD code does de-register
the component binding at the codec exit, too.

Some other details:
- The match component ops assumes that both VGA and HD-audio
  controller PCI entries belong to the same PCI bus, and only accepts
  such an entry.

- The pin2port audio_ops is implemented with assumption of the widget
  NID starting from 3, with step 2 (3, 5, 7, ...).

As of this patch, the DRM component in radeon and amdgpu DRM driver
sides isn't implemented yet, so this change won't give any benefit
alone.  By the following changes in DRM sides, the mission will be
completed.

Signed-off-by: Takashi Iwai <tiwai@suse.de>
---
 sound/pci/hda/patch_hdmi.c | 142 +++++++++++++++++++++++++++++++++----
 1 file changed, 128 insertions(+), 14 deletions(-)
diff mbox series

Patch

diff --git a/sound/pci/hda/patch_hdmi.c b/sound/pci/hda/patch_hdmi.c
index 6750318d1c82..298d40876201 100644
--- a/sound/pci/hda/patch_hdmi.c
+++ b/sound/pci/hda/patch_hdmi.c
@@ -31,6 +31,7 @@ 
 
 #include <linux/init.h>
 #include <linux/delay.h>
+#include <linux/pci.h>
 #include <linux/slab.h>
 #include <linux/module.h>
 #include <linux/pm_runtime.h>
@@ -131,6 +132,7 @@  struct hdmi_pcm {
 };
 
 struct hdmi_spec {
+	struct hda_codec *codec;
 	int num_cvts;
 	struct snd_array cvts; /* struct hdmi_spec_per_cvt */
 	hda_nid_t cvt_nids[4]; /* only for haswell fix */
@@ -175,8 +177,9 @@  struct hdmi_spec {
 	struct hda_multi_out multiout;
 	struct hda_pcm_stream pcm_playback;
 
-	/* i915/powerwell (Haswell+/Valleyview+) specific */
-	bool use_acomp_notifier; /* use i915 eld_notify callback for hotplug */
+	bool use_jack_detect; /* jack detection enabled */
+	bool use_acomp_notifier; /* use eld_notify callback for hotplug */
+	bool acomp_registered; /* audio component registered in this driver */
 	struct drm_audio_component_audio_ops drm_audio_ops;
 
 	struct hdac_chmap chmap;
@@ -775,6 +778,10 @@  static void check_presence_and_report(struct hda_codec *codec, hda_nid_t nid,
 static void jack_callback(struct hda_codec *codec,
 			  struct hda_jack_callback *jack)
 {
+	/* stop polling when notification is enabled */
+	if (codec_has_acomp(codec))
+		return;
+
 	/* hda_jack don't support DP MST */
 	check_presence_and_report(codec, jack->nid, 0);
 }
@@ -833,6 +840,9 @@  static void hdmi_unsol_event(struct hda_codec *codec, unsigned int res)
 	int tag = res >> AC_UNSOL_RES_TAG_SHIFT;
 	int subtag = (res & AC_UNSOL_RES_SUBTAG) >> AC_UNSOL_RES_SUBTAG_SHIFT;
 
+	if (codec_has_acomp(codec))
+		return;
+
 	if (!snd_hda_jack_tbl_get_from_tag(codec, tag)) {
 		codec_dbg(codec, "Unexpected HDMI event tag 0x%x\n", tag);
 		return;
@@ -1639,18 +1649,13 @@  static bool hdmi_present_sense(struct hdmi_spec_per_pin *per_pin, int repoll)
 			snd_hda_power_down_pm(codec);
 			return false;
 		}
-	}
-
-	if (codec_has_acomp(codec)) {
+		ret = hdmi_present_sense_via_verbs(per_pin, repoll);
+		snd_hda_power_down_pm(codec);
+	} else {
 		sync_eld_via_acomp(codec, per_pin);
 		ret = false; /* don't call snd_hda_jack_report_sync() */
-	} else {
-		ret = hdmi_present_sense_via_verbs(per_pin, repoll);
 	}
 
-	if (!codec_has_acomp(codec))
-		snd_hda_power_down_pm(codec);
-
 	return ret;
 }
 
@@ -2242,6 +2247,8 @@  static int generic_hdmi_init(struct hda_codec *codec)
 	struct hdmi_spec *spec = codec->spec;
 	int pin_idx;
 
+	mutex_lock(&spec->pcm_lock);
+	spec->use_jack_detect = !codec->jackpoll_interval;
 	for (pin_idx = 0; pin_idx < spec->num_pins; pin_idx++) {
 		struct hdmi_spec_per_pin *per_pin = get_pin(spec, pin_idx);
 		hda_nid_t pin_nid = per_pin->pin_nid;
@@ -2249,11 +2256,15 @@  static int generic_hdmi_init(struct hda_codec *codec)
 
 		snd_hda_set_dev_select(codec, pin_nid, dev_id);
 		hdmi_init_pin(codec, pin_nid);
-		if (!codec_has_acomp(codec))
+		if (codec_has_acomp(codec))
+			continue;
+		if (spec->use_jack_detect)
+			snd_hda_jack_detect_enable(codec, pin_nid);
+		else
 			snd_hda_jack_detect_enable_callback(codec, pin_nid,
-				codec->jackpoll_interval > 0 ?
-				jack_callback : NULL);
+							    jack_callback);
 	}
+	mutex_unlock(&spec->pcm_lock);
 	return 0;
 }
 
@@ -2286,7 +2297,9 @@  static void generic_hdmi_free(struct hda_codec *codec)
 	struct hdmi_spec *spec = codec->spec;
 	int pin_idx, pcm_idx;
 
-	if (codec_has_acomp(codec))
+	if (spec->acomp_registered)
+		snd_hdac_acomp_exit(&codec->bus->core);
+	else if (codec_has_acomp(codec))
 		snd_hdac_acomp_register_notifier(&codec->bus->core, NULL);
 
 	for (pin_idx = 0; pin_idx < spec->num_pins; pin_idx++) {
@@ -2352,6 +2365,7 @@  static int alloc_generic_hdmi(struct hda_codec *codec)
 	if (!spec)
 		return -ENOMEM;
 
+	spec->codec = codec;
 	spec->ops = generic_standard_hdmi_ops;
 	spec->dev_num = 1;	/* initialize to 1 */
 	mutex_init(&spec->pcm_lock);
@@ -3459,6 +3473,63 @@  static int patch_tegra_hdmi(struct hda_codec *codec)
 #define ATI_HBR_CAPABLE 0x01
 #define ATI_HBR_ENABLE 0x10
 
+/* turn on / off the unsol event jack detection dynamically */
+static void reprogram_jack_detect(struct hda_codec *codec, hda_nid_t nid,
+				  bool use_acomp)
+{
+	struct hda_jack_tbl *tbl;
+
+	tbl = snd_hda_jack_tbl_get(codec, nid);
+	if (tbl) {
+		/* clear unsol even if component notifier is used, or re-enable
+		 * if notifier is cleared
+		 */
+		unsigned int val = use_acomp ? 0 : (AC_USRSP_EN | tbl->tag);
+		snd_hda_codec_write_cache(codec, nid, 0,
+					  AC_VERB_SET_UNSOLICITED_ENABLE, val);
+	} else {
+		/* if no jack entry was defined beforehand, create a new one
+		 * at need (i.e. only when notifier is cleared)
+		 */
+		if (!use_acomp)
+			snd_hda_jack_detect_enable(codec, nid);
+	}
+}
+
+/* set up / clear component notifier dynamically */
+static void acomp_notifier_set(struct drm_audio_component *acomp,
+			       bool use_acomp)
+{
+	struct hdmi_spec *spec;
+	int i;
+
+	spec = container_of(acomp->audio_ops, struct hdmi_spec, drm_audio_ops);
+	mutex_lock(&spec->pcm_lock);
+	spec->use_acomp_notifier = use_acomp;
+	/* reprogram each jack detection logic depending on the notifier */
+	if (spec->use_jack_detect) {
+		for (i = 0; i < spec->num_pins; i++)
+			reprogram_jack_detect(spec->codec,
+					      get_pin(spec, i)->pin_nid,
+					      use_acomp);
+	}
+	mutex_unlock(&spec->pcm_lock);
+}
+
+/* enable / disable the notifier via master bind / unbind */
+static int atihdmi_master_bind(struct device *dev,
+			       struct drm_audio_component *acomp)
+{
+	acomp_notifier_set(acomp, true);
+	return 0;
+}
+
+static void atihdmi_master_unbind(struct device *dev,
+				  struct drm_audio_component *acomp)
+{
+	acomp_notifier_set(acomp, false);
+}
+
 static int atihdmi_pin_get_eld(struct hda_codec *codec, hda_nid_t nid,
 			   unsigned char *buf, int *eld_size)
 {
@@ -3731,6 +3802,39 @@  static int atihdmi_init(struct hda_codec *codec)
 	return 0;
 }
 
+/* check whether both HD-audio and DRM PCI devices belong to the same bus */
+static int match_bound_vga(struct device *dev, void *data)
+{
+	struct hdac_bus *bus = data;
+	struct pci_dev *pci, *master;
+
+	if (!dev_is_pci(dev) || !dev_is_pci(bus->dev))
+		return 0;
+	master = to_pci_dev(bus->dev);
+	pci = to_pci_dev(dev);
+	return master->bus == pci->bus;
+}
+
+/* map from pin NID to port; port is 0-based */
+static int atihdmi_pin2port(void *audio_ptr, int pin_nid)
+{
+	return pin_nid / 2 - 1;
+}
+
+/* audio component notifier for AMD codecs */
+static void atihdmi_pin_eld_notify(void *audio_ptr, int port, int dev_id)
+{
+	struct hda_codec *codec = audio_ptr;
+	hda_nid_t pin_nid = port * 2 + 3; /* 0-based port# */
+
+	/* treat notification only when system is already running and
+	 * not during PM suspend/resume process itself
+	 */
+	if (snd_power_get_state(codec->card) == SNDRV_CTL_POWER_D0 &&
+	    !atomic_read(&(codec)->core.in_pm))
+		check_presence_and_report(codec, pin_nid, dev_id);
+}
+
 static int patch_atihdmi(struct hda_codec *codec)
 {
 	struct hdmi_spec *spec;
@@ -3779,6 +3883,16 @@  static int patch_atihdmi(struct hda_codec *codec)
 	 */
 	codec->link_down_at_suspend = 1;
 
+	/* set up audio component binding */
+	spec->drm_audio_ops.audio_ptr = codec;
+	spec->drm_audio_ops.pin2port = atihdmi_pin2port;
+	spec->drm_audio_ops.pin_eld_notify = atihdmi_pin_eld_notify;
+	spec->drm_audio_ops.master_bind = atihdmi_master_bind;
+	spec->drm_audio_ops.master_unbind = atihdmi_master_unbind;
+	if (!snd_hdac_acomp_init(&codec->bus->core, &spec->drm_audio_ops,
+				 match_bound_vga, 0))
+		spec->acomp_registered = true;
+
 	return 0;
 }