--- /dev/null
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * UCSI DisplayPort Alternate Mode Support
+ *
+ * Copyright (C) 2018, Intel Corporation
+ * Author: Heikki Krogerus <heikki.krogerus@linux.intel.com>
+ */
+
+#include <linux/usb/typec_dp.h>
+#include <linux/usb/pd_vdo.h>
+
+#include "ucsi.h"
+
+#define UCSI_CMD_SET_NEW_CAM(_con_num_, _enter_, _cam_, _am_)          \
+        (UCSI_SET_NEW_CAM | ((_con_num_) << 16) | ((_enter_) << 23) |  \
+         ((_cam_) << 24) | ((u64)(_am_) << 32))
+
+struct ucsi_dp {
+       struct typec_displayport_data data;
+       struct ucsi_connector *con;
+       struct typec_altmode *alt;
+       struct work_struct work;
+       int offset;
+
+       bool override;
+       bool initialized;
+
+       u32 header;
+       u32 *vdo_data;
+       u8 vdo_size;
+};
+
+/*
+ * Note. Alternate mode control is optional feature in UCSI. It means that even
+ * if the system supports alternate modes, the OS may not be aware of them.
+ *
+ * In most cases however, the OS will be able to see the supported alternate
+ * modes, but it may still not be able to configure them, not even enter or exit
+ * them. That is because UCSI defines alt mode details and alt mode "overriding"
+ * as separate options.
+ *
+ * In case alt mode details are supported, but overriding is not, the driver
+ * will still display the supported pin assignments and configuration, but any
+ * changes the user attempts to do will lead into failure with return value of
+ * -EOPNOTSUPP.
+ */
+
+static int ucsi_displayport_enter(struct typec_altmode *alt)
+{
+       struct ucsi_dp *dp = typec_altmode_get_drvdata(alt);
+       struct ucsi_control ctrl;
+       u8 cur = 0;
+       int ret;
+
+       mutex_lock(&dp->con->lock);
+
+       if (!dp->override && dp->initialized) {
+               const struct typec_altmode *p = typec_altmode_get_partner(alt);
+
+               dev_warn(&p->dev,
+                        "firmware doesn't support alternate mode overriding\n");
+               mutex_unlock(&dp->con->lock);
+               return -EOPNOTSUPP;
+       }
+
+       UCSI_CMD_GET_CURRENT_CAM(ctrl, dp->con->num);
+       ret = ucsi_send_command(dp->con->ucsi, &ctrl, &cur, sizeof(cur));
+       if (ret < 0) {
+               if (dp->con->ucsi->ppm->data->version > 0x0100) {
+                       mutex_unlock(&dp->con->lock);
+                       return ret;
+               }
+               cur = 0xff;
+       }
+
+       if (cur != 0xff) {
+               mutex_unlock(&dp->con->lock);
+               return -EBUSY;
+       }
+
+       /*
+        * We can't send the New CAM command yet to the PPM as it needs the
+        * configuration value as well. Pretending that we have now entered the
+        * mode, and letting the alt mode driver continue.
+        */
+
+       dp->header = VDO(USB_TYPEC_DP_SID, 1, CMD_ENTER_MODE);
+       dp->header |= VDO_OPOS(USB_TYPEC_DP_MODE);
+       dp->header |= VDO_CMDT(CMDT_RSP_ACK);
+
+       dp->vdo_data = NULL;
+       dp->vdo_size = 1;
+
+       schedule_work(&dp->work);
+
+       mutex_unlock(&dp->con->lock);
+
+       return 0;
+}
+
+static int ucsi_displayport_exit(struct typec_altmode *alt)
+{
+       struct ucsi_dp *dp = typec_altmode_get_drvdata(alt);
+       struct ucsi_control ctrl;
+       int ret = 0;
+
+       mutex_lock(&dp->con->lock);
+
+       if (!dp->override) {
+               const struct typec_altmode *p = typec_altmode_get_partner(alt);
+
+               dev_warn(&p->dev,
+                        "firmware doesn't support alternate mode overriding\n");
+               ret = -EOPNOTSUPP;
+               goto out_unlock;
+       }
+
+       ctrl.raw_cmd = UCSI_CMD_SET_NEW_CAM(dp->con->num, 0, dp->offset, 0);
+       ret = ucsi_send_command(dp->con->ucsi, &ctrl, NULL, 0);
+       if (ret < 0)
+               goto out_unlock;
+
+       dp->header = VDO(USB_TYPEC_DP_SID, 1, CMD_EXIT_MODE);
+       dp->header |= VDO_OPOS(USB_TYPEC_DP_MODE);
+       dp->header |= VDO_CMDT(CMDT_RSP_ACK);
+
+       dp->vdo_data = NULL;
+       dp->vdo_size = 1;
+
+       schedule_work(&dp->work);
+
+out_unlock:
+       mutex_unlock(&dp->con->lock);
+
+       return ret;
+}
+
+/*
+ * We do not actually have access to the Status Update VDO, so we have to guess
+ * things.
+ */
+static int ucsi_displayport_status_update(struct ucsi_dp *dp)
+{
+       u32 cap = dp->alt->vdo;
+
+       dp->data.status = DP_STATUS_ENABLED;
+
+       /*
+        * If pin assignement D is supported, claiming always
+        * that Multi-function is preferred.
+        */
+       if (DP_CAP_CAPABILITY(cap) & DP_CAP_UFP_D) {
+               dp->data.status |= DP_STATUS_CON_UFP_D;
+
+               if (DP_CAP_UFP_D_PIN_ASSIGN(cap) & BIT(DP_PIN_ASSIGN_D))
+                       dp->data.status |= DP_STATUS_PREFER_MULTI_FUNC;
+       } else {
+               dp->data.status |= DP_STATUS_CON_DFP_D;
+
+               if (DP_CAP_DFP_D_PIN_ASSIGN(cap) & BIT(DP_PIN_ASSIGN_D))
+                       dp->data.status |= DP_STATUS_PREFER_MULTI_FUNC;
+       }
+
+       dp->vdo_data = &dp->data.status;
+       dp->vdo_size = 2;
+
+       return 0;
+}
+
+static int ucsi_displayport_configure(struct ucsi_dp *dp)
+{
+       u32 pins = DP_CONF_GET_PIN_ASSIGN(dp->data.conf);
+       struct ucsi_control ctrl;
+
+       if (!dp->override)
+               return 0;
+
+       ctrl.raw_cmd = UCSI_CMD_SET_NEW_CAM(dp->con->num, 1, dp->offset, pins);
+
+       return ucsi_send_command(dp->con->ucsi, &ctrl, NULL, 0);
+}
+
+static int ucsi_displayport_vdm(struct typec_altmode *alt,
+                               u32 header, const u32 *data, int count)
+{
+       struct ucsi_dp *dp = typec_altmode_get_drvdata(alt);
+       int cmd_type = PD_VDO_CMDT(header);
+       int cmd = PD_VDO_CMD(header);
+
+       mutex_lock(&dp->con->lock);
+
+       if (!dp->override && dp->initialized) {
+               const struct typec_altmode *p = typec_altmode_get_partner(alt);
+
+               dev_warn(&p->dev,
+                        "firmware doesn't support alternate mode overriding\n");
+               mutex_unlock(&dp->con->lock);
+               return -EOPNOTSUPP;
+       }
+
+       switch (cmd_type) {
+       case CMDT_INIT:
+               dp->header = VDO(USB_TYPEC_DP_SID, 1, cmd);
+               dp->header |= VDO_OPOS(USB_TYPEC_DP_MODE);
+
+               switch (cmd) {
+               case DP_CMD_STATUS_UPDATE:
+                       if (ucsi_displayport_status_update(dp))
+                               dp->header |= VDO_CMDT(CMDT_RSP_NAK);
+                       else
+                               dp->header |= VDO_CMDT(CMDT_RSP_ACK);
+                       break;
+               case DP_CMD_CONFIGURE:
+                       dp->data.conf = *data;
+                       if (ucsi_displayport_configure(dp)) {
+                               dp->header |= VDO_CMDT(CMDT_RSP_NAK);
+                       } else {
+                               dp->header |= VDO_CMDT(CMDT_RSP_ACK);
+                               if (dp->initialized)
+                                       ucsi_altmode_update_active(dp->con);
+                               else
+                                       dp->initialized = true;
+                       }
+                       break;
+               default:
+                       dp->header |= VDO_CMDT(CMDT_RSP_ACK);
+                       break;
+               }
+
+               schedule_work(&dp->work);
+               break;
+       default:
+               break;
+       }
+
+       mutex_unlock(&dp->con->lock);
+
+       return 0;
+}
+
+static const struct typec_altmode_ops ucsi_displayport_ops = {
+       .enter = ucsi_displayport_enter,
+       .exit = ucsi_displayport_exit,
+       .vdm = ucsi_displayport_vdm,
+};
+
+static void ucsi_displayport_work(struct work_struct *work)
+{
+       struct ucsi_dp *dp = container_of(work, struct ucsi_dp, work);
+       int ret;
+
+       mutex_lock(&dp->con->lock);
+
+       ret = typec_altmode_vdm(dp->alt, dp->header,
+                               dp->vdo_data, dp->vdo_size);
+       if (ret)
+               dev_err(&dp->alt->dev, "VDM 0x%x failed\n", dp->header);
+
+       dp->vdo_data = NULL;
+       dp->vdo_size = 0;
+       dp->header = 0;
+
+       mutex_unlock(&dp->con->lock);
+}
+
+void ucsi_displayport_remove_partner(struct typec_altmode *alt)
+{
+       struct ucsi_dp *dp;
+
+       if (!alt)
+               return;
+
+       dp = typec_altmode_get_drvdata(alt);
+       dp->data.conf = 0;
+       dp->data.status = 0;
+       dp->initialized = false;
+}
+
+struct typec_altmode *ucsi_register_displayport(struct ucsi_connector *con,
+                                               bool override, int offset,
+                                               struct typec_altmode_desc *desc)
+{
+       u8 all_assignments = BIT(DP_PIN_ASSIGN_C) | BIT(DP_PIN_ASSIGN_D) |
+                            BIT(DP_PIN_ASSIGN_E);
+       struct typec_altmode *alt;
+       struct ucsi_dp *dp;
+
+       /* We can't rely on the firmware with the capabilities. */
+       desc->vdo |= DP_CAP_DP_SIGNALING | DP_CAP_RECEPTACLE;
+
+       /* Claiming that we support all pin assignments */
+       desc->vdo |= all_assignments << 8;
+       desc->vdo |= all_assignments << 16;
+
+       alt = typec_port_register_altmode(con->port, desc);
+       if (IS_ERR(alt))
+               return alt;
+
+       dp = devm_kzalloc(&alt->dev, sizeof(*dp), GFP_KERNEL);
+       if (!dp) {
+               typec_unregister_altmode(alt);
+               return ERR_PTR(-ENOMEM);
+       }
+
+       INIT_WORK(&dp->work, ucsi_displayport_work);
+       dp->override = override;
+       dp->offset = offset;
+       dp->con = con;
+       dp->alt = alt;
+
+       alt->ops = &ucsi_displayport_ops;
+       typec_altmode_set_drvdata(alt, dp);
+
+       return alt;
+}