ALSA: hda - hdmi: Add ATI/AMD multi-channel audio support
authorAnssi Hannula <anssi.hannula@iki.fi>
Thu, 24 Oct 2013 18:10:35 +0000 (21:10 +0300)
committerTakashi Iwai <tiwai@suse.de>
Thu, 24 Oct 2013 20:53:43 +0000 (22:53 +0200)
ATI/AMD codecs do not support all the standard HDA HDMI/DP functions,
instead various vendor-specific verbs are provided.

This commit addresses these missing functions:
- standard channel mapping support
- standard infoframe configuration support

ATI/AMD provides their own verbs that allow the following:
- setting CA for infoframe
- setting down-mix information for infoframe
- channel pair remapping
- individual channel remapping (revision ID 3+, 0x100300+)

The documentation for the verbs has now been released by AMD:
http://www.x.org/docs/AMD/AMD_HDA_verbs_v2.pdf

Add support for the ATI/AMD specific verbs and use them instead of the
generic methods on ATI/AMD codecs. This allows multi-channel PCM audio
to work.

Channel remapping is restricted to pairwise mapping on codecs with
revision ID 2 (0x100200 as reported by procfs codec#X) or lower. This
means cards up to Radeon HD7670 as far as I know. This will not affect
standard multi-channel modes since these codecs support automatic
FC-LFE swapping for HDMI.

ATI/AMD codecs do not advertise all of their supported rates, formats
and channel counts, therefore that information is forced accordingly so
that all HDMI 1.x PCM parameters are marked as supported.

Support for multiple ports is also added to patch_atihdmi so that
0x1002aa01 codecs with multiple ports will work properly when switched
back to that patch.

v2: splitted ELD emulation to a separate patch, tlv fixes
v3: adapted to the new hdmi_ops infrastructure, fixed rev3+ vendor id

Signed-off-by: Anssi Hannula <anssi.hannula@iki.fi>
Tested-by: Peter Frühberger <fritsch@xbmc.org> # v2
Tested-by: Olivier Langlois <olivier@trillion01.com> # v2+rev3fix
Signed-off-by: Takashi Iwai <tiwai@suse.de>
sound/pci/hda/patch_hdmi.c

index acc00fe..1114be0 100644 (file)
@@ -6,6 +6,7 @@
  *  Copyright (c) 2006 ATI Technologies Inc.
  *  Copyright (c) 2008 NVIDIA Corp.  All rights reserved.
  *  Copyright (c) 2008 Wei Ni <wni@nvidia.com>
+ *  Copyright (c) 2013 Anssi Hannula <anssi.hannula@iki.fi>
  *
  *  Authors:
  *                     Wu Fengguang <wfg@linux.intel.com>
@@ -128,7 +129,7 @@ struct hdmi_spec {
        struct hdmi_eld temp_eld;
        struct hdmi_ops ops;
        /*
-        * Non-generic ATI/NVIDIA specific
+        * Non-generic VIA/NVIDIA specific
         */
        struct hda_multi_out multiout;
        struct hda_pcm_stream pcm_playback;
@@ -2784,49 +2785,292 @@ static int patch_nvhdmi_8ch_7x(struct hda_codec *codec)
 }
 
 /*
- * ATI-specific implementations
- *
- * FIXME: we may omit the whole this and use the generic code once after
- * it's confirmed to work.
+ * ATI/AMD-specific implementations
  */
 
-#define ATIHDMI_CVT_NID                0x02    /* audio converter */
-#define ATIHDMI_PIN_NID                0x03    /* HDMI output pin */
+#define is_amdhdmi_rev3_or_later(codec) \
+       ((codec)->vendor_id == 0x1002aa01 && ((codec)->revision_id & 0xff00) >= 0x0300)
+#define has_amd_full_remap_support(codec) is_amdhdmi_rev3_or_later(codec)
+
+/* ATI/AMD specific HDA pin verbs, see the AMD HDA Verbs specification */
+#define ATI_VERB_SET_CHANNEL_ALLOCATION        0x771
+#define ATI_VERB_SET_DOWNMIX_INFO      0x772
+#define ATI_VERB_SET_MULTICHANNEL_01   0x777
+#define ATI_VERB_SET_MULTICHANNEL_23   0x778
+#define ATI_VERB_SET_MULTICHANNEL_45   0x779
+#define ATI_VERB_SET_MULTICHANNEL_67   0x77a
+#define ATI_VERB_SET_MULTICHANNEL_1    0x785
+#define ATI_VERB_SET_MULTICHANNEL_3    0x786
+#define ATI_VERB_SET_MULTICHANNEL_5    0x787
+#define ATI_VERB_SET_MULTICHANNEL_7    0x788
+#define ATI_VERB_SET_MULTICHANNEL_MODE 0x789
+#define ATI_VERB_GET_CHANNEL_ALLOCATION        0xf71
+#define ATI_VERB_GET_DOWNMIX_INFO      0xf72
+#define ATI_VERB_GET_MULTICHANNEL_01   0xf77
+#define ATI_VERB_GET_MULTICHANNEL_23   0xf78
+#define ATI_VERB_GET_MULTICHANNEL_45   0xf79
+#define ATI_VERB_GET_MULTICHANNEL_67   0xf7a
+#define ATI_VERB_GET_MULTICHANNEL_1    0xf85
+#define ATI_VERB_GET_MULTICHANNEL_3    0xf86
+#define ATI_VERB_GET_MULTICHANNEL_5    0xf87
+#define ATI_VERB_GET_MULTICHANNEL_7    0xf88
+#define ATI_VERB_GET_MULTICHANNEL_MODE 0xf89
+
+#define ATI_OUT_ENABLE 0x1
+
+#define ATI_MULTICHANNEL_MODE_PAIRED   0
+#define ATI_MULTICHANNEL_MODE_SINGLE   1
+
+static void atihdmi_pin_setup_infoframe(struct hda_codec *codec, hda_nid_t pin_nid, int ca,
+                                       int active_channels, int conn_type)
+{
+       snd_hda_codec_write(codec, pin_nid, 0, ATI_VERB_SET_CHANNEL_ALLOCATION, ca);
+}
+
+static int atihdmi_paired_swap_fc_lfe(int pos)
+{
+       /*
+        * ATI/AMD have automatic FC/LFE swap built-in
+        * when in pairwise mapping mode.
+        */
+
+       switch (pos) {
+               /* see channel_allocations[].speakers[] */
+               case 2: return 3;
+               case 3: return 2;
+               default: break;
+       }
+
+       return pos;
+}
+
+static int atihdmi_paired_chmap_validate(int ca, int chs, unsigned char *map)
+{
+       struct cea_channel_speaker_allocation *cap;
+       int i, j;
+
+       /* check that only channel pairs need to be remapped on old pre-rev3 ATI/AMD */
+
+       cap = &channel_allocations[get_channel_allocation_order(ca)];
+       for (i = 0; i < chs; ++i) {
+               int mask = to_spk_mask(map[i]);
+               bool ok = false;
+               bool companion_ok = false;
+
+               if (!mask)
+                       continue;
+
+               for (j = 0 + i % 2; j < 8; j += 2) {
+                       int chan_idx = 7 - atihdmi_paired_swap_fc_lfe(j);
+                       if (cap->speakers[chan_idx] == mask) {
+                               /* channel is in a supported position */
+                               ok = true;
+
+                               if (i % 2 == 0 && i + 1 < chs) {
+                                       /* even channel, check the odd companion */
+                                       int comp_chan_idx = 7 - atihdmi_paired_swap_fc_lfe(j + 1);
+                                       int comp_mask_req = to_spk_mask(map[i+1]);
+                                       int comp_mask_act = cap->speakers[comp_chan_idx];
+
+                                       if (comp_mask_req == comp_mask_act)
+                                               companion_ok = true;
+                                       else
+                                               return -EINVAL;
+                               }
+                               break;
+                       }
+               }
+
+               if (!ok)
+                       return -EINVAL;
+
+               if (companion_ok)
+                       i++; /* companion channel already checked */
+       }
+
+       return 0;
+}
+
+static int atihdmi_pin_set_slot_channel(struct hda_codec *codec, hda_nid_t pin_nid,
+                                       int hdmi_slot, int stream_channel)
+{
+       int verb;
+       int ati_channel_setup = 0;
+
+       if (hdmi_slot > 7)
+               return -EINVAL;
+
+       if (!has_amd_full_remap_support(codec)) {
+               hdmi_slot = atihdmi_paired_swap_fc_lfe(hdmi_slot);
+
+               /* In case this is an odd slot but without stream channel, do not
+                * disable the slot since the corresponding even slot could have a
+                * channel. In case neither have a channel, the slot pair will be
+                * disabled when this function is called for the even slot. */
+               if (hdmi_slot % 2 != 0 && stream_channel == 0xf)
+                       return 0;
+
+               hdmi_slot -= hdmi_slot % 2;
+
+               if (stream_channel != 0xf)
+                       stream_channel -= stream_channel % 2;
+       }
+
+       verb = ATI_VERB_SET_MULTICHANNEL_01 + hdmi_slot/2 + (hdmi_slot % 2) * 0x00e;
+
+       /* ati_channel_setup format: [7..4] = stream_channel_id, [1] = mute, [0] = enable */
+
+       if (stream_channel != 0xf)
+               ati_channel_setup = (stream_channel << 4) | ATI_OUT_ENABLE;
+
+       return snd_hda_codec_write(codec, pin_nid, 0, verb, ati_channel_setup);
+}
+
+static int atihdmi_pin_get_slot_channel(struct hda_codec *codec, hda_nid_t pin_nid,
+                                       int asp_slot)
+{
+       bool was_odd = false;
+       int ati_asp_slot = asp_slot;
+       int verb;
+       int ati_channel_setup;
+
+       if (asp_slot > 7)
+               return -EINVAL;
+
+       if (!has_amd_full_remap_support(codec)) {
+               ati_asp_slot = atihdmi_paired_swap_fc_lfe(asp_slot);
+               if (ati_asp_slot % 2 != 0) {
+                       ati_asp_slot -= 1;
+                       was_odd = true;
+               }
+       }
+
+       verb = ATI_VERB_GET_MULTICHANNEL_01 + ati_asp_slot/2 + (ati_asp_slot % 2) * 0x00e;
+
+       ati_channel_setup = snd_hda_codec_read(codec, pin_nid, 0, verb, 0);
+
+       if (!(ati_channel_setup & ATI_OUT_ENABLE))
+               return 0xf;
+
+       return ((ati_channel_setup & 0xf0) >> 4) + !!was_odd;
+}
 
-static int atihdmi_playback_pcm_prepare(struct hda_pcm_stream *hinfo,
-                                       struct hda_codec *codec,
-                                       unsigned int stream_tag,
-                                       unsigned int format,
-                                       struct snd_pcm_substream *substream)
+static int atihdmi_paired_chmap_cea_alloc_validate_get_type(struct cea_channel_speaker_allocation *cap,
+                                                           int channels)
+{
+       int c;
+
+       /*
+        * Pre-rev3 ATI/AMD codecs operate in a paired channel mode, so
+        * we need to take that into account (a single channel may take 2
+        * channel slots if we need to carry a silent channel next to it).
+        * On Rev3+ AMD codecs this function is not used.
+        */
+       int chanpairs = 0;
+
+       /* We only produce even-numbered channel count TLVs */
+       if ((channels % 2) != 0)
+               return -1;
+
+       for (c = 0; c < 7; c += 2) {
+               if (cap->speakers[c] || cap->speakers[c+1])
+                       chanpairs++;
+       }
+
+       if (chanpairs * 2 != channels)
+               return -1;
+
+       return SNDRV_CTL_TLVT_CHMAP_PAIRED;
+}
+
+static void atihdmi_paired_cea_alloc_to_tlv_chmap(struct cea_channel_speaker_allocation *cap,
+                                                 unsigned int *chmap, int channels)
+{
+       /* produce paired maps for pre-rev3 ATI/AMD codecs */
+       int count = 0;
+       int c;
+
+       for (c = 7; c >= 0; c--) {
+               int chan = 7 - atihdmi_paired_swap_fc_lfe(7 - c);
+               int spk = cap->speakers[chan];
+               if (!spk) {
+                       /* add N/A channel if the companion channel is occupied */
+                       if (cap->speakers[chan + (chan % 2 ? -1 : 1)])
+                               chmap[count++] = SNDRV_CHMAP_NA;
+
+                       continue;
+               }
+
+               chmap[count++] = spk_to_chmap(spk);
+       }
+
+       WARN_ON(count != channels);
+}
+
+static int atihdmi_init(struct hda_codec *codec)
 {
        struct hdmi_spec *spec = codec->spec;
-       struct hdmi_spec_per_cvt *per_cvt = get_cvt(spec, 0);
-       int chans = substream->runtime->channels;
-       int i, err;
+       int pin_idx, err;
 
-       err = simple_playback_pcm_prepare(hinfo, codec, stream_tag, format,
-                                         substream);
-       if (err < 0)
+       err = generic_hdmi_init(codec);
+
+       if (err)
                return err;
-       snd_hda_codec_write(codec, per_cvt->cvt_nid, 0,
-                           AC_VERB_SET_CVT_CHAN_COUNT, chans - 1);
-       /* FIXME: XXX */
-       for (i = 0; i < chans; i++) {
-               snd_hda_codec_write(codec, per_cvt->cvt_nid, 0,
-                                   AC_VERB_SET_HDMI_CHAN_SLOT,
-                                   (i << 4) | i);
+
+       for (pin_idx = 0; pin_idx < spec->num_pins; pin_idx++) {
+               struct hdmi_spec_per_pin *per_pin = get_pin(spec, pin_idx);
+
+               /* make sure downmix information in infoframe is zero */
+               snd_hda_codec_write(codec, per_pin->pin_nid, 0, ATI_VERB_SET_DOWNMIX_INFO, 0);
+
+               /* enable channel-wise remap mode if supported */
+               if (has_amd_full_remap_support(codec))
+                       snd_hda_codec_write(codec, per_pin->pin_nid, 0,
+                                           ATI_VERB_SET_MULTICHANNEL_MODE,
+                                           ATI_MULTICHANNEL_MODE_SINGLE);
        }
+
        return 0;
 }
 
 static int patch_atihdmi(struct hda_codec *codec)
 {
        struct hdmi_spec *spec;
-       int err = patch_simple_hdmi(codec, ATIHDMI_CVT_NID, ATIHDMI_PIN_NID);
-       if (err < 0)
+       struct hdmi_spec_per_cvt *per_cvt;
+       int err, cvt_idx;
+
+       err = patch_generic_hdmi(codec);
+
+       if (err)
                return err;
+
+       codec->patch_ops.init = atihdmi_init;
+
        spec = codec->spec;
-       spec->pcm_playback.ops.prepare = atihdmi_playback_pcm_prepare;
+
+       spec->ops.pin_get_slot_channel = atihdmi_pin_get_slot_channel;
+       spec->ops.pin_set_slot_channel = atihdmi_pin_set_slot_channel;
+       spec->ops.pin_setup_infoframe = atihdmi_pin_setup_infoframe;
+
+       if (!has_amd_full_remap_support(codec)) {
+               /* override to ATI/AMD-specific versions with pairwise mapping */
+               spec->ops.chmap_cea_alloc_validate_get_type =
+                       atihdmi_paired_chmap_cea_alloc_validate_get_type;
+               spec->ops.cea_alloc_to_tlv_chmap = atihdmi_paired_cea_alloc_to_tlv_chmap;
+               spec->ops.chmap_validate = atihdmi_paired_chmap_validate;
+       }
+
+       /* ATI/AMD converters do not advertise all of their capabilities */
+       for (cvt_idx = 0; cvt_idx < spec->num_cvts; cvt_idx++) {
+               per_cvt = get_cvt(spec, cvt_idx);
+               per_cvt->channels_max = max(per_cvt->channels_max, 8u);
+               per_cvt->rates |= SUPPORTED_RATES;
+               per_cvt->formats |= SUPPORTED_FORMATS;
+               per_cvt->maxbps = max(per_cvt->maxbps, 24u);
+       }
+
+       spec->channels_max = max(spec->channels_max, 8u);
+
        return 0;
 }
 
@@ -2846,7 +3090,7 @@ static const struct hda_codec_preset snd_hda_preset_hdmi[] = {
 { .id = 0x1002793c, .name = "RS600 HDMI",      .patch = patch_atihdmi },
 { .id = 0x10027919, .name = "RS600 HDMI",      .patch = patch_atihdmi },
 { .id = 0x1002791a, .name = "RS690/780 HDMI",  .patch = patch_atihdmi },
-{ .id = 0x1002aa01, .name = "R6xx HDMI",       .patch = patch_generic_hdmi },
+{ .id = 0x1002aa01, .name = "R6xx HDMI",       .patch = patch_atihdmi },
 { .id = 0x10951390, .name = "SiI1390 HDMI",    .patch = patch_generic_hdmi },
 { .id = 0x10951392, .name = "SiI1392 HDMI",    .patch = patch_generic_hdmi },
 { .id = 0x17e80047, .name = "Chrontel HDMI",   .patch = patch_generic_hdmi },