root/sys/dev/pci/drm/apple/dcp.c
// SPDX-License-Identifier: GPL-2.0-only OR MIT
/* Copyright 2021 Alyssa Rosenzweig <alyssa@rosenzweig.io> */

#include <linux/align.h>
#include <linux/apple-mailbox.h>
#include <linux/bitmap.h>
#include <linux/clk.h>
#include <linux/completion.h>
#include <linux/component.h>
#include <linux/delay.h>
#include <linux/dma-mapping.h>
#include <linux/gpio/consumer.h>
#include <linux/iommu.h>
#include <linux/jiffies.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/of_address.h>
#include <linux/of_device.h>
#include <linux/of_platform.h>
#include <linux/slab.h>
#include <linux/soc/apple/rtkit.h>
#include <linux/string.h>
#include <linux/workqueue.h>

#include <drm/drm_fb_dma_helper.h>
#include <drm/drm_fourcc.h>
#include <drm/drm_framebuffer.h>
#include <drm/drm_module.h>
#include <drm/drm_probe_helper.h>
#include <drm/drm_vblank.h>

#include "afk.h"
#include "dcp.h"
#include "dcp-internal.h"
#include "iomfb.h"
#include "parser.h"
#include "trace.h"

#define APPLE_DCP_COPROC_CPU_CONTROL     0x44
#define APPLE_DCP_COPROC_CPU_CONTROL_RUN BIT(4)

#define DCP_BOOT_TIMEOUT msecs_to_jiffies(1000)

static bool show_notch;
module_param(show_notch, bool, 0644);
MODULE_PARM_DESC(show_notch, "Use the full display height and shows the notch");

/* HACK: moved here to avoid circular dependency between apple_drv and dcp */
void dcp_drm_crtc_vblank(struct apple_crtc *crtc)
{
        unsigned long flags;

        spin_lock_irqsave(&crtc->base.dev->event_lock, flags);
        if (crtc->event) {
                drm_crtc_send_vblank_event(&crtc->base, crtc->event);
                crtc->event = NULL;
        }
        spin_unlock_irqrestore(&crtc->base.dev->event_lock, flags);
}

void dcp_set_dimensions(struct apple_dcp *dcp)
{
        int i;
        int width_mm = dcp->width_mm;
        int height_mm = dcp->height_mm;

        if (width_mm == 0 || height_mm == 0) {
                width_mm = dcp->panel.width_mm;
                height_mm = dcp->panel.height_mm;
        }

        /* Set the connector info */
        if (dcp->connector) {
                struct drm_connector *connector = &dcp->connector->base;

                mutex_lock(&connector->dev->mode_config.mutex);
                connector->display_info.width_mm = width_mm;
                connector->display_info.height_mm = height_mm;
                mutex_unlock(&connector->dev->mode_config.mutex);
        }

        /*
         * Fix up any probed modes. Modes are created when parsing
         * TimingElements, dimensions are calculated when parsing
         * DisplayAttributes, and TimingElements may be sent first
         */
        for (i = 0; i < dcp->nr_modes; ++i) {
                dcp->modes[i].mode.width_mm = width_mm;
                dcp->modes[i].mode.height_mm = height_mm;
        }
}

bool dcp_has_panel(struct apple_dcp *dcp)
{
        return dcp->panel.width_mm > 0;
}

/*
 * Helper to send a DRM vblank event. We do not know how call swap_submit_dcp
 * without surfaces. To avoid timeouts in drm_atomic_helper_wait_for_vblanks
 * send a vblank event via a workqueue.
 */
static void dcp_delayed_vblank(struct work_struct *work)
{
        struct apple_dcp *dcp;

        dcp = container_of(work, struct apple_dcp, vblank_wq);
        mdelay(5);
        dcp_drm_crtc_vblank(dcp->crtc);
}

static void dcp_recv_msg(void *cookie, u8 endpoint, u64 message)
{
        struct apple_dcp *dcp = cookie;

        trace_dcp_recv_msg(dcp, endpoint, message);

        switch (endpoint) {
        case IOMFB_ENDPOINT:
                return iomfb_recv_msg(dcp, message);
        case SYSTEM_ENDPOINT:
                afk_receive_message(dcp->systemep, message);
                return;
        case DISP0_ENDPOINT:
                afk_receive_message(dcp->ibootep, message);
                return;
        case DPTX_ENDPOINT:
                afk_receive_message(dcp->dptxep, message);
                return;
        default:
                WARN(endpoint, "unknown DCP endpoint %hhu\n", endpoint);
        }
}

static void dcp_rtk_crashed(void *cookie)
{
        struct apple_dcp *dcp = cookie;

        dcp->crashed = true;
        dev_err(dcp->dev, "DCP has crashed\n");
        if (dcp->connector) {
                dcp->connector->connected = 0;
                schedule_work(&dcp->connector->hotplug_wq);
        }
        complete(&dcp->start_done);
}

