From 051392bfdc6dc54563ed9909cc1164e8d734af43 Mon Sep 17 00:00:00 2001
From: David Turner <david.turner@raspberrypi.com>
Date: Wed, 28 Feb 2024 10:55:39 +0000
Subject: [PATCH 0930/1085] vc4: Add jack detection to HDMI audio driver

Add ALSA jack detection to the vc4-hdmi audio driver so userspace knows
when to add/remove HDMI audio devices.

Signed-off-by: David Turner <david.turner@raspberrypi.com>
---
 drivers/gpu/drm/vc4/vc4_hdmi.c | 61 ++++++++++++++++++++++++++++++++--
 drivers/gpu/drm/vc4/vc4_hdmi.h | 27 +++++++++++++++
 2 files changed, 86 insertions(+), 2 deletions(-)

--- a/drivers/gpu/drm/vc4/vc4_hdmi.c
+++ b/drivers/gpu/drm/vc4/vc4_hdmi.c
@@ -50,6 +50,7 @@
 #include <linux/reset.h>
 #include <sound/dmaengine_pcm.h>
 #include <sound/hdmi-codec.h>
+#include <sound/jack.h>
 #include <sound/pcm_drm_eld.h>
 #include <sound/pcm_params.h>
 #include <sound/soc.h>
