root/sound/hda/codecs/hdmi/nvhdmi-mcp.c
// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Legacy Nvidia HDMI codec support
 */

#include <linux/init.h>
#include <linux/slab.h>
#include <linux/module.h>
#include <sound/core.h>
#include <sound/hdaudio.h>
#include <sound/hda_codec.h>
#include "hda_local.h"
#include "hdmi_local.h"

enum { MODEL_2CH, MODEL_8CH };

#define Nv_VERB_SET_Channel_Allocation          0xF79
#define Nv_VERB_SET_Info_Frame_Checksum         0xF7A
#define Nv_VERB_SET_Audio_Protection_On         0xF98
#define Nv_VERB_SET_Audio_Protection_Off        0xF99

#define nvhdmi_master_con_nid_7x        0x04
#define nvhdmi_master_pin_nid_7x        0x05

static const hda_nid_t nvhdmi_con_nids_7x[4] = {
        /*front, rear, clfe, rear_surr */
        0x6, 0x8, 0xa, 0xc,
};

static const struct hda_verb nvhdmi_basic_init_7x_2ch[] = {
        /* set audio protect on */
        { 0x1, Nv_VERB_SET_Audio_Protection_On, 0x1},
        /* enable digital output on pin widget */
        { 0x5, AC_VERB_SET_PIN_WIDGET_CONTROL, PIN_OUT | 0x5 },
        {} /* terminator */
};

static const struct hda_verb nvhdmi_basic_init_7x_8ch[] = {
        /* set audio protect on */
        { 0x1, Nv_VERB_SET_Audio_Protection_On, 0x1},
        /* enable digital output on pin widget */
        { 0x5, AC_VERB_SET_PIN_WIDGET_CONTROL, PIN_OUT | 0x5 },
        { 0x7, AC_VERB_SET_PIN_WIDGET_CONTROL, PIN_OUT | 0x5 },
        { 0x9, AC_VERB_SET_PIN_WIDGET_CONTROL, PIN_OUT | 0x5 },
        { 0xb, AC_VERB_SET_PIN_WIDGET_CONTROL, PIN_OUT | 0x5 },
        { 0xd, AC_VERB_SET_PIN_WIDGET_CONTROL, PIN_OUT | 0x5 },
        {} /* terminator */
};

static int nvhdmi_mcp_init(struct hda_codec *codec)
{
        struct hdmi_spec *spec = codec->spec;

        if (spec->multiout.max_channels == 2)
                snd_hda_sequence_write(codec, nvhdmi_basic_init_7x_2ch);
        else
                snd_hda_sequence_write(codec, nvhdmi_basic_init_7x_8ch);
        return 0;
}

static void nvhdmi_8ch_7x_set_info_frame_parameters(struct hda_codec *codec,
                                                    int channels)
{
        unsigned int chanmask;
        int chan = channels ? (channels - 1) : 1;

        switch (channels) {
        default:
        case 0:
        case 2:
                chanmask = 0x00;
                break;
        case 4:
                chanmask = 0x08;
                break;
        case 6:
                chanmask = 0x0b;
                break;
        case 8:
                chanmask = 0x13;
                break;
        }

        /* Set the audio infoframe channel allocation and checksum fields.  The
         * channel count is computed implicitly by the hardware.
         */
        snd_hda_codec_write(codec, 0x1, 0,
                        Nv_VERB_SET_Channel_Allocation, chanmask);

        snd_hda_codec_write(codec, 0x1, 0,
                        Nv_VERB_SET_Info_Frame_Checksum,
                        (0x71 - chan - chanmask));
}

static int nvhdmi_8ch_7x_pcm_close(struct hda_pcm_stream *hinfo,
                                   struct hda_codec *codec,
                                   struct snd_pcm_substream *substream)
{
        struct hdmi_spec *spec = codec->spec;
        int i;

        snd_hda_codec_write(codec, nvhdmi_master_con_nid_7x,
                        0, AC_VERB_SET_CHANNEL_STREAMID, 0);
        for (i = 0; i < 4; i++) {
                /* set the stream id */
                snd_hda_codec_write(codec, nvhdmi_con_nids_7x[i], 0,
                                AC_VERB_SET_CHANNEL_STREAMID, 0);
                /* set the stream format */
                snd_hda_codec_write(codec, nvhdmi_con_nids_7x[i], 0,
                                AC_VERB_SET_STREAM_FORMAT, 0);
        }

        /* The audio hardware sends a channel count of 0x7 (8ch) when all the
         * streams are disabled.
         */
        nvhdmi_8ch_7x_set_info_frame_parameters(codec, 8);

        return snd_hda_multi_out_dig_close(codec, &spec->multiout);
}

static int nvhdmi_8ch_7x_pcm_prepare(struct hda_pcm_stream *hinfo,
                                     struct hda_codec *codec,
                                     unsigned int stream_tag,
                                     unsigned int format,
                                     struct snd_pcm_substream *substream)
{
        int chs;
        unsigned int dataDCC2, channel_id;
        int i;
        struct hdmi_spec *spec = codec->spec;
        struct hda_spdif_out *spdif;
        struct hdmi_spec_per_cvt *per_cvt;