static int dcp_rtk_shmem_setup(void *cookie, struct apple_rtkit_shmem *bfr)
{
        struct apple_dcp *dcp = cookie;

        if (bfr->iova) {
                struct iommu_domain *domain =
                        iommu_get_domain_for_dev(dcp->dev);
                phys_addr_t phy_addr;

                if (!domain)
                        return -ENOMEM;

                // TODO: get map from device-tree
                phy_addr = iommu_iova_to_phys(domain, bfr->iova);
                if (!phy_addr)
                        return -ENOMEM;

                // TODO: verify phy_addr, cache attribute
                bfr->buffer = memremap(phy_addr, bfr->size, MEMREMAP_WB);
                if (!bfr->buffer)
                        return -ENOMEM;

                bfr->is_mapped = true;
                dev_info(dcp->dev,
                         "shmem_setup: iova: %lx -> pa: %lx -> iomem: %lx\n",
                         (uintptr_t)bfr->iova, (uintptr_t)phy_addr,
                         (uintptr_t)bfr->buffer);
        } else {
                bfr->buffer = dma_alloc_coherent(dcp->dev, bfr->size,
                                                 &bfr->iova, GFP_KERNEL);
                if (!bfr->buffer)
                        return -ENOMEM;

                dev_info(dcp->dev, "shmem_setup: iova: %lx, buffer: %lx\n",
                         (uintptr_t)bfr->iova, (uintptr_t)bfr->buffer);
        }

        return 0;
}

static void dcp_rtk_shmem_destroy(void *cookie, struct apple_rtkit_shmem *bfr)
{
        struct apple_dcp *dcp = cookie;

        if (bfr->is_mapped)
                memunmap(bfr->buffer);
        else
                dma_free_coherent(dcp->dev, bfr->size, bfr->buffer, bfr->iova);
}

static struct apple_rtkit_ops rtkit_ops = {
        .crashed = dcp_rtk_crashed,
        .recv_message = dcp_recv_msg,
        .shmem_setup = dcp_rtk_shmem_setup,
        .shmem_destroy = dcp_rtk_shmem_destroy,
};

void dcp_send_message(struct apple_dcp *dcp, u8 endpoint, u64 message)
{
        trace_dcp_send_msg(dcp, endpoint, message);
        apple_rtkit_send_message(dcp->rtk, endpoint, message, NULL,
                                 true);
}

int dcp_crtc_atomic_check(struct drm_crtc *crtc, struct drm_atomic_state *state)
{
        struct platform_device *pdev = to_apple_crtc(crtc)->dcp;
        struct apple_dcp *dcp = platform_get_drvdata(pdev);
        struct drm_plane_state *new_state;
        struct drm_plane *plane;
        struct drm_crtc_state *crtc_state;
        int plane_idx, plane_count = 0;
        bool needs_modeset;

        if (dcp->crashed)
                return -EINVAL;

        crtc_state = drm_atomic_get_new_crtc_state(state, crtc);

        needs_modeset = drm_atomic_crtc_needs_modeset(crtc_state) || !dcp->valid_mode;
        if (!needs_modeset && !dcp->connector->connected) {
                dev_err(dcp->dev, "crtc_atomic_check: disconnected but no modeset\n");
                return -EINVAL;
        }

        for_each_new_plane_in_state(state, plane, new_state, plane_idx) {
                /* skip planes not for this crtc */
                if (new_state->crtc != crtc)
                        continue;

                plane_count += 1;
        }

        if (plane_count > DCP_MAX_PLANES) {
                dev_err(dcp->dev, "crtc_atomic_check: Blend supports only 2 layers!\n");
                return -EINVAL;
        }

        return 0;
}
EXPORT_SYMBOL_GPL(dcp_crtc_atomic_check);

int dcp_get_connector_type(struct platform_device *pdev)
{
        struct apple_dcp *dcp = platform_get_drvdata(pdev);

        return (dcp->connector_type);
}
EXPORT_SYMBOL_GPL(dcp_get_connector_type);

#define DPTX_CONNECT_TIMEOUT msecs_to_jiffies(1000)

static int dcp_dptx_connect(struct apple_dcp *dcp, u32 port)
{
        int ret = 0;

        if (!dcp->phy) {
                dev_warn(dcp->dev, "dcp_dptx_connect: missing phy\n");
                return -ENODEV;
        }
        dev_info(dcp->dev, "%s(port=%d)\n", __func__, port);

        mutex_lock(&dcp->hpd_mutex);
        if (!dcp->dptxport[port].enabled) {
                dev_warn(dcp->dev, "dcp_dptx_connect: dptx service for port %d not enabled\n", port);
                ret = -ENODEV;
                goto out_unlock;
        }

        if (dcp->dptxport[port].connected)
                goto out_unlock;

        reinit_completion(&dcp->dptxport[port].linkcfg_completion);
        dcp->dptxport[port].atcphy = dcp->phy;
        dptxport_connect(dcp->dptxport[port].service, 0, dcp->dptx_phy, dcp->dptx_die);
        dptxport_request_display(dcp->dptxport[port].service);
        dcp->dptxport[port].connected = true;

        mutex_unlock(&dcp->hpd_mutex);
        ret = wait_for_completion_timeout(&dcp->dptxport[port].linkcfg_completion,
                                    DPTX_CONNECT_TIMEOUT);
        if (ret < 0)
                dev_warn(dcp->dev, "dcp_dptx_connect: port %d link complete failed:%d\n",
                         port, ret);
        else
                dev_dbg(dcp->dev, "dcp_dptx_connect: waited %d ms for link\n",
                        jiffies_to_msecs(DPTX_CONNECT_TIMEOUT - ret));

        usleep_range(5, 10);

        return 0;

out_unlock:
        mutex_unlock(&dcp->hpd_mutex);
        return ret;
}