@@ -421,7 +422,7 @@ static void vc4_hdmi_handle_hotplug(stru
 				    enum drm_connector_status status)
 {
 	struct drm_connector *connector = &vc4_hdmi->connector;
-	struct edid *edid;
+	struct edid *edid = NULL;
 	int ret;
 
 	/*
@@ -439,12 +440,25 @@ static void vc4_hdmi_handle_hotplug(stru
 	 * the lock for now.
 	 */
 
+	if (status != connector_status_disconnected)
+		edid = drm_get_edid(connector, vc4_hdmi->ddc);
+
+	/*
+	 * Report plugged/unplugged events to ALSA jack detection.  Do this
+	 * *after* EDID probing, otherwise userspace might try to bring up
+	 * audio before it's ready.
+	 */
+	mutex_lock(&vc4_hdmi->update_plugged_status_lock);
+	if (vc4_hdmi->plugged_cb && vc4_hdmi->codec_dev)
+		vc4_hdmi->plugged_cb(vc4_hdmi->codec_dev,
+				     status != connector_status_disconnected);
+	mutex_unlock(&vc4_hdmi->update_plugged_status_lock);
+
 	if (status == connector_status_disconnected) {
 		cec_phys_addr_invalidate(vc4_hdmi->cec_adap);
 		return;
 	}
 
-	edid = drm_get_edid(connector, vc4_hdmi->ddc);
 	if (!edid)
 		return;
 
@@ -2697,8 +2711,23 @@ static int vc4_hdmi_audio_get_eld(struct
 	return 0;
 }
 
+static int vc4_hdmi_audio_hook_plugged_cb(struct device *dev, void *data,
+					   hdmi_codec_plugged_cb fn,
+					   struct device *codec_dev)
+{
+	struct vc4_hdmi *vc4_hdmi = dev_get_drvdata(dev);
+
+	mutex_lock(&vc4_hdmi->update_plugged_status_lock);
+	vc4_hdmi->plugged_cb = fn;
+	vc4_hdmi->codec_dev = codec_dev;
+	mutex_unlock(&vc4_hdmi->update_plugged_status_lock);
+
+	return 0;
+}
+
 static const struct hdmi_codec_ops vc4_hdmi_codec_ops = {
 	.get_eld = vc4_hdmi_audio_get_eld,
+	.hook_plugged_cb = vc4_hdmi_audio_hook_plugged_cb,
 	.prepare = vc4_hdmi_audio_prepare,
 	.audio_shutdown = vc4_hdmi_audio_shutdown,
 	.audio_startup = vc4_hdmi_audio_startup,
@@ -2718,6 +2747,22 @@ static void vc4_hdmi_audio_codec_release
 	vc4_hdmi->audio.codec_pdev = NULL;
 }
 
+static int vc4_hdmi_codec_init(struct snd_soc_pcm_runtime *rtd)
+{
+	struct vc4_hdmi *vc4_hdmi = snd_soc_card_get_drvdata(rtd->card);
+	struct snd_soc_component *component = asoc_rtd_to_codec(rtd, 0)->component;
+	int ret;
+
+	ret = snd_soc_card_jack_new(rtd->card, "HDMI Jack", SND_JACK_LINEOUT,
+				    &vc4_hdmi->hdmi_jack);
+	if (ret) {
+		dev_err(rtd->dev, "HDMI Jack creation failed: %d\n", ret);
+		return ret;
+	}
+
+	return snd_soc_component_set_jack(component, &vc4_hdmi->hdmi_jack, NULL);
+}
+
 static int vc4_hdmi_audio_init(struct vc4_hdmi *vc4_hdmi)
 {
 	const struct vc4_hdmi_register *mai_data =
@@ -2847,6 +2892,8 @@ static int vc4_hdmi_audio_init(struct vc
 	dai_link->codecs->name = dev_name(&codec_pdev->dev);
 	dai_link->platforms->name = dev_name(dev);
 
+	dai_link->init = vc4_hdmi_codec_init;
+
 	card->dai_link = dai_link;
 	card->num_links = 1;
 	card->name = vc4_hdmi->variant->card_name;
@@ -3707,6 +3754,8 @@ static int vc4_hdmi_bind(struct device *
 	if (ret)
 		return ret;
 
+	mutex_init(&vc4_hdmi->update_plugged_status_lock);
+
 	spin_lock_init(&vc4_hdmi->hw_lock);
 	INIT_DELAYED_WORK(&vc4_hdmi->scrambling_work, vc4_hdmi_scrambling_wq);
 
@@ -3819,8 +3868,16 @@ err_put_runtime_pm:
 	return ret;
 }
 
+static void vc4_hdmi_unbind(struct device *dev, struct device *master, void *data)
+{
+	struct vc4_hdmi *vc4_hdmi = dev_get_drvdata(dev);
+
+	mutex_destroy(&vc4_hdmi->update_plugged_status_lock);
+}
+
 static const struct component_ops vc4_hdmi_ops = {
 	.bind   = vc4_hdmi_bind,
+	.unbind = vc4_hdmi_unbind,
 };
 
 static int vc4_hdmi_dev_probe(struct platform_device *pdev)
--- a/drivers/gpu/drm/vc4/vc4_hdmi.h
+++ b/drivers/gpu/drm/vc4/vc4_hdmi.h
@@ -2,8 +2,10 @@
 #define _VC4_HDMI_H_
 
 #include <drm/drm_connector.h>
+#include <linux/mutex.h>
 #include <media/cec.h>
 #include <sound/dmaengine_pcm.h>
+#include <sound/hdmi-codec.h>
 #include <sound/soc.h>
 
 #include "vc4_drv.h"
@@ -228,6 +230,31 @@ struct vc4_hdmi {
 	 * for use outside of KMS hooks. Protected by @mutex.
 	 */
 	enum vc4_hdmi_output_format output_format;
+
+	/**
+	 * @plugged_cb: Callback provided by hdmi-codec to indicate that an
+	 * HDMI hotplug occurred and jack state should be updated. Protected by
+	 * @update_plugged_status_lock.
+	 */
+	hdmi_codec_plugged_cb plugged_cb;
+
+	/**
+	 * @plugged_cb: Context for plugged_cb. Protected by
+	 * @update_plugged_status_lock.
+	 */
+	struct device *codec_dev;
+
+	/**
+	 * @update_plugged_status_lock: Prevents a race condition where an HDMI
+	 * hotplug might occur between @plugged_cb and @codec_dev being set.
+	 */
+	struct mutex update_plugged_status_lock;
+
+	/**
+	 * @hdmi_jack: Represents the connection state of the HDMI plug, for
+	 * ALSA jack detection.
+	 */
+	struct snd_soc_jack hdmi_jack;
 };
 
 #define connector_to_vc4_hdmi(_connector)				\
