root/drivers/gpu/drm/display/drm_hdmi_audio_helper.c
// SPDX-License-Identifier: MIT
/*
 * Copyright (c) 2024 Linaro Ltd
 */

#include <linux/export.h>
#include <linux/mutex.h>
#include <linux/of_graph.h>
#include <linux/platform_device.h>

#include <drm/drm_connector.h>
#include <drm/drm_device.h>
#include <drm/display/drm_hdmi_audio_helper.h>

#include <sound/hdmi-codec.h>

static int drm_connector_hdmi_audio_startup(struct device *dev, void *data)
{
        struct drm_connector *connector = data;
        const struct drm_connector_hdmi_audio_funcs *funcs =
                connector->hdmi_audio.funcs;

        if (funcs->startup)
                return funcs->startup(connector);

        return 0;
}

static int drm_connector_hdmi_audio_prepare(struct device *dev, void *data,
                                            struct hdmi_codec_daifmt *fmt,
                                            struct hdmi_codec_params *hparms)
{
        struct drm_connector *connector = data;
        const struct drm_connector_hdmi_audio_funcs *funcs =
                connector->hdmi_audio.funcs;

        return funcs->prepare(connector, fmt, hparms);
}

static void drm_connector_hdmi_audio_shutdown(struct device *dev, void *data)
{
        struct drm_connector *connector = data;
        const struct drm_connector_hdmi_audio_funcs *funcs =
                connector->hdmi_audio.funcs;

        return funcs->shutdown(connector);
}

static int drm_connector_hdmi_audio_mute_stream(struct device *dev, void *data,
                                                bool enable, int direction)
{
        struct drm_connector *connector = data;
        const struct drm_connector_hdmi_audio_funcs *funcs =
                connector->hdmi_audio.funcs;

        if (funcs->mute_stream)
                return funcs->mute_stream(connector, enable, direction);

        return -ENOTSUPP;
}

static int drm_connector_hdmi_audio_get_dai_id(struct snd_soc_component *comment,
                                               struct device_node *endpoint,
                                               void *data)
{
        struct drm_connector *connector = data;
        struct of_endpoint of_ep;
        int ret;

        if (connector->hdmi_audio.dai_port < 0)
                return -ENOTSUPP;

        ret = of_graph_parse_endpoint(endpoint, &of_ep);
        if (ret < 0)
                return ret;

        if (of_ep.port == connector->hdmi_audio.dai_port)
                return 0;

        return -EINVAL;
}

static int drm_connector_hdmi_audio_get_eld(struct device *dev, void *data,
                                            uint8_t *buf, size_t len)
{
        struct drm_connector *connector = data;

        mutex_lock(&connector->eld_mutex);
        memcpy(buf, connector->eld, min(sizeof(connector->eld), len));
        mutex_unlock(&connector->eld_mutex);

        return 0;
}

static int drm_connector_hdmi_audio_hook_plugged_cb(struct device *dev,
                                                    void *data,
                                                    hdmi_codec_plugged_cb fn,
                                                    struct device *codec_dev)
{
        struct drm_connector *connector = data;

        mutex_lock(&connector->hdmi_audio.lock);

        connector->hdmi_audio.plugged_cb = fn;
        connector->hdmi_audio.plugged_cb_dev = codec_dev;

        if (fn)
                fn(codec_dev, connector->hdmi_audio.last_state);

        mutex_unlock(&connector->hdmi_audio.lock);

        return 0;
}

void drm_connector_hdmi_audio_plugged_notify(struct drm_connector *connector,
                                             bool plugged)
{
        mutex_lock(&connector->hdmi_audio.lock);

        connector->hdmi_audio.last_state = plugged;

        if (connector->hdmi_audio.plugged_cb &&
            connector->hdmi_audio.plugged_cb_dev)
                connector->hdmi_audio.plugged_cb(connector->hdmi_audio.plugged_cb_dev,
                                                 connector->hdmi_audio.last_state);

        mutex_unlock(&connector->hdmi_audio.lock);
}
EXPORT_SYMBOL(drm_connector_hdmi_audio_plugged_notify);

static const struct hdmi_codec_ops drm_connector_hdmi_audio_ops = {
        .audio_startup = drm_connector_hdmi_audio_startup,
        .prepare = drm_connector_hdmi_audio_prepare,
        .audio_shutdown = drm_connector_hdmi_audio_shutdown,
        .mute_stream = drm_connector_hdmi_audio_mute_stream,
        .get_eld = drm_connector_hdmi_audio_get_eld,
        .get_dai_id = drm_connector_hdmi_audio_get_dai_id,
        .hook_plugged_cb = drm_connector_hdmi_audio_hook_plugged_cb,
};

/**
 * drm_connector_hdmi_audio_init - Initialize HDMI Codec device for the DRM connector
 * @connector: A pointer to the connector to allocate codec for
 * @hdmi_codec_dev: device to be used as a parent for the HDMI Codec
 * @funcs: callbacks for this HDMI Codec
 * @max_i2s_playback_channels: maximum number of playback I2S channels
 * @i2s_formats: set of I2S formats (use 0 for a bus-specific set)
 * @spdif_playback: set if HDMI codec has S/PDIF playback port
 * @dai_port: sound DAI port, -1 if it is not enabled
 *
 * Create a HDMI codec device to be used with the specified connector.
 *
 * Returns:
 * Zero on success, error code on failure.
 */
int drm_connector_hdmi_audio_init(struct drm_connector *connector,
                                  struct device *hdmi_codec_dev,
                                  const struct drm_connector_hdmi_audio_funcs *funcs,
                                  unsigned int max_i2s_playback_channels,
                                  u64 i2s_formats,
                                  bool spdif_playback,
                                  int dai_port)
{
        struct hdmi_codec_pdata codec_pdata = {
                .ops = &drm_connector_hdmi_audio_ops,
                .max_i2s_channels = max_i2s_playback_channels,
                .i2s = !!max_i2s_playback_channels,
                .i2s_formats = i2s_formats,
                .spdif = spdif_playback,
                .no_i2s_capture = true,
                .no_spdif_capture = true,
                .data = connector,
        };
        struct platform_device *pdev;

        if (!funcs ||
            !funcs->prepare ||
            !funcs->shutdown)
                return -EINVAL;

        connector->hdmi_audio.funcs = funcs;
        connector->hdmi_audio.dai_port = dai_port;

        pdev = platform_device_register_data(hdmi_codec_dev,
                                             HDMI_CODEC_DRV_NAME,
                                             PLATFORM_DEVID_AUTO,
                                             &codec_pdata, sizeof(codec_pdata));
        if (IS_ERR(pdev))
                return PTR_ERR(pdev);

        connector->hdmi_audio.codec_pdev = pdev;

        return 0;
}
EXPORT_SYMBOL(drm_connector_hdmi_audio_init);