        guard(mutex)(&codec->spdif_mutex);
        per_cvt = get_cvt(spec, 0);
        spdif = snd_hda_spdif_out_of_nid(codec, per_cvt->cvt_nid);

        chs = substream->runtime->channels;

        dataDCC2 = 0x2;

        /* turn off SPDIF once; otherwise the IEC958 bits won't be updated */
        if (codec->spdif_status_reset && (spdif->ctls & AC_DIG1_ENABLE))
                snd_hda_codec_write(codec,
                                nvhdmi_master_con_nid_7x,
                                0,
                                AC_VERB_SET_DIGI_CONVERT_1,
                                spdif->ctls & ~AC_DIG1_ENABLE & 0xff);

        /* set the stream id */
        snd_hda_codec_write(codec, nvhdmi_master_con_nid_7x, 0,
                        AC_VERB_SET_CHANNEL_STREAMID, (stream_tag << 4) | 0x0);

        /* set the stream format */
        snd_hda_codec_write(codec, nvhdmi_master_con_nid_7x, 0,
                        AC_VERB_SET_STREAM_FORMAT, format);

        /* turn on again (if needed) */
        /* enable and set the channel status audio/data flag */
        if (codec->spdif_status_reset && (spdif->ctls & AC_DIG1_ENABLE)) {
                snd_hda_codec_write(codec,
                                nvhdmi_master_con_nid_7x,
                                0,
                                AC_VERB_SET_DIGI_CONVERT_1,
                                spdif->ctls & 0xff);
                snd_hda_codec_write(codec,
                                nvhdmi_master_con_nid_7x,
                                0,
                                AC_VERB_SET_DIGI_CONVERT_2, dataDCC2);
        }

        for (i = 0; i < 4; i++) {
                if (chs == 2)
                        channel_id = 0;
                else
                        channel_id = i * 2;

                /* turn off SPDIF once;
                 *otherwise the IEC958 bits won't be updated
                 */
                if (codec->spdif_status_reset &&
                (spdif->ctls & AC_DIG1_ENABLE))
                        snd_hda_codec_write(codec,
                                nvhdmi_con_nids_7x[i],
                                0,
                                AC_VERB_SET_DIGI_CONVERT_1,
                                spdif->ctls & ~AC_DIG1_ENABLE & 0xff);
                /* set the stream id */
                snd_hda_codec_write(codec,
                                nvhdmi_con_nids_7x[i],
                                0,
                                AC_VERB_SET_CHANNEL_STREAMID,
                                (stream_tag << 4) | channel_id);
                /* set the stream format */
                snd_hda_codec_write(codec,
                                nvhdmi_con_nids_7x[i],
                                0,
                                AC_VERB_SET_STREAM_FORMAT,
                                format);
                /* turn on again (if needed) */
                /* enable and set the channel status audio/data flag */
                if (codec->spdif_status_reset &&
                (spdif->ctls & AC_DIG1_ENABLE)) {
                        snd_hda_codec_write(codec,
                                        nvhdmi_con_nids_7x[i],
                                        0,
                                        AC_VERB_SET_DIGI_CONVERT_1,
                                        spdif->ctls & 0xff);
                        snd_hda_codec_write(codec,
                                        nvhdmi_con_nids_7x[i],
                                        0,
                                        AC_VERB_SET_DIGI_CONVERT_2, dataDCC2);
                }
        }

        nvhdmi_8ch_7x_set_info_frame_parameters(codec, chs);

        return 0;
}

static const struct hda_pcm_stream nvhdmi_pcm_playback_8ch_7x = {
        .substreams = 1,
        .channels_min = 2,
        .channels_max = 8,
        .nid = nvhdmi_master_con_nid_7x,
        .rates = SUPPORTED_RATES,
        .maxbps = SUPPORTED_MAXBPS,
        .formats = SUPPORTED_FORMATS,
        .ops = {
                .open = snd_hda_hdmi_simple_pcm_open,
                .close = nvhdmi_8ch_7x_pcm_close,
                .prepare = nvhdmi_8ch_7x_pcm_prepare
        },
};

static int nvhdmi_mcp_build_pcms(struct hda_codec *codec)
{
        struct hdmi_spec *spec = codec->spec;
        int err;

        err = snd_hda_hdmi_simple_build_pcms(codec);
        if (!err && spec->multiout.max_channels == 8) {
                struct hda_pcm *info = get_pcm_rec(spec, 0);

                info->own_chmap = true;
        }
        return err;
}

