drm: Add component-aware simple encoder

This is a simple DRM encoder that gets its connector timings information
from a OF subnode in the device tree and exposes that as a "discovered"
panel. It can be used together with component-based DRM drivers in an
emulated environment where no real encoder or connector hardware exists
and the display output is configured outside the kernel.

Signed-off-by: Liviu Dudau <Liviu.Dudau@arm.com>
[Backported to gem5's 4.3 kernel]
Signed-off-by: Andreas Sandberg <andreas.sandberg@arm.com>
[Ported to gem5's 4.9 kernel. Added dep. on VIDEOMODE_HELPERS.]
Signed-off-by: Andreas Sandberg <andreas.sandberg@arm.com>
[Ported to gem5's 4.14 kernel.]
Signed-off-by: Andreas Sandberg <andreas.sandberg@arm.com>
diff --git a/drivers/gpu/drm/Kconfig b/drivers/gpu/drm/Kconfig
index 83cb2a8..ae5bb46 100644
--- a/drivers/gpu/drm/Kconfig
+++ b/drivers/gpu/drm/Kconfig
@@ -203,6 +203,15 @@
 	  as used by Mesa's software renderer for enhanced performance.
 	  If M is selected the module will be called vgem.
 
+config DRM_VIRT_ENCODER
+	tristate "Virtual OF-based encoder"
+	depends on DRM && OF
+	select VIDEOMODE_HELPERS
+	help
+	  Choose this option to get a virtual encoder and its associated
+	  connector that will use the device tree to read the display
+	  timings information. If M is selected the module will be called
+	  drm_vencoder.
 
 source "drivers/gpu/drm/exynos/Kconfig"
 
diff --git a/drivers/gpu/drm/Makefile b/drivers/gpu/drm/Makefile
index 8ce0703..3f09ab4 100644
--- a/drivers/gpu/drm/Makefile
+++ b/drivers/gpu/drm/Makefile
@@ -47,6 +47,9 @@
 
 CFLAGS_drm_trace_points.o := -I$(src)
 
+drm_vencoder-y := drm_virtual_encoder.o
+obj-$(CONFIG_DRM_VIRT_ENCODER) += drm_vencoder.o
+
 obj-$(CONFIG_DRM)	+= drm.o
 obj-$(CONFIG_DRM_MIPI_DSI) += drm_mipi_dsi.o
 obj-$(CONFIG_DRM_ARM)	+= arm/
diff --git a/drivers/gpu/drm/drm_virtual_encoder.c b/drivers/gpu/drm/drm_virtual_encoder.c
new file mode 100644
index 0000000..d82cdc1
--- /dev/null
+++ b/drivers/gpu/drm/drm_virtual_encoder.c
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2016 ARM Limited
+ * Author: Liviu Dudau <Liviu.Dudau@arm.com>
+ *
+ * Dummy encoder and connector that use the OF to "discover" the attached
+ * display timings. Can be used in situations where the encoder and connector's
+ * functionality are emulated and no setup steps are needed, or to describe
+ * attached panels for which no driver exists but can be used without
+ * additional hardware setup.
+ *
+ * The encoder also uses the component framework so that it can be a quick
+ * replacement for existing drivers when testing in an emulated environment.
+ *
+ * This file is subject to the terms and conditions of the GNU General Public
+ * License.  See the file COPYING in the main directory of this archive
+ * for more details.
+ *
+ */
+
+#include <drm/drmP.h>
+#include <drm/drm_crtc.h>
+#include <drm/drm_atomic_helper.h>
+#include <drm/drm_crtc_helper.h>
+#include <drm/drm_of.h>
+#include <linux/component.h>
+#include <video/display_timing.h>
+#include <video/of_display_timing.h>
+#include <video/videomode.h>
+
+struct drm_virt_priv {
+	struct drm_connector connector;
+	struct drm_encoder encoder;
+	struct display_timings *timings;
+};
+
+#define connector_to_drm_virt_priv(x) \
+	container_of(x, struct drm_virt_priv, connector)
+
+#define encoder_to_drm_virt_priv(x) \
+	container_of(x, struct drm_virt_priv, encoder)
+
+static void drm_virtcon_destroy(struct drm_connector *connector)
+{
+	struct drm_virt_priv *conn = connector_to_drm_virt_priv(connector);
+
+	drm_connector_cleanup(connector);
+	display_timings_release(conn->timings);
+}
+
+static enum drm_connector_status
+drm_virtcon_detect(struct drm_connector *connector, bool force)
+{
+	return connector_status_connected;
+}
+
+static const struct drm_connector_funcs drm_virtcon_funcs = {
+	.reset = drm_atomic_helper_connector_reset,
+	.detect	= drm_virtcon_detect,
+	.fill_modes = drm_helper_probe_single_connector_modes,
+	.destroy = drm_virtcon_destroy,
+	.atomic_duplicate_state = drm_atomic_helper_connector_duplicate_state,
+	.atomic_destroy_state = drm_atomic_helper_connector_destroy_state,
+};
+
+static int drm_virtcon_get_modes(struct drm_connector *connector)
+{
+	struct drm_virt_priv *conn = connector_to_drm_virt_priv(connector);
+	struct display_timings *timings = conn->timings;
+	int i;
+
+	for (i = 0; i < timings->num_timings; i++) {
+		struct drm_display_mode *mode = drm_mode_create(connector->dev);
+		struct videomode vm;
+
+		if (videomode_from_timings(timings, &vm, i))
+			break;
+
+		drm_display_mode_from_videomode(&vm, mode);
+		mode->type = DRM_MODE_TYPE_DRIVER;
+		if (timings->native_mode == i)
+			mode->type = DRM_MODE_TYPE_PREFERRED;
+
+		drm_mode_set_name(mode);
+		drm_mode_probed_add(connector, mode);
+	}
+
+	return i;
+}
+
+static int drm_virtcon_mode_valid(struct drm_connector *connector,
+				   struct drm_display_mode *mode)
+{
+	return MODE_OK;
+}
+
+struct drm_encoder *drm_virtcon_best_encoder(struct drm_connector *connector)
+{
+	struct drm_virt_priv *priv = connector_to_drm_virt_priv(connector);
+
+	return &priv->encoder;
+}
+
+struct drm_encoder *
+drm_virtcon_atomic_best_encoder(struct drm_connector *connector,
+				 struct drm_connector_state *connector_state)
+{
+	struct drm_virt_priv *priv = connector_to_drm_virt_priv(connector);
+
+	return &priv->encoder;
+}
+
+static const struct drm_connector_helper_funcs drm_virtcon_helper_funcs = {
+	.get_modes = drm_virtcon_get_modes,
+	.mode_valid = drm_virtcon_mode_valid,
+	.best_encoder = drm_virtcon_best_encoder,
+	.atomic_best_encoder = drm_virtcon_atomic_best_encoder,
+};
+
+static void drm_vencoder_destroy(struct drm_encoder *encoder)
+{
+	drm_encoder_cleanup(encoder);
+}
+
+static const struct drm_encoder_funcs drm_vencoder_funcs = {
+	.destroy = drm_vencoder_destroy,
+};
+
+static void drm_vencoder_dpms(struct drm_encoder *encoder, int mode)
+{
+	/* nothing needed */
+}
+
+static bool drm_vencoder_mode_fixup(struct drm_encoder *encoder,
+				    const struct drm_display_mode *mode,
+				    struct drm_display_mode *adjusted_mode)
+{
+	/* nothing needed */
+	return true;
+}
+
+static void drm_vencoder_prepare(struct drm_encoder *encoder)
+{
+	drm_vencoder_dpms(encoder, DRM_MODE_DPMS_OFF);
+}
+
+static void drm_vencoder_commit(struct drm_encoder *encoder)
+{
+	drm_vencoder_dpms(encoder, DRM_MODE_DPMS_ON);
+}
+
+static void drm_vencoder_mode_set(struct drm_encoder *encoder,
+				  struct drm_display_mode *mode,
+				  struct drm_display_mode *adjusted_mode)
+{
+	/* nothing needed */
+}
+
+static const struct drm_encoder_helper_funcs drm_vencoder_helper_funcs = {
+	.dpms		= drm_vencoder_dpms,
+	.mode_fixup	= drm_vencoder_mode_fixup,
+	.prepare	= drm_vencoder_prepare,
+	.commit		= drm_vencoder_commit,
+	.mode_set	= drm_vencoder_mode_set,
+};
+
+static int drm_vencoder_bind(struct device *dev, struct device *master,
+			     void *data)
+{
+	struct drm_encoder *encoder;
+	struct drm_virt_priv *con;
+	struct drm_connector *connector;
+	struct drm_device *drm = data;
+	u32 crtcs = 0;
+	int ret;
+
+	con = devm_kzalloc(dev, sizeof(*con), GFP_KERNEL);
+	if (!con)
+		return -ENOMEM;
+
+	dev_set_drvdata(dev, con);
+	connector = &con->connector;
+	encoder = &con->encoder;
+
+	if (dev->of_node) {
+		struct drm_bridge *bridge;
+		crtcs = drm_of_find_possible_crtcs(drm, dev->of_node);
+		bridge = of_drm_find_bridge(dev->of_node);
+		if (bridge) {
+			ret = drm_bridge_attach(encoder, bridge, NULL);
+			if (ret) {
+				DRM_ERROR("Failed to initialize bridge\n");
+				return ret;
+			}
+		}
+		con->timings = of_get_display_timings(dev->of_node);
+		if (!con->timings) {
+			dev_err(dev, "failed to get display panel timings\n");
+			return ENXIO;
+		}
+	}
+
+	/* If no CRTCs were found, fall back to the old encoder's behaviour */
+	if (crtcs == 0) {
+		dev_warn(dev, "Falling back to first CRTC\n");
+		crtcs = 1 << 0;
+	}
+
+	encoder->possible_crtcs = crtcs ? crtcs : 1;
+	encoder->possible_clones = 0;
+
+	ret = drm_encoder_init(drm, encoder, &drm_vencoder_funcs,
+			       DRM_MODE_ENCODER_VIRTUAL, NULL);
+	if (ret)
+		goto encoder_init_err;
+
+	drm_encoder_helper_add(encoder, &drm_vencoder_helper_funcs);
+
+	/* bogus values, pretend we're a 24" screen for DPI calculations */
+	connector->display_info.width_mm = 519;
+	connector->display_info.height_mm = 324;
+	connector->interlace_allowed = false;
+	connector->doublescan_allowed = false;
+	connector->polled = 0;
+
+	ret = drm_connector_init(drm, connector, &drm_virtcon_funcs,
+				 DRM_MODE_CONNECTOR_VIRTUAL);
+	if (ret)
+		goto connector_init_err;
+
+	drm_connector_helper_add(connector, &drm_virtcon_helper_funcs);
+
+	drm_connector_register(connector);
+
+	ret = drm_mode_connector_attach_encoder(connector, encoder);
+	if (ret)
+		goto attach_err;
+
+	return ret;
+
+attach_err:
+	drm_connector_unregister(connector);
+	drm_connector_cleanup(connector);
+connector_init_err:
+	drm_encoder_cleanup(encoder);
+encoder_init_err:
+	display_timings_release(con->timings);
+
+	return ret;
+};
+
+static void drm_vencoder_unbind(struct device *dev, struct device *master,
+				void *data)
+{
+	struct drm_virt_priv *con = dev_get_drvdata(dev);
+
+	drm_connector_unregister(&con->connector);
+	drm_connector_cleanup(&con->connector);
+	drm_encoder_cleanup(&con->encoder);
+	display_timings_release(con->timings);
+}
+
+static const struct component_ops drm_vencoder_ops = {
+	.bind = drm_vencoder_bind,
+	.unbind = drm_vencoder_unbind,
+};
+
+static int drm_vencoder_probe(struct platform_device *pdev)
+{
+	return component_add(&pdev->dev, &drm_vencoder_ops);
+}
+
+static int drm_vencoder_remove(struct platform_device *pdev)
+{
+	component_del(&pdev->dev, &drm_vencoder_ops);
+	return 0;
+}
+
+static const struct of_device_id drm_vencoder_of_match[] = {
+	{ .compatible = "drm,virtual-encoder", },
+	{},
+};
+MODULE_DEVICE_TABLE(of, drm_vencoder_of_match);
+
+static struct platform_driver drm_vencoder_driver = {
+	.probe = drm_vencoder_probe,
+	.remove = drm_vencoder_remove,
+	.driver = {
+		.name = "drm_vencoder",
+		.of_match_table = drm_vencoder_of_match,
+	},
+};
+
+module_platform_driver(drm_vencoder_driver);
+
+MODULE_AUTHOR("Liviu Dudau");
+MODULE_DESCRIPTION("Virtual DRM Encoder");
+MODULE_LICENSE("GPL v2");
diff --git a/include/drm/drm_connector.h b/include/drm/drm_connector.h
index ea8da40..3a5ada0 100644
--- a/include/drm/drm_connector.h
+++ b/include/drm/drm_connector.h
@@ -1070,4 +1070,7 @@
 #define drm_for_each_connector_iter(connector, iter) \
 	while ((connector = drm_connector_list_iter_next(iter)))
 
+
+extern int drm_create_virtual_connector(struct drm_device *dev);
+
 #endif