static int dcp_dptx_disconnect(struct apple_dcp *dcp, u32 port)
{
        dev_info(dcp->dev, "%s(port=%d)\n", __func__, port);

        mutex_lock(&dcp->hpd_mutex);
        if (dcp->dptxport[port].enabled && dcp->dptxport[port].connected) {
                dptxport_release_display(dcp->dptxport[port].service);
                dcp->dptxport[port].connected = false;
        }
        mutex_unlock(&dcp->hpd_mutex);

        return 0;
}

static irqreturn_t dcp_dp2hdmi_hpd(int irq, void *data)
{
        struct apple_dcp *dcp = data;
        bool connected = gpiod_get_value_cansleep(dcp->hdmi_hpd);

        /* do nothing on disconnect and trust that dcp detects it itself.
         * Parallel disconnect HPDs result drm disabling the CRTC even when it
         * should not.
         * The interrupt should be changed to rising but for now the disconnect
         * IRQs might be helpful for debugging.
         */
        dev_info(dcp->dev, "DP2HDMI HPD irq, connected:%d\n", connected);

        if (connected)
                dcp_dptx_connect(dcp, 0);

        return IRQ_HANDLED;
}

void dcp_link(struct platform_device *pdev, struct apple_crtc *crtc,
              struct apple_connector *connector)
{
        struct apple_dcp *dcp = platform_get_drvdata(pdev);

        dcp->crtc = crtc;
        dcp->connector = connector;
}
EXPORT_SYMBOL_GPL(dcp_link);

int dcp_start(struct platform_device *pdev)
{
        struct apple_dcp *dcp = platform_get_drvdata(pdev);
        int ret;

        init_completion(&dcp->start_done);

        /* start RTKit endpoints */
        ret = systemep_init(dcp);
        if (ret)
                dev_warn(dcp->dev, "Failed to start system endpoint: %d\n", ret);

        if (dcp->phy && dcp->fw_compat >= DCP_FIRMWARE_V_13_5) {
                ret = ibootep_init(dcp);
                if (ret)
                        dev_warn(dcp->dev, "Failed to start IBOOT endpoint: %d\n",
                                 ret);

                ret = dptxep_init(dcp);
                if (ret)
                        dev_warn(dcp->dev, "Failed to start DPTX endpoint: %d\n",
                                 ret);
                else if (dcp->dptxport[0].enabled) {
                        bool connected;
                        /* force disconnect on start - necessary if the display
                         * is already up from m1n1
                         */
                        dptxport_set_hpd(dcp->dptxport[0].service, false);
                        dptxport_release_display(dcp->dptxport[0].service);
                        usleep_range(10 * USEC_PER_MSEC, 25 * USEC_PER_MSEC);

                        connected = gpiod_get_value_cansleep(dcp->hdmi_hpd);
                        dev_info(dcp->dev, "%s: DP2HDMI HPD connected:%d\n", __func__, connected);

                        // necessary on j473/j474 but not on j314c
                        if (connected)
                                dcp_dptx_connect(dcp, 0);
                }
        } else if (dcp->phy)
                dev_warn(dcp->dev, "OS firmware incompatible with dptxport EP\n");

        ret = iomfb_start_rtkit(dcp);
        if (ret)
                dev_err(dcp->dev, "Failed to start IOMFB endpoint: %d\n", ret);

        return ret;
}
EXPORT_SYMBOL(dcp_start);

static int dcp_enable_dp2hdmi_hpd(struct apple_dcp *dcp)
{
        if (dcp->hdmi_hpd_irq)
                enable_irq(dcp->hdmi_hpd_irq);

        return 0;
}

int dcp_wait_ready(struct platform_device *pdev, u64 timeout)
{
        struct apple_dcp *dcp = platform_get_drvdata(pdev);
        int ret;

        if (dcp->crashed)
                return -ENODEV;
        if (dcp->active)
                return dcp_enable_dp2hdmi_hpd(dcp);;
        if (timeout <= 0)
                return -ETIMEDOUT;

        ret = wait_for_completion_timeout(&dcp->start_done, timeout);
        if (ret < 0)
                return ret;

        if (dcp->crashed)
                return -ENODEV;

        if (dcp->active)
                dcp_enable_dp2hdmi_hpd(dcp);

        return dcp->active ? 0 : -ETIMEDOUT;
}
EXPORT_SYMBOL(dcp_wait_ready);

static void __maybe_unused dcp_sleep(struct apple_dcp *dcp)
{
        switch (dcp->fw_compat) {
        case DCP_FIRMWARE_V_12_3:
                iomfb_sleep_v12_3(dcp);
                break;
        case DCP_FIRMWARE_V_13_5:
                iomfb_sleep_v13_3(dcp);
                break;
        default:
                WARN_ONCE(true, "Unexpected firmware version: %u\n", dcp->fw_compat);
                break;
        }
}