static int nvhdmi_mcp_build_controls(struct hda_codec *codec)
{
        struct hdmi_spec *spec = codec->spec;
        struct hda_pcm *info;
        struct snd_pcm_chmap *chmap;
        int err;

        err = snd_hda_hdmi_simple_build_controls(codec);
        if (err < 0)
                return err;

        if (spec->multiout.max_channels != 8)
                return 0;

        /* add channel maps */
        info = get_pcm_rec(spec, 0);
        err = snd_pcm_add_chmap_ctls(info->pcm,
                                     SNDRV_PCM_STREAM_PLAYBACK,
                                     snd_pcm_alt_chmaps, 8, 0, &chmap);
        if (err < 0)
                return err;
        switch (codec->preset->vendor_id) {
        case 0x10de0002:
        case 0x10de0003:
        case 0x10de0005:
        case 0x10de0006:
                chmap->channel_mask = (1U << 2) | (1U << 8);
                break;
        case 0x10de0007:
                chmap->channel_mask = (1U << 2) | (1U << 6) | (1U << 8);
        }
        return 0;
}

static const unsigned int channels_2_6_8[] = {
        2, 6, 8
};

static const unsigned int channels_2_8[] = {
        2, 8
};

static const struct snd_pcm_hw_constraint_list hw_constraints_2_6_8_channels = {
        .count = ARRAY_SIZE(channels_2_6_8),
        .list = channels_2_6_8,
        .mask = 0,
};

static const struct snd_pcm_hw_constraint_list hw_constraints_2_8_channels = {
        .count = ARRAY_SIZE(channels_2_8),
        .list = channels_2_8,
        .mask = 0,
};

static int nvhdmi_mcp_probe(struct hda_codec *codec,
                            const struct hda_device_id *id)
{
        struct hdmi_spec *spec;
        int err;

        err = snd_hda_hdmi_simple_probe(codec, nvhdmi_master_con_nid_7x,
                                        nvhdmi_master_pin_nid_7x);
        if (err < 0)
                return err;

        /* override the PCM rates, etc, as the codec doesn't give full list */
        spec = codec->spec;
        spec->pcm_playback.rates = SUPPORTED_RATES;
        spec->pcm_playback.maxbps = SUPPORTED_MAXBPS;
        spec->pcm_playback.formats = SUPPORTED_FORMATS;
        spec->nv_dp_workaround = true;

        if (id->driver_data == MODEL_2CH)
                return 0;

        spec->multiout.max_channels = 8;
        spec->pcm_playback = nvhdmi_pcm_playback_8ch_7x;

        switch (codec->preset->vendor_id) {
        case 0x10de0002:
        case 0x10de0003:
        case 0x10de0005:
        case 0x10de0006:
                spec->hw_constraints_channels = &hw_constraints_2_8_channels;
                break;
        case 0x10de0007:
                spec->hw_constraints_channels = &hw_constraints_2_6_8_channels;
                break;
        default:
                break;
        }

        /* Initialize the audio infoframe channel mask and checksum to something
         * valid
         */
        nvhdmi_8ch_7x_set_info_frame_parameters(codec, 8);

        return 0;
}

static const struct hda_codec_ops nvhdmi_mcp_codec_ops = {
        .probe = nvhdmi_mcp_probe,
        .remove = snd_hda_hdmi_simple_remove,
        .build_pcms = nvhdmi_mcp_build_pcms,
        .build_controls = nvhdmi_mcp_build_controls,
        .init = nvhdmi_mcp_init,
        .unsol_event = snd_hda_hdmi_simple_unsol_event,
};

static const struct hda_device_id snd_hda_id_nvhdmi_mcp[] = {
        HDA_CODEC_ID_MODEL(0x10de0001, "MCP73 HDMI",    MODEL_2CH),
        HDA_CODEC_ID_MODEL(0x10de0002, "MCP77/78 HDMI", MODEL_8CH),
        HDA_CODEC_ID_MODEL(0x10de0003, "MCP77/78 HDMI", MODEL_8CH),
        HDA_CODEC_ID_MODEL(0x10de0004, "GPU 04 HDMI",   MODEL_8CH),
        HDA_CODEC_ID_MODEL(0x10de0005, "MCP77/78 HDMI", MODEL_8CH),
        HDA_CODEC_ID_MODEL(0x10de0006, "MCP77/78 HDMI", MODEL_8CH),
        HDA_CODEC_ID_MODEL(0x10de0007, "MCP79/7A HDMI", MODEL_8CH),
        HDA_CODEC_ID_MODEL(0x10de0067, "MCP67 HDMI",    MODEL_2CH),
        HDA_CODEC_ID_MODEL(0x10de8001, "MCP73 HDMI",    MODEL_2CH),
        HDA_CODEC_ID_MODEL(0x10de8067, "MCP67/68 HDMI", MODEL_2CH),
        {} /* terminator */
};
MODULE_DEVICE_TABLE(hdaudio, snd_hda_id_nvhdmi_mcp);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Legacy Nvidia HDMI HD-audio codec");
MODULE_IMPORT_NS("SND_HDA_CODEC_HDMI");

static struct hda_codec_driver nvhdmi_mcp_driver = {
        .id = snd_hda_id_nvhdmi_mcp,
        .ops = &nvhdmi_mcp_codec_ops,
};

module_hda_codec_driver(nvhdmi_mcp_driver);