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 --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; }