void dcp_poweron(struct platform_device *pdev)
{
        struct apple_dcp *dcp = platform_get_drvdata(pdev);

        if (dcp->hdmi_hpd) {
                bool connected = gpiod_get_value_cansleep(dcp->hdmi_hpd);
                dev_info(dcp->dev, "%s: DP2HDMI HPD connected:%d\n", __func__, connected);

                if (connected)
                        dcp_dptx_connect(dcp, 0);
        }

        switch (dcp->fw_compat) {
        case DCP_FIRMWARE_V_12_3:
                iomfb_poweron_v12_3(dcp);
                break;
        case DCP_FIRMWARE_V_13_5:
                iomfb_poweron_v13_3(dcp);
                break;
        default:
                WARN_ONCE(true, "Unexpected firmware version: %u\n", dcp->fw_compat);
                break;
        }
}
EXPORT_SYMBOL(dcp_poweron);

void dcp_poweroff(struct platform_device *pdev)
{
        struct apple_dcp *dcp = platform_get_drvdata(pdev);

        switch (dcp->fw_compat) {
        case DCP_FIRMWARE_V_12_3:
                iomfb_poweroff_v12_3(dcp);
                break;
        case DCP_FIRMWARE_V_13_5:
                iomfb_poweroff_v13_3(dcp);
                break;
        default:
                WARN_ONCE(true, "Unexpected firmware version: %u\n", dcp->fw_compat);
                break;
        }

        if (dcp->hdmi_hpd) {
                bool connected = gpiod_get_value_cansleep(dcp->hdmi_hpd);
                if (!connected)
                        dcp_dptx_disconnect(dcp, 0);
        }
}
EXPORT_SYMBOL(dcp_poweroff);

static void dcp_work_register_backlight(struct work_struct *work)
{
        int ret;
        struct apple_dcp *dcp;

        dcp = container_of(work, struct apple_dcp, bl_register_wq);

        mutex_lock(&dcp->bl_register_mutex);
        if (dcp->brightness.bl_dev)
                goto out_unlock;

        /* try to register backlight device, */
        ret = dcp_backlight_register(dcp);
        if (ret) {
                dev_err(dcp->dev, "Unable to register backlight device\n");
                dcp->brightness.maximum = 0;
        }

out_unlock:
        mutex_unlock(&dcp->bl_register_mutex);
}

static void dcp_work_update_backlight(struct work_struct *work)
{
        struct apple_dcp *dcp;

        dcp = container_of(work, struct apple_dcp, bl_update_wq);

        dcp_backlight_update(dcp);
}

static int dcp_create_piodma_iommu_dev(struct apple_dcp *dcp)
{
        int ret;
        struct device_node *node = of_get_child_by_name(dcp->dev->of_node, "piodma");

        if (!node)
                return dev_err_probe(dcp->dev, -ENODEV,
                                     "Failed to get piodma child DT node\n");

        dcp->piodma = of_platform_device_create(node, NULL, dcp->dev);
        if (!dcp->piodma) {
                of_node_put(node);
                return dev_err_probe(dcp->dev, -ENODEV, "Failed to gcreate piodma pdev for %pOF\n", node);
        }

        ret = dma_set_mask_and_coherent(&dcp->piodma->dev, DMA_BIT_MASK(42));
        if (ret)
                goto err_destroy_pdev;

        ret = of_dma_configure(&dcp->piodma->dev, node, true);
        if (ret) {
                ret = dev_err_probe(dcp->dev, ret,
                        "Failed to configure IOMMU child DMA\n");
                goto err_destroy_pdev;
        }
        of_node_put(node);

        dcp->iommu_dom = iommu_domain_alloc(&platform_bus_type);
        if (!dcp->iommu_dom) {
                ret = -ENOMEM;
                goto err_destroy_pdev;
        }

        ret = iommu_attach_device(dcp->iommu_dom, &dcp->piodma->dev);
        if (ret) {
                ret = dev_err_probe(dcp->dev, ret,
                                        "Failed to attach IOMMU child domain\n");
                goto err_free_domain;
        }

        return 0;
err_free_domain:
        iommu_domain_free(dcp->iommu_dom);
err_destroy_pdev:
        of_node_put(node);
        of_platform_device_destroy(&dcp->piodma->dev, NULL);
        return ret;
}

static int dcp_get_bw_scratch_reg(struct apple_dcp *dcp, u32 expected)
{
        struct of_phandle_args ph_args;
        u32 addr_idx, disp_idx, offset;
        int ret;

        ret = of_parse_phandle_with_args(dcp->dev->of_node, "apple,bw-scratch",
                                   "#apple,bw-scratch-cells", 0, &ph_args);
        if (ret < 0) {
                dev_err(dcp->dev, "Failed to read 'apple,bw-scratch': %d\n", ret);
                return ret;
        }

        if (ph_args.args_count != 3) {
                dev_err(dcp->dev, "Unexpected 'apple,bw-scratch' arg count %d\n",
                        ph_args.args_count);
                ret = -EINVAL;
                goto err_of_node_put;
        }

        addr_idx = ph_args.args[0];
        disp_idx = ph_args.args[1];
        offset = ph_args.args[2];

        if (disp_idx != expected || disp_idx >= MAX_DISP_REGISTERS) {
                dev_err(dcp->dev, "Unexpected disp_reg value in 'apple,bw-scratch': %d\n",
                        disp_idx);
                ret = -EINVAL;
                goto err_of_node_put;
        }

        ret = of_address_to_resource(ph_args.np, addr_idx, &dcp->disp_bw_scratch_res);
        if (ret < 0) {
                dev_err(dcp->dev, "Failed to get 'apple,bw-scratch' resource %d from %pOF\n",
                        addr_idx, ph_args.np);
                goto err_of_node_put;
        }
        if (offset > resource_size(&dcp->disp_bw_scratch_res) - 4) {
                ret = -EINVAL;
                goto err_of_node_put;
        }

        dcp->disp_registers[disp_idx] = &dcp->disp_bw_scratch_res;
        dcp->disp_bw_scratch_index = disp_idx;
        dcp->disp_bw_scratch_offset = offset;
        ret = 0;

err_of_node_put:
        of_node_put(ph_args.np);
        return ret;
}

static int dcp_get_bw_doorbell_reg(struct apple_dcp *dcp, u32 expected)
{
        struct of_phandle_args ph_args;
        u32 addr_idx, disp_idx;
        int ret;

        ret = of_parse_phandle_with_args(dcp->dev->of_node, "apple,bw-doorbell",
                                   "#apple,bw-doorbell-cells", 0, &ph_args);
        if (ret < 0) {
                dev_err(dcp->dev, "Failed to read 'apple,bw-doorbell': %d\n", ret);
                return ret;
        }

        if (ph_args.args_count != 2) {
                dev_err(dcp->dev, "Unexpected 'apple,bw-doorbell' arg count %d\n",
                        ph_args.args_count);
                ret = -EINVAL;
                goto err_of_node_put;
        }

        addr_idx = ph_args.args[0];
        disp_idx = ph_args.args[1];

        if (disp_idx != expected || disp_idx >= MAX_DISP_REGISTERS) {
                dev_err(dcp->dev, "Unexpected disp_reg value in 'apple,bw-doorbell': %d\n",
                        disp_idx);
                ret = -EINVAL;
                goto err_of_node_put;
        }

        ret = of_address_to_resource(ph_args.np, addr_idx, &dcp->disp_bw_doorbell_res);
        if (ret < 0) {
                dev_err(dcp->dev, "Failed to get 'apple,bw-doorbell' resource %d from %pOF\n",
                        addr_idx, ph_args.np);
                goto err_of_node_put;
        }
        dcp->disp_bw_doorbell_index = disp_idx;
        dcp->disp_registers[disp_idx] = &dcp->disp_bw_doorbell_res;
        ret = 0;

err_of_node_put:
        of_node_put(ph_args.np);
        return ret;
}

static int dcp_get_disp_regs(struct apple_dcp *dcp)
{
        struct platform_device *pdev = to_platform_device(dcp->dev);
        int count = pdev->num_resources - 1;
        int i, ret;

        if (count <= 0 || count > MAX_DISP_REGISTERS)
                return -EINVAL;

        for (i = 0; i < count; ++i) {
                dcp->disp_registers[i] =
                        platform_get_resource(pdev, IORESOURCE_MEM, 1 + i);
        }

        /* load pmgr bandwidth scratch resource and offset */
        ret = dcp_get_bw_scratch_reg(dcp, count);
        if (ret < 0)
                return ret;
        count += 1;

        /* load pmgr bandwidth doorbell resource if present (only on t8103) */
        if (of_property_present(dcp->dev->of_node, "apple,bw-doorbell")) {
                ret = dcp_get_bw_doorbell_reg(dcp, count);
                if (ret < 0)
                        return ret;
                count += 1;
        }

        dcp->nr_disp_registers = count;
        return 0;
}

#define DCP_FW_VERSION_MIN_LEN  3
#define DCP_FW_VERSION_MAX_LEN  5
#define DCP_FW_VERSION_STR_LEN  (DCP_FW_VERSION_MAX_LEN * 4)

static int dcp_read_fw_version(struct device *dev, const char *name,
                               char *version_str)
{
        u32 ver[DCP_FW_VERSION_MAX_LEN];
        int len_str;
        int len;

        len = of_property_read_variable_u32_array(dev->of_node, name, ver,
                                                  DCP_FW_VERSION_MIN_LEN,
                                                  DCP_FW_VERSION_MAX_LEN);

        switch (len) {
        case 3:
                len_str = scnprintf(version_str, DCP_FW_VERSION_STR_LEN,
                                    "%d.%d.%d", ver[0], ver[1], ver[2]);
                break;
        case 4:
                len_str = scnprintf(version_str, DCP_FW_VERSION_STR_LEN,
                                    "%d.%d.%d.%d", ver[0], ver[1], ver[2],
                                    ver[3]);
                break;
        case 5:
                len_str = scnprintf(version_str, DCP_FW_VERSION_STR_LEN,
                                    "%d.%d.%d.%d.%d", ver[0], ver[1], ver[2],
                                    ver[3], ver[4]);
                break;
        default:
                len_str = strscpy(version_str, "UNKNOWN",
                                  DCP_FW_VERSION_STR_LEN);
                if (len >= 0)
                        len = -EOVERFLOW;
                break;
        }

        if (len_str >= DCP_FW_VERSION_STR_LEN)
                dev_warn(dev, "'%s' truncated: '%s'\n", name, version_str);

        return len;
}

static enum dcp_firmware_version dcp_check_firmware_version(struct device *dev)
{
        char compat_str[DCP_FW_VERSION_STR_LEN];
        char fw_str[DCP_FW_VERSION_STR_LEN];
        int ret;

        /* firmware version is just informative */
        dcp_read_fw_version(dev, "apple,firmware-version", fw_str);

        ret = dcp_read_fw_version(dev, "apple,firmware-compat", compat_str);
        if (ret < 0) {
                dev_err(dev, "Could not read 'apple,firmware-compat': %d\n", ret);
                return DCP_FIRMWARE_UNKNOWN;
        }

        if (strncmp(compat_str, "12.3.0", sizeof(compat_str)) == 0)
                return DCP_FIRMWARE_V_12_3;
        /*
         * m1n1 reports firmware version 13.5 as compatible with 13.3. This is
         * only true for the iomfb endpoint. The interface for the dptx-port
         * endpoint changed between 13.3 and 13.5. The driver will only support
         * firmware 13.5. Check the actual firmware version for compat version
         * 13.3 until m1n1 reports 13.5 as "firmware-compat".
         */
        else if ((strncmp(compat_str, "13.3.0", sizeof(compat_str)) == 0) &&
                 (strncmp(fw_str, "13.5.0", sizeof(compat_str)) == 0))
                return DCP_FIRMWARE_V_13_5;
        else if (strncmp(compat_str, "13.5.0", sizeof(compat_str)) == 0)
                return DCP_FIRMWARE_V_13_5;

        dev_err(dev, "DCP firmware-compat %s (FW: %s) is not supported\n",
                compat_str, fw_str);

        return DCP_FIRMWARE_UNKNOWN;
}

static int dcp_comp_bind(struct device *dev, struct device *main, void *data)
{
        struct device_node *panel_np;
        struct apple_dcp *dcp = dev_get_drvdata(dev);
        u32 cpu_ctrl;
        int ret;

        ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(42));
        if (ret)
                return ret;

        dcp->coproc_reg = devm_platform_ioremap_resource_byname(to_platform_device(dev), "coproc");
        if (IS_ERR(dcp->coproc_reg))
                return PTR_ERR(dcp->coproc_reg);

        of_property_read_u32(dev->of_node, "apple,dcp-index",
                                           &dcp->index);
        of_property_read_u32(dev->of_node, "apple,dptx-phy",
                                           &dcp->dptx_phy);
        of_property_read_u32(dev->of_node, "apple,dptx-die",
                                           &dcp->dptx_die);
        if (dcp->index || dcp->dptx_phy || dcp->dptx_die)
                dev_info(dev, "DCP index:%u dptx target phy: %u dptx die: %u\n",
                         dcp->index, dcp->dptx_phy, dcp->dptx_die);
        rw_init(&dcp->hpd_mutex, "aplhpd");

        if (!show_notch)
                ret = of_property_read_u32(dev->of_node, "apple,notch-height",
                                           &dcp->notch_height);

        if (dcp->notch_height > MAX_NOTCH_HEIGHT)
                dcp->notch_height = MAX_NOTCH_HEIGHT;
        if (dcp->notch_height > 0)
                dev_info(dev, "Detected display with notch of %u pixel\n", dcp->notch_height);

        /* initialize brightness scale to a sensible default to avoid divide by 0*/
        dcp->brightness.scale = 65536;
        panel_np = of_get_compatible_child(dev->of_node, "apple,panel-mini-led");
        if (panel_np)
                dcp->panel.has_mini_led = true;
        else
                panel_np = of_get_compatible_child(dev->of_node, "apple,panel");

        if (panel_np) {
                const char height_prop[2][16] = { "adj-height-mm", "height-mm" };

                if (of_device_is_available(panel_np)) {
                        ret = of_property_read_u32(panel_np, "apple,max-brightness",
                                                   &dcp->brightness.maximum);
                        if (ret)
                                dev_err(dev, "Missing property 'apple,max-brightness'\n");
                }

                of_property_read_u32(panel_np, "width-mm", &dcp->panel.width_mm);
                /* use adjusted height as long as the notch is hidden */
                of_property_read_u32(panel_np, height_prop[!dcp->notch_height],
                                     &dcp->panel.height_mm);

                of_node_put(panel_np);
                dcp->connector_type = DRM_MODE_CONNECTOR_eDP;
                INIT_WORK(&dcp->bl_register_wq, dcp_work_register_backlight);
                rw_init(&dcp->bl_register_mutex, "dcpbl");
                INIT_WORK(&dcp->bl_update_wq, dcp_work_update_backlight);
        } else if (of_property_match_string(dev->of_node, "apple,connector-type", "HDMI-A") >= 0)
                dcp->connector_type = DRM_MODE_CONNECTOR_HDMIA;
        else if (of_property_match_string(dev->of_node, "apple,connector-type", "DP") >= 0)
                dcp->connector_type = DRM_MODE_CONNECTOR_DisplayPort;
        else if (of_property_match_string(dev->of_node, "apple,connector-type", "USB-C") >= 0)
                dcp->connector_type = DRM_MODE_CONNECTOR_USB;
        else
                dcp->connector_type = DRM_MODE_CONNECTOR_Unknown;

        ret = dcp_create_piodma_iommu_dev(dcp);
        if (ret)
                return dev_err_probe(dev, ret,
                                "Failed to created PIODMA iommu child device");

        ret = dcp_get_disp_regs(dcp);
        if (ret) {
                dev_err(dev, "failed to find display registers\n");
                return ret;
        }

        dcp->clk = devm_clk_get(dev, NULL);
        if (IS_ERR(dcp->clk))
                return dev_err_probe(dev, PTR_ERR(dcp->clk),
                                     "Unable to find clock\n");

        bitmap_zero(dcp->memdesc_map, DCP_MAX_MAPPINGS);
        // TDOD: mem_desc IDs start at 1, for simplicity just skip '0' entry
        set_bit(0, dcp->memdesc_map);

        INIT_WORK(&dcp->vblank_wq, dcp_delayed_vblank);

        dcp->swapped_out_fbs =
                (struct list_head)LIST_HEAD_INIT(dcp->swapped_out_fbs);

        cpu_ctrl =
                readl_relaxed(dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL);
        writel_relaxed(cpu_ctrl | APPLE_DCP_COPROC_CPU_CONTROL_RUN,
                       dcp->coproc_reg + APPLE_DCP_COPROC_CPU_CONTROL);

        dcp->rtk = devm_apple_rtkit_init(dev, dcp, "mbox", 0, &rtkit_ops);
        if (IS_ERR(dcp->rtk))
                return dev_err_probe(dev, PTR_ERR(dcp->rtk),
                                     "Failed to initialize RTKit\n");

        ret = apple_rtkit_wake(dcp->rtk);
        if (ret)
                return dev_err_probe(dev, ret,
                                     "Failed to boot RTKit: %d\n", ret);
        return ret;
}

/*
 * We need to shutdown DCP before tearing down the display subsystem. Otherwise
 * the DCP will crash and briefly flash a green screen of death.
 */
static void dcp_comp_unbind(struct device *dev, struct device *main, void *data)
{
        struct apple_dcp *dcp = dev_get_drvdata(dev);

        if (dcp->hdmi_hpd_irq)
                disable_irq(dcp->hdmi_hpd_irq);

        if (dcp && dcp->shmem)
                iomfb_shutdown(dcp);

        if (dcp->piodma) {
                iommu_detach_device(dcp->iommu_dom, &dcp->piodma->dev);
                iommu_domain_free(dcp->iommu_dom);
                /* TODO: the piodma platform device has to be destroyed but
                 *       doing so leads to all kind of breakage.
                 */
                // of_platform_device_destroy(&dcp->piodma->dev, NULL);
                dcp->piodma = NULL;
        }

        devm_clk_put(dev, dcp->clk);
        dcp->clk = NULL;
}

static const struct component_ops dcp_comp_ops = {
        .bind   = dcp_comp_bind,
        .unbind = dcp_comp_unbind,
};

static int dcp_platform_probe(struct platform_device *pdev)
{
        enum dcp_firmware_version fw_compat;
        struct device *dev = &pdev->dev;
        struct apple_dcp *dcp;
        u32 mux_index;

        fw_compat = dcp_check_firmware_version(dev);
        if (fw_compat == DCP_FIRMWARE_UNKNOWN)
                return -ENODEV;

        /* Check for "apple,bw-scratch" to avoid probing appledrm with outdated
         * device trees. This prevents replacing simpledrm and ending up without
         * display.
         */
        if (!of_property_present(dev->of_node, "apple,bw-scratch"))
                return dev_err_probe(dev, -ENODEV, "Incompatible devicetree! "
                        "Use devicetree matching this kernel.\n");

        dcp = devm_kzalloc(dev, sizeof(*dcp), GFP_KERNEL);
        if (!dcp)
                return -ENOMEM;

        dcp->fw_compat = fw_compat;
        dcp->dev = dev;
        dcp->hw = *(struct apple_dcp_hw_data *)of_device_get_match_data(dev);

        platform_set_drvdata(pdev, dcp);

        dcp->phy = devm_phy_optional_get(dev, "dp-phy");
        if (IS_ERR(dcp->phy)) {
                dev_err(dev, "Failed to get dp-phy: %ld\n", PTR_ERR(dcp->phy));
                return PTR_ERR(dcp->phy);
        }
        if (dcp->phy) {
                int ret;
                /*
                 * Request DP2HDMI related GPIOs as optional for DP-altmode
                 * compatibility. J180D misses a dp2hdmi-pwren GPIO in the
                 * template ADT. TODO: check device ADT
                 */
                dcp->hdmi_hpd = devm_gpiod_get_optional(dev, "hdmi-hpd", GPIOD_IN);
                if (IS_ERR(dcp->hdmi_hpd))
                        return PTR_ERR(dcp->hdmi_hpd);
                if (dcp->hdmi_hpd) {
                        int irq = gpiod_to_irq(dcp->hdmi_hpd);
                        if (irq < 0) {
                                dev_err(dev, "failed to translate HDMI hpd GPIO to IRQ\n");
                                return irq;
                        }
                        dcp->hdmi_hpd_irq = irq;

                        ret = devm_request_threaded_irq(dev, dcp->hdmi_hpd_irq,
                                                NULL, dcp_dp2hdmi_hpd,
                                                IRQF_ONESHOT | IRQF_NO_AUTOEN |
                                                IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING,
                                                "dp2hdmi-hpd-irq", dcp);
                        if (ret < 0) {
                                dev_err(dev, "failed to request HDMI hpd irq %d: %d\n",
                                        irq, ret);
                                return ret;
                        }
                }

                /*
                 * Power DP2HDMI on as it is required for the HPD irq.
                 * TODO: check if one is sufficient for the hpd to save power
                 *       on battery powered Macbooks.
                 */
                dcp->hdmi_pwren = devm_gpiod_get_optional(dev, "hdmi-pwren", GPIOD_OUT_HIGH);
                if (IS_ERR(dcp->hdmi_pwren))
                        return PTR_ERR(dcp->hdmi_pwren);

                dcp->dp2hdmi_pwren = devm_gpiod_get_optional(dev, "dp2hdmi-pwren", GPIOD_OUT_HIGH);
                if (IS_ERR(dcp->dp2hdmi_pwren))
                        return PTR_ERR(dcp->dp2hdmi_pwren);

                ret = of_property_read_u32(dev->of_node, "mux-index", &mux_index);
                if (!ret) {
                        dcp->xbar = devm_mux_control_get(dev, "dp-xbar");
                        if (IS_ERR(dcp->xbar)) {
                                dev_err(dev, "Failed to get dp-xbar: %ld\n", PTR_ERR(dcp->xbar));
                                return PTR_ERR(dcp->xbar);
                        }
                        ret = mux_control_select(dcp->xbar, mux_index);
                        if (ret)
                                dev_warn(dev, "mux_control_select failed: %d\n", ret);
                }
        }

        return component_add(&pdev->dev, &dcp_comp_ops);
}

#ifdef __linux__

static int dcp_platform_remove(struct platform_device *pdev)
{
        component_del(&pdev->dev, &dcp_comp_ops);

        return 0;
}

static void dcp_platform_shutdown(struct platform_device *pdev)
{
        component_del(&pdev->dev, &dcp_comp_ops);
}

#endif

static int dcp_platform_suspend(struct device *dev)
{
        struct apple_dcp *dcp = dev_get_drvdata(dev);

        if (dcp->hdmi_hpd_irq) {
                disable_irq(dcp->hdmi_hpd_irq);
                dcp_dptx_disconnect(dcp, 0);
        }
        /*
         * Set the device as a wakeup device, which forces its power
         * domains to stay on. We need this as we do not support full
         * shutdown properly yet.
         */
        device_set_wakeup_path(dev);

        return 0;
}

static int dcp_platform_resume(struct device *dev)
{
        struct apple_dcp *dcp = dev_get_drvdata(dev);

        if (dcp->hdmi_hpd_irq)
                enable_irq(dcp->hdmi_hpd_irq);

        if (dcp->hdmi_hpd) {
                bool connected = gpiod_get_value_cansleep(dcp->hdmi_hpd);
                dev_info(dcp->dev, "resume: HPD connected:%d\n", connected);
                if (connected)
                        dcp_dptx_connect(dcp, 0);
        }

        return 0;
}

static DEFINE_SIMPLE_DEV_PM_OPS(dcp_platform_pm_ops,
                                dcp_platform_suspend, dcp_platform_resume);


static const struct apple_dcp_hw_data apple_dcp_hw_t6020 = {
        .num_dptx_ports = 1,
};

static const struct apple_dcp_hw_data apple_dcp_hw_t8112 = {
        .num_dptx_ports = 2,
};

static const struct apple_dcp_hw_data apple_dcp_hw_dcp = {
        .num_dptx_ports = 0,
};

static const struct apple_dcp_hw_data apple_dcp_hw_dcpext = {
        .num_dptx_ports = 2,
};

static const struct of_device_id of_match[] = {
        { .compatible = "apple,t6020-dcp", .data = &apple_dcp_hw_t6020,  },
        { .compatible = "apple,t8112-dcp", .data = &apple_dcp_hw_t8112,  },
        { .compatible = "apple,dcp",       .data = &apple_dcp_hw_dcp,    },
        { .compatible = "apple,dcpext",    .data = &apple_dcp_hw_dcpext, },
        {}
};
MODULE_DEVICE_TABLE(of, of_match);

#ifdef __linux__

static struct platform_driver apple_platform_driver = {
        .probe          = dcp_platform_probe,
        .remove         = dcp_platform_remove,
        .shutdown       = dcp_platform_shutdown,
        .driver = {
                .name = "apple-dcp",
                .of_match_table = of_match,
                .pm = pm_sleep_ptr(&dcp_platform_pm_ops),
        },
};

drm_module_platform_driver(apple_platform_driver);

MODULE_AUTHOR("Alyssa Rosenzweig <alyssa@rosenzweig.io>");
MODULE_DESCRIPTION("Apple Display Controller DRM driver");
MODULE_LICENSE("Dual MIT/GPL");

#endif